Skip to content

Merge main-next into main. #636

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

Merged
merged 15 commits into from
Aug 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ extension Array where Element == PackageDescription.SwiftSetting {
.enableExperimentalFeature("AvailabilityMacro=_clockAPI:macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0"),
.enableExperimentalFeature("AvailabilityMacro=_regexAPI:macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0"),
.enableExperimentalFeature("AvailabilityMacro=_swiftVersionAPI:macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0"),
.enableExperimentalFeature("AvailabilityMacro=_synchronizationAPI:macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0"),

.enableExperimentalFeature("AvailabilityMacro=_distantFuture:macOS 99.0, iOS 99.0, watchOS 99.0, tvOS 99.0, visionOS 99.0"),
]
Expand Down
4 changes: 0 additions & 4 deletions Sources/Testing/ABI/EntryPoints/ABIEntryPoint.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,7 @@
//

#if canImport(Foundation) && !SWT_NO_ABI_ENTRY_POINT
#if SWT_BUILDING_WITH_CMAKE
@_implementationOnly import _TestingInternals
#else
private import _TestingInternals
#endif

extension ABIv0 {
/// The type of the entry point to the testing library used by tools that want
Expand Down
6 changes: 6 additions & 0 deletions Sources/Testing/Events/Clock.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,13 @@ extension Test {
/// The wall-clock time corresponding to this instant.
fileprivate(set) var wall: TimeValue = {
var wall = timespec()
#if os(Android)
// Android headers recommend `clock_gettime` over `timespec_get` which
// is available with API Level 29+ for `TIME_UTC`.
clock_gettime(CLOCK_REALTIME, &wall)
#else
timespec_get(&wall, TIME_UTC)
#endif
return TimeValue(wall)
}()
#endif
Expand Down
164 changes: 146 additions & 18 deletions Sources/Testing/ExitTests/ExitCondition.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,9 @@ public enum ExitCondition: Sendable {
/// | Linux | [`<stdlib.h>`](https://sourceware.org/glibc/manual/latest/html_node/Exit-Status.html), `<sysexits.h>` |
/// | Windows | [`<stdlib.h>`](https://learn.microsoft.com/en-us/cpp/c-runtime-library/exit-success-exit-failure) |
///
/// On POSIX-like systems including macOS and Linux, only the low unsigned 8
/// bits (0&ndash;255) of the exit code are reliably preserved and reported to
/// a parent process.
/// On macOS and Windows, the full exit code reported by the process is
/// yielded 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.
case exitCode(_ exitCode: CInt)

/// The process terminated with the given signal.
Expand All @@ -62,43 +62,171 @@ public enum ExitCondition: Sendable {
/// | 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) |
/// | Windows | [`<signal.h>`](https://learn.microsoft.com/en-us/cpp/c-runtime-library/signal-constants) |
///
/// On Windows, by default, the C runtime will terminate a process with exit
/// code `-3` if a raised signal is not handled, exactly as if `exit(-3)` were
/// called. As a result, this case is unavailable on that platform. Developers
/// should use ``failure`` instead when testing signal handling on Windows.
#if os(Windows)
@available(*, unavailable, message: "On Windows, use .failure instead.")
#endif
case signal(_ signal: CInt)
}

// MARK: -
// MARK: - Equatable

#if SWT_NO_EXIT_TESTS
@available(*, unavailable, message: "Exit tests are not available on this platform.")
#endif
extension ExitCondition {
/// Check whether this instance matches another.
/// Check whether or not two values of this type are equal.
///
/// - Parameters:
/// - other: The other instance to compare against.
/// - lhs: One value to compare.
/// - rhs: Another value to compare.
///
/// - Returns: Whether or not this instance is equal to, or at least covers,
/// the other instance.
func matches(_ other: ExitCondition) -> Bool {
return switch (self, other) {
case (.failure, .failure):
true
/// - Returns: Whether or not `lhs` and `rhs` are equal.
///
/// Two instances of this type can be compared; if either instance is equal to
/// ``failure``, it will compare equal to any instance except ``success``. To
/// check if two instances are exactly equal, use the ``===(_:_:)`` operator:
///
/// ```swift
/// let lhs: ExitCondition = .failure
/// let rhs: ExitCondition = .signal(SIGINT)
/// print(lhs == rhs) // prints "true"
/// print(lhs === rhs) // prints "false"
/// ```
///
/// This special behavior means that the ``==(_:_:)`` operator is not
/// transitive, and does not satisfy the requirements of
/// [`Equatable`](https://developer.apple.com/documentation/swift/equatable)
/// or [`Hashable`](https://developer.apple.com/documentation/swift/hashable).
///
/// For any values `a` and `b`, `a == b` implies that `a != b` is `false`.
public static func ==(lhs: Self, rhs: Self) -> Bool {
#if SWT_NO_EXIT_TESTS
fatalError("Unsupported")
#else
return switch (lhs, rhs) {
case let (.failure, .exitCode(exitCode)), let (.exitCode(exitCode), .failure):
exitCode != EXIT_SUCCESS
#if !os(Windows)
case (.failure, .signal), (.signal, .failure):
// All terminating signals are considered failures.
true
#endif
default:
lhs === rhs
}
#endif
}

/// Check whether or not two values of this type are _not_ equal.
///
/// - Parameters:
/// - lhs: One value to compare.
/// - rhs: Another value to compare.
///
/// - Returns: Whether or not `lhs` and `rhs` are _not_ equal.
///
/// Two instances of this type can be compared; if either instance is equal to
/// ``failure``, it will compare equal to any instance except ``success``. To
/// check if two instances are not exactly equal, use the ``!==(_:_:)``
/// operator:
///
/// ```swift
/// let lhs: ExitCondition = .failure
/// let rhs: ExitCondition = .signal(SIGINT)
/// print(lhs != rhs) // prints "false"
/// print(lhs !== rhs) // prints "true"
/// ```
///
/// This special behavior means that the ``!=(_:_:)`` operator is not
/// transitive, and does not satisfy the requirements of
/// [`Equatable`](https://developer.apple.com/documentation/swift/equatable)
/// or [`Hashable`](https://developer.apple.com/documentation/swift/hashable).
///
/// For any values `a` and `b`, `a == b` implies that `a != b` is `false`.
public static func !=(lhs: Self, rhs: Self) -> Bool {
#if SWT_NO_EXIT_TESTS
fatalError("Unsupported")
#else
!(lhs == rhs)
#endif
}

/// Check whether or not two values of this type are identical.
///
/// - Parameters:
/// - lhs: One value to compare.
/// - rhs: Another value to compare.
///
/// - Returns: Whether or not `lhs` and `rhs` are identical.
///
/// Two instances of this type can be compared; if either instance is equal to
/// ``failure``, it will compare equal to any instance except ``success``. To
/// check if two instances are exactly equal, use the ``===(_:_:)`` operator:
///
/// ```swift
/// let lhs: ExitCondition = .failure
/// let rhs: ExitCondition = .signal(SIGINT)
/// print(lhs == rhs) // prints "true"
/// print(lhs === rhs) // prints "false"
/// ```
///
/// This special behavior means that the ``==(_:_:)`` operator is not
/// transitive, and does not satisfy the requirements of
/// [`Equatable`](https://developer.apple.com/documentation/swift/equatable)
/// or [`Hashable`](https://developer.apple.com/documentation/swift/hashable).
///
/// For any values `a` and `b`, `a === b` implies that `a !== b` is `false`.
public static func ===(lhs: Self, rhs: Self) -> Bool {
return switch (lhs, rhs) {
case (.failure, .failure):
true
case let (.exitCode(lhs), .exitCode(rhs)):
lhs == rhs
#if !os(Windows)
case let (.signal(lhs), .signal(rhs)):
lhs == rhs
case (.signal, .failure), (.failure, .signal):
// All terminating signals are considered failures.
true
case (.signal, .exitCode), (.exitCode, .signal):
// Signals do not match exit codes.
false
#endif
default:
false
}
}

/// Check whether or not two values of this type are _not_ identical.
///
/// - Parameters:
/// - lhs: One value to compare.
/// - rhs: Another value to compare.
///
/// - Returns: Whether or not `lhs` and `rhs` are _not_ identical.
///
/// Two instances of this type can be compared; if either instance is equal to
/// ``failure``, it will compare equal to any instance except ``success``. To
/// check if two instances are not exactly equal, use the ``!==(_:_:)``
/// operator:
///
/// ```swift
/// let lhs: ExitCondition = .failure
/// let rhs: ExitCondition = .signal(SIGINT)
/// print(lhs != rhs) // prints "false"
/// print(lhs !== rhs) // prints "true"
/// ```
///
/// This special behavior means that the ``!=(_:_:)`` operator is not
/// transitive, and does not satisfy the requirements of
/// [`Equatable`](https://developer.apple.com/documentation/swift/equatable)
/// or [`Hashable`](https://developer.apple.com/documentation/swift/hashable).
///
/// For any values `a` and `b`, `a === b` implies that `a !== b` is `false`.
public static func !==(lhs: Self, rhs: Self) -> Bool {
#if SWT_NO_EXIT_TESTS
fatalError("Unsupported")
#else
!(lhs === rhs)
#endif
}
}
16 changes: 10 additions & 6 deletions Sources/Testing/ExitTests/ExitTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public struct ExitTest: Sendable {
public var expectedExitCondition: ExitCondition

/// The body closure of the exit test.
fileprivate var body: @Sendable () async -> Void
fileprivate var body: @Sendable () async throws -> Void

/// The source location of the exit test.
///
Expand All @@ -37,12 +37,16 @@ public struct ExitTest: Sendable {
/// terminate the process in a way that causes the corresponding expectation
/// to fail.
public func callAsFunction() async -> Never {
await body()
do {
try await body()
} catch {
_errorInMain(error)
}

// Run some glue code that terminates the process with an exit condition
// that does not match the expected one. If the exit test's body doesn't
// terminate, we'll manually call exit() and cause the test to fail.
let expectingFailure = expectedExitCondition.matches(.failure)
let expectingFailure = expectedExitCondition == .failure
exit(expectingFailure ? EXIT_SUCCESS : EXIT_FAILURE)
}
}
Expand All @@ -63,7 +67,7 @@ public protocol __ExitTestContainer {
static var __sourceLocation: SourceLocation { get }

/// The body function of the exit test.
static var __body: @Sendable () async -> Void { get }
static var __body: @Sendable () async throws -> Void { get }
}

extension ExitTest {
Expand Down Expand Up @@ -118,7 +122,7 @@ extension ExitTest {
/// convention.
func callExitTest(
exitsWith expectedExitCondition: ExitCondition,
performing body: @escaping @Sendable () async -> Void,
performing body: @escaping @Sendable () async throws -> Void,
expression: __Expression,
comments: @autoclosure () -> [Comment],
isRequired: Bool,
Expand Down Expand Up @@ -150,7 +154,7 @@ func callExitTest(
}

return __checkValue(
expectedExitCondition.matches(actualExitCondition),
expectedExitCondition == actualExitCondition,
expression: expression,
expressionWithCapturedRuntimeValues: expression.capturingRuntimeValues(actualExitCondition),
mismatchedExitConditionDescription: String(describingForTest: expectedExitCondition),
Expand Down
42 changes: 37 additions & 5 deletions Sources/Testing/Expectations/Expectation+Macro.swift
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,34 @@ public macro require(
sourceLocation: SourceLocation = #_sourceLocation
) -> Bool = #externalMacro(module: "TestingMacros", type: "AmbiguousRequireMacro")

/// Unwrap an optional value or, if it is `nil`, fail and throw an error.
///
/// - Parameters:
/// - optionalValue: The optional value to be unwrapped.
/// - comment: A comment describing the expectation.
/// - sourceLocation: The source location to which recorded expectations and
/// issues should be attributed.
///
/// - Returns: The unwrapped value of `optionalValue`.
///
/// - Throws: An instance of ``ExpectationFailedError`` if `optionalValue` is
/// `nil`.
///
/// If `optionalValue` is `nil`, an ``Issue`` is recorded for the test that is
/// running in the current task and an instance of ``ExpectationFailedError`` is
/// thrown.
///
/// This overload of ``require(_:_:sourceLocation:)-6w9oo`` is used when a
/// non-optional, non-`Bool` value is passed to `#require()`. It emits a warning
/// diagnostic indicating that the expectation is redundant.
@freestanding(expression)
@_documentation(visibility: private)
public macro require<T>(
_ optionalValue: T,
_ comment: @autoclosure () -> Comment? = nil,
sourceLocation: SourceLocation = #_sourceLocation
) -> T = #externalMacro(module: "TestingMacros", type: "NonOptionalRequireMacro")

// MARK: - Matching errors by type

/// Check that an expression always throws an error of a given type.
Expand Down Expand Up @@ -440,7 +468,9 @@ public macro require(
/// a clean environment for execution, it is not called within the context of
/// the original test. If `expression` does not terminate the child process, the
/// process is terminated automatically as if the main function of the child
/// process were allowed to return naturally.
/// process were allowed to return naturally. If an error is thrown from
/// `expression`, it is handed as if the error were thrown from `main()` and the
/// process is terminated.
///
/// Once the child process terminates, the parent process resumes and compares
/// its exit status against `exitCondition`. If they match, the exit test has
Expand Down Expand Up @@ -488,8 +518,8 @@ public macro require(
/// issues should be attributed.
/// - expression: The expression to be evaluated.
///
/// - Throws: An instance of ``ExpectationFailedError`` if `condition` evaluates
/// to `false`.
/// - Throws: An instance of ``ExpectationFailedError`` if the exit condition of
/// the child process does not equal `expectedExitCondition`.
///
/// Use this overload of `#require()` when an expression will cause the current
/// process to terminate and the nature of that termination will determine if
Expand All @@ -515,7 +545,9 @@ public macro require(
/// a clean environment for execution, it is not called within the context of
/// the original test. If `expression` does not terminate the child process, the
/// process is terminated automatically as if the main function of the child
/// process were allowed to return naturally.
/// process were allowed to return naturally. If an error is thrown from
/// `expression`, it is handed as if the error were thrown from `main()` and the
/// process is terminated.
///
/// Once the child process terminates, the parent process resumes and compares
/// its exit status against `exitCondition`. If they match, the exit test has
Expand Down Expand Up @@ -550,5 +582,5 @@ public macro require(
exitsWith expectedExitCondition: ExitCondition,
_ comment: @autoclosure () -> Comment? = nil,
sourceLocation: SourceLocation = #_sourceLocation,
performing expression: @convention(thin) () async -> Void
performing expression: @convention(thin) () async throws -> Void
) = #externalMacro(module: "TestingMacros", type: "ExitTestRequireMacro")
10 changes: 6 additions & 4 deletions Sources/Testing/Expectations/ExpectationChecking+Macro.swift
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,10 @@ public func __checkValue(
// Post an event for the expectation regardless of whether or not it passed.
// If the current event handler is not configured to handle events of this
// kind, this event is discarded.
var expectation = Expectation(evaluatedExpression: expression, isPassing: condition, isRequired: isRequired, sourceLocation: sourceLocation)
Event.post(.expectationChecked(expectation))
lazy var expectation = Expectation(evaluatedExpression: expression, isPassing: condition, isRequired: isRequired, sourceLocation: sourceLocation)
if Configuration.deliverExpectationCheckedEvents {
Event.post(.expectationChecked(expectation))
}

// Early exit if the expectation passed.
if condition {
Expand Down Expand Up @@ -1103,15 +1105,15 @@ public func __checkClosureCall<R>(
@_spi(Experimental)
public func __checkClosureCall(
exitsWith expectedExitCondition: ExitCondition,
performing body: @convention(thin) () async -> Void,
performing body: @convention(thin) () async throws -> Void,
expression: __Expression,
comments: @autoclosure () -> [Comment],
isRequired: Bool,
sourceLocation: SourceLocation
) async -> Result<Void, any Error> {
await callExitTest(
exitsWith: expectedExitCondition,
performing: { await body() },
performing: { try await body() },
expression: expression,
comments: comments(),
isRequired: isRequired,
Expand Down
Loading