Skip to content

Polling expectations (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

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

younata
Copy link

@younata younata commented May 15, 2025

This implements polling expectations, 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
Comment on lines +217 to +225
taskGroup.addTask {
do {
try await Task.sleep(for: timeout)
} catch {}
// Task.sleep will only throw if it's cancelled, at which point this
// taskgroup has already returned and we don't care about the value
// returned here.
return await pollingProcessor.didTimeout()
}
Copy link
Author

Choose a reason for hiding this comment

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

Per discussion in forum, this is unsafe & needs to be rethought.

Comment on lines +176 to +183
private actor Recorder<R: Sendable> {
var lastValue: R?

/// Record a new value to be returned
func record(value: R) {
self.lastValue = value
}
}
Copy link
Author

Choose a reason for hiding this comment

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

Ugh. I really dislike this approach, but I wanted to get this PR up so that others could actually test this proposal out & I couldn't think of a better way to handle this. Hopefully one of the benefits of a non-timed-based approach to polling will be that returning values will be much simpler/not a tacked-on hack.

Comment on lines +48 to +85
switch result {
case .timedOut:
expectation.isPassing = false
Issue(
kind: .expectationFailed(expectation),
comments: comments,
sourceContext: sourceContext
).record()
case .timedOutWithoutRunning:
expectation.isPassing = false
Issue(
kind: .expectationFailed(expectation),
comments: comments,
sourceContext: sourceContext
).record()
case .finished:
return __checkValue(
true,
expression: expression,
comments: comments,
isRequired: isRequired,
sourceLocation: sourceLocation
)
case .failed:
return __checkValue(
false,
expression: expression,
comments: comments,
isRequired: isRequired,
sourceLocation: sourceLocation
)
case .cancelled:
Issue(
kind: .system,
comments: comments,
sourceContext: sourceContext
).record()
}
Copy link
Author

Choose a reason for hiding this comment

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

a) I'm going to deduplicate this analysis with the nearly-identical section in the other version of run. But time constraints & a desire to let others try out this code were why this was left in.
b) I'd really like to be able to provide additional explanations behind why each issue is recorded, but it wasn't clear what additional arguments to Issue (if any, maybe I need to set a property and I missed that?) I should use to provide that additional context.

Comment on lines +38 to +48
await confirmation("Polling failed", expectedCount: 1) { failed in
var configuration = Configuration()
configuration.eventHandler = { event, _ in
if case .issueRecorded = event.kind {
failed()
}
}
await Test {
await #expect(until: .passesOnce) { false }
}.run(configuration: configuration)
}
Copy link
Author

Choose a reason for hiding this comment

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

I'm going to write a test helper to make it much easier to verify that issues were reported, as withKnownIssue is the wrong semantics for what I want. I'm also tempted to make it available under ForToolsIntegrationOnly, but I also think that should be a separate pitch?

I really want something that I can use like:

let issues = await collectIssues {
    #expect(Bool(false))
}

Comment on lines +337 to +343
fileprivate func isCloseTo(other: Self, within delta: Self) -> Bool {
var distance = self - other
if (distance < Self.zero) {
distance *= -1
}
return distance <= delta
}
Copy link
Author

@younata younata May 19, 2025

Choose a reason for hiding this comment

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

I want this to be in the swift standard library and apply to all numeric types. It would make comparing floating point types so much more doable, but is also useful for integer types when you only care that a value is close to another value.

In my ideal world, this would work like 1.000001 == 1 ± 1e-3 (or 1.00001 == 1 +- 1e-3, to avoid using the hard-to-type ± symbol). But that would involve adding another ternary operator to the language (... not quite. You could do this without adding another ternary operator), which would be much more involved (and far less likely to succeed) than adding a method to a protocol in the standard library.

Copy link
Contributor

Choose a reason for hiding this comment

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

swift-numerics has this, more or less, but we can't add it as a dependency of course. :)

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.

2 participants