GlucoseStoreTests.swift 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423
  1. //
  2. // GlucoseStoreTests.swift
  3. // LoopKitTests
  4. //
  5. // Created by Darin Krauss on 12/30/19.
  6. // Copyright © 2020 LoopKit Authors. All rights reserved.
  7. //
  8. import XCTest
  9. import HealthKit
  10. import CoreData
  11. @testable import LoopKit
  12. class GlucoseStoreTests: PersistenceControllerTestCase {
  13. var healthStore: HKHealthStoreMock!
  14. var glucoseStore: GlucoseStore!
  15. override func setUp() {
  16. super.setUp()
  17. healthStore = HKHealthStoreMock()
  18. glucoseStore = GlucoseStore(cacheStore: cacheStore,
  19. provenanceIdentifier: Bundle.main.bundleIdentifier!)
  20. let semaphore = DispatchSemaphore(value: 0)
  21. cacheStore.onReady { (error) in
  22. semaphore.signal()
  23. }
  24. semaphore.wait()
  25. }
  26. override func tearDown() {
  27. glucoseStore = nil
  28. healthStore = nil
  29. super.tearDown()
  30. }
  31. func testLatestGlucoseIsSetAfterStoreAndClearedAfterPurge() {
  32. let storeCompletion = expectation(description: "Storage completion")
  33. let storedQuantity = HKQuantity(unit: .milligramsPerDeciliter, doubleValue: 80)
  34. let device = HKDevice(name: "Unit Test Mock CGM",
  35. manufacturer: "Device Manufacturer",
  36. model: "Device Model",
  37. hardwareVersion: "Device Hardware Version",
  38. firmwareVersion: "Device Firmware Version",
  39. softwareVersion: "Device Software Version",
  40. localIdentifier: "Device Local Identifier",
  41. udiDeviceIdentifier: "Device UDI Device Identifier")
  42. let sample = NewGlucoseSample(date: Date(), quantity: storedQuantity, condition: nil, trend: nil, trendRate: nil, isDisplayOnly: false, wasUserEntered: false, syncIdentifier: "random", device: device)
  43. glucoseStore.addGlucoseSamples([sample]) { (result) in
  44. switch result {
  45. case .failure(let error):
  46. XCTFail("Unexpected failure: \(error)")
  47. case .success(let samples):
  48. XCTAssertEqual(storedQuantity, samples.first!.quantity)
  49. }
  50. storeCompletion.fulfill()
  51. }
  52. wait(for: [storeCompletion], timeout: 2)
  53. XCTAssertEqual(storedQuantity, self.glucoseStore.latestGlucose?.quantity)
  54. let purgeCompletion = expectation(description: "Storage completion")
  55. let predicate = HKQuery.predicateForObjects(from: [device])
  56. glucoseStore.purgeAllGlucoseSamples(healthKitPredicate: predicate) { (error) in
  57. if let error = error {
  58. XCTFail("Unexpected failure: \(error)")
  59. }
  60. purgeCompletion.fulfill()
  61. }
  62. wait(for: [purgeCompletion], timeout: 2)
  63. XCTAssertNil(self.glucoseStore.latestGlucose)
  64. }
  65. }
  66. class GlucoseStoreRemoteDataServiceQueryAnchorTests: XCTestCase {
  67. var rawValue: GlucoseStore.QueryAnchor.RawValue = [
  68. "modificationCounter": Int64(123)
  69. ]
  70. func testInitializerDefault() {
  71. let queryAnchor = GlucoseStore.QueryAnchor()
  72. XCTAssertEqual(queryAnchor.modificationCounter, 0)
  73. }
  74. func testInitializerRawValue() {
  75. let queryAnchor = GlucoseStore.QueryAnchor(rawValue: rawValue)
  76. XCTAssertNotNil(queryAnchor)
  77. XCTAssertEqual(queryAnchor?.modificationCounter, 123)
  78. }
  79. func testInitializerRawValueMissingModificationCounter() {
  80. rawValue["modificationCounter"] = nil
  81. XCTAssertNil(GlucoseStore.QueryAnchor(rawValue: rawValue))
  82. }
  83. func testInitializerRawValueInvalidModificationCounter() {
  84. rawValue["modificationCounter"] = "123"
  85. XCTAssertNil(GlucoseStore.QueryAnchor(rawValue: rawValue))
  86. }
  87. func testRawValueWithDefault() {
  88. let rawValue = GlucoseStore.QueryAnchor().rawValue
  89. XCTAssertEqual(rawValue.count, 1)
  90. XCTAssertEqual(rawValue["modificationCounter"] as? Int64, Int64(0))
  91. }
  92. func testRawValueWithNonDefault() {
  93. var queryAnchor = GlucoseStore.QueryAnchor()
  94. queryAnchor.modificationCounter = 123
  95. let rawValue = queryAnchor.rawValue
  96. XCTAssertEqual(rawValue.count, 1)
  97. XCTAssertEqual(rawValue["modificationCounter"] as? Int64, Int64(123))
  98. }
  99. }
  100. class GlucoseStoreRemoteDataServiceQueryTests: PersistenceControllerTestCase {
  101. var healthStore: HKHealthStoreMock!
  102. var glucoseStore: GlucoseStore!
  103. var completion: XCTestExpectation!
  104. var queryAnchor: GlucoseStore.QueryAnchor!
  105. var limit: Int!
  106. override func setUp() {
  107. super.setUp()
  108. glucoseStore = GlucoseStore(cacheStore: cacheStore,
  109. provenanceIdentifier: Bundle.main.bundleIdentifier!)
  110. let semaphore = DispatchSemaphore(value: 0)
  111. cacheStore.onReady { (error) in
  112. semaphore.signal()
  113. }
  114. semaphore.wait()
  115. completion = expectation(description: "Completion")
  116. queryAnchor = GlucoseStore.QueryAnchor()
  117. limit = Int.max
  118. }
  119. override func tearDown() {
  120. limit = nil
  121. queryAnchor = nil
  122. completion = nil
  123. glucoseStore = nil
  124. healthStore = nil
  125. super.tearDown()
  126. }
  127. func testEmptyWithDefaultQueryAnchor() {
  128. glucoseStore.executeGlucoseQuery(fromQueryAnchor: queryAnchor, limit: limit) { result in
  129. switch result {
  130. case .failure(let error):
  131. XCTFail("Unexpected failure: \(error)")
  132. case .success(let anchor, let data):
  133. XCTAssertEqual(anchor.modificationCounter, 0)
  134. XCTAssertEqual(data.count, 0)
  135. }
  136. self.completion.fulfill()
  137. }
  138. wait(for: [completion], timeout: 2, enforceOrder: true)
  139. }
  140. func testEmptyWithMissingQueryAnchor() {
  141. queryAnchor = nil
  142. glucoseStore.executeGlucoseQuery(fromQueryAnchor: queryAnchor, limit: limit) { result in
  143. switch result {
  144. case .failure(let error):
  145. XCTFail("Unexpected failure: \(error)")
  146. case .success(let anchor, let data):
  147. XCTAssertEqual(anchor.modificationCounter, 0)
  148. XCTAssertEqual(data.count, 0)
  149. }
  150. self.completion.fulfill()
  151. }
  152. wait(for: [completion], timeout: 2, enforceOrder: true)
  153. }
  154. func testEmptyWithNonDefaultQueryAnchor() {
  155. queryAnchor.modificationCounter = 1
  156. glucoseStore.executeGlucoseQuery(fromQueryAnchor: queryAnchor, limit: limit) { result in
  157. switch result {
  158. case .failure(let error):
  159. XCTFail("Unexpected failure: \(error)")
  160. case .success(let anchor, let data):
  161. XCTAssertEqual(anchor.modificationCounter, 1)
  162. XCTAssertEqual(data.count, 0)
  163. }
  164. self.completion.fulfill()
  165. }
  166. wait(for: [completion], timeout: 2, enforceOrder: true)
  167. }
  168. func testDataWithUnusedQueryAnchor() {
  169. let syncIdentifiers = [generateSyncIdentifier(), generateSyncIdentifier(), generateSyncIdentifier()]
  170. addData(withSyncIdentifiers: syncIdentifiers)
  171. glucoseStore.executeGlucoseQuery(fromQueryAnchor: queryAnchor, limit: limit) { result in
  172. switch result {
  173. case .failure(let error):
  174. XCTFail("Unexpected failure: \(error)")
  175. case .success(let anchor, let data):
  176. XCTAssertEqual(anchor.modificationCounter, 3)
  177. XCTAssertEqual(data.count, 3)
  178. for (index, syncIdentifier) in syncIdentifiers.enumerated() {
  179. XCTAssertEqual(data[index].syncIdentifier, syncIdentifier)
  180. XCTAssertEqual(data[index].syncVersion, index)
  181. }
  182. }
  183. self.completion.fulfill()
  184. }
  185. wait(for: [completion], timeout: 2, enforceOrder: true)
  186. }
  187. func testDataWithStaleQueryAnchor() {
  188. let syncIdentifiers = [generateSyncIdentifier(), generateSyncIdentifier(), generateSyncIdentifier()]
  189. addData(withSyncIdentifiers: syncIdentifiers)
  190. queryAnchor.modificationCounter = 2
  191. glucoseStore.executeGlucoseQuery(fromQueryAnchor: queryAnchor, limit: limit) { result in
  192. switch result {
  193. case .failure(let error):
  194. XCTFail("Unexpected failure: \(error)")
  195. case .success(let anchor, let data):
  196. XCTAssertEqual(anchor.modificationCounter, 3)
  197. XCTAssertEqual(data.count, 1)
  198. XCTAssertEqual(data[0].syncIdentifier, syncIdentifiers[2])
  199. XCTAssertEqual(data[0].syncVersion, 2)
  200. }
  201. self.completion.fulfill()
  202. }
  203. wait(for: [completion], timeout: 2, enforceOrder: true)
  204. }
  205. func testDataWithCurrentQueryAnchor() {
  206. let syncIdentifiers = [generateSyncIdentifier(), generateSyncIdentifier(), generateSyncIdentifier()]
  207. addData(withSyncIdentifiers: syncIdentifiers)
  208. queryAnchor.modificationCounter = 3
  209. glucoseStore.executeGlucoseQuery(fromQueryAnchor: queryAnchor, limit: limit) { result in
  210. switch result {
  211. case .failure(let error):
  212. XCTFail("Unexpected failure: \(error)")
  213. case .success(let anchor, let data):
  214. XCTAssertEqual(anchor.modificationCounter, 3)
  215. XCTAssertEqual(data.count, 0)
  216. }
  217. self.completion.fulfill()
  218. }
  219. wait(for: [completion], timeout: 2, enforceOrder: true)
  220. }
  221. func testDataWithLimitZero() {
  222. let syncIdentifiers = [generateSyncIdentifier(), generateSyncIdentifier(), generateSyncIdentifier()]
  223. addData(withSyncIdentifiers: syncIdentifiers)
  224. limit = 0
  225. glucoseStore.executeGlucoseQuery(fromQueryAnchor: queryAnchor, limit: limit) { result in
  226. switch result {
  227. case .failure(let error):
  228. XCTFail("Unexpected failure: \(error)")
  229. case .success(let anchor, let data):
  230. XCTAssertEqual(anchor.modificationCounter, 0)
  231. XCTAssertEqual(data.count, 0)
  232. }
  233. self.completion.fulfill()
  234. }
  235. wait(for: [completion], timeout: 2, enforceOrder: true)
  236. }
  237. func testDataWithLimitCoveredByData() {
  238. let syncIdentifiers = [generateSyncIdentifier(), generateSyncIdentifier(), generateSyncIdentifier()]
  239. addData(withSyncIdentifiers: syncIdentifiers)
  240. limit = 2
  241. glucoseStore.executeGlucoseQuery(fromQueryAnchor: queryAnchor, limit: limit) { result in
  242. switch result {
  243. case .failure(let error):
  244. XCTFail("Unexpected failure: \(error)")
  245. case .success(let anchor, let data):
  246. XCTAssertEqual(anchor.modificationCounter, 2)
  247. XCTAssertEqual(data.count, 2)
  248. XCTAssertEqual(data[0].syncIdentifier, syncIdentifiers[0])
  249. XCTAssertEqual(data[0].syncVersion, 0)
  250. XCTAssertEqual(data[1].syncIdentifier, syncIdentifiers[1])
  251. XCTAssertEqual(data[1].syncVersion, 1)
  252. }
  253. self.completion.fulfill()
  254. }
  255. wait(for: [completion], timeout: 2, enforceOrder: true)
  256. }
  257. private func addData(withSyncIdentifiers syncIdentifiers: [String]) {
  258. cacheStore.managedObjectContext.performAndWait {
  259. for (index, syncIdentifier) in syncIdentifiers.enumerated() {
  260. let cachedGlucoseObject = CachedGlucoseObject(context: self.cacheStore.managedObjectContext)
  261. cachedGlucoseObject.uuid = UUID()
  262. cachedGlucoseObject.provenanceIdentifier = syncIdentifier
  263. cachedGlucoseObject.syncIdentifier = syncIdentifier
  264. cachedGlucoseObject.syncVersion = index
  265. cachedGlucoseObject.value = 123
  266. cachedGlucoseObject.unitString = HKUnit.milligramsPerDeciliter.unitString
  267. cachedGlucoseObject.startDate = Date()
  268. self.cacheStore.save()
  269. }
  270. }
  271. }
  272. private func generateSyncIdentifier() -> String {
  273. return UUID().uuidString
  274. }
  275. }
  276. class GlucoseStoreCriticalEventLogExportTests: PersistenceControllerTestCase {
  277. var glucoseStore: GlucoseStore!
  278. var outputStream: MockOutputStream!
  279. var progress: Progress!
  280. override func setUp() {
  281. super.setUp()
  282. let samples = [NewGlucoseSample(date: dateFormatter.date(from: "2100-01-02T03:08:00Z")!, quantity: HKQuantity(unit: .milligramsPerDeciliter, doubleValue: 111), condition: nil, trend: nil, trendRate: nil, isDisplayOnly: false, wasUserEntered: false, syncIdentifier: "18CF3948-0B3D-4B12-8BFE-14986B0E6784", syncVersion: 1),
  283. NewGlucoseSample(date: dateFormatter.date(from: "2100-01-02T03:10:00Z")!, quantity: HKQuantity(unit: .milligramsPerDeciliter, doubleValue: 112), condition: nil, trend: .up, trendRate: HKQuantity(unit: .milligramsPerDeciliterPerMinute, doubleValue: 1.0), isDisplayOnly: false, wasUserEntered: false, syncIdentifier: "C86DEB61-68E9-464E-9DD5-96A9CB445FD3", syncVersion: 2),
  284. NewGlucoseSample(date: dateFormatter.date(from: "2100-01-02T03:04:00Z")!, quantity: HKQuantity(unit: .milligramsPerDeciliter, doubleValue: 113), condition: nil, trend: .up, trendRate: HKQuantity(unit: .milligramsPerDeciliterPerMinute, doubleValue: 1.0), isDisplayOnly: false, wasUserEntered: false, syncIdentifier: "2B03D96C-6F5D-4140-99CD-80C3E64D6010", syncVersion: 3),
  285. NewGlucoseSample(date: dateFormatter.date(from: "2100-01-02T03:06:00Z")!, quantity: HKQuantity(unit: .milligramsPerDeciliter, doubleValue: 114), condition: nil, trend: .up, trendRate: HKQuantity(unit: .milligramsPerDeciliterPerMinute, doubleValue: 1.0), isDisplayOnly: false, wasUserEntered: false, syncIdentifier: "FF1C4F01-3558-4FB2-957E-FA1522C4735E", syncVersion: 4),
  286. NewGlucoseSample(date: dateFormatter.date(from: "2100-01-02T03:02:00Z")!, quantity: HKQuantity(unit: .milligramsPerDeciliter, doubleValue: 400), condition: .aboveRange, trend: .upUpUp, trendRate: HKQuantity(unit: .milligramsPerDeciliterPerMinute, doubleValue: 1.0), isDisplayOnly: false, wasUserEntered: false, syncIdentifier: "71B699D7-0E8F-4B13-B7A1-E7751EB78E74", syncVersion: 5)]
  287. glucoseStore = GlucoseStore(cacheStore: cacheStore,
  288. provenanceIdentifier: Bundle.main.bundleIdentifier!)
  289. let dispatchGroup = DispatchGroup()
  290. dispatchGroup.enter()
  291. glucoseStore.addNewGlucoseSamples(samples: samples) { error in
  292. XCTAssertNil(error)
  293. dispatchGroup.leave()
  294. }
  295. dispatchGroup.wait()
  296. outputStream = MockOutputStream()
  297. progress = Progress()
  298. }
  299. override func tearDown() {
  300. glucoseStore = nil
  301. super.tearDown()
  302. }
  303. func testExportProgressTotalUnitCount() {
  304. switch glucoseStore.exportProgressTotalUnitCount(startDate: dateFormatter.date(from: "2100-01-02T03:03:00Z")!,
  305. endDate: dateFormatter.date(from: "2100-01-02T03:09:00Z")!) {
  306. case .failure(let error):
  307. XCTFail("Unexpected failure: \(error)")
  308. case .success(let progressTotalUnitCount):
  309. XCTAssertEqual(progressTotalUnitCount, 3 * 1)
  310. }
  311. }
  312. func testExportProgressTotalUnitCountEmpty() {
  313. switch glucoseStore.exportProgressTotalUnitCount(startDate: dateFormatter.date(from: "2100-01-02T03:00:00Z")!,
  314. endDate: dateFormatter.date(from: "2100-01-02T03:01:00Z")!) {
  315. case .failure(let error):
  316. XCTFail("Unexpected failure: \(error)")
  317. case .success(let progressTotalUnitCount):
  318. XCTAssertEqual(progressTotalUnitCount, 0)
  319. }
  320. }
  321. func testExport() {
  322. XCTAssertNil(glucoseStore.export(startDate: dateFormatter.date(from: "2100-01-02T03:03:00Z")!,
  323. endDate: dateFormatter.date(from: "2100-01-02T03:09:00Z")!,
  324. to: outputStream,
  325. progress: progress))
  326. XCTAssertEqual(outputStream.string, """
  327. [
  328. {"isDisplayOnly":false,"modificationCounter":1,"provenanceIdentifier":"com.apple.dt.xctest.tool","startDate":"2100-01-02T03:08:00.000Z","syncIdentifier":"18CF3948-0B3D-4B12-8BFE-14986B0E6784","syncVersion":1,"unitString":"mg/dL","value":111,"wasUserEntered":false},
  329. {"isDisplayOnly":false,"modificationCounter":3,"provenanceIdentifier":"com.apple.dt.xctest.tool","startDate":"2100-01-02T03:04:00.000Z","syncIdentifier":"2B03D96C-6F5D-4140-99CD-80C3E64D6010","syncVersion":3,"trend":3,"trendRateValue":1,"unitString":"mg/dL","value":113,"wasUserEntered":false},
  330. {"isDisplayOnly":false,"modificationCounter":4,"provenanceIdentifier":"com.apple.dt.xctest.tool","startDate":"2100-01-02T03:06:00.000Z","syncIdentifier":"FF1C4F01-3558-4FB2-957E-FA1522C4735E","syncVersion":4,"trend":3,"trendRateValue":1,"unitString":"mg/dL","value":114,"wasUserEntered":false}
  331. ]
  332. """
  333. )
  334. XCTAssertEqual(progress.completedUnitCount, 3 * 1)
  335. }
  336. func testExportEmpty() {
  337. XCTAssertNil(glucoseStore.export(startDate: dateFormatter.date(from: "2100-01-02T03:00:00Z")!,
  338. endDate: dateFormatter.date(from: "2100-01-02T03:01:00Z")!,
  339. to: outputStream,
  340. progress: progress))
  341. XCTAssertEqual(outputStream.string, "[]")
  342. XCTAssertEqual(progress.completedUnitCount, 0)
  343. }
  344. func testExportCancelled() {
  345. progress.cancel()
  346. XCTAssertEqual(glucoseStore.export(startDate: dateFormatter.date(from: "2100-01-02T03:03:00Z")!,
  347. endDate: dateFormatter.date(from: "2100-01-02T03:09:00Z")!,
  348. to: outputStream,
  349. progress: progress) as? CriticalEventLogError, CriticalEventLogError.cancelled)
  350. }
  351. private let dateFormatter = ISO8601DateFormatter()
  352. }