DoseStoreTests.swift 76 KB

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