SettingsStoreTests.swift 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980
  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. @testable import LoopKit
  10. class SettingsStorePersistenceTests: PersistenceControllerTestCase, SettingsStoreDelegate {
  11. var settingsStore: SettingsStore!
  12. override func setUp() {
  13. super.setUp()
  14. settingsStoreHasUpdatedSettingsDataHandler = nil
  15. settingsStore = SettingsStore(store: cacheStore, expireAfter: .hours(1))
  16. settingsStore.delegate = self
  17. }
  18. override func tearDown() {
  19. settingsStore.delegate = nil
  20. settingsStore = nil
  21. settingsStoreHasUpdatedSettingsDataHandler = nil
  22. super.tearDown()
  23. }
  24. // MARK: - SettingsStoreDelegate
  25. var settingsStoreHasUpdatedSettingsDataHandler: ((_ : SettingsStore) -> Void)?
  26. func settingsStoreHasUpdatedSettingsData(_ settingsStore: SettingsStore) {
  27. settingsStoreHasUpdatedSettingsDataHandler?(settingsStore)
  28. }
  29. // MARK: -
  30. func testStoreSettings() {
  31. let storeSettingsHandler = expectation(description: "Store settings handler")
  32. let storeSettingsCompletion = expectation(description: "Store settings completion")
  33. var handlerInvocation = 0
  34. settingsStoreHasUpdatedSettingsDataHandler = { settingsStore in
  35. handlerInvocation += 1
  36. switch handlerInvocation {
  37. case 1:
  38. storeSettingsHandler.fulfill()
  39. default:
  40. XCTFail("Unexpected handler invocation")
  41. }
  42. }
  43. settingsStore.storeSettings(StoredSettings()) {
  44. storeSettingsCompletion.fulfill()
  45. }
  46. wait(for: [storeSettingsHandler, storeSettingsCompletion], timeout: 2, enforceOrder: true)
  47. }
  48. func testStoreSettingsMultiple() {
  49. let storeSettingsHandler1 = expectation(description: "Store settings handler 1")
  50. let storeSettingsHandler2 = expectation(description: "Store settings handler 2")
  51. let storeSettingsCompletion1 = expectation(description: "Store settings completion 1")
  52. let storeSettingsCompletion2 = expectation(description: "Store settings completion 2")
  53. var handlerInvocation = 0
  54. settingsStoreHasUpdatedSettingsDataHandler = { settingsStore in
  55. handlerInvocation += 1
  56. switch handlerInvocation {
  57. case 1:
  58. storeSettingsHandler1.fulfill()
  59. case 2:
  60. storeSettingsHandler2.fulfill()
  61. default:
  62. XCTFail("Unexpected handler invocation")
  63. }
  64. }
  65. settingsStore.storeSettings(StoredSettings()) {
  66. storeSettingsCompletion1.fulfill()
  67. }
  68. settingsStore.storeSettings(StoredSettings()) {
  69. storeSettingsCompletion2.fulfill()
  70. }
  71. wait(for: [storeSettingsHandler1, storeSettingsCompletion1, storeSettingsHandler2, storeSettingsCompletion2], timeout: 2, enforceOrder: true)
  72. }
  73. // MARK: -
  74. func testSettingsObjectEncodable() throws {
  75. cacheStore.managedObjectContext.performAndWait {
  76. do {
  77. let object = SettingsObject(context: cacheStore.managedObjectContext)
  78. object.data = try PropertyListEncoder().encode(StoredSettings.test)
  79. object.date = dateFormatter.date(from: "2100-01-02T03:03:00Z")!
  80. object.modificationCounter = 123
  81. try assertSettingsObjectEncodable(object, encodesJSON: """
  82. {
  83. "data" : {
  84. "basalRateSchedule" : {
  85. "items" : [
  86. {
  87. "startTime" : 0,
  88. "value" : 1
  89. },
  90. {
  91. "startTime" : 21600,
  92. "value" : 1.5
  93. },
  94. {
  95. "startTime" : 64800,
  96. "value" : 1.25
  97. }
  98. ],
  99. "referenceTimeInterval" : 0,
  100. "repeatInterval" : 86400,
  101. "timeZone" : {
  102. "identifier" : "America/Los_Angeles"
  103. }
  104. },
  105. "bloodGlucoseUnit" : "mg/dL",
  106. "carbRatioSchedule" : {
  107. "unit" : "g",
  108. "valueSchedule" : {
  109. "items" : [
  110. {
  111. "startTime" : 0,
  112. "value" : 15
  113. },
  114. {
  115. "startTime" : 32400,
  116. "value" : 14
  117. },
  118. {
  119. "startTime" : 72000,
  120. "value" : 18
  121. }
  122. ],
  123. "referenceTimeInterval" : 0,
  124. "repeatInterval" : 86400,
  125. "timeZone" : {
  126. "identifier" : "America/Los_Angeles"
  127. }
  128. }
  129. },
  130. "date" : "2020-05-14T22:48:15Z",
  131. "deviceToken" : "DeviceTokenString",
  132. "dosingEnabled" : true,
  133. "glucoseTargetRangeSchedule" : {
  134. "override" : {
  135. "end" : "2020-05-14T14:48:15Z",
  136. "start" : "2020-05-14T12:48:15Z",
  137. "value" : {
  138. "maxValue" : 115,
  139. "minValue" : 105
  140. }
  141. },
  142. "rangeSchedule" : {
  143. "unit" : "mg/dL",
  144. "valueSchedule" : {
  145. "items" : [
  146. {
  147. "startTime" : 0,
  148. "value" : {
  149. "maxValue" : 110,
  150. "minValue" : 100
  151. }
  152. },
  153. {
  154. "startTime" : 25200,
  155. "value" : {
  156. "maxValue" : 100,
  157. "minValue" : 90
  158. }
  159. },
  160. {
  161. "startTime" : 75600,
  162. "value" : {
  163. "maxValue" : 120,
  164. "minValue" : 110
  165. }
  166. }
  167. ],
  168. "referenceTimeInterval" : 0,
  169. "repeatInterval" : 86400,
  170. "timeZone" : {
  171. "identifier" : "America/Los_Angeles"
  172. }
  173. }
  174. }
  175. },
  176. "insulinModel" : {
  177. "actionDuration" : 21600,
  178. "modelType" : "rapidAdult",
  179. "peakActivity" : 10800
  180. },
  181. "insulinSensitivitySchedule" : {
  182. "unit" : "mg/dL",
  183. "valueSchedule" : {
  184. "items" : [
  185. {
  186. "startTime" : 0,
  187. "value" : 45
  188. },
  189. {
  190. "startTime" : 10800,
  191. "value" : 40
  192. },
  193. {
  194. "startTime" : 54000,
  195. "value" : 50
  196. }
  197. ],
  198. "referenceTimeInterval" : 0,
  199. "repeatInterval" : 86400,
  200. "timeZone" : {
  201. "identifier" : "America/Los_Angeles"
  202. }
  203. }
  204. },
  205. "maximumBasalRatePerHour" : 3.5,
  206. "maximumBolus" : 10,
  207. "overridePresets" : [
  208. {
  209. "duration" : {
  210. "finite" : {
  211. "duration" : 3600
  212. }
  213. },
  214. "id" : "2A67A303-5203-4CB8-8263-79498265368E",
  215. "name" : "Apple",
  216. "settings" : {
  217. "insulinNeedsScaleFactor" : 2,
  218. "targetRangeInMgdl" : {
  219. "maxValue" : 140,
  220. "minValue" : 130
  221. }
  222. },
  223. "symbol" : "🍎"
  224. }
  225. ],
  226. "preMealOverride" : {
  227. "actualEnd" : {
  228. "type" : "natural"
  229. },
  230. "context" : "preMeal",
  231. "duration" : "indefinite",
  232. "enactTrigger" : "local",
  233. "settings" : {
  234. "insulinNeedsScaleFactor" : 0.5,
  235. "targetRangeInMgdl" : {
  236. "maxValue" : 90,
  237. "minValue" : 80
  238. }
  239. },
  240. "startDate" : "2020-05-14T14:38:39Z",
  241. "syncIdentifier" : "2A67A303-5203-1234-8263-79498265368E"
  242. },
  243. "preMealTargetRange" : {
  244. "maxValue" : 90,
  245. "minValue" : 80
  246. },
  247. "scheduleOverride" : {
  248. "actualEnd" : {
  249. "type" : "natural"
  250. },
  251. "context" : "preMeal",
  252. "duration" : {
  253. "finite" : {
  254. "duration" : 3600
  255. }
  256. },
  257. "enactTrigger" : {
  258. "remote" : {
  259. "address" : "127.0.0.1"
  260. }
  261. },
  262. "settings" : {
  263. "insulinNeedsScaleFactor" : 1.5,
  264. "targetRangeInMgdl" : {
  265. "maxValue" : 120,
  266. "minValue" : 110
  267. }
  268. },
  269. "startDate" : "2020-05-14T14:48:19Z",
  270. "syncIdentifier" : "2A67A303-1234-4CB8-8263-79498265368E"
  271. },
  272. "suspendThreshold" : {
  273. "unit" : "mg/dL",
  274. "value" : 75
  275. },
  276. "syncIdentifier" : "2A67A303-1234-4CB8-1234-79498265368E",
  277. "workoutTargetRange" : {
  278. "maxValue" : 160,
  279. "minValue" : 150
  280. }
  281. },
  282. "date" : "2100-01-02T03:03:00Z",
  283. "modificationCounter" : 123
  284. }
  285. """
  286. )
  287. } catch let error {
  288. XCTFail("Unexpected failure: \(error)")
  289. }
  290. }
  291. }
  292. private func assertSettingsObjectEncodable(_ original: SettingsObject, encodesJSON string: String) throws {
  293. let data = try encoder.encode(original)
  294. XCTAssertEqual(String(data: data, encoding: .utf8), string)
  295. }
  296. private let dateFormatter = ISO8601DateFormatter()
  297. private let encoder: JSONEncoder = {
  298. let encoder = JSONEncoder()
  299. encoder.outputFormatting = [.prettyPrinted, .sortedKeys, .withoutEscapingSlashes]
  300. encoder.dateEncodingStrategy = .iso8601
  301. return encoder
  302. }()
  303. }
  304. class SettingsStoreQueryAnchorTests: XCTestCase {
  305. var rawValue: SettingsStore.QueryAnchor.RawValue = [
  306. "modificationCounter": Int64(123)
  307. ]
  308. func testInitializerDefault() {
  309. let queryAnchor = SettingsStore.QueryAnchor()
  310. XCTAssertEqual(queryAnchor.modificationCounter, 0)
  311. }
  312. func testInitializerRawValue() {
  313. let queryAnchor = SettingsStore.QueryAnchor(rawValue: rawValue)
  314. XCTAssertNotNil(queryAnchor)
  315. XCTAssertEqual(queryAnchor?.modificationCounter, 123)
  316. }
  317. func testInitializerRawValueMissingModificationCounter() {
  318. rawValue["modificationCounter"] = nil
  319. XCTAssertNil(SettingsStore.QueryAnchor(rawValue: rawValue))
  320. }
  321. func testInitializerRawValueInvalidModificationCounter() {
  322. rawValue["modificationCounter"] = "123"
  323. XCTAssertNil(SettingsStore.QueryAnchor(rawValue: rawValue))
  324. }
  325. func testRawValueWithDefault() {
  326. let rawValue = SettingsStore.QueryAnchor().rawValue
  327. XCTAssertEqual(rawValue.count, 1)
  328. XCTAssertEqual(rawValue["modificationCounter"] as? Int64, Int64(0))
  329. }
  330. func testRawValueWithNonDefault() {
  331. var queryAnchor = SettingsStore.QueryAnchor()
  332. queryAnchor.modificationCounter = 123
  333. let rawValue = queryAnchor.rawValue
  334. XCTAssertEqual(rawValue.count, 1)
  335. XCTAssertEqual(rawValue["modificationCounter"] as? Int64, Int64(123))
  336. }
  337. }
  338. class SettingsStoreQueryTests: PersistenceControllerTestCase {
  339. var settingsStore: SettingsStore!
  340. var completion: XCTestExpectation!
  341. var queryAnchor: SettingsStore.QueryAnchor!
  342. var limit: Int!
  343. override func setUp() {
  344. super.setUp()
  345. settingsStore = SettingsStore(store: cacheStore, expireAfter: .hours(1))
  346. completion = expectation(description: "Completion")
  347. queryAnchor = SettingsStore.QueryAnchor()
  348. limit = Int.max
  349. }
  350. override func tearDown() {
  351. limit = nil
  352. queryAnchor = nil
  353. completion = nil
  354. settingsStore = nil
  355. super.tearDown()
  356. }
  357. // MARK: -
  358. func testEmptyWithDefaultQueryAnchor() {
  359. settingsStore.executeSettingsQuery(fromQueryAnchor: queryAnchor, limit: limit) { result in
  360. switch result {
  361. case .failure(let error):
  362. XCTFail("Unexpected failure: \(error)")
  363. case .success(let anchor, let data):
  364. XCTAssertEqual(anchor.modificationCounter, 0)
  365. XCTAssertEqual(data.count, 0)
  366. }
  367. self.completion.fulfill()
  368. }
  369. wait(for: [completion], timeout: 2, enforceOrder: true)
  370. }
  371. func testEmptyWithMissingQueryAnchor() {
  372. queryAnchor = nil
  373. settingsStore.executeSettingsQuery(fromQueryAnchor: queryAnchor, limit: limit) { result in
  374. switch result {
  375. case .failure(let error):
  376. XCTFail("Unexpected failure: \(error)")
  377. case .success(let anchor, let data):
  378. XCTAssertEqual(anchor.modificationCounter, 0)
  379. XCTAssertEqual(data.count, 0)
  380. }
  381. self.completion.fulfill()
  382. }
  383. wait(for: [completion], timeout: 2, enforceOrder: true)
  384. }
  385. func testEmptyWithNonDefaultQueryAnchor() {
  386. queryAnchor.modificationCounter = 1
  387. settingsStore.executeSettingsQuery(fromQueryAnchor: queryAnchor, limit: limit) { result in
  388. switch result {
  389. case .failure(let error):
  390. XCTFail("Unexpected failure: \(error)")
  391. case .success(let anchor, let data):
  392. XCTAssertEqual(anchor.modificationCounter, 1)
  393. XCTAssertEqual(data.count, 0)
  394. }
  395. self.completion.fulfill()
  396. }
  397. wait(for: [completion], timeout: 2, enforceOrder: true)
  398. }
  399. func testDataWithUnusedQueryAnchor() {
  400. let syncIdentifiers = [generateSyncIdentifier(), generateSyncIdentifier(), generateSyncIdentifier()]
  401. addData(withSyncIdentifiers: syncIdentifiers)
  402. settingsStore.executeSettingsQuery(fromQueryAnchor: queryAnchor, limit: limit) { result in
  403. switch result {
  404. case .failure(let error):
  405. XCTFail("Unexpected failure: \(error)")
  406. case .success(let anchor, let data):
  407. XCTAssertEqual(anchor.modificationCounter, 3)
  408. XCTAssertEqual(data.count, 3)
  409. for (index, syncIdentifier) in syncIdentifiers.enumerated() {
  410. XCTAssertEqual(data[index].syncIdentifier, syncIdentifier)
  411. }
  412. }
  413. self.completion.fulfill()
  414. }
  415. wait(for: [completion], timeout: 2, enforceOrder: true)
  416. }
  417. func testDataWithStaleQueryAnchor() {
  418. let syncIdentifiers = [generateSyncIdentifier(), generateSyncIdentifier(), generateSyncIdentifier()]
  419. addData(withSyncIdentifiers: syncIdentifiers)
  420. queryAnchor.modificationCounter = 2
  421. settingsStore.executeSettingsQuery(fromQueryAnchor: queryAnchor, limit: limit) { result in
  422. switch result {
  423. case .failure(let error):
  424. XCTFail("Unexpected failure: \(error)")
  425. case .success(let anchor, let data):
  426. XCTAssertEqual(anchor.modificationCounter, 3)
  427. XCTAssertEqual(data.count, 1)
  428. XCTAssertEqual(data[0].syncIdentifier, syncIdentifiers[2])
  429. }
  430. self.completion.fulfill()
  431. }
  432. wait(for: [completion], timeout: 2, enforceOrder: true)
  433. }
  434. func testDataWithCurrentQueryAnchor() {
  435. let syncIdentifiers = [generateSyncIdentifier(), generateSyncIdentifier(), generateSyncIdentifier()]
  436. addData(withSyncIdentifiers: syncIdentifiers)
  437. queryAnchor.modificationCounter = 3
  438. settingsStore.executeSettingsQuery(fromQueryAnchor: queryAnchor, limit: limit) { result in
  439. switch result {
  440. case .failure(let error):
  441. XCTFail("Unexpected failure: \(error)")
  442. case .success(let anchor, let data):
  443. XCTAssertEqual(anchor.modificationCounter, 3)
  444. XCTAssertEqual(data.count, 0)
  445. }
  446. self.completion.fulfill()
  447. }
  448. wait(for: [completion], timeout: 2, enforceOrder: true)
  449. }
  450. func testDataWithLimitZero() {
  451. let syncIdentifiers = [generateSyncIdentifier(), generateSyncIdentifier(), generateSyncIdentifier()]
  452. addData(withSyncIdentifiers: syncIdentifiers)
  453. limit = 0
  454. settingsStore.executeSettingsQuery(fromQueryAnchor: queryAnchor, limit: limit) { result in
  455. switch result {
  456. case .failure(let error):
  457. XCTFail("Unexpected failure: \(error)")
  458. case .success(let anchor, let data):
  459. XCTAssertEqual(anchor.modificationCounter, 0)
  460. XCTAssertEqual(data.count, 0)
  461. }
  462. self.completion.fulfill()
  463. }
  464. wait(for: [completion], timeout: 2, enforceOrder: true)
  465. }
  466. func testDataWithLimitCoveredByData() {
  467. let syncIdentifiers = [generateSyncIdentifier(), generateSyncIdentifier(), generateSyncIdentifier()]
  468. addData(withSyncIdentifiers: syncIdentifiers)
  469. limit = 2
  470. settingsStore.executeSettingsQuery(fromQueryAnchor: queryAnchor, limit: limit) { result in
  471. switch result {
  472. case .failure(let error):
  473. XCTFail("Unexpected failure: \(error)")
  474. case .success(let anchor, let data):
  475. XCTAssertEqual(anchor.modificationCounter, 2)
  476. XCTAssertEqual(data.count, 2)
  477. XCTAssertEqual(data[0].syncIdentifier, syncIdentifiers[0])
  478. XCTAssertEqual(data[1].syncIdentifier, syncIdentifiers[1])
  479. }
  480. self.completion.fulfill()
  481. }
  482. wait(for: [completion], timeout: 2, enforceOrder: true)
  483. }
  484. private func addData(withSyncIdentifiers syncIdentifiers: [String]) {
  485. let semaphore = DispatchSemaphore(value: 0)
  486. for syncIdentifier in syncIdentifiers {
  487. self.settingsStore.storeSettings(StoredSettings(syncIdentifier: syncIdentifier)) { semaphore.signal() }
  488. }
  489. for _ in syncIdentifiers { semaphore.wait() }
  490. }
  491. private func generateSyncIdentifier() -> String {
  492. return UUID().uuidString
  493. }
  494. }
  495. class SettingsStoreCriticalEventLogTests: PersistenceControllerTestCase {
  496. var settingsStore: SettingsStore!
  497. var outputStream: MockOutputStream!
  498. var progress: Progress!
  499. override func setUp() {
  500. super.setUp()
  501. let settings = [StoredSettings(date: dateFormatter.date(from: "2100-01-02T03:08:00Z")!, syncIdentifier: "18CF3948-0B3D-4B12-8BFE-14986B0E6784"),
  502. StoredSettings(date: dateFormatter.date(from: "2100-01-02T03:10:00Z")!, syncIdentifier: "C86DEB61-68E9-464E-9DD5-96A9CB445FD3"),
  503. StoredSettings(date: dateFormatter.date(from: "2100-01-02T03:04:00Z")!, syncIdentifier: "2B03D96C-6F5D-4140-99CD-80C3E64D6010"),
  504. StoredSettings(date: dateFormatter.date(from: "2100-01-02T03:06:00Z")!, syncIdentifier: "FF1C4F01-3558-4FB2-957E-FA1522C4735E"),
  505. StoredSettings(date: dateFormatter.date(from: "2100-01-02T03:02:00Z")!, syncIdentifier: "71B699D7-0E8F-4B13-B7A1-E7751EB78E74")]
  506. settingsStore = SettingsStore(store: cacheStore, expireAfter: .hours(1))
  507. let dispatchGroup = DispatchGroup()
  508. dispatchGroup.enter()
  509. settingsStore.addStoredSettings(settings: settings) { error in
  510. XCTAssertNil(error)
  511. dispatchGroup.leave()
  512. }
  513. dispatchGroup.wait()
  514. outputStream = MockOutputStream()
  515. progress = Progress()
  516. }
  517. override func tearDown() {
  518. settingsStore = nil
  519. super.tearDown()
  520. }
  521. func testExportProgressTotalUnitCount() {
  522. switch settingsStore.exportProgressTotalUnitCount(startDate: dateFormatter.date(from: "2100-01-02T03:03:00Z")!,
  523. endDate: dateFormatter.date(from: "2100-01-02T03:09:00Z")!) {
  524. case .failure(let error):
  525. XCTFail("Unexpected failure: \(error)")
  526. case .success(let progressTotalUnitCount):
  527. XCTAssertEqual(progressTotalUnitCount, 3 * 11)
  528. }
  529. }
  530. func testExportProgressTotalUnitCountEmpty() {
  531. switch settingsStore.exportProgressTotalUnitCount(startDate: dateFormatter.date(from: "2100-01-02T03:00:00Z")!,
  532. endDate: dateFormatter.date(from: "2100-01-02T03:01:00Z")!) {
  533. case .failure(let error):
  534. XCTFail("Unexpected failure: \(error)")
  535. case .success(let progressTotalUnitCount):
  536. XCTAssertEqual(progressTotalUnitCount, 0)
  537. }
  538. }
  539. func testExport() {
  540. XCTAssertNil(settingsStore.export(startDate: dateFormatter.date(from: "2100-01-02T03:03:00Z")!,
  541. endDate: dateFormatter.date(from: "2100-01-02T03:09:00Z")!,
  542. to: outputStream,
  543. progress: progress))
  544. XCTAssertEqual(outputStream.string, """
  545. [
  546. {"data":{"date":"2100-01-02T03:08:00.000Z","dosingEnabled":false,"syncIdentifier":"18CF3948-0B3D-4B12-8BFE-14986B0E6784"},"date":"2100-01-02T03:08:00.000Z","modificationCounter":1},
  547. {"data":{"date":"2100-01-02T03:04:00.000Z","dosingEnabled":false,"syncIdentifier":"2B03D96C-6F5D-4140-99CD-80C3E64D6010"},"date":"2100-01-02T03:04:00.000Z","modificationCounter":3},
  548. {"data":{"date":"2100-01-02T03:06:00.000Z","dosingEnabled":false,"syncIdentifier":"FF1C4F01-3558-4FB2-957E-FA1522C4735E"},"date":"2100-01-02T03:06:00.000Z","modificationCounter":4}
  549. ]
  550. """
  551. )
  552. XCTAssertEqual(progress.completedUnitCount, 3 * 11)
  553. }
  554. func testExportEmpty() {
  555. XCTAssertNil(settingsStore.export(startDate: dateFormatter.date(from: "2100-01-02T03:00:00Z")!,
  556. endDate: dateFormatter.date(from: "2100-01-02T03:01:00Z")!,
  557. to: outputStream,
  558. progress: progress))
  559. XCTAssertEqual(outputStream.string, "[]")
  560. XCTAssertEqual(progress.completedUnitCount, 0)
  561. }
  562. func testExportCancelled() {
  563. progress.cancel()
  564. XCTAssertEqual(settingsStore.export(startDate: dateFormatter.date(from: "2100-01-02T03:03:00Z")!,
  565. endDate: dateFormatter.date(from: "2100-01-02T03:09:00Z")!,
  566. to: outputStream,
  567. progress: progress) as? CriticalEventLogError, CriticalEventLogError.cancelled)
  568. }
  569. private let dateFormatter = ISO8601DateFormatter()
  570. }
  571. class StoredSettingsCodableTests: XCTestCase {
  572. func testStoredSettingsCodable() throws {
  573. try assertStoredSettingsCodable(StoredSettings.test, encodesJSON: """
  574. {
  575. "basalRateSchedule" : {
  576. "items" : [
  577. {
  578. "startTime" : 0,
  579. "value" : 1
  580. },
  581. {
  582. "startTime" : 21600,
  583. "value" : 1.5
  584. },
  585. {
  586. "startTime" : 64800,
  587. "value" : 1.25
  588. }
  589. ],
  590. "referenceTimeInterval" : 0,
  591. "repeatInterval" : 86400,
  592. "timeZone" : {
  593. "identifier" : "America/Los_Angeles"
  594. }
  595. },
  596. "bloodGlucoseUnit" : "mg/dL",
  597. "carbRatioSchedule" : {
  598. "unit" : "g",
  599. "valueSchedule" : {
  600. "items" : [
  601. {
  602. "startTime" : 0,
  603. "value" : 15
  604. },
  605. {
  606. "startTime" : 32400,
  607. "value" : 14
  608. },
  609. {
  610. "startTime" : 72000,
  611. "value" : 18
  612. }
  613. ],
  614. "referenceTimeInterval" : 0,
  615. "repeatInterval" : 86400,
  616. "timeZone" : {
  617. "identifier" : "America/Los_Angeles"
  618. }
  619. }
  620. },
  621. "date" : "2020-05-14T22:48:15Z",
  622. "deviceToken" : "DeviceTokenString",
  623. "dosingEnabled" : true,
  624. "glucoseTargetRangeSchedule" : {
  625. "override" : {
  626. "end" : "2020-05-14T14:48:15Z",
  627. "start" : "2020-05-14T12:48:15Z",
  628. "value" : {
  629. "maxValue" : 115,
  630. "minValue" : 105
  631. }
  632. },
  633. "rangeSchedule" : {
  634. "unit" : "mg/dL",
  635. "valueSchedule" : {
  636. "items" : [
  637. {
  638. "startTime" : 0,
  639. "value" : {
  640. "maxValue" : 110,
  641. "minValue" : 100
  642. }
  643. },
  644. {
  645. "startTime" : 25200,
  646. "value" : {
  647. "maxValue" : 100,
  648. "minValue" : 90
  649. }
  650. },
  651. {
  652. "startTime" : 75600,
  653. "value" : {
  654. "maxValue" : 120,
  655. "minValue" : 110
  656. }
  657. }
  658. ],
  659. "referenceTimeInterval" : 0,
  660. "repeatInterval" : 86400,
  661. "timeZone" : {
  662. "identifier" : "America/Los_Angeles"
  663. }
  664. }
  665. }
  666. },
  667. "insulinModel" : {
  668. "actionDuration" : 21600,
  669. "modelType" : "rapidAdult",
  670. "peakActivity" : 10800
  671. },
  672. "insulinSensitivitySchedule" : {
  673. "unit" : "mg/dL",
  674. "valueSchedule" : {
  675. "items" : [
  676. {
  677. "startTime" : 0,
  678. "value" : 45
  679. },
  680. {
  681. "startTime" : 10800,
  682. "value" : 40
  683. },
  684. {
  685. "startTime" : 54000,
  686. "value" : 50
  687. }
  688. ],
  689. "referenceTimeInterval" : 0,
  690. "repeatInterval" : 86400,
  691. "timeZone" : {
  692. "identifier" : "America/Los_Angeles"
  693. }
  694. }
  695. },
  696. "maximumBasalRatePerHour" : 3.5,
  697. "maximumBolus" : 10,
  698. "overridePresets" : [
  699. {
  700. "duration" : {
  701. "finite" : {
  702. "duration" : 3600
  703. }
  704. },
  705. "id" : "2A67A303-5203-4CB8-8263-79498265368E",
  706. "name" : "Apple",
  707. "settings" : {
  708. "insulinNeedsScaleFactor" : 2,
  709. "targetRangeInMgdl" : {
  710. "maxValue" : 140,
  711. "minValue" : 130
  712. }
  713. },
  714. "symbol" : "🍎"
  715. }
  716. ],
  717. "preMealOverride" : {
  718. "actualEnd" : {
  719. "type" : "natural"
  720. },
  721. "context" : "preMeal",
  722. "duration" : "indefinite",
  723. "enactTrigger" : "local",
  724. "settings" : {
  725. "insulinNeedsScaleFactor" : 0.5,
  726. "targetRangeInMgdl" : {
  727. "maxValue" : 90,
  728. "minValue" : 80
  729. }
  730. },
  731. "startDate" : "2020-05-14T14:38:39Z",
  732. "syncIdentifier" : "2A67A303-5203-1234-8263-79498265368E"
  733. },
  734. "preMealTargetRange" : {
  735. "maxValue" : 90,
  736. "minValue" : 80
  737. },
  738. "scheduleOverride" : {
  739. "actualEnd" : {
  740. "type" : "natural"
  741. },
  742. "context" : "preMeal",
  743. "duration" : {
  744. "finite" : {
  745. "duration" : 3600
  746. }
  747. },
  748. "enactTrigger" : {
  749. "remote" : {
  750. "address" : "127.0.0.1"
  751. }
  752. },
  753. "settings" : {
  754. "insulinNeedsScaleFactor" : 1.5,
  755. "targetRangeInMgdl" : {
  756. "maxValue" : 120,
  757. "minValue" : 110
  758. }
  759. },
  760. "startDate" : "2020-05-14T14:48:19Z",
  761. "syncIdentifier" : "2A67A303-1234-4CB8-8263-79498265368E"
  762. },
  763. "suspendThreshold" : {
  764. "unit" : "mg/dL",
  765. "value" : 75
  766. },
  767. "syncIdentifier" : "2A67A303-1234-4CB8-1234-79498265368E",
  768. "workoutTargetRange" : {
  769. "maxValue" : 160,
  770. "minValue" : 150
  771. }
  772. }
  773. """
  774. )
  775. }
  776. private func assertStoredSettingsCodable(_ original: StoredSettings, encodesJSON string: String) throws {
  777. let data = try encoder.encode(original)
  778. XCTAssertEqual(String(data: data, encoding: .utf8), string)
  779. let decoded = try decoder.decode(StoredSettings.self, from: data)
  780. XCTAssertEqual(decoded, original)
  781. }
  782. private let dateFormatter = ISO8601DateFormatter()
  783. private let encoder: JSONEncoder = {
  784. let encoder = JSONEncoder()
  785. encoder.outputFormatting = [.prettyPrinted, .sortedKeys, .withoutEscapingSlashes]
  786. encoder.dateEncodingStrategy = .iso8601
  787. return encoder
  788. }()
  789. private let decoder: JSONDecoder = {
  790. let decoder = JSONDecoder()
  791. decoder.dateDecodingStrategy = .iso8601
  792. return decoder
  793. }()
  794. }
  795. extension StoredSettings: Equatable {
  796. public static func == (lhs: StoredSettings, rhs: StoredSettings) -> Bool {
  797. return lhs.date == rhs.date &&
  798. lhs.dosingEnabled == rhs.dosingEnabled &&
  799. lhs.glucoseTargetRangeSchedule == rhs.glucoseTargetRangeSchedule &&
  800. lhs.preMealTargetRange == rhs.preMealTargetRange &&
  801. lhs.workoutTargetRange == rhs.workoutTargetRange &&
  802. lhs.overridePresets == rhs.overridePresets &&
  803. lhs.scheduleOverride == rhs.scheduleOverride &&
  804. lhs.preMealOverride == rhs.preMealOverride &&
  805. lhs.maximumBasalRatePerHour == rhs.maximumBasalRatePerHour &&
  806. lhs.maximumBolus == rhs.maximumBolus &&
  807. lhs.suspendThreshold == rhs.suspendThreshold &&
  808. lhs.deviceToken == rhs.deviceToken &&
  809. lhs.insulinModel == rhs.insulinModel &&
  810. lhs.basalRateSchedule == rhs.basalRateSchedule &&
  811. lhs.insulinSensitivitySchedule == rhs.insulinSensitivitySchedule &&
  812. lhs.carbRatioSchedule == rhs.carbRatioSchedule &&
  813. lhs.bloodGlucoseUnit == rhs.bloodGlucoseUnit &&
  814. lhs.syncIdentifier == rhs.syncIdentifier
  815. }
  816. }
  817. fileprivate extension StoredSettings {
  818. static var test: StoredSettings {
  819. return StoredSettings(date: dateFormatter.date(from: "2020-05-14T22:48:15Z")!,
  820. dosingEnabled: true,
  821. glucoseTargetRangeSchedule: GlucoseRangeSchedule(rangeSchedule: DailyQuantitySchedule(unit: .milligramsPerDeciliter,
  822. dailyItems: [RepeatingScheduleValue(startTime: .hours(0), value: DoubleRange(minValue: 100.0, maxValue: 110.0)),
  823. RepeatingScheduleValue(startTime: .hours(7), value: DoubleRange(minValue: 90.0, maxValue: 100.0)),
  824. RepeatingScheduleValue(startTime: .hours(21), value: DoubleRange(minValue: 110.0, maxValue: 120.0))],
  825. timeZone: TimeZone(identifier: "America/Los_Angeles")!)!,
  826. override: GlucoseRangeSchedule.Override(value: DoubleRange(minValue: 105.0, maxValue: 115.0),
  827. start: dateFormatter.date(from: "2020-05-14T12:48:15Z")!,
  828. end: dateFormatter.date(from: "2020-05-14T14:48:15Z")!)),
  829. preMealTargetRange: DoubleRange(minValue: 80.0, maxValue: 90.0),
  830. workoutTargetRange: DoubleRange(minValue: 150.0, maxValue: 160.0),
  831. overridePresets: [TemporaryScheduleOverridePreset(id: UUID(uuidString: "2A67A303-5203-4CB8-8263-79498265368E")!,
  832. symbol: "🍎",
  833. name: "Apple",
  834. settings: TemporaryScheduleOverrideSettings(unit: .milligramsPerDeciliter,
  835. targetRange: DoubleRange(minValue: 130.0, maxValue: 140.0),
  836. insulinNeedsScaleFactor: 2.0),
  837. duration: .finite(.minutes(60)))],
  838. scheduleOverride: TemporaryScheduleOverride(context: .preMeal,
  839. settings: TemporaryScheduleOverrideSettings(unit: .milligramsPerDeciliter,
  840. targetRange: DoubleRange(minValue: 110.0, maxValue: 120.0),
  841. insulinNeedsScaleFactor: 1.5),
  842. startDate: dateFormatter.date(from: "2020-05-14T14:48:19Z")!,
  843. duration: .finite(.minutes(60)),
  844. enactTrigger: .remote("127.0.0.1"),
  845. syncIdentifier: UUID(uuidString: "2A67A303-1234-4CB8-8263-79498265368E")!),
  846. preMealOverride: TemporaryScheduleOverride(context: .preMeal,
  847. settings: TemporaryScheduleOverrideSettings(unit: .milligramsPerDeciliter,
  848. targetRange: DoubleRange(minValue: 80.0, maxValue: 90.0),
  849. insulinNeedsScaleFactor: 0.5),
  850. startDate: dateFormatter.date(from: "2020-05-14T14:38:39Z")!,
  851. duration: .indefinite,
  852. enactTrigger: .local,
  853. syncIdentifier: UUID(uuidString: "2A67A303-5203-1234-8263-79498265368E")!),
  854. maximumBasalRatePerHour: 3.5,
  855. maximumBolus: 10.0,
  856. suspendThreshold: GlucoseThreshold(unit: .milligramsPerDeciliter, value: 75.0),
  857. deviceToken: "DeviceTokenString",
  858. insulinModel: StoredInsulinModel(modelType: .rapidAdult, actionDuration: .hours(6), peakActivity: .hours(3)),
  859. basalRateSchedule: BasalRateSchedule(dailyItems: [RepeatingScheduleValue(startTime: .hours(0), value: 1.0),
  860. RepeatingScheduleValue(startTime: .hours(6), value: 1.5),
  861. RepeatingScheduleValue(startTime: .hours(18), value: 1.25)],
  862. timeZone: TimeZone(identifier: "America/Los_Angeles")!),
  863. insulinSensitivitySchedule: InsulinSensitivitySchedule(unit: .milligramsPerDeciliter,
  864. dailyItems: [RepeatingScheduleValue(startTime: .hours(0), value: 45.0),
  865. RepeatingScheduleValue(startTime: .hours(3), value: 40.0),
  866. RepeatingScheduleValue(startTime: .hours(15), value: 50.0)],
  867. timeZone: TimeZone(identifier: "America/Los_Angeles")!),
  868. carbRatioSchedule: CarbRatioSchedule(unit: .gram(),
  869. dailyItems: [RepeatingScheduleValue(startTime: .hours(0), value: 15.0),
  870. RepeatingScheduleValue(startTime: .hours(9), value: 14.0),
  871. RepeatingScheduleValue(startTime: .hours(20), value: 18.0)],
  872. timeZone: TimeZone(identifier: "America/Los_Angeles")!),
  873. bloodGlucoseUnit: .milligramsPerDeciliter,
  874. syncIdentifier: "2A67A303-1234-4CB8-1234-79498265368E")
  875. }
  876. private static let dateFormatter = ISO8601DateFormatter()
  877. }