@@ -14,6 +14,10 @@ internal let defaultPollingConfiguration = (
14
14
pollingInterval: Duration . milliseconds ( 1 )
15
15
)
16
16
17
+ /// A type describing an error thrown when polling fails.
18
+ @_spi ( Experimental)
19
+ public struct PollingFailedError : Error , Equatable { }
20
+
17
21
/// Confirm that some expression eventually returns true
18
22
///
19
23
/// - Parameters:
@@ -76,10 +80,73 @@ public func confirmPassesEventually(
76
80
}
77
81
}
78
82
79
- /// A type describing an error thrown when polling fails to return a non-nil
80
- /// value
83
+ /// Require that some expression eventually returns true
84
+ ///
85
+ /// - Parameters:
86
+ /// - comment: An optional comment to apply to any issues generated by this
87
+ /// function.
88
+ /// - maxPollingIterations: The maximum amount of times to attempt polling.
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 be attempted 1000 times before recording an issue.
94
+ /// `maxPollingIterations` must be greater than 0.
95
+ /// - pollingInterval: The minimum amount of time to wait between polling
96
+ /// attempts.
97
+ /// If nil, this uses whatever value is specified under the last
98
+ /// ``ConfirmPassesEventuallyConfigurationTrait`` added to the test or suite.
99
+ /// If no ``ConfirmPassesEventuallyConfigurationTrait`` has been added, then
100
+ /// polling will wait at least 1 millisecond between polling attempts.
101
+ /// `pollingInterval` must be greater than 0.
102
+ /// - isolation: The actor to which `body` is isolated, if any.
103
+ /// - sourceLocation: The source location to whych any recorded issues should
104
+ /// be attributed.
105
+ /// - body: The function to invoke.
106
+ ///
107
+ /// - Throws: A `PollingFailedError` will be thrown if the expression never
108
+ /// returns true.
109
+ ///
110
+ /// Use polling confirmations to check that an event while a test is running in
111
+ /// complex scenarios where other forms of confirmation are insufficient. For
112
+ /// example, waiting on some state to change that cannot be easily confirmed
113
+ /// through other forms of `confirmation`.
81
114
@_spi ( Experimental)
82
- public struct PollingFailedError : Error { }
115
+ @available ( macOS 13 , iOS 17 , watchOS 9 , tvOS 17 , visionOS 1 , * )
116
+ public func requirePassesEventually(
117
+ _ comment: Comment ? = nil ,
118
+ maxPollingIterations: Int ? = nil ,
119
+ pollingInterval: Duration ? = nil ,
120
+ isolation: isolated ( any Actor ) ? = #isolation,
121
+ sourceLocation: SourceLocation = #_sourceLocation,
122
+ _ body: @escaping ( ) async throws -> Bool
123
+ ) async throws {
124
+ let poller = Poller (
125
+ pollingBehavior: . passesOnce,
126
+ pollingIterations: getValueFromPollingTrait (
127
+ providedValue: maxPollingIterations,
128
+ default: defaultPollingConfiguration. maxPollingIterations,
129
+ \ConfirmPassesEventuallyConfigurationTrait . maxPollingIterations
130
+ ) ,
131
+ pollingInterval: getValueFromPollingTrait (
132
+ providedValue: pollingInterval,
133
+ default: defaultPollingConfiguration. pollingInterval,
134
+ \ConfirmPassesEventuallyConfigurationTrait . pollingInterval
135
+ ) ,
136
+ comment: comment,
137
+ sourceLocation: sourceLocation
138
+ )
139
+ let passed = await poller. evaluate ( raiseIssue: false , isolation: isolation) {
140
+ do {
141
+ return try await body ( )
142
+ } catch {
143
+ return false
144
+ }
145
+ }
146
+ if !passed {
147
+ throw PollingFailedError ( )
148
+ }
149
+ }
83
150
84
151
/// Confirm that some expression eventually returns a non-nil value
85
152
///
@@ -108,7 +175,7 @@ public struct PollingFailedError: Error {}
108
175
/// - Returns: The first non-nil value returned by `body`.
109
176
///
110
177
/// - Throws: A `PollingFailedError` will be thrown if `body` never returns a
111
- /// non-optional value
178
+ /// non-optional value.
112
179
///
113
180
/// Use polling confirmations to check that an event while a test is running in
114
181
/// complex scenarios where other forms of confirmation are insufficient. For
@@ -215,6 +282,72 @@ public func confirmAlwaysPasses(
215
282
}
216
283
}
217
284
285
+ /// Require that some expression always returns true
286
+ ///
287
+ /// - Parameters:
288
+ /// - comment: An optional comment to apply to any issues generated by this
289
+ /// function.
290
+ /// - maxPollingIterations: The maximum amount of times to attempt polling.
291
+ /// If nil, this uses whatever value is specified under the last
292
+ /// ``ConfirmAlwaysPassesConfigurationTrait`` added to the test or suite.
293
+ /// If no ``ConfirmAlwaysPassesConfigurationTrait`` has been added, then
294
+ /// polling will be attempted 1000 times before recording an issue.
295
+ /// `maxPollingIterations` must be greater than 0.
296
+ /// - pollingInterval: The minimum amount of time to wait between polling
297
+ /// attempts.
298
+ /// If nil, this uses whatever value is specified under the last
299
+ /// ``ConfirmAlwaysPassesConfigurationTrait`` added to the test or suite.
300
+ /// If no ``ConfirmAlwaysPassesConfigurationTrait`` has been added, then
301
+ /// polling will wait at least 1 millisecond between polling attempts.
302
+ /// `pollingInterval` must be greater than 0.
303
+ /// - isolation: The actor to which `body` is isolated, if any.
304
+ /// - sourceLocation: The source location to whych any recorded issues should
305
+ /// be attributed.
306
+ /// - body: The function to invoke.
307
+ ///
308
+ /// - Throws: A `PollingFailedError` will be thrown if the expression ever
309
+ /// returns false.
310
+ ///
311
+ /// Use polling confirmations to check that an event while a test is running in
312
+ /// complex scenarios where other forms of confirmation are insufficient. For
313
+ /// example, confirming that some state does not change.
314
+ @_spi ( Experimental)
315
+ @available ( macOS 13 , iOS 17 , watchOS 9 , tvOS 17 , visionOS 1 , * )
316
+ public func requireAlwaysPasses(
317
+ _ comment: Comment ? = nil ,
318
+ maxPollingIterations: Int ? = nil ,
319
+ pollingInterval: Duration ? = nil ,
320
+ isolation: isolated ( any Actor ) ? = #isolation,
321
+ sourceLocation: SourceLocation = #_sourceLocation,
322
+ _ body: @escaping ( ) async throws -> Bool
323
+ ) async throws {
324
+ let poller = Poller (
325
+ pollingBehavior: . passesAlways,
326
+ pollingIterations: getValueFromPollingTrait (
327
+ providedValue: maxPollingIterations,
328
+ default: defaultPollingConfiguration. maxPollingIterations,
329
+ \ConfirmAlwaysPassesConfigurationTrait . maxPollingIterations
330
+ ) ,
331
+ pollingInterval: getValueFromPollingTrait (
332
+ providedValue: pollingInterval,
333
+ default: defaultPollingConfiguration. pollingInterval,
334
+ \ConfirmAlwaysPassesConfigurationTrait . pollingInterval
335
+ ) ,
336
+ comment: comment,
337
+ sourceLocation: sourceLocation
338
+ )
339
+ let passed = await poller. evaluate ( raiseIssue: false , isolation: isolation) {
340
+ do {
341
+ return try await body ( )
342
+ } catch {
343
+ return false
344
+ }
345
+ }
346
+ if !passed {
347
+ throw PollingFailedError ( )
348
+ }
349
+ }
350
+
218
351
/// A helper function to de-duplicate the logic of grabbing configuration from
219
352
/// either the passed-in value (if given), the hardcoded default, and the
220
353
/// appropriate configuration trait.
@@ -368,23 +501,38 @@ private struct Poller {
368
501
/// Evaluate polling, and process the result, raising an issue if necessary.
369
502
///
370
503
/// - Parameters:
504
+ /// - raiseIssue: Whether or not to raise an issue.
505
+ /// This should only be false for `requirePassesEventually` or
506
+ /// `requireAlwaysPasses`.
507
+ /// - isolation: The isolation to use
371
508
/// - body: The expression to poll
509
+ ///
510
+ /// - Returns: Whether or not polling passed.
511
+ ///
372
512
/// - Side effects: If polling fails (see `PollingBehavior`), then this will
373
513
/// record an issue.
374
- func evaluate(
514
+ @discardableResult func evaluate(
515
+ raiseIssue: Bool = true ,
375
516
isolation: isolated ( any Actor ) ? ,
376
517
_ body: @escaping ( ) async -> Bool
377
- ) async {
518
+ ) async -> Bool {
378
519
precondition ( pollingIterations > 0 )
379
520
precondition ( pollingInterval > Duration . zero)
380
521
let result = await poll (
381
522
expression: body
382
523
)
383
- result. issue (
524
+ if let issue = result. issue (
384
525
comment: comment,
385
526
sourceContext: . init( backtrace: . current( ) , sourceLocation: sourceLocation) ,
386
527
pollingBehavior: pollingBehavior
387
- ) ? . record ( )
528
+ ) {
529
+ if raiseIssue {
530
+ issue. record ( )
531
+ }
532
+ return false
533
+ } else {
534
+ return true
535
+ }
388
536
}
389
537
390
538
/// This function contains the logic for continuously polling an expression,
0 commit comments