DoseStoreTests.swift 51 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054
  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. insulinModelSettings: InsulinModelSettings(model: WalshInsulinModel(actionDuration: .hours(4))),
  20. 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]]]),
  21. 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]]]),
  22. syncVersion: 1,
  23. provenanceIdentifier: Bundle.main.bundleIdentifier!
  24. )
  25. let queryFinishedExpectation = expectation(description: "query finished")
  26. doseStore.insulinOnBoard(at: Date()) { (result) in
  27. switch result {
  28. case .failure(let error):
  29. XCTFail("Unexpected error: \(error)")
  30. case .success(let value):
  31. XCTAssertEqual(0, value.value)
  32. }
  33. queryFinishedExpectation.fulfill()
  34. }
  35. waitForExpectations(timeout: 3)
  36. }
  37. func testPumpEventTypeDoseMigration() {
  38. cacheStore.managedObjectContext.performAndWait {
  39. let event = PumpEvent(entity: PumpEvent.entity(), insertInto: cacheStore.managedObjectContext)
  40. event.date = Date()
  41. event.duration = .minutes(30)
  42. event.unit = .unitsPerHour
  43. event.type = .tempBasal
  44. event.value = 0.5
  45. event.doseType = nil
  46. XCTAssertNotNil(event.dose)
  47. XCTAssertEqual(.tempBasal, event.dose!.type)
  48. }
  49. }
  50. func testDeduplication() {
  51. cacheStore.managedObjectContext.performAndWait {
  52. let bolus1 = PumpEvent(context: cacheStore.managedObjectContext)
  53. bolus1.date = DateFormatter.descriptionFormatter.date(from: "2018-04-30 02:12:42 +0000")
  54. bolus1.raw = Data(hexadecimalString: "0100a600a6001b006a0c335d12")!
  55. bolus1.type = PumpEventType.bolus
  56. bolus1.dose = DoseEntry(type: .bolus, startDate: bolus1.date!, value: 4.15, unit: .units, syncIdentifier: bolus1.raw?.hexadecimalString)
  57. let bolus2 = PumpEvent(context: cacheStore.managedObjectContext)
  58. bolus2.date = DateFormatter.descriptionFormatter.date(from: "2018-04-30 00:00:00 +0000")
  59. bolus2.raw = Data(hexadecimalString: "0100a600a6001b006a0c335d12")!
  60. bolus2.type = PumpEventType.bolus
  61. bolus2.dose = DoseEntry(type: .bolus, startDate: bolus2.date!, value: 0.15, unit: .units, syncIdentifier: bolus1.raw?.hexadecimalString)
  62. let request: NSFetchRequest<PumpEvent> = PumpEvent.fetchRequest()
  63. let eventsBeforeSave = try! cacheStore.managedObjectContext.fetch(request)
  64. XCTAssertEqual(2, eventsBeforeSave.count)
  65. try! cacheStore.managedObjectContext.save()
  66. let eventsAfterSave = try! cacheStore.managedObjectContext.fetch(request)
  67. XCTAssertEqual(1, eventsAfterSave.count)
  68. }
  69. }
  70. /// See https://github.com/LoopKit/Loop/issues/853
  71. func testOutOfOrderDosesSyncedToHealth() {
  72. let formatter = DateFormatter.descriptionFormatter
  73. let f = { (input) in
  74. return formatter.date(from: input)!
  75. }
  76. // 1. Create a DoseStore
  77. let healthStore = HKHealthStoreMock()
  78. let doseStore = DoseStore(
  79. healthStore: healthStore,
  80. cacheStore: cacheStore,
  81. observationEnabled: false,
  82. insulinModelSettings: InsulinModelSettings(model: WalshInsulinModel(actionDuration: .hours(4))),
  83. 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]]]),
  84. 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]]]),
  85. syncVersion: 1,
  86. provenanceIdentifier: Bundle.main.bundleIdentifier!,
  87. // Set the current date
  88. test_currentDate: f("2018-12-12 18:07:14 +0000")
  89. )
  90. // 2. Add a temp basal which has already ended. It should be saved to Health
  91. let pumpEvents1 = [
  92. NewPumpEvent(date: f("2018-12-12 17:35:58 +0000"), dose: nil, isMutable: false, 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),
  93. 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), isMutable: false, 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)
  94. ]
  95. doseStore.insulinDeliveryStore.test_lastBasalEndDate = f("2018-12-12 17:35:58 +0000")
  96. let addPumpEvents1 = expectation(description: "add pumpEvents1")
  97. addPumpEvents1.expectedFulfillmentCount = 2
  98. healthStore.setSaveHandler({ (objects, success, error) in
  99. XCTAssertEqual(1, objects.count)
  100. let sample = objects.first as! HKQuantitySample
  101. XCTAssertEqual(HKInsulinDeliveryReason.basal, sample.insulinDeliveryReason)
  102. XCTAssertNil(error)
  103. addPumpEvents1.fulfill()
  104. })
  105. let lastBasalEndDateSetExpectation = expectation(description: "last basal end date set")
  106. lastBasalEndDateSetExpectation.assertForOverFulfill = false
  107. doseStore.insulinDeliveryStore.test_lastBasalEndDateDidSet = {
  108. lastBasalEndDateSetExpectation.fulfill()
  109. }
  110. doseStore.addPumpEvents(pumpEvents1, lastReconciliation: Date()) { (error) in
  111. XCTAssertNil(error)
  112. addPumpEvents1.fulfill()
  113. }
  114. waitForExpectations(timeout: 3)
  115. XCTAssertEqual(f("2018-12-12 18:05:58 +0000"), doseStore.insulinDeliveryStore.test_lastBasalEndDate)
  116. // 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)
  117. // Even though it is before lastBasalEndDate, it should be saved to HealthKit.
  118. doseStore.insulinDeliveryStore.test_currentDate = f("2018-12-12 18:16:23 +0000")
  119. let pumpEvents2 = [
  120. 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), isMutable: false, 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)
  121. ]
  122. let addPumpEvents2 = expectation(description: "add pumpEvents2")
  123. addPumpEvents2.expectedFulfillmentCount = 3
  124. healthStore.setSaveHandler({ (objects, success, error) in
  125. XCTAssertEqual(1, objects.count)
  126. let sample = objects.first as! HKQuantitySample
  127. XCTAssertEqual(HKInsulinDeliveryReason.bolus, sample.insulinDeliveryReason)
  128. XCTAssertEqual(5.0, sample.quantity.doubleValue(for: .internationalUnit()))
  129. XCTAssertEqual(f("2018-12-12 18:05:14 +0000"), sample.startDate)
  130. XCTAssertNil(error)
  131. addPumpEvents2.fulfill()
  132. })
  133. doseStore.insulinDeliveryStore.test_lastBasalEndDateDidSet = {
  134. addPumpEvents2.fulfill()
  135. }
  136. doseStore.addPumpEvents(pumpEvents2, lastReconciliation: Date()) { (error) in
  137. XCTAssertNil(error)
  138. addPumpEvents2.fulfill()
  139. }
  140. waitForExpectations(timeout: 3)
  141. XCTAssertEqual(f("2018-12-12 18:05:58 +0000"), doseStore.insulinDeliveryStore.test_lastBasalEndDate)
  142. // Add the next set of pump events, which haven't completed and shouldn't be saved to HealthKit
  143. doseStore.insulinDeliveryStore.test_currentDate = f("2018-12-12 18:21:22 +0000")
  144. let pumpEvents3 = [
  145. NewPumpEvent(date: f("2018-12-12 18:16:31 +0000"), dose: nil, isMutable: false, 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),
  146. 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), isMutable: false, 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),
  147. ]
  148. let addPumpEvents3 = expectation(description: "add pumpEvents3")
  149. addPumpEvents3.expectedFulfillmentCount = 1
  150. healthStore.setSaveHandler({ (objects, success, error) in
  151. XCTFail()
  152. })
  153. doseStore.insulinDeliveryStore.test_lastBasalEndDateDidSet = {
  154. XCTFail()
  155. }
  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_lastBasalEndDate)
  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. insulinModelSettings: InsulinModelSettings(model: WalshInsulinModel(actionDuration: .hours(4))),
  176. 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]]]),
  177. 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]]]),
  178. syncVersion: 1,
  179. provenanceIdentifier: Bundle.main.bundleIdentifier!,
  180. // Set the current date (5 minutes later)
  181. test_currentDate: f("2018-11-29 11:04:27 +0000")
  182. )
  183. doseStore.pumpRecordsBasalProfileStartEvents = false
  184. doseStore.insulinDeliveryStore.test_lastBasalEndDate = f("2018-11-29 10:54:28 +0000")
  185. // Add a temp basal. It hasn't finished yet, and should not be saved to Health
  186. let pumpEvents1 = [
  187. NewPumpEvent(date: f("2018-11-29 10:59:28 +0000"), dose: nil, isMutable: false, 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),
  188. 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), isMutable: false, 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)
  189. ]
  190. let addPumpEvents1 = expectation(description: "add pumpEvents1")
  191. addPumpEvents1.expectedFulfillmentCount = 1
  192. healthStore.setSaveHandler({ (objects, success, error) in
  193. XCTFail()
  194. })
  195. doseStore.addPumpEvents(pumpEvents1, lastReconciliation: Date()) { (error) in
  196. XCTAssertNil(error)
  197. addPumpEvents1.fulfill()
  198. }
  199. waitForExpectations(timeout: 3)
  200. XCTAssertEqual(f("2018-11-29 10:54:28 +0000"), doseStore.insulinDeliveryStore.test_lastBasalEndDate)
  201. XCTAssertEqual(f("2018-11-29 10:59:28 +0000"), doseStore.pumpEventQueryAfterDate)
  202. // Add the next query of the same pump events (no new data) 5 minutes later. Expect the same result
  203. doseStore.insulinDeliveryStore.test_currentDate = f("2018-11-29 11:09:27 +0000")
  204. let addPumpEvents2 = expectation(description: "add pumpEvents2")
  205. addPumpEvents2.expectedFulfillmentCount = 1
  206. healthStore.setSaveHandler({ (objects, success, error) in
  207. XCTFail()
  208. })
  209. doseStore.insulinDeliveryStore.test_lastBasalEndDateDidSet = {
  210. XCTFail()
  211. }
  212. doseStore.addPumpEvents(pumpEvents1, lastReconciliation: Date()) { (error) in
  213. XCTAssertNil(error)
  214. addPumpEvents2.fulfill()
  215. }
  216. waitForExpectations(timeout: 3)
  217. XCTAssertEqual(f("2018-11-29 10:54:28 +0000"), doseStore.insulinDeliveryStore.test_lastBasalEndDate)
  218. XCTAssertEqual(f("2018-11-29 10:59:28 +0000"), doseStore.pumpEventQueryAfterDate)
  219. // Add the next set of pump events, including the last temp basal change.
  220. // The previous, completed basal entries should be saved to Health
  221. doseStore.insulinDeliveryStore.test_currentDate = f("2018-11-29 11:14:28 +0000")
  222. let pumpEvents3 = [
  223. NewPumpEvent(date: f("2018-11-29 11:09:27 +0000"), dose: nil, isMutable: false, 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),
  224. 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), isMutable: false, 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)
  225. ]
  226. let addPumpEvents3 = expectation(description: "add pumpEvents3")
  227. addPumpEvents3.expectedFulfillmentCount = 3
  228. healthStore.setSaveHandler({ (objects, success, error) in
  229. XCTAssertEqual(3, objects.count)
  230. let basal = objects[0] as! HKQuantitySample
  231. XCTAssertEqual(HKInsulinDeliveryReason.basal, basal.insulinDeliveryReason)
  232. XCTAssertEqual(f("2018-11-29 10:54:28 +0000"), basal.startDate)
  233. XCTAssertEqual(f("2018-11-29 10:59:28 +0000"), basal.endDate)
  234. XCTAssertEqual("BasalRateSchedule 2018-11-29T10:54:28Z 2018-11-29T10:59:28Z", basal.metadata![HKMetadataKeySyncIdentifier] as! String)
  235. let temp1 = objects[1] as! HKQuantitySample
  236. XCTAssertEqual(HKInsulinDeliveryReason.basal, temp1.insulinDeliveryReason)
  237. XCTAssertEqual(f("2018-11-29 10:59:28 +0000"), temp1.startDate)
  238. XCTAssertEqual(f("2018-11-29 11:00:00 +0000"), temp1.endDate)
  239. XCTAssertEqual("5bffc7cace53e48e87f7cfcb 1/2", temp1.metadata![HKMetadataKeySyncIdentifier] as! String)
  240. XCTAssertEqual(0.003, temp1.quantity.doubleValue(for: .internationalUnit()), accuracy: 0.01)
  241. let temp2 = objects[2] as! HKQuantitySample
  242. XCTAssertEqual(HKInsulinDeliveryReason.basal, temp2.insulinDeliveryReason)
  243. XCTAssertEqual(f("2018-11-29 11:00:00 +0000"), temp2.startDate)
  244. XCTAssertEqual(f("2018-11-29 11:09:27 +0000"), temp2.endDate)
  245. XCTAssertEqual("5bffc7cace53e48e87f7cfcb 2/2", temp2.metadata![HKMetadataKeySyncIdentifier] as! String)
  246. XCTAssertEqual(0.047, temp2.quantity.doubleValue(for: .internationalUnit()), accuracy: 0.01)
  247. XCTAssertNil(error)
  248. addPumpEvents3.fulfill()
  249. })
  250. doseStore.insulinDeliveryStore.test_lastBasalEndDateDidSet = {
  251. addPumpEvents3.fulfill()
  252. }
  253. doseStore.addPumpEvents(pumpEvents3, lastReconciliation: Date()) { (error) in
  254. XCTAssertNil(error)
  255. addPumpEvents3.fulfill()
  256. }
  257. waitForExpectations(timeout: 3)
  258. XCTAssertEqual(f("2018-11-29 11:09:27 +0000"), doseStore.insulinDeliveryStore.test_lastBasalEndDate)
  259. XCTAssertEqual(f("2018-11-29 11:09:27 +0000"), doseStore.pumpEventQueryAfterDate)
  260. // Add the next set of pump events, including the last temp basal cancel
  261. doseStore.insulinDeliveryStore.test_currentDate = f("2018-11-29 11:19:28 +0000")
  262. let pumpEvents4 = [
  263. NewPumpEvent(date: f("2018-11-29 11:14:28 +0000"), dose: nil, isMutable: false, 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),
  264. 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), isMutable: false, 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)
  265. ]
  266. let addPumpEvents4 = expectation(description: "add pumpEvents4")
  267. addPumpEvents4.expectedFulfillmentCount = 3
  268. healthStore.setSaveHandler({ (objects, success, error) in
  269. XCTAssertEqual(1, objects.count)
  270. let temp = objects[0] as! HKQuantitySample
  271. XCTAssertEqual(HKInsulinDeliveryReason.basal, temp.insulinDeliveryReason)
  272. XCTAssertEqual(f("2018-11-29 11:09:27 +0000"), temp.startDate)
  273. XCTAssertEqual(f("2018-11-29 11:14:28 +0000"), temp.endDate)
  274. XCTAssertEqual("5bffca22ce53e48e87f7d624", temp.metadata![HKMetadataKeySyncIdentifier] as! String)
  275. XCTAssertEqual(0.05, temp.quantity.doubleValue(for: .internationalUnit()), accuracy: 0.01)
  276. XCTAssertNil(error)
  277. addPumpEvents4.fulfill()
  278. })
  279. doseStore.insulinDeliveryStore.test_lastBasalEndDateDidSet = {
  280. addPumpEvents4.fulfill()
  281. }
  282. doseStore.addPumpEvents(pumpEvents4, lastReconciliation: Date()) { (error) in
  283. XCTAssertNil(error)
  284. addPumpEvents4.fulfill()
  285. }
  286. waitForExpectations(timeout: 3)
  287. XCTAssertEqual(f("2018-11-29 11:14:28 +0000"), doseStore.pumpEventQueryAfterDate)
  288. XCTAssertEqual(f("2018-11-29 11:14:28 +0000"), doseStore.insulinDeliveryStore.test_lastBasalEndDate)
  289. }
  290. }
  291. class DoseStoreQueryAnchorTests: XCTestCase {
  292. var rawValue: DoseStore.QueryAnchor.RawValue = [
  293. "modificationCounter": Int64(123)
  294. ]
  295. func testInitializerDefault() {
  296. let queryAnchor = DoseStore.QueryAnchor()
  297. XCTAssertEqual(queryAnchor.modificationCounter, 0)
  298. }
  299. func testInitializerRawValue() {
  300. let queryAnchor = DoseStore.QueryAnchor(rawValue: rawValue)
  301. XCTAssertNotNil(queryAnchor)
  302. XCTAssertEqual(queryAnchor?.modificationCounter, 123)
  303. }
  304. func testInitializerRawValueMissingModificationCounter() {
  305. rawValue["modificationCounter"] = nil
  306. XCTAssertNil(DoseStore.QueryAnchor(rawValue: rawValue))
  307. }
  308. func testInitializerRawValueInvalidModificationCounter() {
  309. rawValue["modificationCounter"] = "123"
  310. XCTAssertNil(DoseStore.QueryAnchor(rawValue: rawValue))
  311. }
  312. func testRawValueWithDefault() {
  313. let rawValue = DoseStore.QueryAnchor().rawValue
  314. XCTAssertEqual(rawValue.count, 1)
  315. XCTAssertEqual(rawValue["modificationCounter"] as? Int64, Int64(0))
  316. }
  317. func testRawValueWithNonDefault() {
  318. var queryAnchor = DoseStore.QueryAnchor()
  319. queryAnchor.modificationCounter = 123
  320. let rawValue = queryAnchor.rawValue
  321. XCTAssertEqual(rawValue.count, 1)
  322. XCTAssertEqual(rawValue["modificationCounter"] as? Int64, Int64(123))
  323. }
  324. }
  325. class DoseStoreQueryTests: PersistenceControllerTestCase {
  326. let insulinModel = WalshInsulinModel(actionDuration: .hours(4))
  327. 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]]])
  328. 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]]])
  329. var doseStore: DoseStore!
  330. var completion: XCTestExpectation!
  331. var queryAnchor: DoseStore.QueryAnchor!
  332. var limit: Int!
  333. override func setUp() {
  334. super.setUp()
  335. doseStore = DoseStore(healthStore: HKHealthStoreMock(),
  336. cacheStore: cacheStore,
  337. observationEnabled: false,
  338. insulinModelSettings: InsulinModelSettings(model: insulinModel),
  339. basalProfile: basalProfile,
  340. insulinSensitivitySchedule: insulinSensitivitySchedule,
  341. provenanceIdentifier: Bundle.main.bundleIdentifier!)
  342. completion = expectation(description: "Completion")
  343. queryAnchor = DoseStore.QueryAnchor()
  344. limit = Int.max
  345. }
  346. override func tearDown() {
  347. limit = nil
  348. queryAnchor = nil
  349. completion = nil
  350. doseStore = nil
  351. super.tearDown()
  352. }
  353. func testDoseEmptyWithDefaultQueryAnchor() {
  354. doseStore.executeDoseQuery(fromQueryAnchor: queryAnchor, limit: limit) { result in
  355. switch result {
  356. case .failure(let error):
  357. XCTFail("Unexpected failure: \(error)")
  358. case .success(let anchor, let data):
  359. XCTAssertEqual(anchor.modificationCounter, 0)
  360. XCTAssertEqual(data.count, 0)
  361. }
  362. self.completion.fulfill()
  363. }
  364. wait(for: [completion], timeout: 2, enforceOrder: true)
  365. }
  366. func testDoseEmptyWithMissingQueryAnchor() {
  367. queryAnchor = nil
  368. doseStore.executeDoseQuery(fromQueryAnchor: queryAnchor, limit: limit) { result in
  369. switch result {
  370. case .failure(let error):
  371. XCTFail("Unexpected failure: \(error)")
  372. case .success(let anchor, let data):
  373. XCTAssertEqual(anchor.modificationCounter, 0)
  374. XCTAssertEqual(data.count, 0)
  375. }
  376. self.completion.fulfill()
  377. }
  378. wait(for: [completion], timeout: 2, enforceOrder: true)
  379. }
  380. func testDoseEmptyWithNonDefaultQueryAnchor() {
  381. queryAnchor.modificationCounter = 1
  382. doseStore.executeDoseQuery(fromQueryAnchor: queryAnchor, limit: limit) { result in
  383. switch result {
  384. case .failure(let error):
  385. XCTFail("Unexpected failure: \(error)")
  386. case .success(let anchor, let data):
  387. XCTAssertEqual(anchor.modificationCounter, 1)
  388. XCTAssertEqual(data.count, 0)
  389. }
  390. self.completion.fulfill()
  391. }
  392. wait(for: [completion], timeout: 2, enforceOrder: true)
  393. }
  394. func testDoseDataWithUnusedQueryAnchor() {
  395. let syncIdentifiers = [generateSyncIdentifier(), generateSyncIdentifier(), generateSyncIdentifier()]
  396. addPumpEventData(withSyncIdentifiers: [generateSyncIdentifier(), generateSyncIdentifier()])
  397. addDoseData(withSyncIdentifiers: syncIdentifiers)
  398. addPumpEventData(withSyncIdentifiers: [generateSyncIdentifier(), generateSyncIdentifier()])
  399. doseStore.executeDoseQuery(fromQueryAnchor: queryAnchor, limit: limit) { result in
  400. switch result {
  401. case .failure(let error):
  402. XCTFail("Unexpected failure: \(error)")
  403. case .success(let anchor, let data):
  404. XCTAssertEqual(anchor.modificationCounter, 5)
  405. XCTAssertEqual(data.count, 3)
  406. for (index, syncIdentifier) in syncIdentifiers.enumerated() {
  407. XCTAssertEqual(data[index].syncIdentifier, syncIdentifier)
  408. }
  409. }
  410. self.completion.fulfill()
  411. }
  412. wait(for: [completion], timeout: 2, enforceOrder: true)
  413. }
  414. func testDoseDataWithStaleQueryAnchor() {
  415. let syncIdentifiers = [generateSyncIdentifier(), generateSyncIdentifier(), generateSyncIdentifier()]
  416. addPumpEventData(withSyncIdentifiers: [generateSyncIdentifier(), generateSyncIdentifier()])
  417. addDoseData(withSyncIdentifiers: syncIdentifiers)
  418. addPumpEventData(withSyncIdentifiers: [generateSyncIdentifier(), generateSyncIdentifier()])
  419. queryAnchor.modificationCounter = 4
  420. doseStore.executeDoseQuery(fromQueryAnchor: queryAnchor, limit: limit) { result in
  421. switch result {
  422. case .failure(let error):
  423. XCTFail("Unexpected failure: \(error)")
  424. case .success(let anchor, let data):
  425. XCTAssertEqual(anchor.modificationCounter, 5)
  426. XCTAssertEqual(data.count, 1)
  427. XCTAssertEqual(data[0].syncIdentifier, syncIdentifiers[2])
  428. }
  429. self.completion.fulfill()
  430. }
  431. wait(for: [completion], timeout: 2, enforceOrder: true)
  432. }
  433. func testDoseDataWithCurrentQueryAnchor() {
  434. let syncIdentifiers = [generateSyncIdentifier(), generateSyncIdentifier(), generateSyncIdentifier()]
  435. addPumpEventData(withSyncIdentifiers: [generateSyncIdentifier(), generateSyncIdentifier()])
  436. addDoseData(withSyncIdentifiers: syncIdentifiers)
  437. addPumpEventData(withSyncIdentifiers: [generateSyncIdentifier(), generateSyncIdentifier()])
  438. queryAnchor.modificationCounter = 5
  439. doseStore.executeDoseQuery(fromQueryAnchor: queryAnchor, limit: limit) { result in
  440. switch result {
  441. case .failure(let error):
  442. XCTFail("Unexpected failure: \(error)")
  443. case .success(let anchor, let data):
  444. XCTAssertEqual(anchor.modificationCounter, 5)
  445. XCTAssertEqual(data.count, 0)
  446. }
  447. self.completion.fulfill()
  448. }
  449. wait(for: [completion], timeout: 2, enforceOrder: true)
  450. }
  451. func testDoseDataWithLimitZero() {
  452. let syncIdentifiers = [generateSyncIdentifier(), generateSyncIdentifier(), generateSyncIdentifier()]
  453. addPumpEventData(withSyncIdentifiers: [generateSyncIdentifier(), generateSyncIdentifier()])
  454. addDoseData(withSyncIdentifiers: syncIdentifiers)
  455. addPumpEventData(withSyncIdentifiers: [generateSyncIdentifier(), generateSyncIdentifier()])
  456. limit = 0
  457. doseStore.executeDoseQuery(fromQueryAnchor: queryAnchor, limit: limit) { result in
  458. switch result {
  459. case .failure(let error):
  460. XCTFail("Unexpected failure: \(error)")
  461. case .success(let anchor, let data):
  462. XCTAssertEqual(anchor.modificationCounter, 0)
  463. XCTAssertEqual(data.count, 0)
  464. }
  465. self.completion.fulfill()
  466. }
  467. wait(for: [completion], timeout: 2, enforceOrder: true)
  468. }
  469. func testDoseDataWithLimitCoveredByData() {
  470. let syncIdentifiers = [generateSyncIdentifier(), generateSyncIdentifier(), generateSyncIdentifier()]
  471. addPumpEventData(withSyncIdentifiers: [generateSyncIdentifier(), generateSyncIdentifier()])
  472. addDoseData(withSyncIdentifiers: syncIdentifiers)
  473. addPumpEventData(withSyncIdentifiers: [generateSyncIdentifier(), generateSyncIdentifier()])
  474. limit = 2
  475. doseStore.executeDoseQuery(fromQueryAnchor: queryAnchor, limit: limit) { result in
  476. switch result {
  477. case .failure(let error):
  478. XCTFail("Unexpected failure: \(error)")
  479. case .success(let anchor, let data):
  480. XCTAssertEqual(anchor.modificationCounter, 4)
  481. XCTAssertEqual(data.count, 2)
  482. XCTAssertEqual(data[0].syncIdentifier, syncIdentifiers[0])
  483. XCTAssertEqual(data[1].syncIdentifier, syncIdentifiers[1])
  484. }
  485. self.completion.fulfill()
  486. }
  487. wait(for: [completion], timeout: 2, enforceOrder: true)
  488. }
  489. func testPumpEventEmptyWithDefaultQueryAnchor() {
  490. doseStore.executePumpEventQuery(fromQueryAnchor: queryAnchor, limit: limit) { result in
  491. switch result {
  492. case .failure(let error):
  493. XCTFail("Unexpected failure: \(error)")
  494. case .success(let anchor, let data):
  495. XCTAssertEqual(anchor.modificationCounter, 0)
  496. XCTAssertEqual(data.count, 0)
  497. }
  498. self.completion.fulfill()
  499. }
  500. wait(for: [completion], timeout: 2, enforceOrder: true)
  501. }
  502. func testPumpEventEmptyWithMissingQueryAnchor() {
  503. queryAnchor = nil
  504. doseStore.executePumpEventQuery(fromQueryAnchor: queryAnchor, limit: limit) { result in
  505. switch result {
  506. case .failure(let error):
  507. XCTFail("Unexpected failure: \(error)")
  508. case .success(let anchor, let data):
  509. XCTAssertEqual(anchor.modificationCounter, 0)
  510. XCTAssertEqual(data.count, 0)
  511. }
  512. self.completion.fulfill()
  513. }
  514. wait(for: [completion], timeout: 2, enforceOrder: true)
  515. }
  516. func testPumpEventEmptyWithNonDefaultQueryAnchor() {
  517. queryAnchor.modificationCounter = 1
  518. doseStore.executePumpEventQuery(fromQueryAnchor: queryAnchor, limit: limit) { result in
  519. switch result {
  520. case .failure(let error):
  521. XCTFail("Unexpected failure: \(error)")
  522. case .success(let anchor, let data):
  523. XCTAssertEqual(anchor.modificationCounter, 1)
  524. XCTAssertEqual(data.count, 0)
  525. }
  526. self.completion.fulfill()
  527. }
  528. wait(for: [completion], timeout: 2, enforceOrder: true)
  529. }
  530. func testPumpEventDataWithUnusedQueryAnchor() {
  531. let syncIdentifiers = [generateSyncIdentifier(), generateSyncIdentifier(), generateSyncIdentifier()]
  532. addDoseData(withSyncIdentifiers: [generateSyncIdentifier(), generateSyncIdentifier()])
  533. addPumpEventData(withSyncIdentifiers: syncIdentifiers)
  534. addDoseData(withSyncIdentifiers: [generateSyncIdentifier(), generateSyncIdentifier()])
  535. doseStore.executePumpEventQuery(fromQueryAnchor: queryAnchor, limit: limit) { result in
  536. switch result {
  537. case .failure(let error):
  538. XCTFail("Unexpected failure: \(error)")
  539. case .success(let anchor, let data):
  540. XCTAssertEqual(anchor.modificationCounter, 5)
  541. XCTAssertEqual(data.count, 3)
  542. for (index, syncIdentifier) in syncIdentifiers.enumerated() {
  543. XCTAssertEqual(data[index].raw?.hexadecimalString, syncIdentifier)
  544. }
  545. }
  546. self.completion.fulfill()
  547. }
  548. wait(for: [completion], timeout: 2, enforceOrder: true)
  549. }
  550. func testPumpEventDataWithStaleQueryAnchor() {
  551. let syncIdentifiers = [generateSyncIdentifier(), generateSyncIdentifier(), generateSyncIdentifier()]
  552. addDoseData(withSyncIdentifiers: [generateSyncIdentifier(), generateSyncIdentifier()])
  553. addPumpEventData(withSyncIdentifiers: syncIdentifiers)
  554. addDoseData(withSyncIdentifiers: [generateSyncIdentifier(), generateSyncIdentifier()])
  555. queryAnchor.modificationCounter = 4
  556. doseStore.executePumpEventQuery(fromQueryAnchor: queryAnchor, limit: limit) { result in
  557. switch result {
  558. case .failure(let error):
  559. XCTFail("Unexpected failure: \(error)")
  560. case .success(let anchor, let data):
  561. XCTAssertEqual(anchor.modificationCounter, 5)
  562. XCTAssertEqual(data.count, 1)
  563. XCTAssertEqual(data[0].raw?.hexadecimalString, syncIdentifiers[2])
  564. }
  565. self.completion.fulfill()
  566. }
  567. wait(for: [completion], timeout: 2, enforceOrder: true)
  568. }
  569. func testPumpEventDataWithCurrentQueryAnchor() {
  570. let syncIdentifiers = [generateSyncIdentifier(), generateSyncIdentifier(), generateSyncIdentifier()]
  571. addDoseData(withSyncIdentifiers: [generateSyncIdentifier(), generateSyncIdentifier()])
  572. addPumpEventData(withSyncIdentifiers: syncIdentifiers)
  573. addDoseData(withSyncIdentifiers: [generateSyncIdentifier(), generateSyncIdentifier()])
  574. queryAnchor.modificationCounter = 5
  575. doseStore.executePumpEventQuery(fromQueryAnchor: queryAnchor, limit: limit) { result in
  576. switch result {
  577. case .failure(let error):
  578. XCTFail("Unexpected failure: \(error)")
  579. case .success(let anchor, let data):
  580. XCTAssertEqual(anchor.modificationCounter, 5)
  581. XCTAssertEqual(data.count, 0)
  582. }
  583. self.completion.fulfill()
  584. }
  585. wait(for: [completion], timeout: 2, enforceOrder: true)
  586. }
  587. func testPumpEventDataWithLimitCoveredByData() {
  588. let syncIdentifiers = [generateSyncIdentifier(), generateSyncIdentifier(), generateSyncIdentifier()]
  589. addDoseData(withSyncIdentifiers: [generateSyncIdentifier(), generateSyncIdentifier()])
  590. addPumpEventData(withSyncIdentifiers: syncIdentifiers)
  591. addDoseData(withSyncIdentifiers: [generateSyncIdentifier(), generateSyncIdentifier()])
  592. limit = 2
  593. doseStore.executePumpEventQuery(fromQueryAnchor: queryAnchor, limit: limit) { result in
  594. switch result {
  595. case .failure(let error):
  596. XCTFail("Unexpected failure: \(error)")
  597. case .success(let anchor, let data):
  598. XCTAssertEqual(anchor.modificationCounter, 4)
  599. XCTAssertEqual(data.count, 2)
  600. XCTAssertEqual(data[0].raw?.hexadecimalString, syncIdentifiers[0])
  601. XCTAssertEqual(data[1].raw?.hexadecimalString, syncIdentifiers[1])
  602. }
  603. self.completion.fulfill()
  604. }
  605. wait(for: [completion], timeout: 2, enforceOrder: true)
  606. }
  607. private func addDoseData(withSyncIdentifiers syncIdentifiers: [String]) {
  608. cacheStore.managedObjectContext.performAndWait {
  609. for syncIdentifier in syncIdentifiers {
  610. let pumpEvent = PumpEvent(context: self.cacheStore.managedObjectContext)
  611. pumpEvent.type = PumpEventType.doseTypes.randomElement()!
  612. switch pumpEvent.type {
  613. case .basal:
  614. pumpEvent.dose = DoseEntry(type: .basal, startDate: Date(), value: 0.75, unit: .unitsPerHour)
  615. case .bolus:
  616. pumpEvent.dose = DoseEntry(type: .bolus, startDate: Date(), value: 1.25, unit: .units)
  617. case .resume:
  618. pumpEvent.dose = DoseEntry(resumeDate: Date())
  619. case .suspend:
  620. pumpEvent.dose = DoseEntry(suspendDate: Date())
  621. case .tempBasal:
  622. pumpEvent.dose = DoseEntry(type: .tempBasal, startDate: Date(), value: 0, unit: .units)
  623. default:
  624. break
  625. }
  626. pumpEvent.raw = Data(hexadecimalString: syncIdentifier)
  627. self.cacheStore.save()
  628. }
  629. }
  630. }
  631. private func addPumpEventData(withSyncIdentifiers syncIdentifiers: [String]) {
  632. cacheStore.managedObjectContext.performAndWait {
  633. for syncIdentifier in syncIdentifiers {
  634. let pumpEvent = PumpEvent(context: self.cacheStore.managedObjectContext)
  635. pumpEvent.date = Date()
  636. pumpEvent.type = PumpEventType.nonDoseTypes.randomElement()!
  637. pumpEvent.raw = Data(hexadecimalString: syncIdentifier)
  638. self.cacheStore.save()
  639. }
  640. }
  641. }
  642. private func generateSyncIdentifier() -> String {
  643. return UUID().data.hexadecimalString
  644. }
  645. }
  646. class DoseStoreCriticalEventLogTests: PersistenceControllerTestCase {
  647. let insulinModel = WalshInsulinModel(actionDuration: .hours(4))
  648. 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]]])
  649. 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]]])
  650. var doseStore: DoseStore!
  651. var outputStream: MockOutputStream!
  652. var progress: Progress!
  653. override func setUp() {
  654. super.setUp()
  655. let persistedDate = dateFormatter.date(from: "2100-01-02T03:000:00Z")!
  656. let url = URL(string: "http://a.b.com")!
  657. 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, isMutable: false),
  658. PersistedPumpEvent(date: dateFormatter.date(from: "2100-01-02T03:10:00Z")!, persistedDate: persistedDate, dose: nil, isUploaded: false, objectIDURL: url, raw: nil, title: nil, type: nil, isMutable: false),
  659. PersistedPumpEvent(date: dateFormatter.date(from: "2100-01-02T03:04:00Z")!, persistedDate: persistedDate, dose: nil, isUploaded: false, objectIDURL: url, raw: nil, title: nil, type: nil, isMutable: false),
  660. PersistedPumpEvent(date: dateFormatter.date(from: "2100-01-02T03:06:00Z")!, persistedDate: persistedDate, dose: nil, isUploaded: false, objectIDURL: url, raw: nil, title: nil, type: nil, isMutable: false),
  661. PersistedPumpEvent(date: dateFormatter.date(from: "2100-01-02T03:02:00Z")!, persistedDate: persistedDate, dose: nil, isUploaded: false, objectIDURL: url, raw: nil, title: nil, type: nil, isMutable: false)]
  662. doseStore = DoseStore(healthStore: HKHealthStoreMock(),
  663. cacheStore: cacheStore,
  664. observationEnabled: false,
  665. insulinModelSettings: InsulinModelSettings(model: insulinModel),
  666. basalProfile: basalProfile,
  667. insulinSensitivitySchedule: insulinSensitivitySchedule,
  668. provenanceIdentifier: Bundle.main.bundleIdentifier!)
  669. XCTAssertNil(doseStore.addPumpEvents(events: events))
  670. outputStream = MockOutputStream()
  671. progress = Progress()
  672. }
  673. override func tearDown() {
  674. doseStore = nil
  675. super.tearDown()
  676. }
  677. func testExportProgressTotalUnitCount() {
  678. switch doseStore.exportProgressTotalUnitCount(startDate: dateFormatter.date(from: "2100-01-02T03:03:00Z")!,
  679. endDate: dateFormatter.date(from: "2100-01-02T03:09:00Z")!) {
  680. case .failure(let error):
  681. XCTFail("Unexpected failure: \(error)")
  682. case .success(let progressTotalUnitCount):
  683. XCTAssertEqual(progressTotalUnitCount, 3 * 1)
  684. }
  685. }
  686. func testExportProgressTotalUnitCountEmpty() {
  687. switch doseStore.exportProgressTotalUnitCount(startDate: dateFormatter.date(from: "2100-01-02T03:00:00Z")!,
  688. endDate: dateFormatter.date(from: "2100-01-02T03:01:00Z")!) {
  689. case .failure(let error):
  690. XCTFail("Unexpected failure: \(error)")
  691. case .success(let progressTotalUnitCount):
  692. XCTAssertEqual(progressTotalUnitCount, 0)
  693. }
  694. }
  695. func testExport() {
  696. XCTAssertNil(doseStore.export(startDate: dateFormatter.date(from: "2100-01-02T03:03:00Z")!,
  697. endDate: dateFormatter.date(from: "2100-01-02T03:09:00Z")!,
  698. to: outputStream,
  699. progress: progress))
  700. XCTAssertEqual(outputStream.string, """
  701. [
  702. {"createdAt":"2100-01-02T03:00:00.000Z","date":"2100-01-02T03:08:00.000Z","duration":0,"insulinType":0,"modificationCounter":1,"mutable":false,"uploaded":false},
  703. {"createdAt":"2100-01-02T03:00:00.000Z","date":"2100-01-02T03:04:00.000Z","duration":0,"insulinType":0,"modificationCounter":3,"mutable":false,"uploaded":false},
  704. {"createdAt":"2100-01-02T03:00:00.000Z","date":"2100-01-02T03:06:00.000Z","duration":0,"insulinType":0,"modificationCounter":4,"mutable":false,"uploaded":false}
  705. ]
  706. """
  707. )
  708. XCTAssertEqual(progress.completedUnitCount, 3 * 1)
  709. }
  710. func testExportEmpty() {
  711. XCTAssertNil(doseStore.export(startDate: dateFormatter.date(from: "2100-01-02T03:00:00Z")!,
  712. endDate: dateFormatter.date(from: "2100-01-02T03:01:00Z")!,
  713. to: outputStream,
  714. progress: progress))
  715. XCTAssertEqual(outputStream.string, "[]")
  716. XCTAssertEqual(progress.completedUnitCount, 0)
  717. }
  718. func testExportCancelled() {
  719. progress.cancel()
  720. XCTAssertEqual(doseStore.export(startDate: dateFormatter.date(from: "2100-01-02T03:03:00Z")!,
  721. endDate: dateFormatter.date(from: "2100-01-02T03:09:00Z")!,
  722. to: outputStream,
  723. progress: progress) as? CriticalEventLogError, CriticalEventLogError.cancelled)
  724. }
  725. private let dateFormatter = ISO8601DateFormatter()
  726. }
  727. class DoseStoreEffectTests: PersistenceControllerTestCase {
  728. var doseStore: DoseStore!
  729. var insulinSensitivitySchedule: InsulinSensitivitySchedule {
  730. return InsulinSensitivitySchedule(unit: HKUnit.milligramsPerDeciliter, dailyItems: [RepeatingScheduleValue(startTime: 0.0, value: 40.0)], timeZone: .currentFixed)!
  731. }
  732. let dateFormatter = ISO8601DateFormatter.localTimeDate()
  733. override func setUp() {
  734. super.setUp()
  735. let healthStore = HKHealthStoreMock()
  736. let exponentialInsulinModel: InsulinModel = ExponentialInsulinModelPreset.rapidActingAdult
  737. let startDate = dateFormatter.date(from: "2015-07-13T12:00:00")!
  738. doseStore = DoseStore(
  739. healthStore: healthStore,
  740. observeHealthKitSamplesFromOtherApps: false,
  741. cacheStore: cacheStore,
  742. observationEnabled: false,
  743. insulinModelSettings: InsulinModelSettings(model: exponentialInsulinModel),
  744. basalProfile: BasalRateSchedule(dailyItems: [RepeatingScheduleValue(startTime: .hours(0), value: 1.0)]),
  745. insulinSensitivitySchedule: insulinSensitivitySchedule,
  746. overrideHistory: TemporaryScheduleOverrideHistory(),
  747. provenanceIdentifier: Bundle.main.bundleIdentifier!,
  748. test_currentDate: startDate
  749. )
  750. }
  751. override func tearDown() {
  752. doseStore = nil
  753. super.tearDown()
  754. }
  755. func loadGlucoseEffectFixture(_ resourceName: String) -> [GlucoseEffect] {
  756. let fixture: [JSONDictionary] = loadFixture(resourceName)
  757. let dateFormatter = ISO8601DateFormatter.localTimeDate()
  758. return fixture.map {
  759. return GlucoseEffect(startDate: dateFormatter.date(from: $0["date"] as! String)!, quantity: HKQuantity(unit: HKUnit(from: $0["unit"] as! String), doubleValue:$0["amount"] as! Double))
  760. }
  761. }
  762. func loadDoseFixture(_ resourceName: String) -> [DoseEntry] {
  763. let fixture: [JSONDictionary] = loadFixture(resourceName)
  764. let dateFormatter = ISO8601DateFormatter.localTimeDate()
  765. return fixture.compactMap {
  766. guard let unit = DoseUnit(rawValue: $0["unit"] as! String),
  767. let pumpType = PumpEventType(rawValue: $0["type"] as! String),
  768. let type = DoseType(pumpEventType: pumpType)
  769. else {
  770. return nil
  771. }
  772. var scheduledBasalRate: HKQuantity? = nil
  773. if let scheduled = $0["scheduled"] as? Double {
  774. scheduledBasalRate = HKQuantity(unit: unit.unit, doubleValue: scheduled)
  775. }
  776. return DoseEntry(
  777. type: type,
  778. startDate: dateFormatter.date(from: $0["start_at"] as! String)!,
  779. endDate: dateFormatter.date(from: $0["end_at"] as! String)!,
  780. value: $0["amount"] as! Double,
  781. unit: unit,
  782. description: $0["description"] as? String,
  783. syncIdentifier: $0["raw"] as? String,
  784. scheduledBasalRate: scheduledBasalRate
  785. )
  786. }
  787. }
  788. func injectDoseEvents(from fixture: String) {
  789. let events = loadDoseFixture(fixture).map {
  790. NewPumpEvent(
  791. date: $0.startDate,
  792. dose: $0,
  793. isMutable: false,
  794. raw: Data(UUID().uuidString.utf8),
  795. title: "",
  796. type: $0.type.pumpEventType
  797. )
  798. }
  799. doseStore.addPumpEvents(events, lastReconciliation: nil) { error in
  800. if error != nil {
  801. XCTFail("Doses should be added successfully to dose store")
  802. }
  803. }
  804. }
  805. func testGlucoseEffectFromTempBasal() {
  806. injectDoseEvents(from: "basal_dose")
  807. let output = loadGlucoseEffectFixture("effect_from_basal_output_exponential")
  808. var insulinEffects: [GlucoseEffect]!
  809. let startDate = dateFormatter.date(from: "2015-07-13T12:00:00")!
  810. let updateGroup = DispatchGroup()
  811. updateGroup.enter()
  812. doseStore.getGlucoseEffects(start: startDate) { (result) -> Void in
  813. switch result {
  814. case .failure(let error):
  815. print(error)
  816. XCTFail("Mock should always return success")
  817. case .success(let effects):
  818. insulinEffects = effects
  819. }
  820. updateGroup.leave()
  821. }
  822. updateGroup.wait()
  823. XCTAssertEqual(output.count, insulinEffects.count)
  824. for (expected, calculated) in zip(output, insulinEffects) {
  825. XCTAssertEqual(expected.startDate, calculated.startDate)
  826. XCTAssertEqual(expected.quantity.doubleValue(for: HKUnit.milligramsPerDeciliter), calculated.quantity.doubleValue(for: HKUnit.milligramsPerDeciliter), accuracy: 1.0, String(describing: expected.startDate))
  827. }
  828. }
  829. func testGlucoseEffectFromTempBasalWithOldDoses() {
  830. injectDoseEvents(from: "basal_dose_with_expired")
  831. let output = loadGlucoseEffectFixture("effect_from_basal_output_exponential")
  832. var insulinEffects: [GlucoseEffect]!
  833. let startDate = dateFormatter.date(from: "2015-07-13T12:00:00")!
  834. let updateGroup = DispatchGroup()
  835. updateGroup.enter()
  836. doseStore.getGlucoseEffects(start: startDate) { (result) -> Void in
  837. switch result {
  838. case .failure(let error):
  839. print(error)
  840. XCTFail("Mock should always return success")
  841. case .success(let effects):
  842. insulinEffects = effects
  843. }
  844. updateGroup.leave()
  845. }
  846. updateGroup.wait()
  847. XCTAssertEqual(output.count, insulinEffects.count)
  848. for (expected, calculated) in zip(output, insulinEffects) {
  849. XCTAssertEqual(expected.startDate, calculated.startDate)
  850. XCTAssertEqual(expected.quantity.doubleValue(for: HKUnit.milligramsPerDeciliter), calculated.quantity.doubleValue(for: HKUnit.milligramsPerDeciliter), accuracy: 1.0, String(describing: expected.startDate))
  851. }
  852. }
  853. func testGlucoseEffectFromHistory() {
  854. injectDoseEvents(from: "dose_history_with_delivered_units")
  855. let output = loadGlucoseEffectFixture("effect_from_history_exponential_delivered_units_output")
  856. var insulinEffects: [GlucoseEffect]!
  857. let startDate = dateFormatter.date(from: "2016-01-30T15:40:49")!
  858. let updateGroup = DispatchGroup()
  859. updateGroup.enter()
  860. doseStore.getGlucoseEffects(start: startDate) { (result) -> Void in
  861. switch result {
  862. case .failure(let error):
  863. print(error)
  864. XCTFail("Mock should always return success")
  865. case .success(let effects):
  866. insulinEffects = effects
  867. }
  868. updateGroup.leave()
  869. }
  870. updateGroup.wait()
  871. XCTAssertEqual(output.count, insulinEffects.count)
  872. for (expected, calculated) in zip(output, insulinEffects) {
  873. XCTAssertEqual(expected.startDate, calculated.startDate)
  874. XCTAssertEqual(expected.quantity.doubleValue(for: HKUnit.milligramsPerDeciliter), calculated.quantity.doubleValue(for: HKUnit.milligramsPerDeciliter), accuracy: 1.0, String(describing: expected.startDate))
  875. }
  876. }
  877. }