|
@@ -0,0 +1,457 @@
|
|
|
|
|
+import CoreData
|
|
|
|
|
+import Foundation
|
|
|
|
|
+import Testing
|
|
|
|
|
+
|
|
|
|
|
+@testable import Trio
|
|
|
|
|
+
|
|
|
|
|
+/// Golden tests certifying that the native `GlucoseStored` → `BloodGlucose` mapping
|
|
|
|
|
+/// (`OpenAPS.mapToBloodGlucose`) reproduces, byte for byte, the glucose the algorithm used to
|
|
|
|
|
+/// receive through the old JSON round-trip (`GlucoseStored` → `AlgorithmGlucose` → JSON →
|
|
|
|
|
+/// `JSONBridge.glucose`). The golden literals below were captured from that old path while it
|
|
|
|
|
+/// still existed (via a temporary differential run), so a match proves the algorithm still sees
|
|
|
|
|
+/// identical inputs now that the JSON round-trip is gone.
|
|
|
|
|
+///
|
|
|
|
|
+/// The comparison is field by field (see `expectFieldsEqual`), NOT `==`: `BloodGlucose.==` only
|
|
|
|
|
+/// compares `dateString`, so a plain array comparison would pass even if
|
|
|
|
|
+/// `glucose`/`sgv`/`direction`/`id`/`type` differed — exactly the coerced fields this migration
|
|
|
|
|
+/// must preserve.
|
|
|
|
|
+///
|
|
|
|
|
+/// Fixtures use fixed dates and ids so the mapping is fully deterministic and independent of the
|
|
|
|
|
+/// wall-clock-relative time-window fetch (which is unchanged by this migration and stays covered
|
|
|
|
|
+/// by `GlucoseSmoothingTests`).
|
|
|
|
|
+@Suite("Glucose Native Conversion Tests", .serialized) struct GlucoseNativeConversionTests {
|
|
|
|
|
+ var coreDataStack: CoreDataStack!
|
|
|
|
|
+ var testContext: NSManagedObjectContext!
|
|
|
|
|
+
|
|
|
|
|
+ init() async throws {
|
|
|
|
|
+ coreDataStack = try await CoreDataStack.createForTests()
|
|
|
|
|
+ testContext = coreDataStack.newTaskContext()
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // MARK: - Golden tests (native mapping vs frozen old-path output)
|
|
|
|
|
+
|
|
|
|
|
+ @Test("CGM readings without smoothing map identically") func testCGMNoSmoothing() async throws {
|
|
|
|
|
+ await insertGlucose(glucose: 120, isManual: false, date: fixedDate(minutesAgo: 0), direction: "Flat", id: uuid(1))
|
|
|
|
|
+ await insertGlucose(glucose: 95, isManual: false, date: fixedDate(minutesAgo: 5), direction: "FortyFiveDown", id: uuid(2))
|
|
|
|
|
+ await insertGlucose(glucose: 150, isManual: true, date: fixedDate(minutesAgo: 10), direction: nil, id: uuid(3))
|
|
|
|
|
+
|
|
|
|
|
+ try await assertNativeMatchesGolden(shouldSmoothGlucose: false, [
|
|
|
|
|
+ BloodGlucose(
|
|
|
|
|
+ id: uuid(1).uuidString,
|
|
|
|
|
+ sgv: 120,
|
|
|
|
|
+ direction: .flat,
|
|
|
|
|
+ date: 1_700_000_000_000,
|
|
|
|
|
+ dateString: Date(timeIntervalSince1970: 1_700_000_000),
|
|
|
|
|
+ type: "sgv"
|
|
|
|
|
+ ),
|
|
|
|
|
+ BloodGlucose(
|
|
|
|
|
+ id: uuid(2).uuidString,
|
|
|
|
|
+ sgv: 95,
|
|
|
|
|
+ direction: .fortyFiveDown,
|
|
|
|
|
+ date: 1_699_999_700_000,
|
|
|
|
|
+ dateString: Date(timeIntervalSince1970: 1_699_999_700),
|
|
|
|
|
+ type: "sgv"
|
|
|
|
|
+ ),
|
|
|
|
|
+ BloodGlucose(
|
|
|
|
|
+ id: uuid(3).uuidString,
|
|
|
|
|
+ date: 1_699_999_400_000,
|
|
|
|
|
+ dateString: Date(timeIntervalSince1970: 1_699_999_400),
|
|
|
|
|
+ glucose: 150,
|
|
|
|
|
+ type: "sgv"
|
|
|
|
|
+ )
|
|
|
|
|
+ ])
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test("Smoothed CGM value is used and rounded identically") func testSmoothedValueUsed() async throws {
|
|
|
|
|
+ await insertGlucose(
|
|
|
|
|
+ glucose: 120,
|
|
|
|
|
+ isManual: false,
|
|
|
|
|
+ date: fixedDate(minutesAgo: 0),
|
|
|
|
|
+ smoothed: Decimal(string: "118.6"),
|
|
|
|
|
+ direction: "Flat",
|
|
|
|
|
+ id: uuid(1)
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ try await assertNativeMatchesGolden(shouldSmoothGlucose: true, [
|
|
|
|
|
+ BloodGlucose(
|
|
|
|
|
+ id: uuid(1).uuidString,
|
|
|
|
|
+ sgv: 119,
|
|
|
|
|
+ direction: .flat,
|
|
|
|
|
+ date: 1_700_000_000_000,
|
|
|
|
|
+ dateString: Date(timeIntervalSince1970: 1_700_000_000),
|
|
|
|
|
+ type: "sgv"
|
|
|
|
|
+ )
|
|
|
|
|
+ ])
|
|
|
|
|
+
|
|
|
|
|
+ let native = try await nativeBloodGlucose(shouldSmoothGlucose: true)
|
|
|
|
|
+ #expect(native.first?.sgv == 119, "118.6 should round to 119 in the sgv field")
|
|
|
|
|
+ #expect(native.first?.glucose == nil, "CGM readings must not populate the manual `glucose` field")
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test("Zero smoothed value falls back to the raw value") func testSmoothedZeroFallsBackToRaw() async throws {
|
|
|
|
|
+ await insertGlucose(
|
|
|
|
|
+ glucose: 120,
|
|
|
|
|
+ isManual: false,
|
|
|
|
|
+ date: fixedDate(minutesAgo: 0),
|
|
|
|
|
+ smoothed: 0,
|
|
|
|
|
+ direction: "Flat",
|
|
|
|
|
+ id: uuid(1)
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ try await assertNativeMatchesGolden(shouldSmoothGlucose: true, [
|
|
|
|
|
+ BloodGlucose(
|
|
|
|
|
+ id: uuid(1).uuidString,
|
|
|
|
|
+ sgv: 120,
|
|
|
|
|
+ direction: .flat,
|
|
|
|
|
+ date: 1_700_000_000_000,
|
|
|
|
|
+ dateString: Date(timeIntervalSince1970: 1_700_000_000),
|
|
|
|
|
+ type: "sgv"
|
|
|
|
|
+ )
|
|
|
|
|
+ ])
|
|
|
|
|
+
|
|
|
|
|
+ let native = try await nativeBloodGlucose(shouldSmoothGlucose: true)
|
|
|
|
|
+ #expect(native.first?.sgv == 120, "A zero smoothed value must fall back to the raw 120")
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test("Nil smoothed value falls back to the raw value") func testSmoothedNilFallsBackToRaw() async throws {
|
|
|
|
|
+ await insertGlucose(
|
|
|
|
|
+ glucose: 120,
|
|
|
|
|
+ isManual: false,
|
|
|
|
|
+ date: fixedDate(minutesAgo: 0),
|
|
|
|
|
+ smoothed: nil,
|
|
|
|
|
+ direction: "Flat",
|
|
|
|
|
+ id: uuid(1)
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ try await assertNativeMatchesGolden(shouldSmoothGlucose: true, [
|
|
|
|
|
+ BloodGlucose(
|
|
|
|
|
+ id: uuid(1).uuidString,
|
|
|
|
|
+ sgv: 120,
|
|
|
|
|
+ direction: .flat,
|
|
|
|
|
+ date: 1_700_000_000_000,
|
|
|
|
|
+ dateString: Date(timeIntervalSince1970: 1_700_000_000),
|
|
|
|
|
+ type: "sgv"
|
|
|
|
|
+ )
|
|
|
|
|
+ ])
|
|
|
|
|
+
|
|
|
|
|
+ let native = try await nativeBloodGlucose(shouldSmoothGlucose: true)
|
|
|
|
|
+ #expect(native.first?.sgv == 120, "A nil smoothed value must fall back to the raw 120")
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test("Manual entries ignore smoothing and populate the glucose field") func testManualIgnoresSmoothing() async throws {
|
|
|
|
|
+ await insertGlucose(
|
|
|
|
|
+ glucose: 150,
|
|
|
|
|
+ isManual: true,
|
|
|
|
|
+ date: fixedDate(minutesAgo: 0),
|
|
|
|
|
+ smoothed: 140,
|
|
|
|
|
+ direction: nil,
|
|
|
|
|
+ id: uuid(1)
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ try await assertNativeMatchesGolden(shouldSmoothGlucose: true, [
|
|
|
|
|
+ BloodGlucose(
|
|
|
|
|
+ id: uuid(1).uuidString,
|
|
|
|
|
+ date: 1_700_000_000_000,
|
|
|
|
|
+ dateString: Date(timeIntervalSince1970: 1_700_000_000),
|
|
|
|
|
+ glucose: 150,
|
|
|
|
|
+ type: "sgv"
|
|
|
|
|
+ )
|
|
|
|
|
+ ])
|
|
|
|
|
+
|
|
|
|
|
+ let native = try await nativeBloodGlucose(shouldSmoothGlucose: true)
|
|
|
|
|
+ #expect(native.first?.glucose == 150, "Manual entries must use the raw value in the `glucose` field")
|
|
|
|
|
+ #expect(native.first?.sgv == nil, "Manual entries must not populate the `sgv` field")
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test("Direction variants and nil map identically") func testDirectionVariants() async throws {
|
|
|
|
|
+ await insertGlucose(glucose: 110, isManual: false, date: fixedDate(minutesAgo: 0), direction: "TripleUp", id: uuid(1))
|
|
|
|
|
+ await insertGlucose(glucose: 111, isManual: false, date: fixedDate(minutesAgo: 5), direction: "FortyFiveUp", id: uuid(2))
|
|
|
|
|
+ await insertGlucose(glucose: 112, isManual: false, date: fixedDate(minutesAgo: 10), direction: "NONE", id: uuid(3))
|
|
|
|
|
+ await insertGlucose(glucose: 113, isManual: false, date: fixedDate(minutesAgo: 15), direction: nil, id: uuid(4))
|
|
|
|
|
+
|
|
|
|
|
+ try await assertNativeMatchesGolden(shouldSmoothGlucose: false, [
|
|
|
|
|
+ BloodGlucose(
|
|
|
|
|
+ id: uuid(1).uuidString,
|
|
|
|
|
+ sgv: 110,
|
|
|
|
|
+ direction: .tripleUp,
|
|
|
|
|
+ date: 1_700_000_000_000,
|
|
|
|
|
+ dateString: Date(timeIntervalSince1970: 1_700_000_000),
|
|
|
|
|
+ type: "sgv"
|
|
|
|
|
+ ),
|
|
|
|
|
+ BloodGlucose(
|
|
|
|
|
+ id: uuid(2).uuidString,
|
|
|
|
|
+ sgv: 111,
|
|
|
|
|
+ direction: .fortyFiveUp,
|
|
|
|
|
+ date: 1_699_999_700_000,
|
|
|
|
|
+ dateString: Date(timeIntervalSince1970: 1_699_999_700),
|
|
|
|
|
+ type: "sgv"
|
|
|
|
|
+ ),
|
|
|
|
|
+ // `.none` here is BloodGlucose.Direction.none ("NONE"), not Optional.none — spell it out.
|
|
|
|
|
+ BloodGlucose(
|
|
|
|
|
+ id: uuid(3).uuidString,
|
|
|
|
|
+ sgv: 112,
|
|
|
|
|
+ direction: BloodGlucose.Direction.none,
|
|
|
|
|
+ date: 1_699_999_400_000,
|
|
|
|
|
+ dateString: Date(timeIntervalSince1970: 1_699_999_400),
|
|
|
|
|
+ type: "sgv"
|
|
|
|
|
+ ),
|
|
|
|
|
+ BloodGlucose(
|
|
|
|
|
+ id: uuid(4).uuidString,
|
|
|
|
|
+ sgv: 113,
|
|
|
|
|
+ date: 1_699_999_100_000,
|
|
|
|
|
+ dateString: Date(timeIntervalSince1970: 1_699_999_100),
|
|
|
|
|
+ type: "sgv"
|
|
|
|
|
+ )
|
|
|
|
|
+ ])
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test("Sub-millisecond dates are truncated identically") func testMillisecondTruncation() async throws {
|
|
|
|
|
+ // A date with sub-millisecond precision: the old path round-trips it through an ISO8601
|
|
|
|
|
+ // fractional-seconds string (millisecond precision), so both fields must be ms-truncated.
|
|
|
|
|
+ await insertGlucose(
|
|
|
|
|
+ glucose: 100,
|
|
|
|
|
+ isManual: false,
|
|
|
|
|
+ date: fixedDate(minutesAgo: 0, plusSeconds: 0.123_456),
|
|
|
|
|
+ direction: "Flat",
|
|
|
|
|
+ id: uuid(1)
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ try await assertNativeMatchesGolden(shouldSmoothGlucose: false, [
|
|
|
|
|
+ BloodGlucose(
|
|
|
|
|
+ id: uuid(1).uuidString,
|
|
|
|
|
+ sgv: 100,
|
|
|
|
|
+ direction: .flat,
|
|
|
|
|
+ date: 1_700_000_000_123,
|
|
|
|
|
+ dateString: Date(timeIntervalSince1970: 1_700_000_000.123),
|
|
|
|
|
+ type: "sgv"
|
|
|
|
|
+ )
|
|
|
|
|
+ ])
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test("A descending multi-entry sequence maps identically") func testDescendingSequence() async throws {
|
|
|
|
|
+ for i in 0 ..< 6 {
|
|
|
|
|
+ await insertGlucose(
|
|
|
|
|
+ glucose: Int16(100 + i),
|
|
|
|
|
+ isManual: false,
|
|
|
|
|
+ date: fixedDate(minutesAgo: Double(i) * 5),
|
|
|
|
|
+ direction: "Flat",
|
|
|
|
|
+ id: uuid(i + 1)
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try await assertNativeMatchesGolden(shouldSmoothGlucose: false, [
|
|
|
|
|
+ BloodGlucose(
|
|
|
|
|
+ id: uuid(1).uuidString,
|
|
|
|
|
+ sgv: 100,
|
|
|
|
|
+ direction: .flat,
|
|
|
|
|
+ date: 1_700_000_000_000,
|
|
|
|
|
+ dateString: Date(timeIntervalSince1970: 1_700_000_000),
|
|
|
|
|
+ type: "sgv"
|
|
|
|
|
+ ),
|
|
|
|
|
+ BloodGlucose(
|
|
|
|
|
+ id: uuid(2).uuidString,
|
|
|
|
|
+ sgv: 101,
|
|
|
|
|
+ direction: .flat,
|
|
|
|
|
+ date: 1_699_999_700_000,
|
|
|
|
|
+ dateString: Date(timeIntervalSince1970: 1_699_999_700),
|
|
|
|
|
+ type: "sgv"
|
|
|
|
|
+ ),
|
|
|
|
|
+ BloodGlucose(
|
|
|
|
|
+ id: uuid(3).uuidString,
|
|
|
|
|
+ sgv: 102,
|
|
|
|
|
+ direction: .flat,
|
|
|
|
|
+ date: 1_699_999_400_000,
|
|
|
|
|
+ dateString: Date(timeIntervalSince1970: 1_699_999_400),
|
|
|
|
|
+ type: "sgv"
|
|
|
|
|
+ ),
|
|
|
|
|
+ BloodGlucose(
|
|
|
|
|
+ id: uuid(4).uuidString,
|
|
|
|
|
+ sgv: 103,
|
|
|
|
|
+ direction: .flat,
|
|
|
|
|
+ date: 1_699_999_100_000,
|
|
|
|
|
+ dateString: Date(timeIntervalSince1970: 1_699_999_100),
|
|
|
|
|
+ type: "sgv"
|
|
|
|
|
+ ),
|
|
|
|
|
+ BloodGlucose(
|
|
|
|
|
+ id: uuid(5).uuidString,
|
|
|
|
|
+ sgv: 104,
|
|
|
|
|
+ direction: .flat,
|
|
|
|
|
+ date: 1_699_998_800_000,
|
|
|
|
|
+ dateString: Date(timeIntervalSince1970: 1_699_998_800),
|
|
|
|
|
+ type: "sgv"
|
|
|
|
|
+ ),
|
|
|
|
|
+ BloodGlucose(
|
|
|
|
|
+ id: uuid(6).uuidString,
|
|
|
|
|
+ sgv: 105,
|
|
|
|
|
+ direction: .flat,
|
|
|
|
|
+ date: 1_699_998_500_000,
|
|
|
|
|
+ dateString: Date(timeIntervalSince1970: 1_699_998_500),
|
|
|
|
|
+ type: "sgv"
|
|
|
|
|
+ )
|
|
|
|
|
+ ])
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test("Smoothing rounding boundaries map identically") func testRoundingBoundaries() async throws {
|
|
|
|
|
+ await insertGlucose(
|
|
|
|
|
+ glucose: 100,
|
|
|
|
|
+ isManual: false,
|
|
|
|
|
+ date: fixedDate(minutesAgo: 0),
|
|
|
|
|
+ smoothed: Decimal(string: "118.5"),
|
|
|
|
|
+ id: uuid(1)
|
|
|
|
|
+ )
|
|
|
|
|
+ await insertGlucose(
|
|
|
|
|
+ glucose: 100,
|
|
|
|
|
+ isManual: false,
|
|
|
|
|
+ date: fixedDate(minutesAgo: 5),
|
|
|
|
|
+ smoothed: Decimal(string: "118.4"),
|
|
|
|
|
+ id: uuid(2)
|
|
|
|
|
+ )
|
|
|
|
|
+ await insertGlucose(
|
|
|
|
|
+ glucose: 100,
|
|
|
|
|
+ isManual: false,
|
|
|
|
|
+ date: fixedDate(minutesAgo: 10),
|
|
|
|
|
+ smoothed: Decimal(string: "119.5"),
|
|
|
|
|
+ id: uuid(3)
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ try await assertNativeMatchesGolden(shouldSmoothGlucose: true, [
|
|
|
|
|
+ BloodGlucose(
|
|
|
|
|
+ id: uuid(1).uuidString,
|
|
|
|
|
+ sgv: 119,
|
|
|
|
|
+ date: 1_700_000_000_000,
|
|
|
|
|
+ dateString: Date(timeIntervalSince1970: 1_700_000_000),
|
|
|
|
|
+ type: "sgv"
|
|
|
|
|
+ ),
|
|
|
|
|
+ BloodGlucose(
|
|
|
|
|
+ id: uuid(2).uuidString,
|
|
|
|
|
+ sgv: 118,
|
|
|
|
|
+ date: 1_699_999_700_000,
|
|
|
|
|
+ dateString: Date(timeIntervalSince1970: 1_699_999_700),
|
|
|
|
|
+ type: "sgv"
|
|
|
|
|
+ ),
|
|
|
|
|
+ BloodGlucose(
|
|
|
|
|
+ id: uuid(3).uuidString,
|
|
|
|
|
+ sgv: 120,
|
|
|
|
|
+ date: 1_699_999_400_000,
|
|
|
|
|
+ dateString: Date(timeIntervalSince1970: 1_699_999_400),
|
|
|
|
|
+ type: "sgv"
|
|
|
|
|
+ )
|
|
|
|
|
+ ])
|
|
|
|
|
+
|
|
|
|
|
+ let native = try await nativeBloodGlucose(shouldSmoothGlucose: true)
|
|
|
|
|
+ #expect(native.map(\.sgv) == [119, 118, 120], ".plain scale-0 rounding: 118.5→119, 118.4→118, 119.5→120")
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // MARK: - Comparison helpers
|
|
|
|
|
+
|
|
|
|
|
+ /// Asserts the native mapping reproduces the frozen golden `BloodGlucose` values. The goldens
|
|
|
|
|
+ /// were captured from the old `AlgorithmGlucose` → JSON → `JSONBridge.glucose` path (see the
|
|
|
|
|
+ /// differential run in this migration's history), so a match proves the algorithm still receives
|
|
|
|
|
+ /// identical glucose after the JSON round-trip was removed.
|
|
|
|
|
+ private func assertNativeMatchesGolden(shouldSmoothGlucose: Bool, _ golden: [BloodGlucose]) async throws {
|
|
|
|
|
+ let native = try await nativeBloodGlucose(shouldSmoothGlucose: shouldSmoothGlucose)
|
|
|
|
|
+
|
|
|
|
|
+ #expect(native.count == golden.count, "native produced \(native.count) entries, golden has \(golden.count)")
|
|
|
|
|
+
|
|
|
|
|
+ for (index, pair) in zip(native, golden).enumerated() {
|
|
|
|
|
+ expectFieldsEqual(pair.0, pair.1, entry: index)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /// Field-by-field comparison. We can't use `==`: `BloodGlucose.==` only compares `dateString`,
|
|
|
|
|
+ /// so a direct comparison would pass even if `sgv`/`glucose`/`direction`/`id`/`type` differed —
|
|
|
|
|
+ /// exactly the coerced fields we must pin.
|
|
|
|
|
+ ///
|
|
|
|
|
+ /// `dateString` is compared at millisecond resolution rather than as an exact `Date`. The mapping
|
|
|
|
|
+ /// keeps the reading's full-precision date in memory, but only millisecond precision is ever
|
|
|
|
|
+ /// observable — it's what `date` pins and what the value serializes to — and Core Data's `Double`
|
|
|
|
|
+ /// round-trip perturbs sub-millisecond bits anyway, so an exact `Date` comparison would be flaky.
|
|
|
|
|
+ private func expectFieldsEqual(_ actual: BloodGlucose, _ expected: BloodGlucose, entry index: Int) {
|
|
|
|
|
+ #expect(actual.id == expected.id, "entry \(index): id \(actual.id) != \(expected.id)")
|
|
|
|
|
+ #expect(actual.legacyId == expected.legacyId, "entry \(index): legacyId")
|
|
|
|
|
+ #expect(
|
|
|
|
|
+ actual.sgv == expected.sgv,
|
|
|
|
|
+ "entry \(index): sgv \(actual.sgv.map(String.init) ?? "nil") != \(expected.sgv.map(String.init) ?? "nil")"
|
|
|
|
|
+ )
|
|
|
|
|
+ #expect(
|
|
|
|
|
+ actual.glucose == expected.glucose,
|
|
|
|
|
+ "entry \(index): glucose \(actual.glucose.map(String.init) ?? "nil") != \(expected.glucose.map(String.init) ?? "nil")"
|
|
|
|
|
+ )
|
|
|
|
|
+ #expect(actual.mbg == expected.mbg, "entry \(index): mbg")
|
|
|
|
|
+ #expect(
|
|
|
|
|
+ actual.direction == expected.direction,
|
|
|
|
|
+ "entry \(index): direction \(actual.direction?.rawValue ?? "nil") != \(expected.direction?.rawValue ?? "nil")"
|
|
|
|
|
+ )
|
|
|
|
|
+ #expect(actual.date == expected.date, "entry \(index): date \(actual.date) != \(expected.date)")
|
|
|
|
|
+ #expect(
|
|
|
|
|
+ Self.millisecondString(actual.dateString) == Self.millisecondString(expected.dateString),
|
|
|
|
|
+ "entry \(index): dateString \(Self.millisecondString(actual.dateString)) != \(Self.millisecondString(expected.dateString))"
|
|
|
|
|
+ )
|
|
|
|
|
+ #expect(actual.type == expected.type, "entry \(index): type \(actual.type ?? "nil") != \(expected.type ?? "nil")")
|
|
|
|
|
+ #expect(actual.unfiltered == expected.unfiltered, "entry \(index): unfiltered")
|
|
|
|
|
+ #expect(actual.filtered == expected.filtered, "entry \(index): filtered")
|
|
|
|
|
+ #expect(actual.noise == expected.noise, "entry \(index): noise")
|
|
|
|
|
+ #expect(actual.activationDate == expected.activationDate, "entry \(index): activationDate")
|
|
|
|
|
+ #expect(actual.sessionStartDate == expected.sessionStartDate, "entry \(index): sessionStartDate")
|
|
|
|
|
+ #expect(actual.transmitterID == expected.transmitterID, "entry \(index): transmitterID")
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static func millisecondString(_ date: Date) -> String {
|
|
|
|
|
+ Formatter.iso8601withFractionalSeconds.string(from: date)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private func nativeBloodGlucose(shouldSmoothGlucose: Bool) async throws -> [BloodGlucose] {
|
|
|
|
|
+ try await testContext.perform {
|
|
|
|
|
+ try self.fetchRowsNewestFirst().map {
|
|
|
|
|
+ OpenAPS.mapToBloodGlucose($0, shouldSmoothGlucose: shouldSmoothGlucose, roundingBehavior: Self.roundingBehavior)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /// Must be called from within `testContext.perform`.
|
|
|
|
|
+ private func fetchRowsNewestFirst() throws -> [GlucoseStored] {
|
|
|
|
|
+ let request = GlucoseStored.fetchRequest()
|
|
|
|
|
+ request.sortDescriptors = [NSSortDescriptor(key: "date", ascending: false)]
|
|
|
|
|
+ return try testContext.fetch(request)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static let roundingBehavior = NSDecimalNumberHandler(
|
|
|
|
|
+ roundingMode: .plain,
|
|
|
|
|
+ scale: 0,
|
|
|
|
|
+ raiseOnExactness: false,
|
|
|
|
|
+ raiseOnOverflow: false,
|
|
|
|
|
+ raiseOnUnderflow: false,
|
|
|
|
|
+ raiseOnDivideByZero: false
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ // MARK: - Fixture helpers
|
|
|
|
|
+
|
|
|
|
|
+ private func insertGlucose(
|
|
|
|
|
+ glucose: Int16,
|
|
|
|
|
+ isManual: Bool,
|
|
|
|
|
+ date: Date,
|
|
|
|
|
+ smoothed: Decimal? = nil,
|
|
|
|
|
+ direction: String? = nil,
|
|
|
|
|
+ id: UUID
|
|
|
|
|
+ ) async {
|
|
|
|
|
+ await testContext.perform {
|
|
|
|
|
+ let object = GlucoseStored(context: self.testContext)
|
|
|
|
|
+ object.glucose = glucose
|
|
|
|
|
+ object.isManual = isManual
|
|
|
|
|
+ object.date = date
|
|
|
|
|
+ object.smoothedGlucose = smoothed.map { NSDecimalNumber(decimal: $0) }
|
|
|
|
|
+ object.direction = direction
|
|
|
|
|
+ object.id = id
|
|
|
|
|
+ try! self.testContext.save()
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /// A fixed base timestamp (2023-11-14T22:13:20Z) so fixtures are deterministic and reproducible.
|
|
|
|
|
+ private func fixedDate(minutesAgo: Double, plusSeconds: Double = 0) -> Date {
|
|
|
|
|
+ Date(timeIntervalSince1970: 1_700_000_000 + plusSeconds - minutesAgo * 60)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private func uuid(_ n: Int) -> UUID {
|
|
|
|
|
+ UUID(uuidString: String(format: "00000000-0000-0000-0000-%012d", n))!
|
|
|
|
|
+ }
|
|
|
|
|
+}
|