1 contributor
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<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
}
}