Skip to content

Commit 8191e36

Browse files
authored
Emit test failure when warning in release. (#3024)
* Emit test failure when warning in release. * wip
1 parent 7e22846 commit 8191e36

13 files changed

+590
-624
lines changed

Sources/ComposableArchitecture/Internal/RuntimeWarnings.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@ func runtimeWarn(
4343
fputs("\(formatter.string(from: Date())) [\(category)] \(message)\n", stderr)
4444
#endif
4545
}
46+
#else
47+
if _XCTIsTesting {
48+
XCTFail(message())
49+
}
4650
#endif
4751
}
4852

Tests/ComposableArchitectureTests/CompatibilityTests.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ final class CompatibilityTests: BaseTCATestCase {
8282
@MainActor
8383
func testCaseStudy_ActionReentranceFromStateObservation() {
8484
var cancellables: Set<AnyCancellable> = []
85+
defer { _ = cancellables }
8586

8687
let store = Store<Int, Int>(initialState: 0) {
8788
Reduce { state, action in

Tests/ComposableArchitectureTests/ComposableArchitectureTests.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ final class ComposableArchitectureTests: BaseTCATestCase {
7070
@MainActor
7171
func testSimultaneousWorkOrdering() {
7272
var cancellables: Set<AnyCancellable> = []
73+
defer { _ = cancellables }
7374

7475
let mainQueue = DispatchQueue.test
7576

Tests/ComposableArchitectureTests/EffectRunTests.swift

Lines changed: 32 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -127,50 +127,48 @@ final class EffectRunTests: BaseTCATestCase {
127127
await store.send(.tapped).finish()
128128
}
129129

130-
#if DEBUG
131-
@MainActor
132-
func testRunEscapeFailure() async {
133-
XCTExpectFailure {
134-
$0.compactDescription == """
135-
An action was sent from a completed effect:
130+
@MainActor
131+
func testRunEscapeFailure() async {
132+
XCTExpectFailure {
133+
$0.compactDescription == """
134+
An action was sent from a completed effect:
136135
137-
Action:
138-
EffectRunTests.Action.response
136+
Action:
137+
EffectRunTests.Action.response
139138
140-
Effect returned from:
141-
EffectRunTests.Action.tap
139+
Effect returned from:
140+
EffectRunTests.Action.tap
142141
143-
Avoid sending actions using the 'send' argument from 'Effect.run' after the effect has \
144-
completed. This can happen if you escape the 'send' argument in an unstructured context.
142+
Avoid sending actions using the 'send' argument from 'Effect.run' after the effect has \
143+
completed. This can happen if you escape the 'send' argument in an unstructured context.
145144
146-
To fix this, make sure that your 'run' closure does not return until you're done \
147-
calling 'send'.
148-
"""
149-
}
145+
To fix this, make sure that your 'run' closure does not return until you're done \
146+
calling 'send'.
147+
"""
148+
}
150149

151-
enum Action { case tap, response }
150+
enum Action { case tap, response }
152151

153-
let queue = DispatchQueue.test
152+
let queue = DispatchQueue.test
154153

155-
let store = Store(initialState: 0) {
156-
Reduce<Int, Action> { _, action in
157-
switch action {
158-
case .tap:
159-
return .run { send in
160-
Task(priority: .userInitiated) {
161-
try await queue.sleep(for: .seconds(1))
162-
await send(.response)
163-
}
154+
let store = Store(initialState: 0) {
155+
Reduce<Int, Action> { _, action in
156+
switch action {
157+
case .tap:
158+
return .run { send in
159+
Task(priority: .userInitiated) {
160+
try await queue.sleep(for: .seconds(1))
161+
await send(.response)
164162
}
165-
case .response:
166-
return .none
167163
}
164+
case .response:
165+
return .none
168166
}
169167
}
170-
171-
let viewStore = ViewStore(store, observe: { $0 })
172-
await viewStore.send(.tap).finish()
173-
await queue.advance(by: .seconds(1))
174168
}
175-
#endif
169+
170+
let viewStore = ViewStore(store, observe: { $0 })
171+
await viewStore.send(.tap).finish()
172+
await queue.advance(by: .seconds(1))
173+
}
176174
}

Tests/ComposableArchitectureTests/ObserveTests.swift

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -37,16 +37,14 @@
3737

3838
@MainActor
3939
func testNestedObservation() async throws {
40-
#if DEBUG
41-
XCTExpectFailure {
42-
$0.compactDescription == """
43-
An "observe" was called from another "observe" closure, which can lead to \
44-
over-observation and unintended side effects.
40+
XCTExpectFailure {
41+
$0.compactDescription == """
42+
An "observe" was called from another "observe" closure, which can lead to \
43+
over-observation and unintended side effects.
4544
46-
Avoid nested closures by moving child observation into their own lifecycle methods.
47-
"""
48-
}
49-
#endif
45+
Avoid nested closures by moving child observation into their own lifecycle methods.
46+
"""
47+
}
5048

5149
let model = Model()
5250
var counts: [Int] = []

Tests/ComposableArchitectureTests/Reducers/ForEachReducerTests.swift

Lines changed: 25 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -37,40 +37,38 @@
3737
await store.send(.buttonTapped)
3838
}
3939

40-
#if DEBUG
41-
@MainActor
42-
func testMissingElement() async {
43-
let store = TestStore(initialState: Elements.State()) {
44-
EmptyReducer<Elements.State, Elements.Action>()
45-
.forEach(\.rows, action: \.rows) {}
46-
}
47-
48-
XCTExpectFailure {
49-
$0.compactDescription == """
50-
A "forEach" at "\(#fileID):\(#line - 5)" received an action for a missing element. …
40+
@MainActor
41+
func testMissingElement() async {
42+
let store = TestStore(initialState: Elements.State()) {
43+
EmptyReducer<Elements.State, Elements.Action>()
44+
.forEach(\.rows, action: \.rows) {}
45+
}
5146

52-
Action:
53-
Elements.Action.rows(.element(id:, action:))
47+
XCTExpectFailure {
48+
$0.compactDescription == """
49+
A "forEach" at "\(#fileID):\(#line - 5)" received an action for a missing element. …
5450
55-
This is generally considered an application logic error, and can happen for a few reasons:
51+
Action:
52+
Elements.Action.rows(.element(id:, action:))
5653
57-
• A parent reducer removed an element with this ID before this reducer ran. This reducer \
58-
must run before any other reducer removes an element, which ensures that element \
59-
reducers can handle their actions while their state is still available.
54+
This is generally considered an application logic error, and can happen for a few reasons:
6055
61-
• An in-flight effect emitted this action when state contained no element at this ID. \
62-
While it may be perfectly reasonable to ignore this action, consider canceling the \
63-
associated effect before an element is removed, especially if it is a long-living effect.
56+
• A parent reducer removed an element with this ID before this reducer ran. This reducer \
57+
must run before any other reducer removes an element, which ensures that element \
58+
reducers can handle their actions while their state is still available.
6459
65-
• This action was sent to the store while its state contained no element at this ID. To \
66-
fix this make sure that actions for this reducer can only be sent from a view store when \
67-
its state contains an element at this id. In SwiftUI applications, use "ForEachStore".
68-
"""
69-
}
60+
• An in-flight effect emitted this action when state contained no element at this ID. \
61+
While it may be perfectly reasonable to ignore this action, consider canceling the \
62+
associated effect before an element is removed, especially if it is a long-living effect.
7063
71-
await store.send(\.rows[id:1], "Blob Esq.")
64+
• This action was sent to the store while its state contained no element at this ID. To \
65+
fix this make sure that actions for this reducer can only be sent from a view store when \
66+
its state contains an element at this id. In SwiftUI applications, use "ForEachStore".
67+
"""
7268
}
73-
#endif
69+
70+
await store.send(\.rows[id:1], "Blob Esq.")
71+
}
7472

7573
@MainActor
7674
func testAutomaticEffectCancellation() async {

Tests/ComposableArchitectureTests/Reducers/IfCaseLetReducerTests.swift

Lines changed: 29 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -31,45 +31,43 @@ final class IfCaseLetReducerTests: BaseTCATestCase {
3131
}
3232
}
3333

34-
#if DEBUG
35-
@MainActor
36-
func testNilChild() async {
37-
struct SomeError: Error, Equatable {}
38-
39-
let store = TestStore(initialState: Result.failure(SomeError())) {
40-
EmptyReducer<Result<Int, SomeError>, Result<Int, SomeError>>()
41-
.ifCaseLet(\.success, action: \.success) {}
42-
}
34+
@MainActor
35+
func testNilChild() async {
36+
struct SomeError: Error, Equatable {}
4337

44-
XCTExpectFailure {
45-
$0.compactDescription == """
46-
An "ifCaseLet" at "\(#fileID):\(#line - 5)" received a child action when child state was \
47-
set to a different case. …
38+
let store = TestStore(initialState: Result.failure(SomeError())) {
39+
EmptyReducer<Result<Int, SomeError>, Result<Int, SomeError>>()
40+
.ifCaseLet(\.success, action: \.success) {}
41+
}
4842

49-
Action:
50-
Result.success(1)
51-
State:
52-
Result.failure(IfCaseLetReducerTests.SomeError())
43+
XCTExpectFailure {
44+
$0.compactDescription == """
45+
An "ifCaseLet" at "\(#fileID):\(#line - 5)" received a child action when child state was \
46+
set to a different case. …
5347
54-
This is generally considered an application logic error, and can happen for a few reasons:
48+
Action:
49+
Result.success(1)
50+
State:
51+
Result.failure(IfCaseLetReducerTests.SomeError())
5552
56-
• A parent reducer set "Result" to a different case before this reducer ran. This \
57-
reducer must run before any other reducer sets child state to a different case. This \
58-
ensures that child reducers can handle their actions while their state is still available.
53+
This is generally considered an application logic error, and can happen for a few reasons:
5954
60-
• An in-flight effect emitted this action when child state was unavailable. While it may \
61-
be perfectly reasonable to ignore this action, consider canceling the associated effect \
62-
before child state changes to another case, especially if it is a long-living effect.
55+
• A parent reducer set "Result" to a different case before this reducer ran. This \
56+
reducer must run before any other reducer sets child state to a different case. This \
57+
ensures that child reducers can handle their actions while their state is still available.
6358
64-
• This action was sent to the store while state was another case. Make sure that actions \
65-
for this reducer can only be sent from a view store when state is set to the appropriate \
66-
case. In SwiftUI applications, use "SwitchStore".
67-
"""
68-
}
59+
• An in-flight effect emitted this action when child state was unavailable. While it may \
60+
be perfectly reasonable to ignore this action, consider canceling the associated effect \
61+
before child state changes to another case, especially if it is a long-living effect.
6962
70-
await store.send(.success(1))
63+
• This action was sent to the store while state was another case. Make sure that actions \
64+
for this reducer can only be sent from a view store when state is set to the appropriate \
65+
case. In SwiftUI applications, use "SwitchStore".
66+
"""
7167
}
72-
#endif
68+
69+
await store.send(.success(1))
70+
}
7371

7472
@MainActor
7573
func testEffectCancellation_Siblings() async {

Tests/ComposableArchitectureTests/Reducers/IfLetReducerTests.swift

Lines changed: 27 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -3,42 +3,40 @@ import XCTest
33

44
@available(*, deprecated, message: "TODO: Update to use case pathable syntax with Swift 5.9")
55
final class IfLetReducerTests: BaseTCATestCase {
6-
#if DEBUG
7-
@MainActor
8-
func testNilChild() async {
9-
let store = TestStore(initialState: Int?.none) {
10-
EmptyReducer<Int?, Void>()
11-
.ifLet(\.self, action: \.self) {}
12-
}
6+
@MainActor
7+
func testNilChild() async {
8+
let store = TestStore(initialState: Int?.none) {
9+
EmptyReducer<Int?, Void>()
10+
.ifLet(\.self, action: \.self) {}
11+
}
1312

14-
XCTExpectFailure {
15-
$0.compactDescription == """
16-
An "ifLet" at "\(#fileID):\(#line - 5)" received a child action when child state was \
17-
"nil". …
13+
XCTExpectFailure {
14+
$0.compactDescription == """
15+
An "ifLet" at "\(#fileID):\(#line - 5)" received a child action when child state was \
16+
"nil". …
1817
19-
Action:
20-
()
18+
Action:
19+
()
2120
22-
This is generally considered an application logic error, and can happen for a few \
23-
reasons:
21+
This is generally considered an application logic error, and can happen for a few \
22+
reasons:
2423
25-
• A parent reducer set child state to "nil" before this reducer ran. This reducer must \
26-
run before any other reducer sets child state to "nil". This ensures that child \
27-
reducers can handle their actions while their state is still available.
24+
• A parent reducer set child state to "nil" before this reducer ran. This reducer must \
25+
run before any other reducer sets child state to "nil". This ensures that child \
26+
reducers can handle their actions while their state is still available.
2827
29-
• An in-flight effect emitted this action when child state was "nil". While it may be \
30-
perfectly reasonable to ignore this action, consider canceling the associated effect \
31-
before child state becomes "nil", especially if it is a long-living effect.
28+
• An in-flight effect emitted this action when child state was "nil". While it may be \
29+
perfectly reasonable to ignore this action, consider canceling the associated effect \
30+
before child state becomes "nil", especially if it is a long-living effect.
3231
33-
• This action was sent to the store while state was "nil". Make sure that actions for \
34-
this reducer can only be sent from a view store when state is non-"nil". In SwiftUI \
35-
applications, use "IfLetStore".
36-
"""
37-
}
38-
39-
await store.send(())
32+
• This action was sent to the store while state was "nil". Make sure that actions for \
33+
this reducer can only be sent from a view store when state is non-"nil". In SwiftUI \
34+
applications, use "IfLetStore".
35+
"""
4036
}
41-
#endif
37+
38+
await store.send(())
39+
}
4240

4341
@MainActor
4442
func testEffectCancellation() async {

0 commit comments

Comments
 (0)