DoseStoreTests.swift 83 KB

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