ProfileJavascriptTests.swift 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345
  1. import Foundation
  2. import Testing
  3. @testable import Trio
  4. struct ProfileGeneratorTests {
  5. // Base test inputs that match the JavaScript test setup
  6. private func createBaseInputs() -> (
  7. PumpSettings,
  8. BGTargets,
  9. [BasalProfileEntry],
  10. InsulinSensitivities,
  11. Preferences,
  12. CarbRatios,
  13. [TempTarget],
  14. String,
  15. TrioSettings
  16. ) {
  17. let pumpSettings = PumpSettings(
  18. insulinActionCurve: 10,
  19. maxBolus: 10,
  20. maxBasal: 2
  21. )
  22. let bgTargets = BGTargets(
  23. units: .mgdL,
  24. userPreferredUnits: .mgdL,
  25. targets: [
  26. BGTargetEntry(low: 100, high: 120, start: "00:00", offset: 0)
  27. ]
  28. )
  29. let basalProfile = [
  30. BasalProfileEntry(start: "00:00", minutes: 0, rate: 1.0)
  31. ]
  32. let isf = InsulinSensitivities(
  33. units: .mgdL,
  34. userPreferredUnits: .mgdL,
  35. sensitivities: [
  36. InsulinSensitivityEntry(sensitivity: 100, offset: 0, start: "00:00")
  37. ]
  38. )
  39. let preferences = Preferences()
  40. let carbRatios = CarbRatios(
  41. units: .grams,
  42. schedule: [
  43. CarbRatioEntry(start: "00:00", offset: 0, ratio: 20)
  44. ]
  45. )
  46. let tempTargets: [TempTarget] = []
  47. let model = "523"
  48. let trioSettings = TrioSettings()
  49. return (pumpSettings, bgTargets, basalProfile, isf, preferences, carbRatios, tempTargets, model, trioSettings)
  50. }
  51. @Test("Basic profile generation should create profile with correct values") func testBasicProfileGeneration() throws {
  52. let inputs = createBaseInputs()
  53. let profile = try ProfileGenerator.generate(
  54. pumpSettings: inputs.0,
  55. bgTargets: inputs.1,
  56. basalProfile: inputs.2,
  57. isf: inputs.3,
  58. preferences: inputs.4,
  59. carbRatios: inputs.5,
  60. tempTargets: inputs.6,
  61. model: inputs.7,
  62. clock: Date()
  63. )
  64. #expect(profile.maxIob == 0)
  65. #expect(profile.dia == 10)
  66. #expect(profile.sens == 100)
  67. #expect(profile.currentBasal == 1)
  68. #expect(profile.maxBg == 100)
  69. #expect(profile.minBg == 100)
  70. #expect(profile.carbRatio == 20)
  71. }
  72. @Test("Profile with active temp target should use temp target values") func testProfileWithTempTarget() throws {
  73. var inputs = createBaseInputs()
  74. // Create temp target 5 minutes ago that lasts 20 minutes
  75. let currentTime = Date()
  76. let creationDate = currentTime.addingTimeInterval(-5 * 60)
  77. let tempTarget = TempTarget(
  78. name: "Eating Soon",
  79. createdAt: creationDate,
  80. targetTop: 80,
  81. targetBottom: 80,
  82. duration: 20,
  83. enteredBy: "Test",
  84. reason: "Eating Soon",
  85. isPreset: nil,
  86. enabled: nil,
  87. halfBasalTarget: nil
  88. )
  89. inputs.6 = [tempTarget]
  90. let profile = try ProfileGenerator.generate(
  91. pumpSettings: inputs.0,
  92. bgTargets: inputs.1,
  93. basalProfile: inputs.2,
  94. isf: inputs.3,
  95. preferences: inputs.4,
  96. carbRatios: inputs.5,
  97. tempTargets: inputs.6,
  98. model: inputs.7,
  99. clock: currentTime
  100. )
  101. #expect(profile.maxIob == 0)
  102. #expect(profile.dia == 10)
  103. #expect(profile.sens == 100)
  104. #expect(profile.currentBasal == 1)
  105. #expect(profile.maxBg == 80)
  106. #expect(profile.minBg == 80)
  107. #expect(profile.carbRatio == 20)
  108. #expect(profile.temptargetSet == true)
  109. }
  110. @Test("Profile with expired temp target should use default values") func testProfileWithExpiredTempTarget() throws {
  111. var inputs = createBaseInputs()
  112. // Create temp target 90 minutes ago
  113. let currentTime = Date()
  114. let creationDate = currentTime.addingTimeInterval(-90 * 60)
  115. let tempTarget = TempTarget(
  116. name: "Eating Soon",
  117. createdAt: creationDate,
  118. targetTop: 80,
  119. targetBottom: 80,
  120. duration: 20,
  121. enteredBy: "Test",
  122. reason: "Eating Soon",
  123. isPreset: nil,
  124. enabled: nil,
  125. halfBasalTarget: nil
  126. )
  127. inputs.6 = [tempTarget]
  128. let profile = try ProfileGenerator.generate(
  129. pumpSettings: inputs.0,
  130. bgTargets: inputs.1,
  131. basalProfile: inputs.2,
  132. isf: inputs.3,
  133. preferences: inputs.4,
  134. carbRatios: inputs.5,
  135. tempTargets: inputs.6,
  136. model: inputs.7,
  137. clock: currentTime
  138. )
  139. #expect(profile.maxIob == 0)
  140. #expect(profile.dia == 10)
  141. #expect(profile.sens == 100)
  142. #expect(profile.currentBasal == 1)
  143. #expect(profile.maxBg == 100)
  144. #expect(profile.minBg == 100)
  145. #expect(profile.carbRatio == 20)
  146. }
  147. @Test("Profile with zero duration temp target should use default values") func testProfileWithZeroDurationTempTarget() throws {
  148. var inputs = createBaseInputs()
  149. // Create temp target 5 minutes ago with 0 duration
  150. let currentTime = Date()
  151. let creationDate = currentTime.addingTimeInterval(-5 * 60)
  152. let tempTarget = TempTarget(
  153. name: "Eating Soon",
  154. createdAt: creationDate,
  155. targetTop: 80,
  156. targetBottom: 80,
  157. duration: 0,
  158. enteredBy: "Test",
  159. reason: "Eating Soon",
  160. isPreset: nil,
  161. enabled: nil,
  162. halfBasalTarget: nil
  163. )
  164. inputs.6 = [tempTarget]
  165. let profile = try ProfileGenerator.generate(
  166. pumpSettings: inputs.0,
  167. bgTargets: inputs.1,
  168. basalProfile: inputs.2,
  169. isf: inputs.3,
  170. preferences: inputs.4,
  171. carbRatios: inputs.5,
  172. tempTargets: inputs.6,
  173. model: inputs.7,
  174. clock: currentTime
  175. )
  176. #expect(profile.maxIob == 0)
  177. #expect(profile.dia == 10)
  178. #expect(profile.sens == 100)
  179. #expect(profile.currentBasal == 1)
  180. #expect(profile.maxBg == 100)
  181. #expect(profile.minBg == 100)
  182. #expect(profile.carbRatio == 20)
  183. }
  184. @Test("Profile generation with invalid DIA should throw error") func testInvalidDIA() throws {
  185. var inputs = createBaseInputs()
  186. inputs.0 = PumpSettings(
  187. insulinActionCurve: 1,
  188. maxBolus: 10,
  189. maxBasal: 2
  190. )
  191. #expect(throws: ProfileError.invalidDIA(value: 1)) {
  192. _ = try ProfileGenerator.generate(
  193. pumpSettings: inputs.0,
  194. bgTargets: inputs.1,
  195. basalProfile: inputs.2,
  196. isf: inputs.3,
  197. preferences: inputs.4,
  198. carbRatios: inputs.5,
  199. tempTargets: inputs.6,
  200. model: inputs.7,
  201. clock: Date()
  202. )
  203. }
  204. }
  205. @Test("Profile generation with zero basal rate should throw error") func testCurrentBasalZero() throws {
  206. var inputs = createBaseInputs()
  207. inputs.2 = [
  208. BasalProfileEntry(start: "00:00", minutes: 0, rate: 0.0)
  209. ]
  210. // the reason it throws this error is due to some complex logic
  211. // in Javascript around the handling of nil and 0 basal rate entries
  212. #expect(throws: ProfileError.invalidMaxDailyBasal(value: 0)) {
  213. _ = try ProfileGenerator.generate(
  214. pumpSettings: inputs.0,
  215. bgTargets: inputs.1,
  216. basalProfile: inputs.2,
  217. isf: inputs.3,
  218. preferences: inputs.4,
  219. carbRatios: inputs.5,
  220. tempTargets: inputs.6,
  221. model: inputs.7,
  222. clock: Date()
  223. )
  224. }
  225. }
  226. @Test("Profile should store model string correctly") func testModelString() throws {
  227. var inputs = createBaseInputs()
  228. inputs.7 = "\"554\"\n"
  229. let profile = try ProfileGenerator.generate(
  230. pumpSettings: inputs.0,
  231. bgTargets: inputs.1,
  232. basalProfile: inputs.2,
  233. isf: inputs.3,
  234. preferences: inputs.4,
  235. carbRatios: inputs.5,
  236. tempTargets: inputs.6,
  237. model: inputs.7,
  238. clock: Date()
  239. )
  240. #expect(profile.model == "554")
  241. }
  242. @Test("Profile should use temptargetSet key in output json") func testTempTargetSetKey() async throws {
  243. var inputs = createBaseInputs()
  244. inputs.7 = "\"554\"\n"
  245. let now = Date()
  246. let tempTargets = [
  247. TempTarget(
  248. name: nil,
  249. createdAt: now - 1.hoursToSeconds,
  250. targetTop: 100,
  251. targetBottom: 80,
  252. duration: 120,
  253. enteredBy: nil,
  254. reason: nil,
  255. isPreset: nil,
  256. enabled: nil,
  257. halfBasalTarget: nil
  258. )
  259. ]
  260. let openAps = OpenAPS(storage: BaseFileStorage(), tddStorage: MockTDDStorage())
  261. let jsResult = await openAps.makeProfileJavascript(
  262. preferences: inputs.4,
  263. pumpSettings: inputs.0,
  264. bgTargets: inputs.1,
  265. basalProfile: inputs.2,
  266. isf: inputs.3,
  267. carbRatio: inputs.5,
  268. tempTargets: tempTargets,
  269. model: inputs.7,
  270. autotune: RawJSON.null,
  271. trioSettings: inputs.8
  272. )
  273. let (swiftResult, makeProfileInputs) = OpenAPSSwift.makeProfile(
  274. preferences: inputs.4,
  275. pumpSettings: inputs.0,
  276. bgTargets: inputs.1,
  277. basalProfile: inputs.2,
  278. isf: inputs.3,
  279. carbRatio: inputs.5,
  280. tempTargets: tempTargets,
  281. model: inputs.7,
  282. trioSettings: inputs.8,
  283. clock: now
  284. )
  285. let comparison = JSONCompare.createComparison(
  286. function: .makeProfile,
  287. swift: swiftResult,
  288. swiftDuration: 1.0,
  289. javascript: jsResult,
  290. javascriptDuration: 1.0,
  291. iobInputs: nil,
  292. mealInputs: nil,
  293. autosensInputs: nil,
  294. determineBasalInputs: nil,
  295. makeProfileInputs: makeProfileInputs
  296. )
  297. if comparison.resultType == .valueDifference {
  298. print(comparison.differences!.prettyPrintedJSON!)
  299. }
  300. #expect(comparison.resultType == .matching)
  301. }
  302. }