PumpHistoryStorageTests.swift 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375
  1. import CoreData
  2. import Foundation
  3. import Swinject
  4. import Testing
  5. @testable import LoopKit
  6. @testable import Trio
  7. @Suite(.serialized) struct PumpHistoryStorageTests: Injectable {
  8. @Injected() var storage: PumpHistoryStorage!
  9. let resolver: Resolver
  10. let coreDataStack = CoreDataStack.createForTests()
  11. let testContext: NSManagedObjectContext
  12. typealias PumpEvent = PumpEventStored.EventType
  13. init() {
  14. // Create test context
  15. // As we are only using this single test context to initialize our in-memory PumpHistoryStorage we need to perform the Unit Tests serialized
  16. // TODO: is this really correct or does PersistentHistoryTracking also work in this in memory Coredata stack. This would allow me to use a single test context per test and perform the tests in parallel!
  17. testContext = coreDataStack.newTaskContext()
  18. // Create assembler with test assembly
  19. let assembler = Assembler([
  20. StorageAssembly(),
  21. ServiceAssembly(),
  22. APSAssembly(),
  23. NetworkAssembly(),
  24. UIAssembly(),
  25. SecurityAssembly(),
  26. TestAssembly(testContext: testContext) // Add our test assembly last to override PumpHistoryStorage
  27. ])
  28. resolver = assembler.resolver
  29. injectServices(resolver)
  30. }
  31. @Test("Storage is correctly initialized") func testStorageInitialization() {
  32. // Verify storage exists
  33. #expect(storage != nil, "PumpHistoryStorage should be injected")
  34. // Verify it's the correct type
  35. #expect(
  36. storage is BasePumpHistoryStorage, "Storage should be of type BasePumpHistoryStorage"
  37. )
  38. // Verify we can access the update publisher
  39. #expect(storage.updatePublisher != nil, "Update publisher should be available")
  40. }
  41. @Test("Test read and delete using generic CoreDataStack functions") func testFetchAndDeletePumpEvents() async throws {
  42. // Given
  43. let date = Date()
  44. // Insert mock entry
  45. let events: [LoopKit.NewPumpEvent] = [
  46. LoopKit.NewPumpEvent(
  47. date: date,
  48. dose: LoopKit.DoseEntry(
  49. type: .bolus,
  50. startDate: date,
  51. value: 0.5,
  52. unit: .units,
  53. deliveredUnits: nil,
  54. description: nil,
  55. syncIdentifier: nil,
  56. scheduledBasalRate: nil,
  57. insulinType: .lyumjev,
  58. automatic: false,
  59. manuallyEntered: false,
  60. isMutable: false
  61. ),
  62. raw: Data(),
  63. title: "Test Bolus for Fetch",
  64. type: .bolus
  65. )
  66. ]
  67. // Store test event and wait for storage to complete the task
  68. await storage.storePumpEvents(events)
  69. // try await Task.sleep(nanoseconds: 1_000_000_000)
  70. // When - Fetch events with our generic fetch function
  71. let fetchedEvents = await coreDataStack.fetchEntitiesAsync(
  72. ofType: PumpEventStored.self,
  73. onContext: testContext,
  74. predicate: NSPredicate(
  75. format: "type == %@ AND timestamp == %@",
  76. PumpEvent.bolus.rawValue,
  77. date as NSDate
  78. ),
  79. key: "timestamp",
  80. ascending: false
  81. )
  82. guard let fetchedEvents = fetchedEvents as? [PumpEventStored] else { return }
  83. // Then
  84. #expect(fetchedEvents.count == 1, "Should have found exactly one event")
  85. let fetchedEvent = fetchedEvents.first
  86. #expect(fetchedEvent?.type == PumpEvent.bolus.rawValue, "Should be a bolus event")
  87. #expect(fetchedEvent?.bolus?.amount as? Decimal == 0.5, "Bolus amount should be 0.5")
  88. #expect(
  89. abs(fetchedEvent?.timestamp?.timeIntervalSince(date) ?? 1) < 1,
  90. "Timestamp should match"
  91. )
  92. // When - Delete event
  93. if let fetchedEvent = fetchedEvent {
  94. await coreDataStack.deleteObject(identifiedBy: fetchedEvent.objectID)
  95. }
  96. // Then - Verify deletion
  97. let eventsAfterDeletion = await coreDataStack.fetchEntitiesAsync(
  98. ofType: PumpEventStored.self,
  99. onContext: testContext,
  100. predicate: NSPredicate(
  101. format: "type == %@ AND timestamp == %@",
  102. PumpEvent.bolus.rawValue,
  103. date as NSDate
  104. ),
  105. key: "timestamp",
  106. ascending: false
  107. )
  108. guard let eventsAfterDeletion = eventsAfterDeletion as? [PumpEventStored] else { return }
  109. #expect(eventsAfterDeletion.isEmpty, "Should have no events after deletion")
  110. }
  111. @Test("Test store function in PumpHistoryStorage") func testStorePumpEvents() async throws {
  112. // Given
  113. let date = Date()
  114. let tenMinAgo = date.addingTimeInterval(-10.minutes.timeInterval)
  115. let halfHourInFuture = date.addingTimeInterval(30.minutes.timeInterval)
  116. // Get initial entries to compare to final entries later
  117. let initialEntries = try await testContext.perform {
  118. try testContext.fetch(PumpEventStored.fetchRequest())
  119. }
  120. // Create 2 test events, 1 bolus + 1 temp basal event
  121. let events: [LoopKit.NewPumpEvent] = [
  122. // SMB
  123. LoopKit.NewPumpEvent(
  124. date: tenMinAgo,
  125. dose: LoopKit.DoseEntry(
  126. type: .bolus,
  127. startDate: tenMinAgo,
  128. value: 0.4,
  129. unit: .units,
  130. deliveredUnits: nil,
  131. description: nil,
  132. syncIdentifier: nil,
  133. scheduledBasalRate: nil,
  134. insulinType: .lyumjev,
  135. automatic: true,
  136. manuallyEntered: false,
  137. isMutable: false
  138. ),
  139. raw: Data(),
  140. title: "Test Bolus",
  141. type: .bolus
  142. ),
  143. // Temp Basal event
  144. LoopKit.NewPumpEvent(
  145. date: date,
  146. dose: LoopKit.DoseEntry(
  147. type: .tempBasal,
  148. startDate: date,
  149. endDate: halfHourInFuture,
  150. value: 1.2,
  151. unit: .unitsPerHour,
  152. deliveredUnits: nil,
  153. description: nil,
  154. syncIdentifier: nil,
  155. scheduledBasalRate: nil,
  156. insulinType: .lyumjev,
  157. automatic: true,
  158. manuallyEntered: false,
  159. isMutable: true
  160. ),
  161. raw: Data(),
  162. title: "Test Temp Basal",
  163. type: .tempBasal
  164. )
  165. ]
  166. // When
  167. // Store in our in-memory PumphistoryStorage
  168. await storage.storePumpEvents(events)
  169. // Wait for the events to be stored
  170. // try await Task.sleep(nanoseconds: 1_000_000_000)
  171. // Then
  172. // Fetch all events after storing
  173. let finalEntries = try await testContext.perform {
  174. try testContext.fetch(PumpEventStored.fetchRequest())
  175. }
  176. // Verify there were no initial entries
  177. #expect(initialEntries.isEmpty, "There should be no initial entries")
  178. // Verify count increased by 2
  179. #expect(finalEntries.count == initialEntries.count + 2, "Should have added 2 new events")
  180. // Verify bolus event
  181. let bolusEvent = finalEntries.first {
  182. $0.type == PumpEvent.bolus.rawValue &&
  183. abs($0.timestamp?.timeIntervalSince(tenMinAgo) ?? 1) < 1
  184. }
  185. #expect(bolusEvent != nil, "Should have found bolus event")
  186. #expect(bolusEvent?.bolus?.amount as? Decimal == 0.4, "Bolus amount should be 0.4")
  187. #expect(bolusEvent?.isUploadedToNS == false, "Should not be uploaded to NS")
  188. #expect(bolusEvent?.isUploadedToHealth == false, "Should not be uploaded to Health")
  189. #expect(bolusEvent?.isUploadedToTidepool == false, "Should not be uploaded to Tidepool")
  190. #expect(bolusEvent?.bolus?.isSMB == true, "Should be a SMB")
  191. #expect(bolusEvent?.bolus?.isExternal == false, "Should not be external insulin")
  192. // Verify temp basal event
  193. let tempBasalEvent = finalEntries.first {
  194. $0.type == PumpEvent.tempBasal.rawValue &&
  195. abs($0.timestamp?.timeIntervalSince(date) ?? 1) < 1
  196. }
  197. #expect(tempBasalEvent != nil, "Should have found temp basal event")
  198. #expect(tempBasalEvent?.tempBasal?.rate as? Decimal == 1.2, "Temp basal rate should be 1.2")
  199. #expect(tempBasalEvent?.tempBasal?.duration == 30, "Temp basal duration should be 30 minutes")
  200. #expect(tempBasalEvent?.isUploadedToNS == false, "Should not be uploaded to NS")
  201. #expect(tempBasalEvent?.isUploadedToHealth == false, "Should not be uploaded to Health")
  202. #expect(bolusEvent?.isUploadedToTidepool == false, "Should not be uploaded to Tidepool")
  203. }
  204. @Test("Test store function for manual boluses") func testStorePumpEventsWithManualBoluses() async throws {
  205. // Given
  206. let date = Date()
  207. // Insert mock entry
  208. let events: [LoopKit.NewPumpEvent] = [
  209. LoopKit.NewPumpEvent(
  210. date: date,
  211. dose: LoopKit.DoseEntry(
  212. type: .bolus,
  213. startDate: date,
  214. value: 4,
  215. unit: .units,
  216. deliveredUnits: nil,
  217. description: nil,
  218. syncIdentifier: nil,
  219. scheduledBasalRate: nil,
  220. insulinType: .lyumjev,
  221. automatic: false,
  222. manuallyEntered: true,
  223. isMutable: false
  224. ),
  225. raw: Data(),
  226. title: "Test Bolus",
  227. type: .bolus
  228. )
  229. ]
  230. // Store test event and wait for storage to complete the task
  231. await storage.storePumpEvents(events)
  232. // try await Task.sleep(nanoseconds: 1_000_000_000)
  233. // When - Fetch events with our generic fetch function
  234. let fetchedEvents = await coreDataStack.fetchEntitiesAsync(
  235. ofType: PumpEventStored.self,
  236. onContext: testContext,
  237. predicate: NSPredicate(
  238. format: "type == %@ AND timestamp == %@",
  239. PumpEvent.bolus.rawValue,
  240. date as NSDate
  241. ),
  242. key: "timestamp",
  243. ascending: false
  244. )
  245. guard let fetchedEvents = fetchedEvents as? [PumpEventStored] else { return }
  246. // Then
  247. #expect(fetchedEvents.count == 1, "Should have found exactly one event")
  248. let fetchedEvent = fetchedEvents.first
  249. #expect(fetchedEvent?.type == PumpEvent.bolus.rawValue, "Should be a bolus event")
  250. #expect(fetchedEvent?.bolus?.amount as? Decimal == 4, "Bolus amount should be 4 U")
  251. #expect(
  252. abs(fetchedEvent?.timestamp?.timeIntervalSince(date) ?? 1) < 1,
  253. "Timestamp should match"
  254. )
  255. #expect(fetchedEvent?.bolus?.isSMB == false, "Should not be a SMB")
  256. // TODO: - check this
  257. #expect(fetchedEvent?.bolus?.isExternal == false, "Should not be external Insulin")
  258. #expect(fetchedEvent?.isUploadedToNS == false, "Should not be uploaded to NS")
  259. #expect(fetchedEvent?.isUploadedToHealth == false, "Should not be uploaded to Health")
  260. #expect(fetchedEvent?.isUploadedToTidepool == false, "Should not be uploaded to Tidepool")
  261. }
  262. @Test("Measure performance of PumpHistory storage operations") func testStoragePerformance() async throws {
  263. // STEP 1: Setup test data
  264. let date = Date()
  265. let amount: Decimal = 4.0
  266. let events = [
  267. NewPumpEvent(
  268. date: date,
  269. dose: DoseEntry(
  270. type: .bolus,
  271. startDate: date,
  272. endDate: date.addingTimeInterval(1),
  273. value: Double(amount),
  274. unit: .units,
  275. deliveredUnits: Double(amount),
  276. description: nil,
  277. syncIdentifier: "test_bolus_1",
  278. scheduledBasalRate: nil,
  279. insulinType: .lyumjev,
  280. automatic: false,
  281. manuallyEntered: true,
  282. isMutable: false
  283. ),
  284. raw: Data(),
  285. title: "Test Bolus",
  286. type: .bolus
  287. )
  288. ]
  289. // STEP 2: Test storePumpEvents performance
  290. let storeStartTime = CFAbsoluteTimeGetCurrent()
  291. await storage.storePumpEvents(events)
  292. let storeTime = CFAbsoluteTimeGetCurrent() - storeStartTime
  293. debug(.default, "storePumpEvents time: \(String(format: "%.4f", storeTime)) seconds")
  294. // STEP 3: Test Nightscout upload fetch performance
  295. let nsStartTime = CFAbsoluteTimeGetCurrent()
  296. let nsEvents = await storage.getPumpHistoryNotYetUploadedToNightscout()
  297. let nsTime = CFAbsoluteTimeGetCurrent() - nsStartTime
  298. debug(.default, "Nightscout fetch time: \(String(format: "%.4f", nsTime)) seconds")
  299. // STEP 4: Test HealthKit upload fetch performance
  300. let healthStartTime = CFAbsoluteTimeGetCurrent()
  301. let healthEvents = await storage.getPumpHistoryNotYetUploadedToHealth()
  302. let healthTime = CFAbsoluteTimeGetCurrent() - healthStartTime
  303. debug(.default, "HealthKit fetch time: \(String(format: "%.4f", healthTime)) seconds")
  304. // STEP 5: Test Tidepool upload fetch performance
  305. let tidepoolStartTime = CFAbsoluteTimeGetCurrent()
  306. let tidepoolEvents = await storage.getPumpHistoryNotYetUploadedToTidepool()
  307. let tidepoolTime = CFAbsoluteTimeGetCurrent() - tidepoolStartTime
  308. debug(.default, "Tidepool fetch time: \(String(format: "%.4f", tidepoolTime)) seconds")
  309. // Performance expectations
  310. #expect(storeTime < 0.1, "Storing events should take less than 0.1 seconds")
  311. #expect(nsTime < 0.01, "Fetching Nightscout events should take less than 0.05 seconds")
  312. #expect(healthTime < 0.01, "Fetching HealthKit events should take less than 0.05 seconds")
  313. #expect(tidepoolTime < 0.01, "Fetching Tidepool events should take less than 0.05 seconds")
  314. // Log each total time
  315. debug(.default, "Total storePumpEvents time: \(String(format: "%.4f", storeTime)) seconds")
  316. debug(.default, "Total Nightscout fetch time: \(String(format: "%.4f", nsTime)) seconds")
  317. debug(.default, "Total HealthKit fetch time: \(String(format: "%.4f", healthTime)) seconds")
  318. debug(.default, "Total Tidepool fetch time: \(String(format: "%.4f", tidepoolTime)) seconds")
  319. // Verify data integrity
  320. #expect(!nsEvents.isEmpty, "Should have found event for Nightscout")
  321. #expect(!healthEvents.isEmpty, "Should have found event for HealthKit")
  322. #expect(!tidepoolEvents.isEmpty, "Should have found event for Tidepool")
  323. }
  324. }