From f304a82589731568a81ec3dd64e193c6249e956a Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Mon, 30 Sep 2024 10:58:31 -0400 Subject: [PATCH] Store test content in a custom metadata section. See also: https://github.com/swiftlang/swift/issues/76698 Resolves #735. --- Documentation/ABI/TestContent.md | 112 +++++ Documentation/Porting.md | 28 +- Package.swift | 2 + Sources/Testing/ExitTests/ExitTest.swift | 56 ++- Sources/Testing/Test+Discovery.swift | 118 ++++-- Sources/Testing/Test+Macro.swift | 8 +- Sources/Testing/Test.swift | 13 + Sources/TestingMacros/CMakeLists.txt | 1 + Sources/TestingMacros/ConditionMacro.swift | 37 +- .../TestingMacros/SuiteDeclarationMacro.swift | 54 ++- .../Additions/TokenSyntaxAdditions.swift | 12 + .../Support/TestContentGeneration.swift | 120 ++++++ .../TestingMacros/TestDeclarationMacro.swift | 102 ++--- Sources/_TestingInternals/Discovery.cpp | 394 ++++++------------ Sources/_TestingInternals/include/Discovery.h | 68 ++- Sources/_TestingInternals/include/Includes.h | 4 + .../TestDeclarationMacroTests.swift | 2 +- Tests/TestingTests/MiscellaneousTests.swift | 23 + Tests/TestingTests/ZipTests.swift | 2 +- cmake/modules/shared/CompilerSettings.cmake | 2 + 20 files changed, 716 insertions(+), 442 deletions(-) create mode 100644 Documentation/ABI/TestContent.md create mode 100644 Sources/TestingMacros/Support/TestContentGeneration.swift diff --git a/Documentation/ABI/TestContent.md b/Documentation/ABI/TestContent.md new file mode 100644 index 000000000..5c29f6c7c --- /dev/null +++ b/Documentation/ABI/TestContent.md @@ -0,0 +1,112 @@ +# Runtime-discoverable test content + + + +This document describes the format and location of test content that the testing +library emits at compile time and can discover at runtime. + +## Basic format + +Swift Testing uses the [ELF Note format](https://man7.org/linux/man-pages/man5/elf.5.html) +to store individual records of test content. Records created and discoverable by +the testing library are stored in dedicated platform-specific sections: + +| Platform | Binary Format | Section Name | +|-|:-:|-| +| macOS | Mach-O | `__DATA_CONST,__swift5_tests` | +| iOS | Mach-O | `__DATA_CONST,__swift5_tests` | +| watchOS | Mach-O | `__DATA_CONST,__swift5_tests` | +| tvOS | Mach-O | `__DATA_CONST,__swift5_tests` | +| visionOS | Mach-O | `__DATA_CONST,__swift5_tests` | +| Linux | ELF | `PT_NOTE`[^1] | +| FreeBSD | ELF | `PT_NOTE`[^1] | +| Android | ELF | `PT_NOTE`[^1] | +| WASI | Statically Linked | `swift5_tests` | +| Windows | PE/COFF | `.sw5test` | + +[^1]: On platforms that use the ELF binary format natively, test content records + are stored in ELF program headers of type `PT_NOTE`. Take care not to + remove these program headers (for example, by invoking [`strip(1)`](https://www.man7.org/linux/man-pages/man1/strip.1.html).) + +### Determining the type of test content + +Regardless of platform, all test content records created and discoverable by the +testing library start have the name `"Swift Testing"` stored in the implied +`n_name` field of their underlying ELF Notes. Each record's _type_ (stored in +the underlying ELF Note's `n_type` field) determines how the record will be +interpreted at runtime: + +| Type Value | Interpretation | +|-:|-| +| < `0` | Undefined (**do not use**) | +| `0` ... `99` | Reserved | +| `100` | Test or suite declaration | +| `101` | Exit test | + + + +### Loading test content from a record + +For all currently-defined record types, the header and name are followed by a +structure of the following form: + +```c +struct SWTTestContent { + bool (* accessor)(void *); + uint64_t flags; +}; +``` + +#### The accessor field + +The function `accessor` is a C function whose signature in Swift can be restated +as: + +```swift +@convention(c) (_ outValue: UnsafeMutableRawPointer) -> Bool +``` + +When called, it initializes the memory at `outValue` to an instance of some +Swift type and returns `true`, or returns `false` if it could not generate the +relevant content. On successful return, the caller is responsible for +deinitializing the memory at `outValue` when done with it. + +The concrete Swift type of `accessor`'s result depends on the type of record: + +| Type Value | Return Type | +|-:|-| +| < `0` | Undefined (**do not use**) | +| `0` ... `99` | `nil` | +| `100` | `@Sendable () async -> Test`[^2] | +| `101` | `ExitTest` (owned by caller) | + +[^2]: This signature is not the signature of `accessor`, but of the Swift + function reference it returns. This level of indirection is necessary + because loading a test or suite declaration is an asynchronous operation, + but C functions cannot be `async`. + +#### The flags field + +For test or suite declarations (type `100`), the following flags are defined: + +| Bit | Description | +|-:|-| +| `1 << 0` | This record contains a suite declaration | +| `1 << 1` | This record contains a parameterized test function declaration | + +For exit test declarations (type `101`), no flags are currently defined. + +## Third-party test content + +TODO: elaborate how tools can reuse the same `n_name` and `n_type` fields to +supplement Swift Testing's data, or use a different `n_name` field to store +arbitrary other data in the test content section that Swift Testing will ignore. diff --git a/Documentation/Porting.md b/Documentation/Porting.md index 0745cd6ad..3d2bfd37a 100644 --- a/Documentation/Porting.md +++ b/Documentation/Porting.md @@ -113,13 +113,14 @@ Once the header is included, we can call `GetDateTime()` from `Clock.swift`: ## Runtime test discovery When porting to a new platform, you may need to provide a new implementation for -`enumerateTypeMetadataSections()` in `Discovery.cpp`. Test discovery is -dependent on Swift metadata discovery which is an inherently platform-specific -operation. +`enumerateTestContentSections()` in `Discovery.cpp`. Test discovery is dependent +on Swift metadata discovery which is an inherently platform-specific operation. -_Most_ platforms will be able to reuse the implementation used by Linux and -Windows that calls an internal Swift runtime function to enumerate available -metadata. If you are porting Swift Testing to Classic, this function won't be +_Most_ platforms in use today use the ELF image format and will be able to reuse +the implementation used by Linux. That implementation calls `dl_iterate_phdr()` +in the GNU C Library to enumerate available metadata. + +If you are porting Swift Testing to Classic, `dl_iterate_phdr()` won't be available, so you'll need to write a custom implementation instead. Assuming that the Swift compiler emits section information into the resource fork on Classic, you could use the [Resource Manager](https://developer.apple.com/library/archive/documentation/mac/pdf/MoreMacintoshToolbox.pdf) @@ -132,16 +133,21 @@ to load that information: // ... +#elif defined(macintosh) +template -+static void enumerateTypeMetadataSections(const SectionEnumerator& body) { ++static void enumerateTestContentSections(const SectionEnumerator& body) { + ResFileRefNum refNum; + if (noErr == GetTopResourceFile(&refNum)) { + ResFileRefNum oldRefNum = refNum; + do { + UseResFile(refNum); -+ Handle handle = Get1NamedResource('swft', "\p__swift5_types"); ++ Handle handle = Get1NamedResource('swft', "\p__swift5_tests"); + if (handle && *handle) { -+ size_t size = GetHandleSize(handle); -+ body(*handle, size); ++ auto imageAddress = reinterpret_cast(static_cast(refNum)); ++ SWTSectionBounds sb = { imageAddress, *handle, GetHandleSize(handle) }; ++ bool stop = false; ++ body(sb, &stop); ++ if (stop) { ++ break; ++ } + } + } while (noErr == GetNextResourceFile(refNum, &refNum)); + UseResFile(oldRefNum); @@ -150,7 +156,7 @@ to load that information: #else #warning Platform-specific implementation missing: Runtime test discovery unavailable template - static void enumerateTypeMetadataSections(const SectionEnumerator& body) {} + static void enumerateTestContentSections(const SectionEnumerator& body) {} #endif ``` diff --git a/Package.swift b/Package.swift index 32ea88bc3..d41b35949 100644 --- a/Package.swift +++ b/Package.swift @@ -128,6 +128,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/ExitTests/ExitTest.swift b/Sources/Testing/ExitTests/ExitTest.swift index 8923429ad..2c5dfd358 100644 --- a/Sources/Testing/ExitTests/ExitTest.swift +++ b/Sources/Testing/ExitTests/ExitTest.swift @@ -24,15 +24,29 @@ public struct ExitTest: Sendable, ~Copyable { /// The expected exit condition of the exit test. public var expectedExitCondition: ExitCondition - /// The body closure of the exit test. - fileprivate var body: @Sendable () async throws -> Void = {} - /// The source location of the exit test. /// /// The source location is unique to each exit test and is consistent between /// processes, so it can be used to uniquely identify an exit test at runtime. public var sourceLocation: SourceLocation + /// The body closure of the exit test. + fileprivate var body: @Sendable () async throws -> Void + + /// Initialize an exit test at runtime. + /// + /// - Warning: This initializer is used to implement the `#expect(exitsWith:)` + /// macro. Do not use it directly. + public init( + __expectedExitCondition expectedExitCondition: ExitCondition, + sourceLocation: SourceLocation, + body: @escaping @Sendable () async throws -> Void = {} + ) { + self.expectedExitCondition = expectedExitCondition + self.sourceLocation = sourceLocation + self.body = body + } + /// Disable crash reporting, crash logging, or core dumps for the current /// process. private static func _disableCrashReporting() { @@ -102,28 +116,7 @@ public struct ExitTest: Sendable, ~Copyable { // MARK: - Discovery -/// A protocol describing a type that contains an exit test. -/// -/// - Warning: This protocol is used to implement the `#expect(exitsWith:)` -/// macro. Do not use it directly. -@_alwaysEmitConformanceMetadata -@_spi(Experimental) -public protocol __ExitTestContainer { - /// The expected exit condition of the exit test. - static var __expectedExitCondition: ExitCondition { get } - - /// The source location of the exit test. - static var __sourceLocation: SourceLocation { get } - - /// The body function of the exit test. - static var __body: @Sendable () async throws -> Void { get } -} - extension ExitTest { - /// A string that appears within all auto-generated types conforming to the - /// `__ExitTestContainer` protocol. - private static let _exitTestContainerTypeNameMagic = "__🟠$exit_test_body__" - /// Find the exit test function at the given source location. /// /// - Parameters: @@ -131,15 +124,16 @@ extension ExitTest { /// /// - Returns: The specified exit test function, or `nil` if no such exit test /// could be found. + @_spi(ForToolsIntegrationOnly) public static func find(at sourceLocation: SourceLocation) -> Self? { var result: Self? - enumerateTypes(withNamesContaining: _exitTestContainerTypeNameMagic) { _, type, stop in - if let type = type as? any __ExitTestContainer.Type, type.__sourceLocation == sourceLocation { + enumerateTestContent(ofKind: .exitTest, as: ExitTest.self) { _, exitTest, _, stop in + if exitTest.sourceLocation == sourceLocation { result = ExitTest( - expectedExitCondition: type.__expectedExitCondition, - body: type.__body, - sourceLocation: type.__sourceLocation + __expectedExitCondition: exitTest.expectedExitCondition, + sourceLocation: exitTest.sourceLocation, + body: exitTest.body ) stop = true } @@ -183,7 +177,7 @@ func callExitTest( let actualExitCondition: ExitCondition do { - let exitTest = ExitTest(expectedExitCondition: expectedExitCondition, sourceLocation: sourceLocation) + let exitTest = ExitTest(__expectedExitCondition: expectedExitCondition, sourceLocation: sourceLocation) actualExitCondition = try await configuration.exitTestHandler(exitTest) } catch { // An error here would indicate a problem in the exit test handler such as a @@ -295,7 +289,7 @@ extension ExitTest { // External tools authors should set up their own back channel mechanisms // and ensure they're installed before calling ExitTest.callAsFunction(). guard var result = find(at: sourceLocation) else { - return nil + fatalError("Could not find an exit test that should have been located at \(sourceLocation).") } // We can't say guard let here because it counts as a consume. diff --git a/Sources/Testing/Test+Discovery.swift b/Sources/Testing/Test+Discovery.swift index 7c92933e1..c87584f2f 100644 --- a/Sources/Testing/Test+Discovery.swift +++ b/Sources/Testing/Test+Discovery.swift @@ -8,23 +8,9 @@ // See https://swift.org/CONTRIBUTORS.txt for Swift project authors // -private import _TestingInternals - -/// 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 { - /// The set of tests contained by this type. - static var __tests: [Test] { get async } -} +internal import _TestingInternals extension Test { - /// A string that appears within all auto-generated types conforming to the - /// `__TestContainer` protocol. - private static let _testContainerTypeNameMagic = "__🟠$test_container__" - /// All available ``Test`` instances in the process, according to the runtime. /// /// The order of values in this sequence is unspecified. @@ -47,16 +33,30 @@ extension Test { /// contain duplicates; callers should use ``all`` instead. private static var _all: some Sequence { get async { - await withTaskGroup(of: [Self].self) { taskGroup in - enumerateTypes(withNamesContaining: _testContainerTypeNameMagic) { _, type, _ in - if let type = type as? any __TestContainer.Type { - taskGroup.addTask { - await type.__tests - } - } + var generators = [@Sendable () async -> Test]() + + // Walk all test content and gather generator functions. Note we don't + // actually call the generators yet because enumerating test content may + // involve holding some internal lock such as the ones in libobjc or + // dl_iterate_phdr(), and we don't want to accidentally deadlock if the + // user code we call ends up loading another image. + enumerateTestContent(ofKind: .testDeclaration, as: (@Sendable () async -> Test).self) { imageAddress, generator, _, _ in + nonisolated(unsafe) let imageAddress = imageAddress + generators.append { @Sendable in + var result = await generator() + result.imageAddress = imageAddress + return result } + } - return await taskGroup.reduce(into: [], +=) + // *Now* we call all the generators and return their results. + return await withTaskGroup(of: Self.self) { taskGroup in + for generator in generators { + taskGroup.addTask { + await generator() + } + } + return await taskGroup.reduce(into: []) { $0.append($1) } } } } @@ -111,35 +111,71 @@ extension Test { // MARK: - -/// The type of callback called by ``enumerateTypes(withNamesContaining:_:)``. +/// The type of callback called by `_enumerateTestContent(_:)`. +/// +/// - Parameters: +/// - record: A pointer to a structure containing information about the +/// enumerated test content. +/// - stop: A pointer to a boolean variable indicating whether test content +/// enumeration should stop after the function returns. Set `*stop` to +/// `true` to stop test content enumeration. +private typealias _TestContentEnumerator = (_ record: UnsafePointer, _ stop: UnsafeMutablePointer) -> Void + +/// Enumerate all test content known to Swift and found in the current process. +/// +/// - Parameters: +/// - body: A function to invoke, once per raw test content record. +/// +/// This function enumerates all raw test content records discovered at runtime. +/// Callers should prefer ``enumerateTestContent(ofKind:as:_:)`` instead. +private func _enumerateTestContent(_ body: _TestContentEnumerator) { + withoutActuallyEscaping(body) { body in + withUnsafePointer(to: body) { context in + swt_enumerateTestContent(.init(mutating: context)) { record, stop, context in + let body = context!.load(as: _TestContentEnumerator.self) + body(record, stop) + } + } + } +} + +/// The type of callback called by ``enumerateTestContent(ofKind:as:_:)``. /// /// - Parameters: /// - imageAddress: A pointer to the start of the image. This value is _not_ /// equal to the value returned from `dlopen()`. On platforms that do not -/// support dynamic loading (and so do not have loadable images), this -/// argument is unspecified. -/// - type: A Swift type. +/// support dynamic loading (and so do not have loadable images), the value +/// of this argument is unspecified. +/// - content: The enumerated test content. +/// - flags: Flags associated with `content`. The value of this argument is +/// dependent on the type of test content being enumerated. /// - stop: An `inout` boolean variable indicating whether type enumeration /// should stop after the function returns. Set `stop` to `true` to stop /// type enumeration. -typealias TypeEnumerator = (_ imageAddress: UnsafeRawPointer?, _ type: Any.Type, _ stop: inout Bool) -> Void +typealias TestContentEnumerator = (_ imageAddress: UnsafeRawPointer?, _ content: borrowing T, _ flags: UInt64, _ stop: inout Bool) -> Void where T: ~Copyable -/// Enumerate all types known to Swift found in the current process whose names -/// contain a given substring. +/// Enumerate all test content known to Swift and found in the current process. /// /// - Parameters: -/// - nameSubstring: A string which the names of matching classes all contain. -/// - body: A function to invoke, once per matching type. -func enumerateTypes(withNamesContaining nameSubstring: String, _ typeEnumerator: TypeEnumerator) { - withoutActuallyEscaping(typeEnumerator) { typeEnumerator in - withUnsafePointer(to: typeEnumerator) { context in - swt_enumerateTypes(withNamesContaining: nameSubstring, .init(mutating: context)) { imageAddress, type, stop, context in - let typeEnumerator = context!.load(as: TypeEnumerator.self) - let type = unsafeBitCast(type, to: Any.Type.self) - var stop2 = false - typeEnumerator(imageAddress, type, &stop2) - stop.pointee = stop2 +/// - kind: The kind of test content to look for. +/// - type: The Swift type of test content to look for. +/// - body: A function to invoke, once per matching test content record. +func enumerateTestContent(ofKind kind: SWTTestContentKind, as type: T.Type, _ body: TestContentEnumerator) where T: ~Copyable { + _enumerateTestContent { record, stop in + if record.pointee.kind != kind { + return + } + withUnsafeTemporaryAllocation(of: type, capacity: 1) { buffer in + // Load the content from the record via its accessor function. + guard let accessor = record.pointee.accessor, accessor(buffer.baseAddress!) else { + return } + defer { + buffer.deinitialize() + } + + // Call the callback. + body(record.pointee.imageAddress, buffer.baseAddress!.pointee, record.pointee.flags, &stop.pointee) } } } diff --git a/Sources/Testing/Test+Macro.swift b/Sources/Testing/Test+Macro.swift index 637256b16..33402e68e 100644 --- a/Sources/Testing/Test+Macro.swift +++ b/Sources/Testing/Test+Macro.swift @@ -8,6 +8,10 @@ // See https://swift.org/CONTRIBUTORS.txt for Swift project authors // +/// This file provides support for the `@Test` macro. Other than the macro +/// itself, the symbols in this file should not be used directly and are subject +/// to change as the testing library evolves. + #if _runtime(_ObjC) public import ObjectiveC @@ -42,10 +46,6 @@ public typealias __XCTestCompatibleSelector = Never #endif } -/// This file provides support for the `@Test` macro. Other than the macro -/// itself, the symbols in this file should not be used directly and are subject -/// to change as the testing library evolves. - // MARK: - @Suite /// Declare a test suite. diff --git a/Sources/Testing/Test.swift b/Sources/Testing/Test.swift index 0e4292078..2053e4d18 100644 --- a/Sources/Testing/Test.swift +++ b/Sources/Testing/Test.swift @@ -53,6 +53,19 @@ public struct Test: Sendable { /// The source location of this test. public var sourceLocation: SourceLocation + /// The base address of the image containing this test, if available. + /// + /// On platforms that do not support dynamic loading of images, the value of + /// this property is `nil`. Otherwise, the value is platform-specific, but + /// generally equal to the address of the first byte of the image mapped into + /// memory. + /// + /// On Apple platforms, this property's value is equivalent to a pointer to a + /// `mach_header` value. On Windows, it is equivalent to an `HMODULE`. It is + /// never equivalent to the pointer returned from a call to `dlopen()` (on + /// platforms that have that function.) + nonisolated(unsafe) var imageAddress: UnsafeRawPointer? + /// Information about the type containing this test, if any. /// /// If a test is associated with a free function or static function, the value diff --git a/Sources/TestingMacros/CMakeLists.txt b/Sources/TestingMacros/CMakeLists.txt index c91620449..29744fb9b 100644 --- a/Sources/TestingMacros/CMakeLists.txt +++ b/Sources/TestingMacros/CMakeLists.txt @@ -102,6 +102,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 92daafb2a..2275008bd 100644 --- a/Sources/TestingMacros/ConditionMacro.swift +++ b/Sources/TestingMacros/ConditionMacro.swift @@ -395,20 +395,35 @@ 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("") + decls.append( + """ + func \(accessorName)(_ outValue: UnsafeMutableRawPointer) -> Bool { + outValue.initializeMemory( + as: Testing.ExitTest.self, + to: .init( + __expectedExitCondition: \(arguments[expectedExitConditionIndex].expression.trimmed), + sourceLocation: \(createSourceLocationExpr(of: macro, context: context)), + body: \(bodyThunkName) + ) + ) + return true + } + """ + ) + + let enumName = context.makeUniqueName("") + let sectionContent = makeTestContentRecordDecl( + named: .identifier("__sectionContent"), + in: TypeSyntax(IdentifierTypeSyntax(name: enumName)), + ofKind: .exitTest, + accessingWith: accessorName + ) decls.append( """ @available(*, deprecated, message: "This type is an implementation detail of the testing library. Do not use it directly.") - enum \(enumName): Testing.__ExitTestContainer, Sendable { - static var __sourceLocation: Testing.SourceLocation { - \(createSourceLocationExpr(of: macro, context: context)) - } - static var __body: @Sendable () async throws -> Void { - \(bodyThunkName) - } - static var __expectedExitCondition: Testing.ExitCondition { - \(arguments[expectedExitConditionIndex].expression.trimmed) - } + enum \(enumName) { + \(sectionContent) } """ ) diff --git a/Sources/TestingMacros/SuiteDeclarationMacro.swift b/Sources/TestingMacros/SuiteDeclarationMacro.swift index 3b193bb65..8d6ba357d 100644 --- a/Sources/TestingMacros/SuiteDeclarationMacro.swift +++ b/Sources/TestingMacros/SuiteDeclarationMacro.swift @@ -126,33 +126,45 @@ public struct SuiteDeclarationMacro: MemberMacro, PeerMacro, Sendable { // Parse the @Suite attribute. let attributeInfo = AttributeInfo(byParsing: suiteAttribute, on: declaration, in: context) - // The emitted type must be public or the compiler can optimize it away - // (since it is not actually used anywhere that the compiler can see.) - // - // The emitted type must be deprecated to avoid causing warnings in client - // code since it references the suite metatype, which may be deprecated - // to allow test functions to validate deprecated APIs. The emitted type is - // also annotated unavailable, since it's meant only for use by the testing - // library at runtime. The compiler does not allow combining 'unavailable' - // and 'deprecated' into a single availability attribute: rdar://111329796 - let typeName = declaration.type.tokens(viewMode: .fixedUp).map(\.textWithoutBackticks).joined() - let enumName = context.makeUniqueName("__🟠$test_container__suite__\(typeName)") + // We need an extra trampoline through a static property getter (rather than + // just having this logic in the C thunk) because some versions of the Swift + // compiler think that the presence of a `try` keyword in `testsBody` means + // that it must be throwing (disregarding autoclosures.) + let accessorName = context.makeUniqueName("") result.append( """ - @available(*, deprecated, message: "This type is an implementation detail of the testing library. Do not use it directly.") - enum \(enumName): Testing.__TestContainer { - static var __tests: [Testing.Test] { - get async {[ - .__type( - \(declaration.type.trimmed).self, - \(raw: attributeInfo.functionArgumentList(in: context)) - ) - ]} - } + @available(*, deprecated, message: "This property is an implementation detail of the testing library. Do not use it directly.") + @Sendable private static func \(accessorName)() async -> Testing.Test { + .__type( + \(declaration.type.trimmed).self, + \(raw: attributeInfo.functionArgumentList(in: context)) + ) } """ ) + let cAccessorName = context.makeUniqueName("") + result.append( + """ + @available(*, deprecated, message: "This property is an implementation detail of the testing library. Do not use it directly.") + private static let \(cAccessorName): @convention(c) (UnsafeMutableRawPointer) -> Bool = { + $0.initializeMemory(as: (@Sendable () async -> Testing.Test).self, to: \(accessorName)) + return true + } + """ + ) + + let sectionContentName = context.makeUniqueName("") + result.append( + makeTestContentRecordDecl( + named: sectionContentName, + in: declaration.type, + ofKind: .testDeclaration, + accessingWith: cAccessorName, + flags: 1 << 0 /* suite decl */ + ) + ) + return result } } diff --git a/Sources/TestingMacros/Support/Additions/TokenSyntaxAdditions.swift b/Sources/TestingMacros/Support/Additions/TokenSyntaxAdditions.swift index 2281f9f5a..16260f91e 100644 --- a/Sources/TestingMacros/Support/Additions/TokenSyntaxAdditions.swift +++ b/Sources/TestingMacros/Support/Additions/TokenSyntaxAdditions.swift @@ -19,3 +19,15 @@ extension TokenSyntax { text.filter { $0 != "`" } } } + + /// 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 000000000..dfe930b20 --- /dev/null +++ b/Sources/TestingMacros/Support/TestContentGeneration.swift @@ -0,0 +1,120 @@ +// +// 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 Discovery.h and TestContent.md. +enum TestContentKind: Int32 { + /// A test or suite declaration. + case testDeclaration = 100 + + /// An exit test. + case exitTest = 101 +} + +/// The name of ELF notes generated by this code. +/// +/// The value of this property corresponds to the implied `n_name` field of an +/// ELF note. It includes one or more trailing null characters. +private let _swiftTestingELFNoteName: [CChar] = { + // The size of the note name field. This value must be a multiple of the size + // of a pointer (on the target) plus four to ensure correct alignment. + let count = 20 + assert((count - 4) % MemoryLayout.stride == 0, "Swift Testing note name length must be a multiple of pointer size +4") + + // Make sure this string matches the one in Discovery.cpp! + var name = "Swift Testing".utf8.map { CChar(bitPattern: $0) } + assert(count > name.count, "Insufficient space for Swift Testing note name") + + // Pad out to the correct length with zero bytes. + name += repeatElement(0, count: count - name.count) + + return name +}() + +/// The name of ELF notes generated by this code as a tuple expression and its +/// corresponding type. +private var _swiftTestingELFNoteNameTuple: (expression: TupleExprSyntax, type: TupleTypeSyntax) { + let name = _swiftTestingELFNoteName + let ccharType = TupleTypeElementSyntax(type: IdentifierTypeSyntax(name: .identifier("CChar"))) + + return ( + TupleExprSyntax { + for c in name { + LabeledExprSyntax(expression: IntegerLiteralExprSyntax(Int(c))) + } + }, + TupleTypeSyntax( + elements: TupleTypeElementListSyntax { + for _ in name { + ccharType + } + } + ) + ) +} + +/// 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 note being emitted. +/// - accessorName: The Swift name of an `@convention(c)` function to emit +/// into the resulting record. +/// - flags: Flags to emit as part of this note. The value of this argument is +/// dependent on the kind of test content this instance represents. +/// +/// - 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. +/// +/// When the ELF `PT_NOTE` format is in use, the `kind` argument is used as the +/// value of the note's `n_type` field. +func makeTestContentRecordDecl(named name: TokenSyntax, in typeName: TypeSyntax? = nil, ofKind kind: TestContentKind, accessingWith accessorName: TokenSyntax, flags: UInt64 = 0) -> DeclSyntax { + let elfNoteName = _swiftTestingELFNoteNameTuple + return """ + #if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) || os(visionOS) + @_section("__DATA_CONST,__swift5_tests") + #elseif os(Linux) || os(FreeBSD) || os(Android) + @_section(".note.swift.test") + #elseif os(WASI) + @_section("swift5_tests") + #elseif os(Windows) + @_section(".sw5test") + #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): ( + namesz: Int32, + descsz: Int32, + type: Int32, + name: \(elfNoteName.type), + accessor: @convention(c) (UnsafeMutableRawPointer) -> Bool, + flags: UInt64 + ) = ( + Int32(MemoryLayout<\(elfNoteName.type)>.stride), + Int32(MemoryLayout.stride + MemoryLayout.stride), + \(raw: kind.rawValue) as Int32, + \(elfNoteName.expression) as \(elfNoteName.type), + \(accessorName) as @convention(c) (UnsafeMutableRawPointer) -> Bool, + \(raw: flags) as UInt64 + ) + """ +} diff --git a/Sources/TestingMacros/TestDeclarationMacro.swift b/Sources/TestingMacros/TestDeclarationMacro.swift index f23369027..cacc4ab60 100644 --- a/Sources/TestingMacros/TestDeclarationMacro.swift +++ b/Sources/TestingMacros/TestDeclarationMacro.swift @@ -228,17 +228,6 @@ public struct TestDeclarationMacro: PeerMacro, Sendable { } } - /// 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. /// @@ -367,7 +356,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) } """ @@ -432,16 +421,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), - in: \(typeNameExpr), - xcTestCompatibleSelector: \(selectorExpr ?? "nil"), - \(raw: attributeInfo.functionArgumentList(in: context)), - parameters: \(raw: functionDecl.testFunctionParameterList), - testFunction: \(thunkDecl.name) - ) - ] + return .__function( + named: \(literal: functionDecl.completeName), + 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 @@ -458,16 +445,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), - 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), + in: \(typeNameExpr), + xcTestCompatibleSelector: \(selectorExpr ?? "nil"), + \(raw: attributeInfo.functionArgumentList(in: context)), + testFunction: {} + ) } """ ) @@ -482,30 +467,47 @@ public struct TestDeclarationMacro: PeerMacro, Sendable { ) } - // The emitted type must be public or the compiler can optimize it away - // (since it is not actually used anywhere that the compiler can see.) - // - // The emitted type must be deprecated to avoid causing warnings in client - // code since it references the test function thunk, which is itself - // deprecated to allow test functions to validate deprecated APIs. The - // emitted type is also annotated unavailable, since it's meant only for use - // by the testing library at runtime. The compiler does not allow combining - // 'unavailable' and 'deprecated' into a single availability attribute: - // rdar://111329796 - let enumName = context.makeUniqueName(thunking: functionDecl, withPrefix: "__🟠$test_container__function__") + // We need an extra trampoline through a static property getter (rather than + // just having this logic in the C thunk) because some versions of the Swift + // compiler think that the presence of a `try` keyword in `testsBody` means + // that it must be throwing (disregarding autoclosures.) + let accessorName = context.makeUniqueName(thunking: functionDecl) result.append( """ - @available(*, deprecated, message: "This type is an implementation detail of the testing library. Do not use it directly.") - enum \(enumName): Testing.__TestContainer { - static var __tests: [Testing.Test] { - get async { - \(raw: testsBody) - } - } + @available(*, deprecated, message: "This property is an implementation detail of the testing library. Do not use it directly.") + @Sendable private \(staticKeyword(for: typeName)) func \(accessorName)() async -> Testing.Test { + \(raw: testsBody) } """ ) + let cAccessorName = context.makeUniqueName(thunking: functionDecl) + result.append( + """ + @available(*, deprecated, message: "This property is an implementation detail of the testing library. Do not use it directly.") + private \(staticKeyword(for: typeName)) let \(cAccessorName): @convention(c) (UnsafeMutableRawPointer) -> Bool = { + $0.initializeMemory(as: (@Sendable () async -> Testing.Test).self, to: \(accessorName)) + return true + } + """ + ) + + var flags = UInt64(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: cAccessorName, + flags: flags + ) + ) + return result } } diff --git a/Sources/_TestingInternals/Discovery.cpp b/Sources/_TestingInternals/Discovery.cpp index dc9e882ae..e36f2069a 100644 --- a/Sources/_TestingInternals/Discovery.cpp +++ b/Sources/_TestingInternals/Discovery.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -28,14 +29,14 @@ #include #endif -/// Enumerate over all Swift type metadata sections in the current process. +/// Enumerate over all Swift test content sections in the current process. /// /// - Parameters: -/// - body: A function to call once for every section in the current process. -/// A pointer to the first type metadata record and the number of records -/// are passed to this function. +/// - body: A function to call once for every section in the current process +/// that contains test content. A pointer to the first test content record +/// and the size, in bytes, of the section are passed to this function. template -static void enumerateTypeMetadataSections(const SectionEnumerator& body); +static void enumerateTestContentSections(const SectionEnumerator& body); /// A type that acts as a C++ [Allocator](https://en.cppreference.com/w/cpp/named_req/Allocator) /// without using global `operator new` or `operator delete`. @@ -59,11 +60,6 @@ struct SWTHeapAllocator { }; /// A structure describing the bounds of a Swift metadata section. -/// -/// The template argument `T` is the element type of the metadata section. -/// Instances of this type can be used with a range-based `for`-loop to iterate -/// the contents of the section. -template struct SWTSectionBounds { /// The base address of the image containing the section, if known. const void *imageAddress; @@ -73,169 +69,89 @@ struct SWTSectionBounds { /// The size of the section in bytes. size_t size; - - const struct SWTTypeMetadataRecord *begin(void) const { - return reinterpret_cast(start); - } - - const struct SWTTypeMetadataRecord *end(void) const { - return reinterpret_cast(reinterpret_cast(start) + size); - } }; /// A type that acts as a C++ [Container](https://en.cppreference.com/w/cpp/named_req/Container) -/// and which contains a sequence of instances of `SWTSectionBounds`. -template -using SWTSectionBoundsList = std::vector, SWTHeapAllocator>>; +/// and which contains a sequence of instances of `SWTSectionBounds`. +using SWTSectionBoundsList = std::vector>; -#pragma mark - Swift ABI +#pragma mark - Swift Testing ABI -#if defined(__PTRAUTH_INTRINSICS__) -#include -#define SWT_PTRAUTH __ptrauth +#if defined(__ELF__) +using SWTELFNoteHeader = ElfW(Nhdr); #else -#define SWT_PTRAUTH(...) -#endif -#define SWT_PTRAUTH_SWIFT_TYPE_DESCRIPTOR SWT_PTRAUTH(ptrauth_key_process_independent_data, 1, 0xae86) - -/// A type representing a pointer relative to itself. -/// -/// This type is derived from `RelativeDirectPointerIntPair` in the Swift -/// repository. -template -struct SWTRelativePointer { -private: - int32_t _offset; - -public: - SWTRelativePointer(const SWTRelativePointer&) = delete; - SWTRelativePointer(const SWTRelativePointer&&) = delete; - SWTRelativePointer& operator =(const SWTRelativePointer&) = delete; - SWTRelativePointer& operator =(const SWTRelativePointer&&) = delete; - - int32_t getRawValue(void) const { - return _offset; - } - - const T *_Nullable get(void) const& { - int32_t maskedOffset = getRawValue() & ~maskValue; - if (maskedOffset == 0) { - return nullptr; - } - - auto offset = static_cast(static_cast(maskedOffset)); - auto result = reinterpret_cast(reinterpret_cast(this) + offset); -#if defined(__PTRAUTH_INTRINSICS__) - if (std::is_function_v && result) { - result = ptrauth_strip(result, ptrauth_key_function_pointer); - result = ptrauth_sign_unauthenticated(result, ptrauth_key_function_pointer, 0); - } -#endif - return reinterpret_cast(result); - } - - const T *_Nullable operator ->(void) const& { - return get(); - } -}; - -/// A type representing a 32-bit absolute function pointer, usually used on platforms -/// where relative function pointers are not supported. +/// A redeclaration of `ElfW(Nhdr)` for platforms that do not use ELF binaries. /// -/// This type is derived from `AbsoluteFunctionPointer` in the Swift repository. -template -struct SWTAbsoluteFunctionPointer { -private: - T *_pointer; - static_assert(sizeof(T *) == sizeof(int32_t), "Function pointer must be 32-bit when using compact absolute pointer"); - -public: - const T *_Nullable get(void) const & { - return _pointer; - } - - const T *_Nullable operator ->(void) const & { - return get(); - } +/// For a description of the fields in this structure, see the documentation for +/// the ELF binary format. +struct SWTELFNoteHeader { + int32_t n_namesz; + int32_t n_descsz; + int32_t n_type; }; - -/// A type representing a pointer relative to itself with low bits reserved for -/// use as flags. -/// -/// This type is derived from `RelativeDirectPointerIntPair` in the Swift -/// repository. -template -struct SWTRelativePointerIntPair: public SWTRelativePointer { - I getInt() const & { - return I(this->getRawValue() & maskValue); - } -}; - -template -#if defined(__wasm32__) -using SWTCompactFunctionPointer = SWTAbsoluteFunctionPointer; -#else -using SWTCompactFunctionPointer = SWTRelativePointer; #endif -/// A type representing a metatype as constructed during compilation of a Swift -/// module. +/// A "raw" test content record as it appears in a loaded image. /// -/// This type is derived from `TargetTypeContextDescriptor` in the Swift -/// repository. -struct SWTTypeContextDescriptor { -private: - uint32_t _flags; - SWTRelativePointer _parent; - SWTRelativePointer _name; - - struct MetadataAccessResponse { - void *value; - size_t state; - }; - using MetadataAccessFunction = __attribute__((swiftcall)) MetadataAccessResponse(size_t); - SWTCompactFunctionPointer _metadataAccessFunction; - -public: - const char *_Nullable getName(void) const& { - return _name.get(); +/// The layout of this type is equivalent to that of an ELF Note. On platforms +/// that use the ELF binary format, instances of this type can be found in +/// program headers of type `PT_NOTE`. On other platforms, instances of this +/// type can be found in dedicated platform-specific locations (for Mach-O and +/// COFF/PE, the `"__DATA_CONST,__swift5_tests"` and `".sw5test"` sections, +/// respectively.) +/// +/// For more information about the ELF binary format and ELF Notes specifically, +/// see [the documentation](https://en.wikipedia.org/wiki/Executable_and_Linkable_Format) +/// for the ELF binary format. +struct SWTRawTestContentRecord: private SWTELFNoteHeader { + /// Get the type of this record. + /// + /// The result of this function corresponds to the ELF Note header's `n_type` + /// field. The testing library uses this field to store the test content + /// record's `kind` field. + int32_t getType() const { + return n_type; } - void *_Nullable getMetadata(void) const& { - if (auto fp = _metadataAccessFunction.get()) { - return (* fp)(0xFF).value; + /// Get the name of this record. + /// + /// If the underlying storage does not specify a name, this function returns + /// the empty string. + std::string_view getName() const { + if (n_namesz == 0) { + return ""; } - return nullptr; + auto start = reinterpret_cast(this + 1); + auto size = ::strnlen(start, n_namesz); + return std::string_view { start, size }; } - bool isGeneric(void) const& { - return (_flags & 0x80u) != 0; - } -}; - -/// A type representing a relative pointer to a type descriptor. -/// -/// This type is derived from `TargetTypeMetadataRecord` in the Swift -/// repository. -struct SWTTypeMetadataRecord { -private: - SWTRelativePointerIntPair _pointer; - -public: - const SWTTypeContextDescriptor *_Nullable getContextDescriptor(void) const { - switch (_pointer.getInt()) { - case 0: // Direct pointer. - return reinterpret_cast(_pointer.get()); - case 1: // Indirect pointer (pointer to a pointer.) - // The inner pointer is signed when pointer authentication - // instructions are available. - if (auto contextDescriptor = reinterpret_cast(_pointer.get())) { - return *contextDescriptor; - } - [[fallthrough]]; - default: // Unsupported or invalid. + /// Get the "description" of this record. + /// + /// The ELF format labels the custom data in an ELF Note its "description". + /// The ELF format does not prescribe any particular format, layout, etc., so + /// this property is untyped. Cast the resulting pointer to an appropriate + /// type only _after_ verifying that its name and type are correct. + const void *getDescription() const { + if (n_descsz == 0) { return nullptr; } + return reinterpret_cast(this + 1) + __builtin_align_up(n_namesz, sizeof(uint32_t)); + } + + /// Get the size of this record in bytes. + /// + /// The result of this function includes the size of the header, name, and + /// description, and is rounded up to 4 or 8 bytes depending on the current + /// system's pointer width. + /// + /// For more information on how to compute the size of an ELF Note, review the + /// source of the `readelf` tool included with GDB. + size_t getSize() const { + return __builtin_align_up( + sizeof(*this) + __builtin_align_up(n_namesz, sizeof(uint32_t)) + __builtin_align_up(n_descsz, sizeof(uint32_t)), + sizeof(uintptr_t) + ); } }; @@ -243,14 +159,7 @@ struct SWTTypeMetadataRecord { #if !defined(SWT_NO_DYNAMIC_LINKING) #pragma mark - Apple implementation -/// Get a copy of the currently-loaded type metadata sections list. -/// -/// - Returns: A list of type metadata sections in images loaded into the -/// current process. The order of the resulting list is unspecified. -/// -/// On ELF-based platforms, the `swift_enumerateAllMetadataSections()` function -/// exported by the runtime serves the same purpose as this function. -static SWTSectionBoundsList getSectionBounds(void) { +static SWTSectionBoundsList getTestContentSections(void) { /// This list is necessarily mutated while a global libobjc- or dyld-owned /// lock is held. Hence, code using this list must avoid potentially /// re-entering either library (otherwise it could potentially deadlock.) @@ -260,13 +169,13 @@ static SWTSectionBoundsList getSectionBounds(void) { /// testing library is not tasked with the same performance constraints as /// Swift's runtime library, we just use a `std::vector` guarded by an unfair /// lock. - static constinit SWTSectionBoundsList *sectionBounds = nullptr; + static constinit SWTSectionBoundsList *sectionBounds = nullptr; static constinit os_unfair_lock lock = OS_UNFAIR_LOCK_INIT; static constinit dispatch_once_t once = 0; dispatch_once_f(&once, nullptr, [] (void *) { - sectionBounds = reinterpret_cast *>(std::malloc(sizeof(SWTSectionBoundsList))); - ::new (sectionBounds) SWTSectionBoundsList(); + sectionBounds = reinterpret_cast(std::malloc(sizeof(SWTSectionBoundsList))); + ::new (sectionBounds) SWTSectionBoundsList(); sectionBounds->reserve(_dyld_image_count()); objc_addLoadImageFunc([] (const mach_header *mh) { @@ -287,7 +196,7 @@ static SWTSectionBoundsList getSectionBounds(void) { // If this image contains the Swift section we need, acquire the lock and // store the section's bounds. unsigned long size = 0; - auto start = getsectiondata(mhn, SEG_TEXT, "__swift5_types", &size); + auto start = getsectiondata(mhn, "__DATA_CONST", "__swift5_tests", &size); if (start && size > 0) { os_unfair_lock_lock(&lock); { sectionBounds->emplace_back(mhn, start, size); @@ -298,7 +207,7 @@ static SWTSectionBoundsList getSectionBounds(void) { // After the first call sets up the loader hook, all calls take the lock and // make a copy of whatever has been loaded so far. - SWTSectionBoundsList result; + SWTSectionBoundsList result; result.reserve(_dyld_image_count()); os_unfair_lock_lock(&lock); { result = *sectionBounds; @@ -308,9 +217,9 @@ static SWTSectionBoundsList getSectionBounds(void) { } template -static void enumerateTypeMetadataSections(const SectionEnumerator& body) { +static void enumerateTestContentSections(const SectionEnumerator& body) { bool stop = false; - for (const auto& sb : getSectionBounds()) { + for (const auto& sb : getTestContentSections()) { body(sb, &stop); if (stop) { break; @@ -325,12 +234,12 @@ static void enumerateTypeMetadataSections(const SectionEnumerator& body) { // only one image (this one) with Swift code in it. // SEE: https://github.com/swiftlang/swift/tree/main/stdlib/public/runtime/ImageInspectionStatic.cpp -extern "C" const char sectionBegin __asm("section$start$__TEXT$__swift5_types"); -extern "C" const char sectionEnd __asm("section$end$__TEXT$__swift5_types"); +extern "C" const char sectionBegin __asm("section$start$__DATA_CONST$__swift5_tests"); +extern "C" const char sectionEnd __asm("section$end$__DATA_CONST$__swift5_tests"); template -static void enumerateTypeMetadataSections(const SectionEnumerator& body) { - SWTSectionBounds sb = { +static void enumerateTestContentSections(const SectionEnumerator& body) { + SWTSectionBounds sb = { nullptr, §ionBegin, static_cast(std::distance(§ionBegin, §ionEnd)) @@ -354,7 +263,7 @@ static void enumerateTypeMetadataSections(const SectionEnumerator& body) { /// in bytes, or `std::nullopt` if the section could not be found. If the /// section was emitted by the Swift toolchain, be aware it will have leading /// and trailing bytes (`sizeof(uintptr_t)` each.) -static std::optional> findSection(HMODULE hModule, const char *sectionName) { +static std::optional findSection(HMODULE hModule, const char *sectionName) { if (!hModule) { return std::nullopt; } @@ -385,7 +294,7 @@ static std::optional> findSection(HMODUL // FIXME: Handle longer names ("/%u") from string table auto thisSectionName = reinterpret_cast(section->Name); if (0 == std::strncmp(sectionName, thisSectionName, IMAGE_SIZEOF_SHORT_NAME)) { - return SWTSectionBounds { hModule, start, size }; + return SWTSectionBounds { hModule, start, size }; } } } @@ -394,7 +303,7 @@ static std::optional> findSection(HMODUL } template -static void enumerateTypeMetadataSections(const SectionEnumerator& body) { +static void enumerateTestContentSections(const SectionEnumerator& body) { // Find all the modules loaded in the current process. We assume there aren't // more than 1024 loaded modules (as does Microsoft sample code.) std::array hModules; @@ -411,10 +320,10 @@ static void enumerateTypeMetadataSections(const SectionEnumerator& body) { // be but it is safer: the callback will eventually invoke developer code that // could theoretically unload a module from the list we're enumerating. (Swift // modules do not support unloading, so we'll just not worry about them.) - SWTSectionBoundsList sectionBounds; + SWTSectionBoundsList sectionBounds; sectionBounds.reserve(hModuleCount); for (size_t i = 0; i < hModuleCount; i++) { - if (auto sb = findSection(hModules[i], ".sw5tymd")) { + if (auto sb = findSection(hModules[i], ".sw5test")) { sectionBounds.push_back(*sb); } } @@ -437,18 +346,18 @@ static void enumerateTypeMetadataSections(const SectionEnumerator& body) { #elif defined(__wasi__) #pragma mark - WASI implementation (statically linked) -extern "C" const char __start_swift5_type_metadata; -extern "C" const char __stop_swift5_type_metadata; +extern "C" const char __start_swift5_tests; +extern "C" const char __stop_swift5_tests; template -static void enumerateTypeMetadataSections(const SectionEnumerator& body) { +static void enumerateTestContentSections(const SectionEnumerator& body) { // WASI only has a single image (so far) and it is statically linked, so all // Swift metadata ends up in the same section bounded by the named symbols // above. So we can just yield the section betwixt them. - const auto& sectionBegin = __start_swift5_type_metadata; - const auto& sectionEnd = __stop_swift5_type_metadata; + const auto& sectionBegin = __start_swift5_tests; + const auto& sectionEnd = __stop_swift5_tests; - SWTSectionBounds sb = { + SWTSectionBounds sb = { nullptr, §ionBegin, static_cast(std::distance(§ionBegin, §ionEnd)) @@ -460,95 +369,70 @@ static void enumerateTypeMetadataSections(const SectionEnumerator& body) { #elif defined(__linux__) || defined(__FreeBSD__) || defined(__ANDROID__) #pragma mark - ELF implementation -/// Specifies the address range corresponding to a section. -struct MetadataSectionRange { - uintptr_t start; - size_t length; -}; - -/// Identifies the address space ranges for the Swift metadata required by the -/// Swift runtime. -struct MetadataSections { - uintptr_t version; - std::atomic baseAddress; - - void *unused0; - void *unused1; - - MetadataSectionRange swift5_protocols; - MetadataSectionRange swift5_protocol_conformances; - MetadataSectionRange swift5_type_metadata; - MetadataSectionRange swift5_typeref; - MetadataSectionRange swift5_reflstr; - MetadataSectionRange swift5_fieldmd; - MetadataSectionRange swift5_assocty; - MetadataSectionRange swift5_replace; - MetadataSectionRange swift5_replac2; - MetadataSectionRange swift5_builtin; - MetadataSectionRange swift5_capture; - MetadataSectionRange swift5_mpenum; - MetadataSectionRange swift5_accessible_functions; -}; - -/// A function exported by the Swift runtime that enumerates all metadata -/// sections loaded into the current process. -SWT_IMPORT_FROM_STDLIB void swift_enumerateAllMetadataSections( - bool (* body)(const MetadataSections *sections, void *context), - void *context -); - template -static void enumerateTypeMetadataSections(const SectionEnumerator& body) { - swift_enumerateAllMetadataSections([] (const MetadataSections *sections, void *context) { - bool stop = false; +static void enumerateTestContentSections(const SectionEnumerator& body) { + dl_iterate_phdr([] (struct dl_phdr_info *info, size_t size, void *context) -> int { + const auto& body = *reinterpret_cast(context); - const auto& body = *reinterpret_cast(context); - MetadataSectionRange section = sections->swift5_type_metadata; - if (section.start && section.length > 0) { - SWTSectionBounds sb = { - sections->baseAddress.load(), - reinterpret_cast(section.start), - section.length - }; - body(sb, &stop); + bool stop = false; + for (size_t i = 0; !stop && i < info->dlpi_phnum; i++) { + const auto& phdr = info->dlpi_phdr[i]; + if (phdr.p_type == PT_NOTE) { + SWTSectionBounds sb = { + reinterpret_cast(info->dlpi_addr), + reinterpret_cast(info->dlpi_addr + phdr.p_vaddr), + static_cast(phdr.p_memsz) + }; + body(sb, &stop); + } } - return !stop; + return stop; }, const_cast(&body)); } #else #warning Platform-specific implementation missing: Runtime test discovery unavailable template -static void enumerateTypeMetadataSections(const SectionEnumerator& body) {} +static void enumerateTestContentSections(const SectionEnumerator& body) {} #endif #pragma mark - -void swt_enumerateTypesWithNamesContaining(const char *nameSubstring, void *context, SWTTypeEnumerator body) { - enumerateTypeMetadataSections([=] (const SWTSectionBounds& sectionBounds, bool *stop) { - for (const auto& record : sectionBounds) { - auto contextDescriptor = record.getContextDescriptor(); - if (!contextDescriptor) { - // This type metadata record is invalid (or we don't understand how to - // get its context descriptor), so skip it. - continue; - } else if (contextDescriptor->isGeneric()) { - // Generic types cannot be fully instantiated without generic - // parameters, which is not something we can know abstractly. +void swt_enumerateTestContent(void *context, SWTTestContentEnumerator body) { + enumerateTestContentSections([=] (const SWTSectionBounds& sb, bool *stop) { + // Because the size of a test content record is not fixed, walking a test + // content section isn't particularly elegant. (Sorry!) + uintptr_t start = reinterpret_cast(sb.start); + size_t offset = 0; + while (!*stop && offset < sb.size) { + const auto& record = *reinterpret_cast(start + offset); + offset += record.getSize(); + + // Check if we created the record. At this time, we don't care about + // records created by any other component. + if (record.getName() != "Swift Testing") { continue; } - // Check that the type's name passes. This will be more expensive than the - // checks above, but should be cheaper than realizing the metadata. - const char *typeName = contextDescriptor->getName(); - bool nameOK = typeName && nullptr != std::strstr(typeName, nameSubstring); - if (!nameOK) { + // Extract the content of this record now that we know it's ours. + struct Content { + bool (* accessor)(void *outValue); + uint64_t flags; + }; + auto content = reinterpret_cast(record.getDescription()); + if (!content) { continue; } - if (void *typeMetadata = contextDescriptor->getMetadata()) { - body(sectionBounds.imageAddress, typeMetadata, stop, context); - } + // Create a "public-facing" copy of the record that can be passed back up + // to Swift for further processing. + SWTTestContentRecord recordCopy { + sb.imageAddress, + static_cast(record.getType()), + content->accessor, + content->flags + }; + body(&recordCopy, stop, context); } }); } diff --git a/Sources/_TestingInternals/include/Discovery.h b/Sources/_TestingInternals/include/Discovery.h index d12f623ee..06e059f46 100644 --- a/Sources/_TestingInternals/include/Discovery.h +++ b/Sources/_TestingInternals/include/Discovery.h @@ -16,32 +16,68 @@ SWT_ASSUME_NONNULL_BEGIN -/// The type of callback called by `swt_enumerateTypes()`. +/// 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 TestContentGeneration.swift and TestContent.md. +typedef enum SWTTestContentKind: int32_t { + /// A test or suite declaration. + SWTTestContentKindTestDeclaration = 100, + + /// An exit test. + SWTTestContentKindExitTest = 101, + +} __attribute__((enum_extensibility(closed))) SWTTestContentKind; + +/// A type describing a record of test content found in the current process. +/// +/// The layout of this structure does not represent the layout of a test content +/// record in memory or on disk. See `SWTRawTestContentRecord` in Discovery.cpp +/// for more information. +typedef struct SWTTestContentRecord { + /// The base address of the image containing the test content, if available. + const void *_Null_unspecified imageAddress; + + /// The kind of test content. + SWTTestContentKind kind; + + /// A function which, when called, produces the test content as a retained + /// Swift object. + /// + /// - Returns: The represented test content, or `nullptr` if it could not be + /// produced. The caller of this function is responsible for releasing the + /// result using the `Unmanaged` interface. + bool (* _Nullable accessor)(void *outValue); + + /// Flags for this record. + /// + /// The value of this property is dependent on the kind of test content this + /// instance represents. + uint64_t flags; +} SWTTestContentRecord; + +/// The type of callback called by `swt_enumerateTestContent()`. /// /// - Parameters: -/// - imageAddress: A pointer to the start of the image. This value is _not_ -/// equal to the value returned from `dlopen()`. On platforms that do not -/// support dynamic loading (and so do not have loadable images), this -/// argument is unspecified. -/// - typeMetadata: A type metadata pointer that can be bitcast to `Any.Type`. -/// - stop: A pointer to a boolean variable indicating whether type +/// - record: A pointer to a structure containing information about the +/// enumerated test content. +/// - stop: A pointer to a boolean variable indicating whether test content /// enumeration should stop after the function returns. Set `*stop` to -/// `true` to stop type enumeration. +/// `true` to stop test content enumeration. /// - context: An arbitrary pointer passed by the caller to -/// `swt_enumerateTypes()`. -typedef void (* SWTTypeEnumerator)(const void *_Null_unspecified imageAddress, void *typeMetadata, bool *stop, void *_Null_unspecified context); +/// `swt_enumerateTestContent()`. +typedef void (* SWTTestContentEnumerator)(const SWTTestContentRecord *record, bool *stop, void *_Null_unspecified context); -/// Enumerate all types known to Swift found in the current process. +/// Enumerate all test content known to Swift and found in the current process. /// /// - Parameters: -/// - nameSubstring: A string which the names of matching classes all contain. /// - context: An arbitrary pointer to pass to `body`. /// - body: A function to invoke, once per matching type. -SWT_EXTERN void swt_enumerateTypesWithNamesContaining( - const char *nameSubstring, +SWT_EXTERN void swt_enumerateTestContent( void *_Null_unspecified context, - SWTTypeEnumerator body -) SWT_SWIFT_NAME(swt_enumerateTypes(withNamesContaining:_:_:)); + SWTTestContentEnumerator body +) SWT_SWIFT_NAME(swt_enumerateTestContent(_:_:)); SWT_ASSUME_NONNULL_END diff --git a/Sources/_TestingInternals/include/Includes.h b/Sources/_TestingInternals/include/Includes.h index 420a8c745..d74c7790c 100644 --- a/Sources/_TestingInternals/include/Includes.h +++ b/Sources/_TestingInternals/include/Includes.h @@ -92,6 +92,10 @@ #include #endif +#if __has_include() +#include +#endif + #if __has_include() #include #endif diff --git a/Tests/TestingMacrosTests/TestDeclarationMacroTests.swift b/Tests/TestingMacrosTests/TestDeclarationMacroTests.swift index fffa06664..bdfccf683 100644 --- a/Tests/TestingMacrosTests/TestDeclarationMacroTests.swift +++ b/Tests/TestingMacrosTests/TestDeclarationMacroTests.swift @@ -360,7 +360,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 d8482c612..534792416 100644 --- a/Tests/TestingTests/MiscellaneousTests.swift +++ b/Tests/TestingTests/MiscellaneousTests.swift @@ -9,6 +9,7 @@ // @testable @_spi(Experimental) @_spi(ForToolsIntegrationOnly) import Testing +private import _TestingInternals @Test(/* name unspecified */ .hidden) @Sendable func freeSyncFunction() {} @@ -548,4 +549,26 @@ struct MiscellaneousTests { } #expect(duration < .seconds(1)) } + +#if !SWT_NO_DYNAMIC_LINKING + @Test("Can get the image address of a test function") func imageAddress() throws { + let test = try #require(Test.current) + let imageAddress = try #require(test.imageAddress) +#if SWT_TARGET_OS_APPLE || os(Linux) || os(FreeBSD) || os(Android) + var info = Dl_info() + #expect(0 != dladdr(imageAddress, &info)) + #expect(imageAddress == info.dli_fbase!) +#elseif os(Windows) + let hModule = HMODULE(mutating: imageAddress) + #expect(GetProcAddress(hModule, "someFunctionICanFindDynamically") != nil) +#endif + } +#endif } + +// MARK: - Fixtures + +#if os(Windows) +@_cdecl("someFunctionICanFindDynamically") +func someFunctionICanFindDynamically() {} +#endif diff --git a/Tests/TestingTests/ZipTests.swift b/Tests/TestingTests/ZipTests.swift index bd767b222..210d1688a 100644 --- a/Tests/TestingTests/ZipTests.swift +++ b/Tests/TestingTests/ZipTests.swift @@ -23,6 +23,6 @@ struct ZipTests { @Test("All elements of two ranges are equal", arguments: zip(0 ..< 10, 0 ..< 10)) func allElementsEqual(i: Int, j: Int) { - #expect(i == j) + #expect(i == j, "") } } diff --git a/cmake/modules/shared/CompilerSettings.cmake b/cmake/modules/shared/CompilerSettings.cmake index 9b59963fc..6f1736da1 100644 --- a/cmake/modules/shared/CompilerSettings.cmake +++ b/cmake/modules/shared/CompilerSettings.cmake @@ -15,6 +15,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)