diff --git a/Tests/ComposableArchitectureTests/EffectTests.swift b/Tests/ComposableArchitectureTests/EffectTests.swift index 9faaea44e..cb518a2f4 100644 --- a/Tests/ComposableArchitectureTests/EffectTests.swift +++ b/Tests/ComposableArchitectureTests/EffectTests.swift @@ -4,12 +4,12 @@ import XCTest // `@MainActor` introduces issues gathering tests on Linux #if !os(Linux) -@MainActor -final class EffectTests: XCTestCase { + @MainActor + final class EffectTests: XCTestCase { let mainQueue = TestScheduler() func testEraseToEffectWithError() { - struct Error: Swift.Error, Equatable {} + struct Error: Swift.Error, Equatable {} SignalProducer(result: .success(42)) .startWithResult { XCTAssertNoDifference($0, .success(42)) } @@ -21,112 +21,112 @@ final class EffectTests: XCTestCase { .startWithResult { XCTAssertNoDifference($0, .success(42)) } SignalProducer(result: .success(42)) - .catchToEffect { - switch $0 { - case let .success(val): - return val - case .failure: - return -1 + .catchToEffect { + switch $0 { + case let .success(val): + return val + case .failure: + return -1 + } } - } .producer .startWithValues { XCTAssertNoDifference($0, 42) } SignalProducer(result: .failure(Error())) - .catchToEffect { - switch $0 { - case let .success(val): - return val - case .failure: - return -1 + .catchToEffect { + switch $0 { + case let .success(val): + return val + case .failure: + return -1 + } } - } .producer .startWithValues { XCTAssertNoDifference($0, -1) } - } + } - #if swift(>=5.7) && (canImport(RegexBuilder) || !os(macOS) && !targetEnvironment(macCatalyst)) - func testConcatenate() async { - if #available(iOS 16, macOS 13, tvOS 16, watchOS 9, *) { - let clock = TestClock() - var values: [Int] = [] - - let effect = EffectProducer.concatenate( - (1...3).map { count in - .task { - try await clock.sleep(for: .seconds(count)) - return count + #if swift(>=5.7) && (canImport(RegexBuilder) || !os(macOS) && !targetEnvironment(macCatalyst)) + func testConcatenate() async { + if #available(iOS 16, macOS 13, tvOS 16, watchOS 9, *) { + let clock = TestClock() + var values: [Int] = [] + + let effect = EffectProducer.concatenate( + (1...3).map { count in + .task { + try await clock.sleep(for: .seconds(count)) + return count + } } - } - ) + ) effect.producer.startWithValues { values.append($0) } - XCTAssertEqual(values, []) + XCTAssertEqual(values, []) - await clock.advance(by: .seconds(1)) - XCTAssertEqual(values, [1]) + await clock.advance(by: .seconds(1)) + XCTAssertEqual(values, [1]) - await clock.advance(by: .seconds(2)) - XCTAssertEqual(values, [1, 2]) + await clock.advance(by: .seconds(2)) + XCTAssertEqual(values, [1, 2]) - await clock.advance(by: .seconds(3)) - XCTAssertEqual(values, [1, 2, 3]) + await clock.advance(by: .seconds(3)) + XCTAssertEqual(values, [1, 2, 3]) - await clock.run() - XCTAssertEqual(values, [1, 2, 3]) + await clock.run() + XCTAssertEqual(values, [1, 2, 3]) + } } - } - #endif + #endif - func testConcatenateOneEffect() { - var values: [Int] = [] + func testConcatenateOneEffect() { + var values: [Int] = [] - let effect = EffectTask.concatenate( + let effect = EffectTask.concatenate( EffectTask(value: 1).deferred(for: 1, scheduler: mainQueue) - ) + ) effect.producer.startWithValues { values.append($0) } - XCTAssertEqual(values, []) - - self.mainQueue.advance(by: 1) - XCTAssertEqual(values, [1]) + XCTAssertEqual(values, []) - self.mainQueue.run() - XCTAssertEqual(values, [1]) - } + self.mainQueue.advance(by: 1) + XCTAssertEqual(values, [1]) - #if swift(>=5.7) && (canImport(RegexBuilder) || !os(macOS) && !targetEnvironment(macCatalyst)) - func testMerge() async { - if #available(iOS 16, macOS 13, tvOS 16, watchOS 9, *) { - let clock = TestClock() + self.mainQueue.run() + XCTAssertEqual(values, [1]) + } - let effect = EffectProducer.merge( - (1...3).map { count in - .task { - try await clock.sleep(for: .seconds(count)) - return count + #if swift(>=5.7) && (canImport(RegexBuilder) || !os(macOS) && !targetEnvironment(macCatalyst)) + func testMerge() async { + if #available(iOS 16, macOS 13, tvOS 16, watchOS 9, *) { + let clock = TestClock() + + let effect = EffectProducer.merge( + (1...3).map { count in + .task { + try await clock.sleep(for: .seconds(count)) + return count + } } - } - ) + ) - var values: [Int] = [] + var values: [Int] = [] effect.producer.startWithValues { values.append($0) } - XCTAssertEqual(values, []) + XCTAssertEqual(values, []) - await clock.advance(by: .seconds(1)) - XCTAssertEqual(values, [1]) + await clock.advance(by: .seconds(1)) + XCTAssertEqual(values, [1]) - await clock.advance(by: .seconds(1)) - XCTAssertEqual(values, [1, 2]) + await clock.advance(by: .seconds(1)) + XCTAssertEqual(values, [1, 2]) - await clock.advance(by: .seconds(1)) - XCTAssertEqual(values, [1, 2, 3]) + await clock.advance(by: .seconds(1)) + XCTAssertEqual(values, [1, 2, 3]) + } } - } - #endif + #endif func testEffectRunInitializer() { let effect = EffectTask.run { observer in @@ -134,226 +134,226 @@ final class EffectTests: XCTestCase { observer.send(value: 2) self.mainQueue.schedule(after: self.mainQueue.currentDate.addingTimeInterval(1)) { observer.send(value: 3) - } + } self.mainQueue.schedule(after: self.mainQueue.currentDate.addingTimeInterval(2)) { observer.send(value: 4) observer.sendCompleted() - } + } return AnyDisposable() - } + } - var values: [Int] = [] - var isComplete = false - effect + var values: [Int] = [] + var isComplete = false + effect .producer .on(completed: { isComplete = true }, value: { values.append($0) }) .start() - XCTAssertEqual(values, [1, 2]) - XCTAssertEqual(isComplete, false) + XCTAssertEqual(values, [1, 2]) + XCTAssertEqual(isComplete, false) - self.mainQueue.advance(by: 1) + self.mainQueue.advance(by: 1) - XCTAssertEqual(values, [1, 2, 3]) - XCTAssertEqual(isComplete, false) + XCTAssertEqual(values, [1, 2, 3]) + XCTAssertEqual(isComplete, false) - self.mainQueue.advance(by: 1) + self.mainQueue.advance(by: 1) - XCTAssertEqual(values, [1, 2, 3, 4]) - XCTAssertEqual(isComplete, true) - } + XCTAssertEqual(values, [1, 2, 3, 4]) + XCTAssertEqual(isComplete, true) + } func testEffectRunInitializer_WithCancellation() { - enum CancelID {} + enum CancelID {} - let effect = EffectTask.run { subscriber in + let effect = EffectTask.run { subscriber in subscriber.send(value: 1) self.mainQueue.schedule(after: self.mainQueue.currentDate.addingTimeInterval(1)) { subscriber.send(value: 2) - } + } return AnyDisposable() - } - .cancellable(id: CancelID.self) + } + .cancellable(id: CancelID.self) - var values: [Int] = [] - var isComplete = false - effect + var values: [Int] = [] + var isComplete = false + effect .producer .on(completed: { isComplete = true }) .startWithValues { values.append($0) } - XCTAssertEqual(values, [1]) - XCTAssertEqual(isComplete, false) + XCTAssertEqual(values, [1]) + XCTAssertEqual(isComplete, false) - EffectTask.cancel(id: CancelID.self) + EffectTask.cancel(id: CancelID.self) .producer .startWithValues { _ in } - self.mainQueue.advance(by: 1) + self.mainQueue.advance(by: 1) - XCTAssertEqual(values, [1]) - XCTAssertEqual(isComplete, true) - } + XCTAssertEqual(values, [1]) + XCTAssertEqual(isComplete, true) + } - func testDoubleCancelInFlight() { - var result: Int? + func testDoubleCancelInFlight() { + var result: Int? _ = Effect(value: 42) - .cancellable(id: "id", cancelInFlight: true) - .cancellable(id: "id", cancelInFlight: true) + .cancellable(id: "id", cancelInFlight: true) + .cancellable(id: "id", cancelInFlight: true) .producer .startWithValues { result = $0 } - XCTAssertEqual(result, 42) - } + XCTAssertEqual(result, 42) + } #if DEBUG && !os(Linux) - func testUnimplemented() { + func testUnimplemented() { let effect = EffectTask.failing("unimplemented") _ = XCTExpectFailure { - effect + effect .producer .start() - } issueMatcher: { issue in - issue.compactDescription == "unimplemented - An unimplemented effect ran." + } issueMatcher: { issue in + issue.compactDescription == "unimplemented - An unimplemented effect ran." + } } - } - #endif + #endif #if canImport(_Concurrency) && compiler(>=5.5.2) - func testTask() async { - guard #available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) else { return } - let effect = EffectTask.task { 42 } + func testTask() async { + guard #available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) else { return } + let effect = EffectTask.task { 42 } for await result in effect.producer.values { - XCTAssertEqual(result, 42) - } - } - - func testCancellingTask_Infallible() { - @Sendable func work() async -> Int { - do { - try await Task.sleep(nanoseconds: NSEC_PER_MSEC) - XCTFail() - } catch { + XCTAssertEqual(result, 42) + } } - return 42 - } + + func testCancellingTask_Infallible() { + @Sendable func work() async -> Int { + do { + try await Task.sleep(nanoseconds: NSEC_PER_MSEC) + XCTFail() + } catch { + } + return 42 + } let disposable = EffectTask.task { await work() } .producer .on( completed: { XCTFail() }, value: { _ in XCTFail() } - ) + ) .start(on: QueueScheduler.main) .start() disposable.dispose() - _ = XCTWaiter.wait(for: [.init()], timeout: 1.1) - } + _ = XCTWaiter.wait(for: [.init()], timeout: 1.1) + } #endif - func testDependenciesTransferredToEffects_Task() async { - struct Feature: ReducerProtocol { - enum Action: Equatable { - case tap - case response(Int) - } - @Dependency(\.date) var date - func reduce(into state: inout Int, action: Action) -> EffectTask { - switch action { - case .tap: - return .task { - .response(Int(self.date.now.timeIntervalSinceReferenceDate)) + func testDependenciesTransferredToEffects_Task() async { + struct Feature: ReducerProtocol { + enum Action: Equatable { + case tap + case response(Int) + } + @Dependency(\.date) var date + func reduce(into state: inout Int, action: Action) -> EffectTask { + switch action { + case .tap: + return .task { + .response(Int(self.date.now.timeIntervalSinceReferenceDate)) + } + case let .response(value): + state = value + return .none } - case let .response(value): - state = value - return .none } } - } - let store = TestStore( - initialState: 0, - reducer: Feature() - .dependency(\.date, .constant(.init(timeIntervalSinceReferenceDate: 1_234_567_890))) - ) - - await store.send(.tap).finish(timeout: NSEC_PER_SEC) - await store.receive(.response(1_234_567_890)) { - $0 = 1_234_567_890 - } - } + let store = TestStore( + initialState: 0, + reducer: Feature() + .dependency(\.date, .constant(.init(timeIntervalSinceReferenceDate: 1_234_567_890))) + ) - func testDependenciesTransferredToEffects_Run() async { - struct Feature: ReducerProtocol { - enum Action: Equatable { - case tap - case response(Int) + await store.send(.tap).finish(timeout: NSEC_PER_SEC) + await store.receive(.response(1_234_567_890)) { + $0 = 1_234_567_890 } - @Dependency(\.date) var date - func reduce(into state: inout Int, action: Action) -> EffectTask { - switch action { - case .tap: - return .run { send in - await send(.response(Int(self.date.now.timeIntervalSinceReferenceDate))) + } + + func testDependenciesTransferredToEffects_Run() async { + struct Feature: ReducerProtocol { + enum Action: Equatable { + case tap + case response(Int) + } + @Dependency(\.date) var date + func reduce(into state: inout Int, action: Action) -> EffectTask { + switch action { + case .tap: + return .run { send in + await send(.response(Int(self.date.now.timeIntervalSinceReferenceDate))) + } + case let .response(value): + state = value + return .none } - case let .response(value): - state = value - return .none } } - } - let store = TestStore( - initialState: 0, - reducer: Feature() - .dependency(\.date, .constant(.init(timeIntervalSinceReferenceDate: 1_234_567_890))) - ) - - await store.send(.tap).finish(timeout: NSEC_PER_SEC) - await store.receive(.response(1_234_567_890)) { - $0 = 1_234_567_890 - } - } + let store = TestStore( + initialState: 0, + reducer: Feature() + .dependency(\.date, .constant(.init(timeIntervalSinceReferenceDate: 1_234_567_890))) + ) - func testMap() async { - @Dependency(\.date) var date - let effect = - DependencyValues - .withValue(\.date, .init { Date(timeIntervalSince1970: 1_234_567_890) }) { - EffectTask(value: ()) - .map { date() } + await store.send(.tap).finish(timeout: NSEC_PER_SEC) + await store.receive(.response(1_234_567_890)) { + $0 = 1_234_567_890 } - var output: Date? - effect - .producer - .startWithValues { output = $0 } - XCTAssertEqual(output, Date(timeIntervalSince1970: 1_234_567_890)) + } - if #available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) { + func testMap() async { + @Dependency(\.date) var date let effect = DependencyValues .withValue(\.date, .init { Date(timeIntervalSince1970: 1_234_567_890) }) { - EffectTask.task {} + EffectTask(value: ()) .map { date() } } - output = await effect.values.first(where: { _ in true }) + var output: Date? + effect + .producer + .startWithValues { output = $0 } XCTAssertEqual(output, Date(timeIntervalSince1970: 1_234_567_890)) + + if #available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) { + let effect = + DependencyValues + .withValue(\.date, .init { Date(timeIntervalSince1970: 1_234_567_890) }) { + EffectTask.task {} + .map { date() } + } + output = await effect.values.first(where: { _ in true }) + XCTAssertEqual(output, Date(timeIntervalSince1970: 1_234_567_890)) + } } - } - func testCanary1() async { - for _ in 1...100 { - let task = TestStoreTask(rawValue: Task {}, timeout: NSEC_PER_SEC) - await task.finish() + func testCanary1() async { + for _ in 1...100 { + let task = TestStoreTask(rawValue: Task {}, timeout: NSEC_PER_SEC) + await task.finish() + } } - } - func testCanary2() async { - for _ in 1...100 { - let task = TestStoreTask(rawValue: nil, timeout: NSEC_PER_SEC) - await task.finish() + func testCanary2() async { + for _ in 1...100 { + let task = TestStoreTask(rawValue: nil, timeout: NSEC_PER_SEC) + await task.finish() + } } } -} #endif diff --git a/Tests/ComposableArchitectureTests/RuntimeWarningTests.swift b/Tests/ComposableArchitectureTests/RuntimeWarningTests.swift index efe6660ee..2d69aebf6 100644 --- a/Tests/ComposableArchitectureTests/RuntimeWarningTests.swift +++ b/Tests/ComposableArchitectureTests/RuntimeWarningTests.swift @@ -119,78 +119,78 @@ } #if os(macOS) - @MainActor - func testEffectEmitMainThread() async throws { - try XCTSkipIf(ProcessInfo.processInfo.environment["CI"] != nil) - XCTExpectFailure { - [ - """ - An effect completed on a non-main thread. … - - Effect returned from: - RuntimeWarningTests.Action.response - - Make sure to use ".receive(on:)" on any effects that execute on background threads to \ - receive their output on the main thread. - - The "Store" class is not thread-safe, and so all interactions with an instance of \ - "Store" (including all of its scopes and derived view stores) must be done on the main \ - thread. - """, - """ - An effect completed on a non-main thread. … - - Effect returned from: - RuntimeWarningTests.Action.tap - - Make sure to use ".receive(on:)" on any effects that execute on background threads to \ - receive their output on the main thread. - - The "Store" class is not thread-safe, and so all interactions with an instance of \ - "Store" (including all of its scopes and derived view stores) must be done on the main \ - thread. - """, - """ - An effect published an action on a non-main thread. … - - Effect published: - RuntimeWarningTests.Action.response - - Effect returned from: - RuntimeWarningTests.Action.tap - - Make sure to use ".receive(on:)" on any effects that execute on background threads to \ - receive their output on the main thread. - - The "Store" class is not thread-safe, and so all interactions with an instance of \ - "Store" (including all of its scopes and derived view stores) must be done on the main \ - thread. - """, - ] - .contains($0.compactDescription) - } + @MainActor + func testEffectEmitMainThread() async throws { + try XCTSkipIf(ProcessInfo.processInfo.environment["CI"] != nil) + XCTExpectFailure { + [ + """ + An effect completed on a non-main thread. … + + Effect returned from: + RuntimeWarningTests.Action.response + + Make sure to use ".receive(on:)" on any effects that execute on background threads to \ + receive their output on the main thread. + + The "Store" class is not thread-safe, and so all interactions with an instance of \ + "Store" (including all of its scopes and derived view stores) must be done on the main \ + thread. + """, + """ + An effect completed on a non-main thread. … + + Effect returned from: + RuntimeWarningTests.Action.tap + + Make sure to use ".receive(on:)" on any effects that execute on background threads to \ + receive their output on the main thread. + + The "Store" class is not thread-safe, and so all interactions with an instance of \ + "Store" (including all of its scopes and derived view stores) must be done on the main \ + thread. + """, + """ + An effect published an action on a non-main thread. … + + Effect published: + RuntimeWarningTests.Action.response + + Effect returned from: + RuntimeWarningTests.Action.tap + + Make sure to use ".receive(on:)" on any effects that execute on background threads to \ + receive their output on the main thread. + + The "Store" class is not thread-safe, and so all interactions with an instance of \ + "Store" (including all of its scopes and derived view stores) must be done on the main \ + thread. + """, + ] + .contains($0.compactDescription) + } - enum Action { case tap, response } - let store = Store( - initialState: 0, - reducer: Reduce { state, action in - switch action { - case .tap: - return .run { observer in - Thread.detachNewThread { - XCTAssertFalse(Thread.isMainThread, "Effect should send on non-main thread.") - observer.send(value: .response) - observer.sendCompleted() + enum Action { case tap, response } + let store = Store( + initialState: 0, + reducer: Reduce { state, action in + switch action { + case .tap: + return .run { observer in + Thread.detachNewThread { + XCTAssertFalse(Thread.isMainThread, "Effect should send on non-main thread.") + observer.send(value: .response) + observer.sendCompleted() + } + return AnyDisposable {} } - return AnyDisposable {} + case .response: + return .none } - case .response: - return .none } - } - ) - await ViewStore(store).send(.tap).finish() - } + ) + await ViewStore(store).send(.tap).finish() + } #endif @MainActor diff --git a/Tests/ComposableArchitectureTests/TestStoreTests.swift b/Tests/ComposableArchitectureTests/TestStoreTests.swift index 5bc0fb604..922200538 100644 --- a/Tests/ComposableArchitectureTests/TestStoreTests.swift +++ b/Tests/ComposableArchitectureTests/TestStoreTests.swift @@ -4,313 +4,313 @@ 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 - let store = TestStore( - initialState: State(), - reducer: reducer - ) + case .d: + return .cancel(id: 1) + } + } - await store.send(.a) + let store = TestStore( + initialState: State(), + reducer: reducer + ) - await mainQueue.advance(by: 1) + await store.send(.a) - await store.receive(.b1) - await store.receive(.b2) - await store.receive(.b3) + await mainQueue.advance(by: 1) - await store.receive(.c1) - await store.receive(.c2) - await store.receive(.c3) + await store.receive(.b1) + await store.receive(.b2) + await store.receive(.b3) - await store.send(.d) - } + await store.receive(.c1) + await store.receive(.c2) + await store.receive(.c3) - 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) - await store.send(.increment) { - $0.isChanging = true - } - await store.receive(.changed(from: 0, to: 1)) { - $0.isChanging = false - $0.count = 1 - } + let store = TestStore(initialState: State(), reducer: reducer) - 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 } + } - func testDependenciesEarlyBinding() async { - struct Feature: ReducerProtocol { - struct State: Equatable { - var count = 0 - var date: Date - init() { - @Dependency(\.date.now) var now: Date - self.date = now + func testDependenciesEarlyBinding() async { + struct Feature: ReducerProtocol { + struct State: Equatable { + var count = 0 + var date: Date + init() { + @Dependency(\.date.now) var now: Date + self.date = now + } } - } - enum Action: Equatable { - case tap - case response(Int) - } - @Dependency(\.date.now) var now: Date - func reduce(into state: inout State, action: Action) -> EffectTask { - switch action { - case .tap: - state.count += 1 - return .task { .response(42) } - case let .response(number): - state.count = number - state.date = now - return .none + enum Action: Equatable { + case tap + case response(Int) + } + @Dependency(\.date.now) var now: Date + func reduce(into state: inout State, action: Action) -> EffectTask { + switch action { + case .tap: + state.count += 1 + return .task { .response(42) } + case let .response(number): + state.count = number + state.date = now + return .none + } } } - } - let store = TestStore( - initialState: Feature.State(), - reducer: Feature() - ) { - $0.date = .constant(Date(timeIntervalSince1970: 1_234_567_890)) - } + let store = TestStore( + initialState: Feature.State(), + reducer: Feature() + ) { + $0.date = .constant(Date(timeIntervalSince1970: 1_234_567_890)) + } - await store.send(.tap) { - @Dependency(\.date.now) var now: Date - $0.count = 1 - $0.date = now - } - await store.receive(.response(42)) { - @Dependency(\.date.now) var now: Date - $0.count = 42 - $0.date = now + await store.send(.tap) { + @Dependency(\.date.now) var now: Date + $0.count = 1 + $0.date = now + } + await store.receive(.response(42)) { + @Dependency(\.date.now) var now: Date + $0.count = 42 + $0.date = now + } } } -} #endif