SettingsStoreTests.swift 42 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126
  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. "temporaryMuteAlertsSetting" : {
  246. "disabled" : {
  247. }
  248. },
  249. "timeSensitiveSetting" : "enabled"
  250. },
  251. "overridePresets" : [
  252. {
  253. "duration" : {
  254. "finite" : {
  255. "duration" : 3600
  256. }
  257. },
  258. "id" : "2A67A303-5203-4CB8-8263-79498265368E",
  259. "name" : "Apple",
  260. "settings" : {
  261. "insulinNeedsScaleFactor" : 2,
  262. "targetRangeInMgdl" : {
  263. "maxValue" : 140,
  264. "minValue" : 130
  265. }
  266. },
  267. "symbol" : "🍎"
  268. }
  269. ],
  270. "preMealOverride" : {
  271. "actualEnd" : {
  272. "type" : "natural"
  273. },
  274. "context" : "preMeal",
  275. "duration" : "indefinite",
  276. "enactTrigger" : "local",
  277. "settings" : {
  278. "insulinNeedsScaleFactor" : 0.5,
  279. "targetRangeInMgdl" : {
  280. "maxValue" : 90,
  281. "minValue" : 80
  282. }
  283. },
  284. "startDate" : "2020-05-14T14:38:39Z",
  285. "syncIdentifier" : "2A67A303-5203-1234-8263-79498265368E"
  286. },
  287. "preMealTargetRange" : {
  288. "maxValue" : 90,
  289. "minValue" : 80
  290. },
  291. "pumpDevice" : {
  292. "firmwareVersion" : "Pump Firmware Version",
  293. "hardwareVersion" : "Pump Hardware Version",
  294. "localIdentifier" : "Pump Local Identifier",
  295. "manufacturer" : "Pump Manufacturer",
  296. "model" : "Pump Model",
  297. "name" : "Pump Name",
  298. "softwareVersion" : "Pump Software Version",
  299. "udiDeviceIdentifier" : "Pump UDI Device Identifier"
  300. },
  301. "scheduleOverride" : {
  302. "actualEnd" : {
  303. "type" : "natural"
  304. },
  305. "context" : "preMeal",
  306. "duration" : {
  307. "finite" : {
  308. "duration" : 3600
  309. }
  310. },
  311. "enactTrigger" : {
  312. "remote" : {
  313. "address" : "127.0.0.1"
  314. }
  315. },
  316. "settings" : {
  317. "insulinNeedsScaleFactor" : 1.5,
  318. "targetRangeInMgdl" : {
  319. "maxValue" : 120,
  320. "minValue" : 110
  321. }
  322. },
  323. "startDate" : "2020-05-14T14:48:19Z",
  324. "syncIdentifier" : "2A67A303-1234-4CB8-8263-79498265368E"
  325. },
  326. "suspendThreshold" : {
  327. "unit" : "mg/dL",
  328. "value" : 75
  329. },
  330. "syncIdentifier" : "2A67A303-1234-4CB8-1234-79498265368E",
  331. "workoutTargetRange" : {
  332. "maxValue" : 160,
  333. "minValue" : 150
  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 func assertSettingsObjectEncodable(_ original: SettingsObject, encodesJSON string: String) throws {
  347. let data = try encoder.encode(original)
  348. XCTAssertEqual(String(data: data, encoding: .utf8), string)
  349. }
  350. private let dateFormatter = ISO8601DateFormatter()
  351. private let encoder: JSONEncoder = {
  352. let encoder = JSONEncoder()
  353. encoder.outputFormatting = [.prettyPrinted, .sortedKeys, .withoutEscapingSlashes]
  354. encoder.dateEncodingStrategy = .iso8601
  355. return encoder
  356. }()
  357. }
  358. class SettingsStoreQueryAnchorTests: XCTestCase {
  359. var rawValue: SettingsStore.QueryAnchor.RawValue = [
  360. "modificationCounter": Int64(123)
  361. ]
  362. func testInitializerDefault() {
  363. let queryAnchor = SettingsStore.QueryAnchor()
  364. XCTAssertEqual(queryAnchor.modificationCounter, 0)
  365. }
  366. func testInitializerRawValue() {
  367. let queryAnchor = SettingsStore.QueryAnchor(rawValue: rawValue)
  368. XCTAssertNotNil(queryAnchor)
  369. XCTAssertEqual(queryAnchor?.modificationCounter, 123)
  370. }
  371. func testInitializerRawValueMissingModificationCounter() {
  372. rawValue["modificationCounter"] = nil
  373. XCTAssertNil(SettingsStore.QueryAnchor(rawValue: rawValue))
  374. }
  375. func testInitializerRawValueInvalidModificationCounter() {
  376. rawValue["modificationCounter"] = "123"
  377. XCTAssertNil(SettingsStore.QueryAnchor(rawValue: rawValue))
  378. }
  379. func testRawValueWithDefault() {
  380. let rawValue = SettingsStore.QueryAnchor().rawValue
  381. XCTAssertEqual(rawValue.count, 1)
  382. XCTAssertEqual(rawValue["modificationCounter"] as? Int64, Int64(0))
  383. }
  384. func testRawValueWithNonDefault() {
  385. var queryAnchor = SettingsStore.QueryAnchor()
  386. queryAnchor.modificationCounter = 123
  387. let rawValue = queryAnchor.rawValue
  388. XCTAssertEqual(rawValue.count, 1)
  389. XCTAssertEqual(rawValue["modificationCounter"] as? Int64, Int64(123))
  390. }
  391. }
  392. class SettingsStoreQueryTests: PersistenceControllerTestCase {
  393. var settingsStore: SettingsStore!
  394. var completion: XCTestExpectation!
  395. var queryAnchor: SettingsStore.QueryAnchor!
  396. var limit: Int!
  397. override func setUp() {
  398. super.setUp()
  399. settingsStore = SettingsStore(store: cacheStore, expireAfter: .hours(1))
  400. completion = expectation(description: "Completion")
  401. queryAnchor = SettingsStore.QueryAnchor()
  402. limit = Int.max
  403. }
  404. override func tearDown() {
  405. limit = nil
  406. queryAnchor = nil
  407. completion = nil
  408. settingsStore = nil
  409. super.tearDown()
  410. }
  411. // MARK: -
  412. func testEmptyWithDefaultQueryAnchor() {
  413. settingsStore.executeSettingsQuery(fromQueryAnchor: queryAnchor, limit: limit) { result in
  414. switch result {
  415. case .failure(let error):
  416. XCTFail("Unexpected failure: \(error)")
  417. case .success(let anchor, let data):
  418. XCTAssertEqual(anchor.modificationCounter, 0)
  419. XCTAssertEqual(data.count, 0)
  420. }
  421. self.completion.fulfill()
  422. }
  423. wait(for: [completion], timeout: 2, enforceOrder: true)
  424. }
  425. func testEmptyWithMissingQueryAnchor() {
  426. queryAnchor = nil
  427. settingsStore.executeSettingsQuery(fromQueryAnchor: queryAnchor, limit: limit) { result in
  428. switch result {
  429. case .failure(let error):
  430. XCTFail("Unexpected failure: \(error)")
  431. case .success(let anchor, let data):
  432. XCTAssertEqual(anchor.modificationCounter, 0)
  433. XCTAssertEqual(data.count, 0)
  434. }
  435. self.completion.fulfill()
  436. }
  437. wait(for: [completion], timeout: 2, enforceOrder: true)
  438. }
  439. func testEmptyWithNonDefaultQueryAnchor() {
  440. queryAnchor.modificationCounter = 1
  441. settingsStore.executeSettingsQuery(fromQueryAnchor: queryAnchor, limit: limit) { result in
  442. switch result {
  443. case .failure(let error):
  444. XCTFail("Unexpected failure: \(error)")
  445. case .success(let anchor, let data):
  446. XCTAssertEqual(anchor.modificationCounter, 1)
  447. XCTAssertEqual(data.count, 0)
  448. }
  449. self.completion.fulfill()
  450. }
  451. wait(for: [completion], timeout: 2, enforceOrder: true)
  452. }
  453. func testDataWithUnusedQueryAnchor() {
  454. let syncIdentifiers = [generateSyncIdentifier(), generateSyncIdentifier(), generateSyncIdentifier()]
  455. addData(withSyncIdentifiers: syncIdentifiers)
  456. settingsStore.executeSettingsQuery(fromQueryAnchor: queryAnchor, limit: limit) { result in
  457. switch result {
  458. case .failure(let error):
  459. XCTFail("Unexpected failure: \(error)")
  460. case .success(let anchor, let data):
  461. XCTAssertEqual(anchor.modificationCounter, 3)
  462. XCTAssertEqual(data.count, 3)
  463. for (index, syncIdentifier) in syncIdentifiers.enumerated() {
  464. XCTAssertEqual(data[index].syncIdentifier, syncIdentifier)
  465. }
  466. }
  467. self.completion.fulfill()
  468. }
  469. wait(for: [completion], timeout: 2, enforceOrder: true)
  470. }
  471. func testDataWithStaleQueryAnchor() {
  472. let syncIdentifiers = [generateSyncIdentifier(), generateSyncIdentifier(), generateSyncIdentifier()]
  473. addData(withSyncIdentifiers: syncIdentifiers)
  474. queryAnchor.modificationCounter = 2
  475. settingsStore.executeSettingsQuery(fromQueryAnchor: queryAnchor, limit: limit) { result in
  476. switch result {
  477. case .failure(let error):
  478. XCTFail("Unexpected failure: \(error)")
  479. case .success(let anchor, let data):
  480. XCTAssertEqual(anchor.modificationCounter, 3)
  481. XCTAssertEqual(data.count, 1)
  482. XCTAssertEqual(data[0].syncIdentifier, syncIdentifiers[2])
  483. }
  484. self.completion.fulfill()
  485. }
  486. wait(for: [completion], timeout: 2, enforceOrder: true)
  487. }
  488. func testDataWithCurrentQueryAnchor() {
  489. let syncIdentifiers = [generateSyncIdentifier(), generateSyncIdentifier(), generateSyncIdentifier()]
  490. addData(withSyncIdentifiers: syncIdentifiers)
  491. queryAnchor.modificationCounter = 3
  492. settingsStore.executeSettingsQuery(fromQueryAnchor: queryAnchor, limit: limit) { result in
  493. switch result {
  494. case .failure(let error):
  495. XCTFail("Unexpected failure: \(error)")
  496. case .success(let anchor, let data):
  497. XCTAssertEqual(anchor.modificationCounter, 3)
  498. XCTAssertEqual(data.count, 0)
  499. }
  500. self.completion.fulfill()
  501. }
  502. wait(for: [completion], timeout: 2, enforceOrder: true)
  503. }
  504. func testDataWithLimitZero() {
  505. let syncIdentifiers = [generateSyncIdentifier(), generateSyncIdentifier(), generateSyncIdentifier()]
  506. addData(withSyncIdentifiers: syncIdentifiers)
  507. limit = 0
  508. settingsStore.executeSettingsQuery(fromQueryAnchor: queryAnchor, limit: limit) { result in
  509. switch result {
  510. case .failure(let error):
  511. XCTFail("Unexpected failure: \(error)")
  512. case .success(let anchor, let data):
  513. XCTAssertEqual(anchor.modificationCounter, 0)
  514. XCTAssertEqual(data.count, 0)
  515. }
  516. self.completion.fulfill()
  517. }
  518. wait(for: [completion], timeout: 2, enforceOrder: true)
  519. }
  520. func testDataWithLimitCoveredByData() {
  521. let syncIdentifiers = [generateSyncIdentifier(), generateSyncIdentifier(), generateSyncIdentifier()]
  522. addData(withSyncIdentifiers: syncIdentifiers)
  523. limit = 2
  524. settingsStore.executeSettingsQuery(fromQueryAnchor: queryAnchor, limit: limit) { result in
  525. switch result {
  526. case .failure(let error):
  527. XCTFail("Unexpected failure: \(error)")
  528. case .success(let anchor, let data):
  529. XCTAssertEqual(anchor.modificationCounter, 2)
  530. XCTAssertEqual(data.count, 2)
  531. XCTAssertEqual(data[0].syncIdentifier, syncIdentifiers[0])
  532. XCTAssertEqual(data[1].syncIdentifier, syncIdentifiers[1])
  533. }
  534. self.completion.fulfill()
  535. }
  536. wait(for: [completion], timeout: 2, enforceOrder: true)
  537. }
  538. private func addData(withSyncIdentifiers syncIdentifiers: [UUID]) {
  539. let semaphore = DispatchSemaphore(value: 0)
  540. for syncIdentifier in syncIdentifiers {
  541. self.settingsStore.storeSettings(StoredSettings(syncIdentifier: syncIdentifier)) { _ in semaphore.signal() }
  542. }
  543. for _ in syncIdentifiers { semaphore.wait() }
  544. }
  545. private func generateSyncIdentifier() -> UUID { UUID() }
  546. }
  547. class SettingsStoreCriticalEventLogTests: PersistenceControllerTestCase {
  548. var settingsStore: SettingsStore!
  549. var outputStream: MockOutputStream!
  550. var progress: Progress!
  551. override func setUp() {
  552. super.setUp()
  553. 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")!),
  554. StoredSettings(date: dateFormatter.date(from: "2100-01-02T03:10:00Z")!, controllerTimeZone: TimeZone(identifier: "America/Los_Angeles")!, syncIdentifier: UUID(uuidString: "C86DEB61-68E9-464E-9DD5-96A9CB445FD3")!),
  555. StoredSettings(date: dateFormatter.date(from: "2100-01-02T03:04:00Z")!, controllerTimeZone: TimeZone(identifier: "America/Los_Angeles")!, syncIdentifier: UUID(uuidString: "2B03D96C-6F5D-4140-99CD-80C3E64D6010")!),
  556. StoredSettings(date: dateFormatter.date(from: "2100-01-02T03:06:00Z")!, controllerTimeZone: TimeZone(identifier: "America/Los_Angeles")!, syncIdentifier: UUID(uuidString: "FF1C4F01-3558-4FB2-957E-FA1522C4735E")!),
  557. StoredSettings(date: dateFormatter.date(from: "2100-01-02T03:02:00Z")!, controllerTimeZone: TimeZone(identifier: "America/Los_Angeles")!, syncIdentifier: UUID(uuidString: "71B699D7-0E8F-4B13-B7A1-E7751EB78E74")!)]
  558. settingsStore = SettingsStore(store: cacheStore, expireAfter: .hours(1))
  559. let dispatchGroup = DispatchGroup()
  560. dispatchGroup.enter()
  561. settingsStore.addStoredSettings(settings: settings) { error in
  562. XCTAssertNil(error)
  563. dispatchGroup.leave()
  564. }
  565. dispatchGroup.wait()
  566. outputStream = MockOutputStream()
  567. progress = Progress()
  568. }
  569. override func tearDown() {
  570. settingsStore = nil
  571. super.tearDown()
  572. }
  573. func testExportProgressTotalUnitCount() {
  574. switch settingsStore.exportProgressTotalUnitCount(startDate: dateFormatter.date(from: "2100-01-02T03:03:00Z")!,
  575. endDate: dateFormatter.date(from: "2100-01-02T03:09:00Z")!) {
  576. case .failure(let error):
  577. XCTFail("Unexpected failure: \(error)")
  578. case .success(let progressTotalUnitCount):
  579. XCTAssertEqual(progressTotalUnitCount, 3 * 11)
  580. }
  581. }
  582. func testExportProgressTotalUnitCountEmpty() {
  583. switch settingsStore.exportProgressTotalUnitCount(startDate: dateFormatter.date(from: "2100-01-02T03:00:00Z")!,
  584. endDate: dateFormatter.date(from: "2100-01-02T03:01:00Z")!) {
  585. case .failure(let error):
  586. XCTFail("Unexpected failure: \(error)")
  587. case .success(let progressTotalUnitCount):
  588. XCTAssertEqual(progressTotalUnitCount, 0)
  589. }
  590. }
  591. func testExport() {
  592. XCTAssertNil(settingsStore.export(startDate: dateFormatter.date(from: "2100-01-02T03:03:00Z")!,
  593. endDate: dateFormatter.date(from: "2100-01-02T03:09:00Z")!,
  594. to: outputStream,
  595. progress: progress))
  596. XCTAssertEqual(outputStream.string, """
  597. [
  598. {"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},
  599. {"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},
  600. {"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}
  601. ]
  602. """
  603. )
  604. XCTAssertEqual(progress.completedUnitCount, 3 * 11)
  605. }
  606. func testExportEmpty() {
  607. XCTAssertNil(settingsStore.export(startDate: dateFormatter.date(from: "2100-01-02T03:00:00Z")!,
  608. endDate: dateFormatter.date(from: "2100-01-02T03:01:00Z")!,
  609. to: outputStream,
  610. progress: progress))
  611. XCTAssertEqual(outputStream.string, "[]")
  612. XCTAssertEqual(progress.completedUnitCount, 0)
  613. }
  614. func testExportCancelled() {
  615. progress.cancel()
  616. XCTAssertEqual(settingsStore.export(startDate: dateFormatter.date(from: "2100-01-02T03:03:00Z")!,
  617. endDate: dateFormatter.date(from: "2100-01-02T03:09:00Z")!,
  618. to: outputStream,
  619. progress: progress) as? CriticalEventLogError, CriticalEventLogError.cancelled)
  620. }
  621. private let dateFormatter = ISO8601DateFormatter()
  622. }
  623. class StoredSettingsCodableTests: XCTestCase {
  624. func testStoredSettingsCodable() throws {
  625. try assertStoredSettingsCodable(StoredSettings.test, encodesJSON: """
  626. {
  627. "automaticDosingStrategy" : 1,
  628. "basalRateSchedule" : {
  629. "items" : [
  630. {
  631. "startTime" : 0,
  632. "value" : 1
  633. },
  634. {
  635. "startTime" : 21600,
  636. "value" : 1.5
  637. },
  638. {
  639. "startTime" : 64800,
  640. "value" : 1.25
  641. }
  642. ],
  643. "referenceTimeInterval" : 0,
  644. "repeatInterval" : 86400,
  645. "timeZone" : {
  646. "identifier" : "GMT-0700"
  647. }
  648. },
  649. "bloodGlucoseUnit" : "mg/dL",
  650. "carbRatioSchedule" : {
  651. "unit" : "g",
  652. "valueSchedule" : {
  653. "items" : [
  654. {
  655. "startTime" : 0,
  656. "value" : 15
  657. },
  658. {
  659. "startTime" : 32400,
  660. "value" : 14
  661. },
  662. {
  663. "startTime" : 72000,
  664. "value" : 18
  665. }
  666. ],
  667. "referenceTimeInterval" : 0,
  668. "repeatInterval" : 86400,
  669. "timeZone" : {
  670. "identifier" : "GMT-0700"
  671. }
  672. }
  673. },
  674. "cgmDevice" : {
  675. "firmwareVersion" : "CGM Firmware Version",
  676. "hardwareVersion" : "CGM Hardware Version",
  677. "localIdentifier" : "CGM Local Identifier",
  678. "manufacturer" : "CGM Manufacturer",
  679. "model" : "CGM Model",
  680. "name" : "CGM Name",
  681. "softwareVersion" : "CGM Software Version",
  682. "udiDeviceIdentifier" : "CGM UDI Device Identifier"
  683. },
  684. "controllerDevice" : {
  685. "model" : "Controller Model",
  686. "modelIdentifier" : "Controller Model Identifier",
  687. "name" : "Controller Name",
  688. "systemName" : "Controller System Name",
  689. "systemVersion" : "Controller System Version"
  690. },
  691. "controllerTimeZone" : {
  692. "identifier" : "America/Los_Angeles"
  693. },
  694. "date" : "2020-05-14T22:48:15Z",
  695. "defaultRapidActingModel" : {
  696. "actionDuration" : 21600,
  697. "delay" : 600,
  698. "modelType" : "rapidAdult",
  699. "peakActivity" : 10800
  700. },
  701. "deviceToken" : "Device Token String",
  702. "dosingEnabled" : true,
  703. "glucoseTargetRangeSchedule" : {
  704. "override" : {
  705. "end" : "2020-05-14T14:48:15Z",
  706. "start" : "2020-05-14T12:48:15Z",
  707. "value" : {
  708. "maxValue" : 115,
  709. "minValue" : 105
  710. }
  711. },
  712. "rangeSchedule" : {
  713. "unit" : "mg/dL",
  714. "valueSchedule" : {
  715. "items" : [
  716. {
  717. "startTime" : 0,
  718. "value" : {
  719. "maxValue" : 110,
  720. "minValue" : 100
  721. }
  722. },
  723. {
  724. "startTime" : 25200,
  725. "value" : {
  726. "maxValue" : 100,
  727. "minValue" : 90
  728. }
  729. },
  730. {
  731. "startTime" : 75600,
  732. "value" : {
  733. "maxValue" : 120,
  734. "minValue" : 110
  735. }
  736. }
  737. ],
  738. "referenceTimeInterval" : 0,
  739. "repeatInterval" : 86400,
  740. "timeZone" : {
  741. "identifier" : "GMT-0700"
  742. }
  743. }
  744. }
  745. },
  746. "insulinSensitivitySchedule" : {
  747. "unit" : "mg/dL",
  748. "valueSchedule" : {
  749. "items" : [
  750. {
  751. "startTime" : 0,
  752. "value" : 45
  753. },
  754. {
  755. "startTime" : 10800,
  756. "value" : 40
  757. },
  758. {
  759. "startTime" : 54000,
  760. "value" : 50
  761. }
  762. ],
  763. "referenceTimeInterval" : 0,
  764. "repeatInterval" : 86400,
  765. "timeZone" : {
  766. "identifier" : "GMT-0700"
  767. }
  768. }
  769. },
  770. "insulinType" : 1,
  771. "maximumBasalRatePerHour" : 3.5,
  772. "maximumBolus" : 10,
  773. "notificationSettings" : {
  774. "alertSetting" : "disabled",
  775. "alertStyle" : "banner",
  776. "announcementSetting" : "enabled",
  777. "authorizationStatus" : "authorized",
  778. "badgeSetting" : "enabled",
  779. "carPlaySetting" : "notSupported",
  780. "criticalAlertSetting" : "enabled",
  781. "lockScreenSetting" : "disabled",
  782. "notificationCenterSetting" : "notSupported",
  783. "providesAppNotificationSettings" : true,
  784. "scheduledDeliverySetting" : "disabled",
  785. "showPreviewsSetting" : "whenAuthenticated",
  786. "soundSetting" : "enabled",
  787. "temporaryMuteAlertsSetting" : {
  788. "disabled" : {
  789. }
  790. },
  791. "timeSensitiveSetting" : "enabled"
  792. },
  793. "overridePresets" : [
  794. {
  795. "duration" : {
  796. "finite" : {
  797. "duration" : 3600
  798. }
  799. },
  800. "id" : "2A67A303-5203-4CB8-8263-79498265368E",
  801. "name" : "Apple",
  802. "settings" : {
  803. "insulinNeedsScaleFactor" : 2,
  804. "targetRangeInMgdl" : {
  805. "maxValue" : 140,
  806. "minValue" : 130
  807. }
  808. },
  809. "symbol" : "🍎"
  810. }
  811. ],
  812. "preMealOverride" : {
  813. "actualEnd" : {
  814. "type" : "natural"
  815. },
  816. "context" : "preMeal",
  817. "duration" : "indefinite",
  818. "enactTrigger" : "local",
  819. "settings" : {
  820. "insulinNeedsScaleFactor" : 0.5,
  821. "targetRangeInMgdl" : {
  822. "maxValue" : 90,
  823. "minValue" : 80
  824. }
  825. },
  826. "startDate" : "2020-05-14T14:38:39Z",
  827. "syncIdentifier" : "2A67A303-5203-1234-8263-79498265368E"
  828. },
  829. "preMealTargetRange" : {
  830. "maxValue" : 90,
  831. "minValue" : 80
  832. },
  833. "pumpDevice" : {
  834. "firmwareVersion" : "Pump Firmware Version",
  835. "hardwareVersion" : "Pump Hardware Version",
  836. "localIdentifier" : "Pump Local Identifier",
  837. "manufacturer" : "Pump Manufacturer",
  838. "model" : "Pump Model",
  839. "name" : "Pump Name",
  840. "softwareVersion" : "Pump Software Version",
  841. "udiDeviceIdentifier" : "Pump UDI Device Identifier"
  842. },
  843. "scheduleOverride" : {
  844. "actualEnd" : {
  845. "type" : "natural"
  846. },
  847. "context" : "preMeal",
  848. "duration" : {
  849. "finite" : {
  850. "duration" : 3600
  851. }
  852. },
  853. "enactTrigger" : {
  854. "remote" : {
  855. "address" : "127.0.0.1"
  856. }
  857. },
  858. "settings" : {
  859. "insulinNeedsScaleFactor" : 1.5,
  860. "targetRangeInMgdl" : {
  861. "maxValue" : 120,
  862. "minValue" : 110
  863. }
  864. },
  865. "startDate" : "2020-05-14T14:48:19Z",
  866. "syncIdentifier" : "2A67A303-1234-4CB8-8263-79498265368E"
  867. },
  868. "suspendThreshold" : {
  869. "unit" : "mg/dL",
  870. "value" : 75
  871. },
  872. "syncIdentifier" : "2A67A303-1234-4CB8-1234-79498265368E",
  873. "workoutTargetRange" : {
  874. "maxValue" : 160,
  875. "minValue" : 150
  876. }
  877. }
  878. """
  879. )
  880. }
  881. private func assertStoredSettingsCodable(_ original: StoredSettings, encodesJSON string: String) throws {
  882. let data = try encoder.encode(original)
  883. XCTAssertEqual(String(data: data, encoding: .utf8), string)
  884. let decoded = try decoder.decode(StoredSettings.self, from: data)
  885. XCTAssertEqual(decoded, original)
  886. }
  887. private let dateFormatter = ISO8601DateFormatter()
  888. private let encoder: JSONEncoder = {
  889. let encoder = JSONEncoder()
  890. encoder.outputFormatting = [.prettyPrinted, .sortedKeys, .withoutEscapingSlashes]
  891. encoder.dateEncodingStrategy = .iso8601
  892. return encoder
  893. }()
  894. private let decoder: JSONDecoder = {
  895. let decoder = JSONDecoder()
  896. decoder.dateDecodingStrategy = .iso8601
  897. return decoder
  898. }()
  899. }
  900. fileprivate extension StoredSettings {
  901. static var test: StoredSettings {
  902. let controllerTimeZone = TimeZone(identifier: "America/Los_Angeles")!
  903. let scheduleTimeZone = TimeZone(secondsFromGMT: TimeZone(identifier: "America/Phoenix")!.secondsFromGMT())!
  904. let dosingEnabled = true
  905. let glucoseTargetRangeSchedule = GlucoseRangeSchedule(rangeSchedule: DailyQuantitySchedule(unit: .milligramsPerDeciliter,
  906. dailyItems: [RepeatingScheduleValue(startTime: .hours(0), value: DoubleRange(minValue: 100.0, maxValue: 110.0)),
  907. RepeatingScheduleValue(startTime: .hours(7), value: DoubleRange(minValue: 90.0, maxValue: 100.0)),
  908. RepeatingScheduleValue(startTime: .hours(21), value: DoubleRange(minValue: 110.0, maxValue: 120.0))],
  909. timeZone: scheduleTimeZone)!,
  910. override: GlucoseRangeSchedule.Override(value: DoubleRange(minValue: 105.0, maxValue: 115.0),
  911. start: dateFormatter.date(from: "2020-05-14T12:48:15Z")!,
  912. end: dateFormatter.date(from: "2020-05-14T14:48:15Z")!))
  913. let preMealTargetRange = DoubleRange(minValue: 80.0, maxValue: 90.0).quantityRange(for: .milligramsPerDeciliter)
  914. let workoutTargetRange = DoubleRange(minValue: 150.0, maxValue: 160.0).quantityRange(for: .milligramsPerDeciliter)
  915. let overridePresets = [TemporaryScheduleOverridePreset(id: UUID(uuidString: "2A67A303-5203-4CB8-8263-79498265368E")!,
  916. symbol: "🍎",
  917. name: "Apple",
  918. settings: TemporaryScheduleOverrideSettings(unit: .milligramsPerDeciliter,
  919. targetRange: DoubleRange(minValue: 130.0, maxValue: 140.0),
  920. insulinNeedsScaleFactor: 2.0),
  921. duration: .finite(.minutes(60)))]
  922. let scheduleOverride = TemporaryScheduleOverride(context: .preMeal,
  923. settings: TemporaryScheduleOverrideSettings(unit: .milligramsPerDeciliter,
  924. targetRange: DoubleRange(minValue: 110.0, maxValue: 120.0),
  925. insulinNeedsScaleFactor: 1.5),
  926. startDate: dateFormatter.date(from: "2020-05-14T14:48:19Z")!,
  927. duration: .finite(.minutes(60)),
  928. enactTrigger: .remote("127.0.0.1"),
  929. syncIdentifier: UUID(uuidString: "2A67A303-1234-4CB8-8263-79498265368E")!)
  930. let preMealOverride = TemporaryScheduleOverride(context: .preMeal,
  931. settings: TemporaryScheduleOverrideSettings(unit: .milligramsPerDeciliter,
  932. targetRange: DoubleRange(minValue: 80.0, maxValue: 90.0),
  933. insulinNeedsScaleFactor: 0.5),
  934. startDate: dateFormatter.date(from: "2020-05-14T14:38:39Z")!,
  935. duration: .indefinite,
  936. enactTrigger: .local,
  937. syncIdentifier: UUID(uuidString: "2A67A303-5203-1234-8263-79498265368E")!)
  938. let maximumBasalRatePerHour = 3.5
  939. let maximumBolus = 10.0
  940. let suspendThreshold = GlucoseThreshold(unit: .milligramsPerDeciliter, value: 75.0)
  941. let deviceToken = "Device Token String"
  942. let insulinType = InsulinType.humalog
  943. let defaultRapidActingModel = StoredInsulinModel(modelType: .rapidAdult, delay: .minutes(10), actionDuration: .hours(6), peakActivity: .hours(3))
  944. let basalRateSchedule = BasalRateSchedule(dailyItems: [RepeatingScheduleValue(startTime: .hours(0), value: 1.0),
  945. RepeatingScheduleValue(startTime: .hours(6), value: 1.5),
  946. RepeatingScheduleValue(startTime: .hours(18), value: 1.25)],
  947. timeZone: scheduleTimeZone)
  948. let insulinSensitivitySchedule = InsulinSensitivitySchedule(unit: .milligramsPerDeciliter,
  949. dailyItems: [RepeatingScheduleValue(startTime: .hours(0), value: 45.0),
  950. RepeatingScheduleValue(startTime: .hours(3), value: 40.0),
  951. RepeatingScheduleValue(startTime: .hours(15), value: 50.0)],
  952. timeZone: scheduleTimeZone)
  953. let carbRatioSchedule = CarbRatioSchedule(unit: .gram(),
  954. dailyItems: [RepeatingScheduleValue(startTime: .hours(0), value: 15.0),
  955. RepeatingScheduleValue(startTime: .hours(9), value: 14.0),
  956. RepeatingScheduleValue(startTime: .hours(20), value: 18.0)],
  957. timeZone: scheduleTimeZone)
  958. let notificationSettings = NotificationSettings(authorizationStatus: .authorized,
  959. soundSetting: .enabled,
  960. badgeSetting: .enabled,
  961. alertSetting: .disabled,
  962. notificationCenterSetting: .notSupported,
  963. lockScreenSetting: .disabled,
  964. carPlaySetting: .notSupported,
  965. alertStyle: .banner,
  966. showPreviewsSetting: .whenAuthenticated,
  967. criticalAlertSetting: .enabled,
  968. providesAppNotificationSettings: true,
  969. announcementSetting: .enabled,
  970. timeSensitiveSetting: .enabled,
  971. scheduledDeliverySetting: .disabled,
  972. temporaryMuteAlertsSetting: .disabled)
  973. let controllerDevice = StoredSettings.ControllerDevice(name: "Controller Name",
  974. systemName: "Controller System Name",
  975. systemVersion: "Controller System Version",
  976. model: "Controller Model",
  977. modelIdentifier: "Controller Model Identifier")
  978. let cgmDevice = HKDevice(name: "CGM Name",
  979. manufacturer: "CGM Manufacturer",
  980. model: "CGM Model",
  981. hardwareVersion: "CGM Hardware Version",
  982. firmwareVersion: "CGM Firmware Version",
  983. softwareVersion: "CGM Software Version",
  984. localIdentifier: "CGM Local Identifier",
  985. udiDeviceIdentifier: "CGM UDI Device Identifier")
  986. let pumpDevice = HKDevice(name: "Pump Name",
  987. manufacturer: "Pump Manufacturer",
  988. model: "Pump Model",
  989. hardwareVersion: "Pump Hardware Version",
  990. firmwareVersion: "Pump Firmware Version",
  991. softwareVersion: "Pump Software Version",
  992. localIdentifier: "Pump Local Identifier",
  993. udiDeviceIdentifier: "Pump UDI Device Identifier")
  994. let bloodGlucoseUnit = HKUnit.milligramsPerDeciliter
  995. return StoredSettings(date: dateFormatter.date(from: "2020-05-14T22:48:15Z")!,
  996. controllerTimeZone: controllerTimeZone,
  997. dosingEnabled: dosingEnabled,
  998. glucoseTargetRangeSchedule: glucoseTargetRangeSchedule,
  999. preMealTargetRange: preMealTargetRange,
  1000. workoutTargetRange: workoutTargetRange,
  1001. overridePresets: overridePresets,
  1002. scheduleOverride: scheduleOverride,
  1003. preMealOverride: preMealOverride,
  1004. maximumBasalRatePerHour: maximumBasalRatePerHour,
  1005. maximumBolus: maximumBolus,
  1006. suspendThreshold: suspendThreshold,
  1007. deviceToken: deviceToken,
  1008. insulinType: insulinType,
  1009. defaultRapidActingModel: defaultRapidActingModel,
  1010. basalRateSchedule: basalRateSchedule,
  1011. insulinSensitivitySchedule: insulinSensitivitySchedule,
  1012. carbRatioSchedule: carbRatioSchedule,
  1013. notificationSettings: notificationSettings,
  1014. controllerDevice: controllerDevice,
  1015. cgmDevice: cgmDevice,
  1016. pumpDevice: pumpDevice,
  1017. bloodGlucoseUnit: bloodGlucoseUnit,
  1018. automaticDosingStrategy: .automaticBolus,
  1019. syncIdentifier: UUID(uuidString: "2A67A303-1234-4CB8-1234-79498265368E")!)
  1020. }
  1021. private static let dateFormatter = ISO8601DateFormatter()
  1022. }