From b9cfdccf8f0d15f79c82c475acb3d8ba349c94bb 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 | 72 +++++++++++++------ Package.swift | 9 +++ Sources/Testing/CMakeLists.txt | 1 + Sources/Testing/Test+Discovery.swift | 4 +- Sources/Testing/Test.swift | 25 ++++--- .../Support/TestContentGeneration.swift | 8 ++- Sources/_TestingInternals/Discovery.cpp | 3 +- Sources/_TestingInternals/include/Discovery.h | 2 +- 8 files changed, 86 insertions(+), 38 deletions(-) diff --git a/Documentation/ABI/TestContent.md b/Documentation/ABI/TestContent.md index 5c29f6c7c..9981711e1 100644 --- a/Documentation/ABI/TestContent.md +++ b/Documentation/ABI/TestContent.md @@ -21,14 +21,8 @@ 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] | +| macOS, iOS, watchOS, tvOS, visionOS | Mach-O | `__DATA_CONST,__swift5_tests` | +| Linux, FreeBSD, Android | ELF | `PT_NOTE`[^1] | | WASI | Statically Linked | `swift5_tests` | | Windows | PE/COFF | `.sw5test` | @@ -36,13 +30,30 @@ the testing library are stored in dedicated platform-specific sections: 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 +### Record headers 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: +testing library have the following structure: + +```c +struct SWTTestContentHeader { + int32_t n_namesz; + int32_t n_descsz; + int32_t n_type; + char n_name[n_namesz]; + // ... +}; +``` + +The size of `n_name` is dynamic and cannot be statically computed. The testing +library always generates the name `"Swift Testing"` and specifies an `n_namesz` +value of `20` (the string being null-padded to the correct length), but other +content may be present in the same section whose header content differs. For +more information about this structure such as its alignment requirements, see +the documentation for the [ELF format](https://man7.org/linux/man-pages/man5/elf.5.html). + +Each record's _kind_ (stored in the `n_type` field) determines how the record +will be interpreted at runtime: | Type Value | Interpretation | |-:|-| @@ -54,18 +65,28 @@ interpreted at runtime: -### Loading test content from a record +### Record contents -For all currently-defined record types, the header and name are followed by a -structure of the following form: +For all currently-defined record types, the header structure is immediately +followed by the actual content of the record. A test content record currently +contains an `accessor` function to load the corresponding Swift content and a +`flags` field whose value depends on the type of record. The overall structure +of a record therefore looks like: ```c struct SWTTestContent { + SWTTestContentHeader header; bool (* accessor)(void *); - uint64_t flags; + uint32_t flags; + uint32_t reserved; }; ``` +This structure may grow in the future as needed. Check the `header.n_descsz` +field to determine if there are additional fields present. Do not assume that +the size of this structure will remain fixed over time or that all discovered +test content records are the same size. + #### The accessor field The function `accessor` is a C function whose signature in Swift can be restated @@ -80,19 +101,20 @@ 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: +The concrete Swift type of the value written to `outValue` depends on the type +of record: | Type Value | Return Type | |-:|-| | < `0` | Undefined (**do not use**) | -| `0` ... `99` | `nil` | +| `0` ... `99` | Reserved (**do not use**) | | `100` | `@Sendable () async -> Test`[^2] | -| `101` | `ExitTest` (owned by caller) | +| `101` | `ExitTest` (consumed 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`. + function reference it writes to `outValue`. 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 @@ -105,6 +127,10 @@ For test or suite declarations (type `100`), the following flags are defined: For exit test declarations (type `101`), no flags are currently defined. +#### The reserved field + +This field is reserved for future use. Always set it to `0`. + ## Third-party test content TODO: elaborate how tools can reuse the same `n_name` and `n_type` fields to diff --git a/Package.swift b/Package.swift index d41b35949..96347e535 100644 --- a/Package.swift +++ b/Package.swift @@ -164,6 +164,15 @@ extension Array where Element == PackageDescription.CXXSetting { static var packageSettings: Self { var result = Self() + result += [ + .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])), + .define("SWT_NO_SNAPSHOT_TYPES", .when(platforms: [.linux, .windows, .wasi])), + .define("SWT_NO_DYNAMIC_LINKING", .when(platforms: [.wasi])), + .define("SWT_NO_PIPES", .when(platforms: [.wasi])), + ] + // Capture the testing library's version as a C++ string constant. if let git = Context.gitInformation { let testingLibraryVersion = if let tag = git.currentTag { diff --git a/Sources/Testing/CMakeLists.txt b/Sources/Testing/CMakeLists.txt index d02ba6ea5..e565daebe 100644 --- a/Sources/Testing/CMakeLists.txt +++ b/Sources/Testing/CMakeLists.txt @@ -79,6 +79,7 @@ add_library(Testing Test.ID.swift Test.swift Test+Discovery.swift + Test+Discovery+MachO.swift Test+Macro.swift Traits/Bug.swift Traits/Comment.swift diff --git a/Sources/Testing/Test+Discovery.swift b/Sources/Testing/Test+Discovery.swift index c87584f2f..b0a124a8e 100644 --- a/Sources/Testing/Test+Discovery.swift +++ b/Sources/Testing/Test+Discovery.swift @@ -44,7 +44,9 @@ extension Test { nonisolated(unsafe) let imageAddress = imageAddress generators.append { @Sendable in var result = await generator() +#if !SWT_NO_DYNAMIC_LINKING result.imageAddress = imageAddress +#endif return result } } @@ -152,7 +154,7 @@ private func _enumerateTestContent(_ body: _TestContentEnumerator) { /// - stop: An `inout` boolean variable indicating whether type enumeration /// should stop after the function returns. Set `stop` to `true` to stop /// type enumeration. -typealias TestContentEnumerator = (_ imageAddress: UnsafeRawPointer?, _ content: borrowing T, _ flags: UInt64, _ stop: inout Bool) -> Void where T: ~Copyable +typealias TestContentEnumerator = (_ imageAddress: UnsafeRawPointer?, _ content: borrowing T, _ flags: UInt32, _ stop: inout Bool) -> Void where T: ~Copyable /// Enumerate all test content known to Swift and found in the current process. /// diff --git a/Sources/Testing/Test.swift b/Sources/Testing/Test.swift index 2053e4d18..0ffaf4188 100644 --- a/Sources/Testing/Test.swift +++ b/Sources/Testing/Test.swift @@ -53,18 +53,25 @@ public struct Test: Sendable { /// The source location of this test. public var sourceLocation: SourceLocation +#if !SWT_NO_DYNAMIC_LINKING /// 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. + /// This property's value represents the image that contains this test and is + /// equivalent to various platform-specific values: /// - /// 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? + /// | Platform | Equivalent To | + /// |-|-| + /// | macOS, iOS, tvOS, visionOS | `UnsafePointer` | + /// | watchOS | `UnsafePointer` | + /// | Linux, FreeBSD, Android (32-bit) | `UnsafePointer` | + /// | Linux, FreeBSD, Android (64-bit) | `UnsafePointer` | + /// | Windows | `HMODULE` | + /// + /// The value of this property is distinct from the pointer returned by + /// `dlopen()` (on platforms that have that function.) + @_spi(ForToolsIntegrationOnly) + public nonisolated(unsafe) var imageAddress: UnsafeRawPointer? +#endif /// Information about the type containing this test, if any. /// diff --git a/Sources/TestingMacros/Support/TestContentGeneration.swift b/Sources/TestingMacros/Support/TestContentGeneration.swift index dfe930b20..9c8f5e5d8 100644 --- a/Sources/TestingMacros/Support/TestContentGeneration.swift +++ b/Sources/TestingMacros/Support/TestContentGeneration.swift @@ -107,14 +107,16 @@ func makeTestContentRecordDecl(named name: TokenSyntax, in typeName: TypeSyntax? type: Int32, name: \(elfNoteName.type), accessor: @convention(c) (UnsafeMutableRawPointer) -> Bool, - flags: UInt64 + flags: UInt32, + reserved: UInt32 ) = ( Int32(MemoryLayout<\(elfNoteName.type)>.stride), - Int32(MemoryLayout.stride + MemoryLayout.stride), + Int32(MemoryLayout.stride + MemoryLayout.stride + MemoryLayout.stride), \(raw: kind.rawValue) as Int32, \(elfNoteName.expression) as \(elfNoteName.type), \(accessorName) as @convention(c) (UnsafeMutableRawPointer) -> Bool, - \(raw: flags) as UInt64 + \(raw: flags) as UInt32, + 0 as UInt32 ) """ } diff --git a/Sources/_TestingInternals/Discovery.cpp b/Sources/_TestingInternals/Discovery.cpp index ae4ea54ca..f55a07847 100644 --- a/Sources/_TestingInternals/Discovery.cpp +++ b/Sources/_TestingInternals/Discovery.cpp @@ -414,7 +414,8 @@ void swt_enumerateTestContent(void *context, SWTTestContentEnumerator body) { // Extract the content of this record now that we know it's ours. struct Content { bool (* accessor)(void *outValue); - uint64_t flags; + uint32_t flags; + uint32_t reserved; }; auto content = reinterpret_cast(record.getDescription()); if (!content) { diff --git a/Sources/_TestingInternals/include/Discovery.h b/Sources/_TestingInternals/include/Discovery.h index 06e059f46..4205158d9 100644 --- a/Sources/_TestingInternals/include/Discovery.h +++ b/Sources/_TestingInternals/include/Discovery.h @@ -54,7 +54,7 @@ typedef struct SWTTestContentRecord { /// /// The value of this property is dependent on the kind of test content this /// instance represents. - uint64_t flags; + uint32_t flags; } SWTTestContentRecord; /// The type of callback called by `swt_enumerateTestContent()`.