AutosensJsonTests.swift 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245
  1. import Foundation
  2. import Testing
  3. @testable import Trio
  4. @Suite("Autosens using real JSON", .serialized) struct AutosensJsonTests {
  5. let timeZoneForTests = TimeZoneForTests()
  6. // static func from<T: Decodable>(string: String) throws -> T
  7. func loadJson<T: Decodable>(_ name: String) throws -> T {
  8. let testBundle = Bundle(for: BundleReference.self)
  9. let path = testBundle.path(forResource: name, ofType: "json")!
  10. let data = try Data(contentsOf: URL(fileURLWithPath: path))
  11. return try JSONCoding.decoder.decode(T.self, from: data)
  12. }
  13. @Test("Test with resistance") func generateJavascriptInputs() throws {
  14. let glucose: [BloodGlucose] = try loadJson("as-glucose")
  15. let pump: [PumpHistoryEvent] = try loadJson("as-pump")
  16. let basalProfile: [BasalProfileEntry] = try loadJson("as-basal")
  17. let profile: Profile = try loadJson("as-profile")
  18. let carbs: [CarbsEntry] = try loadJson("as-carbs")
  19. let tempTargets: [TempTarget] = try loadJson("as-temp-targets")
  20. let clock = Date("2025-06-08T00:14:35.481Z")!
  21. timeZoneForTests.setTimezone(identifier: "America/Los_Angeles")
  22. let autosensResult = try AutosensGenerator.generate(
  23. glucose: glucose,
  24. pumpHistory: pump,
  25. basalProfile: basalProfile,
  26. profile: profile,
  27. carbs: carbs,
  28. tempTargets: tempTargets,
  29. maxDeviations: 96,
  30. clock: clock,
  31. includeDeviationsForTesting: true
  32. )
  33. let deviationsUnsorted: [Decimal] = try loadJson("deviationsUnsorted")
  34. #expect(autosensResult.ratio == 1.2)
  35. #expect(autosensResult.newisf == 46)
  36. #expect(deviationsUnsorted.count == autosensResult.deviationsUnsorted?.count)
  37. for (ref, calc) in zip(deviationsUnsorted, autosensResult.deviationsUnsorted!) {
  38. // we can get differences due to rounding inconsistencies between
  39. // javascript and swift with negative numbers
  40. #expect(ref.isWithin(0.01, of: calc))
  41. }
  42. timeZoneForTests.resetTimezone()
  43. }
  44. func checkFixedJsAgainstSwift(autosensInputs: AutosensInputs) async throws {
  45. let openAps = OpenAPSFixed()
  46. let (autosensResultSwift, _) = OpenAPSSwift.autosense(
  47. glucose: autosensInputs.glucose,
  48. pumpHistory: autosensInputs.history,
  49. basalProfile: autosensInputs.basalProfile,
  50. profile: try JSONBridge.to(autosensInputs.profile),
  51. carbs: autosensInputs.carbs,
  52. tempTargets: autosensInputs.tempTargets,
  53. clock: autosensInputs.clock,
  54. includeDeviationsForTesting: true
  55. )
  56. let autosensResultJavascript = await openAps.autosenseJavascript(
  57. glucose: autosensInputs.glucose,
  58. pumpHistory: autosensInputs.history,
  59. basalprofile: autosensInputs.basalProfile,
  60. profile: try JSONBridge.to(autosensInputs.profile),
  61. carbs: autosensInputs.carbs,
  62. temptargets: autosensInputs.tempTargets,
  63. clock: autosensInputs.clock
  64. )
  65. let comparison = JSONCompare.createComparison(
  66. function: .autosens,
  67. swift: autosensResultSwift,
  68. swiftDuration: 0.1,
  69. javascript: autosensResultJavascript,
  70. javascriptDuration: 0.1,
  71. iobInputs: nil,
  72. mealInputs: nil,
  73. autosensInputs: nil
  74. )
  75. if comparison.resultType == .valueDifference {
  76. print(comparison.differences!.prettyPrintedJSON!)
  77. }
  78. if comparison.resultType != .matching {
  79. print("REPLAY ERROR: Fixed JS didn't match")
  80. }
  81. #expect(comparison.resultType == .matching)
  82. }
  83. func compareDeviations(swiftJson: String, jsJson: String) throws {
  84. // Parse both JSON strings
  85. let swiftData = swiftJson.data(using: .utf8)!
  86. let jsData = jsJson.data(using: .utf8)!
  87. let swiftDict = try JSONSerialization.jsonObject(with: swiftData) as! [String: Any]
  88. let jsDict = try JSONSerialization.jsonObject(with: jsData) as! [String: Any]
  89. // Extract deviationsUnsorted arrays
  90. let swiftDeviations = swiftDict["deviationsUnsorted"] as! [Any]
  91. let jsDeviations = jsDict["deviationsUnsorted"] as! [Any]
  92. // Convert both to Double arrays
  93. let swiftDoubles = swiftDeviations.compactMap { value -> Double? in
  94. if let number = value as? NSNumber {
  95. return number.doubleValue
  96. }
  97. return nil
  98. }
  99. let jsDoubles = jsDeviations.compactMap { value -> Double? in
  100. if let number = value as? NSNumber {
  101. return number.doubleValue
  102. } else if let string = value as? String {
  103. return Double(string)
  104. }
  105. return nil
  106. }
  107. // Compare the arrays
  108. print("Swift array count: \(swiftDoubles.count)")
  109. print("JS array count: \(jsDoubles.count)")
  110. guard swiftDoubles.count == jsDoubles.count else {
  111. print("Arrays have different lengths!")
  112. print("Swift: \(swiftDoubles)")
  113. print("JS: \(jsDoubles)")
  114. return
  115. }
  116. var differences: [(index: Int, swift: Double, js: Double)] = []
  117. for (index, (swiftVal, jsVal)) in zip(swiftDoubles, jsDoubles).enumerated() {
  118. if abs(swiftVal - jsVal) > 0.001 { // Small tolerance for floating point comparison
  119. differences.append((index: index, swift: swiftVal, js: jsVal))
  120. }
  121. }
  122. if differences.isEmpty {
  123. print("✅ Arrays are identical (within tolerance)")
  124. } else {
  125. print("❌ Found \(differences.count) differences:")
  126. for diff in differences {
  127. print(" Index \(diff.index): Swift=\(diff.swift), JS=\(diff.js)")
  128. }
  129. }
  130. }
  131. @Test(
  132. "should produce same results for autosens for fixed JS",
  133. .enabled(if: false)
  134. ) func replayErrorInputs() async throws {
  135. let timezone = "America/Los_Angeles"
  136. var skippedTimezones = Set<String>()
  137. let files = try await HttpFiles.listFiles()
  138. for filePath in files {
  139. let algorithmComparison = try await HttpFiles.downloadFile(at: filePath)
  140. print("Checking \(filePath) @ \(algorithmComparison.createdAt)")
  141. guard timezone == algorithmComparison.timezone else {
  142. skippedTimezones.insert(algorithmComparison.timezone)
  143. continue
  144. }
  145. guard let autosensInputs = algorithmComparison.autosensInput else {
  146. print("Skipping, no autosensInputs found")
  147. if let str = algorithmComparison.comparisonError {
  148. print(str)
  149. }
  150. if let str = algorithmComparison.swiftException {
  151. print(str)
  152. }
  153. continue
  154. }
  155. timeZoneForTests.setTimezone(identifier: algorithmComparison.timezone)
  156. try await checkFixedJsAgainstSwift(autosensInputs: autosensInputs)
  157. print("Checked \(filePath) @ \(algorithmComparison.createdAt)")
  158. timeZoneForTests.resetTimezone()
  159. }
  160. print("Skipped timezones:")
  161. for skippedTimezone in skippedTimezones {
  162. print(" - \(skippedTimezone)")
  163. }
  164. }
  165. @Test("Format autosens inputs for running in JS", .enabled(if: false)) func formatInputs() async throws {
  166. // this test is meant for one-off analysis so it's ok to hard code
  167. // a file, just make sure to _not_ check in updates to this to
  168. // avoid polluting our change logs
  169. let algorithmComparison = try await HttpFiles.downloadFile(at: "/files/4f38ce73-1526-4bcd-80d5-1dee5b002519.0.json")
  170. let autosensInputs = algorithmComparison.autosensInput!
  171. let encoder = JSONCoding.encoder
  172. let output = try encoder.encode(autosensInputs)
  173. let sharedDir = FileManager.default.temporaryDirectory
  174. let outputURL = sharedDir.appendingPathComponent("autosens_error_inputs.json")
  175. try output.write(to: outputURL)
  176. timeZoneForTests.setTimezone(identifier: algorithmComparison.timezone)
  177. let openAps = OpenAPSFixed()
  178. let (autosensResultSwift, _) = OpenAPSSwift.autosense(
  179. glucose: autosensInputs.glucose,
  180. pumpHistory: autosensInputs.history,
  181. basalProfile: autosensInputs.basalProfile,
  182. profile: try JSONBridge.to(autosensInputs.profile),
  183. carbs: autosensInputs.carbs,
  184. tempTargets: autosensInputs.tempTargets,
  185. clock: autosensInputs.clock,
  186. includeDeviationsForTesting: true
  187. )
  188. let autosensResultJavascript = await openAps.autosenseJavascript(
  189. glucose: autosensInputs.glucose,
  190. pumpHistory: autosensInputs.history,
  191. basalprofile: autosensInputs.basalProfile,
  192. profile: try JSONBridge.to(autosensInputs.profile),
  193. carbs: autosensInputs.carbs,
  194. temptargets: autosensInputs.tempTargets,
  195. clock: autosensInputs.clock
  196. )
  197. if case let .success(swiftJson) = autosensResultSwift, case let .success(jsJson) = autosensResultJavascript {
  198. try compareDeviations(swiftJson: swiftJson, jsJson: jsJson)
  199. }
  200. // Print the path so you can find it
  201. print("Writing to: \(outputURL.path)")
  202. timeZoneForTests.resetTimezone()
  203. }
  204. }