Skip to content

Commit 417cb17

Browse files
committed
[SWT-NNNN] Exit tests
One of the first enhancement requests we received for swift-testing was the ability to test for precondition failures and other critical failures that terminate the current process when they occur. This feature is also frequently requested for XCTest. With swift-testing, we have the opportunity to build such a feature in an ergonomic way. Read the full proposal [here](https://github.com/apple/swift-testing/blob/jgrynspan/exit-tests-proposal/Documentation/Proposals/NNNN-exit-tests.md).
1 parent 44022f4 commit 417cb17

11 files changed

+888
-23
lines changed

Documentation/Proposals/NNNN-exit-tests.md

+787
Large diffs are not rendered by default.

Sources/Testing/ExitTests/ExitTest.Condition.swift

+39-2
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010

1111
private import _TestingInternals
1212

13-
@_spi(Experimental)
1413
#if SWT_NO_EXIT_TESTS
1514
@available(*, unavailable, message: "Exit tests are not available on this platform.")
1615
#endif
@@ -21,6 +20,22 @@ extension ExitTest {
2120
/// exit test is expected to pass or fail by passing them to
2221
/// ``expect(exitsWith:observing:_:sourceLocation:performing:)`` or
2322
/// ``require(exitsWith:observing:_:sourceLocation:performing:)``.
23+
///
24+
/// ## Topics
25+
///
26+
/// ### Successful exit conditions
27+
///
28+
/// - ``success``
29+
///
30+
/// ### Failing exit conditions
31+
///
32+
/// - ``failure``
33+
/// - ``exitCode(_:)``
34+
/// - ``signal(_:)``
35+
///
36+
/// @Metadata {
37+
/// @Available(Swift, introduced: 6.2)
38+
/// }
2439
public struct Condition: Sendable {
2540
/// An enumeration describing the possible conditions for an exit test.
2641
private enum _Kind: Sendable, Equatable {
@@ -38,13 +53,20 @@ extension ExitTest {
3853

3954
// MARK: -
4055

41-
@_spi(Experimental)
4256
#if SWT_NO_EXIT_TESTS
4357
@available(*, unavailable, message: "Exit tests are not available on this platform.")
4458
#endif
4559
extension ExitTest.Condition {
4660
/// A condition that matches when a process terminates successfully with exit
4761
/// code `EXIT_SUCCESS`.
62+
///
63+
/// The C programming language defines two [standard exit codes](https://en.cppreference.com/w/c/program/EXIT_status),
64+
/// `EXIT_SUCCESS` and `EXIT_FAILURE` as well as `0` (as a synonym for
65+
/// `EXIT_SUCCESS`.)
66+
///
67+
/// @Metadata {
68+
/// @Available(Swift, introduced: 6.2)
69+
/// }
4870
public static var success: Self {
4971
// Strictly speaking, the C standard treats 0 as a successful exit code and
5072
// potentially distinct from EXIT_SUCCESS. To my knowledge, no modern
@@ -59,10 +81,17 @@ extension ExitTest.Condition {
5981

6082
/// A condition that matches when a process terminates abnormally with any
6183
/// exit code other than `EXIT_SUCCESS` or with any signal.
84+
///
85+
/// @Metadata {
86+
/// @Available(Swift, introduced: 6.2)
87+
/// }
6288
public static var failure: Self {
6389
Self(_kind: .failure)
6490
}
6591

92+
/// @Metadata {
93+
/// @Available(Swift, introduced: 6.2)
94+
/// }
6695
public init(_ statusAtExit: StatusAtExit) {
6796
self.init(_kind: .statusAtExit(statusAtExit))
6897
}
@@ -89,6 +118,10 @@ extension ExitTest.Condition {
89118
/// the process is yielded to the parent process. Linux and other POSIX-like
90119
/// systems may only reliably report the low unsigned 8 bits (0–255) of
91120
/// the exit code.
121+
///
122+
/// @Metadata {
123+
/// @Available(Swift, introduced: 6.2)
124+
/// }
92125
public static func exitCode(_ exitCode: CInt) -> Self {
93126
#if !SWT_NO_EXIT_TESTS
94127
Self(.exitCode(exitCode))
@@ -113,6 +146,10 @@ extension ExitTest.Condition {
113146
/// | FreeBSD | [`<signal.h>`](https://man.freebsd.org/cgi/man.cgi?signal(3)) |
114147
/// | OpenBSD | [`<signal.h>`](https://man.openbsd.org/signal.3) |
115148
/// | Windows | [`<signal.h>`](https://learn.microsoft.com/en-us/cpp/c-runtime-library/signal-constants) |
149+
///
150+
/// @Metadata {
151+
/// @Available(Swift, introduced: 6.2)
152+
/// }
116153
public static func signal(_ signal: CInt) -> Self {
117154
#if !SWT_NO_EXIT_TESTS
118155
Self(.signal(signal))

Sources/Testing/ExitTests/ExitTest.Result.swift

+17-2
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
// See https://swift.org/CONTRIBUTORS.txt for Swift project authors
99
//
1010

11-
@_spi(Experimental)
1211
#if SWT_NO_EXIT_TESTS
1312
@available(*, unavailable, message: "Exit tests are not available on this platform.")
1413
#endif
@@ -19,11 +18,19 @@ extension ExitTest {
1918
/// Both ``expect(exitsWith:observing:_:sourceLocation:performing:)`` and
2019
/// ``require(exitsWith:observing:_:sourceLocation:performing:)`` return
2120
/// instances of this type.
21+
///
22+
/// @Metadata {
23+
/// @Available(Swift, introduced: 6.2)
24+
/// }
2225
public struct Result: Sendable {
23-
/// The exit condition the exit test exited with.
26+
/// The status of the process hosting the exit test at the time it exits.
2427
///
2528
/// When the exit test passes, the value of this property is equal to the
2629
/// exit status reported by the process that hosted the exit test.
30+
///
31+
/// @Metadata {
32+
/// @Available(Swift, introduced: 6.2)
33+
/// }
2734
public var statusAtExit: StatusAtExit
2835

2936
/// All bytes written to the standard output stream of the exit test before
@@ -50,6 +57,10 @@ extension ExitTest {
5057
///
5158
/// If you did not request standard output content when running an exit
5259
/// test, the value of this property is the empty array.
60+
///
61+
/// @Metadata {
62+
/// @Available(Swift, introduced: 6.2)
63+
/// }
5364
public var standardOutputContent: [UInt8] = []
5465

5566
/// All bytes written to the standard error stream of the exit test before
@@ -76,6 +87,10 @@ extension ExitTest {
7687
///
7788
/// If you did not request standard error content when running an exit test,
7889
/// the value of this property is the empty array.
90+
///
91+
/// @Metadata {
92+
/// @Available(Swift, introduced: 6.2)
93+
/// }
7994
public var standardErrorContent: [UInt8] = []
8095

8196
@_spi(ForToolsIntegrationOnly)

Sources/Testing/ExitTests/ExitTest.swift

+16-10
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,13 @@ private import _TestingInternals
2525
/// A type describing an exit test.
2626
///
2727
/// Instances of this type describe exit tests you create using the
28-
/// ``expect(exitsWith:observing:_:sourceLocation:performing:)``
28+
/// ``expect(exitsWith:observing:_:sourceLocation:performing:)`` or
2929
/// ``require(exitsWith:observing:_:sourceLocation:performing:)`` macro. You
3030
/// don't usually need to interact directly with an instance of this type.
31-
@_spi(Experimental)
31+
///
32+
/// @Metadata {
33+
/// @Available(Swift, introduced: 6.2)
34+
/// }
3235
#if SWT_NO_EXIT_TESTS
3336
@available(*, unavailable, message: "Exit tests are not available on this platform.")
3437
#endif
@@ -97,7 +100,6 @@ public struct ExitTest: Sendable, ~Copyable {
97100
#if !SWT_NO_EXIT_TESTS
98101
// MARK: - Current
99102

100-
@_spi(Experimental)
101103
extension ExitTest {
102104
/// A container type to hold the current exit test.
103105
///
@@ -127,6 +129,10 @@ extension ExitTest {
127129
///
128130
/// The value of this property is constant across all tasks in the current
129131
/// process.
132+
///
133+
/// @Metadata {
134+
/// @Available(Swift, introduced: 6.2)
135+
/// }
130136
public static var current: ExitTest? {
131137
_read {
132138
if let current = _current.rawValue {
@@ -140,7 +146,7 @@ extension ExitTest {
140146

141147
// MARK: - Invocation
142148

143-
@_spi(Experimental) @_spi(ForToolsIntegrationOnly)
149+
@_spi(ForToolsIntegrationOnly)
144150
extension ExitTest {
145151
/// Disable crash reporting, crash logging, or core dumps for the current
146152
/// process.
@@ -245,7 +251,7 @@ extension ExitTest: TestContent {
245251
typealias TestContentAccessorHint = ID
246252
}
247253

248-
@_spi(Experimental) @_spi(ForToolsIntegrationOnly)
254+
@_spi(ForToolsIntegrationOnly)
249255
extension ExitTest {
250256
/// Find the exit test function at the given source location.
251257
///
@@ -381,7 +387,7 @@ extension ABI {
381387
fileprivate typealias BackChannelVersion = v1
382388
}
383389

384-
@_spi(Experimental) @_spi(ForToolsIntegrationOnly)
390+
@_spi(ForToolsIntegrationOnly)
385391
extension ExitTest {
386392
/// A handler that is invoked when an exit test starts.
387393
///
@@ -417,7 +423,7 @@ extension ExitTest {
417423
/// events should be written, or `nil` if the file handle could not be
418424
/// resolved.
419425
private static let _backChannelForEntryPoint: FileHandle? = {
420-
guard let backChannelEnvironmentVariable = Environment.variable(named: "SWT_EXPERIMENTAL_BACKCHANNEL") else {
426+
guard let backChannelEnvironmentVariable = Environment.variable(named: "SWT_BACKCHANNEL") else {
421427
return nil
422428
}
423429

@@ -450,7 +456,7 @@ extension ExitTest {
450456
static func findInEnvironmentForEntryPoint() -> Self? {
451457
// Find the ID of the exit test to run, if any, in the environment block.
452458
var id: ExitTest.ID?
453-
if var idString = Environment.variable(named: "SWT_EXPERIMENTAL_EXIT_TEST_ID") {
459+
if var idString = Environment.variable(named: "SWT_EXIT_TEST_ID") {
454460
id = try? idString.withUTF8 { idBuffer in
455461
try JSON.decode(ExitTest.ID.self, from: UnsafeRawBufferPointer(idBuffer))
456462
}
@@ -583,7 +589,7 @@ extension ExitTest {
583589
// Insert a specific variable that tells the child process which exit test
584590
// to run.
585591
try JSON.withEncoding(of: exitTest.id) { json in
586-
childEnvironment["SWT_EXPERIMENTAL_EXIT_TEST_ID"] = String(decoding: json, as: UTF8.self)
592+
childEnvironment["SWT_EXIT_TEST_ID"] = String(decoding: json, as: UTF8.self)
587593
}
588594

589595
typealias ResultUpdater = @Sendable (inout ExitTest.Result) -> Void
@@ -629,7 +635,7 @@ extension ExitTest {
629635
#warning("Platform-specific implementation missing: back-channel pipe unavailable")
630636
#endif
631637
if let backChannelEnvironmentVariable {
632-
childEnvironment["SWT_EXPERIMENTAL_BACKCHANNEL"] = backChannelEnvironmentVariable
638+
childEnvironment["SWT_BACKCHANNEL"] = backChannelEnvironmentVariable
633639
}
634640

635641
// Spawn the child process.

Sources/Testing/ExitTests/StatusAtExit.swift

+12-2
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,10 @@ private import _TestingInternals
1818
/// expected to pass or fail by passing it to
1919
/// ``expect(exitsWith:observing:_:sourceLocation:performing:)`` or
2020
/// ``require(exitsWith:observing:_:sourceLocation:performing:)``.
21-
@_spi(Experimental)
21+
///
22+
/// @Metadata {
23+
/// @Available(Swift, introduced: 6.2)
24+
/// }
2225
#if SWT_NO_PROCESS_SPAWNING
2326
@available(*, unavailable, message: "Exit tests are not available on this platform.")
2427
#endif
@@ -44,6 +47,10 @@ public enum StatusAtExit: Sendable {
4447
/// the process is yielded to the parent process. Linux and other POSIX-like
4548
/// systems may only reliably report the low unsigned 8 bits (0&ndash;255) of
4649
/// the exit code.
50+
///
51+
/// @Metadata {
52+
/// @Available(Swift, introduced: 6.2)
53+
/// }
4754
case exitCode(_ exitCode: CInt)
4855

4956
/// The process terminated with the given signal.
@@ -61,12 +68,15 @@ public enum StatusAtExit: Sendable {
6168
/// | FreeBSD | [`<signal.h>`](https://man.freebsd.org/cgi/man.cgi?signal(3)) |
6269
/// | OpenBSD | [`<signal.h>`](https://man.openbsd.org/signal.3) |
6370
/// | Windows | [`<signal.h>`](https://learn.microsoft.com/en-us/cpp/c-runtime-library/signal-constants) |
71+
///
72+
/// @Metadata {
73+
/// @Available(Swift, introduced: 6.2)
74+
/// }
6475
case signal(_ signal: CInt)
6576
}
6677

6778
// MARK: - Equatable
6879

69-
@_spi(Experimental)
7080
#if SWT_NO_PROCESS_SPAWNING
7181
@available(*, unavailable, message: "Exit tests are not available on this platform.")
7282
#endif

Sources/Testing/Expectations/Expectation+Macro.swift

+8-2
Original file line numberDiff line numberDiff line change
@@ -582,7 +582,10 @@ public macro require<R>(
582582
/// ```
583583
///
584584
/// An exit test cannot run within another exit test.
585-
@_spi(Experimental)
585+
///
586+
/// @Metadata {
587+
/// @Available(Swift, introduced: 6.2)
588+
/// }
586589
#if SWT_NO_EXIT_TESTS
587590
@available(*, unavailable, message: "Exit tests are not available on this platform.")
588591
#endif
@@ -694,7 +697,10 @@ public macro require<R>(
694697
/// ```
695698
///
696699
/// An exit test cannot run within another exit test.
697-
@_spi(Experimental)
700+
///
701+
/// @Metadata {
702+
/// @Available(Swift, introduced: 6.2)
703+
/// }
698704
#if SWT_NO_EXIT_TESTS
699705
@available(*, unavailable, message: "Exit tests are not available on this platform.")
700706
#endif

Sources/Testing/Expectations/ExpectationChecking+Macro.swift

-1
Original file line numberDiff line numberDiff line change
@@ -1145,7 +1145,6 @@ public func __checkClosureCall<R>(
11451145
///
11461146
/// - Warning: This function is used to implement the `#expect()` and
11471147
/// `#require()` macros. Do not call it directly.
1148-
@_spi(Experimental)
11491148
public func __checkClosureCall(
11501149
identifiedBy exitTestID: (UInt64, UInt64),
11511150
exitsWith expectedExitCondition: ExitTest.Condition,

Sources/Testing/Running/Configuration.swift

+1-2
Original file line numberDiff line numberDiff line change
@@ -217,8 +217,7 @@ public struct Configuration: Sendable {
217217
/// When using the `swift test` command from Swift Package Manager, this
218218
/// property is pre-configured. Otherwise, the default value of this property
219219
/// records an issue indicating that it has not been configured.
220-
@_spi(Experimental)
221-
public var exitTestHandler: ExitTest.Handler = { exitTest in
220+
public var exitTestHandler: ExitTest.Handler = { _ in
222221
throw SystemError(description: "Exit test support has not been implemented by the current testing infrastructure.")
223222
}
224223
#endif

Sources/Testing/Test+Discovery+Legacy.swift

-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ let testContainerTypeNameMagic = "__🟠$test_container__"
3030
/// - Warning: This protocol is used to implement the `#expect(exitsWith:)`
3131
/// macro. Do not use it directly.
3232
@_alwaysEmitConformanceMetadata
33-
@_spi(Experimental)
3433
public protocol __ExitTestContainer {
3534
/// The unique identifier of the exit test.
3635
static var __id: (UInt64, UInt64) { get }

Sources/Testing/Testing.docc/Expectations.md

+7
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,13 @@ the test when the code doesn't satisfy a requirement, use
7272
- ``require(throws:_:sourceLocation:performing:)-4djuw``
7373
- ``require(_:sourceLocation:performing:throws:)``
7474

75+
### Checking how processes exit
76+
77+
- ``expect(exitsWith:observing:_:sourceLocation:performing:)``
78+
- ``require(exitsWith:observing:_:sourceLocation:performing:)``
79+
- ``ExitTest``
80+
- ``StatusAtExit``
81+
7582
### Confirming that asynchronous events occur
7683

7784
- <doc:testing-asynchronous-code>

Tests/TestingTests/ExitTestTests.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
// See https://swift.org/CONTRIBUTORS.txt for Swift project authors
99
//
1010

11-
@testable @_spi(Experimental) @_spi(ForToolsIntegrationOnly) import Testing
11+
@testable @_spi(ForToolsIntegrationOnly) import Testing
1212
private import _TestingInternals
1313

1414
#if !SWT_NO_EXIT_TESTS

0 commit comments

Comments
 (0)