Skip to content

[Experimental] Capturing values in exit tests #1040

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 12 commits into
base: main
Choose a base branch
from

Conversation

grynspan
Copy link
Contributor

@grynspan grynspan commented Mar 27, 2025

This PR implements an experimental form of state capture in exit tests. If you specify a capture list on the test's body closure and explicitly write the type of each captured value, and each value conforms to Sendable, Codable, and (implicitly) Copyable, we'll encode them and send them "over the wire" to the child process:

let a = Int.random(in: 100 ..< 200)
await #expect(exitsWith: .failure) { [a = a as Int] in
  assert(a > 500)
}

This PR is incomplete. Among other details:

  • Need to properly transmit the data, not stuff it in an environment variable
  • Need to implement diagnostics correctly
  • Need to figure out if ExitTest.CapturedValue and __Expression.Value have any synergy. (They do, but it's beyond the scope of this initial/experimental PR.)

We are ultimately constrained by the language here as we don't have real type information for the captured values, nor can we infer captures by inspecting the syntax of the exit test body (hence the need for an explicit capture list with types.) If we had something like decltype() we could apply during macro expansion, you wouldn't need to write x = x as T and could just write x. The macro would use decltype() to produce a thunk function of the form:

@Sendable func __compiler_generated_name__(: decltype(a),: decltype(b),: decltype(c)) async throws {
  let (a, b, c) = (,,)
  try await { /* ... */ }()
}

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 exit-tests ☠️ Work related to exit tests parameterized-testing Related to parameterized testing functionality macros 🔭 Related to Swift macros such as @Test or #expect labels Mar 27, 2025
@grynspan grynspan added this to the Swift 6.x milestone Mar 27, 2025
@grynspan grynspan self-assigned this Mar 27, 2025
@grynspan
Copy link
Contributor Author

@swift-ci test

4 similar comments
@grynspan
Copy link
Contributor Author

@swift-ci test

@grynspan
Copy link
Contributor Author

@swift-ci test

@grynspan
Copy link
Contributor Author

@swift-ci test

@grynspan
Copy link
Contributor Author

@swift-ci test

@grynspan grynspan force-pushed the jgrynspan/exit-test-value-capture branch from c2a2534 to 74101e0 Compare March 29, 2025 17:34
@grynspan
Copy link
Contributor Author

@swift-ci test

3 similar comments
@grynspan
Copy link
Contributor Author

@swift-ci test

@grynspan
Copy link
Contributor Author

@swift-ci test

@grynspan
Copy link
Contributor Author

@swift-ci test

@grynspan grynspan force-pushed the jgrynspan/exit-test-value-capture branch from ec3ff3d to 514ed01 Compare March 31, 2025 19:10
@grynspan
Copy link
Contributor Author

@swift-ci test

1 similar comment
@grynspan
Copy link
Contributor Author

grynspan commented Apr 1, 2025

@swift-ci test

@grynspan grynspan changed the title Capturing values in exit tests [WIP, DNM] Capturing values in exit tests Apr 1, 2025
@grynspan grynspan force-pushed the jgrynspan/exit-test-value-capture branch from 9f06c93 to 6db82cf Compare April 1, 2025 18:06
@grynspan grynspan changed the title [WIP, DNM] Capturing values in exit tests [Experimental] Capturing values in exit tests Apr 6, 2025
This PR implements an experimental form of state capture in exit tests. If you
specify a capture list on the test's body closure and explicitly write the type
of each captured value, _and_ each value conforms to `Sendable`, `Codable`, and
(implicitly) `Copyable`, we'll encode them and send them "over the wire" to the
child process:

```swift
let a = Int.random(in: 100 ..< 200)
await #expect(exitsWith: .failure) { [a = a as Int] in
  assert(a > 500)
}
```

This PR is incomplete. Among other details:

- [] Need to properly transmit the data, not stuff it in an environment variable
- [] Need to capture source location information correctly for error handling
     during value decoding
- [] Need to implement diagnostics correctly

We are ultimately constrained by the language here as we don't have real type
information for the captured values, nor can we infer captures by inspecting the
syntax of the exit test body (hence the need for an explicit capture list with
types.) If we had something like `decltype()` we could apply during macro
expansion, you wouldn't need to write `x = x as T` and could just write `x`.
@grynspan grynspan force-pushed the jgrynspan/exit-test-value-capture branch from 6db82cf to 6f87c49 Compare April 6, 2025 21:41
@grynspan
Copy link
Contributor Author

grynspan commented Apr 6, 2025

@swift-ci test

@grynspan grynspan marked this pull request as ready for review April 6, 2025 21:44
@grynspan
Copy link
Contributor Author

grynspan commented Apr 8, 2025

@swift-ci test

@grynspan
Copy link
Contributor Author

grynspan commented Apr 8, 2025

@swift-ci test

@grynspan
Copy link
Contributor Author

grynspan commented Apr 9, 2025

@swift-ci test

@grynspan
Copy link
Contributor Author

grynspan commented Apr 9, 2025

@swift-ci test

@grynspan
Copy link
Contributor Author

@swift-ci test

@grynspan
Copy link
Contributor Author

@swift-ci test

@grynspan grynspan requested a review from briancroom April 14, 2025 22:18
@grynspan
Copy link
Contributor Author

@swift-ci test

extension Array where Element == ExitTest.CapturedValue {
init<each T>(_ wrappedValues: repeat each T) where repeat each T: Codable & Sendable {
self.init()
repeat self.append(ExitTest.CapturedValue(wrappedValue: each wrappedValues))
Copy link
Contributor

Choose a reason for hiding this comment

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

Any reason not to use parameter pack iteration here and below?

// Make sure the value is of the correct type. If it's not, that's also
// probably a problem with the exit test handler or entry point.
guard let wrappedValue = wrappedValue as? U else {
fatalError("Expected captured value at index \(capturedValues.startIndex) with type '\(String(describingForTest: U.self))', but found an instance of '\(String(describingForTest: Swift.type(of: wrappedValue)))' instead.")
Copy link
Contributor

@stmontgomery stmontgomery Apr 15, 2025

Choose a reason for hiding this comment

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

Include bug report link in these fatalErrors?

Comment on lines -585 to +668
?? parentArguments.dropFirst().last
?? parentArguments.dropFirst().last
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: Intentional indentation change?

Comment on lines +765 to +766
// Create another pipe to send captured values (and possibly other state
// in the future) to the child process.
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this the only "from parent to child" pipe, or was there another one previously and this is a second? (I know there's a pipe in the opposite direction, just asking whether this is the only one which goes from parent to "exit test" child.)

Comment on lines 775 to +778
childEnvironment["SWT_EXPERIMENTAL_BACKCHANNEL"] = backChannelEnvironmentVariable
}
if let capturedValuesEnvironmentVariable = _makeEnvironmentVariable(for: capturedValuesReadEnd) {
childEnvironment["SWT_EXPERIMENTAL_CAPTURED_VALUES"] = capturedValuesEnvironmentVariable
Copy link
Contributor

Choose a reason for hiding this comment

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

I'd find it nice if the two env vars used a similar naming convention as each other

}
let capturedValuesJSON = try fileHandle.readToEnd()
let capturedValuesJSONLines = capturedValuesJSON.split(whereSeparator: \.isASCIINewline)
assert(capturedValues.count == capturedValuesJSONLines.count, "Expected to decode \(capturedValues.count) captured value(s) for the current exit test, but received \(capturedValuesJSONLines.count). Please file a bug report at https://github.com/swiftlang/swift-testing/issues/new")
Copy link
Contributor

Choose a reason for hiding this comment

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

Given this is an assert (and thus disabled in optimized/release builds), the bug report link seems less important here. Or should it be precondition, or something stronger?

Comment on lines +16 to +19
#if !SWT_NO_LIBRARY_MACRO_PLUGINS
private import Foundation
private import SwiftParser
#endif
Copy link
Contributor

Choose a reason for hiding this comment

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

Curious about these import changes: what is Foundation being used for, and why are they guarded by !SWT_NO_LIBRARY_MACRO_PLUGINS even though that guard doesn't appear anywhere in this file?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request exit-tests ☠️ Work related to exit tests macros 🔭 Related to Swift macros such as @Test or #expect parameterized-testing Related to parameterized testing functionality public-api Affects public API
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants