DosingDecisionStoreTests.swift 49 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190
  1. //
  2. // DosingDecisionStoreTests.swift
  3. // LoopKitTests
  4. //
  5. // Created by Darin Krauss on 1/6/20.
  6. // Copyright © 2020 LoopKit Authors. All rights reserved.
  7. //
  8. import XCTest
  9. import HealthKit
  10. @testable import LoopKit
  11. class DosingDecisionStorePersistenceTests: PersistenceControllerTestCase, DosingDecisionStoreDelegate {
  12. var dosingDecisionStore: DosingDecisionStore!
  13. override func setUp() {
  14. super.setUp()
  15. dosingDecisionStoreHasUpdatedDosingDecisionDataHandler = nil
  16. dosingDecisionStore = DosingDecisionStore(store: cacheStore, expireAfter: .hours(1))
  17. dosingDecisionStore.delegate = self
  18. }
  19. override func tearDown() {
  20. dosingDecisionStore.delegate = nil
  21. dosingDecisionStore = nil
  22. dosingDecisionStoreHasUpdatedDosingDecisionDataHandler = nil
  23. super.tearDown()
  24. }
  25. // MARK: - DosingDecisionStoreDelegate
  26. var dosingDecisionStoreHasUpdatedDosingDecisionDataHandler: ((_ : DosingDecisionStore) -> Void)?
  27. func dosingDecisionStoreHasUpdatedDosingDecisionData(_ dosingDecisionStore: DosingDecisionStore) {
  28. dosingDecisionStoreHasUpdatedDosingDecisionDataHandler?(dosingDecisionStore)
  29. }
  30. // MARK: -
  31. func testStoreDosingDecision() {
  32. let storeDosingDecisionHandler = expectation(description: "Store dosing decision handler")
  33. let storeDosingDecisionCompletion = expectation(description: "Store dosing decision completion")
  34. var handlerInvocation = 0
  35. dosingDecisionStoreHasUpdatedDosingDecisionDataHandler = { dosingDecisionStore in
  36. handlerInvocation += 1
  37. switch handlerInvocation {
  38. case 1:
  39. storeDosingDecisionHandler.fulfill()
  40. default:
  41. XCTFail("Unexpected handler invocation")
  42. }
  43. }
  44. dosingDecisionStore.storeDosingDecision(StoredDosingDecision(reason: "test")) {
  45. storeDosingDecisionCompletion.fulfill()
  46. }
  47. wait(for: [storeDosingDecisionHandler, storeDosingDecisionCompletion], timeout: 2, enforceOrder: true)
  48. }
  49. func testStoreDosingDecisionMultiple() {
  50. let storeDosingDecisionHandler1 = expectation(description: "Store dosing decision handler 1")
  51. let storeDosingDecisionHandler2 = expectation(description: "Store dosing decision handler 2")
  52. let storeDosingDecisionCompletion1 = expectation(description: "Store dosing decision completion 1")
  53. let storeDosingDecisionCompletion2 = expectation(description: "Store dosing decision completion 2")
  54. var handlerInvocation = 0
  55. dosingDecisionStoreHasUpdatedDosingDecisionDataHandler = { dosingDecisionStore in
  56. handlerInvocation += 1
  57. switch handlerInvocation {
  58. case 1:
  59. storeDosingDecisionHandler1.fulfill()
  60. case 2:
  61. storeDosingDecisionHandler2.fulfill()
  62. default:
  63. XCTFail("Unexpected handler invocation")
  64. }
  65. }
  66. dosingDecisionStore.storeDosingDecision(StoredDosingDecision(reason: "test")) {
  67. storeDosingDecisionCompletion1.fulfill()
  68. }
  69. dosingDecisionStore.storeDosingDecision(StoredDosingDecision(reason: "test")) {
  70. storeDosingDecisionCompletion2.fulfill()
  71. }
  72. wait(for: [storeDosingDecisionHandler1, storeDosingDecisionCompletion1, storeDosingDecisionHandler2, storeDosingDecisionCompletion2], timeout: 2, enforceOrder: true)
  73. }
  74. func testDosingDecisionObjectEncodable() throws {
  75. cacheStore.managedObjectContext.performAndWait {
  76. do {
  77. let object = DosingDecisionObject(context: cacheStore.managedObjectContext)
  78. object.data = try PropertyListEncoder().encode(StoredDosingDecision.test)
  79. object.date = dateFormatter.date(from: "2100-01-02T03:03:00Z")!
  80. object.modificationCounter = 123
  81. let data = try encoder.encode(object)
  82. XCTAssertEqual(String(data: data, encoding: .utf8), """
  83. {
  84. "data" : {
  85. "automaticDoseRecommendation" : {
  86. "basalAdjustment" : {
  87. "duration" : 1800,
  88. "unitsPerHour" : 0.75
  89. },
  90. "bolusUnits" : 1.25
  91. },
  92. "carbEntry" : {
  93. "absorptionTime" : 18000,
  94. "foodType" : "Pizza",
  95. "provenanceIdentifier" : "com.loopkit.loop",
  96. "quantity" : 29,
  97. "startDate" : "2020-01-02T03:00:23Z",
  98. "syncIdentifier" : "2B03D96C-6F5D-4140-99CD-80C3E64D6010",
  99. "syncVersion" : 2,
  100. "userCreatedDate" : "2020-05-14T22:06:12Z",
  101. "userUpdatedDate" : "2020-05-14T22:07:32Z",
  102. "uuid" : "135CDABE-9343-7242-4233-1020384789AE"
  103. },
  104. "carbsOnBoard" : {
  105. "endDate" : "2020-05-14T23:18:41Z",
  106. "startDate" : "2020-05-14T22:48:41Z",
  107. "value" : 45.5
  108. },
  109. "cgmManagerStatus" : {
  110. "device" : {
  111. "firmwareVersion" : "CGM Firmware Version",
  112. "hardwareVersion" : "CGM Hardware Version",
  113. "localIdentifier" : "CGM Local Identifier",
  114. "manufacturer" : "CGM Manufacturer",
  115. "model" : "CGM Model",
  116. "name" : "CGM Name",
  117. "softwareVersion" : "CGM Software Version",
  118. "udiDeviceIdentifier" : "CGM UDI Device Identifier"
  119. },
  120. "hasValidSensorSession" : true,
  121. "lastCommunicationDate" : "2020-05-14T22:07:01Z"
  122. },
  123. "controllerStatus" : {
  124. "batteryLevel" : 0.5,
  125. "batteryState" : "charging"
  126. },
  127. "controllerTimeZone" : {
  128. "identifier" : "America/Los_Angeles"
  129. },
  130. "date" : "2020-05-14T22:38:14Z",
  131. "errors" : [
  132. {
  133. "id" : "alpha"
  134. },
  135. {
  136. "details" : {
  137. "size" : "tiny"
  138. },
  139. "id" : "bravo"
  140. }
  141. ],
  142. "glucoseTargetRangeSchedule" : {
  143. "override" : {
  144. "end" : "2020-05-14T23:12:17Z",
  145. "start" : "2020-05-14T21:12:17Z",
  146. "value" : {
  147. "maxValue" : 115,
  148. "minValue" : 105
  149. }
  150. },
  151. "rangeSchedule" : {
  152. "unit" : "mg/dL",
  153. "valueSchedule" : {
  154. "items" : [
  155. {
  156. "startTime" : 0,
  157. "value" : {
  158. "maxValue" : 110,
  159. "minValue" : 100
  160. }
  161. },
  162. {
  163. "startTime" : 25200,
  164. "value" : {
  165. "maxValue" : 100,
  166. "minValue" : 90
  167. }
  168. },
  169. {
  170. "startTime" : 75600,
  171. "value" : {
  172. "maxValue" : 120,
  173. "minValue" : 110
  174. }
  175. }
  176. ],
  177. "referenceTimeInterval" : 0,
  178. "repeatInterval" : 86400,
  179. "timeZone" : {
  180. "identifier" : "GMT-0700"
  181. }
  182. }
  183. }
  184. },
  185. "historicalGlucose" : [
  186. {
  187. "quantity" : 117.3,
  188. "quantityUnit" : "mg/dL",
  189. "startDate" : "2020-05-14T22:29:15Z"
  190. },
  191. {
  192. "quantity" : 119.5,
  193. "quantityUnit" : "mg/dL",
  194. "startDate" : "2020-05-14T22:33:15Z"
  195. },
  196. {
  197. "quantity" : 121.8,
  198. "quantityUnit" : "mg/dL",
  199. "startDate" : "2020-05-14T22:38:15Z"
  200. }
  201. ],
  202. "insulinOnBoard" : {
  203. "startDate" : "2020-05-14T22:38:26Z",
  204. "value" : 1.5
  205. },
  206. "lastReservoirValue" : {
  207. "startDate" : "2020-05-14T22:07:19Z",
  208. "unitVolume" : 113.3
  209. },
  210. "manualBolusRecommendation" : {
  211. "date" : "2020-05-14T22:38:16Z",
  212. "recommendation" : {
  213. "amount" : 1.2,
  214. "notice" : {
  215. "predictedGlucoseBelowTarget" : {
  216. "minGlucose" : {
  217. "endDate" : "2020-05-14T23:03:15Z",
  218. "quantity" : 75.5,
  219. "quantityUnit" : "mg/dL",
  220. "startDate" : "2020-05-14T23:03:15Z"
  221. }
  222. }
  223. },
  224. "pendingInsulin" : 0.75
  225. }
  226. },
  227. "manualBolusRequested" : 0.8,
  228. "manualGlucoseSample" : {
  229. "condition" : "aboveRange",
  230. "device" : {
  231. "firmwareVersion" : "Device Firmware Version",
  232. "hardwareVersion" : "Device Hardware Version",
  233. "localIdentifier" : "Device Local Identifier",
  234. "manufacturer" : "Device Manufacturer",
  235. "model" : "Device Model",
  236. "name" : "Device Name",
  237. "softwareVersion" : "Device Software Version",
  238. "udiDeviceIdentifier" : "Device UDI Device Identifier"
  239. },
  240. "provenanceIdentifier" : "com.loopkit.loop",
  241. "quantity" : 400,
  242. "startDate" : "2020-05-14T22:09:00Z",
  243. "syncIdentifier" : "d3876f59-adb3-4a4f-8b29-315cda22062e",
  244. "syncVersion" : 1,
  245. "trend" : 7,
  246. "trendRate" : -10.2,
  247. "uuid" : "DA0CED44-E4F1-49C4-BAF8-6EFA6D75525F",
  248. "wasUserEntered" : true
  249. },
  250. "originalCarbEntry" : {
  251. "absorptionTime" : 18000,
  252. "foodType" : "Pizza",
  253. "provenanceIdentifier" : "com.loopkit.loop",
  254. "quantity" : 19,
  255. "startDate" : "2020-01-02T03:00:23Z",
  256. "syncIdentifier" : "2B03D96C-6F5D-4140-99CD-80C3E64D6010",
  257. "syncVersion" : 1,
  258. "userCreatedDate" : "2020-05-14T22:06:12Z",
  259. "uuid" : "18CF3948-0B3D-4B12-8BFE-14986B0E6784"
  260. },
  261. "predictedGlucose" : [
  262. {
  263. "quantity" : 123.3,
  264. "quantityUnit" : "mg/dL",
  265. "startDate" : "2020-05-14T22:43:15Z"
  266. },
  267. {
  268. "quantity" : 125.5,
  269. "quantityUnit" : "mg/dL",
  270. "startDate" : "2020-05-14T22:48:15Z"
  271. },
  272. {
  273. "quantity" : 127.8,
  274. "quantityUnit" : "mg/dL",
  275. "startDate" : "2020-05-14T22:53:15Z"
  276. }
  277. ],
  278. "pumpManagerStatus" : {
  279. "basalDeliveryState" : "initiatingTempBasal",
  280. "bolusState" : "noBolus",
  281. "deliveryIsUncertain" : false,
  282. "device" : {
  283. "firmwareVersion" : "Pump Firmware Version",
  284. "hardwareVersion" : "Pump Hardware Version",
  285. "localIdentifier" : "Pump Local Identifier",
  286. "manufacturer" : "Pump Manufacturer",
  287. "model" : "Pump Model",
  288. "name" : "Pump Name",
  289. "softwareVersion" : "Pump Software Version",
  290. "udiDeviceIdentifier" : "Pump UDI Device Identifier"
  291. },
  292. "insulinType" : 0,
  293. "pumpBatteryChargeRemaining" : 0.75,
  294. "timeZone" : {
  295. "identifier" : "GMT-0700"
  296. }
  297. },
  298. "reason" : "test",
  299. "scheduleOverride" : {
  300. "actualEnd" : {
  301. "type" : "natural"
  302. },
  303. "context" : "preMeal",
  304. "duration" : {
  305. "finite" : {
  306. "duration" : 3600
  307. }
  308. },
  309. "enactTrigger" : "local",
  310. "settings" : {
  311. "insulinNeedsScaleFactor" : 1.5,
  312. "targetRangeInMgdl" : {
  313. "maxValue" : 90,
  314. "minValue" : 80
  315. }
  316. },
  317. "startDate" : "2020-05-14T22:22:01Z",
  318. "syncIdentifier" : "394818CF-99CD-4B12-99CD-0E678414986B"
  319. },
  320. "settings" : {
  321. "syncIdentifier" : "2B03D96C-99CD-4140-99CD-80C3E64D6011"
  322. },
  323. "syncIdentifier" : "2A67A303-5203-4CB8-8263-79498265368E",
  324. "warnings" : [
  325. {
  326. "id" : "one"
  327. },
  328. {
  329. "details" : {
  330. "size" : "small"
  331. },
  332. "id" : "two"
  333. }
  334. ]
  335. },
  336. "date" : "2100-01-02T03:03:00Z",
  337. "modificationCounter" : 123
  338. }
  339. """
  340. )
  341. } catch let error {
  342. XCTFail("Unexpected failure: \(error)")
  343. }
  344. }
  345. }
  346. private let dateFormatter = ISO8601DateFormatter()
  347. private let encoder: JSONEncoder = {
  348. let encoder = JSONEncoder()
  349. encoder.outputFormatting = [.prettyPrinted, .sortedKeys, .withoutEscapingSlashes]
  350. encoder.dateEncodingStrategy = .iso8601
  351. return encoder
  352. }()
  353. }
  354. class DosingDecisionStoreQueryAnchorTests: XCTestCase {
  355. var rawValue: DosingDecisionStore.QueryAnchor.RawValue = [
  356. "modificationCounter": Int64(123)
  357. ]
  358. func testInitializerDefault() {
  359. let queryAnchor = DosingDecisionStore.QueryAnchor()
  360. XCTAssertEqual(queryAnchor.modificationCounter, 0)
  361. }
  362. func testInitializerRawValue() {
  363. let queryAnchor = DosingDecisionStore.QueryAnchor(rawValue: rawValue)
  364. XCTAssertNotNil(queryAnchor)
  365. XCTAssertEqual(queryAnchor?.modificationCounter, 123)
  366. }
  367. func testInitializerRawValueMissingModificationCounter() {
  368. rawValue["modificationCounter"] = nil
  369. XCTAssertNil(DosingDecisionStore.QueryAnchor(rawValue: rawValue))
  370. }
  371. func testInitializerRawValueInvalidModificationCounter() {
  372. rawValue["modificationCounter"] = "123"
  373. XCTAssertNil(DosingDecisionStore.QueryAnchor(rawValue: rawValue))
  374. }
  375. func testRawValueWithDefault() {
  376. let rawValue = DosingDecisionStore.QueryAnchor().rawValue
  377. XCTAssertEqual(rawValue.count, 1)
  378. XCTAssertEqual(rawValue["modificationCounter"] as? Int64, Int64(0))
  379. }
  380. func testRawValueWithNonDefault() {
  381. var queryAnchor = DosingDecisionStore.QueryAnchor()
  382. queryAnchor.modificationCounter = 123
  383. let rawValue = queryAnchor.rawValue
  384. XCTAssertEqual(rawValue.count, 1)
  385. XCTAssertEqual(rawValue["modificationCounter"] as? Int64, Int64(123))
  386. }
  387. }
  388. class DosingDecisionStoreQueryTests: PersistenceControllerTestCase {
  389. var dosingDecisionStore: DosingDecisionStore!
  390. var completion: XCTestExpectation!
  391. var queryAnchor: DosingDecisionStore.QueryAnchor!
  392. var limit: Int!
  393. override func setUp() {
  394. super.setUp()
  395. dosingDecisionStore = DosingDecisionStore(store: cacheStore, expireAfter: .hours(1))
  396. completion = expectation(description: "Completion")
  397. queryAnchor = DosingDecisionStore.QueryAnchor()
  398. limit = Int.max
  399. }
  400. override func tearDown() {
  401. limit = nil
  402. queryAnchor = nil
  403. completion = nil
  404. dosingDecisionStore = nil
  405. super.tearDown()
  406. }
  407. // MARK: -
  408. func testEmptyWithDefaultQueryAnchor() {
  409. dosingDecisionStore.executeDosingDecisionQuery(fromQueryAnchor: queryAnchor, limit: limit) { result in
  410. switch result {
  411. case .failure(let error):
  412. XCTFail("Unexpected failure: \(error)")
  413. case .success(let anchor, let data):
  414. XCTAssertEqual(anchor.modificationCounter, 0)
  415. XCTAssertEqual(data.count, 0)
  416. }
  417. self.completion.fulfill()
  418. }
  419. wait(for: [completion], timeout: 2, enforceOrder: true)
  420. }
  421. func testEmptyWithMissingQueryAnchor() {
  422. queryAnchor = nil
  423. dosingDecisionStore.executeDosingDecisionQuery(fromQueryAnchor: queryAnchor, limit: limit) { result in
  424. switch result {
  425. case .failure(let error):
  426. XCTFail("Unexpected failure: \(error)")
  427. case .success(let anchor, let data):
  428. XCTAssertEqual(anchor.modificationCounter, 0)
  429. XCTAssertEqual(data.count, 0)
  430. }
  431. self.completion.fulfill()
  432. }
  433. wait(for: [completion], timeout: 2, enforceOrder: true)
  434. }
  435. func testEmptyWithNonDefaultQueryAnchor() {
  436. queryAnchor.modificationCounter = 1
  437. dosingDecisionStore.executeDosingDecisionQuery(fromQueryAnchor: queryAnchor, limit: limit) { result in
  438. switch result {
  439. case .failure(let error):
  440. XCTFail("Unexpected failure: \(error)")
  441. case .success(let anchor, let data):
  442. XCTAssertEqual(anchor.modificationCounter, 1)
  443. XCTAssertEqual(data.count, 0)
  444. }
  445. self.completion.fulfill()
  446. }
  447. wait(for: [completion], timeout: 2, enforceOrder: true)
  448. }
  449. func testDataWithUnusedQueryAnchor() {
  450. let syncIdentifiers = [generateSyncIdentifier(), generateSyncIdentifier(), generateSyncIdentifier()]
  451. addData(withSyncIdentifiers: syncIdentifiers)
  452. dosingDecisionStore.executeDosingDecisionQuery(fromQueryAnchor: queryAnchor, limit: limit) { result in
  453. switch result {
  454. case .failure(let error):
  455. XCTFail("Unexpected failure: \(error)")
  456. case .success(let anchor, let data):
  457. XCTAssertEqual(anchor.modificationCounter, 3)
  458. XCTAssertEqual(data.count, 3)
  459. for (index, syncIdentifier) in syncIdentifiers.enumerated() {
  460. XCTAssertEqual(data[index].syncIdentifier, syncIdentifier)
  461. }
  462. }
  463. self.completion.fulfill()
  464. }
  465. wait(for: [completion], timeout: 2, enforceOrder: true)
  466. }
  467. func testDataWithStaleQueryAnchor() {
  468. let syncIdentifiers = [generateSyncIdentifier(), generateSyncIdentifier(), generateSyncIdentifier()]
  469. addData(withSyncIdentifiers: syncIdentifiers)
  470. queryAnchor.modificationCounter = 2
  471. dosingDecisionStore.executeDosingDecisionQuery(fromQueryAnchor: queryAnchor, limit: limit) { result in
  472. switch result {
  473. case .failure(let error):
  474. XCTFail("Unexpected failure: \(error)")
  475. case .success(let anchor, let data):
  476. XCTAssertEqual(anchor.modificationCounter, 3)
  477. XCTAssertEqual(data.count, 1)
  478. XCTAssertEqual(data[0].syncIdentifier, syncIdentifiers[2])
  479. }
  480. self.completion.fulfill()
  481. }
  482. wait(for: [completion], timeout: 2, enforceOrder: true)
  483. }
  484. func testDataWithCurrentQueryAnchor() {
  485. let syncIdentifiers = [generateSyncIdentifier(), generateSyncIdentifier(), generateSyncIdentifier()]
  486. addData(withSyncIdentifiers: syncIdentifiers)
  487. queryAnchor.modificationCounter = 3
  488. dosingDecisionStore.executeDosingDecisionQuery(fromQueryAnchor: queryAnchor, limit: limit) { result in
  489. switch result {
  490. case .failure(let error):
  491. XCTFail("Unexpected failure: \(error)")
  492. case .success(let anchor, let data):
  493. XCTAssertEqual(anchor.modificationCounter, 3)
  494. XCTAssertEqual(data.count, 0)
  495. }
  496. self.completion.fulfill()
  497. }
  498. wait(for: [completion], timeout: 2, enforceOrder: true)
  499. }
  500. func testDataWithLimitZero() {
  501. let syncIdentifiers = [generateSyncIdentifier(), generateSyncIdentifier(), generateSyncIdentifier()]
  502. addData(withSyncIdentifiers: syncIdentifiers)
  503. limit = 0
  504. dosingDecisionStore.executeDosingDecisionQuery(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 testDataWithLimitCoveredByData() {
  517. let syncIdentifiers = [generateSyncIdentifier(), generateSyncIdentifier(), generateSyncIdentifier()]
  518. addData(withSyncIdentifiers: syncIdentifiers)
  519. limit = 2
  520. dosingDecisionStore.executeDosingDecisionQuery(fromQueryAnchor: queryAnchor, limit: limit) { result in
  521. switch result {
  522. case .failure(let error):
  523. XCTFail("Unexpected failure: \(error)")
  524. case .success(let anchor, let data):
  525. XCTAssertEqual(anchor.modificationCounter, 2)
  526. XCTAssertEqual(data.count, 2)
  527. XCTAssertEqual(data[0].syncIdentifier, syncIdentifiers[0])
  528. XCTAssertEqual(data[1].syncIdentifier, syncIdentifiers[1])
  529. }
  530. self.completion.fulfill()
  531. }
  532. wait(for: [completion], timeout: 2, enforceOrder: true)
  533. }
  534. private func addData(withSyncIdentifiers syncIdentifiers: [UUID]) {
  535. let semaphore = DispatchSemaphore(value: 0)
  536. for syncIdentifier in syncIdentifiers {
  537. self.dosingDecisionStore.storeDosingDecision(StoredDosingDecision(reason: "test", syncIdentifier: syncIdentifier)) { semaphore.signal() }
  538. }
  539. for _ in syncIdentifiers { semaphore.wait() }
  540. }
  541. private func generateSyncIdentifier() -> UUID { UUID() }
  542. }
  543. class DosingDecisionStoreCriticalEventLogTests: PersistenceControllerTestCase {
  544. var dosingDecisionStore: DosingDecisionStore!
  545. var outputStream: MockOutputStream!
  546. var progress: Progress!
  547. override func setUp() {
  548. super.setUp()
  549. let dosingDecisions = [StoredDosingDecision(date: dateFormatter.date(from: "2100-01-02T03:08:00Z")!, controllerTimeZone: TimeZone(identifier: "America/Los_Angeles")!, reason: "test", syncIdentifier: UUID(uuidString: "18CF3948-0B3D-4B12-8BFE-14986B0E6784")!),
  550. StoredDosingDecision(date: dateFormatter.date(from: "2100-01-02T03:10:00Z")!, controllerTimeZone: TimeZone(identifier: "America/Los_Angeles")!, reason: "test", syncIdentifier: UUID(uuidString: "C86DEB61-68E9-464E-9DD5-96A9CB445FD3")!),
  551. StoredDosingDecision(date: dateFormatter.date(from: "2100-01-02T03:04:00Z")!, controllerTimeZone: TimeZone(identifier: "America/Los_Angeles")!, reason: "test", syncIdentifier: UUID(uuidString: "2B03D96C-6F5D-4140-99CD-80C3E64D6010")!),
  552. StoredDosingDecision(date: dateFormatter.date(from: "2100-01-02T03:06:00Z")!, controllerTimeZone: TimeZone(identifier: "America/Los_Angeles")!, reason: "test", syncIdentifier: UUID(uuidString: "FF1C4F01-3558-4FB2-957E-FA1522C4735E")!),
  553. StoredDosingDecision(date: dateFormatter.date(from: "2100-01-02T03:02:00Z")!, controllerTimeZone: TimeZone(identifier: "America/Los_Angeles")!, reason: "test", syncIdentifier: UUID(uuidString: "71B699D7-0E8F-4B13-B7A1-E7751EB78E74")!)]
  554. dosingDecisionStore = DosingDecisionStore(store: cacheStore, expireAfter: .hours(1))
  555. let dispatchGroup = DispatchGroup()
  556. dispatchGroup.enter()
  557. dosingDecisionStore.addStoredDosingDecisions(dosingDecisions: dosingDecisions) { error in
  558. XCTAssertNil(error)
  559. dispatchGroup.leave()
  560. }
  561. dispatchGroup.wait()
  562. outputStream = MockOutputStream()
  563. progress = Progress()
  564. }
  565. override func tearDown() {
  566. dosingDecisionStore = nil
  567. super.tearDown()
  568. }
  569. func testExportProgressTotalUnitCount() {
  570. switch dosingDecisionStore.exportProgressTotalUnitCount(startDate: dateFormatter.date(from: "2100-01-02T03:03:00Z")!,
  571. endDate: dateFormatter.date(from: "2100-01-02T03:09:00Z")!) {
  572. case .failure(let error):
  573. XCTFail("Unexpected failure: \(error)")
  574. case .success(let progressTotalUnitCount):
  575. XCTAssertEqual(progressTotalUnitCount, 3 * 33)
  576. }
  577. }
  578. func testExportProgressTotalUnitCountEmpty() {
  579. switch dosingDecisionStore.exportProgressTotalUnitCount(startDate: dateFormatter.date(from: "2100-01-02T03:00:00Z")!,
  580. endDate: dateFormatter.date(from: "2100-01-02T03:01:00Z")!) {
  581. case .failure(let error):
  582. XCTFail("Unexpected failure: \(error)")
  583. case .success(let progressTotalUnitCount):
  584. XCTAssertEqual(progressTotalUnitCount, 0)
  585. }
  586. }
  587. func testExport() {
  588. XCTAssertNil(dosingDecisionStore.export(startDate: dateFormatter.date(from: "2100-01-02T03:03:00Z")!,
  589. endDate: dateFormatter.date(from: "2100-01-02T03:09:00Z")!,
  590. to: outputStream,
  591. progress: progress))
  592. XCTAssertEqual(outputStream.string, """
  593. [
  594. {"data":{"controllerTimeZone":{"identifier":"America/Los_Angeles"},"date":"2100-01-02T03:08:00.000Z","reason":"test","syncIdentifier":"18CF3948-0B3D-4B12-8BFE-14986B0E6784"},"date":"2100-01-02T03:08:00.000Z","modificationCounter":1},
  595. {"data":{"controllerTimeZone":{"identifier":"America/Los_Angeles"},"date":"2100-01-02T03:04:00.000Z","reason":"test","syncIdentifier":"2B03D96C-6F5D-4140-99CD-80C3E64D6010"},"date":"2100-01-02T03:04:00.000Z","modificationCounter":3},
  596. {"data":{"controllerTimeZone":{"identifier":"America/Los_Angeles"},"date":"2100-01-02T03:06:00.000Z","reason":"test","syncIdentifier":"FF1C4F01-3558-4FB2-957E-FA1522C4735E"},"date":"2100-01-02T03:06:00.000Z","modificationCounter":4}
  597. ]
  598. """
  599. )
  600. XCTAssertEqual(progress.completedUnitCount, 3 * 33)
  601. }
  602. func testExportEmpty() {
  603. XCTAssertNil(dosingDecisionStore.export(startDate: dateFormatter.date(from: "2100-01-02T03:00:00Z")!,
  604. endDate: dateFormatter.date(from: "2100-01-02T03:01:00Z")!,
  605. to: outputStream,
  606. progress: progress))
  607. XCTAssertEqual(outputStream.string, "[]")
  608. XCTAssertEqual(progress.completedUnitCount, 0)
  609. }
  610. func testExportCancelled() {
  611. progress.cancel()
  612. XCTAssertEqual(dosingDecisionStore.export(startDate: dateFormatter.date(from: "2100-01-02T03:03:00Z")!,
  613. endDate: dateFormatter.date(from: "2100-01-02T03:09:00Z")!,
  614. to: outputStream,
  615. progress: progress) as? CriticalEventLogError, CriticalEventLogError.cancelled)
  616. }
  617. private let dateFormatter = ISO8601DateFormatter()
  618. }
  619. class StoredDosingDecisionCodableTests: XCTestCase {
  620. func testCodable() throws {
  621. let data = try encoder.encode(StoredDosingDecision.test)
  622. XCTAssertEqual(String(data: data, encoding: .utf8), """
  623. {
  624. "automaticDoseRecommendation" : {
  625. "basalAdjustment" : {
  626. "duration" : 1800,
  627. "unitsPerHour" : 0.75
  628. },
  629. "bolusUnits" : 1.25
  630. },
  631. "carbEntry" : {
  632. "absorptionTime" : 18000,
  633. "foodType" : "Pizza",
  634. "provenanceIdentifier" : "com.loopkit.loop",
  635. "quantity" : 29,
  636. "startDate" : "2020-01-02T03:00:23Z",
  637. "syncIdentifier" : "2B03D96C-6F5D-4140-99CD-80C3E64D6010",
  638. "syncVersion" : 2,
  639. "userCreatedDate" : "2020-05-14T22:06:12Z",
  640. "userUpdatedDate" : "2020-05-14T22:07:32Z",
  641. "uuid" : "135CDABE-9343-7242-4233-1020384789AE"
  642. },
  643. "carbsOnBoard" : {
  644. "endDate" : "2020-05-14T23:18:41Z",
  645. "startDate" : "2020-05-14T22:48:41Z",
  646. "value" : 45.5
  647. },
  648. "cgmManagerStatus" : {
  649. "device" : {
  650. "firmwareVersion" : "CGM Firmware Version",
  651. "hardwareVersion" : "CGM Hardware Version",
  652. "localIdentifier" : "CGM Local Identifier",
  653. "manufacturer" : "CGM Manufacturer",
  654. "model" : "CGM Model",
  655. "name" : "CGM Name",
  656. "softwareVersion" : "CGM Software Version",
  657. "udiDeviceIdentifier" : "CGM UDI Device Identifier"
  658. },
  659. "hasValidSensorSession" : true,
  660. "lastCommunicationDate" : "2020-05-14T22:07:01Z"
  661. },
  662. "controllerStatus" : {
  663. "batteryLevel" : 0.5,
  664. "batteryState" : "charging"
  665. },
  666. "controllerTimeZone" : {
  667. "identifier" : "America/Los_Angeles"
  668. },
  669. "date" : "2020-05-14T22:38:14Z",
  670. "errors" : [
  671. {
  672. "id" : "alpha"
  673. },
  674. {
  675. "details" : {
  676. "size" : "tiny"
  677. },
  678. "id" : "bravo"
  679. }
  680. ],
  681. "glucoseTargetRangeSchedule" : {
  682. "override" : {
  683. "end" : "2020-05-14T23:12:17Z",
  684. "start" : "2020-05-14T21:12:17Z",
  685. "value" : {
  686. "maxValue" : 115,
  687. "minValue" : 105
  688. }
  689. },
  690. "rangeSchedule" : {
  691. "unit" : "mg/dL",
  692. "valueSchedule" : {
  693. "items" : [
  694. {
  695. "startTime" : 0,
  696. "value" : {
  697. "maxValue" : 110,
  698. "minValue" : 100
  699. }
  700. },
  701. {
  702. "startTime" : 25200,
  703. "value" : {
  704. "maxValue" : 100,
  705. "minValue" : 90
  706. }
  707. },
  708. {
  709. "startTime" : 75600,
  710. "value" : {
  711. "maxValue" : 120,
  712. "minValue" : 110
  713. }
  714. }
  715. ],
  716. "referenceTimeInterval" : 0,
  717. "repeatInterval" : 86400,
  718. "timeZone" : {
  719. "identifier" : "GMT-0700"
  720. }
  721. }
  722. }
  723. },
  724. "historicalGlucose" : [
  725. {
  726. "quantity" : 117.3,
  727. "quantityUnit" : "mg/dL",
  728. "startDate" : "2020-05-14T22:29:15Z"
  729. },
  730. {
  731. "quantity" : 119.5,
  732. "quantityUnit" : "mg/dL",
  733. "startDate" : "2020-05-14T22:33:15Z"
  734. },
  735. {
  736. "quantity" : 121.8,
  737. "quantityUnit" : "mg/dL",
  738. "startDate" : "2020-05-14T22:38:15Z"
  739. }
  740. ],
  741. "insulinOnBoard" : {
  742. "startDate" : "2020-05-14T22:38:26Z",
  743. "value" : 1.5
  744. },
  745. "lastReservoirValue" : {
  746. "startDate" : "2020-05-14T22:07:19Z",
  747. "unitVolume" : 113.3
  748. },
  749. "manualBolusRecommendation" : {
  750. "date" : "2020-05-14T22:38:16Z",
  751. "recommendation" : {
  752. "amount" : 1.2,
  753. "notice" : {
  754. "predictedGlucoseBelowTarget" : {
  755. "minGlucose" : {
  756. "endDate" : "2020-05-14T23:03:15Z",
  757. "quantity" : 75.5,
  758. "quantityUnit" : "mg/dL",
  759. "startDate" : "2020-05-14T23:03:15Z"
  760. }
  761. }
  762. },
  763. "pendingInsulin" : 0.75
  764. }
  765. },
  766. "manualBolusRequested" : 0.8,
  767. "manualGlucoseSample" : {
  768. "condition" : "aboveRange",
  769. "device" : {
  770. "firmwareVersion" : "Device Firmware Version",
  771. "hardwareVersion" : "Device Hardware Version",
  772. "localIdentifier" : "Device Local Identifier",
  773. "manufacturer" : "Device Manufacturer",
  774. "model" : "Device Model",
  775. "name" : "Device Name",
  776. "softwareVersion" : "Device Software Version",
  777. "udiDeviceIdentifier" : "Device UDI Device Identifier"
  778. },
  779. "provenanceIdentifier" : "com.loopkit.loop",
  780. "quantity" : 400,
  781. "startDate" : "2020-05-14T22:09:00Z",
  782. "syncIdentifier" : "d3876f59-adb3-4a4f-8b29-315cda22062e",
  783. "syncVersion" : 1,
  784. "trend" : 7,
  785. "trendRate" : -10.2,
  786. "uuid" : "DA0CED44-E4F1-49C4-BAF8-6EFA6D75525F",
  787. "wasUserEntered" : true
  788. },
  789. "originalCarbEntry" : {
  790. "absorptionTime" : 18000,
  791. "foodType" : "Pizza",
  792. "provenanceIdentifier" : "com.loopkit.loop",
  793. "quantity" : 19,
  794. "startDate" : "2020-01-02T03:00:23Z",
  795. "syncIdentifier" : "2B03D96C-6F5D-4140-99CD-80C3E64D6010",
  796. "syncVersion" : 1,
  797. "userCreatedDate" : "2020-05-14T22:06:12Z",
  798. "uuid" : "18CF3948-0B3D-4B12-8BFE-14986B0E6784"
  799. },
  800. "predictedGlucose" : [
  801. {
  802. "quantity" : 123.3,
  803. "quantityUnit" : "mg/dL",
  804. "startDate" : "2020-05-14T22:43:15Z"
  805. },
  806. {
  807. "quantity" : 125.5,
  808. "quantityUnit" : "mg/dL",
  809. "startDate" : "2020-05-14T22:48:15Z"
  810. },
  811. {
  812. "quantity" : 127.8,
  813. "quantityUnit" : "mg/dL",
  814. "startDate" : "2020-05-14T22:53:15Z"
  815. }
  816. ],
  817. "pumpManagerStatus" : {
  818. "basalDeliveryState" : "initiatingTempBasal",
  819. "bolusState" : "noBolus",
  820. "deliveryIsUncertain" : false,
  821. "device" : {
  822. "firmwareVersion" : "Pump Firmware Version",
  823. "hardwareVersion" : "Pump Hardware Version",
  824. "localIdentifier" : "Pump Local Identifier",
  825. "manufacturer" : "Pump Manufacturer",
  826. "model" : "Pump Model",
  827. "name" : "Pump Name",
  828. "softwareVersion" : "Pump Software Version",
  829. "udiDeviceIdentifier" : "Pump UDI Device Identifier"
  830. },
  831. "insulinType" : 0,
  832. "pumpBatteryChargeRemaining" : 0.75,
  833. "timeZone" : {
  834. "identifier" : "GMT-0700"
  835. }
  836. },
  837. "reason" : "test",
  838. "scheduleOverride" : {
  839. "actualEnd" : {
  840. "type" : "natural"
  841. },
  842. "context" : "preMeal",
  843. "duration" : {
  844. "finite" : {
  845. "duration" : 3600
  846. }
  847. },
  848. "enactTrigger" : "local",
  849. "settings" : {
  850. "insulinNeedsScaleFactor" : 1.5,
  851. "targetRangeInMgdl" : {
  852. "maxValue" : 90,
  853. "minValue" : 80
  854. }
  855. },
  856. "startDate" : "2020-05-14T22:22:01Z",
  857. "syncIdentifier" : "394818CF-99CD-4B12-99CD-0E678414986B"
  858. },
  859. "settings" : {
  860. "syncIdentifier" : "2B03D96C-99CD-4140-99CD-80C3E64D6011"
  861. },
  862. "syncIdentifier" : "2A67A303-5203-4CB8-8263-79498265368E",
  863. "warnings" : [
  864. {
  865. "id" : "one"
  866. },
  867. {
  868. "details" : {
  869. "size" : "small"
  870. },
  871. "id" : "two"
  872. }
  873. ]
  874. }
  875. """
  876. )
  877. }
  878. private func assertStoredDosingDecisionCodable(_ original: StoredDosingDecision, encodesJSON string: String) throws {
  879. let data = try encoder.encode(original)
  880. XCTAssertEqual(String(data: data, encoding: .utf8), string)
  881. let decoded = try decoder.decode(StoredDosingDecision.self, from: data)
  882. XCTAssertEqual(decoded, original)
  883. }
  884. private let dateFormatter = ISO8601DateFormatter()
  885. private let encoder: JSONEncoder = {
  886. let encoder = JSONEncoder()
  887. encoder.outputFormatting = [.prettyPrinted, .sortedKeys, .withoutEscapingSlashes]
  888. encoder.dateEncodingStrategy = .iso8601
  889. return encoder
  890. }()
  891. private let decoder: JSONDecoder = {
  892. let decoder = JSONDecoder()
  893. decoder.dateDecodingStrategy = .iso8601
  894. return decoder
  895. }()
  896. }
  897. extension StoredDosingDecision: Equatable {
  898. public static func == (lhs: StoredDosingDecision, rhs: StoredDosingDecision) -> Bool {
  899. return lhs.date == rhs.date &&
  900. lhs.controllerTimeZone == rhs.controllerTimeZone &&
  901. lhs.reason == rhs.reason &&
  902. lhs.settings == rhs.settings &&
  903. lhs.controllerStatus == rhs.controllerStatus &&
  904. lhs.pumpManagerStatus == rhs.pumpManagerStatus &&
  905. lhs.cgmManagerStatus == rhs.cgmManagerStatus &&
  906. lhs.lastReservoirValue == rhs.lastReservoirValue &&
  907. lhs.historicalGlucose == rhs.historicalGlucose &&
  908. lhs.originalCarbEntry == rhs.originalCarbEntry &&
  909. lhs.carbEntry == rhs.carbEntry &&
  910. lhs.manualGlucoseSample == rhs.manualGlucoseSample &&
  911. lhs.carbsOnBoard == rhs.carbsOnBoard &&
  912. lhs.insulinOnBoard == rhs.insulinOnBoard &&
  913. lhs.glucoseTargetRangeSchedule == rhs.glucoseTargetRangeSchedule &&
  914. lhs.predictedGlucose == rhs.predictedGlucose &&
  915. lhs.automaticDoseRecommendation == rhs.automaticDoseRecommendation &&
  916. lhs.manualBolusRecommendation == rhs.manualBolusRecommendation &&
  917. lhs.manualBolusRequested == rhs.manualBolusRequested &&
  918. lhs.warnings == rhs.warnings &&
  919. lhs.errors == rhs.errors &&
  920. lhs.syncIdentifier == rhs.syncIdentifier
  921. }
  922. }
  923. extension StoredDosingDecision.LastReservoirValue: Equatable {
  924. public static func == (lhs: StoredDosingDecision.LastReservoirValue, rhs: StoredDosingDecision.LastReservoirValue) -> Bool {
  925. return lhs.startDate == rhs.startDate && lhs.unitVolume == rhs.unitVolume
  926. }
  927. }
  928. extension ManualBolusRecommendationWithDate: Equatable {
  929. public static func == (lhs: ManualBolusRecommendationWithDate, rhs: ManualBolusRecommendationWithDate) -> Bool {
  930. return lhs.recommendation == rhs.recommendation && lhs.date == rhs.date
  931. }
  932. }
  933. extension ManualBolusRecommendation: Equatable {
  934. public static func == (lhs: ManualBolusRecommendation, rhs: ManualBolusRecommendation) -> Bool {
  935. return lhs.amount == rhs.amount && lhs.pendingInsulin == rhs.pendingInsulin && lhs.notice == rhs.notice
  936. }
  937. }
  938. fileprivate extension StoredDosingDecision {
  939. static var test: StoredDosingDecision {
  940. let controllerTimeZone = TimeZone(identifier: "America/Los_Angeles")!
  941. let scheduleTimeZone = TimeZone(secondsFromGMT: TimeZone(identifier: "America/Phoenix")!.secondsFromGMT())!
  942. let reason = "test"
  943. let settings = StoredDosingDecision.Settings(syncIdentifier: UUID(uuidString: "2B03D96C-99CD-4140-99CD-80C3E64D6011")!)
  944. let scheduleOverride = TemporaryScheduleOverride(context: .preMeal,
  945. settings: TemporaryScheduleOverrideSettings(unit: .milligramsPerDeciliter,
  946. targetRange: DoubleRange(minValue: 80.0,
  947. maxValue: 90.0),
  948. insulinNeedsScaleFactor: 1.5),
  949. startDate: dateFormatter.date(from: "2020-05-14T22:22:01Z")!,
  950. duration: .finite(.hours(1)),
  951. enactTrigger: .local,
  952. syncIdentifier: UUID(uuidString: "394818CF-99CD-4B12-99CD-0E678414986B")!)
  953. let controllerStatus = StoredDosingDecision.ControllerStatus(batteryState: .charging,
  954. batteryLevel: 0.5)
  955. let pumpManagerStatus = PumpManagerStatus(timeZone: scheduleTimeZone,
  956. device: HKDevice(name: "Pump Name",
  957. manufacturer: "Pump Manufacturer",
  958. model: "Pump Model",
  959. hardwareVersion: "Pump Hardware Version",
  960. firmwareVersion: "Pump Firmware Version",
  961. softwareVersion: "Pump Software Version",
  962. localIdentifier: "Pump Local Identifier",
  963. udiDeviceIdentifier: "Pump UDI Device Identifier"),
  964. pumpBatteryChargeRemaining: 0.75,
  965. basalDeliveryState: .initiatingTempBasal,
  966. bolusState: .noBolus,
  967. insulinType: .novolog)
  968. let cgmManagerStatus = CGMManagerStatus(hasValidSensorSession: true,
  969. lastCommunicationDate: dateFormatter.date(from: "2020-05-14T22:07:01Z")!,
  970. device: HKDevice(name: "CGM Name",
  971. manufacturer: "CGM Manufacturer",
  972. model: "CGM Model",
  973. hardwareVersion: "CGM Hardware Version",
  974. firmwareVersion: "CGM Firmware Version",
  975. softwareVersion: "CGM Software Version",
  976. localIdentifier: "CGM Local Identifier",
  977. udiDeviceIdentifier: "CGM UDI Device Identifier"))
  978. let lastReservoirValue = StoredDosingDecision.LastReservoirValue(startDate: dateFormatter.date(from: "2020-05-14T22:07:19Z")!,
  979. unitVolume: 113.3)
  980. let historicalGlucose = [HistoricalGlucoseValue(startDate: dateFormatter.date(from: "2020-05-14T22:29:15Z")!,
  981. quantity: HKQuantity(unit: .milligramsPerDeciliter, doubleValue: 117.3)),
  982. HistoricalGlucoseValue(startDate: dateFormatter.date(from: "2020-05-14T22:33:15Z")!,
  983. quantity: HKQuantity(unit: .milligramsPerDeciliter, doubleValue: 119.5)),
  984. HistoricalGlucoseValue(startDate: dateFormatter.date(from: "2020-05-14T22:38:15Z")!,
  985. quantity: HKQuantity(unit: .milligramsPerDeciliter, doubleValue: 121.8))]
  986. let originalCarbEntry = StoredCarbEntry(startDate: dateFormatter.date(from: "2020-01-02T03:00:23Z")!,
  987. quantity: HKQuantity(unit: .gram(), doubleValue: 19),
  988. uuid: UUID(uuidString: "18CF3948-0B3D-4B12-8BFE-14986B0E6784")!,
  989. provenanceIdentifier: "com.loopkit.loop",
  990. syncIdentifier: "2B03D96C-6F5D-4140-99CD-80C3E64D6010",
  991. syncVersion: 1,
  992. foodType: "Pizza",
  993. absorptionTime: .hours(5),
  994. createdByCurrentApp: true,
  995. userCreatedDate: dateFormatter.date(from: "2020-05-14T22:06:12Z")!,
  996. userUpdatedDate: nil)
  997. let carbEntry = StoredCarbEntry(startDate: dateFormatter.date(from: "2020-01-02T03:00:23Z")!,
  998. quantity: HKQuantity(unit: .gram(), doubleValue: 29),
  999. uuid: UUID(uuidString: "135CDABE-9343-7242-4233-1020384789AE")!,
  1000. provenanceIdentifier: "com.loopkit.loop",
  1001. syncIdentifier: "2B03D96C-6F5D-4140-99CD-80C3E64D6010",
  1002. syncVersion: 2,
  1003. foodType: "Pizza",
  1004. absorptionTime: .hours(5),
  1005. createdByCurrentApp: true,
  1006. userCreatedDate: dateFormatter.date(from: "2020-05-14T22:06:12Z")!,
  1007. userUpdatedDate: dateFormatter.date(from: "2020-05-14T22:07:32Z")!)
  1008. let manualGlucoseSample = StoredGlucoseSample(uuid: UUID(uuidString: "da0ced44-e4f1-49c4-baf8-6efa6d75525f")!,
  1009. provenanceIdentifier: "com.loopkit.loop",
  1010. syncIdentifier: "d3876f59-adb3-4a4f-8b29-315cda22062e",
  1011. syncVersion: 1,
  1012. startDate: dateFormatter.date(from: "2020-05-14T22:09:00Z")!,
  1013. quantity: HKQuantity(unit: .milligramsPerDeciliter, doubleValue: 400),
  1014. condition: .aboveRange,
  1015. trend: .downDownDown,
  1016. trendRate: HKQuantity(unit: .milligramsPerDeciliterPerMinute, doubleValue: -10.2),
  1017. isDisplayOnly: false,
  1018. wasUserEntered: true,
  1019. device: HKDevice(name: "Device Name",
  1020. manufacturer: "Device Manufacturer",
  1021. model: "Device Model",
  1022. hardwareVersion: "Device Hardware Version",
  1023. firmwareVersion: "Device Firmware Version",
  1024. softwareVersion: "Device Software Version",
  1025. localIdentifier: "Device Local Identifier",
  1026. udiDeviceIdentifier: "Device UDI Device Identifier"),
  1027. healthKitEligibleDate: nil)
  1028. let carbsOnBoard = CarbValue(startDate: dateFormatter.date(from: "2020-05-14T22:48:41Z")!,
  1029. endDate: dateFormatter.date(from: "2020-05-14T23:18:41Z")!,
  1030. value: 45.5)
  1031. let insulinOnBoard = InsulinValue(startDate: dateFormatter.date(from: "2020-05-14T22:38:26Z")!, value: 1.5)
  1032. let glucoseTargetRangeSchedule = GlucoseRangeSchedule(rangeSchedule: DailyQuantitySchedule(unit: .milligramsPerDeciliter,
  1033. dailyItems: [RepeatingScheduleValue(startTime: .hours(0), value: DoubleRange(minValue: 100.0, maxValue: 110.0)),
  1034. RepeatingScheduleValue(startTime: .hours(7), value: DoubleRange(minValue: 90.0, maxValue: 100.0)),
  1035. RepeatingScheduleValue(startTime: .hours(21), value: DoubleRange(minValue: 110.0, maxValue: 120.0))],
  1036. timeZone: scheduleTimeZone)!,
  1037. override: GlucoseRangeSchedule.Override(value: DoubleRange(minValue: 105.0, maxValue: 115.0),
  1038. start: dateFormatter.date(from: "2020-05-14T21:12:17Z")!,
  1039. end: dateFormatter.date(from: "2020-05-14T23:12:17Z")!))
  1040. let predictedGlucose = [PredictedGlucoseValue(startDate: dateFormatter.date(from: "2020-05-14T22:43:15Z")!,
  1041. quantity: HKQuantity(unit: .milligramsPerDeciliter, doubleValue: 123.3)),
  1042. PredictedGlucoseValue(startDate: dateFormatter.date(from: "2020-05-14T22:48:15Z")!,
  1043. quantity: HKQuantity(unit: .milligramsPerDeciliter, doubleValue: 125.5)),
  1044. PredictedGlucoseValue(startDate: dateFormatter.date(from: "2020-05-14T22:53:15Z")!,
  1045. quantity: HKQuantity(unit: .milligramsPerDeciliter, doubleValue: 127.8))]
  1046. let tempBasalRecommendation = TempBasalRecommendation(unitsPerHour: 0.75,
  1047. duration: .minutes(30))
  1048. let automaticDoseRecommendation = AutomaticDoseRecommendation(basalAdjustment: tempBasalRecommendation, bolusUnits: 1.25)
  1049. let manualBolusRecommendation = ManualBolusRecommendationWithDate(recommendation: ManualBolusRecommendation(amount: 1.2,
  1050. pendingInsulin: 0.75,
  1051. notice: .predictedGlucoseBelowTarget(minGlucose: PredictedGlucoseValue(startDate: dateFormatter.date(from: "2020-05-14T23:03:15Z")!,
  1052. quantity: HKQuantity(unit: .milligramsPerDeciliter, doubleValue: 75.5)))),
  1053. date: dateFormatter.date(from: "2020-05-14T22:38:16Z")!)
  1054. let manualBolusRequested = 0.8
  1055. let warnings: [Issue] = [Issue(id: "one"),
  1056. Issue(id: "two", details: ["size": "small"])]
  1057. let errors: [Issue] = [Issue(id: "alpha"),
  1058. Issue(id: "bravo", details: ["size": "tiny"])]
  1059. return StoredDosingDecision(date: dateFormatter.date(from: "2020-05-14T22:38:14Z")!,
  1060. controllerTimeZone: controllerTimeZone,
  1061. reason: reason,
  1062. settings: settings,
  1063. scheduleOverride: scheduleOverride,
  1064. controllerStatus: controllerStatus,
  1065. pumpManagerStatus: pumpManagerStatus,
  1066. cgmManagerStatus: cgmManagerStatus,
  1067. lastReservoirValue: lastReservoirValue,
  1068. historicalGlucose: historicalGlucose,
  1069. originalCarbEntry: originalCarbEntry,
  1070. carbEntry: carbEntry,
  1071. manualGlucoseSample: manualGlucoseSample,
  1072. carbsOnBoard: carbsOnBoard,
  1073. insulinOnBoard: insulinOnBoard,
  1074. glucoseTargetRangeSchedule: glucoseTargetRangeSchedule,
  1075. predictedGlucose: predictedGlucose,
  1076. automaticDoseRecommendation: automaticDoseRecommendation,
  1077. manualBolusRecommendation: manualBolusRecommendation,
  1078. manualBolusRequested: manualBolusRequested,
  1079. warnings: warnings,
  1080. errors: errors,
  1081. syncIdentifier: UUID(uuidString: "2A67A303-5203-4CB8-8263-79498265368E")!)
  1082. }
  1083. private static let dateFormatter = ISO8601DateFormatter()
  1084. }