CarbStoreTests.swift 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448
  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 CarbStoreTests: PersistenceControllerTestCase, CarbStoreSyncDelegate {
  12. var carbStore: CarbStore!
  13. var healthStore: HKHealthStoreMock!
  14. override func setUp() {
  15. super.setUp()
  16. healthStore = HKHealthStoreMock()
  17. carbStore = CarbStore(healthStore: healthStore, cacheStore: cacheStore)
  18. carbStore.testQueryStore = healthStore
  19. carbStore.syncDelegate = self
  20. }
  21. override func tearDown() {
  22. carbStore.syncDelegate = nil
  23. carbStore = nil
  24. healthStore = nil
  25. uploadMessages = []
  26. deleteMessages = []
  27. uploadHandler = nil
  28. deleteHandler = nil
  29. super.tearDown()
  30. }
  31. // MARK: - CarbStoreSyncDelegate
  32. var uploadMessages: [(entries: [StoredCarbEntry], completion: ([StoredCarbEntry]) -> Void)] = []
  33. var uploadHandler: ((_: [StoredCarbEntry], _: ([StoredCarbEntry]) -> Void) -> Void)?
  34. func carbStore(_ carbStore: CarbStore, hasEntriesNeedingUpload entries: [StoredCarbEntry], completion: @escaping ([StoredCarbEntry]) -> Void) {
  35. uploadMessages.append((entries: entries, completion: completion))
  36. uploadHandler?(entries, completion)
  37. }
  38. var deleteMessages: [(entries: [DeletedCarbEntry], completion: ([DeletedCarbEntry]) -> Void)] = []
  39. var deleteHandler: ((_: [DeletedCarbEntry], _: ([DeletedCarbEntry]) -> Void) -> Void)?
  40. func carbStore(_ carbStore: CarbStore, hasDeletedEntries entries: [DeletedCarbEntry], completion: @escaping ([DeletedCarbEntry]) -> Void) {
  41. deleteMessages.append((entries: entries, completion: completion))
  42. deleteHandler?(entries, completion)
  43. }
  44. // MARK: -
  45. /// Adds a new entry, validates its uploading/uploaded transition
  46. func testAddAndSyncSuccessful() {
  47. let entry = NewCarbEntry(quantity: HKQuantity(unit: .gram(), doubleValue: 10), startDate: Date(), foodType: nil, absorptionTime: .hours(3))
  48. let addCarb = expectation(description: "Add carb entry")
  49. let uploading = expectation(description: "Sync delegate: upload")
  50. let uploaded = expectation(description: "Sync delegate: completed")
  51. // 2. assert sync delegate called
  52. uploadHandler = { (entries, completion) in
  53. XCTAssertEqual(1, entries.count)
  54. // 3. assert entered in db as uploading
  55. self.cacheStore.managedObjectContext.performAndWait {
  56. let objects: [CachedCarbObject] = self.cacheStore.managedObjectContext.all()
  57. XCTAssertEqual(1, objects.count)
  58. XCTAssertEqual(.uploading, objects.first!.uploadState)
  59. }
  60. uploading.fulfill()
  61. var entry = entries.first!
  62. entry.externalID = "1234"
  63. entry.isUploaded = true
  64. completion([entry])
  65. // 4. call delegate completion
  66. self.cacheStore.managedObjectContext.performAndWait {
  67. let objects: [CachedCarbObject] = self.cacheStore.managedObjectContext.all()
  68. // 5. assert entered in db as uploaded
  69. XCTAssertEqual(.uploaded, objects.first!.uploadState)
  70. uploaded.fulfill()
  71. }
  72. }
  73. // 1. Add carb
  74. carbStore.addCarbEntry(entry) { (result) in
  75. addCarb.fulfill()
  76. }
  77. wait(for: [addCarb, uploading, uploaded], timeout: 2, enforceOrder: true)
  78. }
  79. /// Adds a new entry, validates its uploading/notUploaded transition
  80. func testAddAndSyncFailed() {
  81. let entry = NewCarbEntry(quantity: HKQuantity(unit: .gram(), doubleValue: 10), startDate: Date(), foodType: nil, absorptionTime: .hours(3))
  82. let addCarb = expectation(description: "Add carb entry")
  83. let uploading = expectation(description: "Sync delegate: upload")
  84. let uploaded = expectation(description: "Sync delegate: completed")
  85. // 2. assert sync delegate called
  86. uploadHandler = { (entries, completion) in
  87. XCTAssertEqual(1, entries.count)
  88. // 3. assert entered in db as uploading
  89. self.cacheStore.managedObjectContext.performAndWait {
  90. let objects: [CachedCarbObject] = self.cacheStore.managedObjectContext.all()
  91. XCTAssertEqual(1, objects.count)
  92. XCTAssertEqual(.uploading, objects.first!.uploadState)
  93. }
  94. uploading.fulfill()
  95. completion(entries) // Not uploaded
  96. // 4. call delegate completion
  97. self.cacheStore.managedObjectContext.performAndWait {
  98. let objects: [CachedCarbObject] = self.cacheStore.managedObjectContext.all()
  99. // 5. assert entered in db as not uploaded
  100. XCTAssertEqual(.notUploaded, objects.first!.uploadState)
  101. uploaded.fulfill()
  102. }
  103. }
  104. // 1. Add carb
  105. carbStore.addCarbEntry(entry) { (result) in
  106. addCarb.fulfill()
  107. }
  108. wait(for: [addCarb, uploading, uploaded], timeout: 2, enforceOrder: true)
  109. }
  110. /// Adds two entries, validating their transition as the delegate calls completion out-of-order
  111. func testAddAndSyncInterleve() {
  112. let entry1 = NewCarbEntry(quantity: HKQuantity(unit: .gram(), doubleValue: 10), startDate: Date(), foodType: nil, absorptionTime: .hours(3))
  113. let entry2 = NewCarbEntry(quantity: HKQuantity(unit: .gram(), doubleValue: 10), startDate: Date(), foodType: nil, absorptionTime: .hours(3))
  114. let addCarb1 = expectation(description: "Add carb entry")
  115. let addCarb2 = expectation(description: "Add carb entry")
  116. let uploading1 = expectation(description: "Sync delegate: upload")
  117. let uploading2 = expectation(description: "Sync delegate: upload")
  118. let uploaded = expectation(description: "Sync delegate: completed")
  119. uploaded.expectedFulfillmentCount = 2
  120. // 2. assert sync delegate called
  121. uploadHandler = { (entries, completion) in
  122. XCTAssertEqual(1, entries.count)
  123. // 3. assert entered in db as uploading
  124. self.cacheStore.managedObjectContext.performAndWait {
  125. let objects: [CachedCarbObject] = self.cacheStore.managedObjectContext.all()
  126. XCTAssertEqual(self.uploadMessages.count, objects.count)
  127. for object in objects {
  128. XCTAssertEqual(.uploading, object.uploadState)
  129. }
  130. }
  131. switch self.uploadMessages.count {
  132. case 1:
  133. uploading1.fulfill()
  134. self.carbStore.addCarbEntry(entry2) { (result) in
  135. addCarb2.fulfill()
  136. }
  137. case 2:
  138. uploading2.fulfill()
  139. for index in (0...1).reversed() {
  140. var entry = self.uploadMessages[index].entries.first!
  141. entry.externalID = "\(index)"
  142. entry.isUploaded = true
  143. self.uploadMessages[index].completion([entry])
  144. // 4. call delegate completion
  145. self.cacheStore.managedObjectContext.performAndWait {
  146. let objects: [CachedCarbObject] = self.cacheStore.managedObjectContext.all()
  147. for object in objects {
  148. if object.externalID == "\(index)" {
  149. XCTAssertEqual(.uploaded, object.uploadState)
  150. }
  151. }
  152. uploaded.fulfill()
  153. }
  154. }
  155. default:
  156. XCTFail()
  157. }
  158. }
  159. // 1. Add carb 1
  160. carbStore.addCarbEntry(entry1) { (result) in
  161. addCarb1.fulfill()
  162. }
  163. wait(for: [addCarb1, uploading1, addCarb2, uploading2, uploaded], timeout: 2, enforceOrder: true)
  164. }
  165. /// Adds an entry with a failed upload, validates its requested again for sync on next entry
  166. func testAddAndSyncMultipleCallbacks() {
  167. let entry1 = NewCarbEntry(quantity: HKQuantity(unit: .gram(), doubleValue: 10), startDate: Date(), foodType: nil, absorptionTime: .hours(3))
  168. let entry2 = NewCarbEntry(quantity: HKQuantity(unit: .gram(), doubleValue: 10), startDate: Date(), foodType: nil, absorptionTime: .hours(3))
  169. let addCarb1 = expectation(description: "Add carb entry")
  170. let addCarb2 = expectation(description: "Add carb entry")
  171. let uploading1 = expectation(description: "Sync delegate: upload")
  172. let uploading2 = expectation(description: "Sync delegate: upload")
  173. let uploaded = expectation(description: "Sync delegate: completed")
  174. uploaded.expectedFulfillmentCount = 2
  175. // 2. assert sync delegate called
  176. uploadHandler = { (entries, completion) in
  177. // 3. assert entered in db as uploading
  178. self.cacheStore.managedObjectContext.performAndWait {
  179. let objects: [CachedCarbObject] = self.cacheStore.managedObjectContext.all()
  180. XCTAssertEqual(self.uploadMessages.count, objects.count)
  181. for object in objects {
  182. XCTAssertEqual(.uploading, object.uploadState)
  183. }
  184. }
  185. switch self.uploadMessages.count {
  186. case 1:
  187. XCTAssertEqual(1, entries.count)
  188. uploading1.fulfill()
  189. completion(entries) // Not uploaded
  190. self.carbStore.addCarbEntry(entry2) { (result) in
  191. addCarb2.fulfill()
  192. }
  193. case 2:
  194. XCTAssertEqual(2, entries.count)
  195. uploading2.fulfill()
  196. for index in (0...1).reversed() {
  197. var entry = self.uploadMessages[index].entries.first!
  198. entry.externalID = "\(index)"
  199. entry.isUploaded = true
  200. self.uploadMessages[index].completion([entry])
  201. // 4. call delegate completion
  202. self.cacheStore.managedObjectContext.performAndWait {
  203. let objects: [CachedCarbObject] = self.cacheStore.managedObjectContext.all()
  204. for object in objects {
  205. if object.externalID == "\(index)" {
  206. XCTAssertEqual(.uploaded, object.uploadState)
  207. }
  208. }
  209. uploaded.fulfill()
  210. }
  211. }
  212. default:
  213. XCTFail()
  214. }
  215. }
  216. // 1. Add carb 1
  217. carbStore.addCarbEntry(entry1) { (result) in
  218. addCarb1.fulfill()
  219. }
  220. wait(for: [addCarb1, uploading1, addCarb2, uploading2, uploaded], timeout: 2, enforceOrder: true)
  221. }
  222. /// Adds and uploads an entry, then modifies it and validates its re-upload
  223. func testModifyUploadedCarb() {
  224. let entry1 = NewCarbEntry(quantity: HKQuantity(unit: .gram(), doubleValue: 10), startDate: Date(), foodType: nil, absorptionTime: .hours(3))
  225. let entry2 = NewCarbEntry(quantity: HKQuantity(unit: .gram(), doubleValue: 15), startDate: Date(), foodType: nil, absorptionTime: .hours(3))
  226. let sample2 = HKQuantitySample(type: carbStore.sampleType as! HKQuantityType, quantity: entry2.quantity, start: entry2.startDate, end: entry2.endDate)
  227. let addCarb1 = expectation(description: "Add carb entry")
  228. let addCarb2 = expectation(description: "Add carb entry")
  229. let uploading1 = expectation(description: "Sync delegate: upload")
  230. let uploading2 = expectation(description: "Sync delegate: upload")
  231. let uploaded = expectation(description: "Sync delegate: completed")
  232. var lastUUID: UUID?
  233. // 2. assert sync delegate called
  234. uploadHandler = { (entries, completion) in
  235. // 3. assert entered in db as uploading
  236. self.cacheStore.managedObjectContext.performAndWait {
  237. let objects: [CachedCarbObject] = self.cacheStore.managedObjectContext.all()
  238. XCTAssertEqual(1, objects.count)
  239. XCTAssertEqual(.uploading, objects[0].uploadState)
  240. if let lastUUID = lastUUID {
  241. XCTAssertNotEqual(lastUUID, objects[0].uuid)
  242. }
  243. lastUUID = objects[0].uuid
  244. }
  245. switch self.uploadMessages.count {
  246. case 1:
  247. XCTAssertEqual(1, entries.count)
  248. uploading1.fulfill()
  249. var entry = entries.first!
  250. entry.externalID = "1234"
  251. entry.isUploaded = true
  252. completion([entry])
  253. self.healthStore.queryResults = (samples: [sample2], error: nil)
  254. self.carbStore.replaceCarbEntry(entries.first!, withEntry: entry2) { (result) in
  255. addCarb2.fulfill()
  256. self.healthStore.queryResults = nil
  257. }
  258. case 2:
  259. XCTAssertEqual(1, entries.count)
  260. XCTAssertEqual("1234", entries.first!.externalID)
  261. XCTAssertFalse(entries.first!.isUploaded)
  262. uploading2.fulfill()
  263. var entry = entries.first!
  264. entry.isUploaded = true
  265. completion([entry])
  266. // 4. call delegate completion
  267. self.cacheStore.managedObjectContext.performAndWait {
  268. let objects: [CachedCarbObject] = self.cacheStore.managedObjectContext.all()
  269. XCTAssertEqual(1, objects.count)
  270. for object in objects {
  271. XCTAssertEqual(.uploaded, object.uploadState)
  272. }
  273. uploaded.fulfill()
  274. }
  275. default:
  276. XCTFail()
  277. }
  278. }
  279. deleteHandler = { (entries, completion) in
  280. XCTFail()
  281. }
  282. // 1. Add carb 1
  283. carbStore.addCarbEntry(entry1) { (result) in
  284. addCarb1.fulfill()
  285. }
  286. wait(for: [addCarb1, uploading1, addCarb2, uploading2, uploaded], timeout: 2, enforceOrder: true)
  287. }
  288. /// Adds an entry, modifying it before upload completes, validating the delegate is then asked to delete it
  289. func testModifyNotUploadedCarb() {
  290. let entry1 = NewCarbEntry(quantity: HKQuantity(unit: .gram(), doubleValue: 10), startDate: Date(), foodType: nil, absorptionTime: .hours(3))
  291. let entry2 = NewCarbEntry(quantity: HKQuantity(unit: .gram(), doubleValue: 15), startDate: Date(), foodType: nil, absorptionTime: .hours(3))
  292. let sample2 = HKQuantitySample(type: carbStore.sampleType as! HKQuantityType, quantity: entry2.quantity, start: entry2.startDate, end: entry2.endDate)
  293. let addCarb1 = expectation(description: "Add carb entry")
  294. let addCarb2 = expectation(description: "Add carb entry")
  295. let uploading1 = expectation(description: "Sync delegate: upload")
  296. let uploading2 = expectation(description: "Sync delegate: upload")
  297. let uploaded = expectation(description: "Sync delegate: completed")
  298. let deleted = expectation(description: "Sync delegate: deleted")
  299. // 2. assert sync delegate called
  300. uploadHandler = { (entries, completion) in
  301. // 3. assert entered in db as uploading
  302. self.cacheStore.managedObjectContext.performAndWait {
  303. let objects: [CachedCarbObject] = self.cacheStore.managedObjectContext.all()
  304. XCTAssertEqual(1, objects.count)
  305. for object in objects {
  306. XCTAssertEqual(.uploading, object.uploadState)
  307. }
  308. }
  309. switch self.uploadMessages.count {
  310. case 1:
  311. XCTAssertEqual(1, entries.count)
  312. uploading1.fulfill()
  313. self.healthStore.queryResults = (samples: [sample2], error: nil)
  314. self.carbStore.replaceCarbEntry(entries.first!, withEntry: entry2) { (result) in
  315. addCarb2.fulfill()
  316. self.healthStore.queryResults = nil
  317. }
  318. case 2:
  319. XCTAssertEqual(1, entries.count)
  320. XCTAssertNil(entries.first!.externalID)
  321. XCTAssertFalse(entries.first!.isUploaded)
  322. uploading2.fulfill()
  323. var entry = entries.first!
  324. entry.externalID = "1234"
  325. entry.isUploaded = true
  326. completion([entry])
  327. entry = self.uploadMessages[0].entries.first!
  328. entry.externalID = "5678"
  329. entry.isUploaded = true
  330. self.uploadMessages[0].completion([entry])
  331. // 4. call delegate completion
  332. self.cacheStore.managedObjectContext.performAndWait {
  333. let objects: [CachedCarbObject] = self.cacheStore.managedObjectContext.all()
  334. XCTAssertEqual(1, objects.count)
  335. for object in objects {
  336. XCTAssertEqual("1234", object.externalID)
  337. XCTAssertEqual(.uploaded, object.uploadState)
  338. }
  339. uploaded.fulfill()
  340. }
  341. default:
  342. XCTFail()
  343. }
  344. }
  345. deleteHandler = { (entries, completion) in
  346. XCTAssertEqual(1, entries.count)
  347. var entry = entries[0]
  348. XCTAssertEqual("5678", entry.externalID)
  349. XCTAssertFalse(entry.isUploaded)
  350. self.cacheStore.managedObjectContext.performAndWait {
  351. let objects: [DeletedCarbObject] = self.cacheStore.managedObjectContext.all()
  352. XCTAssertEqual(1, objects.count)
  353. for object in objects {
  354. XCTAssertEqual("5678", object.externalID)
  355. XCTAssertEqual(.uploading, object.uploadState)
  356. }
  357. }
  358. entry.isUploaded = true
  359. completion([entry])
  360. self.cacheStore.managedObjectContext.performAndWait {
  361. let objects: [DeletedCarbObject] = self.cacheStore.managedObjectContext.all()
  362. XCTAssertEqual(0, objects.count)
  363. }
  364. deleted.fulfill()
  365. }
  366. // 1. Add carb 1
  367. carbStore.addCarbEntry(entry1) { (result) in
  368. addCarb1.fulfill()
  369. }
  370. wait(for: [addCarb1, uploading1, addCarb2, uploading2, uploaded, deleted], timeout: 2, enforceOrder: true)
  371. }
  372. }