CarbStoreTests.swift 72 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347
  1. //
  2. // CarbStoreTests.swift
  3. // LoopKitTests
  4. //
  5. // Copyright © 2018 LoopKit Authors. All rights reserved.
  6. //
  7. import XCTest
  8. import HealthKit
  9. import CoreData
  10. @testable import LoopKit
  11. class CarbStorePersistenceTests: PersistenceControllerTestCase, CarbStoreDelegate {
  12. var healthStore: HKHealthStoreMock!
  13. var carbStore: CarbStore!
  14. override func setUp() {
  15. super.setUp()
  16. healthStore = HKHealthStoreMock()
  17. carbStore = CarbStore(
  18. healthStore: healthStore,
  19. cacheStore: cacheStore,
  20. cacheLength: .hours(24),
  21. defaultAbsorptionTimes: (fast: .minutes(30), medium: .hours(3), slow: .hours(5)),
  22. observationInterval: 0,
  23. provenanceIdentifier: Bundle.main.bundleIdentifier!)
  24. carbStore.testQueryStore = healthStore
  25. carbStore.delegate = self
  26. let semaphore = DispatchSemaphore(value: 0)
  27. cacheStore.onReady { (error) in
  28. semaphore.signal()
  29. }
  30. semaphore.wait()
  31. }
  32. override func tearDown() {
  33. carbStore.delegate = nil
  34. carbStore = nil
  35. healthStore = nil
  36. carbStoreHasUpdatedCarbDataHandler = nil
  37. super.tearDown()
  38. }
  39. // MARK: - CarbStoreDelegate
  40. var carbStoreHasUpdatedCarbDataHandler: ((_ : CarbStore) -> Void)?
  41. func carbStoreHasUpdatedCarbData(_ carbStore: CarbStore) {
  42. carbStoreHasUpdatedCarbDataHandler?(carbStore)
  43. }
  44. func carbStore(_ carbStore: CarbStore, didError error: CarbStore.CarbStoreError) {}
  45. // MARK: -
  46. func testGetCarbEntriesAfterAdd() {
  47. let firstCarbEntry = NewCarbEntry(quantity: HKQuantity(unit: .gram(), doubleValue: 10), startDate: Date(timeIntervalSinceNow: -1), foodType: "First", absorptionTime: .hours(5))
  48. let secondCarbEntry = NewCarbEntry(quantity: HKQuantity(unit: .gram(), doubleValue: 20), startDate: Date(), foodType: "Second", absorptionTime: .hours(3))
  49. let thirdCarbEntry = NewCarbEntry(quantity: HKQuantity(unit: .gram(), doubleValue: 30), startDate: Date(timeIntervalSinceNow: 1), foodType: "Third", absorptionTime: .minutes(30))
  50. let getCarbEntriesCompletion = expectation(description: "Get carb entries completion")
  51. carbStore.addCarbEntry(firstCarbEntry) { (_) in
  52. DispatchQueue.main.async {
  53. self.carbStore.addCarbEntry(secondCarbEntry) { (_) in
  54. DispatchQueue.main.async {
  55. self.carbStore.addCarbEntry(thirdCarbEntry) { (_) in
  56. DispatchQueue.main.async {
  57. self.carbStore.getCarbEntries(start: Date().addingTimeInterval(-.minutes(1))) { result in
  58. getCarbEntriesCompletion.fulfill()
  59. switch result {
  60. case .failure(let error):
  61. XCTFail("Unexpected failure: \(error)")
  62. case .success(let entries):
  63. XCTAssertEqual(entries.count, 3)
  64. // First
  65. XCTAssertNotNil(entries[0].uuid)
  66. XCTAssertEqual(entries[0].provenanceIdentifier, Bundle.main.bundleIdentifier!)
  67. XCTAssertNotNil(entries[0].syncIdentifier)
  68. XCTAssertEqual(entries[0].syncVersion, 1)
  69. XCTAssertEqual(entries[0].startDate, firstCarbEntry.startDate)
  70. XCTAssertEqual(entries[0].quantity, firstCarbEntry.quantity)
  71. XCTAssertEqual(entries[0].foodType, firstCarbEntry.foodType)
  72. XCTAssertEqual(entries[0].absorptionTime, firstCarbEntry.absorptionTime)
  73. XCTAssertEqual(entries[0].createdByCurrentApp, true)
  74. XCTAssertEqual(entries[0].userCreatedDate, firstCarbEntry.date)
  75. XCTAssertNil(entries[0].userUpdatedDate)
  76. // Second
  77. XCTAssertNotNil(entries[1].uuid)
  78. XCTAssertEqual(entries[1].provenanceIdentifier, Bundle.main.bundleIdentifier!)
  79. XCTAssertNotNil(entries[1].syncIdentifier)
  80. XCTAssertEqual(entries[1].syncVersion, 1)
  81. XCTAssertEqual(entries[1].startDate, secondCarbEntry.startDate)
  82. XCTAssertEqual(entries[1].quantity, secondCarbEntry.quantity)
  83. XCTAssertEqual(entries[1].foodType, secondCarbEntry.foodType)
  84. XCTAssertEqual(entries[1].absorptionTime, secondCarbEntry.absorptionTime)
  85. XCTAssertEqual(entries[1].createdByCurrentApp, true)
  86. XCTAssertEqual(entries[1].userCreatedDate, secondCarbEntry.date)
  87. XCTAssertNil(entries[1].userUpdatedDate)
  88. // Third
  89. XCTAssertNotNil(entries[2].uuid)
  90. XCTAssertEqual(entries[2].provenanceIdentifier, Bundle.main.bundleIdentifier!)
  91. XCTAssertNotNil(entries[2].syncIdentifier)
  92. XCTAssertEqual(entries[2].syncVersion, 1)
  93. XCTAssertEqual(entries[2].startDate, thirdCarbEntry.startDate)
  94. XCTAssertEqual(entries[2].quantity, thirdCarbEntry.quantity)
  95. XCTAssertEqual(entries[2].foodType, thirdCarbEntry.foodType)
  96. XCTAssertEqual(entries[2].absorptionTime, thirdCarbEntry.absorptionTime)
  97. XCTAssertEqual(entries[2].createdByCurrentApp, true)
  98. XCTAssertEqual(entries[2].userCreatedDate, thirdCarbEntry.date)
  99. XCTAssertNil(entries[2].userUpdatedDate)
  100. }
  101. }
  102. }
  103. }
  104. }
  105. }
  106. }
  107. }
  108. wait(for: [getCarbEntriesCompletion], timeout: 2, enforceOrder: true)
  109. }
  110. // MARK: -
  111. func testAddCarbEntry() {
  112. let addCarbEntry = NewCarbEntry(quantity: HKQuantity(unit: .gram(), doubleValue: 10), startDate: Date(), foodType: "Add", absorptionTime: .hours(3))
  113. let addHealthStoreHandler = expectation(description: "Add health store handler")
  114. let addCarbEntryCompletion = expectation(description: "Add carb entry completion")
  115. let addCarbEntryHandler = expectation(description: "Add carb entry handler")
  116. let getCarbEntriesCompletion = expectation(description: "Get carb entries completion")
  117. var handlerInvocation = 0
  118. var addUUID: UUID?
  119. var addSyncIdentifier: String?
  120. healthStore.setSaveHandler({ (objects, success, error) in
  121. XCTAssertEqual(1, objects.count)
  122. let sample = objects.first as! HKQuantitySample
  123. XCTAssertEqual(sample.absorptionTime, addCarbEntry.absorptionTime)
  124. XCTAssertEqual(sample.foodType, addCarbEntry.foodType)
  125. XCTAssertEqual(sample.quantity, addCarbEntry.quantity)
  126. XCTAssertEqual(sample.startDate, addCarbEntry.startDate)
  127. XCTAssertNotNil(sample.syncIdentifier)
  128. XCTAssertEqual(sample.syncVersion, 1)
  129. XCTAssertEqual(sample.userCreatedDate, addCarbEntry.date)
  130. XCTAssertNil(sample.userUpdatedDate)
  131. addUUID = sample.uuid
  132. addSyncIdentifier = sample.syncIdentifier
  133. addHealthStoreHandler.fulfill()
  134. })
  135. carbStoreHasUpdatedCarbDataHandler = { (carbStore) in
  136. handlerInvocation += 1
  137. self.cacheStore.managedObjectContext.performAndWait {
  138. switch handlerInvocation {
  139. case 1:
  140. addCarbEntryHandler.fulfill()
  141. let objects: [CachedCarbObject] = self.cacheStore.managedObjectContext.all()
  142. XCTAssertEqual(objects.count, 1)
  143. // Added object
  144. XCTAssertEqual(objects[0].absorptionTime, addCarbEntry.absorptionTime)
  145. XCTAssertEqual(objects[0].createdByCurrentApp, true)
  146. XCTAssertEqual(objects[0].foodType, addCarbEntry.foodType)
  147. XCTAssertEqual(objects[0].grams, addCarbEntry.quantity.doubleValue(for: .gram()))
  148. XCTAssertEqual(objects[0].startDate, addCarbEntry.startDate)
  149. XCTAssertEqual(objects[0].uuid, addUUID)
  150. XCTAssertEqual(objects[0].provenanceIdentifier, Bundle.main.bundleIdentifier!)
  151. XCTAssertEqual(objects[0].syncIdentifier, addSyncIdentifier)
  152. XCTAssertEqual(objects[0].syncVersion, 1)
  153. XCTAssertEqual(objects[0].userCreatedDate, addCarbEntry.date)
  154. XCTAssertNil(objects[0].userUpdatedDate)
  155. XCTAssertNil(objects[0].userDeletedDate)
  156. XCTAssertEqual(objects[0].operation, .create)
  157. XCTAssertNotNil(objects[0].addedDate)
  158. XCTAssertNil(objects[0].supercededDate)
  159. XCTAssertGreaterThan(objects[0].anchorKey, 0)
  160. DispatchQueue.main.async {
  161. carbStore.getCarbEntries(start: Date().addingTimeInterval(-.minutes(1))) { result in
  162. getCarbEntriesCompletion.fulfill()
  163. switch result {
  164. case .failure(let error):
  165. XCTFail("Unexpected failure: \(error)")
  166. case .success(let entries):
  167. XCTAssertEqual(entries.count, 1)
  168. // Added sample
  169. XCTAssertEqual(entries[0].uuid, addUUID)
  170. XCTAssertEqual(entries[0].provenanceIdentifier, Bundle.main.bundleIdentifier!)
  171. XCTAssertEqual(entries[0].syncIdentifier, addSyncIdentifier)
  172. XCTAssertEqual(entries[0].syncVersion, 1)
  173. XCTAssertEqual(entries[0].startDate, addCarbEntry.startDate)
  174. XCTAssertEqual(entries[0].quantity, addCarbEntry.quantity)
  175. XCTAssertEqual(entries[0].foodType, addCarbEntry.foodType)
  176. XCTAssertEqual(entries[0].absorptionTime, addCarbEntry.absorptionTime)
  177. XCTAssertEqual(entries[0].createdByCurrentApp, true)
  178. XCTAssertEqual(entries[0].userCreatedDate, addCarbEntry.date)
  179. XCTAssertNil(entries[0].userUpdatedDate)
  180. }
  181. }
  182. }
  183. default:
  184. XCTFail("Unexpected handler invocation")
  185. }
  186. }
  187. }
  188. carbStore.addCarbEntry(addCarbEntry) { (result) in
  189. addCarbEntryCompletion.fulfill()
  190. }
  191. wait(for: [addHealthStoreHandler, addCarbEntryCompletion, addCarbEntryHandler, getCarbEntriesCompletion], timeout: 10, enforceOrder: true)
  192. }
  193. func testAddAndReplaceCarbEntry() {
  194. let addCarbEntry = NewCarbEntry(quantity: HKQuantity(unit: .gram(), doubleValue: 10), startDate: Date(), foodType: "Add", absorptionTime: .hours(3))
  195. let replaceCarbEntry = NewCarbEntry(quantity: HKQuantity(unit: .gram(), doubleValue: 15), startDate: Date(), foodType: "Replace", absorptionTime: .hours(4))
  196. let addHealthStoreHandler = expectation(description: "Add health store handler")
  197. let addCarbEntryCompletion = expectation(description: "Add carb entry completion")
  198. let addCarbEntryHandler = expectation(description: "Add carb entry handler")
  199. let updateHealthStoreHandler = expectation(description: "Update health store handler")
  200. let updateCarbEntryCompletion = expectation(description: "Update carb entry completion")
  201. let updateCarbEntryHandler = expectation(description: "Update carb entry handler")
  202. let getCarbEntriesCompletion = expectation(description: "Get carb entries completion")
  203. var handlerInvocation = 0
  204. var addUUID: UUID?
  205. var addSyncIdentifier: String?
  206. var addAnchorKey: Int64?
  207. var updateUUID: UUID?
  208. healthStore.setSaveHandler({ (objects, success, error) in
  209. XCTAssertEqual(1, objects.count)
  210. let sample = objects.first as! HKQuantitySample
  211. XCTAssertEqual(sample.absorptionTime, addCarbEntry.absorptionTime)
  212. XCTAssertEqual(sample.foodType, addCarbEntry.foodType)
  213. XCTAssertEqual(sample.quantity, addCarbEntry.quantity)
  214. XCTAssertEqual(sample.startDate, addCarbEntry.startDate)
  215. XCTAssertNotNil(sample.syncIdentifier)
  216. XCTAssertEqual(sample.syncVersion, 1)
  217. XCTAssertEqual(sample.userCreatedDate, addCarbEntry.date)
  218. XCTAssertNil(sample.userUpdatedDate)
  219. addUUID = sample.uuid
  220. addSyncIdentifier = sample.syncIdentifier
  221. addHealthStoreHandler.fulfill()
  222. })
  223. carbStoreHasUpdatedCarbDataHandler = { (carbStore) in
  224. handlerInvocation += 1
  225. self.cacheStore.managedObjectContext.performAndWait {
  226. switch handlerInvocation {
  227. case 1:
  228. addCarbEntryHandler.fulfill()
  229. let objects: [CachedCarbObject] = self.cacheStore.managedObjectContext.all()
  230. XCTAssertEqual(objects.count, 1)
  231. // Added object
  232. XCTAssertEqual(objects[0].absorptionTime, addCarbEntry.absorptionTime)
  233. XCTAssertEqual(objects[0].createdByCurrentApp, true)
  234. XCTAssertEqual(objects[0].foodType, addCarbEntry.foodType)
  235. XCTAssertEqual(objects[0].grams, addCarbEntry.quantity.doubleValue(for: .gram()))
  236. XCTAssertEqual(objects[0].startDate, addCarbEntry.startDate)
  237. XCTAssertEqual(objects[0].uuid, addUUID)
  238. XCTAssertEqual(objects[0].provenanceIdentifier, Bundle.main.bundleIdentifier!)
  239. XCTAssertEqual(objects[0].syncIdentifier, addSyncIdentifier)
  240. XCTAssertEqual(objects[0].syncVersion, 1)
  241. XCTAssertEqual(objects[0].userCreatedDate, addCarbEntry.date)
  242. XCTAssertNil(objects[0].userUpdatedDate)
  243. XCTAssertNil(objects[0].userDeletedDate)
  244. XCTAssertEqual(objects[0].operation, .create)
  245. XCTAssertNotNil(objects[0].addedDate)
  246. XCTAssertNil(objects[0].supercededDate)
  247. XCTAssertGreaterThan(objects[0].anchorKey, 0)
  248. addAnchorKey = objects[0].anchorKey
  249. self.healthStore.setSaveHandler({ (objects, success, error) in
  250. XCTAssertEqual(1, objects.count)
  251. let sample = objects.first as! HKQuantitySample
  252. XCTAssertNotEqual(sample.uuid, addUUID)
  253. XCTAssertEqual(sample.absorptionTime, replaceCarbEntry.absorptionTime)
  254. XCTAssertEqual(sample.foodType, replaceCarbEntry.foodType)
  255. XCTAssertEqual(sample.quantity, replaceCarbEntry.quantity)
  256. XCTAssertEqual(sample.startDate, replaceCarbEntry.startDate)
  257. XCTAssertEqual(sample.syncIdentifier, addSyncIdentifier)
  258. XCTAssertEqual(sample.syncVersion, 2)
  259. XCTAssertEqual(sample.userCreatedDate, addCarbEntry.date)
  260. XCTAssertEqual(sample.userUpdatedDate, replaceCarbEntry.date)
  261. updateUUID = sample.uuid
  262. updateHealthStoreHandler.fulfill()
  263. })
  264. self.carbStore.replaceCarbEntry(StoredCarbEntry(managedObject: objects[0]), withEntry: replaceCarbEntry) { (result) in
  265. updateCarbEntryCompletion.fulfill()
  266. }
  267. case 2:
  268. updateCarbEntryHandler.fulfill()
  269. let objects: [CachedCarbObject] = self.cacheStore.managedObjectContext.all().sorted { $0.syncVersion! < $1.syncVersion! }
  270. XCTAssertEqual(objects.count, 2)
  271. // Added object, superceded
  272. XCTAssertEqual(objects[0].absorptionTime, addCarbEntry.absorptionTime)
  273. XCTAssertEqual(objects[0].createdByCurrentApp, true)
  274. XCTAssertEqual(objects[0].foodType, addCarbEntry.foodType)
  275. XCTAssertEqual(objects[0].grams, addCarbEntry.quantity.doubleValue(for: .gram()))
  276. XCTAssertEqual(objects[0].startDate, addCarbEntry.startDate)
  277. XCTAssertEqual(objects[0].uuid, addUUID)
  278. XCTAssertEqual(objects[0].provenanceIdentifier, Bundle.main.bundleIdentifier!)
  279. XCTAssertEqual(objects[0].syncIdentifier, addSyncIdentifier)
  280. XCTAssertEqual(objects[0].syncVersion, 1)
  281. XCTAssertEqual(objects[0].userCreatedDate, addCarbEntry.date)
  282. XCTAssertNil(objects[0].userUpdatedDate)
  283. XCTAssertNil(objects[0].userDeletedDate)
  284. XCTAssertEqual(objects[0].operation, .create)
  285. XCTAssertNotNil(objects[0].addedDate)
  286. XCTAssertNotNil(objects[0].supercededDate)
  287. XCTAssertEqual(objects[0].anchorKey, addAnchorKey)
  288. // Updated object
  289. XCTAssertEqual(objects[1].absorptionTime, replaceCarbEntry.absorptionTime)
  290. XCTAssertEqual(objects[1].createdByCurrentApp, true)
  291. XCTAssertEqual(objects[1].foodType, replaceCarbEntry.foodType)
  292. XCTAssertEqual(objects[1].grams, replaceCarbEntry.quantity.doubleValue(for: .gram()))
  293. XCTAssertEqual(objects[1].startDate, replaceCarbEntry.startDate)
  294. XCTAssertEqual(objects[1].uuid, updateUUID)
  295. XCTAssertEqual(objects[1].provenanceIdentifier, Bundle.main.bundleIdentifier!)
  296. XCTAssertEqual(objects[1].syncIdentifier, addSyncIdentifier)
  297. XCTAssertEqual(objects[1].syncVersion, 2)
  298. XCTAssertEqual(objects[1].userCreatedDate, addCarbEntry.date)
  299. XCTAssertEqual(objects[1].userUpdatedDate, replaceCarbEntry.date)
  300. XCTAssertNil(objects[1].userDeletedDate)
  301. XCTAssertEqual(objects[1].operation, .update)
  302. XCTAssertNotNil(objects[1].addedDate)
  303. XCTAssertNil(objects[1].supercededDate)
  304. XCTAssertGreaterThan(objects[1].anchorKey, addAnchorKey!)
  305. DispatchQueue.main.async {
  306. carbStore.getCarbEntries(start: Date().addingTimeInterval(-.minutes(1))) { result in
  307. getCarbEntriesCompletion.fulfill()
  308. switch result {
  309. case .failure(let error):
  310. XCTFail("Unexpected failure: \(error)")
  311. case .success(let entries):
  312. XCTAssertEqual(entries.count, 1)
  313. // Updated sample
  314. XCTAssertEqual(entries[0].uuid, updateUUID)
  315. XCTAssertEqual(entries[0].provenanceIdentifier, Bundle.main.bundleIdentifier!)
  316. XCTAssertEqual(entries[0].syncIdentifier, addSyncIdentifier)
  317. XCTAssertEqual(entries[0].syncVersion, 2)
  318. XCTAssertEqual(entries[0].startDate, replaceCarbEntry.startDate)
  319. XCTAssertEqual(entries[0].quantity, replaceCarbEntry.quantity)
  320. XCTAssertEqual(entries[0].foodType, replaceCarbEntry.foodType)
  321. XCTAssertEqual(entries[0].absorptionTime, replaceCarbEntry.absorptionTime)
  322. XCTAssertEqual(entries[0].createdByCurrentApp, true)
  323. XCTAssertEqual(entries[0].userCreatedDate, addCarbEntry.date)
  324. XCTAssertEqual(entries[0].userUpdatedDate, replaceCarbEntry.date)
  325. }
  326. }
  327. }
  328. default:
  329. XCTFail("Unexpected handler invocation")
  330. }
  331. }
  332. }
  333. carbStore.addCarbEntry(addCarbEntry) { (result) in
  334. addCarbEntryCompletion.fulfill()
  335. }
  336. wait(for: [addHealthStoreHandler, addCarbEntryCompletion, addCarbEntryHandler, updateHealthStoreHandler, updateCarbEntryCompletion, updateCarbEntryHandler, getCarbEntriesCompletion], timeout: 10, enforceOrder: true)
  337. }
  338. func testAddAndDeleteCarbEntry() {
  339. let addCarbEntry = NewCarbEntry(quantity: HKQuantity(unit: .gram(), doubleValue: 10), startDate: Date(), foodType: "Add", absorptionTime: .hours(3))
  340. let addHealthStoreHandler = expectation(description: "Add health store handler")
  341. let addCarbEntryCompletion = expectation(description: "Add carb entry completion")
  342. let addCarbEntryHandler = expectation(description: "Add carb entry handler")
  343. let deleteHealthStoreHandler = expectation(description: "Delete health store handler")
  344. let deleteCarbEntryCompletion = expectation(description: "Delete carb entry completion")
  345. let deleteCarbEntryHandler = expectation(description: "Delete carb entry handler")
  346. let getCarbEntriesCompletion = expectation(description: "Get carb entries completion")
  347. var handlerInvocation = 0
  348. var addUUID: UUID?
  349. var addSyncIdentifier: String?
  350. var addAnchorKey: Int64?
  351. healthStore.setSaveHandler({ (objects, success, error) in
  352. XCTAssertEqual(1, objects.count)
  353. let sample = objects.first as! HKQuantitySample
  354. XCTAssertEqual(sample.absorptionTime, addCarbEntry.absorptionTime)
  355. XCTAssertEqual(sample.foodType, addCarbEntry.foodType)
  356. XCTAssertEqual(sample.quantity, addCarbEntry.quantity)
  357. XCTAssertEqual(sample.startDate, addCarbEntry.startDate)
  358. XCTAssertNotNil(sample.syncIdentifier)
  359. XCTAssertEqual(sample.syncVersion, 1)
  360. XCTAssertEqual(sample.userCreatedDate, addCarbEntry.date)
  361. XCTAssertNil(sample.userUpdatedDate)
  362. addUUID = sample.uuid
  363. addSyncIdentifier = sample.syncIdentifier
  364. addHealthStoreHandler.fulfill()
  365. })
  366. carbStoreHasUpdatedCarbDataHandler = { (carbStore) in
  367. handlerInvocation += 1
  368. self.cacheStore.managedObjectContext.performAndWait {
  369. switch handlerInvocation {
  370. case 1:
  371. addCarbEntryHandler.fulfill()
  372. let objects: [CachedCarbObject] = self.cacheStore.managedObjectContext.all()
  373. XCTAssertEqual(objects.count, 1)
  374. // Added object
  375. XCTAssertEqual(objects[0].absorptionTime, addCarbEntry.absorptionTime)
  376. XCTAssertEqual(objects[0].createdByCurrentApp, true)
  377. XCTAssertEqual(objects[0].foodType, addCarbEntry.foodType)
  378. XCTAssertEqual(objects[0].grams, addCarbEntry.quantity.doubleValue(for: .gram()))
  379. XCTAssertEqual(objects[0].startDate, addCarbEntry.startDate)
  380. XCTAssertEqual(objects[0].uuid, addUUID)
  381. XCTAssertEqual(objects[0].provenanceIdentifier, Bundle.main.bundleIdentifier!)
  382. XCTAssertEqual(objects[0].syncIdentifier, addSyncIdentifier)
  383. XCTAssertEqual(objects[0].syncVersion, 1)
  384. XCTAssertEqual(objects[0].userCreatedDate, addCarbEntry.date)
  385. XCTAssertNil(objects[0].userUpdatedDate)
  386. XCTAssertNil(objects[0].userDeletedDate)
  387. XCTAssertEqual(objects[0].operation, .create)
  388. XCTAssertNotNil(objects[0].addedDate)
  389. XCTAssertNil(objects[0].supercededDate)
  390. XCTAssertGreaterThan(objects[0].anchorKey, 0)
  391. addUUID = objects[0].uuid
  392. addSyncIdentifier = objects[0].syncIdentifier
  393. addAnchorKey = objects[0].anchorKey
  394. self.healthStore.setDeletedObjectsHandler({ (objectType, predicate, success, count, error) in
  395. XCTAssertEqual(objectType, HKObjectType.quantityType(forIdentifier: .dietaryCarbohydrates))
  396. XCTAssertEqual(predicate.predicateFormat, "UUID == \(addUUID!)")
  397. deleteHealthStoreHandler.fulfill()
  398. })
  399. self.carbStore.deleteCarbEntry(StoredCarbEntry(managedObject: objects[0])) { (result) in
  400. deleteCarbEntryCompletion.fulfill()
  401. }
  402. case 2:
  403. deleteCarbEntryHandler.fulfill()
  404. let objects: [CachedCarbObject] = self.cacheStore.managedObjectContext.all()
  405. XCTAssertEqual(objects.count, 2)
  406. // Added object, superceded
  407. XCTAssertEqual(objects[0].absorptionTime, addCarbEntry.absorptionTime)
  408. XCTAssertEqual(objects[0].createdByCurrentApp, true)
  409. XCTAssertEqual(objects[0].foodType, addCarbEntry.foodType)
  410. XCTAssertEqual(objects[0].grams, addCarbEntry.quantity.doubleValue(for: .gram()))
  411. XCTAssertEqual(objects[0].startDate, addCarbEntry.startDate)
  412. XCTAssertEqual(objects[0].uuid, addUUID)
  413. XCTAssertEqual(objects[0].provenanceIdentifier, Bundle.main.bundleIdentifier!)
  414. XCTAssertEqual(objects[0].syncIdentifier, addSyncIdentifier)
  415. XCTAssertEqual(objects[0].syncVersion, 1)
  416. XCTAssertEqual(objects[0].userCreatedDate, addCarbEntry.date)
  417. XCTAssertNil(objects[0].userUpdatedDate)
  418. XCTAssertNil(objects[0].userDeletedDate)
  419. XCTAssertEqual(objects[0].operation, .create)
  420. XCTAssertNotNil(objects[0].addedDate)
  421. XCTAssertNotNil(objects[0].supercededDate)
  422. XCTAssertEqual(objects[0].anchorKey, addAnchorKey)
  423. // Deleted object
  424. XCTAssertEqual(objects[1].absorptionTime, addCarbEntry.absorptionTime)
  425. XCTAssertEqual(objects[1].createdByCurrentApp, true)
  426. XCTAssertEqual(objects[1].foodType, addCarbEntry.foodType)
  427. XCTAssertEqual(objects[1].grams, addCarbEntry.quantity.doubleValue(for: .gram()))
  428. XCTAssertEqual(objects[1].startDate, addCarbEntry.startDate)
  429. XCTAssertNil(objects[1].uuid)
  430. XCTAssertEqual(objects[1].provenanceIdentifier, Bundle.main.bundleIdentifier!)
  431. XCTAssertEqual(objects[1].syncIdentifier, addSyncIdentifier)
  432. XCTAssertEqual(objects[1].syncVersion, 1)
  433. XCTAssertEqual(objects[1].userCreatedDate, addCarbEntry.date)
  434. XCTAssertNil(objects[1].userUpdatedDate)
  435. XCTAssertNotNil(objects[1].userDeletedDate)
  436. XCTAssertEqual(objects[1].operation, .delete)
  437. XCTAssertNotNil(objects[1].addedDate)
  438. XCTAssertNil(objects[1].supercededDate)
  439. XCTAssertGreaterThan(objects[1].anchorKey, addAnchorKey!)
  440. DispatchQueue.main.async {
  441. carbStore.getCarbEntries(start: Date().addingTimeInterval(-.minutes(1))) { result in
  442. getCarbEntriesCompletion.fulfill()
  443. switch result {
  444. case .failure(let error):
  445. XCTFail("Unexpected failure: \(error)")
  446. case .success(let entries):
  447. XCTAssertEqual(entries.count, 0)
  448. }
  449. }
  450. }
  451. default:
  452. XCTFail("Unexpected handler invocation")
  453. }
  454. }
  455. }
  456. carbStore.addCarbEntry(addCarbEntry) { (result) in
  457. addCarbEntryCompletion.fulfill()
  458. }
  459. wait(for: [addHealthStoreHandler, addCarbEntryCompletion, addCarbEntryHandler, deleteHealthStoreHandler, deleteCarbEntryCompletion, deleteCarbEntryHandler, getCarbEntriesCompletion], timeout: 10, enforceOrder: true)
  460. }
  461. func testAddAndReplaceAndDeleteCarbEntry() {
  462. let addCarbEntry = NewCarbEntry(quantity: HKQuantity(unit: .gram(), doubleValue: 10), startDate: Date(), foodType: "Add", absorptionTime: .hours(3))
  463. let replaceCarbEntry = NewCarbEntry(quantity: HKQuantity(unit: .gram(), doubleValue: 15), startDate: Date(), foodType: "Replace", absorptionTime: .hours(4))
  464. let addHealthStoreHandler = expectation(description: "Add health store handler")
  465. let addCarbEntryCompletion = expectation(description: "Add carb entry completion")
  466. let addCarbEntryHandler = expectation(description: "Add carb entry handler")
  467. let updateHealthStoreHandler = expectation(description: "Update health store handler")
  468. let updateCarbEntryCompletion = expectation(description: "Update carb entry completion")
  469. let updateCarbEntryHandler = expectation(description: "Update carb entry handler")
  470. let deleteHealthStoreHandler = expectation(description: "Delete health store handler")
  471. let deleteCarbEntryCompletion = expectation(description: "Delete carb entry completion")
  472. let deleteCarbEntryHandler = expectation(description: "Delete carb entry handler")
  473. let getCarbEntriesCompletion = expectation(description: "Get carb entries completion")
  474. var handlerInvocation = 0
  475. var addUUID: UUID?
  476. var addSyncIdentifier: String?
  477. var addAnchorKey: Int64?
  478. var updateUUID: UUID?
  479. var updateAnchorKey: Int64?
  480. healthStore.setSaveHandler({ (objects, success, error) in
  481. XCTAssertEqual(1, objects.count)
  482. let sample = objects.first as! HKQuantitySample
  483. XCTAssertEqual(sample.absorptionTime, addCarbEntry.absorptionTime)
  484. XCTAssertEqual(sample.foodType, addCarbEntry.foodType)
  485. XCTAssertEqual(sample.quantity, addCarbEntry.quantity)
  486. XCTAssertEqual(sample.startDate, addCarbEntry.startDate)
  487. XCTAssertNotNil(sample.syncIdentifier)
  488. XCTAssertEqual(sample.syncVersion, 1)
  489. XCTAssertEqual(sample.userCreatedDate, addCarbEntry.date)
  490. XCTAssertNil(sample.userUpdatedDate)
  491. addUUID = sample.uuid
  492. addSyncIdentifier = sample.syncIdentifier
  493. addHealthStoreHandler.fulfill()
  494. })
  495. carbStoreHasUpdatedCarbDataHandler = { (carbStore) in
  496. handlerInvocation += 1
  497. self.cacheStore.managedObjectContext.performAndWait {
  498. switch handlerInvocation {
  499. case 1:
  500. addCarbEntryHandler.fulfill()
  501. let objects: [CachedCarbObject] = self.cacheStore.managedObjectContext.all()
  502. XCTAssertEqual(objects.count, 1)
  503. // Added object
  504. XCTAssertEqual(objects[0].absorptionTime, addCarbEntry.absorptionTime)
  505. XCTAssertEqual(objects[0].createdByCurrentApp, true)
  506. XCTAssertEqual(objects[0].foodType, addCarbEntry.foodType)
  507. XCTAssertEqual(objects[0].grams, addCarbEntry.quantity.doubleValue(for: .gram()))
  508. XCTAssertEqual(objects[0].startDate, addCarbEntry.startDate)
  509. XCTAssertEqual(objects[0].uuid, addUUID)
  510. XCTAssertEqual(objects[0].provenanceIdentifier, Bundle.main.bundleIdentifier!)
  511. XCTAssertEqual(objects[0].syncIdentifier, addSyncIdentifier)
  512. XCTAssertEqual(objects[0].syncVersion, 1)
  513. XCTAssertEqual(objects[0].userCreatedDate, addCarbEntry.date)
  514. XCTAssertNil(objects[0].userUpdatedDate)
  515. XCTAssertNil(objects[0].userDeletedDate)
  516. XCTAssertEqual(objects[0].operation, .create)
  517. XCTAssertNotNil(objects[0].addedDate)
  518. XCTAssertNil(objects[0].supercededDate)
  519. XCTAssertGreaterThan(objects[0].anchorKey, 0)
  520. addAnchorKey = objects[0].anchorKey
  521. self.healthStore.setSaveHandler({ (objects, success, error) in
  522. XCTAssertEqual(1, objects.count)
  523. let sample = objects.first as! HKQuantitySample
  524. XCTAssertNotEqual(sample.uuid, addUUID)
  525. XCTAssertEqual(sample.absorptionTime, replaceCarbEntry.absorptionTime)
  526. XCTAssertEqual(sample.foodType, replaceCarbEntry.foodType)
  527. XCTAssertEqual(sample.quantity, replaceCarbEntry.quantity)
  528. XCTAssertEqual(sample.startDate, replaceCarbEntry.startDate)
  529. XCTAssertEqual(sample.syncIdentifier, addSyncIdentifier)
  530. XCTAssertEqual(sample.syncVersion, 2)
  531. XCTAssertEqual(sample.userCreatedDate, addCarbEntry.date)
  532. XCTAssertEqual(sample.userUpdatedDate, replaceCarbEntry.date)
  533. updateUUID = sample.uuid
  534. updateHealthStoreHandler.fulfill()
  535. })
  536. self.carbStore.replaceCarbEntry(StoredCarbEntry(managedObject: objects[0]), withEntry: replaceCarbEntry) { (result) in
  537. updateCarbEntryCompletion.fulfill()
  538. }
  539. case 2:
  540. updateCarbEntryHandler.fulfill()
  541. let objects: [CachedCarbObject] = self.cacheStore.managedObjectContext.all().sorted { $0.syncVersion! < $1.syncVersion! }
  542. XCTAssertEqual(objects.count, 2)
  543. // Added object, superceded
  544. XCTAssertEqual(objects[0].absorptionTime, addCarbEntry.absorptionTime)
  545. XCTAssertEqual(objects[0].createdByCurrentApp, true)
  546. XCTAssertEqual(objects[0].foodType, addCarbEntry.foodType)
  547. XCTAssertEqual(objects[0].grams, addCarbEntry.quantity.doubleValue(for: .gram()))
  548. XCTAssertEqual(objects[0].startDate, addCarbEntry.startDate)
  549. XCTAssertEqual(objects[0].uuid, addUUID)
  550. XCTAssertEqual(objects[0].provenanceIdentifier, Bundle.main.bundleIdentifier!)
  551. XCTAssertEqual(objects[0].syncIdentifier, addSyncIdentifier)
  552. XCTAssertEqual(objects[0].syncVersion, 1)
  553. XCTAssertEqual(objects[0].userCreatedDate, addCarbEntry.date)
  554. XCTAssertNil(objects[0].userUpdatedDate)
  555. XCTAssertNil(objects[0].userDeletedDate)
  556. XCTAssertEqual(objects[0].operation, .create)
  557. XCTAssertNotNil(objects[0].addedDate)
  558. XCTAssertNotNil(objects[0].supercededDate)
  559. XCTAssertEqual(objects[0].anchorKey, addAnchorKey)
  560. // Updated object
  561. XCTAssertEqual(objects[1].absorptionTime, replaceCarbEntry.absorptionTime)
  562. XCTAssertEqual(objects[1].createdByCurrentApp, true)
  563. XCTAssertEqual(objects[1].foodType, replaceCarbEntry.foodType)
  564. XCTAssertEqual(objects[1].grams, replaceCarbEntry.quantity.doubleValue(for: .gram()))
  565. XCTAssertEqual(objects[1].startDate, replaceCarbEntry.startDate)
  566. XCTAssertEqual(objects[1].uuid, updateUUID)
  567. XCTAssertEqual(objects[1].provenanceIdentifier, Bundle.main.bundleIdentifier!)
  568. XCTAssertEqual(objects[1].syncIdentifier, addSyncIdentifier)
  569. XCTAssertEqual(objects[1].syncVersion, 2)
  570. XCTAssertEqual(objects[1].userCreatedDate, addCarbEntry.date)
  571. XCTAssertEqual(objects[1].userUpdatedDate, replaceCarbEntry.date)
  572. XCTAssertNil(objects[1].userDeletedDate)
  573. XCTAssertEqual(objects[1].operation, .update)
  574. XCTAssertNotNil(objects[1].addedDate)
  575. XCTAssertNil(objects[1].supercededDate)
  576. XCTAssertGreaterThan(objects[1].anchorKey, addAnchorKey!)
  577. updateAnchorKey = objects[1].anchorKey
  578. self.healthStore.setDeletedObjectsHandler({ (objectType, predicate, success, count, error) in
  579. XCTAssertEqual(objectType, HKObjectType.quantityType(forIdentifier: .dietaryCarbohydrates))
  580. XCTAssertEqual(predicate.predicateFormat, "UUID == \(updateUUID!)")
  581. deleteHealthStoreHandler.fulfill()
  582. })
  583. self.carbStore.deleteCarbEntry(StoredCarbEntry(managedObject: objects[1])) { (result) in
  584. deleteCarbEntryCompletion.fulfill()
  585. }
  586. case 3:
  587. deleteCarbEntryHandler.fulfill()
  588. let objects: [CachedCarbObject] = self.cacheStore.managedObjectContext.all().sorted { $0.syncVersion! < $1.syncVersion! }
  589. XCTAssertEqual(objects.count, 3)
  590. // Added object, superceded
  591. XCTAssertEqual(objects[0].absorptionTime, addCarbEntry.absorptionTime)
  592. XCTAssertEqual(objects[0].createdByCurrentApp, true)
  593. XCTAssertEqual(objects[0].foodType, addCarbEntry.foodType)
  594. XCTAssertEqual(objects[0].grams, addCarbEntry.quantity.doubleValue(for: .gram()))
  595. XCTAssertEqual(objects[0].startDate, addCarbEntry.startDate)
  596. XCTAssertEqual(objects[0].uuid, addUUID)
  597. XCTAssertEqual(objects[0].provenanceIdentifier, Bundle.main.bundleIdentifier!)
  598. XCTAssertEqual(objects[0].syncIdentifier, addSyncIdentifier)
  599. XCTAssertEqual(objects[0].syncVersion, 1)
  600. XCTAssertEqual(objects[0].userCreatedDate, addCarbEntry.date)
  601. XCTAssertNil(objects[0].userUpdatedDate)
  602. XCTAssertNil(objects[0].userDeletedDate)
  603. XCTAssertEqual(objects[0].operation, .create)
  604. XCTAssertNotNil(objects[0].addedDate)
  605. XCTAssertNotNil(objects[0].supercededDate)
  606. XCTAssertEqual(objects[0].anchorKey, addAnchorKey)
  607. // Updated object, superceded
  608. XCTAssertEqual(objects[1].absorptionTime, replaceCarbEntry.absorptionTime)
  609. XCTAssertEqual(objects[1].createdByCurrentApp, true)
  610. XCTAssertEqual(objects[1].foodType, replaceCarbEntry.foodType)
  611. XCTAssertEqual(objects[1].grams, replaceCarbEntry.quantity.doubleValue(for: .gram()))
  612. XCTAssertEqual(objects[1].startDate, replaceCarbEntry.startDate)
  613. XCTAssertEqual(objects[1].uuid, updateUUID)
  614. XCTAssertEqual(objects[1].provenanceIdentifier, Bundle.main.bundleIdentifier!)
  615. XCTAssertEqual(objects[1].syncIdentifier, addSyncIdentifier)
  616. XCTAssertEqual(objects[1].syncVersion, 2)
  617. XCTAssertEqual(objects[1].userCreatedDate, addCarbEntry.date)
  618. XCTAssertEqual(objects[1].userUpdatedDate, replaceCarbEntry.date)
  619. XCTAssertNil(objects[1].userDeletedDate)
  620. XCTAssertEqual(objects[1].operation, .update)
  621. XCTAssertNotNil(objects[1].addedDate)
  622. XCTAssertNotNil(objects[1].supercededDate)
  623. XCTAssertEqual(objects[1].anchorKey, updateAnchorKey)
  624. // Deleted object
  625. XCTAssertEqual(objects[2].absorptionTime, replaceCarbEntry.absorptionTime)
  626. XCTAssertEqual(objects[2].createdByCurrentApp, true)
  627. XCTAssertEqual(objects[2].foodType, replaceCarbEntry.foodType)
  628. XCTAssertEqual(objects[2].grams, replaceCarbEntry.quantity.doubleValue(for: .gram()))
  629. XCTAssertEqual(objects[2].startDate, replaceCarbEntry.startDate)
  630. XCTAssertNil(objects[2].uuid)
  631. XCTAssertEqual(objects[2].provenanceIdentifier, Bundle.main.bundleIdentifier!)
  632. XCTAssertEqual(objects[2].syncIdentifier, addSyncIdentifier)
  633. XCTAssertEqual(objects[2].syncVersion, 2)
  634. XCTAssertEqual(objects[2].userCreatedDate, addCarbEntry.date)
  635. XCTAssertEqual(objects[2].userUpdatedDate, replaceCarbEntry.date)
  636. XCTAssertNotNil(objects[2].userDeletedDate)
  637. XCTAssertEqual(objects[2].operation, .delete)
  638. XCTAssertNotNil(objects[2].addedDate)
  639. XCTAssertNil(objects[2].supercededDate)
  640. XCTAssertGreaterThan(objects[2].anchorKey, updateAnchorKey!)
  641. DispatchQueue.main.async {
  642. carbStore.getCarbEntries(start: Date().addingTimeInterval(-.minutes(1))) { result in
  643. getCarbEntriesCompletion.fulfill()
  644. switch result {
  645. case .failure(let error):
  646. XCTFail("Unexpected failure: \(error)")
  647. case .success(let entries):
  648. XCTAssertEqual(entries.count, 0)
  649. }
  650. }
  651. }
  652. default:
  653. XCTFail("Unexpected handler invocation")
  654. }
  655. }
  656. }
  657. carbStore.addCarbEntry(addCarbEntry) { (result) in
  658. addCarbEntryCompletion.fulfill()
  659. }
  660. wait(for: [addHealthStoreHandler, addCarbEntryCompletion, addCarbEntryHandler, updateHealthStoreHandler, updateCarbEntryCompletion, updateCarbEntryHandler, deleteHealthStoreHandler, deleteCarbEntryCompletion, deleteCarbEntryHandler, getCarbEntriesCompletion], timeout: 10, enforceOrder: true)
  661. }
  662. // MARK: -
  663. func testGetSyncCarbObjects() {
  664. let firstCarbEntry = NewCarbEntry(quantity: HKQuantity(unit: .gram(), doubleValue: 10), startDate: Date(timeIntervalSinceNow: -1), foodType: "First", absorptionTime: .hours(5))
  665. let secondCarbEntry = NewCarbEntry(quantity: HKQuantity(unit: .gram(), doubleValue: 20), startDate: Date(), foodType: "Second", absorptionTime: .hours(3))
  666. let thirdCarbEntry = NewCarbEntry(quantity: HKQuantity(unit: .gram(), doubleValue: 30), startDate: Date(timeIntervalSinceNow: 1), foodType: "Third", absorptionTime: .minutes(30))
  667. let getSyncCarbObjectsCompletion = expectation(description: "Get sync carb objects completion")
  668. carbStore.addCarbEntry(firstCarbEntry) { (_) in
  669. DispatchQueue.main.async {
  670. self.carbStore.addCarbEntry(secondCarbEntry) { (_) in
  671. DispatchQueue.main.async {
  672. self.carbStore.addCarbEntry(thirdCarbEntry) { (_) in
  673. DispatchQueue.main.async {
  674. self.carbStore.getSyncCarbObjects(start: Date().addingTimeInterval(-.minutes(1))) { result in
  675. getSyncCarbObjectsCompletion.fulfill()
  676. switch result {
  677. case .failure(let error):
  678. XCTFail("Unexpected failure: \(error)")
  679. case .success(let objects):
  680. XCTAssertEqual(objects.count, 3)
  681. // First
  682. XCTAssertEqual(objects[0].absorptionTime, firstCarbEntry.absorptionTime)
  683. XCTAssertEqual(objects[0].createdByCurrentApp, true)
  684. XCTAssertEqual(objects[0].foodType, firstCarbEntry.foodType)
  685. XCTAssertEqual(objects[0].grams, firstCarbEntry.quantity.doubleValue(for: .gram()))
  686. XCTAssertEqual(objects[0].startDate, firstCarbEntry.startDate)
  687. XCTAssertNotNil(objects[0].uuid)
  688. XCTAssertEqual(objects[0].provenanceIdentifier, Bundle.main.bundleIdentifier!)
  689. XCTAssertNotNil(objects[0].syncIdentifier)
  690. XCTAssertEqual(objects[0].syncVersion, 1)
  691. XCTAssertEqual(objects[0].userCreatedDate, firstCarbEntry.date)
  692. XCTAssertNil(objects[0].userUpdatedDate)
  693. XCTAssertNil(objects[0].userDeletedDate)
  694. XCTAssertEqual(objects[0].operation, .create)
  695. XCTAssertNotNil(objects[0].addedDate)
  696. XCTAssertNil(objects[0].supercededDate)
  697. // Second
  698. XCTAssertEqual(objects[1].absorptionTime, secondCarbEntry.absorptionTime)
  699. XCTAssertEqual(objects[1].createdByCurrentApp, true)
  700. XCTAssertEqual(objects[1].foodType, secondCarbEntry.foodType)
  701. XCTAssertEqual(objects[1].grams, secondCarbEntry.quantity.doubleValue(for: .gram()))
  702. XCTAssertEqual(objects[1].startDate, secondCarbEntry.startDate)
  703. XCTAssertNotNil(objects[1].uuid)
  704. XCTAssertEqual(objects[1].provenanceIdentifier, Bundle.main.bundleIdentifier!)
  705. XCTAssertNotNil(objects[1].syncIdentifier)
  706. XCTAssertEqual(objects[1].syncVersion, 1)
  707. XCTAssertEqual(objects[1].userCreatedDate, secondCarbEntry.date)
  708. XCTAssertNil(objects[1].userUpdatedDate)
  709. XCTAssertNil(objects[1].userDeletedDate)
  710. XCTAssertEqual(objects[1].operation, .create)
  711. XCTAssertNotNil(objects[1].addedDate)
  712. XCTAssertNil(objects[1].supercededDate)
  713. // Third
  714. XCTAssertEqual(objects[2].absorptionTime, thirdCarbEntry.absorptionTime)
  715. XCTAssertEqual(objects[2].createdByCurrentApp, true)
  716. XCTAssertEqual(objects[2].foodType, thirdCarbEntry.foodType)
  717. XCTAssertEqual(objects[2].grams, thirdCarbEntry.quantity.doubleValue(for: .gram()))
  718. XCTAssertEqual(objects[2].startDate, thirdCarbEntry.startDate)
  719. XCTAssertNotNil(objects[2].uuid)
  720. XCTAssertEqual(objects[2].provenanceIdentifier, Bundle.main.bundleIdentifier!)
  721. XCTAssertNotNil(objects[2].syncIdentifier)
  722. XCTAssertEqual(objects[2].syncVersion, 1)
  723. XCTAssertEqual(objects[2].userCreatedDate, thirdCarbEntry.date)
  724. XCTAssertNil(objects[2].userUpdatedDate)
  725. XCTAssertNil(objects[2].userDeletedDate)
  726. XCTAssertEqual(objects[2].operation, .create)
  727. XCTAssertNotNil(objects[2].addedDate)
  728. XCTAssertNil(objects[2].supercededDate)
  729. }
  730. }
  731. }
  732. }
  733. }
  734. }
  735. }
  736. }
  737. wait(for: [getSyncCarbObjectsCompletion], timeout: 2, enforceOrder: true)
  738. }
  739. func testSetSyncCarbObjects() {
  740. let carbEntry = NewCarbEntry(quantity: HKQuantity(unit: .gram(), doubleValue: 10), startDate: Date(timeIntervalSinceNow: -1), foodType: "First", absorptionTime: .hours(5))
  741. let syncCarbObjects = [SyncCarbObject(absorptionTime: .hours(5),
  742. createdByCurrentApp: true,
  743. foodType: "Pizza",
  744. grams: 45,
  745. startDate: Date(timeIntervalSinceNow: -30),
  746. uuid: UUID(),
  747. provenanceIdentifier: "com.loopkit.Loop",
  748. syncIdentifier: UUID().uuidString,
  749. syncVersion: 4,
  750. userCreatedDate: Date(timeIntervalSinceNow: -35),
  751. userUpdatedDate: Date(timeIntervalSinceNow: -34),
  752. userDeletedDate: nil,
  753. operation: .update,
  754. addedDate: Date(timeIntervalSinceNow: -34),
  755. supercededDate: nil),
  756. SyncCarbObject(absorptionTime: .hours(3),
  757. createdByCurrentApp: false,
  758. foodType: "Pasta",
  759. grams: 25,
  760. startDate: Date(timeIntervalSinceNow: -15),
  761. uuid: UUID(),
  762. provenanceIdentifier: "com.abc.Example",
  763. syncIdentifier: UUID().uuidString,
  764. syncVersion: 1,
  765. userCreatedDate: Date(timeIntervalSinceNow: -16),
  766. userUpdatedDate: nil,
  767. userDeletedDate: nil,
  768. operation: .create,
  769. addedDate: Date(timeIntervalSinceNow: -16),
  770. supercededDate: nil),
  771. SyncCarbObject(absorptionTime: .minutes(30),
  772. createdByCurrentApp: true,
  773. foodType: "Sugar",
  774. grams: 15,
  775. startDate: Date(timeIntervalSinceNow: 0),
  776. uuid: UUID(),
  777. provenanceIdentifier: "com.loopkit.Loop",
  778. syncIdentifier: UUID().uuidString,
  779. syncVersion: 1,
  780. userCreatedDate: Date(timeIntervalSinceNow: -1),
  781. userUpdatedDate: nil,
  782. userDeletedDate: nil,
  783. operation: .create,
  784. addedDate: Date(timeIntervalSinceNow: -1),
  785. supercededDate: nil)
  786. ]
  787. let getCarbEntriesCompletion = expectation(description: "Get carb entries completion")
  788. // Add a carb entry first, that will be purged when setSyncCarbObjects is invoked
  789. carbStore.addCarbEntry(carbEntry) { (_) in
  790. DispatchQueue.main.async {
  791. self.carbStore.setSyncCarbObjects(syncCarbObjects) { (error) in
  792. XCTAssertNil(error)
  793. DispatchQueue.main.async {
  794. self.carbStore.getCarbEntries(start: Date().addingTimeInterval(-.minutes(1))) { result in
  795. getCarbEntriesCompletion.fulfill()
  796. switch result {
  797. case .failure(let error):
  798. XCTFail("Unexpected failure: \(error)")
  799. case .success(let entries):
  800. XCTAssertEqual(entries.count, 3)
  801. for index in 0..<3 {
  802. XCTAssertEqual(entries[index].uuid, syncCarbObjects[index].uuid)
  803. XCTAssertEqual(entries[index].provenanceIdentifier, syncCarbObjects[index].provenanceIdentifier)
  804. XCTAssertEqual(entries[index].syncIdentifier, syncCarbObjects[index].syncIdentifier)
  805. XCTAssertEqual(entries[index].syncVersion, syncCarbObjects[index].syncVersion)
  806. XCTAssertEqual(entries[index].startDate, syncCarbObjects[index].startDate)
  807. XCTAssertEqual(entries[index].quantity, syncCarbObjects[index].quantity)
  808. XCTAssertEqual(entries[index].foodType, syncCarbObjects[index].foodType)
  809. XCTAssertEqual(entries[index].absorptionTime, syncCarbObjects[index].absorptionTime)
  810. XCTAssertEqual(entries[index].createdByCurrentApp, syncCarbObjects[index].createdByCurrentApp)
  811. XCTAssertEqual(entries[index].userCreatedDate, syncCarbObjects[index].userCreatedDate)
  812. XCTAssertEqual(entries[index].userCreatedDate, syncCarbObjects[index].userCreatedDate)
  813. }
  814. }
  815. }
  816. }
  817. }
  818. }
  819. }
  820. wait(for: [getCarbEntriesCompletion], timeout: 2, enforceOrder: true)
  821. }
  822. // MARK: -
  823. private func generateSyncIdentifier() -> String {
  824. return UUID().uuidString
  825. }
  826. }
  827. class CarbStoreQueryAnchorTests: XCTestCase {
  828. var rawValue: CarbStore.QueryAnchor.RawValue = [
  829. "anchorKey": Int64(123)
  830. ]
  831. func testInitializerDefault() {
  832. let queryAnchor = CarbStore.QueryAnchor()
  833. XCTAssertEqual(queryAnchor.anchorKey, 0)
  834. }
  835. func testInitializerRawValue() {
  836. let queryAnchor = CarbStore.QueryAnchor(rawValue: rawValue)
  837. XCTAssertNotNil(queryAnchor)
  838. XCTAssertEqual(queryAnchor?.anchorKey, 123)
  839. }
  840. func testInitializerRawValueMissingAnchorKey() {
  841. rawValue["anchorKey"] = nil
  842. XCTAssertNil(CarbStore.QueryAnchor(rawValue: rawValue))
  843. }
  844. func testInitializerRawValueInvalidAnchorKey() {
  845. rawValue["anchorKey"] = "123"
  846. XCTAssertNil(CarbStore.QueryAnchor(rawValue: rawValue))
  847. }
  848. func testInitializerRawValueIgnoresDeprecatedStoredModificationCounter() {
  849. rawValue["storedModificationCounter"] = Int64(456)
  850. let queryAnchor = CarbStore.QueryAnchor(rawValue: rawValue)
  851. XCTAssertNotNil(queryAnchor)
  852. XCTAssertEqual(queryAnchor?.anchorKey, 123)
  853. }
  854. func testInitializerRawValueUsesDeprecatedStoredModificationCounter() {
  855. rawValue["anchorKey"] = nil
  856. rawValue["storedModificationCounter"] = Int64(456)
  857. let queryAnchor = CarbStore.QueryAnchor(rawValue: rawValue)
  858. XCTAssertNotNil(queryAnchor)
  859. XCTAssertEqual(queryAnchor?.anchorKey, 456)
  860. }
  861. func testRawValueWithDefault() {
  862. let rawValue = CarbStore.QueryAnchor().rawValue
  863. XCTAssertEqual(rawValue.count, 1)
  864. XCTAssertEqual(rawValue["anchorKey"] as? Int64, Int64(0))
  865. }
  866. func testRawValueWithNonDefault() {
  867. var queryAnchor = CarbStore.QueryAnchor()
  868. queryAnchor.anchorKey = 123
  869. let rawValue = queryAnchor.rawValue
  870. XCTAssertEqual(rawValue.count, 1)
  871. XCTAssertEqual(rawValue["anchorKey"] as? Int64, Int64(123))
  872. }
  873. }
  874. class CarbStoreQueryTests: PersistenceControllerTestCase {
  875. var carbStore: CarbStore!
  876. var completion: XCTestExpectation!
  877. var queryAnchor: CarbStore.QueryAnchor!
  878. var limit: Int!
  879. override func setUp() {
  880. super.setUp()
  881. carbStore = CarbStore(
  882. healthStore: HKHealthStoreMock(),
  883. cacheStore: cacheStore,
  884. cacheLength: .hours(24),
  885. defaultAbsorptionTimes: (fast: .minutes(30), medium: .hours(3), slow: .hours(5)),
  886. observationInterval: 0,
  887. provenanceIdentifier: Bundle.main.bundleIdentifier!)
  888. let semaphore = DispatchSemaphore(value: 0)
  889. cacheStore.onReady { (error) in
  890. semaphore.signal()
  891. }
  892. semaphore.wait()
  893. completion = expectation(description: "Completion")
  894. queryAnchor = CarbStore.QueryAnchor()
  895. limit = Int.max
  896. }
  897. override func tearDown() {
  898. limit = nil
  899. queryAnchor = nil
  900. completion = nil
  901. carbStore = nil
  902. super.tearDown()
  903. }
  904. func testEmptyWithDefaultQueryAnchor() {
  905. carbStore.executeCarbQuery(fromQueryAnchor: queryAnchor, limit: limit) { result in
  906. switch result {
  907. case .failure(let error):
  908. XCTFail("Unexpected failure: \(error)")
  909. case .success(let anchor, let created, let updated, let deleted):
  910. XCTAssertEqual(anchor.anchorKey, 0)
  911. XCTAssertEqual(created.count, 0)
  912. XCTAssertEqual(updated.count, 0)
  913. XCTAssertEqual(deleted.count, 0)
  914. }
  915. self.completion.fulfill()
  916. }
  917. wait(for: [completion], timeout: 2, enforceOrder: true)
  918. }
  919. func testEmptyWithMissingQueryAnchor() {
  920. queryAnchor = nil
  921. carbStore.executeCarbQuery(fromQueryAnchor: queryAnchor, limit: limit) { result in
  922. switch result {
  923. case .failure(let error):
  924. XCTFail("Unexpected failure: \(error)")
  925. case .success(let anchor, let created, let updated, let deleted):
  926. XCTAssertEqual(anchor.anchorKey, 0)
  927. XCTAssertEqual(created.count, 0)
  928. XCTAssertEqual(updated.count, 0)
  929. XCTAssertEqual(deleted.count, 0)
  930. }
  931. self.completion.fulfill()
  932. }
  933. wait(for: [completion], timeout: 2, enforceOrder: true)
  934. }
  935. func testEmptyWithNonDefaultQueryAnchor() {
  936. queryAnchor.anchorKey = 1
  937. carbStore.executeCarbQuery(fromQueryAnchor: queryAnchor, limit: limit) { result in
  938. switch result {
  939. case .failure(let error):
  940. XCTFail("Unexpected failure: \(error)")
  941. case .success(let anchor, let created, let updated, let deleted):
  942. XCTAssertEqual(anchor.anchorKey, 1)
  943. XCTAssertEqual(created.count, 0)
  944. XCTAssertEqual(updated.count, 0)
  945. XCTAssertEqual(deleted.count, 0)
  946. }
  947. self.completion.fulfill()
  948. }
  949. wait(for: [completion], timeout: 2, enforceOrder: true)
  950. }
  951. func testDataWithUnusedQueryAnchor() {
  952. let syncIdentifiers = [generateSyncIdentifier(), generateSyncIdentifier(), generateSyncIdentifier()]
  953. addData(withSyncIdentifiers: syncIdentifiers)
  954. carbStore.executeCarbQuery(fromQueryAnchor: queryAnchor, limit: limit) { result in
  955. switch result {
  956. case .failure(let error):
  957. XCTFail("Unexpected failure: \(error)")
  958. case .success(let anchor, let created, let updated, let deleted):
  959. XCTAssertEqual(anchor.anchorKey, 3)
  960. XCTAssertEqual(created.count, 1)
  961. XCTAssertEqual(created[0].syncIdentifier, syncIdentifiers[0])
  962. XCTAssertEqual(created[0].syncVersion, 0)
  963. XCTAssertEqual(updated.count, 1)
  964. XCTAssertEqual(updated[0].syncIdentifier, syncIdentifiers[1])
  965. XCTAssertEqual(updated[0].syncVersion, 1)
  966. XCTAssertEqual(deleted.count, 1)
  967. XCTAssertEqual(deleted[0].syncIdentifier, syncIdentifiers[2])
  968. XCTAssertEqual(deleted[0].syncVersion, 2)
  969. }
  970. self.completion.fulfill()
  971. }
  972. wait(for: [completion], timeout: 2, enforceOrder: true)
  973. }
  974. func testDataWithStaleQueryAnchor() {
  975. let syncIdentifiers = [generateSyncIdentifier(), generateSyncIdentifier(), generateSyncIdentifier()]
  976. addData(withSyncIdentifiers: syncIdentifiers)
  977. queryAnchor.anchorKey = 2
  978. carbStore.executeCarbQuery(fromQueryAnchor: queryAnchor, limit: limit) { result in
  979. switch result {
  980. case .failure(let error):
  981. XCTFail("Unexpected failure: \(error)")
  982. case .success(let anchor, let created, let updated, let deleted):
  983. XCTAssertEqual(anchor.anchorKey, 3)
  984. XCTAssertEqual(created.count, 0)
  985. XCTAssertEqual(updated.count, 0)
  986. XCTAssertEqual(deleted.count, 1)
  987. XCTAssertEqual(deleted[0].syncIdentifier, syncIdentifiers[2])
  988. XCTAssertEqual(deleted[0].syncVersion, 2)
  989. }
  990. self.completion.fulfill()
  991. }
  992. wait(for: [completion], timeout: 2, enforceOrder: true)
  993. }
  994. func testDataWithCurrentQueryAnchor() {
  995. let syncIdentifiers = [generateSyncIdentifier(), generateSyncIdentifier(), generateSyncIdentifier()]
  996. addData(withSyncIdentifiers: syncIdentifiers)
  997. queryAnchor.anchorKey = 3
  998. carbStore.executeCarbQuery(fromQueryAnchor: queryAnchor, limit: limit) { result in
  999. switch result {
  1000. case .failure(let error):
  1001. XCTFail("Unexpected failure: \(error)")
  1002. case .success(let anchor, let created, let updated, let deleted):
  1003. XCTAssertEqual(anchor.anchorKey, 3)
  1004. XCTAssertEqual(created.count, 0)
  1005. XCTAssertEqual(updated.count, 0)
  1006. XCTAssertEqual(deleted.count, 0)
  1007. }
  1008. self.completion.fulfill()
  1009. }
  1010. wait(for: [completion], timeout: 2, enforceOrder: true)
  1011. }
  1012. func testDataWithLimitZero() {
  1013. let syncIdentifiers = [generateSyncIdentifier(), generateSyncIdentifier(), generateSyncIdentifier()]
  1014. addData(withSyncIdentifiers: syncIdentifiers)
  1015. limit = 0
  1016. carbStore.executeCarbQuery(fromQueryAnchor: queryAnchor, limit: limit) { result in
  1017. switch result {
  1018. case .failure(let error):
  1019. XCTFail("Unexpected failure: \(error)")
  1020. case .success(let anchor, let created, let updated, let deleted):
  1021. XCTAssertEqual(anchor.anchorKey, 0)
  1022. XCTAssertEqual(created.count, 0)
  1023. XCTAssertEqual(updated.count, 0)
  1024. XCTAssertEqual(deleted.count, 0)
  1025. }
  1026. self.completion.fulfill()
  1027. }
  1028. wait(for: [completion], timeout: 2, enforceOrder: true)
  1029. }
  1030. func testDataWithLimitCoveredByData() {
  1031. let syncIdentifiers = [generateSyncIdentifier(), generateSyncIdentifier(), generateSyncIdentifier()]
  1032. addData(withSyncIdentifiers: syncIdentifiers)
  1033. limit = 2
  1034. carbStore.executeCarbQuery(fromQueryAnchor: queryAnchor, limit: limit) { result in
  1035. switch result {
  1036. case .failure(let error):
  1037. XCTFail("Unexpected failure: \(error)")
  1038. case .success(let anchor, let created, let updated, let deleted):
  1039. XCTAssertEqual(anchor.anchorKey, 2)
  1040. XCTAssertEqual(created.count, 1)
  1041. XCTAssertEqual(created[0].syncIdentifier, syncIdentifiers[0])
  1042. XCTAssertEqual(created[0].syncVersion, 0)
  1043. XCTAssertEqual(updated.count, 1)
  1044. XCTAssertEqual(updated[0].syncIdentifier, syncIdentifiers[1])
  1045. XCTAssertEqual(updated[0].syncVersion, 1)
  1046. XCTAssertEqual(deleted.count, 0)
  1047. }
  1048. self.completion.fulfill()
  1049. }
  1050. wait(for: [completion], timeout: 2, enforceOrder: true)
  1051. }
  1052. private func addData(withSyncIdentifiers syncIdentifiers: [String]) {
  1053. cacheStore.managedObjectContext.performAndWait {
  1054. for (index, syncIdentifier) in syncIdentifiers.enumerated() {
  1055. let cachedCarbObject = CachedCarbObject(context: self.cacheStore.managedObjectContext)
  1056. cachedCarbObject.createdByCurrentApp = true
  1057. cachedCarbObject.startDate = Date()
  1058. cachedCarbObject.uuid = UUID()
  1059. cachedCarbObject.syncIdentifier = syncIdentifier
  1060. cachedCarbObject.syncVersion = index
  1061. cachedCarbObject.operation = Operation(rawValue: index % Operation.allCases.count)!
  1062. cachedCarbObject.addedDate = Date()
  1063. self.cacheStore.save()
  1064. }
  1065. }
  1066. }
  1067. private func generateSyncIdentifier() -> String {
  1068. return UUID().uuidString
  1069. }
  1070. }
  1071. class CarbStoreCriticalEventLogTests: PersistenceControllerTestCase {
  1072. var carbStore: CarbStore!
  1073. var outputStream: MockOutputStream!
  1074. var progress: Progress!
  1075. override func setUp() {
  1076. super.setUp()
  1077. let objects = [SyncCarbObject(absorptionTime: nil, createdByCurrentApp: true, foodType: nil, grams: 11, startDate: dateFormatter.date(from: "2100-01-02T03:08:00Z")!, uuid: nil, provenanceIdentifier: Bundle.main.bundleIdentifier!, syncIdentifier: nil, syncVersion: nil, userCreatedDate: nil, userUpdatedDate: nil, userDeletedDate: nil, operation: .create, addedDate: dateFormatter.date(from: "2100-01-02T03:08:00Z")!, supercededDate: nil),
  1078. SyncCarbObject(absorptionTime: nil, createdByCurrentApp: true, foodType: nil, grams: 12, startDate: dateFormatter.date(from: "2100-01-02T03:10:00Z")!, uuid: nil, provenanceIdentifier: Bundle.main.bundleIdentifier!, syncIdentifier: nil, syncVersion: nil, userCreatedDate: nil, userUpdatedDate: nil, userDeletedDate: nil, operation: .create, addedDate: dateFormatter.date(from: "2100-01-02T03:10:00Z")!, supercededDate: nil),
  1079. SyncCarbObject(absorptionTime: nil, createdByCurrentApp: true, foodType: nil, grams: 13, startDate: dateFormatter.date(from: "2100-01-02T03:04:00Z")!, uuid: nil, provenanceIdentifier: Bundle.main.bundleIdentifier!, syncIdentifier: nil, syncVersion: nil, userCreatedDate: nil, userUpdatedDate: nil, userDeletedDate: nil, operation: .create, addedDate: dateFormatter.date(from: "2100-01-02T02:04:00Z")!, supercededDate: dateFormatter.date(from: "2100-01-02T03:04:00Z")!),
  1080. SyncCarbObject(absorptionTime: nil, createdByCurrentApp: true, foodType: nil, grams: 14, startDate: dateFormatter.date(from: "2100-01-02T03:06:00Z")!, uuid: nil, provenanceIdentifier: Bundle.main.bundleIdentifier!, syncIdentifier: nil, syncVersion: nil, userCreatedDate: nil, userUpdatedDate: nil, userDeletedDate: nil, operation: .create, addedDate: dateFormatter.date(from: "2100-01-02T03:06:00Z")!, supercededDate: nil),
  1081. SyncCarbObject(absorptionTime: nil, createdByCurrentApp: true, foodType: nil, grams: 15, startDate: dateFormatter.date(from: "2100-01-02T03:02:00Z")!, uuid: nil, provenanceIdentifier: Bundle.main.bundleIdentifier!, syncIdentifier: nil, syncVersion: nil, userCreatedDate: nil, userUpdatedDate: nil, userDeletedDate: nil, operation: .create, addedDate: dateFormatter.date(from: "2100-01-02T03:02:00Z")!, supercededDate: nil)]
  1082. carbStore = CarbStore(
  1083. healthStore: HKHealthStoreMock(),
  1084. cacheStore: cacheStore,
  1085. cacheLength: .hours(24),
  1086. defaultAbsorptionTimes: (fast: .minutes(30), medium: .hours(3), slow: .hours(5)),
  1087. observationInterval: 0,
  1088. provenanceIdentifier: Bundle.main.bundleIdentifier!)
  1089. let dispatchGroup = DispatchGroup()
  1090. dispatchGroup.enter()
  1091. carbStore.setSyncCarbObjects(objects) { error in
  1092. XCTAssertNil(error)
  1093. dispatchGroup.leave()
  1094. }
  1095. dispatchGroup.wait()
  1096. outputStream = MockOutputStream()
  1097. progress = Progress()
  1098. }
  1099. override func tearDown() {
  1100. carbStore = nil
  1101. super.tearDown()
  1102. }
  1103. func testExportProgressTotalUnitCount() {
  1104. switch carbStore.exportProgressTotalUnitCount(startDate: dateFormatter.date(from: "2100-01-02T03:03:00Z")!,
  1105. endDate: dateFormatter.date(from: "2100-01-02T03:09:00Z")!) {
  1106. case .failure(let error):
  1107. XCTFail("Unexpected failure: \(error)")
  1108. case .success(let progressTotalUnitCount):
  1109. XCTAssertEqual(progressTotalUnitCount, 3 * 1)
  1110. }
  1111. }
  1112. func testExportProgressTotalUnitCountEmpty() {
  1113. switch carbStore.exportProgressTotalUnitCount(startDate: dateFormatter.date(from: "2100-01-02T03:00:00Z")!,
  1114. endDate: dateFormatter.date(from: "2100-01-02T03:01:00Z")!) {
  1115. case .failure(let error):
  1116. XCTFail("Unexpected failure: \(error)")
  1117. case .success(let progressTotalUnitCount):
  1118. XCTAssertEqual(progressTotalUnitCount, 0)
  1119. }
  1120. }
  1121. func testExport() {
  1122. XCTAssertNil(carbStore.export(startDate: dateFormatter.date(from: "2100-01-02T03:03:00Z")!,
  1123. endDate: dateFormatter.date(from: "2100-01-02T03:09:00Z")!,
  1124. to: outputStream,
  1125. progress: progress))
  1126. XCTAssertEqual(outputStream.string, """
  1127. [
  1128. {"addedDate":"2100-01-02T03:08:00.000Z","anchorKey":1,"createdByCurrentApp":true,"grams":11,"operation":0,"provenanceIdentifier":"com.apple.dt.xctest.tool","startDate":"2100-01-02T03:08:00.000Z"},
  1129. {"addedDate":"2100-01-02T02:04:00.000Z","anchorKey":3,"createdByCurrentApp":true,"grams":13,"operation":0,"provenanceIdentifier":"com.apple.dt.xctest.tool","startDate":"2100-01-02T03:04:00.000Z","supercededDate":"2100-01-02T03:04:00.000Z"},
  1130. {"addedDate":"2100-01-02T03:06:00.000Z","anchorKey":4,"createdByCurrentApp":true,"grams":14,"operation":0,"provenanceIdentifier":"com.apple.dt.xctest.tool","startDate":"2100-01-02T03:06:00.000Z"}
  1131. ]
  1132. """
  1133. )
  1134. XCTAssertEqual(progress.completedUnitCount, 3 * 1)
  1135. }
  1136. func testExportEmpty() {
  1137. XCTAssertNil(carbStore.export(startDate: dateFormatter.date(from: "2100-01-02T03:00:00Z")!,
  1138. endDate: dateFormatter.date(from: "2100-01-02T03:01:00Z")!,
  1139. to: outputStream,
  1140. progress: progress))
  1141. XCTAssertEqual(outputStream.string, "[]")
  1142. XCTAssertEqual(progress.completedUnitCount, 0)
  1143. }
  1144. func testExportCancelled() {
  1145. progress.cancel()
  1146. XCTAssertEqual(carbStore.export(startDate: dateFormatter.date(from: "2100-01-02T03:03:00Z")!,
  1147. endDate: dateFormatter.date(from: "2100-01-02T03:09:00Z")!,
  1148. to: outputStream,
  1149. progress: progress) as? CriticalEventLogError, CriticalEventLogError.cancelled)
  1150. }
  1151. private let dateFormatter = ISO8601DateFormatter()
  1152. }