Skip to content

Commit fed51ab

Browse files
committed
Swift Testing Polling Expectations:
- Change the default timeout. - Change how unexpected thrown errors are treated. - Add future direction to add change monitoring via Observation - Add alternative considered of Just Use A While Loop - Add alternative considered for shorter default timeouts
1 parent 6c1a092 commit fed51ab

File tree

1 file changed

+57
-48
lines changed

1 file changed

+57
-48
lines changed

proposals/testing/NNNN-polling-expectations.md

Lines changed: 57 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ This proposal introduces new overloads of the `#expect()` and `#require()`
4444
macros that take, as arguments, a closure and a timeout value. When called,
4545
these macros will continuously evaluate the closure until either the specific
4646
condition passes, or the timeout has passed. The timeout period will default
47-
to 1 second.
47+
to 1 minute.
4848

4949
There are 2 Polling Behaviors that we will add: Passes Once and Passes Always.
5050
Passes 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")
118105
where 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")
167141
where 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

206184
These 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
240218
non-nil value. If it passes, the value returned by the last time the
241219
expression 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-
246221
When an error is expected, then the expression is not considered to pass
247222
unless it throws an error that equals the expected error or returns true when
248223
evaluated by the `errorMatcher`. After which the polling continues under the
249224
specified 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

253231
This is a new interface that is unlikely to collide with any existing
@@ -264,8 +242,39 @@ tools may integrate with them.
264242
The timeout default could be configured as a Suite or Test trait. Additionally,
265243
it 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

271280
Instead of creating the `PollingBehavior` type, we could have introduced more

0 commit comments

Comments
 (0)