From db994cc2ff8dc2bb8584e784840e75b3901a8fc1 Mon Sep 17 00:00:00 2001 From: Andhika Setiadi Date: Mon, 3 Apr 2023 10:41:56 +0700 Subject: [PATCH] Update Code base to 0.50.0 Version (#85) * 0.48.0 - fix small typos * 0.48.0 - update docs TestStore * 0.48.0 update again docs * fix some test on core rxTCA * 0.50.0 - update changes based from #1816 * fix some assert warning & add some new test cases * add Equatable for TestStore.init based PR #1857 * add some comment related changes #1856 * Apply fix changes for ReducerBuilder from #1863 * push update package.resolved * Remove commented code and fix warnings * update workflow * Revert "update workflow" This reverts commit c795fc369b144ebed207faa9cf808cc14963dd0d. --------- Co-authored-by: Jefferson Setiawan --- .../xcshareddata/swiftpm/Package.resolved | 9 + .../Internal/RuntimeWarnings.swift | 2 + .../AnyReducer/AnyReducerCompatibility.swift | 2 +- .../Reducer/ReducerBuilder.swift | 137 +++----------- .../Reducer/Reducers/CombineReducers.swift | 5 +- .../Reducer/Reducers/ForEachReducer.swift | 11 +- .../Reducer/Reducers/IfCaseLetReducer.swift | 11 +- .../Reducer/Reducers/IfLetReducer.swift | 11 +- .../Reducer/Reducers/Scope.swift | 30 +-- Sources/RxComposableArchitecture/Store.swift | 2 +- .../TestSupport/TestStore.swift | 136 +++++++++++-- .../ForEachReducerTests.swift | 4 +- .../RuntimeWarningTests.swift | 16 ++ .../RxComposableArchitectureTests.swift | 179 ++++++++++-------- .../TestStoreNonExhaustiveTests.swift | 6 +- 15 files changed, 317 insertions(+), 244 deletions(-) diff --git a/Examples/Examples.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Examples/Examples.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index cbb182c..063aa71 100644 --- a/Examples/Examples.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Examples/Examples.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -37,6 +37,15 @@ "version": "0.8.1" } }, + { + "package": "swift-custom-dump", + "repositoryURL": "https://github.com/pointfreeco/swift-custom-dump", + "state": { + "branch": null, + "revision": "de8ba65649e7ee317b9daf27dd5eebf34bd4be57", + "version": "0.9.1" + } + }, { "package": "xctest-dynamic-overlay", "repositoryURL": "https://github.com/pointfreeco/xctest-dynamic-overlay", diff --git a/Sources/RxComposableArchitecture/Internal/RuntimeWarnings.swift b/Sources/RxComposableArchitecture/Internal/RuntimeWarnings.swift index ca7b9c8..a7f9b9e 100644 --- a/Sources/RxComposableArchitecture/Internal/RuntimeWarnings.swift +++ b/Sources/RxComposableArchitecture/Internal/RuntimeWarnings.swift @@ -1,3 +1,5 @@ +import Foundation + @_transparent @usableFromInline @inline(__always) diff --git a/Sources/RxComposableArchitecture/Reducer/AnyReducer/AnyReducerCompatibility.swift b/Sources/RxComposableArchitecture/Reducer/AnyReducer/AnyReducerCompatibility.swift index d638ae0..8975163 100644 --- a/Sources/RxComposableArchitecture/Reducer/AnyReducer/AnyReducerCompatibility.swift +++ b/Sources/RxComposableArchitecture/Reducer/AnyReducer/AnyReducerCompatibility.swift @@ -10,7 +10,7 @@ extension Reduce { } extension AnyReducer { - public init(@ReducerBuilderOf _ build: @escaping (Environment) -> R) + public init(@ReducerBuilder _ build: @escaping (Environment) -> R) where R.State == State, R.Action == Action { self.init { state, action, environment in build(environment).reduce(into: &state, action: action) diff --git a/Sources/RxComposableArchitecture/Reducer/ReducerBuilder.swift b/Sources/RxComposableArchitecture/Reducer/ReducerBuilder.swift index ead286d..bf2c7ad 100644 --- a/Sources/RxComposableArchitecture/Reducer/ReducerBuilder.swift +++ b/Sources/RxComposableArchitecture/Reducer/ReducerBuilder.swift @@ -7,23 +7,20 @@ /// See ``CombineReducers`` for an entry point into a reducer builder context. @resultBuilder public enum ReducerBuilder { -#if swift(>=5.7) @inlinable - public static func buildArray( - _ reducers: [some ReducerProtocol] - ) -> some ReducerProtocol { + public static func buildArray(_ reducers: [R]) -> _SequenceMany + where R.State == State, R.Action == Action { _SequenceMany(reducers: reducers) } @inlinable - public static func buildBlock() -> some ReducerProtocol { + public static func buildBlock() -> EmptyReducer { EmptyReducer() } @inlinable - public static func buildBlock( - _ reducer: some ReducerProtocol - ) -> some ReducerProtocol { + public static func buildBlock(_ reducer: R) -> R + where R.State == State, R.Action == Action { reducer } @@ -44,64 +41,48 @@ public enum ReducerBuilder { } @inlinable - public static func buildExpression( - _ expression: some ReducerProtocol - ) -> some ReducerProtocol { + public static func buildExpression(_ expression: R) -> R + where R.State == State, R.Action == Action { expression } @inlinable - public static func buildFinalResult( - _ reducer: some ReducerProtocol - ) -> some ReducerProtocol { + public static func buildFinalResult(_ reducer: R) -> R + where R.State == State, R.Action == Action { reducer } @inlinable - public static func buildLimitedAvailability( - _ wrapped: some ReducerProtocol - ) -> some ReducerProtocol { - _Optional(wrapped: wrapped) + public static func buildLimitedAvailability( + _ wrapped: R + ) -> Reduce + where R.State == State, R.Action == Action { + Reduce(wrapped) } @inlinable - public static func buildOptional( - _ wrapped: (some ReducerProtocol)? - ) -> some ReducerProtocol { - _Optional(wrapped: wrapped) + public static func buildOptional(_ wrapped: R?) -> R? + where R.State == State, R.Action == Action { + wrapped } @inlinable - public static func buildPartialBlock( - first: some ReducerProtocol - ) -> some ReducerProtocol { + public static func buildPartialBlock( + first: R + ) -> R + where R.State == State, R.Action == Action { first } @inlinable - public static func buildPartialBlock( - accumulated: some ReducerProtocol, next: some ReducerProtocol - ) -> some ReducerProtocol { + public static func buildPartialBlock( + accumulated: R0, next: R1 + ) -> _Sequence + where R0.State == State, R0.Action == Action, R1.State == State, R1.Action == Action { _Sequence(accumulated, next) } -#else - @inlinable - public static func buildArray(_ reducers: [R]) -> _SequenceMany - where R.State == State, R.Action == Action { - _SequenceMany(reducers: reducers) - } - - @inlinable - public static func buildBlock() -> EmptyReducer { - EmptyReducer() - } - - @inlinable - public static func buildBlock(_ reducer: R) -> R - where R.State == State, R.Action == Action { - reducer - } +#if swift(<5.7) @inlinable public static func buildBlock< R0: ReducerProtocol, @@ -330,54 +311,12 @@ public enum ReducerBuilder { ) } - @inlinable - public static func buildEither( - first reducer: R0 - ) -> _Conditional - where R0.State == State, R0.Action == Action { - .first(reducer) - } - - @inlinable - public static func buildEither( - second reducer: R1 - ) -> _Conditional - where R1.State == State, R1.Action == Action { - .second(reducer) - } - - @inlinable - public static func buildExpression(_ expression: R) -> R - where R.State == State, R.Action == Action { - expression - } - - @inlinable - public static func buildFinalResult(_ reducer: R) -> R - where R.State == State, R.Action == Action { - reducer - } - @_disfavoredOverload @inlinable public static func buildFinalResult(_ reducer: R) -> Reduce where R.State == State, R.Action == Action { Reduce(reducer) } - - @inlinable - public static func buildLimitedAvailability( - _ wrapped: R - ) -> _Optional - where R.State == State, R.Action == Action { - _Optional(wrapped: wrapped) - } - - @inlinable - public static func buildOptional(_ wrapped: R?) -> _Optional - where R.State == State, R.Action == Action { - _Optional(wrapped: wrapped) - } #endif public enum _Conditional: ReducerProtocol @@ -402,28 +341,6 @@ public enum ReducerBuilder { } } - public struct _Optional: ReducerProtocol { - @usableFromInline - let wrapped: Wrapped? - - @usableFromInline - init(wrapped: Wrapped?) { - self.wrapped = wrapped - } - - @inlinable - public func reduce( - into state: inout Wrapped.State, action: Wrapped.Action - ) -> Effect { - switch wrapped { - case let .some(wrapped): - return wrapped.reduce(into: &state, action: action) - case .none: - return .none - } - } - } - public struct _Sequence: ReducerProtocol where R0.State == R1.State, R0.Action == R1.Action { @usableFromInline @@ -462,5 +379,3 @@ public enum ReducerBuilder { } } } - -public typealias ReducerBuilderOf = ReducerBuilder diff --git a/Sources/RxComposableArchitecture/Reducer/Reducers/CombineReducers.swift b/Sources/RxComposableArchitecture/Reducer/Reducers/CombineReducers.swift index 160bc34..542972c 100644 --- a/Sources/RxComposableArchitecture/Reducer/Reducers/CombineReducers.swift +++ b/Sources/RxComposableArchitecture/Reducer/Reducers/CombineReducers.swift @@ -15,7 +15,8 @@ /// .ifLet(\.child, action: /Action.child) /// } /// ``` -public struct CombineReducers: ReducerProtocol { +public struct CombineReducers: ReducerProtocol +where State == Reducers.State, Action == Reducers.Action { @usableFromInline let reducers: Reducers @@ -24,7 +25,7 @@ public struct CombineReducers: ReducerProtocol { /// - Parameter build: A reducer builder. @inlinable public init( - @ReducerBuilderOf _ build: () -> Reducers + @ReducerBuilder _ build: () -> Reducers ) { self.init(internal: build()) } diff --git a/Sources/RxComposableArchitecture/Reducer/Reducers/ForEachReducer.swift b/Sources/RxComposableArchitecture/Reducer/Reducers/ForEachReducer.swift index 5759da5..d6b0460 100644 --- a/Sources/RxComposableArchitecture/Reducer/Reducers/ForEachReducer.swift +++ b/Sources/RxComposableArchitecture/Reducer/Reducers/ForEachReducer.swift @@ -50,14 +50,15 @@ extension ReducerProtocol { /// state. /// - Returns: A reducer that combines the child reducer with the parent reducer. @inlinable - public func forEach( - _ toElementsState: WritableKeyPath>, - action toElementAction: CasePath, - @ReducerBuilderOf _ element: () -> Element, + public func forEach( + _ toElementsState: WritableKeyPath>, + action toElementAction: CasePath, + @ReducerBuilder _ element: () -> Element, file: StaticString = #file, fileID: StaticString = #fileID, line: UInt = #line - ) -> _ForEachReducer { + ) -> _ForEachReducer + where ElementState == Element.State, ElementAction == Element.Action { _ForEachReducer( parent: self, toElementsState: toElementsState, diff --git a/Sources/RxComposableArchitecture/Reducer/Reducers/IfCaseLetReducer.swift b/Sources/RxComposableArchitecture/Reducer/Reducers/IfCaseLetReducer.swift index 4ba4a59..ba5dbb8 100644 --- a/Sources/RxComposableArchitecture/Reducer/Reducers/IfCaseLetReducer.swift +++ b/Sources/RxComposableArchitecture/Reducer/Reducers/IfCaseLetReducer.swift @@ -46,14 +46,15 @@ extension ReducerProtocol { /// present /// - Returns: A reducer that combines the child reducer with the parent reducer. @inlinable - public func ifCaseLet( - _ toCaseState: CasePath, - action toCaseAction: CasePath, - @ReducerBuilderOf then case: () -> Case, + public func ifCaseLet( + _ toCaseState: CasePath, + action toCaseAction: CasePath, + @ReducerBuilder then case: () -> Case, file: StaticString = #file, fileID: StaticString = #fileID, line: UInt = #line - ) -> _IfCaseLetReducer { + ) -> _IfCaseLetReducer + where CaseState == Case.State, CaseAction == Case.Action { .init( parent: self, child: `case`(), diff --git a/Sources/RxComposableArchitecture/Reducer/Reducers/IfLetReducer.swift b/Sources/RxComposableArchitecture/Reducer/Reducers/IfLetReducer.swift index e77f030..ff5ecc5 100644 --- a/Sources/RxComposableArchitecture/Reducer/Reducers/IfLetReducer.swift +++ b/Sources/RxComposableArchitecture/Reducer/Reducers/IfLetReducer.swift @@ -43,14 +43,15 @@ extension ReducerProtocol { /// state. /// - Returns: A reducer that combines the child reducer with the parent reducer. @inlinable - public func ifLet( - _ toWrappedState: WritableKeyPath, - action toWrappedAction: CasePath, - @ReducerBuilderOf then wrapped: () -> Wrapped, + public func ifLet( + _ toWrappedState: WritableKeyPath, + action toWrappedAction: CasePath, + @ReducerBuilder then wrapped: () -> Wrapped, file: StaticString = #file, fileID: StaticString = #fileID, line: UInt = #line - ) -> _IfLetReducer { + ) -> _IfLetReducer + where WrappedState == Wrapped.State, WrappedAction == Wrapped.Action { .init( parent: self, child: wrapped(), diff --git a/Sources/RxComposableArchitecture/Reducer/Reducers/Scope.swift b/Sources/RxComposableArchitecture/Reducer/Reducers/Scope.swift index 78153d6..84fdbbb 100644 --- a/Sources/RxComposableArchitecture/Reducer/Reducers/Scope.swift +++ b/Sources/RxComposableArchitecture/Reducer/Reducers/Scope.swift @@ -143,11 +143,11 @@ public struct Scope: ReducerP /// - toChildAction: A case path from parent action to a case containing child actions. /// - child: A reducer that will be invoked with child actions against child state. @inlinable - public init( - state toChildState: WritableKeyPath, - action toChildAction: CasePath, - @ReducerBuilderOf _ child: () -> Child - ) { + public init( + state toChildState: WritableKeyPath, + action toChildAction: CasePath, + @ReducerBuilder _ child: () -> Child + ) where ChildState == Child.State, ChildAction == Child.Action { self.init( toChildState: .keyPath(toChildState), toChildAction: toChildAction, @@ -156,14 +156,14 @@ public struct Scope: ReducerP } @inlinable - public init( - state toChildState: OptionalPath, - action toChildAction: CasePath, - @ReducerBuilderOf _ child: () -> Child, + public init( + state toChildState: OptionalPath, + action toChildAction: CasePath, + @ReducerBuilder _ child: () -> Child, file: StaticString = #file, fileID: StaticString = #fileID, line: UInt = #line - ) { + ) where ChildState == Child.State, ChildAction == Child.Action { self.init( toChildState: .optionalPath(toChildState, file: file, fileID: fileID, line: line), toChildAction: toChildAction, @@ -231,14 +231,14 @@ public struct Scope: ReducerP /// - toChildAction: A case path from parent action to a case containing child actions. /// - child: A reducer that will be invoked with child actions against child state. @inlinable - public init( - state toChildState: CasePath, - action toChildAction: CasePath, - @ReducerBuilderOf _ child: () -> Child, + public init( + state toChildState: CasePath, + action toChildAction: CasePath, + @ReducerBuilder _ child: () -> Child, file: StaticString = #file, fileID: StaticString = #fileID, line: UInt = #line - ) { + ) where ChildState == Child.State, ChildAction == Child.Action { self.init( toChildState: .optionalPath(OptionalPath(toChildState), file: file, fileID: fileID, line: line), toChildAction: toChildAction, diff --git a/Sources/RxComposableArchitecture/Store.swift b/Sources/RxComposableArchitecture/Store.swift index 8af228f..0d3fb71 100644 --- a/Sources/RxComposableArchitecture/Store.swift +++ b/Sources/RxComposableArchitecture/Store.swift @@ -498,7 +498,7 @@ public final class Store { case let .send(action, originatingAction: nil): runtimeWarn( """ - "ViewStore.send" was called on a non-main thread with: \(debugCaseOutput(action)) … + "Store.send/ViewStore.send" was called on a non-main thread with: \(debugCaseOutput(action)) … 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 \ diff --git a/Sources/RxComposableArchitecture/TestSupport/TestStore.swift b/Sources/RxComposableArchitecture/TestSupport/TestStore.swift index 0ca255a..1dd39d3 100644 --- a/Sources/RxComposableArchitecture/TestSupport/TestStore.swift +++ b/Sources/RxComposableArchitecture/TestSupport/TestStore.swift @@ -565,11 +565,12 @@ public final class TestStore( + /// - prepareDependencies: A closure that can be used to override dependencies that will be + /// accessed during the test. These dependencies will be used when producing the initial + /// state. + public convenience init( initialState: @autoclosure () -> State, - reducer: Reducer, + reducer: R, prepareDependencies: (inout DependencyValues) -> Void = { _ in }, file: StaticString = #file, line: UInt = #line, @@ -577,12 +578,108 @@ public final class TestStore and the documentation of ``TestStore`` for more information on how to best + /// use a test store. + /// + /// - Parameters: + /// - initialState: The state the feature starts in. + /// - reducer: The reducer that powers the runtime of the feature. + /// - toScopedState: A function that transforms the reducer's state into scoped state. This + /// state will be asserted against as it is mutated by the reducer. Useful for testing view + /// store state transformations. + /// - prepareDependencies: A closure that can be used to override dependencies that will be + /// accessed during the test. These dependencies will be used when producing the initial + /// state. + public convenience init( + initialState: @autoclosure () -> State, + reducer: R, + observe toScopedState: @escaping (State) -> ScopedState, + prepareDependencies: (inout DependencyValues) -> Void = { _ in }, + file: StaticString = #file, + line: UInt = #line, + failingWhenNothingChange: Bool = true, + useNewScope: Bool = false + ) + where + R.State == State, + R.Action == Action, + ScopedState: Equatable, + Action == ScopedAction, + Environment == Void + { + self.init( + initialState: initialState(), + reducer: reducer, + observe: toScopedState, + send: { $0 }, + prepareDependencies: prepareDependencies, + file: file, + line: line, + failingWhenNothingChange: failingWhenNothingChange, + useNewScope: useNewScope + ) + } + + /// Creates a scoped test store with an initial state and a reducer powering its runtime. + /// + /// See and the documentation of ``TestStore`` for more information on how to best + /// use a test store. + /// + /// - Parameters: + /// - initialState: The state the feature starts in. + /// - reducer: The reducer that powers the runtime of the feature. + /// - toScopedState: A function that transforms the reducer's state into scoped state. This + /// state will be asserted against as it is mutated by the reducer. Useful for testing view + /// store state transformations. + /// - fromScopedAction: A function that wraps a more scoped action in the reducer's action. + /// Scoped actions can be "sent" to the store, while any reducer action may be received. + /// Useful for testing view store action transformations. + /// - prepareDependencies: A closure that can be used to override dependencies that will be + /// accessed during the test. These dependencies will be used when producing the initial + /// state. + public init( + initialState: @autoclosure () -> State, + reducer: R, + observe toScopedState: @escaping (State) -> ScopedState, + send fromScopedAction: @escaping (ScopedAction) -> Action, + prepareDependencies: (inout DependencyValues) -> Void = { _ in }, + file: StaticString = #file, + line: UInt = #line, + failingWhenNothingChange: Bool = true, + useNewScope: Bool = false + ) + where + R.State == State, + R.Action == Action, + ScopedState: Equatable, + Environment == Void + { + /// Notes: We still use old implementation + /// on 0.50.0 version, this code already using `withDependencies` implementation + /// which required us to do vendored `swift-dependencies` + /// var dependencies = DependencyValues() dependencies.context = .test prepareDependencies(&dependencies) @@ -592,18 +689,14 @@ public final class TestStore( state toScopedState: @escaping (ScopedState) -> S, action fromScopedAction: @escaping (A) -> ScopedAction @@ -1779,6 +1880,13 @@ extension TestStore { /// - Parameter toScopedState: A function that transforms the reducer's state into scoped state. /// This state will be asserted against as it is mutated by the reducer. Useful for testing view /// store state transformations. + @available( + *, + deprecated, + message: """ + Use 'TestStore.init(initialState:reducer:observe:)' to scope a test store's state. + """ + ) public func scope( state toScopedState: @escaping (ScopedState) -> S ) -> TestStore { diff --git a/Tests/RxComposableArchitectureTests/ForEachReducerTests.swift b/Tests/RxComposableArchitectureTests/ForEachReducerTests.swift index b8c737e..25f4cc5 100644 --- a/Tests/RxComposableArchitectureTests/ForEachReducerTests.swift +++ b/Tests/RxComposableArchitectureTests/ForEachReducerTests.swift @@ -85,7 +85,7 @@ struct Elements: ReducerProtocol { } #if swift(>=5.7) var body: some ReducerProtocol { - Reduce { state, action in + Reduce { state, action in .none } .forEach(\.rows, action: /Action.row) { @@ -99,7 +99,7 @@ struct Elements: ReducerProtocol { } #else var body: Reduce { - Reduce { state, action in + Reduce { state, action in .none } .forEach(\.rows, action: /Action.row) { diff --git a/Tests/RxComposableArchitectureTests/RuntimeWarningTests.swift b/Tests/RxComposableArchitectureTests/RuntimeWarningTests.swift index 3e0707a..6a920f1 100644 --- a/Tests/RxComposableArchitectureTests/RuntimeWarningTests.swift +++ b/Tests/RxComposableArchitectureTests/RuntimeWarningTests.swift @@ -79,5 +79,21 @@ final class RuntimeWarningTests: XCTestCase { } _ = XCTWaiter.wait(for: [.init()], timeout: 0.5) } + + func testStoreSendMainThread() { + XCTExpectFailure { + $0.compactDescription == """ + "Store.send/ViewStore.send" was called on a non-main thread with: () … + + 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. + """ + } + + let store = Store(initialState: 0, reducer: EmptyReducer()) + Task { + store.send(()) + } + _ = XCTWaiter.wait(for: [.init()], timeout: 0.5) + } } #endif diff --git a/Tests/RxComposableArchitectureTests/RxComposableArchitectureTests.swift b/Tests/RxComposableArchitectureTests/RxComposableArchitectureTests.swift index b860073..394a2e6 100644 --- a/Tests/RxComposableArchitectureTests/RxComposableArchitectureTests.swift +++ b/Tests/RxComposableArchitectureTests/RxComposableArchitectureTests.swift @@ -9,47 +9,54 @@ import RxComposableArchitecture import RxSwift import XCTest +import XCTestDynamicOverlay internal class RxComposableArchitectureTests: XCTestCase { internal func testScheduling() { - enum CounterAction: Equatable { - case incrAndSquareLater - case incrNow - case squareNow - } - - let counterReducer = AnyReducer { - state, action, scheduler in - switch action { - case .incrAndSquareLater: - return .merge( - Effect(value: .incrNow) - .delay(.seconds(2), scheduler: scheduler) - .eraseToEffect(), - Effect(value: .squareNow) - .delay(.seconds(1), scheduler: scheduler) - .eraseToEffect(), - Effect(value: .squareNow) - .delay(.seconds(2), scheduler: scheduler) - .eraseToEffect() - ) - case .incrNow: - state += 1 - return .none - case .squareNow: - state *= state - return .none + struct Counter: ReducerProtocol { + typealias State = Int + + enum Action: Equatable { + case incrAndSquareLater + case incrNow + case squareNow + } + + @Dependency(\.mainQueue) var scheduler + + func reduce(into state: inout Int, action: Action) -> Effect { + switch action { + case .incrAndSquareLater: + return .merge( + Effect(value: .incrNow) + .delay(.seconds(2), scheduler: scheduler) + .eraseToEffect(), + Effect(value: .squareNow) + .delay(.seconds(1), scheduler: scheduler) + .eraseToEffect(), + Effect(value: .squareNow) + .delay(.seconds(2), scheduler: scheduler) + .eraseToEffect() + ) + case .incrNow: + state += 1 + return .none + case .squareNow: + state *= state + return .none + } } } - + let scheduler = TestScheduler(initialClock: 0) let store = TestStore( initialState: 2, - reducer: counterReducer, - environment: scheduler, + reducer: Counter(), useNewScope: true ) + + store.dependencies.mainQueue = scheduler store.send(.incrAndSquareLater) scheduler.advance(by: .seconds(1)) @@ -64,36 +71,27 @@ internal class RxComposableArchitectureTests: XCTestCase { store.receive(.incrNow) { $0 = 626 } store.receive(.squareNow) { $0 = 391_876 } } - + internal func testLongLivingEffects() { - typealias Environment = ( - startEffect: Effect, - stopEffect: Effect - ) - + let subject = PublishSubject() + enum Action { case end, incr, start } - let reducer = AnyReducer { state, action, environment in + let reducer = Reduce { state, action in switch action { case .end: - return environment.stopEffect.fireAndForget() + return .fireAndForget { subject.onCompleted() } case .incr: state += 1 return .none case .start: - return environment.startEffect.map { Action.incr } + return subject.eraseToEffect().map { Action.incr } } } - - let subject = PublishSubject() - + let store = TestStore( initialState: 0, reducer: reducer, - environment: ( - startEffect: subject.eraseToEffect(), - stopEffect: .fireAndForget { subject.onCompleted() } - ), useNewScope: true ) @@ -105,49 +103,51 @@ internal class RxComposableArchitectureTests: XCTestCase { } internal func testCancellation() { - enum Action: Equatable { - case cancel - case incr - case response(Int) - } - - struct Environment { - let fetch: (Int) -> Effect - let mainQueue: TestScheduler - } - - let reducer = AnyReducer { state, action, environment in - enum CancelId {} - - switch action { - case .cancel: - return .cancel(id: CancelId.self) + struct Cancellation: ReducerProtocol { + typealias State = Int + + enum Action: Equatable { + case cancel + case incr + case response(Int) + } + + @Dependency(\.myEnvironment) var environment + @Dependency(\.mainQueue) var mainQueue + + func reduce(into state: inout Int, action: Action) -> Effect { + enum CancelId {} + + switch action { + case .cancel: + return .cancel(id: CancelId.self) + + case .incr: + state += 1 + return environment.fetch(state) + .observeOn(mainQueue) + .map(Action.response) + .eraseToEffect() + .cancellable(id: CancelId.self) - case .incr: - state += 1 - return environment.fetch(state) - .observeOn(environment.mainQueue) - .map(Action.response) - .eraseToEffect() - .cancellable(id: CancelId.self) - - case let .response(value): - state = value - return .none + case let .response(value): + state = value + return .none + } } } - + + let scheduler = TestScheduler(initialClock: 0) let store = TestStore( initialState: 0, - reducer: reducer, - environment: Environment( - fetch: { value in Effect(value: value * value) }, - mainQueue: scheduler - ), + reducer: Cancellation(), useNewScope: true ) + + store.dependencies.myEnvironment.fetch = { value in Effect(value: value * value) } + store.dependencies.mainQueue = scheduler store.send(.incr) { $0 = 1 } scheduler.advance(by: .milliseconds(1)) @@ -158,3 +158,22 @@ internal class RxComposableArchitectureTests: XCTestCase { scheduler.run() } } + +private struct MyEnvironment: DependencyKey { + var fetch: (Int) -> Effect + + static let liveValue: MyEnvironment = MyEnvironment( + fetch: { value in Effect(value: value * value) } + ) + + static let testValue: MyEnvironment = MyEnvironment( + fetch: unimplemented("unimplemented fetch") + ) +} + +extension DependencyValues { + fileprivate var myEnvironment: MyEnvironment { + get { self[MyEnvironment.self] } + set { self[MyEnvironment.self] = newValue } + } +} diff --git a/Tests/RxComposableArchitectureTests/TestStoreNonExhaustiveTests.swift b/Tests/RxComposableArchitectureTests/TestStoreNonExhaustiveTests.swift index c3d1161..676f73a 100644 --- a/Tests/RxComposableArchitectureTests/TestStoreNonExhaustiveTests.swift +++ b/Tests/RxComposableArchitectureTests/TestStoreNonExhaustiveTests.swift @@ -360,13 +360,13 @@ final class TestStoreNonExhaustiveTests: XCTestCase { } issueMatcher: { $0.compactDescription == """ A state change does not match expectation: … - -   TestStoreNonExhaustiveTests.State( + +   TestStoreNonExhaustiveTests.Feature.State( − count: 2, + count: 1,   isLoggedIn: true   ) - + (Expected: −, Actual: +) """ }