Skip to content

Commit e935270

Browse files
authored
Tie long-living effect assertions to line of originating action (#413)
* Tie long-living effect assertions to line of originating action * Fix * fix * More CI
1 parent f8608c7 commit e935270

File tree

2 files changed

+57
-40
lines changed

2 files changed

+57
-40
lines changed

.github/workflows/ci.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ jobs:
1616
xcode:
1717
- 11.3
1818
- 11.7
19+
- 12.4
1920
steps:
2021
- uses: actions/checkout@v2
2122
- name: Select Xcode ${{ matrix.xcode }}
@@ -29,7 +30,7 @@ jobs:
2930
# strategy:
3031
# matrix:
3132
# xcode:
32-
# - 12.3
33+
# - 12.4
3334
# steps:
3435
# - uses: actions/checkout@v2
3536
# - name: Select Xcode ${{ matrix.xcode }}

Sources/ComposableArchitecture/TestSupport/TestStore.swift

Lines changed: 55 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -222,14 +222,14 @@
222222
line: UInt = #line
223223
) {
224224
var receivedActions: [(action: Action, state: State)] = []
225-
var longLivingEffects: [String: Set<UUID>] = [:]
225+
var longLivingEffects: Set<LongLivingEffect> = []
226226
var snapshotState = self.state
227227

228228
let store = Store(
229229
initialState: self.state,
230230
reducer: Reducer<State, TestAction, Void> { state, action, _ in
231231
let effects: Effect<Action, Never>
232-
switch action {
232+
switch action.origin {
233233
case let .send(localAction):
234234
effects = self.reducer.run(&state, self.fromLocalAction(localAction), self.environment)
235235
snapshotState = state
@@ -239,27 +239,21 @@
239239
receivedActions.append((action, state))
240240
}
241241

242-
let key = debugCaseOutput(action)
243-
let id = UUID()
242+
let effect = LongLivingEffect(file: action.file, line: action.line)
244243
return
245244
effects
246245
.handleEvents(
247-
receiveSubscription: { _ in longLivingEffects[key, default: []].insert(id) },
248-
receiveCompletion: { _ in longLivingEffects[key]?.remove(id) },
249-
receiveCancel: { longLivingEffects[key]?.remove(id) }
246+
receiveSubscription: { _ in longLivingEffects.insert(effect) },
247+
receiveCompletion: { _ in longLivingEffects.remove(effect) },
248+
receiveCancel: { longLivingEffects.remove(effect) }
250249
)
251-
.map(TestAction.receive)
250+
.map { .init(origin: .receive($0), file: action.file, line: action.line) }
252251
.eraseToEffect()
253-
254252
},
255253
environment: ()
256254
)
257255
defer { self.state = store.state.value }
258256

259-
let viewStore = ViewStore(
260-
store.scope(state: self.toLocalState, action: TestAction.send)
261-
)
262-
263257
func assert(step: Step) {
264258
var expectedState = toLocalState(snapshotState)
265259

@@ -301,7 +295,13 @@
301295
file: step.file, line: step.line
302296
)
303297
}
304-
viewStore.send(action)
298+
ViewStore(
299+
store.scope(
300+
state: self.toLocalState,
301+
action: { .init(origin: .send($0), file: step.file, line: step.line) }
302+
)
303+
)
304+
.send(action)
305305
do {
306306
try update(&expectedState)
307307
} catch {
@@ -404,33 +404,43 @@
404404
)
405405
}
406406

407-
let unfinishedActions = longLivingEffects.filter { !$0.value.isEmpty }.map { $0.key }
408-
if unfinishedActions.count > 0 {
409-
let initiatingActions = unfinishedActions.map { "\($0)" }.joined(separator: "\n")
410-
let pluralSuffix = unfinishedActions.count == 1 ? "" : "s"
411-
407+
for effect in longLivingEffects {
412408
_XCTFail(
413409
"""
414-
Some effects are still running. All effects must complete by the end of the assertion.
415-
416-
The effects that are still running were started by the following action\(pluralSuffix):
417-
418-
\(initiatingActions)
410+
An effect returned for this action is still running. It must complete before the end of \
411+
the assertion. …
412+
413+
To fix, inspect any effects the reducer returns for this action and ensure that all of \
414+
them complete by the end of the test. There are a few reasons why an effect may not have \
415+
completed:
416+
417+
• If an effect uses a scheduler (via "receive(on:)", "delay", "debounce", etc.), make \
418+
sure that you wait enough time for the scheduler to perform the effect. If you are using \
419+
a test scheduler, advance the scheduler so that the effects may complete, or consider \
420+
using an immediate scheduler to immediately perform the effect instead.
421+
422+
• If you are returning a long-living effect (timers, notifications, subjects, etc.), \
423+
then make sure those effects are torn down by marking the effect ".cancellable" and \
424+
returning a corresponding cancellation effect ("Effect.cancel") from another action, or, \
425+
if your effect is driven by a Combine subject, send it a completion.
426+
""",
427+
file: effect.file,
428+
line: effect.line
429+
)
430+
}
431+
}
419432

420-
To fix you need to inspect the effects returned from the above action\(pluralSuffix) and \
421-
make sure that all of them are completed by the end of your assertion. There are a few \
422-
reasons why your effects may not have completed:
433+
private struct LongLivingEffect: Hashable {
434+
let id = UUID()
435+
let file: StaticString
436+
let line: UInt
423437

424-
• If you are using a scheduler in your effect, then make sure that you wait enough time \
425-
for the effect to finish. If you are using a test scheduler, then make sure you advance \
426-
the scheduler so that the effects complete.
438+
static func == (lhs: Self, rhs: Self) -> Bool {
439+
lhs.id == rhs.id
440+
}
427441

428-
• If you are using long-living effects (for example timers, notifications, etc.), then \
429-
ensure those effects are completed by returning an `Effect.cancel` effect from a \
430-
particular action in your reducer, and sending that action in the test.
431-
""",
432-
file: file, line: line
433-
)
442+
func hash(into hasher: inout Hasher) {
443+
self.id.hash(into: &hasher)
434444
}
435445
}
436446
}
@@ -581,9 +591,15 @@
581591
}
582592
}
583593

584-
private enum TestAction {
585-
case send(LocalAction)
586-
case receive(Action)
594+
private struct TestAction {
595+
let origin: Origin
596+
let file: StaticString
597+
let line: UInt
598+
599+
enum Origin {
600+
case send(LocalAction)
601+
case receive(Action)
602+
}
587603
}
588604
}
589605

0 commit comments

Comments
 (0)