AutosensJsonTests.swift 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209
  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 debug info
  53. let swiftDebugInfo = swiftDict["debugInfo"] as! [Any]
  54. let jsDebugInfo = jsDict["debugInfo"] as! [Any]
  55. for (s, js) in zip(swiftDebugInfo, jsDebugInfo) {
  56. print("Debug Info")
  57. print(" - Swift: \(s)")
  58. print(" - JS: \(js)")
  59. }
  60. // Extract deviationsUnsorted arrays
  61. let swiftDeviations = swiftDict["deviationsUnsorted"] as! [Any]
  62. let jsDeviations = jsDict["deviationsUnsorted"] as! [Any]
  63. // Convert both to Double arrays
  64. let swiftDoubles = swiftDeviations.compactMap { value -> Double? in
  65. if let number = value as? NSNumber {
  66. return number.doubleValue
  67. }
  68. return nil
  69. }
  70. let jsDoubles = jsDeviations.compactMap { value -> Double? in
  71. if let number = value as? NSNumber {
  72. return number.doubleValue
  73. } else if let string = value as? String {
  74. return Double(string)
  75. }
  76. return nil
  77. }
  78. // Compare the arrays
  79. print("Swift array count: \(swiftDoubles.count)")
  80. print("JS array count: \(jsDoubles.count)")
  81. guard swiftDoubles.count == jsDoubles.count else {
  82. print("Arrays have different lengths!")
  83. let count = max(swiftDoubles.count, jsDoubles.count)
  84. var index = 0
  85. while index < count {
  86. let swiftDouble = index < swiftDoubles.count ? String(swiftDoubles[index]) : "nil"
  87. let jsDouble = index < jsDoubles.count ? String(jsDoubles[index]) : "nil"
  88. print("Index: \(index), Swift: \(swiftDouble), JS: \(jsDouble)")
  89. index += 1
  90. }
  91. return
  92. }
  93. var differences: [(index: Int, swift: Double, js: Double)] = []
  94. for (index, (swiftVal, jsVal)) in zip(swiftDoubles, jsDoubles).enumerated() {
  95. if abs(swiftVal - jsVal) > 0.001 { // Small tolerance for floating point comparison
  96. differences.append((index: index, swift: swiftVal, js: jsVal))
  97. }
  98. }
  99. if differences.isEmpty {
  100. print("✅ Arrays are identical (within tolerance)")
  101. } else {
  102. print("❌ Found \(differences.count) differences:")
  103. for diff in differences {
  104. print(" Index \(diff.index): Swift=\(diff.swift), JS=\(diff.js)")
  105. }
  106. }
  107. }
  108. @Test(
  109. "should produce same results for autosens for fixed JS",
  110. .enabled(if: ReplayTests.enabled)
  111. ) func replayErrorInputs() async throws {
  112. let timezone = ReplayTests.timezone
  113. let files = try await HttpFiles.listFiles()
  114. for filePath in files {
  115. let algorithmComparison = try await HttpFiles.downloadFile(at: filePath)
  116. print("Checking \(filePath) @ \(algorithmComparison.createdAt)")
  117. guard timezone == algorithmComparison.timezone else {
  118. continue
  119. }
  120. guard let autosensInputs = algorithmComparison.autosensInput else {
  121. print("Skipping, no autosensInputs found")
  122. if let str = algorithmComparison.comparisonError {
  123. print(str)
  124. }
  125. if let str = algorithmComparison.swiftException {
  126. print(str)
  127. }
  128. continue
  129. }
  130. timeZoneForTests.setTimezone(identifier: algorithmComparison.timezone)
  131. try await checkFixedJsAgainstSwift(autosensInputs: autosensInputs)
  132. print("Checked \(filePath) @ \(algorithmComparison.createdAt)")
  133. timeZoneForTests.resetTimezone()
  134. }
  135. }
  136. @Test("Format autosens inputs for running in JS", .enabled(if: false)) func formatInputs() async throws {
  137. // this test is meant for one-off analysis so it's ok to hard code
  138. // a file, just make sure to _not_ check in updates to this to
  139. // avoid polluting our change logs
  140. let algorithmComparison = try await HttpFiles.downloadFile(at: "/files/432be489-adfd-4799-b469-8d3794d5188e.0.json")
  141. let autosensInputs = algorithmComparison.autosensInput!
  142. let encoder = JSONCoding.encoder
  143. let output = try encoder.encode(autosensInputs)
  144. let sharedDir = FileManager.default.temporaryDirectory
  145. let outputURL = sharedDir.appendingPathComponent("autosens_error_inputs.json")
  146. try output.write(to: outputURL)
  147. // Print the path so you can find it
  148. print("Writing to: \(outputURL.path)")
  149. timeZoneForTests.setTimezone(identifier: algorithmComparison.timezone)
  150. let openAps = OpenAPSFixed()
  151. let (autosensResultSwift, _) = OpenAPSSwift.autosense(
  152. glucose: autosensInputs.glucose,
  153. pumpHistory: autosensInputs.history,
  154. basalProfile: autosensInputs.basalProfile,
  155. profile: try JSONBridge.to(autosensInputs.profile),
  156. carbs: autosensInputs.carbs,
  157. tempTargets: autosensInputs.tempTargets,
  158. clock: autosensInputs.clock,
  159. includeDeviationsForTesting: true
  160. )
  161. let autosensResultJavascript = await openAps.autosenseJavascript(
  162. glucose: autosensInputs.glucose,
  163. pumpHistory: autosensInputs.history,
  164. basalprofile: autosensInputs.basalProfile,
  165. profile: try JSONBridge.to(autosensInputs.profile),
  166. carbs: autosensInputs.carbs,
  167. temptargets: autosensInputs.tempTargets,
  168. clock: autosensInputs.clock
  169. )
  170. if case let .success(swiftJson) = autosensResultSwift, case let .success(jsJson) = autosensResultJavascript {
  171. try compareDeviations(swiftJson: swiftJson, jsJson: jsJson)
  172. }
  173. try await checkFixedJsAgainstSwift(autosensInputs: autosensInputs)
  174. timeZoneForTests.resetTimezone()
  175. }
  176. }