8
8
// See https://swift.org/CONTRIBUTORS.txt for Swift project authors
9
9
//
10
10
11
+ @available ( macOS 13 , iOS 17 , watchOS 9 , tvOS 17 , visionOS 1 , * )
12
+ internal let defaultPollingConfiguration = (
13
+ maxPollingIterations: 1000 ,
14
+ pollingInterval: Duration . milliseconds ( 1 )
15
+ )
16
+
11
17
/// Confirm that some expression eventually returns true
12
18
///
13
19
/// - Parameters:
14
20
/// - comment: An optional comment to apply to any issues generated by this
15
21
/// function.
22
+ /// - maxPollingIterations: The maximum amount of times to attempt polling.
23
+ /// If nil, this uses whatever value is specified under the last
24
+ /// ``ConfirmPassesEventuallyConfigurationTrait`` added to the test or suite.
25
+ /// If no ``ConfirmPassesEventuallyConfigurationTrait`` has been added, then
26
+ /// polling will be attempted 1000 times before recording an issue.
27
+ /// `maxPollingIterations` must be greater than 0.
28
+ /// - pollingInterval: The minimum amount of time to wait between polling attempts.
29
+ /// If nil, this uses whatever value is specified under the last
30
+ /// ``ConfirmPassesEventuallyConfigurationTrait`` added to the test or suite.
31
+ /// If no ``ConfirmPassesEventuallyConfigurationTrait`` has been added, then
32
+ /// polling will wait at least 1 millisecond between polling attempts.
33
+ /// `pollingInterval` must be greater than 0.
16
34
/// - isolation: The actor to which `body` is isolated, if any.
17
35
/// - sourceLocation: The source location to whych any recorded issues should
18
36
/// be attributed.
26
44
@available ( macOS 13 , iOS 17 , watchOS 9 , tvOS 17 , visionOS 1 , * )
27
45
public func confirmPassesEventually(
28
46
_ comment: Comment ? = nil ,
29
- maxPollingIterations: Int = 1000 ,
30
- pollingInterval: Duration = . milliseconds ( 1 ) ,
47
+ maxPollingIterations: Int ? = nil ,
48
+ pollingInterval: Duration ? = nil ,
31
49
isolation: isolated ( any Actor ) ? = #isolation,
32
50
sourceLocation: SourceLocation = #_sourceLocation,
33
51
_ body: @escaping ( ) async throws -> Bool
34
52
) async {
35
53
let poller = Poller (
36
54
pollingBehavior: . passesOnce,
37
- pollingIterations: maxPollingIterations,
38
- pollingInterval: pollingInterval,
55
+ pollingIterations: getValueFromPollingTrait (
56
+ providedValue: maxPollingIterations,
57
+ default: defaultPollingConfiguration. maxPollingIterations,
58
+ \ConfirmPassesEventuallyConfigurationTrait . maxPollingIterations
59
+ ) ,
60
+ pollingInterval: getValueFromPollingTrait (
61
+ providedValue: pollingInterval,
62
+ default: defaultPollingConfiguration. pollingInterval,
63
+ \ConfirmPassesEventuallyConfigurationTrait . pollingInterval
64
+ ) ,
39
65
comment: comment,
40
66
sourceLocation: sourceLocation
41
67
)
@@ -58,6 +84,18 @@ public struct PollingFailedError: Error {}
58
84
/// - Parameters:
59
85
/// - comment: An optional comment to apply to any issues generated by this
60
86
/// function.
87
+ /// - maxPollingIterations: The maximum amount of times to attempt polling.
88
+ /// If nil, this uses whatever value is specified under the last
89
+ /// ``ConfirmPassesEventuallyConfigurationTrait`` added to the test or suite.
90
+ /// If no ``ConfirmPassesEventuallyConfigurationTrait`` has been added, then
91
+ /// polling will be attempted 1000 times before recording an issue.
92
+ /// `maxPollingIterations` must be greater than 0.
93
+ /// - pollingInterval: The minimum amount of time to wait between polling attempts.
94
+ /// If nil, this uses whatever value is specified under the last
95
+ /// ``ConfirmPassesEventuallyConfigurationTrait`` added to the test or suite.
96
+ /// If no ``ConfirmPassesEventuallyConfigurationTrait`` has been added, then
97
+ /// polling will wait at least 1 millisecond between polling attempts.
98
+ /// `pollingInterval` must be greater than 0.
61
99
/// - isolation: The actor to which `body` is isolated, if any.
62
100
/// - sourceLocation: The source location to whych any recorded issues should
63
101
/// be attributed.
@@ -77,17 +115,25 @@ public struct PollingFailedError: Error {}
77
115
@discardableResult
78
116
public func confirmPassesEventually< R> (
79
117
_ comment: Comment ? = nil ,
80
- maxPollingIterations: Int = 1000 ,
81
- pollingInterval: Duration = . milliseconds ( 1 ) ,
118
+ maxPollingIterations: Int ? = nil ,
119
+ pollingInterval: Duration ? = nil ,
82
120
isolation: isolated ( any Actor ) ? = #isolation,
83
121
sourceLocation: SourceLocation = #_sourceLocation,
84
122
_ body: @escaping ( ) async throws -> R ?
85
123
) async throws -> R where R: Sendable {
86
124
let recorder = PollingRecorder < R > ( )
87
125
let poller = Poller (
88
126
pollingBehavior: . passesOnce,
89
- pollingIterations: maxPollingIterations,
90
- pollingInterval: pollingInterval,
127
+ pollingIterations: getValueFromPollingTrait (
128
+ providedValue: maxPollingIterations,
129
+ default: defaultPollingConfiguration. maxPollingIterations,
130
+ \ConfirmPassesEventuallyConfigurationTrait . maxPollingIterations
131
+ ) ,
132
+ pollingInterval: getValueFromPollingTrait (
133
+ providedValue: pollingInterval,
134
+ default: defaultPollingConfiguration. pollingInterval,
135
+ \ConfirmPassesEventuallyConfigurationTrait . pollingInterval
136
+ ) ,
91
137
comment: comment,
92
138
sourceLocation: sourceLocation
93
139
)
@@ -110,6 +156,18 @@ public func confirmPassesEventually<R>(
110
156
/// - Parameters:
111
157
/// - comment: An optional comment to apply to any issues generated by this
112
158
/// function.
159
+ /// - maxPollingIterations: The maximum amount of times to attempt polling.
160
+ /// 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
163
+ /// polling will be attempted 1000 times before recording an issue.
164
+ /// `maxPollingIterations` must be greater than 0.
165
+ /// - pollingInterval: The minimum amount of time to wait between polling attempts.
166
+ /// 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
169
+ /// polling will wait at least 1 millisecond between polling attempts.
170
+ /// `pollingInterval` must be greater than 0.
113
171
/// - isolation: The actor to which `body` is isolated, if any.
114
172
/// - sourceLocation: The source location to whych any recorded issues should
115
173
/// be attributed.
@@ -122,16 +180,24 @@ public func confirmPassesEventually<R>(
122
180
@available ( macOS 13 , iOS 17 , watchOS 9 , tvOS 17 , visionOS 1 , * )
123
181
public func confirmAlwaysPasses(
124
182
_ comment: Comment ? = nil ,
125
- maxPollingIterations: Int = 1000 ,
126
- pollingInterval: Duration = . milliseconds ( 1 ) ,
183
+ maxPollingIterations: Int ? = nil ,
184
+ pollingInterval: Duration ? = nil ,
127
185
isolation: isolated ( any Actor ) ? = #isolation,
128
186
sourceLocation: SourceLocation = #_sourceLocation,
129
187
_ body: @escaping ( ) async throws -> Bool
130
188
) async {
131
189
let poller = Poller (
132
190
pollingBehavior: . passesAlways,
133
- pollingIterations: maxPollingIterations,
134
- pollingInterval: pollingInterval,
191
+ pollingIterations: getValueFromPollingTrait (
192
+ providedValue: maxPollingIterations,
193
+ default: defaultPollingConfiguration. maxPollingIterations,
194
+ \ConfirmPassesAlwaysConfigurationTrait . maxPollingIterations
195
+ ) ,
196
+ pollingInterval: getValueFromPollingTrait (
197
+ providedValue: pollingInterval,
198
+ default: defaultPollingConfiguration. pollingInterval,
199
+ \ConfirmPassesAlwaysConfigurationTrait . pollingInterval
200
+ ) ,
135
201
comment: comment,
136
202
sourceLocation: sourceLocation
137
203
)
@@ -144,48 +210,34 @@ public func confirmAlwaysPasses(
144
210
}
145
211
}
146
212
147
- /// Confirm that some expression always returns a non-optional value
213
+ /// A helper function to de-duplicate the logic of grabbing configuration from
214
+ /// either the passed-in value (if given), the hardcoded default, and the
215
+ /// appropriate configuration trait.
148
216
///
149
- /// - Parameters:
150
- /// - comment: An optional comment to apply to any issues generated by this
151
- /// function.
152
- /// - isolation: The actor to which `body` is isolated, if any.
153
- /// - sourceLocation: The source location to whych any recorded issues should
154
- /// be attributed.
155
- /// - body: The function to invoke.
217
+ /// The provided value, if non-nil is returned. Otherwise, this looks for
218
+ /// the last `TraitKind` specified, and if one exists, returns the value
219
+ /// as determined by `keyPath`.
220
+ /// If no configuration trait has been applied, then this returns the `default`.
156
221
///
157
- /// - Returns: The value from the last time `body` was invoked.
158
- ///
159
- /// - Throws: A `PollingFailedError` will be thrown if `body` ever returns a
160
- /// non-optional value
161
- ///
162
- /// Use polling confirmations to check that an event while a test is running in
163
- /// complex scenarios where other forms of confirmation are insufficient. For
164
- /// example, confirming that some state does not change.
165
- @_spi ( Experimental)
166
- @available ( macOS 13 , iOS 17 , watchOS 9 , tvOS 17 , visionOS 1 , * )
167
- public func confirmAlwaysPasses< R> (
168
- _ comment: Comment ? = nil ,
169
- maxPollingIterations: Int = 1000 ,
170
- pollingInterval: Duration = . milliseconds( 1 ) ,
171
- isolation: isolated ( any Actor ) ? = #isolation,
172
- sourceLocation: SourceLocation = #_sourceLocation,
173
- _ body: @escaping ( ) async throws -> R ?
174
- ) async {
175
- let poller = Poller (
176
- pollingBehavior: . passesAlways,
177
- pollingIterations: maxPollingIterations,
178
- pollingInterval: pollingInterval,
179
- comment: comment,
180
- sourceLocation: sourceLocation
181
- )
182
- await poller. evaluate ( isolation: isolation) {
183
- do {
184
- return try await body ( ) != nil
185
- } catch {
186
- return false
187
- }
222
+ /// - Parameters:
223
+ /// - providedValue: The value provided by the test author when calling
224
+ /// `confirmPassesEventually` or `confirmAlwaysPasses`.
225
+ /// - default: The harded coded default value, as defined in
226
+ /// `defaultPollingConfiguration`
227
+ /// - keyPath: The keyPath mapping from `TraitKind` to the desired value type.
228
+ private func getValueFromPollingTrait< TraitKind, Value> (
229
+ providedValue: Value ? ,
230
+ default: Value ,
231
+ _ keyPath: KeyPath < TraitKind , Value >
232
+ ) -> Value {
233
+ if let providedValue { return providedValue }
234
+ 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`
188
239
}
240
+ return trait [ keyPath: keyPath]
189
241
}
190
242
191
243
/// A type to record the last value returned by a closure returning an optional
@@ -321,6 +373,8 @@ private struct Poller {
321
373
isolation: isolated ( any Actor ) ? ,
322
374
_ body: @escaping ( ) async -> Bool
323
375
) async {
376
+ precondition ( pollingIterations > 0 )
377
+ precondition ( pollingInterval > Duration . zero)
324
378
let result = await poll (
325
379
expression: body
326
380
)
0 commit comments