DetermineBasalJsonTests.swift 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254
  1. import Foundation
  2. import Testing
  3. @testable import Trio
  4. @Suite("DetermineBasal testing using JSON inputs", .serialized) struct DetermineBasalJsonTests {
  5. let timeZoneForTests = TimeZoneForTests()
  6. @Test(
  7. "DetermineBasal should produce same results for fixed JS",
  8. .enabled(if: ReplayTests.enabled)
  9. ) func replayErrorInputs() async throws {
  10. // Note: This test case can only test one timezone per invocation
  11. // so you need to manually change this to try out errors from
  12. // different timezones
  13. let testingTimezone = ReplayTests.timezone
  14. let files = try await HttpFiles.listFiles()
  15. for filePath in files {
  16. let algorithmComparison = try await HttpFiles.downloadFile(at: filePath)
  17. print("Checking \(filePath) @ \(algorithmComparison.createdAt)")
  18. guard algorithmComparison.timezone == testingTimezone else {
  19. continue
  20. }
  21. guard let determineBasalInput = algorithmComparison.determineBasalInput else {
  22. print("Skipping, no determineBasalInput found")
  23. if let str = algorithmComparison.comparisonError {
  24. print(str)
  25. }
  26. if let str = algorithmComparison.swiftException {
  27. print(str)
  28. #expect(Bool(false), "Swift exception on determine")
  29. }
  30. continue
  31. }
  32. timeZoneForTests.setTimezone(identifier: algorithmComparison.timezone)
  33. try await checkFixedJsAgainstSwift(determineBasalInput: determineBasalInput)
  34. print("Checked \(filePath) \(algorithmComparison.timezone)")
  35. timeZoneForTests.resetTimezone()
  36. }
  37. }
  38. func checkFixedJsAgainstSwift(determineBasalInput: DetermineBasalInputs) async throws {
  39. let openAps = OpenAPSFixed()
  40. let (determineBasalResultSwift, _) = OpenAPSSwift.determineBasal(
  41. glucose: determineBasalInput.glucose,
  42. currentTemp: determineBasalInput.currentTemp,
  43. iob: try JSONBridge.to(determineBasalInput.iob),
  44. profile: try JSONBridge.to(determineBasalInput.profile),
  45. autosens: try JSONBridge.to(determineBasalInput.autosens),
  46. meal: try JSONBridge.to(determineBasalInput.meal),
  47. microBolusAllowed: determineBasalInput.microBolusAllowed,
  48. reservoir: determineBasalInput.reservoir ?? 0,
  49. pumpHistory: determineBasalInput.pumpHistory,
  50. preferences: determineBasalInput.preferences,
  51. basalProfile: determineBasalInput.basalProfile,
  52. trioCustomOrefVariables: determineBasalInput.trioCustomOrefVariables,
  53. clock: determineBasalInput.clock,
  54. includeDebugOutputs: true
  55. )
  56. let determineBasalResultJavascript = try await openAps.determineBasalJavascript(
  57. glucose: determineBasalInput.glucose,
  58. currentTemp: determineBasalInput.currentTemp,
  59. iob: try JSONBridge.to(determineBasalInput.iob),
  60. profile: try JSONBridge.to(determineBasalInput.profile),
  61. autosens: try JSONBridge.to(determineBasalInput.autosens),
  62. meal: try JSONBridge.to(determineBasalInput.meal),
  63. microBolusAllowed: determineBasalInput.microBolusAllowed,
  64. reservoir: determineBasalInput.reservoir ?? 0,
  65. pumpHistory: determineBasalInput.pumpHistory,
  66. preferences: determineBasalInput.preferences,
  67. basalProfile: determineBasalInput.basalProfile,
  68. trioCustomOrefVariables: determineBasalInput.trioCustomOrefVariables,
  69. clock: determineBasalInput.clock
  70. )
  71. let comparison = JSONCompare.createComparison(
  72. function: .determineBasal,
  73. swift: determineBasalResultSwift,
  74. swiftDuration: 0.1,
  75. javascript: determineBasalResultJavascript,
  76. javascriptDuration: 0.1,
  77. iobInputs: nil,
  78. mealInputs: nil,
  79. autosensInputs: nil,
  80. determineBasalInputs: nil
  81. )
  82. if comparison.resultType == .valueDifference {
  83. print(comparison.differences!.prettyPrintedJSON!)
  84. }
  85. if comparison.resultType != .matching {
  86. print("REPLAY ERROR: Fixed JS didn't match")
  87. }
  88. #expect(comparison.resultType == .matching)
  89. }
  90. @Test("Format determineBasal inputs for running in JS", .enabled(if: false)) func formatInputs() async throws {
  91. let openAps = OpenAPSFixed()
  92. // this test is meant for one-off analysis so it's ok to hard code
  93. // a file, just make sure to _not_ check in updates to this to
  94. // avoid polluting our change logs
  95. let algorithmComparison = try await HttpFiles.downloadFile(at: "/files/f1d04efa-c39b-4f0a-9955-65ab663ff9fb.0.json")
  96. let determineBasalInput = algorithmComparison.determineBasalInput!
  97. let encoder = JSONCoding.encoder
  98. let output = try encoder.encode(determineBasalInput)
  99. let sharedDir = FileManager.default.temporaryDirectory
  100. let outputURL = sharedDir.appendingPathComponent("determine_basal_error_inputs.json")
  101. // Print the path so you can find it
  102. print("Writing to: \(outputURL.path)")
  103. try output.write(to: outputURL)
  104. timeZoneForTests.setTimezone(identifier: algorithmComparison.timezone)
  105. let (determineBasalResultSwift, _) = OpenAPSSwift.determineBasal(
  106. glucose: determineBasalInput.glucose,
  107. currentTemp: determineBasalInput.currentTemp,
  108. iob: try JSONBridge.to(determineBasalInput.iob),
  109. profile: try JSONBridge.to(determineBasalInput.profile),
  110. autosens: try JSONBridge.to(determineBasalInput.autosens),
  111. meal: try JSONBridge.to(determineBasalInput.meal),
  112. microBolusAllowed: determineBasalInput.microBolusAllowed,
  113. reservoir: determineBasalInput.reservoir ?? 0,
  114. pumpHistory: determineBasalInput.pumpHistory,
  115. preferences: determineBasalInput.preferences,
  116. basalProfile: determineBasalInput.basalProfile,
  117. trioCustomOrefVariables: determineBasalInput.trioCustomOrefVariables,
  118. clock: determineBasalInput.clock,
  119. includeDebugOutputs: true
  120. )
  121. print("Swift result")
  122. switch determineBasalResultSwift {
  123. case let .success(rawJson):
  124. print(rawJson)
  125. case let .failure(error):
  126. print(error.localizedDescription)
  127. }
  128. let determineBasalResultJavascript = try await openAps.determineBasalJavascript(
  129. glucose: determineBasalInput.glucose,
  130. currentTemp: determineBasalInput.currentTemp,
  131. iob: try JSONBridge.to(determineBasalInput.iob),
  132. profile: try JSONBridge.to(determineBasalInput.profile),
  133. autosens: try JSONBridge.to(determineBasalInput.autosens),
  134. meal: try JSONBridge.to(determineBasalInput.meal),
  135. microBolusAllowed: determineBasalInput.microBolusAllowed,
  136. reservoir: determineBasalInput.reservoir ?? 0,
  137. pumpHistory: determineBasalInput.pumpHistory,
  138. preferences: determineBasalInput.preferences,
  139. basalProfile: determineBasalInput.basalProfile,
  140. trioCustomOrefVariables: determineBasalInput.trioCustomOrefVariables,
  141. clock: determineBasalInput.clock
  142. )
  143. print("Fixed JS result")
  144. switch determineBasalResultJavascript {
  145. case let .success(rawJson):
  146. print(rawJson)
  147. case let .failure(error):
  148. print(error.localizedDescription)
  149. }
  150. let comparison = JSONCompare.createComparison(
  151. function: .determineBasal,
  152. swift: determineBasalResultSwift,
  153. swiftDuration: 0.1,
  154. javascript: determineBasalResultJavascript,
  155. javascriptDuration: 0.1,
  156. iobInputs: nil,
  157. mealInputs: nil,
  158. autosensInputs: nil,
  159. determineBasalInputs: nil
  160. )
  161. if comparison.resultType == .valueDifference {
  162. print(comparison.differences!.prettyPrintedJSON!)
  163. printForecasts(comparison.differences)
  164. }
  165. #expect(comparison.resultType == .matching)
  166. timeZoneForTests.resetTimezone()
  167. }
  168. func printForecasts(_ values: [String: Any]?) {
  169. guard let values = values else { return }
  170. guard let forecasts = values["predBGs"] as? Trio.ValueDifference else { return }
  171. let js = forecasts.js.toDictionary()
  172. let swift = forecasts.swift.toDictionary()
  173. for forecastType in ["IOB", "ZT", "UAM", "COB"] {
  174. print("")
  175. guard let swiftForecast = swift[forecastType]?.toIntArray(),
  176. let jsForecast = js[forecastType]?.toIntArray()
  177. else {
  178. print("missing \(forecastType) forecast, skipping")
  179. continue
  180. }
  181. if swiftForecast.count == jsForecast.count {
  182. print(forecastType)
  183. } else {
  184. print("\(forecastType) has length mismatch ❌")
  185. }
  186. print("Row\tSft\tJS\tMatch")
  187. print("--------------")
  188. for (row, values) in zip(swiftForecast, jsForecast).enumerated() {
  189. let pass: String
  190. if abs(values.0 - values.1) <= 1 {
  191. pass = "✅"
  192. } else {
  193. pass = "❌"
  194. }
  195. print("\(row)\t\(values.0)\t\(values.1)\t\(pass)")
  196. }
  197. }
  198. }
  199. }
  200. extension JSONValue {
  201. func toDictionary() -> [String: Trio.JSONValue] {
  202. switch self {
  203. case let .object(dict):
  204. return dict
  205. default:
  206. fatalError()
  207. }
  208. }
  209. func toIntArray() -> [Int] {
  210. switch self {
  211. case let .array(array):
  212. return array.map { $0.toInt() }
  213. default:
  214. fatalError()
  215. }
  216. }
  217. func toInt() -> Int {
  218. switch self {
  219. case let .number(number):
  220. return Int(number)
  221. default:
  222. fatalError()
  223. }
  224. }
  225. }