Skip to content

Commit

Permalink
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
  • Loading branch information
grynspan committed Jan 13, 2025
1 parent 5927c86 commit fc30eeb
Show file tree
Hide file tree
Showing 18 changed files with 343 additions and 63 deletions.
6 changes: 6 additions & 0 deletions Documentation/Porting.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,8 +145,10 @@ to load that information:
+ let resourceName: Str255 = switch kind {
+ case .testContent:
+ "__swift5_tests"
+#if !SWT_NO_LEGACY_TEST_DISCOVERY
+ case .typeMetadata:
+ "__swift5_types"
+#endif
+ }
+
+ let oldRefNum = CurResFile()
Expand Down Expand Up @@ -219,15 +221,19 @@ diff --git a/Sources/_TestingInternals/Discovery.cpp b/Sources/_TestingInternals
+#elif defined(macintosh)
+extern "C" const char testContentSectionBegin __asm__("...");
+extern "C" const char testContentSectionEnd __asm__("...");
+#if !defined(SWT_NO_LEGACY_TEST_DISCOVERY)
+extern "C" const char typeMetadataSectionBegin __asm__("...");
+extern "C" const char typeMetadataSectionEnd __asm__("...");
+#endif
#else
#warning Platform-specific implementation missing: Runtime test discovery unavailable (static)
static const char testContentSectionBegin = 0;
static const char& testContentSectionEnd = testContentSectionBegin;
#if !defined(SWT_NO_LEGACY_TEST_DISCOVERY)
static const char typeMetadataSectionBegin = 0;
static const char& typeMetadataSectionEnd = testContentSectionBegin;
#endif
#endif
```

These symbols must have unique addresses corresponding to the first byte of the
Expand Down
2 changes: 2 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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])),
Expand Down
42 changes: 18 additions & 24 deletions Sources/Testing/Discovery+Platform.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,10 @@ struct SectionBounds: Sendable {
/// The test content metadata section.
case testContent

#if !SWT_NO_LEGACY_TEST_DISCOVERY
/// The type metadata section.
case typeMetadata
#endif
}

/// All section bounds of the given kind found in the current process.
Expand Down Expand Up @@ -72,36 +74,22 @@ private let _startCollectingSectionBounds: Void = {
return
}

// If this image contains the Swift section we need, acquire the lock and
// If this image contains the Swift section(s) we need, acquire the lock and
// store the section's bounds.
let testContentSectionBounds: SectionBounds? = {
func findSectionBounds(forSectionNamed segmentName: String, _ sectionName: String, ofKind kind: SectionBounds.Kind) {
var size = CUnsignedLong(0)
if let start = getsectiondata(mh, "__DATA_CONST", "__swift5_tests", &size), size > 0 {
if let start = getsectiondata(mh, segmentName, sectionName, &size), size > 0 {
let buffer = UnsafeRawBufferPointer(start: start, count: Int(clamping: size))
return SectionBounds(imageAddress: mh, buffer: buffer)
}
return nil
}()

let typeMetadataSectionBounds: SectionBounds? = {
var size = CUnsignedLong(0)
if let start = getsectiondata(mh, "__TEXT", "__swift5_types", &size), size > 0 {
let buffer = UnsafeRawBufferPointer(start: start, count: Int(clamping: size))
return SectionBounds(imageAddress: mh, buffer: buffer)
}
return nil
}()

if testContentSectionBounds != nil || typeMetadataSectionBounds != nil {
_sectionBounds.withLock { sectionBounds in
if let testContentSectionBounds {
sectionBounds[.testContent]!.append(testContentSectionBounds)
}
if let typeMetadataSectionBounds {
sectionBounds[.typeMetadata]!.append(typeMetadataSectionBounds)
let sb = SectionBounds(imageAddress: mh, buffer: buffer)
_sectionBounds.withLock { sectionBounds in
sectionBounds[kind]!.append(sb)
}
}
}
findSectionBounds(forSectionNamed: "__DATA_CONST", "__swift5_tests", ofKind: .testContent)
#if !SWT_NO_LEGACY_TEST_DISCOVERY
findSectionBounds(forSectionNamed: "__TEXT", "__swift5_types", ofKind: .typeMetadata)
#endif
}

#if _runtime(_ObjC)
Expand Down Expand Up @@ -160,8 +148,10 @@ private func _sectionBounds(_ kind: SectionBounds.Kind) -> [SectionBounds] {
let range = switch context.pointee.kind {
case .testContent:
sections.swift5_tests
#if !SWT_NO_LEGACY_TEST_DISCOVERY
case .typeMetadata:
sections.swift5_type_metadata
#endif
}
let start = UnsafeRawPointer(bitPattern: range.start)
let size = Int(clamping: range.length)
Expand Down Expand Up @@ -250,8 +240,10 @@ private func _sectionBounds(_ kind: SectionBounds.Kind) -> [SectionBounds] {
let sectionName = switch kind {
case .testContent:
".sw5test"
#if !SWT_NO_LEGACY_TEST_DISCOVERY
case .typeMetadata:
".sw5tymd"
#endif
}
return HMODULE.all.compactMap { _findSection(named: sectionName, in: $0) }
}
Expand Down Expand Up @@ -283,8 +275,10 @@ private func _sectionBounds(_ kind: SectionBounds.Kind) -> CollectionOfOne<Secti
let (sectionBegin, sectionEnd) = switch kind {
case .testContent:
SWTTestContentSectionBounds
#if !SWT_NO_LEGACY_TEST_DISCOVERY
case .typeMetadata:
SWTTypeMetadataSectionBounds
#endif
}
let buffer = UnsafeRawBufferPointer(start: sectionBegin, count: max(0, sectionEnd - sectionBegin))
let sb = SectionBounds(imageAddress: nil, buffer: buffer)
Expand Down
2 changes: 2 additions & 0 deletions Sources/Testing/ExitTests/ExitTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
result = types(withNamesContaining: exitTestContainerTypeNameMagic).lazy
Expand All @@ -261,6 +262,7 @@ extension ExitTest {
)
}
}
#endif

return result
}
Expand Down
2 changes: 2 additions & 0 deletions Sources/Testing/Test+Discovery+Legacy.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -67,3 +68,4 @@ func types(withNamesContaining nameSubstring: String) -> some Sequence<Any.Type>
.withMemoryRebound(to: Any.Type.self) { Array($0) }
}
}
#endif
2 changes: 2 additions & 0 deletions Sources/Testing/Test+Discovery.swift
Original file line number Diff line number Diff line change
Expand Up @@ -55,13 +55,15 @@ extension Test: TestContent {
}
}

#if !SWT_NO_LEGACY_TEST_DISCOVERY
if discoveryMode != .newOnly && generators.isEmpty {
generators += types(withNamesContaining: testContainerTypeNameMagic).lazy
.compactMap { $0 as? any __TestContainer.Type }
.map { type in
{ @Sendable in await type.__tests }
}
}
#endif

// *Now* we call all the generators and return their results.
// Reduce into a set rather than an array to deduplicate tests that were
Expand Down
2 changes: 2 additions & 0 deletions Sources/TestingMacros/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand Down
52 changes: 50 additions & 2 deletions Sources/TestingMacros/ConditionMacro.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
Expand All @@ -448,6 +495,7 @@ extension ExitTestConditionMacro {
}
"""
)
#endif

arguments[trailingClosureIndex].expression = ExprSyntax(
ClosureExprSyntax {
Expand Down
46 changes: 43 additions & 3 deletions Sources/TestingMacros/SuiteDeclarationMacro.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.)
//
Expand All @@ -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
}
Expand Down
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
Expand Up @@ -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("")
}
Loading

0 comments on commit fc30eeb

Please sign in to comment.