DoseStoreTests.swift 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315
  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 testPumpEventTypeDoseMigration() {
  13. cacheStore.managedObjectContext.performAndWait {
  14. let event = PumpEvent(entity: PumpEvent.entity(), insertInto: cacheStore.managedObjectContext)
  15. event.date = Date()
  16. event.duration = .minutes(30)
  17. event.unit = .unitsPerHour
  18. event.type = .tempBasal
  19. event.value = 0.5
  20. event.doseType = nil
  21. XCTAssertNotNil(event.dose)
  22. XCTAssertEqual(.tempBasal, event.dose!.type)
  23. }
  24. }
  25. func testDeduplication() {
  26. cacheStore.managedObjectContext.performAndWait {
  27. let bolus1 = PumpEvent(context: cacheStore.managedObjectContext)
  28. bolus1.date = DateFormatter.descriptionFormatter.date(from: "2018-04-30 02:12:42 +0000")
  29. bolus1.raw = Data(hexadecimalString: "0100a600a6001b006a0c335d12")!
  30. bolus1.type = PumpEventType.bolus
  31. bolus1.dose = DoseEntry(type: .bolus, startDate: bolus1.date!, value: 4.15, unit: .units, syncIdentifier: bolus1.raw?.hexadecimalString)
  32. let bolus2 = PumpEvent(context: cacheStore.managedObjectContext)
  33. bolus2.date = DateFormatter.descriptionFormatter.date(from: "2018-04-30 00:00:00 +0000")
  34. bolus2.raw = Data(hexadecimalString: "0100a600a6001b006a0c335d12")!
  35. bolus2.type = PumpEventType.bolus
  36. bolus2.dose = DoseEntry(type: .bolus, startDate: bolus2.date!, value: 0.15, unit: .units, syncIdentifier: bolus1.raw?.hexadecimalString)
  37. let request: NSFetchRequest<PumpEvent> = PumpEvent.fetchRequest()
  38. let eventsBeforeSave = try! cacheStore.managedObjectContext.fetch(request)
  39. XCTAssertEqual(2, eventsBeforeSave.count)
  40. try! cacheStore.managedObjectContext.save()
  41. let eventsAfterSave = try! cacheStore.managedObjectContext.fetch(request)
  42. XCTAssertEqual(1, eventsAfterSave.count)
  43. }
  44. }
  45. /// See https://github.com/LoopKit/Loop/issues/853
  46. func testOutOfOrderDosesSyncedToHealth() {
  47. let formatter = DateFormatter.descriptionFormatter
  48. let f = { (input) in
  49. return formatter.date(from: input)!
  50. }
  51. // 1. Create a DoseStore
  52. let healthStore = HKHealthStoreMock()
  53. let doseStore = DoseStore(
  54. healthStore: healthStore,
  55. cacheStore: cacheStore,
  56. observationEnabled: false,
  57. insulinModel: WalshInsulinModel(actionDuration: .hours(4)),
  58. 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]]]),
  59. 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]]]),
  60. syncVersion: 1,
  61. // Set the current date
  62. test_currentDate: f("2018-12-12 18:07:14 +0000")
  63. )
  64. // 2. Add a temp basal which has already ended. It should be saved to Health
  65. let pumpEvents1 = [
  66. 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),
  67. 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)
  68. ]
  69. doseStore.insulinDeliveryStore.test_lastBasalEndDate = f("2018-12-12 17:35:58 +0000")
  70. let addPumpEvents1 = expectation(description: "add pumpEvents1")
  71. addPumpEvents1.expectedFulfillmentCount = 3
  72. healthStore.setSaveHandler({ (objects, success, error) in
  73. XCTAssertEqual(1, objects.count)
  74. let sample = objects.first as! HKQuantitySample
  75. XCTAssertEqual(HKInsulinDeliveryReason.basal, sample.insulinDeliveryReason)
  76. XCTAssertNil(error)
  77. addPumpEvents1.fulfill()
  78. })
  79. doseStore.insulinDeliveryStore.test_lastBasalEndDateDidSet = {
  80. addPumpEvents1.fulfill()
  81. }
  82. doseStore.addPumpEvents(pumpEvents1, lastReconciliation: Date()) { (error) in
  83. XCTAssertNil(error)
  84. addPumpEvents1.fulfill()
  85. }
  86. waitForExpectations(timeout: 3)
  87. XCTAssertEqual(f("2018-12-12 18:05:58 +0000"), doseStore.insulinDeliveryStore.test_lastBasalEndDate)
  88. // 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)
  89. // Even though it is before lastBasalEndDate, it should be saved to HealthKit.
  90. doseStore.insulinDeliveryStore.test_currentDate = f("2018-12-12 18:16:23 +0000")
  91. let pumpEvents2 = [
  92. 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)
  93. ]
  94. let addPumpEvents2 = expectation(description: "add pumpEvents2")
  95. addPumpEvents2.expectedFulfillmentCount = 3
  96. healthStore.setSaveHandler({ (objects, success, error) in
  97. XCTAssertEqual(1, objects.count)
  98. let sample = objects.first as! HKQuantitySample
  99. XCTAssertEqual(HKInsulinDeliveryReason.bolus, sample.insulinDeliveryReason)
  100. XCTAssertEqual(5.0, sample.quantity.doubleValue(for: .internationalUnit()))
  101. XCTAssertEqual(f("2018-12-12 18:05:14 +0000"), sample.startDate)
  102. XCTAssertNil(error)
  103. addPumpEvents2.fulfill()
  104. })
  105. doseStore.insulinDeliveryStore.test_lastBasalEndDateDidSet = {
  106. addPumpEvents2.fulfill()
  107. }
  108. doseStore.addPumpEvents(pumpEvents2, lastReconciliation: Date()) { (error) in
  109. XCTAssertNil(error)
  110. addPumpEvents2.fulfill()
  111. }
  112. waitForExpectations(timeout: 3)
  113. XCTAssertEqual(f("2018-12-12 18:05:58 +0000"), doseStore.insulinDeliveryStore.test_lastBasalEndDate)
  114. // Add the next set of pump events, which haven't completed and shouldn't be saved to HealthKit
  115. doseStore.insulinDeliveryStore.test_currentDate = f("2018-12-12 18:21:22 +0000")
  116. let pumpEvents3 = [
  117. 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),
  118. 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),
  119. ]
  120. let addPumpEvents3 = expectation(description: "add pumpEvents3")
  121. addPumpEvents3.expectedFulfillmentCount = 1
  122. healthStore.setSaveHandler({ (objects, success, error) in
  123. XCTFail()
  124. })
  125. doseStore.insulinDeliveryStore.test_lastBasalEndDateDidSet = {
  126. XCTFail()
  127. }
  128. doseStore.addPumpEvents(pumpEvents3, lastReconciliation: Date()) { (error) in
  129. XCTAssertNil(error)
  130. addPumpEvents3.fulfill()
  131. }
  132. waitForExpectations(timeout: 3)
  133. XCTAssertEqual(f("2018-12-12 18:05:58 +0000"), doseStore.insulinDeliveryStore.test_lastBasalEndDate)
  134. }
  135. /// https://github.com/LoopKit/Loop/issues/852
  136. func testSplitBasalsSyncedToHealth() {
  137. let formatter = DateFormatter.descriptionFormatter
  138. let f = { (input) in
  139. return formatter.date(from: input)!
  140. }
  141. // Create a DoseStore
  142. let healthStore = HKHealthStoreMock()
  143. let doseStore = DoseStore(
  144. healthStore: healthStore,
  145. cacheStore: cacheStore,
  146. observationEnabled: false,
  147. insulinModel: WalshInsulinModel(actionDuration: .hours(4)),
  148. 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]]]),
  149. 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]]]),
  150. syncVersion: 1,
  151. // Set the current date (5 minutes later)
  152. test_currentDate: f("2018-11-29 11:04:27 +0000")
  153. )
  154. doseStore.pumpRecordsBasalProfileStartEvents = false
  155. doseStore.insulinDeliveryStore.test_lastBasalEndDate = f("2018-11-29 10:54:28 +0000")
  156. // Add a temp basal. It hasn't finished yet, and should not be saved to Health
  157. let pumpEvents1 = [
  158. 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),
  159. 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)
  160. ]
  161. let addPumpEvents1 = expectation(description: "add pumpEvents1")
  162. addPumpEvents1.expectedFulfillmentCount = 1
  163. healthStore.setSaveHandler({ (objects, success, error) in
  164. XCTFail()
  165. })
  166. doseStore.insulinDeliveryStore.test_lastBasalEndDateDidSet = {
  167. XCTFail()
  168. }
  169. doseStore.addPumpEvents(pumpEvents1, lastReconciliation: Date()) { (error) in
  170. XCTAssertNil(error)
  171. addPumpEvents1.fulfill()
  172. }
  173. waitForExpectations(timeout: 3)
  174. XCTAssertEqual(f("2018-11-29 10:54:28 +0000"), doseStore.insulinDeliveryStore.test_lastBasalEndDate)
  175. XCTAssertEqual(f("2018-11-29 10:59:28 +0000"), doseStore.pumpEventQueryAfterDate)
  176. // Add the next query of the same pump events (no new data) 5 minutes later. Expect the same result
  177. doseStore.insulinDeliveryStore.test_currentDate = f("2018-11-29 11:09:27 +0000")
  178. let addPumpEvents2 = expectation(description: "add pumpEvents2")
  179. addPumpEvents2.expectedFulfillmentCount = 1
  180. healthStore.setSaveHandler({ (objects, success, error) in
  181. XCTFail()
  182. })
  183. doseStore.insulinDeliveryStore.test_lastBasalEndDateDidSet = {
  184. XCTFail()
  185. }
  186. doseStore.addPumpEvents(pumpEvents1, lastReconciliation: Date()) { (error) in
  187. XCTAssertNil(error)
  188. addPumpEvents2.fulfill()
  189. }
  190. waitForExpectations(timeout: 3)
  191. XCTAssertEqual(f("2018-11-29 10:54:28 +0000"), doseStore.insulinDeliveryStore.test_lastBasalEndDate)
  192. XCTAssertEqual(f("2018-11-29 10:59:28 +0000"), doseStore.pumpEventQueryAfterDate)
  193. // Add the next set of pump events, including the last temp basal change.
  194. // The previous, completed basal entries should be saved to Health
  195. doseStore.insulinDeliveryStore.test_currentDate = f("2018-11-29 11:14:28 +0000")
  196. let pumpEvents3 = [
  197. 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),
  198. 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)
  199. ]
  200. let addPumpEvents3 = expectation(description: "add pumpEvents3")
  201. addPumpEvents3.expectedFulfillmentCount = 3
  202. healthStore.setSaveHandler({ (objects, success, error) in
  203. XCTAssertEqual(3, objects.count)
  204. let basal = objects[0] as! HKQuantitySample
  205. XCTAssertEqual(HKInsulinDeliveryReason.basal, basal.insulinDeliveryReason)
  206. XCTAssertEqual(f("2018-11-29 10:54:28 +0000"), basal.startDate)
  207. XCTAssertEqual(f("2018-11-29 10:59:28 +0000"), basal.endDate)
  208. XCTAssertEqual("BasalRateSchedule 2018-11-29T10:54:28Z 2018-11-29T10:59:28Z", basal.metadata![HKMetadataKeySyncIdentifier] as! String)
  209. let temp1 = objects[1] as! HKQuantitySample
  210. XCTAssertEqual(HKInsulinDeliveryReason.basal, temp1.insulinDeliveryReason)
  211. XCTAssertEqual(f("2018-11-29 10:59:28 +0000"), temp1.startDate)
  212. XCTAssertEqual(f("2018-11-29 11:00:00 +0000"), temp1.endDate)
  213. XCTAssertEqual("5bffc7cace53e48e87f7cfcb 1/2", temp1.metadata![HKMetadataKeySyncIdentifier] as! String)
  214. XCTAssertEqual(0.003, temp1.quantity.doubleValue(for: .internationalUnit()), accuracy: 0.01)
  215. let temp2 = objects[2] as! HKQuantitySample
  216. XCTAssertEqual(HKInsulinDeliveryReason.basal, temp2.insulinDeliveryReason)
  217. XCTAssertEqual(f("2018-11-29 11:00:00 +0000"), temp2.startDate)
  218. XCTAssertEqual(f("2018-11-29 11:09:27 +0000"), temp2.endDate)
  219. XCTAssertEqual("5bffc7cace53e48e87f7cfcb 2/2", temp2.metadata![HKMetadataKeySyncIdentifier] as! String)
  220. XCTAssertEqual(0.047, temp2.quantity.doubleValue(for: .internationalUnit()), accuracy: 0.01)
  221. XCTAssertNil(error)
  222. addPumpEvents3.fulfill()
  223. })
  224. doseStore.insulinDeliveryStore.test_lastBasalEndDateDidSet = {
  225. addPumpEvents3.fulfill()
  226. }
  227. doseStore.addPumpEvents(pumpEvents3, lastReconciliation: Date()) { (error) in
  228. XCTAssertNil(error)
  229. addPumpEvents3.fulfill()
  230. }
  231. waitForExpectations(timeout: 3)
  232. XCTAssertEqual(f("2018-11-29 11:09:27 +0000"), doseStore.insulinDeliveryStore.test_lastBasalEndDate)
  233. XCTAssertEqual(f("2018-11-29 11:09:27 +0000"), doseStore.pumpEventQueryAfterDate)
  234. // Add the next set of pump events, including the last temp basal cancel
  235. doseStore.insulinDeliveryStore.test_currentDate = f("2018-11-29 11:19:28 +0000")
  236. let pumpEvents4 = [
  237. 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),
  238. 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)
  239. ]
  240. let addPumpEvents4 = expectation(description: "add pumpEvents4")
  241. addPumpEvents4.expectedFulfillmentCount = 3
  242. healthStore.setSaveHandler({ (objects, success, error) in
  243. XCTAssertEqual(1, objects.count)
  244. let temp = objects[0] as! HKQuantitySample
  245. XCTAssertEqual(HKInsulinDeliveryReason.basal, temp.insulinDeliveryReason)
  246. XCTAssertEqual(f("2018-11-29 11:09:27 +0000"), temp.startDate)
  247. XCTAssertEqual(f("2018-11-29 11:14:28 +0000"), temp.endDate)
  248. XCTAssertEqual("5bffca22ce53e48e87f7d624", temp.metadata![HKMetadataKeySyncIdentifier] as! String)
  249. XCTAssertEqual(0.05, temp.quantity.doubleValue(for: .internationalUnit()), accuracy: 0.01)
  250. XCTAssertNil(error)
  251. addPumpEvents4.fulfill()
  252. })
  253. doseStore.insulinDeliveryStore.test_lastBasalEndDateDidSet = {
  254. addPumpEvents4.fulfill()
  255. }
  256. doseStore.addPumpEvents(pumpEvents4, lastReconciliation: Date()) { (error) in
  257. XCTAssertNil(error)
  258. addPumpEvents4.fulfill()
  259. }
  260. waitForExpectations(timeout: 3)
  261. XCTAssertEqual(f("2018-11-29 11:14:28 +0000"), doseStore.pumpEventQueryAfterDate)
  262. XCTAssertEqual(f("2018-11-29 11:14:28 +0000"), doseStore.insulinDeliveryStore.test_lastBasalEndDate)
  263. }
  264. }