HealthProbe / HealthProbeTests / CoreDataArchiveCacheStoreTests.swift
1 contributor
125 lines | 5.977kb
import CoreData
import HealthKit
import XCTest
@testable import HealthProbe

final class CoreDataArchiveCacheStoreTests: XCTestCase {
    private var temporaryDirectory: URL!

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

    override func tearDownWithError() throws {
        if let temporaryDirectory {
            try? FileManager.default.removeItem(at: temporaryDirectory)
        }
        temporaryDirectory = nil
    }

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

        _ = try await archive.upsertSamples([firstSample], observedAt: Date(timeIntervalSince1970: 3_000))
        _ = try await archive.upsertSamples([firstSample, secondSample], observedAt: Date(timeIntervalSince1970: 3_060))
        try await archive.recordDisappearance(
            sampleUUIDHash: HashService.sampleUUIDHash(firstSample.uuid.uuidString),
            sampleTypeIdentifier: HKQuantityTypeIdentifier.stepCount.rawValue,
            observedMissingAt: Date(timeIntervalSince1970: 3_120)
        )
        try await archive.markVerification(
            sampleType: firstSample.sampleType,
            verifiedAt: Date(timeIntervalSince1970: 3_180)
        )

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

        XCTAssertEqual(summary.observationRows, 4)
        XCTAssertEqual(summary.typeSummaryRows, 4)
        XCTAssertGreaterThanOrEqual(summary.dailyAggregateRows, 1)
        XCTAssertEqual(summary.archiveHealthRows, 1)
        XCTAssertEqual(try count("CachedObservationRow", in: context), 4)
        XCTAssertEqual(try count("CachedTypeSummary", in: context), 4)
        XCTAssertEqual(try count("CachedArchiveHealth", in: context), 1)

        let latestObservation = try fetchFirst(
            "CachedObservationRow",
            predicate: NSPredicate(format: "observationID == %d", 4),
            in: context
        )
        XCTAssertEqual(latestObservation?.value(forKey: "visibleRecordCount") as? Int64, 1)
        XCTAssertEqual(latestObservation?.value(forKey: "cacheSchemaVersion") as? Int64, Int64(CoreDataArchiveCacheStore.cacheSchemaVersion))

        let latestRow = try XCTUnwrap(cache.latestObservationRow())
        XCTAssertEqual(latestRow.observationID, 4)
        XCTAssertEqual(latestRow.visibleRecordCount, 1)
        XCTAssertEqual(latestRow.cacheSchemaVersion, CoreDataArchiveCacheStore.cacheSchemaVersion)

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

        let diff = try XCTUnwrap(cache.diffSummary(
            fromObservationID: 1,
            toObservationID: 2,
            sampleTypeIdentifier: HKQuantityTypeIdentifier.stepCount.rawValue
        ))
        XCTAssertEqual(diff.appearedCount, 1)
        XCTAssertEqual(diff.disappearedCount, 0)
        XCTAssertEqual(diff.representationChangedCount, 0)

        let health = try XCTUnwrap(cache.latestArchiveHealthStatus())
        XCTAssertEqual(health.archiveSchemaVersion, 2)
        XCTAssertEqual(health.lastIntegrityStatus, "ok")
    }

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

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

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

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

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

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

    private func fetchFirst(
        _ entityName: String,
        predicate: NSPredicate,
        in context: NSManagedObjectContext
    ) throws -> NSManagedObject? {
        let request = NSFetchRequest<NSManagedObject>(entityName: entityName)
        request.predicate = predicate
        request.fetchLimit = 1
        return try context.fetch(request).first
    }
}