HealthProbe / HealthProbeTests / CoreDataArchiveCacheStoreTests.swift
Newer Older
116 lines | 5.615kb
Bogdan Timofte authored 2 weeks ago
1
import CoreData
2
import HealthKit
3
import XCTest
4
@testable import HealthProbe
5

            
6
final class CoreDataArchiveCacheStoreTests: XCTestCase {
7
    private var temporaryDirectory: URL!
8

            
9
    override func setUpWithError() throws {
10
        temporaryDirectory = FileManager.default.temporaryDirectory
11
            .appending(path: "HealthProbeCacheTests-\(UUID().uuidString)", directoryHint: .isDirectory)
12
        try FileManager.default.createDirectory(at: temporaryDirectory, withIntermediateDirectories: true)
13
    }
14

            
15
    override func tearDownWithError() throws {
16
        if let temporaryDirectory {
17
            try? FileManager.default.removeItem(at: temporaryDirectory)
18
        }
19
        temporaryDirectory = nil
20
    }
21

            
22
    func testRebuildCreatesCoreDataRowsFromSQLiteArchive() async throws {
23
        let archiveURL = temporaryDirectory.appending(path: "Archive.sqlite")
24
        let archive = SQLiteHealthArchiveStore(databaseURL: archiveURL)
25
        let firstSample = makeStepCountSample(value: 42, start: 1_000)
26
        let secondSample = makeStepCountSample(value: 7, start: 2_000)
27

            
28
        _ = try await archive.upsertSamples([firstSample], observedAt: Date(timeIntervalSince1970: 3_000))
29
        _ = try await archive.upsertSamples([firstSample, secondSample], observedAt: Date(timeIntervalSince1970: 3_060))
30
        try await archive.recordDisappearance(
31
            sampleUUIDHash: HashService.sampleUUIDHash(firstSample.uuid.uuidString),
32
            sampleTypeIdentifier: HKQuantityTypeIdentifier.stepCount.rawValue,
33
            observedMissingAt: Date(timeIntervalSince1970: 3_120)
34
        )
35
        try await archive.markVerification(
36
            sampleType: firstSample.sampleType,
37
            verifiedAt: Date(timeIntervalSince1970: 3_180)
38
        )
39

            
40
        let cache = try CoreDataArchiveCacheStore(inMemory: true)
41
        let summary = try cache.rebuild(fromArchiveAt: archiveURL)
42
        let context = cache.container.viewContext
43

            
44
        XCTAssertEqual(summary.observationRows, 4)
45
        XCTAssertEqual(summary.typeSummaryRows, 4)
46
        XCTAssertGreaterThanOrEqual(summary.dailyAggregateRows, 1)
47
        XCTAssertEqual(summary.archiveHealthRows, 1)
48
        XCTAssertEqual(try count("CachedObservationRow", in: context), 4)
49
        XCTAssertEqual(try count("CachedTypeSummary", in: context), 4)
50
        XCTAssertEqual(try count("CachedArchiveHealth", in: context), 1)
51

            
52
        let latestObservation = try fetchFirst(
53
            "CachedObservationRow",
54
            predicate: NSPredicate(format: "observationID == %d", 4),
55
            in: context
56
        )
57
        XCTAssertEqual(latestObservation?.value(forKey: "visibleRecordCount") as? Int64, 1)
58
        XCTAssertEqual(latestObservation?.value(forKey: "cacheSchemaVersion") as? Int64, Int64(CoreDataArchiveCacheStore.cacheSchemaVersion))
Bogdan Timofte authored 2 weeks ago
59

            
60
        let latestRow = try XCTUnwrap(cache.latestObservationRow())
61
        XCTAssertEqual(latestRow.observationID, 4)
62
        XCTAssertEqual(latestRow.visibleRecordCount, 1)
63
        XCTAssertEqual(latestRow.cacheSchemaVersion, CoreDataArchiveCacheStore.cacheSchemaVersion)
64

            
65
        let summaries = try cache.typeSummaries(observationID: latestRow.observationID)
66
        XCTAssertEqual(summaries.count, 1)
67
        XCTAssertEqual(summaries.first?.sampleTypeIdentifier, HKQuantityTypeIdentifier.stepCount.rawValue)
68

            
69
        let health = try XCTUnwrap(cache.latestArchiveHealthStatus())
70
        XCTAssertEqual(health.archiveSchemaVersion, 2)
71
        XCTAssertEqual(health.lastIntegrityStatus, "ok")
Bogdan Timofte authored 2 weeks ago
72
    }
73

            
74
    func testDeletingCacheDoesNotDeleteSQLiteArchiveAndRebuildRestoresRows() async throws {
75
        let archiveURL = temporaryDirectory.appending(path: "Archive.sqlite")
76
        let archive = SQLiteHealthArchiveStore(databaseURL: archiveURL)
77
        let sample = makeStepCountSample(value: 10, start: 1_000)
78
        _ = try await archive.upsertSamples([sample], observedAt: Date(timeIntervalSince1970: 2_000))
79

            
80
        let cache = try CoreDataArchiveCacheStore(inMemory: true)
81
        _ = try cache.rebuild(fromArchiveAt: archiveURL)
82
        try cache.deleteCache()
83

            
84
        XCTAssertEqual(try count("CachedObservationRow", in: cache.container.viewContext), 0)
85
        let integrityReport = try await archive.checkIntegrity()
86
        XCTAssertTrue(integrityReport.passed)
87

            
88
        let rebuilt = try cache.rebuild(fromArchiveAt: archiveURL)
89
        XCTAssertEqual(rebuilt.observationRows, 1)
90
        XCTAssertEqual(try count("CachedObservationRow", in: cache.container.viewContext), 1)
91
    }
92

            
93
    private func makeStepCountSample(value: Double, start: TimeInterval, end: TimeInterval? = nil) -> HKQuantitySample {
94
        let quantityType = HKQuantityType.quantityType(forIdentifier: .stepCount)!
95
        let quantity = HKQuantity(unit: .count(), doubleValue: value)
96
        let startDate = Date(timeIntervalSince1970: start)
97
        let endDate = Date(timeIntervalSince1970: end ?? (start + 300))
98
        return HKQuantitySample(type: quantityType, quantity: quantity, start: startDate, end: endDate)
99
    }
100

            
101
    private func count(_ entityName: String, in context: NSManagedObjectContext) throws -> Int {
102
        let request = NSFetchRequest<NSManagedObject>(entityName: entityName)
103
        return try context.count(for: request)
104
    }
105

            
106
    private func fetchFirst(
107
        _ entityName: String,
108
        predicate: NSPredicate,
109
        in context: NSManagedObjectContext
110
    ) throws -> NSManagedObject? {
111
        let request = NSFetchRequest<NSManagedObject>(entityName: entityName)
112
        request.predicate = predicate
113
        request.fetchLimit = 1
114
        return try context.fetch(request).first
115
    }
116
}