diff --git a/Sources/Testing/ExitTests/ExitTest.swift b/Sources/Testing/ExitTests/ExitTest.swift index a28e2eede..5b800f0c0 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: + /// - 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) @@ -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..0be944ad9 100644 --- a/Sources/Testing/Test+Discovery+Legacy.swift +++ b/Sources/Testing/Test+Discovery+Legacy.swift @@ -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 {} diff --git a/Sources/Testing/Test+Discovery.swift b/Sources/Testing/Test+Discovery.swift index 0d0695f6c..a8cc831c4 100644 --- a/Sources/Testing/Test+Discovery.swift +++ b/Sources/Testing/Test+Discovery.swift @@ -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..01cac9a3a 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..b47109291 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)) - ) - ]} + 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..21faed78f 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) - } + 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..e2bdd7830 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 {