SetTempBasalTests.swift 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305
  1. import Foundation
  2. import Testing
  3. @testable import Trio
  4. /// A direct port of the Javascript `set-temp-basal.test.js` tests
  5. @Suite("Set Temp Basal Tests") struct SetTempBasalTests {
  6. /// Helper to create a default profile for tests.
  7. private func createProfile(
  8. currentBasal: Decimal = 0.8,
  9. maxDailyBasal: Decimal = 1.3,
  10. maxBasal: Decimal = 3.0,
  11. skipNeutralTemps: Bool = false,
  12. maxDailySafetyMultiplier: Decimal = 3,
  13. currentBasalSafetyMultiplier: Decimal = 4,
  14. model: String? = nil
  15. ) -> Profile {
  16. var profile = Profile()
  17. profile.currentBasal = currentBasal
  18. profile.maxDailyBasal = maxDailyBasal
  19. profile.maxBasal = maxBasal
  20. profile.skipNeutralTemps = skipNeutralTemps
  21. profile.maxDailySafetyMultiplier = maxDailySafetyMultiplier
  22. profile.currentBasalSafetyMultiplier = currentBasalSafetyMultiplier
  23. profile.model = model
  24. return profile
  25. }
  26. /// Helper to create a default determination object.
  27. private func createDetermination() -> Determination {
  28. Determination(
  29. id: UUID(),
  30. reason: "",
  31. units: nil,
  32. insulinReq: nil,
  33. eventualBG: nil,
  34. sensitivityRatio: nil,
  35. rate: nil,
  36. duration: nil,
  37. iob: nil,
  38. cob: nil,
  39. predictions: nil,
  40. deliverAt: Date(),
  41. carbsReq: nil,
  42. temp: .absolute,
  43. bg: nil,
  44. reservoir: nil,
  45. isf: nil,
  46. timestamp: Date(),
  47. tdd: nil,
  48. current_target: nil,
  49. manualBolusErrorString: nil,
  50. minDelta: nil,
  51. expectedDelta: nil,
  52. minGuardBG: nil,
  53. minPredBG: nil,
  54. threshold: nil,
  55. carbRatio: nil,
  56. received: false
  57. )
  58. }
  59. /// Helper to create a TempBasal object
  60. private func createCurrentTemp(rate: Decimal = 0, duration: Decimal = 0) -> TempBasal {
  61. TempBasal(
  62. duration: Int(truncating: duration as NSNumber),
  63. rate: rate,
  64. temp: .absolute,
  65. timestamp: Date()
  66. )
  67. }
  68. @Test("should cancel temp") func cancelTemp() throws {
  69. let profile = createProfile()
  70. let determination = createDetermination()
  71. let currentTemp = createCurrentTemp()
  72. let requestedTemp = try TempBasalFunctions.setTempBasal(
  73. rate: 0,
  74. duration: 0,
  75. profile: profile,
  76. determination: determination,
  77. currentTemp: currentTemp
  78. )
  79. #expect(requestedTemp.rate == 0)
  80. #expect(requestedTemp.duration == 0)
  81. }
  82. @Test("should set zero temp") func setZeroTemp() throws {
  83. let profile = createProfile()
  84. let determination = createDetermination()
  85. let currentTemp = createCurrentTemp()
  86. let requestedTemp = try TempBasalFunctions.setTempBasal(
  87. rate: 0,
  88. duration: 30,
  89. profile: profile,
  90. determination: determination,
  91. currentTemp: currentTemp
  92. )
  93. #expect(requestedTemp.rate == 0)
  94. #expect(requestedTemp.duration == 30)
  95. }
  96. @Test("should set high temp") func setHighTemp() throws {
  97. let profile = createProfile()
  98. let determination = createDetermination()
  99. let currentTemp = createCurrentTemp()
  100. let requestedTemp = try TempBasalFunctions.setTempBasal(
  101. rate: 2,
  102. duration: 30,
  103. profile: profile,
  104. determination: determination,
  105. currentTemp: currentTemp
  106. )
  107. #expect(requestedTemp.rate == 2)
  108. #expect(requestedTemp.duration == 30)
  109. }
  110. @Test("should not set basal on skip neutral mode") func skipNeutralMode() throws {
  111. // Test case 1: Current temp is active
  112. var profile = createProfile(currentBasal: 0.8, skipNeutralTemps: true)
  113. var determination = createDetermination()
  114. var currentTemp = createCurrentTemp(duration: 10)
  115. var requestedTemp = try TempBasalFunctions.setTempBasal(
  116. rate: 0.8,
  117. duration: 30,
  118. profile: profile,
  119. determination: determination,
  120. currentTemp: currentTemp
  121. )
  122. #expect(requestedTemp.duration == 0)
  123. // Test case 2: No current temp
  124. determination = createDetermination()
  125. currentTemp = createCurrentTemp() // duration = 0
  126. requestedTemp = try TempBasalFunctions.setTempBasal(
  127. rate: 0.8,
  128. duration: 30,
  129. profile: profile,
  130. determination: determination,
  131. currentTemp: currentTemp
  132. )
  133. #expect(requestedTemp.reason.contains("no temp basal is active, doing nothing") == true)
  134. }
  135. @Test("should limit high temp to max_basal") func limitToMaxBasal() throws {
  136. let profile = createProfile(maxBasal: 3.0)
  137. let determination = createDetermination()
  138. let currentTemp = createCurrentTemp()
  139. let requestedTemp = try TempBasalFunctions.setTempBasal(
  140. rate: 4,
  141. duration: 30,
  142. profile: profile,
  143. determination: determination,
  144. currentTemp: currentTemp
  145. )
  146. #expect(requestedTemp.rate == 3.0)
  147. #expect(requestedTemp.duration == 30)
  148. }
  149. @Test("should limit high temp to 3 * max_daily_basal") func limitToMaxDailyBasal() throws {
  150. let profile = createProfile(currentBasal: 1.0, maxDailyBasal: 1.3, maxBasal: 10.0)
  151. let determination = createDetermination()
  152. let currentTemp = createCurrentTemp()
  153. let requestedTemp = try TempBasalFunctions.setTempBasal(
  154. rate: 6,
  155. duration: 30,
  156. profile: profile,
  157. determination: determination,
  158. currentTemp: currentTemp
  159. )
  160. #expect(requestedTemp.rate == 3.9)
  161. #expect(requestedTemp.duration == 30)
  162. }
  163. @Test("should limit high temp to 4 * current_basal") func limitToCurrentBasal() throws {
  164. let profile = createProfile(currentBasal: 0.7, maxDailyBasal: 1.3, maxBasal: 10.0)
  165. let determination = createDetermination()
  166. let currentTemp = createCurrentTemp()
  167. let requestedTemp = try TempBasalFunctions.setTempBasal(
  168. rate: 6,
  169. duration: 30,
  170. profile: profile,
  171. determination: determination,
  172. currentTemp: currentTemp
  173. )
  174. #expect(requestedTemp.rate == 2.8)
  175. #expect(requestedTemp.duration == 30)
  176. }
  177. @Test("should temp to 0 when requested rate is less than 0") func rateLessThanZero() throws {
  178. let profile = createProfile(currentBasal: 0.7, maxDailyBasal: 1.3, maxBasal: 10.0)
  179. let determination = createDetermination()
  180. let currentTemp = createCurrentTemp()
  181. let requestedTemp = try TempBasalFunctions.setTempBasal(
  182. rate: -1,
  183. duration: 30,
  184. profile: profile,
  185. determination: determination,
  186. currentTemp: currentTemp
  187. )
  188. #expect(requestedTemp.rate == 0)
  189. #expect(requestedTemp.duration == 30)
  190. }
  191. @Test("should limit high temp to 4 * max_daily_basal when overridden") func limitWithOverrideMaxDaily() throws {
  192. let profile = createProfile(currentBasal: 2.0, maxDailyBasal: 1.3, maxBasal: 10.0, maxDailySafetyMultiplier: 4)
  193. let determination = createDetermination()
  194. let currentTemp = createCurrentTemp()
  195. let requestedTemp = try TempBasalFunctions.setTempBasal(
  196. rate: 6,
  197. duration: 30,
  198. profile: profile,
  199. determination: determination,
  200. currentTemp: currentTemp
  201. )
  202. #expect(requestedTemp.rate == 5.2)
  203. #expect(requestedTemp.duration == 30)
  204. }
  205. @Test("should limit high temp to 5 * current_basal when overridden") func limitWithOverrideCurrentBasal() throws {
  206. let profile = createProfile(currentBasal: 0.7, maxDailyBasal: 1.3, maxBasal: 10.0, currentBasalSafetyMultiplier: 5)
  207. let determination = createDetermination()
  208. let currentTemp = createCurrentTemp()
  209. let requestedTemp = try TempBasalFunctions.setTempBasal(
  210. rate: 6,
  211. duration: 30,
  212. profile: profile,
  213. determination: determination,
  214. currentTemp: currentTemp
  215. )
  216. #expect(requestedTemp.rate == 3.5)
  217. #expect(requestedTemp.duration == 30)
  218. }
  219. @Test("should allow small basal change when current temp is also small") func allowSmallChange() throws {
  220. let profile = createProfile(
  221. currentBasal: 0.075,
  222. maxDailyBasal: 1.3,
  223. maxBasal: 10.0,
  224. currentBasalSafetyMultiplier: 5,
  225. model: "523"
  226. )
  227. let determination = createDetermination()
  228. let currentTemp = createCurrentTemp(rate: 0.025, duration: 24)
  229. let requestedTemp = try TempBasalFunctions.setTempBasal(
  230. rate: 0,
  231. duration: 30,
  232. profile: profile,
  233. determination: determination,
  234. currentTemp: currentTemp
  235. )
  236. #expect(requestedTemp.rate == 0)
  237. #expect(requestedTemp.duration == 30)
  238. }
  239. @Test("should not allow small basal change when current temp is large") func disallowSmallChange() throws {
  240. let profile = createProfile(
  241. currentBasal: 10.075,
  242. maxDailyBasal: 11.3,
  243. maxBasal: 50.0,
  244. currentBasalSafetyMultiplier: 5,
  245. model: "523"
  246. )
  247. let determination = createDetermination()
  248. let currentTemp = createCurrentTemp(rate: 10.1, duration: 24)
  249. let requestedTemp = try TempBasalFunctions.setTempBasal(
  250. rate: 10.125,
  251. duration: 30,
  252. profile: profile,
  253. determination: determination,
  254. currentTemp: currentTemp
  255. )
  256. #expect(requestedTemp.reason.contains("no temp required") == true)
  257. }
  258. @Test("should set neutral temp") func setNeutralTemp() throws {
  259. let profile = createProfile(currentBasal: 0.8, skipNeutralTemps: false)
  260. let determination = createDetermination()
  261. let currentTemp = createCurrentTemp()
  262. let requestedTemp = try TempBasalFunctions.setTempBasal(
  263. rate: 0.8,
  264. duration: 30,
  265. profile: profile,
  266. determination: determination,
  267. currentTemp: currentTemp
  268. )
  269. #expect(requestedTemp.rate == 0.8)
  270. #expect(requestedTemp.duration == 30)
  271. #expect(requestedTemp.reason == "Setting neutral temp basal of 0.8U/hr")
  272. }
  273. }