Skip to content

Commit 5f5c59e

Browse files
mbrandonwmluisbrown
authored andcommitted
Fix a few flakey tests (#1344)
* Try fixing some flakey tests. * store finish * wip * wip * wip * wip * wip * try out a timeout * wip * Update Package.swift Co-authored-by: Stephen Celis <[email protected]> (cherry picked from commit eb43df7d09525a6cd5b6f63577f387447a35144e) # Conflicts: # Package.swift # Sources/ComposableArchitecture/TestStore.swift # Tests/ComposableArchitectureTests/EffectCancellationTests.swift
1 parent 7e8cf39 commit 5f5c59e

File tree

4 files changed

+173
-158
lines changed

4 files changed

+173
-158
lines changed

Examples/TicTacToe/tic-tac-toe/Tests/TwoFactorSwiftUITests/TwoFactorSwiftUITests.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,5 +85,7 @@ final class TwoFactorSwiftUITests: XCTestCase {
8585
await store.send(.alertDismissed) {
8686
$0.alert = nil
8787
}
88+
89+
await store.finish()
8890
}
8991
}

Sources/ComposableArchitecture/TestStore.swift

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,7 @@
212212
/// ``finish(timeout:file:line:)-53gi5``.
213213
public var timeout: UInt64
214214

215+
private let effectDidSubscribe = AsyncStream<Void>.streamWithContinuation()
215216
private let file: StaticString
216217
private let fromScopedAction: (ScopedAction) -> Action
217218
private var line: UInt
@@ -237,7 +238,7 @@
237238
self.reducer = reducer
238239
self.state = initialState
239240
self.toScopedState = toScopedState
240-
self.timeout = 100 * NSEC_PER_MSEC
241+
self.timeout = NSEC_PER_SEC
241242

242243
self.store = Store(
243244
initialState: initialState,
@@ -259,7 +260,10 @@
259260
effects
260261
.producer
261262
.on(
262-
starting: { [weak self] in self?.inFlightEffects.insert(effect) },
263+
starting: { [weak self] in
264+
self?.inFlightEffects.insert(effect)
265+
self?.effectDidSubscribe.continuation.yield()
266+
},
263267
completed: { [weak self] in self?.inFlightEffects.remove(effect) },
264268
disposed: { [weak self] in self?.inFlightEffects.remove(effect) }
265269
)
@@ -279,11 +283,11 @@
279283
@available(iOS 16, macOS 13, tvOS 16, watchOS 9, *)
280284
@MainActor
281285
public func finish(
282-
timeout duration: Duration,
286+
timeout duration: Duration? = nil,
283287
file: StaticString = #file,
284288
line: UInt = #line
285289
) async {
286-
await self.finish(timeout: duration.nanoseconds, file: file, line: line)
290+
await self.finish(timeout: duration?.nanoseconds, file: file, line: line)
287291
}
288292
#endif
289293

@@ -292,6 +296,7 @@
292296
/// Can be used to assert that all effects have finished.
293297
///
294298
/// - Parameter nanoseconds: The amount of time to wait before asserting.
299+
@_disfavoredOverload
295300
@MainActor
296301
public func finish(
297302
timeout nanoseconds: UInt64? = nil,
@@ -530,7 +535,7 @@
530535
var expectedState = self.toScopedState(self.state)
531536
let previousState = self.state
532537
let task = self.store.send(.init(origin: .send(action), file: file, line: line))
533-
await Task.megaYield()
538+
await self.effectDidSubscribe.stream.first(where: { _ in true })
534539
do {
535540
let currentState = self.state
536541
self.state = previousState
@@ -941,22 +946,23 @@
941946
}
942947

943948
#if swift(>=5.7)
944-
@available(iOS 16, macOS 13, tvOS 16, watchOS 9, *)
945949
/// Asserts the underlying task finished.
946950
///
947951
/// - Parameter duration: The amount of time to wait before asserting.
952+
@available(iOS 16, macOS 13, tvOS 16, watchOS 9, *)
948953
public func finish(
949-
timeout duration: Duration,
954+
timeout duration: Duration? = nil,
950955
file: StaticString = #file,
951956
line: UInt = #line
952957
) async {
953-
await self.finish(timeout: duration.nanoseconds, file: file, line: line)
958+
await self.finish(timeout: duration?.nanoseconds, file: file, line: line)
954959
}
955960
#endif
956961

957962
/// Asserts the underlying task finished.
958963
///
959964
/// - Parameter nanoseconds: The amount of time to wait before asserting.
965+
@_disfavoredOverload
960966
public func finish(
961967
timeout nanoseconds: UInt64? = nil,
962968
file: StaticString = #file,
@@ -1012,7 +1018,7 @@
10121018
}
10131019

10141020
extension Task where Success == Never, Failure == Never {
1015-
static func megaYield(count: Int = 3) async {
1021+
static func megaYield(count: Int = 6) async {
10161022
for _ in 1...count {
10171023
await Task<Void, Never>.detached(priority: .low) { await Task.yield() }.value
10181024
}

Tests/ComposableArchitectureTests/ComposableArchitectureTests.swift

Lines changed: 126 additions & 125 deletions
Original file line numberDiff line numberDiff line change
@@ -4,155 +4,156 @@ import XCTest
44

55
// `@MainActor` introduces issues gathering tests on Linux
66
#if !os(Linux)
7-
@MainActor
8-
final class ComposableArchitectureTests: XCTestCase {
9-
func testScheduling() async {
10-
enum CounterAction: Equatable {
11-
case incrAndSquareLater
12-
case incrNow
13-
case squareNow
14-
}
7+
@MainActor
8+
final class ComposableArchitectureTests: XCTestCase {
9+
func testScheduling() async {
10+
enum CounterAction: Equatable {
11+
case incrAndSquareLater
12+
case incrNow
13+
case squareNow
14+
}
1515

16-
let counterReducer = Reducer<Int, CounterAction, DateScheduler> {
17-
state, action, scheduler in
18-
switch action {
19-
case .incrAndSquareLater:
20-
return .merge(
21-
Effect(value: .incrNow).deferred(for: 2, scheduler: scheduler),
22-
Effect(value: .squareNow).deferred(for: 1, scheduler: scheduler),
23-
Effect(value: .squareNow).deferred(for: 2, scheduler: scheduler)
24-
)
25-
case .incrNow:
26-
state += 1
27-
return .none
28-
case .squareNow:
29-
state *= state
30-
return .none
31-
}
16+
let counterReducer = Reducer<Int, CounterAction, DateScheduler> {
17+
state, action, scheduler in
18+
switch action {
19+
case .incrAndSquareLater:
20+
return .merge(
21+
Effect(value: .incrNow).deferred(for: 2, scheduler: scheduler),
22+
Effect(value: .squareNow).deferred(for: 1, scheduler: scheduler),
23+
Effect(value: .squareNow).deferred(for: 2, scheduler: scheduler)
24+
)
25+
case .incrNow:
26+
state += 1
27+
return .none
28+
case .squareNow:
29+
state *= state
30+
return .none
3231
}
33-
34-
let mainQueue = TestScheduler()
35-
36-
let store = TestStore(
37-
initialState: 2,
38-
reducer: counterReducer,
39-
environment: mainQueue
40-
)
41-
42-
await store.send(.incrAndSquareLater)
43-
await mainQueue.advance(by: 1)
44-
await store.receive(.squareNow) { $0 = 4 }
45-
await mainQueue.advance(by: 1)
46-
await store.receive(.incrNow) { $0 = 5 }
47-
await store.receive(.squareNow) { $0 = 25 }
48-
49-
await store.send(.incrAndSquareLater)
50-
await mainQueue.advance(by: 2)
51-
await store.receive(.squareNow) { $0 = 625 }
52-
await store.receive(.incrNow) { $0 = 626 }
53-
await store.receive(.squareNow) { $0 = 391876 }
5432
}
5533

56-
func testSimultaneousWorkOrdering() {
57-
let testScheduler = TestScheduler()
34+
let mainQueue = TestScheduler()
35+
36+
let store = TestStore(
37+
initialState: 2,
38+
reducer: counterReducer,
39+
environment: mainQueue
40+
)
41+
42+
await store.send(.incrAndSquareLater)
43+
await mainQueue.advance(by: 1)
44+
await store.receive(.squareNow) { $0 = 4 }
45+
await mainQueue.advance(by: 1)
46+
await store.receive(.incrNow) { $0 = 5 }
47+
await store.receive(.squareNow) { $0 = 25 }
48+
49+
await store.send(.incrAndSquareLater)
50+
await mainQueue.advance(by: 2)
51+
await store.receive(.squareNow) { $0 = 625 }
52+
await store.receive(.incrNow) { $0 = 626 }
53+
await store.receive(.squareNow) { $0 = 391876 }
54+
}
5855

59-
var values: [Int] = []
60-
testScheduler.schedule(after: .seconds(0), interval: .seconds(1)) { values.append(1) }
61-
testScheduler.schedule(after: .seconds(0), interval: .seconds(2)) { values.append(42) }
56+
func testSimultaneousWorkOrdering() {
57+
let testScheduler = TestScheduler()
6258

63-
XCTAssertNoDifference(values, [])
64-
testScheduler.advance()
65-
XCTAssertNoDifference(values, [1, 42])
66-
testScheduler.advance(by: 2)
67-
XCTAssertNoDifference(values, [1, 42, 1, 42, 1])
68-
}
59+
var values: [Int] = []
60+
testScheduler.schedule(after: .seconds(0), interval: .seconds(1)) { values.append(1) }
61+
testScheduler.schedule(after: .seconds(0), interval: .seconds(2)) { values.append(42) }
6962

70-
func testLongLivingEffects() async {
71-
typealias Environment = (
72-
startEffect: Effect<Void, Never>,
73-
stopEffect: Effect<Never, Never>
74-
)
63+
XCTAssertNoDifference(values, [])
64+
testScheduler.advance()
65+
XCTAssertNoDifference(values, [1, 42])
66+
testScheduler.advance(by: 2)
67+
XCTAssertNoDifference(values, [1, 42, 1, 42, 1])
68+
}
7569

76-
enum Action { case end, incr, start }
77-
78-
let reducer = Reducer<Int, Action, Environment> { state, action, environment in
79-
switch action {
80-
case .end:
81-
return environment.stopEffect.fireAndForget()
82-
case .incr:
83-
state += 1
84-
return .none
85-
case .start:
86-
return environment.startEffect.map { Action.incr }
87-
}
70+
func testLongLivingEffects() async {
71+
typealias Environment = (
72+
startEffect: Effect<Void, Never>,
73+
stopEffect: Effect<Never, Never>
74+
)
75+
76+
enum Action { case end, incr, start }
77+
78+
let reducer = Reducer<Int, Action, Environment> { state, action, environment in
79+
switch action {
80+
case .end:
81+
return environment.stopEffect.fireAndForget()
82+
case .incr:
83+
state += 1
84+
return .none
85+
case .start:
86+
return environment.startEffect.map { Action.incr }
8887
}
88+
}
8989

90-
let subject = Signal<Void, Never>.pipe()
90+
let subject = Signal<Void, Never>.pipe()
9191

92-
let store = TestStore(
93-
initialState: 0,
94-
reducer: reducer,
95-
environment: (
96-
startEffect: subject.output.producer.eraseToEffect(),
97-
stopEffect: .fireAndForget { subject.input.sendCompleted() }
98-
)
92+
let store = TestStore(
93+
initialState: 0,
94+
reducer: reducer,
95+
environment: (
96+
startEffect: subject.output.producer.eraseToEffect(),
97+
stopEffect: .fireAndForget { subject.input.sendCompleted() }
9998
)
99+
)
100100

101-
await store.send(.start)
102-
await store.send(.incr) { $0 = 1 }
103-
subject.input.send(value: ())
104-
await store.receive(.incr) { $0 = 2 }
105-
await store.send(.end)
106-
}
107-
108-
func testCancellation() async {
109-
let mainQueue = TestScheduler()
101+
await store.send(.start)
102+
await store.send(.incr) { $0 = 1 }
103+
subject.input.send(value: ())
104+
await store.receive(.incr) { $0 = 2 }
105+
await store.send(.end)
106+
}
110107

111-
enum Action: Equatable {
112-
case cancel
113-
case incr
114-
case response(Int)
115-
}
108+
func testCancellation() async {
109+
let mainQueue = TestScheduler()
116110

117-
struct Environment {
118-
let fetch: (Int) async -> Int
119-
}
111+
enum Action: Equatable {
112+
case cancel
113+
case incr
114+
case response(Int)
115+
}
120116

121-
let reducer = Reducer<Int, Action, Environment> { state, action, environment in
122-
enum CancelID {}
117+
struct Environment {
118+
let fetch: (Int) async -> Int
119+
}
123120

124-
switch action {
125-
case .cancel:
126-
return .cancel(id: CancelID.self)
121+
let reducer = Reducer<Int, Action, Environment> { state, action, environment in
122+
enum CancelID {}
127123

128-
case .incr:
129-
state += 1
130-
return .task { [state] in
131-
try await mainQueue.sleep(for: .seconds(1))
132-
return .response(await environment.fetch(state))
133-
}
134-
.cancellable(id: CancelID.self)
124+
switch action {
125+
case .cancel:
126+
return .cancel(id: CancelID.self)
135127

136-
case let .response(value):
137-
state = value
138-
return .none
128+
case .incr:
129+
state += 1
130+
return .task { [state] in
131+
try await mainQueue.sleep(for: .seconds(1))
132+
return .response(await environment.fetch(state))
139133
}
134+
.cancellable(id: CancelID.self)
135+
136+
case let .response(value):
137+
state = value
138+
return .none
140139
}
140+
}
141141

142-
let store = TestStore(
143-
initialState: 0,
144-
reducer: reducer,
145-
environment: Environment(
146-
fetch: { value in value * value }
147-
)
142+
let store = TestStore(
143+
initialState: 0,
144+
reducer: reducer,
145+
environment: Environment(
146+
fetch: { value in value * value }
148147
)
148+
)
149149

150-
await store.send(.incr) { $0 = 1 }
151-
await mainQueue.advance(by: .seconds(1))
152-
await store.receive(.response(1))
150+
await store.send(.incr) { $0 = 1 }
151+
await mainQueue.advance(by: .seconds(1))
152+
await store.receive(.response(1))
153153

154-
await store.send(.incr) { $0 = 2 }
155-
await store.send(.cancel)
156-
}
154+
await store.send(.incr) { $0 = 2 }
155+
await store.send(.cancel)
156+
await store.finish()
157157
}
158+
}
158159
#endif

0 commit comments

Comments
 (0)