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 {
reportIssue(
"""
An action was sent from a completed effect:
An action was sent from a completed effect.
Action:
\(debugCaseOutput(effectAction))

View File

@@ -123,7 +123,7 @@ public struct DismissEffect: Sendable {
else {
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 \
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:
//
// Unhandled actions:
// .timerTick
// .timerTick
}
}

View File

@@ -113,7 +113,7 @@ extension Effect {
guard let handler else {
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))

View File

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

View File

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

View File

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

View File

@@ -500,16 +500,16 @@ extension Store where State: ObservableState {
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:
• 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 \
to update these scopes: \
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 \
bindings. Read the migration guide for 1.7 to update those APIs: \
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 {
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:
\(debugCaseOutput(action))
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 \
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 \
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 \
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(
"""
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:
\(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:
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 \
different case. This 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 \
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 \
case. In SwiftUI applications, use "SwitchStore".
""",

View File

@@ -287,22 +287,22 @@ public struct _IfLetReducer<Parent: Reducer, Child: Reducer>: Reducer {
reportIssue(
"""
An "ifLet" at "\(self.fileID):\(self.line)" received a child action when child state was \
"nil".
"nil".
Action:
\(String(customDumping: action).indent(by: 4))
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 \
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 \
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 \
applications, use "IfLetStore".
""",

View File

@@ -632,7 +632,7 @@ public struct _PresentationReducer<Base: Reducer, Destination: Reducer>: Reducer
reportIssue(
"""
An "ifLet" at "\(self.fileID):\(self.line)" received a presentation action when \
destination state was absent.
destination state was absent.
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 \
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 \
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 \
from effects that start from this reducer.
""",

View File

@@ -347,7 +347,7 @@ public struct Scope<ParentState, ParentAction, Child: Reducer>: Reducer {
reportIssue(
"""
A "Scope" at "\(fileID):\(line)" received a child action when child state was set to a \
different case.
different case.
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 \
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 \
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 \
child reducer in the 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 \
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 \
case. In SwiftUI applications, use "SwitchStore".
""",

View File

@@ -526,22 +526,22 @@ public struct _StackReducer<Base: Reducer, Destination: Reducer>: Reducer {
} else {
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:
\(debugCaseOutput(destinationAction))
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 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 \
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 \
its state contains an element at this id. In SwiftUI applications, use \
"NavigationStack.init(path:)" with a binding to a store.
@@ -566,7 +566,7 @@ public struct _StackReducer<Base: Reducer, Destination: Reducer>: Reducer {
reportIssue(
"""
A "forEach" at "\(self.fileID):\(self.line)" received a "popFrom" action for a missing \
element.
element.
ID:
\(id)
@@ -586,7 +586,7 @@ public struct _StackReducer<Base: Reducer, Destination: Reducer>: Reducer {
reportIssue(
"""
A "forEach" at "\(self.fileID):\(self.line)" received a "push" action for an element it \
already contains.
already contains.
ID:
\(id)
@@ -606,7 +606,7 @@ public struct _StackReducer<Base: Reducer, Destination: Reducer>: Reducer {
reportIssue(
"""
A "forEach" at "\(self.fileID):\(self.line)" received a "push" action with an \
unexpected generational ID.
unexpected generational ID.
Received ID:
\(id)

View File

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

View File

@@ -663,26 +663,26 @@ public final class TestStore<State: Equatable, Action> {
reportIssueHelper(
"""
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 \
them complete by the end of the test. There are a few reasons why an effect may 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.
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 \
clock/scheduler, advance it so that the effects may complete, or 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 \
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 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.exhaustivity = .off".
""",
@@ -709,12 +709,12 @@ public final class TestStore<State: Equatable, Action> {
if !self.reducer.receivedActions.isEmpty {
let actions = self.reducer.receivedActions
.map(\.action)
.map { " " + debugCaseOutput($0, abbreviated: true) }
.map { " " + debugCaseOutput($0, abbreviated: true) }
.joined(separator: "\n")
reportIssueHelper(
"""
The store received \(self.reducer.receivedActions.count) unexpected \
action\(self.reducer.receivedActions.count == 1 ? "" : "s"): …
action\(self.reducer.receivedActions.count == 1 ? "" : "s").
Unhandled actions:
\(actions)
@@ -965,7 +965,7 @@ extension TestStore {
reportIssueHelper(
"""
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)
""",
@@ -1197,7 +1197,7 @@ extension TestStore {
} catch {
reportIssue(
"""
Skipped assertions: …
Skipped assertions.
Threw error: \(error)
""",
@@ -1241,7 +1241,7 @@ extension TestStore {
: "State was not expected to change, but a change occurred"
reportIssueHelper(
"""
\(messageHeading): …
\(messageHeading).
\(difference)\(postamble.isEmpty ? "" : "\n\n\(postamble)")
""",
@@ -1292,7 +1292,7 @@ extension TestStore where Action: Equatable {
self.receiveAction(
matching: { expectedAction == $0 },
failureMessage: """
Expected to receive the following action, but didn't:
Expected to receive the following action, but didn't:
\(expectedActionDump)
""",
@@ -2133,7 +2133,7 @@ extension TestStore {
reportIssueHelper(
"""
\(actions.count) received action\
\(actions.count == 1 ? " was" : "s were") skipped:
\(actions.count == 1 ? " was" : "s were") skipped.
\(actionsDump)
""",
@@ -2151,7 +2151,7 @@ extension TestStore {
.contains(where: { action, _ in predicate(receivedAction) })
reportIssueHelper(
"""
Received unexpected action\(receivedActionLater ? " before this one" : ""):
Received unexpected action\(receivedActionLater ? " before this one" : ""):
\(unexpectedActionDescription(receivedAction))
""",
@@ -2427,7 +2427,7 @@ extension TestStore {
reportIssueHelper(
"""
\(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)
""",
@@ -2550,7 +2550,7 @@ extension TestStore {
withExpectedIssue {
reportIssue(
"""
Skipped assertions: …
Skipped assertions.
\(message)
""",

View File

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

View File

@@ -47,14 +47,16 @@ final class EffectRunTests: BaseTCATestCase {
func testRunUnhandledFailure() async {
var line: UInt!
XCTExpectFailure(nil, enabled: nil, strict: nil) {
$0.compactDescription == """
failed - An "Effect.run" returned from "\(#fileID):\(line+1)" threw an unhandled error. …
$0.compactDescription.hasSuffix(
"""
An "Effect.run" returned from "\(#fileID):\(line+1)" threw an unhandled error.
EffectRunTests.Failure()
All non-cancellation errors must be explicitly handled via the "catch" parameter on \
"Effect.run", or via a "do" block.
"""
)
}
struct State: Equatable {}
enum Action: Equatable { case tapped, response }
@@ -126,8 +128,9 @@ final class EffectRunTests: BaseTCATestCase {
@MainActor
func testRunEscapeFailure() async throws {
XCTExpectFailure {
$0.compactDescription == """
failed - An action was sent from a completed effect:
$0.compactDescription.hasSuffix(
"""
An action was sent from a completed effect.
Action:
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 \
calling 'send'.
"""
)
}
enum Action { case tap, response }

View File

@@ -78,11 +78,7 @@ final class ObservableTests: BaseTCATestCase {
}
func testReplace() async {
#if swift(<6.2)
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
XCTTODO("Ideally this would pass but we cannot detect this kind of mutation currently.")
var state = ChildState(count: 42)
let didChange = LockIsolated(false)
@@ -98,12 +94,7 @@ final class ObservableTests: BaseTCATestCase {
}
func testReset() async {
#if swift(<6.2)
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
XCTTODO("Ideally this would pass but we cannot detect this kind of mutation currently.")
var state = ChildState(count: 42)
let didChange = LockIsolated(false)

View File

@@ -41,27 +41,28 @@ final class ForEachReducerTests: BaseTCATestCase {
}
XCTExpectFailure {
$0.compactDescription == """
failed - A "forEach" at "\(#fileID):\(#line - 5)" received an action for a missing \
element.
$0.compactDescription.hasSuffix(
"""
A "forEach" at "\(#fileID):\(#line - 6)" received an action for a missing element.
Action:
Elements.Action.rows(.element(id:, action:))
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 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 \
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 \
its state contains an element at this id. In SwiftUI applications, use "ForEachStore".
"""
)
}
await store.send(\.rows[id: 1], "Blob Esq.")

View File

@@ -39,9 +39,10 @@ final class IfCaseLetReducerTests: BaseTCATestCase {
}
XCTExpectFailure {
$0.compactDescription == """
failed - An "ifCaseLet" at "\(#fileID):\(#line - 5)" received a child action when child \
state was set to a different case. …
$0.compactDescription.hasSuffix(
"""
An "ifCaseLet" at "\(#fileID):\(#line - 6)" received a child action when child state was \
set to a different case.
Action:
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:
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 \
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 \
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 \
case. In SwiftUI applications, use "SwitchStore".
"""
)
}
await store.send(.success(1))

View File

@@ -10,9 +10,10 @@ final class IfLetReducerTests: BaseTCATestCase {
}
XCTExpectFailure {
$0.compactDescription == """
failed - An "ifLet" at "\(#fileID):\(#line - 5)" received a child action when child state \
was "nil". …
$0.compactDescription.hasSuffix(
"""
An "ifLet" at "\(#fileID):\(#line - 6)" received a child action when child state \
was "nil".
Action:
()
@@ -20,18 +21,19 @@ final class IfLetReducerTests: BaseTCATestCase {
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 \
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 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 \
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 \
applications, use "IfLetStore".
"""
)
}
await store.send(())

View File

@@ -37,17 +37,21 @@ final class PresentationReducerTests: BaseTCATestCase {
XCTExpectFailure {
parent.$child[case: /Child.text]?.append("!")
} issueMatcher: {
$0.compactDescription == """
failed - Can't modify unrelated case "int"
$0.compactDescription.hasSuffix(
"""
Can't modify unrelated case "int"
"""
)
}
XCTExpectFailure {
parent.$child[case: /Child.text] = nil
} issueMatcher: {
$0.compactDescription == """
failed - Can't modify unrelated case "int"
$0.compactDescription.hasSuffix(
"""
Can't modify unrelated case "int"
"""
)
}
XCTAssertEqual(parent.child, .int(42))
@@ -937,7 +941,7 @@ final class PresentationReducerTests: BaseTCATestCase {
}
)
return .none
case let .presentChild(id):
case .presentChild(let id):
state.destination = .child(Child.State(id: id ?? self.uuid()))
return .none
}
@@ -1219,7 +1223,7 @@ final class PresentationReducerTests: BaseTCATestCase {
var body: some Reducer<State, Action> {
Reduce { state, action in
switch action {
case let .response(value):
case .response(let value):
state.count = value
return .none
case .startButtonTapped:
@@ -1315,7 +1319,7 @@ final class PresentationReducerTests: BaseTCATestCase {
var body: some Reducer<State, Action> {
Reduce { state, action in
switch action {
case let .response(value):
case .response(let value):
state.count = value
return .none
case .startButtonTapped:
@@ -1418,7 +1422,7 @@ final class PresentationReducerTests: BaseTCATestCase {
var body: some Reducer<State, Action> {
Reduce { state, action in
switch action {
case let .response(value):
case .response(let value):
state.count = value
return .none
case .startButtonTapped:
@@ -1545,7 +1549,7 @@ final class PresentationReducerTests: BaseTCATestCase {
case .presentChild:
state.child = Child.State()
return .none
case let .response(value):
case .response(let value):
state.count = value
return .none
case .startButtonTapped:
@@ -1722,24 +1726,26 @@ final class PresentationReducerTests: BaseTCATestCase {
}
XCTExpectFailure {
$0.compactDescription == """
failed - An "ifLet" at \
"ComposableArchitectureTests/PresentationReducerTests.swift:\(#line - 13)" received a \
presentation action when destination state was absent. …
$0.compactDescription.hasSuffix(
"""
An "ifLet" at \
"ComposableArchitectureTests/PresentationReducerTests.swift:\(#line - 14)" received a \
presentation action when destination state was absent.
Action:
PresentationReducerTests.Parent.Action.child(.dismiss)
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 \
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 \
from effects that start from this reducer.
"""
)
}
await store.send(.child(.dismiss))
@@ -1778,24 +1784,26 @@ final class PresentationReducerTests: BaseTCATestCase {
}
XCTExpectFailure {
$0.compactDescription == """
failed - An "ifLet" at \
"ComposableArchitectureTests/PresentationReducerTests.swift:\(#line - 13)" received a \
presentation action when destination state was absent. …
$0.compactDescription.hasSuffix(
"""
An "ifLet" at \
"ComposableArchitectureTests/PresentationReducerTests.swift:\(#line - 14)" received a \
presentation action when destination state was absent.
Action:
PresentationReducerTests.Parent.Action.child(.presented(.tap))
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 \
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 \
from effects that start from this reducer.
"""
)
}
await store.send(.child(.presented(.tap)))
@@ -2104,7 +2112,8 @@ final class PresentationReducerTests: BaseTCATestCase {
ConfirmationDialogState {
TextState("Hello!")
} actions: {
})
}
)
return .none
case .destination(.presented(.dialog(.showAlert))):
state.destination = .alert(AlertState { TextState("Hello!") })
@@ -2150,7 +2159,8 @@ final class PresentationReducerTests: BaseTCATestCase {
ConfirmationDialogState {
TextState("Hello!")
} actions: {
})
}
)
}
await store.send(.destination(.dismiss)) {
$0.destination = nil
@@ -2258,31 +2268,33 @@ final class PresentationReducerTests: BaseTCATestCase {
XCTExpectFailure {
$0.sourceCodeContext.location?.fileURL.absoluteString.contains("BaseTCATestCase") == true
|| $0.sourceCodeContext.location?.lineNumber == line + 1
&& $0.compactDescription == """
failed - An effect returned for this action is still running. It must complete before \
the end of the test. …
&& $0.compactDescription.hasSuffix(
"""
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 \
them complete by the end of the test. There are a few reasons why an effect may 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.
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 \
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.
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 \
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 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.exhaustivity = .off".
"""
)
}
}
@@ -2300,7 +2312,7 @@ final class PresentationReducerTests: BaseTCATestCase {
var body: some Reducer<State, Action> {
Reduce { state, action in
switch action {
case let .response(value):
case .response(let value):
state.count = value
return .none
case .tap:
@@ -2337,7 +2349,7 @@ final class PresentationReducerTests: BaseTCATestCase {
await send(.response(42))
}
.cancellable(id: Child.CancelID())
case let .response(value):
case .response(let value):
state.count = value
return .none
}
@@ -2571,10 +2583,10 @@ final class PresentationReducerTests: BaseTCATestCase {
}
XCTExpectFailure {
$0.compactDescription.hasPrefix(
$0.compactDescription.contains(
"""
failed - A "Scope" at "\(#fileID):\(line)" received a child action when child state was \
set to a different case.
A "Scope" at "\(#fileID):\(line)" received a child action when child state was \
set to a different case.
"""
)
}

View File

@@ -28,17 +28,21 @@ final class StackReducerTests: BaseTCATestCase {
XCTExpectFailure {
stack[id: 0, case: /Element.text]?.append("!")
} issueMatcher: {
$0.compactDescription == """
failed - Can't modify unrelated case "int"
$0.compactDescription.hasSuffix(
"""
Can't modify unrelated case "int"
"""
)
}
XCTExpectFailure {
stack[id: 0, case: /Element.text] = nil
} issueMatcher: {
$0.compactDescription == """
failed - Can't modify unrelated case "int"
$0.compactDescription.hasSuffix(
"""
Can't modify unrelated case "int"
"""
)
}
XCTAssertEqual(Array(stack), [.int(42)])
@@ -261,8 +265,9 @@ final class StackReducerTests: BaseTCATestCase {
}
XCTExpectFailure {
$0.compactDescription == """
failed - Received unexpected action: …
$0.compactDescription.hasSuffix(
"""
Received unexpected action:
StackReducerTests.Parent.Action.children(
.popFrom(id: #1)
@@ -271,6 +276,7 @@ final class StackReducerTests: BaseTCATestCase {
(Expected: , Received: +)
"""
)
}
await store.send(.children(.element(id: 0, action: .tap)))
@@ -561,7 +567,7 @@ final class StackReducerTests: BaseTCATestCase {
switch action {
case .cancel:
return .cancel(id: CancelID.cancel)
case let .response(value):
case .response(let value):
state.count = value
return .none
case .tap:
@@ -661,7 +667,7 @@ final class StackReducerTests: BaseTCATestCase {
var body: some Reducer<State, Action> {
Reduce { state, action in
switch action {
case let .response(value):
case .response(let value):
state.count += value
return .none
case .tap:
@@ -774,28 +780,30 @@ final class StackReducerTests: BaseTCATestCase {
let line = #line - 3
XCTExpectFailure {
$0.compactDescription == """
failed - A "forEach" at "ComposableArchitectureTests/StackReducerTests.swift:\(line)" \
received an action for a missing element. …
$0.compactDescription.hasSuffix(
"""
A "forEach" at "ComposableArchitectureTests/StackReducerTests.swift:\(line)" received an \
action for a missing element.
Action:
()
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 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 \
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 \
its state contains an element at this id. In SwiftUI applications, use \
"NavigationStack.init(path:)" with a binding to a store.
"""
)
}
var path = StackState<Int>()
@@ -823,15 +831,17 @@ final class StackReducerTests: BaseTCATestCase {
let line = #line - 3
XCTExpectFailure {
$0.compactDescription == """
failed - A "forEach" at "ComposableArchitectureTests/StackReducerTests.swift:\(line)" \
received a "popFrom" action for a missing element. …
$0.compactDescription.hasSuffix(
"""
A "forEach" at "ComposableArchitectureTests/StackReducerTests.swift:\(line)" \
received a "popFrom" action for a missing element.
ID:
#999
Path IDs:
[#0]
"""
)
}
let store = TestStore(initialState: Parent.State(path: StackState<Int>([1]))) {
@@ -874,31 +884,33 @@ final class StackReducerTests: BaseTCATestCase {
XCTExpectFailure {
$0.sourceCodeContext.location?.fileURL.absoluteString.contains("BaseTCATestCase") == true
|| $0.sourceCodeContext.location?.lineNumber == line + 1
&& $0.compactDescription == """
failed - An effect returned for this action is still running. It must complete before \
the end of the test. …
&& $0.compactDescription.hasSuffix(
"""
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 them complete by the end of the test. There are a few reasons why an effect may \
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.
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 \
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.
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 \
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 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.exhaustivity = .off".
"""
)
}
}
@@ -918,7 +930,7 @@ final class StackReducerTests: BaseTCATestCase {
try await self.mainQueue.sleep(for: .seconds(count))
await send(.response(42))
}
case let .response(value):
case .response(let value):
state.count = value
return .none
}
@@ -1084,15 +1096,17 @@ final class StackReducerTests: BaseTCATestCase {
}
XCTExpectFailure {
$0.compactDescription == """
failed - A "forEach" at "ComposableArchitectureTests/StackReducerTests.swift:\(line)" \
received a "push" action for an element it already contains. …
$0.compactDescription.hasSuffix(
"""
A "forEach" at "ComposableArchitectureTests/StackReducerTests.swift:\(line)" \
received a "push" action for an element it already contains.
ID:
#0
Path IDs:
[#0]
"""
)
}
await store.send(.child(.push(id: 0, state: Child.State()))) {
@@ -1128,15 +1142,17 @@ final class StackReducerTests: BaseTCATestCase {
}
XCTExpectFailure {
$0.compactDescription == """
failed - A "forEach" at "ComposableArchitectureTests/StackReducerTests.swift:\(line)" \
received a "push" action with an unexpected generational ID. …
$0.compactDescription.hasSuffix(
"""
A "forEach" at "ComposableArchitectureTests/StackReducerTests.swift:\(line)" received a \
"push" action with an unexpected generational ID.
Received ID:
#1
Expected ID:
#0
"""
)
}
await store.send(.child(.push(id: 1, state: Child.State()))) {
@@ -1169,8 +1185,9 @@ final class StackReducerTests: BaseTCATestCase {
}
XCTExpectFailure {
$0.compactDescription == """
failed - A state change does not match expectation: …
$0.compactDescription.hasSuffix(
"""
A state change does not match expectation.
StackReducerTests.Parent.State(
children: [
@@ -1181,6 +1198,7 @@ final class StackReducerTests: BaseTCATestCase {
(Expected: , Actual: +)
"""
)
}
await store.send(.child(.push(id: 0, state: Child.State()))) {
$0.children[id: 1] = Child.State()

View File

@@ -16,15 +16,17 @@
let store = Store<State, Action>(initialState: State()) {}
XCTExpectFailure {
$0.compactDescription == """
failed - A binding action sent from a store for binding state defined at \
"\(#fileID):\(line)" was not handled. …
$0.compactDescription.hasSuffix(
"""
A binding action sent from a store for binding state defined at "\(#fileID):\(line)" was \
not handled.
Action:
RuntimeWarningTests.Action.binding(.set(_, 42))
To fix this, invoke "BindingReducer()" from your feature reducer's "body".
"""
)
}
let viewStore = ViewStore(store, observe: { $0 })
@@ -46,14 +48,16 @@
let store = Store<State, Action>(initialState: State()) {}
XCTExpectFailure {
$0.compactDescription == """
failed - A binding action sent from a store was not handled. …
$0.compactDescription.hasSuffix(
"""
A binding action sent from a store was not handled.
Action:
RuntimeWarningTests.Action.binding(.set(_, 42))
To fix this, invoke "BindingReducer()" from your feature reducer's "body".
"""
)
}
store.count = 42
@@ -72,15 +76,17 @@
let store = Store<State, Action>(initialState: State()) {}
XCTExpectFailure {
$0.compactDescription == """
failed - A binding action sent from a store for binding state defined at \
"\(#fileID):\(line)" was not handled. …
$0.compactDescription.hasSuffix(
"""
A binding action sent from a store for binding state defined at "\(#fileID):\(line)" was \
not handled.
Action:
RuntimeWarningTests.Action.binding(.set(_, 42))
To fix this, invoke "BindingReducer()" from your feature reducer's "body".
"""
)
}
let viewStore = ViewStore(store, observe: { $0 })
@@ -112,11 +118,12 @@
column: 1
] = .init()
} issueMatcher: {
$0.compactDescription == """
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 \
write to this binding with a path that has pushed a new element onto the stack, or \
popped one or more elements from the stack.
$0.compactDescription.hasSuffix(
"""
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 write to \
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 \
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 \
the store.
"""
)
}
}
@@ -168,9 +176,9 @@
column: 1
] = nil
} issueMatcher: {
$0.compactDescription == """
failed - A binding at "file.swift:1" was set to "nil", but the store destination wasn't \
nil'd out.
$0.compactDescription.hasSuffix(
"""
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 \
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 \
store.
"""
)
}
}
}

View File

@@ -15,21 +15,21 @@ final class ScopeCacheTests: BaseTCATestCase {
.scope(state: \.child, action: \.child.presented)?
.send(.show)
} issueMatcher: {
$0.compactDescription == """
failed - Scoping from uncached StoreOf<Feature> is not compatible with observation.
$0.compactDescription.hasSuffix(
"""
Scoping from uncached 'StoreOf<Feature>' is not compatible with observation.
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 \
scopes: \
https://swiftpackageindex.com/pointfreeco/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5
scopes: 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 \
bindings. Read the migration guide for 1.7 to update those APIs: \
https://swiftpackageindex.com/pointfreeco/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7
bindings. Read the migration guide for 1.7 to update those APIs: https://swiftpackageindex.com/pointfreeco/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7
"""
)
}
store.send(.child(.dismiss))
}
@@ -76,21 +76,21 @@ final class ScopeCacheTests: BaseTCATestCase {
}
_ = cancellable
} issueMatcher: {
$0.compactDescription == """
failed - Scoping from uncached StoreOf<Feature> is not compatible with observation.
$0.compactDescription.hasSuffix(
"""
Scoping from uncached 'StoreOf<Feature>' is not compatible with observation.
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 \
scopes: \
https://swiftpackageindex.com/pointfreeco/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5
scopes: 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 \
bindings. Read the migration guide for 1.7 to update those APIs: \
https://swiftpackageindex.com/pointfreeco/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7
bindings. Read the migration guide for 1.7 to update those APIs: 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)
)
} issueMatcher: {
$0.compactDescription == """
failed - Scoping from uncached StoreOf<Feature> is not compatible with observation.
$0.compactDescription.hasSuffix(
"""
Scoping from uncached 'StoreOf<Feature>' is not compatible with observation.
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 \
scopes: \
https://swiftpackageindex.com/pointfreeco/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5
scopes: 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 \
bindings. Read the migration guide for 1.7 to update those APIs: \
https://swiftpackageindex.com/pointfreeco/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7
bindings. Read the migration guide for 1.7 to update those APIs: https://swiftpackageindex.com/pointfreeco/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7
"""
)
}
}
}

View File

@@ -45,9 +45,10 @@ final class ScopeTests: BaseTCATestCase {
}
XCTExpectFailure {
$0.compactDescription == """
failed - A "Scope" at "\(#fileID):\(#line - 5)" received a child action when child state \
was set to a different case. …
$0.compactDescription.hasSuffix(
"""
A "Scope" at "\(#fileID):\(#line - 6)" received a child action when child state \
was set to a different case.
Action:
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:
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. \
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 \
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 \
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 \
case. In SwiftUI applications, use "SwitchStore".
"""
)
}
await store.send(.name("Blob"))

View File

@@ -14,8 +14,9 @@ final class TaskResultTests: BaseTCATestCase {
TaskResult<Never>.failure(Failure(message: "Something went wrong"))
)
} issueMatcher: {
$0.compactDescription == """
failed - "TaskResultTests.Failure" is not equatable. …
$0.compactDescription.hasSuffix(
"""
"TaskResultTests.Failure" is not equatable.
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.
"""
)
}
}
@@ -40,8 +42,9 @@ final class TaskResultTests: BaseTCATestCase {
TaskResult<Never>.failure(Failure2(message: "Something went wrong"))
)
} issueMatcher: {
$0.compactDescription == """
failed - Difference: …
$0.compactDescription.hasSuffix(
"""
Difference: …
TaskResult.failure(
TaskResultTests.Failure1(message: "Something went wrong")
@@ -50,6 +53,7 @@ final class TaskResultTests: BaseTCATestCase {
(First: , Second: +)
"""
)
}
}
@@ -61,8 +65,9 @@ final class TaskResultTests: BaseTCATestCase {
XCTExpectFailure {
_ = TaskResult<Never>.failure(Failure(message: "Something went wrong")).hashValue
} issueMatcher: {
$0.compactDescription == """
failed - "TaskResultTests.Failure" is not hashable. …
$0.compactDescription.hasSuffix(
"""
"TaskResultTests.Failure" is not hashable.
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.
"""
)
}
}
#endif

View File

@@ -15,22 +15,26 @@ final class TestStoreFailureTests: BaseTCATestCase {
}
XCTExpectFailure {
$0.compactDescription == """
failed - Expected state to change, but no change occurred.
$0.compactDescription.hasSuffix(
"""
Expected state to change, but no change occurred.
The trailing closure made no observable modifications to state. If no change to state is \
expected, omit the trailing closure.
"""
)
}
await store.send(.first) { _ = $0 }
XCTExpectFailure {
$0.compactDescription == """
failed - Expected state to change, but no change occurred.
$0.compactDescription.hasSuffix(
"""
Expected state to change, but no change occurred.
The trailing closure made no observable modifications to state. If no change to state is \
expected, omit the trailing closure.
"""
)
}
await store.receive(.second) { _ = $0 }
}
@@ -46,14 +50,16 @@ final class TestStoreFailureTests: BaseTCATestCase {
}
XCTExpectFailure {
$0.compactDescription == """
failed - A state change does not match expectation: …
$0.compactDescription.hasSuffix(
"""
A state change does not match expectation.
TestStoreFailureTests.State(count: 0)
+ TestStoreFailureTests.State(count: 1)
(Expected: , Actual: +)
"""
)
}
await store.send(()) { $0.count = 0 }
}
@@ -69,14 +75,16 @@ final class TestStoreFailureTests: BaseTCATestCase {
}
XCTExpectFailure {
$0.compactDescription == """
failed - State was not expected to change, but a change occurred: …
$0.compactDescription.hasSuffix(
"""
State was not expected to change, but a change occurred.
TestStoreFailureTests.State(count: 0)
+ TestStoreFailureTests.State(count: 1)
(Expected: , Actual: +)
"""
)
}
await store.send(())
}
@@ -98,14 +106,16 @@ final class TestStoreFailureTests: BaseTCATestCase {
await store.send(.first)
XCTExpectFailure {
$0.compactDescription == """
failed - State was not expected to change, but a change occurred: …
$0.compactDescription.hasSuffix(
"""
State was not expected to change, but a change occurred.
TestStoreFailureTests.State(count: 0)
+ TestStoreFailureTests.State(count: 1)
(Expected: , Actual: +)
"""
)
}
await store.receive(.second)
}
@@ -123,16 +133,18 @@ final class TestStoreFailureTests: BaseTCATestCase {
}
XCTExpectFailure {
$0.compactDescription == """
failed - The store received 1 unexpected action: …
$0.compactDescription.hasSuffix(
"""
The store received 1 unexpected action.
Unhandled actions:
.second
.second
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 \
store: "store.exhaustivity = .off".
"""
)
}
await store.send(.first)
}
@@ -152,16 +164,18 @@ final class TestStoreFailureTests: BaseTCATestCase {
await store.send(.first)
XCTExpectFailure {
$0.compactDescription == """
failed - The store received 1 unexpected action: …
$0.compactDescription.hasSuffix(
"""
The store received 1 unexpected action.
Unhandled actions:
.second
.second
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 \
store: "store.exhaustivity = .off".
"""
)
}
await store.finish()
}
@@ -176,31 +190,33 @@ final class TestStoreFailureTests: BaseTCATestCase {
}
XCTExpectFailure {
$0.compactDescription == """
failed - An effect returned for this action is still running. It must complete before the \
end of the test. …
$0.compactDescription.hasSuffix(
"""
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 \
them complete by the end of the test. There are a few reasons why an effect may 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.
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 \
clock/scheduler, advance it so that the effects may complete, or consider using an \
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 \
corresponding cancellation effect ("Effect.cancel") from another action, 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.exhaustivity = .off".
"""
)
}
await store.send(())
}
@@ -220,13 +236,15 @@ final class TestStoreFailureTests: BaseTCATestCase {
await store.send(.first)
XCTExpectFailure {
$0.compactDescription == """
failed - Must handle 1 received action before sending an action: …
$0.compactDescription.hasSuffix(
"""
Must handle 1 received action before sending an action.
Unhandled actions: [
[0]: .second
]
"""
)
}
await store.send(.first)
@@ -242,11 +260,13 @@ final class TestStoreFailureTests: BaseTCATestCase {
}
XCTExpectFailure {
$0.compactDescription == """
failed - Expected to receive the following action, but didn't: …
$0.compactDescription.hasSuffix(
"""
Expected to receive the following action, but didn't:
TestStoreFailureTests.Action.action
"""
)
}
await store.receive(.action)
}
@@ -269,14 +289,16 @@ final class TestStoreFailureTests: BaseTCATestCase {
await store.send(.first)
XCTExpectFailure {
$0.compactDescription == """
failed - Received unexpected action: …
$0.compactDescription.hasSuffix(
"""
Received unexpected action:
TestStoreFailureTests.Action.first
+ TestStoreFailureTests.Action.second
(Expected: , Received: +)
"""
)
}
await store.receive(.first)
}
@@ -288,7 +310,7 @@ final class TestStoreFailureTests: BaseTCATestCase {
}
XCTExpectFailure {
$0.compactDescription == "failed - Threw error: SomeError()"
$0.compactDescription.hasSuffix("Threw error: SomeError()")
}
await store.send(()) { _ in
struct SomeError: Error {}

View File

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

View File

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