From ea05314f86ba3364ebaa252915d4068b3fc1607f Mon Sep 17 00:00:00 2001 From: Joe Fabisevich Date: Mon, 15 Apr 2024 17:35:48 -0400 Subject: [PATCH 1/5] A fix for Boutique items not being removed from memory when chaining .remove(items) --- Sources/Boutique/Store.swift | 37 ++-- Tests/BoutiqueTests/AsyncStoreTests.swift | 156 ++++++++-------- .../BoutiqueTests/AsyncStoredValueTests.swift | 30 +-- Tests/BoutiqueTests/BoutiqueItem.swift | 24 +-- .../SecurelyStoredValueTests.swift | 28 +-- Tests/BoutiqueTests/StoreTests.swift | 171 ++++++++++-------- Tests/BoutiqueTests/StoredTests.swift | 156 ++++++++-------- Tests/BoutiqueTests/StoredValueTests.swift | 62 +++---- 8 files changed, 345 insertions(+), 319 deletions(-) diff --git a/Sources/Boutique/Store.swift b/Sources/Boutique/Store.swift index 1b12e03..d6e9325 100644 --- a/Sources/Boutique/Store.swift +++ b/Sources/Boutique/Store.swift @@ -385,14 +385,16 @@ public extension Store { // Internal versions of the `insert`, `remove`, and `removeAll` function code paths so we can avoid duplicating code. internal extension Store { func performInsert(_ item: Item, firstRemovingExistingItems existingItemsStrategy: ItemRemovalStrategy? = nil) async throws { + var currentItems = await self.items + if let strategy = existingItemsStrategy { - // Remove items from disk and memory based on the cache invalidation strategy - var removedItems: [Item] = [item] - try await self.removeItems(&removedItems, withStrategy: strategy) + var removedItems = [item] + try await self.removeItemsFromStorageEngine(&removedItems, withStrategy: strategy) + // If we remove this one it will error + self.removeItemsFromMemory(¤tItems, withStrategy: strategy, identifier: cacheIdentifier) } // Take the current items array and turn it into an OrderedDictionary. - let currentItems = await self.items let identifier = item[keyPath: self.cacheIdentifier] let currentItemsKeys = currentItems.map({ $0[keyPath: self.cacheIdentifier] }) var currentValuesDictionary = OrderedDictionary(uniqueKeys: currentItemsKeys, values: currentItems) @@ -407,11 +409,16 @@ internal extension Store { } func performInsert(_ items: [Item], firstRemovingExistingItems existingItemsStrategy: ItemRemovalStrategy? = nil) async throws { + var currentItems = await self.items if let strategy = existingItemsStrategy { // Remove items from disk and memory based on the cache invalidation strategy var removedItems = items - try await self.removeItems(&removedItems, withStrategy: strategy) + try await self.removeItemsFromStorageEngine(&removedItems, withStrategy: strategy) + // This one is fine to remove... but why? + // Is it the way we construct the items in the ordered dictionary? + // If so should the two just use the same approach — perhaps sharing all the same code except for the last call to `persistItem` vs. `persistItems`? + self.removeItemsFromMemory(¤tItems, withStrategy: strategy, identifier: cacheIdentifier) } var insertedItemsDictionary = OrderedDictionary() @@ -424,7 +431,6 @@ internal extension Store { } // Take the current items array and turn it into an OrderedDictionary. - let currentItems = await self.items let currentItemsKeys = currentItems.map({ $0[keyPath: self.cacheIdentifier] }) var currentValuesDictionary = OrderedDictionary(uniqueKeys: currentItemsKeys, values: currentItems) @@ -477,7 +483,6 @@ internal extension Store { } private extension Store { - func persistItem(_ item: Item) async throws { let cacheKey = CacheKey(item[keyPath: self.cacheIdentifier]) @@ -503,18 +508,12 @@ private extension Store { try await self.storageEngine.remove(keys: itemKeys) } - func removeItems(_ items: inout [Item], withStrategy strategy: ItemRemovalStrategy) async throws { + func removeItemsFromStorageEngine(_ items: inout [Item], withStrategy strategy: ItemRemovalStrategy) async throws { let itemsToRemove = strategy.removedItems(items) // If we're using the `.removeNone` strategy then there are no items to invalidate and we can return early guard itemsToRemove.count != 0 else { return } - items = items.filter { item in - !itemsToRemove.contains(where: { - $0[keyPath: cacheIdentifier] == item[keyPath: cacheIdentifier] - } - )} - let itemKeys = itemsToRemove.map({ CacheKey(verbatim: $0[keyPath: self.cacheIdentifier]) }) if itemKeys.count == 1 { @@ -523,4 +522,14 @@ private extension Store { try await self.storageEngine.remove(keys: itemKeys) } } + + func removeItemsFromMemory(_ items: inout [Item], withStrategy strategy: ItemRemovalStrategy, identifier: KeyPath) { + let itemsToRemove = strategy.removedItems(items) + + items = items.filter { item in + !itemsToRemove.contains(where: { + $0[keyPath: identifier] == item[keyPath: identifier] + } + )} + } } diff --git a/Tests/BoutiqueTests/AsyncStoreTests.swift b/Tests/BoutiqueTests/AsyncStoreTests.swift index 7b9d5cc..6cb6249 100644 --- a/Tests/BoutiqueTests/AsyncStoreTests.swift +++ b/Tests/BoutiqueTests/AsyncStoreTests.swift @@ -20,80 +20,80 @@ final class AsyncStoreTests: XCTestCase { @MainActor func testInsertingItem() async throws { - try await asyncStore.insert(BoutiqueItem.coat) - XCTAssertTrue(asyncStore.items.contains(BoutiqueItem.coat)) + try await asyncStore.insert(.coat) + XCTAssertTrue(asyncStore.items.contains(.coat)) - try await asyncStore.insert(BoutiqueItem.belt) - XCTAssertTrue(asyncStore.items.contains(BoutiqueItem.belt)) + try await asyncStore.insert(.belt) + XCTAssertTrue(asyncStore.items.contains(.belt)) XCTAssertEqual(asyncStore.items.count, 2) } @MainActor func testInsertingItems() async throws { - try await asyncStore.insert([BoutiqueItem.coat, BoutiqueItem.sweater, BoutiqueItem.sweater, BoutiqueItem.purse]) - XCTAssertTrue(asyncStore.items.contains(BoutiqueItem.coat)) - XCTAssertTrue(asyncStore.items.contains(BoutiqueItem.sweater)) - XCTAssertTrue(asyncStore.items.contains(BoutiqueItem.purse)) + try await asyncStore.insert([.coat, .sweater, .sweater, .purse]) + XCTAssertTrue(asyncStore.items.contains(.coat)) + XCTAssertTrue(asyncStore.items.contains(.sweater)) + XCTAssertTrue(asyncStore.items.contains(.purse)) } @MainActor func testInsertingDuplicateItems() async throws { XCTAssertTrue(asyncStore.items.isEmpty) - try await asyncStore.insert(BoutiqueItem.allItems) + try await asyncStore.insert(.allItems) XCTAssertEqual(asyncStore.items.count, 4) } @MainActor func testReadingItems() async throws { - try await asyncStore.insert(BoutiqueItem.allItems) + try await asyncStore.insert(.allItems) - XCTAssertEqual(asyncStore.items[0], BoutiqueItem.coat) - XCTAssertEqual(asyncStore.items[1], BoutiqueItem.sweater) - XCTAssertEqual(asyncStore.items[2], BoutiqueItem.purse) - XCTAssertEqual(asyncStore.items[3], BoutiqueItem.belt) + XCTAssertEqual(asyncStore.items[0], .coat) + XCTAssertEqual(asyncStore.items[1], .sweater) + XCTAssertEqual(asyncStore.items[2], .purse) + XCTAssertEqual(asyncStore.items[3], .belt) XCTAssertEqual(asyncStore.items.count, 4) } @MainActor func testReadingPersistedItems() async throws { - try await asyncStore.insert(BoutiqueItem.allItems) + try await asyncStore.insert(.allItems) // The new store has to fetch items from disk. let newStore = try await Store( storage: SQLiteStorageEngine.default(appendingPath: "Tests"), cacheIdentifier: \.merchantID) - XCTAssertEqual(newStore.items[0], BoutiqueItem.coat) - XCTAssertEqual(newStore.items[1], BoutiqueItem.sweater) - XCTAssertEqual(newStore.items[2], BoutiqueItem.purse) - XCTAssertEqual(newStore.items[3], BoutiqueItem.belt) + XCTAssertEqual(newStore.items[0], .coat) + XCTAssertEqual(newStore.items[1], .sweater) + XCTAssertEqual(newStore.items[2], .purse) + XCTAssertEqual(newStore.items[3], .belt) XCTAssertEqual(newStore.items.count, 4) } @MainActor func testRemovingItems() async throws { - try await asyncStore.insert(BoutiqueItem.allItems) - try await asyncStore.remove(BoutiqueItem.coat) + try await asyncStore.insert(.allItems) + try await asyncStore.remove(.coat) - XCTAssertFalse(asyncStore.items.contains(BoutiqueItem.coat)) + XCTAssertFalse(asyncStore.items.contains(.coat)) - XCTAssertTrue(asyncStore.items.contains(BoutiqueItem.sweater)) - XCTAssertTrue(asyncStore.items.contains(BoutiqueItem.purse)) + XCTAssertTrue(asyncStore.items.contains(.sweater)) + XCTAssertTrue(asyncStore.items.contains(.purse)) - try await asyncStore.remove([BoutiqueItem.sweater, BoutiqueItem.purse]) - XCTAssertFalse(asyncStore.items.contains(BoutiqueItem.sweater)) - XCTAssertFalse(asyncStore.items.contains(BoutiqueItem.purse)) + try await asyncStore.remove([.sweater, .purse]) + XCTAssertFalse(asyncStore.items.contains(.sweater)) + XCTAssertFalse(asyncStore.items.contains(.purse)) } @MainActor func testRemoveAll() async throws { - try await asyncStore.insert(BoutiqueItem.coat) + try await asyncStore.insert(.coat) XCTAssertEqual(asyncStore.items.count, 1) try await asyncStore.removeAll() - try await asyncStore.insert(BoutiqueItem.uniqueItems) + try await asyncStore.insert(.uniqueItems) XCTAssertEqual(asyncStore.items.count, 4) try await asyncStore.removeAll() XCTAssertTrue(asyncStore.items.isEmpty) @@ -101,112 +101,112 @@ final class AsyncStoreTests: XCTestCase { @MainActor func testChainingInsertOperations() async throws { - try await asyncStore.insert(BoutiqueItem.uniqueItems) + try await asyncStore.insert(.uniqueItems) try await asyncStore - .remove(BoutiqueItem.coat) - .insert(BoutiqueItem.belt) - .insert(BoutiqueItem.belt) + .remove(.coat) + .insert(.belt) + .insert(.belt) .run() XCTAssertEqual(asyncStore.items.count, 3) - XCTAssertTrue(asyncStore.items.contains(BoutiqueItem.sweater)) - XCTAssertTrue(asyncStore.items.contains(BoutiqueItem.purse)) - XCTAssertTrue(asyncStore.items.contains(BoutiqueItem.belt)) - XCTAssertFalse(asyncStore.items.contains(BoutiqueItem.coat)) + XCTAssertTrue(asyncStore.items.contains(.sweater)) + XCTAssertTrue(asyncStore.items.contains(.purse)) + XCTAssertTrue(asyncStore.items.contains(.belt)) + XCTAssertFalse(asyncStore.items.contains(.coat)) try await asyncStore.removeAll() try await asyncStore - .insert(BoutiqueItem.belt) - .insert(BoutiqueItem.coat) - .remove([BoutiqueItem.belt]) - .insert(BoutiqueItem.sweater) + .insert(.belt) + .insert(.coat) + .remove([.belt]) + .insert(.sweater) .run() XCTAssertEqual(asyncStore.items.count, 2) - XCTAssertTrue(asyncStore.items.contains(BoutiqueItem.coat)) - XCTAssertTrue(asyncStore.items.contains(BoutiqueItem.sweater)) - XCTAssertFalse(asyncStore.items.contains(BoutiqueItem.belt)) + XCTAssertTrue(asyncStore.items.contains(.coat)) + XCTAssertTrue(asyncStore.items.contains(.sweater)) + XCTAssertFalse(asyncStore.items.contains(.belt)) try await asyncStore - .insert(BoutiqueItem.belt) - .insert(BoutiqueItem.coat) - .insert(BoutiqueItem.purse) - .remove([BoutiqueItem.belt, .coat]) - .insert(BoutiqueItem.sweater) + .insert(.belt) + .insert(.coat) + .insert(.purse) + .remove([.belt, .coat]) + .insert(.sweater) .run() XCTAssertEqual(asyncStore.items.count, 2) - XCTAssertTrue(asyncStore.items.contains(BoutiqueItem.sweater)) - XCTAssertTrue(asyncStore.items.contains(BoutiqueItem.purse)) - XCTAssertFalse(asyncStore.items.contains(BoutiqueItem.coat)) - XCTAssertFalse(asyncStore.items.contains(BoutiqueItem.belt)) + XCTAssertTrue(asyncStore.items.contains(.sweater)) + XCTAssertTrue(asyncStore.items.contains(.purse)) + XCTAssertFalse(asyncStore.items.contains(.coat)) + XCTAssertFalse(asyncStore.items.contains(.belt)) try await asyncStore.removeAll() try await asyncStore - .insert(BoutiqueItem.coat) - .insert([BoutiqueItem.purse, BoutiqueItem.belt]) + .insert(.coat) + .insert([.purse, .belt]) .run() XCTAssertEqual(asyncStore.items.count, 3) - XCTAssertTrue(asyncStore.items.contains(BoutiqueItem.purse)) - XCTAssertTrue(asyncStore.items.contains(BoutiqueItem.belt)) - XCTAssertTrue(asyncStore.items.contains(BoutiqueItem.coat)) + XCTAssertTrue(asyncStore.items.contains(.purse)) + XCTAssertTrue(asyncStore.items.contains(.belt)) + XCTAssertTrue(asyncStore.items.contains(.coat)) } @MainActor func testChainingRemoveOperations() async throws { try await asyncStore - .insert(BoutiqueItem.uniqueItems) - .remove(BoutiqueItem.belt) - .remove(BoutiqueItem.purse) + .insert(.uniqueItems) + .remove(.belt) + .remove(.purse) .run() XCTAssertEqual(asyncStore.items.count, 2) - XCTAssertTrue(asyncStore.items.contains(BoutiqueItem.sweater)) - XCTAssertTrue(asyncStore.items.contains(BoutiqueItem.coat)) + XCTAssertTrue(asyncStore.items.contains(.sweater)) + XCTAssertTrue(asyncStore.items.contains(.coat)) - try await asyncStore.insert(BoutiqueItem.uniqueItems) + try await asyncStore.insert(.uniqueItems) XCTAssertEqual(asyncStore.items.count, 4) try await asyncStore - .remove([BoutiqueItem.sweater, BoutiqueItem.coat]) - .remove(BoutiqueItem.belt) + .remove([.sweater, .coat]) + .remove(.belt) .run() XCTAssertEqual(asyncStore.items.count, 1) - XCTAssertTrue(asyncStore.items.contains(BoutiqueItem.purse)) + XCTAssertTrue(asyncStore.items.contains(.purse)) try await asyncStore .removeAll() - .insert(BoutiqueItem.belt) + .insert(.belt) .run() XCTAssertEqual(asyncStore.items.count, 1) - XCTAssertTrue(asyncStore.items.contains(BoutiqueItem.belt)) + XCTAssertTrue(asyncStore.items.contains(.belt)) try await asyncStore .removeAll() - .remove(BoutiqueItem.belt) - .insert(BoutiqueItem.belt) + .remove(.belt) + .insert(.belt) .run() XCTAssertEqual(asyncStore.items.count, 1) - XCTAssertTrue(asyncStore.items.contains(BoutiqueItem.belt)) + XCTAssertTrue(asyncStore.items.contains(.belt)) } @MainActor func testChainingOperationsDontExecuteUnlessRun() async throws { let operation = try await asyncStore - .insert(BoutiqueItem.coat) - .insert([BoutiqueItem.purse, BoutiqueItem.belt]) + .insert(.coat) + .insert([.purse, .belt]) XCTAssertEqual(asyncStore.items.count, 0) - XCTAssertFalse(asyncStore.items.contains(BoutiqueItem.purse)) - XCTAssertFalse(asyncStore.items.contains(BoutiqueItem.belt)) - XCTAssertFalse(asyncStore.items.contains(BoutiqueItem.coat)) + XCTAssertFalse(asyncStore.items.contains(.purse)) + XCTAssertFalse(asyncStore.items.contains(.belt)) + XCTAssertFalse(asyncStore.items.contains(.coat)) // Adding this line to get rid of the error about // `operation` being unused, given that's the point of the test. @@ -215,7 +215,7 @@ final class AsyncStoreTests: XCTestCase { @MainActor func testPublishedItemsSubscription() async throws { - let uniqueItems = BoutiqueItem.uniqueItems + let uniqueItems: [BoutiqueItem] = .uniqueItems let expectation = XCTestExpectation(description: "uniqueItems is published and read") asyncStore.$items diff --git a/Tests/BoutiqueTests/AsyncStoredValueTests.swift b/Tests/BoutiqueTests/AsyncStoredValueTests.swift index af34745..56ac4d0 100644 --- a/Tests/BoutiqueTests/AsyncStoredValueTests.swift +++ b/Tests/BoutiqueTests/AsyncStoredValueTests.swift @@ -30,13 +30,13 @@ final class AsyncStoredValueTests: XCTestCase { } func testStorageEngineBackedStoredValue() async throws { - XCTAssertEqual(self.storedItem, BoutiqueItem.coat) + XCTAssertEqual(self.storedItem, .coat) - try await self.$storedItem.set(BoutiqueItem.belt) - XCTAssertEqual(self.storedItem, BoutiqueItem.belt) + try await self.$storedItem.set(.belt) + XCTAssertEqual(self.storedItem, .belt) try await self.$storedItem.reset() - XCTAssertEqual(self.storedItem, BoutiqueItem.coat) + XCTAssertEqual(self.storedItem, .coat) } func testBoolAsyncStoredValue() async throws { @@ -81,11 +81,11 @@ final class AsyncStoredValueTests: XCTestCase { func testStoredArrayValueAppend() async throws { XCTAssertEqual(self.storedArrayValue, []) - try await self.$storedArrayValue.append(BoutiqueItem.sweater) - XCTAssertEqual(self.storedArrayValue, [BoutiqueItem.sweater]) + try await self.$storedArrayValue.append(.sweater) + XCTAssertEqual(self.storedArrayValue, [.sweater]) - try await self.$storedArrayValue.append(BoutiqueItem.belt) - XCTAssertEqual(self.storedArrayValue, [BoutiqueItem.sweater, BoutiqueItem.belt]) + try await self.$storedArrayValue.append(.belt) + XCTAssertEqual(self.storedArrayValue, [.sweater, .belt]) } func testStoredArrayValueTogglePresence() async throws { @@ -106,10 +106,10 @@ final class AsyncStoredValueTests: XCTestCase { } func testStoredBinding() async throws { - XCTAssertEqual(self.$storedBinding.binding.wrappedValue, Binding.constant(BoutiqueItem.sweater).wrappedValue) + XCTAssertEqual(self.$storedBinding.binding.wrappedValue, Binding.constant(.sweater).wrappedValue) - try await self.$storedBinding.set(BoutiqueItem.belt) - XCTAssertEqual(self.$storedBinding.binding.wrappedValue, Binding.constant(BoutiqueItem.belt).wrappedValue) + try await self.$storedBinding.set(.belt) + XCTAssertEqual(self.$storedBinding.binding.wrappedValue, Binding.constant(.belt).wrappedValue) } func testStoredValuePublishedSubscription() async throws { @@ -122,15 +122,15 @@ final class AsyncStoredValueTests: XCTestCase { values.append(item) if values.count == 4 { - XCTAssertEqual(values, [BoutiqueItem.coat, .sweater, .purse, .belt]) + XCTAssertEqual(values, [.coat, .sweater, .purse, .belt]) expectation.fulfill() } }) .store(in: &cancellables) - try await self.$storedItem.set(BoutiqueItem.sweater) - try await self.$storedItem.set(BoutiqueItem.purse) - try await self.$storedItem.set(BoutiqueItem.belt) + try await self.$storedItem.set(.sweater) + try await self.$storedItem.set(.purse) + try await self.$storedItem.set(.belt) wait(for: [expectation], timeout: 1) } diff --git a/Tests/BoutiqueTests/BoutiqueItem.swift b/Tests/BoutiqueTests/BoutiqueItem.swift index 1fff748..02c2b5d 100644 --- a/Tests/BoutiqueTests/BoutiqueItem.swift +++ b/Tests/BoutiqueTests/BoutiqueItem.swift @@ -34,19 +34,21 @@ extension BoutiqueItem { merchantID: "4", value: "Belt" ) +} - static let allItems = [ - BoutiqueItem.coat, - BoutiqueItem.sweater, - BoutiqueItem.purse, - BoutiqueItem.belt, - BoutiqueItem.duplicateBelt +extension [BoutiqueItem] { + static let allItems: [BoutiqueItem] = [ + .coat, + .sweater, + .purse, + .belt, + .duplicateBelt ] - static let uniqueItems = [ - BoutiqueItem.coat, - BoutiqueItem.sweater, - BoutiqueItem.purse, - BoutiqueItem.belt, + static let uniqueItems: [BoutiqueItem] = [ + .coat, + .sweater, + .purse, + .belt, ] } diff --git a/Tests/BoutiqueTests/SecurelyStoredValueTests.swift b/Tests/BoutiqueTests/SecurelyStoredValueTests.swift index 8f53080..7ca2fb5 100644 --- a/Tests/BoutiqueTests/SecurelyStoredValueTests.swift +++ b/Tests/BoutiqueTests/SecurelyStoredValueTests.swift @@ -123,11 +123,11 @@ final class SecurelyStoredValueTests: XCTestCase { func testStoredDictionary() async throws { XCTAssertEqual(self.storedDictionary, nil) - try await self.$storedDictionary.update(key: BoutiqueItem.sweater.merchantID, value: BoutiqueItem.sweater) - XCTAssertEqual(self.storedDictionary, [BoutiqueItem.sweater.merchantID : BoutiqueItem.sweater]) + try await self.$storedDictionary.update(key: BoutiqueItem.sweater.merchantID, value: .sweater) + XCTAssertEqual(self.storedDictionary, [BoutiqueItem.sweater.merchantID : .sweater]) try await self.$storedDictionary.update(key: BoutiqueItem.belt.merchantID, value: nil) - XCTAssertEqual(self.storedDictionary, [BoutiqueItem.sweater.merchantID : BoutiqueItem.sweater]) + XCTAssertEqual(self.storedDictionary, [BoutiqueItem.sweater.merchantID : .sweater]) try await self.$storedDictionary.update(key: BoutiqueItem.sweater.merchantID, value: nil) XCTAssertEqual(self.storedDictionary, [:]) @@ -136,11 +136,11 @@ final class SecurelyStoredValueTests: XCTestCase { func testStoredArrayValueAppend() async throws { XCTAssertEqual(self.storedArray, nil) - try await self.$storedArray.append(BoutiqueItem.sweater) - XCTAssertEqual(self.storedArray, [BoutiqueItem.sweater]) + try await self.$storedArray.append(.sweater) + XCTAssertEqual(self.storedArray, [.sweater]) - try await self.$storedArray.append(BoutiqueItem.belt) - XCTAssertEqual(self.storedArray, [BoutiqueItem.sweater, BoutiqueItem.belt]) + try await self.$storedArray.append(.belt) + XCTAssertEqual(self.storedArray, [.sweater, .belt]) } @MainActor @@ -150,8 +150,8 @@ final class SecurelyStoredValueTests: XCTestCase { // Using wrappedValue for our tests to work around the fact that Binding doesn't conform to Equatable XCTAssertEqual(self.$storedBinding.binding.wrappedValue, nil) - try self.$storedBinding.set(BoutiqueItem.belt) - XCTAssertEqual(self.$storedBinding.binding.wrappedValue, Binding.constant(BoutiqueItem.belt).wrappedValue) + try self.$storedBinding.set(.belt) + XCTAssertEqual(self.$storedBinding.binding.wrappedValue, Binding.constant(.belt).wrappedValue) } func testPublishedValueSubscription() async throws { @@ -166,16 +166,16 @@ final class SecurelyStoredValueTests: XCTestCase { } if values.count == 4 { - XCTAssertEqual(values, [BoutiqueItem.coat, .purse, .sweater, .belt]) + XCTAssertEqual(values, [.coat, .purse, .sweater, .belt]) expectation.fulfill() } }) .store(in: &cancellables) - try await self.$storedItem.set(BoutiqueItem.coat) - try await self.$storedItem.set(BoutiqueItem.purse) - try await self.$storedItem.set(BoutiqueItem.sweater) - try await self.$storedItem.set(BoutiqueItem.belt) + try await self.$storedItem.set(.coat) + try await self.$storedItem.set(.purse) + try await self.$storedItem.set(.sweater) + try await self.$storedItem.set(.belt) await fulfillment(of: [expectation], timeout: 1) } diff --git a/Tests/BoutiqueTests/StoreTests.swift b/Tests/BoutiqueTests/StoreTests.swift index 6ca57d8..a37d64d 100644 --- a/Tests/BoutiqueTests/StoreTests.swift +++ b/Tests/BoutiqueTests/StoreTests.swift @@ -26,80 +26,80 @@ final class StoreTests: XCTestCase { @MainActor func testInsertingItem() async throws { - try await store.insert(BoutiqueItem.coat) - XCTAssertTrue(store.items.contains(BoutiqueItem.coat)) + try await store.insert(.coat) + XCTAssertTrue(store.items.contains(.coat)) - try await store.insert(BoutiqueItem.belt) - XCTAssertTrue(store.items.contains(BoutiqueItem.belt)) + try await store.insert(.belt) + XCTAssertTrue(store.items.contains(.belt)) XCTAssertEqual(store.items.count, 2) } @MainActor func testInsertingItems() async throws { - try await store.insert([BoutiqueItem.coat, BoutiqueItem.sweater, BoutiqueItem.sweater, BoutiqueItem.purse]) - XCTAssertTrue(store.items.contains(BoutiqueItem.coat)) - XCTAssertTrue(store.items.contains(BoutiqueItem.sweater)) - XCTAssertTrue(store.items.contains(BoutiqueItem.purse)) + try await store.insert([.coat, .sweater, .sweater, .purse]) + XCTAssertTrue(store.items.contains(.coat)) + XCTAssertTrue(store.items.contains(.sweater)) + XCTAssertTrue(store.items.contains(.purse)) } @MainActor func testInsertingDuplicateItems() async throws { XCTAssertTrue(store.items.isEmpty) - try await store.insert(BoutiqueItem.allItems) + try await store.insert(.allItems) XCTAssertEqual(store.items.count, 4) } @MainActor func testReadingItems() async throws { - try await store.insert(BoutiqueItem.allItems) + try await store.insert(.allItems) - XCTAssertEqual(store.items[0], BoutiqueItem.coat) - XCTAssertEqual(store.items[1], BoutiqueItem.sweater) - XCTAssertEqual(store.items[2], BoutiqueItem.purse) - XCTAssertEqual(store.items[3], BoutiqueItem.belt) + XCTAssertEqual(store.items[0], .coat) + XCTAssertEqual(store.items[1], .sweater) + XCTAssertEqual(store.items[2], .purse) + XCTAssertEqual(store.items[3], .belt) XCTAssertEqual(store.items.count, 4) } @MainActor func testReadingPersistedItems() async throws { - try await store.insert(BoutiqueItem.allItems) + try await store.insert(.allItems) // The new store has to fetch items from disk. let newStore = try await Store( storage: SQLiteStorageEngine.default(appendingPath: "Tests"), cacheIdentifier: \.merchantID) - XCTAssertEqual(newStore.items[0], BoutiqueItem.coat) - XCTAssertEqual(newStore.items[1], BoutiqueItem.sweater) - XCTAssertEqual(newStore.items[2], BoutiqueItem.purse) - XCTAssertEqual(newStore.items[3], BoutiqueItem.belt) + XCTAssertEqual(newStore.items[0], .coat) + XCTAssertEqual(newStore.items[1], .sweater) + XCTAssertEqual(newStore.items[2], .purse) + XCTAssertEqual(newStore.items[3], .belt) XCTAssertEqual(newStore.items.count, 4) } @MainActor func testRemovingItems() async throws { - try await store.insert(BoutiqueItem.allItems) - try await store.remove(BoutiqueItem.coat) + try await store.insert(.allItems) + try await store.remove(.coat) - XCTAssertFalse(store.items.contains(BoutiqueItem.coat)) + XCTAssertFalse(store.items.contains(.coat)) - XCTAssertTrue(store.items.contains(BoutiqueItem.sweater)) - XCTAssertTrue(store.items.contains(BoutiqueItem.purse)) + XCTAssertTrue(store.items.contains(.sweater)) + XCTAssertTrue(store.items.contains(.purse)) - try await store.remove([BoutiqueItem.sweater, BoutiqueItem.purse]) - XCTAssertFalse(store.items.contains(BoutiqueItem.sweater)) - XCTAssertFalse(store.items.contains(BoutiqueItem.purse)) + try await store.remove([.sweater, .purse]) + XCTAssertFalse(store.items.contains(.sweater)) + XCTAssertFalse(store.items.contains(.purse)) } @MainActor func testRemoveAll() async throws { - try await store.insert(BoutiqueItem.coat) + try await store.insert(.coat) XCTAssertEqual(store.items.count, 1) try await store.removeAll() - try await store.insert(BoutiqueItem.uniqueItems) + try await store.insert(.uniqueItems) XCTAssertEqual(store.items.count, 4) try await store.removeAll() XCTAssertTrue(store.items.isEmpty) @@ -107,112 +107,127 @@ final class StoreTests: XCTestCase { @MainActor func testChainingInsertOperations() async throws { - try await store.insert(BoutiqueItem.uniqueItems) + try await store.insert(.uniqueItems) try await store - .remove(BoutiqueItem.coat) - .insert(BoutiqueItem.belt) - .insert(BoutiqueItem.belt) + .remove(.coat) + .insert(.belt) + .insert(.belt) .run() XCTAssertEqual(store.items.count, 3) - XCTAssertTrue(store.items.contains(BoutiqueItem.sweater)) - XCTAssertTrue(store.items.contains(BoutiqueItem.purse)) - XCTAssertTrue(store.items.contains(BoutiqueItem.belt)) - XCTAssertFalse(store.items.contains(BoutiqueItem.coat)) + XCTAssertTrue(store.items.contains(.sweater)) + XCTAssertTrue(store.items.contains(.purse)) + XCTAssertTrue(store.items.contains(.belt)) + XCTAssertFalse(store.items.contains(.coat)) try await store.removeAll() try await store - .insert(BoutiqueItem.belt) - .insert(BoutiqueItem.coat) - .remove([BoutiqueItem.belt]) - .insert(BoutiqueItem.sweater) + .insert(.belt) + .insert(.coat) + .remove([.belt]) + .insert(.sweater) .run() XCTAssertEqual(store.items.count, 2) - XCTAssertTrue(store.items.contains(BoutiqueItem.coat)) - XCTAssertTrue(store.items.contains(BoutiqueItem.sweater)) - XCTAssertFalse(store.items.contains(BoutiqueItem.belt)) + XCTAssertTrue(store.items.contains(.coat)) + XCTAssertTrue(store.items.contains(.sweater)) + XCTAssertFalse(store.items.contains(.belt)) + + try await store.removeAll() + + try await store + .insert([.belt, .coat]) + .insert(.sweater) + .remove([.belt]) + .run() + + XCTAssertEqual(store.items.count, 2) + XCTAssertTrue(store.items.contains(.coat)) + XCTAssertTrue(store.items.contains(.sweater)) + XCTAssertFalse(store.items.contains(.belt)) + + try await store.removeAll() try await store - .insert(BoutiqueItem.belt) - .insert(BoutiqueItem.coat) - .insert(BoutiqueItem.purse) - .remove([BoutiqueItem.belt, .coat]) - .insert(BoutiqueItem.sweater) + .insert(.belt) + .insert(.coat) + .insert(.purse) + .remove([.belt, .coat]) + .insert(.sweater) .run() XCTAssertEqual(store.items.count, 2) - XCTAssertTrue(store.items.contains(BoutiqueItem.sweater)) - XCTAssertTrue(store.items.contains(BoutiqueItem.purse)) - XCTAssertFalse(store.items.contains(BoutiqueItem.coat)) - XCTAssertFalse(store.items.contains(BoutiqueItem.belt)) + XCTAssertTrue(store.items.contains(.sweater)) + XCTAssertTrue(store.items.contains(.purse)) + XCTAssertFalse(store.items.contains(.coat)) + XCTAssertFalse(store.items.contains(.belt)) try await store.removeAll() try await store - .insert(BoutiqueItem.coat) - .insert([BoutiqueItem.purse, BoutiqueItem.belt]) + .insert(.coat) + .insert([.purse, .belt]) .run() XCTAssertEqual(store.items.count, 3) - XCTAssertTrue(store.items.contains(BoutiqueItem.purse)) - XCTAssertTrue(store.items.contains(BoutiqueItem.belt)) - XCTAssertTrue(store.items.contains(BoutiqueItem.coat)) + XCTAssertTrue(store.items.contains(.purse)) + XCTAssertTrue(store.items.contains(.belt)) + XCTAssertTrue(store.items.contains(.coat)) } @MainActor func testChainingRemoveOperations() async throws { try await store - .insert(BoutiqueItem.uniqueItems) - .remove(BoutiqueItem.belt) - .remove(BoutiqueItem.purse) + .insert(.uniqueItems) + .remove(.belt) + .remove(.purse) .run() XCTAssertEqual(store.items.count, 2) - XCTAssertTrue(store.items.contains(BoutiqueItem.sweater)) - XCTAssertTrue(store.items.contains(BoutiqueItem.coat)) + XCTAssertTrue(store.items.contains(.sweater)) + XCTAssertTrue(store.items.contains(.coat)) - try await store.insert(BoutiqueItem.uniqueItems) + try await store.insert(.uniqueItems) XCTAssertEqual(store.items.count, 4) try await store - .remove([BoutiqueItem.sweater, BoutiqueItem.coat]) - .remove(BoutiqueItem.belt) + .remove([.sweater, .coat]) + .remove(.belt) .run() XCTAssertEqual(store.items.count, 1) - XCTAssertTrue(store.items.contains(BoutiqueItem.purse)) + XCTAssertTrue(store.items.contains(.purse)) try await store .removeAll() - .insert(BoutiqueItem.belt) + .insert(.belt) .run() XCTAssertEqual(store.items.count, 1) - XCTAssertTrue(store.items.contains(BoutiqueItem.belt)) + XCTAssertTrue(store.items.contains(.belt)) try await store .removeAll() - .remove(BoutiqueItem.belt) - .insert(BoutiqueItem.belt) + .remove(.belt) + .insert(.belt) .run() XCTAssertEqual(store.items.count, 1) - XCTAssertTrue(store.items.contains(BoutiqueItem.belt)) + XCTAssertTrue(store.items.contains(.belt)) } @MainActor func testChainingOperationsDontExecuteUnlessRun() async throws { let operation = try await store - .insert(BoutiqueItem.coat) - .insert([BoutiqueItem.purse, BoutiqueItem.belt]) + .insert(.coat) + .insert([.purse, .belt]) XCTAssertEqual(store.items.count, 0) - XCTAssertFalse(store.items.contains(BoutiqueItem.purse)) - XCTAssertFalse(store.items.contains(BoutiqueItem.belt)) - XCTAssertFalse(store.items.contains(BoutiqueItem.coat)) + XCTAssertFalse(store.items.contains(.purse)) + XCTAssertFalse(store.items.contains(.belt)) + XCTAssertFalse(store.items.contains(.coat)) // Adding this line to get rid of the error about // `operation` being unused, given that's the point of the test. @@ -221,7 +236,7 @@ final class StoreTests: XCTestCase { @MainActor func testPublishedItemsSubscription() async throws { - let uniqueItems = BoutiqueItem.uniqueItems + let uniqueItems: [BoutiqueItem] = .uniqueItems let expectation = XCTestExpectation(description: "uniqueItems is published and read") store.$items diff --git a/Tests/BoutiqueTests/StoredTests.swift b/Tests/BoutiqueTests/StoredTests.swift index 376cde7..a7019ca 100644 --- a/Tests/BoutiqueTests/StoredTests.swift +++ b/Tests/BoutiqueTests/StoredTests.swift @@ -23,44 +23,44 @@ final class StoredTests: XCTestCase { @MainActor func testInsertingItem() async throws { - try await $items.insert(BoutiqueItem.coat) - XCTAssertTrue(items.contains(BoutiqueItem.coat)) + try await $items.insert(.coat) + XCTAssertTrue(items.contains(.coat)) - try await $items.insert(BoutiqueItem.belt) - XCTAssertTrue(items.contains(BoutiqueItem.belt)) + try await $items.insert(.belt) + XCTAssertTrue(items.contains(.belt)) XCTAssertEqual(items.count, 2) } @MainActor func testInsertingItems() async throws { - try await $items.insert([BoutiqueItem.coat, BoutiqueItem.sweater, BoutiqueItem.sweater, BoutiqueItem.purse]) - XCTAssertTrue(items.contains(BoutiqueItem.coat)) - XCTAssertTrue(items.contains(BoutiqueItem.sweater)) - XCTAssertTrue(items.contains(BoutiqueItem.purse)) + try await $items.insert([.coat, .sweater, .sweater, .purse]) + XCTAssertTrue(items.contains(.coat)) + XCTAssertTrue(items.contains(.sweater)) + XCTAssertTrue(items.contains(.purse)) } @MainActor func testInsertingDuplicateItems() async throws { XCTAssertTrue(items.isEmpty) - try await $items.insert(BoutiqueItem.allItems) + try await $items.insert(.allItems) XCTAssertEqual(items.count, 4) } @MainActor func testReadingItems() async throws { - try await $items.insert(BoutiqueItem.allItems) + try await $items.insert(.allItems) - XCTAssertEqual(items[0], BoutiqueItem.coat) - XCTAssertEqual(items[1], BoutiqueItem.sweater) - XCTAssertEqual(items[2], BoutiqueItem.purse) - XCTAssertEqual(items[3], BoutiqueItem.belt) + XCTAssertEqual(items[0], .coat) + XCTAssertEqual(items[1], .sweater) + XCTAssertEqual(items[2], .purse) + XCTAssertEqual(items[3], .belt) XCTAssertEqual(items.count, 4) } @MainActor func testReadingPersistedItems() async throws { - try await $items.insert(BoutiqueItem.allItems) + try await $items.insert(.allItems) // The new store has to fetch items from disk. let newStore = try await Store( @@ -70,34 +70,34 @@ final class StoredTests: XCTestCase { XCTAssertEqual(newStore.items.count, 4) - XCTAssertEqual(newStore.items[0], BoutiqueItem.coat) - XCTAssertEqual(newStore.items[1], BoutiqueItem.sweater) - XCTAssertEqual(newStore.items[2], BoutiqueItem.purse) - XCTAssertEqual(newStore.items[3], BoutiqueItem.belt) + XCTAssertEqual(newStore.items[0], .coat) + XCTAssertEqual(newStore.items[1], .sweater) + XCTAssertEqual(newStore.items[2], .purse) + XCTAssertEqual(newStore.items[3], .belt) } @MainActor func testRemovingItems() async throws { - try await $items.insert(BoutiqueItem.allItems) - try await $items.remove(BoutiqueItem.coat) + try await $items.insert(.allItems) + try await $items.remove(.coat) - XCTAssertFalse(items.contains(BoutiqueItem.coat)) + XCTAssertFalse(items.contains(.coat)) - XCTAssertTrue(items.contains(BoutiqueItem.sweater)) - XCTAssertTrue(items.contains(BoutiqueItem.purse)) + XCTAssertTrue(items.contains(.sweater)) + XCTAssertTrue(items.contains(.purse)) - try await $items.remove([BoutiqueItem.sweater, BoutiqueItem.purse]) - XCTAssertFalse(items.contains(BoutiqueItem.sweater)) - XCTAssertFalse(items.contains(BoutiqueItem.purse)) + try await $items.remove([.sweater, .purse]) + XCTAssertFalse(items.contains(.sweater)) + XCTAssertFalse(items.contains(.purse)) } @MainActor func testRemoveAll() async throws { - try await $items.insert(BoutiqueItem.coat) + try await $items.insert(.coat) XCTAssertEqual(items.count, 1) try await $items.removeAll() - try await $items.insert(BoutiqueItem.uniqueItems) + try await $items.insert(.uniqueItems) XCTAssertEqual(items.count, 4) try await $items.removeAll() XCTAssertTrue(items.isEmpty) @@ -105,112 +105,112 @@ final class StoredTests: XCTestCase { @MainActor func testChainingInsertOperations() async throws { - try await $items.insert(BoutiqueItem.uniqueItems) + try await $items.insert(.uniqueItems) try await $items - .remove(BoutiqueItem.coat) - .insert(BoutiqueItem.belt) - .insert(BoutiqueItem.belt) + .remove(.coat) + .insert(.belt) + .insert(.belt) .run() XCTAssertEqual(items.count, 3) - XCTAssertTrue(items.contains(BoutiqueItem.sweater)) - XCTAssertTrue(items.contains(BoutiqueItem.purse)) - XCTAssertTrue(items.contains(BoutiqueItem.belt)) - XCTAssertFalse(items.contains(BoutiqueItem.coat)) + XCTAssertTrue(items.contains(.sweater)) + XCTAssertTrue(items.contains(.purse)) + XCTAssertTrue(items.contains(.belt)) + XCTAssertFalse(items.contains(.coat)) try await $items.removeAll() try await $items - .insert(BoutiqueItem.belt) - .insert(BoutiqueItem.coat) - .remove([BoutiqueItem.belt]) - .insert(BoutiqueItem.sweater) + .insert(.belt) + .insert(.coat) + .remove([.belt]) + .insert(.sweater) .run() XCTAssertEqual(items.count, 2) - XCTAssertTrue(items.contains(BoutiqueItem.coat)) - XCTAssertTrue(items.contains(BoutiqueItem.sweater)) - XCTAssertFalse(items.contains(BoutiqueItem.belt)) + XCTAssertTrue(items.contains(.coat)) + XCTAssertTrue(items.contains(.sweater)) + XCTAssertFalse(items.contains(.belt)) try await $items - .insert(BoutiqueItem.belt) - .insert(BoutiqueItem.coat) - .insert(BoutiqueItem.purse) - .remove([BoutiqueItem.belt, .coat]) - .insert(BoutiqueItem.sweater) + .insert(.belt) + .insert(.coat) + .insert(.purse) + .remove([.belt, .coat]) + .insert(.sweater) .run() XCTAssertEqual(items.count, 2) - XCTAssertTrue(items.contains(BoutiqueItem.sweater)) - XCTAssertTrue(items.contains(BoutiqueItem.purse)) - XCTAssertFalse(items.contains(BoutiqueItem.coat)) - XCTAssertFalse(items.contains(BoutiqueItem.belt)) + XCTAssertTrue(items.contains(.sweater)) + XCTAssertTrue(items.contains(.purse)) + XCTAssertFalse(items.contains(.coat)) + XCTAssertFalse(items.contains(.belt)) try await $items.removeAll() try await $items - .insert(BoutiqueItem.coat) - .insert([BoutiqueItem.purse, BoutiqueItem.belt]) + .insert(.coat) + .insert([.purse, .belt]) .run() XCTAssertEqual(items.count, 3) - XCTAssertTrue(items.contains(BoutiqueItem.purse)) - XCTAssertTrue(items.contains(BoutiqueItem.belt)) - XCTAssertTrue(items.contains(BoutiqueItem.coat)) + XCTAssertTrue(items.contains(.purse)) + XCTAssertTrue(items.contains(.belt)) + XCTAssertTrue(items.contains(.coat)) } @MainActor func testChainingRemoveOperations() async throws { try await $items - .insert(BoutiqueItem.uniqueItems) - .remove(BoutiqueItem.belt) - .remove(BoutiqueItem.purse) + .insert(.uniqueItems) + .remove(.belt) + .remove(.purse) .run() XCTAssertEqual(items.count, 2) - XCTAssertTrue(items.contains(BoutiqueItem.sweater)) - XCTAssertTrue(items.contains(BoutiqueItem.coat)) + XCTAssertTrue(items.contains(.sweater)) + XCTAssertTrue(items.contains(.coat)) - try await $items.insert(BoutiqueItem.uniqueItems) + try await $items.insert(.uniqueItems) XCTAssertEqual(items.count, 4) try await $items - .remove([BoutiqueItem.sweater, BoutiqueItem.coat]) - .remove(BoutiqueItem.belt) + .remove([.sweater, .coat]) + .remove(.belt) .run() XCTAssertEqual(items.count, 1) - XCTAssertTrue(items.contains(BoutiqueItem.purse)) + XCTAssertTrue(items.contains(.purse)) try await $items .removeAll() - .insert(BoutiqueItem.belt) + .insert(.belt) .run() XCTAssertEqual(items.count, 1) - XCTAssertTrue(items.contains(BoutiqueItem.belt)) + XCTAssertTrue(items.contains(.belt)) try await $items .removeAll() - .remove(BoutiqueItem.belt) - .insert(BoutiqueItem.belt) + .remove(.belt) + .insert(.belt) .run() XCTAssertEqual(items.count, 1) - XCTAssertTrue(items.contains(BoutiqueItem.belt)) + XCTAssertTrue(items.contains(.belt)) } @MainActor func testChainingOperationsDontExecuteUnlessRun() async throws { let operation = try await $items - .insert(BoutiqueItem.coat) - .insert([BoutiqueItem.purse, BoutiqueItem.belt]) + .insert(.coat) + .insert([.purse, .belt]) XCTAssertEqual(items.count, 0) - XCTAssertFalse(items.contains(BoutiqueItem.purse)) - XCTAssertFalse(items.contains(BoutiqueItem.belt)) - XCTAssertFalse(items.contains(BoutiqueItem.coat)) + XCTAssertFalse(items.contains(.purse)) + XCTAssertFalse(items.contains(.belt)) + XCTAssertFalse(items.contains(.coat)) // Adding this line to get rid of the error about // `operation` being unused, given that's the point of the test. @@ -219,7 +219,7 @@ final class StoredTests: XCTestCase { @MainActor func testPublishedItemsSubscription() async throws { - let uniqueItems = BoutiqueItem.uniqueItems + let uniqueItems = [BoutiqueItem].uniqueItems let expectation = XCTestExpectation(description: "uniqueItems is published and read") $items.$items diff --git a/Tests/BoutiqueTests/StoredValueTests.swift b/Tests/BoutiqueTests/StoredValueTests.swift index c69f753..ecfc280 100644 --- a/Tests/BoutiqueTests/StoredValueTests.swift +++ b/Tests/BoutiqueTests/StoredValueTests.swift @@ -7,7 +7,7 @@ final class StoredValueTests: XCTestCase { private var cancellables: Set = [] @StoredValue(key: "storedItem") - private var storedItem = BoutiqueItem.coat + private var storedItem = .coat @StoredValue(key: "storedNilValue") private var storedNilValue = nil @@ -35,43 +35,43 @@ final class StoredValueTests: XCTestCase { } func testStoredValueOperations() async throws { - XCTAssertEqual(self.storedItem, BoutiqueItem.coat) + XCTAssertEqual(self.storedItem, .coat) - await self.$storedItem.set(BoutiqueItem.belt) - XCTAssertEqual(self.storedItem, BoutiqueItem.belt) + await self.$storedItem.set(.belt) + XCTAssertEqual(self.storedItem, .belt) await self.$storedItem.reset() - XCTAssertEqual(self.storedItem, BoutiqueItem.coat) + XCTAssertEqual(self.storedItem, .coat) - await self.$storedItem.set(BoutiqueItem.sweater) - XCTAssertEqual(self.storedItem, BoutiqueItem.sweater) + await self.$storedItem.set(.sweater) + XCTAssertEqual(self.storedItem, .sweater) } @MainActor func testStoredValueOnMainActorOperations() async throws { - XCTAssertEqual(self.storedItem, BoutiqueItem.coat) + XCTAssertEqual(self.storedItem, .coat) - self.$storedItem.set(BoutiqueItem.belt) - XCTAssertEqual(self.storedItem, BoutiqueItem.belt) + self.$storedItem.set(.belt) + XCTAssertEqual(self.storedItem, .belt) self.$storedItem.reset() - XCTAssertEqual(self.storedItem, BoutiqueItem.coat) + XCTAssertEqual(self.storedItem, .coat) - self.$storedItem.set(BoutiqueItem.sweater) - XCTAssertEqual(self.storedItem, BoutiqueItem.sweater) + self.$storedItem.set(.sweater) + XCTAssertEqual(self.storedItem, .sweater) } func testStoredNilValue() async throws { XCTAssertEqual(self.storedNilValue, nil) - await self.$storedNilValue.set(BoutiqueItem.belt) - XCTAssertEqual(self.storedNilValue, BoutiqueItem.belt) + await self.$storedNilValue.set(.belt) + XCTAssertEqual(self.storedNilValue, .belt) await self.$storedNilValue.reset() XCTAssertEqual(self.storedNilValue, nil) - await self.$storedNilValue.set(BoutiqueItem.sweater) - XCTAssertEqual(self.storedNilValue, BoutiqueItem.sweater) + await self.$storedNilValue.set(.sweater) + XCTAssertEqual(self.storedNilValue, .sweater) } func testStoredBoolValueToggle() async throws { @@ -90,11 +90,11 @@ final class StoredValueTests: XCTestCase { func testStoredDictionaryValueUpdate() async throws { XCTAssertEqual(self.storedDictionaryValue, [:]) - await self.$storedDictionaryValue.update(key: BoutiqueItem.sweater.merchantID, value: BoutiqueItem.sweater) - XCTAssertEqual(self.storedDictionaryValue, [BoutiqueItem.sweater.merchantID : BoutiqueItem.sweater]) + await self.$storedDictionaryValue.update(key: BoutiqueItem.sweater.merchantID, value: .sweater) + XCTAssertEqual(self.storedDictionaryValue, [BoutiqueItem.sweater.merchantID : .sweater]) await self.$storedDictionaryValue.update(key: BoutiqueItem.belt.merchantID, value: nil) - XCTAssertEqual(self.storedDictionaryValue, [BoutiqueItem.sweater.merchantID : BoutiqueItem.sweater]) + XCTAssertEqual(self.storedDictionaryValue, [BoutiqueItem.sweater.merchantID : .sweater]) await self.$storedDictionaryValue.update(key: BoutiqueItem.sweater.merchantID, value: nil) XCTAssertEqual(self.storedDictionaryValue, [:]) @@ -103,11 +103,11 @@ final class StoredValueTests: XCTestCase { func testStoredArrayValueAppend() async throws { XCTAssertEqual(self.storedArrayValue, []) - await self.$storedArrayValue.append(BoutiqueItem.sweater) - XCTAssertEqual(self.storedArrayValue, [BoutiqueItem.sweater]) + await self.$storedArrayValue.append(.sweater) + XCTAssertEqual(self.storedArrayValue, [.sweater]) - await self.$storedArrayValue.append(BoutiqueItem.belt) - XCTAssertEqual(self.storedArrayValue, [BoutiqueItem.sweater, BoutiqueItem.belt]) + await self.$storedArrayValue.append(.belt) + XCTAssertEqual(self.storedArrayValue, [.sweater, .belt]) } func testStoredArrayValueTogglePresence() async throws { @@ -130,11 +130,11 @@ final class StoredValueTests: XCTestCase { @MainActor func testStoredBinding() async throws { // Using wrappedValue for our tests to work around the fact that Binding doesn't conform to Equatable - XCTAssertEqual(self.$storedBinding.binding.wrappedValue, Binding.constant(BoutiqueItem.sweater).wrappedValue) + XCTAssertEqual(self.$storedBinding.binding.wrappedValue, Binding.constant(.sweater).wrappedValue) - self.$storedBinding.set(BoutiqueItem.belt) + self.$storedBinding.set(.belt) - XCTAssertEqual(self.$storedBinding.binding.wrappedValue, Binding.constant(BoutiqueItem.belt).wrappedValue) + XCTAssertEqual(self.$storedBinding.binding.wrappedValue, Binding.constant(.belt).wrappedValue) } func testPublishedValueSubscription() async throws { @@ -147,15 +147,15 @@ final class StoredValueTests: XCTestCase { values.append(item) if values.count == 4 { - XCTAssertEqual(values, [BoutiqueItem.coat, .purse, .sweater, .belt]) + XCTAssertEqual(values, [.coat, .purse, .sweater, .belt]) expectation.fulfill() } }) .store(in: &cancellables) - await self.$storedItem.set(BoutiqueItem.purse) - await self.$storedItem.set(BoutiqueItem.sweater) - await self.$storedItem.set(BoutiqueItem.belt) + await self.$storedItem.set(.purse) + await self.$storedItem.set(.sweater) + await self.$storedItem.set(.belt) await fulfillment(of: [expectation], timeout: 1) } From 5a2781d48369ca926541bc05dbe34acc47670b69 Mon Sep 17 00:00:00 2001 From: Joe Fabisevich Date: Tue, 16 Apr 2024 14:06:08 -0400 Subject: [PATCH 2/5] Enabling code coverage in SPM --- Package.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Package.swift b/Package.swift index 0432357..e4617ea 100644 --- a/Package.swift +++ b/Package.swift @@ -29,6 +29,9 @@ let package = Package( exclude: [ "../../Images", "../../Performance Profiler", + ], + swiftSettings: [ + .define("ENABLE_TESTABILITY", .when(configuration: .debug)) ] ), .testTarget( From 047ae4ae87c996510f27e63683b4e41ffd52ff1e Mon Sep 17 00:00:00 2001 From: Joe Fabisevich Date: Tue, 16 Apr 2024 14:06:29 -0400 Subject: [PATCH 3/5] Adding more tests to cover scenarios where inserts occur after removes --- Tests/BoutiqueTests/StoredTests.swift | 46 +++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/Tests/BoutiqueTests/StoredTests.swift b/Tests/BoutiqueTests/StoredTests.swift index a7019ca..ba43cb7 100644 --- a/Tests/BoutiqueTests/StoredTests.swift +++ b/Tests/BoutiqueTests/StoredTests.swift @@ -133,11 +133,30 @@ final class StoredTests: XCTestCase { XCTAssertTrue(items.contains(.sweater)) XCTAssertFalse(items.contains(.belt)) + try await $items.removeAll() + try await $items .insert(.belt) .insert(.coat) .insert(.purse) .remove([.belt, .coat]) + .insert([.sweater]) + .run() + + XCTAssertEqual(items.count, 2) + XCTAssertTrue(items.contains(.sweater)) + XCTAssertTrue(items.contains(.purse)) + XCTAssertFalse(items.contains(.coat)) + XCTAssertFalse(items.contains(.belt)) + + try await $items.removeAll() + + try await $items + .insert(.belt) + .insert(.coat) + .insert(.purse) + .remove(.belt) + .remove(.coat) .insert(.sweater) .run() @@ -158,6 +177,33 @@ final class StoredTests: XCTestCase { XCTAssertTrue(items.contains(.purse)) XCTAssertTrue(items.contains(.belt)) XCTAssertTrue(items.contains(.coat)) + + try await $items.removeAll() + + try await $items + .insert(.coat) + .insert([.purse, .belt]) + .remove(.purse) + .run() + + XCTAssertEqual(items.count, 2) + XCTAssertFalse(items.contains(.purse)) + XCTAssertTrue(items.contains(.belt)) + XCTAssertTrue(items.contains(.coat)) + + try await $items.removeAll() + + try await $items + .insert([.coat]) + .remove(.coat) + .insert([.purse, .belt]) + .remove(.purse) + .run() + + XCTAssertEqual(items.count, 1) + XCTAssertFalse(items.contains(.purse)) + XCTAssertTrue(items.contains(.belt)) + XCTAssertFalse(items.contains(.coat)) } @MainActor From 0e51481c64ef27abe09d2033b8de5c723de1b7a5 Mon Sep 17 00:00:00 2001 From: Joe Fabisevich Date: Tue, 16 Apr 2024 14:21:44 -0400 Subject: [PATCH 4/5] Adding more tests for insert/remove chains --- Tests/BoutiqueTests/AsyncStoreTests.swift | 165 +++++++++++++++------- Tests/BoutiqueTests/StoreTests.swift | 75 ++++++++-- Tests/BoutiqueTests/StoredTests.swift | 26 +++- 3 files changed, 208 insertions(+), 58 deletions(-) diff --git a/Tests/BoutiqueTests/AsyncStoreTests.swift b/Tests/BoutiqueTests/AsyncStoreTests.swift index 6cb6249..5060156 100644 --- a/Tests/BoutiqueTests/AsyncStoreTests.swift +++ b/Tests/BoutiqueTests/AsyncStoreTests.swift @@ -3,31 +3,30 @@ import Combine import XCTest final class AsyncStoreTests: XCTestCase { - private var asyncStore: Store! private var cancellables: Set = [] - + override func setUp() async throws { asyncStore = try await Store( storage: SQLiteStorageEngine.default(appendingPath: "Tests"), cacheIdentifier: \.merchantID) try await asyncStore.removeAll() } - + override func tearDown() { cancellables.removeAll() } - + @MainActor func testInsertingItem() async throws { try await asyncStore.insert(.coat) XCTAssertTrue(asyncStore.items.contains(.coat)) - + try await asyncStore.insert(.belt) XCTAssertTrue(asyncStore.items.contains(.belt)) XCTAssertEqual(asyncStore.items.count, 2) } - + @MainActor func testInsertingItems() async throws { try await asyncStore.insert([.coat, .sweater, .sweater, .purse]) @@ -35,127 +34,198 @@ final class AsyncStoreTests: XCTestCase { XCTAssertTrue(asyncStore.items.contains(.sweater)) XCTAssertTrue(asyncStore.items.contains(.purse)) } - + @MainActor func testInsertingDuplicateItems() async throws { XCTAssertTrue(asyncStore.items.isEmpty) try await asyncStore.insert(.allItems) XCTAssertEqual(asyncStore.items.count, 4) } - + @MainActor func testReadingItems() async throws { try await asyncStore.insert(.allItems) - + XCTAssertEqual(asyncStore.items[0], .coat) XCTAssertEqual(asyncStore.items[1], .sweater) XCTAssertEqual(asyncStore.items[2], .purse) XCTAssertEqual(asyncStore.items[3], .belt) - + XCTAssertEqual(asyncStore.items.count, 4) } - + @MainActor func testReadingPersistedItems() async throws { try await asyncStore.insert(.allItems) - + // The new store has to fetch items from disk. let newStore = try await Store( storage: SQLiteStorageEngine.default(appendingPath: "Tests"), cacheIdentifier: \.merchantID) - + XCTAssertEqual(newStore.items[0], .coat) XCTAssertEqual(newStore.items[1], .sweater) XCTAssertEqual(newStore.items[2], .purse) XCTAssertEqual(newStore.items[3], .belt) - + XCTAssertEqual(newStore.items.count, 4) } - + @MainActor func testRemovingItems() async throws { try await asyncStore.insert(.allItems) try await asyncStore.remove(.coat) - + XCTAssertFalse(asyncStore.items.contains(.coat)) - + XCTAssertTrue(asyncStore.items.contains(.sweater)) XCTAssertTrue(asyncStore.items.contains(.purse)) - + try await asyncStore.remove([.sweater, .purse]) XCTAssertFalse(asyncStore.items.contains(.sweater)) XCTAssertFalse(asyncStore.items.contains(.purse)) } - + @MainActor func testRemoveAll() async throws { try await asyncStore.insert(.coat) XCTAssertEqual(asyncStore.items.count, 1) try await asyncStore.removeAll() - + try await asyncStore.insert(.uniqueItems) XCTAssertEqual(asyncStore.items.count, 4) try await asyncStore.removeAll() XCTAssertTrue(asyncStore.items.isEmpty) } - + @MainActor func testChainingInsertOperations() async throws { try await asyncStore.insert(.uniqueItems) - + try await asyncStore .remove(.coat) .insert(.belt) .insert(.belt) .run() - + XCTAssertEqual(asyncStore.items.count, 3) XCTAssertTrue(asyncStore.items.contains(.sweater)) XCTAssertTrue(asyncStore.items.contains(.purse)) XCTAssertTrue(asyncStore.items.contains(.belt)) XCTAssertFalse(asyncStore.items.contains(.coat)) - + try await asyncStore.removeAll() - + try await asyncStore .insert(.belt) .insert(.coat) .remove([.belt]) .insert(.sweater) .run() - + XCTAssertEqual(asyncStore.items.count, 2) XCTAssertTrue(asyncStore.items.contains(.coat)) XCTAssertTrue(asyncStore.items.contains(.sweater)) XCTAssertFalse(asyncStore.items.contains(.belt)) - + + try await asyncStore.removeAll() + try await asyncStore .insert(.belt) .insert(.coat) .insert(.purse) .remove([.belt, .coat]) + .insert([.sweater]) + .run() + + XCTAssertEqual(asyncStore.items.count, 2) + XCTAssertTrue(asyncStore.items.contains(.sweater)) + XCTAssertTrue(asyncStore.items.contains(.purse)) + XCTAssertFalse(asyncStore.items.contains(.coat)) + XCTAssertFalse(asyncStore.items.contains(.belt)) + + try await asyncStore.removeAll() + + try await asyncStore + .insert(.belt) + .insert(.coat) + .insert(.purse) + .remove(.belt) + .remove(.coat) .insert(.sweater) .run() - + XCTAssertEqual(asyncStore.items.count, 2) XCTAssertTrue(asyncStore.items.contains(.sweater)) XCTAssertTrue(asyncStore.items.contains(.purse)) XCTAssertFalse(asyncStore.items.contains(.coat)) XCTAssertFalse(asyncStore.items.contains(.belt)) - + try await asyncStore.removeAll() - + try await asyncStore .insert(.coat) .insert([.purse, .belt]) .run() - + XCTAssertEqual(asyncStore.items.count, 3) XCTAssertTrue(asyncStore.items.contains(.purse)) XCTAssertTrue(asyncStore.items.contains(.belt)) XCTAssertTrue(asyncStore.items.contains(.coat)) + + try await asyncStore.removeAll() + + try await asyncStore + .insert(.coat) + .insert([.purse, .belt]) + .remove(.purse) + .run() + + XCTAssertEqual(asyncStore.items.count, 2) + XCTAssertFalse(asyncStore.items.contains(.purse)) + XCTAssertTrue(asyncStore.items.contains(.belt)) + XCTAssertTrue(asyncStore.items.contains(.coat)) + + try await asyncStore.removeAll() + + try await asyncStore + .insert([.coat]) + .remove(.coat) + .insert([.purse, .belt]) + .remove(.purse) + .run() + + XCTAssertEqual(asyncStore.items.count, 1) + XCTAssertFalse(asyncStore.items.contains(.purse)) + XCTAssertTrue(asyncStore.items.contains(.belt)) + XCTAssertFalse(asyncStore.items.contains(.coat)) + + try await asyncStore.removeAll() + + try await asyncStore + .insert([.coat]) + .remove(.coat) + .insert([.purse, .belt]) + .removeAll() + .run() + + XCTAssertEqual(asyncStore.items.count, 0) + XCTAssertFalse(asyncStore.items.contains(.purse)) + XCTAssertFalse(asyncStore.items.contains(.belt)) + XCTAssertFalse(asyncStore.items.contains(.coat)) + + try await asyncStore + .insert([.coat]) + .removeAll() + .insert([.purse, .belt]) + .run() + + XCTAssertEqual(asyncStore.items.count, 2) + XCTAssertTrue(asyncStore.items.contains(.purse)) + XCTAssertTrue(asyncStore.items.contains(.belt)) + XCTAssertFalse(asyncStore.items.contains(.coat)) } - + @MainActor func testChainingRemoveOperations() async throws { try await asyncStore @@ -163,61 +233,61 @@ final class AsyncStoreTests: XCTestCase { .remove(.belt) .remove(.purse) .run() - + XCTAssertEqual(asyncStore.items.count, 2) XCTAssertTrue(asyncStore.items.contains(.sweater)) XCTAssertTrue(asyncStore.items.contains(.coat)) - + try await asyncStore.insert(.uniqueItems) XCTAssertEqual(asyncStore.items.count, 4) - + try await asyncStore .remove([.sweater, .coat]) .remove(.belt) .run() - + XCTAssertEqual(asyncStore.items.count, 1) XCTAssertTrue(asyncStore.items.contains(.purse)) - + try await asyncStore .removeAll() .insert(.belt) .run() - + XCTAssertEqual(asyncStore.items.count, 1) XCTAssertTrue(asyncStore.items.contains(.belt)) - + try await asyncStore .removeAll() .remove(.belt) .insert(.belt) .run() - + XCTAssertEqual(asyncStore.items.count, 1) XCTAssertTrue(asyncStore.items.contains(.belt)) } - + @MainActor func testChainingOperationsDontExecuteUnlessRun() async throws { let operation = try await asyncStore .insert(.coat) .insert([.purse, .belt]) - + XCTAssertEqual(asyncStore.items.count, 0) XCTAssertFalse(asyncStore.items.contains(.purse)) XCTAssertFalse(asyncStore.items.contains(.belt)) XCTAssertFalse(asyncStore.items.contains(.coat)) - + // Adding this line to get rid of the error about // `operation` being unused, given that's the point of the test. _ = operation } - + @MainActor func testPublishedItemsSubscription() async throws { let uniqueItems: [BoutiqueItem] = .uniqueItems let expectation = XCTestExpectation(description: "uniqueItems is published and read") - + asyncStore.$items .dropFirst() .sink(receiveValue: { items in @@ -225,12 +295,11 @@ final class AsyncStoreTests: XCTestCase { expectation.fulfill() }) .store(in: &cancellables) - + XCTAssertTrue(asyncStore.items.isEmpty) - + // Sets items under the hood try await asyncStore.insert(uniqueItems) wait(for: [expectation], timeout: 1) } - } diff --git a/Tests/BoutiqueTests/StoreTests.swift b/Tests/BoutiqueTests/StoreTests.swift index a37d64d..b708082 100644 --- a/Tests/BoutiqueTests/StoreTests.swift +++ b/Tests/BoutiqueTests/StoreTests.swift @@ -15,11 +15,11 @@ final class StoreTests: XCTestCase { storage: SQLiteStorageEngine.default(appendingPath: "Tests"), cacheIdentifier: \.merchantID) } - + store = makeNonAsyncStore() try await store.removeAll() } - + override func tearDown() { cancellables.removeAll() } @@ -64,12 +64,12 @@ final class StoreTests: XCTestCase { @MainActor func testReadingPersistedItems() async throws { try await store.insert(.allItems) - + // The new store has to fetch items from disk. let newStore = try await Store( storage: SQLiteStorageEngine.default(appendingPath: "Tests"), cacheIdentifier: \.merchantID) - + XCTAssertEqual(newStore.items[0], .coat) XCTAssertEqual(newStore.items[1], .sweater) XCTAssertEqual(newStore.items[2], .purse) @@ -138,14 +138,17 @@ final class StoreTests: XCTestCase { try await store.removeAll() try await store - .insert([.belt, .coat]) - .insert(.sweater) - .remove([.belt]) + .insert(.belt) + .insert(.coat) + .insert(.purse) + .remove([.belt, .coat]) + .insert([.sweater]) .run() XCTAssertEqual(store.items.count, 2) - XCTAssertTrue(store.items.contains(.coat)) XCTAssertTrue(store.items.contains(.sweater)) + XCTAssertTrue(store.items.contains(.purse)) + XCTAssertFalse(store.items.contains(.coat)) XCTAssertFalse(store.items.contains(.belt)) try await store.removeAll() @@ -154,7 +157,8 @@ final class StoreTests: XCTestCase { .insert(.belt) .insert(.coat) .insert(.purse) - .remove([.belt, .coat]) + .remove(.belt) + .remove(.coat) .insert(.sweater) .run() @@ -175,6 +179,58 @@ final class StoreTests: XCTestCase { XCTAssertTrue(store.items.contains(.purse)) XCTAssertTrue(store.items.contains(.belt)) XCTAssertTrue(store.items.contains(.coat)) + + try await store.removeAll() + + try await store + .insert(.coat) + .insert([.purse, .belt]) + .remove(.purse) + .run() + + XCTAssertEqual(store.items.count, 2) + XCTAssertFalse(store.items.contains(.purse)) + XCTAssertTrue(store.items.contains(.belt)) + XCTAssertTrue(store.items.contains(.coat)) + + try await store.removeAll() + + try await store + .insert([.coat]) + .remove(.coat) + .insert([.purse, .belt]) + .remove(.purse) + .run() + + XCTAssertEqual(store.items.count, 1) + XCTAssertFalse(store.items.contains(.purse)) + XCTAssertTrue(store.items.contains(.belt)) + XCTAssertFalse(store.items.contains(.coat)) + + try await store.removeAll() + + try await store + .insert([.coat]) + .remove(.coat) + .insert([.purse, .belt]) + .removeAll() + .run() + + XCTAssertEqual(store.items.count, 0) + XCTAssertFalse(store.items.contains(.purse)) + XCTAssertFalse(store.items.contains(.belt)) + XCTAssertFalse(store.items.contains(.coat)) + + try await store + .insert([.coat]) + .removeAll() + .insert([.purse, .belt]) + .run() + + XCTAssertEqual(store.items.count, 2) + XCTAssertTrue(store.items.contains(.purse)) + XCTAssertTrue(store.items.contains(.belt)) + XCTAssertFalse(store.items.contains(.coat)) } @MainActor @@ -254,3 +310,4 @@ final class StoreTests: XCTestCase { wait(for: [expectation], timeout: 1) } } + diff --git a/Tests/BoutiqueTests/StoredTests.swift b/Tests/BoutiqueTests/StoredTests.swift index ba43cb7..0626fb2 100644 --- a/Tests/BoutiqueTests/StoredTests.swift +++ b/Tests/BoutiqueTests/StoredTests.swift @@ -204,6 +204,31 @@ final class StoredTests: XCTestCase { XCTAssertFalse(items.contains(.purse)) XCTAssertTrue(items.contains(.belt)) XCTAssertFalse(items.contains(.coat)) + + try await $items.removeAll() + + try await $items + .insert([.coat]) + .remove(.coat) + .insert([.purse, .belt]) + .removeAll() + .run() + + XCTAssertEqual(items.count, 0) + XCTAssertFalse(items.contains(.purse)) + XCTAssertFalse(items.contains(.belt)) + XCTAssertFalse(items.contains(.coat)) + + try await $items + .insert([.coat]) + .removeAll() + .insert([.purse, .belt]) + .run() + + XCTAssertEqual(items.count, 2) + XCTAssertTrue(items.contains(.purse)) + XCTAssertTrue(items.contains(.belt)) + XCTAssertFalse(items.contains(.coat)) } @MainActor @@ -283,4 +308,3 @@ final class StoredTests: XCTestCase { wait(for: [expectation], timeout: 1) } } - From b8f54b58a5e97a240314c94ac4bf3abe575df57f Mon Sep 17 00:00:00 2001 From: Joe Fabisevich Date: Mon, 22 Apr 2024 16:07:12 -0400 Subject: [PATCH 5/5] Adding a mechanism to force removal of keychain values when shape of data does not align --- Sources/Boutique/SecurelyStoredValue.swift | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/Sources/Boutique/SecurelyStoredValue.swift b/Sources/Boutique/SecurelyStoredValue.swift index a3a8824..2dd5200 100644 --- a/Sources/Boutique/SecurelyStoredValue.swift +++ b/Sources/Boutique/SecurelyStoredValue.swift @@ -128,6 +128,8 @@ public struct SecurelyStoredValue { public func remove() throws { if self.wrappedValue != nil { try self.removeItem() + } else if self.wrappedValue == nil && Self.keychainValueExists(group: self.group, service: self.keychainService, account: self.key) { + try self.removeItem() } } @@ -234,6 +236,22 @@ private extension SecurelyStoredValue { } } + static func keychainValueExists(group: String?, service: String, account: String) -> Bool { + let keychainQuery = [ + kSecClass: kSecClassGenericPassword, + kSecAttrService: service, + kSecAttrAccount: account, + kSecReturnData: true + ] + .withGroup(group) + .mapToStringDictionary() + + var extractedData: AnyObject? + let status = SecItemCopyMatching(keychainQuery as CFDictionary, &extractedData) + + return status != errSecItemNotFound + } + var keychainService: String { self.service ?? Self.defaultService }