From 725a40f5068b890530e515e38ad361ab045945bf 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/ABI/TestContent.md | 28 ++++-- Documentation/Porting.md | 6 ++ Package.swift | 2 + Sources/Testing/Discovery+Platform.swift | 25 +++-- Sources/Testing/Discovery.swift | 19 +++- Sources/Testing/ExitTests/ExitTest.swift | 4 + Sources/Testing/Test+Discovery+Legacy.swift | 2 + Sources/Testing/Test+Discovery.swift | 6 ++ Sources/TestingMacros/CMakeLists.txt | 2 + Sources/TestingMacros/ConditionMacro.swift | 42 ++++++++- .../TestingMacros/SuiteDeclarationMacro.swift | 54 +++++++++-- .../IntegerLiteralExprSyntaxAdditions.swift | 18 ++++ .../Additions/TokenSyntaxAdditions.swift | 11 +++ .../Support/AttributeDiscovery.swift | 25 ++++- .../Support/TestContentGeneration.swift | 89 ++++++++++++++++++ .../TestingMacros/TestDeclarationMacro.swift | 93 ++++++++++++------- Sources/_TestingInternals/Discovery.cpp | 12 +++ .../TestDeclarationMacroTests.swift | 2 +- Tests/TestingTests/MiscellaneousTests.swift | 17 ++++ cmake/modules/shared/CompilerSettings.cmake | 2 + 20 files changed, 394 insertions(+), 65 deletions(-) create mode 100644 Sources/TestingMacros/Support/Additions/IntegerLiteralExprSyntaxAdditions.swift create mode 100644 Sources/TestingMacros/Support/TestContentGeneration.swift diff --git a/Documentation/ABI/TestContent.md b/Documentation/ABI/TestContent.md index 4f1346b95..7cf88a42f 100644 --- a/Documentation/ABI/TestContent.md +++ b/Documentation/ABI/TestContent.md @@ -41,10 +41,15 @@ Regardless of platform, all test content records created and discoverable by the testing library have the following layout: ```swift +typealias Accessor = @convention(c) ( + _ outValue: UnsafeMutableRawPointer, + _ hint: UnsafeRawPointer? +) -> CBool + typealias TestContentRecord = ( kind: UInt32, reserved1: UInt32, - accessor: (@convention(c) (_ outValue: UnsafeMutableRawPointer, _ hint: UnsafeRawPointer?) -> CBool)?, + accessor: Accessor?, context: UInt, reserved2: UInt ) @@ -54,25 +59,30 @@ This type has natural size, stride, and alignment. Its fields are native-endian. If needed, this type can be represented in C as a structure: ```c +typedef bool (* SWTAccessor)( + void *outValue, + const void *_Null_unspecified hint +); + struct SWTTestContentRecord { uint32_t kind; uint32_t reserved1; - bool (* _Nullable accessor)(void *outValue, const void *_Null_unspecified hint); + SWTAccessor _Nullable accessor; uintptr_t context; uintptr_t reserved2; }; ``` -Do not use the `__TestContentRecord` typealias defined in the testing library. -This type exists to support the testing library's macros and may change in the -future (e.g. to accomodate a generic argument or to make use of one of the -reserved fields.) +Do not use the `__TestContentRecord` or `__TestContentRecordAccessor` type +aliases defined in the testing library. These types exist to support the testing +library's macros and may change in the future (e.g. to accomodate a generic +argument or to make use of a reserved field.) -Instead, define your own copy of this type where needed—you can copy the -definition above _verbatim_. If your test record type's `context` field (as +Instead, define your own copy of these types where needed—you can copy the +definitions above _verbatim_. If your test record type's `context` field (as described below) is a pointer type, make sure to change its type in your version of `TestContentRecord` accordingly so that, on systems with pointer -authentication enabled, the pointer is correctly resigned at load time. +authentication enabled, the pointer is correctly re-signed at load time. ### Record content 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 4a22b98e2..57c6ea42c 100644 --- a/Package.swift +++ b/Package.swift @@ -156,6 +156,8 @@ extension Array where Element == PackageDescription.SwiftSetting { .enableExperimentalFeature("AccessLevelOnImport"), .enableUpcomingFeature("InternalImportsByDefault"), + .enableExperimentalFeature("SymbolLinkageMarkers"), + .define("SWT_TARGET_OS_APPLE", .when(platforms: [.macOS, .iOS, .macCatalyst, .watchOS, .tvOS, .visionOS])), .define("SWT_NO_EXIT_TESTS", .when(platforms: [.iOS, .watchOS, .tvOS, .visionOS, .wasi, .android])), diff --git a/Sources/Testing/Discovery+Platform.swift b/Sources/Testing/Discovery+Platform.swift index 3f66b62ad..26dbea40f 100644 --- a/Sources/Testing/Discovery+Platform.swift +++ b/Sources/Testing/Discovery+Platform.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. @@ -77,18 +79,23 @@ private let _startCollectingSectionBounds: Void = { // If this image contains the Swift section(s) we need, acquire the lock and // store the section's bounds. - func findSectionBounds(forSectionNamed segmentName: String, _ sectionName: String, ofKind kind: SectionBounds.Kind) { + for kind in SectionBounds.Kind.allCases { + let (segmentName, sectionName) = switch kind { + case .testContent: + ("__DATA_CONST", "__swift5_tests") +#if !SWT_NO_LEGACY_TEST_DISCOVERY + case .typeMetadata: + ("__TEXT", "__swift5_types") +#endif + } var size = CUnsignedLong(0) if let start = getsectiondata(mh, segmentName, sectionName, &size), size > 0 { let buffer = UnsafeRawBufferPointer(start: start, count: Int(clamping: size)) - let sb = SectionBounds(imageAddress: mh, buffer: buffer) _sectionBounds.withLock { sectionBounds in - sectionBounds[kind]!.append(sb) + sectionBounds[kind]!.append(SectionBounds(imageAddress: mh, buffer: buffer)) } } } - findSectionBounds(forSectionNamed: "__DATA_CONST", "__swift5_tests", ofKind: .testContent) - findSectionBounds(forSectionNamed: "__TEXT", "__swift5_types", ofKind: .typeMetadata) } #if _runtime(_ObjC) @@ -97,7 +104,7 @@ private let _startCollectingSectionBounds: Void = { } #else _dyld_register_func_for_add_image { mh, _ in - addSectionBounds(from: mh) + addSectionBounds(from: mh!) } #endif }() @@ -147,8 +154,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) @@ -237,8 +246,10 @@ private func _sectionBounds(_ kind: SectionBounds.Kind) -> some Sequence
CollectionOfOne CBool + /// The content of a test content record. /// /// - Parameters: @@ -24,7 +41,7 @@ private import _TestingInternals public typealias __TestContentRecord = ( kind: UInt32, reserved1: UInt32, - accessor: (@convention(c) (_ outValue: UnsafeMutableRawPointer, _ hint: UnsafeRawPointer?) -> CBool)?, + accessor: __TestContentRecordAccessor?, context: UInt, reserved2: UInt ) diff --git a/Sources/Testing/ExitTests/ExitTest.swift b/Sources/Testing/ExitTests/ExitTest.swift index f6ea88d22..af96e6b31 100644 --- a/Sources/Testing/ExitTests/ExitTest.swift +++ b/Sources/Testing/ExitTests/ExitTest.swift @@ -247,11 +247,15 @@ extension ExitTest { } } +#if !SWT_NO_LEGACY_TEST_DISCOVERY // Call the legacy lookup function that discovers tests embedded in types. return types(withNamesContaining: exitTestContainerTypeNameMagic).lazy .compactMap { $0 as? any __ExitTestContainer.Type } .first { $0.__id == id } .map { ExitTest(__identifiedBy: $0.__id, body: $0.__body) } +#else + return nil +#endif } } diff --git a/Sources/Testing/Test+Discovery+Legacy.swift b/Sources/Testing/Test+Discovery+Legacy.swift index 6ee080051..d6110f1c9 100644 --- a/Sources/Testing/Test+Discovery+Legacy.swift +++ b/Sources/Testing/Test+Discovery+Legacy.swift @@ -10,6 +10,7 @@ private import _TestingInternals +#if !SWT_NO_LEGACY_TEST_DISCOVERY /// A protocol describing a type that contains tests. /// /// - Warning: This protocol is used to implement the `@Test` macro. Do not use @@ -60,3 +61,4 @@ func types(withNamesContaining nameSubstring: String) -> some Sequence .map { unsafeBitCast($0, to: Any.Type.self) } } } +#endif diff --git a/Sources/Testing/Test+Discovery.swift b/Sources/Testing/Test+Discovery.swift index 015a0b1c8..b50c333d4 100644 --- a/Sources/Testing/Test+Discovery.swift +++ b/Sources/Testing/Test+Discovery.swift @@ -46,6 +46,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) @@ -54,6 +55,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. @@ -72,6 +76,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 @@ -85,6 +90,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 346cb68bf..9ab7e5d4d 100644 --- a/Sources/TestingMacros/ConditionMacro.swift +++ b/Sources/TestingMacros/ConditionMacro.swift @@ -435,11 +435,41 @@ extension ExitTestConditionMacro { // Create a local type that can be discovered at runtime and which contains // the exit test body. - let enumName = context.makeUniqueName("__🟠$exit_test_body__") + let enumName = context.makeUniqueName("") + let testContentRecordDecl = makeTestContentRecordDecl( + named: .identifier("testContentRecord"), + in: TypeSyntax(IdentifierTypeSyntax(name: enumName)), + ofKind: .exitTest, + accessingWith: .identifier("accessor") + ) + decls.append( + """ + #if hasFeature(SymbolLinkageMarkers) + @available(*, deprecated, message: "This type is an implementation detail of the testing library. Do not use it directly.") + enum \(enumName) { + private static let accessor: Testing.__TestContentRecordAccessor = { outValue, hint in + let id = \(exitTestIDExpr) + if let hintedID = hint?.load(as: Testing.__ExitTest.ID.self), hintedID != id { + return false + } + let outValue = outValue.assumingMemoryBound(to: Testing.__ExitTest.self) + outValue.initialize(to: .init(__identifiedBy: id, body: \(bodyThunkName))) + return true + } + + \(testContentRecordDecl) + } + #endif + """ + ) + +#if !SWT_NO_LEGACY_TEST_DISCOVERY + // Emit a legacy type declaration if SymbolLinkageMarkers is off. + let legacyEnumName = context.makeUniqueName("__🟠$exit_test_body__") decls.append( """ @available(*, deprecated, message: "This type is an implementation detail of the testing library. Do not use it directly.") - enum \(enumName): Testing.__ExitTestContainer, Sendable { + enum \(legacyEnumName): Testing.__ExitTestContainer, Sendable { static var __id: Testing.__ExitTest.ID { \(exitTestIDExpr) } @@ -449,12 +479,16 @@ extension ExitTestConditionMacro { } """ ) +#endif arguments[trailingClosureIndex].expression = ExprSyntax( ClosureExprSyntax { for decl in decls { - CodeBlockItemSyntax(item: .decl(decl)) - .with(\.trailingTrivia, .newline) + CodeBlockItemSyntax( + leadingTrivia: .newline, + item: .decl(decl), + trailingTrivia: .newline + ) } } ) diff --git a/Sources/TestingMacros/SuiteDeclarationMacro.swift b/Sources/TestingMacros/SuiteDeclarationMacro.swift index c9fb6bb08..6d5b62882 100644 --- a/Sources/TestingMacros/SuiteDeclarationMacro.swift +++ b/Sources/TestingMacros/SuiteDeclarationMacro.swift @@ -127,6 +127,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)) + ) + } + """ + ) + + 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, _ in + let outValue = outValue.assumingMemoryBound(to: (@Sendable () async -> Testing.Test).self) + outValue.initialize(to: \(generatorName)) + return true + } + """ + + let testContentRecordDecl = makeTestContentRecordDecl( + named: context.makeUniqueName("testContentRecord"), + in: declaration.type, + ofKind: .testDeclaration, + accessingWith: accessorName, + context: attributeInfo.testContentRecordFlags + ) + + result.append( + """ + #if hasFeature(SymbolLinkageMarkers) + \(accessorDecl) + + \(testContentRecordDecl) + #endif + """ + ) + +#if !SWT_NO_LEGACY_TEST_DISCOVERY + // Emit a legacy type declaration if SymbolLinkageMarkers is off. + // // The emitted type must be public or the compiler can optimize it away // (since it is not actually used anywhere that the compiler can see.) // @@ -143,16 +187,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..39f21b733 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 1a3f2c448..13bcee5d5 100644 --- a/Sources/TestingMacros/TestDeclarationMacro.swift +++ b/Sources/TestingMacros/TestDeclarationMacro.swift @@ -172,17 +172,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. /// @@ -340,7 +329,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) } """ @@ -405,16 +394,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 @@ -430,16 +417,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: {} + ) } """ ) @@ -454,6 +439,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) + } + """ + ) + + 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, _ in + let outValue = outValue.assumingMemoryBound(to: (@Sendable () async -> Testing.Test).self) + outValue.initialize(to: \(generatorName)) + return true + } + """ + + 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 + """ + ) + +#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.) // @@ -471,12 +497,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/_TestingInternals/Discovery.cpp b/Sources/_TestingInternals/Discovery.cpp index 314b7794d..4a35ff4ef 100644 --- a/Sources/_TestingInternals/Discovery.cpp +++ b/Sources/_TestingInternals/Discovery.cpp @@ -10,9 +10,11 @@ #include "Discovery.h" +#if !defined(SWT_NO_LEGACY_TEST_DISCOVERY) #include #include #include +#endif #if defined(SWT_NO_DYNAMIC_LINKING) #pragma mark - Statically-linked section bounds @@ -20,30 +22,39 @@ #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 const void *_Nonnull const SWTTestContentSectionBounds[2] = { &testContentSectionBegin, &testContentSectionEnd }; +#if !defined(SWT_NO_LEGACY_TEST_DISCOVERY) const void *_Nonnull const SWTTypeMetadataSectionBounds[2] = { &typeMetadataSectionBegin, &typeMetadataSectionEnd }; #endif +#endif +#if !defined(SWT_NO_LEGACY_TEST_DISCOVERY) #pragma mark - Swift ABI #if defined(__PTRAUTH_INTRINSICS__) @@ -217,3 +228,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 a77acfea1..069c6cd4d 100644 --- a/Tests/TestingMacrosTests/TestDeclarationMacroTests.swift +++ b/Tests/TestingMacrosTests/TestDeclarationMacroTests.swift @@ -402,7 +402,7 @@ struct TestDeclarationMacroTests { func differentFunctionTypes(input: String, expectedTypeName: String?, otherCode: String?) throws { let (output, _) = try parse(input) - #expect(output.contains("__TestContainer")) + #expect(output.contains("@_section")) if let expectedTypeName { #expect(output.contains(expectedTypeName)) } diff --git a/Tests/TestingTests/MiscellaneousTests.swift b/Tests/TestingTests/MiscellaneousTests.swift index a172f7d5a..ac13527fe 100644 --- a/Tests/TestingTests/MiscellaneousTests.swift +++ b/Tests/TestingTests/MiscellaneousTests.swift @@ -660,4 +660,21 @@ struct MiscellaneousTests { }) } #endif + +#if !SWT_NO_LEGACY_TEST_DISCOVERY + @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 } diff --git a/cmake/modules/shared/CompilerSettings.cmake b/cmake/modules/shared/CompilerSettings.cmake index 49e2579fe..13fa101e2 100644 --- a/cmake/modules/shared/CompilerSettings.cmake +++ b/cmake/modules/shared/CompilerSettings.cmake @@ -16,6 +16,8 @@ add_compile_options( add_compile_options( "SHELL:$<$:-Xfrontend -enable-upcoming-feature -Xfrontend ExistentialAny>" "SHELL:$<$:-Xfrontend -enable-upcoming-feature -Xfrontend InternalImportsByDefault>") +add_compile_options( + "SHELL:$<$:-Xfrontend -enable-experimental-feature -Xfrontend SymbolLinkageMarkers>") # Platform-specific definitions. if(APPLE)