1- # Polling Expectations
1+ # Polling Confirmations
22
3- * Proposal: [ ST-NNNN] ( NNNN-polling-expectations .md )
3+ * Proposal: [ ST-NNNN] ( NNNN-polling-confirmations .md )
44* Authors: [ Rachel Brindle] ( https://github.com/younata )
55* Review Manager: TBD
66* Status: ** Awaiting review**
@@ -42,7 +42,9 @@ actor Aquarium {
4242
4343This proposal introduces new members of the ` confirmation ` family of functions:
4444` confirmPassesEventually ` and ` confirmAlwaysPasses ` . These functions take in
45- a closure to be continuously evaluated until the specific condition passes.
45+ a closure to be repeatedly evaluated until the specific condition passes,
46+ waiting at least some amount of time - specified by ` pollingInterval ` and
47+ defaulting to 1 millisecond - before evaluating the closure again.
4648
4749` confirmPassesEventually ` will evaluate the closure until the first time it
4850returns true or a non-nil value. ` confirmAlwaysPasses ` will evaluate the
@@ -75,6 +77,21 @@ testing library:
7577/// - Parameters:
7678/// - comment: An optional comment to apply to any issues generated by this
7779/// function.
80+ /// - maxPollingIterations: The maximum amount of times to attempt polling.
81+ /// If nil, this uses whatever value is specified under the last
82+ /// ``ConfirmPassesEventuallyConfigurationTrait`` added to the test or
83+ /// suite.
84+ /// If no ``ConfirmPassesEventuallyConfigurationTrait`` has been added, then
85+ /// polling will be attempted 1000 times before recording an issue.
86+ /// `maxPollingIterations` must be greater than 0.
87+ /// - pollingInterval: The minimum amount of time to wait between polling
88+ /// attempts.
89+ /// If nil, this uses whatever value is specified under the last
90+ /// ``ConfirmPassesEventuallyConfigurationTrait`` added to the test or
91+ /// suite.
92+ /// If no ``ConfirmPassesEventuallyConfigurationTrait`` has been added, then
93+ /// polling will wait at least 1 millisecond between polling attempts.
94+ /// `pollingInterval` must be greater than 0.
7895/// - isolation: The actor to which `body` is isolated, if any.
7996/// - sourceLocation: The source location to whych any recorded issues should
8097/// be attributed.
@@ -87,6 +104,8 @@ testing library:
87104@available (macOS 13 , iOS 17 , watchOS 9 , tvOS 17 , visionOS 1 , * )
88105public func confirmPassesEventually (
89106 _ comment : Comment? = nil ,
107+ maxPollingIterations : Int ? = nil ,
108+ pollingInterval : Duration? = nil ,
90109 isolation : isolated (any Actor)? = #isolation ,
91110 sourceLocation : SourceLocation = #_sourceLocation ,
92111 _ body : @escaping () async throws -> Bool
@@ -97,6 +116,20 @@ public func confirmPassesEventually(
97116/// - Parameters:
98117/// - comment: An optional comment to apply to any issues generated by this
99118/// function.
119+ /// - maxPollingIterations: The maximum amount of times to attempt polling.
120+ /// If nil, this uses whatever value is specified under the last
121+ /// ``ConfirmPassesEventuallyConfigurationTrait`` added to the test or
122+ /// suite.
123+ /// If no ``ConfirmPassesEventuallyConfigurationTrait`` has been added, then
124+ /// polling will be attempted 1000 times before recording an issue.
125+ /// `maxPollingIterations` must be greater than 0.
126+ /// - pollingInterval: The minimum amount of time to wait between polling
127+ /// attempts.
128+ /// If nil, this uses whatever value is specified under the last
129+ /// ``ConfirmPassesEventuallyConfigurationTrait`` added to the test or suite.
130+ /// If no ``ConfirmPassesEventuallyConfigurationTrait`` has been added, then
131+ /// polling will wait at least 1 millisecond between polling attempts.
132+ /// `pollingInterval` must be greater than 0.
100133/// - isolation: The actor to which `body` is isolated, if any.
101134/// - sourceLocation: The source location to whych any recorded issues should
102135/// be attributed.
@@ -114,6 +147,8 @@ public func confirmPassesEventually(
114147@available (macOS 13 , iOS 17 , watchOS 9 , tvOS 17 , visionOS 1 , * )
115148public func confirmPassesEventually <R >(
116149 _ comment : Comment? = nil ,
150+ maxPollingIterations : Int ? = nil ,
151+ pollingInterval : Duration? = nil ,
117152 isolation : isolated (any Actor)? = #isolation ,
118153 sourceLocation : SourceLocation = #_sourceLocation ,
119154 _ body : @escaping () async throws -> R?
@@ -124,6 +159,19 @@ public func confirmPassesEventually<R>(
124159/// - Parameters:
125160/// - comment: An optional comment to apply to any issues generated by this
126161/// function.
162+ /// - maxPollingIterations: The maximum amount of times to attempt polling.
163+ /// If nil, this uses whatever value is specified under the last
164+ /// ``ConfirmAlwaysPassesConfigurationTrait`` added to the test or suite.
165+ /// If no ``ConfirmAlwaysPassesConfigurationTrait`` has been added, then
166+ /// polling will be attempted 1000 times before recording an issue.
167+ /// `maxPollingIterations` must be greater than 0.
168+ /// - pollingInterval: The minimum amount of time to wait between polling
169+ /// attempts.
170+ /// If nil, this uses whatever value is specified under the last
171+ /// ``ConfirmAlwaysPassesConfigurationTrait`` added to the test or suite.
172+ /// If no ``ConfirmAlwaysPassesConfigurationTrait`` has been added, then
173+ /// polling will wait at least 1 millisecond between polling attempts.
174+ /// `pollingInterval` must be greater than 0.
127175/// - isolation: The actor to which `body` is isolated, if any.
128176/// - sourceLocation: The source location to whych any recorded issues should
129177/// be attributed.
@@ -135,36 +183,12 @@ public func confirmPassesEventually<R>(
135183@available (macOS 13 , iOS 17 , watchOS 9 , tvOS 17 , visionOS 1 , * )
136184public func confirmAlwaysPasses (
137185 _ comment : Comment? = nil ,
186+ maxPollingIterations : Int ? = nil ,
187+ pollingInterval : Duration? = nil ,
138188 isolation : isolated (any Actor)? = #isolation ,
139189 sourceLocation : SourceLocation = #_sourceLocation ,
140190 _ body : @escaping () async throws -> Bool
141191) async
142-
143- /// Confirm that some expression always returns a non-optional value
144- ///
145- /// - Parameters:
146- /// - comment: An optional comment to apply to any issues generated by this
147- /// function.
148- /// - isolation: The actor to which `body` is isolated, if any.
149- /// - sourceLocation: The source location to whych any recorded issues should
150- /// be attributed.
151- /// - body: The function to invoke.
152- ///
153- /// - Returns: The value from the last time `body` was invoked.
154- ///
155- /// - Throws: A `PollingFailedError` will be thrown if `body` ever returns a
156- /// non-optional value
157- ///
158- /// Use polling confirmations to check that an event while a test is running in
159- /// complex scenarios where other forms of confirmation are insufficient. For
160- /// example, confirming that some state does not change.
161- @available (macOS 13 , iOS 17 , watchOS 9 , tvOS 17 , visionOS 1 , * )
162- public func confirmAlwaysPasses <R >(
163- _ comment : Comment? = nil ,
164- isolation : isolated (any Actor)? = #isolation ,
165- sourceLocation : SourceLocation = #_sourceLocation ,
166- _ body : @escaping () async throws -> R?
167- ) async throws -> R where R: Sendable
168192```
169193
170194### New Error Type
@@ -184,9 +208,99 @@ A new Issue.Kind, `confirmationPollingFailed` will be added to represent the
184208case here confirmation polling fails. This issue kind will be recorded when
185209polling fails.
186210
211+ ### New Traits
212+
213+ Two new traits will be added to change the default values for the
214+ ` maxPollingIterations ` and ` pollingInterval ` arguments. Test authors often
215+ want to poll for the ` passesEventually ` behavior more than they poll for the
216+ ` alwaysPasses ` behavior, which is why there are separate traits for configuring
217+ defaults for these functions.
218+
219+ ``` swift
220+ /// A trait to provide a default polling configuration to all usages of
221+ /// ``confirmPassesEventually`` within a test or suite.
222+ ///
223+ /// To add this trait to a test, use the
224+ /// ``Trait/confirmPassesEventuallyDefaults`` function.
225+ @available (macOS 13 , iOS 17 , watchOS 9 , tvOS 17 , visionOS 1 , * )
226+ public struct ConfirmPassesEventuallyConfigurationTrait : TestTrait , SuiteTrait {
227+ public var maxPollingIterations: Int ?
228+ public var pollingInterval: Duration?
229+
230+ public var isRecursive: Bool { true }
231+
232+ public init (maxPollingIterations : Int ? , pollingInterval : Duration? )
233+ }
234+
235+ /// A trait to provide a default polling configuration to all usages of
236+ /// ``confirmPassesAlways`` within a test or suite.
237+ ///
238+ /// To add this trait to a test, use the ``Trait/confirmAlwaysPassesDefaults``
239+ /// function.
240+ @available (macOS 13 , iOS 17 , watchOS 9 , tvOS 17 , visionOS 1 , * )
241+ public struct ConfirmAlwaysPassesConfigurationTrait : TestTrait , SuiteTrait {
242+ public var maxPollingIterations: Int ?
243+ public var pollingInterval: Duration?
244+
245+ public var isRecursive: Bool { true }
246+
247+ public init (maxPollingIterations : Int ? , pollingInterval : Duration? )
248+ }
249+
250+ @available (macOS 13 , iOS 17 , watchOS 9 , tvOS 17 , visionOS 1 , * )
251+ extension Trait where Self == ConfirmPassesEventuallyConfigurationTrait {
252+ /// Specifies defaults for ``confirmPassesEventually`` in the test or suite.
253+ ///
254+ /// - Parameters:
255+ /// - maxPollingIterations: The maximum amount of times to attempt polling.
256+ /// If nil, polling will be attempted up to 1000 times.
257+ /// `maxPollingIterations` must be greater than 0.
258+ /// - pollingInterval: The minimum amount of time to wait between polling
259+ /// attempts.
260+ /// If nil, polling will wait at least 1 millisecond between polling
261+ /// attempts.
262+ /// `pollingInterval` must be greater than 0.
263+ public static func confirmPassesEventuallyDefaults (
264+ maxPollingIterations : Int ? = nil ,
265+ pollingInterval : Duration? = nil
266+ ) -> Self
267+ }
268+
269+ @available (macOS 13 , iOS 17 , watchOS 9 , tvOS 17 , visionOS 1 , * )
270+ extension Trait where Self == ConfirmPassesAlwaysConfigurationTrait {
271+ /// Specifies defaults for ``confirmAlwaysPasses`` in the test or suite.
272+ ///
273+ /// - Parameters:
274+ /// - maxPollingIterations: The maximum amount of times to attempt polling.
275+ /// If nil, polling will be attempted up to 1000 times.
276+ /// `maxPollingIterations` must be greater than 0.
277+ /// - pollingInterval: The minimum amount of time to wait between polling
278+ /// attempts.
279+ /// If nil, polling will wait at least 1 millisecond between polling
280+ /// attempts.
281+ /// `pollingInterval` must be greater than 0.
282+ public static func confirmAlwaysPassesDefaults (
283+ maxPollingIterations : Int ? = nil ,
284+ pollingInterval : Duration? = nil
285+ ) -> Self
286+ }
287+ ```
288+
289+ Specifying ` maxPollingIterations ` or ` pollingInterval ` directly on either
290+ ` confirmPassesEventually ` or ` confirmAlwaysPasses ` will override any value
291+ provided by the trait.
292+
293+ ### Default Polling Configuration
294+
295+ For both ` confirmPassesEventually ` and ` confirmsAlwaysPasses ` , the Testing
296+ library will default ` maxPollingIterations ` to 1000, and ` pollingInterval ` to
297+ 1 millisecond. This allows for tests on lightly-loaded systems such as developer
298+ workstations to run in a little over 1 second wall-clock time, while still
299+ being able to gracefully handle running on large loads.
300+
187301### Platform Availability
188302
189- Polling expectations will not be available on platforms that do not support
303+ Polling confirmations will not be available on platforms that do not support
190304Swift Concurrency.
191305
192306### Usage
@@ -211,7 +325,8 @@ will end, and no failure will be reported.
211325
212326Polling will be stopped in the following cases:
213327
214- - After the expression has been evaluated 1 million times.
328+ - After the expression has been evaluated up to the count specified in
329+ ` maxPollingInterations ` .
215330- If the task that started the polling is cancelled.
216331- For ` confirmPassesEventually ` : The first time the closure returns true or a
217332 non-nil value
@@ -253,6 +368,31 @@ tests are executed serially, the concurrent test runner the testing library uses
253368means that timeouts are inherently unreliable. Importantly, timeouts become more
254369unreliable the more tests in the test suite.
255370
371+ ### Use only polling iterations
372+
373+ Another option considered was only using polling iterations. Naively, this
374+ would write the main polling loop as:
375+
376+ ``` swift
377+ func poll (iterations : Int , expression : () -> Bool ) async -> Bool {
378+ for _ in 0 ..< iterations {
379+ if expression () { return true }
380+ Task.yield ()
381+ }
382+ return false
383+ }
384+ ```
385+
386+ However, while this works and is resistant to many of the issues timeouts face
387+ in concurrent testing environments, it is extremely difficult for test authors
388+ to predict a good-enough polling iterations value. It is much easier for a
389+ human to predict that something should take a second or two than it is for a
390+ human to predict how many polling intervals it should take before the state
391+ changes - polling even a million times on a lightly-loaded system can finish in
392+ well under a millisecond. Because of this, we decided to add on the polling
393+ interval argument: a minimum duration to wait between polling, to make it much
394+ easier for test authors to predict a good-enough guess for when to stop polling.
395+
256396### Use macros instead of functions
257397
258398Instead of adding new bare functions, polling could be written as additional
0 commit comments