IobSuspendTests.swift 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380
  1. import Foundation
  2. import Testing
  3. @testable import Trio
  4. @Suite("IOB Suspend Logic Tests") struct IobSuspendTests {
  5. // Helper function to create a basic basal profile
  6. func createBasicBasalProfile() -> [BasalProfileEntry] {
  7. [
  8. BasalProfileEntry(
  9. start: "00:00:00",
  10. minutes: 0,
  11. rate: 1
  12. )
  13. ]
  14. }
  15. // Helper function to create a multi-rate basal profile
  16. func createMultiRateBasalProfile() -> [BasalProfileEntry] {
  17. [
  18. BasalProfileEntry(
  19. start: "00:00:00",
  20. minutes: 0,
  21. rate: 1
  22. ),
  23. BasalProfileEntry(
  24. start: "00:30:00",
  25. minutes: 30,
  26. rate: 2
  27. )
  28. ]
  29. }
  30. @Test("should handle basic suspend and resume") func handleBasicSuspendAndResume() async throws {
  31. let basalprofile = createBasicBasalProfile()
  32. // Create fixed test dates (matching JavaScript test)
  33. let formatter = ISO8601DateFormatter()
  34. formatter.formatOptions = [.withInternetDateTime]
  35. let now = formatter.date(from: "2016-06-13T01:00:00Z")!
  36. let timestamp30mAgo = formatter.date(from: "2016-06-13T00:30:00Z")!
  37. let timestamp15mAgo = formatter.date(from: "2016-06-13T00:45:00Z")!
  38. let pumpHistory = [
  39. ComputedPumpHistoryEvent.forTest(
  40. type: .tempBasal,
  41. timestamp: timestamp30mAgo,
  42. duration: nil,
  43. rate: 2,
  44. temp: .absolute
  45. ),
  46. ComputedPumpHistoryEvent.forTest(
  47. type: .tempBasalDuration,
  48. timestamp: timestamp30mAgo,
  49. durationMin: 30
  50. ),
  51. ComputedPumpHistoryEvent.forTest(
  52. type: .pumpSuspend,
  53. timestamp: timestamp15mAgo
  54. ),
  55. ComputedPumpHistoryEvent.forTest(
  56. type: .pumpResume,
  57. timestamp: now
  58. )
  59. ]
  60. var profile = Profile()
  61. profile.currentBasal = 1
  62. profile.maxDailyBasal = 1
  63. profile.dia = 3
  64. profile.basalprofile = basalprofile
  65. profile.suspendZerosIob = true
  66. let treatments = try IobHistory.calcTempTreatments(
  67. history: pumpHistory,
  68. profile: profile,
  69. clock: now,
  70. autosens: nil,
  71. zeroTempDuration: nil
  72. )
  73. // Calculate expected insulin impact:
  74. // 15m at 2 U/h - 1 U/h = 0.25U
  75. // 15m at 0 U/h - 1 U/h = -0.25U
  76. // Total: 0U
  77. #expect(treatments.netInsulin().isWithin(0.01, of: 0.0))
  78. }
  79. @Test("should handle suspend prior to history window") func handleSuspendPriorToHistoryWindow() async throws {
  80. let basalprofile = createBasicBasalProfile()
  81. // Create fixed test dates (matching JavaScript test)
  82. let formatter = ISO8601DateFormatter()
  83. formatter.formatOptions = [.withInternetDateTime]
  84. let now = formatter.date(from: "2016-06-13T08:00:00Z")!
  85. let resumeTime = formatter.date(from: "2016-06-13T07:00:00Z")!
  86. let tempStartTime = formatter.date(from: "2016-06-13T07:30:00Z")!
  87. let pumpHistory = [
  88. ComputedPumpHistoryEvent.forTest(
  89. type: .pumpResume,
  90. timestamp: resumeTime
  91. ),
  92. ComputedPumpHistoryEvent.forTest(
  93. type: .tempBasal,
  94. timestamp: tempStartTime,
  95. duration: nil,
  96. rate: 2,
  97. temp: .absolute
  98. ),
  99. ComputedPumpHistoryEvent.forTest(
  100. type: .tempBasalDuration,
  101. timestamp: tempStartTime,
  102. durationMin: 30
  103. )
  104. ]
  105. var profile = Profile()
  106. profile.currentBasal = 1
  107. profile.maxDailyBasal = 1
  108. profile.dia = 10 // Longer DIA to match JS test
  109. profile.basalprofile = basalprofile
  110. profile.suspendZerosIob = true
  111. let treatments = try IobHistory.calcTempTreatments(
  112. history: pumpHistory,
  113. profile: profile,
  114. clock: now,
  115. autosens: nil,
  116. zeroTempDuration: nil
  117. )
  118. // Calculate expected insulin impact:
  119. // 7h at 0 U/h - 1U/h = -7 (this may need adjustment based on implementation)
  120. // 30m at 2 U/h - 1 U/h = 0.5U
  121. // Total: approximately -6.5U
  122. // Note: This test case may need adjustments based on how you implement the suspend
  123. // prior to history window logic in your Swift port
  124. let netInsulin = treatments.netInsulin()
  125. // The exact value might vary due to implementation details, but the general direction should be consistent
  126. #expect(netInsulin < -6.0)
  127. }
  128. @Test("should handle current suspension") func handleCurrentSuspension() async throws {
  129. let basalprofile = createBasicBasalProfile()
  130. // Setting up the dates for the test
  131. let now = Calendar.current.startOfDay(for: Date()) + 60.minutesToSeconds
  132. let suspendTime = now - 30.minutesToSeconds
  133. let tempStartTime = now - 45.minutesToSeconds
  134. let pumpHistory = [
  135. ComputedPumpHistoryEvent.forTest(
  136. type: .tempBasal,
  137. timestamp: tempStartTime,
  138. duration: nil,
  139. rate: 2,
  140. temp: .absolute
  141. ),
  142. ComputedPumpHistoryEvent.forTest(
  143. type: .tempBasalDuration,
  144. timestamp: tempStartTime,
  145. durationMin: 30
  146. ),
  147. ComputedPumpHistoryEvent.forTest(
  148. type: .pumpSuspend,
  149. timestamp: suspendTime
  150. )
  151. ]
  152. var profile = Profile()
  153. profile.currentBasal = 1
  154. profile.maxDailyBasal = 1
  155. profile.dia = 3
  156. profile.basalprofile = basalprofile
  157. profile.suspendZerosIob = true
  158. let treatments = try IobHistory.calcTempTreatments(
  159. history: pumpHistory,
  160. profile: profile,
  161. clock: now,
  162. autosens: nil,
  163. zeroTempDuration: nil
  164. )
  165. // Calculate expected insulin impact:
  166. // 15m at 2 U/h - 1U/h = 0.25
  167. // 30m at 0 U/h - 1U/h = -0.5
  168. // Total: -0.25U
  169. #expect(treatments.netInsulin().isWithin(0.01, of: -0.25))
  170. }
  171. @Test("should handle multiple suspend-resume cycles") func handleMultipleSuspendResumeCycles() async throws {
  172. let basalprofile = createBasicBasalProfile()
  173. // Setting up the dates for the test
  174. let now = Calendar.current.startOfDay(for: Date()) + 90.minutesToSeconds
  175. // Create history with 2 suspend-resume cycles
  176. let suspend1 = now - 90.minutesToSeconds
  177. let resume1 = now - 75.minutesToSeconds
  178. let tempStart = now - 60.minutesToSeconds
  179. let suspend2 = now - 45.minutesToSeconds
  180. let resume2 = now - 30.minutesToSeconds
  181. let pumpHistory = [
  182. ComputedPumpHistoryEvent.forTest(
  183. type: .pumpSuspend,
  184. timestamp: suspend1
  185. ),
  186. ComputedPumpHistoryEvent.forTest(
  187. type: .pumpResume,
  188. timestamp: resume1
  189. ),
  190. ComputedPumpHistoryEvent.forTest(
  191. type: .tempBasal,
  192. timestamp: tempStart,
  193. duration: nil,
  194. rate: 2,
  195. temp: .absolute
  196. ),
  197. ComputedPumpHistoryEvent.forTest(
  198. type: .tempBasalDuration,
  199. timestamp: tempStart,
  200. durationMin: 60
  201. ),
  202. ComputedPumpHistoryEvent.forTest(
  203. type: .pumpSuspend,
  204. timestamp: suspend2
  205. ),
  206. ComputedPumpHistoryEvent.forTest(
  207. type: .pumpResume,
  208. timestamp: resume2
  209. )
  210. ]
  211. var profile = Profile()
  212. profile.currentBasal = 1
  213. profile.maxDailyBasal = 1
  214. profile.dia = 3
  215. profile.basalprofile = basalprofile
  216. profile.suspendZerosIob = true
  217. let treatments = try IobHistory.calcTempTreatments(
  218. history: pumpHistory,
  219. profile: profile,
  220. clock: now,
  221. autosens: nil,
  222. zeroTempDuration: nil
  223. )
  224. // Calculate expected insulin impact:
  225. // 15m at 0 U/h - 1 U/h = -0.25
  226. // 15m at 2 U/h - 1 U/h = 0.25
  227. // 15m at 0 U/h - 1 U/h = -0.25
  228. // 30m at 2 U/h - 1 U/h = 0.5
  229. // Total: 0.25U
  230. #expect(treatments.netInsulin().isWithin(0.01, of: 0.25))
  231. }
  232. @Test("should handle suspend with basal profile changes") func handleSuspendWithBasalProfileChanges() async throws {
  233. let basalprofile = createMultiRateBasalProfile()
  234. // Create fixed test dates (matching JavaScript test)
  235. let formatter = ISO8601DateFormatter()
  236. formatter.formatOptions = [.withInternetDateTime]
  237. let calendar = Calendar.current
  238. let currentTime = Date()
  239. let startTime = Calendar.current.startOfDay(for: currentTime) + 15.minutesToSeconds
  240. let suspendTime = Calendar.current.startOfDay(for: currentTime) + 30.minutesToSeconds
  241. let resumeTime = Calendar.current.startOfDay(for: currentTime) + 45.minutesToSeconds
  242. let endTime = Calendar.current.startOfDay(for: currentTime) + 60.minutesToSeconds
  243. let pumpHistory = [
  244. ComputedPumpHistoryEvent.forTest(
  245. type: .tempBasal,
  246. timestamp: startTime,
  247. duration: nil,
  248. rate: 3,
  249. temp: .absolute
  250. ),
  251. ComputedPumpHistoryEvent.forTest(
  252. type: .tempBasalDuration,
  253. timestamp: startTime,
  254. durationMin: 45
  255. ),
  256. ComputedPumpHistoryEvent.forTest(
  257. type: .pumpSuspend,
  258. timestamp: suspendTime
  259. ),
  260. ComputedPumpHistoryEvent.forTest(
  261. type: .pumpResume,
  262. timestamp: resumeTime
  263. )
  264. ]
  265. var profile = Profile()
  266. profile.currentBasal = 1
  267. profile.maxDailyBasal = 2
  268. profile.dia = 3
  269. profile.basalprofile = basalprofile
  270. profile.suspendZerosIob = true
  271. let treatments = try IobHistory.calcTempTreatments(
  272. history: pumpHistory,
  273. profile: profile,
  274. clock: endTime,
  275. autosens: nil,
  276. zeroTempDuration: nil
  277. )
  278. // Calculate expected insulin impact:
  279. // 15m at 3 U/h - 1 U/h = 0.5U (from start to basal change)
  280. // 15m at 0 U/h - 2 U/h = -0.5U (from basal change and suspend)
  281. // 15m at 3 U/h - 2 U/h = 0.25U (resume to finish)
  282. // Total: 0.25U
  283. #expect(treatments.netInsulin().isWithin(0.01, of: 0.25))
  284. }
  285. @Test("should properly handle IOB impact with suspends") func handleIobImpactWithSuspends() async throws {
  286. let basalprofile = createBasicBasalProfile()
  287. // Setting up the dates for the test
  288. let now = Calendar.current.startOfDay(for: Date()) + 90.minutesToSeconds
  289. let tempStart = now - 60.minutesToSeconds
  290. let suspendTime = now - 30.minutesToSeconds
  291. let resumeTime = now
  292. let pumpHistory = [
  293. ComputedPumpHistoryEvent.forTest(
  294. type: .tempBasal,
  295. timestamp: tempStart,
  296. duration: nil,
  297. rate: 2,
  298. temp: .absolute
  299. ),
  300. ComputedPumpHistoryEvent.forTest(
  301. type: .tempBasalDuration,
  302. timestamp: tempStart,
  303. durationMin: 30
  304. ),
  305. ComputedPumpHistoryEvent.forTest(
  306. type: .pumpSuspend,
  307. timestamp: suspendTime
  308. ),
  309. ComputedPumpHistoryEvent.forTest(
  310. type: .pumpResume,
  311. timestamp: resumeTime
  312. )
  313. ]
  314. var profile = Profile()
  315. profile.currentBasal = 1
  316. profile.maxDailyBasal = 1
  317. profile.dia = 3
  318. profile.basalprofile = basalprofile
  319. profile.suspendZerosIob = true
  320. let treatments = try IobHistory.calcTempTreatments(
  321. history: pumpHistory,
  322. profile: profile,
  323. clock: now,
  324. autosens: nil,
  325. zeroTempDuration: nil
  326. )
  327. // Calculate expected insulin impact:
  328. // 30m at 2 U/h - 1 U/h = 0.5U (from temp start to temp end)
  329. // 30m at 0 U/h - 1 U/h = -0.5U (from suspend to resume)
  330. // Total: 0U
  331. #expect(treatments.netInsulin().isWithin(0.01, of: 0.0))
  332. }
  333. }