-
Notifications
You must be signed in to change notification settings - Fork 125
Polling confirmations (under Experimental spi) #1115
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
74262d2 to
3469f50
Compare
3469f50 to
d2f979a
Compare
There was a problem hiding this 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.
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.
… confirmation failed
Mark defaultPollingConfiguration as private Add polling files to CMakeLists Move Polling and Confirmations.swift to a new Confirmations directory Rename PollingFailureReason to PallingFailedError.Reason
3ff21e5 to
454cca8
Compare
|
The link in the description links to the old path for the proposal doc. I think it should be updated to point to https://github.com/younata/swift-evolution/blob/younata/testing-polling-expectations/proposals/testing/NNNN-polling-confirmations.md instead (I think I have edit permissions to make this description change but wasn't sure if that would be polite to do 😅 ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Overall this looks great to me, and I mostly had a bunch of nitpicks about the docs. I'll caveat by saying I've only read through the proposal recently and skimmed the pitch forum post, so I hope I didn't rehash a bunch of previous questions :)
| /// with a matching stopCondition. | ||
| /// If no such trait has been added, then polling will be attempted for | ||
| /// about 1 second before recording an issue. | ||
| /// `duration` must be greater than 0. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Might also be worth mentioning that duration > interval based on the current implementation.
Although from a user perspective, should duration == interval be permissible? I would interpret that basically a single poll (so perhaps not very useful), but still a possible use case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for bringing this up!
I'll update the documentation & add some tests mentioning this.
I don't see any real harm in disallowing the case where duration == interval. It is not very useful - at least not at the moment - but to your point, it is a possible use case someone might have.
| /// | ||
| /// - Parameters: | ||
| /// - comment: A user-specified comment describing this confirmation. | ||
| /// - stopCondition: When to stop polling. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As obvious as it might sound, I think it would help to explicitly note that nil maps to the false stop condition. That could either go here or maybe in the docs for PollingStopCondition. Based on my reading of the docs for firstPass, I assumed it would only be used with Bool and not with optional return types as well:
/// Evaluates the expression until the first time it returns true.
/// If it does not pass once by the time the timeout is reached, then a
/// failure will be reported.
case firstPass
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's a good point. I'm going to update the docs for PollingStopCondition to be about pass/fail, and then have the documentation for both forms of confirmation specifically mention what is considered a pass/fail.
I'm also adding a test that explicitly documents the case where you return false when you pass in a closure returning Optional<Bool>.
| // large. In which case, we should fall back to Int.max. | ||
|
|
||
| let failureReason: PollingFailedError.Reason | ||
| switch await poll(iterations: iterations, expression: body) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should the isolation parameter for this method also get forwarded to poll?
I'm not too familiar with actor isolated parameters so it's totally possible I'm missing something here.
| @_spi(Experimental) | ||
| public enum PollingStopCondition: Sendable, Equatable, Codable { | ||
| /// Evaluates the expression until the first time it returns true. | ||
| /// If it does not pass once by the time the timeout is reached, then a |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: would it make sense to say duration instead of timeout here? I don't see "timeout" appearing elsewhere in the polling confirmation API or documentation.
| if let result { | ||
| return .succeeded(result) | ||
| } else { | ||
| return .continuePolling |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I realised too late my note below is overly pedantic and I actually think this function is fairly clear as-is. I kinda already typed it out and figured I might as well share it though :)
I'd find it a bit clearer to check for the "timeout" scenario here compared to the call site in poll:
| return .continuePolling | |
| return wasLastPollingAttempt ? .failed : .continuePolling |
Or alternatively an early check on the last polling attempt which would simplify the switch:
guard !wasLastPollingAttempt else {
return result.map { .succeeded($0) } ?? .failed
}
switch self {
// ...There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I appreciate the feedback, and while pedantic, I do agree that makes it easier to read.
I think this is a good suggestion for clarity purposes. It does make the last line in poll redundant (return .failed after the for loop exits), but necessary to satisfy the compiler. I'll add a comment to the end of that method explaining this.
| /// - Parameters: | ||
| /// - comment: A user-specified comment describing this confirmation. | ||
| /// - stopCondition: When to stop polling. | ||
| /// - duration: The expected length of time to continue polling for. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it also be worth mentioning that time to run the body isn't accounted for in duration? I can see someone unfamiliar with the full details of polling confirmations writing something like:
try await confirmation(until: .firstPass, within: .seconds(10)) {
tenSecondLongOperation()
}And then being surprised it runs much longer than 10 seconds total.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the awesome comprehensive tests!
| await incrementor.increment() != 0 | ||
| } | ||
| let count = await incrementor.count | ||
| #expect(await count == 10) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this await is unnecessary:
| #expect(await count == 10) | |
| #expect(count == 10) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm going to instead inline the await incrementor.count back into this and in the other places.
The reason this was separated out was to make debugging the test failure easier - the failure message when incrementor.count wasn't equal to 10 did not mention what the then-current value of incrementor was. But that's not necessary any longer.
| #expect(await incrementor.count == 50) | ||
| } | ||
|
|
||
| #if !SWT_NO_EXIT_TESTS |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice use of exit tests!
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
confirmationapi. However, I've found theconfirmationto 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: