DoseStoreTests.swift 81 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476
  1. //
  2. // DoseStoreTests.swift
  3. // LoopKit
  4. //
  5. // Copyright © 2017 LoopKit Authors. All rights reserved.
  6. //
  7. import XCTest
  8. import CoreData
  9. import HealthKit
  10. @testable import LoopKit
  11. class DoseStoreTests: PersistenceControllerTestCase {
  12. func loadReservoirFixture(_ resourceName: String) -> [NewReservoirValue] {
  13. let fixture: [JSONDictionary] = loadFixture(resourceName)
  14. let dateFormatter = ISO8601DateFormatter.localTimeDate(timeZone: .utcTimeZone)
  15. return fixture.map {
  16. return NewReservoirValue(startDate: dateFormatter.date(from: $0["date"] as! String)!, unitVolume: $0["amount"] as! Double)
  17. }
  18. }
  19. func defaultStore(testingDate: Date? = nil) -> DoseStore {
  20. let healthStore = HKHealthStoreMock()
  21. return DoseStore(
  22. healthStore: healthStore,
  23. cacheStore: cacheStore,
  24. observationEnabled: false,
  25. insulinModelProvider: StaticInsulinModelProvider(WalshInsulinModel(actionDuration: .hours(4))),
  26. longestEffectDuration: .hours(4),
  27. basalProfile: BasalRateSchedule(rawValue: ["timeZone": -28800, "items": [["value": 0.75, "startTime": 0.0], ["value": 0.8, "startTime": 10800.0], ["value": 0.85, "startTime": 32400.0], ["value": 1.0, "startTime": 68400.0]]]),
  28. insulinSensitivitySchedule: InsulinSensitivitySchedule(rawValue: ["unit": "mg/dL", "timeZone": -28800, "items": [["value": 40.0, "startTime": 0.0], ["value": 35.0, "startTime": 21600.0], ["value": 40.0, "startTime": 57600.0]]]),
  29. syncVersion: 1,
  30. provenanceIdentifier: Bundle.main.bundleIdentifier!,
  31. test_currentDate: testingDate
  32. )
  33. }
  34. let testingDateFormatter = DateFormatter.descriptionFormatter
  35. func testingDate(_ input: String) -> Date {
  36. return testingDateFormatter.date(from: input)!
  37. }
  38. func testEmptyDoseStoreReturnsZeroInsulinOnBoard() {
  39. let doseStore = defaultStore()
  40. let queryFinishedExpectation = expectation(description: "query finished")
  41. doseStore.insulinOnBoard(at: Date()) { (result) in
  42. switch result {
  43. case .failure(let error):
  44. XCTFail("Unexpected error: \(error)")
  45. case .success(let value):
  46. XCTAssertEqual(0, value.value)
  47. }
  48. queryFinishedExpectation.fulfill()
  49. }
  50. waitForExpectations(timeout: 3)
  51. }
  52. func testGetNormalizedDoseEntriesUsingReservoir() {
  53. let now = testingDate("2022-09-05 02:04:00 +0000")
  54. let doseStore = defaultStore(testingDate: now)
  55. let reservoirReadings = loadReservoirFixture("reservoir_iob_test")
  56. let storageExpectations = expectation(description: "reservoir store finished")
  57. storageExpectations.expectedFulfillmentCount = reservoirReadings.count + 1
  58. for reading in reservoirReadings.reversed() {
  59. doseStore.addReservoirValue(reading.unitVolume, at: reading.startDate) { _, _, _, _ in storageExpectations.fulfill() }
  60. }
  61. let bolusStart = testingDate("2022-09-05 01:49:47 +0000")
  62. let bolusEnd = testingDate("2022-09-05 01:51:19 +0000")
  63. let bolus = DoseEntry(type: .bolus, startDate: bolusStart, endDate: bolusEnd, value: 2.3, unit: .units, isMutable: true)
  64. let pumpEvent = NewPumpEvent(date: bolus.startDate, dose: bolus, raw: Data(hexadecimalString: "0000")!, title: "Bolus 2.3U")
  65. doseStore.addPumpEvents([pumpEvent], lastReconciliation: testingDate("2022-09-05 01:50:18 +0000")) { error in
  66. storageExpectations.fulfill()
  67. }
  68. waitForExpectations(timeout: 2)
  69. let queryFinishedExpectation = expectation(description: "query finished")
  70. doseStore.insulinOnBoard(at: now) { (result) in
  71. switch result {
  72. case .failure(let error):
  73. XCTFail("Unexpected error: \(error)")
  74. case .success(let value):
  75. XCTAssertEqual(2.25, value.value, accuracy: 0.01)
  76. }
  77. queryFinishedExpectation.fulfill()
  78. }
  79. waitForExpectations(timeout: 3)
  80. }
  81. func testMutableDosesIncludedInIOB() {
  82. let now = testingDate("2023-01-08 17:11:14 +0000")
  83. let doseStore = defaultStore(testingDate: now)
  84. let reservoirReadings = loadReservoirFixture("reservoir_for_iob_missing")
  85. let storageExpectations = expectation(description: "reservoir store finished")
  86. storageExpectations.expectedFulfillmentCount = reservoirReadings.count + 1
  87. for reading in reservoirReadings.reversed() {
  88. doseStore.addReservoirValue(reading.unitVolume, at: reading.startDate) { _, _, _, _ in storageExpectations.fulfill() }
  89. }
  90. let lastReconciliation = testingDate("2023-01-08 17:08:27 +0000")
  91. // NewPumpEvent(date: 2023-01-08 17:04:58 +0000, dose: Optional(LoopKit.DoseEntry(type: LoopKit.DoseType.bolus, startDate: 2023-01-08 17:04:58 +0000, endDate: 2023-01-08 17:08:24 +0000, value: 5.15, unit: LoopKit.DoseUnit.units, deliveredUnits: Optional(5.15), description: nil, insulinType: Optional(LoopKit.InsulinType.novolog), automatic: Optional(false), manuallyEntered: false, syncIdentifier: Optional("464327afd390446786cced682f22448f"), isMutable: true, wasProgrammedByPumpUI: false, scheduledBasalRate: nil)), raw: 16 bytes, type: Optional(LoopKit.PumpEventType.bolus), title: "Bolus", alarmType: nil),
  92. // NewPumpEvent(date: 2023-01-08 17:02:35 +0000, dose: Optional(LoopKit.DoseEntry(type: LoopKit.DoseType.tempBasal, startDate: 2023-01-08 17:02:35 +0000, endDate: 2023-01-08 17:32:35 +0000, value: 0.575, unit: LoopKit.DoseUnit.unitsPerHour, deliveredUnits: nil, description: nil, insulinType: Optional(LoopKit.InsulinType.novolog), automatic: Optional(true), manuallyEntered: false, syncIdentifier: Optional("61487bd5d34f4ff49a7f0766066e7773"), isMutable: false, wasProgrammedByPumpUI: false, scheduledBasalRate: nil)), raw: 16 bytes, type: Optional(LoopKit.PumpEventType.tempBasal), title: "Temp Basal", alarmType: nil)]
  93. let bolusStart = testingDate("2023-01-08 17:04:58 +0000")
  94. let bolusEnd = testingDate("2023-01-08 17:08:24 +0000")
  95. let bolus = DoseEntry(type: .bolus, startDate: bolusStart, endDate: bolusEnd, value: 5.15, unit: .units, isMutable: true)
  96. let tempBasalStart = testingDate("2023-01-08 17:02:35 +0000")
  97. let tempBasalEnd = testingDate("2023-01-08 17:32:35 +0000")
  98. let tempBasal = DoseEntry(type: .tempBasal, startDate: tempBasalStart, endDate: tempBasalEnd, value:0.575, unit: .unitsPerHour, isMutable: false)
  99. let pumpEvents: [NewPumpEvent] = [
  100. NewPumpEvent(date: bolus.startDate, dose: bolus, raw: Data(hexadecimalString: "0000")!, title: "Bolus 5.15U"),
  101. NewPumpEvent(date: tempBasal.startDate, dose: tempBasal, raw: Data(hexadecimalString: "0001")!, title: "TempBasal 0.575 U/hr")
  102. ]
  103. doseStore.addPumpEvents(pumpEvents, lastReconciliation: lastReconciliation) { error in
  104. storageExpectations.fulfill()
  105. }
  106. waitForExpectations(timeout: 2)
  107. let queryFinishedExpectation = expectation(description: "query finished")
  108. doseStore.insulinOnBoard(at: now) { (result) in
  109. switch result {
  110. case .failure(let error):
  111. XCTFail("Unexpected error: \(error)")
  112. case .success(let value):
  113. XCTAssertEqual(5.07, value.value, accuracy: 0.01)
  114. }
  115. queryFinishedExpectation.fulfill()
  116. }
  117. waitForExpectations(timeout: 3)
  118. }
  119. func testPumpEventTypeDoseMigration() {
  120. cacheStore.managedObjectContext.performAndWait {
  121. let event = PumpEvent(entity: PumpEvent.entity(), insertInto: cacheStore.managedObjectContext)
  122. event.date = Date()
  123. event.duration = .minutes(30)
  124. event.unit = .unitsPerHour
  125. event.type = .tempBasal
  126. event.value = 0.5
  127. event.doseType = nil
  128. XCTAssertNotNil(event.dose)
  129. XCTAssertEqual(.tempBasal, event.dose!.type)
  130. }
  131. }
  132. func testDeduplication() {
  133. cacheStore.managedObjectContext.performAndWait {
  134. let bolus1 = PumpEvent(context: cacheStore.managedObjectContext)
  135. bolus1.date = DateFormatter.descriptionFormatter.date(from: "2018-04-30 02:12:42 +0000")
  136. bolus1.raw = Data(hexadecimalString: "0100a600a6001b006a0c335d12")!
  137. bolus1.type = PumpEventType.bolus
  138. bolus1.dose = DoseEntry(type: .bolus, startDate: bolus1.date!, value: 4.15, unit: .units, syncIdentifier: bolus1.raw?.hexadecimalString)
  139. let bolus2 = PumpEvent(context: cacheStore.managedObjectContext)
  140. bolus2.date = DateFormatter.descriptionFormatter.date(from: "2018-04-30 00:00:00 +0000")
  141. bolus2.raw = Data(hexadecimalString: "0100a600a6001b006a0c335d12")!
  142. bolus2.type = PumpEventType.bolus
  143. bolus2.dose = DoseEntry(type: .bolus, startDate: bolus2.date!, value: 0.15, unit: .units, syncIdentifier: bolus1.raw?.hexadecimalString)
  144. let request: NSFetchRequest<PumpEvent> = PumpEvent.fetchRequest()
  145. let eventsBeforeSave = try! cacheStore.managedObjectContext.fetch(request)
  146. XCTAssertEqual(2, eventsBeforeSave.count)
  147. try! cacheStore.managedObjectContext.save()
  148. let eventsAfterSave = try! cacheStore.managedObjectContext.fetch(request)
  149. XCTAssertEqual(1, eventsAfterSave.count)
  150. }
  151. }
  152. /// See https://github.com/LoopKit/Loop/issues/853
  153. func testOutOfOrderDosesSyncedToHealth() {
  154. let formatter = DateFormatter.descriptionFormatter
  155. let f = { (input) in
  156. return formatter.date(from: input)!
  157. }
  158. // 1. Create a DoseStore
  159. let healthStore = HKHealthStoreMock()
  160. let doseStore = DoseStore(
  161. healthStore: healthStore,
  162. cacheStore: cacheStore,
  163. observationEnabled: false,
  164. insulinModelProvider: StaticInsulinModelProvider(WalshInsulinModel(actionDuration: .hours(4))),
  165. longestEffectDuration: .hours(4),
  166. basalProfile: BasalRateSchedule(rawValue: ["timeZone": -28800, "items": [["value": 0.75, "startTime": 0.0], ["value": 0.8, "startTime": 10800.0], ["value": 0.85, "startTime": 32400.0], ["value": 1.0, "startTime": 68400.0]]]),
  167. insulinSensitivitySchedule: InsulinSensitivitySchedule(rawValue: ["unit": "mg/dL", "timeZone": -28800, "items": [["value": 40.0, "startTime": 0.0], ["value": 35.0, "startTime": 21600.0], ["value": 40.0, "startTime": 57600.0]]]),
  168. syncVersion: 1,
  169. provenanceIdentifier: Bundle.main.bundleIdentifier!,
  170. // Set the current date
  171. test_currentDate: f("2018-12-12 18:07:14 +0000")
  172. )
  173. // 2. Add a temp basal which has already ended. It should be saved to Health
  174. let pumpEvents1 = [
  175. NewPumpEvent(date: f("2018-12-12 17:35:58 +0000"), dose: nil, raw: UUID().data, title: "TempBasalPumpEvent(length: 8, rawData: 8 bytes, rateType: MinimedKit.TempBasalPumpEvent.RateType.Absolute, rate: 2.125, timestamp: calendar: gregorian (fixed) year: 2018 month: 12 day: 12 hour: 9 minute: 35 second: 58 isLeapMonth: false )", type: nil),
  176. NewPumpEvent(date: f("2018-12-12 17:35:58 +0000"), dose: DoseEntry(type: .tempBasal, startDate: f("2018-12-12 17:35:58 +0000"), endDate: f("2018-12-12 18:05:58 +0000"), value: 2.125, unit: .unitsPerHour), raw: Data(hexadecimalString: "1601fa23094c12")!, title: "TempBasalDurationPumpEvent(length: 7, rawData: 7 bytes, duration: 30, timestamp: calendar: gregorian (fixed) year: 2018 month: 12 day: 12 hour: 9 minute: 35 second: 58 isLeapMonth: false )", type: .tempBasal)
  177. ]
  178. doseStore.insulinDeliveryStore.test_lastImmutableBasalEndDate = f("2018-12-12 17:35:58 +0000")
  179. let addPumpEvents1 = expectation(description: "add pumpEvents1")
  180. addPumpEvents1.expectedFulfillmentCount = 2
  181. healthStore.setSaveHandler({ (objects, success, error) in
  182. XCTAssertEqual(1, objects.count)
  183. let sample = objects.first as! HKQuantitySample
  184. XCTAssertEqual(HKInsulinDeliveryReason.basal, sample.insulinDeliveryReason)
  185. XCTAssertNil(error)
  186. addPumpEvents1.fulfill()
  187. })
  188. let lastBasalEndDateSetExpectation = expectation(description: "last basal end date set")
  189. lastBasalEndDateSetExpectation.assertForOverFulfill = false
  190. doseStore.insulinDeliveryStore.test_lastImmutableBasalEndDateDidSet = {
  191. lastBasalEndDateSetExpectation.fulfill()
  192. }
  193. doseStore.addPumpEvents(pumpEvents1, lastReconciliation: Date()) { (error) in
  194. XCTAssertNil(error)
  195. addPumpEvents1.fulfill()
  196. }
  197. waitForExpectations(timeout: 3)
  198. XCTAssertEqual(f("2018-12-12 18:05:58 +0000"), doseStore.insulinDeliveryStore.test_lastImmutableBasalEndDate)
  199. // 3. Add a bolus a little later, which started before the last temp basal ends, but wasn't written to pump history until it completed (x22 pump behavior)
  200. // Even though it is before lastBasalEndDate, it should be saved to HealthKit.
  201. doseStore.insulinDeliveryStore.test_currentDate = f("2018-12-12 18:16:23 +0000")
  202. let pumpEvents2 = [
  203. NewPumpEvent(date: f("2018-12-12 18:05:14 +0000"), dose: DoseEntry(type: .bolus, startDate: f("2018-12-12 18:05:14 +0000"), endDate: f("2018-12-12 18:05:14 +0000"), value: 5.0, unit: .units), raw: Data(hexadecimalString: "01323200ce052a0c12")!, title: "BolusNormalPumpEvent(length: 9, rawData: 9 bytes, timestamp: calendar: gregorian (fixed) year: 2018 month: 12 day: 12 hour: 10 minute: 5 second: 14 isLeapMonth: false , unabsorbedInsulinRecord: nil, amount: 5.0, programmed: 5.0, unabsorbedInsulinTotal: 0.0, type: MinimedKit.BolusNormalPumpEvent.BolusType.normal, duration: 0.0, deliveryUnitsPerMinute: 1.5)", type: .bolus)
  204. ]
  205. let addPumpEvents2 = expectation(description: "add pumpEvents2")
  206. addPumpEvents2.expectedFulfillmentCount = 3
  207. healthStore.setSaveHandler({ (objects, success, error) in
  208. XCTAssertEqual(1, objects.count)
  209. let sample = objects.first as! HKQuantitySample
  210. XCTAssertEqual(HKInsulinDeliveryReason.bolus, sample.insulinDeliveryReason)
  211. XCTAssertEqual(5.0, sample.quantity.doubleValue(for: .internationalUnit()))
  212. XCTAssertEqual(f("2018-12-12 18:05:14 +0000"), sample.startDate)
  213. XCTAssertNil(error)
  214. addPumpEvents2.fulfill()
  215. })
  216. doseStore.insulinDeliveryStore.test_lastImmutableBasalEndDateDidSet = {
  217. addPumpEvents2.fulfill()
  218. }
  219. doseStore.addPumpEvents(pumpEvents2, lastReconciliation: Date()) { (error) in
  220. XCTAssertNil(error)
  221. addPumpEvents2.fulfill()
  222. }
  223. waitForExpectations(timeout: 3)
  224. XCTAssertEqual(f("2018-12-12 18:05:58 +0000"), doseStore.insulinDeliveryStore.test_lastImmutableBasalEndDate)
  225. // Add the next set of pump events, which haven't completed and shouldn't be saved to HealthKit
  226. doseStore.insulinDeliveryStore.test_currentDate = f("2018-12-12 18:21:22 +0000")
  227. let pumpEvents3 = [
  228. NewPumpEvent(date: f("2018-12-12 18:16:31 +0000"), dose: nil, raw: UUID().data, title: "TempBasalPumpEvent(length: 8, rawData: 8 bytes, rateType: MinimedKit.TempBasalPumpEvent.RateType.Absolute, rate: 0.0, timestamp: calendar: gregorian (fixed) year: 2018 month: 12 day: 12 hour: 10 minute: 16 second: 31 isLeapMonth: false )", type: nil),
  229. NewPumpEvent(date: f("2018-12-12 18:16:31 +0000"), dose: DoseEntry(type: .tempBasal, startDate: f("2018-12-12 18:16:31 +0000"), endDate: f("2018-12-12 18:46:31 +0000"), value: 0.0, unit: .unitsPerHour), raw: Data(hexadecimalString: "1601df100a4c12")!, title: "TempBasalDurationPumpEvent(length: 7, rawData: 7 bytes, duration: 30, timestamp: calendar: gregorian (fixed) year: 2018 month: 12 day: 12 hour: 10 minute: 16 second: 31 isLeapMonth: false )", type: .tempBasal),
  230. ]
  231. let addPumpEvents3 = expectation(description: "add pumpEvents3")
  232. addPumpEvents3.expectedFulfillmentCount = 1
  233. healthStore.setSaveHandler({ (objects, success, error) in
  234. XCTFail()
  235. })
  236. doseStore.insulinDeliveryStore.test_lastImmutableBasalEndDateDidSet = nil
  237. doseStore.addPumpEvents(pumpEvents3, lastReconciliation: Date()) { (error) in
  238. XCTAssertNil(error)
  239. addPumpEvents3.fulfill()
  240. }
  241. waitForExpectations(timeout: 3)
  242. XCTAssertEqual(f("2018-12-12 18:05:58 +0000"), doseStore.insulinDeliveryStore.test_lastImmutableBasalEndDate)
  243. }
  244. /// https://github.com/LoopKit/Loop/issues/852
  245. func testSplitBasalsSyncedToHealth() {
  246. let formatter = DateFormatter.descriptionFormatter
  247. let f = { (input) in
  248. return formatter.date(from: input)!
  249. }
  250. // Create a DoseStore
  251. let healthStore = HKHealthStoreMock()
  252. let doseStore = DoseStore(
  253. healthStore: healthStore,
  254. cacheStore: cacheStore,
  255. observationEnabled: false,
  256. insulinModelProvider: StaticInsulinModelProvider(WalshInsulinModel(actionDuration: .hours(4))),
  257. longestEffectDuration: .hours(4),
  258. basalProfile: BasalRateSchedule(rawValue: ["timeZone": -28800, "items": [["value": 0.75, "startTime": 0.0], ["value": 0.8, "startTime": 10800.0], ["value": 0.85, "startTime": 32400.0], ["value": 1.0, "startTime": 68400.0]]]),
  259. insulinSensitivitySchedule: InsulinSensitivitySchedule(rawValue: ["unit": "mg/dL", "timeZone": -28800, "items": [["value": 40.0, "startTime": 0.0], ["value": 35.0, "startTime": 21600.0], ["value": 40.0, "startTime": 57600.0]]]),
  260. syncVersion: 1,
  261. provenanceIdentifier: Bundle.main.bundleIdentifier!,
  262. // Set the current date (5 minutes later)
  263. test_currentDate: f("2018-11-29 11:04:27 +0000")
  264. )
  265. doseStore.pumpRecordsBasalProfileStartEvents = false
  266. doseStore.insulinDeliveryStore.test_lastImmutableBasalEndDate = f("2018-11-29 10:54:28 +0000")
  267. // Add a temp basal. It hasn't finished yet, and should not be saved to Health
  268. let pumpEvents1 = [
  269. NewPumpEvent(date: f("2018-11-29 10:59:28 +0000"), dose: nil, raw: UUID().data, title: "TempBasalPumpEvent(length: 8, rawData: 8 bytes, rateType: MinimedKit.TempBasalPumpEvent.RateType.Absolute, rate: 0.3, timestamp: calendar: gregorian (fixed) year: 2018 month: 11 day: 29 hour: 2 minute: 59 second: 28 isLeapMonth: false )", type: nil),
  270. NewPumpEvent(date: f("2018-11-29 10:59:28 +0000"), dose: DoseEntry(type: .tempBasal, startDate: f("2018-11-29 10:59:28 +0000"), endDate: f("2018-11-29 11:29:28 +0000"), value: 0.3, unit: .unitsPerHour), raw: Data(hexadecimalString: "5bffc7cace53e48e87f7cfcb")!, title: "TempBasalDurationPumpEvent(length: 7, rawData: 7 bytes, duration: 30, timestamp: calendar: gregorian (fixed) year: 2018 month: 11 day: 29 hour: 2 minute: 59 second: 28 isLeapMonth: false )", type: .tempBasal)
  271. ]
  272. let addPumpEvents1 = expectation(description: "add pumpEvents1")
  273. addPumpEvents1.expectedFulfillmentCount = 1
  274. healthStore.setSaveHandler({ (objects, success, error) in
  275. XCTFail()
  276. })
  277. doseStore.addPumpEvents(pumpEvents1, lastReconciliation: Date()) { (error) in
  278. XCTAssertNil(error)
  279. addPumpEvents1.fulfill()
  280. }
  281. waitForExpectations(timeout: 3)
  282. XCTAssertEqual(f("2018-11-29 10:54:28 +0000"), doseStore.insulinDeliveryStore.test_lastImmutableBasalEndDate)
  283. XCTAssertEqual(f("2018-11-29 10:59:28 +0000"), doseStore.pumpEventQueryAfterDate)
  284. // Add the next query of the same pump events (no new data) 5 minutes later. Expect the same result
  285. doseStore.insulinDeliveryStore.test_currentDate = f("2018-11-29 11:09:27 +0000")
  286. let addPumpEvents2 = expectation(description: "add pumpEvents2")
  287. addPumpEvents2.expectedFulfillmentCount = 1
  288. healthStore.setSaveHandler({ (objects, success, error) in
  289. XCTFail()
  290. })
  291. doseStore.insulinDeliveryStore.test_lastImmutableBasalEndDateDidSet = {
  292. XCTFail()
  293. }
  294. doseStore.addPumpEvents(pumpEvents1, lastReconciliation: Date()) { (error) in
  295. XCTAssertNil(error)
  296. addPumpEvents2.fulfill()
  297. }
  298. waitForExpectations(timeout: 3)
  299. XCTAssertEqual(f("2018-11-29 10:54:28 +0000"), doseStore.insulinDeliveryStore.test_lastImmutableBasalEndDate)
  300. XCTAssertEqual(f("2018-11-29 10:59:28 +0000"), doseStore.pumpEventQueryAfterDate)
  301. // Add the next set of pump events, including the last temp basal change.
  302. // The previous, completed basal entries should be saved to Health
  303. doseStore.insulinDeliveryStore.test_currentDate = f("2018-11-29 11:14:28 +0000")
  304. let pumpEvents3 = [
  305. NewPumpEvent(date: f("2018-11-29 11:09:27 +0000"), dose: nil, raw: UUID().data, title: "TempBasalPumpEvent(length: 8, rawData: 8 bytes, rateType: MinimedKit.TempBasalPumpEvent.RateType.Absolute, rate: 0.325, timestamp: calendar: gregorian (fixed) year: 2018 month: 11 day: 29 hour: 3 minute: 9 second: 27 isLeapMonth: false )", type: nil),
  306. NewPumpEvent(date: f("2018-11-29 11:09:27 +0000"), dose: DoseEntry(type: .tempBasal, startDate: f("2018-11-29 11:09:27 +0000"), endDate: f("2018-11-29 11:39:27 +0000"), value: 0.325, unit: .unitsPerHour), raw: Data(hexadecimalString: "5bffca22ce53e48e87f7d624")!, title: "TempBasalDurationPumpEvent(length: 7, rawData: 7 bytes, duration: 30, timestamp: calendar: gregorian (fixed) year: 2018 month: 11 day: 29 hour: 3 minute: 9 second: 27 isLeapMonth: false )", type: .tempBasal)
  307. ]
  308. let addPumpEvents3 = expectation(description: "add pumpEvents3")
  309. addPumpEvents3.expectedFulfillmentCount = 3
  310. healthStore.setSaveHandler({ (objects, success, error) in
  311. XCTAssertEqual(3, objects.count)
  312. let basal = objects[0] as! HKQuantitySample
  313. XCTAssertEqual(HKInsulinDeliveryReason.basal, basal.insulinDeliveryReason)
  314. XCTAssertEqual(f("2018-11-29 10:54:28 +0000"), basal.startDate)
  315. XCTAssertEqual(f("2018-11-29 10:59:28 +0000"), basal.endDate)
  316. XCTAssertEqual("BasalRateSchedule 2018-11-29T10:54:28Z 2018-11-29T10:59:28Z", basal.metadata![HKMetadataKeySyncIdentifier] as! String)
  317. let temp1 = objects[1] as! HKQuantitySample
  318. XCTAssertEqual(HKInsulinDeliveryReason.basal, temp1.insulinDeliveryReason)
  319. XCTAssertEqual(f("2018-11-29 10:59:28 +0000"), temp1.startDate)
  320. XCTAssertEqual(f("2018-11-29 11:00:00 +0000"), temp1.endDate)
  321. XCTAssertEqual("5bffc7cace53e48e87f7cfcb 1/2", temp1.metadata![HKMetadataKeySyncIdentifier] as! String)
  322. XCTAssertEqual(0.003, temp1.quantity.doubleValue(for: .internationalUnit()), accuracy: 0.01)
  323. let temp2 = objects[2] as! HKQuantitySample
  324. XCTAssertEqual(HKInsulinDeliveryReason.basal, temp2.insulinDeliveryReason)
  325. XCTAssertEqual(f("2018-11-29 11:00:00 +0000"), temp2.startDate)
  326. XCTAssertEqual(f("2018-11-29 11:09:27 +0000"), temp2.endDate)
  327. XCTAssertEqual("5bffc7cace53e48e87f7cfcb 2/2", temp2.metadata![HKMetadataKeySyncIdentifier] as! String)
  328. XCTAssertEqual(0.047, temp2.quantity.doubleValue(for: .internationalUnit()), accuracy: 0.01)
  329. XCTAssertNil(error)
  330. addPumpEvents3.fulfill()
  331. })
  332. doseStore.insulinDeliveryStore.test_lastImmutableBasalEndDateDidSet = {
  333. addPumpEvents3.fulfill()
  334. }
  335. doseStore.addPumpEvents(pumpEvents3, lastReconciliation: Date()) { (error) in
  336. XCTAssertNil(error)
  337. addPumpEvents3.fulfill()
  338. }
  339. waitForExpectations(timeout: 3)
  340. XCTAssertEqual(f("2018-11-29 11:09:27 +0000"), doseStore.insulinDeliveryStore.test_lastImmutableBasalEndDate)
  341. XCTAssertEqual(f("2018-11-29 11:09:27 +0000"), doseStore.pumpEventQueryAfterDate)
  342. // Add the next set of pump events, including the last immutable temp basal cancel
  343. doseStore.insulinDeliveryStore.test_currentDate = f("2018-11-29 11:19:28 +0000")
  344. let pumpEvents4 = [
  345. NewPumpEvent(date: f("2018-11-29 11:14:28 +0000"), dose: nil, raw: UUID().data, title: "TempBasalPumpEvent(length: 8, rawData: 8 bytes, rateType: MinimedKit.TempBasalPumpEvent.RateType.Absolute, rate: 0, timestamp: calendar: gregorian (fixed) year: 2018 month: 11 day: 29 hour: 3 minute: 14 second: 28 isLeapMonth: false )", type: nil),
  346. NewPumpEvent(date: f("2018-11-29 11:14:28 +0000"), dose: DoseEntry(type: .tempBasal, startDate: f("2018-11-29 11:14:28 +0000"), endDate: f("2018-11-29 11:14:28 +0000"), value: 0.0, unit: .unitsPerHour), raw: Data(hexadecimalString: "5bffced1ce53e48e87f7e33b")!, title: "TempBasalDurationPumpEvent(length: 7, rawData: 7 bytes, duration: 30, timestamp: calendar: gregorian (fixed) year: 2018 month: 11 day: 29 hour: 3 minute: 14 second: 28 isLeapMonth: false )", type: .tempBasal)
  347. ]
  348. let addPumpEvents4 = expectation(description: "add pumpEvents4")
  349. addPumpEvents4.expectedFulfillmentCount = 3
  350. healthStore.setSaveHandler({ (objects, success, error) in
  351. XCTAssertEqual(1, objects.count)
  352. let temp = objects[0] as! HKQuantitySample
  353. XCTAssertEqual(HKInsulinDeliveryReason.basal, temp.insulinDeliveryReason)
  354. XCTAssertEqual(f("2018-11-29 11:09:27 +0000"), temp.startDate)
  355. XCTAssertEqual(f("2018-11-29 11:14:28 +0000"), temp.endDate)
  356. XCTAssertEqual("5bffca22ce53e48e87f7d624", temp.metadata![HKMetadataKeySyncIdentifier] as! String)
  357. XCTAssertEqual(0.05, temp.quantity.doubleValue(for: .internationalUnit()), accuracy: 0.01)
  358. XCTAssertNil(error)
  359. addPumpEvents4.fulfill()
  360. })
  361. doseStore.insulinDeliveryStore.test_lastImmutableBasalEndDateDidSet = {
  362. addPumpEvents4.fulfill()
  363. }
  364. doseStore.addPumpEvents(pumpEvents4, lastReconciliation: Date()) { (error) in
  365. XCTAssertNil(error)
  366. addPumpEvents4.fulfill()
  367. }
  368. waitForExpectations(timeout: 3)
  369. XCTAssertEqual(f("2018-11-29 11:14:28 +0000"), doseStore.pumpEventQueryAfterDate)
  370. XCTAssertEqual(f("2018-11-29 11:14:28 +0000"), doseStore.insulinDeliveryStore.test_lastImmutableBasalEndDate)
  371. // Add the final mutable pump event, it should NOT be synced to HealthKit
  372. doseStore.insulinDeliveryStore.test_currentDate = f("2018-11-29 11:24:28 +0000")
  373. let pumpEvents5 = [
  374. NewPumpEvent(date: f("2018-11-29 11:14:28 +0000"), dose: DoseEntry(type: .tempBasal, startDate: f("2018-11-29 11:14:28 +0000"), endDate: f("2018-11-29 11:44:28 +0000"), value: 1.0, unit: .unitsPerHour, isMutable: true), raw: Data(hexadecimalString: "e48e87f7e33b5bffced1ce53")!, title: "TempBasalDurationPumpEvent(length: 7, rawData: 7 bytes, duration: 30, timestamp: calendar: gregorian (fixed) year: 2018 month: 11 day: 29 hour: 3 minute: 14 second: 28 isLeapMonth: false )", type: .tempBasal)
  375. ]
  376. let addPumpEvents5 = expectation(description: "add pumpEvents5")
  377. addPumpEvents5.expectedFulfillmentCount = 2
  378. healthStore.setSaveHandler({ (objects, success, error) in
  379. XCTFail()
  380. })
  381. doseStore.insulinDeliveryStore.test_lastImmutableBasalEndDateDidSet = {
  382. addPumpEvents5.fulfill()
  383. }
  384. doseStore.addPumpEvents(pumpEvents5, lastReconciliation: Date()) { (error) in
  385. XCTAssertNil(error)
  386. addPumpEvents5.fulfill()
  387. }
  388. waitForExpectations(timeout: 3)
  389. XCTAssertEqual(f("2018-11-29 11:14:28 +0000"), doseStore.pumpEventQueryAfterDate)
  390. XCTAssertEqual(f("2018-11-29 11:14:28 +0000"), doseStore.insulinDeliveryStore.test_lastImmutableBasalEndDate)
  391. }
  392. func testAddPumpEventsProgrammedByPumpUI() {
  393. let formatter = DateFormatter.descriptionFormatter
  394. let f = { (input) in
  395. return formatter.date(from: input)!
  396. }
  397. // 1. Create a DoseStore
  398. let doseStore = DoseStore(
  399. healthStore: HKHealthStoreMock(),
  400. cacheStore: cacheStore,
  401. observationEnabled: false,
  402. insulinModelProvider: StaticInsulinModelProvider(WalshInsulinModel(actionDuration: .hours(4))),
  403. longestEffectDuration: .hours(4),
  404. basalProfile: BasalRateSchedule(rawValue: ["timeZone": -28800, "items": [["value": 0.75, "startTime": 0.0], ["value": 0.8, "startTime": 10800.0], ["value": 0.85, "startTime": 32400.0], ["value": 1.0, "startTime": 37800.0]]]),
  405. insulinSensitivitySchedule: InsulinSensitivitySchedule(rawValue: ["unit": "mg/dL", "timeZone": -28800, "items": [["value": 40.0, "startTime": 0.0], ["value": 35.0, "startTime": 21600.0], ["value": 40.0, "startTime": 57600.0]]]),
  406. syncVersion: 1,
  407. provenanceIdentifier: Bundle.main.bundleIdentifier!,
  408. // Set the current date
  409. test_currentDate: f("2018-12-12 18:07:14 +0000")
  410. )
  411. // 2. Add a temp basal which has already ended. It should persist in InsulinDeliveryStore.
  412. let pumpEvents1 = [
  413. NewPumpEvent(date: f("2018-12-12 17:35:00 +0000"), dose: nil, raw: UUID().data, title: "TempBasalPumpEvent(length: 8, rawData: 8 bytes, rateType: MinimedKit.TempBasalPumpEvent.RateType.Absolute, rate: 2.125, timestamp: calendar: gregorian (fixed) year: 2018 month: 12 day: 12 hour: 9 minute: 35 second: 0 isLeapMonth: false )", type: nil),
  414. NewPumpEvent(date: f("2018-12-12 17:35:00 +0000"), dose: DoseEntry(type: .tempBasal, startDate: f("2018-12-12 17:35:00 +0000"), endDate: f("2018-12-12 18:05:00 +0000"), value: 2.125, unit: .unitsPerHour, wasProgrammedByPumpUI: true), raw: Data(hexadecimalString: "1601fa23094c12")!, title: "TempBasalDurationPumpEvent(length: 7, rawData: 7 bytes, duration: 30, timestamp: calendar: gregorian (fixed) year: 2018 month: 12 day: 12 hour: 9 minute: 35 second: 0 isLeapMonth: false )", type: .tempBasal)
  415. ]
  416. doseStore.insulinDeliveryStore.test_lastImmutableBasalEndDate = f("2018-12-12 17:35:00 +0000")
  417. doseStore.insulinDeliveryStore.test_currentDate = f("2018-12-12 18:07:14 +0000")
  418. let addPumpEvents1 = expectation(description: "addPumpEvents1")
  419. addPumpEvents1.expectedFulfillmentCount = 2
  420. doseStore.addPumpEvents(pumpEvents1, lastReconciliation: Date()) { (error) in
  421. XCTAssertNil(error)
  422. doseStore.insulinDeliveryStore.getDoseEntries { result in
  423. switch result {
  424. case .failure:
  425. XCTFail()
  426. case .success(let doseEntries):
  427. XCTAssertEqual(doseEntries.count, 1)
  428. XCTAssertEqual(doseEntries[0].type, .tempBasal)
  429. XCTAssertEqual(doseEntries[0].startDate, f("2018-12-12 17:35:00 +0000"))
  430. XCTAssertEqual(doseEntries[0].endDate, f("2018-12-12 18:05:00 +0000"))
  431. XCTAssertEqual(doseEntries[0].value, 2.125)
  432. XCTAssertEqual(doseEntries[0].deliveredUnits, 1.05)
  433. XCTAssertEqual(doseEntries[0].syncIdentifier, "1601fa23094c12")
  434. XCTAssertEqual(doseEntries[0].scheduledBasalRate, HKQuantity(unit: .internationalUnitsPerHour, doubleValue: 0.85))
  435. XCTAssertFalse(doseEntries[0].isMutable)
  436. }
  437. addPumpEvents1.fulfill();
  438. }
  439. doseStore.insulinDeliveryStore.getDoseEntries(includeMutable: true) { result in
  440. switch result {
  441. case .failure:
  442. XCTFail()
  443. case .success(let doseEntries):
  444. XCTAssertEqual(doseEntries.count, 1)
  445. XCTAssertEqual(doseEntries[0].type, .tempBasal)
  446. XCTAssertEqual(doseEntries[0].startDate, f("2018-12-12 17:35:00 +0000"))
  447. XCTAssertEqual(doseEntries[0].endDate, f("2018-12-12 18:05:00 +0000"))
  448. XCTAssertEqual(doseEntries[0].value, 2.125)
  449. XCTAssertEqual(doseEntries[0].deliveredUnits, 1.05)
  450. XCTAssertEqual(doseEntries[0].syncIdentifier, "1601fa23094c12")
  451. XCTAssertEqual(doseEntries[0].scheduledBasalRate, HKQuantity(unit: .internationalUnitsPerHour, doubleValue: 0.85))
  452. XCTAssertFalse(doseEntries[0].isMutable)
  453. XCTAssertTrue(doseEntries[0].wasProgrammedByPumpUI)
  454. }
  455. addPumpEvents1.fulfill();
  456. }
  457. }
  458. waitForExpectations(timeout: 3)
  459. XCTAssertEqual(f("2018-12-12 18:05:00 +0000"), doseStore.insulinDeliveryStore.test_lastImmutableBasalEndDate)
  460. }
  461. func testAddPumpEventsPurgesMutableDosesFromInsulinDeliveryStore() {
  462. let formatter = DateFormatter.descriptionFormatter
  463. let f = { (input) in
  464. return formatter.date(from: input)!
  465. }
  466. // 1. Create a DoseStore
  467. let doseStore = DoseStore(
  468. healthStore: HKHealthStoreMock(),
  469. cacheStore: cacheStore,
  470. observationEnabled: false,
  471. insulinModelProvider: StaticInsulinModelProvider(WalshInsulinModel(actionDuration: .hours(4))),
  472. longestEffectDuration: .hours(4),
  473. basalProfile: BasalRateSchedule(rawValue: ["timeZone": -28800, "items": [["value": 0.75, "startTime": 0.0], ["value": 0.8, "startTime": 10800.0], ["value": 0.85, "startTime": 32400.0], ["value": 1.0, "startTime": 37800.0]]]),
  474. insulinSensitivitySchedule: InsulinSensitivitySchedule(rawValue: ["unit": "mg/dL", "timeZone": -28800, "items": [["value": 40.0, "startTime": 0.0], ["value": 35.0, "startTime": 21600.0], ["value": 40.0, "startTime": 57600.0]]]),
  475. syncVersion: 1,
  476. provenanceIdentifier: Bundle.main.bundleIdentifier!,
  477. // Set the current date
  478. test_currentDate: f("2018-12-12 18:07:14 +0000")
  479. )
  480. // 2. Add a temp basal which has already ended. It should persist in InsulinDeliveryStore.
  481. let pumpEvents1 = [
  482. NewPumpEvent(date: f("2018-12-12 17:35:00 +0000"), dose: nil, raw: UUID().data, title: "TempBasalPumpEvent(length: 8, rawData: 8 bytes, rateType: MinimedKit.TempBasalPumpEvent.RateType.Absolute, rate: 2.125, timestamp: calendar: gregorian (fixed) year: 2018 month: 12 day: 12 hour: 9 minute: 35 second: 0 isLeapMonth: false )", type: nil),
  483. NewPumpEvent(date: f("2018-12-12 17:35:00 +0000"), dose: DoseEntry(type: .tempBasal, startDate: f("2018-12-12 17:35:00 +0000"), endDate: f("2018-12-12 18:05:00 +0000"), value: 2.125, unit: .unitsPerHour), raw: Data(hexadecimalString: "1601fa23094c12")!, title: "TempBasalDurationPumpEvent(length: 7, rawData: 7 bytes, duration: 30, timestamp: calendar: gregorian (fixed) year: 2018 month: 12 day: 12 hour: 9 minute: 35 second: 0 isLeapMonth: false )", type: .tempBasal)
  484. ]
  485. doseStore.insulinDeliveryStore.test_lastImmutableBasalEndDate = f("2018-12-12 17:35:00 +0000")
  486. doseStore.insulinDeliveryStore.test_currentDate = f("2018-12-12 18:07:14 +0000")
  487. let addPumpEvents1 = expectation(description: "addPumpEvents1")
  488. addPumpEvents1.expectedFulfillmentCount = 2
  489. doseStore.addPumpEvents(pumpEvents1, lastReconciliation: Date()) { (error) in
  490. XCTAssertNil(error)
  491. doseStore.insulinDeliveryStore.getDoseEntries { result in
  492. switch result {
  493. case .failure:
  494. XCTFail()
  495. case .success(let doseEntries):
  496. XCTAssertEqual(doseEntries.count, 1)
  497. XCTAssertEqual(doseEntries[0].type, .tempBasal)
  498. XCTAssertEqual(doseEntries[0].startDate, f("2018-12-12 17:35:00 +0000"))
  499. XCTAssertEqual(doseEntries[0].endDate, f("2018-12-12 18:05:00 +0000"))
  500. XCTAssertEqual(doseEntries[0].value, 2.125)
  501. XCTAssertEqual(doseEntries[0].deliveredUnits, 1.05)
  502. XCTAssertEqual(doseEntries[0].syncIdentifier, "1601fa23094c12")
  503. XCTAssertEqual(doseEntries[0].scheduledBasalRate, HKQuantity(unit: .internationalUnitsPerHour, doubleValue: 0.85))
  504. XCTAssertFalse(doseEntries[0].isMutable)
  505. }
  506. addPumpEvents1.fulfill();
  507. }
  508. doseStore.insulinDeliveryStore.getDoseEntries(includeMutable: true) { result in
  509. switch result {
  510. case .failure:
  511. XCTFail()
  512. case .success(let doseEntries):
  513. XCTAssertEqual(doseEntries.count, 1)
  514. XCTAssertEqual(doseEntries[0].type, .tempBasal)
  515. XCTAssertEqual(doseEntries[0].startDate, f("2018-12-12 17:35:00 +0000"))
  516. XCTAssertEqual(doseEntries[0].endDate, f("2018-12-12 18:05:00 +0000"))
  517. XCTAssertEqual(doseEntries[0].value, 2.125)
  518. XCTAssertEqual(doseEntries[0].deliveredUnits, 1.05)
  519. XCTAssertEqual(doseEntries[0].syncIdentifier, "1601fa23094c12")
  520. XCTAssertEqual(doseEntries[0].scheduledBasalRate, HKQuantity(unit: .internationalUnitsPerHour, doubleValue: 0.85))
  521. XCTAssertFalse(doseEntries[0].isMutable)
  522. }
  523. addPumpEvents1.fulfill();
  524. }
  525. }
  526. waitForExpectations(timeout: 3)
  527. XCTAssertEqual(f("2018-12-12 18:05:00 +0000"), doseStore.insulinDeliveryStore.test_lastImmutableBasalEndDate)
  528. // 3. Add a mutable temp basal. It should persist in InsulinDeliveryStore.
  529. let pumpEvents2 = [
  530. NewPumpEvent(date: f("2018-12-12 18:05:00 +0000"), dose: DoseEntry(type: .tempBasal, startDate: f("2018-12-12 18:05:00 +0000"), endDate: f("2018-12-12 18:25:00 +0000"), value: 1.375, unit: .unitsPerHour, isMutable: true, wasProgrammedByPumpUI: true), raw: Data(hexadecimalString: "3094c121601fa2")!, title: "TempBasalDurationPumpEvent(length: 7, rawData: 7 bytes, duration: 30, timestamp: calendar: gregorian (fixed) year: 2018 month: 12 day: 12 hour: 10 minute: 05 second: 0 isLeapMonth: false )", type: .tempBasal)
  531. ]
  532. doseStore.insulinDeliveryStore.test_lastImmutableBasalEndDate = f("2018-12-12 18:05:00 +0000")
  533. doseStore.insulinDeliveryStore.test_currentDate = f("2018-12-12 18:07:14 +0000")
  534. let addPumpEvents2 = expectation(description: "addPumpEvents2")
  535. addPumpEvents2.expectedFulfillmentCount = 2
  536. doseStore.addPumpEvents(pumpEvents2, lastReconciliation: Date()) { (error) in
  537. XCTAssertNil(error)
  538. doseStore.insulinDeliveryStore.getDoseEntries { result in
  539. switch result {
  540. case .failure:
  541. XCTFail()
  542. case .success(let doseEntries):
  543. XCTAssertEqual(doseEntries.count, 1)
  544. XCTAssertEqual(doseEntries[0].type, .tempBasal)
  545. XCTAssertEqual(doseEntries[0].startDate, f("2018-12-12 17:35:00 +0000"))
  546. XCTAssertEqual(doseEntries[0].endDate, f("2018-12-12 18:05:00 +0000"))
  547. XCTAssertEqual(doseEntries[0].value, 2.125)
  548. XCTAssertEqual(doseEntries[0].deliveredUnits, 1.05)
  549. XCTAssertEqual(doseEntries[0].syncIdentifier, "1601fa23094c12")
  550. XCTAssertEqual(doseEntries[0].scheduledBasalRate, HKQuantity(unit: .internationalUnitsPerHour, doubleValue: 0.85))
  551. XCTAssertFalse(doseEntries[0].isMutable)
  552. XCTAssertFalse(doseEntries[0].wasProgrammedByPumpUI)
  553. }
  554. addPumpEvents2.fulfill();
  555. }
  556. doseStore.insulinDeliveryStore.getDoseEntries(includeMutable: true) { result in
  557. switch result {
  558. case .failure:
  559. XCTFail()
  560. case .success(let doseEntries):
  561. XCTAssertEqual(doseEntries.count, 2)
  562. XCTAssertEqual(doseEntries[0].type, .tempBasal)
  563. XCTAssertEqual(doseEntries[0].startDate, f("2018-12-12 17:35:00 +0000"))
  564. XCTAssertEqual(doseEntries[0].endDate, f("2018-12-12 18:05:00 +0000"))
  565. XCTAssertEqual(doseEntries[0].value, 2.125)
  566. XCTAssertEqual(doseEntries[0].deliveredUnits, 1.05)
  567. XCTAssertEqual(doseEntries[0].syncIdentifier, "1601fa23094c12")
  568. XCTAssertEqual(doseEntries[0].scheduledBasalRate, HKQuantity(unit: .internationalUnitsPerHour, doubleValue: 0.85))
  569. XCTAssertFalse(doseEntries[0].isMutable)
  570. XCTAssertEqual(doseEntries[1].type, .tempBasal)
  571. XCTAssertEqual(doseEntries[1].startDate, f("2018-12-12 18:05:00 +0000"))
  572. XCTAssertEqual(doseEntries[1].endDate, f("2018-12-12 18:25:00 +0000"))
  573. XCTAssertEqual(doseEntries[1].value, 1.375)
  574. XCTAssertNil(doseEntries[1].deliveredUnits)
  575. XCTAssertEqual(doseEntries[1].syncIdentifier, "3094c121601fa2")
  576. XCTAssertEqual(doseEntries[1].scheduledBasalRate, HKQuantity(unit: .internationalUnitsPerHour, doubleValue: 0.85))
  577. XCTAssertTrue(doseEntries[1].isMutable)
  578. XCTAssertTrue(doseEntries[1].wasProgrammedByPumpUI)
  579. }
  580. addPumpEvents2.fulfill();
  581. }
  582. }
  583. waitForExpectations(timeout: 3)
  584. XCTAssertEqual(f("2018-12-12 18:05:00 +0000"), doseStore.insulinDeliveryStore.test_lastImmutableBasalEndDate)
  585. // 4. Update the mutable temp basal that crossing scheduled basal boundary. It should persist in InsulinDeliveryStore.
  586. let pumpEvents3 = [
  587. NewPumpEvent(date: f("2018-12-12 18:05:00 +0000"), dose: DoseEntry(type: .tempBasal, startDate: f("2018-12-12 18:05:00 +0000"), endDate: f("2018-12-12 18:35:00 +0000"), value: 0.875, unit: .unitsPerHour, isMutable: true), raw: Data(hexadecimalString: "3094c121601fa2")!, title: "TempBasalDurationPumpEvent(length: 7, rawData: 7 bytes, duration: 30, timestamp: calendar: gregorian (fixed) year: 2018 month: 12 day: 12 hour: 10 minute: 05 second: 0 isLeapMonth: false )", type: .tempBasal)
  588. ]
  589. doseStore.insulinDeliveryStore.test_lastImmutableBasalEndDate = f("2018-12-12 18:05:00 +0000")
  590. doseStore.insulinDeliveryStore.test_currentDate = f("2018-12-12 18:07:14 +0000")
  591. let addPumpEvents3 = expectation(description: "addPumpEvents3")
  592. addPumpEvents3.expectedFulfillmentCount = 2
  593. doseStore.addPumpEvents(pumpEvents3, lastReconciliation: Date()) { (error) in
  594. XCTAssertNil(error)
  595. doseStore.insulinDeliveryStore.getDoseEntries { result in
  596. switch result {
  597. case .failure:
  598. XCTFail()
  599. case .success(let doseEntries):
  600. XCTAssertEqual(doseEntries.count, 1)
  601. XCTAssertEqual(doseEntries[0].type, .tempBasal)
  602. XCTAssertEqual(doseEntries[0].startDate, f("2018-12-12 17:35:00 +0000"))
  603. XCTAssertEqual(doseEntries[0].endDate, f("2018-12-12 18:05:00 +0000"))
  604. XCTAssertEqual(doseEntries[0].value, 2.125)
  605. XCTAssertEqual(doseEntries[0].deliveredUnits, 1.05)
  606. XCTAssertEqual(doseEntries[0].syncIdentifier, "1601fa23094c12")
  607. XCTAssertEqual(doseEntries[0].scheduledBasalRate, HKQuantity(unit: .internationalUnitsPerHour, doubleValue: 0.85))
  608. XCTAssertFalse(doseEntries[0].isMutable)
  609. }
  610. addPumpEvents3.fulfill();
  611. }
  612. doseStore.insulinDeliveryStore.getDoseEntries(includeMutable: true) { result in
  613. switch result {
  614. case .failure:
  615. XCTFail()
  616. case .success(let doseEntries):
  617. XCTAssertEqual(doseEntries.count, 3)
  618. XCTAssertEqual(doseEntries[0].type, .tempBasal)
  619. XCTAssertEqual(doseEntries[0].startDate, f("2018-12-12 17:35:00 +0000"))
  620. XCTAssertEqual(doseEntries[0].endDate, f("2018-12-12 18:05:00 +0000"))
  621. XCTAssertEqual(doseEntries[0].value, 2.125)
  622. XCTAssertEqual(doseEntries[0].deliveredUnits, 1.05)
  623. XCTAssertEqual(doseEntries[0].syncIdentifier, "1601fa23094c12")
  624. XCTAssertEqual(doseEntries[0].scheduledBasalRate, HKQuantity(unit: .internationalUnitsPerHour, doubleValue: 0.85))
  625. XCTAssertFalse(doseEntries[0].isMutable)
  626. XCTAssertEqual(doseEntries[1].type, .tempBasal)
  627. XCTAssertEqual(doseEntries[1].startDate, f("2018-12-12 18:05:00 +0000"))
  628. XCTAssertEqual(doseEntries[1].endDate, f("2018-12-12 18:30:00 +0000"))
  629. XCTAssertEqual(doseEntries[1].value, 0.875)
  630. XCTAssertNil(doseEntries[1].deliveredUnits)
  631. XCTAssertEqual(doseEntries[1].syncIdentifier, "3094c121601fa2 1/2")
  632. XCTAssertEqual(doseEntries[1].scheduledBasalRate, HKQuantity(unit: .internationalUnitsPerHour, doubleValue: 0.85))
  633. XCTAssertTrue(doseEntries[1].isMutable)
  634. XCTAssertEqual(doseEntries[2].type, .tempBasal)
  635. XCTAssertEqual(doseEntries[2].startDate, f("2018-12-12 18:30:00 +0000"))
  636. XCTAssertEqual(doseEntries[2].endDate, f("2018-12-12 18:35:00 +0000"))
  637. XCTAssertEqual(doseEntries[2].value, 0.875)
  638. XCTAssertNil(doseEntries[2].deliveredUnits)
  639. XCTAssertEqual(doseEntries[2].syncIdentifier, "3094c121601fa2 2/2")
  640. XCTAssertEqual(doseEntries[2].scheduledBasalRate, HKQuantity(unit: .internationalUnitsPerHour, doubleValue: 1.0))
  641. XCTAssertTrue(doseEntries[2].isMutable)
  642. }
  643. addPumpEvents3.fulfill();
  644. }
  645. }
  646. waitForExpectations(timeout: 3)
  647. XCTAssertEqual(f("2018-12-12 18:05:00 +0000"), doseStore.insulinDeliveryStore.test_lastImmutableBasalEndDate)
  648. // 5. Add a different mutable temp basal that crossing scheduled basal boundary. It should persist in InsulinDeliveryStore. A scheduled basal should be added.
  649. let pumpEvents4 = [
  650. NewPumpEvent(date: f("2018-12-12 18:15:00 +0000"), dose: DoseEntry(type: .tempBasal, startDate: f("2018-12-12 18:15:00 +0000"), endDate: f("2018-12-12 18:45:00 +0000"), value: 0.5, unit: .unitsPerHour, isMutable: true), raw: Data(hexadecimalString: "121601f3094ca2")!, title: "TempBasalDurationPumpEvent(length: 7, rawData: 7 bytes, duration: 30, timestamp: calendar: gregorian (fixed) year: 2018 month: 12 day: 12 hour: 10 minute: 15 second: 0 isLeapMonth: false )", type: .tempBasal)
  651. ]
  652. var basalDoseEntry: DoseEntry? = nil
  653. doseStore.insulinDeliveryStore.test_lastImmutableBasalEndDate = f("2018-12-12 18:05:00 +0000")
  654. doseStore.insulinDeliveryStore.test_currentDate = f("2018-12-12 18:17:14 +0000")
  655. let addPumpEvents4 = expectation(description: "addPumpEvents4")
  656. addPumpEvents4.expectedFulfillmentCount = 2
  657. doseStore.addPumpEvents(pumpEvents4, lastReconciliation: Date()) { (error) in
  658. XCTAssertNil(error)
  659. doseStore.insulinDeliveryStore.getDoseEntries { result in
  660. switch result {
  661. case .failure:
  662. XCTFail()
  663. case .success(let doseEntries):
  664. XCTAssertEqual(doseEntries.count, 2)
  665. XCTAssertEqual(doseEntries[0].type, .tempBasal)
  666. XCTAssertEqual(doseEntries[0].startDate, f("2018-12-12 17:35:00 +0000"))
  667. XCTAssertEqual(doseEntries[0].endDate, f("2018-12-12 18:05:00 +0000"))
  668. XCTAssertEqual(doseEntries[0].value, 2.125)
  669. XCTAssertEqual(doseEntries[0].deliveredUnits, 1.05)
  670. XCTAssertEqual(doseEntries[0].syncIdentifier, "1601fa23094c12")
  671. XCTAssertEqual(doseEntries[0].scheduledBasalRate, HKQuantity(unit: .internationalUnitsPerHour, doubleValue: 0.85))
  672. XCTAssertFalse(doseEntries[0].isMutable)
  673. XCTAssertEqual(doseEntries[1].type, .basal)
  674. XCTAssertEqual(doseEntries[1].startDate, f("2018-12-12 18:05:00 +0000"))
  675. XCTAssertEqual(doseEntries[1].endDate, f("2018-12-12 18:15:00 +0000"))
  676. XCTAssertEqual(doseEntries[1].value, 0.15)
  677. XCTAssertNil(doseEntries[1].deliveredUnits)
  678. XCTAssertEqual(doseEntries[1].syncIdentifier, "BasalRateSchedule 2018-12-12T18:05:00Z 2018-12-12T18:15:00Z")
  679. XCTAssertEqual(doseEntries[1].scheduledBasalRate, HKQuantity(unit: .internationalUnitsPerHour, doubleValue: 0.85))
  680. XCTAssertFalse(doseEntries[1].isMutable)
  681. basalDoseEntry = doseEntries[1]
  682. }
  683. addPumpEvents4.fulfill();
  684. }
  685. doseStore.insulinDeliveryStore.getDoseEntries(includeMutable: true) { result in
  686. switch result {
  687. case .failure:
  688. XCTFail()
  689. case .success(let doseEntries):
  690. XCTAssertEqual(doseEntries.count, 4)
  691. XCTAssertEqual(doseEntries[0].type, .tempBasal)
  692. XCTAssertEqual(doseEntries[0].startDate, f("2018-12-12 17:35:00 +0000"))
  693. XCTAssertEqual(doseEntries[0].endDate, f("2018-12-12 18:05:00 +0000"))
  694. XCTAssertEqual(doseEntries[0].value, 2.125)
  695. XCTAssertEqual(doseEntries[0].deliveredUnits, 1.05)
  696. XCTAssertEqual(doseEntries[0].syncIdentifier, "1601fa23094c12")
  697. XCTAssertEqual(doseEntries[0].scheduledBasalRate, HKQuantity(unit: .internationalUnitsPerHour, doubleValue: 0.85))
  698. XCTAssertFalse(doseEntries[0].isMutable)
  699. XCTAssertEqual(doseEntries[1].type, .basal)
  700. XCTAssertEqual(doseEntries[1].startDate, f("2018-12-12 18:05:00 +0000"))
  701. XCTAssertEqual(doseEntries[1].endDate, f("2018-12-12 18:15:00 +0000"))
  702. XCTAssertEqual(doseEntries[1].value, 0.15)
  703. XCTAssertNil(doseEntries[1].deliveredUnits)
  704. XCTAssertEqual(doseEntries[1].syncIdentifier, "BasalRateSchedule 2018-12-12T18:05:00Z 2018-12-12T18:15:00Z")
  705. XCTAssertEqual(doseEntries[1].scheduledBasalRate, HKQuantity(unit: .internationalUnitsPerHour, doubleValue: 0.85))
  706. XCTAssertFalse(doseEntries[1].isMutable)
  707. XCTAssertEqual(doseEntries[2].type, .tempBasal)
  708. XCTAssertEqual(doseEntries[2].startDate, f("2018-12-12 18:15:00 +0000"))
  709. XCTAssertEqual(doseEntries[2].endDate, f("2018-12-12 18:30:00 +0000"))
  710. XCTAssertEqual(doseEntries[2].value, 0.5)
  711. XCTAssertNil(doseEntries[2].deliveredUnits)
  712. XCTAssertEqual(doseEntries[2].syncIdentifier, "121601f3094ca2 1/2")
  713. XCTAssertEqual(doseEntries[2].scheduledBasalRate, HKQuantity(unit: .internationalUnitsPerHour, doubleValue: 0.85))
  714. XCTAssertTrue(doseEntries[2].isMutable)
  715. XCTAssertEqual(doseEntries[3].type, .tempBasal)
  716. XCTAssertEqual(doseEntries[3].startDate, f("2018-12-12 18:30:00 +0000"))
  717. XCTAssertEqual(doseEntries[3].endDate, f("2018-12-12 18:45:00 +0000"))
  718. XCTAssertEqual(doseEntries[3].value, 0.5)
  719. XCTAssertNil(doseEntries[3].deliveredUnits)
  720. XCTAssertEqual(doseEntries[3].syncIdentifier, "121601f3094ca2 2/2")
  721. XCTAssertEqual(doseEntries[3].scheduledBasalRate, HKQuantity(unit: .internationalUnitsPerHour, doubleValue: 1.0))
  722. XCTAssertTrue(doseEntries[3].isMutable)
  723. }
  724. addPumpEvents4.fulfill();
  725. }
  726. }
  727. waitForExpectations(timeout: 3)
  728. XCTAssertEqual(f("2018-12-12 18:15:00 +0000"), doseStore.insulinDeliveryStore.test_lastImmutableBasalEndDate)
  729. // 6. Deleted scheduled basal dose entry. Tombstones entry so should not be returned again.
  730. let addPumpEvents5 = expectation(description: "addPumpEvents5")
  731. doseStore.deleteDose(basalDoseEntry!) { result in
  732. XCTAssertNil(result)
  733. addPumpEvents5.fulfill();
  734. }
  735. waitForExpectations(timeout: 3)
  736. XCTAssertEqual(f("2018-12-12 18:05:00 +0000"), doseStore.insulinDeliveryStore.test_lastImmutableBasalEndDate)
  737. // 7. Add an immutable temp basal that crossing scheduled basal boundary. It should persist in InsulinDeliveryStore.
  738. let pumpEvents6 = [
  739. NewPumpEvent(date: f("2018-12-12 18:25:00 +0000"), dose: DoseEntry(type: .tempBasal, startDate: f("2018-12-12 18:25:00 +0000"), endDate: f("2018-12-12 18:40:00 +0000"), value: 0.75, unit: .unitsPerHour), raw: Data(hexadecimalString: "1201f3094c16a2")!, title: "TempBasalDurationPumpEvent(length: 7, rawData: 7 bytes, duration: 30, timestamp: calendar: gregorian (fixed) year: 2018 month: 12 day: 12 hour: 10 minute: 25 second: 0 isLeapMonth: false )", type: .tempBasal)
  740. ]
  741. doseStore.insulinDeliveryStore.test_lastImmutableBasalEndDate = f("2018-12-12 18:05:00 +0000")
  742. doseStore.insulinDeliveryStore.test_currentDate = f("2018-12-12 18:41:14 +0000")
  743. let addPumpEvents6 = expectation(description: "addPumpEvents6")
  744. addPumpEvents6.expectedFulfillmentCount = 2
  745. doseStore.addPumpEvents(pumpEvents6, lastReconciliation: Date()) { (error) in
  746. XCTAssertNil(error)
  747. doseStore.insulinDeliveryStore.getDoseEntries { result in
  748. switch result {
  749. case .failure:
  750. XCTFail()
  751. case .success(let doseEntries):
  752. XCTAssertEqual(doseEntries.count, 4)
  753. XCTAssertEqual(doseEntries[0].type, .tempBasal)
  754. XCTAssertEqual(doseEntries[0].startDate, f("2018-12-12 17:35:00 +0000"))
  755. XCTAssertEqual(doseEntries[0].endDate, f("2018-12-12 18:05:00 +0000"))
  756. XCTAssertEqual(doseEntries[0].value, 2.125)
  757. XCTAssertEqual(doseEntries[0].deliveredUnits, 1.05)
  758. XCTAssertEqual(doseEntries[0].syncIdentifier, "1601fa23094c12")
  759. XCTAssertEqual(doseEntries[0].scheduledBasalRate, HKQuantity(unit: .internationalUnitsPerHour, doubleValue: 0.85))
  760. XCTAssertFalse(doseEntries[0].isMutable)
  761. XCTAssertEqual(doseEntries[1].type, .basal)
  762. XCTAssertEqual(doseEntries[1].startDate, f("2018-12-12 18:05:00 +0000"))
  763. XCTAssertEqual(doseEntries[1].endDate, f("2018-12-12 18:25:00 +0000"))
  764. XCTAssertEqual(doseEntries[1].value, 0.3)
  765. XCTAssertNil(doseEntries[1].deliveredUnits)
  766. XCTAssertEqual(doseEntries[1].syncIdentifier, "BasalRateSchedule 2018-12-12T18:05:00Z 2018-12-12T18:25:00Z")
  767. XCTAssertEqual(doseEntries[1].scheduledBasalRate, HKQuantity(unit: .internationalUnitsPerHour, doubleValue: 0.85))
  768. XCTAssertFalse(doseEntries[1].isMutable)
  769. XCTAssertEqual(doseEntries[2].type, .tempBasal)
  770. XCTAssertEqual(doseEntries[2].startDate, f("2018-12-12 18:25:00 +0000"))
  771. XCTAssertEqual(doseEntries[2].endDate, f("2018-12-12 18:30:00 +0000"))
  772. XCTAssertEqual(doseEntries[2].value, 0.75)
  773. XCTAssertEqual(doseEntries[2].deliveredUnits, 0.06666666666666667)
  774. XCTAssertEqual(doseEntries[2].syncIdentifier, "1201f3094c16a2 1/2")
  775. XCTAssertEqual(doseEntries[2].scheduledBasalRate, HKQuantity(unit: .internationalUnitsPerHour, doubleValue: 0.85))
  776. XCTAssertFalse(doseEntries[2].isMutable)
  777. XCTAssertEqual(doseEntries[3].type, .tempBasal)
  778. XCTAssertEqual(doseEntries[3].startDate, f("2018-12-12 18:30:00 +0000"))
  779. XCTAssertEqual(doseEntries[3].endDate, f("2018-12-12 18:40:00 +0000"))
  780. XCTAssertEqual(doseEntries[3].value, 0.75)
  781. XCTAssertEqual(doseEntries[3].deliveredUnits, 0.13333333333333333)
  782. XCTAssertEqual(doseEntries[3].syncIdentifier, "1201f3094c16a2 2/2")
  783. XCTAssertEqual(doseEntries[3].scheduledBasalRate, HKQuantity(unit: .internationalUnitsPerHour, doubleValue: 1.0))
  784. XCTAssertFalse(doseEntries[3].isMutable)
  785. }
  786. addPumpEvents6.fulfill();
  787. }
  788. doseStore.insulinDeliveryStore.getDoseEntries(includeMutable: true) { result in
  789. switch result {
  790. case .failure:
  791. XCTFail()
  792. case .success(let doseEntries):
  793. XCTAssertEqual(doseEntries.count, 4)
  794. XCTAssertEqual(doseEntries[0].type, .tempBasal)
  795. XCTAssertEqual(doseEntries[0].startDate, f("2018-12-12 17:35:00 +0000"))
  796. XCTAssertEqual(doseEntries[0].endDate, f("2018-12-12 18:05:00 +0000"))
  797. XCTAssertEqual(doseEntries[0].value, 2.125)
  798. XCTAssertEqual(doseEntries[0].deliveredUnits, 1.05)
  799. XCTAssertEqual(doseEntries[0].syncIdentifier, "1601fa23094c12")
  800. XCTAssertEqual(doseEntries[0].scheduledBasalRate, HKQuantity(unit: .internationalUnitsPerHour, doubleValue: 0.85))
  801. XCTAssertFalse(doseEntries[0].isMutable)
  802. XCTAssertEqual(doseEntries[1].type, .basal)
  803. XCTAssertEqual(doseEntries[1].startDate, f("2018-12-12 18:05:00 +0000"))
  804. XCTAssertEqual(doseEntries[1].endDate, f("2018-12-12 18:25:00 +0000"))
  805. XCTAssertEqual(doseEntries[1].value, 0.3)
  806. XCTAssertNil(doseEntries[1].deliveredUnits)
  807. XCTAssertEqual(doseEntries[1].syncIdentifier, "BasalRateSchedule 2018-12-12T18:05:00Z 2018-12-12T18:25:00Z")
  808. XCTAssertEqual(doseEntries[1].scheduledBasalRate, HKQuantity(unit: .internationalUnitsPerHour, doubleValue: 0.85))
  809. XCTAssertFalse(doseEntries[1].isMutable)
  810. XCTAssertEqual(doseEntries[2].type, .tempBasal)
  811. XCTAssertEqual(doseEntries[2].startDate, f("2018-12-12 18:25:00 +0000"))
  812. XCTAssertEqual(doseEntries[2].endDate, f("2018-12-12 18:30:00 +0000"))
  813. XCTAssertEqual(doseEntries[2].value, 0.75)
  814. XCTAssertEqual(doseEntries[2].deliveredUnits, 0.06666666666666667)
  815. XCTAssertEqual(doseEntries[2].syncIdentifier, "1201f3094c16a2 1/2")
  816. XCTAssertEqual(doseEntries[2].scheduledBasalRate, HKQuantity(unit: .internationalUnitsPerHour, doubleValue: 0.85))
  817. XCTAssertFalse(doseEntries[2].isMutable)
  818. XCTAssertEqual(doseEntries[3].type, .tempBasal)
  819. XCTAssertEqual(doseEntries[3].startDate, f("2018-12-12 18:30:00 +0000"))
  820. XCTAssertEqual(doseEntries[3].endDate, f("2018-12-12 18:40:00 +0000"))
  821. XCTAssertEqual(doseEntries[3].value, 0.75)
  822. XCTAssertEqual(doseEntries[3].deliveredUnits, 0.13333333333333333)
  823. XCTAssertEqual(doseEntries[3].syncIdentifier, "1201f3094c16a2 2/2")
  824. XCTAssertEqual(doseEntries[3].scheduledBasalRate, HKQuantity(unit: .internationalUnitsPerHour, doubleValue: 1.0))
  825. XCTAssertFalse(doseEntries[3].isMutable)
  826. }
  827. addPumpEvents6.fulfill();
  828. }
  829. }
  830. waitForExpectations(timeout: 3)
  831. XCTAssertEqual(f("2018-12-12 18:40:00 +0000"), doseStore.insulinDeliveryStore.test_lastImmutableBasalEndDate)
  832. }
  833. }
  834. class DoseStoreQueryAnchorTests: XCTestCase {
  835. var rawValue: DoseStore.QueryAnchor.RawValue = [
  836. "modificationCounter": Int64(123)
  837. ]
  838. func testInitializerDefault() {
  839. let queryAnchor = DoseStore.QueryAnchor()
  840. XCTAssertEqual(queryAnchor.modificationCounter, 0)
  841. }
  842. func testInitializerRawValue() {
  843. let queryAnchor = DoseStore.QueryAnchor(rawValue: rawValue)
  844. XCTAssertNotNil(queryAnchor)
  845. XCTAssertEqual(queryAnchor?.modificationCounter, 123)
  846. }
  847. func testInitializerRawValueMissingModificationCounter() {
  848. rawValue["modificationCounter"] = nil
  849. XCTAssertNil(DoseStore.QueryAnchor(rawValue: rawValue))
  850. }
  851. func testInitializerRawValueInvalidModificationCounter() {
  852. rawValue["modificationCounter"] = "123"
  853. XCTAssertNil(DoseStore.QueryAnchor(rawValue: rawValue))
  854. }
  855. func testRawValueWithDefault() {
  856. let rawValue = DoseStore.QueryAnchor().rawValue
  857. XCTAssertEqual(rawValue.count, 1)
  858. XCTAssertEqual(rawValue["modificationCounter"] as? Int64, Int64(0))
  859. }
  860. func testRawValueWithNonDefault() {
  861. var queryAnchor = DoseStore.QueryAnchor()
  862. queryAnchor.modificationCounter = 123
  863. let rawValue = queryAnchor.rawValue
  864. XCTAssertEqual(rawValue.count, 1)
  865. XCTAssertEqual(rawValue["modificationCounter"] as? Int64, Int64(123))
  866. }
  867. }
  868. class DoseStoreQueryTests: PersistenceControllerTestCase {
  869. let insulinModel = WalshInsulinModel(actionDuration: .hours(4))
  870. let basalProfile = BasalRateSchedule(rawValue: ["timeZone": -28800, "items": [["value": 0.75, "startTime": 0.0], ["value": 0.8, "startTime": 10800.0], ["value": 0.85, "startTime": 32400.0], ["value": 1.0, "startTime": 68400.0]]])
  871. let insulinSensitivitySchedule = InsulinSensitivitySchedule(rawValue: ["unit": "mg/dL", "timeZone": -28800, "items": [["value": 40.0, "startTime": 0.0], ["value": 35.0, "startTime": 21600.0], ["value": 40.0, "startTime": 57600.0]]])
  872. var doseStore: DoseStore!
  873. var completion: XCTestExpectation!
  874. var queryAnchor: DoseStore.QueryAnchor!
  875. var limit: Int!
  876. override func setUp() {
  877. super.setUp()
  878. doseStore = DoseStore(healthStore: HKHealthStoreMock(),
  879. cacheStore: cacheStore,
  880. observationEnabled: false,
  881. insulinModelProvider: StaticInsulinModelProvider(insulinModel),
  882. longestEffectDuration: insulinModel.effectDuration,
  883. basalProfile: basalProfile,
  884. insulinSensitivitySchedule: insulinSensitivitySchedule,
  885. provenanceIdentifier: Bundle.main.bundleIdentifier!)
  886. completion = expectation(description: "Completion")
  887. queryAnchor = DoseStore.QueryAnchor()
  888. limit = Int.max
  889. }
  890. override func tearDown() {
  891. limit = nil
  892. queryAnchor = nil
  893. completion = nil
  894. doseStore = nil
  895. super.tearDown()
  896. }
  897. func testPumpEventEmptyWithDefaultQueryAnchor() {
  898. doseStore.executePumpEventQuery(fromQueryAnchor: queryAnchor, limit: limit) { result in
  899. switch result {
  900. case .failure(let error):
  901. XCTFail("Unexpected failure: \(error)")
  902. case .success(let anchor, let data):
  903. XCTAssertEqual(anchor.modificationCounter, 0)
  904. XCTAssertEqual(data.count, 0)
  905. }
  906. self.completion.fulfill()
  907. }
  908. wait(for: [completion], timeout: 2, enforceOrder: true)
  909. }
  910. func testPumpEventEmptyWithMissingQueryAnchor() {
  911. queryAnchor = nil
  912. doseStore.executePumpEventQuery(fromQueryAnchor: queryAnchor, limit: limit) { result in
  913. switch result {
  914. case .failure(let error):
  915. XCTFail("Unexpected failure: \(error)")
  916. case .success(let anchor, let data):
  917. XCTAssertEqual(anchor.modificationCounter, 0)
  918. XCTAssertEqual(data.count, 0)
  919. }
  920. self.completion.fulfill()
  921. }
  922. wait(for: [completion], timeout: 2, enforceOrder: true)
  923. }
  924. func testPumpEventEmptyWithNonDefaultQueryAnchor() {
  925. queryAnchor.modificationCounter = 1
  926. doseStore.executePumpEventQuery(fromQueryAnchor: queryAnchor, limit: limit) { result in
  927. switch result {
  928. case .failure(let error):
  929. XCTFail("Unexpected failure: \(error)")
  930. case .success(let anchor, let data):
  931. XCTAssertEqual(anchor.modificationCounter, 1)
  932. XCTAssertEqual(data.count, 0)
  933. }
  934. self.completion.fulfill()
  935. }
  936. wait(for: [completion], timeout: 2, enforceOrder: true)
  937. }
  938. func testPumpEventDataWithUnusedQueryAnchor() {
  939. let syncIdentifiers = [generateSyncIdentifier(), generateSyncIdentifier(), generateSyncIdentifier()]
  940. addPumpEventData(withSyncIdentifiers: syncIdentifiers)
  941. doseStore.executePumpEventQuery(fromQueryAnchor: queryAnchor, limit: limit) { result in
  942. switch result {
  943. case .failure(let error):
  944. XCTFail("Unexpected failure: \(error)")
  945. case .success(let anchor, let data):
  946. XCTAssertEqual(anchor.modificationCounter, 3)
  947. XCTAssertEqual(data.count, 3)
  948. for (index, syncIdentifier) in syncIdentifiers.enumerated() {
  949. XCTAssertEqual(data[index].raw?.hexadecimalString, syncIdentifier)
  950. }
  951. }
  952. self.completion.fulfill()
  953. }
  954. wait(for: [completion], timeout: 2, enforceOrder: true)
  955. }
  956. func testPumpEventDataWithStaleQueryAnchor() {
  957. let syncIdentifiers = [generateSyncIdentifier(), generateSyncIdentifier(), generateSyncIdentifier()]
  958. addPumpEventData(withSyncIdentifiers: syncIdentifiers)
  959. queryAnchor.modificationCounter = 2
  960. doseStore.executePumpEventQuery(fromQueryAnchor: queryAnchor, limit: limit) { result in
  961. switch result {
  962. case .failure(let error):
  963. XCTFail("Unexpected failure: \(error)")
  964. case .success(let anchor, let data):
  965. XCTAssertEqual(anchor.modificationCounter, 3)
  966. XCTAssertEqual(data.count, 1)
  967. XCTAssertEqual(data[0].raw?.hexadecimalString, syncIdentifiers[2])
  968. }
  969. self.completion.fulfill()
  970. }
  971. wait(for: [completion], timeout: 2, enforceOrder: true)
  972. }
  973. func testPumpEventDataWithCurrentQueryAnchor() {
  974. let syncIdentifiers = [generateSyncIdentifier(), generateSyncIdentifier(), generateSyncIdentifier()]
  975. addPumpEventData(withSyncIdentifiers: syncIdentifiers)
  976. queryAnchor.modificationCounter = 3
  977. doseStore.executePumpEventQuery(fromQueryAnchor: queryAnchor, limit: limit) { result in
  978. switch result {
  979. case .failure(let error):
  980. XCTFail("Unexpected failure: \(error)")
  981. case .success(let anchor, let data):
  982. XCTAssertEqual(anchor.modificationCounter, 3)
  983. XCTAssertEqual(data.count, 0)
  984. }
  985. self.completion.fulfill()
  986. }
  987. wait(for: [completion], timeout: 2, enforceOrder: true)
  988. }
  989. func testPumpEventDataWithLimitCoveredByData() {
  990. let syncIdentifiers = [generateSyncIdentifier(), generateSyncIdentifier(), generateSyncIdentifier()]
  991. addPumpEventData(withSyncIdentifiers: syncIdentifiers)
  992. limit = 2
  993. doseStore.executePumpEventQuery(fromQueryAnchor: queryAnchor, limit: limit) { result in
  994. switch result {
  995. case .failure(let error):
  996. XCTFail("Unexpected failure: \(error)")
  997. case .success(let anchor, let data):
  998. XCTAssertEqual(anchor.modificationCounter, 2)
  999. XCTAssertEqual(data.count, 2)
  1000. XCTAssertEqual(data[0].raw?.hexadecimalString, syncIdentifiers[0])
  1001. XCTAssertEqual(data[1].raw?.hexadecimalString, syncIdentifiers[1])
  1002. }
  1003. self.completion.fulfill()
  1004. }
  1005. wait(for: [completion], timeout: 2, enforceOrder: true)
  1006. }
  1007. private func addPumpEventData(withSyncIdentifiers syncIdentifiers: [String]) {
  1008. cacheStore.managedObjectContext.performAndWait {
  1009. for syncIdentifier in syncIdentifiers {
  1010. let pumpEvent = PumpEvent(context: self.cacheStore.managedObjectContext)
  1011. pumpEvent.date = Date()
  1012. pumpEvent.type = PumpEventType.allCases.randomElement()!
  1013. pumpEvent.raw = Data(hexadecimalString: syncIdentifier)
  1014. self.cacheStore.save()
  1015. }
  1016. }
  1017. }
  1018. private func generateSyncIdentifier() -> String {
  1019. return UUID().data.hexadecimalString
  1020. }
  1021. }
  1022. class DoseStoreCriticalEventLogTests: PersistenceControllerTestCase {
  1023. let insulinModel = WalshInsulinModel(actionDuration: .hours(4))
  1024. let basalProfile = BasalRateSchedule(rawValue: ["timeZone": -28800, "items": [["value": 0.75, "startTime": 0.0], ["value": 0.8, "startTime": 10800.0], ["value": 0.85, "startTime": 32400.0], ["value": 1.0, "startTime": 68400.0]]])
  1025. let insulinSensitivitySchedule = InsulinSensitivitySchedule(rawValue: ["unit": "mg/dL", "timeZone": -28800, "items": [["value": 40.0, "startTime": 0.0], ["value": 35.0, "startTime": 21600.0], ["value": 40.0, "startTime": 57600.0]]])
  1026. var doseStore: DoseStore!
  1027. var outputStream: MockOutputStream!
  1028. var progress: Progress!
  1029. override func setUp() {
  1030. super.setUp()
  1031. let persistedDate = dateFormatter.date(from: "2100-01-02T03:00:00Z")!
  1032. let url = URL(string: "http://a.b.com")!
  1033. let events = [PersistedPumpEvent(date: dateFormatter.date(from: "2100-01-02T03:08:00Z")!, persistedDate: persistedDate, dose: nil, isUploaded: false, objectIDURL: url, raw: nil, title: nil, type: nil),
  1034. PersistedPumpEvent(date: dateFormatter.date(from: "2100-01-02T03:10:00Z")!, persistedDate: persistedDate, dose: nil, isUploaded: false, objectIDURL: url, raw: nil, title: nil, type: nil),
  1035. PersistedPumpEvent(date: dateFormatter.date(from: "2100-01-02T03:04:00Z")!, persistedDate: persistedDate, dose: nil, isUploaded: false, objectIDURL: url, raw: nil, title: nil, type: nil),
  1036. PersistedPumpEvent(date: dateFormatter.date(from: "2100-01-02T03:06:00Z")!, persistedDate: persistedDate, dose: nil, isUploaded: false, objectIDURL: url, raw: nil, title: nil, type: nil),
  1037. PersistedPumpEvent(date: dateFormatter.date(from: "2100-01-02T03:02:00Z")!, persistedDate: persistedDate, dose: nil, isUploaded: false, objectIDURL: url, raw: nil, title: nil, type: nil)]
  1038. doseStore = DoseStore(healthStore: HKHealthStoreMock(),
  1039. cacheStore: cacheStore,
  1040. observationEnabled: false,
  1041. insulinModelProvider: StaticInsulinModelProvider(insulinModel),
  1042. longestEffectDuration: insulinModel.effectDuration,
  1043. basalProfile: basalProfile,
  1044. insulinSensitivitySchedule: insulinSensitivitySchedule,
  1045. provenanceIdentifier: Bundle.main.bundleIdentifier!)
  1046. XCTAssertNil(doseStore.addPumpEvents(events: events))
  1047. outputStream = MockOutputStream()
  1048. progress = Progress()
  1049. }
  1050. override func tearDown() {
  1051. doseStore = nil
  1052. super.tearDown()
  1053. }
  1054. func testExportProgressTotalUnitCount() {
  1055. switch doseStore.exportProgressTotalUnitCount(startDate: dateFormatter.date(from: "2100-01-02T03:03:00Z")!,
  1056. endDate: dateFormatter.date(from: "2100-01-02T03:09:00Z")!) {
  1057. case .failure(let error):
  1058. XCTFail("Unexpected failure: \(error)")
  1059. case .success(let progressTotalUnitCount):
  1060. XCTAssertEqual(progressTotalUnitCount, 3 * 1)
  1061. }
  1062. }
  1063. func testExportProgressTotalUnitCountEmpty() {
  1064. switch doseStore.exportProgressTotalUnitCount(startDate: dateFormatter.date(from: "2100-01-02T03:00:00Z")!,
  1065. endDate: dateFormatter.date(from: "2100-01-02T03:01:00Z")!) {
  1066. case .failure(let error):
  1067. XCTFail("Unexpected failure: \(error)")
  1068. case .success(let progressTotalUnitCount):
  1069. XCTAssertEqual(progressTotalUnitCount, 0)
  1070. }
  1071. }
  1072. func testExport() {
  1073. XCTAssertNil(doseStore.export(startDate: dateFormatter.date(from: "2100-01-02T03:03:00Z")!,
  1074. endDate: dateFormatter.date(from: "2100-01-02T03:09:00Z")!,
  1075. to: outputStream,
  1076. progress: progress))
  1077. XCTAssertEqual(outputStream.string, """
  1078. [
  1079. {"createdAt":"2100-01-02T03:00:00.000Z","date":"2100-01-02T03:08:00.000Z","duration":0,"insulinType":0,"modificationCounter":1,"mutable":false,"uploaded":false,"wasProgrammedByPumpUI":false},
  1080. {"createdAt":"2100-01-02T03:00:00.000Z","date":"2100-01-02T03:04:00.000Z","duration":0,"insulinType":0,"modificationCounter":3,"mutable":false,"uploaded":false,"wasProgrammedByPumpUI":false},
  1081. {"createdAt":"2100-01-02T03:00:00.000Z","date":"2100-01-02T03:06:00.000Z","duration":0,"insulinType":0,"modificationCounter":4,"mutable":false,"uploaded":false,"wasProgrammedByPumpUI":false}
  1082. ]
  1083. """
  1084. )
  1085. XCTAssertEqual(progress.completedUnitCount, 3 * 1)
  1086. }
  1087. func testExportEmpty() {
  1088. XCTAssertNil(doseStore.export(startDate: dateFormatter.date(from: "2100-01-02T03:00:00Z")!,
  1089. endDate: dateFormatter.date(from: "2100-01-02T03:01:00Z")!,
  1090. to: outputStream,
  1091. progress: progress))
  1092. XCTAssertEqual(outputStream.string, "[]")
  1093. XCTAssertEqual(progress.completedUnitCount, 0)
  1094. }
  1095. func testExportCancelled() {
  1096. progress.cancel()
  1097. XCTAssertEqual(doseStore.export(startDate: dateFormatter.date(from: "2100-01-02T03:03:00Z")!,
  1098. endDate: dateFormatter.date(from: "2100-01-02T03:09:00Z")!,
  1099. to: outputStream,
  1100. progress: progress) as? CriticalEventLogError, CriticalEventLogError.cancelled)
  1101. }
  1102. private let dateFormatter = ISO8601DateFormatter()
  1103. }
  1104. class DoseStoreEffectTests: PersistenceControllerTestCase {
  1105. var doseStore: DoseStore!
  1106. var insulinSensitivitySchedule: InsulinSensitivitySchedule {
  1107. return InsulinSensitivitySchedule(unit: HKUnit.milligramsPerDeciliter, dailyItems: [RepeatingScheduleValue(startTime: 0.0, value: 40.0)], timeZone: .currentFixed)!
  1108. }
  1109. let dateFormatter = ISO8601DateFormatter.localTimeDate()
  1110. override func setUp() {
  1111. super.setUp()
  1112. let healthStore = HKHealthStoreMock()
  1113. let exponentialInsulinModel: InsulinModel = ExponentialInsulinModelPreset.rapidActingAdult
  1114. let startDate = dateFormatter.date(from: "2015-07-13T12:00:00")!
  1115. doseStore = DoseStore(
  1116. healthStore: healthStore,
  1117. observeHealthKitSamplesFromOtherApps: false,
  1118. cacheStore: cacheStore,
  1119. observationEnabled: false,
  1120. insulinModelProvider: StaticInsulinModelProvider(exponentialInsulinModel),
  1121. longestEffectDuration: exponentialInsulinModel.effectDuration,
  1122. basalProfile: BasalRateSchedule(dailyItems: [RepeatingScheduleValue(startTime: .hours(0), value: 1.0)]),
  1123. insulinSensitivitySchedule: insulinSensitivitySchedule,
  1124. overrideHistory: TemporaryScheduleOverrideHistory(),
  1125. provenanceIdentifier: Bundle.main.bundleIdentifier!,
  1126. test_currentDate: startDate
  1127. )
  1128. }
  1129. override func tearDown() {
  1130. doseStore = nil
  1131. super.tearDown()
  1132. }
  1133. func loadGlucoseEffectFixture(_ resourceName: String) -> [GlucoseEffect] {
  1134. let fixture: [JSONDictionary] = loadFixture(resourceName)
  1135. let dateFormatter = ISO8601DateFormatter.localTimeDate()
  1136. return fixture.map {
  1137. return GlucoseEffect(startDate: dateFormatter.date(from: $0["date"] as! String)!, quantity: HKQuantity(unit: HKUnit(from: $0["unit"] as! String), doubleValue:$0["amount"] as! Double))
  1138. }
  1139. }
  1140. func loadDoseFixture(_ resourceName: String) -> [DoseEntry] {
  1141. let fixture: [JSONDictionary] = loadFixture(resourceName)
  1142. let dateFormatter = ISO8601DateFormatter.localTimeDate()
  1143. return fixture.compactMap {
  1144. guard let unit = DoseUnit(rawValue: $0["unit"] as! String),
  1145. let pumpType = PumpEventType(rawValue: $0["type"] as! String),
  1146. let type = DoseType(pumpEventType: pumpType)
  1147. else {
  1148. return nil
  1149. }
  1150. var scheduledBasalRate: HKQuantity? = nil
  1151. if let scheduled = $0["scheduled"] as? Double {
  1152. scheduledBasalRate = HKQuantity(unit: unit.unit, doubleValue: scheduled)
  1153. }
  1154. return DoseEntry(
  1155. type: type,
  1156. startDate: dateFormatter.date(from: $0["start_at"] as! String)!,
  1157. endDate: dateFormatter.date(from: $0["end_at"] as! String)!,
  1158. value: $0["amount"] as! Double,
  1159. unit: unit,
  1160. description: $0["description"] as? String,
  1161. syncIdentifier: $0["raw"] as? String,
  1162. scheduledBasalRate: scheduledBasalRate
  1163. )
  1164. }
  1165. }
  1166. func injectDoseEvents(from fixture: String) {
  1167. let events = loadDoseFixture(fixture).map {
  1168. NewPumpEvent(
  1169. date: $0.startDate,
  1170. dose: $0,
  1171. raw: Data(UUID().uuidString.utf8),
  1172. title: "",
  1173. type: $0.type.pumpEventType
  1174. )
  1175. }
  1176. let updateGroup = DispatchGroup()
  1177. updateGroup.enter()
  1178. doseStore.addPumpEvents(events, lastReconciliation: nil) { error in
  1179. if error != nil {
  1180. XCTFail("Doses should be added successfully to dose store")
  1181. }
  1182. updateGroup.leave()
  1183. }
  1184. updateGroup.wait()
  1185. }
  1186. func testGlucoseEffectFromTempBasal() {
  1187. injectDoseEvents(from: "basal_dose")
  1188. let output = loadGlucoseEffectFixture("effect_from_basal_output_exponential")
  1189. var insulinEffects: [GlucoseEffect]!
  1190. let startDate = dateFormatter.date(from: "2015-07-13T12:00:00")!
  1191. let updateGroup = DispatchGroup()
  1192. updateGroup.enter()
  1193. doseStore.getGlucoseEffects(start: startDate) { (result) -> Void in
  1194. switch result {
  1195. case .failure(let error):
  1196. print(error)
  1197. XCTFail("Mock should always return success")
  1198. case .success(let effects):
  1199. insulinEffects = effects
  1200. }
  1201. updateGroup.leave()
  1202. }
  1203. updateGroup.wait()
  1204. XCTAssertEqual(output.count, insulinEffects.count)
  1205. for (expected, calculated) in zip(output, insulinEffects) {
  1206. XCTAssertEqual(expected.startDate, calculated.startDate)
  1207. XCTAssertEqual(expected.quantity.doubleValue(for: HKUnit.milligramsPerDeciliter), calculated.quantity.doubleValue(for: HKUnit.milligramsPerDeciliter), accuracy: 1.0, String(describing: expected.startDate))
  1208. }
  1209. }
  1210. func testGlucoseEffectFromTempBasalWithOldDoses() {
  1211. injectDoseEvents(from: "basal_dose_with_expired")
  1212. let output = loadGlucoseEffectFixture("effect_from_basal_output_exponential")
  1213. var insulinEffects: [GlucoseEffect]!
  1214. let startDate = dateFormatter.date(from: "2015-07-13T12:00:00")!
  1215. let updateGroup = DispatchGroup()
  1216. updateGroup.enter()
  1217. doseStore.getGlucoseEffects(start: startDate) { (result) -> Void in
  1218. switch result {
  1219. case .failure(let error):
  1220. print(error)
  1221. XCTFail("Mock should always return success")
  1222. case .success(let effects):
  1223. insulinEffects = effects
  1224. }
  1225. updateGroup.leave()
  1226. }
  1227. updateGroup.wait()
  1228. XCTAssertEqual(output.count, insulinEffects.count)
  1229. for (expected, calculated) in zip(output, insulinEffects) {
  1230. XCTAssertEqual(expected.startDate, calculated.startDate)
  1231. XCTAssertEqual(expected.quantity.doubleValue(for: HKUnit.milligramsPerDeciliter), calculated.quantity.doubleValue(for: HKUnit.milligramsPerDeciliter), accuracy: 1.0, String(describing: expected.startDate))
  1232. }
  1233. }
  1234. func testGlucoseEffectFromHistory() {
  1235. injectDoseEvents(from: "dose_history_with_delivered_units")
  1236. let output = loadGlucoseEffectFixture("effect_from_history_exponential_delivered_units_output")
  1237. var insulinEffects: [GlucoseEffect]!
  1238. let startDate = dateFormatter.date(from: "2016-01-30T15:40:49")!
  1239. let updateGroup = DispatchGroup()
  1240. updateGroup.enter()
  1241. doseStore.getGlucoseEffects(start: startDate) { (result) -> Void in
  1242. switch result {
  1243. case .failure(let error):
  1244. print(error)
  1245. XCTFail("Mock should always return success")
  1246. case .success(let effects):
  1247. insulinEffects = effects
  1248. }
  1249. updateGroup.leave()
  1250. }
  1251. updateGroup.wait()
  1252. XCTAssertEqual(output.count, insulinEffects.count)
  1253. for (expected, calculated) in zip(output, insulinEffects) {
  1254. XCTAssertEqual(expected.startDate, calculated.startDate)
  1255. XCTAssertEqual(expected.quantity.doubleValue(for: HKUnit.milligramsPerDeciliter), calculated.quantity.doubleValue(for: HKUnit.milligramsPerDeciliter), accuracy: 1.0, String(describing: expected.startDate))
  1256. }
  1257. }
  1258. }