Skip to content

Commit ef109b0

Browse files
committed
Rewrite confirmPassesEventually when returning an optional to remove the PollingRecorder actor.
Now, this uses a separate method for evaluating polling to remove that actor.
1 parent bc01e1b commit ef109b0

File tree

1 file changed

+84
-27
lines changed

1 file changed

+84
-27
lines changed

Sources/Testing/Polling/Polling.swift

Lines changed: 84 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -190,9 +190,8 @@ public func confirmPassesEventually<R>(
190190
pollingInterval: Duration? = nil,
191191
isolation: isolated (any Actor)? = #isolation,
192192
sourceLocation: SourceLocation = #_sourceLocation,
193-
_ body: @escaping () async throws -> R?
194-
) async throws -> R where R: Sendable {
195-
let recorder = PollingRecorder<R>()
193+
_ body: @escaping () async throws -> sending R?
194+
) async throws -> R {
196195
let poller = Poller(
197196
pollingBehavior: .passesOnce,
198197
pollingIterations: getValueFromPollingTrait(
@@ -208,15 +207,15 @@ public func confirmPassesEventually<R>(
208207
comment: comment,
209208
sourceLocation: sourceLocation
210209
)
211-
await poller.evaluate(isolation: isolation) {
210+
let recordedValue = await poller.evaluate(isolation: isolation) {
212211
do {
213-
return try await recorder.record(value: body())
212+
return try await body()
214213
} catch {
215-
return false
214+
return nil
216215
}
217216
}
218217

219-
if let value = await recorder.lastValue {
218+
if let value = recordedValue {
220219
return value
221220
}
222221
throw PollingFailedError()
@@ -375,26 +374,6 @@ private func getValueFromPollingTrait<TraitKind, Value>(
375374
return traitValues.last ?? `default`
376375
}
377376

378-
/// A type to record the last value returned by a closure returning an optional
379-
/// This is only used in the `confirm` polling functions evaluating an optional.
380-
private actor PollingRecorder<R: Sendable> {
381-
var lastValue: R?
382-
383-
/// Record a new value to be returned
384-
func record(value: R) {
385-
self.lastValue = value
386-
}
387-
388-
func record(value: R?) -> Bool {
389-
if let value {
390-
self.lastValue = value
391-
return true
392-
} else {
393-
return false
394-
}
395-
}
396-
}
397-
398377
/// A type for managing polling
399378
@available(macOS 13, iOS 17, watchOS 9, tvOS 17, visionOS 1, *)
400379
private struct Poller {
@@ -567,4 +546,82 @@ private struct Poller {
567546
}
568547
return .ranToCompletion
569548
}
549+
550+
/// Evaluate polling, and process the result, raising an issue if necessary.
551+
///
552+
/// - Note: This method is only intended to be used when pollingBehavior is
553+
/// `.passesOnce`
554+
///
555+
/// - Parameters:
556+
/// - raiseIssue: Whether or not to raise an issue.
557+
/// This should only be false for `requirePassesEventually` or
558+
/// `requireAlwaysPasses`.
559+
/// - isolation: The isolation to use
560+
/// - body: The expression to poll
561+
///
562+
/// - Returns: the value if polling passed, nil otherwise.
563+
///
564+
/// - Side effects: If polling fails (see `PollingBehavior`), then this will
565+
/// record an issue.
566+
@discardableResult func evaluate<R>(
567+
raiseIssue: Bool = true,
568+
isolation: isolated (any Actor)?,
569+
_ body: @escaping () async -> sending R?
570+
) async -> R? {
571+
precondition(pollingIterations > 0)
572+
precondition(pollingInterval > Duration.zero)
573+
let (result, value) = await poll(
574+
expression: body
575+
)
576+
if let issue = result.issue(
577+
comment: comment,
578+
sourceContext: .init(backtrace: .current(), sourceLocation: sourceLocation),
579+
pollingBehavior: pollingBehavior
580+
) {
581+
if raiseIssue {
582+
issue.record()
583+
}
584+
return value
585+
} else {
586+
return value
587+
}
588+
}
589+
590+
/// This function contains the logic for continuously polling an expression,
591+
/// as well as processing the results of that expression
592+
///
593+
/// - Note: This method is only intended to be used when pollingBehavior is
594+
/// `.passesOnce`
595+
///
596+
/// - Parameters:
597+
/// - expression: An expression to continuously evaluate
598+
/// - behavior: The polling behavior to use
599+
/// - timeout: How long to poll for unitl the timeout triggers.
600+
/// - Returns: The result of this polling and the most recent value if the
601+
/// result is .finished, otherwise nil.
602+
private func poll<R>(
603+
isolation: isolated (any Actor)? = #isolation,
604+
expression: @escaping () async -> sending R?
605+
) async -> (PollResult, R?) {
606+
for iteration in 0..<pollingIterations {
607+
let lastResult = await expression()
608+
if let result = pollingBehavior.processFinishedExpression(
609+
expressionResult: lastResult != nil
610+
) {
611+
return (result, lastResult)
612+
}
613+
if iteration == (pollingIterations - 1) {
614+
// don't bother sleeping if it's the last iteration.
615+
break
616+
}
617+
do {
618+
try await Task.sleep(for: pollingInterval)
619+
} catch {
620+
// `Task.sleep` should only throw an error if it's cancelled
621+
// during the sleep period.
622+
return (.cancelled, nil)
623+
}
624+
}
625+
return (.ranToCompletion, nil)
626+
}
570627
}

0 commit comments

Comments
 (0)