88// See https://swift.org/CONTRIBUTORS.txt for Swift project authors
99//
1010
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+
1117/// Confirm that some expression eventually returns true
1218///
1319/// - Parameters:
1420/// - comment: An optional comment to apply to any issues generated by this
1521/// 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.
1634/// - isolation: The actor to which `body` is isolated, if any.
1735/// - sourceLocation: The source location to whych any recorded issues should
1836/// be attributed.
2644@available ( macOS 13 , iOS 17 , watchOS 9 , tvOS 17 , visionOS 1 , * )
2745public func confirmPassesEventually(
2846 _ comment: Comment ? = nil ,
29- maxPollingIterations: Int = 1000 ,
30- pollingInterval: Duration = . milliseconds ( 1 ) ,
47+ maxPollingIterations: Int ? = nil ,
48+ pollingInterval: Duration ? = nil ,
3149 isolation: isolated ( any Actor ) ? = #isolation,
3250 sourceLocation: SourceLocation = #_sourceLocation,
3351 _ body: @escaping ( ) async throws -> Bool
3452) async {
3553 let poller = Poller (
3654 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+ ) ,
3965 comment: comment,
4066 sourceLocation: sourceLocation
4167 )
@@ -58,6 +84,18 @@ public struct PollingFailedError: Error {}
5884/// - Parameters:
5985/// - comment: An optional comment to apply to any issues generated by this
6086/// 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.
6199/// - isolation: The actor to which `body` is isolated, if any.
62100/// - sourceLocation: The source location to whych any recorded issues should
63101/// be attributed.
@@ -77,17 +115,25 @@ public struct PollingFailedError: Error {}
77115@discardableResult
78116public func confirmPassesEventually< R> (
79117 _ comment: Comment ? = nil ,
80- maxPollingIterations: Int = 1000 ,
81- pollingInterval: Duration = . milliseconds ( 1 ) ,
118+ maxPollingIterations: Int ? = nil ,
119+ pollingInterval: Duration ? = nil ,
82120 isolation: isolated ( any Actor ) ? = #isolation,
83121 sourceLocation: SourceLocation = #_sourceLocation,
84122 _ body: @escaping ( ) async throws -> R ?
85123) async throws -> R where R: Sendable {
86124 let recorder = PollingRecorder < R > ( )
87125 let poller = Poller (
88126 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+ ) ,
91137 comment: comment,
92138 sourceLocation: sourceLocation
93139 )
@@ -110,6 +156,18 @@ public func confirmPassesEventually<R>(
110156/// - Parameters:
111157/// - comment: An optional comment to apply to any issues generated by this
112158/// 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.
113171/// - isolation: The actor to which `body` is isolated, if any.
114172/// - sourceLocation: The source location to whych any recorded issues should
115173/// be attributed.
@@ -122,16 +180,24 @@ public func confirmPassesEventually<R>(
122180@available ( macOS 13 , iOS 17 , watchOS 9 , tvOS 17 , visionOS 1 , * )
123181public func confirmAlwaysPasses(
124182 _ comment: Comment ? = nil ,
125- maxPollingIterations: Int = 1000 ,
126- pollingInterval: Duration = . milliseconds ( 1 ) ,
183+ maxPollingIterations: Int ? = nil ,
184+ pollingInterval: Duration ? = nil ,
127185 isolation: isolated ( any Actor ) ? = #isolation,
128186 sourceLocation: SourceLocation = #_sourceLocation,
129187 _ body: @escaping ( ) async throws -> Bool
130188) async {
131189 let poller = Poller (
132190 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+ ) ,
135201 comment: comment,
136202 sourceLocation: sourceLocation
137203 )
@@ -144,48 +210,34 @@ public func confirmAlwaysPasses(
144210 }
145211}
146212
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.
148216///
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`.
156221///
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`
188239 }
240+ return trait [ keyPath: keyPath]
189241}
190242
191243/// A type to record the last value returned by a closure returning an optional
@@ -321,6 +373,8 @@ private struct Poller {
321373 isolation: isolated ( any Actor ) ? ,
322374 _ body: @escaping ( ) async -> Bool
323375 ) async {
376+ precondition ( pollingIterations > 0 )
377+ precondition ( pollingInterval > Duration . zero)
324378 let result = await poll (
325379 expression: body
326380 )
0 commit comments