@@ -14,6 +14,10 @@ internal let defaultPollingConfiguration = (
1414 pollingInterval: Duration . milliseconds ( 1 )
1515)
1616
17+ /// A type describing an error thrown when polling fails.
18+ @_spi ( Experimental)
19+ public struct PollingFailedError : Error , Equatable { }
20+
1721/// Confirm that some expression eventually returns true
1822///
1923/// - Parameters:
@@ -76,10 +80,73 @@ public func confirmPassesEventually(
7680 }
7781}
7882
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`.
81114@_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+ }
83150
84151/// Confirm that some expression eventually returns a non-nil value
85152///
@@ -108,7 +175,7 @@ public struct PollingFailedError: Error {}
108175/// - Returns: The first non-nil value returned by `body`.
109176///
110177/// - Throws: A `PollingFailedError` will be thrown if `body` never returns a
111- /// non-optional value
178+ /// non-optional value.
112179///
113180/// Use polling confirmations to check that an event while a test is running in
114181/// complex scenarios where other forms of confirmation are insufficient. For
@@ -215,6 +282,72 @@ public func confirmAlwaysPasses(
215282 }
216283}
217284
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+
218351/// A helper function to de-duplicate the logic of grabbing configuration from
219352/// either the passed-in value (if given), the hardcoded default, and the
220353/// appropriate configuration trait.
@@ -368,23 +501,38 @@ private struct Poller {
368501 /// Evaluate polling, and process the result, raising an issue if necessary.
369502 ///
370503 /// - 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
371508 /// - body: The expression to poll
509+ ///
510+ /// - Returns: Whether or not polling passed.
511+ ///
372512 /// - Side effects: If polling fails (see `PollingBehavior`), then this will
373513 /// record an issue.
374- func evaluate(
514+ @discardableResult func evaluate(
515+ raiseIssue: Bool = true ,
375516 isolation: isolated ( any Actor ) ? ,
376517 _ body: @escaping ( ) async -> Bool
377- ) async {
518+ ) async -> Bool {
378519 precondition ( pollingIterations > 0 )
379520 precondition ( pollingInterval > Duration . zero)
380521 let result = await poll (
381522 expression: body
382523 )
383- result. issue (
524+ if let issue = result. issue (
384525 comment: comment,
385526 sourceContext: . init( backtrace: . current( ) , sourceLocation: sourceLocation) ,
386527 pollingBehavior: pollingBehavior
387- ) ? . record ( )
528+ ) {
529+ if raiseIssue {
530+ issue. record ( )
531+ }
532+ return false
533+ } else {
534+ return true
535+ }
388536 }
389537
390538 /// This function contains the logic for continuously polling an expression,
0 commit comments