JSONImporterTests.swift 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226
  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. class BundleReference {}
  13. @Suite("JSON Importer Tests", .serialized) struct JSONImporterTests: Injectable {
  14. var coreDataStack: CoreDataStack!
  15. var context: NSManagedObjectContext!
  16. var importer: JSONImporter!
  17. init() async throws {
  18. // In-memory Core Data for tests
  19. coreDataStack = try await CoreDataStack.createForTests()
  20. context = coreDataStack.newTaskContext()
  21. importer = JSONImporter(context: context, coreDataStack: coreDataStack)
  22. }
  23. @Test("Import glucose history with value checks") func testImportGlucoseHistoryDetails() async throws {
  24. let testBundle = Bundle(for: BundleReference.self)
  25. let path = testBundle.path(forResource: "glucose", ofType: "json")!
  26. let url = URL(filePath: path)
  27. let now = Date("2025-04-28T19:32:52.000Z")!
  28. try await importer.importGlucoseHistory(url: url, now: now)
  29. // run the import againt to check our deduplication logic
  30. try await importer.importGlucoseHistory(url: url, now: now)
  31. let allReadings = try await coreDataStack.fetchEntitiesAsync(
  32. ofType: GlucoseStored.self,
  33. onContext: context,
  34. predicate: NSPredicate(format: "TRUEPREDICATE"),
  35. key: "date",
  36. ascending: false
  37. ) as? [GlucoseStored] ?? []
  38. #expect(allReadings.count == 274)
  39. #expect(allReadings.first?.glucose == 115)
  40. #expect(allReadings.first?.date == Date("2025-04-28T19:32:51.727Z"))
  41. #expect(allReadings.last?.glucose == 127)
  42. #expect(allReadings.last?.date == Date("2025-04-27T19:37:50.327Z"))
  43. let manualCount = allReadings.filter({ $0.isManual }).count
  44. #expect(manualCount == 1)
  45. }
  46. @Test("Skip importing old glucose values") func testSkipImportOldGlucoseValues() async throws {
  47. let testBundle = Bundle(for: BundleReference.self)
  48. let path = testBundle.path(forResource: "glucose", ofType: "json")!
  49. let url = URL(filePath: path)
  50. // more than 24 hours in the future from the most recent entry
  51. let now = Date("2025-04-29T19:32:52.000Z")!
  52. try await importer.importGlucoseHistory(url: url, now: now)
  53. let allReadings = try await coreDataStack.fetchEntitiesAsync(
  54. ofType: GlucoseStored.self,
  55. onContext: context,
  56. predicate: NSPredicate(format: "TRUEPREDICATE"),
  57. key: "date",
  58. ascending: false
  59. ) as? [GlucoseStored] ?? []
  60. #expect(allReadings.isEmpty)
  61. }
  62. @Test("Import pump history with value checks") func testImportPumpHistoryDetails() async throws {
  63. let testBundle = Bundle(for: BundleReference.self)
  64. let path = testBundle.path(forResource: "pumphistory-24h-zoned", ofType: "json")!
  65. let url = URL(filePath: path)
  66. let now = Date("2025-04-29T01:33:58.000Z")!
  67. try await importer.importPumpHistory(url: url, now: now)
  68. // test out deduplication logic
  69. try await importer.importPumpHistory(url: url, now: now)
  70. let allReadings = try await coreDataStack.fetchEntitiesAsync(
  71. ofType: PumpEventStored.self,
  72. onContext: context,
  73. predicate: NSPredicate(format: "TRUEPREDICATE"),
  74. key: "timestamp",
  75. ascending: false
  76. ) as? [PumpEventStored] ?? []
  77. let objectIds = allReadings.map(\.objectID)
  78. let parsedHistory = OpenAPS.loadAndMapPumpEvents(objectIds, from: context)
  79. var bolusTotal = 0.0
  80. var bolusCount = 0
  81. var smbCount = 0
  82. var rateTotal = 0.0
  83. var tempBasalCount = 0
  84. var durationTotal = 0
  85. var suspendCount = 0
  86. var resumeCount = 0
  87. for event in parsedHistory {
  88. switch event {
  89. case let .bolus(bolus):
  90. bolusTotal += bolus.amount
  91. bolusCount += 1
  92. if bolus.isSMB {
  93. smbCount += 1
  94. }
  95. case let .tempBasal(tempBasal):
  96. rateTotal += tempBasal.rate
  97. tempBasalCount += 1
  98. case let .tempBasalDuration(tempBasalDuration):
  99. durationTotal += tempBasalDuration.duration
  100. case .suspend:
  101. suspendCount += 1
  102. case .resume:
  103. resumeCount += 1
  104. default:
  105. fatalError("unhandled pump event")
  106. }
  107. }
  108. // see the scripts/pump-history-stats.py file for where these come from
  109. #expect(parsedHistory.count == 77)
  110. #expect(bolusCount == 23)
  111. #expect(smbCount == 21)
  112. #expect(bolusTotal.isApproximatelyEqual(to: 8.1, epsilon: 0.01))
  113. #expect(tempBasalCount == 26)
  114. #expect(rateTotal.isApproximatelyEqual(to: 20.08, epsilon: 0.001))
  115. #expect(durationTotal == 900)
  116. #expect(suspendCount == 1)
  117. #expect(resumeCount == 1)
  118. }
  119. @Test("Skipping old pump history entries") func testSkipOldPumpHistoryEntries() async throws {
  120. let testBundle = Bundle(for: BundleReference.self)
  121. let path = testBundle.path(forResource: "pumphistory-24h-zoned", ofType: "json")!
  122. let url = URL(filePath: path)
  123. let now = Date("2025-04-30T01:33:58.000Z")!
  124. try await importer.importPumpHistory(url: url, now: now)
  125. let allReadings = try await coreDataStack.fetchEntitiesAsync(
  126. ofType: PumpEventStored.self,
  127. onContext: context,
  128. predicate: NSPredicate(format: "TRUEPREDICATE"),
  129. key: "timestamp",
  130. ascending: false
  131. ) as? [PumpEventStored] ?? []
  132. #expect(allReadings.isEmpty)
  133. }
  134. @Test("Import carb history with value checks") func testImportCarbHistoryDetails() async throws {
  135. let testBundle = Bundle(for: BundleReference.self)
  136. let path = testBundle.path(forResource: "carbhistory", ofType: "json")!
  137. let url = URL(filePath: path)
  138. let now = Date("2025-04-28T19:32:52.000Z")!
  139. try await importer.importCarbHistory(url: url, now: now)
  140. // run the import againt to check our deduplication logic
  141. try await importer.importCarbHistory(url: url, now: now)
  142. let allCarbEntries = try await coreDataStack.fetchEntitiesAsync(
  143. ofType: CarbEntryStored.self,
  144. onContext: context,
  145. predicate: NSPredicate(format: "TRUEPREDICATE"),
  146. key: "date",
  147. ascending: false
  148. ) as? [CarbEntryStored] ?? []
  149. #expect(allCarbEntries.count == 8)
  150. #expect(allCarbEntries.first?.carbs == 10)
  151. #expect(allCarbEntries.first?.note == "Snack 🍪")
  152. #expect(allCarbEntries.first?.date == Date("2025-04-28T18:36:06.968Z"))
  153. #expect(allCarbEntries.last?.carbs == 25)
  154. #expect(allCarbEntries.last?.date == Date("2025-04-28T05:03:43.332Z"))
  155. }
  156. @Test("Skip importing old carb entries") func testSkipImportOldCarbEntries() async throws {
  157. let testBundle = Bundle(for: BundleReference.self)
  158. let path = testBundle.path(forResource: "carbhistory", ofType: "json")!
  159. let url = URL(filePath: path)
  160. // more than 24 hours in the future from the most recent entry
  161. let now = Date("2025-04-29T19:32:52.000Z")!
  162. try await importer.importCarbHistory(url: url, now: now)
  163. let allCarbEntries = try await coreDataStack.fetchEntitiesAsync(
  164. ofType: CarbEntryStored.self,
  165. onContext: context,
  166. predicate: NSPredicate(format: "TRUEPREDICATE"),
  167. key: "date",
  168. ascending: false
  169. ) as? [CarbEntryStored] ?? []
  170. #expect(allCarbEntries.isEmpty)
  171. }
  172. }
  173. extension Double {
  174. func isApproximatelyEqual(to other: Double, epsilon: Double?) -> Bool {
  175. // If no epsilon provided, require exact match
  176. guard let epsilon = epsilon else {
  177. return self == other
  178. }
  179. // Handle exact equality
  180. if self == other {
  181. return true
  182. }
  183. // Handle infinity and NaN
  184. if isInfinite || other.isInfinite || isNaN || other.isNaN {
  185. return self == other
  186. }
  187. // For values, use simple absolute difference
  188. return abs(self - other) <= epsilon
  189. }
  190. }