GlucoseStoreTests.swift 18 KB

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