Skip to content

Commit 16f75ed

Browse files
committed
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 9b3b251 commit 16f75ed

File tree

2 files changed

+57
-42
lines changed

2 files changed

+57
-42
lines changed

.github/workflows/ci.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ jobs:
1515
matrix:
1616
xcode:
1717
- 11.7
18-
- 12.3
18+
- 12.4
1919
steps:
2020
- uses: actions/checkout@v2
2121
- name: Select Xcode ${{ matrix.xcode }}
@@ -29,7 +29,7 @@ jobs:
2929
# strategy:
3030
# matrix:
3131
# xcode:
32-
# - 12.3
32+
# - 12.4
3333
# steps:
3434
# - uses: actions/checkout@v2
3535
# - name: Select Xcode ${{ matrix.xcode }}

Sources/ComposableArchitecture/TestSupport/TestStore.swift

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

227227
let store = Store(
228228
initialState: self.state,
229229
reducer: Reducer<State, TestAction, Void> { state, action, _ in
230230
let effects: Effect<Action, Never>
231-
switch action {
231+
switch action.origin {
232232
case let .send(localAction):
233233
effects = self.reducer.run(&state, self.fromLocalAction(localAction), self.environment)
234234
snapshotState = state
@@ -238,25 +238,18 @@
238238
receivedActions.append((action, state))
239239
}
240240

241-
let key = debugCaseOutput(action)
242-
let id = UUID()
241+
let effect = LongLivingEffect(file: action.file, line: action.line)
243242
return
244243
effects.on(
245-
starting: { longLivingEffects[key, default: []].insert(id) },
246-
completed: { longLivingEffects[key]?.remove(id) },
247-
// terminated: { longLivingEffects[key]?.remove(id) },
248-
disposed: { longLivingEffects[key]?.remove(id) }
244+
starting: { longLivingEffects.insert(effect) },
245+
completed: { longLivingEffects.remove(effect) },
246+
disposed: { longLivingEffects.remove(effect) }
249247
)
250-
.map(TestAction.receive)
251-
248+
.map { .init(origin: .receive($0), file: action.file, line: action.line) }
252249
},
253250
environment: ()
254251
)
255-
256252
defer { self.state = store.$state.value }
257-
let viewStore = ViewStore(
258-
store.scope(state: self.toLocalState, action: TestAction.send)
259-
)
260253

261254
func assert(step: Step) {
262255
var expectedState = toLocalState(snapshotState)
@@ -299,7 +292,13 @@
299292
file: step.file, line: step.line
300293
)
301294
}
302-
viewStore.send(action)
295+
ViewStore(
296+
store.scope(
297+
state: self.toLocalState,
298+
action: { .init(origin: .send($0), file: step.file, line: step.line) }
299+
)
300+
)
301+
.send(action)
303302
do {
304303
try update(&expectedState)
305304
} catch {
@@ -402,33 +401,43 @@
402401
)
403402
}
404403

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

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

422-
• If you are using a scheduler in your effect, then make sure that you wait enough time \
423-
for the effect to finish. If you are using a test scheduler, then make sure you advance \
424-
the scheduler so that the effects complete.
435+
static func == (lhs: Self, rhs: Self) -> Bool {
436+
lhs.id == rhs.id
437+
}
425438

426-
• If you are using long-living effects (for example timers, notifications, etc.), then \
427-
ensure those effects are completed by returning an `Effect.cancel` effect from a \
428-
particular action in your reducer, and sending that action in the test.
429-
""",
430-
file: file, line: line
431-
)
439+
func hash(into hasher: inout Hasher) {
440+
self.id.hash(into: &hasher)
432441
}
433442
}
434443
}
@@ -579,9 +588,15 @@
579588
}
580589
}
581590

582-
private enum TestAction {
583-
case send(LocalAction)
584-
case receive(Action)
591+
private struct TestAction {
592+
let origin: Origin
593+
let file: StaticString
594+
let line: UInt
595+
596+
enum Origin {
597+
case send(LocalAction)
598+
case receive(Action)
599+
}
585600
}
586601
}
587602

0 commit comments

Comments
 (0)