AutosensJsonTests.swift 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193
  1. import Foundation
  2. import Testing
  3. @testable import Trio
  4. @Suite("Autosens using real JSON", .serialized) struct AutosensJsonTests {
  5. let timeZoneForTests = TimeZoneForTests()
  6. func checkFixedJsAgainstSwift(autosensInputs: AutosensInputs) async throws {
  7. let openAps = OpenAPSFixed()
  8. let (autosensResultSwift, _) = OpenAPSSwift.autosense(
  9. glucose: autosensInputs.glucose,
  10. pumpHistory: autosensInputs.history,
  11. basalProfile: autosensInputs.basalProfile,
  12. profile: try JSONBridge.to(autosensInputs.profile),
  13. carbs: autosensInputs.carbs,
  14. tempTargets: autosensInputs.tempTargets,
  15. clock: autosensInputs.clock,
  16. includeDeviationsForTesting: true
  17. )
  18. let autosensResultJavascript = await openAps.autosenseJavascript(
  19. glucose: autosensInputs.glucose,
  20. pumpHistory: autosensInputs.history,
  21. basalprofile: autosensInputs.basalProfile,
  22. profile: try JSONBridge.to(autosensInputs.profile),
  23. carbs: autosensInputs.carbs,
  24. temptargets: autosensInputs.tempTargets,
  25. clock: autosensInputs.clock
  26. )
  27. let comparison = JSONCompare.createComparison(
  28. function: .autosens,
  29. swift: autosensResultSwift,
  30. swiftDuration: 0.1,
  31. javascript: autosensResultJavascript,
  32. javascriptDuration: 0.1,
  33. iobInputs: nil,
  34. mealInputs: nil,
  35. autosensInputs: nil,
  36. determineBasalInputs: nil
  37. )
  38. if comparison.resultType == .valueDifference {
  39. print(comparison.differences!.prettyPrintedJSON!)
  40. }
  41. if comparison.resultType != .matching {
  42. print("REPLAY ERROR: Fixed JS didn't match")
  43. }
  44. #expect(comparison.resultType == .matching)
  45. }
  46. func compareDeviations(swiftJson: String, jsJson: String) throws {
  47. // Parse both JSON strings
  48. let swiftData = swiftJson.data(using: .utf8)!
  49. let jsData = jsJson.data(using: .utf8)!
  50. let swiftDict = try JSONSerialization.jsonObject(with: swiftData) as! [String: Any]
  51. let jsDict = try JSONSerialization.jsonObject(with: jsData) as! [String: Any]
  52. // Extract deviationsUnsorted arrays
  53. let swiftDeviations = swiftDict["deviationsUnsorted"] as! [Any]
  54. let jsDeviations = jsDict["deviationsUnsorted"] as! [Any]
  55. // Convert both to Double arrays
  56. let swiftDoubles = swiftDeviations.compactMap { value -> Double? in
  57. if let number = value as? NSNumber {
  58. return number.doubleValue
  59. }
  60. return nil
  61. }
  62. let jsDoubles = jsDeviations.compactMap { value -> Double? in
  63. if let number = value as? NSNumber {
  64. return number.doubleValue
  65. } else if let string = value as? String {
  66. return Double(string)
  67. }
  68. return nil
  69. }
  70. // Compare the arrays
  71. print("Swift array count: \(swiftDoubles.count)")
  72. print("JS array count: \(jsDoubles.count)")
  73. guard swiftDoubles.count == jsDoubles.count else {
  74. print("Arrays have different lengths!")
  75. print("Swift: \(swiftDoubles)")
  76. print("JS: \(jsDoubles)")
  77. return
  78. }
  79. var differences: [(index: Int, swift: Double, js: Double)] = []
  80. for (index, (swiftVal, jsVal)) in zip(swiftDoubles, jsDoubles).enumerated() {
  81. if abs(swiftVal - jsVal) > 0.001 { // Small tolerance for floating point comparison
  82. differences.append((index: index, swift: swiftVal, js: jsVal))
  83. }
  84. }
  85. if differences.isEmpty {
  86. print("✅ Arrays are identical (within tolerance)")
  87. } else {
  88. print("❌ Found \(differences.count) differences:")
  89. for diff in differences {
  90. print(" Index \(diff.index): Swift=\(diff.swift), JS=\(diff.js)")
  91. }
  92. }
  93. }
  94. @Test(
  95. "should produce same results for autosens for fixed JS",
  96. .enabled(if: ReplayTests.enabled)
  97. ) func replayErrorInputs() async throws {
  98. let timezone = ReplayTests.timezone
  99. let files = try await HttpFiles.listFiles()
  100. for filePath in files {
  101. let algorithmComparison = try await HttpFiles.downloadFile(at: filePath)
  102. print("Checking \(filePath) @ \(algorithmComparison.createdAt)")
  103. guard timezone == algorithmComparison.timezone else {
  104. continue
  105. }
  106. guard let autosensInputs = algorithmComparison.autosensInput else {
  107. print("Skipping, no autosensInputs found")
  108. if let str = algorithmComparison.comparisonError {
  109. print(str)
  110. }
  111. if let str = algorithmComparison.swiftException {
  112. print(str)
  113. #expect(Bool(false), "Swift exception on autosens")
  114. }
  115. continue
  116. }
  117. timeZoneForTests.setTimezone(identifier: algorithmComparison.timezone)
  118. try await checkFixedJsAgainstSwift(autosensInputs: autosensInputs)
  119. print("Checked \(filePath) @ \(algorithmComparison.createdAt)")
  120. timeZoneForTests.resetTimezone()
  121. }
  122. }
  123. @Test("Format autosens inputs for running in JS", .enabled(if: false)) func formatInputs() async throws {
  124. // this test is meant for one-off analysis so it's ok to hard code
  125. // a file, just make sure to _not_ check in updates to this to
  126. // avoid polluting our change logs
  127. let algorithmComparison = try await HttpFiles.downloadFile(at: "/files/432be489-adfd-4799-b469-8d3794d5188e.0.json")
  128. let autosensInputs = algorithmComparison.autosensInput!
  129. let encoder = JSONCoding.encoder
  130. let output = try encoder.encode(autosensInputs)
  131. let sharedDir = FileManager.default.temporaryDirectory
  132. let outputURL = sharedDir.appendingPathComponent("autosens_error_inputs.json")
  133. try output.write(to: outputURL)
  134. timeZoneForTests.setTimezone(identifier: algorithmComparison.timezone)
  135. let openAps = OpenAPSFixed()
  136. let (autosensResultSwift, _) = OpenAPSSwift.autosense(
  137. glucose: autosensInputs.glucose,
  138. pumpHistory: autosensInputs.history,
  139. basalProfile: autosensInputs.basalProfile,
  140. profile: try JSONBridge.to(autosensInputs.profile),
  141. carbs: autosensInputs.carbs,
  142. tempTargets: autosensInputs.tempTargets,
  143. clock: autosensInputs.clock,
  144. includeDeviationsForTesting: true
  145. )
  146. let autosensResultJavascript = await openAps.autosenseJavascript(
  147. glucose: autosensInputs.glucose,
  148. pumpHistory: autosensInputs.history,
  149. basalprofile: autosensInputs.basalProfile,
  150. profile: try JSONBridge.to(autosensInputs.profile),
  151. carbs: autosensInputs.carbs,
  152. temptargets: autosensInputs.tempTargets,
  153. clock: autosensInputs.clock
  154. )
  155. if case let .success(swiftJson) = autosensResultSwift, case let .success(jsJson) = autosensResultJavascript {
  156. try compareDeviations(swiftJson: swiftJson, jsJson: jsJson)
  157. }
  158. // Print the path so you can find it
  159. print("Writing to: \(outputURL.path)")
  160. timeZoneForTests.resetTimezone()
  161. }
  162. }