Skip to content

Commit 5ccb3b7

Browse files
authored
Improve diagnostic output when an exit test fails. (#577)
This PR ensures that the expected and actual exit conditions are included in diagnostic output when an exit test fails. I haven't yet given `ExitCondition` a custom test description, but for now this allows you to see at a glance what went wrong. ### Example test ```swift @test func f() async { await #expect(exitsWith: .exitCode(12345)) { exit(99999) } } ``` ### Example output > Expectation failed: .exitCode(12345): exit(99999) → .exitCode(99999) ### 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 42f0306 commit 5ccb3b7

File tree

6 files changed

+20
-24
lines changed

6 files changed

+20
-24
lines changed

Sources/Testing/ExitTests/ExitTest.swift

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -149,21 +149,11 @@ func callExitTest(
149149
)
150150
}
151151

152-
lazy var actualValue: CInt? = switch actualExitCondition {
153-
case .failure:
154-
nil
155-
case let .exitCode(exitCode):
156-
exitCode
157-
#if !os(Windows)
158-
case let .signal(signal):
159-
signal
160-
#endif
161-
}
162-
163152
return __checkValue(
164153
expectedExitCondition.matches(actualExitCondition),
165154
expression: expression,
166-
expressionWithCapturedRuntimeValues: expression.capturingRuntimeValues(actualValue),
155+
expressionWithCapturedRuntimeValues: expression.capturingRuntimeValues(actualExitCondition),
156+
mismatchedExitConditionDescription: String(describingForTest: expectedExitCondition),
167157
comments: comments(),
168158
isRequired: isRequired,
169159
sourceLocation: sourceLocation

Sources/Testing/Expectations/Expectation.swift

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ public struct Expectation: Sendable {
1818
///
1919
/// If this expectation passed, the value of this property is `nil` because no
2020
/// error mismatch occurred.
21-
@_spi(ForToolsIntegrationOnly)
21+
@_spi(Experimental) @_spi(ForToolsIntegrationOnly)
2222
public var mismatchedErrorDescription: String?
2323

2424
/// A description of the difference between the operands in the expression
@@ -27,9 +27,16 @@ public struct Expectation: Sendable {
2727
/// If this expectation passed, the value of this property is `nil` because
2828
/// the difference is only computed when necessary to assist with diagnosing
2929
/// test failures.
30-
@_spi(ForToolsIntegrationOnly)
30+
@_spi(Experimental) @_spi(ForToolsIntegrationOnly)
3131
public var differenceDescription: String?
3232

33+
/// A description of the exit condition that was expected to be matched.
34+
///
35+
/// If this expectation passed, the value of this property is `nil` because no
36+
/// exit test failed.
37+
@_spi(Experimental) @_spi(ForToolsIntegrationOnly)
38+
public var mismatchedExitConditionDescription: String?
39+
3340
/// Whether the expectation passed or failed.
3441
///
3542
/// An expectation is considered to pass when its condition evaluates to
@@ -93,7 +100,7 @@ extension Expectation {
93100
/// - Parameter expectation: The real expectation.
94101
public init(snapshotting expectation: borrowing Expectation) {
95102
self.evaluatedExpression = expectation.evaluatedExpression
96-
self.mismatchedErrorDescription = expectation.mismatchedErrorDescription
103+
self.mismatchedErrorDescription = expectation.mismatchedErrorDescription ?? expectation.mismatchedExitConditionDescription
97104
self.differenceDescription = expectation.differenceDescription
98105
self.isPassing = expectation.isPassing
99106
self.isRequired = expectation.isRequired

Sources/Testing/Expectations/ExpectationChecking+Macro.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ public func __checkValue(
6464
expressionWithCapturedRuntimeValues: @autoclosure () -> __Expression? = nil,
6565
mismatchedErrorDescription: @autoclosure () -> String? = nil,
6666
difference: @autoclosure () -> String? = nil,
67+
mismatchedExitConditionDescription: @autoclosure () -> String? = nil,
6768
comments: @autoclosure () -> [Comment],
6869
isRequired: Bool,
6970
sourceLocation: SourceLocation
@@ -106,6 +107,7 @@ public func __checkValue(
106107
// only evaluated and included lazily upon failure.
107108
expectation.mismatchedErrorDescription = mismatchedErrorDescription()
108109
expectation.differenceDescription = difference()
110+
expectation.mismatchedExitConditionDescription = mismatchedExitConditionDescription()
109111

110112
// Ensure the backtrace is captured here so it has fewer extraneous frames
111113
// from the testing framework which aren't relevant to the user.

Sources/Testing/Issues/Issue.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,8 @@ extension Issue.Kind: CustomStringConvertible {
178178
case let .expectationFailed(expectation):
179179
if let mismatchedErrorDescription = expectation.mismatchedErrorDescription {
180180
"Expectation failed: \(mismatchedErrorDescription)"
181+
} else if let mismatchedExitConditionDescription = expectation.mismatchedExitConditionDescription {
182+
"Expectation failed: \(mismatchedExitConditionDescription)"
181183
} else {
182184
"Expectation failed: \(expectation.evaluatedExpression.expandedDescription())"
183185
}

Sources/TestingMacros/Support/ConditionArgumentParsing.swift

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -230,13 +230,8 @@ private func _parseCondition(from expr: AsExprSyntax, for macro: some Freestandi
230230
/// - Returns: An instance of ``Condition`` describing `expr`.
231231
private func _parseCondition(from expr: ClosureExprSyntax, for macro: some FreestandingMacroExpansionSyntax, in context: some MacroExpansionContext) -> Condition {
232232
if expr.signature == nil && expr.statements.count == 1, let item = expr.statements.first?.item {
233-
if case let .expr(bodyExpr) = item {
234-
return Condition(
235-
"__checkValue",
236-
arguments: [Argument(expression: expr)],
237-
expression: _parseCondition(from: bodyExpr, for: macro, in: context).expression
238-
)
239-
}
233+
// TODO: capture closures as a different kind of Testing.Expression with a
234+
// separate subexpression per code item.
240235

241236
// If a closure contains a single statement or declaration, we can't
242237
// meaningfully break it down as an expression, but we can still capture its

Tests/TestingMacrosTests/ConditionMacroTests.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ struct ConditionMacroTests {
4545
##"#expect(1 is Int)"##:
4646
##"Testing.__checkCast(1, is: (Int).self, expression: .__fromBinaryOperation(.__fromSyntaxNode("1"), "is", .__fromSyntaxNode("Int")), comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##,
4747
##"#expect("123") { 1 == 2 } then: { foo() }"##:
48-
##"Testing.__checkClosureCall(performing: { 1 == 2 }, then: { foo() }, expression: .__fromBinaryOperation(.__fromSyntaxNode("1"), "==", .__fromSyntaxNode("2")), comments: ["123"], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##,
48+
##"Testing.__checkClosureCall(performing: { 1 == 2 }, then: { foo() }, expression: .__fromSyntaxNode("1 == 2"), comments: ["123"], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##,
4949
##"#expect("123") { let x = 0 }"##:
5050
##"Testing.__checkClosureCall(performing: { let x = 0 }, expression: .__fromSyntaxNode("let x = 0"), comments: ["123"], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##,
5151
##"#expect("123") { let x = 0; return x == 0 }"##:
@@ -121,7 +121,7 @@ struct ConditionMacroTests {
121121
##"#require(1 is Int)"##:
122122
##"Testing.__checkCast(1, is: (Int).self, expression: .__fromBinaryOperation(.__fromSyntaxNode("1"), "is", .__fromSyntaxNode("Int")), comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##,
123123
##"#require("123") { 1 == 2 } then: { foo() }"##:
124-
##"Testing.__checkClosureCall(performing: { 1 == 2 }, then: { foo() }, expression: .__fromBinaryOperation(.__fromSyntaxNode("1"), "==", .__fromSyntaxNode("2")), comments: ["123"], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##,
124+
##"Testing.__checkClosureCall(performing: { 1 == 2 }, then: { foo() }, expression: .__fromSyntaxNode("1 == 2"), comments: ["123"], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##,
125125
##"#require("123") { let x = 0 }"##:
126126
##"Testing.__checkClosureCall(performing: { let x = 0 }, expression: .__fromSyntaxNode("let x = 0"), comments: ["123"], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##,
127127
##"#require("123") { let x = 0; return x == 0 }"##:

0 commit comments

Comments
 (0)