Skip to content

Commit 5b7d798

Browse files
committed
Configure polling confirmations as timeout & polling interval
This is less direct, but much more intuitive for test authors. Also add exit tests confirming that these values are non-negative
1 parent 4bd6141 commit 5b7d798

File tree

3 files changed

+161
-77
lines changed

3 files changed

+161
-77
lines changed

Sources/Testing/Polling/Polling.swift

Lines changed: 58 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
@available(macOS 13, iOS 17, watchOS 9, tvOS 17, visionOS 1, *)
1212
internal let defaultPollingConfiguration = (
13-
maxPollingIterations: 1000,
13+
pollingDuration: Duration.seconds(1),
1414
pollingInterval: Duration.milliseconds(1)
1515
)
1616

@@ -23,12 +23,14 @@ public struct PollingFailedError: Error, Equatable {}
2323
/// - Parameters:
2424
/// - comment: An optional comment to apply to any issues generated by this
2525
/// function.
26-
/// - maxPollingIterations: The maximum amount of times to attempt polling.
26+
/// - pollingDuration: The expected length of time to continue polling for.
27+
/// This value may not correspond to the wall-clock time that polling lasts for, especially
28+
/// on highly-loaded systems with a lot of tests running.
2729
/// If nil, this uses whatever value is specified under the last
2830
/// ``ConfirmPassesEventuallyConfigurationTrait`` added to the test or
2931
/// suite.
3032
/// If no ``ConfirmPassesEventuallyConfigurationTrait`` has been added, then
31-
/// polling will be attempted 1000 times before recording an issue.
33+
/// polling will be attempted for about 1 second before recording an issue.
3234
/// `maxPollingIterations` must be greater than 0.
3335
/// - pollingInterval: The minimum amount of time to wait between polling
3436
/// attempts.
@@ -50,18 +52,18 @@ public struct PollingFailedError: Error, Equatable {}
5052
@available(macOS 13, iOS 17, watchOS 9, tvOS 17, visionOS 1, *)
5153
public func confirmPassesEventually(
5254
_ comment: Comment? = nil,
53-
maxPollingIterations: Int? = nil,
55+
pollingDuration: Duration? = nil,
5456
pollingInterval: Duration? = nil,
5557
isolation: isolated (any Actor)? = #isolation,
5658
sourceLocation: SourceLocation = #_sourceLocation,
5759
_ body: @escaping () async throws -> Bool
5860
) async {
5961
let poller = Poller(
6062
pollingBehavior: .passesOnce,
61-
pollingIterations: getValueFromPollingTrait(
62-
providedValue: maxPollingIterations,
63-
default: defaultPollingConfiguration.maxPollingIterations,
64-
\ConfirmPassesEventuallyConfigurationTrait.maxPollingIterations
63+
pollingDuration: getValueFromPollingTrait(
64+
providedValue: pollingDuration,
65+
default: defaultPollingConfiguration.pollingDuration,
66+
\ConfirmPassesEventuallyConfigurationTrait.pollingDuration
6567
),
6668
pollingInterval: getValueFromPollingTrait(
6769
providedValue: pollingInterval,
@@ -85,12 +87,14 @@ public func confirmPassesEventually(
8587
/// - Parameters:
8688
/// - comment: An optional comment to apply to any issues generated by this
8789
/// function.
88-
/// - maxPollingIterations: The maximum amount of times to attempt polling.
90+
/// - pollingDuration: The expected length of time to continue polling for.
91+
/// This value may not correspond to the wall-clock time that polling lasts for, especially
92+
/// on highly-loaded systems with a lot of tests running.
8993
/// If nil, this uses whatever value is specified under the last
9094
/// ``ConfirmPassesEventuallyConfigurationTrait`` added to the test or
9195
/// suite.
9296
/// If no ``ConfirmPassesEventuallyConfigurationTrait`` has been added, then
93-
/// polling will be attempted 1000 times before recording an issue.
97+
/// polling will be attempted for about 1 second before recording an issue.
9498
/// `maxPollingIterations` must be greater than 0.
9599
/// - pollingInterval: The minimum amount of time to wait between polling
96100
/// attempts.
@@ -115,18 +119,18 @@ public func confirmPassesEventually(
115119
@available(macOS 13, iOS 17, watchOS 9, tvOS 17, visionOS 1, *)
116120
public func requirePassesEventually(
117121
_ comment: Comment? = nil,
118-
maxPollingIterations: Int? = nil,
122+
pollingDuration: Duration? = nil,
119123
pollingInterval: Duration? = nil,
120124
isolation: isolated (any Actor)? = #isolation,
121125
sourceLocation: SourceLocation = #_sourceLocation,
122126
_ body: @escaping () async throws -> Bool
123127
) async throws {
124128
let poller = Poller(
125129
pollingBehavior: .passesOnce,
126-
pollingIterations: getValueFromPollingTrait(
127-
providedValue: maxPollingIterations,
128-
default: defaultPollingConfiguration.maxPollingIterations,
129-
\ConfirmPassesEventuallyConfigurationTrait.maxPollingIterations
130+
pollingDuration: getValueFromPollingTrait(
131+
providedValue: pollingDuration,
132+
default: defaultPollingConfiguration.pollingDuration,
133+
\ConfirmPassesEventuallyConfigurationTrait.pollingDuration
130134
),
131135
pollingInterval: getValueFromPollingTrait(
132136
providedValue: pollingInterval,
@@ -153,12 +157,14 @@ public func requirePassesEventually(
153157
/// - Parameters:
154158
/// - comment: An optional comment to apply to any issues generated by this
155159
/// function.
156-
/// - maxPollingIterations: The maximum amount of times to attempt polling.
160+
/// - pollingDuration: The expected length of time to continue polling for.
161+
/// This value may not correspond to the wall-clock time that polling lasts for, especially
162+
/// on highly-loaded systems with a lot of tests running.
157163
/// If nil, this uses whatever value is specified under the last
158164
/// ``ConfirmPassesEventuallyConfigurationTrait`` added to the test or
159165
/// suite.
160166
/// If no ``ConfirmPassesEventuallyConfigurationTrait`` has been added, then
161-
/// polling will be attempted 1000 times before recording an issue.
167+
/// polling will be attempted for about 1 second before recording an issue.
162168
/// `maxPollingIterations` must be greater than 0.
163169
/// - pollingInterval: The minimum amount of time to wait between polling
164170
/// attempts.
@@ -186,18 +192,18 @@ public func requirePassesEventually(
186192
@discardableResult
187193
public func confirmPassesEventually<R>(
188194
_ comment: Comment? = nil,
189-
maxPollingIterations: Int? = nil,
195+
pollingDuration: Duration? = nil,
190196
pollingInterval: Duration? = nil,
191197
isolation: isolated (any Actor)? = #isolation,
192198
sourceLocation: SourceLocation = #_sourceLocation,
193199
_ body: @escaping () async throws -> sending R?
194200
) async throws -> R {
195201
let poller = Poller(
196202
pollingBehavior: .passesOnce,
197-
pollingIterations: getValueFromPollingTrait(
198-
providedValue: maxPollingIterations,
199-
default: defaultPollingConfiguration.maxPollingIterations,
200-
\ConfirmPassesEventuallyConfigurationTrait.maxPollingIterations
203+
pollingDuration: getValueFromPollingTrait(
204+
providedValue: pollingDuration,
205+
default: defaultPollingConfiguration.pollingDuration,
206+
\ConfirmPassesEventuallyConfigurationTrait.pollingDuration
201207
),
202208
pollingInterval: getValueFromPollingTrait(
203209
providedValue: pollingInterval,
@@ -226,11 +232,13 @@ public func confirmPassesEventually<R>(
226232
/// - Parameters:
227233
/// - comment: An optional comment to apply to any issues generated by this
228234
/// function.
229-
/// - maxPollingIterations: The maximum amount of times to attempt polling.
235+
/// - pollingDuration: The expected length of time to continue polling for.
236+
/// This value may not correspond to the wall-clock time that polling lasts for, especially
237+
/// on highly-loaded systems with a lot of tests running.
230238
/// If nil, this uses whatever value is specified under the last
231239
/// ``ConfirmAlwaysPassesConfigurationTrait`` added to the test or suite.
232240
/// If no ``ConfirmAlwaysPassesConfigurationTrait`` has been added, then
233-
/// polling will be attempted 1000 times before recording an issue.
241+
/// polling will be attempted for about 1 second before recording an issue.
234242
/// `maxPollingIterations` must be greater than 0.
235243
/// - pollingInterval: The minimum amount of time to wait between polling
236244
/// attempts.
@@ -251,18 +259,18 @@ public func confirmPassesEventually<R>(
251259
@available(macOS 13, iOS 17, watchOS 9, tvOS 17, visionOS 1, *)
252260
public func confirmAlwaysPasses(
253261
_ comment: Comment? = nil,
254-
maxPollingIterations: Int? = nil,
262+
pollingDuration: Duration? = nil,
255263
pollingInterval: Duration? = nil,
256264
isolation: isolated (any Actor)? = #isolation,
257265
sourceLocation: SourceLocation = #_sourceLocation,
258266
_ body: @escaping () async throws -> Bool
259267
) async {
260268
let poller = Poller(
261269
pollingBehavior: .passesAlways,
262-
pollingIterations: getValueFromPollingTrait(
263-
providedValue: maxPollingIterations,
264-
default: defaultPollingConfiguration.maxPollingIterations,
265-
\ConfirmAlwaysPassesConfigurationTrait.maxPollingIterations
270+
pollingDuration: getValueFromPollingTrait(
271+
providedValue: pollingDuration,
272+
default: defaultPollingConfiguration.pollingDuration,
273+
\ConfirmAlwaysPassesConfigurationTrait.pollingDuration
266274
),
267275
pollingInterval: getValueFromPollingTrait(
268276
providedValue: pollingInterval,
@@ -286,11 +294,13 @@ public func confirmAlwaysPasses(
286294
/// - Parameters:
287295
/// - comment: An optional comment to apply to any issues generated by this
288296
/// function.
289-
/// - maxPollingIterations: The maximum amount of times to attempt polling.
297+
/// - pollingDuration: The expected length of time to continue polling for.
298+
/// This value may not correspond to the wall-clock time that polling lasts for, especially
299+
/// on highly-loaded systems with a lot of tests running.
290300
/// If nil, this uses whatever value is specified under the last
291301
/// ``ConfirmAlwaysPassesConfigurationTrait`` added to the test or suite.
292302
/// If no ``ConfirmAlwaysPassesConfigurationTrait`` has been added, then
293-
/// polling will be attempted 1000 times before recording an issue.
303+
/// polling will be attempted for about 1 second before recording an issue.
294304
/// `maxPollingIterations` must be greater than 0.
295305
/// - pollingInterval: The minimum amount of time to wait between polling
296306
/// attempts.
@@ -314,18 +324,18 @@ public func confirmAlwaysPasses(
314324
@available(macOS 13, iOS 17, watchOS 9, tvOS 17, visionOS 1, *)
315325
public func requireAlwaysPasses(
316326
_ comment: Comment? = nil,
317-
maxPollingIterations: Int? = nil,
327+
pollingDuration: Duration? = nil,
318328
pollingInterval: Duration? = nil,
319329
isolation: isolated (any Actor)? = #isolation,
320330
sourceLocation: SourceLocation = #_sourceLocation,
321331
_ body: @escaping () async throws -> Bool
322332
) async throws {
323333
let poller = Poller(
324334
pollingBehavior: .passesAlways,
325-
pollingIterations: getValueFromPollingTrait(
326-
providedValue: maxPollingIterations,
327-
default: defaultPollingConfiguration.maxPollingIterations,
328-
\ConfirmAlwaysPassesConfigurationTrait.maxPollingIterations
335+
pollingDuration: getValueFromPollingTrait(
336+
providedValue: pollingDuration,
337+
default: defaultPollingConfiguration.pollingDuration,
338+
\ConfirmAlwaysPassesConfigurationTrait.pollingDuration
329339
),
330340
pollingInterval: getValueFromPollingTrait(
331341
providedValue: pollingInterval,
@@ -466,8 +476,8 @@ private struct Poller {
466476
/// while the expression continues to pass)
467477
let pollingBehavior: PollingBehavior
468478

469-
// How many times to poll
470-
let pollingIterations: Int
479+
// Approximately how long to poll for
480+
let pollingDuration: Duration
471481
// Minimum waiting period between polling
472482
let pollingInterval: Duration
473483

@@ -526,9 +536,16 @@ private struct Poller {
526536
isolation: isolated (any Actor)?,
527537
_ body: @escaping () async -> sending R?
528538
) async -> R? {
529-
precondition(pollingIterations > 0)
539+
precondition(pollingDuration > Duration.zero)
530540
precondition(pollingInterval > Duration.zero)
541+
precondition(pollingDuration > pollingInterval)
542+
let durationSeconds = Double(pollingDuration.components.seconds) + Double(pollingDuration.components.attoseconds) * 1e-18
543+
let intervalSeconds = Double(pollingInterval.components.seconds) + Double(pollingInterval.components.attoseconds) * 1e-18
544+
545+
let pollingIterations = max(Int(durationSeconds / intervalSeconds), 1)
546+
531547
let (result, value) = await poll(
548+
pollingIterations: pollingIterations,
532549
expression: body
533550
)
534551
if let issue = result.issue(
@@ -552,12 +569,14 @@ private struct Poller {
552569
/// `.passesOnce`
553570
///
554571
/// - Parameters:
572+
/// - pollingIterations: The maximum amount of times to continue polling.
555573
/// - expression: An expression to continuously evaluate
556574
/// - behavior: The polling behavior to use
557575
/// - timeout: How long to poll for unitl the timeout triggers.
558576
/// - Returns: The result of this polling and the most recent value if the
559577
/// result is .finished, otherwise nil.
560578
private func poll<R>(
579+
pollingIterations: Int,
561580
isolation: isolated (any Actor)? = #isolation,
562581
expression: @escaping () async -> sending R?
563582
) async -> (PollResult, R?) {

Sources/Testing/Traits/PollingConfigurationTrait.swift

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,13 @@
1313
@_spi(Experimental)
1414
@available(macOS 13, iOS 17, watchOS 9, tvOS 17, visionOS 1, *)
1515
public struct ConfirmPassesEventuallyConfigurationTrait: TestTrait, SuiteTrait {
16-
public var maxPollingIterations: Int?
16+
public var pollingDuration: Duration?
1717
public var pollingInterval: Duration?
1818

1919
public var isRecursive: Bool { true }
2020

21-
public init(maxPollingIterations: Int?, pollingInterval: Duration?) {
22-
self.maxPollingIterations = maxPollingIterations
21+
public init(pollingDuration: Duration?, pollingInterval: Duration?) {
22+
self.pollingDuration = pollingDuration
2323
self.pollingInterval = pollingInterval
2424
}
2525
}
@@ -32,13 +32,13 @@ public struct ConfirmPassesEventuallyConfigurationTrait: TestTrait, SuiteTrait {
3232
@_spi(Experimental)
3333
@available(macOS 13, iOS 17, watchOS 9, tvOS 17, visionOS 1, *)
3434
public struct ConfirmAlwaysPassesConfigurationTrait: TestTrait, SuiteTrait {
35-
public var maxPollingIterations: Int?
35+
public var pollingDuration: Duration?
3636
public var pollingInterval: Duration?
3737

3838
public var isRecursive: Bool { true }
3939

40-
public init(maxPollingIterations: Int?, pollingInterval: Duration?) {
41-
self.maxPollingIterations = maxPollingIterations
40+
public init(pollingDuration: Duration?, pollingInterval: Duration?) {
41+
self.pollingDuration = pollingDuration
4242
self.pollingInterval = pollingInterval
4343
}
4444
}
@@ -49,20 +49,22 @@ extension Trait where Self == ConfirmPassesEventuallyConfigurationTrait {
4949
/// Specifies defaults for ``confirmPassesEventually`` in the test or suite.
5050
///
5151
/// - Parameters:
52-
/// - maxPollingIterations: The maximum amount of times to attempt polling.
53-
/// If nil, polling will be attempted up to 1000 times.
54-
/// `maxPollingIterations` must be greater than 0.
52+
/// - pollingDuration: The expected amount of times to continue polling for.
53+
/// This value may not correspond to the wall-clock time that polling lasts for, especially
54+
/// on highly-loaded systems with a lot of tests running.
55+
/// if nil, polling will be attempted for approximately 1 second.
56+
/// `pollingDuration` must be greater than 0.
5557
/// - pollingInterval: The minimum amount of time to wait between polling
5658
/// attempts.
5759
/// If nil, polling will wait at least 1 millisecond between polling
5860
/// attempts.
5961
/// `pollingInterval` must be greater than 0.
6062
public static func confirmPassesEventuallyDefaults(
61-
maxPollingIterations: Int? = nil,
63+
pollingDuration: Duration? = nil,
6264
pollingInterval: Duration? = nil
6365
) -> Self {
6466
ConfirmPassesEventuallyConfigurationTrait(
65-
maxPollingIterations: maxPollingIterations,
67+
pollingDuration: pollingDuration,
6668
pollingInterval: pollingInterval
6769
)
6870
}
@@ -74,20 +76,22 @@ extension Trait where Self == ConfirmAlwaysPassesConfigurationTrait {
7476
/// Specifies defaults for ``confirmPassesAlways`` in the test or suite.
7577
///
7678
/// - Parameters:
77-
/// - maxPollingIterations: The maximum amount of times to attempt polling.
78-
/// If nil, polling will be attempted up to 1000 times.
79-
/// `maxPollingIterations` must be greater than 0.
79+
/// - pollingDuration: The expected amount of times to continue polling for.
80+
/// This value may not correspond to the wall-clock time that polling lasts for, especially
81+
/// on highly-loaded systems with a lot of tests running.
82+
/// if nil, polling will be attempted for approximately 1 second.
83+
/// `pollingDuration` must be greater than 0.
8084
/// - pollingInterval: The minimum amount of time to wait between polling
8185
/// attempts.
8286
/// If nil, polling will wait at least 1 millisecond between polling
8387
/// attempts.
8488
/// `pollingInterval` must be greater than 0.
8589
public static func confirmAlwaysPassesDefaults(
86-
maxPollingIterations: Int? = nil,
90+
pollingDuration: Duration? = nil,
8791
pollingInterval: Duration? = nil
8892
) -> Self {
8993
ConfirmAlwaysPassesConfigurationTrait(
90-
maxPollingIterations: maxPollingIterations,
94+
pollingDuration: pollingDuration,
9195
pollingInterval: pollingInterval
9296
)
9397
}

0 commit comments

Comments
 (0)