From 67f6c4c0297154d0b7c2744d0789a7a81f38530d Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Sun, 9 Mar 2025 10:35:47 -0400 Subject: [PATCH 1/4] Refine type-based test discovery mechanism to use test content records. This PR changes how we discover tests in the type metadata section to more closely align with how the test content section works. This change will allow for a smoother transition to the test content section and our use of `@section` by using the same underlying structures (as much as is feasible.) Client code that uses the new `_TestDiscovery` module will need fewer changes to adapt. --- Sources/Testing/ExitTests/ExitTest.swift | 47 +++++- Sources/Testing/Test+Discovery+Legacy.swift | 53 +++--- Sources/Testing/Test+Discovery.swift | 39 ++++- Sources/TestingMacros/CMakeLists.txt | 2 + Sources/TestingMacros/ConditionMacro.swift | 28 +++- .../TestingMacros/SuiteDeclarationMacro.swift | 65 +++++--- .../IntegerLiteralExprSyntaxAdditions.swift | 18 ++ .../Additions/TokenSyntaxAdditions.swift | 11 ++ .../Support/AttributeDiscovery.swift | 30 +++- .../Support/TestContentGeneration.swift | 74 +++++++++ .../TestingMacros/TestDeclarationMacro.swift | 95 ++++++----- .../_TestDiscovery/TestContentRecord.swift | 154 +++++++++++++++--- .../TestDeclarationMacroTests.swift | 2 +- .../TestingTests/TypeNameConflictTests.swift | 2 +- 14 files changed, 477 insertions(+), 143 deletions(-) create mode 100644 Sources/TestingMacros/Support/Additions/IntegerLiteralExprSyntaxAdditions.swift create mode 100644 Sources/TestingMacros/Support/TestContentGeneration.swift diff --git a/Sources/Testing/ExitTests/ExitTest.swift b/Sources/Testing/ExitTests/ExitTest.swift index a28e2eede..d8c254522 100644 --- a/Sources/Testing/ExitTests/ExitTest.swift +++ b/Sources/Testing/ExitTests/ExitTest.swift @@ -244,6 +244,40 @@ extension ExitTest: DiscoverableAsTestContent { } typealias TestContentAccessorHint = ID + + /// Store the exit test into the given memory. + /// + /// - Parameters: + /// - outValue: The uninitialized memory to store the exit test into. + /// - id: The unique identifier of the exit test to store. + /// - body: The body closure of the exit test to store. + /// - 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) @@ -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 } } diff --git a/Sources/Testing/Test+Discovery+Legacy.swift b/Sources/Testing/Test+Discovery+Legacy.swift index dfb8d84c5..f974e3829 100644 --- a/Sources/Testing/Test+Discovery+Legacy.swift +++ b/Sources/Testing/Test+Discovery+Legacy.swift @@ -8,38 +8,35 @@ // 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. -@_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__" +/// This protocol is not part of the public interface of the testing library. +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 {} diff --git a/Sources/Testing/Test+Discovery.swift b/Sources/Testing/Test+Discovery.swift index 0d0695f6c..5c2d86f32 100644 --- a/Sources/Testing/Test+Discovery.swift +++ b/Sources/Testing/Test+Discovery.swift @@ -19,7 +19,7 @@ extension Test { /// indirect `async` accessor function rather than directly producing /// instances of ``Test``, but functions are non-nominal types and cannot /// directly conform to protocols. - fileprivate struct Generator: DiscoverableAsTestContent, RawRepresentable { + struct Generator: DiscoverableAsTestContent, RawRepresentable { static var testContentKind: UInt32 { 0x74657374 } @@ -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. @@ -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) } } } diff --git a/Sources/TestingMacros/CMakeLists.txt b/Sources/TestingMacros/CMakeLists.txt index 4fc8b3b58..b0d809665 100644 --- a/Sources/TestingMacros/CMakeLists.txt +++ b/Sources/TestingMacros/CMakeLists.txt @@ -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) diff --git a/Sources/TestingMacros/ConditionMacro.swift b/Sources/TestingMacros/ConditionMacro.swift index b4f5af1c3..c1c04069b 100644 --- a/Sources/TestingMacros/ConditionMacro.swift +++ b/Sources/TestingMacros/ConditionMacro.swift @@ -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 } } """ diff --git a/Sources/TestingMacros/SuiteDeclarationMacro.swift b/Sources/TestingMacros/SuiteDeclarationMacro.swift index c9fb6bb08..a65a7a420 100644 --- a/Sources/TestingMacros/SuiteDeclarationMacro.swift +++ b/Sources/TestingMacros/SuiteDeclarationMacro.swift @@ -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( @@ -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. @@ -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 @@ -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)) - ) - ]} + private final class \(className): Testing.__TestContentRecordContainer { + override nonisolated class var __testContentRecord: Testing.__TestContentRecord { + \(testContentRecordName) } } """ diff --git a/Sources/TestingMacros/Support/Additions/IntegerLiteralExprSyntaxAdditions.swift b/Sources/TestingMacros/Support/Additions/IntegerLiteralExprSyntaxAdditions.swift new file mode 100644 index 000000000..e2310b44f --- /dev/null +++ b/Sources/TestingMacros/Support/Additions/IntegerLiteralExprSyntaxAdditions.swift @@ -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)) + } +} diff --git a/Sources/TestingMacros/Support/Additions/TokenSyntaxAdditions.swift b/Sources/TestingMacros/Support/Additions/TokenSyntaxAdditions.swift index 12e6abb24..2be9977d5 100644 --- a/Sources/TestingMacros/Support/Additions/TokenSyntaxAdditions.swift +++ b/Sources/TestingMacros/Support/Additions/TokenSyntaxAdditions.swift @@ -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("") +} diff --git a/Sources/TestingMacros/Support/AttributeDiscovery.swift b/Sources/TestingMacros/Support/AttributeDiscovery.swift index dce4bddd3..84d96cf84 100644 --- a/Sources/TestingMacros/Support/AttributeDiscovery.swift +++ b/Sources/TestingMacros/Support/AttributeDiscovery.swift @@ -16,8 +16,8 @@ import SwiftSyntaxMacros /// /// If the developer specified Self.something as an argument to the `@Test` or /// `@Suite` attribute, we will currently incorrectly infer Self as equalling -/// the `__TestContainer` type we emit rather than the type containing the -/// test. This class strips off `Self.` wherever that occurs. +/// the container type that we emit rather than the type containing the test. +/// This class strips off `Self.` wherever that occurs. /// /// Note that this operation is technically incorrect if a subexpression of the /// attribute declares a type and refers to it with `Self`. We accept this @@ -60,6 +60,9 @@ struct AttributeInfo { /// The attribute node that was parsed to produce this instance. var attribute: AttributeSyntax + /// The declaration to which ``attribute`` was attached. + var declaration: DeclSyntax + /// The display name of the attribute, if present. var displayName: StringLiteralExprSyntax? @@ -85,6 +88,21 @@ struct AttributeInfo { /// as the canonical source location of the test or suite. var sourceLocation: ExprSyntax + /// Flags to apply to the test content record generated from this instance. + var testContentRecordFlags: UInt32 { + var result = UInt32(0) + + if declaration.is(FunctionDeclSyntax.self) { + if hasFunctionArguments { + result |= 1 << 1 /* is parameterized */ + } + } else { + result |= 1 << 0 /* suite decl */ + } + + return result + } + /// Create an instance of this type by parsing a `@Test` or `@Suite` /// attribute. /// @@ -92,13 +110,11 @@ struct AttributeInfo { /// - attribute: The attribute whose arguments should be extracted. If this /// attribute is not a `@Test` or `@Suite` attribute, the result is /// unspecified. - /// - declaration: The declaration to which `attribute` is attached. For - /// technical reasons, this argument is only constrained to - /// `SyntaxProtocol`, however an instance of a type conforming to - /// `DeclSyntaxProtocol & WithAttributesSyntax` is expected. + /// - declaration: The declaration to which `attribute` is attached. /// - context: The macro context in which the expression is being parsed. - init(byParsing attribute: AttributeSyntax, on declaration: some SyntaxProtocol, in context: some MacroExpansionContext) { + init(byParsing attribute: AttributeSyntax, on declaration: some DeclSyntaxProtocol, in context: some MacroExpansionContext) { self.attribute = attribute + self.declaration = DeclSyntax(declaration) var displayNameArgument: LabeledExprListSyntax.Element? var nonDisplayNameArguments: [Argument] = [] diff --git a/Sources/TestingMacros/Support/TestContentGeneration.swift b/Sources/TestingMacros/Support/TestContentGeneration.swift new file mode 100644 index 000000000..646bd97d4 --- /dev/null +++ b/Sources/TestingMacros/Support/TestContentGeneration.swift @@ -0,0 +1,74 @@ +// +// 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 { + switch self { + case .testDeclaration: + .blockComment("/* 'test' */") + case .exitTest: + .blockComment("/* 'exit' */") + } + } +} + +/// 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 contextExpr = if context == 0 { + IntegerLiteralExprSyntax(0) + } else { + IntegerLiteralExprSyntax(context, radix: .binary) + } + + return """ + @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), + \(contextExpr), + 0 + ) + """ +} diff --git a/Sources/TestingMacros/TestDeclarationMacro.swift b/Sources/TestingMacros/TestDeclarationMacro.swift index 1b9f995bc..c085ffc42 100644 --- a/Sources/TestingMacros/TestDeclarationMacro.swift +++ b/Sources/TestingMacros/TestDeclarationMacro.swift @@ -28,7 +28,7 @@ public struct TestDeclarationMacro: PeerMacro, Sendable { let functionDecl = declaration.cast(FunctionDeclSyntax.self) let typeName = context.typeOfLexicalContext - return _createTestContainerDecls(for: functionDecl, on: typeName, testAttribute: node, in: context) + return _createTestDecls(for: functionDecl, on: typeName, testAttribute: node, in: context) } public static var formatMode: FormatMode { @@ -364,8 +364,7 @@ public struct TestDeclarationMacro: PeerMacro, Sendable { return thunkDecl.cast(FunctionDeclSyntax.self) } - /// Create a declaration for a type that conforms to the `__TestContainer` - /// protocol and which contains a test for the given function. + /// Create the declarations necessary to discover a test at runtime. /// /// - Parameters: /// - functionDecl: The function declaration the result should encapsulate. @@ -376,7 +375,7 @@ public struct TestDeclarationMacro: PeerMacro, Sendable { /// /// - Returns: An array of declarations providing runtime information about /// the test function `functionDecl`. - private static func _createTestContainerDecls( + private static func _createTestDecls( for functionDecl: FunctionDeclSyntax, on typeName: TypeSyntax?, testAttribute: AttributeSyntax, @@ -421,16 +420,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 @@ -446,16 +443,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: {} + ) } """ ) @@ -470,25 +465,45 @@ public struct TestDeclarationMacro: PeerMacro, Sendable { ) } - // 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 test function thunk, which is itself - // 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 enumName = context.makeUniqueName(thunking: functionDecl, withPrefix: "__🟠$test_container__function__") + let generatorName = context.makeUniqueName(thunking: functionDecl, withPrefix: "generator") + result.append( + """ + @available(*, deprecated, message: "This property is an implementation detail of the testing library. Do not use it directly.") + @Sendable private \(staticKeyword(for: typeName)) func \(generatorName)() async -> Testing.Test { + \(raw: testsBody) + } + """ + ) + + let accessorName = context.makeUniqueName(thunking: functionDecl, withPrefix: "accessor") + 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 let \(accessorName): Testing.__TestContentRecordAccessor = { outValue, type, _ in + Testing.Test.__store(\(generatorName), into: outValue, asTypeAt: type) + } + """ + ) + + let testContentRecordName = context.makeUniqueName(thunking: functionDecl, withPrefix: "testContentRecord") + result.append( + makeTestContentRecordDecl( + named: testContentRecordName, + in: typeName, + ofKind: .testDeclaration, + accessingWith: accessorName, + context: attributeInfo.testContentRecordFlags + ) + ) + + // Emit a type that contains a reference to the test content record. + let className = context.makeUniqueName(thunking: functionDecl, withPrefix: "__🟠$") 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 { - \(raw: testsBody) - } + private final class \(className): Testing.__TestContentRecordContainer { + override nonisolated class var __testContentRecord: Testing.__TestContentRecord { + \(testContentRecordName) } } """ diff --git a/Sources/_TestDiscovery/TestContentRecord.swift b/Sources/_TestDiscovery/TestContentRecord.swift index 35e8392f6..ed5c42238 100644 --- a/Sources/_TestDiscovery/TestContentRecord.swift +++ b/Sources/_TestDiscovery/TestContentRecord.swift @@ -83,13 +83,40 @@ public struct TestContentRecord where T: DiscoverableAsTestContent & ~Copyabl /// with interfaces such as `dlsym()` that expect such a pointer. public private(set) nonisolated(unsafe) var imageAddress: UnsafeRawPointer? - /// The address of the underlying test content record loaded from a metadata - /// section. - private nonisolated(unsafe) var _recordAddress: UnsafePointer<_TestContentRecord> + /// A type defining storage for the underlying test content record. + private enum _RecordStorage: @unchecked Sendable { + /// The test content record is stored by address. + case atAddress(UnsafePointer<_TestContentRecord>) + + /// The test content record is stored in-place. + case inline(_TestContentRecord) + } + + /// Storage for `_record`. + private var _recordStorage: _RecordStorage + + /// The underlying test content record. + private var _record: _TestContentRecord { + _read { + switch _recordStorage { + case let .atAddress(recordAddress): + yield recordAddress.pointee + case let .inline(record): + yield record + } + } + } fileprivate init(imageAddress: UnsafeRawPointer?, recordAddress: UnsafePointer<_TestContentRecord>) { + precondition(recordAddress.pointee.kind == T.testContentKind) self.imageAddress = imageAddress - self._recordAddress = recordAddress + self._recordStorage = .atAddress(recordAddress) + } + + fileprivate init(imageAddress: UnsafeRawPointer?, record: _TestContentRecord) { + precondition(record.kind == T.testContentKind) + self.imageAddress = imageAddress + self._recordStorage = .inline(record) } /// The type of the ``context`` property. @@ -98,7 +125,7 @@ public struct TestContentRecord where T: DiscoverableAsTestContent & ~Copyabl /// The context of this test content record. public var context: Context { T.validateMemoryLayout() - return withUnsafeBytes(of: _recordAddress.pointee.context) { context in + return withUnsafeBytes(of: _record.context) { context in context.load(as: Context.self) } } @@ -120,7 +147,7 @@ public struct TestContentRecord where T: DiscoverableAsTestContent & ~Copyabl /// than once on the same instance, the testing library calls the underlying /// test content record's accessor function each time. public func load(withHint hint: Hint? = nil) -> T? { - guard let accessor = _recordAddress.pointee.accessor else { + guard let accessor = _record.accessor else { return nil } @@ -176,11 +203,16 @@ extension TestContentRecord: CustomStringConvertible { let kind = Self._asciiKind.map { asciiKind in "'\(asciiKind)' (\(hexKind))" } ?? hexKind - let recordAddress = imageAddress.map { imageAddress in - let recordAddressDelta = UnsafeRawPointer(_recordAddress) - imageAddress - return "\(imageAddress)+0x\(String(recordAddressDelta, radix: 16))" - } ?? "\(_recordAddress)" - return "<\(typeName) \(recordAddress)> { kind: \(kind), context: \(context) }" + switch _recordStorage { + case let .atAddress(recordAddress): + let recordAddress = imageAddress.map { imageAddress in + let recordAddressDelta = UnsafeRawPointer(recordAddress) - imageAddress + return "\(imageAddress)+0x\(String(recordAddressDelta, radix: 16))" + } ?? "\(recordAddress)" + return "<\(typeName) \(recordAddress)> { kind: \(kind), context: \(context) }" + case .inline: + return "<\(typeName)> { kind: \(kind), context: \(context) }" + } } } @@ -216,19 +248,99 @@ extension DiscoverableAsTestContent where Self: ~Copyable { private import _TestingInternals -/// Get all types known to Swift found in the current process whose names -/// contain a given substring. +/// A protocol describing a type, emitted at compile time or macro expansion +/// time, that represents a single test content record. /// -/// - Parameters: -/// - nameSubstring: A string which the names of matching classes all contain. +/// Use this protocol to make discoverable any test content records contained in +/// the type metadata section (the "legacy discovery mechanism"). For example, +/// if you have creasted a test content record named `myRecord` and your test +/// content record typealias is named `MyRecordType`: +/// +/// ```swift +/// private enum MyRecordContainer: TestContentRecordContainer { +/// nonisolated static func storeTestContentRecord(to outTestContentRecord: UnsafeMutableRawPointer) -> Bool { +/// outTestContentRecord.initializeMemory(as: MyRecordType.self, to: myRecord) +/// return true +/// } +/// } +/// ``` +/// +/// Then, at discovery time, call ``DiscoverableAsTestContent/allTypeMetadataBasedTestContentRecords()`` +/// to look up `myRecord`. /// -/// - Returns: A sequence of Swift types whose names contain `nameSubstring`. +/// Types that represent test content and that should be discoverable at runtime +/// should not conform to this protocol. Instead, they should conform to +/// ``DiscoverableAsTestContent``. @_spi(Experimental) @_spi(ForToolsIntegrationOnly) +@_alwaysEmitConformanceMetadata @available(swift, deprecated: 100000.0, message: "Do not adopt this functionality in new code. It will be removed in a future release.") -public func types(withNamesContaining nameSubstring: String) -> some Sequence { - SectionBounds.all(.typeMetadata).lazy.flatMap { sb in - stride(from: sb.buffer.baseAddress!, to: sb.buffer.baseAddress! + sb.buffer.count, by: SWTTypeMetadataRecordByteCount).lazy - .compactMap { swt_getType(fromTypeMetadataRecord: $0, ifNameContains: nameSubstring) } - .map { unsafeBitCast($0, to: Any.Type.self) } +public protocol TestContentRecordContainer { + /// Store this container's corresponding test content record to memory. + /// + /// - Parameters: + /// - outTestContentRecord: A pointer to uninitialized memory large enough + /// to hold a test content record. The memory is untyped so that client + /// code can use a custom definition of the test content record tuple + /// type. + /// + /// - Returns: Whether or not `outTestContentRecord` was initialized. If this + /// function returns `true`, the caller is responsible for deinitializing + /// said memory after it is done using it. + nonisolated static func storeTestContentRecord(to outTestContentRecord: UnsafeMutableRawPointer) -> Bool +} + +extension DiscoverableAsTestContent where Self: ~Copyable { + /// Make a test content record of this type from the given test content record + /// container type if it matches this type's requirements. + /// + /// - Parameters: + /// - containerType: The test content record container type. + /// - sb: The section bounds containing `containerType` and, thus, the test + /// content record. + /// + /// - Returns: A new test content record value, or `nil` if `containerType` + /// failed to store a record or if the record's kind did not match this + /// type's ``testContentKind`` property. + private static func _makeTestContentRecord(from containerType: (some TestContentRecordContainer).Type, in sb: SectionBounds) -> TestContentRecord? { + withUnsafeTemporaryAllocation(of: _TestContentRecord.self, capacity: 1) { buffer in + // Load the record from the container type. + guard containerType.storeTestContentRecord(to: buffer.baseAddress!) else { + return nil + } + let record = buffer.baseAddress!.move() + + // Make sure that the record's kind matches. + guard record.kind == Self.testContentKind else { + return nil + } + + // Construct the TestContentRecord instance from the record. + return TestContentRecord(imageAddress: sb.imageAddress, record: record) + } + } + + /// Get all test content of this type known to Swift and found in the current + /// process using the legacy discovery mechanism. + /// + /// - Returns: A sequence of instances of ``TestContentRecord``. Only test + /// content records matching this ``TestContent`` type's requirements are + /// included in the sequence. + /// + /// @Comment { + /// - Bug: This function returns an instance of `AnySequence` instead of an + /// opaque type due to a compiler crash. ([143080508](rdar://143080508)) + /// } + @available(swift, deprecated: 100000.0, message: "Do not adopt this functionality in new code. It will be removed in a future release.") + public static func allTypeMetadataBasedTestContentRecords() -> AnySequence> { + validateMemoryLayout() + + let result = SectionBounds.all(.typeMetadata).lazy.flatMap { sb in + stride(from: sb.buffer.baseAddress!, to: sb.buffer.baseAddress! + sb.buffer.count, by: SWTTypeMetadataRecordByteCount).lazy + .compactMap { swt_getType(fromTypeMetadataRecord: $0, ifNameContains: "__🟠$") } + .map { unsafeBitCast($0, to: Any.Type.self) } + .compactMap { $0 as? any TestContentRecordContainer.Type } + .compactMap { _makeTestContentRecord(from: $0, in: sb) } + } + return AnySequence(result) } } diff --git a/Tests/TestingMacrosTests/TestDeclarationMacroTests.swift b/Tests/TestingMacrosTests/TestDeclarationMacroTests.swift index 96eb9075c..6ac3542a9 100644 --- a/Tests/TestingMacrosTests/TestDeclarationMacroTests.swift +++ b/Tests/TestingMacrosTests/TestDeclarationMacroTests.swift @@ -408,7 +408,7 @@ struct TestDeclarationMacroTests { func differentFunctionTypes(input: String, expectedTypeName: String?, otherCode: String?) throws { let (output, _) = try parse(input) - #expect(output.contains("__TestContainer")) + #expect(output.contains("__TestContentRecordContainer")) if let expectedTypeName { #expect(output.contains(expectedTypeName)) } diff --git a/Tests/TestingTests/TypeNameConflictTests.swift b/Tests/TestingTests/TypeNameConflictTests.swift index e3698cb4f..7a0bc7961 100644 --- a/Tests/TestingTests/TypeNameConflictTests.swift +++ b/Tests/TestingTests/TypeNameConflictTests.swift @@ -27,7 +27,7 @@ struct TypeNameConflictTests { // MARK: - Fixtures fileprivate struct SourceLocation {} -fileprivate struct __TestContainer {} +fileprivate struct __TestContentRecordContainer {} fileprivate struct __XCTestCompatibleSelector {} fileprivate func __forward(_ value: R) async throws { From 1a3f430f2510dc133b817e9b8df7a93bea000890 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Mon, 10 Mar 2025 13:59:44 -0400 Subject: [PATCH 2/4] Incorporate feedback; use a different emoji to avoid namespace stompin' on Xcode 16 symbols --- Sources/Testing/ExitTests/ExitTest.swift | 2 +- Sources/TestingMacros/ConditionMacro.swift | 2 +- Sources/TestingMacros/SuiteDeclarationMacro.swift | 2 +- Sources/TestingMacros/TestDeclarationMacro.swift | 2 +- Sources/_TestDiscovery/TestContentRecord.swift | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Sources/Testing/ExitTests/ExitTest.swift b/Sources/Testing/ExitTests/ExitTest.swift index d8c254522..5b800f0c0 100644 --- a/Sources/Testing/ExitTests/ExitTest.swift +++ b/Sources/Testing/ExitTests/ExitTest.swift @@ -248,9 +248,9 @@ extension ExitTest: DiscoverableAsTestContent { /// Store the exit test into the given memory. /// /// - Parameters: - /// - outValue: The uninitialized memory to store the exit test into. /// - 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. diff --git a/Sources/TestingMacros/ConditionMacro.swift b/Sources/TestingMacros/ConditionMacro.swift index c1c04069b..01cac9a3a 100644 --- a/Sources/TestingMacros/ConditionMacro.swift +++ b/Sources/TestingMacros/ConditionMacro.swift @@ -452,7 +452,7 @@ extension ExitTestConditionMacro { // Create a local type that can be discovered at runtime and which contains // the exit test body. - let className = context.makeUniqueName("__🟠$") + let className = context.makeUniqueName("__🟡$") let testContentRecordDecl = makeTestContentRecordDecl( named: .identifier("testContentRecord"), in: TypeSyntax(IdentifierTypeSyntax(name: className)), diff --git a/Sources/TestingMacros/SuiteDeclarationMacro.swift b/Sources/TestingMacros/SuiteDeclarationMacro.swift index a65a7a420..be08de84a 100644 --- a/Sources/TestingMacros/SuiteDeclarationMacro.swift +++ b/Sources/TestingMacros/SuiteDeclarationMacro.swift @@ -161,7 +161,7 @@ public struct SuiteDeclarationMacro: MemberMacro, PeerMacro, Sendable { ) // Emit a type that contains a reference to the test content record. - let className = context.makeUniqueName("__🟠$") + let className = context.makeUniqueName("__🟡$") result.append( """ @available(*, deprecated, message: "This type is an implementation detail of the testing library. Do not use it directly.") diff --git a/Sources/TestingMacros/TestDeclarationMacro.swift b/Sources/TestingMacros/TestDeclarationMacro.swift index c085ffc42..9ec56af90 100644 --- a/Sources/TestingMacros/TestDeclarationMacro.swift +++ b/Sources/TestingMacros/TestDeclarationMacro.swift @@ -497,7 +497,7 @@ public struct TestDeclarationMacro: PeerMacro, Sendable { ) // Emit a type that contains a reference to the test content record. - let className = context.makeUniqueName(thunking: functionDecl, withPrefix: "__🟠$") + let className = context.makeUniqueName(thunking: functionDecl, withPrefix: "__🟡$") result.append( """ @available(*, deprecated, message: "This type is an implementation detail of the testing library. Do not use it directly.") diff --git a/Sources/_TestDiscovery/TestContentRecord.swift b/Sources/_TestDiscovery/TestContentRecord.swift index ed5c42238..e2bdd7830 100644 --- a/Sources/_TestDiscovery/TestContentRecord.swift +++ b/Sources/_TestDiscovery/TestContentRecord.swift @@ -336,7 +336,7 @@ extension DiscoverableAsTestContent where Self: ~Copyable { let result = SectionBounds.all(.typeMetadata).lazy.flatMap { sb in stride(from: sb.buffer.baseAddress!, to: sb.buffer.baseAddress! + sb.buffer.count, by: SWTTypeMetadataRecordByteCount).lazy - .compactMap { swt_getType(fromTypeMetadataRecord: $0, ifNameContains: "__🟠$") } + .compactMap { swt_getType(fromTypeMetadataRecord: $0, ifNameContains: "__🟡$") } .map { unsafeBitCast($0, to: Any.Type.self) } .compactMap { $0 as? any TestContentRecordContainer.Type } .compactMap { _makeTestContentRecord(from: $0, in: sb) } From ffedee989400abf791777a5723b84099048baf3a Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Mon, 10 Mar 2025 14:34:39 -0400 Subject: [PATCH 3/4] Don't make emitted classes private --- Sources/TestingMacros/SuiteDeclarationMacro.swift | 2 +- Sources/TestingMacros/TestDeclarationMacro.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/TestingMacros/SuiteDeclarationMacro.swift b/Sources/TestingMacros/SuiteDeclarationMacro.swift index be08de84a..b47109291 100644 --- a/Sources/TestingMacros/SuiteDeclarationMacro.swift +++ b/Sources/TestingMacros/SuiteDeclarationMacro.swift @@ -165,7 +165,7 @@ public struct SuiteDeclarationMacro: MemberMacro, PeerMacro, Sendable { result.append( """ @available(*, deprecated, message: "This type is an implementation detail of the testing library. Do not use it directly.") - private final class \(className): Testing.__TestContentRecordContainer { + final class \(className): Testing.__TestContentRecordContainer { override nonisolated class var __testContentRecord: Testing.__TestContentRecord { \(testContentRecordName) } diff --git a/Sources/TestingMacros/TestDeclarationMacro.swift b/Sources/TestingMacros/TestDeclarationMacro.swift index 9ec56af90..21faed78f 100644 --- a/Sources/TestingMacros/TestDeclarationMacro.swift +++ b/Sources/TestingMacros/TestDeclarationMacro.swift @@ -501,7 +501,7 @@ public struct TestDeclarationMacro: PeerMacro, Sendable { result.append( """ @available(*, deprecated, message: "This type is an implementation detail of the testing library. Do not use it directly.") - private final class \(className): Testing.__TestContentRecordContainer { + final class \(className): Testing.__TestContentRecordContainer { override nonisolated class var __testContentRecord: Testing.__TestContentRecord { \(testContentRecordName) } From f5690dca86fcf76e5704ade5ccfa9a33487d203e Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Mon, 10 Mar 2025 14:54:11 -0400 Subject: [PATCH 4/4] Tweaks --- Sources/Testing/Test+Discovery+Legacy.swift | 1 + Sources/Testing/Test+Discovery.swift | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Sources/Testing/Test+Discovery+Legacy.swift b/Sources/Testing/Test+Discovery+Legacy.swift index f974e3829..0be944ad9 100644 --- a/Sources/Testing/Test+Discovery+Legacy.swift +++ b/Sources/Testing/Test+Discovery+Legacy.swift @@ -15,6 +15,7 @@ /// `_TestDiscovery` module to appear in `Testing.private.swiftinterface`. /// /// This protocol is not part of the public interface of the testing library. +@_alwaysEmitConformanceMetadata protocol TestContentRecordContainer: _TestDiscovery.TestContentRecordContainer {} /// An abstract base class describing a type that contains tests. diff --git a/Sources/Testing/Test+Discovery.swift b/Sources/Testing/Test+Discovery.swift index 5c2d86f32..a8cc831c4 100644 --- a/Sources/Testing/Test+Discovery.swift +++ b/Sources/Testing/Test+Discovery.swift @@ -19,7 +19,7 @@ extension Test { /// indirect `async` accessor function rather than directly producing /// instances of ``Test``, but functions are non-nominal types and cannot /// directly conform to protocols. - struct Generator: DiscoverableAsTestContent, RawRepresentable { + fileprivate struct Generator: DiscoverableAsTestContent, RawRepresentable { static var testContentKind: UInt32 { 0x74657374 }