소스 검색

Add determination import unit tests WIP

Deniz Cengiz 1 년 전
부모
커밋
27076b322f

+ 1 - 1
Model/Helper/NSPredicates.swift

@@ -124,7 +124,7 @@ extension NSPredicate {
     static func predicateForDateBetween(start: Date, end: Date) -> NSPredicate {
         NSPredicate(format: "date >= %@ AND date <= %@", start as NSDate, end as NSDate)
     }
-    
+
     static func predicateForDeliverAtBetween(start: Date, end: Date) -> NSPredicate {
         NSPredicate(format: "deliverAt >= %@ AND deliverAt <= %@", start as NSDate, end as NSDate)
     }

+ 12 - 10
Model/JSONImporter.swift

@@ -54,7 +54,7 @@ class JSONImporter {
 
         return Set(allReadings.compactMap(\.date))
     }
-    
+
     /// Retrieves the set of dates for all oref determinations currently stored in CoreData.
     ///
     /// - Parameters:
@@ -110,7 +110,7 @@ class JSONImporter {
             try self.context.save()
         }
     }
-    
+
     /// Imports oref determination from a JSON file into CoreData.
     ///
     /// The function reads oref determination data from the provided JSON file and stores new entries
@@ -130,13 +130,15 @@ class JSONImporter {
 
         /// Helper function to check if entries are from within the last 24 hours that do not yet exist in Core Data
         func checkDeterminationDate(_ date: Date) -> Bool {
-            return date >= twentyFourHoursAgo && date <= now && !existingDates.contains(date)
+            date >= twentyFourHoursAgo && date <= now && !existingDates.contains(date)
         }
-        
-        guard let enactedDeliverAt = enactedDetermination.deliverAt, let suggestedDeliverAt = suggestedDetermination.deliverAt else {
+
+        guard let enactedDeliverAt = enactedDetermination.deliverAt,
+              let suggestedDeliverAt = suggestedDetermination.deliverAt
+        else {
             throw JSONImporterError.missingGlucoseValueInGlucoseEntry // TODO: adjust error
         }
-        
+
         guard checkDeterminationDate(enactedDeliverAt), checkDeterminationDate(suggestedDeliverAt) else {
             return
         }
@@ -151,7 +153,7 @@ class JSONImporter {
             if suggestedDeliverAt != enactedDeliverAt {
                 try suggestedDetermination.store(in: backgroundContext)
             }
-            
+
             try enactedDetermination.store(in: backgroundContext)
 
             try backgroundContext.save()
@@ -307,7 +309,7 @@ extension Determination: Codable {
 
     /// Helper function to convert `Determination` to `OrefDetermination` while importing JSON glucose entries
     func store(in context: NSManagedObjectContext) throws {
-        // TODO: some guards here ?! 
+        // TODO: some guards here ?!
         let newOrefDetermination = OrefDetermination(context: context)
         newOrefDetermination.id = UUID()
         newOrefDetermination.insulinSensitivity = decimalToNSDecimalNumber(isf)
@@ -315,7 +317,7 @@ extension Determination: Codable {
         newOrefDetermination.eventualBG = eventualBG.map(NSDecimalNumber.init)
         newOrefDetermination.deliverAt = deliverAt
         newOrefDetermination.timestamp = timestamp
-        newOrefDetermination.enacted = received ?? false 
+        newOrefDetermination.enacted = received ?? false
         newOrefDetermination.insulinForManualBolus = decimalToNSDecimalNumber(insulinForManualBolus)
         newOrefDetermination.carbRatio = decimalToNSDecimalNumber(carbRatio)
         newOrefDetermination.glucose = decimalToNSDecimalNumber(bg)
@@ -357,7 +359,7 @@ extension Determination: Codable {
                 }
         }
     }
-    
+
     func decimalToNSDecimalNumber(_ value: Decimal?) -> NSDecimalNumber? {
         guard let value = value else { return nil }
         return NSDecimalNumber(decimal: value)

+ 8 - 0
Trio.xcodeproj/project.pbxproj

@@ -621,6 +621,8 @@
 		DDD1631C2C4C697400CD525A /* AddOverrideForm.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD1631B2C4C697400CD525A /* AddOverrideForm.swift */; };
 		DDD1631F2C4C6F6900CD525A /* TrioCoreDataPersistentContainer.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = DDD1631D2C4C6F6900CD525A /* TrioCoreDataPersistentContainer.xcdatamodeld */; };
 		DDD6D4D32CDE90720029439A /* EstimatedA1cDisplayUnit.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD6D4D22CDE90720029439A /* EstimatedA1cDisplayUnit.swift */; };
+		DDD78AD92DC421B500AC63F3 /* enacted.json in Resources */ = {isa = PBXBuildFile; fileRef = DDD78AD72DC421B500AC63F3 /* enacted.json */; };
+		DDD78ADA2DC421B500AC63F3 /* suggested.json in Resources */ = {isa = PBXBuildFile; fileRef = DDD78AD82DC421B500AC63F3 /* suggested.json */; };
 		DDE179522C910127003CDDB7 /* MealPresetStored+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE179322C910127003CDDB7 /* MealPresetStored+CoreDataClass.swift */; };
 		DDE179532C910127003CDDB7 /* MealPresetStored+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE179332C910127003CDDB7 /* MealPresetStored+CoreDataProperties.swift */; };
 		DDE179542C910127003CDDB7 /* LoopStatRecord+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE179342C910127003CDDB7 /* LoopStatRecord+CoreDataClass.swift */; };
@@ -1428,6 +1430,8 @@
 		DDD1631B2C4C697400CD525A /* AddOverrideForm.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddOverrideForm.swift; sourceTree = "<group>"; };
 		DDD1631E2C4C6F6900CD525A /* TrioCoreDataPersistentContainer.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = TrioCoreDataPersistentContainer.xcdatamodel; sourceTree = "<group>"; };
 		DDD6D4D22CDE90720029439A /* EstimatedA1cDisplayUnit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EstimatedA1cDisplayUnit.swift; sourceTree = "<group>"; };
+		DDD78AD72DC421B500AC63F3 /* enacted.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = enacted.json; sourceTree = "<group>"; };
+		DDD78AD82DC421B500AC63F3 /* suggested.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = suggested.json; sourceTree = "<group>"; };
 		DDE179322C910127003CDDB7 /* MealPresetStored+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MealPresetStored+CoreDataClass.swift"; sourceTree = "<group>"; };
 		DDE179332C910127003CDDB7 /* MealPresetStored+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MealPresetStored+CoreDataProperties.swift"; sourceTree = "<group>"; };
 		DDE179342C910127003CDDB7 /* LoopStatRecord+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "LoopStatRecord+CoreDataClass.swift"; sourceTree = "<group>"; };
@@ -2554,6 +2558,8 @@
 		3B997DD22DC02AEF006B6BB2 /* JSONImporterData */ = {
 			isa = PBXGroup;
 			children = (
+				DDD78AD72DC421B500AC63F3 /* enacted.json */,
+				DDD78AD82DC421B500AC63F3 /* suggested.json */,
 				3B997DD12DC02AEF006B6BB2 /* glucose.json */,
 			);
 			path = JSONImporterData;
@@ -3904,6 +3910,8 @@
 			buildActionMask = 2147483647;
 			files = (
 				3B997DD32DC02AEF006B6BB2 /* glucose.json in Resources */,
+				DDD78AD92DC421B500AC63F3 /* enacted.json in Resources */,
+				DDD78ADA2DC421B500AC63F3 /* suggested.json in Resources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};

+ 224 - 0
TrioTests/JSONImporterData/enacted.json

@@ -0,0 +1,224 @@
+{
+  "ISF" : 4.6,
+  "recieved" : true,
+  "reason" : "Autosens ratio: 0.99, ISF: 4.5→4.6, COB: 34, Dev: 4.2, BGI: -0.4, CR: 15, Target: 5.2, minPredBG 6.5, minGuardBG 4.3, IOBpredBG 2.7, COBpredBG 8.9, UAMpredBG 3.4, TDD: 26.95 U, 83% Bolus 17% Basal, Dynamic ISF/CR: On/Off, Sigmoid function, AF: 0.14, Basal ratio: 0.93; Eventual BG 8.9 >= 5.2,  insulinReq 0.29; setting 60m low temp of 0U/h. Microbolusing 0.1U. ",
+  "current_target" : 94,
+  "expectedDelta" : -5.9,
+  "insulinReq" : 0.29,
+  "predBGs" : {
+    "ZT" : [
+      85,
+      78,
+      71,
+      64,
+      58,
+      52,
+      47,
+      42,
+      39,
+      39,
+      39,
+      39,
+      39,
+      39,
+      39,
+      39,
+      39,
+      39,
+      39,
+      39,
+      39,
+      39,
+      39,
+      39,
+      39,
+      39,
+      39,
+      39,
+      39,
+      39,
+      39,
+      39,
+      39,
+      39,
+      39,
+      39,
+      39,
+      39,
+      42,
+      44,
+      47,
+      49,
+      52,
+      54,
+      57,
+      59,
+      62,
+      65
+    ],
+    "IOB" : [
+      85,
+      89,
+      92,
+      95,
+      97,
+      99,
+      99,
+      99,
+      99,
+      97,
+      95,
+      92,
+      89,
+      85,
+      82,
+      79,
+      77,
+      74,
+      72,
+      70,
+      68,
+      66,
+      65,
+      63,
+      62,
+      60,
+      59,
+      58,
+      57,
+      56,
+      56,
+      55,
+      54,
+      53,
+      53,
+      52,
+      52,
+      51,
+      51,
+      51,
+      50,
+      50,
+      50,
+      50,
+      49
+    ],
+    "UAM" : [
+      85,
+      89,
+      93,
+      96,
+      99,
+      101,
+      102,
+      103,
+      104,
+      104,
+      103,
+      102,
+      100,
+      98,
+      95,
+      92,
+      89,
+      87,
+      84,
+      82,
+      80,
+      78,
+      77,
+      75,
+      74,
+      73,
+      71,
+      70,
+      69,
+      69,
+      68,
+      67,
+      66,
+      66,
+      65,
+      65,
+      64,
+      64,
+      63,
+      63,
+      63,
+      62,
+      62,
+      62,
+      61
+    ],
+    "COB" : [
+      85,
+      90,
+      94,
+      99,
+      103,
+      108,
+      112,
+      117,
+      121,
+      125,
+      130,
+      134,
+      137,
+      141,
+      145,
+      148,
+      151,
+      154,
+      157,
+      159,
+      161,
+      163,
+      165,
+      166,
+      167,
+      168,
+      168,
+      168,
+      168,
+      168,
+      167,
+      166,
+      165,
+      165,
+      164,
+      164,
+      163,
+      163,
+      162,
+      162,
+      161,
+      161,
+      161,
+      161,
+      160
+    ]
+  },
+  "reservoir" : 3735928559,
+  "IOB" : 1.249,
+  "eventualBG" : 160,
+  "units" : 0.1,
+  "TDD" : 26.95,
+  "bg" : 85,
+  "duration" : 60,
+  "deliverAt" : "2025-04-28T19:41:43.564Z",
+  "manualBolusErrorString" : 0,
+  "rate" : 0,
+  "temp" : "absolute",
+  "minDelta" : 5,
+  "COB" : 34,
+  "insulin" : {
+    "scheduled_basal" : 0.25,
+    "bolus" : 22.4,
+    "TDD" : 26.95,
+    "temp_basal" : 4.3
+  },
+  "insulinForManualBolus" : 0.8,
+  "timestamp" : "2025-04-28T19:41:48.453Z",
+  "sensitivityRatio" : 0.9863849810728643,
+  "threshold" : 3.7
+}

+ 223 - 0
TrioTests/JSONImporterData/suggested.json

@@ -0,0 +1,223 @@
+{
+  "sensitivityRatio" : 0.9863849810728643,
+  "timestamp" : "2025-04-28T19:41:43.564Z",
+  "COB" : 34,
+  "IOB" : 1.249,
+  "reason" : "Autosens ratio: 0.99, ISF: 4.5→4.6, COB: 34, Dev: 4.2, BGI: -0.4, CR: 15, Target: 5.2, minPredBG 6.5, minGuardBG 4.3, IOBpredBG 2.7, COBpredBG 8.9, UAMpredBG 3.4, TDD: 26.95 U, 83% Bolus 17% Basal, Dynamic ISF/CR: On/Off, Sigmoid function, AF: 0.14, Basal ratio: 0.93; Eventual BG 8.9 >= 5.2,  insulinReq 0.29; setting 60m low temp of 0U/h. Microbolusing 0.1U. ",
+  "eventualBG" : 160,
+  "reservoir" : 3735928559,
+  "insulinReq" : 0.29,
+  "TDD" : 26.95,
+  "insulin" : {
+    "temp_basal" : 4.3,
+    "scheduled_basal" : 0.25,
+    "TDD" : 26.95,
+    "bolus" : 22.4
+  },
+  "predBGs" : {
+    "IOB" : [
+      85,
+      89,
+      92,
+      95,
+      97,
+      99,
+      99,
+      99,
+      99,
+      97,
+      95,
+      92,
+      89,
+      85,
+      82,
+      79,
+      77,
+      74,
+      72,
+      70,
+      68,
+      66,
+      65,
+      63,
+      62,
+      60,
+      59,
+      58,
+      57,
+      56,
+      56,
+      55,
+      54,
+      53,
+      53,
+      52,
+      52,
+      51,
+      51,
+      51,
+      50,
+      50,
+      50,
+      50,
+      49
+    ],
+    "ZT" : [
+      85,
+      78,
+      71,
+      64,
+      58,
+      52,
+      47,
+      42,
+      39,
+      39,
+      39,
+      39,
+      39,
+      39,
+      39,
+      39,
+      39,
+      39,
+      39,
+      39,
+      39,
+      39,
+      39,
+      39,
+      39,
+      39,
+      39,
+      39,
+      39,
+      39,
+      39,
+      39,
+      39,
+      39,
+      39,
+      39,
+      39,
+      39,
+      42,
+      44,
+      47,
+      49,
+      52,
+      54,
+      57,
+      59,
+      62,
+      65
+    ],
+    "UAM" : [
+      85,
+      89,
+      93,
+      96,
+      99,
+      101,
+      102,
+      103,
+      104,
+      104,
+      103,
+      102,
+      100,
+      98,
+      95,
+      92,
+      89,
+      87,
+      84,
+      82,
+      80,
+      78,
+      77,
+      75,
+      74,
+      73,
+      71,
+      70,
+      69,
+      69,
+      68,
+      67,
+      66,
+      66,
+      65,
+      65,
+      64,
+      64,
+      63,
+      63,
+      63,
+      62,
+      62,
+      62,
+      61
+    ],
+    "COB" : [
+      85,
+      90,
+      94,
+      99,
+      103,
+      108,
+      112,
+      117,
+      121,
+      125,
+      130,
+      134,
+      137,
+      141,
+      145,
+      148,
+      151,
+      154,
+      157,
+      159,
+      161,
+      163,
+      165,
+      166,
+      167,
+      168,
+      168,
+      168,
+      168,
+      168,
+      167,
+      166,
+      165,
+      165,
+      164,
+      164,
+      163,
+      163,
+      162,
+      162,
+      161,
+      161,
+      161,
+      161,
+      160
+    ]
+  },
+  "ISF" : 4.6,
+  "rate" : 0,
+  "minDelta" : 5,
+  "expectedDelta" : -5.9,
+  "threshold" : 3.7,
+  "insulinForManualBolus" : 0.8,
+  "duration" : 60,
+  "temp" : "absolute",
+  "manualBolusErrorString" : 0,
+  "deliverAt" : "2025-04-28T19:41:43.564Z",
+  "current_target" : 94,
+  "units" : 0.1,
+  "bg" : 85
+}

+ 79 - 0
TrioTests/JSONImporterTests.swift

@@ -17,6 +17,7 @@ class BundleReference {}
     var coreDataStack: CoreDataStack!
     var context: NSManagedObjectContext!
     var importer: JSONImporter!
+    @Injected() var determinationStorage: DeterminationStorage!
 
     init() async throws {
         // In-memory Core Data for tests
@@ -72,4 +73,82 @@ class BundleReference {}
 
         #expect(allReadings.isEmpty)
     }
+
+    @Test("Import determination data with value checks") func testImportDeterminationDetails() async throws {
+        let testBundle = Bundle(for: BundleReference.self)
+        let enactedPath = testBundle.path(forResource: "enacted", ofType: "json")!
+        let enactedUrl = URL(filePath: enactedPath)
+        let suggestedPath = testBundle.path(forResource: "suggested", ofType: "json")!
+        let suggestedUrl = URL(filePath: suggestedPath)
+
+        let now = Date("2025-04-28T20:50:00.000Z")!
+        try await importer.importOrefDetermination(enactedUrl: enactedUrl, suggestedUrl: suggestedUrl, now: now)
+        // run the import againt to check our deduplication logic
+        try await importer.importOrefDetermination(enactedUrl: enactedUrl, suggestedUrl: suggestedUrl, now: now)
+
+        let determinations = try await coreDataStack.fetchEntitiesAsync(
+            ofType: OrefDetermination.self,
+            onContext: context,
+            predicate: NSPredicate(format: "TRUEPREDICATE"),
+            key: "deliverAt",
+            ascending: false
+        ) as? [OrefDetermination] ?? []
+
+        #expect(determinations.count == 1) // single determination, as enacted.deliverAt and suggested.deliverAt match
+
+        let determination = determinations.first!
+
+        #expect(determination.deliverAt == Date("2025-04-28T19:41:43.564Z"))
+        #expect(determination.timestamp == Date("2025-04-28T19:41:48.453Z"))
+        #expect(determination.enacted == true)
+        #expect(determination.reason?.starts(with: "Autosens ratio: 0.99") == true)
+        #expect(determination.insulinReq == Decimal(string: "0.29").map(NSDecimalNumber.init))
+        #expect(determination.eventualBG! == NSDecimalNumber(160))
+        #expect(determination.sensitivityRatio == Decimal(string: "0.9863849810728643").map(NSDecimalNumber.init))
+        #expect(determination.rate == Decimal(string: "0").map(NSDecimalNumber.init))
+        #expect(determination.duration == NSDecimalNumber(60))
+        #expect(determination.iob == Decimal(string: "1.249").map(NSDecimalNumber.init))
+        #expect(determination.cob == 34)
+        #expect(determination.temp == "absolute")
+        #expect(determination.glucose == NSDecimalNumber(85))
+        #expect(determination.reservoir == Decimal(string: "3735928559").map(NSDecimalNumber.init))
+        #expect(determination.insulinSensitivity == Decimal(string: "4.6").map(NSDecimalNumber.init))
+        #expect(determination.currentTarget == Decimal(string: "94").map(NSDecimalNumber.init))
+        #expect(determination.insulinForManualBolus == Decimal(string: "0.8").map(NSDecimalNumber.init))
+        #expect(determination.manualBolusErrorString == Decimal(string: "0").map(NSDecimalNumber.init))
+        #expect(determination.minDelta == NSDecimalNumber(5))
+        #expect(determination.expectedDelta == Decimal(string: "-5.9").map(NSDecimalNumber.init))
+        #expect(determination.threshold == Decimal(string: "3.7").map(NSDecimalNumber.init))
+        #expect(determination.carbRatio == nil) // not present in JSON
+
+        // TODO: fix forecast testing
+//        let forecastHierarchy = try await determinationStorage.fetchForecastHierarchy(for: determination.objectID, in: context)
+//
+//        for entry in forecastHierarchy {
+//            let (_, forecast, values) = await determinationStorage.fetchForecastObjects(for: entry, in: context)
+//
+//            // Basic checks
+//            #expect(forecast != nil)
+//            #expect(!values.isEmpty)
+//
+//            if let forecast = forecast {
+//                let sortedValues = values.sorted { $0.index < $1.index }
+//                let prefix = sortedValues.prefix(5).compactMap(\.value)
+//                let type = forecast.type?.lowercased()
+//
+//                switch type {
+//                case "zt":
+//                    #expect(prefix == [85, 78, 71, 64, 58])
+//                case "iob":
+//                    #expect(prefix == [85, 89, 92, 95, 97])
+//                case "uam":
+//                    #expect(prefix == [85, 89, 93, 96, 99])
+//                case "cob":
+//                    #expect(prefix == [85, 90, 94, 99, 103])
+//                default:
+//                    break // Skip unknown forecast types silently
+//                }
+//            }
+//        }
+    }
 }