Skip to content

Commit 56b5f7c

Browse files
authored
Add experimental exit test support. (#307)
This PR adds a new kind of expectation called an "exit test", "death test", "expected crasher", etc. It can be invoked by writing: ```swift #expect(exitsWith: .failure) { fatalError("How dare you!") } ``` It works by spinning up a second process that re-runs the same test function. The parent process awaits the return of the child process, while the child process executes the closure passed to `#expect()`. This is similar to calling `fork()`, however that function is fundamentally unusable on Darwin and unavailable on Windows, so I've pursued a different implementation here that works across macOS, Linux, and Windows. There are some constraints to using exit tests that are documented, but which are not enforced at compile time. swift-syntax's `lexicalContext` should allow us to do general checks for (some of) these constraints at compile time. Resolves #157. (Sort of.) ### 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 3c0a692 commit 56b5f7c

23 files changed

+1243
-26
lines changed

Package.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ extension Array where Element == PackageDescription.SwiftSetting {
125125
.define("SWT_TARGET_OS_APPLE", .when(platforms: [.macOS, .iOS, .macCatalyst, .watchOS, .tvOS, .visionOS])),
126126

127127
.define("SWT_NO_FILE_IO", .when(platforms: [.wasi])),
128+
.define("SWT_NO_EXIT_TESTS", .when(platforms: [.iOS, .watchOS, .tvOS, .visionOS, .wasi])),
128129
]
129130
}
130131

[email protected]

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ extension Array where Element == PackageDescription.SwiftSetting {
125125
.define("SWT_TARGET_OS_APPLE", .when(platforms: [.macOS, .iOS, .macCatalyst, .watchOS, .tvOS, .visionOS])),
126126

127127
.define("SWT_NO_FILE_IO", .when(platforms: [.wasi])),
128+
.define("SWT_NO_EXIT_TESTS", .when(platforms: [.iOS, .watchOS, .tvOS, .visionOS, .wasi])),
128129
]
129130
}
130131

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
//
2+
// This source file is part of the Swift.org open source project
3+
//
4+
// Copyright (c) 2024 Apple Inc. and the Swift project authors
5+
// Licensed under Apache License v2.0 with Runtime Library Exception
6+
//
7+
// See https://swift.org/LICENSE.txt for license information
8+
// See https://swift.org/CONTRIBUTORS.txt for Swift project authors
9+
//
10+
11+
private import TestingInternals
12+
13+
/// An enumeration describing possible conditions under which an exit test will
14+
/// succeed or fail.
15+
///
16+
/// Values of this type can be passed to
17+
/// ``expect(exitsWith:_:sourceLocation:performing:)`` or
18+
/// ``require(exitsWith:_:sourceLocation:performing:)`` to configure which exit
19+
/// statuses should be considered successful.
20+
@_spi(Experimental)
21+
#if SWT_NO_EXIT_TESTS
22+
@available(*, unavailable, message: "Exit tests are not available on this platform.")
23+
#endif
24+
public enum ExitCondition: Sendable {
25+
/// The process terminated successfully with status `EXIT_SUCCESS`.
26+
public static var success: Self { .exitCode(EXIT_SUCCESS) }
27+
28+
/// The process terminated abnormally with any status other than
29+
/// `EXIT_SUCCESS` or with any signal.
30+
case failure
31+
32+
/// The process terminated with the given exit code.
33+
///
34+
/// - Parameters:
35+
/// - exitCode: The exit code yielded by the process.
36+
///
37+
/// The C programming language defines two [standard exit codes](https://en.cppreference.com/w/c/program/EXIT_status),
38+
/// `EXIT_SUCCESS` and `EXIT_FAILURE`. Platforms may additionally define their
39+
/// own non-standard exit codes:
40+
///
41+
/// | Platform | Header |
42+
/// |-|-|
43+
/// | macOS | [`<stdlib.h>`](https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man3/_Exit.3.html), [`<sysexits.h>`](https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man3/sysexits.3.html) |
44+
/// | Linux | `<stdlib.h>`, `<sysexits.h>` |
45+
/// | Windows | [`<stdlib.h>`](https://learn.microsoft.com/en-us/cpp/c-runtime-library/exit-success-exit-failure) |
46+
///
47+
/// On POSIX-like systems including macOS and Linux, only the low unsigned 8
48+
/// bits (0&ndash;255) of the exit code are reliably preserved and reported to
49+
/// a parent process.
50+
case exitCode(_ exitCode: CInt)
51+
52+
/// The process terminated with the given signal.
53+
///
54+
/// - Parameters:
55+
/// - signal: The signal that terminated the process.
56+
///
57+
/// The C programming language defines a number of [standard signals](https://en.cppreference.com/w/c/program/SIG_types).
58+
/// Platforms may additionally define their own non-standard signal codes:
59+
///
60+
/// | Platform | Header |
61+
/// |-|-|
62+
/// | macOS | [`<signal.h>`](https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man3/signal.3.html) |
63+
/// | Linux | `<signal.h>` |
64+
/// | Windows | [`<signal.h>`](https://learn.microsoft.com/en-us/cpp/c-runtime-library/signal-constants) |
65+
#if os(Windows)
66+
@available(*, unavailable, message: "On Windows, use .failure instead.")
67+
#endif
68+
case signal(_ signal: CInt)
69+
}
70+
71+
// MARK: -
72+
73+
#if SWT_NO_EXIT_TESTS
74+
@available(*, unavailable, message: "Exit tests are not available on this platform.")
75+
#endif
76+
extension ExitCondition {
77+
/// Check whether this instance matches another.
78+
///
79+
/// - Parameters:
80+
/// - other: The other instance to compare against.
81+
///
82+
/// - Returns: Whether or not this instance is equal to, or at least covers,
83+
/// the other instance.
84+
func matches(_ other: ExitCondition) -> Bool {
85+
return switch (self, other) {
86+
case (.failure, .failure):
87+
true
88+
case let (.failure, .exitCode(exitCode)), let (.exitCode(exitCode), .failure):
89+
exitCode != EXIT_SUCCESS
90+
case let (.exitCode(lhs), .exitCode(rhs)):
91+
lhs == rhs
92+
#if !os(Windows)
93+
case let (.signal(lhs), .signal(rhs)):
94+
lhs == rhs
95+
case (.signal, .failure), (.failure, .signal):
96+
// All terminating signals are considered failures.
97+
true
98+
case (.signal, .exitCode), (.exitCode, .signal):
99+
// Signals do not match exit codes.
100+
false
101+
#endif
102+
}
103+
}
104+
}

0 commit comments

Comments
 (0)