Skip to content

Refine type-based test discovery mechanism to use test content records. #1010

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

Merged
merged 4 commits into from
Mar 10, 2025
Merged
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
47 changes: 40 additions & 7 deletions Sources/Testing/ExitTests/ExitTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,40 @@ extension ExitTest: DiscoverableAsTestContent {
}

typealias TestContentAccessorHint = ID

/// Store the exit test into the given memory.
///
/// - Parameters:
/// - id: The unique identifier of the exit test to store.
/// - body: The body closure of the exit test to store.
/// - outValue: The uninitialized memory to store the exit test into.
/// - typeAddress: A pointer to the expected type of the exit test as passed
/// to the test content record calling this function.
/// - hintAddress: A pointer to an instance of ``ID`` to use as a hint.
///
/// - Returns: Whether or not an exit test was stored into `outValue`.
///
/// - Warning: This function is used to implement the `#expect(exitsWith:)`
/// macro. Do not use it directly.
public static func __store(
_ id: (UInt64, UInt64),
_ body: @escaping @Sendable () async throws -> Void,
into outValue: UnsafeMutableRawPointer,
asTypeAt typeAddress: UnsafeRawPointer,
withHintAt hintAddress: UnsafeRawPointer? = nil
) -> CBool {
let callerExpectedType = TypeInfo(describing: typeAddress.load(as: Any.Type.self))
let selfType = TypeInfo(describing: Self.self)
guard callerExpectedType == selfType else {
return false
}
let id = ID(id)
if let hintedID = hintAddress?.load(as: ID.self), hintedID != id {
return false
}
outValue.initializeMemory(as: Self.self, to: Self(id: id, body: body))
return true
}
}

@_spi(Experimental) @_spi(ForToolsIntegrationOnly)
Expand All @@ -262,15 +296,14 @@ 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 { ID($0.__id) == id }
.map { ExitTest(id: ID($0.__id), body: $0.__body) }
#else
for record in Self.allTypeMetadataBasedTestContentRecords() {
if let exitTest = record.load(withHint: id) {
return exitTest
}
}

return nil
#endif
}
}

Expand Down
52 changes: 25 additions & 27 deletions Sources/Testing/Test+Discovery+Legacy.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,38 +8,36 @@
// See https://swift.org/CONTRIBUTORS.txt for Swift project authors
//

private import _TestingInternals
@_spi(Experimental) @_spi(ForToolsIntegrationOnly) internal import _TestDiscovery

/// A protocol describing a type that contains tests.
/// A shadow declaration of `_TestDiscovery.TestContentRecordContainer` that
/// allows us to add public conformances to it without causing the
/// `_TestDiscovery` module to appear in `Testing.private.swiftinterface`.
///
/// - Warning: This protocol is used to implement the `@Test` macro. Do not use
/// it directly.
/// This protocol is not part of the public interface of the testing library.
@_alwaysEmitConformanceMetadata
public protocol __TestContainer {
/// The set of tests contained by this type.
static var __tests: [Test] { get async }
}

/// A string that appears within all auto-generated types conforming to the
/// `__TestContainer` protocol.
let testContainerTypeNameMagic = "__🟠$test_container__"
protocol TestContentRecordContainer: _TestDiscovery.TestContentRecordContainer {}

#if !SWT_NO_EXIT_TESTS
/// A protocol describing a type that contains an exit test.
/// An abstract base class describing a type that contains tests.
///
/// - Warning: This protocol is used to implement the `#expect(exitsWith:)`
/// macro. Do not use it directly.
@_alwaysEmitConformanceMetadata
@_spi(Experimental)
public protocol __ExitTestContainer {
/// The unique identifier of the exit test.
static var __id: (UInt64, UInt64) { get }
/// - Warning: This class is used to implement the `@Test` macro. Do not use it
/// directly.
open class __TestContentRecordContainer: TestContentRecordContainer {
/// The corresponding test content record.
///
/// - Warning: This property is used to implement the `@Test` macro. Do not
/// use it directly.
open nonisolated class var __testContentRecord: __TestContentRecord {
(0, 0, nil, 0, 0)
}

/// The body function of the exit test.
static var __body: @Sendable () async throws -> Void { get }
static func storeTestContentRecord(to outTestContentRecord: UnsafeMutableRawPointer) -> Bool {
outTestContentRecord.withMemoryRebound(to: __TestContentRecord.self, capacity: 1) { outTestContentRecord in
outTestContentRecord.initialize(to: __testContentRecord)
return true
}
}
}

/// A string that appears within all auto-generated types conforming to the
/// `__ExitTestContainer` protocol.
let exitTestContainerTypeNameMagic = "__🟠$exit_test_body__"
#endif
@available(*, unavailable)
extension __TestContentRecordContainer: Sendable {}
37 changes: 29 additions & 8 deletions Sources/Testing/Test+Discovery.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,30 @@ extension Test {
var rawValue: @Sendable () async -> Test
}

/// Store the test generator function into the given memory.
///
/// - Parameters:
/// - generator: The generator function to store.
/// - outValue: The uninitialized memory to store `generator` into.
/// - typeAddress: A pointer to the expected type of `generator` as passed
/// to the test content record calling this function.
///
/// - Returns: Whether or not `generator` was stored into `outValue`.
///
/// - Warning: This function is used to implement the `@Test` macro. Do not
/// use it directly.
public static func __store(
_ generator: @escaping @Sendable () async -> Test,
into outValue: UnsafeMutableRawPointer,
asTypeAt typeAddress: UnsafeRawPointer
) -> CBool {
guard typeAddress.load(as: Any.Type.self) == Generator.self else {
return false
}
outValue.initializeMemory(as: Generator.self, to: .init(rawValue: generator))
return true
}

/// All available ``Test`` instances in the process, according to the runtime.
///
/// The order of values in this sequence is unspecified.
Expand Down Expand Up @@ -64,15 +88,12 @@ extension Test {

// Perform legacy test discovery if needed.
if useLegacyMode && result.isEmpty {
let types = types(withNamesContaining: testContainerTypeNameMagic).lazy
.compactMap { $0 as? any __TestContainer.Type }
await withTaskGroup(of: [Self].self) { taskGroup in
for type in types {
taskGroup.addTask {
await type.__tests
}
let generators = Generator.allTypeMetadataBasedTestContentRecords().lazy.compactMap { $0.load() }
await withTaskGroup(of: Self.self) { taskGroup in
for generator in generators {
taskGroup.addTask { await generator.rawValue() }
}
result = await taskGroup.reduce(into: result) { $0.formUnion($1) }
result = await taskGroup.reduce(into: result) { $0.insert($1) }
}
}

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
28 changes: 22 additions & 6 deletions Sources/TestingMacros/ConditionMacro.swift
Original file line number Diff line number Diff line change
Expand Up @@ -452,16 +452,32 @@ 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 className = context.makeUniqueName("__🟡$")
let testContentRecordDecl = makeTestContentRecordDecl(
named: .identifier("testContentRecord"),
in: TypeSyntax(IdentifierTypeSyntax(name: className)),
ofKind: .exitTest,
accessingWith: .identifier("accessor")
)

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 {
static var __id: (Swift.UInt64, Swift.UInt64) {
\(exitTestIDExpr)
final class \(className): Testing.__TestContentRecordContainer {
private nonisolated static let accessor: Testing.__TestContentRecordAccessor = { outValue, type, hint in
Testing.ExitTest.__store(
\(exitTestIDExpr),
\(bodyThunkName),
into: outValue,
asTypeAt: type,
withHintAt: hint
)
}
static var __body: @Sendable () async throws -> Void {
\(bodyThunkName)

\(testContentRecordDecl)

override nonisolated class var __testContentRecord: Testing.__TestContentRecord {
testContentRecord
}
}
"""
Expand Down
65 changes: 42 additions & 23 deletions Sources/TestingMacros/SuiteDeclarationMacro.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public struct SuiteDeclarationMacro: MemberMacro, PeerMacro, Sendable {
guard _diagnoseIssues(with: declaration, suiteAttribute: node, in: context) else {
return []
}
return _createTestContainerDecls(for: declaration, suiteAttribute: node, in: context)
return _createSuiteDecls(for: declaration, suiteAttribute: node, in: context)
}

public static func expansion(
Expand Down Expand Up @@ -97,8 +97,7 @@ public struct SuiteDeclarationMacro: MemberMacro, PeerMacro, Sendable {
return !diagnostics.lazy.map(\.severity).contains(.error)
}

/// Create a declaration for a type that conforms to the `__TestContainer`
/// protocol and which contains the given suite type.
/// Create the declarations necessary to discover a suite at runtime.
///
/// - Parameters:
/// - declaration: The type declaration the result should encapsulate.
Expand All @@ -107,7 +106,7 @@ public struct SuiteDeclarationMacro: MemberMacro, PeerMacro, Sendable {
///
/// - Returns: An array of declarations providing runtime information about
/// the test suite type `declaration`.
private static func _createTestContainerDecls(
private static func _createSuiteDecls(
for declaration: some DeclGroupSyntax,
suiteAttribute: AttributeSyntax,
in context: some MacroExpansionContext
Expand All @@ -127,28 +126,48 @@ public struct SuiteDeclarationMacro: MemberMacro, PeerMacro, Sendable {
// Parse the @Suite attribute.
let attributeInfo = AttributeInfo(byParsing: suiteAttribute, on: declaration, in: context)

// 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.)
//
// The emitted type must be deprecated to avoid causing warnings in client
// code since it references the suite metatype, which may be deprecated
// to allow test functions to validate deprecated APIs. The emitted type is
// also annotated unavailable, since it's meant only for use by the testing
// library at runtime. The compiler does not allow combining 'unavailable'
// and 'deprecated' into a single availability attribute: rdar://111329796
let typeName = declaration.type.tokens(viewMode: .fixedUp).map(\.textWithoutBackticks).joined()
let enumName = context.makeUniqueName("__🟠$test_container__suite__\(typeName)")
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")
result.append(
"""
@available(*, deprecated, message: "This property is an implementation detail of the testing library. Do not use it directly.")
private nonisolated static let \(accessorName): Testing.__TestContentRecordAccessor = { outValue, type, _ in
Testing.Test.__store(\(generatorName), into: outValue, asTypeAt: type)
}
"""
)

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

// Emit a type that contains a reference to the test content record.
let className = context.makeUniqueName("__🟡$")
result.append(
"""
@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))
)
]}
final class \(className): Testing.__TestContentRecordContainer {
override nonisolated class var __testContentRecord: Testing.__TestContentRecord {
\(testContentRecordName)
}
}
"""
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