-
Notifications
You must be signed in to change notification settings - Fork 120
Enable exit test value capturing #1165
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
Changes from all commits
41d94d2
f82a53c
e31531d
83ce833
c2c8cb9
1ff817d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -67,21 +67,7 @@ The parent process doesn't call the body of the exit test. Instead, the child | |
process treats the body of the exit test as its `main()` function and calls it | ||
directly. | ||
|
||
- Note: Because the body acts as the `main()` function of a new process, it | ||
can't capture any state originating in the parent process or from its lexical | ||
context. For example, the following exit test will fail to compile because it | ||
captures a variable declared outside the exit test itself: | ||
|
||
```swift | ||
@Test func `Customer won't eat food unless it's nutritious`() async { | ||
let isNutritious = false | ||
await #expect(processExitsWith: .failure) { | ||
var food = ... | ||
food.isNutritious = isNutritious // ❌ ERROR: trying to capture state here | ||
Customer.current.eat(food) | ||
} | ||
} | ||
``` | ||
<!-- TODO: discuss @MainActor isolation or lack thereof --> | ||
|
||
If the body returns before the child process exits, the process exits as if | ||
`main()` returned normally. If the body throws an error, Swift handles it as if | ||
|
@@ -106,6 +92,59 @@ status of the child process against the expected exit condition you passed. If | |
they match, the exit test passes; otherwise, it fails and the testing library | ||
records an issue. | ||
|
||
### Capture state from the parent process | ||
|
||
To pass information from the parent process to the child process, you specify | ||
the Swift values you want to pass in a [capture list](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/closures/#Capturing-Values) | ||
on the exit test's body: | ||
|
||
```swift | ||
@Test(arguments: Food.allJunkFood) | ||
func `Customer won't eat food unless it's nutritious`(_ food: Food) async { | ||
await #expect(processExitsWith: .failure) { [food] in | ||
Customer.current.eat(food) | ||
} | ||
} | ||
``` | ||
|
||
- Note: If you use this macro with a Swift compiler version lower than 6.3, it | ||
doesn't support capturing state. | ||
|
||
If a captured value is an argument to the current function or is `self`, its | ||
type is inferred at compile time. Otherwise, explicitly specify the type of the | ||
Comment on lines
+113
to
+114
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. "…the compiler infers the value's type." to avoid passive voice. |
||
value using the `as` operator: | ||
|
||
```swift | ||
@Test func `Customer won't eat food unless it's nutritious`() async { | ||
var food = ... | ||
food.isNutritious = false | ||
await #expect(processExitsWith: .failure) { [self, food = food as Food] in | ||
self.prepare(food) | ||
Customer.current.eat(food) | ||
} | ||
} | ||
``` | ||
|
||
Every value you capture in an exit test must conform to [`Sendable`](https://developer.apple.com/documentation/swift/sendable) | ||
and [`Codable`](https://developer.apple.com/documentation/swift/codable). Each | ||
value is encoded by the parent process using [`encode(to:)`](https://developer.apple.com/documentation/swift/encodable/encode(to:)) | ||
and is decoded by the child process [`init(from:)`](https://developer.apple.com/documentation/swift/decodable/init(from:)) | ||
before being passed to the exit test body. | ||
Comment on lines
+128
to
+132
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Passive voice here, too. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Feels worse somehow. Thoughts? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How about:
Because it's the testing library that actually does all of this. |
||
|
||
If a captured value's type does not conform to both `Sendable` and `Codable`, or | ||
if the value is not explicitly specified in the exit test body's capture list, | ||
the compiler emits an error: | ||
|
||
```swift | ||
@Test func `Customer won't eat food unless it's nutritious`() async { | ||
var food = ... | ||
food.isNutritious = false | ||
await #expect(processExitsWith: .failure) { | ||
Customer.current.eat(food) // ❌ ERROR: implicitly capturing 'food' | ||
} | ||
} | ||
``` | ||
|
||
### Gather output from the child process | ||
|
||
The ``expect(processExitsWith:observing:_:sourceLocation:performing:)`` and | ||
|
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'd suggest "the macro" instead of "this macro" here, to be clear you're talking about that one rather than
@Test
which the snippet also uses.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.
This language is the same as what we did for
#expect(throws:)
. Did you want to change it there too?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 meant to say "the
expect(processExitsWith:)
macro", not sure why that didn't come out. Anyway, if we're writing in the reference doc for a macro, "this macro" clearly refers to the one we're describing. In an article, and particularly one where we've just used two different macros in one example, there's no "this" binding.