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

Replay makeProfile invocations

This commit includes the test cases needed to replay Profile
inocations. It also includes appropriate profile.js and
profile-prepare.js files that take a `clock` as an input and uses that
vs the current time of day to facilitate replay.
Sam King 3 месяцев назад
Родитель
Сommit
3a3f707e6d

+ 8 - 0
Trio.xcodeproj/project.pbxproj

@@ -319,6 +319,8 @@
 		3BC26E552D7418830066ACD6 /* IobSuspendTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BC26E542D7418830066ACD6 /* IobSuspendTests.swift */; };
 		3BC4053B2D931620006A03E9 /* IobJsonTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BC4053A2D931620006A03E9 /* IobJsonTests.swift */; };
 		3BCA5F7C2DC7B16400A7EAC7 /* pumphistory-with-external.json in Resources */ = {isa = PBXBuildFile; fileRef = 3BCA5F7B2DC7B15400A7EAC7 /* pumphistory-with-external.json */; };
+		3BCDDD312F219AF900496A94 /* ProfileJsonTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BCDDD302F219AF300496A94 /* ProfileJsonTests.swift */; };
+		3BCDDD772F21A5F300496A94 /* profile-prepare.js in Resources */ = {isa = PBXBuildFile; fileRef = 3BCDDD762F21A5F300496A94 /* profile-prepare.js */; };
 		3BCE75B32D4B38AE009E9453 /* InsulinSensitivities+Convert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BCE75B22D4B38A0009E9453 /* InsulinSensitivities+Convert.swift */; };
 		3BCE75B52D4B391F009E9453 /* Decimal+rounding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BCE75B42D4B3917009E9453 /* Decimal+rounding.swift */; };
 		3BD433AE2F01CDE600897F7D /* autosens-prepare-24.js in Resources */ = {isa = PBXBuildFile; fileRef = 3BD433AD2F01CDD900897F7D /* autosens-prepare-24.js */; };
@@ -1255,6 +1257,8 @@
 		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>"; };
 		3BCA5F7B2DC7B15400A7EAC7 /* pumphistory-with-external.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "pumphistory-with-external.json"; sourceTree = "<group>"; };
+		3BCDDD302F219AF300496A94 /* ProfileJsonTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileJsonTests.swift; sourceTree = "<group>"; };
+		3BCDDD762F21A5F300496A94 /* profile-prepare.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; path = "profile-prepare.js"; sourceTree = "<group>"; };
 		3BCE75B22D4B38A0009E9453 /* InsulinSensitivities+Convert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "InsulinSensitivities+Convert.swift"; sourceTree = "<group>"; };
 		3BCE75B42D4B3917009E9453 /* Decimal+rounding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Decimal+rounding.swift"; sourceTree = "<group>"; };
 		3BD433AD2F01CDD900897F7D /* autosens-prepare-24.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; path = "autosens-prepare-24.js"; sourceTree = "<group>"; };
@@ -2991,6 +2995,7 @@
 				3B5CD2C12D4AECD500CE213C /* ProfileBasalTests.swift */,
 				3B5CD2C32D4AECD500CE213C /* ProfileCarbsTests.swift */,
 				3B5CD2C42D4AECD500CE213C /* ProfileIsfTests.swift */,
+				3BCDDD302F219AF300496A94 /* ProfileJsonTests.swift */,
 				3B5CD2C52D4AECD500CE213C /* ProfileJavascriptTests.swift */,
 				3B5CD2C62D4AECD500CE213C /* ProfileTargetsTests.swift */,
 				3BAC92CC2E57859600B853DA /* SetTempBasalTests.swift */,
@@ -3044,6 +3049,7 @@
 				3BF92F292D86DEE9006B545A /* meal.js */,
 				3BBE323B2F12AB22005F9665 /* meal-prepare.js */,
 				3BF92F2A2D86DEE9006B545A /* profile.js */,
+				3BCDDD762F21A5F300496A94 /* profile-prepare.js */,
 			);
 			path = bundle;
 			sourceTree = "<group>";
@@ -4487,6 +4493,7 @@
 				3BD6CE262DC24CFD00FA0472 /* pumphistory-24h-zoned.json in Resources */,
 				DDD78A912DC4064800AC63F3 /* carbhistory.json in Resources */,
 				3B997DD32DC02AEF006B6BB2 /* glucose.json in Resources */,
+				3BCDDD772F21A5F300496A94 /* profile-prepare.js in Resources */,
 				3BD433B02F01CDF500897F7D /* autosens-prepare-8.js in Resources */,
 				DDD78AD92DC421B500AC63F3 /* enacted.json in Resources */,
 				DD3C47B32DC5608A003DD20D /* newerSuggested.json in Resources */,
@@ -5278,6 +5285,7 @@
 				3B8B5D3C2DF523C000365ED3 /* AutosensJsonTests.swift in Sources */,
 				3B2A3BC32E2B19F700658FB9 /* DynamicISFTests.swift in Sources */,
 				BD8FC0662D661A0000B95AED /* GlucoseStorageTests.swift in Sources */,
+				3BCDDD312F219AF900496A94 /* ProfileJsonTests.swift in Sources */,
 				BD8FC05B2D6618AF00B95AED /* DeterminationStorageTests.swift in Sources */,
 				3BAAE60C2DE7766C0049589B /* DynamicISFEnableTests.swift in Sources */,
 				3BAC929B2E55FF5300B853DA /* DetermineBasalEnableSmbTests.swift in Sources */,

+ 93 - 0
TrioTests/OpenAPSSwiftTests/ProfileJsonTests.swift

@@ -0,0 +1,93 @@
+import Foundation
+import Testing
+@testable import Trio
+
+@Suite("Profile testing using JSON inputs", .serialized) struct ProfileJsonTests {
+    let timeZoneForTests = TimeZoneForTests()
+
+    @Test(
+        "Profile should produce same results for fixed JS",
+        .enabled(if: ReplayTests.enabled)
+    ) func replayErrorInputs() async throws {
+        // Note: This test case can only test one timezone per invocation
+        // so you need to manually change this to try out errors from
+        // different timezones
+        let testingTimezone = ReplayTests.timezone
+        let files = try await HttpFiles.listFiles()
+        for filePath in files {
+            let algorithmComparison = try await HttpFiles.downloadFile(at: filePath)
+            print("Checking \(filePath) @ \(algorithmComparison.createdAt)")
+            guard algorithmComparison.timezone == testingTimezone else {
+                continue
+            }
+            guard let profileInputs = algorithmComparison.makeProfileInput else {
+                print("Skipping, no profileInputs found")
+                if let str = algorithmComparison.comparisonError {
+                    print(str)
+                }
+                if let str = algorithmComparison.swiftException {
+                    print(str)
+                }
+                continue
+            }
+
+            timeZoneForTests.setTimezone(identifier: algorithmComparison.timezone)
+            try await checkFixedJsAgainstSwift(profileInputs: profileInputs)
+            print("Checked \(filePath) \(algorithmComparison.timezone)")
+            timeZoneForTests.resetTimezone()
+        }
+    }
+
+    func checkFixedJsAgainstSwift(profileInputs: MakeProfileInputs) async throws {
+        let openAps = OpenAPSFixed()
+        let (profileResultSwift, _) = OpenAPSSwift.makeProfile(
+            preferences: profileInputs.preferences,
+            pumpSettings: profileInputs.pumpSettings,
+            bgTargets: profileInputs.bgTargets,
+            basalProfile: profileInputs.basalProfile,
+            isf: profileInputs.isf,
+            carbRatio: profileInputs.carbRatios,
+            tempTargets: profileInputs.tempTargets,
+            model: profileInputs.model,
+            trioSettings: profileInputs.trioSettings,
+            clock: profileInputs.clock
+        )
+
+        let profileResultJavascript = await openAps.makeProfileJavascript(
+            preferences: profileInputs.preferences,
+            pumpSettings: profileInputs.pumpSettings,
+            bgTargets: profileInputs.bgTargets,
+            basalProfile: profileInputs.basalProfile,
+            isf: profileInputs.isf,
+            carbRatio: profileInputs.carbRatios,
+            tempTargets: profileInputs.tempTargets,
+            model: profileInputs.model,
+            autotune: RawJSON.null,
+            trioSettings: profileInputs.trioSettings,
+            clock: profileInputs.clock
+        )
+
+        let comparison = JSONCompare.createComparison(
+            function: .makeProfile,
+            swift: profileResultSwift,
+            swiftDuration: 0.1,
+            javascript: profileResultJavascript,
+            javascriptDuration: 0.1,
+            iobInputs: nil,
+            mealInputs: nil,
+            autosensInputs: nil,
+            determineBasalInputs: nil,
+            makeProfileInputs: nil
+        )
+
+        if comparison.resultType == .valueDifference {
+            print(comparison.differences!.prettyPrintedJSON!)
+        }
+
+        if comparison.resultType != .matching {
+            print("REPLAY ERROR: Fixed JS didn't match")
+        }
+
+        #expect(comparison.resultType == .matching)
+    }
+}

+ 113 - 0
TrioTests/OpenAPSSwiftTests/javascript/bundle/profile-prepare.js

@@ -0,0 +1,113 @@
+//для pumpprofile.json параметры: settings/settings.json settings/bg_targets.json settings/insulin_sensitivities.json settings/basal_profile.json preferences.json settings/carb_ratios.json settings/temptargets.json settings/model.json
+//для profile.json параметры: settings/settings.json settings/bg_targets.json settings/insulin_sensitivities.json settings/basal_profile.json preferences.json settings/carb_ratios.json settings/temptargets.json settings/model.json settings/autotune.json
+
+function generate(pumpsettings_data, bgtargets_data, isf_data, basalprofile_data, preferences_input = false, carbratio_input = false, temptargets_input = false, model_input = false, autotune_input = false, trio_data, clock_input = false) {
+    if (bgtargets_data.units !== 'mg/dL') {
+        if (bgtargets_data.units === 'mmol/L') {
+            for (var i = 0, len = bgtargets_data.targets.length; i < len; i++) {
+                bgtargets_data.targets[i].high = bgtargets_data.targets[i].high * 18;
+                bgtargets_data.targets[i].low = bgtargets_data.targets[i].low * 18;
+            }
+            bgtargets_data.units = 'mg/dL';
+        } else {
+            return { "error" : 'BG Target data is expected to be expressed in mg/dL or mmol/L. Found '+ bgtargets_data.units };
+        }
+    }
+    
+    if (isf_data.units !== 'mg/dL') {
+        if (isf_data.units === 'mmol/L') {
+            for (var i = 0, len = isf_data.sensitivities.length; i < len; i++) {
+                isf_data.sensitivities[i].sensitivity = isf_data.sensitivities[i].sensitivity * 18;
+            }
+            isf_data.units = 'mg/dL';
+        } else {
+            return { "error" : 'ISF is expected to be expressed in mg/dL or mmol/L. Found '+ isf_data.units };
+        }
+    }
+
+    var autotune_data = { };
+    if (autotune_input) {
+        autotune_data = autotune_input;
+    }
+
+    var temptargets_data = { };
+    if (temptargets_input) {
+        temptargets_data = temptargets_input;
+    }
+    
+    var trioData = { };
+    if (trio_data) {
+        trioData = trio_data;
+    }
+
+    var clock = null;
+    if (clock_input) {
+        clock = new Date(clock_input);
+    }
+
+    var model_data = { };
+    if (model_input) {
+        model_data = model_input.replace(/"/gi, '');
+    }
+
+    var carbratio_data = { };
+    if (carbratio_input) {
+        var errors = [ ];
+        if (!(carbratio_input.schedule && carbratio_input.schedule[0].start && carbratio_input.schedule[0].ratio)) {
+          errors.push("Carb ratio data should have an array called schedule with a start and ratio fields.");
+        }
+        if (carbratio_input.units !== 'grams' && carbratio_input.units !== 'exchanges')  {
+          errors.push("Carb ratio should have units field set to 'grams' or 'exchanges'.");
+        }
+        if (errors.length) {
+          return { "error" : errors.join(' ') };
+        }
+        carbratio_data = carbratio_input;
+    }
+
+    var preferences = { };
+    if (preferences_input) {
+        preferences = preferences_input;
+        if (preferences.curve === "rapid-acting") {
+            if (preferences.useCustomPeakTime) {
+                preferences.insulinPeakTime =
+                Math.max(50, Math.min(preferences.insulinPeakTime, 120));
+            } else { preferences.insulinPeakTime = 75; }
+        } 
+        else if (preferences.curve === "ultra-rapid") {
+            if (preferences.useCustomPeakTime) {
+                preferences.insulinPeakTime =
+                Math.max(35, Math.min(preferences.insulinPeakTime, 100));
+            } else { preferences.insulinPeakTime = 55; }
+        }
+    }
+
+    var inputs = { };
+    //add all preferences to the inputs
+    for (var pref in preferences) {
+      if (preferences.hasOwnProperty(pref)) {
+        inputs[pref] = preferences[pref];
+      }
+    }
+
+    inputs.max_iob = inputs.max_iob || 0;
+    //set these after to make sure nothing happens if they are also set in preferences
+    inputs.settings = pumpsettings_data;
+    inputs.targets = bgtargets_data;
+    inputs.basals = basalprofile_data;
+    inputs.isf = isf_data;
+    inputs.carbratio = carbratio_data;
+    inputs.temptargets = temptargets_data;
+    inputs.model = model_data;
+    inputs.autotune = autotune_data;
+    inputs.clock = clock;
+
+    if (autotune_data) {
+        if (autotune_data.basalprofile) { inputs.basals = autotune_data.basalprofile; }
+        if (!trioData.onlyAutotuneBasals) {
+            if (autotune_data.isfProfile) { inputs.isf = autotune_data.isfProfile; }
+            if (autotune_data.carb_ratio) { inputs.carbratio.schedule[0].ratio = autotune_data.carb_ratio; }
+        }
+    }
+    return trio_profile(inputs);
+}

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


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

@@ -211,6 +211,51 @@ final class OpenAPSFixed {
             return .failure(error)
         }
     }
+
+    func makeProfileJavascript(
+        preferences: JSON,
+        pumpSettings: JSON,
+        bgTargets: JSON,
+        basalProfile: JSON,
+        isf: JSON,
+        carbRatio: JSON,
+        tempTargets: JSON,
+        model: JSON,
+        autotune: JSON,
+        trioSettings: JSON,
+        clock: JSON
+    ) async -> OrefFunctionResult {
+        do {
+            let testBundle = Bundle(for: OpenAPSFixed.self)
+            let result = try await withCheckedThrowingContinuation { continuation in
+                let jsWorker = JavaScriptWorker(poolSize: 1)
+                jsWorker.inCommonContext { worker in
+                    worker.evaluateBatch(scripts: [
+                        Script(name: "prepare/log.js"),
+                        Script.fromTestingBundle(name: "profile.js", bundle: testBundle),
+                        Script.fromTestingBundle(name: "profile-prepare.js", bundle: testBundle)
+                    ])
+                    let result = worker.call(function: "generate", with: [
+                        pumpSettings,
+                        bgTargets,
+                        isf,
+                        basalProfile,
+                        preferences,
+                        carbRatio,
+                        tempTargets,
+                        model,
+                        autotune,
+                        trioSettings,
+                        clock
+                    ])
+                    continuation.resume(returning: result)
+                }
+            }
+            return .success(result)
+        } catch {
+            return .failure(error)
+        }
+    }
 }
 
 extension Script {