Skip to content
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

Store test content in a custom metadata section. #880

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
14 changes: 7 additions & 7 deletions Documentation/ABI/TestContent.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,16 +75,16 @@ struct SWTTestContentRecord {
};
```

Do not use the `__TestContentRecord` typealias defined in the testing library.
This type exists to support the testing library's macros and may change in the
future (e.g. to accomodate a generic argument or to make use of one of the
reserved fields.)
Do not use the `__TestContentRecord` or `__TestContentRecordAccessor` type
aliases defined in the testing library. These types exist to support the testing
library's macros and may change in the future (e.g. to accomodate a generic
argument or to make use of a reserved field.)

Instead, define your own copy of this type where needed—you can copy the
definition above _verbatim_. If your test record type's `context` field (as
Instead, define your own copy of these types where needed—you can copy the
definitions above _verbatim_. If your test record type's `context` field (as
described below) is a pointer type, make sure to change its type in your version
of `TestContentRecord` accordingly so that, on systems with pointer
authentication enabled, the pointer is correctly resigned at load time.
authentication enabled, the pointer is correctly re-signed at load time.

### Record content

Expand Down
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
11 changes: 9 additions & 2 deletions Sources/Testing/Discovery+Platform.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,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 @@ -60,8 +62,10 @@ extension SectionBounds.Kind {
switch self {
case .testContent:
("__DATA_CONST", "__swift5_tests")
#if !SWT_NO_LEGACY_TEST_DISCOVERY
case .typeMetadata:
("__TEXT", "__swift5_types")
#endif
}
}
}
Expand Down Expand Up @@ -101,9 +105,8 @@ private let _startCollectingSectionBounds: Void = {
var size = CUnsignedLong(0)
if let start = getsectiondata(mh, segmentName.utf8Start, sectionName.utf8Start, &size), size > 0 {
let buffer = UnsafeRawBufferPointer(start: start, count: Int(clamping: size))
let sb = SectionBounds(imageAddress: mh, buffer: buffer)
_sectionBounds.withLock { sectionBounds in
sectionBounds[kind]!.append(sb)
sectionBounds[kind]!.append(SectionBounds(imageAddress: mh, buffer: buffer))
}
}
}
Expand Down Expand Up @@ -165,8 +168,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 @@ -255,8 +260,10 @@ private func _sectionBounds(_ kind: SectionBounds.Kind) -> some Sequence<Section
let sectionName = switch kind {
case .testContent:
".sw5test"
#if !SWT_NO_LEGACY_TEST_DISCOVERY
case .typeMetadata:
".sw5tymd"
#endif
}
return HMODULE.all.lazy.compactMap { _findSection(named: sectionName, in: $0) }
}
Expand Down
11 changes: 11 additions & 0 deletions Sources/Testing/Discovery.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,17 @@ public typealias __TestContentRecord = (
reserved2: UInt
)

/// Compare two move-only types.
///
/// This operator is provided because the Swift standard library does not yet
/// provide a `==` operator for move-only types. ([134276458](rdar://134276458))
///
/// - Warning: This operator is used to implement the `#expect(exitsWith:)`
/// macro. Do not use it directly. It will be removed in a future update.
public func ==(__lhs: (some ~Copyable).Type, rhs: (some ~Copyable).Type) -> Bool {
TypeInfo(describing: __lhs) == TypeInfo(describing: rhs)
}

// MARK: -

/// A protocol describing a type that can be stored as test content at compile
Expand Down
4 changes: 4 additions & 0 deletions Sources/Testing/ExitTests/ExitTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -247,11 +247,15 @@ extension ExitTest {
}
}

#if !SWT_NO_LEGACY_TEST_DISCOVERY
// Call the legacy lookup function that discovers tests embedded in types.
return types(withNamesContaining: exitTestContainerTypeNameMagic).lazy
.compactMap { $0 as? any __ExitTestContainer.Type }
.first { $0.__id == id }
.map { ExitTest(__identifiedBy: $0.__id, body: $0.__body) }
#else
return nil
#endif
}
}

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 @@ -60,3 +61,4 @@ func types(withNamesContaining nameSubstring: String) -> some Sequence<Any.Type>
.map { unsafeBitCast($0, to: Any.Type.self) }
}
}
#endif
6 changes: 6 additions & 0 deletions Sources/Testing/Test+Discovery.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ extension Test {
// the legacy and new mechanisms, but we can set an environment variable
// to explicitly select one or the other. When we remove legacy support,
// we can also remove this enumeration and environment variable check.
#if !SWT_NO_LEGACY_TEST_DISCOVERY
let (useNewMode, useLegacyMode) = switch Environment.flag(named: "SWT_USE_LEGACY_TEST_DISCOVERY") {
case .none:
(true, true)
Expand All @@ -61,6 +62,9 @@ extension Test {
case .some(false):
(true, false)
}
#else
let useNewMode = true
#endif

// Walk all test content and gather generator functions, then call them in
// a task group and collate their results.
Expand All @@ -79,6 +83,7 @@ extension Test {
}
}

#if !SWT_NO_LEGACY_TEST_DISCOVERY
// Perform legacy test discovery if needed.
if useLegacyMode && result.isEmpty {
let types = types(withNamesContaining: testContainerTypeNameMagic).lazy
Expand All @@ -92,6 +97,7 @@ extension Test {
result = await taskGroup.reduce(into: result) { $0.formUnion($1) }
}
}
#endif

return result
}
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
45 changes: 41 additions & 4 deletions Sources/TestingMacros/ConditionMacro.swift
Original file line number Diff line number Diff line change
Expand Up @@ -435,11 +435,44 @@ 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 enumName = context.makeUniqueName("")
let testContentRecordDecl = makeTestContentRecordDecl(
named: .identifier("testContentRecord"),
in: TypeSyntax(IdentifierTypeSyntax(name: enumName)),
ofKind: .exitTest,
accessingWith: .identifier("accessor")
)
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) {
private static let accessor: Testing.__TestContentRecordAccessor = { outValue, type, hint in
guard type.load(as: (any ~Swift.Copyable.Type).self) == Testing.__ExitTest.self else {
return false
}
let id = \(exitTestIDExpr)
if let hintedID = hint?.load(as: Testing.__ExitTest.ID.self), hintedID != id {
return false
}
let outValue = outValue.assumingMemoryBound(to: Testing.__ExitTest.self)
outValue.initialize(to: .init(__identifiedBy: id, body: \(bodyThunkName)))
return true
}

\(testContentRecordDecl)
}
#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 __id: Testing.__ExitTest.ID {
\(exitTestIDExpr)
}
Expand All @@ -449,12 +482,16 @@ extension ExitTestConditionMacro {
}
"""
)
#endif

arguments[trailingClosureIndex].expression = ExprSyntax(
ClosureExprSyntax {
for decl in decls {
CodeBlockItemSyntax(item: .decl(decl))
.with(\.trailingTrivia, .newline)
CodeBlockItemSyntax(
leadingTrivia: .newline,
item: .decl(decl),
trailingTrivia: .newline
)
}
}
)
Expand Down
58 changes: 52 additions & 6 deletions Sources/TestingMacros/SuiteDeclarationMacro.swift
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,54 @@ public struct SuiteDeclarationMacro: MemberMacro, PeerMacro, Sendable {
// Parse the @Suite attribute.
let attributeInfo = AttributeInfo(byParsing: suiteAttribute, on: declaration, in: context)

let generatorName = context.makeUniqueName("generator")
result.append(
"""
@available(*, deprecated, message: "This property is an implementation detail of the testing library. Do not use it directly.")
@Sendable private static func \(generatorName)() async -> Testing.Test {
.__type(
\(declaration.type.trimmed).self,
\(raw: attributeInfo.functionArgumentList(in: context))
)
}
"""
)

let accessorName = context.makeUniqueName("accessor")
let accessorDecl: DeclSyntax = """
@available(*, deprecated, message: "This property is an implementation detail of the testing library. Do not use it directly.")
private static let \(accessorName): Testing.__TestContentRecordAccessor = { outValue, type, _ in
typealias Generator = @Sendable () async -> Testing.Test
guard type.load(as: Any.Type.self) == Generator.self else {
return false
}
let outValue = outValue.assumingMemoryBound(to: Generator.self)
outValue.initialize(to: \(generatorName))
return true
}
"""

let testContentRecordDecl = makeTestContentRecordDecl(
named: context.makeUniqueName("testContentRecord"),
in: declaration.type,
ofKind: .testDeclaration,
accessingWith: accessorName,
context: attributeInfo.testContentRecordFlags
)

result.append(
"""
#if hasFeature(SymbolLinkageMarkers)
\(accessorDecl)

\(testContentRecordDecl)
#endif
"""
)

#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 @@ -143,16 +191,14 @@ 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 {[
.__type(
\(declaration.type.trimmed).self,
\(raw: attributeInfo.functionArgumentList(in: context))
)
]}
get async {
[await \(generatorName)()]
}
}
}
"""
)
#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))
}
}
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