DoseStoreTests.swift 78 KB

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