Skip to content
7 changes: 7 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,13 @@ let package = Package(
path: "Tests/_MemorySafeTestingTests",
swiftSettings: .packageSettings(isTestTarget: true) + [.strictMemorySafety()]
),
.testTarget(
name: "SubexpressionShowcase",
dependencies: [
"Testing",
],
swiftSettings: .packageSettings(isTestTarget: true)
),

.macro(
name: "TestingMacros",
Expand Down
3 changes: 2 additions & 1 deletion Sources/Testing/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ add_library(Testing
Expectations/Expectation.swift
Expectations/Expectation+Macro.swift
Expectations/ExpectationChecking+Macro.swift
Expectations/ExpectationContext.swift
Issues/Confirmation.swift
Issues/ErrorSnapshot.swift
Issues/Issue.swift
Expand All @@ -76,7 +77,7 @@ add_library(Testing
SourceAttribution/CustomTestReflectable.swift
SourceAttribution/CustomTestStringConvertible.swift
SourceAttribution/Expression.swift
SourceAttribution/Expression+Macro.swift
SourceAttribution/ExpressionID.swift
SourceAttribution/SourceBounds.swift
SourceAttribution/SourceContext.swift
SourceAttribution/SourceLocation.swift
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ extension Event.Symbol {
case .attachment:
return "\(_ansiEscapeCodePrefix)94m\(symbolCharacter)\(_resetANSIEscapeCode)"
case .details:
return symbolCharacter
return "\(symbolCharacter)"
}
}
return symbolCharacter
Expand Down
34 changes: 10 additions & 24 deletions Sources/Testing/ExitTests/ExitTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -464,17 +464,16 @@ extension ExitTest {
/// This function contains the common implementation for all
/// `await #expect(processExitsWith:) { }` invocations regardless of calling
/// convention.
func callExitTest(
nonisolated(nonsending) func callExitTest(
identifiedBy exitTestID: (UInt64, UInt64, UInt64, UInt64),
encodingCapturedValues capturedValues: [ExitTest.CapturedValue],
processExitsWith expectedExitCondition: ExitTest.Condition,
observing observedValues: [any PartialKeyPath<ExitTest.Result> & Sendable],
expression: __Expression,
sourceCode: @escaping @autoclosure @Sendable () -> KeyValuePairs<__ExpressionID, String>,
comments: @autoclosure () -> [Comment],
isRequired: Bool,
isolation: isolated (any Actor)? = #isolation,
sourceLocation: SourceLocation
) async -> Result<ExitTest.Result?, any Error> {
) async -> Result<ExitTest.Result?, ExpectationFailedError> {
guard let configuration = Configuration.current ?? Configuration.all.first else {
preconditionFailure("A test must be running on the current task to use #expect(processExitsWith:).")
}
Expand Down Expand Up @@ -527,27 +526,14 @@ func callExitTest(
}

// Plumb the exit test's result through the general expectation machinery.
func expressionWithCapturedRuntimeValues() -> __Expression {
var expression = expression.capturingRuntimeValues(result.exitStatus)

expression.subexpressions = [expectedExitCondition.exitStatus, result.exitStatus]
.compactMap { exitStatus in
guard let exitStatus, let exitStatusName = exitStatus.name else {
return nil
}
return __Expression(
exitStatusName,
runtimeValue: __Expression.Value(describing: exitStatus.code)
)
}

return expression
}
return __checkValue(
let expectationContext = __ExpectationContext<Bool>(
sourceCode: [.root: String(describingForTest: expectedExitCondition)],
runtimeValues: [.root: { Expression.Value(reflecting: result.exitStatus) }]
)
return check(
expectedExitCondition.isApproximatelyEqual(to: result.exitStatus),
expression: expression,
expressionWithCapturedRuntimeValues: expressionWithCapturedRuntimeValues(),
mismatchedExitConditionDescription: #"expected exit status "\#(expectedExitCondition)", but "\#(result.exitStatus)" was reported instead"#,
expectationContext: expectationContext,
mismatchedErrorDescription: nil,
comments: comments(),
isRequired: isRequired,
sourceLocation: sourceLocation
Expand Down
12 changes: 6 additions & 6 deletions Sources/Testing/Expectations/Expectation+Macro.swift
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,10 @@
/// running in the current task and an instance of ``ExpectationFailedError`` is
/// thrown.
@freestanding(expression) public macro require<T>(
_ optionalValue: T?,
_ optionalValue: consuming T?,
_ comment: @autoclosure () -> Comment? = nil,
sourceLocation: SourceLocation = #_sourceLocation
) -> T = #externalMacro(module: "TestingMacros", type: "RequireMacro")
) -> T = #externalMacro(module: "TestingMacros", type: "UnwrapMacro") where T: ~Copyable

/// Unwrap an optional boolean value or, if it is `nil`, fail and throw an
/// error.
Expand All @@ -89,7 +89,7 @@
/// running in the current task and an instance of ``ExpectationFailedError`` is
/// thrown.
///
/// This overload of ``require(_:_:sourceLocation:)-6w9oo`` checks if
/// This overload of ``require(_:_:sourceLocation:)-5l63q`` checks if
/// `optionalValue` may be ambiguous (i.e. it is unclear if the developer
/// intended to check for a boolean value or unwrap an optional boolean value)
/// and provides additional compile-time diagnostics when it is.
Expand Down Expand Up @@ -118,16 +118,16 @@ public macro require(
/// running in the current task and an instance of ``ExpectationFailedError`` is
/// thrown.
///
/// This overload of ``require(_:_:sourceLocation:)-6w9oo`` is used when a
/// This overload of ``require(_:_:sourceLocation:)-5l63q`` 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,
_ optionalValue: consuming T,
_ comment: @autoclosure () -> Comment? = nil,
sourceLocation: SourceLocation = #_sourceLocation
) -> T = #externalMacro(module: "TestingMacros", type: "NonOptionalRequireMacro")
) -> T = #externalMacro(module: "TestingMacros", type: "NonOptionalRequireMacro") where T: ~Copyable

// MARK: - Matching errors by type

Expand Down
17 changes: 6 additions & 11 deletions Sources/Testing/Expectations/Expectation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,14 @@
public struct Expectation: Sendable {
/// The expression evaluated by this expectation.
@_spi(ForToolsIntegrationOnly)
public var evaluatedExpression: Expression
public internal(set) var evaluatedExpression: Expression

/// A description of the error mismatch that occurred, if any.
///
/// If this expectation passed, the value of this property is `nil` because no
/// error mismatch occurred.
@_spi(Experimental) @_spi(ForToolsIntegrationOnly)
public var mismatchedErrorDescription: String?
public internal(set) var mismatchedErrorDescription: String?

/// A description of the difference between the operands in the expression
/// evaluated by this expectation, if the difference could be determined.
Expand All @@ -28,14 +28,9 @@ public struct Expectation: Sendable {
/// the difference is only computed when necessary to assist with diagnosing
/// test failures.
@_spi(Experimental) @_spi(ForToolsIntegrationOnly)
public var differenceDescription: String?

/// A description of the exit condition that was expected to be matched.
///
/// If this expectation passed, the value of this property is `nil` because no
/// exit test failed.
@_spi(Experimental) @_spi(ForToolsIntegrationOnly)
public var mismatchedExitConditionDescription: String?
public var differenceDescription: String? {
evaluatedExpression.differenceDescription
}

/// Whether the expectation passed or failed.
///
Expand Down Expand Up @@ -100,7 +95,7 @@ extension Expectation {
/// - Parameter expectation: The real expectation.
public init(snapshotting expectation: borrowing Expectation) {
self.evaluatedExpression = expectation.evaluatedExpression
self.mismatchedErrorDescription = expectation.mismatchedErrorDescription ?? expectation.mismatchedExitConditionDescription
self.mismatchedErrorDescription = expectation.mismatchedErrorDescription
self.differenceDescription = expectation.differenceDescription
self.isPassing = expectation.isPassing
self.isRequired = expectation.isRequired
Expand Down
Loading
Loading