Skip to content

Commit 32c0a7d

Browse files
authored
Clean up exit test implementation post-merge. (#340)
This PR contains a bunch of tweaks/clean-up to the exit test implementation I landed in #307. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated.
1 parent 56b5f7c commit 32c0a7d

File tree

3 files changed

+54
-102
lines changed

3 files changed

+54
-102
lines changed

Sources/Testing/ExitTests/ExitTest.swift

Lines changed: 51 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -148,18 +148,7 @@ func callExitTest(
148148
let actualExitCondition: ExitCondition
149149
do {
150150
let exitTest = ExitTest(expectedExitCondition: expectedExitCondition, body: body, sourceLocation: sourceLocation)
151-
guard let exitCondition = try await configuration.exitTestHandler(exitTest) else {
152-
// This exit test was not run by the handler. Return successfully (and
153-
// move on to the next one.)
154-
return __checkValue(
155-
true,
156-
expression: expression,
157-
comments: comments(),
158-
isRequired: isRequired,
159-
sourceLocation: sourceLocation
160-
)
161-
}
162-
actualExitCondition = exitCondition
151+
actualExitCondition = try await configuration.exitTestHandler(exitTest)
163152
} catch {
164153
// An error here would indicate a problem in the exit test handler such as a
165154
// failure to find the process' path, to construct arguments to the
@@ -206,8 +195,7 @@ extension ExitTest {
206195
/// - Parameters:
207196
/// - exitTest: The exit test that is starting.
208197
///
209-
/// - Returns: The condition under which the exit test exited, or `nil` if the
210-
/// exit test was not invoked.
198+
/// - Returns: The condition under which the exit test exited.
211199
///
212200
/// - Throws: Any error that prevents the normal invocation or execution of
213201
/// the exit test.
@@ -223,7 +211,7 @@ extension ExitTest {
223211
/// are available or the child environment is otherwise terminated. The parent
224212
/// environment is then responsible for interpreting those results and
225213
/// recording any issues that occur.
226-
public typealias Handler = @Sendable (_ exitTest: borrowing ExitTest) async throws -> ExitCondition?
214+
public typealias Handler = @Sendable (_ exitTest: borrowing ExitTest) async throws -> ExitCondition
227215

228216
/// Find the exit test function specified by the given command-line arguments,
229217
/// if any.
@@ -256,59 +244,65 @@ extension ExitTest {
256244
/// For a description of the inputs and outputs of this function, see the
257245
/// documentation for ``ExitTest/Handler``.
258246
static func handlerForSwiftPM(forXCTestCaseIdentifiedBy xcTestCaseIdentifier: String? = nil) -> Handler {
259-
let parentEnvironment = ProcessInfo.processInfo.environment
260-
261-
return { exitTest in
262-
let actualExitCode: Int32
263-
let wasSignalled: Bool
264-
do {
265-
let childProcessURL: URL = try URL(fileURLWithPath: CommandLine.executablePath, isDirectory: false)
247+
// The environment could change between invocations if a test calls setenv()
248+
// or unsetenv(), so we need to recompute the child environment each time.
249+
// The executable and XCTest bundle paths should not change over time, so we
250+
// can precompute them.
251+
let childProcessExecutablePath = Result { try CommandLine.executablePath }
266252

267-
// We only need to pass arguments when hosted by XCTest.
268-
var childArguments = [String]()
269-
if let xcTestCaseIdentifier {
270-
#if os(macOS)
271-
childArguments += ["-XCTest", xcTestCaseIdentifier]
253+
// We only need to pass arguments when hosted by XCTest.
254+
let childArguments: [String] = {
255+
var result = [String]()
256+
if let xcTestCaseIdentifier {
257+
#if SWT_TARGET_OS_APPLE
258+
result += ["-XCTest", xcTestCaseIdentifier]
272259
#else
273-
childArguments.append(xcTestCaseIdentifier)
260+
result.append(xcTestCaseIdentifier)
274261
#endif
275-
if let xctestTargetPath = parentEnvironment["XCTestBundlePath"] {
276-
childArguments.append(xctestTargetPath)
277-
} else if let xctestTargetPath = CommandLine.arguments().last {
278-
childArguments.append(xctestTargetPath)
279-
}
262+
if let xctestTargetPath = Environment.variable(named: "XCTestBundlePath") {
263+
result.append(xctestTargetPath)
264+
} else if let xctestTargetPath = CommandLine.arguments().last {
265+
result.append(xctestTargetPath)
280266
}
267+
}
268+
return result
269+
}()
281270

282-
// Inherit the environment from the parent process and add our own
283-
// variable indicating which exit test will run, then make any necessary
284-
// platform-specific changes.
285-
var childEnvironment: [String: String] = parentEnvironment
286-
childEnvironment["SWT_EXPERIMENTAL_EXIT_TEST_SOURCE_LOCATION"] = try String(data: JSONEncoder().encode(exitTest.sourceLocation), encoding: .utf8)!
271+
return { exitTest in
272+
let childProcessURL = try URL(fileURLWithPath: childProcessExecutablePath.get(), isDirectory: false)
273+
274+
// Inherit the environment from the parent process and make any necessary
275+
// platform-specific changes.
276+
var childEnvironment = ProcessInfo.processInfo.environment
287277
#if SWT_TARGET_OS_APPLE
288-
if childEnvironment["XCTestSessionIdentifier"] != nil {
289-
// We need to remove Xcode's environment variables from the child
290-
// environment to avoid accidentally accidentally recursing.
291-
for key in childEnvironment.keys where key.starts(with: "XCTest") {
292-
childEnvironment.removeValue(forKey: key)
293-
}
294-
}
278+
// We need to remove Xcode's environment variables from the child
279+
// environment to avoid accidentally accidentally recursing.
280+
for key in childEnvironment.keys where key.starts(with: "XCTest") {
281+
childEnvironment.removeValue(forKey: key)
282+
}
295283
#elseif os(Linux)
296-
if childEnvironment["SWIFT_BACKTRACE"] == nil {
297-
// Disable interactive backtraces unless explicitly enabled to reduce
298-
// the noise level during the exit test. Only needed on Linux.
299-
childEnvironment["SWIFT_BACKTRACE"] = "enable=no"
300-
}
284+
if childEnvironment["SWIFT_BACKTRACE"] == nil {
285+
// Disable interactive backtraces unless explicitly enabled to reduce
286+
// the noise level during the exit test. Only needed on Linux.
287+
childEnvironment["SWIFT_BACKTRACE"] = "enable=no"
288+
}
301289
#endif
290+
// Insert a specific variable that tells the child process which exit test
291+
// to run.
292+
childEnvironment["SWT_EXPERIMENTAL_EXIT_TEST_SOURCE_LOCATION"] = try String(data: JSONEncoder().encode(exitTest.sourceLocation), encoding: .utf8)!
302293

294+
let actualExitCode: Int32
295+
let wasSignalled: Bool
296+
do {
303297
(actualExitCode, wasSignalled) = try await withCheckedThrowingContinuation { continuation in
298+
let process = Process()
299+
process.executableURL = childProcessURL
300+
process.arguments = childArguments
301+
process.environment = childEnvironment
302+
process.terminationHandler = { process in
303+
continuation.resume(returning: (process.terminationStatus, process.terminationReason == .uncaughtSignal))
304+
}
304305
do {
305-
let process = Process()
306-
process.executableURL = childProcessURL
307-
process.arguments = childArguments
308-
process.environment = childEnvironment
309-
process.terminationHandler = { process in
310-
continuation.resume(returning: (process.terminationStatus, process.terminationReason == .uncaughtSignal))
311-
}
312306
try process.run()
313307
} catch {
314308
continuation.resume(throwing: error)

Sources/Testing/Expectations/ExpectationChecking+Macro.swift

Lines changed: 3 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1076,47 +1076,13 @@ public func __checkClosureCall<R>(
10761076
/// Check that an expression always exits (terminates the current process) with
10771077
/// a given status.
10781078
///
1079-
/// This overload is used for `await #expect(exitsWith:) { }` invocations when
1080-
/// the body of the exit test is a synchronous function. Because no arguments
1081-
/// are passed into the exit test and no errors can be thrown, a C function
1082-
/// (`@convention(c)`) can be used to prevent accidentally closing over state
1083-
/// from the parent process.
1079+
/// This overload is used for `await #expect(exitsWith:) { }` invocations. Note
1080+
/// that the `body` argument is thin here because it cannot meaningfully capture
1081+
/// state from the enclosing context.
10841082
///
10851083
/// - Warning: This function is used to implement the `#expect()` and
10861084
/// `#require()` macros. Do not call it directly.
10871085
@_spi(Experimental)
1088-
public func __checkClosureCall(
1089-
exitsWith expectedExitCondition: ExitCondition,
1090-
performing body: @convention(c) () -> Void,
1091-
expression: Expression,
1092-
comments: @autoclosure () -> [Comment],
1093-
isRequired: Bool,
1094-
sourceLocation: SourceLocation
1095-
) async -> Result<Void, any Error> {
1096-
await callExitTest(
1097-
exitsWith: expectedExitCondition,
1098-
performing: { body() },
1099-
expression: expression,
1100-
comments: comments(),
1101-
isRequired: isRequired,
1102-
sourceLocation: sourceLocation
1103-
)
1104-
}
1105-
1106-
/// Check that an expression always exits (terminates the current process) with
1107-
/// a given status.
1108-
///
1109-
/// This overload is used for `await #expect(exitsWith:) { }` invocations when
1110-
/// the body of the exit test is an `async` function. The body must be
1111-
/// implemented using `@convention(thin)` to prevent accidentally closing over
1112-
/// state from the parent process, but the diagnostics emitted for thin
1113-
/// functions that close over state are less clear than those emitted for C
1114-
/// functions.
1115-
///
1116-
/// - Warning: This function is used to implement the `#expect()` and
1117-
/// `#require()` macros. Do not call it directly.
1118-
@_spi(Experimental)
1119-
@_disfavoredOverload
11201086
public func __checkClosureCall(
11211087
exitsWith expectedExitCondition: ExitCondition,
11221088
performing body: @convention(thin) () async -> Void,

Tests/TestingTests/ExitTestTests.swift

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -75,14 +75,6 @@ private import TestingInternals
7575
}
7676
}
7777

78-
// Mock an exit test that is not a match (and so is not run.)
79-
configuration.exitTestHandler = { _ in nil }
80-
await Test {
81-
await #expect(exitsWith: .success) {
82-
Issue.record("Unreachable")
83-
}
84-
}.run(configuration: configuration)
85-
8678
// Mock an exit test where the process exits successfully.
8779
configuration.exitTestHandler = { _ in
8880
return .exitCode(EXIT_SUCCESS)

0 commit comments

Comments
 (0)