JSONImporterTests.swift 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489
  1. //
  2. // JSONImporterTests.swift
  3. // Trio
  4. //
  5. // Created by Cengiz Deniz on 21.04.25.
  6. //
  7. import CoreData
  8. import Foundation
  9. import Swinject
  10. import Testing
  11. @testable import Trio
  12. @Suite("JSON Importer Tests") struct JSONImporterTests: Injectable {
  13. let resolver: Resolver = TrioApp().resolver
  14. var coreDataStack: CoreDataStack!
  15. var context: NSManagedObjectContext!
  16. var importer: JSONImporter!
  17. let fileManager = FileManager.default
  18. let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
  19. @Injected() var fileStorage: FileStorage!
  20. init() async throws {
  21. injectServices(resolver)
  22. // In-memory Core Data for tests
  23. coreDataStack = try await CoreDataStack.createForTests()
  24. context = coreDataStack.newTaskContext()
  25. importer = JSONImporter(context: context)
  26. // Clear import flags and remove fixtures
  27. let flags = [
  28. "pumpHistoryImported",
  29. "carbHistoryImported",
  30. "glucoseHistoryImported",
  31. "enactedHistoryImported"
  32. ]
  33. flags.forEach { UserDefaults.standard.removeObject(forKey: $0) }
  34. let comps = [
  35. OpenAPS.Monitor.pumpHistory,
  36. OpenAPS.Monitor.carbHistory,
  37. OpenAPS.Monitor.glucose,
  38. OpenAPS.Enact.enacted
  39. ]
  40. comps.forEach { try? fileManager.removeItem(at: documentsURL.appendingPathComponent($0)) }
  41. }
  42. private let iso8601WithFractionalSecondsFormatter: ISO8601DateFormatter = {
  43. let formatter = ISO8601DateFormatter()
  44. // ensure it parses the full internet date+time with milliseconds
  45. formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
  46. return formatter
  47. }()
  48. /// Parses an ISO‑8601 string (e.g. "2025-04-17T10:00:00.000Z") into a `Date`.
  49. /// - Parameter isoString: the ISO‑8601 date string.
  50. /// - Returns: a `Date` if parsing succeeds, or `nil` otherwise.
  51. func dateFromISOString(_ isoString: String) -> Date? {
  52. iso8601WithFractionalSecondsFormatter.date(from: isoString)
  53. }
  54. @Test("Import pump history with value checks") func testImportPumpHistoryDetails() async throws {
  55. let pumpHistory = [
  56. PumpHistoryEvent(
  57. id: "9DDAA42F-465C-4812-9422-9933FB1CC290",
  58. type: .bolus,
  59. timestamp: dateFromISOString("2025-04-17T10:00:00.000Z") ?? Date(),
  60. amount: 1.0,
  61. duration: 0,
  62. isSMB: false,
  63. isExternal: true
  64. ),
  65. PumpHistoryEvent(
  66. id: "F958F9A5-78F3-4B6C-AF6C-5B580BBB8A29",
  67. type: .bolus,
  68. timestamp: dateFromISOString("2025-04-17T10:01:00.000Z") ?? Date(),
  69. amount: 2.0,
  70. duration: 0,
  71. isSMB: false,
  72. isExternal: false
  73. ),
  74. PumpHistoryEvent(
  75. id: "CCBE1CDA-EE13-4D7C-8CCC-7361EC9C979D",
  76. type: .bolus,
  77. timestamp: dateFromISOString("2025-04-17T10:02:00.000Z") ?? Date(),
  78. amount: 3.0,
  79. duration: 0,
  80. isSMB: true,
  81. isExternal: false
  82. ),
  83. PumpHistoryEvent(
  84. id: "0FB76585-B6A4-4659-BDD2-B673BE6DD549",
  85. type: .tempBasalDuration,
  86. timestamp: dateFromISOString("2025-04-17T10:05:00.000Z") ?? Date(),
  87. duration: 30
  88. ),
  89. PumpHistoryEvent(
  90. id: "_0FB76585-B6A4-4659-BDD2-B673BE6DD549",
  91. type: .tempBasal,
  92. timestamp: dateFromISOString("2025-04-17T10:05:00.000Z") ?? Date(),
  93. amount: 1.5,
  94. duration: 0,
  95. temp: .absolute
  96. ),
  97. PumpHistoryEvent(
  98. id: "24909A93-0BC7-46D0-837F-9B2028E22BFC",
  99. type: .pumpSuspend,
  100. timestamp: dateFromISOString("2025-04-17T10:10:00.000Z") ?? Date()
  101. ),
  102. PumpHistoryEvent(
  103. id: "BDEF7F55-48FE-447D-876C-19260ADE5ECA",
  104. type: .pumpResume,
  105. timestamp: dateFromISOString("2025-04-17T10:10:00.000Z") ?? Date()
  106. ),
  107. PumpHistoryEvent(
  108. id: "1CAEEFA3-D740-4EA0-83B4-D28860991639",
  109. type: .rewind,
  110. timestamp: dateFromISOString("2025-04-17T10:10:00.000Z") ?? Date()
  111. ),
  112. PumpHistoryEvent(
  113. id: "CD019C44-57F0-4CB0-BBDF-8B6C40A48E99",
  114. type: .prime,
  115. timestamp: dateFromISOString("2025-04-17T10:10:00.000Z") ?? Date()
  116. )
  117. ]
  118. fileStorage.save(pumpHistory, as: OpenAPS.Monitor.pumpHistory)
  119. // Import
  120. await importer.importPumpHistoryIfNeeded()
  121. // Fetch all imported events
  122. let events = try await coreDataStack.fetchEntitiesAsync(
  123. ofType: PumpEventStored.self,
  124. onContext: context,
  125. predicate: NSPredicate(format: "TRUEPREDICATE"),
  126. key: "timestamp",
  127. ascending: true
  128. ) as? [PumpEventStored] ?? []
  129. // Verify total count
  130. #expect(events.count == 8, "Should import all 8 pump events") // TBR should be combination of TB duration and TBR, so 8, not 9
  131. let iso8601 = ISO8601DateFormatter()
  132. iso8601.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
  133. // Verify the three Boluses
  134. let bolusEvents = events.filter { $0.type == PumpEventStored.EventType.bolus.rawValue }
  135. #expect(bolusEvents.count == 3, "Three bolus events")
  136. let extDate = iso8601.date(from: "2025-04-17T10:00:00.000Z")!
  137. let nonExtDate = iso8601.date(from: "2025-04-17T10:01:00.000Z")!
  138. let smbDate = iso8601.date(from: "2025-04-17T10:02:00.000Z")!
  139. // external bolus
  140. #expect(
  141. bolusEvents.contains {
  142. abs($0.timestamp!.timeIntervalSince(extDate)) < 0.001 &&
  143. $0.bolus?.amount == NSDecimalNumber(value: 1.0) &&
  144. $0.bolus?.isExternal == true &&
  145. $0.bolus?.isSMB == false
  146. },
  147. "External bolus (1.0 U) at 10:00"
  148. )
  149. // non‑external
  150. #expect(
  151. bolusEvents.contains {
  152. abs($0.timestamp!.timeIntervalSince(nonExtDate)) < 0.001 &&
  153. $0.bolus?.amount == NSDecimalNumber(value: 2.0) &&
  154. $0.bolus?.isExternal == false &&
  155. $0.bolus?.isSMB == false
  156. },
  157. "Non‑external bolus (2.0 U) at 10:01"
  158. )
  159. // SMB
  160. #expect(
  161. bolusEvents.contains {
  162. abs($0.timestamp!.timeIntervalSince(smbDate)) < 0.001 &&
  163. $0.bolus?.amount == NSDecimalNumber(value: 3.0) &&
  164. $0.bolus?.isExternal == false &&
  165. $0.bolus?.isSMB == true
  166. },
  167. "SMB bolus (3.0 U) at 10:02"
  168. )
  169. // Verify TempBasalDuration + TempBasal
  170. let durDate = iso8601.date(from: "2025-04-17T10:05:00.000Z")!
  171. let durEvt = events.first {
  172. abs($0.timestamp!.timeIntervalSince(durDate)) < 0.001 &&
  173. $0.tempBasal?.duration == 30 &&
  174. $0.tempBasal?.rate == NSDecimalNumber(value: 1.5)
  175. }
  176. #expect(durEvt != nil, "TempBasalRate at 10:05 for 30 min with rate 1.5 U/h")
  177. // Verify the four “marker” events
  178. let markers: [(type: PumpEventStored.EventType, ts: String)] = [
  179. (.pumpSuspend, "2025-04-17T10:10:00.000Z"),
  180. (.pumpResume, "2025-04-17T10:15:00.000Z"),
  181. (.rewind, "2025-04-17T10:20:00.000Z"),
  182. (.prime, "2025-04-17T10:25:00.000Z")
  183. ]
  184. for (eventType, tsString) in markers {
  185. let date = iso8601.date(from: tsString)!
  186. #expect(
  187. events.contains {
  188. $0.type == eventType.rawValue &&
  189. abs($0.timestamp!.timeIntervalSince(date)) < 0.001
  190. },
  191. "\(eventType) at \(tsString)"
  192. )
  193. }
  194. // Ensure file cleaned up and flag set
  195. let url = documentsURL.appendingPathComponent(OpenAPS.Monitor.pumpHistory)
  196. #expect(!fileManager.fileExists(atPath: url.path), "Pump JSON should be removed")
  197. #expect(UserDefaults.standard.bool(forKey: "pumpHistoryImported"))
  198. }
  199. @Test("Import carb history with property checks") func testImportCarbHistoryDetails() async throws {
  200. let carbHistory = [
  201. CarbsEntry(
  202. id: "CF9BE626-5B4F-421C-825F-BDEB873FF385",
  203. createdAt: dateFromISOString("2025-04-17T10:10:00.000Z") ?? Date(),
  204. actualDate: dateFromISOString("2025-04-17T10:10:00.000Z") ?? Date(),
  205. carbs: 2,
  206. fat: 0,
  207. protein: 0,
  208. note: "",
  209. enteredBy: "Trio",
  210. isFPU: true,
  211. fpuID: "EF2A99A8-3D96-4F92-8412-2D43C8CF6859"
  212. ),
  213. CarbsEntry(
  214. id: "6FFED023-DE5C-4042-8E4D-D876C37F528C",
  215. createdAt: dateFromISOString("2025-04-21T21:58:25.452Z") ?? Date(),
  216. actualDate: dateFromISOString("2025-04-21T21:58:25.452Z") ?? Date(),
  217. carbs: 2,
  218. fat: 0,
  219. protein: 0,
  220. note: "",
  221. enteredBy: "Trio",
  222. isFPU: true,
  223. fpuID: "EF2A99A8-3D96-4F92-8412-2D43C8CF6859"
  224. ),
  225. CarbsEntry(
  226. id: "32859426-03FC-4CF7-B9A1-16E122C04889",
  227. createdAt: dateFromISOString("2025-04-21T21:28:25.452Z") ?? Date(),
  228. actualDate: dateFromISOString("2025-04-21T21:28:25.452Z") ?? Date(),
  229. carbs: 2,
  230. fat: 0,
  231. protein: 0,
  232. note: "",
  233. enteredBy: "Trio",
  234. isFPU: true,
  235. fpuID: "EF2A99A8-3D96-4F92-8412-2D43C8CF6859"
  236. ),
  237. CarbsEntry(
  238. id: "F9AA11B6-8B2E-4FCA-9E3C-EFE582786CFD",
  239. createdAt: dateFromISOString("2025-04-21T20:58:25.452Z") ?? Date(),
  240. actualDate: dateFromISOString("2025-04-21T20:58:25.452Z") ?? Date(),
  241. carbs: 2,
  242. fat: 0,
  243. protein: 0,
  244. note: "",
  245. enteredBy: "Trio",
  246. isFPU: true,
  247. fpuID: "EF2A99A8-3D96-4F92-8412-2D43C8CF6859"
  248. ),
  249. CarbsEntry(
  250. id: "D2986C75-8EEF-4ACB-AE62-35B5391D437D",
  251. createdAt: dateFromISOString("2025-04-21T20:28:25.452Z") ?? Date(),
  252. actualDate: dateFromISOString("2025-04-21T20:28:25.452Z") ?? Date(),
  253. carbs: 2,
  254. fat: 0,
  255. protein: 0,
  256. note: "",
  257. enteredBy: "Trio",
  258. isFPU: true,
  259. fpuID: "EF2A99A8-3D96-4F92-8412-2D43C8CF6859"
  260. ),
  261. CarbsEntry(
  262. id: "E2F186B2-6A8F-4BC4-A038-3C104F988A78",
  263. createdAt: dateFromISOString("2025-04-21T19:58:25.452Z") ?? Date(),
  264. actualDate: dateFromISOString("2025-04-21T19:58:25.452Z") ?? Date(),
  265. carbs: 2,
  266. fat: 0,
  267. protein: 0,
  268. note: "",
  269. enteredBy: "Trio",
  270. isFPU: true,
  271. fpuID: "EF2A99A8-3D96-4F92-8412-2D43C8CF6859"
  272. ),
  273. CarbsEntry(
  274. id: "EEE7F9A0-490C-4E2B-8F04-ED8A77FC7867",
  275. createdAt: dateFromISOString("2025-04-21T18:58:25.452Z") ?? Date(),
  276. actualDate: dateFromISOString("2025-04-21T18:58:25.452Z") ?? Date(),
  277. carbs: 45,
  278. fat: 15,
  279. protein: 25,
  280. note: "",
  281. enteredBy: "Trio",
  282. isFPU: true,
  283. fpuID: nil
  284. ),
  285. CarbsEntry(
  286. id: "283155A7-5AF0-486E-BD3B-F9F8E2354845",
  287. createdAt: dateFromISOString("2025-04-21T16:50:02.104Z") ?? Date(),
  288. actualDate: dateFromISOString("2025-04-21T16:50:02.104Z") ?? Date(),
  289. carbs: 30,
  290. fat: 0,
  291. protein: 0,
  292. note: "",
  293. enteredBy: "Trio",
  294. isFPU: true,
  295. fpuID: nil
  296. )
  297. ]
  298. fileStorage.save(carbHistory, as: OpenAPS.Monitor.carbHistory)
  299. await importer.importCarbHistoryIfNeeded()
  300. // Fetch all imported events
  301. let entries = try await coreDataStack.fetchEntitiesAsync(
  302. ofType: CarbEntryStored.self,
  303. onContext: context,
  304. predicate: NSPredicate(format: "TRUEPREDICATE"),
  305. key: "date",
  306. ascending: false
  307. ) as? [CarbEntryStored] ?? []
  308. #expect(entries.count == 8, "Should import 8 carb entries")
  309. // TODO: add distinct tests
  310. let url = documentsURL.appendingPathComponent(OpenAPS.Monitor.carbHistory)
  311. #expect(!fileManager.fileExists(atPath: url.path))
  312. #expect(UserDefaults.standard.bool(forKey: "carbHistoryImported"))
  313. }
  314. @Test("Import glucose history with manual flag checks") func testImportGlucoseHistoryDetails() async throws {
  315. let glucoseReadings = [
  316. BloodGlucose(
  317. _id: "A2BDFCE8-1978-4E12-9B29-BD11DB44A739",
  318. sgv: 107,
  319. direction: .flat,
  320. date: 1733677520950,
  321. dateString: dateFromISOString("2024-08-23T20:24:07.950Z") ?? Date(),
  322. unfiltered: 107,
  323. filtered: nil,
  324. noise: nil,
  325. glucose: 107,
  326. type: "sgv",
  327. transmitterID: "ABC123"
  328. ),
  329. BloodGlucose(
  330. _id: "A2BDFCE8-1978-4E12-9B29-BD11DB44A739",
  331. sgv: 112,
  332. direction: .fortyFiveUp,
  333. date: 1733676920294,
  334. dateString: dateFromISOString("2024-12-08T16:55:20.295Z") ?? Date(),
  335. unfiltered: 112,
  336. filtered: nil,
  337. noise: nil,
  338. glucose: 112,
  339. type: "sgv",
  340. transmitterID: "ABC123"
  341. ),
  342. BloodGlucose(
  343. _id: "A2BDFCE8-1978-4E12-9B29-BD11DB44A739",
  344. sgv: 97,
  345. direction: .fortyFiveDown,
  346. date: 1733676620784,
  347. dateString: dateFromISOString("2024-12-08T16:50:20.784Z") ?? Date(),
  348. unfiltered: 97,
  349. filtered: nil,
  350. noise: nil,
  351. glucose: 97,
  352. type: "sgv",
  353. transmitterID: "ABC123"
  354. ),
  355. BloodGlucose(
  356. _id: "A2BDFCE8-1978-4E12-9B29-BD11DB44A739",
  357. sgv: 70,
  358. direction: .doubleDown,
  359. date: 1733676320525,
  360. dateString: dateFromISOString("2024-12-08T16:45:20.525Z") ?? Date(),
  361. unfiltered: 70,
  362. filtered: nil,
  363. noise: nil,
  364. glucose: 70,
  365. type: "sgv",
  366. transmitterID: "ABC123"
  367. ),
  368. BloodGlucose(
  369. _id: "A2BDFCE8-1978-4E12-9B29-BD11DB44A739",
  370. sgv: 188,
  371. direction: .doubleUp,
  372. date: 1733676020918,
  373. dateString: dateFromISOString("2024-12-08T16:40:20.919Z") ?? Date(),
  374. unfiltered: 188,
  375. filtered: nil,
  376. noise: nil,
  377. glucose: 188,
  378. type: "sgv",
  379. transmitterID: "ABC123"
  380. )
  381. ]
  382. // Fetch all GlucoseStored entries sorted by date
  383. let allReadings = try await coreDataStack.fetchEntitiesAsync(
  384. ofType: GlucoseStored.self,
  385. onContext: context,
  386. predicate: NSPredicate(format: "TRUEPREDICATE"),
  387. key: "date",
  388. ascending: true
  389. ) as? [GlucoseStored] ?? []
  390. #expect(allReadings.count == 5, "Should have imported 5 glucose readings")
  391. // TODO: add distinct tests
  392. let url = documentsURL.appendingPathComponent(OpenAPS.Monitor.glucose)
  393. #expect(!fileManager.fileExists(atPath: url.path))
  394. #expect(UserDefaults.standard.bool(forKey: "glucoseHistoryImported"))
  395. }
  396. @Test("Import determination history with nested predBGs and values") func testImportDeterminationHistoryDetails() async throws {
  397. let iobValues: [Int] = [
  398. 153, 149, 144, 139, 134, 129, 124, 119, 114, 109,
  399. 103, 98, 92, 87, 81, 76, 71, 65, 60, 55,
  400. 50, 44, 39
  401. ]
  402. let ztValues: [Int] = [
  403. 153, 147, 140, 134, 128, 121, 115, 109, 104, 98,
  404. 93, 87, 83, 78, 74, 70, 66, 63, 60, 57,
  405. 55, 53, 52, 51, 51, 51, 51, 52, 53, 54,
  406. 56, 58, 60, 62, 65, 67, 70, 73, 76, 78,
  407. 81, 84
  408. ]
  409. let uamValues: [Int] = [
  410. 153, 147, 140, 134, 127, 121, 115, 108, 102, 96,
  411. 89, 83, 77, 71, 65, 58, 52, 45, 39
  412. ]
  413. let determination = Determination(
  414. id: UUID(),
  415. reason: "Autosens ratio: 0.94, ISF: 45→48, COB: 0, Dev: 13, BGI: -6, CR: 7.8→8.3, Target: 85, minPredBG 45, minGuardBG -53, IOBpredBG 39, UAMpredBG 39, TDD: 42.2 U, 89% Bolus 11% Basal, Dynamic ISF/CR: On/On, Logarithmic formula, AF: 0.8, Basal ratio: 1.01; minGuardBG -53<70",
  416. units: nil,
  417. insulinReq: Decimal(0),
  418. eventualBG: 46,
  419. sensitivityRatio: Decimal(0.9430005356061704),
  420. rate: Decimal(0),
  421. duration: Decimal(120),
  422. iob: Decimal(2.52),
  423. cob: Decimal(0),
  424. predictions: Predictions(iob: iobValues, zt: ztValues, cob: nil, uam: uamValues),
  425. deliverAt: dateFromISOString("2024-08-01T09:42:08.734Z") ?? Date(),
  426. carbsReq: nil,
  427. temp: TempType(rawValue: "absolute"),
  428. bg: Decimal(153),
  429. reservoir: Decimal(3735928559),
  430. isf: Decimal(48),
  431. timestamp: dateFromISOString("2024-08-01T09:42:09.371Z") ?? Date(),
  432. current_target: Decimal(85),
  433. insulinForManualBolus: nil,
  434. manualBolusErrorString: Decimal(2),
  435. minDelta: Decimal(-4.28),
  436. expectedDelta: Decimal(-4.7),
  437. minGuardBG: Decimal(-53),
  438. minPredBG: nil,
  439. threshold: Decimal(70),
  440. carbRatio: Decimal(8.3),
  441. received: true
  442. )
  443. fileStorage.save(determination, as: OpenAPS.Enact.enacted)
  444. // TODO: add distinct tests
  445. let url = documentsURL.appendingPathComponent(OpenAPS.Enact.enacted)
  446. #expect(!fileManager.fileExists(atPath: url.path))
  447. #expect(UserDefaults.standard.bool(forKey: "enactedHistoryImported"))
  448. }
  449. }