Merge pull request #62 from mergesort/caching

Implementing an additional caching layer for StoredValue
This commit is contained in:
Joe Fabisevich
2024-04-12 11:23:49 -04:00
committed by GitHub
11 changed files with 329 additions and 290 deletions

View File

@@ -0,0 +1,25 @@
import Foundation
/// `CachedValue` exists internally for the purpose of creating a reference value, preventing the need
/// to create a `JSONDecoder` and invoke a decode step every time we need to access a `StoredValue` externally.
internal final class CachedValue<Item: Codable> {
private var cachedValue: Item?
public var retrieveValue: () -> Item
init(retrieveValue: @escaping () -> Item) {
self.retrieveValue = retrieveValue
self.cachedValue = self.retrieveValue()
}
func set(_ value: Item) {
self.cachedValue = value
}
var wrappedValue: Item? {
if let cachedValue {
cachedValue
} else {
self.retrieveValue()
}
}
}

View File

@@ -92,7 +92,12 @@ public struct SecurelyStoredValue<Item: Codable> {
if self.wrappedValue == nil {
try self.insert(value)
} else {
try self.update(value)
// This call to `remove` is a temporary workaround for broken functionality when trying to update a value.
// Since updating a value does not seem to work, I've rewritten `set` to first set a `nil` value
// then the desired value, which will effectively call `set` with a new value, which does work.
// This will be fixed in the future, and we will restore the call-site to say `self.update(value)`.
try self.remove()
try self.insert(value)
}
} else {
try self.remove()

View File

@@ -14,7 +14,6 @@ public extension StoredValue {
}
}
public extension SecurelyStoredValue {
/// A convenient way to create a `Binding` from a `SecurelyStoredValue`.
///

View File

@@ -45,6 +45,8 @@ public struct StoredValue<Item: Codable> {
private let userDefaults: UserDefaults
private let itemSubject: CurrentValueSubject<Item, Never>
private var cachedValue: CachedValue<Item>
public init(wrappedValue: Item, key: String, storage userDefaults: UserDefaults = UserDefaults.standard) {
self.key = key
self.defaultValue = wrappedValue
@@ -52,11 +54,15 @@ public struct StoredValue<Item: Codable> {
let initialValue = Self.storedValue(forKey: key, userDefaults: userDefaults, defaultValue: defaultValue)
self.itemSubject = CurrentValueSubject(initialValue)
self.cachedValue = CachedValue(retrieveValue: {
Self.storedValue(forKey: key, userDefaults: userDefaults, defaultValue: initialValue)
})
}
/// The currently stored value
public var wrappedValue: Item {
Self.storedValue(forKey: self.key, userDefaults: self.userDefaults, defaultValue: self.defaultValue)
self.cachedValue.retrieveValue()
}
/// A ``StoredValue`` which exposes ``set(_:)`` and ``reset()`` functions alongside a ``publisher``.
@@ -94,6 +100,7 @@ public struct StoredValue<Item: Codable> {
let boxedValue = BoxedValue(value: value)
if let data = try? JSONCoders.encoder.encode(boxedValue) {
self.userDefaults.set(data, forKey: self.key)
self.cachedValue.set(value)
self.itemSubject.send(value)
}
}
@@ -123,6 +130,7 @@ public struct StoredValue<Item: Codable> {
let boxedValue = BoxedValue(value: self.defaultValue)
if let data = try? JSONCoders.encoder.encode(boxedValue) {
self.userDefaults.set(data, forKey: self.key)
self.cachedValue.set(self.defaultValue)
self.itemSubject.send(self.defaultValue)
}
}

View File

@@ -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<BoutiqueItem>(
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

View File

@@ -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)
}

View File

@@ -34,7 +34,9 @@ extension BoutiqueItem {
merchantID: "4",
value: "Belt"
)
}
extension [BoutiqueItem] {
static let allItems = [
BoutiqueItem.coat,
BoutiqueItem.sweater,

View File

@@ -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)
}

View File

@@ -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<BoutiqueItem>(
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,112 @@ 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
.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 +221,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

View File

@@ -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<BoutiqueItem>(
@@ -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

View File

@@ -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 {
@@ -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)
}