BolusCalculatorTests.swift 33 KB

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