diff --git a/Package.swift b/Package.swift index 3df51ca48..13dbf61cf 100644 --- a/Package.swift +++ b/Package.swift @@ -127,10 +127,25 @@ let package = Package( "Testing", "_Testing_CoreGraphics", "_Testing_Foundation", + "MemorySafeTestingTests", ], swiftSettings: .packageSettings ), + // Use a plain `.target` instead of a `.testTarget` to avoid the unnecessary + // overhead of having a separate test target for this module. Conceptually, + // the content in this module is no different than content which would + // typically be placed in the `TestingTests` target, except this content + // needs the (module-wide) strict memory safety feature to be enabled. + .target( + name: "MemorySafeTestingTests", + dependencies: [ + "Testing", + ], + path: "Tests/_MemorySafeTestingTests", + swiftSettings: .packageSettings + .strictMemorySafety + ), + .macro( name: "TestingMacros", dependencies: [ @@ -355,6 +370,18 @@ extension Array where Element == PackageDescription.SwiftSetting { return result } + + /// Settings necessary to enable Strict Memory Safety, introduced in + /// [SE-0458: Opt-in Strict Memory Safety Checking](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0458-strict-memory-safety.md#swiftpm-integration). + static var strictMemorySafety: Self { +#if compiler(>=6.2) + // FIXME: Adopt official `.strictMemorySafety()` condition once the minimum + // supported toolchain is 6.2. + [.unsafeFlags(["-strict-memory-safety"])] +#else + [] +#endif + } } extension Array where Element == PackageDescription.CXXSetting { diff --git a/Sources/Testing/ExitTests/ExitTest.swift b/Sources/Testing/ExitTests/ExitTest.swift index 1e9c29c15..beda3eb4e 100644 --- a/Sources/Testing/ExitTests/ExitTest.swift +++ b/Sources/Testing/ExitTests/ExitTest.swift @@ -327,6 +327,9 @@ extension ExitTest { /// /// - 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: @escaping @Sendable (repeat each T) async throws -> Void, diff --git a/Sources/Testing/Test+Discovery.swift b/Sources/Testing/Test+Discovery.swift index 5e9632d70..71862943d 100644 --- a/Sources/Testing/Test+Discovery.swift +++ b/Sources/Testing/Test+Discovery.swift @@ -39,6 +39,9 @@ extension Test { /// /// - Warning: This function is used to implement the `@Test` macro. Do not /// use it directly. +#if compiler(>=6.2) + @safe +#endif public static func __store( _ generator: @escaping @Sendable () async -> Test, into outValue: UnsafeMutableRawPointer, diff --git a/Sources/TestingMacros/ConditionMacro.swift b/Sources/TestingMacros/ConditionMacro.swift index 49630cfc9..9f87dfbd3 100644 --- a/Sources/TestingMacros/ConditionMacro.swift +++ b/Sources/TestingMacros/ConditionMacro.swift @@ -496,10 +496,11 @@ extension ExitTestConditionMacro { var recordDecl: DeclSyntax? #if !SWT_NO_LEGACY_TEST_DISCOVERY let legacyEnumName = context.makeUniqueName("__🟡$") + let unsafeKeyword: TokenSyntax? = isUnsafeKeywordSupported ? .keyword(.unsafe, trailingTrivia: .space) : nil recordDecl = """ enum \(legacyEnumName): Testing.__TestContentRecordContainer { nonisolated static var __testContentRecord: Testing.__TestContentRecord { - \(enumName).testContentRecord + \(unsafeKeyword)\(enumName).testContentRecord } } """ diff --git a/Sources/TestingMacros/SuiteDeclarationMacro.swift b/Sources/TestingMacros/SuiteDeclarationMacro.swift index 60a276689..e44b0460a 100644 --- a/Sources/TestingMacros/SuiteDeclarationMacro.swift +++ b/Sources/TestingMacros/SuiteDeclarationMacro.swift @@ -169,12 +169,13 @@ public struct SuiteDeclarationMacro: MemberMacro, PeerMacro, Sendable { #if !SWT_NO_LEGACY_TEST_DISCOVERY // Emit a type that contains a reference to the test content record. let enumName = context.makeUniqueName("__🟡$") + let unsafeKeyword: TokenSyntax? = isUnsafeKeywordSupported ? .keyword(.unsafe, trailingTrivia: .space) : nil result.append( """ @available(*, deprecated, message: "This type is an implementation detail of the testing library. Do not use it directly.") enum \(enumName): Testing.__TestContentRecordContainer { nonisolated static var __testContentRecord: Testing.__TestContentRecord { - \(testContentRecordName) + \(unsafeKeyword)\(testContentRecordName) } } """ diff --git a/Sources/TestingMacros/Support/EffectfulExpressionHandling.swift b/Sources/TestingMacros/Support/EffectfulExpressionHandling.swift index 494d2fcfc..a0b84e737 100644 --- a/Sources/TestingMacros/Support/EffectfulExpressionHandling.swift +++ b/Sources/TestingMacros/Support/EffectfulExpressionHandling.swift @@ -86,6 +86,17 @@ extension BidirectionalCollection { // MARK: - Inserting effect keywords/thunks +/// Whether or not the `unsafe` expression keyword is supported. +var isUnsafeKeywordSupported: Bool { + // The 'unsafe' keyword was introduced in 6.2 as part of SE-0458. Older + // toolchains are not aware of it. +#if compiler(>=6.2) + true +#else + false +#endif +} + /// Make a function call expression to an effectful thunk function provided by /// the testing library. /// @@ -127,12 +138,7 @@ func applyEffectfulKeywords(_ effectfulKeywords: Set, to expr: some Exp let needAwait = effectfulKeywords.contains(.await) && !expr.is(AwaitExprSyntax.self) let needTry = effectfulKeywords.contains(.try) && !expr.is(TryExprSyntax.self) - // The 'unsafe' keyword was introduced in 6.2 as part of SE-0458. Older - // toolchains are not aware of it, so avoid emitting expressions involving - // that keyword when the macro has been built using an older toolchain. -#if compiler(>=6.2) - let needUnsafe = effectfulKeywords.contains(.unsafe) && !expr.is(UnsafeExprSyntax.self) -#endif + let needUnsafe = isUnsafeKeywordSupported && effectfulKeywords.contains(.unsafe) && !expr.is(UnsafeExprSyntax.self) // First, add thunk function calls. if needAwait { @@ -141,11 +147,9 @@ func applyEffectfulKeywords(_ effectfulKeywords: Set, to expr: some Exp if needTry { expr = _makeCallToEffectfulThunk(.identifier("__requiringTry"), passing: expr) } -#if compiler(>=6.2) if needUnsafe { expr = _makeCallToEffectfulThunk(.identifier("__requiringUnsafe"), passing: expr) } -#endif // Then add keyword expressions. (We do this separately so we end up writing // `try await __r(__r(self))` instead of `try __r(await __r(self))` which is @@ -153,7 +157,7 @@ func applyEffectfulKeywords(_ effectfulKeywords: Set, to expr: some Exp if needAwait { expr = ExprSyntax( AwaitExprSyntax( - awaitKeyword: .keyword(.await).with(\.trailingTrivia, .space), + awaitKeyword: .keyword(.await, trailingTrivia: .space), expression: expr ) ) @@ -161,21 +165,19 @@ func applyEffectfulKeywords(_ effectfulKeywords: Set, to expr: some Exp if needTry { expr = ExprSyntax( TryExprSyntax( - tryKeyword: .keyword(.try).with(\.trailingTrivia, .space), + tryKeyword: .keyword(.try, trailingTrivia: .space), expression: expr ) ) } -#if compiler(>=6.2) if needUnsafe { expr = ExprSyntax( UnsafeExprSyntax( - unsafeKeyword: .keyword(.unsafe).with(\.trailingTrivia, .space), + unsafeKeyword: .keyword(.unsafe, trailingTrivia: .space), expression: expr ) ) } -#endif expr.leadingTrivia = originalExpr.leadingTrivia expr.trailingTrivia = originalExpr.trailingTrivia diff --git a/Sources/TestingMacros/Support/TestContentGeneration.swift b/Sources/TestingMacros/Support/TestContentGeneration.swift index 9a2529cee..2999478de 100644 --- a/Sources/TestingMacros/Support/TestContentGeneration.swift +++ b/Sources/TestingMacros/Support/TestContentGeneration.swift @@ -63,12 +63,13 @@ func makeTestContentRecordDecl(named name: TokenSyntax, in typeName: TypeSyntax? IntegerLiteralExprSyntax(context, radix: .binary) } + let unsafeKeyword: TokenSyntax? = isUnsafeKeywordSupported ? .keyword(.unsafe, trailingTrivia: .space) : nil var result: DeclSyntax = """ @available(*, deprecated, message: "This property is an implementation detail of the testing library. Do not use it directly.") private nonisolated \(staticKeyword(for: typeName)) let \(name): Testing.__TestContentRecord = ( \(kindExpr), \(kind.commentRepresentation) 0, - \(accessorName), + \(unsafeKeyword)\(accessorName), \(contextExpr), 0 ) diff --git a/Sources/TestingMacros/TestDeclarationMacro.swift b/Sources/TestingMacros/TestDeclarationMacro.swift index 0b2d43f1e..58e8259ec 100644 --- a/Sources/TestingMacros/TestDeclarationMacro.swift +++ b/Sources/TestingMacros/TestDeclarationMacro.swift @@ -494,12 +494,13 @@ public struct TestDeclarationMacro: PeerMacro, Sendable { #if !SWT_NO_LEGACY_TEST_DISCOVERY // Emit a type that contains a reference to the test content record. let enumName = context.makeUniqueName(thunking: functionDecl, withPrefix: "__🟡$") + let unsafeKeyword: TokenSyntax? = isUnsafeKeywordSupported ? .keyword(.unsafe, trailingTrivia: .space) : nil result.append( """ @available(*, deprecated, message: "This type is an implementation detail of the testing library. Do not use it directly.") enum \(enumName): Testing.__TestContentRecordContainer { nonisolated static var __testContentRecord: Testing.__TestContentRecord { - \(testContentRecordName) + \(unsafeKeyword)\(testContentRecordName) } } """ diff --git a/Tests/_MemorySafeTestingTests/MemorySafeTestDecls.swift b/Tests/_MemorySafeTestingTests/MemorySafeTestDecls.swift new file mode 100644 index 000000000..baf02c026 --- /dev/null +++ b/Tests/_MemorySafeTestingTests/MemorySafeTestDecls.swift @@ -0,0 +1,31 @@ +// +// 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 +// + +#if compiler(>=6.2) + +@testable import Testing + +#if !hasFeature(StrictMemorySafety) +#error("This file requires strict memory safety to be enabled") +#endif + +@Test(.hidden) +func exampleTestFunction() {} + +@Suite(.hidden) +struct ExampleSuite { + @Test func example() {} +} + +func exampleExitTest() async { + await #expect(processExitsWith: .success) {} +} + +#endif