10
10
11
11
private import _TestingInternals
12
12
13
- #if !SWT_NO_EXIT_TESTS
14
- #if SWT_NO_PIPES
15
- #error("Support for exit tests requires support for (anonymous) pipes.")
16
- #endif
17
-
18
13
/// A type describing an exit test.
19
14
///
20
15
/// Instances of this type describe an exit test defined by the test author and
21
16
/// discovered or called at runtime.
22
- @_spi ( Experimental) @_spi ( ForToolsIntegrationOnly)
17
+ @_spi ( Experimental)
18
+ #if SWT_NO_EXIT_TESTS
19
+ @available ( * , unavailable, message: " Exit tests are not available on this platform. " )
20
+ #endif
23
21
public struct ExitTest : Sendable , ~ Copyable {
22
+ #if !SWT_NO_EXIT_TESTS
24
23
/// The expected exit condition of the exit test.
24
+ @_spi ( ForToolsIntegrationOnly)
25
25
public var expectedExitCondition : ExitCondition
26
26
27
27
/// The body closure of the exit test.
@@ -31,6 +31,7 @@ public struct ExitTest: Sendable, ~Copyable {
31
31
///
32
32
/// The source location is unique to each exit test and is consistent between
33
33
/// processes, so it can be used to uniquely identify an exit test at runtime.
34
+ @_spi ( ForToolsIntegrationOnly)
34
35
public var sourceLocation : SourceLocation
35
36
36
37
/// Disable crash reporting, crash logging, or core dumps for the current
@@ -83,6 +84,7 @@ public struct ExitTest: Sendable, ~Copyable {
83
84
/// to terminate the process; if it does not, the testing library will
84
85
/// terminate the process in a way that causes the corresponding expectation
85
86
/// to fail.
87
+ @_spi ( ForToolsIntegrationOnly)
86
88
public consuming func callAsFunction( ) async -> Never {
87
89
Self . _disableCrashReporting ( )
88
90
@@ -98,8 +100,13 @@ public struct ExitTest: Sendable, ~Copyable {
98
100
let expectingFailure = expectedExitCondition == . failure
99
101
exit ( expectingFailure ? EXIT_SUCCESS : EXIT_FAILURE)
100
102
}
103
+ #endif
101
104
}
102
105
106
+ #if !SWT_NO_EXIT_TESTS
107
+ #if SWT_NO_PIPES
108
+ #error("Support for exit tests requires support for (anonymous) pipes.")
109
+ #endif
103
110
// MARK: - Discovery
104
111
105
112
/// A protocol describing a type that contains an exit test.
@@ -131,6 +138,7 @@ extension ExitTest {
131
138
///
132
139
/// - Returns: The specified exit test function, or `nil` if no such exit test
133
140
/// could be found.
141
+ @_spi ( ForToolsIntegrationOnly)
134
142
public static func find( at sourceLocation: SourceLocation ) -> Self ? {
135
143
var result : Self ?
136
144
@@ -176,35 +184,47 @@ func callExitTest(
176
184
isRequired: Bool ,
177
185
isolation: isolated ( any Actor ) ? = #isolation,
178
186
sourceLocation: SourceLocation
179
- ) async -> Result < Void , any Error > {
187
+ ) async -> ExitTest . Result {
180
188
guard let configuration = Configuration . current ?? Configuration . all. first else {
181
189
preconditionFailure ( " A test must be running on the current task to use #expect(exitsWith:). " )
182
190
}
183
191
184
- let actualExitCondition : ExitCondition
192
+ var result : ExitTest . Result
185
193
do {
186
194
let exitTest = ExitTest ( expectedExitCondition: expectedExitCondition, sourceLocation: sourceLocation)
187
- actualExitCondition = try await configuration. exitTestHandler ( exitTest)
195
+ result = try await configuration. exitTestHandler ( exitTest)
188
196
} catch {
189
197
// An error here would indicate a problem in the exit test handler such as a
190
198
// failure to find the process' path, to construct arguments to the
191
- // subprocess, or to spawn the subprocess. These are not expected to be
192
- // common issues, however they would constitute a failure of the test
193
- // infrastructure rather than the test itself and perhaps should not cause
194
- // the test to terminate early.
195
- let issue = Issue ( kind: . errorCaught( error) , comments: comments ( ) , sourceContext: . init( backtrace: . current( ) , sourceLocation: sourceLocation) )
199
+ // subprocess, or to spawn the subprocess. Such failures are system issues,
200
+ // not test issues, because they constitute failures of the test
201
+ // infrastructure rather than the test itself.
202
+ //
203
+ // But here's a philosophical question: should the exit test also fail with
204
+ // an expectation failure? Arguably no, because the issue is a systemic one
205
+ // and (presumably) not a bug in the test. But also arguably yes, because
206
+ // the exit test did not do what the test author expected it to do.
207
+ let backtrace = Backtrace ( forFirstThrowOf: error) ?? . current( )
208
+ let issue = Issue (
209
+ kind: . system,
210
+ comments: comments ( ) + CollectionOfOne( Comment ( rawValue: String ( describingForTest: error) ) ) ,
211
+ sourceContext: SourceContext ( backtrace: backtrace, sourceLocation: sourceLocation)
212
+ )
196
213
issue. record ( configuration: configuration)
197
214
198
- return __checkValue (
199
- false ,
200
- expression: expression,
201
- comments: comments ( ) ,
202
- isRequired: isRequired,
203
- sourceLocation: sourceLocation
204
- )
215
+ // For lack of a better way to handle an exit test failing in this way,
216
+ // we record the system issue above, then let the expectation fail below by
217
+ // reporting an exit condition that's the inverse of the expected one.
218
+ result = ExitTest . Result ( exitCondition: expectedExitCondition == . failure ? . success : . failure)
205
219
}
206
220
207
- return __checkValue (
221
+ // How did the exit test actually exit?
222
+ let actualExitCondition = result. exitCondition
223
+
224
+ // Plumb the resulting exit condition through the general expectation
225
+ // machinery. If the expectation failed, capture the ExpectationFailedError
226
+ // instance so that calls to #require(exitsWith:) throw it correctly.
227
+ let checkResult = __checkValue (
208
228
expectedExitCondition == actualExitCondition,
209
229
expression: expression,
210
230
expressionWithCapturedRuntimeValues: expression. capturingRuntimeValues ( actualExitCondition) ,
@@ -213,6 +233,11 @@ func callExitTest(
213
233
isRequired: isRequired,
214
234
sourceLocation: sourceLocation
215
235
)
236
+ if case let . failure( error) = checkResult {
237
+ result. caughtError = error
238
+ }
239
+
240
+ return result
216
241
}
217
242
218
243
// MARK: - SwiftPM/tools integration
@@ -223,7 +248,8 @@ extension ExitTest {
223
248
/// - Parameters:
224
249
/// - exitTest: The exit test that is starting.
225
250
///
226
- /// - Returns: The condition under which the exit test exited.
251
+ /// - Returns: The result of the exit test including the condition under which
252
+ /// it exited.
227
253
///
228
254
/// - Throws: Any error that prevents the normal invocation or execution of
229
255
/// the exit test.
@@ -239,7 +265,8 @@ extension ExitTest {
239
265
/// are available or the child environment is otherwise terminated. The parent
240
266
/// environment is then responsible for interpreting those results and
241
267
/// recording any issues that occur.
242
- public typealias Handler = @Sendable ( _ exitTest: borrowing ExitTest ) async throws -> ExitCondition
268
+ @_spi ( ForToolsIntegrationOnly)
269
+ public typealias Handler = @Sendable ( _ exitTest: borrowing ExitTest ) async throws -> ExitTest . Result
243
270
244
271
/// The back channel file handle set up by the parent process.
245
272
///
@@ -337,7 +364,7 @@ extension ExitTest {
337
364
// or unsetenv(), so we need to recompute the child environment each time.
338
365
// The executable and XCTest bundle paths should not change over time, so we
339
366
// can precompute them.
340
- let childProcessExecutablePath = Result { try CommandLine . executablePath }
367
+ let childProcessExecutablePath = Swift . Result { try CommandLine . executablePath }
341
368
342
369
// Construct appropriate arguments for the child process. Generally these
343
370
// arguments are going to be whatever's necessary to respawn the current
@@ -418,7 +445,7 @@ extension ExitTest {
418
445
childEnvironment [ " SWT_EXPERIMENTAL_EXIT_TEST_SOURCE_LOCATION " ] = String ( decoding: json, as: UTF8 . self)
419
446
}
420
447
421
- return try await withThrowingTaskGroup ( of: ExitCondition ? . self) { taskGroup in
448
+ return try await withThrowingTaskGroup ( of: ExitTest . Result ? . self) { taskGroup in
422
449
// Create a "back channel" pipe to handle events from the child process.
423
450
let backChannel = try FileHandle . Pipe ( )
424
451
@@ -453,7 +480,8 @@ extension ExitTest {
453
480
454
481
// Await termination of the child process.
455
482
taskGroup. addTask {
456
- try await wait ( for: processID)
483
+ let exitCondition = try await wait ( for: processID)
484
+ return ExitTest . Result ( exitCondition: exitCondition)
457
485
}
458
486
459
487
// Read back all data written to the back channel by the child process
0 commit comments