Skip to content

Conversation

younata
Copy link

@younata younata commented May 15, 2025

This implements polling confirmations, as described in ST-NNNN.

Motivation:

Being able to monitor changes in the background is something of immense value. Swift Testing already provides an API for this in the confirmation api. However, I've found the confirmation to be hard to work with at times - it requires you to set up a callback for when something changes, which is not always possible or even the right thing to do. Polling provides a very general approach to monitoring all kinds of changes.

Modifications:

This adds a new set of macros for handling polling. A new public enum for the 2 separate polling behaviors, and new types to actually implement polling. All under the experimental spi.

Checklist:

  • Code and documentation should follow the style of the Style Guide.
  • If public symbols are renamed or modified, DocC references should be updated.

@grynspan grynspan added enhancement New feature or request public-api Affects public API api-proposal API proposal PRs (documentation only) issue-handling Related to Issue handling within the testing library macros 🔭 Related to Swift macros such as @Test or #expect labels May 18, 2025
@younata younata requested a review from jerryjrchen as a code owner August 19, 2025 21:22
@younata younata changed the title Polling expectations (under Experimental spi) Polling confirmations (under Experimental spi) Aug 19, 2025
@younata younata force-pushed the polling-expectations branch from 74262d2 to 3469f50 Compare September 8, 2025 22:10
/// ``confirmation(_:until:within:pollingEvery:isolation:sourceLocation:_:)-5tnlk``
/// whenever the polling fails, as described in ``PollingStopCondition``.
@_spi(Experimental)
case pollingConfirmationFailed
Copy link
Contributor

@grynspan grynspan Sep 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we include any associated values with this case? e.g. to inform the console output for this issue kind.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That seems reasonable to do. Maybe for if an error was thrown, and if the stopping condition fails?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's what I'm thinking right now:

/// A type defining why a Polling Confirmation failed
@_spi(Experimental)
public enum PollingConfirmationFailureReason: Sendable {
  /// The polling confirmation failed because the stop condition failed.
  ///
  /// If the stop condition is ``PollingStopCondition.firstPass``, then this
  /// this means that the polling confirmation exceeded the number of polling
  /// iterations without the `body` closure having returned true or a non-nil
  /// value.
  ///
  /// If the stop condition is ``PollingStopCondition.stopsPassing``, then this
  /// means that the `body` closure returned false or nil before the polling
  /// confirmation reached the number of polling iterations.
  case stopConditionFailed(PollingStopCondition)

  /// The polling confirmation failed because the `body` closure threw an error.
  indirect case errorThrown(_ error: any Error)
}

Thoughts?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If an error is thrown, do we want to treat that as the polling confirmation failing per se, or just a separate issue of kind .errorCaught() and the polling confirmation just doesn't record any sort of result?

Preliminary implementation of polling expectations

Make Polling into a function along the lines of Confirmation
Additionally, make PollingBehavior an implementation detail of polling, instead of exposed publicly
Removes any timeouts involved for polling, as they become increasingly unreliable
as the system runs more and more tests

Take in configuration arguments, add polling interval

Add traits for configuring polling

Use consistent naming between confirmAlwaysPasses and the related configuration trait
Stop unnecessarily waiting after the last polling attempt has finished.
Allow for subsequent polling configuration traits which specified nil for a value to fall back to earlier polling configuration traits before falling back to the default.

Add requirePassesEventually and requireAlwaysPasses
These two mirror their confirm counterparts, only throwing an error (instead of recording an issue) when they fail.

Rewrite confirmPassesEventually when returning an optional to remove the PollingRecorder actor.
Now, this uses a separate method for evaluating polling to remove that actor.

Clean up the duplicate Poller.evaluate/Poller.poll methods
Removed the duplicate poll method, and made evaluate-returning-bool into a wrapper for evaluate-returning-optional

Configure polling confirmations as timeout & polling interval
This is less direct, but much more intuitive for test authors.
Also add exit tests confirming that these values are non-negative

Rename to actually use the confirmation name
Follow more english-sentence-like guidance for function naming
Simplify the polling confirmation API down to just 2 public functions, 1 enum, and 1 error type.
Always throw an error when polling fails, get rid of the separate issue recording.

Use a single polling confirmation configuration trait
Instead of mulitple traits per stop condition, just have a single trait
per stop condition.
@younata younata force-pushed the polling-expectations branch from 3469f50 to d2f979a Compare September 22, 2025 03:56
Copy link
Contributor

@plemarquand plemarquand left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall looking really great. Just one comment and a bunch of documentation nits.


/// Default values for polling confirmations.
@available(_clockAPI, *)
internal let defaultPollingConfiguration = (
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this be private?

Suggested change
internal let defaultPollingConfiguration = (
private let _defaultPollingConfiguration = (

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New files, such as this one and PollingConfigurationTrait.swift, need to be added to the CMake scripts. Can you be sure to add them there? See the file list in Sources/Testing/CMakeLists.txt.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In terms of the placement of this new file, I'd like to suggest instead of creating a new Polling/ subdirectory we create a new Confirmations/ subdirectory, add move the existing Confirmation.swift to that new directory, and place this new file there too.

(And if you do take this suggestion and move some files around, be sure to adjust the CMake files accordingly.)


/// A type describing why polling failed
@_spi(Experimental)
public enum PollingFailureReason: Sendable, Codable {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this PollingFailureReason enum would feel more natural nested within PollingFailedError, and then its name could be simplified to Reason (or fully-qualified, PollingFailedError.Reason).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
api-proposal API proposal PRs (documentation only) enhancement New feature or request issue-handling Related to Issue handling within the testing library macros 🔭 Related to Swift macros such as @Test or #expect public-api Affects public API
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants