Skip to content

Commit 599ba10

Browse files
committed
Adding test for Loop functional algorithm
1 parent ea8c5c0 commit 599ba10

File tree

3 files changed

+141
-1
lines changed

3 files changed

+141
-1
lines changed

Loop.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -470,6 +470,7 @@
470470
C1D0B6302986D4D90098D215 /* LocalizedString.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1D0B62F2986D4D90098D215 /* LocalizedString.swift */; };
471471
C1D0B6312986D4D90098D215 /* LocalizedString.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1D0B62F2986D4D90098D215 /* LocalizedString.swift */; };
472472
C1D289B522F90A52003FFBD9 /* BasalDeliveryState.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1D289B422F90A52003FFBD9 /* BasalDeliveryState.swift */; };
473+
C1D476B42A8ED179002C1C87 /* LoopAlgorithmTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1D476B32A8ED179002C1C87 /* LoopAlgorithmTests.swift */; };
473474
C1D6EEA02A06C7270047DE5C /* MKRingProgressView in Frameworks */ = {isa = PBXBuildFile; productRef = C1D6EE9F2A06C7270047DE5C /* MKRingProgressView */; };
474475
C1DE5D23251BFC4D00439E49 /* SimpleBolusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1DE5D22251BFC4D00439E49 /* SimpleBolusView.swift */; };
475476
C1E2773E224177C000354103 /* ClockKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C1E2773D224177C000354103 /* ClockKit.framework */; settings = {ATTRIBUTES = (Weak, ); }; };
@@ -1543,6 +1544,7 @@
15431544
C1D197FE232CF92D0096D646 /* capture-build-details.sh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.sh; path = "capture-build-details.sh"; sourceTree = "<group>"; };
15441545
C1D289B422F90A52003FFBD9 /* BasalDeliveryState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BasalDeliveryState.swift; sourceTree = "<group>"; };
15451546
C1D70F7A2A914F71009FE129 /* he */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = he; path = he.lproj/InfoPlist.strings; sourceTree = "<group>"; };
1547+
C1D476B32A8ED179002C1C87 /* LoopAlgorithmTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoopAlgorithmTests.swift; sourceTree = "<group>"; };
15461548
C1DA986B2843B6F9001D04CC /* PersistedProperty.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistedProperty.swift; sourceTree = "<group>"; };
15471549
C1DE5D22251BFC4D00439E49 /* SimpleBolusView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SimpleBolusView.swift; sourceTree = "<group>"; };
15481550
C1E2773D224177C000354103 /* ClockKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ClockKit.framework; path = Platforms/WatchOS.platform/Developer/SDKs/WatchOS.sdk/System/Library/Frameworks/ClockKit.framework; sourceTree = DEVELOPER_DIR; };
@@ -1864,6 +1866,7 @@
18641866
E9B3552C293592B40076AB04 /* MealDetectionManagerTests.swift */,
18651867
1D70C40026EC0F9D00C62570 /* SupportManagerTests.swift */,
18661868
A9F5F1F4251050EC00E7C8A4 /* ZipArchiveTests.swift */,
1869+
C1D476B32A8ED179002C1C87 /* LoopAlgorithmTests.swift */,
18671870
);
18681871
path = Managers;
18691872
sourceTree = "<group>";
@@ -3991,6 +3994,7 @@
39913994
E93E86A824DDCC4400FF40C8 /* MockDoseStore.swift in Sources */,
39923995
B4D4534128E5CA7900F1A8D9 /* AlertMuterTests.swift in Sources */,
39933996
E98A55F124EDD85E0008715D /* MockDosingDecisionStore.swift in Sources */,
3997+
C1D476B42A8ED179002C1C87 /* LoopAlgorithmTests.swift in Sources */,
39943998
8968B114240C55F10074BB48 /* LoopSettingsTests.swift in Sources */,
39953999
A9BD28E7272226B40071DF15 /* TestLocalizedError.swift in Sources */,
39964000
A9F5F1F5251050EC00E7C8A4 /* ZipArchiveTests.swift in Sources */,
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
//
2+
// LoopAlgorithmTests.swift
3+
// LoopTests
4+
//
5+
// Created by Pete Schwamb on 8/17/23.
6+
// Copyright © 2023 LoopKit Authors. All rights reserved.
7+
//
8+
9+
import XCTest
10+
import LoopKit
11+
import LoopCore
12+
import HealthKit
13+
14+
final class LoopAlgorithmTests: XCTestCase {
15+
16+
override func setUpWithError() throws {
17+
// Put setup code here. This method is called before the invocation of each test method in the class.
18+
}
19+
20+
override func tearDownWithError() throws {
21+
// Put teardown code here. This method is called after the invocation of each test method in the class.
22+
}
23+
24+
public var bundle: Bundle {
25+
return Bundle(for: type(of: self))
26+
}
27+
28+
public func loadFixture<T>(_ resourceName: String) -> T {
29+
let path = bundle.path(forResource: resourceName, ofType: "json")!
30+
return try! JSONSerialization.jsonObject(with: Data(contentsOf: URL(fileURLWithPath: path)), options: []) as! T
31+
}
32+
33+
func loadBasalRateScheduleFixture(_ resourceName: String) -> BasalRateSchedule {
34+
let fixture: [JSONDictionary] = loadFixture(resourceName)
35+
36+
let items = fixture.map {
37+
return RepeatingScheduleValue(startTime: TimeInterval(minutes: $0["minutes"] as! Double), value: $0["rate"] as! Double)
38+
}
39+
40+
return BasalRateSchedule(dailyItems: items, timeZone: .utcTimeZone)!
41+
}
42+
43+
func loadPredictedGlucoseFixture(_ name: String) -> [PredictedGlucoseValue] {
44+
let decoder = JSONDecoder()
45+
decoder.dateDecodingStrategy = .iso8601
46+
47+
let url = bundle.url(forResource: name, withExtension: "json")!
48+
return try! decoder.decode([PredictedGlucoseValue].self, from: try! Data(contentsOf: url))
49+
}
50+
51+
52+
func testLiveCaptureWithFunctionalAlgorithm() throws {
53+
// This matches the "testForecastFromLiveCaptureInputData" test of LoopDataManagerDosingTests,
54+
// Using the same input data, but generating the forecast using LoopPrediction
55+
56+
let mockGlucoseStore = MockGlucoseStore(for: .liveCapture)
57+
let historicGlucose = mockGlucoseStore.storedGlucose!
58+
59+
let mockDoseStore = MockDoseStore(for: .liveCapture)
60+
let doses = mockDoseStore.doseHistory!
61+
62+
let mockCarbStore = MockCarbStore(for: .liveCapture)
63+
let carbEntries = mockCarbStore.carbHistory!
64+
65+
let baseTime = historicGlucose.last!.startDate
66+
let treatmentInterval = LoopAlgorithm.treatmentHistoryDateInterval(for: baseTime)
67+
68+
69+
let isfStart = min(treatmentInterval.start, doses.map { $0.startDate }.min() ?? .distantFuture)
70+
let isfEnd = baseTime.addingTimeInterval(InsulinMath.defaultInsulinActivityDuration).dateCeiledToTimeInterval(GlucoseMath.defaultDelta)
71+
72+
let basalRateSchedule = loadBasalRateScheduleFixture("basal_profile")
73+
74+
let insulinSensitivitySchedule = InsulinSensitivitySchedule(
75+
unit: .milligramsPerDeciliter,
76+
dailyItems: [
77+
RepeatingScheduleValue(startTime: 0, value: 45),
78+
RepeatingScheduleValue(startTime: 32400, value: 55)
79+
],
80+
timeZone: .utcTimeZone
81+
)!
82+
let carbRatioSchedule = CarbRatioSchedule(
83+
unit: .gram(),
84+
dailyItems: [
85+
RepeatingScheduleValue(startTime: 0.0, value: 10.0),
86+
],
87+
timeZone: .utcTimeZone
88+
)!
89+
90+
let glucoseTargetRangeSchedule = GlucoseRangeSchedule(
91+
unit: HKUnit.milligramsPerDeciliter,
92+
dailyItems: [
93+
RepeatingScheduleValue(startTime: TimeInterval(0), value: DoubleRange(minValue: 100, maxValue: 110)),
94+
RepeatingScheduleValue(startTime: TimeInterval(28800), value: DoubleRange(minValue: 90, maxValue: 100)),
95+
RepeatingScheduleValue(startTime: TimeInterval(75600), value: DoubleRange(minValue: 100, maxValue: 110))
96+
],
97+
timeZone: .utcTimeZone)!
98+
99+
let settings = LoopAlgorithmSettings(
100+
basal: basalRateSchedule.between(start: treatmentInterval.start, end: treatmentInterval.end),
101+
sensitivity: insulinSensitivitySchedule.quantitiesBetween(start: isfStart, end: isfEnd),
102+
carbRatio: carbRatioSchedule.between(start: treatmentInterval.start, end: treatmentInterval.end),
103+
target: glucoseTargetRangeSchedule.quantityBetween(start: baseTime, end: isfEnd),
104+
maximumBasalRatePerHour: 5.0,
105+
maximumBolus: 10,
106+
suspendThreshold: GlucoseThreshold(unit: HKUnit.milligramsPerDeciliter, value: 75))
107+
108+
let input = LoopPredictionInput(
109+
glucoseHistory: historicGlucose,
110+
doses: doses,
111+
carbEntries: carbEntries,
112+
settings: settings)
113+
114+
let prediction = try LoopAlgorithm.generatePrediction(input: input)
115+
116+
let expectedPredictedGlucose = loadPredictedGlucoseFixture("live_capture_predicted_glucose")
117+
118+
XCTAssertEqual(expectedPredictedGlucose.count, prediction.glucose.count)
119+
120+
let defaultAccuracy = 1.0 / 40.0
121+
122+
for (expected, calculated) in zip(expectedPredictedGlucose, prediction.glucose) {
123+
XCTAssertEqual(expected.startDate, calculated.startDate)
124+
XCTAssertEqual(expected.quantity.doubleValue(for: .milligramsPerDeciliter), calculated.quantity.doubleValue(for: .milligramsPerDeciliter), accuracy: defaultAccuracy)
125+
}
126+
127+
//XCTAssertEqual(1.99, recommendedBasal!.unitsPerHour, accuracy: defaultAccuracy)
128+
}
129+
130+
func testPerformanceExample() throws {
131+
// This is an example of a performance test case.
132+
self.measure {
133+
// Put the code you want to measure the time of here.
134+
}
135+
}
136+
137+
}

LoopTests/Mock Stores/MockCarbStore.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,5 +174,4 @@ extension MockCarbStore {
174174
return nil
175175
}
176176
}
177-
178177
}

0 commit comments

Comments
 (0)