From d7ffa672d06ffa62cad51a89520d693795b842ce 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 | 2 + Sources/Testing/Discovery+Platform.swift | 12 ++- Sources/Testing/ExitTests/ExitTest.swift | 2 + Sources/Testing/Test+Discovery+Legacy.swift | 2 + Sources/Testing/Test+Discovery.swift | 6 ++ Sources/TestingMacros/CMakeLists.txt | 2 + Sources/TestingMacros/ConditionMacro.swift | 51 +++++++++- .../TestingMacros/SuiteDeclarationMacro.swift | 46 ++++++++- .../IntegerLiteralExprSyntaxAdditions.swift | 18 ++++ .../Additions/TokenSyntaxAdditions.swift | 11 +++ .../Support/TestContentGeneration.swift | 91 ++++++++++++++++++ .../TestingMacros/TestDeclarationMacro.swift | 93 ++++++++++++------- Sources/_TestingInternals/Discovery.cpp | 12 +++ .../TestDeclarationMacroTests.swift | 4 +- Tests/TestingTests/MiscellaneousTests.swift | 17 ++++ cmake/modules/shared/CompilerSettings.cmake | 2 + 17 files changed, 337 insertions(+), 40 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 ce179d53..0cc6bf70 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 4a22b98e..57c6ea42 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 a4280057..e454d038 100644 --- a/Sources/Testing/Discovery+Platform.swift +++ b/Sources/Testing/Discovery+Platform.swift @@ -24,8 +24,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. @@ -85,7 +87,9 @@ private let _startCollectingSectionBounds: Void = { } } findSectionBounds(forSectionNamed: "__DATA_CONST", "__swift5_tests", ofKind: .testContent) +#if !SWT_NO_LEGACY_TEST_DISCOVERY findSectionBounds(forSectionNamed: "__TEXT", "__swift5_types", ofKind: .typeMetadata) +#endif } #if _runtime(_ObjC) @@ -94,7 +98,7 @@ private let _startCollectingSectionBounds: Void = { } #else _dyld_register_func_for_add_image { mh, _ in - addSectionBounds(from: mh) + addSectionBounds(from: mh!) } #endif }() @@ -144,8 +148,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) @@ -234,8 +240,10 @@ private func _sectionBounds(_ kind: SectionBounds.Kind) -> some Sequence
CollectionOfOne 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 015a0b1c..b50c333d 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 4fc8b3b5..b0d80966 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 346cb68b..aa81b476 100644 --- a/Sources/TestingMacros/ConditionMacro.swift +++ b/Sources/TestingMacros/ConditionMacro.swift @@ -435,11 +435,57 @@ 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 accessorName = context.makeUniqueName("") + let outValueArgumentName = context.makeUniqueName("outValue") + let hintArgumentName = context.makeUniqueName("hint") + let idLocalName = context.makeUniqueName("id") + decls.append( + """ + #if hasFeature(SymbolLinkageMarkers) + func \(accessorName)(_ \(outValueArgumentName): UnsafeMutableRawPointer, _ \(hintArgumentName): UnsafeRawPointer?) -> Bool { + let \(idLocalName) = \(exitTestIDExpr) + if let \(hintArgumentName) = \(hintArgumentName)?.load(as: Testing.__ExitTest.ID.self), + \(hintArgumentName) != \(idLocalName) { + return false + } + \(outValueArgumentName).initializeMemory( + as: Testing.__ExitTest.self, + to: .init( + __identifiedBy: \(idLocalName), + body: \(bodyThunkName) + ) + ) + return true + } + #endif + """ + ) + + let enumName = context.makeUniqueName("") + let sectionContent = makeTestContentRecordDecl( + named: .identifier("__sectionContent"), + in: TypeSyntax(IdentifierTypeSyntax(name: enumName)), + ofKind: .exitTest, + accessingWith: accessorName + ) + 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) { + \(sectionContent) + } + #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,6 +495,7 @@ extension ExitTestConditionMacro { } """ ) +#endif arguments[trailingClosureIndex].expression = ExprSyntax( ClosureExprSyntax { diff --git a/Sources/TestingMacros/SuiteDeclarationMacro.swift b/Sources/TestingMacros/SuiteDeclarationMacro.swift index c9fb6bb0..9aa3bfaf 100644 --- a/Sources/TestingMacros/SuiteDeclarationMacro.swift +++ b/Sources/TestingMacros/SuiteDeclarationMacro.swift @@ -127,6 +127,39 @@ public struct SuiteDeclarationMacro: MemberMacro, PeerMacro, Sendable { // Parse the @Suite attribute. let attributeInfo = AttributeInfo(byParsing: suiteAttribute, on: declaration, in: context) + let accessorName = context.makeUniqueName("") + result.append( + """ + #if hasFeature(SymbolLinkageMarkers) + @available(*, deprecated, message: "This property is an implementation detail of the testing library. Do not use it directly.") + private static let \(accessorName): @convention(c) (UnsafeMutableRawPointer, UnsafeRawPointer?) -> Bool = { + $0.initializeMemory(as: (@Sendable () async -> Testing.Test).self) { @Sendable () async in + .__type( + \(declaration.type.trimmed).self, + \(raw: attributeInfo.functionArgumentList(in: context)) + ) + } + _ = $1 // Ignored. + return true + } + #endif + """ + ) + + let sectionContentName = context.makeUniqueName("") + result.append( + makeTestContentRecordDecl( + named: sectionContentName, + in: declaration.type, + ofKind: .testDeclaration, + accessingWith: accessorName, + context: 1 << 0 /* suite decl */ + ) + ) + +#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.) // @@ -142,17 +175,24 @@ 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 {[ + private static var __test: Testing.Test { + get async { .__type( \(declaration.type.trimmed).self, \(raw: attributeInfo.functionArgumentList(in: context)) ) - ]} + } + } + + static var __tests: [Testing.Test] { + get async { + [await __test] + } } } """ ) +#endif return result } diff --git a/Sources/TestingMacros/Support/Additions/IntegerLiteralExprSyntaxAdditions.swift b/Sources/TestingMacros/Support/Additions/IntegerLiteralExprSyntaxAdditions.swift new file mode 100644 index 00000000..e2310b44 --- /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 12e6abb2..39f21b73 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/TestContentGeneration.swift b/Sources/TestingMacros/Support/TestContentGeneration.swift new file mode 100644 index 00000000..1d845cdb --- /dev/null +++ b/Sources/TestingMacros/Support/TestContentGeneration.swift @@ -0,0 +1,91 @@ +// +// 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 hasFeature(SymbolLinkageMarkers) + #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 + ) + #endif + """ +} diff --git a/Sources/TestingMacros/TestDeclarationMacro.swift b/Sources/TestingMacros/TestDeclarationMacro.swift index 1a3f2c44..8b02a9fd 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,41 @@ public struct TestDeclarationMacro: PeerMacro, Sendable { ) } + let accessorName = context.makeUniqueName(thunking: functionDecl) + result.append( + """ + #if hasFeature(SymbolLinkageMarkers) + @available(*, deprecated, message: "This property is an implementation detail of the testing library. Do not use it directly.") + private \(staticKeyword(for: typeName)) let \(accessorName): @convention(c) (UnsafeMutableRawPointer, UnsafeRawPointer?) -> Bool = { + $0.initializeMemory(as: (@Sendable () async -> Testing.Test).self) { @Sendable () async in + \(raw: testsBody) + } + _ = $1 // Ignored. + return true + } + #endif + """ + ) + + var flags = UInt32(0) + if attributeInfo.hasFunctionArguments { + flags |= 1 << 1 /* is parameterized */ + } + + let sectionContentName = context.makeUniqueName(thunking: functionDecl) + result.append( + makeTestContentRecordDecl( + named: sectionContentName, + in: typeName, + ofKind: .testDeclaration, + accessingWith: accessorName, + context: flags + ) + ) + +#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.) // @@ -469,14 +489,21 @@ public struct TestDeclarationMacro: 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] { + private static var __test: Testing.Test { get async { \(raw: testsBody) } } + + static var __tests: [Testing.Test] { + get async { + [await __test] + } + } } """ ) +#endif return result } diff --git a/Sources/_TestingInternals/Discovery.cpp b/Sources/_TestingInternals/Discovery.cpp index 314b7794..4a35ff4e 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 a77acfea..1d7dbbdc 100644 --- a/Tests/TestingMacrosTests/TestDeclarationMacroTests.swift +++ b/Tests/TestingMacrosTests/TestDeclarationMacroTests.swift @@ -402,7 +402,9 @@ struct TestDeclarationMacroTests { func differentFunctionTypes(input: String, expectedTypeName: String?, otherCode: String?) throws { let (output, _) = try parse(input) - #expect(output.contains("__TestContainer")) + withKnownIssue("https://github.com/swiftlang/swift-syntax/issues/2923") { + #expect(output.contains("@_section")) + } if let expectedTypeName { #expect(output.contains(expectedTypeName)) } diff --git a/Tests/TestingTests/MiscellaneousTests.swift b/Tests/TestingTests/MiscellaneousTests.swift index a172f7d5..ac13527f 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 49e2579f..13fa101e 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)