InsulinDeliveryStoreTests.swift 45 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022
  1. //
  2. // InsulinDeliveryStoreTests.swift
  3. // LoopKitHostedTests
  4. //
  5. // Created by Darin Krauss on 10/22/20.
  6. // Copyright © 2020 LoopKit Authors. All rights reserved.
  7. //
  8. import XCTest
  9. import HealthKit
  10. @testable import LoopKit
  11. class InsulinDeliveryStoreTestsBase: PersistenceControllerTestCase {
  12. internal let entry1 = DoseEntry(type: .basal,
  13. startDate: Date(timeIntervalSinceNow: -.minutes(6)),
  14. endDate: Date(timeIntervalSinceNow: -.minutes(5.5)),
  15. value: 1.8,
  16. unit: .unitsPerHour,
  17. deliveredUnits: 0.015,
  18. syncIdentifier: "4B14522E-A7B5-4E73-B76B-5043CD7176B0",
  19. scheduledBasalRate: nil)
  20. internal let entry2 = DoseEntry(type: .tempBasal,
  21. startDate: Date(timeIntervalSinceNow: -.minutes(2)),
  22. endDate: Date(timeIntervalSinceNow: -.minutes(1.5)),
  23. value: 2.4,
  24. unit: .unitsPerHour,
  25. deliveredUnits: 0.02,
  26. syncIdentifier: "A1F8E29B-33D6-4B38-B4CD-D84F14744871",
  27. scheduledBasalRate: HKQuantity(unit: .internationalUnitsPerHour, doubleValue: 1.8))
  28. internal let entry3 = DoseEntry(type: .bolus,
  29. startDate: Date(timeIntervalSinceNow: -.minutes(4)),
  30. endDate: Date(timeIntervalSinceNow: -.minutes(3.5)),
  31. value: 1.0,
  32. unit: .units,
  33. deliveredUnits: nil,
  34. syncIdentifier: "1A1D6192-1521-4469-B962-1B82C4534BB1",
  35. scheduledBasalRate: nil)
  36. internal let device = HKDevice(name: UUID().uuidString,
  37. manufacturer: UUID().uuidString,
  38. model: UUID().uuidString,
  39. hardwareVersion: UUID().uuidString,
  40. firmwareVersion: UUID().uuidString,
  41. softwareVersion: UUID().uuidString,
  42. localIdentifier: UUID().uuidString,
  43. udiDeviceIdentifier: UUID().uuidString)
  44. var healthStore: HKHealthStoreMock!
  45. var insulinDeliveryStore: InsulinDeliveryStore!
  46. var authorizationStatus: HKAuthorizationStatus = .notDetermined
  47. override func setUp() {
  48. super.setUp()
  49. let semaphore = DispatchSemaphore(value: 0)
  50. cacheStore.onReady { error in
  51. XCTAssertNil(error)
  52. semaphore.signal()
  53. }
  54. semaphore.wait()
  55. healthStore = HKHealthStoreMock()
  56. healthStore.authorizationStatus = authorizationStatus
  57. insulinDeliveryStore = InsulinDeliveryStore(healthStore: healthStore,
  58. cacheStore: cacheStore,
  59. cacheLength: .hours(1),
  60. provenanceIdentifier: HKSource.default().bundleIdentifier)
  61. }
  62. override func tearDown() {
  63. let semaphore = DispatchSemaphore(value: 0)
  64. insulinDeliveryStore.purgeAllDoseEntries(healthKitPredicate: HKQuery.predicateForObjects(from: HKSource.default())) { error in
  65. XCTAssertNil(error)
  66. semaphore.signal()
  67. }
  68. semaphore.wait()
  69. insulinDeliveryStore = nil
  70. healthStore = nil
  71. super.tearDown()
  72. }
  73. }
  74. class InsulinDeliveryStoreTestsAuthorized: InsulinDeliveryStoreTestsBase {
  75. override func setUp() {
  76. authorizationStatus = .sharingAuthorized
  77. super.setUp()
  78. }
  79. func testObserverQueryStartup() {
  80. // Check that an observer query was registered even before authorize() is called.
  81. XCTAssertFalse(insulinDeliveryStore.authorizationRequired);
  82. XCTAssertNotNil(insulinDeliveryStore.observerQuery);
  83. }
  84. }
  85. class InsulinDeliveryStoreTests: InsulinDeliveryStoreTestsBase {
  86. // MARK: - HealthKitSampleStore
  87. func testHealthKitQueryAnchorPersistence() {
  88. var observerQuery: HKObserverQueryMock? = nil
  89. var anchoredObjectQuery: HKAnchoredObjectQueryMock? = nil
  90. XCTAssert(insulinDeliveryStore.authorizationRequired);
  91. XCTAssertNil(insulinDeliveryStore.observerQuery);
  92. insulinDeliveryStore.createObserverQuery = { (sampleType, predicate, updateHandler) -> HKObserverQuery in
  93. observerQuery = HKObserverQueryMock(sampleType: sampleType, predicate: predicate, updateHandler: updateHandler)
  94. return observerQuery!
  95. }
  96. let authorizationCompletion = expectation(description: "authorization completion")
  97. insulinDeliveryStore.authorize { (result) in
  98. authorizationCompletion.fulfill()
  99. }
  100. waitForExpectations(timeout: 10)
  101. XCTAssertNotNil(observerQuery)
  102. let anchoredObjectQueryCreationExpectation = expectation(description: "anchored object query creation")
  103. insulinDeliveryStore.createAnchoredObjectQuery = { (sampleType, predicate, anchor, limit, resultsHandler) -> HKAnchoredObjectQuery in
  104. anchoredObjectQuery = HKAnchoredObjectQueryMock(type: sampleType, predicate: predicate, anchor: anchor, limit: limit, resultsHandler: resultsHandler)
  105. anchoredObjectQueryCreationExpectation.fulfill()
  106. return anchoredObjectQuery!
  107. }
  108. let observerQueryCompletionExpectation = expectation(description: "observer query completion")
  109. let observerQueryCompletionHandler = {
  110. observerQueryCompletionExpectation.fulfill()
  111. }
  112. // This simulates a signal marking the arrival of new HK Data.
  113. observerQuery!.updateHandler(observerQuery!, observerQueryCompletionHandler, nil)
  114. wait(for: [anchoredObjectQueryCreationExpectation], timeout: 10)
  115. // Trigger results handler for anchored object query
  116. let returnedAnchor = HKQueryAnchor(fromValue: 5)
  117. anchoredObjectQuery!.resultsHandler(anchoredObjectQuery!, [], [], returnedAnchor, nil)
  118. // Wait for observerQueryCompletionExpectation
  119. waitForExpectations(timeout: 10)
  120. XCTAssertNotNil(insulinDeliveryStore.queryAnchor)
  121. cacheStore.managedObjectContext.performAndWait {}
  122. // Create a new glucose store, and ensure it uses the last query anchor
  123. let newInsulinDeliveryStore = InsulinDeliveryStore(healthStore: healthStore,
  124. cacheStore: cacheStore,
  125. provenanceIdentifier: HKSource.default().bundleIdentifier)
  126. let newAuthorizationCompletion = expectation(description: "authorization completion")
  127. observerQuery = nil
  128. newInsulinDeliveryStore.createObserverQuery = { (sampleType, predicate, updateHandler) -> HKObserverQuery in
  129. observerQuery = HKObserverQueryMock(sampleType: sampleType, predicate: predicate, updateHandler: updateHandler)
  130. return observerQuery!
  131. }
  132. newInsulinDeliveryStore.authorize { (result) in
  133. newAuthorizationCompletion.fulfill()
  134. }
  135. waitForExpectations(timeout: 10)
  136. anchoredObjectQuery = nil
  137. let newAnchoredObjectQueryCreationExpectation = expectation(description: "new anchored object query creation")
  138. newInsulinDeliveryStore.createAnchoredObjectQuery = { (sampleType, predicate, anchor, limit, resultsHandler) -> HKAnchoredObjectQuery in
  139. anchoredObjectQuery = HKAnchoredObjectQueryMock(type: sampleType, predicate: predicate, anchor: anchor, limit: limit, resultsHandler: resultsHandler)
  140. newAnchoredObjectQueryCreationExpectation.fulfill()
  141. return anchoredObjectQuery!
  142. }
  143. // This simulates a signal marking the arrival of new HK Data.
  144. observerQuery!.updateHandler(observerQuery!, {}, nil)
  145. waitForExpectations(timeout: 10)
  146. // Assert new glucose store is querying with the last anchor that our HealthKit mock returned
  147. XCTAssertEqual(returnedAnchor, anchoredObjectQuery?.anchor)
  148. anchoredObjectQuery!.resultsHandler(anchoredObjectQuery!, [], [], returnedAnchor, nil)
  149. }
  150. // MARK: - Fetching
  151. func testGetDoseEntries() {
  152. let addDoseEntriesCompletion = expectation(description: "addDoseEntries")
  153. insulinDeliveryStore.addDoseEntries([entry1, entry2, entry3], from: device, syncVersion: 2) { result in
  154. switch result {
  155. case .failure(let error):
  156. XCTFail("Unexpected failure: \(error)")
  157. case .success:
  158. break
  159. }
  160. addDoseEntriesCompletion.fulfill()
  161. }
  162. waitForExpectations(timeout: 10)
  163. let getDoseEntries1Completion = expectation(description: "getDoseEntries1")
  164. insulinDeliveryStore.getDoseEntries() { result in
  165. switch result {
  166. case .failure(let error):
  167. XCTFail("Unexpected failure: \(error)")
  168. case .success(let entries):
  169. XCTAssertEqual(entries.count, 3)
  170. XCTAssertEqual(entries[0].type, self.entry1.type)
  171. XCTAssertEqual(entries[0].startDate, self.entry1.startDate)
  172. XCTAssertEqual(entries[0].endDate, self.entry1.endDate)
  173. XCTAssertEqual(entries[0].value, 0.015)
  174. XCTAssertEqual(entries[0].unit, .units)
  175. XCTAssertNil(entries[0].deliveredUnits)
  176. XCTAssertEqual(entries[0].description, self.entry1.description)
  177. XCTAssertEqual(entries[0].syncIdentifier, self.entry1.syncIdentifier)
  178. XCTAssertEqual(entries[0].scheduledBasalRate, self.entry1.scheduledBasalRate)
  179. XCTAssertEqual(entries[1].type, self.entry3.type)
  180. XCTAssertEqual(entries[1].startDate, self.entry3.startDate)
  181. XCTAssertEqual(entries[1].endDate, self.entry3.endDate)
  182. XCTAssertEqual(entries[1].value, self.entry3.value)
  183. XCTAssertEqual(entries[1].unit, self.entry3.unit)
  184. XCTAssertEqual(entries[1].deliveredUnits, self.entry3.deliveredUnits)
  185. XCTAssertEqual(entries[1].description, self.entry3.description)
  186. XCTAssertEqual(entries[1].syncIdentifier, self.entry3.syncIdentifier)
  187. XCTAssertEqual(entries[1].scheduledBasalRate, self.entry3.scheduledBasalRate)
  188. XCTAssertEqual(entries[2].type, self.entry2.type)
  189. XCTAssertEqual(entries[2].startDate, self.entry2.startDate)
  190. XCTAssertEqual(entries[2].endDate, self.entry2.endDate)
  191. XCTAssertEqual(entries[2].value, self.entry2.value)
  192. XCTAssertEqual(entries[2].unit, self.entry2.unit)
  193. XCTAssertEqual(entries[2].deliveredUnits, self.entry2.deliveredUnits)
  194. XCTAssertEqual(entries[2].description, self.entry2.description)
  195. XCTAssertEqual(entries[2].syncIdentifier, self.entry2.syncIdentifier)
  196. XCTAssertEqual(entries[2].scheduledBasalRate, self.entry2.scheduledBasalRate)
  197. }
  198. getDoseEntries1Completion.fulfill()
  199. }
  200. waitForExpectations(timeout: 10)
  201. let getDoseEntries2Completion = expectation(description: "getDoseEntries2")
  202. insulinDeliveryStore.getDoseEntries(start: Date(timeIntervalSinceNow: -.minutes(3.75)), end: Date(timeIntervalSinceNow: -.minutes(1.75))) { result in
  203. switch result {
  204. case .failure(let error):
  205. XCTFail("Unexpected failure: \(error)")
  206. case .success(let entries):
  207. XCTAssertEqual(entries.count, 2)
  208. XCTAssertEqual(entries[0].type, self.entry3.type)
  209. XCTAssertEqual(entries[0].startDate, self.entry3.startDate)
  210. XCTAssertEqual(entries[0].endDate, self.entry3.endDate)
  211. XCTAssertEqual(entries[0].value, self.entry3.value)
  212. XCTAssertEqual(entries[0].unit, self.entry3.unit)
  213. XCTAssertEqual(entries[0].deliveredUnits, self.entry3.deliveredUnits)
  214. XCTAssertEqual(entries[0].description, self.entry3.description)
  215. XCTAssertEqual(entries[0].syncIdentifier, self.entry3.syncIdentifier)
  216. XCTAssertEqual(entries[0].scheduledBasalRate, self.entry3.scheduledBasalRate)
  217. XCTAssertEqual(entries[1].type, self.entry2.type)
  218. XCTAssertEqual(entries[1].startDate, self.entry2.startDate)
  219. XCTAssertEqual(entries[1].endDate, self.entry2.endDate)
  220. XCTAssertEqual(entries[1].value, self.entry2.value)
  221. XCTAssertEqual(entries[1].unit, self.entry2.unit)
  222. XCTAssertEqual(entries[1].deliveredUnits, self.entry2.deliveredUnits)
  223. XCTAssertEqual(entries[1].description, self.entry2.description)
  224. XCTAssertEqual(entries[1].syncIdentifier, self.entry2.syncIdentifier)
  225. XCTAssertEqual(entries[1].scheduledBasalRate, self.entry2.scheduledBasalRate)
  226. }
  227. getDoseEntries2Completion.fulfill()
  228. }
  229. waitForExpectations(timeout: 10)
  230. let purgeCachedInsulinDeliveryObjectsCompletion = expectation(description: "purgeCachedInsulinDeliveryObjects")
  231. insulinDeliveryStore.purgeCachedInsulinDeliveryObjects() { error in
  232. XCTAssertNil(error)
  233. purgeCachedInsulinDeliveryObjectsCompletion.fulfill()
  234. }
  235. waitForExpectations(timeout: 10)
  236. let getDoseEntries3Completion = expectation(description: "getDoseEntries3")
  237. insulinDeliveryStore.getDoseEntries() { result in
  238. switch result {
  239. case .failure(let error):
  240. XCTFail("Unexpected failure: \(error)")
  241. case .success(let entries):
  242. XCTAssertEqual(entries.count, 0)
  243. }
  244. getDoseEntries3Completion.fulfill()
  245. }
  246. waitForExpectations(timeout: 10)
  247. }
  248. func testLastBasalEndDate() {
  249. let getLastImmutableBasalEndDate1Completion = expectation(description: "getLastImmutableBasalEndDate1")
  250. insulinDeliveryStore.getLastImmutableBasalEndDate() { result in
  251. switch result {
  252. case .failure(let error):
  253. XCTFail("Unexpected failure: \(error)")
  254. case .success(let lastBasalEndDate):
  255. XCTAssertEqual(lastBasalEndDate, .distantPast)
  256. }
  257. getLastImmutableBasalEndDate1Completion.fulfill()
  258. }
  259. waitForExpectations(timeout: 10)
  260. let addDoseEntriesCompletion = expectation(description: "addDoseEntries")
  261. insulinDeliveryStore.addDoseEntries([entry1, entry2, entry3], from: device, syncVersion: 2) { result in
  262. switch result {
  263. case .failure(let error):
  264. XCTFail("Unexpected failure: \(error)")
  265. case .success:
  266. break
  267. }
  268. addDoseEntriesCompletion.fulfill()
  269. }
  270. waitForExpectations(timeout: 10)
  271. let getLastImmutableBasalEndDate2Completion = expectation(description: "getLastImmutableBasalEndDate2")
  272. insulinDeliveryStore.getLastImmutableBasalEndDate() { result in
  273. switch result {
  274. case .failure(let error):
  275. XCTFail("Unexpected failure: \(error)")
  276. case .success(let lastBasalEndDate):
  277. XCTAssertEqual(lastBasalEndDate, self.entry2.endDate)
  278. }
  279. getLastImmutableBasalEndDate2Completion.fulfill()
  280. }
  281. waitForExpectations(timeout: 10)
  282. let purgeCachedInsulinDeliveryObjectsCompletion = expectation(description: "purgeCachedInsulinDeliveryObjects")
  283. insulinDeliveryStore.purgeCachedInsulinDeliveryObjects() { error in
  284. XCTAssertNil(error)
  285. purgeCachedInsulinDeliveryObjectsCompletion.fulfill()
  286. }
  287. waitForExpectations(timeout: 10)
  288. let getLastImmutableBasalEndDate3Completion = expectation(description: "getLastImmutableBasalEndDate3")
  289. insulinDeliveryStore.getLastImmutableBasalEndDate() { result in
  290. switch result {
  291. case .failure(let error):
  292. XCTFail("Unexpected failure: \(error)")
  293. case .success(let lastBasalEndDate):
  294. XCTAssertEqual(lastBasalEndDate, .distantPast)
  295. }
  296. getLastImmutableBasalEndDate3Completion.fulfill()
  297. }
  298. waitForExpectations(timeout: 10)
  299. }
  300. // MARK: - Modification
  301. func testAddDoseEntries() {
  302. let addDoseEntries1Completion = expectation(description: "addDoseEntries1")
  303. insulinDeliveryStore.addDoseEntries([entry1, entry2, entry3], from: device, syncVersion: 2) { result in
  304. switch result {
  305. case .failure(let error):
  306. XCTFail("Unexpected failure: \(error)")
  307. case .success:
  308. break
  309. }
  310. addDoseEntries1Completion.fulfill()
  311. }
  312. waitForExpectations(timeout: 10)
  313. let getDoseEntries1Completion = expectation(description: "getDoseEntries1")
  314. insulinDeliveryStore.getDoseEntries() { result in
  315. switch result {
  316. case .failure(let error):
  317. XCTFail("Unexpected failure: \(error)")
  318. case .success(let entries):
  319. XCTAssertEqual(entries.count, 3)
  320. XCTAssertEqual(entries[0].type, self.entry1.type)
  321. XCTAssertEqual(entries[0].startDate, self.entry1.startDate)
  322. XCTAssertEqual(entries[0].endDate, self.entry1.endDate)
  323. XCTAssertEqual(entries[0].value, 0.015)
  324. XCTAssertEqual(entries[0].unit, .units)
  325. XCTAssertNil(entries[0].deliveredUnits)
  326. XCTAssertEqual(entries[0].description, self.entry1.description)
  327. XCTAssertEqual(entries[0].syncIdentifier, self.entry1.syncIdentifier)
  328. XCTAssertEqual(entries[0].scheduledBasalRate, self.entry1.scheduledBasalRate)
  329. XCTAssertEqual(entries[1].type, self.entry3.type)
  330. XCTAssertEqual(entries[1].startDate, self.entry3.startDate)
  331. XCTAssertEqual(entries[1].endDate, self.entry3.endDate)
  332. XCTAssertEqual(entries[1].value, self.entry3.value)
  333. XCTAssertEqual(entries[1].unit, self.entry3.unit)
  334. XCTAssertEqual(entries[1].deliveredUnits, self.entry3.deliveredUnits)
  335. XCTAssertEqual(entries[1].description, self.entry3.description)
  336. XCTAssertEqual(entries[1].syncIdentifier, self.entry3.syncIdentifier)
  337. XCTAssertEqual(entries[1].scheduledBasalRate, self.entry3.scheduledBasalRate)
  338. XCTAssertEqual(entries[2].type, self.entry2.type)
  339. XCTAssertEqual(entries[2].startDate, self.entry2.startDate)
  340. XCTAssertEqual(entries[2].endDate, self.entry2.endDate)
  341. XCTAssertEqual(entries[2].value, self.entry2.value)
  342. XCTAssertEqual(entries[2].unit, self.entry2.unit)
  343. XCTAssertEqual(entries[2].deliveredUnits, self.entry2.deliveredUnits)
  344. XCTAssertEqual(entries[2].description, self.entry2.description)
  345. XCTAssertEqual(entries[2].syncIdentifier, self.entry2.syncIdentifier)
  346. XCTAssertEqual(entries[2].scheduledBasalRate, self.entry2.scheduledBasalRate)
  347. }
  348. getDoseEntries1Completion.fulfill()
  349. }
  350. waitForExpectations(timeout: 10)
  351. let addDoseEntries2Completion = expectation(description: "addDoseEntries2")
  352. insulinDeliveryStore.addDoseEntries([entry3, entry1, entry2], from: device, syncVersion: 2) { result in
  353. switch result {
  354. case .failure(let error):
  355. XCTFail("Unexpected failure: \(error)")
  356. case .success:
  357. break
  358. }
  359. addDoseEntries2Completion.fulfill()
  360. }
  361. waitForExpectations(timeout: 10)
  362. let getDoseEntries2Completion = expectation(description: "getDoseEntries2Completion")
  363. insulinDeliveryStore.getDoseEntries() { result in
  364. switch result {
  365. case .failure(let error):
  366. XCTFail("Unexpected failure: \(error)")
  367. case .success(let entries):
  368. XCTAssertEqual(entries.count, 3)
  369. XCTAssertEqual(entries[0].type, self.entry1.type)
  370. XCTAssertEqual(entries[0].startDate, self.entry1.startDate)
  371. XCTAssertEqual(entries[0].endDate, self.entry1.endDate)
  372. XCTAssertEqual(entries[0].value, 0.015)
  373. XCTAssertEqual(entries[0].unit, .units)
  374. XCTAssertNil(entries[0].deliveredUnits)
  375. XCTAssertEqual(entries[0].description, self.entry1.description)
  376. XCTAssertEqual(entries[0].syncIdentifier, self.entry1.syncIdentifier)
  377. XCTAssertEqual(entries[0].scheduledBasalRate, self.entry1.scheduledBasalRate)
  378. XCTAssertEqual(entries[1].type, self.entry3.type)
  379. XCTAssertEqual(entries[1].startDate, self.entry3.startDate)
  380. XCTAssertEqual(entries[1].endDate, self.entry3.endDate)
  381. XCTAssertEqual(entries[1].value, self.entry3.value)
  382. XCTAssertEqual(entries[1].unit, self.entry3.unit)
  383. XCTAssertEqual(entries[1].deliveredUnits, self.entry3.deliveredUnits)
  384. XCTAssertEqual(entries[1].description, self.entry3.description)
  385. XCTAssertEqual(entries[1].syncIdentifier, self.entry3.syncIdentifier)
  386. XCTAssertEqual(entries[1].scheduledBasalRate, self.entry3.scheduledBasalRate)
  387. XCTAssertEqual(entries[2].type, self.entry2.type)
  388. XCTAssertEqual(entries[2].startDate, self.entry2.startDate)
  389. XCTAssertEqual(entries[2].endDate, self.entry2.endDate)
  390. XCTAssertEqual(entries[2].value, self.entry2.value)
  391. XCTAssertEqual(entries[2].unit, self.entry2.unit)
  392. XCTAssertEqual(entries[2].deliveredUnits, self.entry2.deliveredUnits)
  393. XCTAssertEqual(entries[2].description, self.entry2.description)
  394. XCTAssertEqual(entries[2].syncIdentifier, self.entry2.syncIdentifier)
  395. XCTAssertEqual(entries[2].scheduledBasalRate, self.entry2.scheduledBasalRate)
  396. }
  397. getDoseEntries2Completion.fulfill()
  398. }
  399. waitForExpectations(timeout: 10)
  400. }
  401. func testAddDoseEntriesEmpty() {
  402. let addDoseEntriesCompletion = expectation(description: "addDoseEntries")
  403. insulinDeliveryStore.addDoseEntries([], from: device, syncVersion: 2) { result in
  404. switch result {
  405. case .failure(let error):
  406. XCTFail("Unexpected failure: \(error)")
  407. case .success:
  408. break
  409. }
  410. addDoseEntriesCompletion.fulfill()
  411. }
  412. waitForExpectations(timeout: 10)
  413. }
  414. func testAddDoseEntriesNotification() {
  415. let doseEntriesDidChangeCompletion = expectation(description: "doseEntriesDidChange")
  416. let observer = NotificationCenter.default.addObserver(forName: InsulinDeliveryStore.doseEntriesDidChange, object: insulinDeliveryStore, queue: nil) { notification in
  417. doseEntriesDidChangeCompletion.fulfill()
  418. }
  419. let addDoseEntriesCompletion = expectation(description: "addDoseEntries")
  420. insulinDeliveryStore.addDoseEntries([entry1, entry2, entry3], from: device, syncVersion: 2) { result in
  421. switch result {
  422. case .failure(let error):
  423. XCTFail("Unexpected failure: \(error)")
  424. case .success:
  425. break
  426. }
  427. addDoseEntriesCompletion.fulfill()
  428. }
  429. wait(for: [doseEntriesDidChangeCompletion, addDoseEntriesCompletion], timeout: 10, enforceOrder: true)
  430. NotificationCenter.default.removeObserver(observer)
  431. }
  432. func testManuallyEnteredDoses() {
  433. let manualEntry = DoseEntry(type: .bolus,
  434. startDate: Date(timeIntervalSinceNow: -.minutes(15)),
  435. endDate: Date(timeIntervalSinceNow: -.minutes(10)),
  436. value: 3.0,
  437. unit: .units,
  438. syncIdentifier: "C0AB1CBD-6B36-4113-9D49-709A022B2451",
  439. manuallyEntered: true)
  440. let addDoseEntriesCompletion = expectation(description: "addDoseEntries")
  441. insulinDeliveryStore.addDoseEntries([entry1, manualEntry, entry2, entry3], from: device, syncVersion: 2) { result in
  442. switch result {
  443. case .failure(let error):
  444. XCTFail("Unexpected failure: \(error)")
  445. case .success:
  446. break
  447. }
  448. addDoseEntriesCompletion.fulfill()
  449. }
  450. waitForExpectations(timeout: 10)
  451. let getManuallyEnteredDoses1Completion = expectation(description: "getManuallyEnteredDoses1")
  452. insulinDeliveryStore.getManuallyEnteredDoses(since: .distantPast) { result in
  453. switch result {
  454. case .failure(let error):
  455. XCTFail("Unexpected failure: \(error)")
  456. case .success(let entries):
  457. XCTAssertEqual(entries.count, 1)
  458. XCTAssertEqual(entries[0], manualEntry)
  459. }
  460. getManuallyEnteredDoses1Completion.fulfill()
  461. }
  462. waitForExpectations(timeout: 10)
  463. let getManuallyEnteredDoses2Completion = expectation(description: "getManuallyEnteredDoses2")
  464. insulinDeliveryStore.getManuallyEnteredDoses(since: Date(timeIntervalSinceNow: -.minutes(12))) { result in
  465. switch result {
  466. case .failure(let error):
  467. XCTFail("Unexpected failure: \(error)")
  468. case .success(let entries):
  469. XCTAssertEqual(entries.count, 0)
  470. }
  471. getManuallyEnteredDoses2Completion.fulfill()
  472. }
  473. waitForExpectations(timeout: 10)
  474. let deleteAllManuallyEnteredDoses1Completion = expectation(description: "deleteAllManuallyEnteredDoses1")
  475. insulinDeliveryStore.deleteAllManuallyEnteredDoses(since: Date(timeIntervalSinceNow: -.minutes(12))) { error in
  476. XCTAssertNil(error)
  477. deleteAllManuallyEnteredDoses1Completion.fulfill()
  478. }
  479. waitForExpectations(timeout: 10)
  480. let getManuallyEnteredDoses3Completion = expectation(description: "getManuallyEnteredDoses3")
  481. insulinDeliveryStore.getManuallyEnteredDoses(since: .distantPast) { result in
  482. switch result {
  483. case .failure(let error):
  484. XCTFail("Unexpected failure: \(error)")
  485. case .success(let entries):
  486. XCTAssertEqual(entries.count, 1)
  487. XCTAssertEqual(entries[0], manualEntry)
  488. }
  489. getManuallyEnteredDoses3Completion.fulfill()
  490. }
  491. waitForExpectations(timeout: 10)
  492. let deleteAllManuallyEnteredDose2Completion = expectation(description: "deleteAllManuallyEnteredDose2")
  493. insulinDeliveryStore.deleteAllManuallyEnteredDoses(since: Date(timeIntervalSinceNow: -.minutes(20))) { error in
  494. XCTAssertNil(error)
  495. deleteAllManuallyEnteredDose2Completion.fulfill()
  496. }
  497. waitForExpectations(timeout: 10)
  498. let getManuallyEnteredDoses4Completion = expectation(description: "getManuallyEnteredDoses4")
  499. insulinDeliveryStore.getManuallyEnteredDoses(since: .distantPast) { result in
  500. switch result {
  501. case .failure(let error):
  502. XCTFail("Unexpected failure: \(error)")
  503. case .success(let entries):
  504. XCTAssertEqual(entries.count, 0)
  505. }
  506. getManuallyEnteredDoses4Completion.fulfill()
  507. }
  508. waitForExpectations(timeout: 10)
  509. }
  510. // MARK: - Cache Management
  511. func testEarliestCacheDate() {
  512. XCTAssertEqual(insulinDeliveryStore.earliestCacheDate.timeIntervalSinceNow, -.hours(1), accuracy: 1)
  513. }
  514. func testPurgeAllDoseEntries() {
  515. let addDoseEntriesCompletion = expectation(description: "addDoseEntries")
  516. insulinDeliveryStore.addDoseEntries([entry1, entry2, entry3], from: device, syncVersion: 2) { result in
  517. switch result {
  518. case .failure(let error):
  519. XCTFail("Unexpected failure: \(error)")
  520. case .success:
  521. break
  522. }
  523. addDoseEntriesCompletion.fulfill()
  524. }
  525. waitForExpectations(timeout: 10)
  526. let getDoseEntries1Completion = expectation(description: "getDoseEntries1")
  527. insulinDeliveryStore.getDoseEntries() { result in
  528. switch result {
  529. case .failure(let error):
  530. XCTFail("Unexpected failure: \(error)")
  531. case .success(let entries):
  532. XCTAssertEqual(entries.count, 3)
  533. }
  534. getDoseEntries1Completion.fulfill()
  535. }
  536. waitForExpectations(timeout: 10)
  537. let purgeAllDoseEntriesCompletion = expectation(description: "purgeAllDoseEntries")
  538. insulinDeliveryStore.purgeAllDoseEntries(healthKitPredicate: HKQuery.predicateForObjects(from: HKSource.default())) { error in
  539. XCTAssertNil(error)
  540. purgeAllDoseEntriesCompletion.fulfill()
  541. }
  542. waitForExpectations(timeout: 10)
  543. let getDoseEntries2Completion = expectation(description: "getDoseEntries2")
  544. insulinDeliveryStore.getDoseEntries() { result in
  545. switch result {
  546. case .failure(let error):
  547. XCTFail("Unexpected failure: \(error)")
  548. case .success(let entries):
  549. XCTAssertEqual(entries.count, 0)
  550. }
  551. getDoseEntries2Completion.fulfill()
  552. }
  553. waitForExpectations(timeout: 10)
  554. }
  555. func testPurgeExpiredGlucoseObjects() {
  556. let expiredEntry = DoseEntry(type: .bolus,
  557. startDate: Date(timeIntervalSinceNow: -.hours(3)),
  558. endDate: Date(timeIntervalSinceNow: -.hours(2)),
  559. value: 3.0,
  560. unit: .units,
  561. deliveredUnits: nil,
  562. syncIdentifier: "7530B8CA-827A-4DE8-ADE3-9E10FF80A4A9",
  563. scheduledBasalRate: nil)
  564. let addDoseEntriesCompletion = expectation(description: "addDoseEntries")
  565. insulinDeliveryStore.addDoseEntries([entry1, entry2, entry3, expiredEntry], from: device, syncVersion: 2) { result in
  566. switch result {
  567. case .failure(let error):
  568. XCTFail("Unexpected failure: \(error)")
  569. case .success:
  570. break
  571. }
  572. addDoseEntriesCompletion.fulfill()
  573. }
  574. waitForExpectations(timeout: 10)
  575. let getDoseEntriesCompletion = expectation(description: "getDoseEntries")
  576. insulinDeliveryStore.getDoseEntries() { result in
  577. switch result {
  578. case .failure(let error):
  579. XCTFail("Unexpected failure: \(error)")
  580. case .success(let entries):
  581. XCTAssertEqual(entries.count, 3)
  582. }
  583. getDoseEntriesCompletion.fulfill()
  584. }
  585. waitForExpectations(timeout: 10)
  586. }
  587. func testPurgeCachedInsulinDeliveryObjects() {
  588. let addDoseEntriesCompletion = expectation(description: "addDoseEntries")
  589. insulinDeliveryStore.addDoseEntries([entry1, entry2, entry3], from: device, syncVersion: 2) { result in
  590. switch result {
  591. case .failure(let error):
  592. XCTFail("Unexpected failure: \(error)")
  593. case .success:
  594. break
  595. }
  596. addDoseEntriesCompletion.fulfill()
  597. }
  598. waitForExpectations(timeout: 10)
  599. let getDoseEntries1Completion = expectation(description: "getDoseEntries1")
  600. insulinDeliveryStore.getDoseEntries() { result in
  601. switch result {
  602. case .failure(let error):
  603. XCTFail("Unexpected failure: \(error)")
  604. case .success(let entries):
  605. XCTAssertEqual(entries.count, 3)
  606. }
  607. getDoseEntries1Completion.fulfill()
  608. }
  609. waitForExpectations(timeout: 10)
  610. let purgeCachedInsulinDeliveryObjects1Completion = expectation(description: "purgeCachedInsulinDeliveryObjects1")
  611. insulinDeliveryStore.purgeCachedInsulinDeliveryObjects(before: Date(timeIntervalSinceNow: -.minutes(5))) { error in
  612. XCTAssertNil(error)
  613. purgeCachedInsulinDeliveryObjects1Completion.fulfill()
  614. }
  615. waitForExpectations(timeout: 10)
  616. let getDoseEntries2Completion = expectation(description: "getDoseEntries2")
  617. insulinDeliveryStore.getDoseEntries() { result in
  618. switch result {
  619. case .failure(let error):
  620. XCTFail("Unexpected failure: \(error)")
  621. case .success(let entries):
  622. XCTAssertEqual(entries.count, 2)
  623. }
  624. getDoseEntries2Completion.fulfill()
  625. }
  626. waitForExpectations(timeout: 10)
  627. let purgeCachedInsulinDeliveryObjects2Completion = expectation(description: "purgeCachedInsulinDeliveryObjects2")
  628. insulinDeliveryStore.purgeCachedInsulinDeliveryObjects() { error in
  629. XCTAssertNil(error)
  630. purgeCachedInsulinDeliveryObjects2Completion.fulfill()
  631. }
  632. waitForExpectations(timeout: 10)
  633. let getDoseEntries3Completion = expectation(description: "getDoseEntries3")
  634. insulinDeliveryStore.getDoseEntries() { result in
  635. switch result {
  636. case .failure(let error):
  637. XCTFail("Unexpected failure: \(error)")
  638. case .success(let entries):
  639. XCTAssertEqual(entries.count, 0)
  640. }
  641. getDoseEntries3Completion.fulfill()
  642. }
  643. waitForExpectations(timeout: 10)
  644. }
  645. func testPurgeCachedInsulinDeliveryObjectsNotification() {
  646. let addDoseEntriesCompletion = expectation(description: "addDoseEntries")
  647. insulinDeliveryStore.addDoseEntries([entry1, entry2, entry3], from: device, syncVersion: 2) { result in
  648. switch result {
  649. case .failure(let error):
  650. XCTFail("Unexpected failure: \(error)")
  651. case .success:
  652. break
  653. }
  654. addDoseEntriesCompletion.fulfill()
  655. }
  656. waitForExpectations(timeout: 10)
  657. let doseEntriesDidChangeCompletion = expectation(description: "doseEntriesDidChange")
  658. let observer = NotificationCenter.default.addObserver(forName: InsulinDeliveryStore.doseEntriesDidChange, object: insulinDeliveryStore, queue: nil) { notification in
  659. doseEntriesDidChangeCompletion.fulfill()
  660. }
  661. let purgeCachedInsulinDeliveryObjectsCompletion = expectation(description: "purgeCachedInsulinDeliveryObjects")
  662. insulinDeliveryStore.purgeCachedInsulinDeliveryObjects() { error in
  663. XCTAssertNil(error)
  664. purgeCachedInsulinDeliveryObjectsCompletion.fulfill()
  665. }
  666. wait(for: [doseEntriesDidChangeCompletion, purgeCachedInsulinDeliveryObjectsCompletion], timeout: 10, enforceOrder: true)
  667. NotificationCenter.default.removeObserver(observer)
  668. }
  669. }
  670. class InsulinDeliveryStoreQueryAnchorTests: XCTestCase {
  671. var rawValue: InsulinDeliveryStore.QueryAnchor.RawValue = [
  672. "modificationCounter": Int64(123)
  673. ]
  674. func testInitializerDefault() {
  675. let queryAnchor = InsulinDeliveryStore.QueryAnchor()
  676. XCTAssertEqual(queryAnchor.modificationCounter, 0)
  677. }
  678. func testInitializerRawValue() {
  679. let queryAnchor = InsulinDeliveryStore.QueryAnchor(rawValue: rawValue)
  680. XCTAssertNotNil(queryAnchor)
  681. XCTAssertEqual(queryAnchor?.modificationCounter, 123)
  682. }
  683. func testInitializerRawValueMissingModificationCounter() {
  684. rawValue["modificationCounter"] = nil
  685. XCTAssertNil(InsulinDeliveryStore.QueryAnchor(rawValue: rawValue))
  686. }
  687. func testInitializerRawValueInvalidModificationCounter() {
  688. rawValue["modificationCounter"] = "123"
  689. XCTAssertNil(InsulinDeliveryStore.QueryAnchor(rawValue: rawValue))
  690. }
  691. func testRawValueWithDefault() {
  692. let rawValue = InsulinDeliveryStore.QueryAnchor().rawValue
  693. XCTAssertEqual(rawValue.count, 1)
  694. XCTAssertEqual(rawValue["modificationCounter"] as? Int64, Int64(0))
  695. }
  696. func testRawValueWithNonDefault() {
  697. var queryAnchor = InsulinDeliveryStore.QueryAnchor()
  698. queryAnchor.modificationCounter = 123
  699. let rawValue = queryAnchor.rawValue
  700. XCTAssertEqual(rawValue.count, 1)
  701. XCTAssertEqual(rawValue["modificationCounter"] as? Int64, Int64(123))
  702. }
  703. }
  704. class InsulinDeliveryStoreQueryTests: PersistenceControllerTestCase {
  705. let insulinModel = WalshInsulinModel(actionDuration: .hours(4))
  706. 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]]])
  707. 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]]])
  708. var insulinDeliveryStore: InsulinDeliveryStore!
  709. var completion: XCTestExpectation!
  710. var queryAnchor: InsulinDeliveryStore.QueryAnchor!
  711. var limit: Int!
  712. override func setUp() {
  713. super.setUp()
  714. insulinDeliveryStore = InsulinDeliveryStore(healthStore: HKHealthStoreMock(),
  715. storeSamplesToHealthKit: false,
  716. cacheStore: cacheStore,
  717. observationEnabled: false,
  718. provenanceIdentifier: HKSource.default().bundleIdentifier)
  719. completion = expectation(description: "Completion")
  720. queryAnchor = InsulinDeliveryStore.QueryAnchor()
  721. limit = Int.max
  722. }
  723. override func tearDown() {
  724. limit = nil
  725. queryAnchor = nil
  726. completion = nil
  727. insulinDeliveryStore = nil
  728. super.tearDown()
  729. }
  730. func testDoseEmptyWithDefaultQueryAnchor() {
  731. insulinDeliveryStore.executeDoseQuery(fromQueryAnchor: queryAnchor, limit: limit) { result in
  732. switch result {
  733. case .failure(let error):
  734. XCTFail("Unexpected failure: \(error)")
  735. case .success(let anchor, let created, let deleted):
  736. XCTAssertEqual(anchor.modificationCounter, 0)
  737. XCTAssertEqual(created.count, 0)
  738. XCTAssertEqual(deleted.count, 0)
  739. }
  740. self.completion.fulfill()
  741. }
  742. wait(for: [completion], timeout: 2, enforceOrder: true)
  743. }
  744. func testDoseEmptyWithMissingQueryAnchor() {
  745. queryAnchor = nil
  746. insulinDeliveryStore.executeDoseQuery(fromQueryAnchor: queryAnchor, limit: limit) { result in
  747. switch result {
  748. case .failure(let error):
  749. XCTFail("Unexpected failure: \(error)")
  750. case .success(let anchor, let created, let deleted):
  751. XCTAssertEqual(anchor.modificationCounter, 0)
  752. XCTAssertEqual(created.count, 0)
  753. XCTAssertEqual(deleted.count, 0)
  754. }
  755. self.completion.fulfill()
  756. }
  757. wait(for: [completion], timeout: 2, enforceOrder: true)
  758. }
  759. func testDoseEmptyWithNonDefaultQueryAnchor() {
  760. queryAnchor.modificationCounter = 1
  761. insulinDeliveryStore.executeDoseQuery(fromQueryAnchor: queryAnchor, limit: limit) { result in
  762. switch result {
  763. case .failure(let error):
  764. XCTFail("Unexpected failure: \(error)")
  765. case .success(let anchor, let created, let deleted):
  766. XCTAssertEqual(anchor.modificationCounter, 1)
  767. XCTAssertEqual(created.count, 0)
  768. XCTAssertEqual(deleted.count, 0)
  769. }
  770. self.completion.fulfill()
  771. }
  772. wait(for: [completion], timeout: 2, enforceOrder: true)
  773. }
  774. func testDoseDataWithUnusedQueryAnchor() {
  775. let doseData = [DoseDatum(), DoseDatum(deleted: true), DoseDatum()]
  776. addDoseData(doseData)
  777. insulinDeliveryStore.executeDoseQuery(fromQueryAnchor: queryAnchor, limit: limit) { result in
  778. switch result {
  779. case .failure(let error):
  780. XCTFail("Unexpected failure: \(error)")
  781. case .success(let anchor, let created, let deleted):
  782. XCTAssertEqual(anchor.modificationCounter, 3)
  783. XCTAssertEqual(created.count, 2)
  784. XCTAssertEqual(deleted.count, 1)
  785. if created.count >= 2 {
  786. XCTAssertEqual(created[0].syncIdentifier, doseData[0].syncIdentifier)
  787. XCTAssertEqual(created[1].syncIdentifier, doseData[2].syncIdentifier)
  788. }
  789. if deleted.count >= 1 {
  790. XCTAssertEqual(deleted[0].syncIdentifier, doseData[1].syncIdentifier)
  791. }
  792. }
  793. self.completion.fulfill()
  794. }
  795. wait(for: [completion], timeout: 2, enforceOrder: true)
  796. }
  797. func testDoseDataWithStaleQueryAnchor() {
  798. let doseData = [DoseDatum(), DoseDatum(deleted: true), DoseDatum()]
  799. addDoseData(doseData)
  800. queryAnchor.modificationCounter = 2
  801. insulinDeliveryStore.executeDoseQuery(fromQueryAnchor: queryAnchor, limit: limit) { result in
  802. switch result {
  803. case .failure(let error):
  804. XCTFail("Unexpected failure: \(error)")
  805. case .success(let anchor, let created, let deleted):
  806. XCTAssertEqual(anchor.modificationCounter, 3)
  807. XCTAssertEqual(created.count, 1)
  808. XCTAssertEqual(deleted.count, 0)
  809. if created.count >= 1 {
  810. XCTAssertEqual(created[0].syncIdentifier, doseData[2].syncIdentifier)
  811. }
  812. }
  813. self.completion.fulfill()
  814. }
  815. wait(for: [completion], timeout: 2, enforceOrder: true)
  816. }
  817. func testDoseDataWithCurrentQueryAnchor() {
  818. let doseData = [DoseDatum(), DoseDatum(deleted: true), DoseDatum()]
  819. addDoseData(doseData)
  820. queryAnchor.modificationCounter = 3
  821. insulinDeliveryStore.executeDoseQuery(fromQueryAnchor: queryAnchor, limit: limit) { result in
  822. switch result {
  823. case .failure(let error):
  824. XCTFail("Unexpected failure: \(error)")
  825. case .success(let anchor, let created, let deleted):
  826. XCTAssertEqual(anchor.modificationCounter, 3)
  827. XCTAssertEqual(created.count, 0)
  828. XCTAssertEqual(deleted.count, 0)
  829. }
  830. self.completion.fulfill()
  831. }
  832. wait(for: [completion], timeout: 2, enforceOrder: true)
  833. }
  834. func testDoseDataWithLimitCoveredByData() {
  835. let doseData = [DoseDatum(), DoseDatum(deleted: true), DoseDatum()]
  836. addDoseData(doseData)
  837. limit = 2
  838. insulinDeliveryStore.executeDoseQuery(fromQueryAnchor: queryAnchor, limit: limit) { result in
  839. switch result {
  840. case .failure(let error):
  841. XCTFail("Unexpected failure: \(error)")
  842. case .success(let anchor, let created, let deleted):
  843. XCTAssertEqual(anchor.modificationCounter, 2)
  844. XCTAssertEqual(created.count, 1)
  845. XCTAssertEqual(deleted.count, 1)
  846. if created.count >= 1 {
  847. XCTAssertEqual(created[0].syncIdentifier, doseData[0].syncIdentifier)
  848. }
  849. if deleted.count >= 1 {
  850. XCTAssertEqual(deleted[0].syncIdentifier, doseData[1].syncIdentifier)
  851. }
  852. }
  853. self.completion.fulfill()
  854. }
  855. wait(for: [completion], timeout: 2, enforceOrder: true)
  856. }
  857. private func addDoseData(_ doseData: [DoseDatum]) {
  858. cacheStore.managedObjectContext.performAndWait {
  859. for doseDatum in doseData {
  860. let object = CachedInsulinDeliveryObject(context: self.cacheStore.managedObjectContext)
  861. object.provenanceIdentifier = HKSource.default().bundleIdentifier
  862. object.hasLoopKitOrigin = true
  863. object.startDate = Date().addingTimeInterval(-.minutes(15))
  864. object.endDate = Date().addingTimeInterval(-.minutes(5))
  865. object.syncIdentifier = doseDatum.syncIdentifier
  866. object.value = 1.0
  867. object.reason = .bolus
  868. object.createdAt = Date()
  869. object.deletedAt = doseDatum.deleted ? Date() : nil
  870. object.manuallyEntered = false
  871. object.isSuspend = false
  872. self.cacheStore.save()
  873. }
  874. }
  875. }
  876. private struct DoseDatum {
  877. let syncIdentifier: String
  878. let deleted: Bool
  879. init(deleted: Bool = false) {
  880. self.syncIdentifier = UUID().uuidString
  881. self.deleted = deleted
  882. }
  883. }
  884. }