DosingDecisionStoreTests.swift 49 KB

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