Просмотр исходного кода

Merge pull request #451 from kingst/oref-swift-set-timezone-in-tests

Set TimeZone for Swift tests + add more iob diagnostic unit tests
Deniz Cengiz 1 год назад
Родитель
Сommit
4331bb671d

+ 16 - 4
Trio.xcodeproj/project.pbxproj

@@ -278,6 +278,10 @@
 		3B5F45B62D6A239500F70982 /* DoubleApproximateMatching.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B5F45B52D6A239000F70982 /* DoubleApproximateMatching.swift */; };
 		3BAD36B22D7CDC1A00CC298D /* MainLoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BAD36B12D7CDC1400CC298D /* MainLoadingView.swift */; };
 		3BAD36CC2D7D420E00CC298D /* CoreDataInitializationCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BAD36CB2D7D420500CC298D /* CoreDataInitializationCoordinator.swift */; };
+		3BC0AA3B2DA74C87000DF7B7 /* iob-total.js in Resources */ = {isa = PBXBuildFile; fileRef = 3BC0AA3A2DA74C87000DF7B7 /* iob-total.js */; };
+		3BC0AA3E2DA817EC000DF7B7 /* iob-calculate.js in Resources */ = {isa = PBXBuildFile; fileRef = 3BC0AA3C2DA817EC000DF7B7 /* iob-calculate.js */; };
+		3BC0AA3F2DA817EC000DF7B7 /* iob-history.js in Resources */ = {isa = PBXBuildFile; fileRef = 3BC0AA3D2DA817EC000DF7B7 /* iob-history.js */; };
+		3BC0AA412DA8B900000DF7B7 /* iob-history-prepare.js in Resources */ = {isa = PBXBuildFile; fileRef = 3BC0AA402DA8B8F7000DF7B7 /* iob-history-prepare.js */; };
 		3BC26E552D7418830066ACD6 /* IobSuspendTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BC26E542D7418830066ACD6 /* IobSuspendTests.swift */; };
 		3BC4053B2D931620006A03E9 /* IobJsonTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BC4053A2D931620006A03E9 /* IobJsonTests.swift */; };
 		3BCE75B32D4B38AE009E9453 /* InsulinSensitivities+Convert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BCE75B22D4B38A0009E9453 /* InsulinSensitivities+Convert.swift */; };
@@ -297,7 +301,6 @@
 		3BF92F312D86DEE9006B545A /* meal.js in Resources */ = {isa = PBXBuildFile; fileRef = 3BF92F292D86DEE9006B545A /* meal.js */; };
 		3BF92F322D86DEE9006B545A /* glucose-get-last.js in Resources */ = {isa = PBXBuildFile; fileRef = 3BF92F262D86DEE9006B545A /* glucose-get-last.js */; };
 		3BF92F332D86DEE9006B545A /* iob.js in Resources */ = {isa = PBXBuildFile; fileRef = 3BF92F272D86DEE9006B545A /* iob.js */; };
-		3BF92F342D86DEE9006B545A /* iob-history.js in Resources */ = {isa = PBXBuildFile; fileRef = 3BF92F282D86DEE9006B545A /* iob-history.js */; };
 		3BF92F352D86DEE9006B545A /* basal-set-temp.js in Resources */ = {isa = PBXBuildFile; fileRef = 3BF92F242D86DEE9006B545A /* basal-set-temp.js */; };
 		3BF92F362D86DEE9006B545A /* autotune-core.js in Resources */ = {isa = PBXBuildFile; fileRef = 3BF92F222D86DEE9006B545A /* autotune-core.js */; };
 		3BF92F382D86E10B006B545A /* OpenAPSFixed.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BF92F372D86E106006B545A /* OpenAPSFixed.swift */; };
@@ -1094,6 +1097,10 @@
 		3B5F45B52D6A239000F70982 /* DoubleApproximateMatching.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DoubleApproximateMatching.swift; sourceTree = "<group>"; };
 		3BAD36B12D7CDC1400CC298D /* MainLoadingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainLoadingView.swift; sourceTree = "<group>"; };
 		3BAD36CB2D7D420500CC298D /* CoreDataInitializationCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataInitializationCoordinator.swift; sourceTree = "<group>"; };
+		3BC0AA3A2DA74C87000DF7B7 /* iob-total.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; path = "iob-total.js"; sourceTree = "<group>"; };
+		3BC0AA3C2DA817EC000DF7B7 /* iob-calculate.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; path = "iob-calculate.js"; sourceTree = "<group>"; };
+		3BC0AA3D2DA817EC000DF7B7 /* iob-history.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; path = "iob-history.js"; sourceTree = "<group>"; };
+		3BC0AA402DA8B8F7000DF7B7 /* iob-history-prepare.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; path = "iob-history-prepare.js"; sourceTree = "<group>"; };
 		3BC26E542D7418830066ACD6 /* IobSuspendTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IobSuspendTests.swift; sourceTree = "<group>"; };
 		3BC4053A2D931620006A03E9 /* IobJsonTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IobJsonTests.swift; sourceTree = "<group>"; };
 		3BCE75B22D4B38A0009E9453 /* InsulinSensitivities+Convert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "InsulinSensitivities+Convert.swift"; sourceTree = "<group>"; };
@@ -1113,7 +1120,6 @@
 		3BF92F252D86DEE9006B545A /* determine-basal.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; path = "determine-basal.js"; sourceTree = "<group>"; };
 		3BF92F262D86DEE9006B545A /* glucose-get-last.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; path = "glucose-get-last.js"; sourceTree = "<group>"; };
 		3BF92F272D86DEE9006B545A /* iob.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; path = iob.js; sourceTree = "<group>"; };
-		3BF92F282D86DEE9006B545A /* iob-history.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; path = "iob-history.js"; sourceTree = "<group>"; };
 		3BF92F292D86DEE9006B545A /* meal.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; path = meal.js; sourceTree = "<group>"; };
 		3BF92F2A2D86DEE9006B545A /* profile.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; path = profile.js; sourceTree = "<group>"; };
 		3BF92F372D86E106006B545A /* OpenAPSFixed.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenAPSFixed.swift; sourceTree = "<group>"; };
@@ -2703,6 +2709,7 @@
 		3BF92F2B2D86DEE9006B545A /* bundle */ = {
 			isa = PBXGroup;
 			children = (
+				3BC0AA402DA8B8F7000DF7B7 /* iob-history-prepare.js */,
 				3BF92F212D86DEE9006B545A /* autosens.js */,
 				3BF92F222D86DEE9006B545A /* autotune-core.js */,
 				3BF92F232D86DEE9006B545A /* autotune-prep.js */,
@@ -2710,7 +2717,9 @@
 				3BF92F252D86DEE9006B545A /* determine-basal.js */,
 				3BF92F262D86DEE9006B545A /* glucose-get-last.js */,
 				3BF92F272D86DEE9006B545A /* iob.js */,
-				3BF92F282D86DEE9006B545A /* iob-history.js */,
+				3BC0AA3C2DA817EC000DF7B7 /* iob-calculate.js */,
+				3BC0AA3D2DA817EC000DF7B7 /* iob-history.js */,
+				3BC0AA3A2DA74C87000DF7B7 /* iob-total.js */,
 				3BF92F292D86DEE9006B545A /* meal.js */,
 				3BF92F2A2D86DEE9006B545A /* profile.js */,
 			);
@@ -3973,9 +3982,12 @@
 				3BF92F312D86DEE9006B545A /* meal.js in Resources */,
 				3BF92F322D86DEE9006B545A /* glucose-get-last.js in Resources */,
 				3BF92F332D86DEE9006B545A /* iob.js in Resources */,
-				3BF92F342D86DEE9006B545A /* iob-history.js in Resources */,
 				3BF92F352D86DEE9006B545A /* basal-set-temp.js in Resources */,
 				3BF92F362D86DEE9006B545A /* autotune-core.js in Resources */,
+				3BC0AA3B2DA74C87000DF7B7 /* iob-total.js in Resources */,
+				3BC0AA3E2DA817EC000DF7B7 /* iob-calculate.js in Resources */,
+				3BC0AA3F2DA817EC000DF7B7 /* iob-history.js in Resources */,
+				3BC0AA412DA8B900000DF7B7 /* iob-history-prepare.js in Resources */,
 				3BF92F3A2D86F1AA006B545A /* iob-error-log.json in Resources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;

+ 54 - 0
TrioTests/OpenAPSSwiftTests/IobHistoryTests.swift

@@ -522,4 +522,58 @@ import Testing
         // Total: 0.2
         #expect(treatments.netInsulin().isWithin(0.01, of: 0.2))
     }
+
+    @Test("should produce -0.5 IoB") func zerosIoBAroundSuspend() async throws {
+        let basalprofile = [
+            BasalProfileEntry(
+                start: "00:00:00",
+                minutes: 0,
+                rate: 0.65
+            )
+        ]
+
+        let now = Calendar.current.startOfDay(for: Date()) + 60.minutesToSeconds
+
+        let pumpHistory = [
+            ComputedPumpHistoryEvent.forTest(
+                type: .tempBasal,
+                timestamp: now - 45.minutesToSeconds,
+                duration: nil,
+                rate: 0,
+                temp: .absolute
+            ),
+            ComputedPumpHistoryEvent.forTest(
+                type: .tempBasalDuration,
+                timestamp: now - 45.minutesToSeconds,
+                durationMin: 60
+            ),
+            ComputedPumpHistoryEvent.forTest(
+                type: .pumpSuspend,
+                timestamp: now - 40.minutesToSeconds
+            ),
+            ComputedPumpHistoryEvent.forTest(
+                type: .pumpResume,
+                timestamp: now - 39.minutesToSeconds
+            )
+        ]
+
+        var profile = Profile()
+        profile.dia = 10
+        profile.basalprofile = basalprofile
+        profile.currentBasal = 0.65
+        profile.maxDailyBasal = 0.65
+        profile.suspendZerosIob = true
+
+        let treatments = try IobHistory.calcTempTreatments(
+            history: pumpHistory,
+            profile: profile,
+            clock: now,
+            autosens: nil,
+            zeroTempDuration: nil
+        )
+
+        let tempBasals = treatments.filter { $0.type == .tempBasal }
+        print(treatments)
+        #expect(treatments.netInsulin().isWithin(0.01, of: -0.5))
+    }
 }

+ 134 - 3
TrioTests/OpenAPSSwiftTests/IobJsonTests.swift

@@ -18,22 +18,60 @@ class BundleReference {}
 /// You can find more information about it from the `trio-oref-logs` repo.
 @Suite("IoB using real pump history JSON", .serialized) struct IobJsonTests {
     private var originalTZ: String? = ProcessInfo.processInfo.environment["TZ"]
+    private var originalDefaultTimeZone: TimeZone? = TimeZone.current
 
     // Helper function to set timezone
     private func setTimezone(identifier: String) {
+        // Set environment variable
         setenv("TZ", identifier, 1)
         tzset() // Make the change take effect
+
+        // Force update the default TimeZone
+        // This is the critical missing piece
+        if let timeZone = TimeZone(identifier: identifier) {
+            TimeZone.ReferenceType.default = timeZone
+
+            // For extra assurance, you can log to verify
+            print("Timezone set to: \(TimeZone.current.identifier)")
+        } else {
+            print("Failed to create TimeZone with identifier: \(identifier)")
+        }
     }
 
     // Helper function to reset timezone
     private func resetTimezone() {
-        // Restore system timezone
+        // Restore system timezone from environment
         if let originalTZ = originalTZ {
             setenv("TZ", originalTZ, 1)
         } else {
             unsetenv("TZ")
         }
         tzset()
+
+        // Restore original default TimeZone
+        if let originalTimeZone = originalDefaultTimeZone {
+            TimeZone.ReferenceType.default = originalTimeZone
+        }
+    }
+
+    struct IobHistoryResult: Codable {
+        var insulin: Decimal?
+        var rate: Decimal?
+        var duration: Decimal?
+        var timestamp: String?
+        var started_at: String?
+        var created_at: String?
+        var date: Decimal?
+
+        enum CodingKeys: String, CodingKey {
+            case insulin
+            case rate
+            case duration
+            case timestamp
+            case started_at
+            case created_at
+            case date
+        }
     }
 
     // Note: This test case has a memory leak so limit your inputs
@@ -65,8 +103,8 @@ class BundleReference {}
 
             setTimezone(identifier: algorithmComparison.timezone)
 
-            try await checkFixedJsAgainstSwift(iobInputs: algorithmComparison.iobInput!)
-            try await checkBundleJsAgainstSwift(iobInputs: algorithmComparison.iobInput!)
+            try await checkFixedJsAgainstSwift(iobInputs: iobInputs)
+            try await checkBundleJsAgainstSwift(iobInputs: iobInputs)
 
             resetTimezone()
         }
@@ -140,6 +178,91 @@ class BundleReference {}
         #expect(comparison.resultType == .valueDifference)
     }
 
+    func checkHistoryConsistency(swiftTreatments: [ComputedPumpHistoryEvent], jsTreatments: [IobHistoryResult]) {
+        let swiftNetBolus = swiftTreatments.compactMap(\.insulin).filter({ $0 >= 0.1 }).reduce(0, +)
+        let jsNetBolus = jsTreatments.compactMap(\.insulin).filter({ $0 >= 0.1 }).reduce(0, +)
+
+        let swiftNetBasal = swiftTreatments.compactMap(\.insulin).filter({ $0 < 0.1 }).reduce(0, +)
+        let jsNetBasal = jsTreatments.compactMap(\.insulin).filter({ $0 < 0.1 }).reduce(0, +)
+
+        #expect(swiftNetBasal == jsNetBasal)
+        #expect(swiftNetBolus == jsNetBolus)
+    }
+
+    func checkRunningBasal(swiftTreatments: [ComputedPumpHistoryEvent], jsTreatments: [IobHistoryResult]) {
+        let swiftBasals = swiftTreatments.filter({ $0.rate != nil }).filter({ $0.duration! > 0 })
+        let jsBasals = jsTreatments.filter({ $0.rate != nil }).filter({ $0.duration! > 0 })
+
+        #expect(swiftBasals.count == jsBasals.count)
+        for (swift, js) in zip(swiftBasals, jsBasals) {
+            #expect(Decimal(swift.date) == js.date!)
+            #expect(swift.duration!.isWithin(0.01, of: js.duration!))
+            #expect(swift.rate == js.rate)
+
+            let start = js.date!
+            let end = js.date! + js.duration! * 60 * 1000
+            let swiftTempBolus = swiftTreatments
+                .filter({ Decimal($0.date) >= start && Decimal($0.date) < end && $0.insulin != nil && $0.insulin! < 0.1 })
+                .map({ $0.insulin! }).reduce(0, +)
+            let jsTempBolus = jsTreatments
+                .filter({ $0.date! >= start && $0.date! < end && $0.insulin != nil && $0.insulin! < 0.1 }).map({ $0.insulin! })
+                .reduce(0, +)
+
+            if swiftTempBolus != jsTempBolus {
+                print("temp bolus @ \(swift.timestamp) mismatch swift: \(swiftTempBolus) js: \(jsTempBolus)")
+            }
+            #expect(swiftTempBolus == jsTempBolus)
+        }
+    }
+
+    @Test("Debug utility for checking iob-history", .enabled(if: false)) func debugIobHistory() async throws {
+        let testBundle = Bundle(for: BundleReference.self)
+        let path = testBundle.path(forResource: "iob-error-log", ofType: "json")!
+        let data = try Data(contentsOf: URL(fileURLWithPath: path))
+        let decoder = JSONDecoder()
+        decoder.dateDecodingStrategy = .secondsSince1970
+        let algorithmComparison = try decoder.decode(AlgorithmComparison.self, from: data)
+        let iobInputs = algorithmComparison.iobInput!
+
+        setTimezone(identifier: algorithmComparison.timezone)
+
+        let swiftIobHistory = try IobHistory.calcTempTreatments(
+            history: iobInputs.history.map { $0.computedEvent() },
+            profile: iobInputs.profile,
+            clock: iobInputs.clock,
+            autosens: iobInputs.autosens,
+            zeroTempDuration: nil
+        )
+
+        let openAps = OpenAPSFixed()
+        let jsIobHistoryRaw = try await openAps.iobHistory(
+            pumphistory: iobInputs.history,
+            profile: JSONBridge.to(iobInputs.profile),
+            clock: iobInputs.clock,
+            autosens: JSONBridge.to(iobInputs.autosens),
+            zeroTempDuration: RawJSON.null
+        )
+        let jsIobHistory = try JSONDecoder().decode([IobHistoryResult].self, from: jsIobHistoryRaw.rawJSON.data(using: .utf8)!)
+
+        let encoder = JSONCoding.encoder
+        var output = try encoder.encode(swiftIobHistory)
+        var sharedDir = FileManager.default.temporaryDirectory
+        var outputURL = sharedDir.appendingPathComponent("swift_treatments.json")
+        print("Writing to: \(outputURL.path)")
+        try output.write(to: outputURL)
+
+        output = try encoder.encode(jsIobHistory)
+        sharedDir = FileManager.default.temporaryDirectory
+        outputURL = sharedDir.appendingPathComponent("js_treatments.json")
+        print("Writing to: \(outputURL.path)")
+        try output.write(to: outputURL)
+
+        checkHistoryConsistency(swiftTreatments: swiftIobHistory, jsTreatments: jsIobHistory)
+        checkRunningBasal(swiftTreatments: swiftIobHistory, jsTreatments: jsIobHistory)
+
+        resetTimezone()
+    }
+
     /// simple utility for creating inputs for Javascript for use in testing
     @Test("format inputs for Javascript", .enabled(if: false)) func generateJavascriptInputs() throws {
         let testBundle = Bundle(for: BundleReference.self)
@@ -161,6 +284,8 @@ class BundleReference {}
 
         try output.write(to: outputURL)
 
+        setTimezone(identifier: algorithmComparison.timezone)
+
         let treatments = try IobHistory.calcTempTreatments(
             history: iobInputs.history.map { $0.computedEvent() },
             profile: iobInputs.profile,
@@ -169,6 +294,12 @@ class BundleReference {}
             zeroTempDuration: nil
         )
 
+        let iobSomething = try IobCalculation.iobTotal(treatments: treatments, profile: iobInputs.profile, time: iobInputs.clock)
+
+        resetTimezone()
+
+        print(iobSomething.prettyPrintedJSON!)
+
         let treatmentsOut = try encoder.encode(treatments)
         let treatmentsUrl = sharedDir.appendingPathComponent("treatments.json")
 

Разница между файлами не показана из-за своего большого размера
+ 1 - 1
TrioTests/OpenAPSSwiftTests/javascript/bundle/autosens.js


Разница между файлами не показана из-за своего большого размера
+ 1 - 1
TrioTests/OpenAPSSwiftTests/javascript/bundle/autotune-prep.js


Разница между файлами не показана из-за своего большого размера
+ 1 - 0
TrioTests/OpenAPSSwiftTests/javascript/bundle/iob-calculate.js


+ 13 - 0
TrioTests/OpenAPSSwiftTests/javascript/bundle/iob-history-prepare.js

@@ -0,0 +1,13 @@
+function generate(pumphistory_data, profile_data, clock_data, autosens_data, zeroTempDuration) {
+    var inputs = {
+        history: pumphistory_data
+        , profile: profile_data
+        , clock: clock_data
+    };
+
+    if (autosens_data) {
+        inputs.autosens = autosens_data;
+    }
+    
+    return freeaps_iobHistory.calcTempTreatments(inputs, zeroTempDuration);
+}

Разница между файлами не показана из-за своего большого размера
+ 1 - 1
TrioTests/OpenAPSSwiftTests/javascript/bundle/iob-history.js


Разница между файлами не показана из-за своего большого размера
+ 1 - 0
TrioTests/OpenAPSSwiftTests/javascript/bundle/iob-total.js


Разница между файлами не показана из-за своего большого размера
+ 1 - 1
TrioTests/OpenAPSSwiftTests/javascript/bundle/iob.js


Разница между файлами не показана из-за своего большого размера
+ 1 - 1
TrioTests/OpenAPSSwiftTests/javascript/bundle/meal.js


+ 24 - 0
TrioTests/OpenAPSSwiftTests/utils/OpenAPSFixed.swift

@@ -15,6 +15,30 @@ final class OpenAPSFixed {
         return try JSONBridge.to(pumpHistorySwift.sorted(by: { $0.timestamp > $1.timestamp }))
     }
 
+    func iobHistory(pumphistory: JSON, profile: JSON, clock: JSON, autosens: JSON, zeroTempDuration: JSON) async throws -> JSON {
+        let testBundle = Bundle(for: OpenAPSFixed.self)
+        let pumphistory: JSON = try! sortPumpHistory(pumpHistory: pumphistory)
+        let result = try await withCheckedThrowingContinuation { continuation in
+            jsWorker.inCommonContext { worker in
+                worker.evaluateBatch(scripts: [
+                    Script(name: "prepare/log.js"),
+                    Script.fromTestingBundle(name: "iob-history.js", bundle: testBundle),
+                    Script.fromTestingBundle(name: "iob-history-prepare.js", bundle: testBundle)
+                ])
+
+                let result = worker.call(function: "generate", with: [
+                    pumphistory,
+                    profile,
+                    clock,
+                    autosens,
+                    zeroTempDuration
+                ])
+                continuation.resume(returning: result)
+            }
+        }
+        return result
+    }
+
     func iobJavascript(pumphistory: JSON, profile: JSON, clock: JSON, autosens: JSON) async -> OrefFunctionResult {
         do {
             let testBundle = Bundle(for: OpenAPSFixed.self)