BolusCalculatorTests.swift 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615
  1. import Foundation
  2. import Testing
  3. @testable import Trio
  4. @Suite("Bolus Calculator Tests") struct BolusCalculatorTests: Injectable {
  5. @Injected() var calculator: BolusCalculationManager!
  6. @Injected() var settingsManager: SettingsManager!
  7. @Injected() var fileStorage: FileStorage!
  8. @Injected() var apsManager: APSManager!
  9. let resolver = TrioApp().resolver
  10. init() {
  11. injectServices(resolver)
  12. }
  13. @Test("Calculator is correctly initialized") func testCalculatorInitialization() {
  14. #expect(calculator != nil, "BolusCalculationManager should be injected")
  15. #expect(calculator is BaseBolusCalculationManager, "Calculator should be of type BaseBolusCalculationManager")
  16. }
  17. @Test("Calculate insulin for standard meal") func testStandardMealCalculation() async throws {
  18. // STEP 1: Setup test scenario
  19. // We need to provide a CalculationInput struct
  20. let carbs: Decimal = 80
  21. let currentBG: Decimal = 180 // 80 points above target, should result in 2U correction
  22. let deltaBG: Decimal = 5 // Rising trend, should add small correction
  23. let target: Decimal = 100
  24. let isf: Decimal = 40
  25. let carbRatio: Decimal = 10 // Should result in 8U for carbs
  26. let iob: Decimal = 1.0 // Should subtract from final result
  27. let cob: Int16 = 20
  28. let useFattyMealCorrectionFactor: Bool = false
  29. let useSuperBolus: Bool = false
  30. let fattyMealFactor: Decimal = 0.8
  31. let sweetMealFactor: Decimal = 2
  32. let basal: Decimal = 1.5
  33. let fraction: Decimal = 0.8
  34. let maxBolus: Decimal = 10
  35. let maxIOB: Decimal = 15.0
  36. let maxCOB: Decimal = 120.0
  37. let minPredBG: Decimal = 80.0
  38. // STEP 2: Create calculation input
  39. let input = CalculationInput(
  40. carbs: carbs,
  41. currentBG: currentBG,
  42. deltaBG: deltaBG,
  43. target: target,
  44. isf: isf,
  45. carbRatio: carbRatio,
  46. iob: iob,
  47. cob: cob,
  48. useFattyMealCorrectionFactor: useFattyMealCorrectionFactor,
  49. fattyMealFactor: fattyMealFactor,
  50. useSuperBolus: useSuperBolus,
  51. sweetMealFactor: sweetMealFactor,
  52. basal: basal,
  53. fraction: fraction,
  54. maxBolus: maxBolus,
  55. maxIOB: maxIOB,
  56. maxCOB: maxCOB,
  57. minPredBG: minPredBG
  58. )
  59. // STEP 3: Calculate insulin
  60. let result = await calculator.calculateInsulin(input: input)
  61. // STEP 4: Verify results
  62. // Expected calculation breakdown:
  63. // wholeCob = 80g + 20g COB = 100g
  64. // wholeCobInsulin = 100g ÷ 10 g/U = 10U
  65. // targetDifference = currentBG - target = 180 - 100 = 80 mg/dL
  66. // targetDifferenceInsulin = 80 mg/dL ÷ 40 mg/dL/U = 2U
  67. // fifteenMinutesInsulin = 5 mg/dL ÷ 40 mg/dL/U = 0.125U
  68. // correctionInsulin = targetDifferenceInsulin = 2U
  69. // iobInsulinReduction = 1U
  70. // superBolusInsulin = 0U (disabled)
  71. // no adjustment for fatty meals (disabled)
  72. // wholeCalc = round(wholeCobInsulin + correctionInsulin + fifteenMinutesInsulin - iobInsulinReduction, 3) = 11.125U
  73. // insulinCalculated = round(wholeCalc × fraction, 3) = 8.9U
  74. // Calculate expected values with proper rounding using roundBolus method from the apsManager
  75. let wholeCobInsulin = apsManager.roundBolus(amount: Decimal(100) / Decimal(10)) // 10U
  76. let targetDifferenceInsulin = apsManager.roundBolus(amount: Decimal(80) / Decimal(40)) // 2U
  77. let fifteenMinutesInsulin = apsManager.roundBolus(amount: Decimal(5) / Decimal(40)) // 0.125U
  78. let wholeCalc = wholeCobInsulin + targetDifferenceInsulin + fifteenMinutesInsulin - Decimal(1) // 11.125U
  79. let expectedInsulinCalculated = apsManager.roundBolus(amount: wholeCalc * fraction) // 8.9U
  80. #expect(
  81. result.insulinCalculated == expectedInsulinCalculated,
  82. """
  83. Incorrect insulin calculation
  84. Expected: \(expectedInsulinCalculated)U
  85. Actual: \(result.insulinCalculated)U
  86. Components from CalculationResult:
  87. - insulinCalculated: \(result.insulinCalculated)U (expected: \(expectedInsulinCalculated)U)
  88. - wholeCalc: \(result.wholeCalc)U (expected: \(wholeCalc)U)
  89. - correctionInsulin: \(result.correctionInsulin)U (expected: \(targetDifferenceInsulin)U)
  90. - iobInsulinReduction: \(result.iobInsulinReduction)U (expected: 1U)
  91. - superBolusInsulin: \(result.superBolusInsulin)U (expected: 0U)
  92. - targetDifference: \(result.targetDifference) mg/dL (expected: 80 mg/dL)
  93. - targetDifferenceInsulin: \(result.targetDifferenceInsulin)U (expected: \(targetDifferenceInsulin)U)
  94. - fifteenMinutesInsulin: \(result.fifteenMinutesInsulin)U (expected: \(fifteenMinutesInsulin)U)
  95. - wholeCob: \(result.wholeCob)g (expected: 100g)
  96. - wholeCobInsulin: \(result.wholeCobInsulin)U (expected: \(wholeCobInsulin)U)
  97. """
  98. )
  99. // Verify each component from CalculationResult struct with rounded values
  100. #expect(
  101. result.insulinCalculated == expectedInsulinCalculated,
  102. "Final calculated insulin amount should be \(expectedInsulinCalculated)U"
  103. )
  104. #expect(result.wholeCalc == wholeCalc, "Total calculation before fraction should be \(wholeCalc)U")
  105. #expect(
  106. result.correctionInsulin == targetDifferenceInsulin,
  107. "Insulin for BG correction should be \(targetDifferenceInsulin)U"
  108. )
  109. #expect(result.iobInsulinReduction == -1.0, "Absolute IOB reduction amount should be 1U, hence -1U")
  110. #expect(result.superBolusInsulin == 0, "Additional insulin for super bolus should be 0U")
  111. #expect(result.targetDifference == 80, "Difference from target BG should be 80 mg/dL")
  112. #expect(
  113. result.targetDifferenceInsulin == targetDifferenceInsulin,
  114. "Insulin needed for target difference should be \(targetDifferenceInsulin)U"
  115. )
  116. #expect(
  117. result.fifteenMinutesInsulin == fifteenMinutesInsulin,
  118. "Trend-based insulin adjustment should be \(fifteenMinutesInsulin)U"
  119. )
  120. #expect(result.wholeCob == 100, "Total carbs (COB + new carbs) should be 100g")
  121. #expect(result.wholeCobInsulin == wholeCobInsulin, "Insulin for total carbs should be \(wholeCobInsulin)U")
  122. }
  123. @Test("Calculate insulin for fatty meal") func testFattyMealCalculation() async throws {
  124. // STEP 1: Setup test scenario
  125. // We need to provide a CalculationInput struct
  126. let carbs: Decimal = 80
  127. let currentBG: Decimal = 180 // 80 points above target, should result in 2U correction
  128. let deltaBG: Decimal = 5 // Rising trend, should add small correction
  129. let target: Decimal = 100
  130. let isf: Decimal = 40
  131. let carbRatio: Decimal = 10 // Should result in 8U for carbs
  132. let iob: Decimal = 1.0 // Should subtract from final result
  133. let cob: Int16 = 20
  134. let useFattyMealCorrectionFactor: Bool = true // now set to true
  135. let useSuperBolus: Bool = false
  136. let fattyMealFactor: Decimal = 0.8
  137. let sweetMealFactor: Decimal = 2
  138. let basal: Decimal = 1.5
  139. let fraction: Decimal = 0.8
  140. let maxBolus: Decimal = 10
  141. let maxIOB: Decimal = 15.0
  142. let maxCOB: Decimal = 120.0
  143. let minPredBG: Decimal = 80.0
  144. // STEP 2: Create calculation input
  145. let input = CalculationInput(
  146. carbs: carbs,
  147. currentBG: currentBG,
  148. deltaBG: deltaBG,
  149. target: target,
  150. isf: isf,
  151. carbRatio: carbRatio,
  152. iob: iob,
  153. cob: cob,
  154. useFattyMealCorrectionFactor: useFattyMealCorrectionFactor,
  155. fattyMealFactor: fattyMealFactor,
  156. useSuperBolus: useSuperBolus,
  157. sweetMealFactor: sweetMealFactor,
  158. basal: basal,
  159. fraction: fraction,
  160. maxBolus: maxBolus,
  161. maxIOB: maxIOB,
  162. maxCOB: maxCOB,
  163. minPredBG: minPredBG
  164. )
  165. // STEP 3: Calculate insulin with fatty meal enabled
  166. let fattyMealResult = await calculator.calculateInsulin(input: input)
  167. // STEP 4: Calculate insulin with fatty meal disabled for comparison
  168. let standardInput = CalculationInput(
  169. carbs: carbs,
  170. currentBG: currentBG,
  171. deltaBG: deltaBG,
  172. target: target,
  173. isf: isf,
  174. carbRatio: carbRatio,
  175. iob: iob,
  176. cob: cob,
  177. useFattyMealCorrectionFactor: false, // Disabled for comparison
  178. fattyMealFactor: fattyMealFactor,
  179. useSuperBolus: useSuperBolus,
  180. sweetMealFactor: sweetMealFactor,
  181. basal: basal,
  182. fraction: fraction,
  183. maxBolus: maxBolus,
  184. maxIOB: maxIOB,
  185. maxCOB: maxCOB,
  186. minPredBG: minPredBG
  187. )
  188. let standardResult = await calculator.calculateInsulin(input: standardInput)
  189. // STEP 5: Verify results
  190. // Fatty meal should reduce the insulin amount by the fatty meal factor (0.8)
  191. let expectedReduction = fattyMealFactor
  192. let actualReduction = Decimal(
  193. (Double(fattyMealResult.insulinCalculated) / Double(standardResult.insulinCalculated) * 10.0).rounded() / 10.0
  194. )
  195. #expect(
  196. actualReduction == expectedReduction,
  197. """
  198. Fatty meal calculation incorrect
  199. Expected reduction factor: \(expectedReduction)
  200. Actual reduction factor: \(actualReduction)
  201. Standard calculation: \(standardResult.insulinCalculated)U
  202. Fatty meal calculation: \(fattyMealResult.insulinCalculated)U
  203. """
  204. )
  205. }
  206. @Test("Calculate insulin with super bolus") func testSuperBolusCalculation() async throws {
  207. // STEP 1: Setup test scenario
  208. // We need to provide a CalculationInput struct
  209. let carbs: Decimal = 80
  210. let currentBG: Decimal = 180 // 80 points above target, should result in 2U correction
  211. let deltaBG: Decimal = 5 // Rising trend, should add small correction
  212. let target: Decimal = 100
  213. let isf: Decimal = 40
  214. let carbRatio: Decimal = 10 // Should result in 8U for carbs
  215. let iob: Decimal = 1.0 // Should subtract from final result
  216. let cob: Int16 = 20
  217. let useFattyMealCorrectionFactor: Bool = false
  218. let useSuperBolus: Bool = true // Super bolus enabled
  219. let fattyMealFactor: Decimal = 0.8
  220. let sweetMealFactor: Decimal = 2
  221. let basal: Decimal = 1.5 // Will be added to insulin calculation when super bolus is enabled
  222. let fraction: Decimal = 0.8
  223. let maxBolus: Decimal = 10
  224. let maxIOB: Decimal = 15.0
  225. let maxCOB: Decimal = 120.0
  226. let minPredBG: Decimal = 80.0
  227. // STEP 2: Create calculation input with super bolus enabled
  228. let input = CalculationInput(
  229. carbs: carbs,
  230. currentBG: currentBG,
  231. deltaBG: deltaBG,
  232. target: target,
  233. isf: isf,
  234. carbRatio: carbRatio,
  235. iob: iob,
  236. cob: cob,
  237. useFattyMealCorrectionFactor: useFattyMealCorrectionFactor,
  238. fattyMealFactor: fattyMealFactor,
  239. useSuperBolus: useSuperBolus,
  240. sweetMealFactor: sweetMealFactor,
  241. basal: basal,
  242. fraction: fraction,
  243. maxBolus: maxBolus,
  244. maxIOB: maxIOB,
  245. maxCOB: maxCOB,
  246. minPredBG: minPredBG
  247. )
  248. // STEP 3: Calculate insulin with super bolus enabled
  249. let superBolusResult = await calculator.calculateInsulin(input: input)
  250. // STEP 4: Calculate insulin with super bolus disabled for comparison
  251. let standardInput = CalculationInput(
  252. carbs: carbs,
  253. currentBG: currentBG,
  254. deltaBG: deltaBG,
  255. target: target,
  256. isf: isf,
  257. carbRatio: carbRatio,
  258. iob: iob,
  259. cob: cob,
  260. useFattyMealCorrectionFactor: useFattyMealCorrectionFactor,
  261. fattyMealFactor: fattyMealFactor,
  262. useSuperBolus: false, // Disabled for comparison
  263. sweetMealFactor: sweetMealFactor,
  264. basal: basal,
  265. fraction: fraction,
  266. maxBolus: maxBolus,
  267. maxIOB: maxIOB,
  268. maxCOB: maxCOB,
  269. minPredBG: minPredBG
  270. )
  271. let standardResult = await calculator.calculateInsulin(input: standardInput)
  272. // STEP 5: Verify results
  273. // Super bolus should add basal rate * sweetMealFactor to the insulin calculation
  274. let expectedSuperBolusInsulin = basal * sweetMealFactor
  275. #expect(
  276. superBolusResult.superBolusInsulin == expectedSuperBolusInsulin,
  277. """
  278. Super bolus insulin incorrect
  279. Expected: \(expectedSuperBolusInsulin)U (basal \(basal)U × sweetMealFactor \(sweetMealFactor))
  280. Actual: \(superBolusResult.superBolusInsulin)U
  281. """
  282. )
  283. #expect(
  284. superBolusResult.insulinCalculated > standardResult.insulinCalculated,
  285. """
  286. Super bolus calculation incorrect
  287. Expected super bolus calculation to be higher than standard
  288. Super bolus: \(superBolusResult.insulinCalculated)U
  289. Standard: \(standardResult.insulinCalculated)U
  290. Difference: \(superBolusResult.insulinCalculated - standardResult.insulinCalculated)U
  291. """
  292. )
  293. // The difference should be the difference of super bolus (= standard dose + the basal rate * sweetMealFactor) limited by max bolus, and the standard dose.
  294. let actualDifference = (superBolusResult.insulinCalculated - standardResult.insulinCalculated)
  295. let expectedDifference = min(superBolusResult.insulinCalculated, maxBolus) - standardResult.insulinCalculated
  296. #expect(
  297. actualDifference == expectedDifference,
  298. """
  299. Super bolus difference incorrect
  300. Expected difference: min(\(expectedSuperBolusInsulin), \(maxBolus)) U (basal \(basal)U × sweetMealFactor \(sweetMealFactor) + standard dose \(standardResult
  301. .insulinCalculated)) - standard dose \(standardResult.insulinCalculated)
  302. Actual difference: \(actualDifference)U
  303. Standard result: \(standardResult)
  304. SuperBolus result: \(superBolusResult)
  305. """
  306. )
  307. }
  308. @Test("Calculate insulin with zero carbs") func testZeroCarbsCalculation() async throws {
  309. // Given
  310. let carbs: Decimal = 0
  311. // When
  312. let result = await calculator.handleBolusCalculation(
  313. carbs: carbs,
  314. useFattyMealCorrection: false,
  315. useSuperBolus: false
  316. )
  317. // Then
  318. #expect(result.wholeCobInsulin == 0, "Zero carbs should require no insulin for carbs")
  319. }
  320. @Test("Verify settings retrieval") func testGetSettings() async throws {
  321. // Given - Save original settings to restore later
  322. let originalSettings = settingsManager.settings
  323. // Setup test settings
  324. let expectedUnits = GlucoseUnits.mgdL
  325. let expectedFraction: Decimal = 0.7
  326. let expectedFattyMealFactor: Decimal = 0.8
  327. let expectedSweetMealFactor: Decimal = 2
  328. let expectedMaxCarbs: Decimal = 150
  329. // Update settings through settings manager
  330. settingsManager.settings.units = expectedUnits
  331. settingsManager.settings.overrideFactor = expectedFraction
  332. settingsManager.settings.fattyMealFactor = expectedFattyMealFactor
  333. settingsManager.settings.sweetMealFactor = expectedSweetMealFactor
  334. settingsManager.settings.maxCarbs = expectedMaxCarbs
  335. // Save settings to storage
  336. fileStorage.save(settingsManager.settings, as: OpenAPS.Settings.settings)
  337. // When
  338. let (units, fraction, fattyMealFactor, sweetMealFactor, maxCarbs) = await getSettings()
  339. // Then
  340. #expect(units == expectedUnits, "Units should match settings")
  341. #expect(fraction == expectedFraction, "Override factor should match settings")
  342. #expect(fattyMealFactor == expectedFattyMealFactor, "Fatty meal factor should match settings")
  343. #expect(sweetMealFactor == expectedSweetMealFactor, "Sweet meal factor should match settings")
  344. #expect(maxCarbs == expectedMaxCarbs, "Max carbs should match settings")
  345. // Cleanup - Restore original settings
  346. settingsManager.settings = originalSettings
  347. fileStorage.save(originalSettings, as: OpenAPS.Settings.settings)
  348. }
  349. @Test("Verify getCurrentSettingValue returns correct values based on time") func testGetCurrentSettingValue() async throws {
  350. // STEP 1: Backup current settings
  351. let originalBasalProfile = await fileStorage.retrieveAsync(OpenAPS.Settings.basalProfile, as: [BasalProfileEntry].self)
  352. let originalCarbRatios = await fileStorage.retrieveAsync(OpenAPS.Settings.carbRatios, as: CarbRatios.self)
  353. let originalBGTargets = await fileStorage.retrieveAsync(OpenAPS.Settings.bgTargets, as: BGTargets.self)
  354. let originalISFValues = await fileStorage.retrieveAsync(
  355. OpenAPS.Settings.insulinSensitivities,
  356. as: InsulinSensitivities.self
  357. )
  358. // STEP 2: Setup test data with known values
  359. // Note: Entries must be sorted by time for the algorithm to work correctly
  360. let basalProfile = [
  361. BasalProfileEntry(start: "00:00", minutes: 0, rate: 1.0), // 12:00 AM - 6:00 AM: 1.0
  362. BasalProfileEntry(start: "06:00", minutes: 360, rate: 1.2), // 6:00 AM - 12:00 PM: 1.2
  363. BasalProfileEntry(start: "12:00", minutes: 720, rate: 1.1), // 12:00 PM - 6:00 PM: 1.1
  364. BasalProfileEntry(start: "18:00", minutes: 1080, rate: 0.9) // 6:00 PM - 12:00 AM: 0.9
  365. ]
  366. let carbRatios = CarbRatios(
  367. units: .grams,
  368. schedule: [
  369. CarbRatioEntry(start: "00:00", offset: 0, ratio: 10), // 12:00 AM - 12:00 PM: 10
  370. CarbRatioEntry(start: "12:00", offset: 720, ratio: 12) // 12:00 PM - 12:00 AM: 12
  371. ]
  372. )
  373. let bgTargets = BGTargets(
  374. units: .mgdL,
  375. userPreferredUnits: .mgdL,
  376. targets: [
  377. BGTargetEntry(low: 100, high: 120, start: "00:00", offset: 0), // 12:00 AM - 8:00 AM: 100
  378. BGTargetEntry(low: 90, high: 110, start: "08:00", offset: 480) // 8:00 AM - 12:00 AM: 90
  379. ]
  380. )
  381. let isfValues = InsulinSensitivities(
  382. units: .mgdL,
  383. userPreferredUnits: .mgdL,
  384. sensitivities: [
  385. InsulinSensitivityEntry(sensitivity: 40, offset: 0, start: "00:00"), // 12:00 AM - 2:00 PM: 40
  386. InsulinSensitivityEntry(sensitivity: 45, offset: 840, start: "14:00") // 2:00 PM - 12:00 AM: 45
  387. ]
  388. )
  389. // STEP 3: Store test data
  390. fileStorage.save(basalProfile, as: OpenAPS.Settings.basalProfile)
  391. fileStorage.save(carbRatios, as: OpenAPS.Settings.carbRatios)
  392. fileStorage.save(bgTargets, as: OpenAPS.Settings.bgTargets)
  393. fileStorage.save(isfValues, as: OpenAPS.Settings.insulinSensitivities)
  394. // STEP 4: Define test cases with specific times and expected values
  395. // Format: (hour, minute, [setting type: expected value])
  396. let testTimes: [(hour: Int, minute: Int, expected: [SettingType: Decimal])] = [
  397. // Test midnight values (00:00)
  398. (
  399. hour: 0, minute: 0,
  400. expected: [
  401. .basal: 1.0, // First basal rate
  402. .carbRatio: 10, // First carb ratio
  403. .bgTarget: 100, // First target
  404. .isf: 40 // First ISF
  405. ]
  406. ),
  407. // Test mid-morning values (7:00)
  408. (
  409. hour: 7, minute: 0,
  410. expected: [
  411. .basal: 1.2, // Second basal rate (after 6:00)
  412. .carbRatio: 10, // Still first carb ratio
  413. .bgTarget: 100, // Still first target
  414. .isf: 40 // Still first ISF
  415. ]
  416. ),
  417. // Test afternoon values (15:00)
  418. (
  419. hour: 15, minute: 0,
  420. expected: [
  421. .basal: 1.1, // Third basal rate (after 12:00)
  422. .carbRatio: 12, // Second carb ratio (after 12:00)
  423. .bgTarget: 90, // Second target
  424. .isf: 45 // Second ISF (after 14:00)
  425. ]
  426. )
  427. ]
  428. // STEP 5: Test each time point
  429. for testTime in testTimes {
  430. // Create a date object for the test time
  431. let calendar = Calendar.current
  432. var components = calendar.dateComponents([.year, .month, .day], from: Date())
  433. components.hour = testTime.hour
  434. components.minute = testTime.minute
  435. components.second = 0
  436. guard let testDate = calendar.date(from: components) else {
  437. throw TestError("Failed to create test date")
  438. }
  439. // Test each setting type at this time
  440. for (type, expectedValue) in testTime.expected {
  441. // Get the actual value for this setting at the test time
  442. let value = await getCurrentSettingValue(for: type, at: testDate)
  443. // Compare with expected value
  444. #expect(
  445. value == expectedValue,
  446. """
  447. Failed at \(testTime.hour):\(String(format: "%02d", testTime.minute))
  448. Setting: \(type)
  449. Expected: \(expectedValue)
  450. Actual: \(value)
  451. """
  452. )
  453. }
  454. }
  455. // STEP 6: Cleanup - Restore original settings
  456. if let originalBasalProfile = originalBasalProfile {
  457. fileStorage.save(originalBasalProfile, as: OpenAPS.Settings.basalProfile)
  458. }
  459. if let originalCarbRatios = originalCarbRatios {
  460. fileStorage.save(originalCarbRatios, as: OpenAPS.Settings.carbRatios)
  461. }
  462. if let originalBGTargets = originalBGTargets {
  463. fileStorage.save(originalBGTargets, as: OpenAPS.Settings.bgTargets)
  464. }
  465. if let originalISFValues = originalISFValues {
  466. fileStorage.save(originalISFValues, as: OpenAPS.Settings.insulinSensitivities)
  467. }
  468. }
  469. }
  470. // Copied over from BolusCalculationManager as they are not included in the protocol definition (and I don´t want them to be included)
  471. extension BolusCalculatorTests {
  472. private enum SettingType {
  473. case basal
  474. case carbRatio
  475. case bgTarget
  476. case isf
  477. }
  478. /// Retrieves current settings from the SettingsManager
  479. /// - Returns: Tuple containing units, fraction, fattyMealFactor, sweetMealFactor, and maxCarbs settings
  480. private func getSettings() async -> (
  481. units: GlucoseUnits,
  482. fraction: Decimal,
  483. fattyMealFactor: Decimal,
  484. sweetMealFactor: Decimal,
  485. maxCarbs: Decimal
  486. ) {
  487. return (
  488. units: settingsManager.settings.units,
  489. fraction: settingsManager.settings.overrideFactor,
  490. fattyMealFactor: settingsManager.settings.fattyMealFactor,
  491. sweetMealFactor: settingsManager.settings.sweetMealFactor,
  492. maxCarbs: settingsManager.settings.maxCarbs
  493. )
  494. }
  495. /// Gets the current setting value for a specific setting type based on the time of day
  496. /// - Parameter type: The type of setting to retrieve (basal, carbRatio, bgTarget, or isf)
  497. /// - Returns: The current decimal value for the specified setting type
  498. private func getCurrentSettingValue(for type: SettingType, at date: Date) async -> Decimal {
  499. let calendar = Calendar.current
  500. let midnight = calendar.startOfDay(for: date)
  501. let minutesSinceMidnight = calendar.dateComponents([.minute], from: midnight, to: date).minute ?? 0
  502. switch type {
  503. case .basal:
  504. let profile = await getBasalProfile()
  505. return profile.last { $0.minutes <= minutesSinceMidnight }?.rate ?? 0
  506. case .carbRatio:
  507. let ratios = await getCarbRatios()
  508. return ratios.schedule.last { $0.offset <= minutesSinceMidnight }?.ratio ?? 0
  509. case .bgTarget:
  510. let targets = await getBGTargets()
  511. return targets.targets.last { $0.offset <= minutesSinceMidnight }?.low ?? 0
  512. case .isf:
  513. let sensitivities = await getISFValues()
  514. return sensitivities.sensitivities.last { $0.offset <= minutesSinceMidnight }?.sensitivity ?? 0
  515. }
  516. }
  517. /// Retrieves the pump settings from storage
  518. /// - Returns: PumpSettings object containing pump configuration
  519. private func getPumpSettings() async -> PumpSettings {
  520. await fileStorage.retrieveAsync(OpenAPS.Settings.settings, as: PumpSettings.self)
  521. ?? PumpSettings(from: OpenAPS.defaults(for: OpenAPS.Settings.settings))
  522. ?? PumpSettings(insulinActionCurve: 10, maxBolus: 10, maxBasal: 2)
  523. }
  524. /// Retrieves the basal profile from storage
  525. /// - Returns: Array of BasalProfileEntry objects
  526. private func getBasalProfile() async -> [BasalProfileEntry] {
  527. await fileStorage.retrieveAsync(OpenAPS.Settings.basalProfile, as: [BasalProfileEntry].self)
  528. ?? [BasalProfileEntry](from: OpenAPS.defaults(for: OpenAPS.Settings.basalProfile))
  529. ?? []
  530. }
  531. /// Retrieves carb ratios from storage
  532. /// - Returns: CarbRatios object containing carb ratio schedule
  533. private func getCarbRatios() async -> CarbRatios {
  534. await fileStorage.retrieveAsync(OpenAPS.Settings.carbRatios, as: CarbRatios.self)
  535. ?? CarbRatios(from: OpenAPS.defaults(for: OpenAPS.Settings.carbRatios))
  536. ?? CarbRatios(units: .grams, schedule: [])
  537. }
  538. /// Retrieves blood glucose targets from storage
  539. /// - Returns: BGTargets object containing target schedule
  540. private func getBGTargets() async -> BGTargets {
  541. await fileStorage.retrieveAsync(OpenAPS.Settings.bgTargets, as: BGTargets.self)
  542. ?? BGTargets(from: OpenAPS.defaults(for: OpenAPS.Settings.bgTargets))
  543. ?? BGTargets(units: .mgdL, userPreferredUnits: .mgdL, targets: [])
  544. }
  545. /// Retrieves insulin sensitivity factors from storage
  546. /// - Returns: InsulinSensitivities object containing sensitivity schedule
  547. private func getISFValues() async -> InsulinSensitivities {
  548. await fileStorage.retrieveAsync(OpenAPS.Settings.insulinSensitivities, as: InsulinSensitivities.self)
  549. ?? InsulinSensitivities(from: OpenAPS.defaults(for: OpenAPS.Settings.insulinSensitivities))
  550. ?? InsulinSensitivities(
  551. units: .mgdL,
  552. userPreferredUnits: .mgdL,
  553. sensitivities: []
  554. )
  555. }
  556. }