SetTempBasalTests.swift 10 KB

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