IobJsonTests.swift 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274
  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. let timeZoneForTests = TimeZoneForTests()
  18. struct IobHistoryResult: Codable {
  19. var insulin: Decimal?
  20. var rate: Decimal?
  21. var duration: Decimal?
  22. var timestamp: String?
  23. var started_at: String?
  24. var created_at: String?
  25. var date: Decimal?
  26. enum CodingKeys: String, CodingKey {
  27. case insulin
  28. case rate
  29. case duration
  30. case timestamp
  31. case started_at
  32. case created_at
  33. case date
  34. }
  35. }
  36. // Note: This test case has a memory leak so limit your inputs
  37. // to about 250 files at a time
  38. @Test(
  39. "IoB should produce same results for fixed JS and different for bundle JS",
  40. .enabled(if: ReplayTests.enabled)
  41. ) func replayErrorInputs() async throws {
  42. let files = try await HttpFiles.listFiles()
  43. for filePath in files {
  44. let algorithmComparison = try await HttpFiles.downloadFile(at: filePath)
  45. print("Checking \(filePath) @ \(algorithmComparison.createdAt)")
  46. guard let iobInputs = algorithmComparison.iobInput else {
  47. print("Skipping, no iobInputs found")
  48. if let str = algorithmComparison.comparisonError {
  49. print(str)
  50. }
  51. if let str = algorithmComparison.swiftException {
  52. print(str)
  53. }
  54. continue
  55. }
  56. timeZoneForTests.setTimezone(identifier: algorithmComparison.timezone)
  57. try await checkFixedJsAgainstSwift(iobInputs: iobInputs)
  58. try await checkBundleJsAgainstSwift(iobInputs: iobInputs)
  59. timeZoneForTests.resetTimezone()
  60. }
  61. }
  62. func checkFixedJsAgainstSwift(iobInputs: IobInputs) async throws {
  63. let openAps = OpenAPSFixed()
  64. let (iobResultSwift, _) = OpenAPSSwift.iob(
  65. pumphistory: iobInputs.history,
  66. profile: try JSONBridge.to(iobInputs.profile),
  67. clock: iobInputs.clock,
  68. autosens: try JSONBridge.to(iobInputs.autosens)
  69. )
  70. let iobResultJavascript = await openAps.iobJavascript(
  71. pumphistory: iobInputs.history,
  72. profile: try JSONBridge.to(iobInputs.profile),
  73. clock: iobInputs.clock,
  74. autosens: try JSONBridge.to(iobInputs.autosens)
  75. )
  76. let comparison = JSONCompare.createComparison(
  77. function: .iob,
  78. swift: iobResultSwift,
  79. swiftDuration: 0.1,
  80. javascript: iobResultJavascript,
  81. javascriptDuration: 0.1,
  82. iobInputs: nil,
  83. mealInputs: nil,
  84. autosensInputs: nil,
  85. determineBasalInputs: nil
  86. )
  87. if comparison.resultType == .valueDifference {
  88. print(comparison.differences!.prettyPrintedJSON!)
  89. }
  90. if comparison.resultType != .matching {
  91. print("REPLAY ERROR: Fixed JS didn't match")
  92. }
  93. #expect(comparison.resultType == .matching)
  94. }
  95. func checkBundleJsAgainstSwift(iobInputs: IobInputs) async throws {
  96. let openAps = OpenAPS(storage: BaseFileStorage(), tddStorage: MockTDDStorage())
  97. let (iobResultSwift, _) = OpenAPSSwift.iob(
  98. pumphistory: iobInputs.history,
  99. profile: try JSONBridge.to(iobInputs.profile),
  100. clock: iobInputs.clock,
  101. autosens: try JSONBridge.to(iobInputs.autosens)
  102. )
  103. let iobResultJavascript = await openAps.iobJavascript(
  104. pumphistory: iobInputs.history,
  105. profile: try JSONBridge.to(iobInputs.profile),
  106. clock: iobInputs.clock,
  107. autosens: try JSONBridge.to(iobInputs.autosens)
  108. )
  109. let comparison = JSONCompare.createComparison(
  110. function: .iob,
  111. swift: iobResultSwift,
  112. swiftDuration: 0.1,
  113. javascript: iobResultJavascript,
  114. javascriptDuration: 0.1,
  115. iobInputs: nil,
  116. mealInputs: nil,
  117. autosensInputs: nil,
  118. determineBasalInputs: nil
  119. )
  120. if comparison.resultType != .valueDifference {
  121. print("REPLAY ERROR: bundle JS did't produce value difference")
  122. }
  123. #expect(comparison.resultType == .valueDifference)
  124. }
  125. func checkHistoryConsistency(swiftTreatments: [ComputedPumpHistoryEvent], jsTreatments: [IobHistoryResult]) {
  126. let swiftNetBolus = swiftTreatments.compactMap(\.insulin).filter({ $0 >= 0.1 }).reduce(0, +)
  127. let jsNetBolus = jsTreatments.compactMap(\.insulin).filter({ $0 >= 0.1 }).reduce(0, +)
  128. let swiftNetBasal = swiftTreatments.compactMap(\.insulin).filter({ $0 < 0.1 }).reduce(0, +)
  129. let jsNetBasal = jsTreatments.compactMap(\.insulin).filter({ $0 < 0.1 }).reduce(0, +)
  130. #expect(swiftNetBasal == jsNetBasal)
  131. #expect(swiftNetBolus == jsNetBolus)
  132. }
  133. func checkRunningBasal(swiftTreatments: [ComputedPumpHistoryEvent], jsTreatments: [IobHistoryResult]) {
  134. let swiftBasals = swiftTreatments.filter({ $0.rate != nil }).filter({ $0.duration! > 0 })
  135. let jsBasals = jsTreatments.filter({ $0.rate != nil }).filter({ $0.duration! > 0 })
  136. #expect(swiftBasals.count == jsBasals.count)
  137. for (swift, js) in zip(swiftBasals, jsBasals) {
  138. #expect(Decimal(swift.date) == js.date!)
  139. #expect(swift.duration!.isWithin(0.01, of: js.duration!))
  140. #expect(swift.rate == js.rate)
  141. let start = js.date!
  142. let end = js.date! + js.duration! * 60 * 1000
  143. let swiftTempBolus = swiftTreatments
  144. .filter({ Decimal($0.date) >= start && Decimal($0.date) < end && $0.insulin != nil && $0.insulin! < 0.1 })
  145. .map({ $0.insulin! }).reduce(0, +)
  146. let jsTempBolus = jsTreatments
  147. .filter({ $0.date! >= start && $0.date! < end && $0.insulin != nil && $0.insulin! < 0.1 }).map({ $0.insulin! })
  148. .reduce(0, +)
  149. if swiftTempBolus != jsTempBolus {
  150. print("temp bolus @ \(swift.timestamp) mismatch swift: \(swiftTempBolus) js: \(jsTempBolus)")
  151. }
  152. #expect(swiftTempBolus == jsTempBolus)
  153. }
  154. }
  155. @Test("Debug utility for checking iob-history", .enabled(if: ReplayTests.enabled)) func debugIobHistory() async throws {
  156. let testBundle = Bundle(for: BundleReference.self)
  157. let path = testBundle.path(forResource: "iob-error-log", ofType: "json")!
  158. let data = try Data(contentsOf: URL(fileURLWithPath: path))
  159. let decoder = JSONDecoder()
  160. decoder.dateDecodingStrategy = .secondsSince1970
  161. let algorithmComparison = try decoder.decode(AlgorithmComparison.self, from: data)
  162. let iobInputs = algorithmComparison.iobInput!
  163. timeZoneForTests.setTimezone(identifier: algorithmComparison.timezone)
  164. let swiftIobHistory = try IobHistory.calcTempTreatments(
  165. history: iobInputs.history.map { $0.computedEvent() },
  166. profile: iobInputs.profile,
  167. clock: iobInputs.clock,
  168. autosens: iobInputs.autosens,
  169. zeroTempDuration: nil
  170. )
  171. let openAps = OpenAPSFixed()
  172. let jsIobHistoryRaw = try await openAps.iobHistory(
  173. pumphistory: iobInputs.history,
  174. profile: JSONBridge.to(iobInputs.profile),
  175. clock: iobInputs.clock,
  176. autosens: JSONBridge.to(iobInputs.autosens),
  177. zeroTempDuration: RawJSON.null
  178. )
  179. let jsIobHistory = try JSONDecoder().decode([IobHistoryResult].self, from: jsIobHistoryRaw.rawJSON.data(using: .utf8)!)
  180. let encoder = JSONCoding.encoder
  181. var output = try encoder.encode(swiftIobHistory)
  182. var sharedDir = FileManager.default.temporaryDirectory
  183. var outputURL = sharedDir.appendingPathComponent("swift_treatments.json")
  184. print("Writing to: \(outputURL.path)")
  185. try output.write(to: outputURL)
  186. output = try encoder.encode(jsIobHistory)
  187. sharedDir = FileManager.default.temporaryDirectory
  188. outputURL = sharedDir.appendingPathComponent("js_treatments.json")
  189. print("Writing to: \(outputURL.path)")
  190. try output.write(to: outputURL)
  191. checkHistoryConsistency(swiftTreatments: swiftIobHistory, jsTreatments: jsIobHistory)
  192. checkRunningBasal(swiftTreatments: swiftIobHistory, jsTreatments: jsIobHistory)
  193. timeZoneForTests.resetTimezone()
  194. }
  195. /// simple utility for creating inputs for Javascript for use in testing
  196. @Test("format inputs for Javascript", .enabled(if: ReplayTests.enabled)) func generateJavascriptInputs() throws {
  197. let testBundle = Bundle(for: BundleReference.self)
  198. let path = testBundle.path(forResource: "iob-error-log", ofType: "json")!
  199. let data = try Data(contentsOf: URL(fileURLWithPath: path))
  200. let decoder = JSONDecoder()
  201. decoder.dateDecodingStrategy = .secondsSince1970
  202. let algorithmComparison = try decoder.decode(AlgorithmComparison.self, from: data)
  203. let iobInputs = algorithmComparison.iobInput!
  204. let encoder = JSONCoding.encoder
  205. let output = try encoder.encode(iobInputs)
  206. let sharedDir = FileManager.default.temporaryDirectory
  207. let outputURL = sharedDir.appendingPathComponent("js_iob_input_error.json")
  208. // Print the path so you can find it
  209. print("Writing to: \(outputURL.path)")
  210. try output.write(to: outputURL)
  211. timeZoneForTests.setTimezone(identifier: algorithmComparison.timezone)
  212. let treatments = try IobHistory.calcTempTreatments(
  213. history: iobInputs.history.map { $0.computedEvent() },
  214. profile: iobInputs.profile,
  215. clock: iobInputs.clock,
  216. autosens: iobInputs.autosens,
  217. zeroTempDuration: nil
  218. )
  219. let iobSomething = try IobCalculation.iobTotal(treatments: treatments, profile: iobInputs.profile, time: iobInputs.clock)
  220. timeZoneForTests.resetTimezone()
  221. print(iobSomething.prettyPrintedJSON!)
  222. let treatmentsOut = try encoder.encode(treatments)
  223. let treatmentsUrl = sharedDir.appendingPathComponent("treatments.json")
  224. print("Writing to: \(treatmentsUrl.path)")
  225. try treatmentsOut.write(to: treatmentsUrl)
  226. }
  227. }