IobJsonTests.swift 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309
  1. import Foundation
  2. import Testing
  3. @testable import Trio
  4. /// This test suite is to help us debug and verify iob errors from Trio devices
  5. ///
  6. /// There are two key components. First, we have a version of the Javascript that has a number
  7. /// of bugs fixed. We don't want to fix the real Javascript, so we put this fixed Javascript in the
  8. /// testing bundle and use it to run comparisons. If the error we see in the field is one that we know
  9. /// about and have fixed in JS, the Swift and JS implementations will produce the same results. You
  10. /// can find the fixed JS here:
  11. /// https://github.com/kingst/trio-oref/tree/tcd-fixes-for-swift-comparison
  12. ///
  13. /// Second, we have a server that runs (part of `trio-oref-logs`) to serve error logs captured
  14. /// from the field. This server needs to run on the same machine as the simulator where this test runs.
  15. /// You can find more information about it from the `trio-oref-logs` repo.
  16. @Suite("IoB using real pump history JSON", .serialized) struct IobJsonTests {
  17. private var originalTZ: String? = ProcessInfo.processInfo.environment["TZ"]
  18. private var originalDefaultTimeZone: TimeZone? = TimeZone.current
  19. // Helper function to set timezone
  20. private func setTimezone(identifier: String) {
  21. // Set environment variable
  22. setenv("TZ", identifier, 1)
  23. tzset() // Make the change take effect
  24. // Force update the default TimeZone
  25. // This is the critical missing piece
  26. if let timeZone = TimeZone(identifier: identifier) {
  27. TimeZone.ReferenceType.default = timeZone
  28. // For extra assurance, you can log to verify
  29. print("Timezone set to: \(TimeZone.current.identifier)")
  30. } else {
  31. print("Failed to create TimeZone with identifier: \(identifier)")
  32. }
  33. }
  34. // Helper function to reset timezone
  35. private func resetTimezone() {
  36. // Restore system timezone from environment
  37. if let originalTZ = originalTZ {
  38. setenv("TZ", originalTZ, 1)
  39. } else {
  40. unsetenv("TZ")
  41. }
  42. tzset()
  43. // Restore original default TimeZone
  44. if let originalTimeZone = originalDefaultTimeZone {
  45. TimeZone.ReferenceType.default = originalTimeZone
  46. }
  47. }
  48. struct IobHistoryResult: Codable {
  49. var insulin: Decimal?
  50. var rate: Decimal?
  51. var duration: Decimal?
  52. var timestamp: String?
  53. var started_at: String?
  54. var created_at: String?
  55. var date: Decimal?
  56. enum CodingKeys: String, CodingKey {
  57. case insulin
  58. case rate
  59. case duration
  60. case timestamp
  61. case started_at
  62. case created_at
  63. case date
  64. }
  65. }
  66. // Note: This test case has a memory leak so limit your inputs
  67. // to about 250 files at a time
  68. @Test(
  69. "should produce same results for fixed JS and different for bundle JS",
  70. .enabled(if: false)
  71. ) func replayErrorInputs() async throws {
  72. let url = URL(string: "http://localhost:8123/list")!
  73. let (data, _) = try await URLSession.shared.data(from: url)
  74. let files = try JSONDecoder().decode([String].self, from: data)
  75. let fileDataDecoder = JSONDecoder()
  76. fileDataDecoder.dateDecodingStrategy = .secondsSince1970
  77. for filePath in files {
  78. let dataUrl = URL(string: "http://localhost:8123\(filePath)")!
  79. let (data, _) = try await URLSession.shared.data(from: dataUrl)
  80. let algorithmComparison = try fileDataDecoder.decode(AlgorithmComparison.self, from: data)
  81. print("Checking \(filePath) @ \(algorithmComparison.createdAt)")
  82. guard let iobInputs = algorithmComparison.iobInput else {
  83. print("Skipping, no iobInputs found")
  84. if let str = algorithmComparison.comparisonError {
  85. print(str)
  86. }
  87. if let str = algorithmComparison.swiftException {
  88. print(str)
  89. }
  90. continue
  91. }
  92. setTimezone(identifier: algorithmComparison.timezone)
  93. try await checkFixedJsAgainstSwift(iobInputs: iobInputs)
  94. try await checkBundleJsAgainstSwift(iobInputs: iobInputs)
  95. resetTimezone()
  96. }
  97. }
  98. func checkFixedJsAgainstSwift(iobInputs: IobInputs) async throws {
  99. let openAps = OpenAPSFixed()
  100. let (iobResultSwift, _) = OpenAPSSwift.iob(
  101. pumphistory: iobInputs.history,
  102. profile: try JSONBridge.to(iobInputs.profile),
  103. clock: iobInputs.clock,
  104. autosens: try JSONBridge.to(iobInputs.autosens)
  105. )
  106. let iobResultJavascript = await openAps.iobJavascript(
  107. pumphistory: iobInputs.history,
  108. profile: try JSONBridge.to(iobInputs.profile),
  109. clock: iobInputs.clock,
  110. autosens: try JSONBridge.to(iobInputs.autosens)
  111. )
  112. let comparison = JSONCompare.createComparison(
  113. function: .iob,
  114. swift: iobResultSwift,
  115. swiftDuration: 0.1,
  116. javascript: iobResultJavascript,
  117. javascriptDuration: 0.1,
  118. iobInputs: nil
  119. )
  120. if comparison.resultType == .valueDifference {
  121. print(comparison.differences!.prettyPrintedJSON!)
  122. }
  123. if comparison.resultType != .matching {
  124. print("REPLAY ERROR: Fixed JS didn't match")
  125. }
  126. #expect(comparison.resultType == .matching)
  127. }
  128. func checkBundleJsAgainstSwift(iobInputs: IobInputs) async throws {
  129. let openAps = OpenAPS(storage: BaseFileStorage(), tddStorage: MockTDDStorage())
  130. let (iobResultSwift, _) = OpenAPSSwift.iob(
  131. pumphistory: iobInputs.history,
  132. profile: try JSONBridge.to(iobInputs.profile),
  133. clock: iobInputs.clock,
  134. autosens: try JSONBridge.to(iobInputs.autosens)
  135. )
  136. let iobResultJavascript = await openAps.iobJavascript(
  137. pumphistory: iobInputs.history,
  138. profile: try JSONBridge.to(iobInputs.profile),
  139. clock: iobInputs.clock,
  140. autosens: try JSONBridge.to(iobInputs.autosens)
  141. )
  142. let comparison = JSONCompare.createComparison(
  143. function: .iob,
  144. swift: iobResultSwift,
  145. swiftDuration: 0.1,
  146. javascript: iobResultJavascript,
  147. javascriptDuration: 0.1,
  148. iobInputs: nil
  149. )
  150. if comparison.resultType != .valueDifference {
  151. print("REPLAY ERROR: bundle JS did't produce value difference")
  152. }
  153. #expect(comparison.resultType == .valueDifference)
  154. }
  155. func checkHistoryConsistency(swiftTreatments: [ComputedPumpHistoryEvent], jsTreatments: [IobHistoryResult]) {
  156. let swiftNetBolus = swiftTreatments.compactMap(\.insulin).filter({ $0 >= 0.1 }).reduce(0, +)
  157. let jsNetBolus = jsTreatments.compactMap(\.insulin).filter({ $0 >= 0.1 }).reduce(0, +)
  158. let swiftNetBasal = swiftTreatments.compactMap(\.insulin).filter({ $0 < 0.1 }).reduce(0, +)
  159. let jsNetBasal = jsTreatments.compactMap(\.insulin).filter({ $0 < 0.1 }).reduce(0, +)
  160. #expect(swiftNetBasal == jsNetBasal)
  161. #expect(swiftNetBolus == jsNetBolus)
  162. }
  163. func checkRunningBasal(swiftTreatments: [ComputedPumpHistoryEvent], jsTreatments: [IobHistoryResult]) {
  164. let swiftBasals = swiftTreatments.filter({ $0.rate != nil }).filter({ $0.duration! > 0 })
  165. let jsBasals = jsTreatments.filter({ $0.rate != nil }).filter({ $0.duration! > 0 })
  166. #expect(swiftBasals.count == jsBasals.count)
  167. for (swift, js) in zip(swiftBasals, jsBasals) {
  168. #expect(Decimal(swift.date) == js.date!)
  169. #expect(swift.duration!.isWithin(0.01, of: js.duration!))
  170. #expect(swift.rate == js.rate)
  171. let start = js.date!
  172. let end = js.date! + js.duration! * 60 * 1000
  173. let swiftTempBolus = swiftTreatments
  174. .filter({ Decimal($0.date) >= start && Decimal($0.date) < end && $0.insulin != nil && $0.insulin! < 0.1 })
  175. .map({ $0.insulin! }).reduce(0, +)
  176. let jsTempBolus = jsTreatments
  177. .filter({ $0.date! >= start && $0.date! < end && $0.insulin != nil && $0.insulin! < 0.1 }).map({ $0.insulin! })
  178. .reduce(0, +)
  179. if swiftTempBolus != jsTempBolus {
  180. print("temp bolus @ \(swift.timestamp) mismatch swift: \(swiftTempBolus) js: \(jsTempBolus)")
  181. }
  182. #expect(swiftTempBolus == jsTempBolus)
  183. }
  184. }
  185. @Test("Debug utility for checking iob-history", .enabled(if: false)) func debugIobHistory() async throws {
  186. let testBundle = Bundle(for: BundleReference.self)
  187. let path = testBundle.path(forResource: "iob-error-log", ofType: "json")!
  188. let data = try Data(contentsOf: URL(fileURLWithPath: path))
  189. let decoder = JSONDecoder()
  190. decoder.dateDecodingStrategy = .secondsSince1970
  191. let algorithmComparison = try decoder.decode(AlgorithmComparison.self, from: data)
  192. let iobInputs = algorithmComparison.iobInput!
  193. setTimezone(identifier: algorithmComparison.timezone)
  194. let swiftIobHistory = try IobHistory.calcTempTreatments(
  195. history: iobInputs.history.map { $0.computedEvent() },
  196. profile: iobInputs.profile,
  197. clock: iobInputs.clock,
  198. autosens: iobInputs.autosens,
  199. zeroTempDuration: nil
  200. )
  201. let openAps = OpenAPSFixed()
  202. let jsIobHistoryRaw = try await openAps.iobHistory(
  203. pumphistory: iobInputs.history,
  204. profile: JSONBridge.to(iobInputs.profile),
  205. clock: iobInputs.clock,
  206. autosens: JSONBridge.to(iobInputs.autosens),
  207. zeroTempDuration: RawJSON.null
  208. )
  209. let jsIobHistory = try JSONDecoder().decode([IobHistoryResult].self, from: jsIobHistoryRaw.rawJSON.data(using: .utf8)!)
  210. let encoder = JSONCoding.encoder
  211. var output = try encoder.encode(swiftIobHistory)
  212. var sharedDir = FileManager.default.temporaryDirectory
  213. var outputURL = sharedDir.appendingPathComponent("swift_treatments.json")
  214. print("Writing to: \(outputURL.path)")
  215. try output.write(to: outputURL)
  216. output = try encoder.encode(jsIobHistory)
  217. sharedDir = FileManager.default.temporaryDirectory
  218. outputURL = sharedDir.appendingPathComponent("js_treatments.json")
  219. print("Writing to: \(outputURL.path)")
  220. try output.write(to: outputURL)
  221. checkHistoryConsistency(swiftTreatments: swiftIobHistory, jsTreatments: jsIobHistory)
  222. checkRunningBasal(swiftTreatments: swiftIobHistory, jsTreatments: jsIobHistory)
  223. resetTimezone()
  224. }
  225. /// simple utility for creating inputs for Javascript for use in testing
  226. @Test("format inputs for Javascript", .enabled(if: false)) func generateJavascriptInputs() throws {
  227. let testBundle = Bundle(for: BundleReference.self)
  228. let path = testBundle.path(forResource: "iob-error-log", ofType: "json")!
  229. let data = try Data(contentsOf: URL(fileURLWithPath: path))
  230. let decoder = JSONDecoder()
  231. decoder.dateDecodingStrategy = .secondsSince1970
  232. let algorithmComparison = try decoder.decode(AlgorithmComparison.self, from: data)
  233. let iobInputs = algorithmComparison.iobInput!
  234. let encoder = JSONCoding.encoder
  235. let output = try encoder.encode(iobInputs)
  236. let sharedDir = FileManager.default.temporaryDirectory
  237. let outputURL = sharedDir.appendingPathComponent("js_iob_input_error.json")
  238. // Print the path so you can find it
  239. print("Writing to: \(outputURL.path)")
  240. try output.write(to: outputURL)
  241. setTimezone(identifier: algorithmComparison.timezone)
  242. let treatments = try IobHistory.calcTempTreatments(
  243. history: iobInputs.history.map { $0.computedEvent() },
  244. profile: iobInputs.profile,
  245. clock: iobInputs.clock,
  246. autosens: iobInputs.autosens,
  247. zeroTempDuration: nil
  248. )
  249. let iobSomething = try IobCalculation.iobTotal(treatments: treatments, profile: iobInputs.profile, time: iobInputs.clock)
  250. resetTimezone()
  251. print(iobSomething.prettyPrintedJSON!)
  252. let treatmentsOut = try encoder.encode(treatments)
  253. let treatmentsUrl = sharedDir.appendingPathComponent("treatments.json")
  254. print("Writing to: \(treatmentsUrl.path)")
  255. try treatmentsOut.write(to: treatmentsUrl)
  256. }
  257. }