From 7eaaf82a39efa604f86f70b05af206a29fb2cb5b Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Mon, 13 Oct 2025 10:41:00 -0700 Subject: [PATCH] Update `reportIssue` formatting to avoid console errors When a non-ASCII character is fed to `reportIssue`, the following is output to the console: ``` ``` This causes a lot of confusion for folks, so let's update the formatting to avoid this. --- Sources/ComposableArchitecture/Core.swift | 2 +- .../Dependencies/Dismiss.swift | 2 +- .../ImplementingTimer-04-code-0007.swift | 2 +- Sources/ComposableArchitecture/Effect.swift | 2 +- .../Effects/TaskResult.swift | 4 +- .../Observation/Binding+Observation.swift | 2 +- .../NavigationStack+Observation.swift | 2 +- .../Observation/Store+Observation.swift | 6 +- .../Reducer/Reducers/ForEachReducer.swift | 8 +- .../Reducer/Reducers/IfCaseLetReducer.swift | 8 +- .../Reducer/Reducers/IfLetReducer.swift | 8 +- .../Reducers/PresentationReducer.swift | 6 +- .../Reducer/Reducers/Scope.swift | 8 +- .../Reducer/Reducers/StackReducer.swift | 14 ++-- .../SwiftUI/Binding.swift | 2 +- .../ComposableArchitecture/TestStore.swift | 30 +++---- .../EffectFailureTests.swift | 6 +- .../EffectRunTests.swift | 12 ++- .../ObservableTests.swift | 13 +-- .../Reducers/ForEachReducerTests.swift | 13 +-- .../Reducers/IfCaseLetReducerTests.swift | 14 ++-- .../Reducers/IfLetReducerTests.swift | 14 ++-- .../Reducers/PresentationReducerTests.swift | 82 ++++++++++-------- .../Reducers/StackReducerTests.swift | 84 +++++++++++-------- .../RuntimeWarningTests.swift | 41 +++++---- .../ScopeCacheTests.swift | 48 +++++------ .../ScopeTests.swift | 14 ++-- .../TaskResultTests.swift | 18 ++-- .../TestStoreFailureTests.swift | 82 +++++++++++------- .../TestStoreNonExhaustiveTests.swift | 40 +++++---- .../TestStoreTests.swift | 16 ++-- 31 files changed, 344 insertions(+), 259 deletions(-) diff --git a/Sources/ComposableArchitecture/Core.swift b/Sources/ComposableArchitecture/Core.swift index 5a2c876db524..3c753fe5971d 100644 --- a/Sources/ComposableArchitecture/Core.swift +++ b/Sources/ComposableArchitecture/Core.swift @@ -146,7 +146,7 @@ final class RootCore: Core { if isCompleted.value { reportIssue( """ - An action was sent from a completed effect: + An action was sent from a completed effect. Action: \(debugCaseOutput(effectAction)) diff --git a/Sources/ComposableArchitecture/Dependencies/Dismiss.swift b/Sources/ComposableArchitecture/Dependencies/Dismiss.swift index 4c16ffd6f7d2..55e51e61a764 100644 --- a/Sources/ComposableArchitecture/Dependencies/Dismiss.swift +++ b/Sources/ComposableArchitecture/Dependencies/Dismiss.swift @@ -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 \ diff --git a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-04-code-0007.swift b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-04-code-0007.swift index fd6f18823a76..0230db0f80a4 100644 --- a/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-04-code-0007.swift +++ b/Sources/ComposableArchitecture/Documentation.docc/Tutorials/BuildingSyncUps/08-RecordMeeting/ImplementingTimer-04-code-0007.swift @@ -30,6 +30,6 @@ struct RecordMeetingTests { // ❌ The store received 1 unexpected action by the end of this test: … // // Unhandled actions: - // • .timerTick + // .timerTick } } diff --git a/Sources/ComposableArchitecture/Effect.swift b/Sources/ComposableArchitecture/Effect.swift index 505bd797e3f4..bfd4c734f23b 100644 --- a/Sources/ComposableArchitecture/Effect.swift +++ b/Sources/ComposableArchitecture/Effect.swift @@ -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)) diff --git a/Sources/ComposableArchitecture/Effects/TaskResult.swift b/Sources/ComposableArchitecture/Effects/TaskResult.swift index 46297326e711..b5782fc6d13d 100644 --- a/Sources/ComposableArchitecture/Effects/TaskResult.swift +++ b/Sources/ComposableArchitecture/Effects/TaskResult.swift @@ -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: diff --git a/Sources/ComposableArchitecture/Observation/Binding+Observation.swift b/Sources/ComposableArchitecture/Observation/Binding+Observation.swift index 953c708164bf..d462a50cf0bf 100644 --- a/Sources/ComposableArchitecture/Observation/Binding+Observation.swift +++ b/Sources/ComposableArchitecture/Observation/Binding+Observation.swift @@ -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))) diff --git a/Sources/ComposableArchitecture/Observation/NavigationStack+Observation.swift b/Sources/ComposableArchitecture/Observation/NavigationStack+Observation.swift index 42c17a9407eb..e72b200471b1 100644 --- a/Sources/ComposableArchitecture/Observation/NavigationStack+Observation.swift +++ b/Sources/ComposableArchitecture/Observation/NavigationStack+Observation.swift @@ -389,7 +389,7 @@ public struct _NavigationLinkStoreContent: View { """ reportIssue( """ - A navigation link at "\(fileID):\(line)" is unpresentable. … + A navigation link at "\(fileID):\(line)" is not presentable. NavigationStack state element type: \(elementType) diff --git a/Sources/ComposableArchitecture/Observation/Store+Observation.swift b/Sources/ComposableArchitecture/Observation/Store+Observation.swift index f1a2b8c04fe2..3d923136c0fa 100644 --- a/Sources/ComposableArchitecture/Observation/Store+Observation.swift +++ b/Sources/ComposableArchitecture/Observation/Store+Observation.swift @@ -500,16 +500,16 @@ extension Store where State: ObservableState { func uncachedStoreWarning(_ store: Store) -> 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 diff --git a/Sources/ComposableArchitecture/Reducer/Reducers/ForEachReducer.swift b/Sources/ComposableArchitecture/Reducer/Reducers/ForEachReducer.swift index 12de07d199f5..289b98861106 100644 --- a/Sources/ComposableArchitecture/Reducer/Reducers/ForEachReducer.swift +++ b/Sources/ComposableArchitecture/Reducer/Reducers/ForEachReducer.swift @@ -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". """, diff --git a/Sources/ComposableArchitecture/Reducer/Reducers/IfCaseLetReducer.swift b/Sources/ComposableArchitecture/Reducer/Reducers/IfCaseLetReducer.swift index 0c3f55f6d0ef..122ef23ee7d3 100644 --- a/Sources/ComposableArchitecture/Reducer/Reducers/IfCaseLetReducer.swift +++ b/Sources/ComposableArchitecture/Reducer/Reducers/IfCaseLetReducer.swift @@ -215,7 +215,7 @@ public struct _IfCaseLetReducer: 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: 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". """, diff --git a/Sources/ComposableArchitecture/Reducer/Reducers/IfLetReducer.swift b/Sources/ComposableArchitecture/Reducer/Reducers/IfLetReducer.swift index 11fb635370b3..5a0325f869cc 100644 --- a/Sources/ComposableArchitecture/Reducer/Reducers/IfLetReducer.swift +++ b/Sources/ComposableArchitecture/Reducer/Reducers/IfLetReducer.swift @@ -287,22 +287,22 @@ public struct _IfLetReducer: 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". """, diff --git a/Sources/ComposableArchitecture/Reducer/Reducers/PresentationReducer.swift b/Sources/ComposableArchitecture/Reducer/Reducers/PresentationReducer.swift index 6c17d5c6c112..b8b65a3a8864 100644 --- a/Sources/ComposableArchitecture/Reducer/Reducers/PresentationReducer.swift +++ b/Sources/ComposableArchitecture/Reducer/Reducers/PresentationReducer.swift @@ -632,7 +632,7 @@ public struct _PresentationReducer: 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: 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. """, diff --git a/Sources/ComposableArchitecture/Reducer/Reducers/Scope.swift b/Sources/ComposableArchitecture/Reducer/Reducers/Scope.swift index 22a0e7086465..ddb557db2ce1 100644 --- a/Sources/ComposableArchitecture/Reducer/Reducers/Scope.swift +++ b/Sources/ComposableArchitecture/Reducer/Reducers/Scope.swift @@ -347,7 +347,7 @@ public struct Scope: 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: 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". """, diff --git a/Sources/ComposableArchitecture/Reducer/Reducers/StackReducer.swift b/Sources/ComposableArchitecture/Reducer/Reducers/StackReducer.swift index 7d037fe2e382..ec6982d9008d 100644 --- a/Sources/ComposableArchitecture/Reducer/Reducers/StackReducer.swift +++ b/Sources/ComposableArchitecture/Reducer/Reducers/StackReducer.swift @@ -526,22 +526,22 @@ public struct _StackReducer: 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: 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: 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: Reducer { reportIssue( """ A "forEach" at "\(self.fileID):\(self.line)" received a "push" action with an \ - unexpected generational ID. … + unexpected generational ID. Received ID: \(id) diff --git a/Sources/ComposableArchitecture/SwiftUI/Binding.swift b/Sources/ComposableArchitecture/SwiftUI/Binding.swift index de09df1a890a..a34509064cbc 100644 --- a/Sources/ComposableArchitecture/SwiftUI/Binding.swift +++ b/Sources/ComposableArchitecture/SwiftUI/Binding.swift @@ -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))) diff --git a/Sources/ComposableArchitecture/TestStore.swift b/Sources/ComposableArchitecture/TestStore.swift index 0718be7fba6d..dc7c4c190b46 100644 --- a/Sources/ComposableArchitecture/TestStore.swift +++ b/Sources/ComposableArchitecture/TestStore.swift @@ -663,26 +663,26 @@ public final class TestStore { 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 { 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) """, diff --git a/Tests/ComposableArchitectureTests/EffectFailureTests.swift b/Tests/ComposableArchitectureTests/EffectFailureTests.swift index dfaa74bdc1ba..3d3f5745e152 100644 --- a/Tests/ComposableArchitectureTests/EffectFailureTests.swift +++ b/Tests/ComposableArchitectureTests/EffectFailureTests.swift @@ -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 diff --git a/Tests/ComposableArchitectureTests/EffectRunTests.swift b/Tests/ComposableArchitectureTests/EffectRunTests.swift index ae61f10dd7e1..cc2cb8c8134e 100644 --- a/Tests/ComposableArchitectureTests/EffectRunTests.swift +++ b/Tests/ComposableArchitectureTests/EffectRunTests.swift @@ -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 } diff --git a/Tests/ComposableArchitectureTests/ObservableTests.swift b/Tests/ComposableArchitectureTests/ObservableTests.swift index d905cd25ef8e..3c171ed944c4 100644 --- a/Tests/ComposableArchitectureTests/ObservableTests.swift +++ b/Tests/ComposableArchitectureTests/ObservableTests.swift @@ -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) diff --git a/Tests/ComposableArchitectureTests/Reducers/ForEachReducerTests.swift b/Tests/ComposableArchitectureTests/Reducers/ForEachReducerTests.swift index 6b0b3e051b91..15fa3ac08c64 100644 --- a/Tests/ComposableArchitectureTests/Reducers/ForEachReducerTests.swift +++ b/Tests/ComposableArchitectureTests/Reducers/ForEachReducerTests.swift @@ -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.") diff --git a/Tests/ComposableArchitectureTests/Reducers/IfCaseLetReducerTests.swift b/Tests/ComposableArchitectureTests/Reducers/IfCaseLetReducerTests.swift index 00252adc1b59..c8d629f2a1e7 100644 --- a/Tests/ComposableArchitectureTests/Reducers/IfCaseLetReducerTests.swift +++ b/Tests/ComposableArchitectureTests/Reducers/IfCaseLetReducerTests.swift @@ -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)) diff --git a/Tests/ComposableArchitectureTests/Reducers/IfLetReducerTests.swift b/Tests/ComposableArchitectureTests/Reducers/IfLetReducerTests.swift index 7c9c36d596d0..96f6f78eb335 100644 --- a/Tests/ComposableArchitectureTests/Reducers/IfLetReducerTests.swift +++ b/Tests/ComposableArchitectureTests/Reducers/IfLetReducerTests.swift @@ -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(()) diff --git a/Tests/ComposableArchitectureTests/Reducers/PresentationReducerTests.swift b/Tests/ComposableArchitectureTests/Reducers/PresentationReducerTests.swift index 139b605ea6b9..2c2465eb354b 100644 --- a/Tests/ComposableArchitectureTests/Reducers/PresentationReducerTests.swift +++ b/Tests/ComposableArchitectureTests/Reducers/PresentationReducerTests.swift @@ -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 { 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 { 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 { 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 { 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. """ ) } diff --git a/Tests/ComposableArchitectureTests/Reducers/StackReducerTests.swift b/Tests/ComposableArchitectureTests/Reducers/StackReducerTests.swift index 95cc5e4d14ca..9cd1a6a9bc79 100644 --- a/Tests/ComposableArchitectureTests/Reducers/StackReducerTests.swift +++ b/Tests/ComposableArchitectureTests/Reducers/StackReducerTests.swift @@ -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 { 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() @@ -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([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() diff --git a/Tests/ComposableArchitectureTests/RuntimeWarningTests.swift b/Tests/ComposableArchitectureTests/RuntimeWarningTests.swift index 98dab93e89c4..77423aa1ac58 100644 --- a/Tests/ComposableArchitectureTests/RuntimeWarningTests.swift +++ b/Tests/ComposableArchitectureTests/RuntimeWarningTests.swift @@ -16,15 +16,17 @@ let store = Store(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(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(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. """ + ) } } } diff --git a/Tests/ComposableArchitectureTests/ScopeCacheTests.swift b/Tests/ComposableArchitectureTests/ScopeCacheTests.swift index 109885417038..9cbb5ce12fd8 100644 --- a/Tests/ComposableArchitectureTests/ScopeCacheTests.swift +++ b/Tests/ComposableArchitectureTests/ScopeCacheTests.swift @@ -15,21 +15,21 @@ final class ScopeCacheTests: BaseTCATestCase { .scope(state: \.child, action: \.child.presented)? .send(.show) } issueMatcher: { - $0.compactDescription == """ - failed - Scoping from uncached StoreOf is not compatible with observation. + $0.compactDescription.hasSuffix( + """ + Scoping from uncached 'StoreOf' 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 is not compatible with observation. + $0.compactDescription.hasSuffix( + """ + Scoping from uncached 'StoreOf' 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 is not compatible with observation. + $0.compactDescription.hasSuffix( + """ + Scoping from uncached 'StoreOf' 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 """ + ) } } } diff --git a/Tests/ComposableArchitectureTests/ScopeTests.swift b/Tests/ComposableArchitectureTests/ScopeTests.swift index d25250c66195..f9a7e630dc04 100644 --- a/Tests/ComposableArchitectureTests/ScopeTests.swift +++ b/Tests/ComposableArchitectureTests/ScopeTests.swift @@ -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")) diff --git a/Tests/ComposableArchitectureTests/TaskResultTests.swift b/Tests/ComposableArchitectureTests/TaskResultTests.swift index bd091720e8c0..5674faae4d07 100644 --- a/Tests/ComposableArchitectureTests/TaskResultTests.swift +++ b/Tests/ComposableArchitectureTests/TaskResultTests.swift @@ -14,8 +14,9 @@ final class TaskResultTests: BaseTCATestCase { TaskResult.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.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.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 diff --git a/Tests/ComposableArchitectureTests/TestStoreFailureTests.swift b/Tests/ComposableArchitectureTests/TestStoreFailureTests.swift index a890f486a8eb..06dec5fa9359 100644 --- a/Tests/ComposableArchitectureTests/TestStoreFailureTests.swift +++ b/Tests/ComposableArchitectureTests/TestStoreFailureTests.swift @@ -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 {} diff --git a/Tests/ComposableArchitectureTests/TestStoreNonExhaustiveTests.swift b/Tests/ComposableArchitectureTests/TestStoreNonExhaustiveTests.swift index 32f4e176409a..296dfa01318d 100644 --- a/Tests/ComposableArchitectureTests/TestStoreNonExhaustiveTests.swift +++ b/Tests/ComposableArchitectureTests/TestStoreNonExhaustiveTests.swift @@ -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. + """ + ) } } } diff --git a/Tests/ComposableArchitectureTests/TestStoreTests.swift b/Tests/ComposableArchitectureTests/TestStoreTests.swift index d919a1350028..6a2a1c4d0d3e 100644 --- a/Tests/ComposableArchitectureTests/TestStoreTests.swift +++ b/Tests/ComposableArchitectureTests/TestStoreTests.swift @@ -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) }