Skip to content
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

Promote exit tests to API #324

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
Open
81 changes: 63 additions & 18 deletions Sources/Testing/ExitTests/ExitTest.Condition.swift
Original file line number Diff line number Diff line change
@@ -10,7 +10,6 @@

private import _TestingInternals

@_spi(Experimental)
#if SWT_NO_EXIT_TESTS
@available(*, unavailable, message: "Exit tests are not available on this platform.")
#endif
@@ -21,6 +20,22 @@ extension ExitTest {
/// exit test is expected to pass or fail by passing them to
/// ``expect(exitsWith:observing:_:sourceLocation:performing:)`` or
/// ``require(exitsWith:observing:_:sourceLocation:performing:)``.
///
/// ## Topics
///
/// ### Successful exit conditions
///
/// - ``success``
///
/// ### Failing exit conditions
///
/// - ``failure``
/// - ``exitCode(_:)``
/// - ``signal(_:)``
///
/// @Metadata {
/// @Available(Swift, introduced: 6.2)
/// }
public struct Condition: Sendable {
/// An enumeration describing the possible conditions for an exit test.
private enum _Kind: Sendable, Equatable {
@@ -38,13 +53,17 @@ extension ExitTest {

// MARK: -

@_spi(Experimental)
#if SWT_NO_EXIT_TESTS
@available(*, unavailable, message: "Exit tests are not available on this platform.")
#endif
extension ExitTest.Condition {
/// A condition that matches when a process terminates successfully with exit
/// code `EXIT_SUCCESS`.
/// A condition that matches when a process exits normally.
///
/// This condition matches the exit code `EXIT_SUCCESS`.
///
/// @Metadata {
/// @Available(Swift, introduced: 6.2)
/// }
public static var success: Self {
// Strictly speaking, the C standard treats 0 as a successful exit code and
// potentially distinct from EXIT_SUCCESS. To my knowledge, no modern
@@ -57,12 +76,21 @@ extension ExitTest.Condition {
#endif
}

/// A condition that matches when a process terminates abnormally with any
/// exit code other than `EXIT_SUCCESS` or with any signal.
/// A condition that matches when a process exits abnormally
///
/// This condition matches any exit code other than `EXIT_SUCCESS` or any
/// signal that causes the process to exit.
///
/// @Metadata {
/// @Available(Swift, introduced: 6.2)
/// }
public static var failure: Self {
Self(_kind: .failure)
}

/// @Metadata {
/// @Available(Swift, introduced: 6.2)
/// }
public init(_ statusAtExit: StatusAtExit) {
self.init(_kind: .statusAtExit(statusAtExit))
}
@@ -71,24 +99,33 @@ extension ExitTest.Condition {
/// exit code.
///
/// - Parameters:
/// - exitCode: The exit code yielded by the process.
/// - exitCode: The exit code reported by the process.
///
/// The C programming language defines two [standard exit codes](https://en.cppreference.com/w/c/program/EXIT_status),
/// `EXIT_SUCCESS` and `EXIT_FAILURE`. Platforms may additionally define their
/// own non-standard exit codes:
/// The C programming language defines two standard exit codes, `EXIT_SUCCESS`
/// and `EXIT_FAILURE`. Platforms may additionally define their own
/// non-standard exit codes:
///
/// | Platform | Header |
/// |-|-|
/// | 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) |
/// | Linux | [`<stdlib.h>`](https://sourceware.org/glibc/manual/latest/html_node/Exit-Status.html), `<sysexits.h>` |
/// | Linux | [`<stdlib.h>`](https://www.kernel.org/doc/man-pages/online/pages/man3/exit.3.html), [`<sysexits.h>`](https://www.kernel.org/doc/man-pages/online/pages/man3/sysexits.h.3head.html) |
/// | FreeBSD | [`<stdlib.h>`](https://man.freebsd.org/cgi/man.cgi?exit(3)), [`<sysexits.h>`](https://man.freebsd.org/cgi/man.cgi?sysexits(3)) |
/// | OpenBSD | [`<stdlib.h>`](https://man.openbsd.org/exit.3), [`<sysexits.h>`](https://man.openbsd.org/sysexits.3) |
/// | Windows | [`<stdlib.h>`](https://learn.microsoft.com/en-us/cpp/c-runtime-library/exit-success-exit-failure) |
///
/// @Comment {
/// See https://en.cppreference.com/w/c/program/EXIT_status for more
/// information about exit codes defined by the C standard.
/// }
///
/// On macOS, FreeBSD, OpenBSD, and Windows, the full exit code reported by
/// the process is yielded to the parent process. Linux and other POSIX-like
/// the process is reported to the parent process. Linux and other POSIX-like
/// systems may only reliably report the low unsigned 8 bits (0&ndash;255) of
/// the exit code.
///
/// @Metadata {
/// @Available(Swift, introduced: 6.2)
/// }
public static func exitCode(_ exitCode: CInt) -> Self {
#if !SWT_NO_EXIT_TESTS
Self(.exitCode(exitCode))
@@ -97,22 +134,30 @@ extension ExitTest.Condition {
#endif
}

/// Creates a condition that matches when a process terminates with a given
/// signal.
/// Creates a condition that matches when a process exits with a given signal.
///
/// - Parameters:
/// - signal: The signal that terminated the process.
/// - signal: The signal that caused the process to exit.
///
/// The C programming language defines a number of [standard signals](https://en.cppreference.com/w/c/program/SIG_types).
/// Platforms may additionally define their own non-standard signal codes:
/// The C programming language defines a number of standard signals. Platforms
/// may additionally define their own non-standard signal codes:
///
/// | Platform | Header |
/// |-|-|
/// | macOS | [`<signal.h>`](https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man3/signal.3.html) |
/// | Linux | [`<signal.h>`](https://sourceware.org/glibc/manual/latest/html_node/Standard-Signals.html) |
/// | Linux | [`<signal.h>`](https://www.kernel.org/doc/man-pages/online/pages/man7/signal.7.html) |
/// | FreeBSD | [`<signal.h>`](https://man.freebsd.org/cgi/man.cgi?signal(3)) |
/// | OpenBSD | [`<signal.h>`](https://man.openbsd.org/signal.3) |
/// | Windows | [`<signal.h>`](https://learn.microsoft.com/en-us/cpp/c-runtime-library/signal-constants) |
///
/// @Comment {
/// See https://en.cppreference.com/w/c/program/SIG_types for more
/// information about signals defined by the C standard.
/// }
///
/// @Metadata {
/// @Available(Swift, introduced: 6.2)
/// }
public static func signal(_ signal: CInt) -> Self {
#if !SWT_NO_EXIT_TESTS
Self(.signal(signal))
20 changes: 16 additions & 4 deletions Sources/Testing/ExitTests/ExitTest.Result.swift
Original file line number Diff line number Diff line change
@@ -8,7 +8,6 @@
// See https://swift.org/CONTRIBUTORS.txt for Swift project authors
//

@_spi(Experimental)
#if SWT_NO_EXIT_TESTS
@available(*, unavailable, message: "Exit tests are not available on this platform.")
#endif
@@ -19,11 +18,16 @@ extension ExitTest {
/// Both ``expect(exitsWith:observing:_:sourceLocation:performing:)`` and
/// ``require(exitsWith:observing:_:sourceLocation:performing:)`` return
/// instances of this type.
///
/// @Metadata {
/// @Available(Swift, introduced: 6.2)
/// }
public struct Result: Sendable {
/// The exit condition the exit test exited with.
/// The exit status reported by the process hosting the exit test.
///
/// When the exit test passes, the value of this property is equal to the
/// exit status reported by the process that hosted the exit test.
/// @Metadata {
/// @Available(Swift, introduced: 6.2)
/// }
public var statusAtExit: StatusAtExit

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

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

@_spi(ForToolsIntegrationOnly)
30 changes: 18 additions & 12 deletions Sources/Testing/ExitTests/ExitTest.swift
Original file line number Diff line number Diff line change
@@ -26,10 +26,13 @@ private import _TestingInternals
/// A type describing an exit test.
///
/// Instances of this type describe exit tests you create using the
/// ``expect(exitsWith:observing:_:sourceLocation:performing:)``
/// ``expect(exitsWith:observing:_:sourceLocation:performing:)`` or
/// ``require(exitsWith:observing:_:sourceLocation:performing:)`` macro. You
/// don't usually need to interact directly with an instance of this type.
@_spi(Experimental)
///
/// @Metadata {
/// @Available(Swift, introduced: 6.2)
/// }
#if SWT_NO_EXIT_TESTS
@available(*, unavailable, message: "Exit tests are not available on this platform.")
#endif
@@ -112,7 +115,6 @@ public struct ExitTest: Sendable, ~Copyable {
#if !SWT_NO_EXIT_TESTS
// MARK: - Current

@_spi(Experimental)
extension ExitTest {
/// A container type to hold the current exit test.
///
@@ -142,6 +144,10 @@ extension ExitTest {
///
/// The value of this property is constant across all tasks in the current
/// process.
///
/// @Metadata {
/// @Available(Swift, introduced: 6.2)
/// }
public static var current: ExitTest? {
_read {
if let current = _current.rawValue {
@@ -155,7 +161,7 @@ extension ExitTest {

// MARK: - Invocation

@_spi(Experimental) @_spi(ForToolsIntegrationOnly)
@_spi(ForToolsIntegrationOnly)
extension ExitTest {
/// Disable crash reporting, crash logging, or core dumps for the current
/// process.
@@ -294,7 +300,7 @@ extension ExitTest: DiscoverableAsTestContent {
}
}

@_spi(Experimental) @_spi(ForToolsIntegrationOnly)
@_spi(ForToolsIntegrationOnly)
extension ExitTest {
/// Find the exit test function at the given source location.
///
@@ -431,7 +437,7 @@ extension ABI {
fileprivate typealias BackChannelVersion = v1
}

@_spi(Experimental) @_spi(ForToolsIntegrationOnly)
@_spi(ForToolsIntegrationOnly)
extension ExitTest {
/// A handler that is invoked when an exit test starts.
///
@@ -467,13 +473,13 @@ extension ExitTest {
/// events should be written, or `nil` if the file handle could not be
/// resolved.
private static let _backChannelForEntryPoint: FileHandle? = {
guard let backChannelEnvironmentVariable = Environment.variable(named: "SWT_EXPERIMENTAL_BACKCHANNEL") else {
guard let backChannelEnvironmentVariable = Environment.variable(named: "SWT_BACKCHANNEL") else {
return nil
}

// Erase the environment variable so that it cannot accidentally be opened
// twice (nor, in theory, affect the code of the exit test.)
Environment.setVariable(nil, named: "SWT_EXPERIMENTAL_BACKCHANNEL")
Environment.setVariable(nil, named: "SWT_BACKCHANNEL")

var fd: CInt?
#if SWT_TARGET_OS_APPLE || os(Linux) || os(FreeBSD) || os(OpenBSD)
@@ -504,10 +510,10 @@ extension ExitTest {
static func findInEnvironmentForEntryPoint() -> Self? {
// Find the ID of the exit test to run, if any, in the environment block.
var id: ExitTest.ID?
if var idString = Environment.variable(named: "SWT_EXPERIMENTAL_EXIT_TEST_ID") {
if var idString = Environment.variable(named: "SWT_EXIT_TEST_ID") {
// Clear the environment variable. It's an implementation detail and exit
// test code shouldn't be dependent on it. Use ExitTest.current if needed!
Environment.setVariable(nil, named: "SWT_EXPERIMENTAL_EXIT_TEST_ID")
Environment.setVariable(nil, named: "SWT_EXIT_TEST_ID")

id = try? idString.withUTF8 { idBuffer in
try JSON.decode(ExitTest.ID.self, from: UnsafeRawBufferPointer(idBuffer))
@@ -641,7 +647,7 @@ extension ExitTest {
// Insert a specific variable that tells the child process which exit test
// to run.
try JSON.withEncoding(of: exitTest.id) { json in
childEnvironment["SWT_EXPERIMENTAL_EXIT_TEST_ID"] = String(decoding: json, as: UTF8.self)
childEnvironment["SWT_EXIT_TEST_ID"] = String(decoding: json, as: UTF8.self)
}

typealias ResultUpdater = @Sendable (inout ExitTest.Result) -> Void
@@ -687,7 +693,7 @@ extension ExitTest {
#warning("Platform-specific implementation missing: back-channel pipe unavailable")
#endif
if let backChannelEnvironmentVariable {
childEnvironment["SWT_EXPERIMENTAL_BACKCHANNEL"] = backChannelEnvironmentVariable
childEnvironment["SWT_BACKCHANNEL"] = backChannelEnvironmentVariable
}

// Spawn the child process.
Loading