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(try cache.observationCount(), 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 cache.observationCount(), 0) 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(entityName: entityName) return try context.count(for: request) } private func fetchFirst( _ entityName: String, predicate: NSPredicate, in context: NSManagedObjectContext ) throws -> NSManagedObject? { let request = NSFetchRequest(entityName: entityName) request.predicate = predicate request.fetchLimit = 1 return try context.fetch(request).first } }