SettingsStoreTests.swift 42 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115
  1. //
  2. // SettingsStoreTests.swift
  3. // LoopKitTests
  4. //
  5. // Created by Darin Krauss on 1/2/20.
  6. // Copyright © 2020 LoopKit Authors. All rights reserved.
  7. //
  8. import XCTest
  9. import HealthKit
  10. @testable import LoopKit
  11. class SettingsStorePersistenceTests: PersistenceControllerTestCase, SettingsStoreDelegate {
  12. var settingsStore: SettingsStore!
  13. override func setUp() {
  14. super.setUp()
  15. settingsStoreHasUpdatedSettingsDataHandler = nil
  16. settingsStore = SettingsStore(store: cacheStore, expireAfter: .hours(1))
  17. settingsStore.delegate = self
  18. }
  19. override func tearDown() {
  20. settingsStore.delegate = nil
  21. settingsStore = nil
  22. settingsStoreHasUpdatedSettingsDataHandler = nil
  23. super.tearDown()
  24. }
  25. // MARK: - SettingsStoreDelegate
  26. var settingsStoreHasUpdatedSettingsDataHandler: ((_ : SettingsStore) -> Void)?
  27. func settingsStoreHasUpdatedSettingsData(_ settingsStore: SettingsStore) {
  28. settingsStoreHasUpdatedSettingsDataHandler?(settingsStore)
  29. }
  30. // MARK: -
  31. func testStoreSettings() {
  32. let storeSettingsHandler = expectation(description: "Store settings handler")
  33. let storeSettingsCompletion = expectation(description: "Store settings completion")
  34. var handlerInvocation = 0
  35. settingsStoreHasUpdatedSettingsDataHandler = { settingsStore in
  36. handlerInvocation += 1
  37. switch handlerInvocation {
  38. case 1:
  39. storeSettingsHandler.fulfill()
  40. default:
  41. XCTFail("Unexpected handler invocation")
  42. }
  43. }
  44. settingsStore.storeSettings(StoredSettings()) { _ in
  45. storeSettingsCompletion.fulfill()
  46. }
  47. wait(for: [storeSettingsHandler, storeSettingsCompletion], timeout: 2, enforceOrder: true)
  48. }
  49. func testStoreSettingsMultiple() {
  50. let storeSettingsHandler1 = expectation(description: "Store settings handler 1")
  51. let storeSettingsHandler2 = expectation(description: "Store settings handler 2")
  52. let storeSettingsCompletion1 = expectation(description: "Store settings completion 1")
  53. let storeSettingsCompletion2 = expectation(description: "Store settings completion 2")
  54. var handlerInvocation = 0
  55. settingsStoreHasUpdatedSettingsDataHandler = { settingsStore in
  56. handlerInvocation += 1
  57. switch handlerInvocation {
  58. case 1:
  59. storeSettingsHandler1.fulfill()
  60. case 2:
  61. storeSettingsHandler2.fulfill()
  62. default:
  63. XCTFail("Unexpected handler invocation")
  64. }
  65. }
  66. settingsStore.storeSettings(StoredSettings()) { _ in
  67. storeSettingsCompletion1.fulfill()
  68. }
  69. settingsStore.storeSettings(StoredSettings()) { _ in
  70. storeSettingsCompletion2.fulfill()
  71. }
  72. wait(for: [storeSettingsHandler1, storeSettingsCompletion1, storeSettingsHandler2, storeSettingsCompletion2], timeout: 2, enforceOrder: true)
  73. }
  74. // MARK: -
  75. func testSettingsObjectEncodable() throws {
  76. cacheStore.managedObjectContext.performAndWait {
  77. do {
  78. let object = SettingsObject(context: cacheStore.managedObjectContext)
  79. object.data = try PropertyListEncoder().encode(StoredSettings.test)
  80. object.date = dateFormatter.date(from: "2100-01-02T03:03:00Z")!
  81. object.modificationCounter = 123
  82. try assertSettingsObjectEncodable(object, encodesJSON: """
  83. {
  84. "data" : {
  85. "automaticDosingStrategy" : 1,
  86. "basalRateSchedule" : {
  87. "items" : [
  88. {
  89. "startTime" : 0,
  90. "value" : 1
  91. },
  92. {
  93. "startTime" : 21600,
  94. "value" : 1.5
  95. },
  96. {
  97. "startTime" : 64800,
  98. "value" : 1.25
  99. }
  100. ],
  101. "referenceTimeInterval" : 0,
  102. "repeatInterval" : 86400,
  103. "timeZone" : {
  104. "identifier" : "GMT-0700"
  105. }
  106. },
  107. "bloodGlucoseUnit" : "mg/dL",
  108. "carbRatioSchedule" : {
  109. "unit" : "g",
  110. "valueSchedule" : {
  111. "items" : [
  112. {
  113. "startTime" : 0,
  114. "value" : 15
  115. },
  116. {
  117. "startTime" : 32400,
  118. "value" : 14
  119. },
  120. {
  121. "startTime" : 72000,
  122. "value" : 18
  123. }
  124. ],
  125. "referenceTimeInterval" : 0,
  126. "repeatInterval" : 86400,
  127. "timeZone" : {
  128. "identifier" : "GMT-0700"
  129. }
  130. }
  131. },
  132. "cgmDevice" : {
  133. "firmwareVersion" : "CGM Firmware Version",
  134. "hardwareVersion" : "CGM Hardware Version",
  135. "localIdentifier" : "CGM Local Identifier",
  136. "manufacturer" : "CGM Manufacturer",
  137. "model" : "CGM Model",
  138. "name" : "CGM Name",
  139. "softwareVersion" : "CGM Software Version",
  140. "udiDeviceIdentifier" : "CGM UDI Device Identifier"
  141. },
  142. "controllerDevice" : {
  143. "model" : "Controller Model",
  144. "modelIdentifier" : "Controller Model Identifier",
  145. "name" : "Controller Name",
  146. "systemName" : "Controller System Name",
  147. "systemVersion" : "Controller System Version"
  148. },
  149. "controllerTimeZone" : {
  150. "identifier" : "America/Los_Angeles"
  151. },
  152. "date" : "2020-05-14T22:48:15Z",
  153. "defaultRapidActingModel" : {
  154. "actionDuration" : 21600,
  155. "delay" : 600,
  156. "modelType" : "rapidAdult",
  157. "peakActivity" : 10800
  158. },
  159. "deviceToken" : "Device Token String",
  160. "dosingEnabled" : true,
  161. "glucoseTargetRangeSchedule" : {
  162. "override" : {
  163. "end" : "2020-05-14T14:48:15Z",
  164. "start" : "2020-05-14T12:48:15Z",
  165. "value" : {
  166. "maxValue" : 115,
  167. "minValue" : 105
  168. }
  169. },
  170. "rangeSchedule" : {
  171. "unit" : "mg/dL",
  172. "valueSchedule" : {
  173. "items" : [
  174. {
  175. "startTime" : 0,
  176. "value" : {
  177. "maxValue" : 110,
  178. "minValue" : 100
  179. }
  180. },
  181. {
  182. "startTime" : 25200,
  183. "value" : {
  184. "maxValue" : 100,
  185. "minValue" : 90
  186. }
  187. },
  188. {
  189. "startTime" : 75600,
  190. "value" : {
  191. "maxValue" : 120,
  192. "minValue" : 110
  193. }
  194. }
  195. ],
  196. "referenceTimeInterval" : 0,
  197. "repeatInterval" : 86400,
  198. "timeZone" : {
  199. "identifier" : "GMT-0700"
  200. }
  201. }
  202. }
  203. },
  204. "insulinSensitivitySchedule" : {
  205. "unit" : "mg/dL",
  206. "valueSchedule" : {
  207. "items" : [
  208. {
  209. "startTime" : 0,
  210. "value" : 45
  211. },
  212. {
  213. "startTime" : 10800,
  214. "value" : 40
  215. },
  216. {
  217. "startTime" : 54000,
  218. "value" : 50
  219. }
  220. ],
  221. "referenceTimeInterval" : 0,
  222. "repeatInterval" : 86400,
  223. "timeZone" : {
  224. "identifier" : "GMT-0700"
  225. }
  226. }
  227. },
  228. "insulinType" : 1,
  229. "maximumBasalRatePerHour" : 3.5,
  230. "maximumBolus" : 10,
  231. "notificationSettings" : {
  232. "alertSetting" : "disabled",
  233. "alertStyle" : "banner",
  234. "announcementSetting" : "enabled",
  235. "authorizationStatus" : "authorized",
  236. "badgeSetting" : "enabled",
  237. "carPlaySetting" : "notSupported",
  238. "criticalAlertSetting" : "enabled",
  239. "lockScreenSetting" : "disabled",
  240. "notificationCenterSetting" : "notSupported",
  241. "providesAppNotificationSettings" : true,
  242. "scheduledDeliverySetting" : "disabled",
  243. "showPreviewsSetting" : "whenAuthenticated",
  244. "soundSetting" : "enabled",
  245. "timeSensitiveSetting" : "enabled"
  246. },
  247. "overridePresets" : [
  248. {
  249. "duration" : {
  250. "finite" : {
  251. "duration" : 3600
  252. }
  253. },
  254. "id" : "2A67A303-5203-4CB8-8263-79498265368E",
  255. "name" : "Apple",
  256. "settings" : {
  257. "insulinNeedsScaleFactor" : 2,
  258. "targetRangeInMgdl" : {
  259. "maxValue" : 140,
  260. "minValue" : 130
  261. }
  262. },
  263. "symbol" : "🍎"
  264. }
  265. ],
  266. "preMealOverride" : {
  267. "actualEnd" : {
  268. "type" : "natural"
  269. },
  270. "context" : "preMeal",
  271. "duration" : "indefinite",
  272. "enactTrigger" : "local",
  273. "settings" : {
  274. "insulinNeedsScaleFactor" : 0.5,
  275. "targetRangeInMgdl" : {
  276. "maxValue" : 90,
  277. "minValue" : 80
  278. }
  279. },
  280. "startDate" : "2020-05-14T14:38:39Z",
  281. "syncIdentifier" : "2A67A303-5203-1234-8263-79498265368E"
  282. },
  283. "preMealTargetRange" : {
  284. "maxValue" : 90,
  285. "minValue" : 80
  286. },
  287. "pumpDevice" : {
  288. "firmwareVersion" : "Pump Firmware Version",
  289. "hardwareVersion" : "Pump Hardware Version",
  290. "localIdentifier" : "Pump Local Identifier",
  291. "manufacturer" : "Pump Manufacturer",
  292. "model" : "Pump Model",
  293. "name" : "Pump Name",
  294. "softwareVersion" : "Pump Software Version",
  295. "udiDeviceIdentifier" : "Pump UDI Device Identifier"
  296. },
  297. "scheduleOverride" : {
  298. "actualEnd" : {
  299. "type" : "natural"
  300. },
  301. "context" : "preMeal",
  302. "duration" : {
  303. "finite" : {
  304. "duration" : 3600
  305. }
  306. },
  307. "enactTrigger" : {
  308. "remote" : {
  309. "address" : "127.0.0.1"
  310. }
  311. },
  312. "settings" : {
  313. "insulinNeedsScaleFactor" : 1.5,
  314. "targetRangeInMgdl" : {
  315. "maxValue" : 120,
  316. "minValue" : 110
  317. }
  318. },
  319. "startDate" : "2020-05-14T14:48:19Z",
  320. "syncIdentifier" : "2A67A303-1234-4CB8-8263-79498265368E"
  321. },
  322. "suspendThreshold" : {
  323. "unit" : "mg/dL",
  324. "value" : 75
  325. },
  326. "syncIdentifier" : "2A67A303-1234-4CB8-1234-79498265368E",
  327. "workoutTargetRange" : {
  328. "maxValue" : 160,
  329. "minValue" : 150
  330. }
  331. },
  332. "date" : "2100-01-02T03:03:00Z",
  333. "modificationCounter" : 123
  334. }
  335. """
  336. )
  337. } catch let error {
  338. XCTFail("Unexpected failure: \(error)")
  339. }
  340. }
  341. }
  342. private func assertSettingsObjectEncodable(_ original: SettingsObject, encodesJSON string: String) throws {
  343. let data = try encoder.encode(original)
  344. XCTAssertEqual(String(data: data, encoding: .utf8), string)
  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 SettingsStoreQueryAnchorTests: XCTestCase {
  355. var rawValue: SettingsStore.QueryAnchor.RawValue = [
  356. "modificationCounter": Int64(123)
  357. ]
  358. func testInitializerDefault() {
  359. let queryAnchor = SettingsStore.QueryAnchor()
  360. XCTAssertEqual(queryAnchor.modificationCounter, 0)
  361. }
  362. func testInitializerRawValue() {
  363. let queryAnchor = SettingsStore.QueryAnchor(rawValue: rawValue)
  364. XCTAssertNotNil(queryAnchor)
  365. XCTAssertEqual(queryAnchor?.modificationCounter, 123)
  366. }
  367. func testInitializerRawValueMissingModificationCounter() {
  368. rawValue["modificationCounter"] = nil
  369. XCTAssertNil(SettingsStore.QueryAnchor(rawValue: rawValue))
  370. }
  371. func testInitializerRawValueInvalidModificationCounter() {
  372. rawValue["modificationCounter"] = "123"
  373. XCTAssertNil(SettingsStore.QueryAnchor(rawValue: rawValue))
  374. }
  375. func testRawValueWithDefault() {
  376. let rawValue = SettingsStore.QueryAnchor().rawValue
  377. XCTAssertEqual(rawValue.count, 1)
  378. XCTAssertEqual(rawValue["modificationCounter"] as? Int64, Int64(0))
  379. }
  380. func testRawValueWithNonDefault() {
  381. var queryAnchor = SettingsStore.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 SettingsStoreQueryTests: PersistenceControllerTestCase {
  389. var settingsStore: SettingsStore!
  390. var completion: XCTestExpectation!
  391. var queryAnchor: SettingsStore.QueryAnchor!
  392. var limit: Int!
  393. override func setUp() {
  394. super.setUp()
  395. settingsStore = SettingsStore(store: cacheStore, expireAfter: .hours(1))
  396. completion = expectation(description: "Completion")
  397. queryAnchor = SettingsStore.QueryAnchor()
  398. limit = Int.max
  399. }
  400. override func tearDown() {
  401. limit = nil
  402. queryAnchor = nil
  403. completion = nil
  404. settingsStore = nil
  405. super.tearDown()
  406. }
  407. // MARK: -
  408. func testEmptyWithDefaultQueryAnchor() {
  409. settingsStore.executeSettingsQuery(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. settingsStore.executeSettingsQuery(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. settingsStore.executeSettingsQuery(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. settingsStore.executeSettingsQuery(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. settingsStore.executeSettingsQuery(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. settingsStore.executeSettingsQuery(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. settingsStore.executeSettingsQuery(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. settingsStore.executeSettingsQuery(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.settingsStore.storeSettings(StoredSettings(syncIdentifier: syncIdentifier)) { _ in semaphore.signal() }
  538. }
  539. for _ in syncIdentifiers { semaphore.wait() }
  540. }
  541. private func generateSyncIdentifier() -> UUID { UUID() }
  542. }
  543. class SettingsStoreCriticalEventLogTests: PersistenceControllerTestCase {
  544. var settingsStore: SettingsStore!
  545. var outputStream: MockOutputStream!
  546. var progress: Progress!
  547. override func setUp() {
  548. super.setUp()
  549. let settings = [StoredSettings(date: dateFormatter.date(from: "2100-01-02T03:08:00Z")!, controllerTimeZone: TimeZone(identifier: "America/Los_Angeles")!, syncIdentifier: UUID(uuidString: "18CF3948-0B3D-4B12-8BFE-14986B0E6784")!),
  550. StoredSettings(date: dateFormatter.date(from: "2100-01-02T03:10:00Z")!, controllerTimeZone: TimeZone(identifier: "America/Los_Angeles")!, syncIdentifier: UUID(uuidString: "C86DEB61-68E9-464E-9DD5-96A9CB445FD3")!),
  551. StoredSettings(date: dateFormatter.date(from: "2100-01-02T03:04:00Z")!, controllerTimeZone: TimeZone(identifier: "America/Los_Angeles")!, syncIdentifier: UUID(uuidString: "2B03D96C-6F5D-4140-99CD-80C3E64D6010")!),
  552. StoredSettings(date: dateFormatter.date(from: "2100-01-02T03:06:00Z")!, controllerTimeZone: TimeZone(identifier: "America/Los_Angeles")!, syncIdentifier: UUID(uuidString: "FF1C4F01-3558-4FB2-957E-FA1522C4735E")!),
  553. StoredSettings(date: dateFormatter.date(from: "2100-01-02T03:02:00Z")!, controllerTimeZone: TimeZone(identifier: "America/Los_Angeles")!, syncIdentifier: UUID(uuidString: "71B699D7-0E8F-4B13-B7A1-E7751EB78E74")!)]
  554. settingsStore = SettingsStore(store: cacheStore, expireAfter: .hours(1))
  555. let dispatchGroup = DispatchGroup()
  556. dispatchGroup.enter()
  557. settingsStore.addStoredSettings(settings: settings) { error in
  558. XCTAssertNil(error)
  559. dispatchGroup.leave()
  560. }
  561. dispatchGroup.wait()
  562. outputStream = MockOutputStream()
  563. progress = Progress()
  564. }
  565. override func tearDown() {
  566. settingsStore = nil
  567. super.tearDown()
  568. }
  569. func testExportProgressTotalUnitCount() {
  570. switch settingsStore.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 * 11)
  576. }
  577. }
  578. func testExportProgressTotalUnitCountEmpty() {
  579. switch settingsStore.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(settingsStore.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":{"automaticDosingStrategy":0,"bloodGlucoseUnit":"mg/dL","controllerTimeZone":{"identifier":"America/Los_Angeles"},"date":"2100-01-02T03:08:00.000Z","dosingEnabled":false,"syncIdentifier":"18CF3948-0B3D-4B12-8BFE-14986B0E6784"},"date":"2100-01-02T03:08:00.000Z","modificationCounter":1},
  595. {"data":{"automaticDosingStrategy":0,"bloodGlucoseUnit":"mg/dL","controllerTimeZone":{"identifier":"America/Los_Angeles"},"date":"2100-01-02T03:04:00.000Z","dosingEnabled":false,"syncIdentifier":"2B03D96C-6F5D-4140-99CD-80C3E64D6010"},"date":"2100-01-02T03:04:00.000Z","modificationCounter":3},
  596. {"data":{"automaticDosingStrategy":0,"bloodGlucoseUnit":"mg/dL","controllerTimeZone":{"identifier":"America/Los_Angeles"},"date":"2100-01-02T03:06:00.000Z","dosingEnabled":false,"syncIdentifier":"FF1C4F01-3558-4FB2-957E-FA1522C4735E"},"date":"2100-01-02T03:06:00.000Z","modificationCounter":4}
  597. ]
  598. """
  599. )
  600. XCTAssertEqual(progress.completedUnitCount, 3 * 11)
  601. }
  602. func testExportEmpty() {
  603. XCTAssertNil(settingsStore.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(settingsStore.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 StoredSettingsCodableTests: XCTestCase {
  620. func testStoredSettingsCodable() throws {
  621. try assertStoredSettingsCodable(StoredSettings.test, encodesJSON: """
  622. {
  623. "automaticDosingStrategy" : 1,
  624. "basalRateSchedule" : {
  625. "items" : [
  626. {
  627. "startTime" : 0,
  628. "value" : 1
  629. },
  630. {
  631. "startTime" : 21600,
  632. "value" : 1.5
  633. },
  634. {
  635. "startTime" : 64800,
  636. "value" : 1.25
  637. }
  638. ],
  639. "referenceTimeInterval" : 0,
  640. "repeatInterval" : 86400,
  641. "timeZone" : {
  642. "identifier" : "GMT-0700"
  643. }
  644. },
  645. "bloodGlucoseUnit" : "mg/dL",
  646. "carbRatioSchedule" : {
  647. "unit" : "g",
  648. "valueSchedule" : {
  649. "items" : [
  650. {
  651. "startTime" : 0,
  652. "value" : 15
  653. },
  654. {
  655. "startTime" : 32400,
  656. "value" : 14
  657. },
  658. {
  659. "startTime" : 72000,
  660. "value" : 18
  661. }
  662. ],
  663. "referenceTimeInterval" : 0,
  664. "repeatInterval" : 86400,
  665. "timeZone" : {
  666. "identifier" : "GMT-0700"
  667. }
  668. }
  669. },
  670. "cgmDevice" : {
  671. "firmwareVersion" : "CGM Firmware Version",
  672. "hardwareVersion" : "CGM Hardware Version",
  673. "localIdentifier" : "CGM Local Identifier",
  674. "manufacturer" : "CGM Manufacturer",
  675. "model" : "CGM Model",
  676. "name" : "CGM Name",
  677. "softwareVersion" : "CGM Software Version",
  678. "udiDeviceIdentifier" : "CGM UDI Device Identifier"
  679. },
  680. "controllerDevice" : {
  681. "model" : "Controller Model",
  682. "modelIdentifier" : "Controller Model Identifier",
  683. "name" : "Controller Name",
  684. "systemName" : "Controller System Name",
  685. "systemVersion" : "Controller System Version"
  686. },
  687. "controllerTimeZone" : {
  688. "identifier" : "America/Los_Angeles"
  689. },
  690. "date" : "2020-05-14T22:48:15Z",
  691. "defaultRapidActingModel" : {
  692. "actionDuration" : 21600,
  693. "delay" : 600,
  694. "modelType" : "rapidAdult",
  695. "peakActivity" : 10800
  696. },
  697. "deviceToken" : "Device Token String",
  698. "dosingEnabled" : true,
  699. "glucoseTargetRangeSchedule" : {
  700. "override" : {
  701. "end" : "2020-05-14T14:48:15Z",
  702. "start" : "2020-05-14T12:48:15Z",
  703. "value" : {
  704. "maxValue" : 115,
  705. "minValue" : 105
  706. }
  707. },
  708. "rangeSchedule" : {
  709. "unit" : "mg/dL",
  710. "valueSchedule" : {
  711. "items" : [
  712. {
  713. "startTime" : 0,
  714. "value" : {
  715. "maxValue" : 110,
  716. "minValue" : 100
  717. }
  718. },
  719. {
  720. "startTime" : 25200,
  721. "value" : {
  722. "maxValue" : 100,
  723. "minValue" : 90
  724. }
  725. },
  726. {
  727. "startTime" : 75600,
  728. "value" : {
  729. "maxValue" : 120,
  730. "minValue" : 110
  731. }
  732. }
  733. ],
  734. "referenceTimeInterval" : 0,
  735. "repeatInterval" : 86400,
  736. "timeZone" : {
  737. "identifier" : "GMT-0700"
  738. }
  739. }
  740. }
  741. },
  742. "insulinSensitivitySchedule" : {
  743. "unit" : "mg/dL",
  744. "valueSchedule" : {
  745. "items" : [
  746. {
  747. "startTime" : 0,
  748. "value" : 45
  749. },
  750. {
  751. "startTime" : 10800,
  752. "value" : 40
  753. },
  754. {
  755. "startTime" : 54000,
  756. "value" : 50
  757. }
  758. ],
  759. "referenceTimeInterval" : 0,
  760. "repeatInterval" : 86400,
  761. "timeZone" : {
  762. "identifier" : "GMT-0700"
  763. }
  764. }
  765. },
  766. "insulinType" : 1,
  767. "maximumBasalRatePerHour" : 3.5,
  768. "maximumBolus" : 10,
  769. "notificationSettings" : {
  770. "alertSetting" : "disabled",
  771. "alertStyle" : "banner",
  772. "announcementSetting" : "enabled",
  773. "authorizationStatus" : "authorized",
  774. "badgeSetting" : "enabled",
  775. "carPlaySetting" : "notSupported",
  776. "criticalAlertSetting" : "enabled",
  777. "lockScreenSetting" : "disabled",
  778. "notificationCenterSetting" : "notSupported",
  779. "providesAppNotificationSettings" : true,
  780. "scheduledDeliverySetting" : "disabled",
  781. "showPreviewsSetting" : "whenAuthenticated",
  782. "soundSetting" : "enabled",
  783. "timeSensitiveSetting" : "enabled"
  784. },
  785. "overridePresets" : [
  786. {
  787. "duration" : {
  788. "finite" : {
  789. "duration" : 3600
  790. }
  791. },
  792. "id" : "2A67A303-5203-4CB8-8263-79498265368E",
  793. "name" : "Apple",
  794. "settings" : {
  795. "insulinNeedsScaleFactor" : 2,
  796. "targetRangeInMgdl" : {
  797. "maxValue" : 140,
  798. "minValue" : 130
  799. }
  800. },
  801. "symbol" : "🍎"
  802. }
  803. ],
  804. "preMealOverride" : {
  805. "actualEnd" : {
  806. "type" : "natural"
  807. },
  808. "context" : "preMeal",
  809. "duration" : "indefinite",
  810. "enactTrigger" : "local",
  811. "settings" : {
  812. "insulinNeedsScaleFactor" : 0.5,
  813. "targetRangeInMgdl" : {
  814. "maxValue" : 90,
  815. "minValue" : 80
  816. }
  817. },
  818. "startDate" : "2020-05-14T14:38:39Z",
  819. "syncIdentifier" : "2A67A303-5203-1234-8263-79498265368E"
  820. },
  821. "preMealTargetRange" : {
  822. "maxValue" : 90,
  823. "minValue" : 80
  824. },
  825. "pumpDevice" : {
  826. "firmwareVersion" : "Pump Firmware Version",
  827. "hardwareVersion" : "Pump Hardware Version",
  828. "localIdentifier" : "Pump Local Identifier",
  829. "manufacturer" : "Pump Manufacturer",
  830. "model" : "Pump Model",
  831. "name" : "Pump Name",
  832. "softwareVersion" : "Pump Software Version",
  833. "udiDeviceIdentifier" : "Pump UDI Device Identifier"
  834. },
  835. "scheduleOverride" : {
  836. "actualEnd" : {
  837. "type" : "natural"
  838. },
  839. "context" : "preMeal",
  840. "duration" : {
  841. "finite" : {
  842. "duration" : 3600
  843. }
  844. },
  845. "enactTrigger" : {
  846. "remote" : {
  847. "address" : "127.0.0.1"
  848. }
  849. },
  850. "settings" : {
  851. "insulinNeedsScaleFactor" : 1.5,
  852. "targetRangeInMgdl" : {
  853. "maxValue" : 120,
  854. "minValue" : 110
  855. }
  856. },
  857. "startDate" : "2020-05-14T14:48:19Z",
  858. "syncIdentifier" : "2A67A303-1234-4CB8-8263-79498265368E"
  859. },
  860. "suspendThreshold" : {
  861. "unit" : "mg/dL",
  862. "value" : 75
  863. },
  864. "syncIdentifier" : "2A67A303-1234-4CB8-1234-79498265368E",
  865. "workoutTargetRange" : {
  866. "maxValue" : 160,
  867. "minValue" : 150
  868. }
  869. }
  870. """
  871. )
  872. }
  873. private func assertStoredSettingsCodable(_ original: StoredSettings, encodesJSON string: String) throws {
  874. let data = try encoder.encode(original)
  875. XCTAssertEqual(String(data: data, encoding: .utf8), string)
  876. let decoded = try decoder.decode(StoredSettings.self, from: data)
  877. XCTAssertEqual(decoded, original)
  878. }
  879. private let dateFormatter = ISO8601DateFormatter()
  880. private let encoder: JSONEncoder = {
  881. let encoder = JSONEncoder()
  882. encoder.outputFormatting = [.prettyPrinted, .sortedKeys, .withoutEscapingSlashes]
  883. encoder.dateEncodingStrategy = .iso8601
  884. return encoder
  885. }()
  886. private let decoder: JSONDecoder = {
  887. let decoder = JSONDecoder()
  888. decoder.dateDecodingStrategy = .iso8601
  889. return decoder
  890. }()
  891. }
  892. fileprivate extension StoredSettings {
  893. static var test: StoredSettings {
  894. let controllerTimeZone = TimeZone(identifier: "America/Los_Angeles")!
  895. let scheduleTimeZone = TimeZone(secondsFromGMT: TimeZone(identifier: "America/Phoenix")!.secondsFromGMT())!
  896. let dosingEnabled = true
  897. let glucoseTargetRangeSchedule = GlucoseRangeSchedule(rangeSchedule: DailyQuantitySchedule(unit: .milligramsPerDeciliter,
  898. dailyItems: [RepeatingScheduleValue(startTime: .hours(0), value: DoubleRange(minValue: 100.0, maxValue: 110.0)),
  899. RepeatingScheduleValue(startTime: .hours(7), value: DoubleRange(minValue: 90.0, maxValue: 100.0)),
  900. RepeatingScheduleValue(startTime: .hours(21), value: DoubleRange(minValue: 110.0, maxValue: 120.0))],
  901. timeZone: scheduleTimeZone)!,
  902. override: GlucoseRangeSchedule.Override(value: DoubleRange(minValue: 105.0, maxValue: 115.0),
  903. start: dateFormatter.date(from: "2020-05-14T12:48:15Z")!,
  904. end: dateFormatter.date(from: "2020-05-14T14:48:15Z")!))
  905. let preMealTargetRange = DoubleRange(minValue: 80.0, maxValue: 90.0).quantityRange(for: .milligramsPerDeciliter)
  906. let workoutTargetRange = DoubleRange(minValue: 150.0, maxValue: 160.0).quantityRange(for: .milligramsPerDeciliter)
  907. let overridePresets = [TemporaryScheduleOverridePreset(id: UUID(uuidString: "2A67A303-5203-4CB8-8263-79498265368E")!,
  908. symbol: "🍎",
  909. name: "Apple",
  910. settings: TemporaryScheduleOverrideSettings(unit: .milligramsPerDeciliter,
  911. targetRange: DoubleRange(minValue: 130.0, maxValue: 140.0),
  912. insulinNeedsScaleFactor: 2.0),
  913. duration: .finite(.minutes(60)))]
  914. let scheduleOverride = TemporaryScheduleOverride(context: .preMeal,
  915. settings: TemporaryScheduleOverrideSettings(unit: .milligramsPerDeciliter,
  916. targetRange: DoubleRange(minValue: 110.0, maxValue: 120.0),
  917. insulinNeedsScaleFactor: 1.5),
  918. startDate: dateFormatter.date(from: "2020-05-14T14:48:19Z")!,
  919. duration: .finite(.minutes(60)),
  920. enactTrigger: .remote("127.0.0.1"),
  921. syncIdentifier: UUID(uuidString: "2A67A303-1234-4CB8-8263-79498265368E")!)
  922. let preMealOverride = TemporaryScheduleOverride(context: .preMeal,
  923. settings: TemporaryScheduleOverrideSettings(unit: .milligramsPerDeciliter,
  924. targetRange: DoubleRange(minValue: 80.0, maxValue: 90.0),
  925. insulinNeedsScaleFactor: 0.5),
  926. startDate: dateFormatter.date(from: "2020-05-14T14:38:39Z")!,
  927. duration: .indefinite,
  928. enactTrigger: .local,
  929. syncIdentifier: UUID(uuidString: "2A67A303-5203-1234-8263-79498265368E")!)
  930. let maximumBasalRatePerHour = 3.5
  931. let maximumBolus = 10.0
  932. let suspendThreshold = GlucoseThreshold(unit: .milligramsPerDeciliter, value: 75.0)
  933. let deviceToken = "Device Token String"
  934. let insulinType = InsulinType.humalog
  935. let defaultRapidActingModel = StoredInsulinModel(modelType: .rapidAdult, delay: .minutes(10), actionDuration: .hours(6), peakActivity: .hours(3))
  936. let basalRateSchedule = BasalRateSchedule(dailyItems: [RepeatingScheduleValue(startTime: .hours(0), value: 1.0),
  937. RepeatingScheduleValue(startTime: .hours(6), value: 1.5),
  938. RepeatingScheduleValue(startTime: .hours(18), value: 1.25)],
  939. timeZone: scheduleTimeZone)
  940. let insulinSensitivitySchedule = InsulinSensitivitySchedule(unit: .milligramsPerDeciliter,
  941. dailyItems: [RepeatingScheduleValue(startTime: .hours(0), value: 45.0),
  942. RepeatingScheduleValue(startTime: .hours(3), value: 40.0),
  943. RepeatingScheduleValue(startTime: .hours(15), value: 50.0)],
  944. timeZone: scheduleTimeZone)
  945. let carbRatioSchedule = CarbRatioSchedule(unit: .gram(),
  946. dailyItems: [RepeatingScheduleValue(startTime: .hours(0), value: 15.0),
  947. RepeatingScheduleValue(startTime: .hours(9), value: 14.0),
  948. RepeatingScheduleValue(startTime: .hours(20), value: 18.0)],
  949. timeZone: scheduleTimeZone)
  950. let notificationSettings = NotificationSettings(authorizationStatus: .authorized,
  951. soundSetting: .enabled,
  952. badgeSetting: .enabled,
  953. alertSetting: .disabled,
  954. notificationCenterSetting: .notSupported,
  955. lockScreenSetting: .disabled,
  956. carPlaySetting: .notSupported,
  957. alertStyle: .banner,
  958. showPreviewsSetting: .whenAuthenticated,
  959. criticalAlertSetting: .enabled,
  960. providesAppNotificationSettings: true,
  961. announcementSetting: .enabled,
  962. timeSensitiveSetting: .enabled,
  963. scheduledDeliverySetting: .disabled)
  964. let controllerDevice = StoredSettings.ControllerDevice(name: "Controller Name",
  965. systemName: "Controller System Name",
  966. systemVersion: "Controller System Version",
  967. model: "Controller Model",
  968. modelIdentifier: "Controller Model Identifier")
  969. let cgmDevice = HKDevice(name: "CGM Name",
  970. manufacturer: "CGM Manufacturer",
  971. model: "CGM Model",
  972. hardwareVersion: "CGM Hardware Version",
  973. firmwareVersion: "CGM Firmware Version",
  974. softwareVersion: "CGM Software Version",
  975. localIdentifier: "CGM Local Identifier",
  976. udiDeviceIdentifier: "CGM UDI Device Identifier")
  977. let pumpDevice = HKDevice(name: "Pump Name",
  978. manufacturer: "Pump Manufacturer",
  979. model: "Pump Model",
  980. hardwareVersion: "Pump Hardware Version",
  981. firmwareVersion: "Pump Firmware Version",
  982. softwareVersion: "Pump Software Version",
  983. localIdentifier: "Pump Local Identifier",
  984. udiDeviceIdentifier: "Pump UDI Device Identifier")
  985. let bloodGlucoseUnit = HKUnit.milligramsPerDeciliter
  986. return StoredSettings(date: dateFormatter.date(from: "2020-05-14T22:48:15Z")!,
  987. controllerTimeZone: controllerTimeZone,
  988. dosingEnabled: dosingEnabled,
  989. glucoseTargetRangeSchedule: glucoseTargetRangeSchedule,
  990. preMealTargetRange: preMealTargetRange,
  991. workoutTargetRange: workoutTargetRange,
  992. overridePresets: overridePresets,
  993. scheduleOverride: scheduleOverride,
  994. preMealOverride: preMealOverride,
  995. maximumBasalRatePerHour: maximumBasalRatePerHour,
  996. maximumBolus: maximumBolus,
  997. suspendThreshold: suspendThreshold,
  998. deviceToken: deviceToken,
  999. insulinType: insulinType,
  1000. defaultRapidActingModel: defaultRapidActingModel,
  1001. basalRateSchedule: basalRateSchedule,
  1002. insulinSensitivitySchedule: insulinSensitivitySchedule,
  1003. carbRatioSchedule: carbRatioSchedule,
  1004. notificationSettings: notificationSettings,
  1005. controllerDevice: controllerDevice,
  1006. cgmDevice: cgmDevice,
  1007. pumpDevice: pumpDevice,
  1008. bloodGlucoseUnit: bloodGlucoseUnit,
  1009. automaticDosingStrategy: .automaticBolus,
  1010. syncIdentifier: UUID(uuidString: "2A67A303-1234-4CB8-1234-79498265368E")!)
  1011. }
  1012. private static let dateFormatter = ISO8601DateFormatter()
  1013. }