diff --git a/Sources/ComposableArchitecture/Effects/ConcurrencySupport.swift b/Sources/ComposableArchitecture/Effects/ConcurrencySupport.swift index 81e30f723..aa081e0df 100644 --- a/Sources/ComposableArchitecture/Effects/ConcurrencySupport.swift +++ b/Sources/ComposableArchitecture/Effects/ConcurrencySupport.swift @@ -1,424 +1,424 @@ #if canImport(_Concurrency) && compiler(>=5.5.2) -extension AsyncStream { - /// Initializes an `AsyncStream` from any `AsyncSequence`. - /// - /// Useful as a type eraser for live `AsyncSequence`-based dependencies. - /// - /// For example, your feature may want to subscribe to screenshot notifications. You can model - /// this as a dependency client that returns an `AsyncStream`: - /// - /// ```swift - /// struct ScreenshotsClient { - /// var screenshots: () -> AsyncStream - /// func callAsFunction() -> AsyncStream { self.screenshots() } - /// } - /// ``` - /// - /// The "live" implementation of the dependency can supply a stream by erasing the appropriate - /// `NotificationCenter.Notifications` async sequence: - /// - /// ```swift - /// extension ScreenshotsClient { - /// static let live = Self( - /// screenshots: { - /// AsyncStream( - /// NotificationCenter.default - /// .notifications(named: UIApplication.userDidTakeScreenshotNotification) - /// .map { _ in } - /// ) - /// } - /// ) - /// } - /// ``` - /// - /// While your tests can use `AsyncStream.streamWithContinuation` to spin up a controllable stream - /// for tests: - /// - /// ```swift - /// let screenshots = AsyncStream.streamWithContinuation() - /// - /// let store = TestStore( - /// initialState: Feature.State(), - /// reducer: Feature() - /// ) - /// - /// store.dependencies.screenshots.screenshots = { screenshots.stream } - /// - /// screenshots.continuation.yield() // Simulate a screenshot being taken. - /// - /// await store.receive(.screenshotTaken) { ... } - /// ``` - /// - /// - Parameters: - /// - sequence: An `AsyncSequence`. - /// - limit: The maximum number of elements to hold in the buffer. By default, this value is - /// unlimited. Use a `Continuation.BufferingPolicy` to buffer a specified number of oldest or - /// newest elements. - public init( - _ sequence: S, - bufferingPolicy limit: Continuation.BufferingPolicy = .unbounded - ) where S.Element == Element { - self.init(bufferingPolicy: limit) { (continuation: Continuation) in - let task = Task { - do { - for try await element in sequence { - continuation.yield(element) + extension AsyncStream { + /// Initializes an `AsyncStream` from any `AsyncSequence`. + /// + /// Useful as a type eraser for live `AsyncSequence`-based dependencies. + /// + /// For example, your feature may want to subscribe to screenshot notifications. You can model + /// this as a dependency client that returns an `AsyncStream`: + /// + /// ```swift + /// struct ScreenshotsClient { + /// var screenshots: () -> AsyncStream + /// func callAsFunction() -> AsyncStream { self.screenshots() } + /// } + /// ``` + /// + /// The "live" implementation of the dependency can supply a stream by erasing the appropriate + /// `NotificationCenter.Notifications` async sequence: + /// + /// ```swift + /// extension ScreenshotsClient { + /// static let live = Self( + /// screenshots: { + /// AsyncStream( + /// NotificationCenter.default + /// .notifications(named: UIApplication.userDidTakeScreenshotNotification) + /// .map { _ in } + /// ) + /// } + /// ) + /// } + /// ``` + /// + /// While your tests can use `AsyncStream.streamWithContinuation` to spin up a controllable stream + /// for tests: + /// + /// ```swift + /// let screenshots = AsyncStream.streamWithContinuation() + /// + /// let store = TestStore( + /// initialState: Feature.State(), + /// reducer: Feature() + /// ) + /// + /// store.dependencies.screenshots.screenshots = { screenshots.stream } + /// + /// screenshots.continuation.yield() // Simulate a screenshot being taken. + /// + /// await store.receive(.screenshotTaken) { ... } + /// ``` + /// + /// - Parameters: + /// - sequence: An `AsyncSequence`. + /// - limit: The maximum number of elements to hold in the buffer. By default, this value is + /// unlimited. Use a `Continuation.BufferingPolicy` to buffer a specified number of oldest or + /// newest elements. + public init( + _ sequence: S, + bufferingPolicy limit: Continuation.BufferingPolicy = .unbounded + ) where S.Element == Element { + self.init(bufferingPolicy: limit) { (continuation: Continuation) in + let task = Task { + do { + for try await element in sequence { + continuation.yield(element) + } + } catch {} + continuation.finish() + } + continuation.onTermination = + { _ in + task.cancel() } - } catch {} - continuation.finish() + // NB: This explicit cast is needed to work around a compiler bug in Swift 5.5.2 + as @Sendable (Continuation.Termination) -> Void } - continuation.onTermination = - { _ in - task.cancel() - } - // NB: This explicit cast is needed to work around a compiler bug in Swift 5.5.2 - as @Sendable (Continuation.Termination) -> Void } - } - /// Constructs and returns a stream along with its backing continuation. - /// - /// This is handy for immediately escaping the continuation from an async stream, which typically - /// requires multiple steps: - /// - /// ```swift - /// var _continuation: AsyncStream.Continuation! - /// let stream = AsyncStream { continuation = $0 } - /// let continuation = _continuation! - /// - /// // vs. - /// - /// let (stream, continuation) = AsyncStream.streamWithContinuation() - /// ``` - /// - /// This tool is usually used for tests where we need to supply an async sequence to a dependency - /// endpoint and get access to its continuation so that we can emulate the dependency - /// emitting data. For example, suppose you have a dependency exposing an async sequence for - /// listening to notifications. To test this you can use `streamWithContinuation`: - /// - /// ```swift - /// let notifications = AsyncStream.streamWithContinuation() - /// - /// let store = TestStore( - /// initialState: Feature.State(), - /// reducer: Feature() - /// ) - /// - /// store.dependencies.notifications = { notifications.stream } - /// - /// await store.send(.task) - /// notifications.continuation.yield("Hello") // Simulate notification being posted - /// await store.receive(.notification("Hello")) { - /// $0.message = "Hello" - /// } - /// ``` - /// - /// > Warning: ⚠️ `AsyncStream` does not support multiple subscribers, therefore you can only use - /// > this helper to test features that do not subscribe multiple times to the dependency - /// > endpoint. - /// - /// - Parameters: - /// - elementType: The type of element the `AsyncStream` produces. - /// - limit: A Continuation.BufferingPolicy value to set the stream’s buffering behavior. By - /// default, the stream buffers an unlimited number of elements. You can also set the policy to - /// buffer a specified number of oldest or newest elements. - /// - Returns: An `AsyncStream`. - public static func streamWithContinuation( - _ elementType: Element.Type = Element.self, - bufferingPolicy limit: Continuation.BufferingPolicy = .unbounded - ) -> (stream: Self, continuation: Continuation) { - var continuation: Continuation! - return (Self(elementType, bufferingPolicy: limit) { continuation = $0 }, continuation) - } + /// Constructs and returns a stream along with its backing continuation. + /// + /// This is handy for immediately escaping the continuation from an async stream, which typically + /// requires multiple steps: + /// + /// ```swift + /// var _continuation: AsyncStream.Continuation! + /// let stream = AsyncStream { continuation = $0 } + /// let continuation = _continuation! + /// + /// // vs. + /// + /// let (stream, continuation) = AsyncStream.streamWithContinuation() + /// ``` + /// + /// This tool is usually used for tests where we need to supply an async sequence to a dependency + /// endpoint and get access to its continuation so that we can emulate the dependency + /// emitting data. For example, suppose you have a dependency exposing an async sequence for + /// listening to notifications. To test this you can use `streamWithContinuation`: + /// + /// ```swift + /// let notifications = AsyncStream.streamWithContinuation() + /// + /// let store = TestStore( + /// initialState: Feature.State(), + /// reducer: Feature() + /// ) + /// + /// store.dependencies.notifications = { notifications.stream } + /// + /// await store.send(.task) + /// notifications.continuation.yield("Hello") // Simulate notification being posted + /// await store.receive(.notification("Hello")) { + /// $0.message = "Hello" + /// } + /// ``` + /// + /// > Warning: ⚠️ `AsyncStream` does not support multiple subscribers, therefore you can only use + /// > this helper to test features that do not subscribe multiple times to the dependency + /// > endpoint. + /// + /// - Parameters: + /// - elementType: The type of element the `AsyncStream` produces. + /// - limit: A Continuation.BufferingPolicy value to set the stream’s buffering behavior. By + /// default, the stream buffers an unlimited number of elements. You can also set the policy to + /// buffer a specified number of oldest or newest elements. + /// - Returns: An `AsyncStream`. + public static func streamWithContinuation( + _ elementType: Element.Type = Element.self, + bufferingPolicy limit: Continuation.BufferingPolicy = .unbounded + ) -> (stream: Self, continuation: Continuation) { + var continuation: Continuation! + return (Self(elementType, bufferingPolicy: limit) { continuation = $0 }, continuation) + } - /// An `AsyncStream` that never emits and never completes unless cancelled. - public static var never: Self { - Self { _ in } - } + /// An `AsyncStream` that never emits and never completes unless cancelled. + public static var never: Self { + Self { _ in } + } - public static var finished: Self { - Self { $0.finish() } + public static var finished: Self { + Self { $0.finish() } + } } -} -extension AsyncThrowingStream where Failure == Error { - /// Initializes an `AsyncThrowingStream` from any `AsyncSequence`. - /// - /// - Parameters: - /// - sequence: An `AsyncSequence`. - /// - limit: The maximum number of elements to hold in the buffer. By default, this value is - /// unlimited. Use a `Continuation.BufferingPolicy` to buffer a specified number of oldest or - /// newest elements. - public init( - _ sequence: S, - bufferingPolicy limit: Continuation.BufferingPolicy = .unbounded - ) where S.Element == Element { - self.init(bufferingPolicy: limit) { (continuation: Continuation) in - let task = Task { - do { - for try await element in sequence { - continuation.yield(element) + extension AsyncThrowingStream where Failure == Error { + /// Initializes an `AsyncThrowingStream` from any `AsyncSequence`. + /// + /// - Parameters: + /// - sequence: An `AsyncSequence`. + /// - limit: The maximum number of elements to hold in the buffer. By default, this value is + /// unlimited. Use a `Continuation.BufferingPolicy` to buffer a specified number of oldest or + /// newest elements. + public init( + _ sequence: S, + bufferingPolicy limit: Continuation.BufferingPolicy = .unbounded + ) where S.Element == Element { + self.init(bufferingPolicy: limit) { (continuation: Continuation) in + let task = Task { + do { + for try await element in sequence { + continuation.yield(element) + } + continuation.finish() + } catch { + continuation.finish(throwing: error) } - continuation.finish() - } catch { - continuation.finish(throwing: error) } + continuation.onTermination = + { _ in + task.cancel() + } + // NB: This explicit cast is needed to work around a compiler bug in Swift 5.5.2 + as @Sendable (Continuation.Termination) -> Void } - continuation.onTermination = - { _ in - task.cancel() - } - // NB: This explicit cast is needed to work around a compiler bug in Swift 5.5.2 - as @Sendable (Continuation.Termination) -> Void } - } - /// Constructs and returns a stream along with its backing continuation. - /// - /// This is handy for immediately escaping the continuation from an async stream, which typically - /// requires multiple steps: - /// - /// ```swift - /// var _continuation: AsyncThrowingStream.Continuation! - /// let stream = AsyncThrowingStream { continuation = $0 } - /// let continuation = _continuation! - /// - /// // vs. - /// - /// let (stream, continuation) = AsyncThrowingStream.streamWithContinuation() - /// ``` - /// - /// This tool is usually used for tests where we need to supply an async sequence to a dependency - /// endpoint and get access to its continuation so that we can emulate the dependency - /// emitting data. For example, suppose you have a dependency exposing an async sequence for - /// listening to notifications. To test this you can use `streamWithContinuation`: - /// - /// ```swift - /// let notifications = AsyncThrowingStream.streamWithContinuation() - /// - /// let store = TestStore( - /// initialState: Feature.State(), - /// reducer: Feature() - /// ) - /// - /// store.dependencies.notifications = { notifications.stream } - /// - /// await store.send(.task) - /// notifications.continuation.yield("Hello") // Simulate a notification being posted - /// await store.receive(.notification("Hello")) { - /// $0.message = "Hello" - /// } - /// ``` - /// - /// > Warning: ⚠️ `AsyncStream` does not support multiple subscribers, therefore you can only use - /// > this helper to test features that do not subscribe multiple times to the dependency - /// > endpoint. - /// - /// - Parameters: - /// - elementType: The type of element the `AsyncThrowingStream` produces. - /// - limit: A Continuation.BufferingPolicy value to set the stream’s buffering behavior. By - /// default, the stream buffers an unlimited number of elements. You can also set the policy to - /// buffer a specified number of oldest or newest elements. - /// - Returns: An `AsyncThrowingStream`. - public static func streamWithContinuation( - _ elementType: Element.Type = Element.self, - bufferingPolicy limit: Continuation.BufferingPolicy = .unbounded - ) -> (stream: Self, continuation: Continuation) { - var continuation: Continuation! - return (Self(elementType, bufferingPolicy: limit) { continuation = $0 }, continuation) - } - - /// An `AsyncThrowingStream` that never emits and never completes unless cancelled. - public static var never: Self { - Self { _ in } - } - - public static var finished: Self { - Self { $0.finish() } - } -} + /// Constructs and returns a stream along with its backing continuation. + /// + /// This is handy for immediately escaping the continuation from an async stream, which typically + /// requires multiple steps: + /// + /// ```swift + /// var _continuation: AsyncThrowingStream.Continuation! + /// let stream = AsyncThrowingStream { continuation = $0 } + /// let continuation = _continuation! + /// + /// // vs. + /// + /// let (stream, continuation) = AsyncThrowingStream.streamWithContinuation() + /// ``` + /// + /// This tool is usually used for tests where we need to supply an async sequence to a dependency + /// endpoint and get access to its continuation so that we can emulate the dependency + /// emitting data. For example, suppose you have a dependency exposing an async sequence for + /// listening to notifications. To test this you can use `streamWithContinuation`: + /// + /// ```swift + /// let notifications = AsyncThrowingStream.streamWithContinuation() + /// + /// let store = TestStore( + /// initialState: Feature.State(), + /// reducer: Feature() + /// ) + /// + /// store.dependencies.notifications = { notifications.stream } + /// + /// await store.send(.task) + /// notifications.continuation.yield("Hello") // Simulate a notification being posted + /// await store.receive(.notification("Hello")) { + /// $0.message = "Hello" + /// } + /// ``` + /// + /// > Warning: ⚠️ `AsyncStream` does not support multiple subscribers, therefore you can only use + /// > this helper to test features that do not subscribe multiple times to the dependency + /// > endpoint. + /// + /// - Parameters: + /// - elementType: The type of element the `AsyncThrowingStream` produces. + /// - limit: A Continuation.BufferingPolicy value to set the stream’s buffering behavior. By + /// default, the stream buffers an unlimited number of elements. You can also set the policy to + /// buffer a specified number of oldest or newest elements. + /// - Returns: An `AsyncThrowingStream`. + public static func streamWithContinuation( + _ elementType: Element.Type = Element.self, + bufferingPolicy limit: Continuation.BufferingPolicy = .unbounded + ) -> (stream: Self, continuation: Continuation) { + var continuation: Continuation! + return (Self(elementType, bufferingPolicy: limit) { continuation = $0 }, continuation) + } -extension Task where Failure == Never { - /// An async function that never returns. - public static func never() async throws -> Success { - for await element in AsyncStream.never { - return element + /// An `AsyncThrowingStream` that never emits and never completes unless cancelled. + public static var never: Self { + Self { _ in } } - throw _Concurrency.CancellationError() - } -} -extension Task where Success == Never, Failure == Never { - /// An async function that never returns. - public static func never() async throws { - for await _ in AsyncStream.never {} - throw _Concurrency.CancellationError() + public static var finished: Self { + Self { $0.finish() } + } } -} - -/// A generic wrapper for isolating a mutable value to an actor. -/// -/// This type is most useful when writing tests for when you want to inspect what happens inside -/// an effect. For example, suppose you have a feature such that when a button is tapped you -/// track some analytics: -/// -/// ```swift -/// @Dependency(\.analytics) var analytics -/// -/// func reduce(into state: inout State, action: Action) -> EffectTask { -/// switch action { -/// case .buttonTapped: -/// return .fireAndForget { try await self.analytics.track("Button Tapped") } -/// } -/// } -/// ``` -/// -/// Then, in tests we can construct an analytics client that appends events to a mutable array -/// rather than actually sending events to an analytics server. However, in order to do this in -/// a safe way we should use an actor, and ``ActorIsolated`` makes this easy: -/// -/// ```swift -/// @MainActor -/// func testAnalytics() async { -/// let store = TestStore(…) -/// -/// let events = ActorIsolated<[String]>([]) -/// store.dependencies.analytics = AnalyticsClient( -/// track: { event in -/// await events.withValue { $0.append(event) } -/// } -/// ) -/// -/// await store.send(.buttonTapped) -/// -/// await events.withValue { XCTAssertEqual($0, ["Button Tapped"]) } -/// } -/// ``` -@dynamicMemberLookup -public final actor ActorIsolated { - /// The actor-isolated value. - public var value: Value - /// Initializes actor-isolated state around a value. - /// - /// - Parameter value: A value to isolate in an actor. - public init(_ value: Value) { - self.value = value + extension Task where Failure == Never { + /// An async function that never returns. + public static func never() async throws -> Success { + for await element in AsyncStream.never { + return element + } + throw _Concurrency.CancellationError() + } } - public subscript(dynamicMember keyPath: KeyPath) -> Subject { - self.value[keyPath: keyPath] + extension Task where Success == Never, Failure == Never { + /// An async function that never returns. + public static func never() async throws { + for await _ in AsyncStream.never {} + throw _Concurrency.CancellationError() + } } - /// Perform an operation with isolated access to the underlying value. + /// A generic wrapper for isolating a mutable value to an actor. /// - /// Useful for inspecting an actor-isolated value for a test assertion: + /// This type is most useful when writing tests for when you want to inspect what happens inside + /// an effect. For example, suppose you have a feature such that when a button is tapped you + /// track some analytics: /// /// ```swift - /// let didOpenSettings = ActorIsolated(false) - /// store.dependencies.openSettings = { await didOpenSettings.setValue(true) } + /// @Dependency(\.analytics) var analytics /// - /// await store.send(.settingsButtonTapped) - /// - /// await didOpenSettings.withValue { XCTAssertTrue($0) } + /// func reduce(into state: inout State, action: Action) -> EffectTask { + /// switch action { + /// case .buttonTapped: + /// return .fireAndForget { try await self.analytics.track("Button Tapped") } + /// } + /// } /// ``` /// - /// - Parameters: operation: An operation to be performed on the actor with the underlying value. - /// - Returns: The result of the operation. - public func withValue( - _ operation: @Sendable (inout Value) async throws -> T - ) async rethrows -> T { - var value = self.value - defer { self.value = value } - return try await operation(&value) - } - - /// Overwrite the isolated value with a new value. - /// - /// Useful for setting an actor-isolated value when a tested dependency runs. + /// Then, in tests we can construct an analytics client that appends events to a mutable array + /// rather than actually sending events to an analytics server. However, in order to do this in + /// a safe way we should use an actor, and ``ActorIsolated`` makes this easy: /// /// ```swift - /// let didOpenSettings = ActorIsolated(false) - /// store.dependencies.openSettings = { await didOpenSettings.setValue(true) } + /// @MainActor + /// func testAnalytics() async { + /// let store = TestStore(…) + /// + /// let events = ActorIsolated<[String]>([]) + /// store.dependencies.analytics = AnalyticsClient( + /// track: { event in + /// await events.withValue { $0.append(event) } + /// } + /// ) /// - /// await store.send(.settingsButtonTapped) + /// await store.send(.buttonTapped) /// - /// await didOpenSettings.withValue { XCTAssertTrue($0) } + /// await events.withValue { XCTAssertEqual($0, ["Button Tapped"]) } + /// } /// ``` - /// - /// - Parameter newValue: The value to replace the current isolated value with. - public func setValue(_ newValue: Value) { - self.value = newValue - } -} + @dynamicMemberLookup + public final actor ActorIsolated { + /// The actor-isolated value. + public var value: Value -/// A generic wrapper for turning any non-`Sendable` type into a `Sendable` one, in an unchecked -/// manner. -/// -/// Sometimes we need to use types that should be sendable but have not yet been audited for -/// sendability. If we feel confident that the type is truly sendable, and we don't want to blanket -/// disable concurrency warnings for a module via `@preconcurrency import`, then we can selectively -/// make that single type sendable by wrapping it in ``UncheckedSendable``. -/// -/// > Note: By wrapping something in ``UncheckedSendable`` you are asking the compiler to trust -/// you that the type is safe to use from multiple threads, and the compiler cannot help you find -/// potential race conditions in your code. -@dynamicMemberLookup -@propertyWrapper -public struct UncheckedSendable: @unchecked Sendable { - /// The unchecked value. - public var value: Value + /// Initializes actor-isolated state around a value. + /// + /// - Parameter value: A value to isolate in an actor. + public init(_ value: Value) { + self.value = value + } - public init(_ value: Value) { - self.value = value - } + public subscript(dynamicMember keyPath: KeyPath) -> Subject { + self.value[keyPath: keyPath] + } - public init(wrappedValue: Value) { - self.value = wrappedValue - } + /// Perform an operation with isolated access to the underlying value. + /// + /// Useful for inspecting an actor-isolated value for a test assertion: + /// + /// ```swift + /// let didOpenSettings = ActorIsolated(false) + /// store.dependencies.openSettings = { await didOpenSettings.setValue(true) } + /// + /// await store.send(.settingsButtonTapped) + /// + /// await didOpenSettings.withValue { XCTAssertTrue($0) } + /// ``` + /// + /// - Parameters: operation: An operation to be performed on the actor with the underlying value. + /// - Returns: The result of the operation. + public func withValue( + _ operation: @Sendable (inout Value) async throws -> T + ) async rethrows -> T { + var value = self.value + defer { self.value = value } + return try await operation(&value) + } - public var wrappedValue: Value { - _read { yield self.value } - _modify { yield &self.value } + /// Overwrite the isolated value with a new value. + /// + /// Useful for setting an actor-isolated value when a tested dependency runs. + /// + /// ```swift + /// let didOpenSettings = ActorIsolated(false) + /// store.dependencies.openSettings = { await didOpenSettings.setValue(true) } + /// + /// await store.send(.settingsButtonTapped) + /// + /// await didOpenSettings.withValue { XCTAssertTrue($0) } + /// ``` + /// + /// - Parameter newValue: The value to replace the current isolated value with. + public func setValue(_ newValue: Value) { + self.value = newValue + } } - public var projectedValue: Self { - get { self } - set { self = newValue } - } + /// A generic wrapper for turning any non-`Sendable` type into a `Sendable` one, in an unchecked + /// manner. + /// + /// Sometimes we need to use types that should be sendable but have not yet been audited for + /// sendability. If we feel confident that the type is truly sendable, and we don't want to blanket + /// disable concurrency warnings for a module via `@preconcurrency import`, then we can selectively + /// make that single type sendable by wrapping it in ``UncheckedSendable``. + /// + /// > Note: By wrapping something in ``UncheckedSendable`` you are asking the compiler to trust + /// you that the type is safe to use from multiple threads, and the compiler cannot help you find + /// potential race conditions in your code. + @dynamicMemberLookup + @propertyWrapper + public struct UncheckedSendable: @unchecked Sendable { + /// The unchecked value. + public var value: Value - public subscript(dynamicMember keyPath: KeyPath) -> Subject { - self.value[keyPath: keyPath] - } + public init(_ value: Value) { + self.value = value + } + + public init(wrappedValue: Value) { + self.value = wrappedValue + } - public subscript(dynamicMember keyPath: WritableKeyPath) -> Subject { - _read { yield self.value[keyPath: keyPath] } - _modify { yield &self.value[keyPath: keyPath] } + public var wrappedValue: Value { + _read { yield self.value } + _modify { yield &self.value } + } + + public var projectedValue: Self { + get { self } + set { self = newValue } + } + + public subscript(dynamicMember keyPath: KeyPath) -> Subject { + self.value[keyPath: keyPath] + } + + public subscript(dynamicMember keyPath: WritableKeyPath) -> Subject { + _read { yield self.value[keyPath: keyPath] } + _modify { yield &self.value[keyPath: keyPath] } + } } -} -extension UncheckedSendable: Equatable where Value: Equatable {} -extension UncheckedSendable: Hashable where Value: Hashable {} + extension UncheckedSendable: Equatable where Value: Equatable {} + extension UncheckedSendable: Hashable where Value: Hashable {} -extension UncheckedSendable: Decodable where Value: Decodable { - public init(from decoder: Decoder) throws { - do { - let container = try decoder.singleValueContainer() - self.init(wrappedValue: try container.decode(Value.self)) - } catch { - self.init(wrappedValue: try Value(from: decoder)) + extension UncheckedSendable: Decodable where Value: Decodable { + public init(from decoder: Decoder) throws { + do { + let container = try decoder.singleValueContainer() + self.init(wrappedValue: try container.decode(Value.self)) + } catch { + self.init(wrappedValue: try Value(from: decoder)) + } } } -} -extension UncheckedSendable: Encodable where Value: Encodable { - public func encode(to encoder: Encoder) throws { - do { - var container = encoder.singleValueContainer() - try container.encode(self.wrappedValue) - } catch { - try self.wrappedValue.encode(to: encoder) + extension UncheckedSendable: Encodable where Value: Encodable { + public func encode(to encoder: Encoder) throws { + do { + var container = encoder.singleValueContainer() + try container.encode(self.wrappedValue) + } catch { + try self.wrappedValue.encode(to: encoder) + } } } -} #endif diff --git a/Sources/ComposableArchitecture/TestStore.swift b/Sources/ComposableArchitecture/TestStore.swift index 80db42699..09d788747 100644 --- a/Sources/ComposableArchitecture/TestStore.swift +++ b/Sources/ComposableArchitecture/TestStore.swift @@ -1167,35 +1167,35 @@ extension TestStore where ScopedState: Equatable, Action: Equatable { matching: { expectedAction == $0 }, failureMessage: "Expected to receive an action \(expectedAction), but didn't get one.", onReceive: { receivedAction in - if expectedAction != receivedAction { - let difference = TaskResultDebugging.$emitRuntimeWarnings.withValue(false) { - diff(expectedAction, receivedAction, format: .proportional) - .map { "\($0.indent(by: 4))\n\n(Expected: −, Received: +)" } - ?? """ - Expected: - \(String(describing: expectedAction).indent(by: 2)) - - Received: - \(String(describing: receivedAction).indent(by: 2)) - """ - } + if expectedAction != receivedAction { + let difference = TaskResultDebugging.$emitRuntimeWarnings.withValue(false) { + diff(expectedAction, receivedAction, format: .proportional) + .map { "\($0.indent(by: 4))\n\n(Expected: −, Received: +)" } + ?? """ + Expected: + \(String(describing: expectedAction).indent(by: 2)) + + Received: + \(String(describing: receivedAction).indent(by: 2)) + """ + } XCTFailHelper( - """ - Received unexpected action: … + """ + Received unexpected action: … - \(difference) - """, + \(difference) + """, file: file, line: line - ) - } + ) + } }, updateStateToExpectedResult, - file: file, - line: line - ) - } + file: file, + line: line + ) + } /// Asserts a matching action was received from an effect and asserts how the state changes. /// @@ -1243,7 +1243,7 @@ extension TestStore where ScopedState: Equatable, Action: Equatable { file: file, line: line ) - } + } /// Asserts an action was received matching a case path and asserts how the state changes. /// @@ -1553,7 +1553,7 @@ extension TestStore where ScopedState: Equatable, Action: Equatable { _ = { self.receive(casePath, assert: updateStateToExpectedResult, file: file, line: line) }() - await Task.megaYield() + await Task.megaYield() } #endif @@ -1811,7 +1811,7 @@ extension TestStore { ) self.reducer.state = self.reducer.receivedActions.last!.state self.reducer.receivedActions = [] -} + } /// Cancels any currently in-flight effects. /// diff --git a/Tests/ComposableArchitectureTests/CompatibilityTests.swift b/Tests/ComposableArchitectureTests/CompatibilityTests.swift index ea38be5fa..fa9b426b0 100644 --- a/Tests/ComposableArchitectureTests/CompatibilityTests.swift +++ b/Tests/ComposableArchitectureTests/CompatibilityTests.swift @@ -4,122 +4,122 @@ import XCTest // `@MainActor` introduces issues gathering tests on Linux #if !os(Linux) -@MainActor -final class CompatibilityTests: XCTestCase { - // Actions can be re-entrantly sent into the store if an action is sent that holds an object - // which sends an action on deinit. In order to prevent a simultaneous access exception for this - // case we need to use `withExtendedLifetime` on the buffered actions when clearing them out. - func testCaseStudy_ActionReentranceFromClearedBufferCausingDeinitAction() { - let cancelID = UUID() - - struct State: Equatable {} - enum Action: Equatable { - case start - case kickOffAction - case actionSender(OnDeinit) - case stop - - var description: String { - switch self { - case .start: - return "start" - case .kickOffAction: - return "kickOffAction" - case .actionSender: - return "actionSender" - case .stop: - return "stop" + @MainActor + final class CompatibilityTests: XCTestCase { + // Actions can be re-entrantly sent into the store if an action is sent that holds an object + // which sends an action on deinit. In order to prevent a simultaneous access exception for this + // case we need to use `withExtendedLifetime` on the buffered actions when clearing them out. + func testCaseStudy_ActionReentranceFromClearedBufferCausingDeinitAction() { + let cancelID = UUID() + + struct State: Equatable {} + enum Action: Equatable { + case start + case kickOffAction + case actionSender(OnDeinit) + case stop + + var description: String { + switch self { + case .start: + return "start" + case .kickOffAction: + return "kickOffAction" + case .actionSender: + return "actionSender" + case .stop: + return "stop" + } } } - } let (signal, observer) = Signal.pipe() - var handledActions: [String] = [] + var handledActions: [String] = [] - let reducer = AnyReducer { state, action, env in - handledActions.append(action.description) + let reducer = AnyReducer { state, action, env in + handledActions.append(action.description) - switch action { - case .start: + switch action { + case .start: return signal.producer - .eraseToEffect() - .cancellable(id: cancelID) + .eraseToEffect() + .cancellable(id: cancelID) - case .kickOffAction: + case .kickOffAction: return EffectTask(value: .actionSender(OnDeinit { observer.send(value: .stop) })) - case .actionSender: - return .none + case .actionSender: + return .none - case .stop: - return .cancel(id: cancelID) + case .stop: + return .cancel(id: cancelID) + } } - } - let store = Store( - initialState: .init(), - reducer: reducer, - environment: () - ) - - let viewStore = ViewStore(store) - - viewStore.send(.start) - viewStore.send(.kickOffAction) - - XCTAssertEqual( - handledActions, - [ - "start", - "kickOffAction", - "actionSender", - "stop", - ] - ) - } + let store = Store( + initialState: .init(), + reducer: reducer, + environment: () + ) + + let viewStore = ViewStore(store) + + viewStore.send(.start) + viewStore.send(.kickOffAction) + + XCTAssertEqual( + handledActions, + [ + "start", + "kickOffAction", + "actionSender", + "stop", + ] + ) + } - // Actions can be re-entrantly sent into the store while observing changes to the store's state. - // In such cases we need to take special care that those re-entrant actions are handled _after_ - // the original action. - // - // In particular, this means that in the implementation of `Store.send` we need to flip - // `isSending` to false _after_ the store's state mutation is made so that re-entrant actions - // are buffered rather than immediately handled. - func testCaseStudy_ActionReentranceFromStateObservation() { - let store = Store( - initialState: 0, - reducer: .init { state, action, _ in - state = action - return .none - }, - environment: () - ) - - let viewStore = ViewStore(store) + // Actions can be re-entrantly sent into the store while observing changes to the store's state. + // In such cases we need to take special care that those re-entrant actions are handled _after_ + // the original action. + // + // In particular, this means that in the implementation of `Store.send` we need to flip + // `isSending` to false _after_ the store's state mutation is made so that re-entrant actions + // are buffered rather than immediately handled. + func testCaseStudy_ActionReentranceFromStateObservation() { + let store = Store( + initialState: 0, + reducer: .init { state, action, _ in + state = action + return .none + }, + environment: () + ) + + let viewStore = ViewStore(store) viewStore.produced.producer .startWithValues { value in - if value == 1 { - viewStore.send(0) + if value == 1 { + viewStore.send(0) + } } - } - var stateChanges: [Int] = [] + var stateChanges: [Int] = [] viewStore.produced.producer .startWithValues { stateChanges.append($0) } - XCTAssertEqual(stateChanges, [0]) - viewStore.send(1) - XCTAssertEqual(stateChanges, [0, 1, 0]) + XCTAssertEqual(stateChanges, [0]) + viewStore.send(1) + XCTAssertEqual(stateChanges, [0, 1, 0]) + } } -} -private final class OnDeinit: Equatable { - private let onDeinit: () -> Void - init(onDeinit: @escaping () -> Void) { - self.onDeinit = onDeinit + private final class OnDeinit: Equatable { + private let onDeinit: () -> Void + init(onDeinit: @escaping () -> Void) { + self.onDeinit = onDeinit + } + deinit { self.onDeinit() } + static func == (lhs: OnDeinit, rhs: OnDeinit) -> Bool { true } } - deinit { self.onDeinit() } - static func == (lhs: OnDeinit, rhs: OnDeinit) -> Bool { true } -} #endif diff --git a/Tests/ComposableArchitectureTests/ComposableArchitectureTests.swift b/Tests/ComposableArchitectureTests/ComposableArchitectureTests.swift index a9dd6b9cf..dcc90a8e8 100644 --- a/Tests/ComposableArchitectureTests/ComposableArchitectureTests.swift +++ b/Tests/ComposableArchitectureTests/ComposableArchitectureTests.swift @@ -4,155 +4,155 @@ import XCTest // `@MainActor` introduces issues gathering tests on Linux #if !os(Linux) -@MainActor -final class ComposableArchitectureTests: XCTestCase { - func testScheduling() async { - struct Counter: ReducerProtocol { - typealias State = Int - enum Action: Equatable { - case incrAndSquareLater - case incrNow - case squareNow - } - @Dependency(\.mainQueue) var mainQueue - func reduce(into state: inout State, action: Action) -> EffectTask { - switch action { - case .incrAndSquareLater: - return .merge( - EffectTask(value: .incrNow) + @MainActor + final class ComposableArchitectureTests: XCTestCase { + func testScheduling() async { + struct Counter: ReducerProtocol { + typealias State = Int + enum Action: Equatable { + case incrAndSquareLater + case incrNow + case squareNow + } + @Dependency(\.mainQueue) var mainQueue + func reduce(into state: inout State, action: Action) -> EffectTask { + switch action { + case .incrAndSquareLater: + return .merge( + EffectTask(value: .incrNow) .deferred(for: 2, scheduler: self.mainQueue), - EffectTask(value: .squareNow) + EffectTask(value: .squareNow) .deferred(for: 1, scheduler: self.mainQueue), - EffectTask(value: .squareNow) + EffectTask(value: .squareNow) .deferred(for: 2, scheduler: self.mainQueue) - ) - case .incrNow: - state += 1 - return .none - case .squareNow: - state *= state - return .none + ) + case .incrNow: + state += 1 + return .none + case .squareNow: + state *= state + return .none + } } } - } - let store = TestStore( - initialState: 2, - reducer: Counter() - ) + let store = TestStore( + initialState: 2, + reducer: Counter() + ) let mainQueue = TestScheduler() store.dependencies.mainQueue = mainQueue - await store.send(.incrAndSquareLater) - await mainQueue.advance(by: 1) - await store.receive(.squareNow) { $0 = 4 } - await mainQueue.advance(by: 1) - await store.receive(.incrNow) { $0 = 5 } - await store.receive(.squareNow) { $0 = 25 } - - await store.send(.incrAndSquareLater) - await mainQueue.advance(by: 2) - await store.receive(.squareNow) { $0 = 625 } - await store.receive(.incrNow) { $0 = 626 } - await store.receive(.squareNow) { $0 = 391876 } - } + await store.send(.incrAndSquareLater) + await mainQueue.advance(by: 1) + await store.receive(.squareNow) { $0 = 4 } + await mainQueue.advance(by: 1) + await store.receive(.incrNow) { $0 = 5 } + await store.receive(.squareNow) { $0 = 25 } + + await store.send(.incrAndSquareLater) + await mainQueue.advance(by: 2) + await store.receive(.squareNow) { $0 = 625 } + await store.receive(.incrNow) { $0 = 626 } + await store.receive(.squareNow) { $0 = 391876 } + } - func testSimultaneousWorkOrdering() { + func testSimultaneousWorkOrdering() { let mainQueue = TestScheduler() - var values: [Int] = [] + var values: [Int] = [] mainQueue.schedule(after: .seconds(0), interval: .seconds(1)) { values.append(1) } mainQueue.schedule(after: .seconds(0), interval: .seconds(2)) { values.append(42) } - XCTAssertEqual(values, []) - mainQueue.advance() - XCTAssertEqual(values, [1, 42]) - mainQueue.advance(by: 2) + XCTAssertEqual(values, []) + mainQueue.advance() + XCTAssertEqual(values, [1, 42]) + mainQueue.advance(by: 2) XCTAssertEqual(values, [1, 42, 1, 42, 1]) - } + } - func testLongLivingEffects() async { - typealias Environment = ( - startEffect: EffectTask, - stopEffect: EffectTask - ) + func testLongLivingEffects() async { + typealias Environment = ( + startEffect: EffectTask, + stopEffect: EffectTask + ) - enum Action { case end, incr, start } + enum Action { case end, incr, start } - let effect = AsyncStream.streamWithContinuation() + let effect = AsyncStream.streamWithContinuation() - let reducer = Reduce { state, action in - switch action { - case .end: - return .fireAndForget { - effect.continuation.finish() - } - case .incr: - state += 1 - return .none - case .start: - return .run { send in - for await _ in effect.stream { - await send(.incr) + let reducer = Reduce { state, action in + switch action { + case .end: + return .fireAndForget { + effect.continuation.finish() + } + case .incr: + state += 1 + return .none + case .start: + return .run { send in + for await _ in effect.stream { + await send(.incr) + } } } } - } - let store = TestStore(initialState: 0, reducer: reducer) + let store = TestStore(initialState: 0, reducer: reducer) - await store.send(.start) - await store.send(.incr) { $0 = 1 } - effect.continuation.yield() - await store.receive(.incr) { $0 = 2 } - await store.send(.end) - } + await store.send(.start) + await store.send(.incr) { $0 = 1 } + effect.continuation.yield() + await store.receive(.incr) { $0 = 2 } + await store.send(.end) + } - func testCancellation() async { + func testCancellation() async { let mainQueue = TestScheduler() - enum Action: Equatable { - case cancel - case incr - case response(Int) - } + enum Action: Equatable { + case cancel + case incr + case response(Int) + } - let reducer = AnyReducer { state, action, _ in - enum CancelID {} + let reducer = AnyReducer { state, action, _ in + enum CancelID {} - switch action { - case .cancel: - return .cancel(id: CancelID.self) + switch action { + case .cancel: + return .cancel(id: CancelID.self) - case .incr: - state += 1 - return .task { [state] in - try await mainQueue.sleep(for: .seconds(1)) - return .response(state * state) - } - .cancellable(id: CancelID.self) + case .incr: + state += 1 + return .task { [state] in + try await mainQueue.sleep(for: .seconds(1)) + return .response(state * state) + } + .cancellable(id: CancelID.self) - case let .response(value): - state = value - return .none + case let .response(value): + state = value + return .none + } } - } - let store = TestStore( - initialState: 0, - reducer: reducer, - environment: () - ) + let store = TestStore( + initialState: 0, + reducer: reducer, + environment: () + ) - await store.send(.incr) { $0 = 1 } - await mainQueue.advance(by: .seconds(1)) - await store.receive(.response(1)) + await store.send(.incr) { $0 = 1 } + await mainQueue.advance(by: .seconds(1)) + await store.receive(.response(1)) - await store.send(.incr) { $0 = 2 } - await store.send(.cancel) + await store.send(.incr) { $0 = 2 } + await store.send(.cancel) // NB: Wait a bit more time to handle effects so this test is less brittle in CI await store.finish(timeout: NSEC_PER_SEC) + } } -} #endif diff --git a/Tests/ComposableArchitectureTests/StoreTests.swift b/Tests/ComposableArchitectureTests/StoreTests.swift index 157474594..98fcf9701 100644 --- a/Tests/ComposableArchitectureTests/StoreTests.swift +++ b/Tests/ComposableArchitectureTests/StoreTests.swift @@ -4,8 +4,8 @@ import XCTest // `@MainActor` introduces issues gathering tests on Linux #if !os(Linux) -@MainActor -final class StoreTests: XCTestCase { + @MainActor + final class StoreTests: XCTestCase { func testProducedMapping() { struct ChildState: Equatable { @@ -39,534 +39,534 @@ final class StoreTests: XCTestCase { } #if DEBUG - func testCancellableIsRemovedOnImmediatelyCompletingEffect() { - let store = Store(initialState: (), reducer: EmptyReducer()) + func testCancellableIsRemovedOnImmediatelyCompletingEffect() { + let store = Store(initialState: (), reducer: EmptyReducer()) XCTAssertEqual(store.effectDisposables.count, 0) - _ = store.send(()) + _ = store.send(()) XCTAssertEqual(store.effectDisposables.count, 0) - } + } #endif - func testCancellableIsRemovedWhenEffectCompletes() { + func testCancellableIsRemovedWhenEffectCompletes() { let mainQueue = TestScheduler() - let effect = EffectTask(value: ()) + let effect = EffectTask(value: ()) .deferred(for: 1, scheduler: mainQueue) - enum Action { case start, end } + enum Action { case start, end } - let reducer = Reduce({ _, action in - switch action { - case .start: - return effect.map { .end } - case .end: - return .none - } - }) - let store = Store(initialState: (), reducer: reducer) + let reducer = Reduce({ _, action in + switch action { + case .start: + return effect.map { .end } + case .end: + return .none + } + }) + let store = Store(initialState: (), reducer: reducer) XCTAssertEqual(store.effectDisposables.count, 0) - _ = store.send(.start) + _ = store.send(.start) XCTAssertEqual(store.effectDisposables.count, 1) - mainQueue.advance(by: 2) + mainQueue.advance(by: 2) XCTAssertEqual(store.effectDisposables.count, 0) - } + } - func testScopedStoreReceivesUpdatesFromParent() { - let counterReducer = Reduce({ state, _ in - state += 1 - return .none - }) + func testScopedStoreReceivesUpdatesFromParent() { + let counterReducer = Reduce({ state, _ in + state += 1 + return .none + }) - let parentStore = Store(initialState: 0, reducer: counterReducer) - let parentViewStore = ViewStore(parentStore) - let childStore = parentStore.scope(state: String.init) + let parentStore = Store(initialState: 0, reducer: counterReducer) + let parentViewStore = ViewStore(parentStore) + let childStore = parentStore.scope(state: String.init) - var values: [String] = [] + var values: [String] = [] let childViewStore = ViewStore(childStore) childViewStore.produced.producer .startWithValues { values.append($0) } - XCTAssertEqual(values, ["0"]) + XCTAssertEqual(values, ["0"]) - parentViewStore.send(()) + parentViewStore.send(()) - XCTAssertEqual(values, ["0", "1"]) - } + XCTAssertEqual(values, ["0", "1"]) + } - func testParentStoreReceivesUpdatesFromChild() { - let counterReducer = Reduce({ state, _ in - state += 1 - return .none - }) + func testParentStoreReceivesUpdatesFromChild() { + let counterReducer = Reduce({ state, _ in + state += 1 + return .none + }) - let parentStore = Store(initialState: 0, reducer: counterReducer) - let childStore = parentStore.scope(state: String.init) - let childViewStore = ViewStore(childStore) + let parentStore = Store(initialState: 0, reducer: counterReducer) + let childStore = parentStore.scope(state: String.init) + let childViewStore = ViewStore(childStore) - var values: [Int] = [] + var values: [Int] = [] let parentViewStore = ViewStore(parentStore) parentViewStore.produced.producer .startWithValues { values.append($0) } - XCTAssertEqual(values, [0]) + XCTAssertEqual(values, [0]) - childViewStore.send(()) + childViewStore.send(()) - XCTAssertEqual(values, [0, 1]) - } - - func testScopeCallCount() { - let counterReducer = Reduce({ state, _ in - state += 1 - return .none - }) - - var numCalls1 = 0 - _ = Store(initialState: 0, reducer: counterReducer) - .scope(state: { (count: Int) -> Int in - numCalls1 += 1 - return count - }) - - XCTAssertEqual(numCalls1, 1) - } + XCTAssertEqual(values, [0, 1]) + } - func testScopeCallCount2() { - let counterReducer = Reduce({ state, _ in - state += 1 - return .none - }) - - var numCalls1 = 0 - var numCalls2 = 0 - var numCalls3 = 0 - - let store1 = Store(initialState: 0, reducer: counterReducer) - let store2 = - store1 - .scope(state: { (count: Int) -> Int in - numCalls1 += 1 - return count - }) - let store3 = - store2 - .scope(state: { (count: Int) -> Int in - numCalls2 += 1 - return count - }) - let store4 = - store3 - .scope(state: { (count: Int) -> Int in - numCalls3 += 1 - return count + func testScopeCallCount() { + let counterReducer = Reduce({ state, _ in + state += 1 + return .none }) - let viewStore1 = ViewStore(store1) - let viewStore2 = ViewStore(store2) - let viewStore3 = ViewStore(store3) - let viewStore4 = ViewStore(store4) - - XCTAssertEqual(numCalls1, 1) - XCTAssertEqual(numCalls2, 1) - XCTAssertEqual(numCalls3, 1) - - viewStore4.send(()) + var numCalls1 = 0 + _ = Store(initialState: 0, reducer: counterReducer) + .scope(state: { (count: Int) -> Int in + numCalls1 += 1 + return count + }) - XCTAssertEqual(numCalls1, 2) - XCTAssertEqual(numCalls2, 2) - XCTAssertEqual(numCalls3, 2) - - viewStore4.send(()) - - XCTAssertEqual(numCalls1, 3) - XCTAssertEqual(numCalls2, 3) - XCTAssertEqual(numCalls3, 3) - - viewStore4.send(()) - - XCTAssertEqual(numCalls1, 4) - XCTAssertEqual(numCalls2, 4) - XCTAssertEqual(numCalls3, 4) - - viewStore4.send(()) - - XCTAssertEqual(numCalls1, 5) - XCTAssertEqual(numCalls2, 5) - XCTAssertEqual(numCalls3, 5) + XCTAssertEqual(numCalls1, 1) + } - _ = viewStore1 - _ = viewStore2 - _ = viewStore3 - } + func testScopeCallCount2() { + let counterReducer = Reduce({ state, _ in + state += 1 + return .none + }) - func testSynchronousEffectsSentAfterSinking() { - enum Action { - case tap - case next1 - case next2 - case end + var numCalls1 = 0 + var numCalls2 = 0 + var numCalls3 = 0 + + let store1 = Store(initialState: 0, reducer: counterReducer) + let store2 = + store1 + .scope(state: { (count: Int) -> Int in + numCalls1 += 1 + return count + }) + let store3 = + store2 + .scope(state: { (count: Int) -> Int in + numCalls2 += 1 + return count + }) + let store4 = + store3 + .scope(state: { (count: Int) -> Int in + numCalls3 += 1 + return count + }) + + let viewStore1 = ViewStore(store1) + let viewStore2 = ViewStore(store2) + let viewStore3 = ViewStore(store3) + let viewStore4 = ViewStore(store4) + + XCTAssertEqual(numCalls1, 1) + XCTAssertEqual(numCalls2, 1) + XCTAssertEqual(numCalls3, 1) + + viewStore4.send(()) + + XCTAssertEqual(numCalls1, 2) + XCTAssertEqual(numCalls2, 2) + XCTAssertEqual(numCalls3, 2) + + viewStore4.send(()) + + XCTAssertEqual(numCalls1, 3) + XCTAssertEqual(numCalls2, 3) + XCTAssertEqual(numCalls3, 3) + + viewStore4.send(()) + + XCTAssertEqual(numCalls1, 4) + XCTAssertEqual(numCalls2, 4) + XCTAssertEqual(numCalls3, 4) + + viewStore4.send(()) + + XCTAssertEqual(numCalls1, 5) + XCTAssertEqual(numCalls2, 5) + XCTAssertEqual(numCalls3, 5) + + _ = viewStore1 + _ = viewStore2 + _ = viewStore3 } - var values: [Int] = [] - let counterReducer = Reduce({ state, action in - switch action { - case .tap: - return .merge( - EffectTask(value: .next1), - EffectTask(value: .next2), + + func testSynchronousEffectsSentAfterSinking() { + enum Action { + case tap + case next1 + case next2 + case end + } + var values: [Int] = [] + let counterReducer = Reduce({ state, action in + switch action { + case .tap: + return .merge( + EffectTask(value: .next1), + EffectTask(value: .next2), Effect.fireAndForget { values.append(1) } - ) - case .next1: - return .merge( - EffectTask(value: .end), + ) + case .next1: + return .merge( + EffectTask(value: .end), Effect.fireAndForget { values.append(2) } - ) - case .next2: - return .fireAndForget { values.append(3) } - case .end: - return .fireAndForget { values.append(4) } - } - }) - - let store = Store(initialState: (), reducer: counterReducer) + ) + case .next2: + return .fireAndForget { values.append(3) } + case .end: + return .fireAndForget { values.append(4) } + } + }) - _ = ViewStore(store).send(.tap) + let store = Store(initialState: (), reducer: counterReducer) - XCTAssertEqual(values, [1, 2, 3, 4]) - } + _ = ViewStore(store).send(.tap) - func testLotsOfSynchronousActions() { - enum Action { case incr, noop } - let reducer = Reduce({ state, action in - switch action { - case .incr: - state += 1 - return state >= 100_000 ? EffectTask(value: .noop) : EffectTask(value: .incr) - case .noop: - return .none - } - }) + XCTAssertEqual(values, [1, 2, 3, 4]) + } - let store = Store(initialState: 0, reducer: reducer) - _ = ViewStore(store).send(.incr) - XCTAssertEqual(ViewStore(store).state, 100_000) - } + func testLotsOfSynchronousActions() { + enum Action { case incr, noop } + let reducer = Reduce({ state, action in + switch action { + case .incr: + state += 1 + return state >= 100_000 ? EffectTask(value: .noop) : EffectTask(value: .incr) + case .noop: + return .none + } + }) - func testIfLetAfterScope() { - struct AppState: Equatable { - var count: Int? + let store = Store(initialState: 0, reducer: reducer) + _ = ViewStore(store).send(.incr) + XCTAssertEqual(ViewStore(store).state, 100_000) } - let appReducer = Reduce({ state, action in - state.count = action - return .none - }) + func testIfLetAfterScope() { + struct AppState: Equatable { + var count: Int? + } - let parentStore = Store(initialState: AppState(), reducer: appReducer) - let parentViewStore = ViewStore(parentStore) + let appReducer = Reduce({ state, action in + state.count = action + return .none + }) - // NB: This test needs to hold a strong reference to the emitted stores - var outputs: [Int?] = [] - var stores: [Any] = [] + let parentStore = Store(initialState: AppState(), reducer: appReducer) + let parentViewStore = ViewStore(parentStore) - parentStore + // NB: This test needs to hold a strong reference to the emitted stores + var outputs: [Int?] = [] + var stores: [Any] = [] + + parentStore .scope(state: \.count) - .ifLet( - then: { store in - stores.append(store) - outputs.append(ViewStore(store).state) - }, - else: { - outputs.append(nil) + .ifLet( + then: { store in + stores.append(store) + outputs.append(ViewStore(store).state) + }, + else: { + outputs.append(nil) }) - XCTAssertEqual(outputs, [nil]) + XCTAssertEqual(outputs, [nil]) - _ = parentViewStore.send(1) - XCTAssertEqual(outputs, [nil, 1]) + _ = parentViewStore.send(1) + XCTAssertEqual(outputs, [nil, 1]) - _ = parentViewStore.send(nil) - XCTAssertEqual(outputs, [nil, 1, nil]) + _ = parentViewStore.send(nil) + XCTAssertEqual(outputs, [nil, 1, nil]) - _ = parentViewStore.send(1) - XCTAssertEqual(outputs, [nil, 1, nil, 1]) + _ = parentViewStore.send(1) + XCTAssertEqual(outputs, [nil, 1, nil, 1]) - _ = parentViewStore.send(nil) - XCTAssertEqual(outputs, [nil, 1, nil, 1, nil]) + _ = parentViewStore.send(nil) + XCTAssertEqual(outputs, [nil, 1, nil, 1, nil]) - _ = parentViewStore.send(1) - XCTAssertEqual(outputs, [nil, 1, nil, 1, nil, 1]) + _ = parentViewStore.send(1) + XCTAssertEqual(outputs, [nil, 1, nil, 1, nil, 1]) - _ = parentViewStore.send(nil) - XCTAssertEqual(outputs, [nil, 1, nil, 1, nil, 1, nil]) - } + _ = parentViewStore.send(nil) + XCTAssertEqual(outputs, [nil, 1, nil, 1, nil, 1, nil]) + } - func testIfLetTwo() { - let parentStore = Store( - initialState: 0, - reducer: Reduce({ state, action in - if action { - state? += 1 - return .none - } else { - return .task { true } - } - }) - ) + func testIfLetTwo() { + let parentStore = Store( + initialState: 0, + reducer: Reduce({ state, action in + if action { + state? += 1 + return .none + } else { + return .task { true } + } + }) + ) - parentStore - .ifLet(then: { childStore in - let vs = ViewStore(childStore) + parentStore + .ifLet(then: { childStore in + let vs = ViewStore(childStore) - vs + vs .produced.producer .startWithValues { _ in } - vs.send(false) - _ = XCTWaiter.wait(for: [.init()], timeout: 0.1) - vs.send(false) - _ = XCTWaiter.wait(for: [.init()], timeout: 0.1) - vs.send(false) - _ = XCTWaiter.wait(for: [.init()], timeout: 0.1) - XCTAssertEqual(vs.state, 3) - }) - } + vs.send(false) + _ = XCTWaiter.wait(for: [.init()], timeout: 0.1) + vs.send(false) + _ = XCTWaiter.wait(for: [.init()], timeout: 0.1) + vs.send(false) + _ = XCTWaiter.wait(for: [.init()], timeout: 0.1) + XCTAssertEqual(vs.state, 3) + }) + } - func testActionQueuing() async { + func testActionQueuing() async { let subject = Signal.pipe() - enum Action: Equatable { - case incrementTapped - case `init` - case doIncrement - } + enum Action: Equatable { + case incrementTapped + case `init` + case doIncrement + } - let store = TestStore( - initialState: 0, - reducer: Reduce({ state, action in - switch action { - case .incrementTapped: + let store = TestStore( + initialState: 0, + reducer: Reduce({ state, action in + switch action { + case .incrementTapped: subject.input.send(value: ()) - return .none + return .none - case .`init`: + case .`init`: return subject.output.producer .map { .doIncrement } .eraseToEffect() - case .doIncrement: - state += 1 - return .none - } - }) - ) + case .doIncrement: + state += 1 + return .none + } + }) + ) - await store.send(.`init`) - await store.send(.incrementTapped) - await store.receive(.doIncrement) { - $0 = 1 - } - await store.send(.incrementTapped) - await store.receive(.doIncrement) { - $0 = 2 - } + await store.send(.`init`) + await store.send(.incrementTapped) + await store.receive(.doIncrement) { + $0 = 1 + } + await store.send(.incrementTapped) + await store.receive(.doIncrement) { + $0 = 2 + } subject.input.sendCompleted() - } + } - func testCoalesceSynchronousActions() { - let store = Store( - initialState: 0, - reducer: Reduce({ state, action in - switch action { - case 0: - return .merge( - EffectTask(value: 1), - EffectTask(value: 2), - EffectTask(value: 3) - ) - default: - state = action - return .none - } - }) - ) + func testCoalesceSynchronousActions() { + let store = Store( + initialState: 0, + reducer: Reduce({ state, action in + switch action { + case 0: + return .merge( + EffectTask(value: 1), + EffectTask(value: 2), + EffectTask(value: 3) + ) + default: + state = action + return .none + } + }) + ) - var emissions: [Int] = [] - let viewStore = ViewStore(store) + var emissions: [Int] = [] + let viewStore = ViewStore(store) viewStore.produced.producer .startWithValues { emissions.append($0) } - XCTAssertEqual(emissions, [0]) + XCTAssertEqual(emissions, [0]) - viewStore.send(0) + viewStore.send(0) - XCTAssertEqual(emissions, [0, 3]) - } - - func testBufferedActionProcessing() { - struct ChildState: Equatable { - var count: Int? - } - - struct ParentState: Equatable { - var count: Int? - var child: ChildState? - } - - enum ParentAction: Equatable { - case button - case child(Int?) + XCTAssertEqual(emissions, [0, 3]) } - var handledActions: [ParentAction] = [] - let parentReducer = Reduce({ state, action in - handledActions.append(action) + func testBufferedActionProcessing() { + struct ChildState: Equatable { + var count: Int? + } - switch action { - case .button: - state.child = .init(count: nil) - return .none + struct ParentState: Equatable { + var count: Int? + var child: ChildState? + } - case .child(let childCount): - state.count = childCount - return .none + enum ParentAction: Equatable { + case button + case child(Int?) } - }) - .ifLet(\.child, action: /ParentAction.child) { - Reduce({ state, action in - state.count = action - return .none - }) - } - let parentStore = Store( - initialState: ParentState(), - reducer: parentReducer - ) + var handledActions: [ParentAction] = [] + let parentReducer = Reduce({ state, action in + handledActions.append(action) - parentStore - .scope( - state: \.child, - action: ParentAction.child - ) - .ifLet { childStore in - ViewStore(childStore).send(2) - } + switch action { + case .button: + state.child = .init(count: nil) + return .none - XCTAssertEqual(handledActions, []) + case .child(let childCount): + state.count = childCount + return .none + } + }) + .ifLet(\.child, action: /ParentAction.child) { + Reduce({ state, action in + state.count = action + return .none + }) + } - _ = ViewStore(parentStore).send(.button) - XCTAssertEqual( - handledActions, - [ - .button, - .child(2), - ]) - } + let parentStore = Store( + initialState: ParentState(), + reducer: parentReducer + ) - func testCascadingTaskCancellation() async { - enum Action { case task, response, response1, response2 } - let reducer = Reduce({ state, action in - switch action { - case .task: - return .task { .response } - case .response: - return .merge( - SignalProducer { _, _ in }.eraseToEffect(), - .task { .response1 } + parentStore + .scope( + state: \.child, + action: ParentAction.child ) - case .response1: - return .merge( - SignalProducer { _, _ in }.eraseToEffect(), - .task { .response2 } - ) - case .response2: - return SignalProducer { _, _ in }.eraseToEffect() - } - }) - - let store = TestStore( - initialState: 0, - reducer: reducer - ) - - let task = await store.send(.task) - await store.receive(.response) - await store.receive(.response1) - await store.receive(.response2) - await task.cancel() - } + .ifLet { childStore in + ViewStore(childStore).send(2) + } - func testTaskCancellationEmpty() async { - enum Action { case task } + XCTAssertEqual(handledActions, []) + + _ = ViewStore(parentStore).send(.button) + XCTAssertEqual( + handledActions, + [ + .button, + .child(2), + ]) + } - let store = TestStore( - initialState: 0, - reducer: Reduce({ state, action in + func testCascadingTaskCancellation() async { + enum Action { case task, response, response1, response2 } + let reducer = Reduce({ state, action in switch action { case .task: - return .fireAndForget { try await Task.never() } + return .task { .response } + case .response: + return .merge( + SignalProducer { _, _ in }.eraseToEffect(), + .task { .response1 } + ) + case .response1: + return .merge( + SignalProducer { _, _ in }.eraseToEffect(), + .task { .response2 } + ) + case .response2: + return SignalProducer { _, _ in }.eraseToEffect() } }) - ) - await store.send(.task).cancel() - } + let store = TestStore( + initialState: 0, + reducer: reducer + ) - func testScopeCancellation() async throws { - let neverEndingTask = Task { try await Task.never() } + let task = await store.send(.task) + await store.receive(.response) + await store.receive(.response1) + await store.receive(.response2) + await task.cancel() + } - let store = Store( - initialState: (), - reducer: Reduce({ _, _ in - .fireAndForget { - try await neverEndingTask.value - } - }) - ) - let scopedStore = store.scope(state: { $0 }) + func testTaskCancellationEmpty() async { + enum Action { case task } + + let store = TestStore( + initialState: 0, + reducer: Reduce({ state, action in + switch action { + case .task: + return .fireAndForget { try await Task.never() } + } + }) + ) + + await store.send(.task).cancel() + } + + func testScopeCancellation() async throws { + let neverEndingTask = Task { try await Task.never() } - let sendTask = scopedStore.send(()) - await Task.yield() - neverEndingTask.cancel() - try await XCTUnwrap(sendTask).value + let store = Store( + initialState: (), + reducer: Reduce({ _, _ in + .fireAndForget { + try await neverEndingTask.value + } + }) + ) + let scopedStore = store.scope(state: { $0 }) + + let sendTask = scopedStore.send(()) + await Task.yield() + neverEndingTask.cancel() + try await XCTUnwrap(sendTask).value XCTAssertEqual(store.effectDisposables.count, 0) XCTAssertEqual(scopedStore.effectDisposables.count, 0) - } + } - func testOverrideDependenciesDirectlyOnReducer() { - struct Counter: ReducerProtocol { - @Dependency(\.calendar) var calendar - @Dependency(\.locale) var locale - @Dependency(\.timeZone) var timeZone - @Dependency(\.urlSession) var urlSession - - func reduce(into state: inout Int, action: Bool) -> EffectTask { - _ = self.calendar - _ = self.locale - _ = self.timeZone - _ = self.urlSession - state += action ? 1 : -1 - return .none + func testOverrideDependenciesDirectlyOnReducer() { + struct Counter: ReducerProtocol { + @Dependency(\.calendar) var calendar + @Dependency(\.locale) var locale + @Dependency(\.timeZone) var timeZone + @Dependency(\.urlSession) var urlSession + + func reduce(into state: inout Int, action: Bool) -> EffectTask { + _ = self.calendar + _ = self.locale + _ = self.timeZone + _ = self.urlSession + state += action ? 1 : -1 + return .none + } } - } - let store = Store( - initialState: 0, - reducer: Counter() - .dependency(\.calendar, Calendar(identifier: .gregorian)) - .dependency(\.locale, Locale(identifier: "en_US")) - .dependency(\.timeZone, TimeZone(secondsFromGMT: 0)!) - .dependency(\.urlSession, URLSession(configuration: .ephemeral)) - ) + let store = Store( + initialState: 0, + reducer: Counter() + .dependency(\.calendar, Calendar(identifier: .gregorian)) + .dependency(\.locale, Locale(identifier: "en_US")) + .dependency(\.timeZone, TimeZone(secondsFromGMT: 0)!) + .dependency(\.urlSession, URLSession(configuration: .ephemeral)) + ) - ViewStore(store).send(true) + ViewStore(store).send(true) + } } -} #endif diff --git a/Tests/ComposableArchitectureTests/TestStoreFailureTests.swift b/Tests/ComposableArchitectureTests/TestStoreFailureTests.swift index 1961987a7..097bf2992 100644 --- a/Tests/ComposableArchitectureTests/TestStoreFailureTests.swift +++ b/Tests/ComposableArchitectureTests/TestStoreFailureTests.swift @@ -49,7 +49,7 @@ return .none } ) - + XCTExpectFailure { _ = store.send(()) { $0.count = 0 } } issueMatcher: { @@ -267,7 +267,7 @@ func testExpectedStateEqualityMustModify() async { let reducer = Reduce { state, action in switch action { - case true: return Effect(value: false) + case true: return Effect(value: false) case false: return .none } } diff --git a/Tests/ComposableArchitectureTests/TestStoreNonExhaustiveTests.swift b/Tests/ComposableArchitectureTests/TestStoreNonExhaustiveTests.swift index 038ef8a61..3ed178efe 100644 --- a/Tests/ComposableArchitectureTests/TestStoreNonExhaustiveTests.swift +++ b/Tests/ComposableArchitectureTests/TestStoreNonExhaustiveTests.swift @@ -4,7 +4,7 @@ import ComposableArchitecture import ReactiveSwift import XCTest - + @MainActor final class TestStoreNonExhaustiveTests: XCTestCase { func testSkipReceivedActions_NonStrict() async { @@ -227,7 +227,7 @@ reducer: Counter() ) store.exhaustivity = .off(showSkippedAssertions: true) - + store.send(.increment) { $0.count = 1 // Ignoring state change: isEven = false diff --git a/Tests/ComposableArchitectureTests/TestStoreTests.swift b/Tests/ComposableArchitectureTests/TestStoreTests.swift index 427fdd979..3a5a7598b 100644 --- a/Tests/ComposableArchitectureTests/TestStoreTests.swift +++ b/Tests/ComposableArchitectureTests/TestStoreTests.swift @@ -4,266 +4,266 @@ import XCTest // `@MainActor` introduces issues gathering tests on Linux #if !os(Linux) -@MainActor -final class TestStoreTests: XCTestCase { - func testEffectConcatenation() async { - struct State: Equatable {} + @MainActor + final class TestStoreTests: XCTestCase { + func testEffectConcatenation() async { + struct State: Equatable {} - enum Action: Equatable { - case a, b1, b2, b3, c1, c2, c3, d - } + enum Action: Equatable { + case a, b1, b2, b3, c1, c2, c3, d + } let mainQueue = TestScheduler() - let reducer = Reduce { _, action in - switch action { - case .a: - return .merge( - EffectTask.concatenate(.init(value: .b1), .init(value: .c1)) + let reducer = Reduce { _, action in + switch action { + case .a: + return .merge( + EffectTask.concatenate(.init(value: .b1), .init(value: .c1)) .deferred(for: 1, scheduler: mainQueue), Effect.none - .cancellable(id: 1) - ) - case .b1: - return - EffectTask - .concatenate(.init(value: .b2), .init(value: .b3)) - case .c1: - return - EffectTask - .concatenate(.init(value: .c2), .init(value: .c3)) - case .b2, .b3, .c2, .c3: - return .none - - case .d: - return .cancel(id: 1) + .cancellable(id: 1) + ) + case .b1: + return + EffectTask + .concatenate(.init(value: .b2), .init(value: .b3)) + case .c1: + return + EffectTask + .concatenate(.init(value: .c2), .init(value: .c3)) + case .b2, .b3, .c2, .c3: + return .none + + case .d: + return .cancel(id: 1) + } } - } - let store = TestStore( - initialState: State(), - reducer: reducer - ) + let store = TestStore( + initialState: State(), + reducer: reducer + ) - await store.send(.a) + await store.send(.a) - await mainQueue.advance(by: 1) + await mainQueue.advance(by: 1) - await store.receive(.b1) - await store.receive(.b2) - await store.receive(.b3) + await store.receive(.b1) + await store.receive(.b2) + await store.receive(.b3) - await store.receive(.c1) - await store.receive(.c2) - await store.receive(.c3) + await store.receive(.c1) + await store.receive(.c2) + await store.receive(.c3) - await store.send(.d) - } - - func testAsync() async { - enum Action: Equatable { - case tap - case response(Int) + await store.send(.d) } - let store = TestStore( - initialState: 0, - reducer: Reduce { state, action in - switch action { - case .tap: - return .task { .response(42) } - case let .response(number): - state = number - return .none - } + + func testAsync() async { + enum Action: Equatable { + case tap + case response(Int) } - ) + let store = TestStore( + initialState: 0, + reducer: Reduce { state, action in + switch action { + case .tap: + return .task { .response(42) } + case let .response(number): + state = number + return .none + } + } + ) - await store.send(.tap) - await store.receive(.response(42)) { - $0 = 42 + await store.send(.tap) + await store.receive(.response(42)) { + $0 = 42 + } } - } // `XCTExpectFailure` is not supported on Linux #if DEBUG && !os(Linux) - func testExpectedStateEquality() async { - struct State: Equatable { - var count: Int = 0 - var isChanging: Bool = false - } + func testExpectedStateEquality() async { + struct State: Equatable { + var count: Int = 0 + var isChanging: Bool = false + } - enum Action: Equatable { - case increment - case changed(from: Int, to: Int) - } + enum Action: Equatable { + case increment + case changed(from: Int, to: Int) + } - let reducer = Reduce { state, action in - switch action { - case .increment: - state.isChanging = true - return EffectTask(value: .changed(from: state.count, to: state.count + 1)) - case .changed(let from, let to): - state.isChanging = false - if state.count == from { - state.count = to + let reducer = Reduce { state, action in + switch action { + case .increment: + state.isChanging = true + return EffectTask(value: .changed(from: state.count, to: state.count + 1)) + case .changed(let from, let to): + state.isChanging = false + if state.count == from { + state.count = to + } + return .none } - return .none } - } - let store = TestStore(initialState: State(), reducer: reducer) + let store = TestStore(initialState: State(), reducer: reducer) - await store.send(.increment) { - $0.isChanging = true - } - await store.receive(.changed(from: 0, to: 1)) { - $0.isChanging = false - $0.count = 1 - } - - XCTExpectFailure { - _ = store.send(.increment) { + await store.send(.increment) { + $0.isChanging = true + } + await store.receive(.changed(from: 0, to: 1)) { $0.isChanging = false + $0.count = 1 } - } - XCTExpectFailure { - store.receive(.changed(from: 1, to: 2)) { - $0.isChanging = true - $0.count = 1100 + + XCTExpectFailure { + _ = store.send(.increment) { + $0.isChanging = false + } + } + XCTExpectFailure { + store.receive(.changed(from: 1, to: 2)) { + $0.isChanging = true + $0.count = 1100 + } } } - } - func testExpectedStateEqualityMustModify() async { - struct State: Equatable { - var count: Int = 0 - } + func testExpectedStateEqualityMustModify() async { + struct State: Equatable { + var count: Int = 0 + } - enum Action: Equatable { - case noop, finished - } + enum Action: Equatable { + case noop, finished + } - let reducer = Reduce { state, action in - switch action { - case .noop: - return EffectTask(value: .finished) - case .finished: - return .none + let reducer = Reduce { state, action in + switch action { + case .noop: + return EffectTask(value: .finished) + case .finished: + return .none + } } - } - let store = TestStore(initialState: State(), reducer: reducer) + let store = TestStore(initialState: State(), reducer: reducer) - await store.send(.noop) - await store.receive(.finished) + await store.send(.noop) + await store.receive(.finished) - XCTExpectFailure { - _ = store.send(.noop) { - $0.count = 0 + XCTExpectFailure { + _ = store.send(.noop) { + $0.count = 0 + } } - } - XCTExpectFailure { - store.receive(.finished) { - $0.count = 0 + XCTExpectFailure { + store.receive(.finished) { + $0.count = 0 + } } } - } - #endif - - func testStateAccess() async { - enum Action { case a, b, c, d } - let store = TestStore( - initialState: 0, - reducer: Reduce { count, action in - switch action { - case .a: - count += 1 + #endif + + func testStateAccess() async { + enum Action { case a, b, c, d } + let store = TestStore( + initialState: 0, + reducer: Reduce { count, action in + switch action { + case .a: + count += 1 return .merge(Effect(value: .b), Effect(value: .c), Effect(value: .d)) - case .b, .c, .d: - count += 1 - return .none + case .b, .c, .d: + count += 1 + return .none + } } - } - ) + ) - await store.send(.a) { - $0 = 1 - XCTAssertEqual(store.state, 0) - } - XCTAssertEqual(store.state, 1) - await store.receive(.b) { - $0 = 2 + await store.send(.a) { + $0 = 1 + XCTAssertEqual(store.state, 0) + } XCTAssertEqual(store.state, 1) - } - XCTAssertEqual(store.state, 2) - await store.receive(.c) { - $0 = 3 + await store.receive(.b) { + $0 = 2 + XCTAssertEqual(store.state, 1) + } XCTAssertEqual(store.state, 2) - } - XCTAssertEqual(store.state, 3) - await store.receive(.d) { - $0 = 4 + await store.receive(.c) { + $0 = 3 + XCTAssertEqual(store.state, 2) + } XCTAssertEqual(store.state, 3) + await store.receive(.d) { + $0 = 4 + XCTAssertEqual(store.state, 3) + } + XCTAssertEqual(store.state, 4) } - XCTAssertEqual(store.state, 4) - } - func testOverrideDependenciesDirectlyOnReducer() { - struct Counter: ReducerProtocol { - @Dependency(\.calendar) var calendar - @Dependency(\.locale) var locale - @Dependency(\.timeZone) var timeZone - @Dependency(\.urlSession) var urlSession - - func reduce(into state: inout Int, action: Bool) -> EffectTask { - _ = self.calendar - _ = self.locale - _ = self.timeZone - _ = self.urlSession - state += action ? 1 : -1 - return .none + func testOverrideDependenciesDirectlyOnReducer() { + struct Counter: ReducerProtocol { + @Dependency(\.calendar) var calendar + @Dependency(\.locale) var locale + @Dependency(\.timeZone) var timeZone + @Dependency(\.urlSession) var urlSession + + func reduce(into state: inout Int, action: Bool) -> EffectTask { + _ = self.calendar + _ = self.locale + _ = self.timeZone + _ = self.urlSession + state += action ? 1 : -1 + return .none + } } - } - let store = TestStore( - initialState: 0, - reducer: Counter() - .dependency(\.calendar, Calendar(identifier: .gregorian)) - .dependency(\.locale, Locale(identifier: "en_US")) - .dependency(\.timeZone, TimeZone(secondsFromGMT: 0)!) - .dependency(\.urlSession, URLSession(configuration: .ephemeral)) - ) + let store = TestStore( + initialState: 0, + reducer: Counter() + .dependency(\.calendar, Calendar(identifier: .gregorian)) + .dependency(\.locale, Locale(identifier: "en_US")) + .dependency(\.timeZone, TimeZone(secondsFromGMT: 0)!) + .dependency(\.urlSession, URLSession(configuration: .ephemeral)) + ) - store.send(true) { $0 = 1 } - } + store.send(true) { $0 = 1 } + } - func testOverrideDependenciesOnTestStore() { - struct Counter: ReducerProtocol { - @Dependency(\.calendar) var calendar - @Dependency(\.locale) var locale - @Dependency(\.timeZone) var timeZone - @Dependency(\.urlSession) var urlSession - - func reduce(into state: inout Int, action: Bool) -> EffectTask { - _ = self.calendar - _ = self.locale - _ = self.timeZone - _ = self.urlSession - state += action ? 1 : -1 - return .none + func testOverrideDependenciesOnTestStore() { + struct Counter: ReducerProtocol { + @Dependency(\.calendar) var calendar + @Dependency(\.locale) var locale + @Dependency(\.timeZone) var timeZone + @Dependency(\.urlSession) var urlSession + + func reduce(into state: inout Int, action: Bool) -> EffectTask { + _ = self.calendar + _ = self.locale + _ = self.timeZone + _ = self.urlSession + state += action ? 1 : -1 + return .none + } } - } - let store = TestStore( - initialState: 0, - reducer: Counter() - ) - store.dependencies.calendar = Calendar(identifier: .gregorian) - store.dependencies.locale = Locale(identifier: "en_US") - store.dependencies.timeZone = TimeZone(secondsFromGMT: 0)! - store.dependencies.urlSession = URLSession(configuration: .ephemeral) + let store = TestStore( + initialState: 0, + reducer: Counter() + ) + store.dependencies.calendar = Calendar(identifier: .gregorian) + store.dependencies.locale = Locale(identifier: "en_US") + store.dependencies.timeZone = TimeZone(secondsFromGMT: 0)! + store.dependencies.urlSession = URLSession(configuration: .ephemeral) - store.send(true) { $0 = 1 } + store.send(true) { $0 = 1 } + } } -} #endif