Update reportIssue formatting to avoid console errors (#3795)

When a non-ASCII character is fed to `reportIssue`, the following is
output to the console:

```
<decode: bad range for [%@] got [offs:330 len:1073 within:0]>
```

This causes a lot of confusion for folks, so let's update the formatting
to avoid this.
This commit is contained in:
Stephen Celis
2025-10-16 08:35:05 -07:00
committed by GitHub
parent 5cb5b917eb
commit 2ef7c09b0b
31 changed files with 344 additions and 259 deletions

View File

@@ -146,7 +146,7 @@ final class RootCore<Root: Reducer>: Core {
if isCompleted.value { if isCompleted.value {
reportIssue( reportIssue(
""" """
An action was sent from a completed effect: An action was sent from a completed effect.
Action: Action:
\(debugCaseOutput(effectAction)) \(debugCaseOutput(effectAction))

View File

@@ -123,7 +123,7 @@ public struct DismissEffect: Sendable {
else { else {
reportIssue( reportIssue(
""" """
A reducer requested dismissal at "\(fileID):\(line)", but couldn't be dismissed. A reducer requested dismissal at "\(fileID):\(line)", but couldn't be dismissed.
This is generally considered an application logic error, and can happen when a reducer \ This is generally considered an application logic error, and can happen when a reducer \
assumes it runs in a presentation context. If a reducer can run at both the root level \ assumes it runs in a presentation context. If a reducer can run at both the root level \

View File

@@ -30,6 +30,6 @@ struct RecordMeetingTests {
// The store received 1 unexpected action by the end of this test: // The store received 1 unexpected action by the end of this test:
// //
// Unhandled actions: // Unhandled actions:
// .timerTick // .timerTick
} }
} }

View File

@@ -113,7 +113,7 @@ extension Effect {
guard let handler else { guard let handler else {
reportIssue( reportIssue(
""" """
An "Effect.run" returned from "\(fileID):\(line)" threw an unhandled error. An "Effect.run" returned from "\(fileID):\(line)" threw an unhandled error.
\(String(customDumping: error).indent(by: 4)) \(String(customDumping: error).indent(by: 4))

View File

@@ -271,7 +271,7 @@ extension TaskResult: Equatable where Success: Equatable {
let lhsTypeName = typeName(lhsType) let lhsTypeName = typeName(lhsType)
reportIssue( reportIssue(
""" """
"\(lhsTypeName)" is not equatable. "\(lhsTypeName)" is not equatable.
To test two values of this type, it must conform to the "Equatable" protocol. For \ To test two values of this type, it must conform to the "Equatable" protocol. For \
example: example:
@@ -307,7 +307,7 @@ extension TaskResult: Hashable where Success: Hashable {
let errorType = typeName(type(of: error)) let errorType = typeName(type(of: error))
reportIssue( reportIssue(
""" """
"\(errorType)" is not hashable. "\(errorType)" is not hashable.
To hash a value of this type, it must conform to the "Hashable" protocol. For example: To hash a value of this type, it must conform to the "Hashable" protocol. For example:

View File

@@ -111,7 +111,7 @@ extension BindingAction {
} }
reportIssue( reportIssue(
""" """
A binding action sent from a store was not handled. A binding action sent from a store was not handled.
Action: Action:
\(typeName(Action.self)).binding(.set(_, \(valueDump))) \(typeName(Action.self)).binding(.set(_, \(valueDump)))

View File

@@ -389,7 +389,7 @@ public struct _NavigationLinkStoreContent<State, Label: View>: View {
""" """
reportIssue( reportIssue(
""" """
A navigation link at "\(fileID):\(line)" is unpresentable. A navigation link at "\(fileID):\(line)" is not presentable.
NavigationStack state element type: NavigationStack state element type:
\(elementType) \(elementType)

View File

@@ -500,16 +500,16 @@ extension Store where State: ObservableState {
func uncachedStoreWarning<State, Action>(_ store: Store<State, Action>) -> String { func uncachedStoreWarning<State, Action>(_ store: Store<State, Action>) -> String {
""" """
Scoping from uncached \(store) is not compatible with observation. Scoping from uncached '\(store)' is not compatible with observation.
This can happen for one of two reasons: This can happen for one of two reasons:
• A parent view scopes on a store using transform functions, which has been \ 1. A parent view scopes on a store using transform functions, which has been \
deprecated, instead of with key paths and case paths. Read the migration guide for 1.5 \ deprecated, instead of with key paths and case paths. Read the migration guide for 1.5 \
to update these scopes: \ to update these scopes: \
https://swiftpackageindex.com/pointfreeco/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5 https://swiftpackageindex.com/pointfreeco/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5
 A parent feature is using deprecated navigation APIs, such as 'IfLetStore', \ 2. A parent feature is using deprecated navigation APIs, such as 'IfLetStore', \
'SwitchStore', 'ForEachStore', or any navigation view modifiers taking stores instead of \ 'SwitchStore', 'ForEachStore', or any navigation view modifiers taking stores instead of \
bindings. Read the migration guide for 1.7 to update those APIs: \ bindings. Read the migration guide for 1.7 to update those APIs: \
https://swiftpackageindex.com/pointfreeco/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7 https://swiftpackageindex.com/pointfreeco/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7

View File

@@ -281,22 +281,22 @@ public struct _ForEachReducer<
if state[keyPath: self.toElementsState][id: id] == nil { if state[keyPath: self.toElementsState][id: id] == nil {
reportIssue( reportIssue(
""" """
A "forEach" at "\(self.fileID):\(self.line)" received an action for a missing element. A "forEach" at "\(self.fileID):\(self.line)" received an action for a missing element.
Action: Action:
\(debugCaseOutput(action)) \(debugCaseOutput(action))
This is generally considered an application logic error, and can happen for a few reasons: This is generally considered an application logic error, and can happen for a few reasons:
A parent reducer removed an element with this ID before this reducer ran. This reducer \ A parent reducer removed an element with this ID before this reducer ran. This reducer \
must run before any other reducer removes an element, which ensures that element reducers \ must run before any other reducer removes an element, which ensures that element reducers \
can handle their actions while their state is still available. can handle their actions while their state is still available.
An in-flight effect emitted this action when state contained no element at this ID. \ An in-flight effect emitted this action when state contained no element at this ID. \
While it may be perfectly reasonable to ignore this action, consider canceling the \ While it may be perfectly reasonable to ignore this action, consider canceling the \
associated effect before an element is removed, especially if it is a long-living effect. associated effect before an element is removed, especially if it is a long-living effect.
This action was sent to the store while its state contained no element at this ID. To \ This action was sent to the store while its state contained no element at this ID. To \
fix this make sure that actions for this reducer can only be sent from a store when \ fix this make sure that actions for this reducer can only be sent from a store when \
its state contains an element at this id. In SwiftUI applications, use "ForEachStore". its state contains an element at this id. In SwiftUI applications, use "ForEachStore".
""", """,

View File

@@ -215,7 +215,7 @@ public struct _IfCaseLetReducer<Parent: Reducer, Child: Reducer>: Reducer {
reportIssue( reportIssue(
""" """
An "ifCaseLet" at "\(self.fileID):\(self.line)" received a child action when child state \ An "ifCaseLet" at "\(self.fileID):\(self.line)" received a child action when child state \
was set to a different case. was set to a different case.
Action: Action:
\(String(customDumping: action).indent(by: 4)) \(String(customDumping: action).indent(by: 4))
@@ -224,16 +224,16 @@ public struct _IfCaseLetReducer<Parent: Reducer, Child: Reducer>: Reducer {
This is generally considered an application logic error, and can happen for a few reasons: This is generally considered an application logic error, and can happen for a few reasons:
A parent reducer set "\(typeName(Parent.State.self))" to a different case before this \ A parent reducer set "\(typeName(Parent.State.self))" to a different case before this \
reducer ran. This reducer must run before any other reducer sets child state to a \ reducer ran. This reducer must run before any other reducer sets child state to a \
different case. This ensures that child reducers can handle their actions while their \ different case. This ensures that child reducers can handle their actions while their \
state is still available. state is still available.
An in-flight effect emitted this action when child state was unavailable. While it may \ An in-flight effect emitted this action when child state was unavailable. While it may \
be perfectly reasonable to ignore this action, consider canceling the associated effect \ be perfectly reasonable to ignore this action, consider canceling the associated effect \
before child state changes to another case, especially if it is a long-living effect. before child state changes to another case, especially if it is a long-living effect.
This action was sent to the store while state was another case. Make sure that actions \ This action was sent to the store while state was another case. Make sure that actions \
for this reducer can only be sent from a store when state is set to the appropriate \ for this reducer can only be sent from a store when state is set to the appropriate \
case. In SwiftUI applications, use "SwitchStore". case. In SwiftUI applications, use "SwitchStore".
""", """,

View File

@@ -287,22 +287,22 @@ public struct _IfLetReducer<Parent: Reducer, Child: Reducer>: Reducer {
reportIssue( reportIssue(
""" """
An "ifLet" at "\(self.fileID):\(self.line)" received a child action when child state was \ An "ifLet" at "\(self.fileID):\(self.line)" received a child action when child state was \
"nil". "nil".
Action: Action:
\(String(customDumping: action).indent(by: 4)) \(String(customDumping: action).indent(by: 4))
This is generally considered an application logic error, and can happen for a few reasons: This is generally considered an application logic error, and can happen for a few reasons:
A parent reducer set child state to "nil" before this reducer ran. This reducer must run \ A parent reducer set child state to "nil" before this reducer ran. This reducer must run \
before any other reducer sets child state to "nil". This ensures that child reducers can \ before any other reducer sets child state to "nil". This ensures that child reducers can \
handle their actions while their state is still available. handle their actions while their state is still available.
An in-flight effect emitted this action when child state was "nil". While it may be \ An in-flight effect emitted this action when child state was "nil". While it may be \
perfectly reasonable to ignore this action, consider canceling the associated effect \ perfectly reasonable to ignore this action, consider canceling the associated effect \
before child state becomes "nil", especially if it is a long-living effect. before child state becomes "nil", especially if it is a long-living effect.
This action was sent to the store while state was "nil". Make sure that actions for this \ This action was sent to the store while state was "nil". Make sure that actions for this \
reducer can only be sent from a store when state is non-"nil". In SwiftUI \ reducer can only be sent from a store when state is non-"nil". In SwiftUI \
applications, use "IfLetStore". applications, use "IfLetStore".
""", """,

View File

@@ -632,7 +632,7 @@ public struct _PresentationReducer<Base: Reducer, Destination: Reducer>: Reducer
reportIssue( reportIssue(
""" """
An "ifLet" at "\(self.fileID):\(self.line)" received a presentation action when \ An "ifLet" at "\(self.fileID):\(self.line)" received a presentation action when \
destination state was absent. destination state was absent.
Action: Action:
\(debugCaseOutput(action)) \(debugCaseOutput(action))
@@ -640,11 +640,11 @@ public struct _PresentationReducer<Base: Reducer, Destination: Reducer>: Reducer
This is generally considered an application logic error, and can happen for a few \ This is generally considered an application logic error, and can happen for a few \
reasons: reasons:
A parent reducer set destination state to "nil" before this reducer ran. This reducer \ A parent reducer set destination state to "nil" before this reducer ran. This reducer \
must run before any other reducer sets destination state to "nil". This ensures that \ must run before any other reducer sets destination state to "nil". This ensures that \
destination reducers can handle their actions while their state is still present. destination reducers can handle their actions while their state is still present.
This action was sent to the store while destination state was "nil". Make sure that \ This action was sent to the store while destination state was "nil". Make sure that \
actions for this reducer can only be sent from a store when state is present, or \ actions for this reducer can only be sent from a store when state is present, or \
from effects that start from this reducer. from effects that start from this reducer.
""", """,

View File

@@ -347,7 +347,7 @@ public struct Scope<ParentState, ParentAction, Child: Reducer>: Reducer {
reportIssue( reportIssue(
""" """
A "Scope" at "\(fileID):\(line)" received a child action when child state was set to a \ A "Scope" at "\(fileID):\(line)" received a child action when child state was set to a \
different case. different case.
Action: Action:
\(debugCaseOutput(action)) \(debugCaseOutput(action))
@@ -357,18 +357,18 @@ public struct Scope<ParentState, ParentAction, Child: Reducer>: Reducer {
This is generally considered an application logic error, and can happen for a few \ This is generally considered an application logic error, and can happen for a few \
reasons: reasons:
A parent reducer set "\(typeName(ParentState.self))" to a different case before the \ A parent reducer set "\(typeName(ParentState.self))" to a different case before the \
scoped reducer ran. Child reducers must run before any parent reducer sets child state \ scoped reducer ran. Child reducers must run before any parent reducer sets child state \
to a different case. This ensures that child reducers can handle their actions while \ to a different case. This ensures that child reducers can handle their actions while \
their state is still available. Consider using "Reducer.ifCaseLet" to embed this \ their state is still available. Consider using "Reducer.ifCaseLet" to embed this \
child reducer in the parent reducer that change its state to ensure the child reducer \ child reducer in the parent reducer that change its state to ensure the child reducer \
runs first. runs first.
An in-flight effect emitted this action when child state was unavailable. While it may \ An in-flight effect emitted this action when child state was unavailable. While it may \
be perfectly reasonable to ignore this action, consider canceling the associated effect \ be perfectly reasonable to ignore this action, consider canceling the associated effect \
before child state changes to another case, especially if it is a long-living effect. before child state changes to another case, especially if it is a long-living effect.
This action was sent to the store while state was another case. Make sure that actions \ This action was sent to the store while state was another case. Make sure that actions \
for this reducer can only be sent from a store when state is set to the appropriate \ for this reducer can only be sent from a store when state is set to the appropriate \
case. In SwiftUI applications, use "SwitchStore". case. In SwiftUI applications, use "SwitchStore".
""", """,

View File

@@ -526,22 +526,22 @@ public struct _StackReducer<Base: Reducer, Destination: Reducer>: Reducer {
} else { } else {
reportIssue( reportIssue(
""" """
A "forEach" at "\(self.fileID):\(self.line)" received an action for a missing element. A "forEach" at "\(self.fileID):\(self.line)" received an action for a missing element.
Action: Action:
\(debugCaseOutput(destinationAction)) \(debugCaseOutput(destinationAction))
This is generally considered an application logic error, and can happen for a few reasons: This is generally considered an application logic error, and can happen for a few reasons:
A parent reducer removed an element with this ID before this reducer ran. This reducer \ A parent reducer removed an element with this ID before this reducer ran. This reducer \
must run before any other reducer removes an element, which ensures that element \ must run before any other reducer removes an element, which ensures that element \
reducers can handle their actions while their state is still available. reducers can handle their actions while their state is still available.
An in-flight effect emitted this action when state contained no element at this ID. \ An in-flight effect emitted this action when state contained no element at this ID. \
While it may be perfectly reasonable to ignore this action, consider canceling the \ While it may be perfectly reasonable to ignore this action, consider canceling the \
associated effect before an element is removed, especially if it is a long-living effect. associated effect before an element is removed, especially if it is a long-living effect.
This action was sent to the store while its state contained no element at this ID. To \ This action was sent to the store while its state contained no element at this ID. To \
fix this make sure that actions for this reducer can only be sent from a store when \ fix this make sure that actions for this reducer can only be sent from a store when \
its state contains an element at this id. In SwiftUI applications, use \ its state contains an element at this id. In SwiftUI applications, use \
"NavigationStack.init(path:)" with a binding to a store. "NavigationStack.init(path:)" with a binding to a store.
@@ -566,7 +566,7 @@ public struct _StackReducer<Base: Reducer, Destination: Reducer>: Reducer {
reportIssue( reportIssue(
""" """
A "forEach" at "\(self.fileID):\(self.line)" received a "popFrom" action for a missing \ A "forEach" at "\(self.fileID):\(self.line)" received a "popFrom" action for a missing \
element. element.
ID: ID:
\(id) \(id)
@@ -586,7 +586,7 @@ public struct _StackReducer<Base: Reducer, Destination: Reducer>: Reducer {
reportIssue( reportIssue(
""" """
A "forEach" at "\(self.fileID):\(self.line)" received a "push" action for an element it \ A "forEach" at "\(self.fileID):\(self.line)" received a "push" action for an element it \
already contains. already contains.
ID: ID:
\(id) \(id)
@@ -606,7 +606,7 @@ public struct _StackReducer<Base: Reducer, Destination: Reducer>: Reducer {
reportIssue( reportIssue(
""" """
A "forEach" at "\(self.fileID):\(self.line)" received a "push" action with an \ A "forEach" at "\(self.fileID):\(self.line)" received a "push" action with an \
unexpected generational ID. unexpected generational ID.
Received ID: Received ID:
\(id) \(id)

View File

@@ -788,7 +788,7 @@ extension WithViewStore where ViewState: Equatable, Content: View {
""" """
A binding action sent from a store \ A binding action sent from a store \
\(context == .bindingState ? "for binding state defined " : "")at \ \(context == .bindingState ? "for binding state defined " : "")at \
"\(fileID):\(line)" was not handled. "\(fileID):\(line)" was not handled.
Action: Action:
\(typeName(bindableActionType)).binding(.set(_, \(valueDump))) \(typeName(bindableActionType)).binding(.set(_, \(valueDump)))

View File

@@ -663,26 +663,26 @@ public final class TestStore<State: Equatable, Action> {
reportIssueHelper( reportIssueHelper(
""" """
An effect returned for this action is still running. It must complete before the end of \ An effect returned for this action is still running. It must complete before the end of \
the test. the test.
To fix, inspect any effects the reducer returns for this action and ensure that all of \ To fix, inspect any effects the reducer returns for this action and ensure that all of \
them complete by the end of the test. There are a few reasons why an effect may not have \ them complete by the end of the test. There are a few reasons why an effect may not have \
completed: completed:
If using async/await in your effect, it may need a little bit of time to properly \ If using async/await in your effect, it may need a little bit of time to properly \
finish. To fix you can simply perform "await store.finish()" at the end of your test. finish. To fix you can simply perform "await store.finish()" at the end of your test.
If an effect uses a clock (or scheduler, via "receive(on:)", "delay", "debounce", etc.), \ If an effect uses a clock (or scheduler, via "receive(on:)", "delay", "debounce", etc.), \
make sure that you wait enough time for it to perform the effect. If you are using a test \ make sure that you wait enough time for it to perform the effect. If you are using a test \
clock/scheduler, advance it so that the effects may complete, or consider using an \ clock/scheduler, advance it so that the effects may complete, or consider using an \
immediate clock/scheduler to immediately perform the effect instead. immediate clock/scheduler to immediately perform the effect instead.
If you are returning a long-living effect (timers, notifications, subjects, etc.), \ If you are returning a long-living effect (timers, notifications, subjects, etc.), \
then make sure those effects are torn down by marking the effect ".cancellable" and \ then make sure those effects are torn down by marking the effect ".cancellable" and \
returning a corresponding cancellation effect ("Effect.cancel") from another action, or, \ returning a corresponding cancellation effect ("Effect.cancel") from another action, or, \
if your effect is driven by a Combine subject, send it a completion. if your effect is driven by a Combine subject, send it a completion.
If you do not wish to assert on these effects, perform "await \ If you do not wish to assert on these effects, perform "await \
store.skipInFlightEffects()", or consider using a non-exhaustive test store: \ store.skipInFlightEffects()", or consider using a non-exhaustive test store: \
"store.exhaustivity = .off". "store.exhaustivity = .off".
""", """,
@@ -709,12 +709,12 @@ public final class TestStore<State: Equatable, Action> {
if !self.reducer.receivedActions.isEmpty { if !self.reducer.receivedActions.isEmpty {
let actions = self.reducer.receivedActions let actions = self.reducer.receivedActions
.map(\.action) .map(\.action)
.map { " " + debugCaseOutput($0, abbreviated: true) } .map { " " + debugCaseOutput($0, abbreviated: true) }
.joined(separator: "\n") .joined(separator: "\n")
reportIssueHelper( reportIssueHelper(
""" """
The store received \(self.reducer.receivedActions.count) unexpected \ The store received \(self.reducer.receivedActions.count) unexpected \
action\(self.reducer.receivedActions.count == 1 ? "" : "s"): … action\(self.reducer.receivedActions.count == 1 ? "" : "s").
Unhandled actions: Unhandled actions:
\(actions) \(actions)
@@ -965,7 +965,7 @@ extension TestStore {
reportIssueHelper( reportIssueHelper(
""" """
Must handle \(self.reducer.receivedActions.count) received \ Must handle \(self.reducer.receivedActions.count) received \
action\(self.reducer.receivedActions.count == 1 ? "" : "s") before sending an action: … action\(self.reducer.receivedActions.count == 1 ? "" : "s") before sending an action.
Unhandled actions: \(actions) Unhandled actions: \(actions)
""", """,
@@ -1197,7 +1197,7 @@ extension TestStore {
} catch { } catch {
reportIssue( reportIssue(
""" """
Skipped assertions: … Skipped assertions.
Threw error: \(error) Threw error: \(error)
""", """,
@@ -1241,7 +1241,7 @@ extension TestStore {
: "State was not expected to change, but a change occurred" : "State was not expected to change, but a change occurred"
reportIssueHelper( reportIssueHelper(
""" """
\(messageHeading): … \(messageHeading).
\(difference)\(postamble.isEmpty ? "" : "\n\n\(postamble)") \(difference)\(postamble.isEmpty ? "" : "\n\n\(postamble)")
""", """,
@@ -1292,7 +1292,7 @@ extension TestStore where Action: Equatable {
self.receiveAction( self.receiveAction(
matching: { expectedAction == $0 }, matching: { expectedAction == $0 },
failureMessage: """ failureMessage: """
Expected to receive the following action, but didn't: Expected to receive the following action, but didn't:
\(expectedActionDump) \(expectedActionDump)
""", """,
@@ -2133,7 +2133,7 @@ extension TestStore {
reportIssueHelper( reportIssueHelper(
""" """
\(actions.count) received action\ \(actions.count) received action\
\(actions.count == 1 ? " was" : "s were") skipped: \(actions.count == 1 ? " was" : "s were") skipped.
\(actionsDump) \(actionsDump)
""", """,
@@ -2151,7 +2151,7 @@ extension TestStore {
.contains(where: { action, _ in predicate(receivedAction) }) .contains(where: { action, _ in predicate(receivedAction) })
reportIssueHelper( reportIssueHelper(
""" """
Received unexpected action\(receivedActionLater ? " before this one" : ""): Received unexpected action\(receivedActionLater ? " before this one" : ""):
\(unexpectedActionDescription(receivedAction)) \(unexpectedActionDescription(receivedAction))
""", """,
@@ -2427,7 +2427,7 @@ extension TestStore {
reportIssueHelper( reportIssueHelper(
""" """
\(self.reducer.receivedActions.count) received action\ \(self.reducer.receivedActions.count) received action\
\(self.reducer.receivedActions.count == 1 ? " was" : "s were") skipped: \(self.reducer.receivedActions.count == 1 ? " was" : "s were") skipped.
\(actions) \(actions)
""", """,
@@ -2550,7 +2550,7 @@ extension TestStore {
withExpectedIssue { withExpectedIssue {
reportIssue( reportIssue(
""" """
Skipped assertions: … Skipped assertions.
\(message) \(message)
""", """,

View File

@@ -10,14 +10,16 @@
var line: UInt! var line: UInt!
XCTExpectFailure { XCTExpectFailure {
$0.compactDescription == """ $0.compactDescription.hasSuffix(
failed - An "Effect.run" returned from "\(#fileID):\(line+1)" threw an unhandled error. … """
An "Effect.run" returned from "\(#fileID):\(line+1)" threw an unhandled error.
EffectFailureTests.Unexpected() EffectFailureTests.Unexpected()
All non-cancellation errors must be explicitly handled via the "catch" parameter on \ All non-cancellation errors must be explicitly handled via the "catch" parameter on \
"Effect.run", or via a "do" block. "Effect.run", or via a "do" block.
""" """
)
} }
line = #line line = #line

View File

@@ -47,14 +47,16 @@ final class EffectRunTests: BaseTCATestCase {
func testRunUnhandledFailure() async { func testRunUnhandledFailure() async {
var line: UInt! var line: UInt!
XCTExpectFailure(nil, enabled: nil, strict: nil) { XCTExpectFailure(nil, enabled: nil, strict: nil) {
$0.compactDescription == """ $0.compactDescription.hasSuffix(
failed - An "Effect.run" returned from "\(#fileID):\(line+1)" threw an unhandled error. … """
An "Effect.run" returned from "\(#fileID):\(line+1)" threw an unhandled error.
EffectRunTests.Failure() EffectRunTests.Failure()
All non-cancellation errors must be explicitly handled via the "catch" parameter on \ All non-cancellation errors must be explicitly handled via the "catch" parameter on \
"Effect.run", or via a "do" block. "Effect.run", or via a "do" block.
""" """
)
} }
struct State: Equatable {} struct State: Equatable {}
enum Action: Equatable { case tapped, response } enum Action: Equatable { case tapped, response }
@@ -126,8 +128,9 @@ final class EffectRunTests: BaseTCATestCase {
@MainActor @MainActor
func testRunEscapeFailure() async throws { func testRunEscapeFailure() async throws {
XCTExpectFailure { XCTExpectFailure {
$0.compactDescription == """ $0.compactDescription.hasSuffix(
failed - An action was sent from a completed effect: """
An action was sent from a completed effect.
Action: Action:
EffectRunTests.Action.response EffectRunTests.Action.response
@@ -141,6 +144,7 @@ final class EffectRunTests: BaseTCATestCase {
To fix this, make sure that your 'run' closure does not return until you're done \ To fix this, make sure that your 'run' closure does not return until you're done \
calling 'send'. calling 'send'.
""" """
)
} }
enum Action { case tap, response } enum Action { case tap, response }

View File

@@ -78,11 +78,7 @@ final class ObservableTests: BaseTCATestCase {
} }
func testReplace() async { func testReplace() async {
#if swift(<6.2) XCTTODO("Ideally this would pass but we cannot detect this kind of mutation currently.")
if #available(iOS 17, macOS 14, tvOS 14, watchOS 10, *) {
XCTTODO("Ideally this would pass but we cannot detect this kind of mutation currently.")
}
#endif
var state = ChildState(count: 42) var state = ChildState(count: 42)
let didChange = LockIsolated(false) let didChange = LockIsolated(false)
@@ -98,12 +94,7 @@ final class ObservableTests: BaseTCATestCase {
} }
func testReset() async { func testReset() async {
#if swift(<6.2) XCTTODO("Ideally this would pass but we cannot detect this kind of mutation currently.")
if #available(iOS 17, macOS 14, tvOS 14, watchOS 10, *) {
XCTTODO("Ideally this would pass but we cannot detect this kind of mutation currently.")
}
#endif
var state = ChildState(count: 42) var state = ChildState(count: 42)
let didChange = LockIsolated(false) let didChange = LockIsolated(false)

View File

@@ -41,27 +41,28 @@ final class ForEachReducerTests: BaseTCATestCase {
} }
XCTExpectFailure { XCTExpectFailure {
$0.compactDescription == """ $0.compactDescription.hasSuffix(
failed - A "forEach" at "\(#fileID):\(#line - 5)" received an action for a missing \ """
element. A "forEach" at "\(#fileID):\(#line - 6)" received an action for a missing element.
Action: Action:
Elements.Action.rows(.element(id:, action:)) Elements.Action.rows(.element(id:, action:))
This is generally considered an application logic error, and can happen for a few reasons: This is generally considered an application logic error, and can happen for a few reasons:
A parent reducer removed an element with this ID before this reducer ran. This reducer \ A parent reducer removed an element with this ID before this reducer ran. This reducer \
must run before any other reducer removes an element, which ensures that element \ must run before any other reducer removes an element, which ensures that element \
reducers can handle their actions while their state is still available. reducers can handle their actions while their state is still available.
An in-flight effect emitted this action when state contained no element at this ID. \ An in-flight effect emitted this action when state contained no element at this ID. \
While it may be perfectly reasonable to ignore this action, consider canceling the \ While it may be perfectly reasonable to ignore this action, consider canceling the \
associated effect before an element is removed, especially if it is a long-living effect. associated effect before an element is removed, especially if it is a long-living effect.
This action was sent to the store while its state contained no element at this ID. To \ This action was sent to the store while its state contained no element at this ID. To \
fix this make sure that actions for this reducer can only be sent from a store when \ fix this make sure that actions for this reducer can only be sent from a store when \
its state contains an element at this id. In SwiftUI applications, use "ForEachStore". its state contains an element at this id. In SwiftUI applications, use "ForEachStore".
""" """
)
} }
await store.send(\.rows[id: 1], "Blob Esq.") await store.send(\.rows[id: 1], "Blob Esq.")

View File

@@ -39,9 +39,10 @@ final class IfCaseLetReducerTests: BaseTCATestCase {
} }
XCTExpectFailure { XCTExpectFailure {
$0.compactDescription == """ $0.compactDescription.hasSuffix(
failed - An "ifCaseLet" at "\(#fileID):\(#line - 5)" received a child action when child \ """
state was set to a different case. … An "ifCaseLet" at "\(#fileID):\(#line - 6)" received a child action when child state was \
set to a different case.
Action: Action:
Result.success(1) Result.success(1)
@@ -50,18 +51,19 @@ final class IfCaseLetReducerTests: BaseTCATestCase {
This is generally considered an application logic error, and can happen for a few reasons: This is generally considered an application logic error, and can happen for a few reasons:
A parent reducer set "Result" to a different case before this reducer ran. This \ A parent reducer set "Result" to a different case before this reducer ran. This \
reducer must run before any other reducer sets child state to a different case. This \ reducer must run before any other reducer sets child state to a different case. This \
ensures that child reducers can handle their actions while their state is still available. ensures that child reducers can handle their actions while their state is still available.
An in-flight effect emitted this action when child state was unavailable. While it may \ An in-flight effect emitted this action when child state was unavailable. While it may \
be perfectly reasonable to ignore this action, consider canceling the associated effect \ be perfectly reasonable to ignore this action, consider canceling the associated effect \
before child state changes to another case, especially if it is a long-living effect. before child state changes to another case, especially if it is a long-living effect.
This action was sent to the store while state was another case. Make sure that actions \ This action was sent to the store while state was another case. Make sure that actions \
for this reducer can only be sent from a store when state is set to the appropriate \ for this reducer can only be sent from a store when state is set to the appropriate \
case. In SwiftUI applications, use "SwitchStore". case. In SwiftUI applications, use "SwitchStore".
""" """
)
} }
await store.send(.success(1)) await store.send(.success(1))

View File

@@ -10,9 +10,10 @@ final class IfLetReducerTests: BaseTCATestCase {
} }
XCTExpectFailure { XCTExpectFailure {
$0.compactDescription == """ $0.compactDescription.hasSuffix(
failed - An "ifLet" at "\(#fileID):\(#line - 5)" received a child action when child state \ """
was "nil". … An "ifLet" at "\(#fileID):\(#line - 6)" received a child action when child state \
was "nil".
Action: Action:
() ()
@@ -20,18 +21,19 @@ final class IfLetReducerTests: BaseTCATestCase {
This is generally considered an application logic error, and can happen for a few \ This is generally considered an application logic error, and can happen for a few \
reasons: reasons:
A parent reducer set child state to "nil" before this reducer ran. This reducer must \ A parent reducer set child state to "nil" before this reducer ran. This reducer must \
run before any other reducer sets child state to "nil". This ensures that child \ run before any other reducer sets child state to "nil". This ensures that child \
reducers can handle their actions while their state is still available. reducers can handle their actions while their state is still available.
An in-flight effect emitted this action when child state was "nil". While it may be \ An in-flight effect emitted this action when child state was "nil". While it may be \
perfectly reasonable to ignore this action, consider canceling the associated effect \ perfectly reasonable to ignore this action, consider canceling the associated effect \
before child state becomes "nil", especially if it is a long-living effect. before child state becomes "nil", especially if it is a long-living effect.
This action was sent to the store while state was "nil". Make sure that actions for \ This action was sent to the store while state was "nil". Make sure that actions for \
this reducer can only be sent from a store when state is non-"nil". In SwiftUI \ this reducer can only be sent from a store when state is non-"nil". In SwiftUI \
applications, use "IfLetStore". applications, use "IfLetStore".
""" """
)
} }
await store.send(()) await store.send(())

View File

@@ -37,17 +37,21 @@ final class PresentationReducerTests: BaseTCATestCase {
XCTExpectFailure { XCTExpectFailure {
parent.$child[case: /Child.text]?.append("!") parent.$child[case: /Child.text]?.append("!")
} issueMatcher: { } issueMatcher: {
$0.compactDescription == """ $0.compactDescription.hasSuffix(
failed - Can't modify unrelated case "int"
""" """
Can't modify unrelated case "int"
"""
)
} }
XCTExpectFailure { XCTExpectFailure {
parent.$child[case: /Child.text] = nil parent.$child[case: /Child.text] = nil
} issueMatcher: { } issueMatcher: {
$0.compactDescription == """ $0.compactDescription.hasSuffix(
failed - Can't modify unrelated case "int"
""" """
Can't modify unrelated case "int"
"""
)
} }
XCTAssertEqual(parent.child, .int(42)) XCTAssertEqual(parent.child, .int(42))
@@ -937,7 +941,7 @@ final class PresentationReducerTests: BaseTCATestCase {
} }
) )
return .none return .none
case let .presentChild(id): case .presentChild(let id):
state.destination = .child(Child.State(id: id ?? self.uuid())) state.destination = .child(Child.State(id: id ?? self.uuid()))
return .none return .none
} }
@@ -1219,7 +1223,7 @@ final class PresentationReducerTests: BaseTCATestCase {
var body: some Reducer<State, Action> { var body: some Reducer<State, Action> {
Reduce { state, action in Reduce { state, action in
switch action { switch action {
case let .response(value): case .response(let value):
state.count = value state.count = value
return .none return .none
case .startButtonTapped: case .startButtonTapped:
@@ -1315,7 +1319,7 @@ final class PresentationReducerTests: BaseTCATestCase {
var body: some Reducer<State, Action> { var body: some Reducer<State, Action> {
Reduce { state, action in Reduce { state, action in
switch action { switch action {
case let .response(value): case .response(let value):
state.count = value state.count = value
return .none return .none
case .startButtonTapped: case .startButtonTapped:
@@ -1418,7 +1422,7 @@ final class PresentationReducerTests: BaseTCATestCase {
var body: some Reducer<State, Action> { var body: some Reducer<State, Action> {
Reduce { state, action in Reduce { state, action in
switch action { switch action {
case let .response(value): case .response(let value):
state.count = value state.count = value
return .none return .none
case .startButtonTapped: case .startButtonTapped:
@@ -1545,7 +1549,7 @@ final class PresentationReducerTests: BaseTCATestCase {
case .presentChild: case .presentChild:
state.child = Child.State() state.child = Child.State()
return .none return .none
case let .response(value): case .response(let value):
state.count = value state.count = value
return .none return .none
case .startButtonTapped: case .startButtonTapped:
@@ -1722,24 +1726,26 @@ final class PresentationReducerTests: BaseTCATestCase {
} }
XCTExpectFailure { XCTExpectFailure {
$0.compactDescription == """ $0.compactDescription.hasSuffix(
failed - An "ifLet" at \ """
"ComposableArchitectureTests/PresentationReducerTests.swift:\(#line - 13)" received a \ An "ifLet" at \
presentation action when destination state was absent. … "ComposableArchitectureTests/PresentationReducerTests.swift:\(#line - 14)" received a \
presentation action when destination state was absent.
Action: Action:
PresentationReducerTests.Parent.Action.child(.dismiss) PresentationReducerTests.Parent.Action.child(.dismiss)
This is generally considered an application logic error, and can happen for a few reasons: This is generally considered an application logic error, and can happen for a few reasons:
A parent reducer set destination state to "nil" before this reducer ran. This reducer \ A parent reducer set destination state to "nil" before this reducer ran. This reducer \
must run before any other reducer sets destination state to "nil". This ensures that \ must run before any other reducer sets destination state to "nil". This ensures that \
destination reducers can handle their actions while their state is still present. destination reducers can handle their actions while their state is still present.
This action was sent to the store while destination state was "nil". Make sure that \ This action was sent to the store while destination state was "nil". Make sure that \
actions for this reducer can only be sent from a store when state is present, or \ actions for this reducer can only be sent from a store when state is present, or \
from effects that start from this reducer. from effects that start from this reducer.
""" """
)
} }
await store.send(.child(.dismiss)) await store.send(.child(.dismiss))
@@ -1778,24 +1784,26 @@ final class PresentationReducerTests: BaseTCATestCase {
} }
XCTExpectFailure { XCTExpectFailure {
$0.compactDescription == """ $0.compactDescription.hasSuffix(
failed - An "ifLet" at \ """
"ComposableArchitectureTests/PresentationReducerTests.swift:\(#line - 13)" received a \ An "ifLet" at \
presentation action when destination state was absent. … "ComposableArchitectureTests/PresentationReducerTests.swift:\(#line - 14)" received a \
presentation action when destination state was absent.
Action: Action:
PresentationReducerTests.Parent.Action.child(.presented(.tap)) PresentationReducerTests.Parent.Action.child(.presented(.tap))
This is generally considered an application logic error, and can happen for a few reasons: This is generally considered an application logic error, and can happen for a few reasons:
A parent reducer set destination state to "nil" before this reducer ran. This reducer \ A parent reducer set destination state to "nil" before this reducer ran. This reducer \
must run before any other reducer sets destination state to "nil". This ensures that \ must run before any other reducer sets destination state to "nil". This ensures that \
destination reducers can handle their actions while their state is still present. destination reducers can handle their actions while their state is still present.
This action was sent to the store while destination state was "nil". Make sure that \ This action was sent to the store while destination state was "nil". Make sure that \
actions for this reducer can only be sent from a store when state is present, or \ actions for this reducer can only be sent from a store when state is present, or \
from effects that start from this reducer. from effects that start from this reducer.
""" """
)
} }
await store.send(.child(.presented(.tap))) await store.send(.child(.presented(.tap)))
@@ -2104,7 +2112,8 @@ final class PresentationReducerTests: BaseTCATestCase {
ConfirmationDialogState { ConfirmationDialogState {
TextState("Hello!") TextState("Hello!")
} actions: { } actions: {
}) }
)
return .none return .none
case .destination(.presented(.dialog(.showAlert))): case .destination(.presented(.dialog(.showAlert))):
state.destination = .alert(AlertState { TextState("Hello!") }) state.destination = .alert(AlertState { TextState("Hello!") })
@@ -2150,7 +2159,8 @@ final class PresentationReducerTests: BaseTCATestCase {
ConfirmationDialogState { ConfirmationDialogState {
TextState("Hello!") TextState("Hello!")
} actions: { } actions: {
}) }
)
} }
await store.send(.destination(.dismiss)) { await store.send(.destination(.dismiss)) {
$0.destination = nil $0.destination = nil
@@ -2258,31 +2268,33 @@ final class PresentationReducerTests: BaseTCATestCase {
XCTExpectFailure { XCTExpectFailure {
$0.sourceCodeContext.location?.fileURL.absoluteString.contains("BaseTCATestCase") == true $0.sourceCodeContext.location?.fileURL.absoluteString.contains("BaseTCATestCase") == true
|| $0.sourceCodeContext.location?.lineNumber == line + 1 || $0.sourceCodeContext.location?.lineNumber == line + 1
&& $0.compactDescription == """ && $0.compactDescription.hasSuffix(
failed - An effect returned for this action is still running. It must complete before \ """
the end of the test. … An effect returned for this action is still running. It must complete before the end \
of the test.
To fix, inspect any effects the reducer returns for this action and ensure that all of \ To fix, inspect any effects the reducer returns for this action and ensure that all of \
them complete by the end of the test. There are a few reasons why an effect may not \ them complete by the end of the test. There are a few reasons why an effect may not \
have completed: have completed:
If using async/await in your effect, it may need a little bit of time to properly \ If using async/await in your effect, it may need a little bit of time to properly \
finish. To fix you can simply perform "await store.finish()" at the end of your test. finish. To fix you can simply perform "await store.finish()" at the end of your test.
If an effect uses a clock (or scheduler, via "receive(on:)", "delay", "debounce", \ If an effect uses a clock (or scheduler, via "receive(on:)", "delay", "debounce", \
etc.), make sure that you wait enough time for it to perform the effect. If you are \ etc.), make sure that you wait enough time for it to perform the effect. If you are \
using a test clock/scheduler, advance it so that the effects may complete, or consider \ using a test clock/scheduler, advance it so that the effects may complete, or consider \
using an immediate clock/scheduler to immediately perform the effect instead. using an immediate clock/scheduler to immediately perform the effect instead.
If you are returning a long-living effect (timers, notifications, subjects, etc.), \ If you are returning a long-living effect (timers, notifications, subjects, etc.), \
then make sure those effects are torn down by marking the effect ".cancellable" and \ then make sure those effects are torn down by marking the effect ".cancellable" and \
returning a corresponding cancellation effect ("Effect.cancel") from another action, \ returning a corresponding cancellation effect ("Effect.cancel") from another action, \
or, if your effect is driven by a Combine subject, send it a completion. or, if your effect is driven by a Combine subject, send it a completion.
If you do not wish to assert on these effects, perform "await \ If you do not wish to assert on these effects, perform "await \
store.skipInFlightEffects()", or consider using a non-exhaustive test store: \ store.skipInFlightEffects()", or consider using a non-exhaustive test store: \
"store.exhaustivity = .off". "store.exhaustivity = .off".
""" """
)
} }
} }
@@ -2300,7 +2312,7 @@ final class PresentationReducerTests: BaseTCATestCase {
var body: some Reducer<State, Action> { var body: some Reducer<State, Action> {
Reduce { state, action in Reduce { state, action in
switch action { switch action {
case let .response(value): case .response(let value):
state.count = value state.count = value
return .none return .none
case .tap: case .tap:
@@ -2337,7 +2349,7 @@ final class PresentationReducerTests: BaseTCATestCase {
await send(.response(42)) await send(.response(42))
} }
.cancellable(id: Child.CancelID()) .cancellable(id: Child.CancelID())
case let .response(value): case .response(let value):
state.count = value state.count = value
return .none return .none
} }
@@ -2571,10 +2583,10 @@ final class PresentationReducerTests: BaseTCATestCase {
} }
XCTExpectFailure { XCTExpectFailure {
$0.compactDescription.hasPrefix( $0.compactDescription.contains(
""" """
failed - A "Scope" at "\(#fileID):\(line)" received a child action when child state was \ A "Scope" at "\(#fileID):\(line)" received a child action when child state was \
set to a different case. set to a different case.
""" """
) )
} }

View File

@@ -28,17 +28,21 @@ final class StackReducerTests: BaseTCATestCase {
XCTExpectFailure { XCTExpectFailure {
stack[id: 0, case: /Element.text]?.append("!") stack[id: 0, case: /Element.text]?.append("!")
} issueMatcher: { } issueMatcher: {
$0.compactDescription == """ $0.compactDescription.hasSuffix(
failed - Can't modify unrelated case "int"
""" """
Can't modify unrelated case "int"
"""
)
} }
XCTExpectFailure { XCTExpectFailure {
stack[id: 0, case: /Element.text] = nil stack[id: 0, case: /Element.text] = nil
} issueMatcher: { } issueMatcher: {
$0.compactDescription == """ $0.compactDescription.hasSuffix(
failed - Can't modify unrelated case "int"
""" """
Can't modify unrelated case "int"
"""
)
} }
XCTAssertEqual(Array(stack), [.int(42)]) XCTAssertEqual(Array(stack), [.int(42)])
@@ -261,8 +265,9 @@ final class StackReducerTests: BaseTCATestCase {
} }
XCTExpectFailure { XCTExpectFailure {
$0.compactDescription == """ $0.compactDescription.hasSuffix(
failed - Received unexpected action: … """
Received unexpected action:
StackReducerTests.Parent.Action.children( StackReducerTests.Parent.Action.children(
.popFrom(id: #1) .popFrom(id: #1)
@@ -271,6 +276,7 @@ final class StackReducerTests: BaseTCATestCase {
(Expected: , Received: +) (Expected: , Received: +)
""" """
)
} }
await store.send(.children(.element(id: 0, action: .tap))) await store.send(.children(.element(id: 0, action: .tap)))
@@ -561,7 +567,7 @@ final class StackReducerTests: BaseTCATestCase {
switch action { switch action {
case .cancel: case .cancel:
return .cancel(id: CancelID.cancel) return .cancel(id: CancelID.cancel)
case let .response(value): case .response(let value):
state.count = value state.count = value
return .none return .none
case .tap: case .tap:
@@ -661,7 +667,7 @@ final class StackReducerTests: BaseTCATestCase {
var body: some Reducer<State, Action> { var body: some Reducer<State, Action> {
Reduce { state, action in Reduce { state, action in
switch action { switch action {
case let .response(value): case .response(let value):
state.count += value state.count += value
return .none return .none
case .tap: case .tap:
@@ -774,28 +780,30 @@ final class StackReducerTests: BaseTCATestCase {
let line = #line - 3 let line = #line - 3
XCTExpectFailure { XCTExpectFailure {
$0.compactDescription == """ $0.compactDescription.hasSuffix(
failed - A "forEach" at "ComposableArchitectureTests/StackReducerTests.swift:\(line)" \ """
received an action for a missing element. … A "forEach" at "ComposableArchitectureTests/StackReducerTests.swift:\(line)" received an \
action for a missing element.
Action: Action:
() ()
This is generally considered an application logic error, and can happen for a few reasons: This is generally considered an application logic error, and can happen for a few reasons:
A parent reducer removed an element with this ID before this reducer ran. This reducer \ A parent reducer removed an element with this ID before this reducer ran. This reducer \
must run before any other reducer removes an element, which ensures that element \ must run before any other reducer removes an element, which ensures that element \
reducers can handle their actions while their state is still available. reducers can handle their actions while their state is still available.
An in-flight effect emitted this action when state contained no element at this ID. \ An in-flight effect emitted this action when state contained no element at this ID. \
While it may be perfectly reasonable to ignore this action, consider canceling the \ While it may be perfectly reasonable to ignore this action, consider canceling the \
associated effect before an element is removed, especially if it is a long-living effect. associated effect before an element is removed, especially if it is a long-living effect.
This action was sent to the store while its state contained no element at this ID. To \ This action was sent to the store while its state contained no element at this ID. To \
fix this make sure that actions for this reducer can only be sent from a store when \ fix this make sure that actions for this reducer can only be sent from a store when \
its state contains an element at this id. In SwiftUI applications, use \ its state contains an element at this id. In SwiftUI applications, use \
"NavigationStack.init(path:)" with a binding to a store. "NavigationStack.init(path:)" with a binding to a store.
""" """
)
} }
var path = StackState<Int>() var path = StackState<Int>()
@@ -823,15 +831,17 @@ final class StackReducerTests: BaseTCATestCase {
let line = #line - 3 let line = #line - 3
XCTExpectFailure { XCTExpectFailure {
$0.compactDescription == """ $0.compactDescription.hasSuffix(
failed - A "forEach" at "ComposableArchitectureTests/StackReducerTests.swift:\(line)" \ """
received a "popFrom" action for a missing element. … A "forEach" at "ComposableArchitectureTests/StackReducerTests.swift:\(line)" \
received a "popFrom" action for a missing element.
ID: ID:
#999 #999
Path IDs: Path IDs:
[#0] [#0]
""" """
)
} }
let store = TestStore(initialState: Parent.State(path: StackState<Int>([1]))) { let store = TestStore(initialState: Parent.State(path: StackState<Int>([1]))) {
@@ -874,31 +884,33 @@ final class StackReducerTests: BaseTCATestCase {
XCTExpectFailure { XCTExpectFailure {
$0.sourceCodeContext.location?.fileURL.absoluteString.contains("BaseTCATestCase") == true $0.sourceCodeContext.location?.fileURL.absoluteString.contains("BaseTCATestCase") == true
|| $0.sourceCodeContext.location?.lineNumber == line + 1 || $0.sourceCodeContext.location?.lineNumber == line + 1
&& $0.compactDescription == """ && $0.compactDescription.hasSuffix(
failed - An effect returned for this action is still running. It must complete before \ """
the end of the test. … An effect returned for this action is still running. It must complete before \
the end of the test.
To fix, inspect any effects the reducer returns for this action and ensure that all \ To fix, inspect any effects the reducer returns for this action and ensure that all \
of them complete by the end of the test. There are a few reasons why an effect may \ of them complete by the end of the test. There are a few reasons why an effect may \
not have completed: not have completed:
If using async/await in your effect, it may need a little bit of time to properly \ If using async/await in your effect, it may need a little bit of time to properly \
finish. To fix you can simply perform "await store.finish()" at the end of your test. finish. To fix you can simply perform "await store.finish()" at the end of your test.
If an effect uses a clock (or scheduler, via "receive(on:)", "delay", "debounce", \ If an effect uses a clock (or scheduler, via "receive(on:)", "delay", "debounce", \
etc.), make sure that you wait enough time for it to perform the effect. If you are \ etc.), make sure that you wait enough time for it to perform the effect. If you are \
using a test clock/scheduler, advance it so that the effects may complete, or \ using a test clock/scheduler, advance it so that the effects may complete, or \
consider using an immediate clock/scheduler to immediately perform the effect instead. consider using an immediate clock/scheduler to immediately perform the effect instead.
If you are returning a long-living effect (timers, notifications, subjects, etc.), \ If you are returning a long-living effect (timers, notifications, subjects, etc.), \
then make sure those effects are torn down by marking the effect ".cancellable" and \ then make sure those effects are torn down by marking the effect ".cancellable" and \
returning a corresponding cancellation effect ("Effect.cancel") from another action, \ returning a corresponding cancellation effect ("Effect.cancel") from another action, \
or, if your effect is driven by a Combine subject, send it a completion. or, if your effect is driven by a Combine subject, send it a completion.
If you do not wish to assert on these effects, perform "await \ If you do not wish to assert on these effects, perform "await \
store.skipInFlightEffects()", or consider using a non-exhaustive test store: \ store.skipInFlightEffects()", or consider using a non-exhaustive test store: \
"store.exhaustivity = .off". "store.exhaustivity = .off".
""" """
)
} }
} }
@@ -918,7 +930,7 @@ final class StackReducerTests: BaseTCATestCase {
try await self.mainQueue.sleep(for: .seconds(count)) try await self.mainQueue.sleep(for: .seconds(count))
await send(.response(42)) await send(.response(42))
} }
case let .response(value): case .response(let value):
state.count = value state.count = value
return .none return .none
} }
@@ -1084,15 +1096,17 @@ final class StackReducerTests: BaseTCATestCase {
} }
XCTExpectFailure { XCTExpectFailure {
$0.compactDescription == """ $0.compactDescription.hasSuffix(
failed - A "forEach" at "ComposableArchitectureTests/StackReducerTests.swift:\(line)" \ """
received a "push" action for an element it already contains. … A "forEach" at "ComposableArchitectureTests/StackReducerTests.swift:\(line)" \
received a "push" action for an element it already contains.
ID: ID:
#0 #0
Path IDs: Path IDs:
[#0] [#0]
""" """
)
} }
await store.send(.child(.push(id: 0, state: Child.State()))) { await store.send(.child(.push(id: 0, state: Child.State()))) {
@@ -1128,15 +1142,17 @@ final class StackReducerTests: BaseTCATestCase {
} }
XCTExpectFailure { XCTExpectFailure {
$0.compactDescription == """ $0.compactDescription.hasSuffix(
failed - A "forEach" at "ComposableArchitectureTests/StackReducerTests.swift:\(line)" \ """
received a "push" action with an unexpected generational ID. … A "forEach" at "ComposableArchitectureTests/StackReducerTests.swift:\(line)" received a \
"push" action with an unexpected generational ID.
Received ID: Received ID:
#1 #1
Expected ID: Expected ID:
#0 #0
""" """
)
} }
await store.send(.child(.push(id: 1, state: Child.State()))) { await store.send(.child(.push(id: 1, state: Child.State()))) {
@@ -1169,8 +1185,9 @@ final class StackReducerTests: BaseTCATestCase {
} }
XCTExpectFailure { XCTExpectFailure {
$0.compactDescription == """ $0.compactDescription.hasSuffix(
failed - A state change does not match expectation: … """
A state change does not match expectation.
StackReducerTests.Parent.State( StackReducerTests.Parent.State(
children: [ children: [
@@ -1181,6 +1198,7 @@ final class StackReducerTests: BaseTCATestCase {
(Expected: , Actual: +) (Expected: , Actual: +)
""" """
)
} }
await store.send(.child(.push(id: 0, state: Child.State()))) { await store.send(.child(.push(id: 0, state: Child.State()))) {
$0.children[id: 1] = Child.State() $0.children[id: 1] = Child.State()

View File

@@ -16,15 +16,17 @@
let store = Store<State, Action>(initialState: State()) {} let store = Store<State, Action>(initialState: State()) {}
XCTExpectFailure { XCTExpectFailure {
$0.compactDescription == """ $0.compactDescription.hasSuffix(
failed - A binding action sent from a store for binding state defined at \ """
"\(#fileID):\(line)" was not handled. … A binding action sent from a store for binding state defined at "\(#fileID):\(line)" was \
not handled.
Action: Action:
RuntimeWarningTests.Action.binding(.set(_, 42)) RuntimeWarningTests.Action.binding(.set(_, 42))
To fix this, invoke "BindingReducer()" from your feature reducer's "body". To fix this, invoke "BindingReducer()" from your feature reducer's "body".
""" """
)
} }
let viewStore = ViewStore(store, observe: { $0 }) let viewStore = ViewStore(store, observe: { $0 })
@@ -46,14 +48,16 @@
let store = Store<State, Action>(initialState: State()) {} let store = Store<State, Action>(initialState: State()) {}
XCTExpectFailure { XCTExpectFailure {
$0.compactDescription == """ $0.compactDescription.hasSuffix(
failed - A binding action sent from a store was not handled. … """
A binding action sent from a store was not handled.
Action: Action:
RuntimeWarningTests.Action.binding(.set(_, 42)) RuntimeWarningTests.Action.binding(.set(_, 42))
To fix this, invoke "BindingReducer()" from your feature reducer's "body". To fix this, invoke "BindingReducer()" from your feature reducer's "body".
""" """
)
} }
store.count = 42 store.count = 42
@@ -72,15 +76,17 @@
let store = Store<State, Action>(initialState: State()) {} let store = Store<State, Action>(initialState: State()) {}
XCTExpectFailure { XCTExpectFailure {
$0.compactDescription == """ $0.compactDescription.hasSuffix(
failed - A binding action sent from a store for binding state defined at \ """
"\(#fileID):\(line)" was not handled. … A binding action sent from a store for binding state defined at "\(#fileID):\(line)" was \
not handled.
Action: Action:
RuntimeWarningTests.Action.binding(.set(_, 42)) RuntimeWarningTests.Action.binding(.set(_, 42))
To fix this, invoke "BindingReducer()" from your feature reducer's "body". To fix this, invoke "BindingReducer()" from your feature reducer's "body".
""" """
)
} }
let viewStore = ViewStore(store, observe: { $0 }) let viewStore = ViewStore(store, observe: { $0 })
@@ -112,11 +118,12 @@
column: 1 column: 1
] = .init() ] = .init()
} issueMatcher: { } issueMatcher: {
$0.compactDescription == """ $0.compactDescription.hasSuffix(
failed - A navigation stack binding at "file.swift:1" was written to with a path that \ """
has the same number of elements that already exist in the store. A view should only \ A navigation stack binding at "file.swift:1" was written to with a path that has the \
write to this binding with a path that has pushed a new element onto the stack, or \ same number of elements that already exist in the store. A view should only write to \
popped one or more elements from the stack. this binding with a path that has pushed a new element onto the stack, or popped one or \
more elements from the stack.
This usually means the "forEach" has not been integrated with the reducer powering the \ This usually means the "forEach" has not been integrated with the reducer powering the \
store, and this reducer is responsible for handling stack actions. store, and this reducer is responsible for handling stack actions.
@@ -133,6 +140,7 @@
And ensure that every parent reducer is integrated into the root reducer that powers \ And ensure that every parent reducer is integrated into the root reducer that powers \
the store. the store.
""" """
)
} }
} }
@@ -168,9 +176,9 @@
column: 1 column: 1
] = nil ] = nil
} issueMatcher: { } issueMatcher: {
$0.compactDescription == """ $0.compactDescription.hasSuffix(
failed - A binding at "file.swift:1" was set to "nil", but the store destination wasn't \ """
nil'd out. A binding at "file.swift:1" was set to "nil", but the store destination wasn't nil'd out.
This usually means an "ifLet" has not been integrated with the reducer powering the \ This usually means an "ifLet" has not been integrated with the reducer powering the \
store, and this reducer is responsible for handling presentation actions. store, and this reducer is responsible for handling presentation actions.
@@ -187,6 +195,7 @@
And ensure that every parent reducer is integrated into the root reducer that powers the \ And ensure that every parent reducer is integrated into the root reducer that powers the \
store. store.
""" """
)
} }
} }
} }

View File

@@ -15,21 +15,21 @@ final class ScopeCacheTests: BaseTCATestCase {
.scope(state: \.child, action: \.child.presented)? .scope(state: \.child, action: \.child.presented)?
.send(.show) .send(.show)
} issueMatcher: { } issueMatcher: {
$0.compactDescription == """ $0.compactDescription.hasSuffix(
failed - Scoping from uncached StoreOf<Feature> is not compatible with observation. """
Scoping from uncached 'StoreOf<Feature>' is not compatible with observation.
This can happen for one of two reasons: This can happen for one of two reasons:
• A parent view scopes on a store using transform functions, which has been deprecated, \ 1. A parent view scopes on a store using transform functions, which has been deprecated, \
instead of with key paths and case paths. Read the migration guide for 1.5 to update these \ instead of with key paths and case paths. Read the migration guide for 1.5 to update these \
scopes: \ scopes: https://swiftpackageindex.com/pointfreeco/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5
https://swiftpackageindex.com/pointfreeco/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5
 A parent feature is using deprecated navigation APIs, such as 'IfLetStore', \ 2. A parent feature is using deprecated navigation APIs, such as 'IfLetStore', \
'SwitchStore', 'ForEachStore', or any navigation view modifiers taking stores instead of \ 'SwitchStore', 'ForEachStore', or any navigation view modifiers taking stores instead of \
bindings. Read the migration guide for 1.7 to update those APIs: \ bindings. Read the migration guide for 1.7 to update those APIs: https://swiftpackageindex.com/pointfreeco/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7
https://swiftpackageindex.com/pointfreeco/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7
""" """
)
} }
store.send(.child(.dismiss)) store.send(.child(.dismiss))
} }
@@ -76,21 +76,21 @@ final class ScopeCacheTests: BaseTCATestCase {
} }
_ = cancellable _ = cancellable
} issueMatcher: { } issueMatcher: {
$0.compactDescription == """ $0.compactDescription.hasSuffix(
failed - Scoping from uncached StoreOf<Feature> is not compatible with observation. """
Scoping from uncached 'StoreOf<Feature>' is not compatible with observation.
This can happen for one of two reasons: This can happen for one of two reasons:
• A parent view scopes on a store using transform functions, which has been deprecated, \ 1. A parent view scopes on a store using transform functions, which has been deprecated, \
instead of with key paths and case paths. Read the migration guide for 1.5 to update these \ instead of with key paths and case paths. Read the migration guide for 1.5 to update these \
scopes: \ scopes: https://swiftpackageindex.com/pointfreeco/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5
https://swiftpackageindex.com/pointfreeco/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5
 A parent feature is using deprecated navigation APIs, such as 'IfLetStore', \ 2. A parent feature is using deprecated navigation APIs, such as 'IfLetStore', \
'SwitchStore', 'ForEachStore', or any navigation view modifiers taking stores instead of \ 'SwitchStore', 'ForEachStore', or any navigation view modifiers taking stores instead of \
bindings. Read the migration guide for 1.7 to update those APIs: \ bindings. Read the migration guide for 1.7 to update those APIs: https://swiftpackageindex.com/pointfreeco/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7
https://swiftpackageindex.com/pointfreeco/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7
""" """
)
} }
} }
@@ -122,21 +122,21 @@ final class ScopeCacheTests: BaseTCATestCase {
.scope(state: \.rows, action: \.rows) .scope(state: \.rows, action: \.rows)
) )
} issueMatcher: { } issueMatcher: {
$0.compactDescription == """ $0.compactDescription.hasSuffix(
failed - Scoping from uncached StoreOf<Feature> is not compatible with observation. """
Scoping from uncached 'StoreOf<Feature>' is not compatible with observation.
This can happen for one of two reasons: This can happen for one of two reasons:
• A parent view scopes on a store using transform functions, which has been deprecated, \ 1. A parent view scopes on a store using transform functions, which has been deprecated, \
instead of with key paths and case paths. Read the migration guide for 1.5 to update these \ instead of with key paths and case paths. Read the migration guide for 1.5 to update these \
scopes: \ scopes: https://swiftpackageindex.com/pointfreeco/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5
https://swiftpackageindex.com/pointfreeco/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5
 A parent feature is using deprecated navigation APIs, such as 'IfLetStore', \ 2. A parent feature is using deprecated navigation APIs, such as 'IfLetStore', \
'SwitchStore', 'ForEachStore', or any navigation view modifiers taking stores instead of \ 'SwitchStore', 'ForEachStore', or any navigation view modifiers taking stores instead of \
bindings. Read the migration guide for 1.7 to update those APIs: \ bindings. Read the migration guide for 1.7 to update those APIs: https://swiftpackageindex.com/pointfreeco/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7
https://swiftpackageindex.com/pointfreeco/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7
""" """
)
} }
} }
} }

View File

@@ -45,9 +45,10 @@ final class ScopeTests: BaseTCATestCase {
} }
XCTExpectFailure { XCTExpectFailure {
$0.compactDescription == """ $0.compactDescription.hasSuffix(
failed - A "Scope" at "\(#fileID):\(#line - 5)" received a child action when child state \ """
was set to a different case. … A "Scope" at "\(#fileID):\(#line - 6)" received a child action when child state \
was set to a different case.
Action: Action:
Child2.Action.name Child2.Action.name
@@ -56,20 +57,21 @@ final class ScopeTests: BaseTCATestCase {
This is generally considered an application logic error, and can happen for a few reasons: This is generally considered an application logic error, and can happen for a few reasons:
A parent reducer set "Child2.State" to a different case before the scoped reducer ran. \ A parent reducer set "Child2.State" to a different case before the scoped reducer ran. \
Child reducers must run before any parent reducer sets child state to a different case. \ Child reducers must run before any parent reducer sets child state to a different case. \
This ensures that child reducers can handle their actions while their state is still \ This ensures that child reducers can handle their actions while their state is still \
available. Consider using "Reducer.ifCaseLet" to embed this child reducer in the \ available. Consider using "Reducer.ifCaseLet" to embed this child reducer in the \
parent reducer that change its state to ensure the child reducer runs first. parent reducer that change its state to ensure the child reducer runs first.
An in-flight effect emitted this action when child state was unavailable. While it may \ An in-flight effect emitted this action when child state was unavailable. While it may \
be perfectly reasonable to ignore this action, consider canceling the associated effect \ be perfectly reasonable to ignore this action, consider canceling the associated effect \
before child state changes to another case, especially if it is a long-living effect. before child state changes to another case, especially if it is a long-living effect.
This action was sent to the store while state was another case. Make sure that actions \ This action was sent to the store while state was another case. Make sure that actions \
for this reducer can only be sent from a store when state is set to the appropriate \ for this reducer can only be sent from a store when state is set to the appropriate \
case. In SwiftUI applications, use "SwitchStore". case. In SwiftUI applications, use "SwitchStore".
""" """
)
} }
await store.send(.name("Blob")) await store.send(.name("Blob"))

View File

@@ -14,8 +14,9 @@ final class TaskResultTests: BaseTCATestCase {
TaskResult<Never>.failure(Failure(message: "Something went wrong")) TaskResult<Never>.failure(Failure(message: "Something went wrong"))
) )
} issueMatcher: { } issueMatcher: {
$0.compactDescription == """ $0.compactDescription.hasSuffix(
failed - "TaskResultTests.Failure" is not equatable. … """
"TaskResultTests.Failure" is not equatable.
To test two values of this type, it must conform to the "Equatable" protocol. For example: To test two values of this type, it must conform to the "Equatable" protocol. For example:
@@ -23,6 +24,7 @@ final class TaskResultTests: BaseTCATestCase {
See the documentation of "TaskResult" for more information. See the documentation of "TaskResult" for more information.
""" """
)
} }
} }
@@ -40,8 +42,9 @@ final class TaskResultTests: BaseTCATestCase {
TaskResult<Never>.failure(Failure2(message: "Something went wrong")) TaskResult<Never>.failure(Failure2(message: "Something went wrong"))
) )
} issueMatcher: { } issueMatcher: {
$0.compactDescription == """ $0.compactDescription.hasSuffix(
failed - Difference: … """
Difference: …
TaskResult.failure( TaskResult.failure(
TaskResultTests.Failure1(message: "Something went wrong") TaskResultTests.Failure1(message: "Something went wrong")
@@ -50,6 +53,7 @@ final class TaskResultTests: BaseTCATestCase {
(First: , Second: +) (First: , Second: +)
""" """
)
} }
} }
@@ -61,8 +65,9 @@ final class TaskResultTests: BaseTCATestCase {
XCTExpectFailure { XCTExpectFailure {
_ = TaskResult<Never>.failure(Failure(message: "Something went wrong")).hashValue _ = TaskResult<Never>.failure(Failure(message: "Something went wrong")).hashValue
} issueMatcher: { } issueMatcher: {
$0.compactDescription == """ $0.compactDescription.hasSuffix(
failed - "TaskResultTests.Failure" is not hashable. … """
"TaskResultTests.Failure" is not hashable.
To hash a value of this type, it must conform to the "Hashable" protocol. For example: To hash a value of this type, it must conform to the "Hashable" protocol. For example:
@@ -70,6 +75,7 @@ final class TaskResultTests: BaseTCATestCase {
See the documentation of "TaskResult" for more information. See the documentation of "TaskResult" for more information.
""" """
)
} }
} }
#endif #endif

View File

@@ -15,22 +15,26 @@ final class TestStoreFailureTests: BaseTCATestCase {
} }
XCTExpectFailure { XCTExpectFailure {
$0.compactDescription == """ $0.compactDescription.hasSuffix(
failed - Expected state to change, but no change occurred. """
Expected state to change, but no change occurred.
The trailing closure made no observable modifications to state. If no change to state is \ The trailing closure made no observable modifications to state. If no change to state is \
expected, omit the trailing closure. expected, omit the trailing closure.
""" """
)
} }
await store.send(.first) { _ = $0 } await store.send(.first) { _ = $0 }
XCTExpectFailure { XCTExpectFailure {
$0.compactDescription == """ $0.compactDescription.hasSuffix(
failed - Expected state to change, but no change occurred. """
Expected state to change, but no change occurred.
The trailing closure made no observable modifications to state. If no change to state is \ The trailing closure made no observable modifications to state. If no change to state is \
expected, omit the trailing closure. expected, omit the trailing closure.
""" """
)
} }
await store.receive(.second) { _ = $0 } await store.receive(.second) { _ = $0 }
} }
@@ -46,14 +50,16 @@ final class TestStoreFailureTests: BaseTCATestCase {
} }
XCTExpectFailure { XCTExpectFailure {
$0.compactDescription == """ $0.compactDescription.hasSuffix(
failed - A state change does not match expectation: … """
A state change does not match expectation.
TestStoreFailureTests.State(count: 0) TestStoreFailureTests.State(count: 0)
+ TestStoreFailureTests.State(count: 1) + TestStoreFailureTests.State(count: 1)
(Expected: , Actual: +) (Expected: , Actual: +)
""" """
)
} }
await store.send(()) { $0.count = 0 } await store.send(()) { $0.count = 0 }
} }
@@ -69,14 +75,16 @@ final class TestStoreFailureTests: BaseTCATestCase {
} }
XCTExpectFailure { XCTExpectFailure {
$0.compactDescription == """ $0.compactDescription.hasSuffix(
failed - State was not expected to change, but a change occurred: … """
State was not expected to change, but a change occurred.
TestStoreFailureTests.State(count: 0) TestStoreFailureTests.State(count: 0)
+ TestStoreFailureTests.State(count: 1) + TestStoreFailureTests.State(count: 1)
(Expected: , Actual: +) (Expected: , Actual: +)
""" """
)
} }
await store.send(()) await store.send(())
} }
@@ -98,14 +106,16 @@ final class TestStoreFailureTests: BaseTCATestCase {
await store.send(.first) await store.send(.first)
XCTExpectFailure { XCTExpectFailure {
$0.compactDescription == """ $0.compactDescription.hasSuffix(
failed - State was not expected to change, but a change occurred: … """
State was not expected to change, but a change occurred.
TestStoreFailureTests.State(count: 0) TestStoreFailureTests.State(count: 0)
+ TestStoreFailureTests.State(count: 1) + TestStoreFailureTests.State(count: 1)
(Expected: , Actual: +) (Expected: , Actual: +)
""" """
)
} }
await store.receive(.second) await store.receive(.second)
} }
@@ -123,16 +133,18 @@ final class TestStoreFailureTests: BaseTCATestCase {
} }
XCTExpectFailure { XCTExpectFailure {
$0.compactDescription == """ $0.compactDescription.hasSuffix(
failed - The store received 1 unexpected action: … """
The store received 1 unexpected action.
Unhandled actions: Unhandled actions:
.second .second
To fix, explicitly assert against these actions using "store.receive", skip these actions \ To fix, explicitly assert against these actions using "store.receive", skip these actions \
by performing "await store.skipReceivedActions()", or consider using a non-exhaustive test \ by performing "await store.skipReceivedActions()", or consider using a non-exhaustive test \
store: "store.exhaustivity = .off". store: "store.exhaustivity = .off".
""" """
)
} }
await store.send(.first) await store.send(.first)
} }
@@ -152,16 +164,18 @@ final class TestStoreFailureTests: BaseTCATestCase {
await store.send(.first) await store.send(.first)
XCTExpectFailure { XCTExpectFailure {
$0.compactDescription == """ $0.compactDescription.hasSuffix(
failed - The store received 1 unexpected action: … """
The store received 1 unexpected action.
Unhandled actions: Unhandled actions:
.second .second
To fix, explicitly assert against these actions using "store.receive", skip these actions \ To fix, explicitly assert against these actions using "store.receive", skip these actions \
by performing "await store.skipReceivedActions()", or consider using a non-exhaustive test \ by performing "await store.skipReceivedActions()", or consider using a non-exhaustive test \
store: "store.exhaustivity = .off". store: "store.exhaustivity = .off".
""" """
)
} }
await store.finish() await store.finish()
} }
@@ -176,31 +190,33 @@ final class TestStoreFailureTests: BaseTCATestCase {
} }
XCTExpectFailure { XCTExpectFailure {
$0.compactDescription == """ $0.compactDescription.hasSuffix(
failed - An effect returned for this action is still running. It must complete before the \ """
end of the test. … An effect returned for this action is still running. It must complete before the end of \
the test.
To fix, inspect any effects the reducer returns for this action and ensure that all of \ To fix, inspect any effects the reducer returns for this action and ensure that all of \
them complete by the end of the test. There are a few reasons why an effect may not have \ them complete by the end of the test. There are a few reasons why an effect may not have \
completed: completed:
If using async/await in your effect, it may need a little bit of time to properly \ If using async/await in your effect, it may need a little bit of time to properly \
finish. To fix you can simply perform "await store.finish()" at the end of your test. finish. To fix you can simply perform "await store.finish()" at the end of your test.
If an effect uses a clock (or scheduler, via "receive(on:)", "delay", "debounce", etc.), \ If an effect uses a clock (or scheduler, via "receive(on:)", "delay", "debounce", etc.), \
make sure that you wait enough time for it to perform the effect. If you are using a test \ make sure that you wait enough time for it to perform the effect. If you are using a test \
clock/scheduler, advance it so that the effects may complete, or consider using an \ clock/scheduler, advance it so that the effects may complete, or consider using an \
immediate clock/scheduler to immediately perform the effect instead. immediate clock/scheduler to immediately perform the effect instead.
If you are returning a long-living effect (timers, notifications, subjects, etc.), then \ If you are returning a long-living effect (timers, notifications, subjects, etc.), then \
make sure those effects are torn down by marking the effect ".cancellable" and returning a \ make sure those effects are torn down by marking the effect ".cancellable" and returning a \
corresponding cancellation effect ("Effect.cancel") from another action, or, if your \ corresponding cancellation effect ("Effect.cancel") from another action, or, if your \
effect is driven by a Combine subject, send it a completion. effect is driven by a Combine subject, send it a completion.
If you do not wish to assert on these effects, perform "await \ If you do not wish to assert on these effects, perform "await \
store.skipInFlightEffects()", or consider using a non-exhaustive test store: \ store.skipInFlightEffects()", or consider using a non-exhaustive test store: \
"store.exhaustivity = .off". "store.exhaustivity = .off".
""" """
)
} }
await store.send(()) await store.send(())
} }
@@ -220,13 +236,15 @@ final class TestStoreFailureTests: BaseTCATestCase {
await store.send(.first) await store.send(.first)
XCTExpectFailure { XCTExpectFailure {
$0.compactDescription == """ $0.compactDescription.hasSuffix(
failed - Must handle 1 received action before sending an action: … """
Must handle 1 received action before sending an action.
Unhandled actions: [ Unhandled actions: [
[0]: .second [0]: .second
] ]
""" """
)
} }
await store.send(.first) await store.send(.first)
@@ -242,11 +260,13 @@ final class TestStoreFailureTests: BaseTCATestCase {
} }
XCTExpectFailure { XCTExpectFailure {
$0.compactDescription == """ $0.compactDescription.hasSuffix(
failed - Expected to receive the following action, but didn't: … """
Expected to receive the following action, but didn't:
TestStoreFailureTests.Action.action TestStoreFailureTests.Action.action
""" """
)
} }
await store.receive(.action) await store.receive(.action)
} }
@@ -269,14 +289,16 @@ final class TestStoreFailureTests: BaseTCATestCase {
await store.send(.first) await store.send(.first)
XCTExpectFailure { XCTExpectFailure {
$0.compactDescription == """ $0.compactDescription.hasSuffix(
failed - Received unexpected action: … """
Received unexpected action:
TestStoreFailureTests.Action.first TestStoreFailureTests.Action.first
+ TestStoreFailureTests.Action.second + TestStoreFailureTests.Action.second
(Expected: , Received: +) (Expected: , Received: +)
""" """
)
} }
await store.receive(.first) await store.receive(.first)
} }
@@ -288,7 +310,7 @@ final class TestStoreFailureTests: BaseTCATestCase {
} }
XCTExpectFailure { XCTExpectFailure {
$0.compactDescription == "failed - Threw error: SomeError()" $0.compactDescription.hasSuffix("Threw error: SomeError()")
} }
await store.send(()) { _ in await store.send(()) { _ in
struct SomeError: Error {} struct SomeError: Error {}

View File

@@ -41,7 +41,7 @@ final class TestStoreNonExhaustiveTests: BaseTCATestCase {
await store.receive(false) { $0 = 2 } await store.receive(false) { $0 = 2 }
XCTAssertEqual(store.state, 2) XCTAssertEqual(store.state, 2)
XCTExpectFailure { XCTExpectFailure {
$0.compactDescription == "failed - There were no received actions to skip." $0.compactDescription.hasSuffix("There were no received actions to skip.")
} }
await store.skipReceivedActions(strict: true) await store.skipReceivedActions(strict: true)
} }
@@ -111,7 +111,7 @@ final class TestStoreNonExhaustiveTests: BaseTCATestCase {
let task = await store.send(true) let task = await store.send(true)
await task.finish(timeout: NSEC_PER_SEC / 2) await task.finish(timeout: NSEC_PER_SEC / 2)
XCTExpectFailure { XCTExpectFailure {
$0.compactDescription == "failed - There were no in-flight effects to skip." $0.compactDescription.hasSuffix("There were no in-flight effects to skip.")
} }
await store.skipInFlightEffects(strict: true) await store.skipInFlightEffects(strict: true)
} }
@@ -239,8 +239,9 @@ final class TestStoreNonExhaustiveTests: BaseTCATestCase {
store.exhaustivity = .off(showSkippedAssertions: true) store.exhaustivity = .off(showSkippedAssertions: true)
XCTExpectFailure { XCTExpectFailure {
$0.compactDescription == """ $0.compactDescription.hasSuffix(
failed - A state change does not match expectation: … """
A state change does not match expectation.
Counter.State( Counter.State(
count: 0, count: 0,
@@ -250,6 +251,7 @@ final class TestStoreNonExhaustiveTests: BaseTCATestCase {
(Expected: , Actual: +) (Expected: , Actual: +)
""" """
)
} }
await store.send(.increment) { await store.send(.increment) {
$0.count = 0 $0.count = 0
@@ -341,8 +343,9 @@ final class TestStoreNonExhaustiveTests: BaseTCATestCase {
$0.count = 1 $0.count = 1
} }
XCTExpectFailure { XCTExpectFailure {
$0.compactDescription == """ $0.compactDescription.hasSuffix(
failed - A state change does not match expectation: … """
A state change does not match expectation.
TestStoreNonExhaustiveTests.State( TestStoreNonExhaustiveTests.State(
count: 2, count: 2,
@@ -352,6 +355,7 @@ final class TestStoreNonExhaustiveTests: BaseTCATestCase {
(Expected: , Actual: +) (Expected: , Actual: +)
""" """
)
} }
await store.receive(.loggedInResponse(true)) { await store.receive(.loggedInResponse(true)) {
$0.count = 2 $0.count = 2
@@ -629,9 +633,11 @@ final class TestStoreNonExhaustiveTests: BaseTCATestCase {
await store.send(.onAppear) await store.send(.onAppear)
XCTExpectFailure { XCTExpectFailure {
$0.compactDescription == """ $0.compactDescription.hasSuffix(
failed - Expected to receive an action matching case path, but didn't get one.
""" """
Expected to receive an action matching case path, but didn't get one.
"""
)
} }
await store.receive(\.onAppear) await store.receive(\.onAppear)
@@ -650,9 +656,11 @@ final class TestStoreNonExhaustiveTests: BaseTCATestCase {
await store.receive(\.response2) await store.receive(\.response2)
XCTExpectFailure { XCTExpectFailure {
$0.compactDescription == """ $0.compactDescription.hasSuffix(
failed - Expected to receive an action matching case path, but didn't get one.
""" """
Expected to receive an action matching case path, but didn't get one.
"""
)
} }
await store.receive(\.response2) await store.receive(\.response2)
@@ -684,9 +692,11 @@ final class TestStoreNonExhaustiveTests: BaseTCATestCase {
XCTExpectFailure { XCTExpectFailure {
XCTModify(&state.child) { _ in } XCTModify(&state.child) { _ in }
} issueMatcher: { } issueMatcher: {
$0.compactDescription == """ $0.compactDescription.hasSuffix(
failed - XCTModify: Expected "Int" value to be modified but it was unchanged.
""" """
Expected "Int" value to be modified but it was unchanged.
"""
)
} }
} }
await store.receive(.response) { state in await store.receive(.response) { state in
@@ -694,9 +704,11 @@ final class TestStoreNonExhaustiveTests: BaseTCATestCase {
XCTExpectFailure { XCTExpectFailure {
XCTModify(&state.child) { _ in } XCTModify(&state.child) { _ in }
} issueMatcher: { } issueMatcher: {
$0.compactDescription == """ $0.compactDescription.hasSuffix(
failed - XCTModify: Expected "Int" value to be modified but it was unchanged.
""" """
Expected "Int" value to be modified but it was unchanged.
"""
)
} }
} }
} }

View File

@@ -459,14 +459,16 @@ final class TestStoreTests: BaseTCATestCase {
$0 = 1 $0 = 1
} }
} issueMatcher: { } issueMatcher: {
$0.compactDescription == """ $0.compactDescription.hasSuffix(
failed - A state change does not match expectation: … """
A state change does not match expectation.
1 1
+ 0 + 0
(Expected: , Actual: +) (Expected: , Actual: +)
""" """
)
} }
} }
@@ -551,8 +553,9 @@ final class TestStoreTests: BaseTCATestCase {
await store.receive(\.delegate.success, 42) await store.receive(\.delegate.success, 42)
XCTExpectFailure { XCTExpectFailure {
$0.compactDescription == """ $0.compactDescription.hasSuffix(
failed - Received unexpected action: … """
Received unexpected action:
Action.delegate( Action.delegate(
.success(43) .success(43)
@@ -561,6 +564,7 @@ final class TestStoreTests: BaseTCATestCase {
(Expected: , Actual: +) (Expected: , Actual: +)
""" """
)
} }
await store.send(.tap) await store.send(.tap)
await store.receive(\.delegate.success, 43) await store.receive(\.delegate.success, 43)
@@ -652,9 +656,7 @@ final class TestStoreTests: BaseTCATestCase {
} }
await store.send(.dismiss) await store.send(.dismiss)
XCTExpectFailure { XCTExpectFailure {
$0.compactDescription == """ $0.compactDescription.hasSuffix("Can't send action to dismissed test store.")
failed - Can't send action to dismissed test store.
"""
} }
await store.send(.onTask) await store.send(.onTask)
} }