Skip to content

Commit 2dce511

Browse files
committed
Use consistent naming between confirmAlwaysPasses and the related configuration trait
Stop unnecessarily waiting after the last polling attempt has finished. Allow for subsequent polling configuration traits which specified nil for a value to fall back to earlier polling configuration traits before falling back to the default.
1 parent 151aae8 commit 2dce511

File tree

3 files changed

+92
-46
lines changed

3 files changed

+92
-46
lines changed

Sources/Testing/Polling/Polling.swift

Lines changed: 25 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,13 @@ internal let defaultPollingConfiguration = (
2121
/// function.
2222
/// - maxPollingIterations: The maximum amount of times to attempt polling.
2323
/// If nil, this uses whatever value is specified under the last
24-
/// ``ConfirmPassesEventuallyConfigurationTrait`` added to the test or suite.
24+
/// ``ConfirmPassesEventuallyConfigurationTrait`` added to the test or
25+
/// suite.
2526
/// If no ``ConfirmPassesEventuallyConfigurationTrait`` has been added, then
2627
/// polling will be attempted 1000 times before recording an issue.
2728
/// `maxPollingIterations` must be greater than 0.
28-
/// - pollingInterval: The minimum amount of time to wait between polling attempts.
29+
/// - pollingInterval: The minimum amount of time to wait between polling
30+
/// attempts.
2931
/// If nil, this uses whatever value is specified under the last
3032
/// ``ConfirmPassesEventuallyConfigurationTrait`` added to the test or suite.
3133
/// If no ``ConfirmPassesEventuallyConfigurationTrait`` has been added, then
@@ -86,11 +88,13 @@ public struct PollingFailedError: Error {}
8688
/// function.
8789
/// - maxPollingIterations: The maximum amount of times to attempt polling.
8890
/// If nil, this uses whatever value is specified under the last
89-
/// ``ConfirmPassesEventuallyConfigurationTrait`` added to the test or suite.
91+
/// ``ConfirmPassesEventuallyConfigurationTrait`` added to the test or
92+
/// suite.
9093
/// If no ``ConfirmPassesEventuallyConfigurationTrait`` has been added, then
9194
/// polling will be attempted 1000 times before recording an issue.
9295
/// `maxPollingIterations` must be greater than 0.
93-
/// - pollingInterval: The minimum amount of time to wait between polling attempts.
96+
/// - pollingInterval: The minimum amount of time to wait between polling
97+
/// attempts.
9498
/// If nil, this uses whatever value is specified under the last
9599
/// ``ConfirmPassesEventuallyConfigurationTrait`` added to the test or suite.
96100
/// If no ``ConfirmPassesEventuallyConfigurationTrait`` has been added, then
@@ -158,14 +162,15 @@ public func confirmPassesEventually<R>(
158162
/// function.
159163
/// - maxPollingIterations: The maximum amount of times to attempt polling.
160164
/// If nil, this uses whatever value is specified under the last
161-
/// ``ConfirmPassesAlwaysConfigurationTrait`` added to the test or suite.
162-
/// If no ``ConfirmPassesAlwaysConfigurationTrait`` has been added, then
165+
/// ``ConfirmAlwaysPassesConfigurationTrait`` added to the test or suite.
166+
/// If no ``ConfirmAlwaysPassesConfigurationTrait`` has been added, then
163167
/// polling will be attempted 1000 times before recording an issue.
164168
/// `maxPollingIterations` must be greater than 0.
165-
/// - pollingInterval: The minimum amount of time to wait between polling attempts.
169+
/// - pollingInterval: The minimum amount of time to wait between polling
170+
/// attempts.
166171
/// If nil, this uses whatever value is specified under the last
167-
/// ``ConfirmPassesAlwaysConfigurationTrait`` added to the test or suite.
168-
/// If no ``ConfirmPassesAlwaysConfigurationTrait`` has been added, then
172+
/// ``ConfirmAlwaysPassesConfigurationTrait`` added to the test or suite.
173+
/// If no ``ConfirmAlwaysPassesConfigurationTrait`` has been added, then
169174
/// polling will wait at least 1 millisecond between polling attempts.
170175
/// `pollingInterval` must be greater than 0.
171176
/// - isolation: The actor to which `body` is isolated, if any.
@@ -191,12 +196,12 @@ public func confirmAlwaysPasses(
191196
pollingIterations: getValueFromPollingTrait(
192197
providedValue: maxPollingIterations,
193198
default: defaultPollingConfiguration.maxPollingIterations,
194-
\ConfirmPassesAlwaysConfigurationTrait.maxPollingIterations
199+
\ConfirmAlwaysPassesConfigurationTrait.maxPollingIterations
195200
),
196201
pollingInterval: getValueFromPollingTrait(
197202
providedValue: pollingInterval,
198203
default: defaultPollingConfiguration.pollingInterval,
199-
\ConfirmPassesAlwaysConfigurationTrait.pollingInterval
204+
\ConfirmAlwaysPassesConfigurationTrait.pollingInterval
200205
),
201206
comment: comment,
202207
sourceLocation: sourceLocation
@@ -228,16 +233,13 @@ public func confirmAlwaysPasses(
228233
private func getValueFromPollingTrait<TraitKind, Value>(
229234
providedValue: Value?,
230235
default: Value,
231-
_ keyPath: KeyPath<TraitKind, Value>
236+
_ keyPath: KeyPath<TraitKind, Value?>
232237
) -> Value {
233238
if let providedValue { return providedValue }
234239
guard let test = Test.current else { return `default` }
235-
guard let trait = test.traits.compactMap({ $0 as? TraitKind }).last else {
236-
print("No traits of type \(TraitKind.self) found. Returning default.")
237-
print("Traits: \(test.traits)")
238-
return `default`
239-
}
240-
return trait[keyPath: keyPath]
240+
let possibleTraits = test.traits.compactMap { $0 as? TraitKind }
241+
let traitValues = possibleTraits.compactMap { $0[keyPath: keyPath] }
242+
return traitValues.last ?? `default`
241243
}
242244

243245
/// A type to record the last value returned by a closure returning an optional
@@ -397,12 +399,16 @@ private struct Poller {
397399
isolation: isolated (any Actor)? = #isolation,
398400
expression: @escaping () async -> Bool
399401
) async -> PollResult {
400-
for _ in 0..<pollingIterations {
402+
for iteration in 0..<pollingIterations {
401403
if let result = await pollingBehavior.processFinishedExpression(
402404
expressionResult: expression()
403405
) {
404406
return result
405407
}
408+
if iteration == (pollingIterations - 1) {
409+
// don't bother sleeping if it's the last iteration.
410+
break
411+
}
406412
do {
407413
try await Task.sleep(for: pollingInterval)
408414
} catch {

Sources/Testing/Traits/PollingConfigurationTrait.swift

Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,36 +8,38 @@
88
/// A trait to provide a default polling configuration to all usages of
99
/// ``confirmPassesEventually`` within a test or suite.
1010
///
11-
/// To add this trait to a test, use the ``Trait/pollingConfirmationEventually``
11+
/// To add this trait to a test, use the
12+
/// ``Trait/confirmPassesEventuallyDefaults`` function.
1213
@_spi(Experimental)
1314
@available(macOS 13, iOS 17, watchOS 9, tvOS 17, visionOS 1, *)
1415
public struct ConfirmPassesEventuallyConfigurationTrait: TestTrait, SuiteTrait {
15-
public var maxPollingIterations: Int
16-
public var pollingInterval: Duration
16+
public var maxPollingIterations: Int?
17+
public var pollingInterval: Duration?
1718

1819
public var isRecursive: Bool { true }
1920

2021
public init(maxPollingIterations: Int?, pollingInterval: Duration?) {
21-
self.maxPollingIterations = maxPollingIterations ?? defaultPollingConfiguration.maxPollingIterations
22-
self.pollingInterval = pollingInterval ?? defaultPollingConfiguration.pollingInterval
22+
self.maxPollingIterations = maxPollingIterations
23+
self.pollingInterval = pollingInterval
2324
}
2425
}
2526

2627
/// A trait to provide a default polling configuration to all usages of
27-
/// ``confirmPassesAlways`` within a test or suite.
28+
/// ``confirmAlwaysPasses`` within a test or suite.
2829
///
29-
/// To add this trait to a test, use the ``Trait/pollingConfirmationAlways``
30+
/// To add this trait to a test, use the ``Trait/confirmAlwaysPassesDefaults``
31+
/// function.
3032
@_spi(Experimental)
3133
@available(macOS 13, iOS 17, watchOS 9, tvOS 17, visionOS 1, *)
32-
public struct ConfirmPassesAlwaysConfigurationTrait: TestTrait, SuiteTrait {
33-
public var maxPollingIterations: Int
34-
public var pollingInterval: Duration
34+
public struct ConfirmAlwaysPassesConfigurationTrait: TestTrait, SuiteTrait {
35+
public var maxPollingIterations: Int?
36+
public var pollingInterval: Duration?
3537

3638
public var isRecursive: Bool { true }
3739

3840
public init(maxPollingIterations: Int?, pollingInterval: Duration?) {
39-
self.maxPollingIterations = maxPollingIterations ?? defaultPollingConfiguration.maxPollingIterations
40-
self.pollingInterval = pollingInterval ?? defaultPollingConfiguration.pollingInterval
41+
self.maxPollingIterations = maxPollingIterations
42+
self.pollingInterval = pollingInterval
4143
}
4244
}
4345

@@ -52,7 +54,8 @@ extension Trait where Self == ConfirmPassesEventuallyConfigurationTrait {
5254
/// `maxPollingIterations` must be greater than 0.
5355
/// - pollingInterval: The minimum amount of time to wait between polling
5456
/// attempts.
55-
/// If nil, polling will wait at least 1 millisecond between polling attempts.
57+
/// If nil, polling will wait at least 1 millisecond between polling
58+
/// attempts.
5659
/// `pollingInterval` must be greater than 0.
5760
public static func confirmPassesEventuallyDefaults(
5861
maxPollingIterations: Int? = nil,
@@ -67,7 +70,7 @@ extension Trait where Self == ConfirmPassesEventuallyConfigurationTrait {
6770

6871
@_spi(Experimental)
6972
@available(macOS 13, iOS 17, watchOS 9, tvOS 17, visionOS 1, *)
70-
extension Trait where Self == ConfirmPassesAlwaysConfigurationTrait {
73+
extension Trait where Self == ConfirmAlwaysPassesConfigurationTrait {
7174
/// Specifies defaults for ``confirmPassesAlways`` in the test or suite.
7275
///
7376
/// - Parameters:
@@ -76,13 +79,14 @@ extension Trait where Self == ConfirmPassesAlwaysConfigurationTrait {
7679
/// `maxPollingIterations` must be greater than 0.
7780
/// - pollingInterval: The minimum amount of time to wait between polling
7881
/// attempts.
79-
/// If nil, polling will wait at least 1 millisecond between polling attempts.
82+
/// If nil, polling will wait at least 1 millisecond between polling
83+
/// attempts.
8084
/// `pollingInterval` must be greater than 0.
81-
public static func confirmPassesAlwaysDefaults(
85+
public static func confirmAlwaysPassesDefaults(
8286
maxPollingIterations: Int? = nil,
8387
pollingInterval: Duration? = nil
8488
) -> Self {
85-
ConfirmPassesAlwaysConfigurationTrait(
89+
ConfirmAlwaysPassesConfigurationTrait(
8690
maxPollingIterations: maxPollingIterations,
8791
pollingInterval: pollingInterval
8892
)

Tests/TestingTests/PollingTests.swift

Lines changed: 46 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ struct PollingTests {
179179

180180
@Suite(
181181
"Configuration traits",
182-
.confirmPassesAlwaysDefaults(maxPollingIterations: 100)
182+
.confirmAlwaysPassesDefaults(maxPollingIterations: 100)
183183
)
184184
struct WithConfigurationTraits {
185185
@Test("When no test or callsite configuration provided, uses the suite configuration")
@@ -194,7 +194,7 @@ struct PollingTests {
194194

195195
@Test(
196196
"When test configuration porvided, uses the test configuration",
197-
.confirmPassesAlwaysDefaults(maxPollingIterations: 10)
197+
.confirmAlwaysPassesDefaults(maxPollingIterations: 10)
198198
)
199199
func testUsesTestConfigurationOverSuiteConfiguration() async {
200200
let incrementor = Incrementor()
@@ -206,7 +206,7 @@ struct PollingTests {
206206

207207
@Test(
208208
"When callsite configuration provided, uses that",
209-
.confirmPassesAlwaysDefaults(maxPollingIterations: 10)
209+
.confirmAlwaysPassesDefaults(maxPollingIterations: 10)
210210
)
211211
func testUsesCallsiteConfiguration() async {
212212
let incrementor = Incrementor()
@@ -218,10 +218,11 @@ struct PollingTests {
218218
}
219219
}
220220

221-
@Suite("Duration Tests", .disabled("time-sensitive")) struct DurationTests {
221+
@Suite("Duration Tests", .disabled("time-sensitive"))
222+
struct DurationTests {
222223
@Suite("confirmPassesEventually")
223224
struct PassesOnceBehavior {
224-
let delta = Duration.seconds(6)
225+
let delta = Duration.milliseconds(100)
225226

226227
@Test("Simple passing expressions") func trivialHappyPath() async {
227228
let duration = await Test.Clock().measure {
@@ -237,10 +238,11 @@ struct PollingTests {
237238
}
238239
#expect(issues.count == 1)
239240
}
240-
#expect(duration.isCloseTo(other: .seconds(60), within: delta))
241+
#expect(duration.isCloseTo(other: .seconds(2), within: delta))
241242
}
242243

243-
@Test("When the value changes from false to true during execution") func changingFromFail() async {
244+
@Test("When the value changes from false to true during execution")
245+
func changingFromFail() async {
244246
let incrementor = Incrementor()
245247

246248
let duration = await Test.Clock().measure {
@@ -256,18 +258,36 @@ struct PollingTests {
256258
#expect(await incrementor.count == 2)
257259
#expect(duration.isCloseTo(other: .zero, within: delta))
258260
}
261+
262+
@Test("Doesn't wait after the last iteration")
263+
func lastIteration() async {
264+
let duration = await Test.Clock().measure {
265+
let issues = await runTest {
266+
await confirmPassesEventually(
267+
maxPollingIterations: 10,
268+
pollingInterval: .seconds(1) // Wait a long time to handle jitter.
269+
) { false }
270+
}
271+
#expect(issues.count == 1)
272+
}
273+
#expect(
274+
duration.isCloseTo(
275+
other: .seconds(9),
276+
within: .milliseconds(500)
277+
)
278+
)
279+
}
259280
}
260281

261282
@Suite("confirmAlwaysPasses")
262283
struct PassesAlwaysBehavior {
263-
// use a very generous delta for CI reasons.
264-
let delta = Duration.seconds(6)
284+
let delta = Duration.milliseconds(100)
265285

266286
@Test("Simple passing expressions") func trivialHappyPath() async {
267287
let duration = await Test.Clock().measure {
268288
await confirmAlwaysPasses { true }
269289
}
270-
#expect(duration.isCloseTo(other: .seconds(60), within: delta))
290+
#expect(duration.isCloseTo(other: .seconds(1), within: delta))
271291
}
272292

273293
@Test("Simple failing expressions") func trivialSadPath() async {
@@ -279,6 +299,22 @@ struct PollingTests {
279299
}
280300
#expect(duration.isCloseTo(other: .zero, within: delta))
281301
}
302+
303+
@Test("Doesn't wait after the last iteration")
304+
func lastIteration() async {
305+
let duration = await Test.Clock().measure {
306+
await confirmAlwaysPasses(
307+
maxPollingIterations: 10,
308+
pollingInterval: .seconds(1) // Wait a long time to handle jitter.
309+
) { true }
310+
}
311+
#expect(
312+
duration.isCloseTo(
313+
other: .seconds(9),
314+
within: .milliseconds(500)
315+
)
316+
)
317+
}
282318
}
283319
}
284320
}

0 commit comments

Comments
 (0)