Skip to content

Improve the diagnostics for a bad exit test capture. #1146

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 3 commits into from
Jun 12, 2025
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
21 changes: 21 additions & 0 deletions Sources/Testing/ExitTests/ExitTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,27 @@ extension ExitTest {
outValue.initializeMemory(as: Record.self, to: record)
return true
}

/// Attempt to store an invalid exit test into the given memory.
///
/// This overload of `__store()` is provided to suppress diagnostics when a
/// value of an unsupported type is captured as an argument of `body`. It
/// always terminates the current process.
///
/// - Warning: This function is used to implement the
/// `#expect(processExitsWith:)` macro. Do not use it directly.
#if compiler(>=6.2)
@safe
#endif
public static func __store<T>(
_ id: (UInt64, UInt64, UInt64, UInt64),
_ body: T,
into outValue: UnsafeMutableRawPointer,
asTypeAt typeAddress: UnsafeRawPointer,
withHintAt hintAddress: UnsafeRawPointer? = nil
) -> CBool {
fatalError("Unimplemented")
}
}

@_spi(ForToolsIntegrationOnly)
Expand Down
34 changes: 34 additions & 0 deletions Sources/Testing/Expectations/Expectation+Macro.swift
Original file line number Diff line number Diff line change
Expand Up @@ -572,3 +572,37 @@ public macro require(
sourceLocation: SourceLocation = #_sourceLocation,
performing expression: @escaping @Sendable @convention(thin) () async throws -> Void
) -> ExitTest.Result = #externalMacro(module: "TestingMacros", type: "ExitTestRequireMacro")

/// Capture a sendable and codable value to pass to an exit test.
///
/// - Parameters:
/// - value: The captured value.
/// - name: The name of the capture list item corresponding to `value`.
///
/// - Returns: `value` verbatim.
///
/// - Warning: This macro is used to implement the `#expect(processExitsWith:)`
/// macro. Do not use it directly.
@freestanding(expression)
public macro __capturedValue<T>(
_ value: T,
_ name: String
) -> T = #externalMacro(module: "TestingMacros", type: "ExitTestCapturedValueMacro") where T: Sendable & Codable

/// Emit a compile-time diagnostic when an unsupported value is captured by an
/// exit test.
///
/// - Parameters:
/// - value: The captured value.
/// - name: The name of the capture list item corresponding to `value`.
///
/// - Returns: The result of a call to `fatalError()`. `value` is discarded at
/// compile time.
///
/// - Warning: This macro is used to implement the `#expect(processExitsWith:)`
/// macro. Do not use it directly.
@freestanding(expression)
public macro __capturedValue<T>(
_ value: borrowing T,
_ name: String
) -> Never = #externalMacro(module: "TestingMacros", type: "ExitTestBadCapturedValueMacro") where T: ~Copyable & ~Escapable
1 change: 1 addition & 0 deletions Sources/TestingMacros/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ endif()

target_sources(TestingMacros PRIVATE
ConditionMacro.swift
ExitTestCapturedValueMacro.swift
PragmaMacro.swift
SourceLocationMacro.swift
SuiteDeclarationMacro.swift
Expand Down
2 changes: 1 addition & 1 deletion Sources/TestingMacros/ConditionMacro.swift
Original file line number Diff line number Diff line change
Expand Up @@ -551,7 +551,7 @@ extension ExitTestConditionMacro {
label: "encodingCapturedValues",
expression: TupleExprSyntax {
for capturedValue in capturedValues {
LabeledExprSyntax(expression: capturedValue.expression.trimmed)
LabeledExprSyntax(expression: capturedValue.typeCheckedExpression)
}
}
)
Expand Down
54 changes: 54 additions & 0 deletions Sources/TestingMacros/ExitTestCapturedValueMacro.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2025 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for Swift project authors
//

public import SwiftSyntax
import SwiftSyntaxBuilder
public import SwiftSyntaxMacros

/// The implementation of the `#__capturedValue()` macro when the value conforms
/// to the necessary protocols.
///
/// This type is used to implement the `#__capturedValue()` macro. Do not use it
/// directly.
public struct ExitTestCapturedValueMacro: ExpressionMacro, Sendable {
public static func expansion(
of macro: some FreestandingMacroExpansionSyntax,
in context: some MacroExpansionContext
) throws -> ExprSyntax {
let arguments = Array(macro.arguments)
let expr = arguments[0].expression

// No additional processing is required as this expression's type meets our
// requirements.

return expr
}
}

/// The implementation of the `#__capturedValue()` macro when the value does
/// _not_ conform to the necessary protocols.
///
/// This type is used to implement the `#__capturedValue()` macro. Do not use it
/// directly.
public struct ExitTestBadCapturedValueMacro: ExpressionMacro, Sendable {
public static func expansion(
of macro: some FreestandingMacroExpansionSyntax,
in context: some MacroExpansionContext
) throws -> ExprSyntax {
let arguments = Array(macro.arguments)
let expr = arguments[0].expression
let nameExpr = arguments[1].expression.cast(StringLiteralExprSyntax.self)

// Diagnose that the type of 'expr' is invalid.
context.diagnose(.capturedValueMustBeSendableAndCodable(expr, name: nameExpr))

return #"Swift.fatalError("Unsupported")"#
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,15 @@ struct CapturedValueInfo {
/// The type of the captured value.
var type: TypeSyntax

/// The expression to assign to the captured value with type-checking applied.
var typeCheckedExpression: ExprSyntax {
#"#__capturedValue(\#(expression.trimmed), \#(literal: name.trimmedDescription))"#
}

init(_ capture: ClosureCaptureSyntax, in context: some MacroExpansionContext) {
self.capture = capture
self.expression = "()"
self.type = "Swift.Void"
self.expression = #"Swift.fatalError("Unsupported")"#
self.type = "Swift.Never"

// We don't support capture specifiers at this time.
if let specifier = capture.specifier {
Expand Down
18 changes: 18 additions & 0 deletions Sources/TestingMacros/Support/DiagnosticMessage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -827,6 +827,24 @@ extension DiagnosticMessage {
)
}

/// Create a diagnostic message stating that a captured value must conform to
/// `Sendable` and `Codable`.
///
/// - Parameters:
/// - valueExpr: The captured value.
/// - nameExpr: The name of the capture list item corresponding to
/// `valueExpr`.
///
/// - Returns: A diagnostic message.
static func capturedValueMustBeSendableAndCodable(_ valueExpr: ExprSyntax, name nameExpr: StringLiteralExprSyntax) -> Self {
let name = nameExpr.representedLiteralValue ?? valueExpr.trimmedDescription
return Self(
syntax: Syntax(valueExpr),
message: "Type of captured value '\(name)' must conform to 'Sendable' and 'Codable'",
severity: .error
)
}

/// Create a diagnostic message stating that a capture clause cannot be used
/// in an exit test.
///
Expand Down
2 changes: 2 additions & 0 deletions Sources/TestingMacros/TestingMacrosMain.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ struct TestingMacrosMain: CompilerPlugin {
RequireThrowsNeverMacro.self,
ExitTestExpectMacro.self,
ExitTestRequireMacro.self,
ExitTestCapturedValueMacro.self,
ExitTestBadCapturedValueMacro.self,
TagMacro.self,
SourceLocationMacro.self,
PragmaMacro.self,
Expand Down
24 changes: 24 additions & 0 deletions Tests/TestingTests/ExitTestTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -456,6 +456,30 @@ private import _TestingInternals
#expect(instance.x == 123)
}
}

@Test("Capturing #_sourceLocation")
func captureListPreservesSourceLocationMacro() async {
func sl(_ sl: SourceLocation = #_sourceLocation) -> SourceLocation {
sl
}
await #expect(processExitsWith: .success) { [sl = sl() as SourceLocation] in
#expect(sl.fileID == #fileID)
}
}

#if false // intentionally fails to compile
struct NonCodableValue {}

// We can't capture a value that isn't Codable. A unit test is not possible
// for this case as the type checker needs to get involved.
@Test("Capturing a move-only value")
func captureListWithMoveOnlyValue() async {
let x = NonCodableValue()
await #expect(processExitsWith: .success) { [x = x as NonCodableValue] in
_ = x
}
}
#endif
#endif
}

Expand Down