diff --git a/Sources/Testing/ExitTests/ExitTest.swift b/Sources/Testing/ExitTests/ExitTest.swift index 9284310db..c8dec5aa1 100644 --- a/Sources/Testing/ExitTests/ExitTest.swift +++ b/Sources/Testing/ExitTests/ExitTest.swift @@ -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( + _ id: (UInt64, UInt64, UInt64, UInt64), + _ body: T, + into outValue: UnsafeMutableRawPointer, + asTypeAt typeAddress: UnsafeRawPointer, + withHintAt hintAddress: UnsafeRawPointer? = nil + ) -> CBool { + fatalError("Unimplemented") + } } @_spi(ForToolsIntegrationOnly) diff --git a/Sources/Testing/Expectations/Expectation+Macro.swift b/Sources/Testing/Expectations/Expectation+Macro.swift index f85c7042b..fef826bfe 100644 --- a/Sources/Testing/Expectations/Expectation+Macro.swift +++ b/Sources/Testing/Expectations/Expectation+Macro.swift @@ -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( + _ 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( + _ value: borrowing T, + _ name: String +) -> Never = #externalMacro(module: "TestingMacros", type: "ExitTestBadCapturedValueMacro") where T: ~Copyable & ~Escapable diff --git a/Sources/TestingMacros/CMakeLists.txt b/Sources/TestingMacros/CMakeLists.txt index c9a579eaf..effa782a9 100644 --- a/Sources/TestingMacros/CMakeLists.txt +++ b/Sources/TestingMacros/CMakeLists.txt @@ -81,6 +81,7 @@ endif() target_sources(TestingMacros PRIVATE ConditionMacro.swift + ExitTestCapturedValueMacro.swift PragmaMacro.swift SourceLocationMacro.swift SuiteDeclarationMacro.swift diff --git a/Sources/TestingMacros/ConditionMacro.swift b/Sources/TestingMacros/ConditionMacro.swift index 9f87dfbd3..37cbc7339 100644 --- a/Sources/TestingMacros/ConditionMacro.swift +++ b/Sources/TestingMacros/ConditionMacro.swift @@ -551,7 +551,7 @@ extension ExitTestConditionMacro { label: "encodingCapturedValues", expression: TupleExprSyntax { for capturedValue in capturedValues { - LabeledExprSyntax(expression: capturedValue.expression.trimmed) + LabeledExprSyntax(expression: capturedValue.typeCheckedExpression) } } ) diff --git a/Sources/TestingMacros/ExitTestCapturedValueMacro.swift b/Sources/TestingMacros/ExitTestCapturedValueMacro.swift new file mode 100644 index 000000000..0038dac7c --- /dev/null +++ b/Sources/TestingMacros/ExitTestCapturedValueMacro.swift @@ -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")"# + } +} diff --git a/Sources/TestingMacros/Support/ClosureCaptureListParsing.swift b/Sources/TestingMacros/Support/ClosureCaptureListParsing.swift index 41abe711c..a7dca88af 100644 --- a/Sources/TestingMacros/Support/ClosureCaptureListParsing.swift +++ b/Sources/TestingMacros/Support/ClosureCaptureListParsing.swift @@ -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 { diff --git a/Sources/TestingMacros/Support/DiagnosticMessage.swift b/Sources/TestingMacros/Support/DiagnosticMessage.swift index 36186ec4b..3a8957207 100644 --- a/Sources/TestingMacros/Support/DiagnosticMessage.swift +++ b/Sources/TestingMacros/Support/DiagnosticMessage.swift @@ -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. /// diff --git a/Sources/TestingMacros/TestingMacrosMain.swift b/Sources/TestingMacros/TestingMacrosMain.swift index 1894f4282..4e98115d0 100644 --- a/Sources/TestingMacros/TestingMacrosMain.swift +++ b/Sources/TestingMacros/TestingMacrosMain.swift @@ -28,6 +28,8 @@ struct TestingMacrosMain: CompilerPlugin { RequireThrowsNeverMacro.self, ExitTestExpectMacro.self, ExitTestRequireMacro.self, + ExitTestCapturedValueMacro.self, + ExitTestBadCapturedValueMacro.self, TagMacro.self, SourceLocationMacro.self, PragmaMacro.self, diff --git a/Tests/TestingTests/ExitTestTests.swift b/Tests/TestingTests/ExitTestTests.swift index 02be1a140..fb2d47f03 100644 --- a/Tests/TestingTests/ExitTestTests.swift +++ b/Tests/TestingTests/ExitTestTests.swift @@ -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 }