-
Notifications
You must be signed in to change notification settings - Fork 108
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
base: main
Are you sure you want to change the base?
Conversation
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() | ||
} |
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.
Per discussion in forum, this is unsafe & needs to be rethought.
private actor Recorder<R: Sendable> { | ||
var lastValue: R? | ||
|
||
/// Record a new value to be returned | ||
func record(value: R) { | ||
self.lastValue = value | ||
} | ||
} |
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.
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.
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() | ||
} |
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.
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.
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) | ||
} |
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 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))
}
fileprivate func isCloseTo(other: Self, within delta: Self) -> Bool { | ||
var distance = self - other | ||
if (distance < Self.zero) { | ||
distance *= -1 | ||
} | ||
return distance <= delta | ||
} |
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 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.
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.
swift-numerics has this, more or less, but we can't add it as a dependency of course. :)
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 theconfirmation
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: