From e3e5cfe72f2804392cb5a225709d0e7f6f460c74 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Tue, 8 Oct 2024 18:46:00 -0400 Subject: [PATCH] Store test content in a custom metadata section. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR uses the experimental symbol linkage margers feature in the Swift compiler to emit metadata about tests (and exit tests) into a dedicated section of the test executable being built. At runtime, we discover that section and read out the tests from it. This has several benefits over our current model, which involves walking Swift's type metadata table looking for types that conform to a protocol: 1. We don't need to define that protocol as public API in Swift Testing, 1. We don't need to emit type metadata (much larger than what we really need) for every test function, 1. We don't need to duplicate a large chunk of the Swift ABI sources in order to walk the type metadata table correctly, and 1. Almost all the new code is written in Swift, whereas the code it is intended to replace could not be fully represented in Swift and needed to be written in C++. The change also opens up the possibility of supporting generic types in the future because we can emit metadata without needing to emit a nested type (which is not always valid in a generic context.) That's a "future direction" and not covered by this PR specifically. I've defined a layout for entries in the new `swift5_tests` section that should be flexible enough for us in the short-to-medium term and which lets us define additional arbitrary test content record types. The layout of this section is covered in depth in the new [TestContent.md](Documentation/ABI/TestContent.md) article. This functionality is only available if a test target enables the experimental `"SymbolLinkageMarkers"` feature. We continue to emit protocol-conforming types for nowโ€”that code will be removed if and when the experimental feature is properly supported (modulo us adopting relevant changes to the feature's API.) https://github.com/swiftlang/swift-testing/issues/735 https://github.com/swiftlang/swift/issues/76698 https://github.com/swiftlang/swift/pull/78411 --- Documentation/Porting.md | 6 ++ Package.swift | 10 +- Sources/Testing/ExitTests/ExitTest.swift | 34 +++++++ Sources/Testing/Test+Discovery+Legacy.swift | 6 +- Sources/Testing/Test+Discovery.swift | 30 ++++++ Sources/TestingMacros/CMakeLists.txt | 2 + Sources/TestingMacros/ConditionMacro.swift | 50 +++++++++- .../TestingMacros/SuiteDeclarationMacro.swift | 58 +++++++++-- .../IntegerLiteralExprSyntaxAdditions.swift | 18 ++++ .../Additions/TokenSyntaxAdditions.swift | 11 +++ .../Support/AttributeDiscovery.swift | 25 ++++- .../Support/TestContentGeneration.swift | 89 +++++++++++++++++ .../TestingMacros/TestDeclarationMacro.swift | 97 ++++++++++++------- Sources/_TestDiscovery/SectionBounds.swift | 8 ++ .../_TestDiscovery/TestContentRecord.swift | 2 + Sources/_TestingInternals/Discovery.cpp | 12 +++ .../TestDeclarationMacroTests.swift | 5 + Tests/TestingTests/MiscellaneousTests.swift | 17 ++++ 18 files changed, 425 insertions(+), 55 deletions(-) create mode 100644 Sources/TestingMacros/Support/Additions/IntegerLiteralExprSyntaxAdditions.swift create mode 100644 Sources/TestingMacros/Support/TestContentGeneration.swift diff --git a/Documentation/Porting.md b/Documentation/Porting.md index ce179d53d..0cc6bf70b 100644 --- a/Documentation/Porting.md +++ b/Documentation/Porting.md @@ -145,8 +145,10 @@ to load that information: + let resourceName: Str255 = switch kind { + case .testContent: + "__swift5_tests" ++#if !SWT_NO_LEGACY_TEST_DISCOVERY + case .typeMetadata: + "__swift5_types" ++#endif + } + + let oldRefNum = CurResFile() @@ -219,15 +221,19 @@ diff --git a/Sources/_TestingInternals/Discovery.cpp b/Sources/_TestingInternals +#elif defined(macintosh) +extern "C" const char testContentSectionBegin __asm__("..."); +extern "C" const char testContentSectionEnd __asm__("..."); ++#if !defined(SWT_NO_LEGACY_TEST_DISCOVERY) +extern "C" const char typeMetadataSectionBegin __asm__("..."); +extern "C" const char typeMetadataSectionEnd __asm__("..."); ++#endif #else #warning Platform-specific implementation missing: Runtime test discovery unavailable (static) static const char testContentSectionBegin = 0; static const char& testContentSectionEnd = testContentSectionBegin; + #if !defined(SWT_NO_LEGACY_TEST_DISCOVERY) static const char typeMetadataSectionBegin = 0; static const char& typeMetadataSectionEnd = testContentSectionBegin; #endif + #endif ``` These symbols must have unique addresses corresponding to the first byte of the diff --git a/Package.swift b/Package.swift index 11adfc14e..e2257af1a 100644 --- a/Package.swift +++ b/Package.swift @@ -89,10 +89,7 @@ let package = Package( "_Testing_CoreGraphics", "_Testing_Foundation", ], - swiftSettings: .packageSettings + [ - // For testing test content section discovery only - .enableExperimentalFeature("SymbolLinkageMarkers"), - ] + swiftSettings: .packageSettings ), .macro( @@ -205,6 +202,11 @@ extension Array where Element == PackageDescription.SwiftSetting { .enableExperimentalFeature("AccessLevelOnImport"), .enableUpcomingFeature("InternalImportsByDefault"), + // This setting is enabled in the package, but not in the toolchain build + // (via CMake). Enabling it is dependent on acceptance of the @section + // proposal via Swift Evolution. + .enableExperimentalFeature("SymbolLinkageMarkers"), + // When building as a package, the macro plugin always builds as an // executable rather than a library. .define("SWT_NO_LIBRARY_MACRO_PLUGINS"), diff --git a/Sources/Testing/ExitTests/ExitTest.swift b/Sources/Testing/ExitTests/ExitTest.swift index a28e2eede..992a76fb8 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 test generator function 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) diff --git a/Sources/Testing/Test+Discovery+Legacy.swift b/Sources/Testing/Test+Discovery+Legacy.swift index dfb8d84c5..43a85e652 100644 --- a/Sources/Testing/Test+Discovery+Legacy.swift +++ b/Sources/Testing/Test+Discovery+Legacy.swift @@ -10,12 +10,13 @@ private import _TestingInternals +#if !SWT_NO_LEGACY_TEST_DISCOVERY /// A protocol describing a type that contains tests. /// /// - Warning: This protocol is used to implement the `@Test` macro. Do not use /// it directly. @_alwaysEmitConformanceMetadata -public protocol __TestContainer { +public protocol __TestContainer: Sendable { /// The set of tests contained by this type. static var __tests: [Test] { get async } } @@ -31,7 +32,7 @@ let testContainerTypeNameMagic = "__๐ŸŸ $test_container__" /// macro. Do not use it directly. @_alwaysEmitConformanceMetadata @_spi(Experimental) -public protocol __ExitTestContainer { +public protocol __ExitTestContainer: Sendable { /// The unique identifier of the exit test. static var __id: (UInt64, UInt64) { get } @@ -43,3 +44,4 @@ public protocol __ExitTestContainer { /// `__ExitTestContainer` protocol. let exitTestContainerTypeNameMagic = "__๐ŸŸ $exit_test_body__" #endif +#endif diff --git a/Sources/Testing/Test+Discovery.swift b/Sources/Testing/Test+Discovery.swift index 0d0695f6c..ca2b9de47 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. @@ -41,6 +65,7 @@ extension Test { // the legacy and new mechanisms, but we can set an environment variable // to explicitly select one or the other. When we remove legacy support, // we can also remove this enumeration and environment variable check. +#if !SWT_NO_LEGACY_TEST_DISCOVERY let (useNewMode, useLegacyMode) = switch Environment.flag(named: "SWT_USE_LEGACY_TEST_DISCOVERY") { case .none: (true, true) @@ -49,6 +74,9 @@ extension Test { case .some(false): (true, false) } +#else + let useNewMode = true +#endif // Walk all test content and gather generator functions, then call them in // a task group and collate their results. @@ -62,6 +90,7 @@ extension Test { } } +#if !SWT_NO_LEGACY_TEST_DISCOVERY // Perform legacy test discovery if needed. if useLegacyMode && result.isEmpty { let types = types(withNamesContaining: testContainerTypeNameMagic).lazy @@ -75,6 +104,7 @@ extension Test { result = await taskGroup.reduce(into: result) { $0.formUnion($1) } } } +#endif return result } 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..6d0749a3f 100644 --- a/Sources/TestingMacros/ConditionMacro.swift +++ b/Sources/TestingMacros/ConditionMacro.swift @@ -11,6 +11,10 @@ public import SwiftSyntax public import SwiftSyntaxMacros +#if !hasFeature(SymbolLinkageMarkers) && SWT_NO_LEGACY_TEST_DISCOVERY +#error("Platform-specific misconfiguration: either SymbolLinkageMarkers or legacy test discovery is required to expand #expect(exitsWith:)") +#endif + /// A protocol containing the common implementation for the expansions of the /// `#expect()` and `#require()` macros. /// @@ -450,28 +454,64 @@ extension ExitTestConditionMacro { """ ) +#if hasFeature(SymbolLinkageMarkers) // Create a local type that can be discovered at runtime and which contains // the exit test body. - let enumName = context.makeUniqueName("__๐ŸŸ $exit_test_body__") + let enumName = context.makeUniqueName("") + let testContentRecordDecl = makeTestContentRecordDecl( + named: .identifier("testContentRecord"), + in: TypeSyntax(IdentifierTypeSyntax(name: enumName)), + ofKind: .exitTest, + accessingWith: .identifier("accessor") + ) + decls.append( + """ + #if hasFeature(SymbolLinkageMarkers) + @available(*, deprecated, message: "This type is an implementation detail of the testing library. Do not use it directly.") + enum \(enumName) { + private static let accessor: Testing.__TestContentRecordAccessor = { outValue, type, hint in + Testing.ExitTest.__store( + \(exitTestIDExpr), + \(bodyThunkName), + into: outValue, + asTypeAt: type, + withHintAt: hint + ) + } + + \(testContentRecordDecl) + } + #endif + """ + ) +#endif + +#if !SWT_NO_LEGACY_TEST_DISCOVERY + // Emit a legacy type declaration if SymbolLinkageMarkers is off. + let legacyEnumName = context.makeUniqueName("__๐ŸŸ $exit_test_body__") decls.append( """ @available(*, deprecated, message: "This type is an implementation detail of the testing library. Do not use it directly.") - enum \(enumName): Testing.__ExitTestContainer, Sendable { + enum \(legacyEnumName): Testing.__ExitTestContainer { static var __id: (Swift.UInt64, Swift.UInt64) { \(exitTestIDExpr) } - static var __body: @Sendable () async throws -> Void { + static var __body: @Sendable () async throws -> Swift.Void { \(bodyThunkName) } } """ ) +#endif arguments[trailingClosureIndex].expression = ExprSyntax( ClosureExprSyntax { for decl in decls { - CodeBlockItemSyntax(item: .decl(decl)) - .with(\.trailingTrivia, .newline) + CodeBlockItemSyntax( + leadingTrivia: .newline, + item: .decl(decl), + trailingTrivia: .newline + ) } } ) diff --git a/Sources/TestingMacros/SuiteDeclarationMacro.swift b/Sources/TestingMacros/SuiteDeclarationMacro.swift index c9fb6bb08..865819060 100644 --- a/Sources/TestingMacros/SuiteDeclarationMacro.swift +++ b/Sources/TestingMacros/SuiteDeclarationMacro.swift @@ -11,6 +11,10 @@ public import SwiftSyntax public import SwiftSyntaxMacros +#if !hasFeature(SymbolLinkageMarkers) && SWT_NO_LEGACY_TEST_DISCOVERY +#error("Platform-specific misconfiguration: either SymbolLinkageMarkers or legacy test discovery is required to expand @Suite") +#endif + /// A type describing the expansion of the `@Suite` attribute macro. /// /// This type is used to implement the `@Suite` attribute macro. Do not use it @@ -127,6 +131,50 @@ public struct SuiteDeclarationMacro: MemberMacro, PeerMacro, Sendable { // Parse the @Suite attribute. let attributeInfo = AttributeInfo(byParsing: suiteAttribute, on: declaration, in: context) + let generatorName = context.makeUniqueName("generator") + result.append( + """ + @available(*, deprecated, message: "This property is an implementation detail of the testing library. Do not use it directly.") + @Sendable private static func \(generatorName)() async -> Testing.Test { + .__type( + \(declaration.type.trimmed).self, + \(raw: attributeInfo.functionArgumentList(in: context)) + ) + } + """ + ) + +#if hasFeature(SymbolLinkageMarkers) + let accessorName = context.makeUniqueName("accessor") + let accessorDecl: DeclSyntax = """ + @available(*, deprecated, message: "This property is an implementation detail of the testing library. Do not use it directly.") + private static let \(accessorName): Testing.__TestContentRecordAccessor = { outValue, type, _ in + Testing.Test.__store(\(generatorName), into: outValue, asTypeAt: type) + } + """ + + let testContentRecordDecl = makeTestContentRecordDecl( + named: context.makeUniqueName("testContentRecord"), + in: declaration.type, + ofKind: .testDeclaration, + accessingWith: accessorName, + context: attributeInfo.testContentRecordFlags + ) + + result.append( + """ + #if hasFeature(SymbolLinkageMarkers) + \(accessorDecl) + + \(testContentRecordDecl) + #endif + """ + ) +#endif + +#if !SWT_NO_LEGACY_TEST_DISCOVERY + // Emit a legacy type declaration if SymbolLinkageMarkers is off. + // // The emitted type must be public or the compiler can optimize it away // (since it is not actually used anywhere that the compiler can see.) // @@ -143,16 +191,14 @@ public struct SuiteDeclarationMacro: MemberMacro, PeerMacro, Sendable { @available(*, deprecated, message: "This type is an implementation detail of the testing library. Do not use it directly.") enum \(enumName): Testing.__TestContainer { static var __tests: [Testing.Test] { - get async {[ - .__type( - \(declaration.type.trimmed).self, - \(raw: attributeInfo.functionArgumentList(in: context)) - ) - ]} + get async { + [await \(generatorName)()] + } } } """ ) +#endif return result } 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..ca0c1f095 100644 --- a/Sources/TestingMacros/Support/AttributeDiscovery.swift +++ b/Sources/TestingMacros/Support/AttributeDiscovery.swift @@ -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,20 @@ struct AttributeInfo { /// as the canonical source location of the test or suite. var sourceLocation: ExprSyntax + 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 +109,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..91c8039a7 --- /dev/null +++ b/Sources/TestingMacros/Support/TestContentGeneration.swift @@ -0,0 +1,89 @@ +// +// 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? { + return withUnsafeBytes(of: rawValue.bigEndian) { bytes in + if bytes.allSatisfy(Unicode.ASCII.isASCII) { + let characters = String(decoding: bytes, as: Unicode.ASCII.self) + let allAlphanumeric = characters.allSatisfy { $0.isLetter || $0.isWholeNumber } + if allAlphanumeric { + return .blockComment("/* '\(characters)' */") + } + } + return nil + } + } +} + +/// 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 kindComment = kind.commentRepresentation.map { .space + $0 } ?? Trivia() + let contextExpr = if context == 0 { + IntegerLiteralExprSyntax(0) + } else { + IntegerLiteralExprSyntax(context, radix: .binary) + } + + return """ + #if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) || os(visionOS) + @_section("__DATA_CONST,__swift5_tests") + #elseif os(Linux) || os(FreeBSD) || os(OpenBSD) || os(Android) || os(WASI) + @_section("swift5_tests") + #elseif os(Windows) + @_section(".sw5test$B") + #else + @__testing(warning: "Platform-specific implementation missing: test content section name unavailable") + #endif + @_used + @available(*, deprecated, message: "This property is an implementation detail of the testing library. Do not use it directly.") + private \(staticKeyword(for: typeName)) let \(name): Testing.__TestContentRecord = ( + \(kindExpr),\(kindComment) + 0, + \(accessorName), + \(contextExpr), + 0 + ) + """ +} diff --git a/Sources/TestingMacros/TestDeclarationMacro.swift b/Sources/TestingMacros/TestDeclarationMacro.swift index 1b9f995bc..7e37ae294 100644 --- a/Sources/TestingMacros/TestDeclarationMacro.swift +++ b/Sources/TestingMacros/TestDeclarationMacro.swift @@ -11,6 +11,10 @@ public import SwiftSyntax public import SwiftSyntaxMacros +#if !hasFeature(SymbolLinkageMarkers) && SWT_NO_LEGACY_TEST_DISCOVERY +#error("Platform-specific misconfiguration: either SymbolLinkageMarkers or legacy test discovery is required to expand @Test") +#endif + /// A type describing the expansion of the `@Test` attribute macro. /// /// This type is used to implement the `@Test` attribute macro. Do not use it @@ -188,17 +192,6 @@ public struct TestDeclarationMacro: PeerMacro, Sendable { return FunctionParameterClauseSyntax(parameters: parameterList) } - /// 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`. - private static func _staticKeyword(for typeName: TypeSyntax?) -> TokenSyntax { - (typeName != nil) ? .keyword(.static) : .unknown("") - } - /// Create a thunk function with a normalized signature that calls a /// developer-supplied test function. /// @@ -356,7 +349,7 @@ public struct TestDeclarationMacro: PeerMacro, Sendable { let thunkName = context.makeUniqueName(thunking: functionDecl) let thunkDecl: DeclSyntax = """ @available(*, deprecated, message: "This function is an implementation detail of the testing library. Do not use it directly.") - @Sendable private \(_staticKeyword(for: typeName)) func \(thunkName)\(thunkParamsExpr) async throws -> Void { + @Sendable private \(staticKeyword(for: typeName)) func \(thunkName)\(thunkParamsExpr) async throws -> Void { \(thunkBody) } """ @@ -421,16 +414,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 +437,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,6 +459,47 @@ public struct TestDeclarationMacro: PeerMacro, Sendable { ) } + 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) + } + """ + ) + +#if hasFeature(SymbolLinkageMarkers) + let accessorName = context.makeUniqueName(thunking: functionDecl, withPrefix: "accessor") + let accessorDecl: DeclSyntax = """ + @available(*, deprecated, message: "This property is an implementation detail of the testing library. Do not use it directly.") + private \(staticKeyword(for: typeName)) let \(accessorName): Testing.__TestContentRecordAccessor = { outValue, type, _ in + Testing.Test.__store(\(generatorName), into: outValue, asTypeAt: type) + } + """ + + let testContentRecordDecl = makeTestContentRecordDecl( + named: context.makeUniqueName(thunking: functionDecl, withPrefix: "testContentRecord"), + in: typeName, + ofKind: .testDeclaration, + accessingWith: accessorName, + context: attributeInfo.testContentRecordFlags + ) + + result.append( + """ + #if hasFeature(SymbolLinkageMarkers) + \(accessorDecl) + + \(testContentRecordDecl) + #endif + """ + ) +#endif + +#if !SWT_NO_LEGACY_TEST_DISCOVERY + // Emit a legacy type declaration if SymbolLinkageMarkers is off. + // // The emitted type must be public or the compiler can optimize it away // (since it is not actually used anywhere that the compiler can see.) // @@ -487,12 +517,13 @@ public struct TestDeclarationMacro: PeerMacro, Sendable { enum \(enumName): Testing.__TestContainer { static var __tests: [Testing.Test] { get async { - \(raw: testsBody) + [await \(generatorName)()] } } } """ ) +#endif return result } diff --git a/Sources/_TestDiscovery/SectionBounds.swift b/Sources/_TestDiscovery/SectionBounds.swift index 1fc379258..212edbfbf 100644 --- a/Sources/_TestDiscovery/SectionBounds.swift +++ b/Sources/_TestDiscovery/SectionBounds.swift @@ -27,8 +27,10 @@ struct SectionBounds: Sendable { /// The test content metadata section. case testContent +#if !SWT_NO_LEGACY_TEST_DISCOVERY /// The type metadata section. case typeMetadata +#endif } /// All section bounds of the given kind found in the current process. @@ -60,8 +62,10 @@ extension SectionBounds.Kind { switch self { case .testContent: ("__DATA_CONST", "__swift5_tests") +#if !SWT_NO_LEGACY_TEST_DISCOVERY case .typeMetadata: ("__TEXT", "__swift5_types") +#endif } } } @@ -186,8 +190,10 @@ private func _sectionBounds(_ kind: SectionBounds.Kind) -> [SectionBounds] { let range = switch context.pointee.kind { case .testContent: sections.swift5_tests +#if !SWT_NO_LEGACY_TEST_DISCOVERY case .typeMetadata: sections.swift5_type_metadata +#endif } let start = UnsafeRawPointer(bitPattern: range.start) let size = Int(clamping: range.length) @@ -276,8 +282,10 @@ private func _sectionBounds(_ kind: SectionBounds.Kind) -> some Sequence
some Sequence +#if !defined(SWT_NO_LEGACY_TEST_DISCOVERY) #include #include #include +#endif #if defined(SWT_NO_DYNAMIC_LINKING) #pragma mark - Statically-linked section bounds @@ -21,24 +23,32 @@ #if defined(__APPLE__) extern "C" const char testContentSectionBegin __asm("section$start$__DATA_CONST$__swift5_tests"); extern "C" const char testContentSectionEnd __asm("section$end$__DATA_CONST$__swift5_tests"); +#if !defined(SWT_NO_LEGACY_TEST_DISCOVERY) extern "C" const char typeMetadataSectionBegin __asm__("section$start$__TEXT$__swift5_types"); extern "C" const char typeMetadataSectionEnd __asm__("section$end$__TEXT$__swift5_types"); +#endif #elif defined(__wasi__) extern "C" const char testContentSectionBegin __asm__("__start_swift5_tests"); extern "C" const char testContentSectionEnd __asm__("__stop_swift5_tests"); +#if !defined(SWT_NO_LEGACY_TEST_DISCOVERY) extern "C" const char typeMetadataSectionBegin __asm__("__start_swift5_type_metadata"); extern "C" const char typeMetadataSectionEnd __asm__("__stop_swift5_type_metadata"); +#endif #else #warning Platform-specific implementation missing: Runtime test discovery unavailable (static) static const char testContentSectionBegin = 0; static const char& testContentSectionEnd = testContentSectionBegin; +#if !defined(SWT_NO_LEGACY_TEST_DISCOVERY) static const char typeMetadataSectionBegin = 0; static const char& typeMetadataSectionEnd = typeMetadataSectionBegin; #endif +#endif static constexpr const char *const staticallyLinkedSectionBounds[][2] = { { &testContentSectionBegin, &testContentSectionEnd }, +#if !defined(SWT_NO_LEGACY_TEST_DISCOVERY) { &typeMetadataSectionBegin, &typeMetadataSectionEnd }, +#endif }; void swt_getStaticallyLinkedSectionBounds(size_t kind, const void **outSectionBegin, size_t *outByteCount) { @@ -48,6 +58,7 @@ void swt_getStaticallyLinkedSectionBounds(size_t kind, const void **outSectionBe } #endif +#if !defined(SWT_NO_LEGACY_TEST_DISCOVERY) #pragma mark - Swift ABI #if defined(__PTRAUTH_INTRINSICS__) @@ -221,3 +232,4 @@ const void *swt_getTypeFromTypeMetadataRecord(const void *recordAddress, const c return nullptr; } +#endif diff --git a/Tests/TestingMacrosTests/TestDeclarationMacroTests.swift b/Tests/TestingMacrosTests/TestDeclarationMacroTests.swift index 96eb9075c..fc601a870 100644 --- a/Tests/TestingMacrosTests/TestDeclarationMacroTests.swift +++ b/Tests/TestingMacrosTests/TestDeclarationMacroTests.swift @@ -408,7 +408,12 @@ struct TestDeclarationMacroTests { func differentFunctionTypes(input: String, expectedTypeName: String?, otherCode: String?) throws { let (output, _) = try parse(input) +#if hasFeature(SymbolLinkageMarkers) + #expect(output.contains("@_section")) +#endif +#if !SWT_NO_LEGACY_TEST_DISCOVERY #expect(output.contains("__TestContainer")) +#endif if let expectedTypeName { #expect(output.contains(expectedTypeName)) } diff --git a/Tests/TestingTests/MiscellaneousTests.swift b/Tests/TestingTests/MiscellaneousTests.swift index ec7fdd12d..a6c62fdbc 100644 --- a/Tests/TestingTests/MiscellaneousTests.swift +++ b/Tests/TestingTests/MiscellaneousTests.swift @@ -664,4 +664,21 @@ struct MiscellaneousTests { }) } #endif + +#if !SWT_NO_LEGACY_TEST_DISCOVERY && hasFeature(SymbolLinkageMarkers) + @Test("Legacy test discovery finds the same number of tests") func discoveredTestCount() async { + let oldFlag = Environment.variable(named: "SWT_USE_LEGACY_TEST_DISCOVERY") + defer { + Environment.setVariable(oldFlag, named: "SWT_USE_LEGACY_TEST_DISCOVERY") + } + + Environment.setVariable("1", named: "SWT_USE_LEGACY_TEST_DISCOVERY") + let testsWithOldCode = await Array(Test.all).count + + Environment.setVariable("0", named: "SWT_USE_LEGACY_TEST_DISCOVERY") + let testsWithNewCode = await Array(Test.all).count + + #expect(testsWithOldCode == testsWithNewCode) + } +#endif }