@@ -44,7 +44,7 @@ This proposal introduces new overloads of the `#expect()` and `#require()`
4444macros that take, as arguments, a closure and a timeout value. When called,
4545these macros will continuously evaluate the closure until either the specific
4646condition passes, or the timeout has passed. The timeout period will default
47- to 1 second .
47+ to 1 minute .
4848
4949There are 2 Polling Behaviors that we will add: Passes Once and Passes Always.
5050Passes Once will continuously evaluate the expression until the expression
@@ -88,68 +88,42 @@ the testing library:
8888/// changes as the result of activity in another task/queue/thread.
8989@freestanding (expression) public macro expect (
9090 until pollingBehavior : PollingBehavior,
91- timeout : Duration = .seconds (1 ),
91+ timeout : Duration = .seconds (60 ),
9292 _ comment : @autoclosure () -> Comment? = nil ,
9393 sourceLocation : SourceLocation = #_sourceLocation ,
9494 expression : @Sendable () async throws -> Bool
9595) = #externalMacro (module : " TestingMacros" , type : " PollingExpectMacro" )
9696
97- /// Continuously check an expression until it matches the given PollingBehavior
98- ///
99- /// - Parameters:
100- /// - until: The desired PollingBehavior to check for.
101- /// - timeout: How long to run poll the expression until stopping.
102- /// - comment: A comment describing the expectation.
103- /// - sourceLocation: The source location to which the recorded expectations
104- /// and issues should be attributed.
105- /// - expression: The expression to be evaluated.
106- ///
107- /// Use this overload of `#expect()` when you wish to poll whether a value
108- /// changes as the result of activity in another task/queue/thread, and you
109- /// expect the expression to throw an error as part of succeeding
11097@freestanding (expression) public macro expect <E >(
11198 until pollingBehavior : PollingBehavior,
11299 throws error : E,
113- timeout : Duration = .seconds (1 ),
100+ timeout : Duration = .seconds (60 ),
114101 _ comment : @autoclosure () -> Comment? = nil ,
115102 sourceLocation : SourceLocation = #_sourceLocation ,
116- expression : @Sendable () async throws (E) -> Bool
117- ) -> E = #externalMacro (module : " TestingMacros" , type : " PollingExpectMacro" )
103+ expression : @Sendable () async throws -> Bool
104+ ) = #externalMacro (module : " TestingMacros" , type : " PollingExpectMacro" )
118105where E: Error & Equatable
119106
120- @freestanding (expression) public macro expect < E > (
107+ @freestanding (expression) public macro expect (
121108 until pollingBehavior : PollingBehavior,
122- timeout : Duration = .seconds (1 ),
109+ timeout : Duration = .seconds (60 ),
123110 _ comment : @autoclosure () -> Comment? = nil ,
124111 sourceLocation : SourceLocation = #_sourceLocation ,
125- performing expression : @Sendable () async throws (E) -> Bool ,
126- throws errorMatcher : (E) async throws -> Bool
127- ) -> E = #externalMacro (module : " TestingMacros" , type : " PollingExpectMacro" )
128- where E: Error
112+ performing expression : @Sendable () async throws -> Bool ,
113+ throws errorMatcher : (any Error ) async throws -> Bool
114+ ) -> Error = #externalMacro (module : " TestingMacros" , type : " PollingExpectMacro" )
129115
130- /// Continuously check an expression until it matches the given PollingBehavior
131- ///
132- /// - Parameters:
133- /// - until: The desired PollingBehavior to check for.
134- /// - timeout: How long to run poll the expression until stopping.
135- /// - comment: A comment describing the expectation.
136- /// - sourceLocation: The source location to which the recorded expectations
137- /// and issues should be attributed.
138- /// - expression: The expression to be evaluated.
139- ///
140- /// Use this overload of `#require()` when you wish to poll whether a value
141- /// changes as the result of activity in another task/queue/thread.
142116@freestanding (expression) public macro require (
143117 until pollingBehavior : PollingBehavior,
144- timeout : Duration = .seconds (1 ),
118+ timeout : Duration = .seconds (60 ),
145119 _ comment : @autoclosure () -> Comment? = nil ,
146120 sourceLocation : SourceLocation = #_sourceLocation ,
147121 expression : @Sendable () async throws -> Bool
148122) = #externalMacro (module : " TestingMacros" , type : " PollingRequireMacro" )
149123
150124@freestanding (expression) public macro require <R >(
151125 until pollingBehavior : PollingBehavior,
152- timeout : Duration = .seconds (1 ),
126+ timeout : Duration = .seconds (60 ),
153127 _ comment : @autoclosure () -> Comment? = nil ,
154128 sourceLocation : SourceLocation = #_sourceLocation ,
155129 expression : @Sendable () async throws -> R?
@@ -159,22 +133,21 @@ where R: Sendable
159133@freestanding (expression) public macro require <E >(
160134 until pollingBehavior : PollingBehavior,
161135 throws error : E,
162- timeout : Duration = .seconds (1 ),
136+ timeout : Duration = .seconds (60 ),
163137 _ comment : @autoclosure () -> Comment? = nil ,
164138 sourceLocation : SourceLocation = #_sourceLocation ,
165- expression : @Sendable () async throws (E) -> Bool
139+ expression : @Sendable () async throws -> Bool
166140) = #externalMacro (module : " TestingMacros" , type : " PollingRequireMacro" )
167141where E: Error & Equatable
168142
169- @freestanding (expression) public macro require < E > (
143+ @freestanding (expression) public macro require (
170144 until pollingBehavior : PollingBehavior,
171- timeout : Duration = .seconds (1 ),
145+ timeout : Duration = .seconds (60 ),
172146 _ comment : @autoclosure () -> Comment? = nil ,
173147 sourceLocation : SourceLocation = #_sourceLocation ,
174- expression : @Sendable () async throws (E) -> Bool ,
175- throwing errorMatcher : (E ) async throws -> Bool ,
148+ expression : @Sendable () async throws -> Bool ,
149+ throwing errorMatcher : (any Error ) async throws -> Bool ,
176150) = #externalMacro (module : " TestingMacros" , type : " PollingRequireMacro" )
177- where E: Error
178151```
179152
180153### Polling Behavior
@@ -201,6 +174,11 @@ public enum PollingBehavior {
201174}
202175```
203176
177+ ### Platform Availability
178+
179+ Polling expectations will not be available on platforms that do not support
180+ Swift Concurrency, nor on platforms that do not support multiple threads.
181+
204182### Usage
205183
206184These macros can be used with an async test function:
@@ -240,14 +218,14 @@ expectation is considered to have passed if the expression always returns a
240218non-nil value. If it passes, the value returned by the last time the
241219expression is evaluated will be returned by the expectation.
242220
243- When no error is expected, then the first time the expression throws any error
244- will cause the polling expectation to stop & report the error as a failure.
245-
246221When an error is expected, then the expression is not considered to pass
247222unless it throws an error that equals the expected error or returns true when
248223evaluated by the ` errorMatcher ` . After which the polling continues under the
249224specified PollingBehavior.
250225
226+ When no error is expected, then this is treated as if the expression returned
227+ false. This is specifically to invert the case when an error is expected.
228+
251229## Source compatibility
252230
253231This is a new interface that is unlikely to collide with any existing
@@ -264,8 +242,39 @@ tools may integrate with them.
264242The timeout default could be configured as a Suite or Test trait. Additionally,
265243it could be configured in some future global configuration tool.
266244
245+ On the topic of monitoring for changes, we could add a tool integrating with the
246+ Observation module which monitors changes to ` @Observable ` objects during some
247+ lifetime.
248+
267249## Alternatives considered
268250
251+ ### Just use a while loop
252+
253+ Polling could be written as a simple while loop that continuously executes the
254+ expression until it returns, something like:
255+
256+ ``` swift
257+ func poll (timeout : Duration, expression : () -> Bool ) -> Bool {
258+ let clock: Clock = // ...
259+ let endTimestamp = clock.now + timeout
260+ while clock.now < endTimestamp {
261+ if expression () { return true }
262+ }
263+ return false
264+ }
265+ ```
266+
267+ Which works in most naive cases, but is not robust. Notably, This approach does
268+ not handle the case when the expression never returns, or does not return within
269+ the timeout period.
270+
271+ ### Shorter default timeout
272+
273+ Due to the nature of Swift Concurrency scheduling, using short default
274+ timeouts will result in high rates of test flakiness. This is why the default
275+ timeout is 1 minute. We do not recommend that test authors use timeouts any
276+ shorter than this.
277+
269278### Remove ` PollingBehavior ` in favor of more macros
270279
271280Instead of creating the ` PollingBehavior ` type, we could have introduced more
0 commit comments