Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Store test content in a custom metadata section.
Browse files Browse the repository at this point in the history
This PR uses the experimental symbol linkage margers feature in the Swift
compiler to emit metadata about tests (and exit tests) into a dedicated section
of the test executable being built. At runtime, we discover that section and
read out the tests from it.

This has several benefits over our current model, which involves walking Swift's
type metadata table looking for types that conform to a protocol:

1. We don't need to define that protocol as public API in Swift Testing,
1. We don't need to emit type metadata (much larger than what we really need)
   for every test function,
1. We don't need to duplicate a large chunk of the Swift ABI sources in order to
   walk the type metadata table correctly, and
1. Almost all the new code is written in Swift, whereas the code it is intended
   to replace could not be fully represented in Swift and needed to be written
   in C++.

The change also opens up the possibility of supporting generic types in the
future because we can emit metadata without needing to emit a nested type (which
is not always valid in a generic context.) That's a "future direction" and not
covered by this PR specifically.

I've defined a layout for entries in the new `swift5_tests` section that should
be flexible enough for us in the short-to-medium term and which lets us define
additional arbitrary test content record types. The layout of this section is
covered in depth in the new [TestContent.md](Documentation/ABI/TestContent.md)
article.

This functionality is only available if a test target enables the experimental
`"SymbolLinkageMarkers"` feature. We continue to emit protocol-conforming types
for now—that code will be removed if and when the experimental feature is
properly supported (modulo us adopting relevant changes to the feature's API.)

#735
swiftlang/swift#76698
swiftlang/swift#78411
grynspan committed Jan 10, 2025
1 parent 801d5ea commit 4e01f8b
Showing 17 changed files with 388 additions and 82 deletions.
122 changes: 79 additions & 43 deletions Documentation/Porting.md
Original file line number Diff line number Diff line change
@@ -66,7 +66,7 @@ platform-specific attention.
> These errors are produced when the configuration you're trying to build has
> conflicting requirements (for example, attempting to enable support for pipes
> without also enabling support for file I/O.) You should be able to resolve
> these issues by updating Package.swift and/or CompilerSettings.cmake.
> these issues by updating `Package.swift` and/or `CompilerSettings.cmake`.
Most platform dependencies can be resolved through the use of platform-specific
API. For example, Swift Testing uses the C11 standard [`timespec`](https://en.cppreference.com/w/c/chrono/timespec)
@@ -123,69 +123,105 @@ Once the header is included, we can call `GetDateTime()` from `Clock.swift`:
## Runtime test discovery

When porting to a new platform, you may need to provide a new implementation for
`enumerateTypeMetadataSections()` in `Discovery.cpp`. Test discovery is
`_testContentSectionBounds()` in `Discovery+Platform.swift`. Test discovery is
dependent on Swift metadata discovery which is an inherently platform-specific
operation.

_Most_ platforms will be able to reuse the implementation used by Linux and
Windows that calls an internal Swift runtime function to enumerate available
metadata. If you are porting Swift Testing to Classic, this function won't be
available, so you'll need to write a custom implementation instead. Assuming
that the Swift compiler emits section information into the resource fork on
Classic, you could use the [Resource Manager](https://developer.apple.com/library/archive/documentation/mac/pdf/MoreMacintoshToolbox.pdf)
> [!NOTE]
> You do not need to provide an implementation for the function
> `enumerateTypeMetadataSections()` in `Discovery+Legacy.cpp`: it is present for
> backwards compatibility with Swift 6.0 toolchains and will be removed in a
> future release.
_Most_ platforms in use today use the ELF image format and will be able to reuse
the implementation used by Linux.

Classic does not use the ELF image format, so you'll need to write a custom
implementation of `_testContentSectionBounds()` instead. Assuming that the Swift
compiler emits section information into the resource fork on Classic, you would
use the [Resource Manager](https://developer.apple.com/library/archive/documentation/mac/pdf/MoreMacintoshToolbox.pdf)
to load that information:

```diff
--- a/Sources/_TestingInternals/Discovery.cpp
+++ b/Sources/_TestingInternals/Discovery.cpp
--- a/Sources/Testing/Discovery+Platform.swift
+++ b/Sources/Testing/Discovery+Platform.swift

// ...
+#elif defined(macintosh)
+template <typename SectionEnumerator>
+static void enumerateTypeMetadataSections(const SectionEnumerator& body) {
+ ResFileRefNum refNum;
+ if (noErr == GetTopResourceFile(&refNum)) {
+ ResFileRefNum oldRefNum = refNum;
+ do {
+ UseResFile(refNum);
+ Handle handle = Get1NamedResource('swft', "\p__swift5_types");
+ if (handle && *handle) {
+ auto imageAddress = reinterpret_cast<const void *>(static_cast<uintptr_t>(refNum));
+ SWTSectionBounds sb = { imageAddress, *handle, GetHandleSize(handle) };
+ bool stop = false;
+ body(sb, &stop);
+ if (stop) {
+ break;
+ }
+ }
+ } while (noErr == GetNextResourceFile(refNum, &refNum));
+ UseResFile(oldRefNum);
+#elseif os(Classic)
+private func _testContentSectionBounds() -> [SectionBounds] {
+ let oldRefNum = CurResFile()
+ defer {
+ UseResFile(oldRefNum)
+ }
+
+ var refNum = ResFileRefNum(0)
+ guard noErr == GetTopResourceFile(&refNum) else {
+ return []
+ }
+
+ var result = [SectionBounds]()
+ repeat {
+ UseResFile(refNum)
+ guard let handle = Get1NamedResource(ResType("swft"), Str255("__swift5_tests")) else {
+ continue
+ }
+ let sb = SectionBounds(
+ imageAddress: UnsafeRawPointer(bitPattern: UInt(refNum)),
+ start: handle.pointee!,
+ size: GetHandleSize(handle)
+ )
+ result.append(sb)
+ } while noErr == GetNextResourceFile(refNum, &refNum))
+ return result
+}
#else
#warning Platform-specific implementation missing: Runtime test discovery unavailable (dynamic)
template <typename SectionEnumerator>
static void enumerateTypeMetadataSections(const SectionEnumerator& body) {}
private func _testContentSectionBounds() -> [SectionBounds] {
#warning("Platform-specific implementation missing: Runtime test discovery unavailable (dynamic)")
return []
}
#endif
```

You will also need to update the `makeTestContentRecordDecl()` function in the
`TestingMacros` target to emit the correct `@_section` attribute for your
platform. If your platform uses the ELF image format and supports the
`dl_iterate_phdr()` function, add it to the existing `#elseif os(Linux) || ...`
case. Otherwise, add a new case for your platform:

```diff
--- a/Sources/TestingMacros/Support/TestContentGeneration.swift
+++ b/Sources/TestingMacros/Support/TestContentGeneration.swift
// ...
+ #elseif os(Classic)
+ @_section(".rsrc,swft,__swift5_tests")
#else
@__testing(warning: "Platform-specific implementation missing: test content section name unavailable")
#endif
```

Keep in mind that this code is emitted by the `@Test` and `@Suite` macros
directly into test authors' test targets, so you will not be able to use
compiler conditionals defined in the Swift Testing package (including those that
start with `"SWT_"`).

## Runtime test discovery with static linkage

If your platform does not support dynamic linking and loading, you will need to
use static linkage instead. Define the `"SWT_NO_DYNAMIC_LINKING"` compiler
conditional for your platform in both Package.swift and CompilerSettings.cmake,
then define the `sectionBegin` and `sectionEnd` symbols in Discovery.cpp:
conditional for your platform in both `Package.swift` and
`CompilerSettings.cmake`, then define the `testContentSectionBegin` and
`testContentSectionEnd` symbols in `Discovery.cpp`:

```diff
diff --git a/Sources/_TestingInternals/Discovery.cpp b/Sources/_TestingInternals/Discovery.cpp
// ...
+#elif defined(macintosh)
+extern "C" const char sectionBegin __asm__("...");
+extern "C" const char sectionEnd __asm__("...");
+extern "C" const char testContentSectionBegin __asm__("...");
+extern "C" const char testContentSectionEnd __asm__("...");
#else
#warning Platform-specific implementation missing: Runtime test discovery unavailable (static)
static const char sectionBegin = 0;
static const char& sectionEnd = sectionBegin;
static const char testContentSectionBegin = 0;
static const char& testContentSectionEnd = testContentSectionBegin;
#endif
```

@@ -204,12 +240,12 @@ diff --git a/Sources/_TestingInternals/Discovery.cpp b/Sources/_TestingInternals
+#elif defined(macintosh)
+extern "C" const char __linker_defined_begin_symbol;
+extern "C" const char __linker_defined_end_symbol;
+static const auto& sectionBegin = __linker_defined_begin_symbol;
+static const auto& sectionEnd = __linker_defined_end_symbol;
+static const auto& testContentSectionBegin = __linker_defined_begin_symbol;
+static const auto& testContentSectionEnd = __linker_defined_end_symbol;
#else
#warning Platform-specific implementation missing: Runtime test discovery unavailable (static)
static const char sectionBegin = 0;
static const char& sectionEnd = sectionBegin;
static const char testContentSectionBegin = 0;
static const char& testContentSectionEnd = testContentSectionBegin;
#endif
```

2 changes: 2 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -156,6 +156,8 @@ extension Array where Element == PackageDescription.SwiftSetting {
.enableExperimentalFeature("AccessLevelOnImport"),
.enableUpcomingFeature("InternalImportsByDefault"),

.enableExperimentalFeature("SymbolLinkageMarkers"),

.define("SWT_TARGET_OS_APPLE", .when(platforms: [.macOS, .iOS, .macCatalyst, .watchOS, .tvOS, .visionOS])),

.define("SWT_NO_EXIT_TESTS", .when(platforms: [.iOS, .watchOS, .tvOS, .visionOS, .wasi, .android])),
2 changes: 2 additions & 0 deletions Sources/Testing/ExitTests/ExitTest.swift
Original file line number Diff line number Diff line change
@@ -248,6 +248,7 @@ extension ExitTest {
}
}

#if !SWT_NO_LEGACY_TEST_DISCOVERY
if result == nil {
// Call the legacy lookup function that discovers tests embedded in types.
enumerateTypes(withNamesContaining: exitTestContainerTypeNameMagic) { _, type, stop in
@@ -264,6 +265,7 @@ extension ExitTest {
}
}
}
#endif

return result
}
2 changes: 2 additions & 0 deletions Sources/Testing/Test+Discovery+Legacy.swift
Original file line number Diff line number Diff line change
@@ -10,6 +10,7 @@

private import _TestingInternals

#if !SWT_NO_LEGACY_TEST_DISCOVERY
/// A protocol describing a type that contains tests.
///
/// - Warning: This protocol is used to implement the `@Test` macro. Do not use
@@ -81,3 +82,4 @@ func enumerateTypes(withNamesContaining nameSubstring: String, _ typeEnumerator:
}
}
}
#endif
2 changes: 2 additions & 0 deletions Sources/Testing/Test+Discovery.swift
Original file line number Diff line number Diff line change
@@ -55,6 +55,7 @@ extension Test: TestContent {
}
}

#if !SWT_NO_LEGACY_TEST_DISCOVERY
if discoveryMode != .newOnly && generators.isEmpty {
enumerateTypes(withNamesContaining: testContainerTypeNameMagic) { imageAddress, type, _ in
guard let type = type as? any __TestContainer.Type else {
@@ -65,6 +66,7 @@ extension Test: TestContent {
}
}
}
#endif

// *Now* we call all the generators and return their results.
// Reduce into a set rather than an array to deduplicate tests that were
2 changes: 2 additions & 0 deletions Sources/TestingMacros/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -87,6 +87,7 @@ target_sources(TestingMacros PRIVATE
Support/Additions/DeclGroupSyntaxAdditions.swift
Support/Additions/EditorPlaceholderExprSyntaxAdditions.swift
Support/Additions/FunctionDeclSyntaxAdditions.swift
Support/Additions/IntegerLiteralExprSyntaxAdditions.swift
Support/Additions/MacroExpansionContextAdditions.swift
Support/Additions/TokenSyntaxAdditions.swift
Support/Additions/TriviaPieceAdditions.swift
@@ -103,6 +104,7 @@ target_sources(TestingMacros PRIVATE
Support/DiagnosticMessage+Diagnosing.swift
Support/SourceCodeCapturing.swift
Support/SourceLocationGeneration.swift
Support/TestContentGeneration.swift
TagMacro.swift
TestDeclarationMacro.swift
TestingMacrosMain.swift)
52 changes: 50 additions & 2 deletions Sources/TestingMacros/ConditionMacro.swift
Original file line number Diff line number Diff line change
@@ -431,11 +431,58 @@ extension ExitTestConditionMacro {

// Create a local type that can be discovered at runtime and which contains
// the exit test body.
let enumName = context.makeUniqueName("__🟠$exit_test_body__")
let accessorName = context.makeUniqueName("")
let outValueArgumentName = context.makeUniqueName("outValue")
let hintArgumentName = context.makeUniqueName("hint")
let sourceLocationLocalName = context.makeUniqueName("sourceLocation")
decls.append(
"""
#if hasFeature(SymbolLinkageMarkers)
func \(accessorName)(_ \(outValueArgumentName): UnsafeMutableRawPointer, _ \(hintArgumentName): UnsafeRawPointer?) -> Bool {
let \(sourceLocationLocalName) = \(createSourceLocationExpr(of: macro, context: context))
if let \(hintArgumentName) = \(hintArgumentName)?.load(as: Testing.SourceLocation.self),
\(hintArgumentName) != \(sourceLocationLocalName) {
return false
}
\(outValueArgumentName).initializeMemory(
as: Testing.__ExitTest.self,
to: .init(
__expectedExitCondition: \(arguments[expectedExitConditionIndex].expression.trimmed),
sourceLocation: \(sourceLocationLocalName),
body: \(bodyThunkName)
)
)
return true
}
#endif
"""
)

let enumName = context.makeUniqueName("")
let sectionContent = makeTestContentRecordDecl(
named: .identifier("__sectionContent"),
in: TypeSyntax(IdentifierTypeSyntax(name: enumName)),
ofKind: .exitTest,
accessingWith: accessorName
)
decls.append(
"""
#if hasFeature(SymbolLinkageMarkers)
@available(*, deprecated, message: "This type is an implementation detail of the testing library. Do not use it directly.")
enum \(enumName) {
\(sectionContent)
}
#endif
"""
)

#if !SWT_NO_LEGACY_TEST_DISCOVERY
// Emit a legacy type declaration if SymbolLinkageMarkers is off.
let legacyEnumName = context.makeUniqueName("__🟠$exit_test_body__")
decls.append(
"""
@available(*, deprecated, message: "This type is an implementation detail of the testing library. Do not use it directly.")
enum \(enumName): Testing.__ExitTestContainer, Sendable {
enum \(legacyEnumName): Testing.__ExitTestContainer, Sendable {
static var __sourceLocation: Testing.SourceLocation {
\(createSourceLocationExpr(of: macro, context: context))
}
@@ -448,6 +495,7 @@ extension ExitTestConditionMacro {
}
"""
)
#endif

arguments[trailingClosureIndex].expression = ExprSyntax(
ClosureExprSyntax {
46 changes: 43 additions & 3 deletions Sources/TestingMacros/SuiteDeclarationMacro.swift
Original file line number Diff line number Diff line change
@@ -127,6 +127,39 @@ public struct SuiteDeclarationMacro: MemberMacro, PeerMacro, Sendable {
// Parse the @Suite attribute.
let attributeInfo = AttributeInfo(byParsing: suiteAttribute, on: declaration, in: context)

let accessorName = context.makeUniqueName("")
result.append(
"""
#if hasFeature(SymbolLinkageMarkers)
@available(*, deprecated, message: "This property is an implementation detail of the testing library. Do not use it directly.")
private static let \(accessorName): @convention(c) (UnsafeMutableRawPointer, UnsafeRawPointer?) -> Bool = {
$0.initializeMemory(as: (@Sendable () async -> Testing.Test).self) { @Sendable () async in
.__type(
\(declaration.type.trimmed).self,
\(raw: attributeInfo.functionArgumentList(in: context))
)
}
_ = $1 // Ignored.
return true
}
#endif
"""
)

let sectionContentName = context.makeUniqueName("")
result.append(
makeTestContentRecordDecl(
named: sectionContentName,
in: declaration.type,
ofKind: .testDeclaration,
accessingWith: accessorName,
context: 1 << 0 /* suite decl */
)
)

#if !SWT_NO_LEGACY_TEST_DISCOVERY
// Emit a legacy type declaration if SymbolLinkageMarkers is off.
//
// The emitted type must be public or the compiler can optimize it away
// (since it is not actually used anywhere that the compiler can see.)
//
@@ -142,17 +175,24 @@ public struct SuiteDeclarationMacro: MemberMacro, PeerMacro, Sendable {
"""
@available(*, deprecated, message: "This type is an implementation detail of the testing library. Do not use it directly.")
enum \(enumName): Testing.__TestContainer {
static var __tests: [Testing.Test] {
get async {[
private static var __test: Testing.Test {
get async {
.__type(
\(declaration.type.trimmed).self,
\(raw: attributeInfo.functionArgumentList(in: context))
)
]}
}
}
static var __tests: [Testing.Test] {
get async {
[await __test]
}
}
}
"""
)
#endif

return result
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2024 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
//

import SwiftSyntax

extension IntegerLiteralExprSyntax {
init(_ value: some BinaryInteger, radix: IntegerLiteralExprSyntax.Radix = .decimal) {
let stringValue = "\(radix.literalPrefix)\(String(value, radix: radix.size))"
self.init(literal: .integerLiteral(stringValue))
}
}
11 changes: 11 additions & 0 deletions Sources/TestingMacros/Support/Additions/TokenSyntaxAdditions.swift
Original file line number Diff line number Diff line change
@@ -47,3 +47,14 @@ extension TokenSyntax {
return nil
}
}

/// The `static` keyword, if `typeName` is not `nil`.
///
/// - Parameters:
/// - typeName: The name of the type containing the macro being expanded.
///
/// - Returns: A token representing the `static` keyword, or one representing
/// nothing if `typeName` is `nil`.
func staticKeyword(for typeName: TypeSyntax?) -> TokenSyntax {
(typeName != nil) ? .keyword(.static) : .unknown("")
}
91 changes: 91 additions & 0 deletions Sources/TestingMacros/Support/TestContentGeneration.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2024 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
//

import SwiftSyntax
import SwiftSyntaxMacros

/// An enumeration representing the different kinds of test content known to the
/// testing library.
///
/// When adding cases to this enumeration, be sure to also update the
/// corresponding enumeration in TestContent.md.
enum TestContentKind: UInt32 {
/// A test or suite declaration.
case testDeclaration = 0x74657374

/// An exit test.
case exitTest = 0x65786974

/// This kind value as a comment (`/* 'abcd' */`) if it looks like it might be
/// a [FourCC](https://en.wikipedia.org/wiki/FourCC) value, or `nil` if not.
var commentRepresentation: Trivia? {
return withUnsafeBytes(of: rawValue.bigEndian) { bytes in
if bytes.allSatisfy(Unicode.ASCII.isASCII) {
let characters = String(decoding: bytes, as: Unicode.ASCII.self)
let allAlphanumeric = characters.allSatisfy { $0.isLetter || $0.isWholeNumber }
if allAlphanumeric {
return .blockComment("/* '\(characters)' */")
}
}
return nil
}
}
}

/// Make a test content record that can be discovered at runtime by the testing
/// library.
///
/// - Parameters:
/// - name: The name of the record declaration to use in Swift source. The
/// value of this argument should be unique in the context in which the
/// declaration will be emitted.
/// - typeName: The name of the type enclosing the resulting declaration, or
/// `nil` if it will not be emitted into a type's scope.
/// - kind: The kind of test content record being emitted.
/// - accessorName: The Swift name of an `@convention(c)` function to emit
/// into the resulting record.
/// - context: A value to emit as the `context` field of the test content
/// record.
///
/// - Returns: A variable declaration that, when emitted into Swift source, will
/// cause the linker to emit data in a location that is discoverable at
/// runtime.
func makeTestContentRecordDecl(named name: TokenSyntax, in typeName: TypeSyntax? = nil, ofKind kind: TestContentKind, accessingWith accessorName: TokenSyntax, context: UInt32 = 0) -> DeclSyntax {
let kindExpr = IntegerLiteralExprSyntax(kind.rawValue, radix: .hex)
let kindComment = kind.commentRepresentation.map { .space + $0 } ?? Trivia()
let contextExpr = if context == 0 {
IntegerLiteralExprSyntax(0)
} else {
IntegerLiteralExprSyntax(context, radix: .binary)
}

return """
#if hasFeature(SymbolLinkageMarkers)
#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) || os(visionOS)
@_section("__DATA_CONST,__swift5_tests")
#elseif os(Linux) || os(FreeBSD) || os(OpenBSD) || os(Android) || os(WASI)
@_section("swift5_tests")
#elseif os(Windows)
@_section(".sw5test$B")
#else
@__testing(warning: "Platform-specific implementation missing: test content section name unavailable")
#endif
@_used
@available(*, deprecated, message: "This property is an implementation detail of the testing library. Do not use it directly.")
private \(staticKeyword(for: typeName)) let \(name): Testing.__TestContentRecord = (
\(kindExpr),\(kindComment)
0,
\(accessorName),
\(contextExpr),
0
)
#endif
"""
}
93 changes: 60 additions & 33 deletions Sources/TestingMacros/TestDeclarationMacro.swift
Original file line number Diff line number Diff line change
@@ -172,17 +172,6 @@ public struct TestDeclarationMacro: PeerMacro, Sendable {
return FunctionParameterClauseSyntax(parameters: parameterList)
}

/// The `static` keyword, if `typeName` is not `nil`.
///
/// - Parameters:
/// - typeName: The name of the type containing the macro being expanded.
///
/// - Returns: A token representing the `static` keyword, or one representing
/// nothing if `typeName` is `nil`.
private static func _staticKeyword(for typeName: TypeSyntax?) -> TokenSyntax {
(typeName != nil) ? .keyword(.static) : .unknown("")
}

/// Create a thunk function with a normalized signature that calls a
/// developer-supplied test function.
///
@@ -340,7 +329,7 @@ public struct TestDeclarationMacro: PeerMacro, Sendable {
let thunkName = context.makeUniqueName(thunking: functionDecl)
let thunkDecl: DeclSyntax = """
@available(*, deprecated, message: "This function is an implementation detail of the testing library. Do not use it directly.")
@Sendable private \(_staticKeyword(for: typeName)) func \(thunkName)\(thunkParamsExpr) async throws -> Void {
@Sendable private \(staticKeyword(for: typeName)) func \(thunkName)\(thunkParamsExpr) async throws -> Void {
\(thunkBody)
}
"""
@@ -405,16 +394,14 @@ public struct TestDeclarationMacro: PeerMacro, Sendable {

// Create the expression that returns the Test instance for the function.
var testsBody: CodeBlockItemListSyntax = """
return [
.__function(
named: \(literal: functionDecl.completeName.trimmedDescription),
in: \(typeNameExpr),
xcTestCompatibleSelector: \(selectorExpr ?? "nil"),
\(raw: attributeInfo.functionArgumentList(in: context)),
parameters: \(raw: functionDecl.testFunctionParameterList),
testFunction: \(thunkDecl.name)
)
]
return .__function(
named: \(literal: functionDecl.completeName.trimmedDescription),
in: \(typeNameExpr),
xcTestCompatibleSelector: \(selectorExpr ?? "nil"),
\(raw: attributeInfo.functionArgumentList(in: context)),
parameters: \(raw: functionDecl.testFunctionParameterList),
testFunction: \(thunkDecl.name)
)
"""

// If this function has arguments, then it can only be referenced (let alone
@@ -430,16 +417,14 @@ public struct TestDeclarationMacro: PeerMacro, Sendable {
result.append(
"""
@available(*, deprecated, message: "This property is an implementation detail of the testing library. Do not use it directly.")
private \(_staticKeyword(for: typeName)) nonisolated func \(unavailableTestName)() async -> [Testing.Test] {
[
.__function(
named: \(literal: functionDecl.completeName.trimmedDescription),
in: \(typeNameExpr),
xcTestCompatibleSelector: \(selectorExpr ?? "nil"),
\(raw: attributeInfo.functionArgumentList(in: context)),
testFunction: {}
)
]
private \(staticKeyword(for: typeName)) nonisolated func \(unavailableTestName)() async -> Testing.Test {
.__function(
named: \(literal: functionDecl.completeName.trimmedDescription),
in: \(typeNameExpr),
xcTestCompatibleSelector: \(selectorExpr ?? "nil"),
\(raw: attributeInfo.functionArgumentList(in: context)),
testFunction: {}
)
}
"""
)
@@ -454,6 +439,41 @@ public struct TestDeclarationMacro: PeerMacro, Sendable {
)
}

let accessorName = context.makeUniqueName(thunking: functionDecl)
result.append(
"""
#if hasFeature(SymbolLinkageMarkers)
@available(*, deprecated, message: "This property is an implementation detail of the testing library. Do not use it directly.")
private \(staticKeyword(for: typeName)) let \(accessorName): @convention(c) (UnsafeMutableRawPointer, UnsafeRawPointer?) -> Bool = {
$0.initializeMemory(as: (@Sendable () async -> Testing.Test).self) { @Sendable () async in
\(raw: testsBody)
}
_ = $1 // Ignored.
return true
}
#endif
"""
)

var flags = UInt32(0)
if attributeInfo.hasFunctionArguments {
flags |= 1 << 1 /* is parameterized */
}

let sectionContentName = context.makeUniqueName(thunking: functionDecl)
result.append(
makeTestContentRecordDecl(
named: sectionContentName,
in: typeName,
ofKind: .testDeclaration,
accessingWith: accessorName,
context: flags
)
)

#if !SWT_NO_LEGACY_TEST_DISCOVERY
// Emit a legacy type declaration if SymbolLinkageMarkers is off.
//
// The emitted type must be public or the compiler can optimize it away
// (since it is not actually used anywhere that the compiler can see.)
//
@@ -469,14 +489,21 @@ public struct TestDeclarationMacro: PeerMacro, Sendable {
"""
@available(*, deprecated, message: "This type is an implementation detail of the testing library. Do not use it directly.")
enum \(enumName): Testing.__TestContainer {
static var __tests: [Testing.Test] {
private static var __test: Testing.Test {
get async {
\(raw: testsBody)
}
}
static var __tests: [Testing.Test] {
get async {
[await __test]
}
}
}
"""
)
#endif

return result
}
2 changes: 2 additions & 0 deletions Sources/_TestingInternals/Discovery.cpp
Original file line number Diff line number Diff line change
@@ -33,6 +33,7 @@ const void *_Nonnull const SWTTestContentSectionBounds[2] = {
};
#endif

#if !defined(SWT_NO_LEGACY_TEST_DISCOVERY)
#pragma mark - Legacy test discovery

#include <algorithm>
@@ -559,3 +560,4 @@ void swt_enumerateTypesWithNamesContaining(const char *nameSubstring, void *cont
}
});
}
#endif
2 changes: 2 additions & 0 deletions Sources/_TestingInternals/include/Discovery.h
Original file line number Diff line number Diff line change
@@ -73,6 +73,7 @@ SWT_IMPORT_FROM_STDLIB void swift_enumerateAllMetadataSections(
/// when Swift files import the `_TestingInternals` C++ module.
SWT_EXTERN const void *_Nonnull const SWTTestContentSectionBounds[2];

#if !defined(SWT_NO_LEGACY_TEST_DISCOVERY)
#pragma mark - Legacy test discovery

/// The type of callback called by `swt_enumerateTypes()`.
@@ -101,6 +102,7 @@ SWT_EXTERN void swt_enumerateTypesWithNamesContaining(
void *_Null_unspecified context,
SWTTypeEnumerator body
) SWT_SWIFT_NAME(swt_enumerateTypes(withNamesContaining:_:_:));
#endif

SWT_ASSUME_NONNULL_END

4 changes: 3 additions & 1 deletion Tests/TestingMacrosTests/TestDeclarationMacroTests.swift
Original file line number Diff line number Diff line change
@@ -402,7 +402,9 @@ struct TestDeclarationMacroTests {
func differentFunctionTypes(input: String, expectedTypeName: String?, otherCode: String?) throws {
let (output, _) = try parse(input)

#expect(output.contains("__TestContainer"))
withKnownIssue("https://github.com/swiftlang/swift-syntax/issues/2923") {
#expect(output.contains("@_section"))
}
if let expectedTypeName {
#expect(output.contains(expectedTypeName))
}
17 changes: 17 additions & 0 deletions Tests/TestingTests/MiscellaneousTests.swift
Original file line number Diff line number Diff line change
@@ -654,4 +654,21 @@ struct MiscellaneousTests {
}
}
#endif

#if !SWT_NO_LEGACY_TEST_DISCOVERY
@Test("Legacy test discovery finds the same number of tests") func discoveredTestCount() async {
let oldFlag = Environment.variable(named: "SWT_USE_LEGACY_TEST_DISCOVERY")
defer {
Environment.setVariable(oldFlag, named: "SWT_USE_LEGACY_TEST_DISCOVERY")
}

Environment.setVariable("1", named: "SWT_USE_LEGACY_TEST_DISCOVERY")
let testsWithOldCode = await Array(Test.all).count

Environment.setVariable("0", named: "SWT_USE_LEGACY_TEST_DISCOVERY")
let testsWithNewCode = await Array(Test.all).count

#expect(testsWithOldCode == testsWithNewCode)
}
#endif
}
2 changes: 2 additions & 0 deletions cmake/modules/shared/CompilerSettings.cmake
Original file line number Diff line number Diff line change
@@ -16,6 +16,8 @@ add_compile_options(
add_compile_options(
"SHELL:$<$<COMPILE_LANGUAGE:Swift>:-Xfrontend -enable-upcoming-feature -Xfrontend ExistentialAny>"
"SHELL:$<$<COMPILE_LANGUAGE:Swift>:-Xfrontend -enable-upcoming-feature -Xfrontend InternalImportsByDefault>")
add_compile_options(
"SHELL:$<$<COMPILE_LANGUAGE:Swift>:-Xfrontend -enable-experimental-feature -Xfrontend SymbolLinkageMarkers>")

# Platform-specific definitions.
if(APPLE)

0 comments on commit 4e01f8b

Please sign in to comment.