From f9b4c8d69f65c0bf523404b7a22190adbed3b6ca Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Fri, 28 Nov 2025 10:53:47 -0500 Subject: [PATCH 01/22] [WIP] Third-party testing library discovery --- Sources/Testing/ABI/EntryPoints/Library.swift | 199 ++++++++++++++++++ .../ABI/EntryPoints/SwiftPMEntryPoint.swift | 7 + 2 files changed, 206 insertions(+) create mode 100644 Sources/Testing/ABI/EntryPoints/Library.swift diff --git a/Sources/Testing/ABI/EntryPoints/Library.swift b/Sources/Testing/ABI/EntryPoints/Library.swift new file mode 100644 index 000000000..d6430ab17 --- /dev/null +++ b/Sources/Testing/ABI/EntryPoints/Library.swift @@ -0,0 +1,199 @@ +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2023 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 +// + +private import _TestDiscovery +private import _TestingInternals + +@_spi(Experimental) @_spi(ForToolsIntegrationOnly) +public struct Library: Sendable { + /* @c */ fileprivate struct Record { + typealias EntryPoint = @convention(c) ( + _ configurationJSON: UnsafeRawBufferPointer, + _ configurationJSONByteCount: Int, + _ reserved: UInt, + _ context: UnsafeMutableRawPointer, + _ recordJSONHandler: RecordJSONHandler, + _ completionHandler: CompletionHandler + ) -> Void + + typealias RecordJSONHandler = @convention(c) ( + _ recordJSON: UnsafeRawBufferPointer, + _ recordJSONByteCount: Int, + _ reserved: UInt, + _ context: UnsafeMutableRawPointer + ) -> Void + + typealias CompletionHandler = @convention(c) ( + _ exitCode: CInt, + _ reserved: UInt, + _ context: UnsafeMutableRawPointer + ) -> Void + + var name: UnsafePointer + var entryPoint: EntryPoint + var reserved: UInt + } + + private var _record: Record + + public var name: String { + String(validatingCString: _record.name) ?? "" + } + + public func callEntryPoint( + passing args: __CommandLineArguments_v0? = nil, + recordHandler: @escaping @Sendable ( + _ recordJSON: UnsafeRawBufferPointer + ) -> Void = { _ in } + ) async -> CInt { + let configurationJSON: [UInt8] + do { + let args = try args ?? parseCommandLineArguments(from: CommandLine.arguments) + configurationJSON = try JSON.withEncoding(of: args) { configurationJSON in + configurationJSON.withMemoryRebound(to: UInt8.self) { Array($0) } + } + } catch { + // TODO: more advanced error recovery? + return EINVAL + } + + return await withCheckedContinuation { continuation in + struct Context { + var continuation: CheckedContinuation + var recordHandler: @Sendable (UnsafeRawBufferPointer) -> Void + } + let context = Unmanaged.passRetained( + Context( + continuation: continuation, + recordHandler: recordHandler + ) as AnyObject + ).toOpaque() + configurationJSON.withUnsafeBytes { configurationJSON in + _record.entryPoint( + configurationJSON.baseAddress!, + configurationJSON.count, + 0, + context, + /* recordJSONHandler: */ { recordJSON, recordJSONByteCount, _, context in + guard let context = Unmanaged.fromOpaque(context).takeUnretainedValue() as? Context else { + return + } + let recordJSON = UnsafeRawBufferPointer(start: recordJSON, count: recordJSONByteCount) + context.recordHandler(recordJSON) + }, + /* completionHandler: */ { exitCode, _, context in + guard let context = Unmanaged.fromOpaque(context).takeRetainedValue() as? Context else { + return + } + context.continuation.resume(returning: exitCode) + } + ) + } + } + } +} + +// MARK: - Discovery + +@_spi(Experimental) @_spi(ForToolsIntegrationOnly) +extension Library.Record: DiscoverableAsTestContent { + static var testContentKind: TestContentKind { + "main" + } + + typealias TestContentAccessorHint = UnsafePointer +} + +@_spi(Experimental) @_spi(ForToolsIntegrationOnly) +extension Library { + public init?(named name: String) { + let result = name.withCString { name in + TestContentRecord.allTestContentRecords().lazy + .compactMap { $0.load(withHint: name) } + .map(Self.init(_record:)) + .first + } + if let result { + self = result + } else { + return nil + } + } + + public static var all: some Sequence { + TestContentRecord.allTestContentRecords().lazy + .compactMap { $0.load() } + .map(Self.init(_record:)) + } +} + +// MARK: - Our very own entry point + +private let testingLibraryDiscoverableEntryPoint: Library.Record.EntryPoint = { configurationJSON, configurationJSONByteCount, _, context, recordJSONHandler, completionHandler in + do { + let configurationJSON = UnsafeRawBufferPointer(start: configurationJSON, count: configurationJSONByteCount) + let args = try JSON.decode(__CommandLineArguments_v0.self, from: configurationJSON) + let eventHandler = try eventHandlerForStrecamingEvents(withVersionNumber: args.eventStreamVersionNumber, encodeAsJSONLines: false) { recordJSON in + recordJSONHandler(recordJSON.baseAddress!, recordJSON.count, 0, context) + } + + Task.detached { + let exitCode = await Testing.entryPoint(passing: args, eventHandler: eventHandler) + completionHandler(exitCode, 0, context) + } + } catch { + // TODO: more advanced error recovery? + return completionHandler(EINVAL, 0, context) + } +} + +#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 +private let testingLibraryRecord: __TestContentRecord = ( + 0x6D61696E, /* 'main' */ + 0, + { outValue, type, hint, _ in +#if !hasFeature(Embedded) + guard type.load(as: Any.Type.self) == Library.Record.self else { + return false + } +#endif + let hint = hint.map { hint.load(as: UnsafePointer.self) } + if let hint { + guard let hint = String(validatingCString: hint), + String(hint.filter(\.isLetter)).lowercased() == "swifttesting" else { + return false + } + } + let name: StaticString = "Swift Testing" + name.utf8Start.withMemoryRebound(to: CChar.self, capacity: name.utf8CodeUnitCount + 1) { name in + _ = outValue.initializeMemory( + as: Library.Record.self, + to: .init( + name: name, + entryPoint: testingLibraryDiscoverableEntryPoint, + reserved: 0 + ) + ) + } + return true + }, + 0, + 0 +) +} diff --git a/Sources/Testing/ABI/EntryPoints/SwiftPMEntryPoint.swift b/Sources/Testing/ABI/EntryPoints/SwiftPMEntryPoint.swift index 3c72e9f20..42970bc4d 100644 --- a/Sources/Testing/ABI/EntryPoints/SwiftPMEntryPoint.swift +++ b/Sources/Testing/ABI/EntryPoints/SwiftPMEntryPoint.swift @@ -59,6 +59,13 @@ public func __swiftPMEntryPoint(passing args: __CommandLineArguments_v0? = nil) } #endif + // FIXME: this is probably the wrong layering for this check + if let args = try? args ?? parseCommandLineArguments(from: CommandLine.arguments), + let libraryName = args.testingLibrary, + let library = Library(named: libraryName) { + return await library.callEntryPoint(passing: args) + } + return await entryPoint(passing: args, eventHandler: nil) } From b51babd5cde38c85add7c423cdf423881b18eccd Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Fri, 28 Nov 2025 11:53:57 -0500 Subject: [PATCH 02/22] Add --testing-library capture --- Sources/Testing/ABI/EntryPoints/EntryPoint.swift | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Sources/Testing/ABI/EntryPoints/EntryPoint.swift b/Sources/Testing/ABI/EntryPoints/EntryPoint.swift index 6163e7bd1..85167da1a 100644 --- a/Sources/Testing/ABI/EntryPoints/EntryPoint.swift +++ b/Sources/Testing/ABI/EntryPoints/EntryPoint.swift @@ -331,6 +331,9 @@ public struct __CommandLineArguments_v0: Sendable { /// The value of the `--attachments-path` argument. public var attachmentsPath: String? + + /// The value of the `--testing-library` argument. + public var testingLibrary: String? } extension __CommandLineArguments_v0: Codable { @@ -353,6 +356,7 @@ extension __CommandLineArguments_v0: Codable { case repetitions case repeatUntil case attachmentsPath + case testingLibrary } } @@ -466,6 +470,11 @@ func parseCommandLineArguments(from args: [String]) throws -> __CommandLineArgum } #endif + // Testing library + if let testingLibrary = args.argumentValue(forLabel: "--testing-library") { + result.testingLibrary = testingLibrary + } + // XML output if let xunitOutputPath = args.argumentValue(forLabel: "--xunit-output") { result.xunitOutput = xunitOutputPath From 31339ef726de2eb3069aaa8b067059fb1e785297 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Fri, 28 Nov 2025 11:54:43 -0500 Subject: [PATCH 03/22] Fix typo --- Sources/Testing/ABI/EntryPoints/Library.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Sources/Testing/ABI/EntryPoints/Library.swift b/Sources/Testing/ABI/EntryPoints/Library.swift index d6430ab17..cbe03c996 100644 --- a/Sources/Testing/ABI/EntryPoints/Library.swift +++ b/Sources/Testing/ABI/EntryPoints/Library.swift @@ -196,4 +196,3 @@ private let testingLibraryRecord: __TestContentRecord = ( 0, 0 ) -} From 8a63580042d3e840f90706460773be1d44779a4a Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Fri, 28 Nov 2025 11:56:12 -0500 Subject: [PATCH 04/22] Fix various errors --- Sources/Testing/ABI/EntryPoints/Library.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/Testing/ABI/EntryPoints/Library.swift b/Sources/Testing/ABI/EntryPoints/Library.swift index cbe03c996..d95cce372 100644 --- a/Sources/Testing/ABI/EntryPoints/Library.swift +++ b/Sources/Testing/ABI/EntryPoints/Library.swift @@ -8,14 +8,14 @@ // See https://swift.org/CONTRIBUTORS.txt for Swift project authors // -private import _TestDiscovery +@_spi(Experimental) @_spi(ForToolsIntegrationOnly) private import _TestDiscovery private import _TestingInternals @_spi(Experimental) @_spi(ForToolsIntegrationOnly) public struct Library: Sendable { /* @c */ fileprivate struct Record { typealias EntryPoint = @convention(c) ( - _ configurationJSON: UnsafeRawBufferPointer, + _ configurationJSON: UnsafeMutableRawPointer, _ configurationJSONByteCount: Int, _ reserved: UInt, _ context: UnsafeMutableRawPointer, @@ -24,7 +24,7 @@ public struct Library: Sendable { ) -> Void typealias RecordJSONHandler = @convention(c) ( - _ recordJSON: UnsafeRawBufferPointer, + _ recordJSON: UnsafeMutableRawPointer, _ recordJSONByteCount: Int, _ reserved: UInt, _ context: UnsafeMutableRawPointer @@ -36,7 +36,7 @@ public struct Library: Sendable { _ context: UnsafeMutableRawPointer ) -> Void - var name: UnsafePointer + nonisolated(unsafe) var name: UnsafePointer var entryPoint: EntryPoint var reserved: UInt } From 3c7b61e5507e250c3342e9fd1210fab350312ba0 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Fri, 28 Nov 2025 11:56:32 -0500 Subject: [PATCH 05/22] Fix typo --- Sources/Testing/ABI/EntryPoints/Library.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Testing/ABI/EntryPoints/Library.swift b/Sources/Testing/ABI/EntryPoints/Library.swift index d95cce372..4af1e5ad7 100644 --- a/Sources/Testing/ABI/EntryPoints/Library.swift +++ b/Sources/Testing/ABI/EntryPoints/Library.swift @@ -173,7 +173,7 @@ private let testingLibraryRecord: __TestContentRecord = ( return false } #endif - let hint = hint.map { hint.load(as: UnsafePointer.self) } + let hint = hint.map { $0.load(as: UnsafePointer.self) } if let hint { guard let hint = String(validatingCString: hint), String(hint.filter(\.isLetter)).lowercased() == "swifttesting" else { From da0fd086c9e4e19a09472b70b4a1af6fa21d5a23 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Fri, 28 Nov 2025 12:01:16 -0500 Subject: [PATCH 06/22] Fix more typos --- Sources/Testing/ABI/EntryPoints/Library.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/Testing/ABI/EntryPoints/Library.swift b/Sources/Testing/ABI/EntryPoints/Library.swift index 4af1e5ad7..14fd7dda1 100644 --- a/Sources/Testing/ABI/EntryPoints/Library.swift +++ b/Sources/Testing/ABI/EntryPoints/Library.swift @@ -115,7 +115,7 @@ extension Library.Record: DiscoverableAsTestContent { extension Library { public init?(named name: String) { let result = name.withCString { name in - TestContentRecord.allTestContentRecords().lazy + Record.allTestContentRecords().lazy .compactMap { $0.load(withHint: name) } .map(Self.init(_record:)) .first @@ -128,7 +128,7 @@ extension Library { } public static var all: some Sequence { - TestContentRecord.allTestContentRecords().lazy + Record.allTestContentRecords().lazy .compactMap { $0.load() } .map(Self.init(_record:)) } From 1ee9faabe3c2b392c58e2ce9be969802406a3139 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Fri, 28 Nov 2025 12:02:25 -0500 Subject: [PATCH 07/22] Fix pointer mutability --- Sources/Testing/ABI/EntryPoints/Library.swift | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Sources/Testing/ABI/EntryPoints/Library.swift b/Sources/Testing/ABI/EntryPoints/Library.swift index 14fd7dda1..f731c3a12 100644 --- a/Sources/Testing/ABI/EntryPoints/Library.swift +++ b/Sources/Testing/ABI/EntryPoints/Library.swift @@ -15,25 +15,25 @@ private import _TestingInternals public struct Library: Sendable { /* @c */ fileprivate struct Record { typealias EntryPoint = @convention(c) ( - _ configurationJSON: UnsafeMutableRawPointer, + _ configurationJSON: UnsafeRawPointer, _ configurationJSONByteCount: Int, _ reserved: UInt, - _ context: UnsafeMutableRawPointer, + _ context: UnsafeRawPointer, _ recordJSONHandler: RecordJSONHandler, _ completionHandler: CompletionHandler ) -> Void typealias RecordJSONHandler = @convention(c) ( - _ recordJSON: UnsafeMutableRawPointer, + _ recordJSON: UnsafeRawPointer, _ recordJSONByteCount: Int, _ reserved: UInt, - _ context: UnsafeMutableRawPointer + _ context: UnsafeRawPointer ) -> Void typealias CompletionHandler = @convention(c) ( _ exitCode: CInt, _ reserved: UInt, - _ context: UnsafeMutableRawPointer + _ context: UnsafeRawPointer ) -> Void nonisolated(unsafe) var name: UnsafePointer From aa33a987aa0e58f2840dc5b81018b7c9c6deabe3 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Fri, 28 Nov 2025 12:03:04 -0500 Subject: [PATCH 08/22] Fix typo sigh --- Sources/Testing/ABI/EntryPoints/Library.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Testing/ABI/EntryPoints/Library.swift b/Sources/Testing/ABI/EntryPoints/Library.swift index f731c3a12..bdc683b93 100644 --- a/Sources/Testing/ABI/EntryPoints/Library.swift +++ b/Sources/Testing/ABI/EntryPoints/Library.swift @@ -140,7 +140,7 @@ private let testingLibraryDiscoverableEntryPoint: Library.Record.EntryPoint = { do { let configurationJSON = UnsafeRawBufferPointer(start: configurationJSON, count: configurationJSONByteCount) let args = try JSON.decode(__CommandLineArguments_v0.self, from: configurationJSON) - let eventHandler = try eventHandlerForStrecamingEvents(withVersionNumber: args.eventStreamVersionNumber, encodeAsJSONLines: false) { recordJSON in + let eventHandler = try eventHandlerForStreamingEvents(withVersionNumber: args.eventStreamVersionNumber, encodeAsJSONLines: false) { recordJSON in recordJSONHandler(recordJSON.baseAddress!, recordJSON.count, 0, context) } From 0e0a0a78a9334b7e3c7c580b77705a9d925090bc Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Fri, 28 Nov 2025 12:09:12 -0500 Subject: [PATCH 09/22] Nonisolated context argument --- Sources/Testing/ABI/EntryPoints/Library.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/Testing/ABI/EntryPoints/Library.swift b/Sources/Testing/ABI/EntryPoints/Library.swift index bdc683b93..070b5d248 100644 --- a/Sources/Testing/ABI/EntryPoints/Library.swift +++ b/Sources/Testing/ABI/EntryPoints/Library.swift @@ -138,6 +138,7 @@ extension Library { private let testingLibraryDiscoverableEntryPoint: Library.Record.EntryPoint = { configurationJSON, configurationJSONByteCount, _, context, recordJSONHandler, completionHandler in do { + nonisolated(unsafe) let context = context let configurationJSON = UnsafeRawBufferPointer(start: configurationJSON, count: configurationJSONByteCount) let args = try JSON.decode(__CommandLineArguments_v0.self, from: configurationJSON) let eventHandler = try eventHandlerForStreamingEvents(withVersionNumber: args.eventStreamVersionNumber, encodeAsJSONLines: false) { recordJSON in From d23ab76887b47346513dc007b19d72d341e5e2dc Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Fri, 28 Nov 2025 12:14:22 -0500 Subject: [PATCH 10/22] Disable accessor body to see why this one is failing (others compile just fine) --- Sources/Testing/ABI/EntryPoints/Library.swift | 49 ++++++++++--------- 1 file changed, 25 insertions(+), 24 deletions(-) diff --git a/Sources/Testing/ABI/EntryPoints/Library.swift b/Sources/Testing/ABI/EntryPoints/Library.swift index 070b5d248..6ea5b720e 100644 --- a/Sources/Testing/ABI/EntryPoints/Library.swift +++ b/Sources/Testing/ABI/EntryPoints/Library.swift @@ -169,30 +169,31 @@ private let testingLibraryRecord: __TestContentRecord = ( 0x6D61696E, /* 'main' */ 0, { outValue, type, hint, _ in -#if !hasFeature(Embedded) - guard type.load(as: Any.Type.self) == Library.Record.self else { - return false - } -#endif - let hint = hint.map { $0.load(as: UnsafePointer.self) } - if let hint { - guard let hint = String(validatingCString: hint), - String(hint.filter(\.isLetter)).lowercased() == "swifttesting" else { - return false - } - } - let name: StaticString = "Swift Testing" - name.utf8Start.withMemoryRebound(to: CChar.self, capacity: name.utf8CodeUnitCount + 1) { name in - _ = outValue.initializeMemory( - as: Library.Record.self, - to: .init( - name: name, - entryPoint: testingLibraryDiscoverableEntryPoint, - reserved: 0 - ) - ) - } - return true + return false +// #if !hasFeature(Embedded) +// guard type.load(as: Any.Type.self) == Library.Record.self else { +// return false +// } +// #endif +// let hint = hint.map { $0.load(as: UnsafePointer.self) } +// if let hint { +// guard let hint = String(validatingCString: hint), +// String(hint.filter(\.isLetter)).lowercased() == "swifttesting" else { +// return false +// } +// } +// let name: StaticString = "Swift Testing" +// name.utf8Start.withMemoryRebound(to: CChar.self, capacity: name.utf8CodeUnitCount + 1) { name in +// _ = outValue.initializeMemory( +// as: Library.Record.self, +// to: .init( +// name: name, +// entryPoint: testingLibraryDiscoverableEntryPoint, +// reserved: 0 +// ) +// ) +// } +// return true }, 0, 0 From ff0833c48f437d0dc111f48f29fe476155b7bc9e Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Fri, 28 Nov 2025 12:18:23 -0500 Subject: [PATCH 11/22] Callbacks are all sendable by nature, make it explicit --- Sources/Testing/ABI/EntryPoints/Library.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/Testing/ABI/EntryPoints/Library.swift b/Sources/Testing/ABI/EntryPoints/Library.swift index 6ea5b720e..750031443 100644 --- a/Sources/Testing/ABI/EntryPoints/Library.swift +++ b/Sources/Testing/ABI/EntryPoints/Library.swift @@ -14,7 +14,7 @@ private import _TestingInternals @_spi(Experimental) @_spi(ForToolsIntegrationOnly) public struct Library: Sendable { /* @c */ fileprivate struct Record { - typealias EntryPoint = @convention(c) ( + typealias EntryPoint = @Sendable @convention(c) ( _ configurationJSON: UnsafeRawPointer, _ configurationJSONByteCount: Int, _ reserved: UInt, @@ -23,14 +23,14 @@ public struct Library: Sendable { _ completionHandler: CompletionHandler ) -> Void - typealias RecordJSONHandler = @convention(c) ( + typealias RecordJSONHandler = @Sendable @convention(c) ( _ recordJSON: UnsafeRawPointer, _ recordJSONByteCount: Int, _ reserved: UInt, _ context: UnsafeRawPointer ) -> Void - typealias CompletionHandler = @convention(c) ( + typealias CompletionHandler = @Sendable @convention(c) ( _ exitCode: CInt, _ reserved: UInt, _ context: UnsafeRawPointer From ace7a9fdd78d30e2e3a2c032359fc2851a4bcb04 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Fri, 28 Nov 2025 12:24:18 -0500 Subject: [PATCH 12/22] Use a function instead of a closure --- Sources/Testing/ABI/EntryPoints/Library.swift | 32 ++++++++++--------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/Sources/Testing/ABI/EntryPoints/Library.swift b/Sources/Testing/ABI/EntryPoints/Library.swift index 750031443..bb8c3a899 100644 --- a/Sources/Testing/ABI/EntryPoints/Library.swift +++ b/Sources/Testing/ABI/EntryPoints/Library.swift @@ -155,20 +155,7 @@ private let testingLibraryDiscoverableEntryPoint: Library.Record.EntryPoint = { } } -#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 -private let testingLibraryRecord: __TestContentRecord = ( - 0x6D61696E, /* 'main' */ - 0, - { outValue, type, hint, _ in +private func testingLibraryDiscoverableAccessor(_ outValue: UnsafeMutableRawPointer, _ type: UnsafeRawPointer, _ hint: UnsafeRawPointer?, _ reserved: UInt) -> CBool { return false // #if !hasFeature(Embedded) // guard type.load(as: Any.Type.self) == Library.Record.self else { @@ -194,7 +181,22 @@ private let testingLibraryRecord: __TestContentRecord = ( // ) // } // return true - }, +} + +#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 +private let testingLibraryRecord: __TestContentRecord = ( + 0x6D61696E, /* 'main' */ + 0, + testingLibraryDiscoverableAccessor, 0, 0 ) From 9ab77eea0b8c68b193d3cd9c10dae284f9a3b2f0 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Fri, 28 Nov 2025 12:24:39 -0500 Subject: [PATCH 13/22] Use @section and @used --- Sources/Testing/ABI/EntryPoints/Library.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/Testing/ABI/EntryPoints/Library.swift b/Sources/Testing/ABI/EntryPoints/Library.swift index bb8c3a899..40ab3eca2 100644 --- a/Sources/Testing/ABI/EntryPoints/Library.swift +++ b/Sources/Testing/ABI/EntryPoints/Library.swift @@ -184,15 +184,15 @@ private func testingLibraryDiscoverableAccessor(_ outValue: UnsafeMutableRawPoin } #if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) || os(visionOS) -@_section("__DATA_CONST,__swift5_tests") +@section("__DATA_CONST,__swift5_tests") #elseif os(Linux) || os(FreeBSD) || os(OpenBSD) || os(Android) || os(WASI) -@_section("swift5_tests") +@section("swift5_tests") #elseif os(Windows) -@_section(".sw5test$B") +@section(".sw5test$B") #else @__testing(warning: "Platform-specific implementation missing: test content section name unavailable") #endif -@_used +@used private let testingLibraryRecord: __TestContentRecord = ( 0x6D61696E, /* 'main' */ 0, From 7cfdbf532b3ef1c7ed0eca3ac054e4432473a28b Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Fri, 28 Nov 2025 12:31:51 -0500 Subject: [PATCH 14/22] Disable on 6.2 --- Sources/Testing/ABI/EntryPoints/Library.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Sources/Testing/ABI/EntryPoints/Library.swift b/Sources/Testing/ABI/EntryPoints/Library.swift index 40ab3eca2..8b63aaa25 100644 --- a/Sources/Testing/ABI/EntryPoints/Library.swift +++ b/Sources/Testing/ABI/EntryPoints/Library.swift @@ -183,6 +183,7 @@ private func testingLibraryDiscoverableAccessor(_ outValue: UnsafeMutableRawPoin // return true } +#if compiler(>=6.3) #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) @@ -190,7 +191,7 @@ private func testingLibraryDiscoverableAccessor(_ outValue: UnsafeMutableRawPoin #elseif os(Windows) @section(".sw5test$B") #else -@__testing(warning: "Platform-specific implementation missing: test content section name unavailable") +//@__testing(warning: "Platform-specific implementation missing: test content section name unavailable") #endif @used private let testingLibraryRecord: __TestContentRecord = ( @@ -200,3 +201,4 @@ private let testingLibraryRecord: __TestContentRecord = ( 0, 0 ) +#endif \ No newline at end of file From 9d8d27db02789da156045b110c9cb3aacea5343c Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Sat, 6 Dec 2025 19:04:19 -0500 Subject: [PATCH 15/22] Get it building on 6.2 --- .../Testing/ABI/EntryPoints/EntryPoint.swift | 5 + Sources/Testing/ABI/EntryPoints/Library.swift | 197 ++++++++++-------- .../ABI/EntryPoints/SwiftPMEntryPoint.swift | 7 - Sources/_TestingInternals/include/Library.h | 80 +++++++ 4 files changed, 199 insertions(+), 90 deletions(-) create mode 100644 Sources/_TestingInternals/include/Library.h diff --git a/Sources/Testing/ABI/EntryPoints/EntryPoint.swift b/Sources/Testing/ABI/EntryPoints/EntryPoint.swift index 85167da1a..e13cdae8f 100644 --- a/Sources/Testing/ABI/EntryPoints/EntryPoint.swift +++ b/Sources/Testing/ABI/EntryPoints/EntryPoint.swift @@ -37,6 +37,11 @@ func entryPoint(passing args: __CommandLineArguments_v0?, eventHandler: Event.Ha #endif let args = try args ?? parseCommandLineArguments(from: CommandLine.arguments) + + if let library = args.testingLibrary.flatMap(Library.init(named:)) { + return await library.callEntryPoint(passing: args) + } + // Configure the test runner. var configuration = try configurationForEntryPoint(from: args) diff --git a/Sources/Testing/ABI/EntryPoints/Library.swift b/Sources/Testing/ABI/EntryPoints/Library.swift index 8b63aaa25..1767f2f3f 100644 --- a/Sources/Testing/ABI/EntryPoints/Library.swift +++ b/Sources/Testing/ABI/EntryPoints/Library.swift @@ -11,42 +11,28 @@ @_spi(Experimental) @_spi(ForToolsIntegrationOnly) private import _TestDiscovery private import _TestingInternals -@_spi(Experimental) @_spi(ForToolsIntegrationOnly) +/// A type representing a testing library such as Swift Testing or XCTest. +@_spi(Experimental) public struct Library: Sendable { - /* @c */ fileprivate struct Record { - typealias EntryPoint = @Sendable @convention(c) ( - _ configurationJSON: UnsafeRawPointer, - _ configurationJSONByteCount: Int, - _ reserved: UInt, - _ context: UnsafeRawPointer, - _ recordJSONHandler: RecordJSONHandler, - _ completionHandler: CompletionHandler - ) -> Void - - typealias RecordJSONHandler = @Sendable @convention(c) ( - _ recordJSON: UnsafeRawPointer, - _ recordJSONByteCount: Int, - _ reserved: UInt, - _ context: UnsafeRawPointer - ) -> Void - - typealias CompletionHandler = @Sendable @convention(c) ( - _ exitCode: CInt, - _ reserved: UInt, - _ context: UnsafeRawPointer - ) -> Void - - nonisolated(unsafe) var name: UnsafePointer - var entryPoint: EntryPoint - var reserved: UInt - } + /// - Important: The in-memory layout of ``Library`` must _exactly_ match the + /// layout of this type. As such, it must not contain any other stored + /// properties. + private nonisolated(unsafe) var _library: SWTLibrary - private var _record: Record + fileprivate init(_ library: SWTLibrary) { + _library = library + } + /// The human-readable name of this library. + /// + /// For example, the value of this property for an instance of this type that + /// represents the Swift Testing library is `"Swift Testing"`. public var name: String { - String(validatingCString: _record.name) ?? "" + String(validatingCString: _library.name) ?? "" } + /// Call the entry point function of this library. + @_spi(ForToolsIntegrationOnly) public func callEntryPoint( passing args: __CommandLineArguments_v0? = nil, recordHandler: @escaping @Sendable ( @@ -61,7 +47,7 @@ public struct Library: Sendable { } } catch { // TODO: more advanced error recovery? - return EINVAL + return EXIT_FAILURE } return await withCheckedContinuation { continuation in @@ -76,20 +62,20 @@ public struct Library: Sendable { ) as AnyObject ).toOpaque() configurationJSON.withUnsafeBytes { configurationJSON in - _record.entryPoint( + _library.entryPoint( configurationJSON.baseAddress!, configurationJSON.count, 0, context, /* recordJSONHandler: */ { recordJSON, recordJSONByteCount, _, context in - guard let context = Unmanaged.fromOpaque(context).takeUnretainedValue() as? Context else { + guard let context = Unmanaged.fromOpaque(context!).takeUnretainedValue() as? Context else { return } let recordJSON = UnsafeRawBufferPointer(start: recordJSON, count: recordJSONByteCount) context.recordHandler(recordJSON) }, /* completionHandler: */ { exitCode, _, context in - guard let context = Unmanaged.fromOpaque(context).takeRetainedValue() as? Context else { + guard let context = Unmanaged.fromOpaque(context!).takeRetainedValue() as? Context else { return } context.continuation.resume(returning: exitCode) @@ -102,22 +88,33 @@ public struct Library: Sendable { // MARK: - Discovery -@_spi(Experimental) @_spi(ForToolsIntegrationOnly) -extension Library.Record: DiscoverableAsTestContent { - static var testContentKind: TestContentKind { +/// A helper protocol that prevents the conformance of ``Library`` to +/// ``DiscoverableAsTestContent`` from being emitted into the testing library's +/// Swift module or interface files. +private protocol _DiscoverableAsTestContent: DiscoverableAsTestContent {} + +extension Library: _DiscoverableAsTestContent { + fileprivate static var testContentKind: TestContentKind { "main" } - typealias TestContentAccessorHint = UnsafePointer + fileprivate typealias TestContentAccessorHint = UnsafePointer } -@_spi(Experimental) @_spi(ForToolsIntegrationOnly) +@_spi(Experimental) extension Library { + private static let _validateMemoryLayout: Void = { + assert(MemoryLayout.size == MemoryLayout.size, "Library.size (\(MemoryLayout.size)) != SWTLibrary.size (\(MemoryLayout.size)). Please file a bug report at https://github.com/swiftlang/swift-testing/issues/new") + assert(MemoryLayout.stride == MemoryLayout.stride, "Library.stride (\(MemoryLayout.stride)) != SWTLibrary.stride (\(MemoryLayout.stride)). Please file a bug report at https://github.com/swiftlang/swift-testing/issues/new") + assert(MemoryLayout.alignment == MemoryLayout.alignment, "Library.alignment (\(MemoryLayout.alignment)) != SWTLibrary.alignment (\(MemoryLayout.alignment)). Please file a bug report at https://github.com/swiftlang/swift-testing/issues/new") + }() + + @_spi(ForToolsIntegrationOnly) public init?(named name: String) { + Self._validateMemoryLayout let result = name.withCString { name in - Record.allTestContentRecords().lazy + Library.allTestContentRecords().lazy .compactMap { $0.load(withHint: name) } - .map(Self.init(_record:)) .first } if let result { @@ -127,60 +124,83 @@ extension Library { } } + @_spi(ForToolsIntegrationOnly) public static var all: some Sequence { - Record.allTestContentRecords().lazy - .compactMap { $0.load() } - .map(Self.init(_record:)) + Self._validateMemoryLayout + return Library.allTestContentRecords().lazy.compactMap { $0.load() } } } // MARK: - Our very own entry point -private let testingLibraryDiscoverableEntryPoint: Library.Record.EntryPoint = { configurationJSON, configurationJSONByteCount, _, context, recordJSONHandler, completionHandler in +private let _discoverableEntryPoint: SWTLibraryEntryPoint = { configurationJSON, configurationJSONByteCount, _, context, recordJSONHandler, completionHandler in + // Capture appropriate state from the arguments to forward into the canonical + // entry point function. + let contextBitPattern = UInt(bitPattern: context) + let configurationJSON = UnsafeRawBufferPointer(start: configurationJSON, count: configurationJSONByteCount) + var args: __CommandLineArguments_v0 + let eventHandler: Event.Handler do { - nonisolated(unsafe) let context = context - let configurationJSON = UnsafeRawBufferPointer(start: configurationJSON, count: configurationJSONByteCount) - let args = try JSON.decode(__CommandLineArguments_v0.self, from: configurationJSON) - let eventHandler = try eventHandlerForStreamingEvents(withVersionNumber: args.eventStreamVersionNumber, encodeAsJSONLines: false) { recordJSON in - recordJSONHandler(recordJSON.baseAddress!, recordJSON.count, 0, context) - } - - Task.detached { - let exitCode = await Testing.entryPoint(passing: args, eventHandler: eventHandler) - completionHandler(exitCode, 0, context) + args = try JSON.decode(__CommandLineArguments_v0.self, from: configurationJSON) + eventHandler = try eventHandlerForStreamingEvents(withVersionNumber: args.eventStreamVersionNumber, encodeAsJSONLines: false) { recordJSON in + let context = UnsafeRawPointer(bitPattern: contextBitPattern)! + recordJSONHandler(recordJSON.baseAddress!, recordJSON.count, 0, context) } } catch { // TODO: more advanced error recovery? - return completionHandler(EINVAL, 0, context) + return completionHandler(EXIT_FAILURE, 0, context) } + + // Avoid infinite recursion. (Other libraries don't need to clear this field.) + args.testingLibrary = nil + +#if !SWT_NO_UNSTRUCTURED_TASKS + Task.detached { [args] in + let context = UnsafeRawPointer(bitPattern: contextBitPattern)! + let exitCode = await Testing.entryPoint(passing: args, eventHandler: eventHandler) + completionHandler(exitCode, 0, context) + } +#else + let exitCode = Task.runInline { [args] in + await Testing.entryPoint(passing: args, eventHandler: eventHandler) + } + completionHandler(exitCode, 0, context) +#endif } -private func testingLibraryDiscoverableAccessor(_ outValue: UnsafeMutableRawPointer, _ type: UnsafeRawPointer, _ hint: UnsafeRawPointer?, _ reserved: UInt) -> CBool { +private func _discoverableAccessor(_ outValue: UnsafeMutableRawPointer, _ type: UnsafeRawPointer, _ hint: UnsafeRawPointer?, _ reserved: UInt) -> CBool { +#if !hasFeature(Embedded) + // Make sure that the caller supplied the right Swift type. If a testing + // library is implemented in a language other than Swift, they can either: + // ignore this argument; or ask the Swift runtime for the type metadata + // pointer and compare it against the value `type.pointee` (`*type` in C). + guard type.load(as: Any.Type.self) == Library.self else { return false -// #if !hasFeature(Embedded) -// guard type.load(as: Any.Type.self) == Library.Record.self else { -// return false -// } -// #endif -// let hint = hint.map { $0.load(as: UnsafePointer.self) } -// if let hint { -// guard let hint = String(validatingCString: hint), -// String(hint.filter(\.isLetter)).lowercased() == "swifttesting" else { -// return false -// } -// } -// let name: StaticString = "Swift Testing" -// name.utf8Start.withMemoryRebound(to: CChar.self, capacity: name.utf8CodeUnitCount + 1) { name in -// _ = outValue.initializeMemory( -// as: Library.Record.self, -// to: .init( -// name: name, -// entryPoint: testingLibraryDiscoverableEntryPoint, -// reserved: 0 -// ) -// ) -// } -// return true + } +#endif + + // Check if the name of the testing library the caller wants is equivalent to + // "Swift Testing", ignoring case and punctuation. (If the caller did not + // specify a library name, the caller wants records for all libraries.) + let hint = hint.map { $0.load(as: UnsafePointer.self) } + if let hint { + guard let hint = String(validatingCString: hint), + String(hint.filter(\.isLetter)).lowercased() == "swifttesting" else { + return false + } + } + + // Initialize the provided memory to the (ABI-stable) library structure. + _ = outValue.initializeMemory( + as: SWTLibrary.self, + to: .init( + name: swt_getSwiftTestingLibraryName(), + entryPoint: _discoverableEntryPoint, + reserved: (0, 0, 0, 0, 0, 0) + ) + ) + + return true } #if compiler(>=6.3) @@ -194,11 +214,22 @@ private func testingLibraryDiscoverableAccessor(_ outValue: UnsafeMutableRawPoin //@__testing(warning: "Platform-specific implementation missing: test content section name unavailable") #endif @used +#else +#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 +#endif private let testingLibraryRecord: __TestContentRecord = ( 0x6D61696E, /* 'main' */ 0, - testingLibraryDiscoverableAccessor, + _discoverableAccessor, 0, 0 ) -#endif \ No newline at end of file diff --git a/Sources/Testing/ABI/EntryPoints/SwiftPMEntryPoint.swift b/Sources/Testing/ABI/EntryPoints/SwiftPMEntryPoint.swift index 42970bc4d..3c72e9f20 100644 --- a/Sources/Testing/ABI/EntryPoints/SwiftPMEntryPoint.swift +++ b/Sources/Testing/ABI/EntryPoints/SwiftPMEntryPoint.swift @@ -59,13 +59,6 @@ public func __swiftPMEntryPoint(passing args: __CommandLineArguments_v0? = nil) } #endif - // FIXME: this is probably the wrong layering for this check - if let args = try? args ?? parseCommandLineArguments(from: CommandLine.arguments), - let libraryName = args.testingLibrary, - let library = Library(named: libraryName) { - return await library.callEntryPoint(passing: args) - } - return await entryPoint(passing: args, eventHandler: nil) } diff --git a/Sources/_TestingInternals/include/Library.h b/Sources/_TestingInternals/include/Library.h new file mode 100644 index 000000000..cf726e1f5 --- /dev/null +++ b/Sources/_TestingInternals/include/Library.h @@ -0,0 +1,80 @@ +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 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 +// + +#if !defined(SWT_LIBRARY_H) +#define SWT_LIBRARY_H + +#include "Defines.h" +#include "Includes.h" + +SWT_ASSUME_NONNULL_BEGIN + +/* + fileprivate typealias EntryPoint = @Sendable @convention(c) ( + _ configurationJSON: UnsafeRawPointer, + _ configurationJSONByteCount: Int, + _ reserved: UInt, + _ context: UnsafeRawPointer, + _ recordJSONHandler: EntryPointRecordJSONHandler, + _ completionHandler: EntryPointCompletionHandler + ) -> Void + + fileprivate typealias EntryPointRecordJSONHandler = @Sendable @convention(c) ( + _ recordJSON: UnsafeRawPointer, + _ recordJSONByteCount: Int, + _ reserved: UInt, + _ context: UnsafeRawPointer + ) -> Void + + fileprivate typealias EntryPointCompletionHandler = @Sendable @convention(c) ( + _ exitCode: CInt, + _ reserved: UInt, + _ context: UnsafeRawPointer + ) -> Void +*/ + +typedef void (* SWT_SENDABLE SWTLibraryEntryPointRecordJSONHandler)( + const void *recordJSON, + size_t recordJSONByteCount, + uintptr_t reserved, + const void *_Null_unspecified context +); + +typedef void (* SWT_SENDABLE SWTLibraryEntryPointCompletionHandler)( + int exitCode, + uintptr_t reserved, + const void *_Null_unspecified context +); + +typedef void (* SWT_SENDABLE SWTLibraryEntryPoint)( + const void *configurationJSON, + size_t configurationJSONByteCount, + uintptr_t reserved, + const void *_Null_unspecified context, + SWTLibraryEntryPointRecordJSONHandler SWT_SENDABLE recordJSONHandler, + SWTLibraryEntryPointCompletionHandler SWT_SENDABLE completionHandler +); + +/// A C type that provides the in-memory layout of the ``Library`` Swift type. +typedef struct SWTLibrary { + const char *name; + SWTLibraryEntryPoint SWT_SENDABLE entryPoint; + uintptr_t reserved[6]; +} SWTLibrary; + +/// Get the name of the testing library (i.e. `"Swift Testing"`) as a +/// statically-allocated C string. +static inline const char *swt_getSwiftTestingLibraryName(void) { + return "Swift Testing"; +} + +SWT_ASSUME_NONNULL_END + +#endif From 5e870ff9e2c0edb7f3caaebe49cae47514df1412 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Sun, 7 Dec 2025 07:54:40 -0500 Subject: [PATCH 16/22] Work around compiler crash --- Sources/Testing/ABI/EntryPoints/Library.swift | 4 --- Sources/_TestingInternals/include/Library.h | 32 +++---------------- 2 files changed, 4 insertions(+), 32 deletions(-) diff --git a/Sources/Testing/ABI/EntryPoints/Library.swift b/Sources/Testing/ABI/EntryPoints/Library.swift index 1767f2f3f..385714275 100644 --- a/Sources/Testing/ABI/EntryPoints/Library.swift +++ b/Sources/Testing/ABI/EntryPoints/Library.swift @@ -19,10 +19,6 @@ public struct Library: Sendable { /// properties. private nonisolated(unsafe) var _library: SWTLibrary - fileprivate init(_ library: SWTLibrary) { - _library = library - } - /// The human-readable name of this library. /// /// For example, the value of this property for an instance of this type that diff --git a/Sources/_TestingInternals/include/Library.h b/Sources/_TestingInternals/include/Library.h index cf726e1f5..8ef68db0d 100644 --- a/Sources/_TestingInternals/include/Library.h +++ b/Sources/_TestingInternals/include/Library.h @@ -16,44 +16,20 @@ SWT_ASSUME_NONNULL_BEGIN -/* - fileprivate typealias EntryPoint = @Sendable @convention(c) ( - _ configurationJSON: UnsafeRawPointer, - _ configurationJSONByteCount: Int, - _ reserved: UInt, - _ context: UnsafeRawPointer, - _ recordJSONHandler: EntryPointRecordJSONHandler, - _ completionHandler: EntryPointCompletionHandler - ) -> Void - - fileprivate typealias EntryPointRecordJSONHandler = @Sendable @convention(c) ( - _ recordJSON: UnsafeRawPointer, - _ recordJSONByteCount: Int, - _ reserved: UInt, - _ context: UnsafeRawPointer - ) -> Void - - fileprivate typealias EntryPointCompletionHandler = @Sendable @convention(c) ( - _ exitCode: CInt, - _ reserved: UInt, - _ context: UnsafeRawPointer - ) -> Void -*/ - -typedef void (* SWT_SENDABLE SWTLibraryEntryPointRecordJSONHandler)( +typedef void (* SWTLibraryEntryPointRecordJSONHandler)( const void *recordJSON, size_t recordJSONByteCount, uintptr_t reserved, const void *_Null_unspecified context ); -typedef void (* SWT_SENDABLE SWTLibraryEntryPointCompletionHandler)( +typedef void (* SWTLibraryEntryPointCompletionHandler)( int exitCode, uintptr_t reserved, const void *_Null_unspecified context ); -typedef void (* SWT_SENDABLE SWTLibraryEntryPoint)( +typedef void (* SWTLibraryEntryPoint)( const void *configurationJSON, size_t configurationJSONByteCount, uintptr_t reserved, @@ -65,7 +41,7 @@ typedef void (* SWT_SENDABLE SWTLibraryEntryPoint)( /// A C type that provides the in-memory layout of the ``Library`` Swift type. typedef struct SWTLibrary { const char *name; - SWTLibraryEntryPoint SWT_SENDABLE entryPoint; + SWTLibraryEntryPoint entryPoint; uintptr_t reserved[6]; } SWTLibrary; From 82781b9cbe1dad8025bc5805a854cd8d8ebd0c85 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Sun, 7 Dec 2025 08:05:28 -0500 Subject: [PATCH 17/22] Work around bug in new @section constraints --- Sources/Testing/ABI/EntryPoints/Library.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Testing/ABI/EntryPoints/Library.swift b/Sources/Testing/ABI/EntryPoints/Library.swift index 385714275..0642d82c4 100644 --- a/Sources/Testing/ABI/EntryPoints/Library.swift +++ b/Sources/Testing/ABI/EntryPoints/Library.swift @@ -225,7 +225,7 @@ private func _discoverableAccessor(_ outValue: UnsafeMutableRawPointer, _ type: private let testingLibraryRecord: __TestContentRecord = ( 0x6D61696E, /* 'main' */ 0, - _discoverableAccessor, + { _discoverableAccessor($0, $1, $2, $3) }, 0, 0 ) From e938e39539f40b9cd2c3f91369b1b1c016a32fbf Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Sun, 7 Dec 2025 11:43:56 -0500 Subject: [PATCH 18/22] Get it more working, add some initial tests --- Package.swift | 1 + .../Testing/ABI/ABI.Record+Streaming.swift | 37 ++-- Sources/Testing/ABI/ABI.Record.swift | 18 ++ Sources/Testing/ABI/ABI.swift | 6 + .../ABI/Encoded/ABI.EncodedLibrary.swift | 39 ++++ .../Testing/ABI/EntryPoints/EntryPoint.swift | 45 +++- Sources/Testing/ABI/EntryPoints/Library.swift | 159 +++++++++----- Sources/Testing/Events/Event.swift | 29 +++ .../Event.HumanReadableOutputRecorder.swift | 2 +- Sources/Testing/Running/Runner.swift | 1 + .../Support/Additions/StringAdditions.swift | 20 ++ Sources/_TestingInternals/include/Library.h | 9 +- Tests/TestingTests/LibraryTests.swift | 203 ++++++++++++++++++ Tests/TestingTests/SwiftPMTests.swift | 4 + 14 files changed, 494 insertions(+), 79 deletions(-) create mode 100644 Sources/Testing/ABI/Encoded/ABI.EncodedLibrary.swift create mode 100644 Sources/Testing/Support/Additions/StringAdditions.swift create mode 100644 Tests/TestingTests/LibraryTests.swift diff --git a/Package.swift b/Package.swift index a82241527..f770d603d 100644 --- a/Package.swift +++ b/Package.swift @@ -389,6 +389,7 @@ extension Array where Element == PackageDescription.SwiftSetting { // (via CMake). Enabling it is dependent on acceptance of the @section // proposal via Swift Evolution. .enableExperimentalFeature("SymbolLinkageMarkers"), + .enableExperimentalFeature("CompileTimeValuesPreview"), .enableUpcomingFeature("InferIsolatedConformances"), diff --git a/Sources/Testing/ABI/ABI.Record+Streaming.swift b/Sources/Testing/ABI/ABI.Record+Streaming.swift index 1aa1362ec..4f703dea8 100644 --- a/Sources/Testing/ABI/ABI.Record+Streaming.swift +++ b/Sources/Testing/ABI/ABI.Record+Streaming.swift @@ -24,7 +24,13 @@ extension ABI.Version { let humanReadableOutputRecorder = Event.HumanReadableOutputRecorder() return { [eventHandler = eventHandlerCopy] event, context in - if case .testDiscovered = event.kind, let test = context.test { + if case let .libraryDiscovered(library) = event.kind { + if let libraryRecord = ABI.Record(encoding: library) { + try? JSON.withEncoding(of: libraryRecord) { libraryJSON in + eventHandler(libraryJSON) + } + } + } else if case .testDiscovered = event.kind, let test = context.test { try? JSON.withEncoding(of: ABI.Record(encoding: test)) { testJSON in eventHandler(testJSON) } @@ -47,24 +53,25 @@ extension ABI.Xcode16 { forwardingTo eventHandler: @escaping @Sendable (_ recordJSON: UnsafeRawBufferPointer) -> Void ) -> Event.Handler { return { event, context in - if case .testDiscovered = event.kind { + switch event.kind { + case .libraryDiscovered, .testDiscovered: // Discard events of this kind rather than forwarding them to avoid a // crash in Xcode 16 (which does not expect any events to occur before // .runStarted.) return - } - - struct EventAndContextSnapshot: Codable { - var event: Event.Snapshot - var eventContext: Event.Context.Snapshot - } - let snapshot = EventAndContextSnapshot( - event: Event.Snapshot(snapshotting: event), - eventContext: Event.Context.Snapshot(snapshotting: context) - ) - try? JSON.withEncoding(of: snapshot) { eventAndContextJSON in - eventAndContextJSON.withUnsafeBytes { eventAndContextJSON in - eventHandler(eventAndContextJSON) + default: + struct EventAndContextSnapshot: Codable { + var event: Event.Snapshot + var eventContext: Event.Context.Snapshot + } + let snapshot = EventAndContextSnapshot( + event: Event.Snapshot(snapshotting: event), + eventContext: Event.Context.Snapshot(snapshotting: context) + ) + try? JSON.withEncoding(of: snapshot) { eventAndContextJSON in + eventAndContextJSON.withUnsafeBytes { eventAndContextJSON in + eventHandler(eventAndContextJSON) + } } } } diff --git a/Sources/Testing/ABI/ABI.Record.swift b/Sources/Testing/ABI/ABI.Record.swift index 40a8d4bc3..b1dbd1bb2 100644 --- a/Sources/Testing/ABI/ABI.Record.swift +++ b/Sources/Testing/ABI/ABI.Record.swift @@ -18,6 +18,11 @@ extension ABI { struct Record: Sendable where V: ABI.Version { /// An enumeration describing the various kinds of record. enum Kind: Sendable { + /// A testing library. + /// + /// - Warning: Testing libraries are not yet part of the JSON schema. + case library(EncodedLibrary) + /// A test record. case test(EncodedTest) @@ -28,6 +33,13 @@ extension ABI { /// The kind of record. var kind: Kind + init?(encoding library: borrowing Library) { + guard V.includesExperimentalFields else { + return nil + } + kind = .library(EncodedLibrary(encoding: library)) + } + init(encoding test: borrowing Test) { kind = .test(EncodedTest(encoding: test)) } @@ -58,6 +70,9 @@ extension ABI.Record: Codable { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(V.versionNumber, forKey: .version) switch kind { + case let .library(library): + try container.encode("_library", forKey: .kind) + try container.encode(library, forKey: .payload) case let .test(test): try container.encode("test", forKey: .kind) try container.encode(test, forKey: .payload) @@ -81,6 +96,9 @@ extension ABI.Record: Codable { } switch try container.decode(String.self, forKey: .kind) { + case "_library": + let library = try container.decode(ABI.EncodedLibrary.self, forKey: .payload) + kind = .library(library) case "test": let test = try container.decode(ABI.EncodedTest.self, forKey: .payload) kind = .test(test) diff --git a/Sources/Testing/ABI/ABI.swift b/Sources/Testing/ABI/ABI.swift index 7a33970fc..58545577e 100644 --- a/Sources/Testing/ABI/ABI.swift +++ b/Sources/Testing/ABI/ABI.swift @@ -59,6 +59,12 @@ extension ABI { /// - Returns: A type conforming to ``ABI/Version`` that represents the given /// ABI version, or `nil` if no such type exists. static func version(forVersionNumber versionNumber: VersionNumber = ABI.CurrentVersion.versionNumber) -> (any Version.Type)? { + // Special-case the experimental ABI version number (which is intentionally + // higher than any Swift release's version number). + if versionNumber == ExperimentalVersion.versionNumber { + return ExperimentalVersion.self + } + if versionNumber > ABI.HighestVersion.versionNumber { // If the caller requested an ABI version higher than the current Swift // compiler version and it's not an ABI version we've explicitly defined, diff --git a/Sources/Testing/ABI/Encoded/ABI.EncodedLibrary.swift b/Sources/Testing/ABI/Encoded/ABI.EncodedLibrary.swift new file mode 100644 index 000000000..4cdd275f6 --- /dev/null +++ b/Sources/Testing/ABI/Encoded/ABI.EncodedLibrary.swift @@ -0,0 +1,39 @@ +// +// 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 +// + +extension ABI { + /// A type implementing the JSON encoding of ``Library`` for the ABI entry + /// point and event stream output. + /// + /// The properties and members of this type are documented in ABI/JSON.md. + /// + /// This type is not part of the public interface of the testing library. It + /// assists in converting values to JSON; clients that consume this JSON are + /// expected to write their own decoders. + /// + /// - Warning: Testing libraries are not yet part of the JSON schema. + struct EncodedLibrary: Sendable where V: ABI.Version { + /// The human-readable name of the library. + var name: String + + /// The canonical form of the "hint" to run the testing library's tests at + /// runtime. + var canonicalHint: String + + init(encoding library: borrowing Library) { + name = library.name + canonicalHint = library.canonicalHint + } + } +} + +// MARK: - Codable + +extension ABI.EncodedLibrary: Codable {} diff --git a/Sources/Testing/ABI/EntryPoints/EntryPoint.swift b/Sources/Testing/ABI/EntryPoints/EntryPoint.swift index e13cdae8f..73c7ed1dc 100644 --- a/Sources/Testing/ABI/EntryPoints/EntryPoint.swift +++ b/Sources/Testing/ABI/EntryPoints/EntryPoint.swift @@ -30,17 +30,19 @@ func entryPoint(passing args: __CommandLineArguments_v0?, eventHandler: Event.Ha do { #if !SWT_NO_EXIT_TESTS - // If an exit test was specified, run it. `exitTest` returns `Never`. - if let exitTest = ExitTest.findInEnvironmentForEntryPoint() { - await exitTest() - } + // If an exit test was specified, run it. `exitTest` returns `Never`. + if let exitTest = ExitTest.findInEnvironmentForEntryPoint() { + await exitTest() + } #endif let args = try args ?? parseCommandLineArguments(from: CommandLine.arguments) - if let library = args.testingLibrary.flatMap(Library.init(named:)) { +#if !SWT_NO_RUNTIME_LIBRARY_DISCOVERY + if let library = args.testingLibrary.flatMap(Library.init(withHint:)) { return await library.callEntryPoint(passing: args) } +#endif // Configure the test runner. var configuration = try configurationForEntryPoint(from: args) @@ -96,7 +98,7 @@ func entryPoint(passing args: __CommandLineArguments_v0?, eventHandler: Event.Ha // The set of matching tests (or, in the case of `swift test list`, the set // of all tests.) - let tests: [Test] + var tests = [Test]() if args.listTests ?? false { tests = await Array(Test.all) @@ -117,6 +119,28 @@ func entryPoint(passing args: __CommandLineArguments_v0?, eventHandler: Event.Ha for test in tests { Event.post(.testDiscovered, for: (test, nil), configuration: configuration) } + } else if args.experimentalListLibraries ?? false { +#if !SWT_NO_RUNTIME_LIBRARY_DISCOVERY + let libraries = Library.all +#else + let libraries = [Library.swiftTesting] +#endif + + if args.verbosity > .min { + for library in libraries { + // Print the test ID to stdout (classical CLI behavior.) +#if SWT_TARGET_OS_APPLE && !SWT_NO_FILE_IO + try? FileHandle.stdout.write("\(library.name) (\(library.canonicalHint))\n") +#else + print("\(library.name) (\(library.canonicalHint))") +#endif + } + } + + // Post an event for every discovered library (as with tests above). + for library in libraries { + Event.post(.libraryDiscovered(library), for: (nil, nil), configuration: configuration) + } } else { // Run the tests. let runner = await Runner(configuration: configuration) @@ -212,6 +236,9 @@ public struct __CommandLineArguments_v0: Sendable { /// The value of the `--list-tests` argument. public var listTests: Bool? + /// The value of the `--experimental-list-libraries` argument. + public var experimentalListLibraries: Bool? + /// The value of the `--parallel` or `--no-parallel` argument. public var parallel: Bool? @@ -346,6 +373,7 @@ extension __CommandLineArguments_v0: Codable { // do not end up with leading underscores when encoded. enum CodingKeys: String, CodingKey { case listTests + case experimentalListLibraries case parallel case experimentalMaximumParallelizationWidth case symbolicateBacktraces @@ -476,7 +504,7 @@ func parseCommandLineArguments(from args: [String]) throws -> __CommandLineArgum #endif // Testing library - if let testingLibrary = args.argumentValue(forLabel: "--testing-library") { + if let testingLibrary = Environment.variable(named: "SWT_EXPERIMENTAL_LIBRARY") ?? args.argumentValue(forLabel: "--testing-library") { result.testingLibrary = testingLibrary } @@ -498,6 +526,9 @@ func parseCommandLineArguments(from args: [String]) throws -> __CommandLineArgum // makes invocation from e.g. Wasmtime a bit more intuitive/idiomatic. result.listTests = true } + if Environment.flag(named: "SWT_EXPERIMENTAL_LIST_LIBRARIES") == true || args.contains("--experimental-list-libraries") { + result.experimentalListLibraries = true + } // Parallelization (on by default) if args.contains("--no-parallel") { diff --git a/Sources/Testing/ABI/EntryPoints/Library.swift b/Sources/Testing/ABI/EntryPoints/Library.swift index 0642d82c4..af0c84272 100644 --- a/Sources/Testing/ABI/EntryPoints/Library.swift +++ b/Sources/Testing/ABI/EntryPoints/Library.swift @@ -9,24 +9,37 @@ // @_spi(Experimental) @_spi(ForToolsIntegrationOnly) private import _TestDiscovery -private import _TestingInternals +internal import _TestingInternals /// A type representing a testing library such as Swift Testing or XCTest. @_spi(Experimental) public struct Library: Sendable { + /// The underlying instance of ``SWTLibrary``. + /// /// - Important: The in-memory layout of ``Library`` must _exactly_ match the /// layout of this type. As such, it must not contain any other stored /// properties. - private nonisolated(unsafe) var _library: SWTLibrary + nonisolated(unsafe) var rawValue: SWTLibrary /// The human-readable name of this library. /// /// For example, the value of this property for an instance of this type that /// represents the Swift Testing library is `"Swift Testing"`. public var name: String { - String(validatingCString: _library.name) ?? "" + String(validatingCString: rawValue.name) ?? "" + } + + /// The canonical form of the "hint" to run the testing library's tests at + /// runtime. + /// + /// For example, the value of this property for an instance of this type that + /// represents the Swift Testing library is `"swift-testing"`. + @_spi(ForToolsIntegrationOnly) + public var canonicalHint: String { + String(validatingCString: rawValue.canonicalHint) ?? "" } +#if !SWT_NO_RUNTIME_LIBRARY_DISCOVERY /// Call the entry point function of this library. @_spi(ForToolsIntegrationOnly) public func callEntryPoint( @@ -35,9 +48,28 @@ public struct Library: Sendable { _ recordJSON: UnsafeRawBufferPointer ) -> Void = { _ in } ) async -> CInt { + var recordHandler = recordHandler + let configurationJSON: [UInt8] do { let args = try args ?? parseCommandLineArguments(from: CommandLine.arguments) + + // Event stream output + // Automatically write record JSON as JSON lines to the event stream if + // specified by the user. + if let eventStreamOutputPath = args.eventStreamOutputPath { + let file = try FileHandle(forWritingAtPath: eventStreamOutputPath) + recordHandler = { [oldRecordHandler = recordHandler] recordJSON in + JSON.asJSONLine(recordJSON) { recordJSON in + _ = try? file.withLock { + try file.write(recordJSON) + try file.write("\n") + } + } + oldRecordHandler(recordJSON) + } + } + configurationJSON = try JSON.withEncoding(of: args) { configurationJSON in configurationJSON.withMemoryRebound(to: UInt8.self) { Array($0) } } @@ -58,7 +90,7 @@ public struct Library: Sendable { ) as AnyObject ).toOpaque() configurationJSON.withUnsafeBytes { configurationJSON in - _library.entryPoint( + rawValue.entryPoint( configurationJSON.baseAddress!, configurationJSON.count, 0, @@ -80,8 +112,10 @@ public struct Library: Sendable { } } } +#endif } +#if !SWT_NO_RUNTIME_LIBRARY_DISCOVERY // MARK: - Discovery /// A helper protocol that prevents the conformance of ``Library`` to @@ -106,11 +140,11 @@ extension Library { }() @_spi(ForToolsIntegrationOnly) - public init?(named name: String) { + public init?(withHint hint: String) { Self._validateMemoryLayout - let result = name.withCString { name in + let result = hint.withCString { hint in Library.allTestContentRecords().lazy - .compactMap { $0.load(withHint: name) } + .compactMap { $0.load(withHint: hint) } .first } if let result { @@ -126,45 +160,75 @@ extension Library { return Library.allTestContentRecords().lazy.compactMap { $0.load() } } } +#endif -// MARK: - Our very own entry point - -private let _discoverableEntryPoint: SWTLibraryEntryPoint = { configurationJSON, configurationJSONByteCount, _, context, recordJSONHandler, completionHandler in - // Capture appropriate state from the arguments to forward into the canonical - // entry point function. - let contextBitPattern = UInt(bitPattern: context) - let configurationJSON = UnsafeRawBufferPointer(start: configurationJSON, count: configurationJSONByteCount) - var args: __CommandLineArguments_v0 - let eventHandler: Event.Handler - do { - args = try JSON.decode(__CommandLineArguments_v0.self, from: configurationJSON) - eventHandler = try eventHandlerForStreamingEvents(withVersionNumber: args.eventStreamVersionNumber, encodeAsJSONLines: false) { recordJSON in - let context = UnsafeRawPointer(bitPattern: contextBitPattern)! - recordJSONHandler(recordJSON.baseAddress!, recordJSON.count, 0, context) +// MARK: - Referring to Swift Testing directly + +extension Library { + /// TheABI entry point function for the testing library, thunked so that it + /// is compatible with the ``Library`` ABI. + private static let _libraryRecordEntryPoint: SWTLibraryEntryPoint = { configurationJSON, configurationJSONByteCount, _, context, recordJSONHandler, completionHandler in +#if !SWT_NO_RUNTIME_LIBRARY_DISCOVERY + // Capture appropriate state from the arguments to forward into the canonical + // entry point function. + let contextBitPattern = UInt(bitPattern: context) + let configurationJSON = UnsafeRawBufferPointer(start: configurationJSON, count: configurationJSONByteCount) + var args: __CommandLineArguments_v0 + let eventHandler: Event.Handler + do { + args = try JSON.decode(__CommandLineArguments_v0.self, from: configurationJSON) + eventHandler = try eventHandlerForStreamingEvents(withVersionNumber: args.eventStreamVersionNumber, encodeAsJSONLines: false) { recordJSON in + let context = UnsafeRawPointer(bitPattern: contextBitPattern)! + recordJSONHandler(recordJSON.baseAddress!, recordJSON.count, 0, context) + } + } catch { + // TODO: more advanced error recovery? + return completionHandler(EXIT_FAILURE, 0, context) } - } catch { - // TODO: more advanced error recovery? - return completionHandler(EXIT_FAILURE, 0, context) - } - // Avoid infinite recursion. (Other libraries don't need to clear this field.) - args.testingLibrary = nil + // Avoid infinite recursion and double JSON output. (Other libraries don't + // need to clear these fields.) + args.testingLibrary = nil + args.eventStreamOutputPath = nil + // Create an async context and run tests within it. #if !SWT_NO_UNSTRUCTURED_TASKS - Task.detached { [args] in - let context = UnsafeRawPointer(bitPattern: contextBitPattern)! - let exitCode = await Testing.entryPoint(passing: args, eventHandler: eventHandler) + Task.detached { [args] in + let context = UnsafeRawPointer(bitPattern: contextBitPattern)! + let exitCode = await Testing.entryPoint(passing: args, eventHandler: eventHandler) + completionHandler(exitCode, 0, context) + } +#else + let exitCode = Task.runInline { [args] in + await Testing.entryPoint(passing: args, eventHandler: eventHandler) + } completionHandler(exitCode, 0, context) - } +#endif #else - let exitCode = Task.runInline { [args] in - await Testing.entryPoint(passing: args, eventHandler: eventHandler) - } - completionHandler(exitCode, 0, context) + // There is no way to call this function without pointer shenanigans because + // we are not exposing `callEntryPoint()` nor are we emitting a record into + // the test content section. + swt_unreachable() #endif + } + + /// An instance of this type representing Swift Testing itself. + static let swiftTesting: Self = { + Self( + rawValue: .init( + name: StaticString("Swift Testing").constUTF8CString, + canonicalHint: StaticString("swift-testing").constUTF8CString, + entryPoint: _libraryRecordEntryPoint, + reserved: (0, 0, 0, 0, 0) + ) + ) + }() } -private func _discoverableAccessor(_ outValue: UnsafeMutableRawPointer, _ type: UnsafeRawPointer, _ hint: UnsafeRawPointer?, _ reserved: UInt) -> CBool { +#if !SWT_NO_RUNTIME_LIBRARY_DISCOVERY +// MARK: - Our very own library record + +private func _libraryRecordAccessor(_ outValue: UnsafeMutableRawPointer, _ type: UnsafeRawPointer, _ hint: UnsafeRawPointer?, _ reserved: UInt) -> CBool { #if !hasFeature(Embedded) // Make sure that the caller supplied the right Swift type. If a testing // library is implemented in a language other than Swift, they can either: @@ -189,17 +253,13 @@ private func _discoverableAccessor(_ outValue: UnsafeMutableRawPointer, _ type: // Initialize the provided memory to the (ABI-stable) library structure. _ = outValue.initializeMemory( as: SWTLibrary.self, - to: .init( - name: swt_getSwiftTestingLibraryName(), - entryPoint: _discoverableEntryPoint, - reserved: (0, 0, 0, 0, 0, 0) - ) + to: Library.swiftTesting.rawValue ) return true } -#if compiler(>=6.3) +#if compiler(>=6.3) && hasFeature(CompileTimeValuesPreview) #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) @@ -210,7 +270,7 @@ private func _discoverableAccessor(_ outValue: UnsafeMutableRawPointer, _ type: //@__testing(warning: "Platform-specific implementation missing: test content section name unavailable") #endif @used -#else +#elseif 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) @@ -222,10 +282,11 @@ private func _discoverableAccessor(_ outValue: UnsafeMutableRawPointer, _ type: #endif @_used #endif -private let testingLibraryRecord: __TestContentRecord = ( - 0x6D61696E, /* 'main' */ - 0, - { _discoverableAccessor($0, $1, $2, $3) }, - 0, - 0 +private let _libraryRecord: __TestContentRecord = ( + kind: 0x6D61696E, /* 'main' */ + reserved1: 0, + accessor: _libraryRecordAccessor, + context: 0, + reserved2: 0 ) +#endif diff --git a/Sources/Testing/Events/Event.swift b/Sources/Testing/Events/Event.swift index c6285f4f4..6e5d8ec29 100644 --- a/Sources/Testing/Events/Event.swift +++ b/Sources/Testing/Events/Event.swift @@ -13,6 +13,21 @@ public struct Event: Sendable { /// An enumeration describing the various kinds of event that can be observed. public enum Kind: Sendable { + /// A library was discovered during test run planning. + /// + /// - Parameters: + /// - library: A description of the discovered library. + /// + /// This event is recorded once, before events of kind ``testDiscovered``, + /// and indicates which testing library's tests will run. + /// + /// This event is also posted once per library when + /// `swift test --experimental-list-libraries` is called. In that case, + /// events are posted for all discovered libraries regardless of whether or + /// not they would run. + @_spi(Experimental) + indirect case libraryDiscovered(_ library: Library) + /// A test was discovered during test run planning. /// /// This event is recorded once per discovered test when ``Runner/run()`` is @@ -378,6 +393,18 @@ extension Event { extension Event.Kind { /// A serializable enumeration describing the various kinds of event that can be observed. public enum Snapshot: Sendable, Codable { + /// A library was discovered during test run planning. + /// + /// This event is recorded once, before events of kind ``testDiscovered``, + /// and indicates which testing library's tests will run. + /// + /// This event is also posted once per library when + /// `swift test --experimental-list-libraries` is called. In that case, + /// events are posted for all discovered libraries regardless of whether or + /// not they would run. + @_spi(Experimental) + case libraryDiscovered + /// A test was discovered during test run planning. /// /// This event is recorded once per discovered test when ``Runner/run()`` is @@ -523,6 +550,8 @@ extension Event.Kind { /// - kind: The original event kind to snapshot. public init(snapshotting kind: Event.Kind) { switch kind { + case .libraryDiscovered: + self = .libraryDiscovered case .testDiscovered: self = .testDiscovered case .runStarted: diff --git a/Sources/Testing/Events/Recorder/Event.HumanReadableOutputRecorder.swift b/Sources/Testing/Events/Recorder/Event.HumanReadableOutputRecorder.swift index 9f0ac21b1..152e7f8c2 100644 --- a/Sources/Testing/Events/Recorder/Event.HumanReadableOutputRecorder.swift +++ b/Sources/Testing/Events/Recorder/Event.HumanReadableOutputRecorder.swift @@ -349,7 +349,7 @@ extension Event.HumanReadableOutputRecorder { // Finally, produce any messages for the event. switch event.kind { - case .testDiscovered: + case .libraryDiscovered, .testDiscovered: // Suppress events of this kind from output as they are not generally // interesting in human-readable output. break diff --git a/Sources/Testing/Running/Runner.swift b/Sources/Testing/Running/Runner.swift index a6a76189e..03a6d8cec 100644 --- a/Sources/Testing/Running/Runner.swift +++ b/Sources/Testing/Running/Runner.swift @@ -475,6 +475,7 @@ extension Runner { await Configuration.withCurrent(runner.configuration) { // Post an event for every test in the test plan being run. These events // are turned into JSON objects if JSON output is enabled. + Event.post(.libraryDiscovered(.swiftTesting), for: (nil, nil), configuration: runner.configuration) for test in runner.plan.steps.lazy.map(\.test) { Event.post(.testDiscovered, for: (test, nil), configuration: runner.configuration) } diff --git a/Sources/Testing/Support/Additions/StringAdditions.swift b/Sources/Testing/Support/Additions/StringAdditions.swift new file mode 100644 index 000000000..ef7c59fe8 --- /dev/null +++ b/Sources/Testing/Support/Additions/StringAdditions.swift @@ -0,0 +1,20 @@ +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 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 +// + +extension StaticString { + /// This string as a compile-time constant C string. + /// + /// - Precondition: This instance of `StaticString` must have been constructed + /// from a string literal, not a Unicode scalar value. + var constUTF8CString: UnsafePointer { + precondition(hasPointerRepresentation, "Cannot construct a compile-time constant C string from a StaticString without pointer representation.") + return UnsafeRawPointer(utf8Start).bindMemory(to: CChar.self, capacity: utf8CodeUnitCount) + } +} diff --git a/Sources/_TestingInternals/include/Library.h b/Sources/_TestingInternals/include/Library.h index 8ef68db0d..93334f4f7 100644 --- a/Sources/_TestingInternals/include/Library.h +++ b/Sources/_TestingInternals/include/Library.h @@ -41,16 +41,11 @@ typedef void (* SWTLibraryEntryPoint)( /// A C type that provides the in-memory layout of the ``Library`` Swift type. typedef struct SWTLibrary { const char *name; + const char *canonicalHint; // TODO: better name SWTLibraryEntryPoint entryPoint; - uintptr_t reserved[6]; + uintptr_t reserved[5]; } SWTLibrary; -/// Get the name of the testing library (i.e. `"Swift Testing"`) as a -/// statically-allocated C string. -static inline const char *swt_getSwiftTestingLibraryName(void) { - return "Swift Testing"; -} - SWT_ASSUME_NONNULL_END #endif diff --git a/Tests/TestingTests/LibraryTests.swift b/Tests/TestingTests/LibraryTests.swift new file mode 100644 index 000000000..ebec2cf10 --- /dev/null +++ b/Tests/TestingTests/LibraryTests.swift @@ -0,0 +1,203 @@ +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2023 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 +// + +#if !SWT_NO_RUNTIME_LIBRARY_DISCOVERY +@testable @_spi(Experimental) @_spi(ForToolsIntegrationOnly) import Testing +private import _TestingInternals + +private import Foundation // for JSONSerialization + +struct `Library tests` { + @Test func `Find Swift Testing library`() throws { + let library = try #require(Library(withHint: "SwIfTtEsTiNg")) + #expect(library.name == "Swift Testing") + #expect(library.canonicalHint == "swift-testing") + } + + @Test func `Run mock library`() async throws { + try await confirmation("(Mock) issue recorded") { issueRecorded in + let library = try #require(Library(withHint: "mock")) + + var args = __CommandLineArguments_v0() + args.eventStreamVersionNumber = ABI.v0.versionNumber + let exitCode = await library.callEntryPoint(passing: args) { recordJSON in + do { + let recordJSON = Data(recordJSON) + let jsonObject = try JSONSerialization.jsonObject(with: recordJSON) + let record = try #require(jsonObject as? [String: Any]) + if let kind = record["kind"] as? String, let payload = record["payload"] as? [String: Any] { + if kind == "event", let eventKind = payload["kind"] as? String { + if eventKind == "issueRecorded" { + issueRecorded() + } + } + } + } catch { + Issue.record(error) + } + } + #expect(exitCode == EXIT_SUCCESS) + } + } +} + +// MARK: - Fixtures + +extension Library { + private static let _mockRecordEntryPoint: SWTLibraryEntryPoint = { configurationJSON, configurationJSONByteCount, _, context, recordJSONHandler, completionHandler in + let tests: [[String: Any]] = [ + [ + "kind": "function", + "name": "mock_test_1", + "sourceLocation": [ + "fileID": "__C/mock_file.pascal", + "filePath": "/tmp/mock_file.pascal", + "_filePath": "/tmp/mock_file.pascal", + "line": 1, + "column": 1, + ], + "id": "mock_test_1_id", + "isParameterized": false + ] + ] + + let events: [[String: Any]] = [ + [ + "kind": "runStarted", + ], + [ + "kind": "testStarted", + "testID": "mock_test_1_id" + ], + [ + "kind": "issueRecorded", + "testID": "mock_test_1_id", + "issue": [ + "isKnown": false, + "sourceLocation": [ + "fileID": "__C/mock_file.pascal", + "filePath": "/tmp/mock_file.pascal", + "_filePath": "/tmp/mock_file.pascal", + "line": 20, + "column": 1, + ], + ] + ], + [ + "kind": "testEnded", + "testID": "mock_test_1_id" + ], + [ + "kind": "runEnded", + ], + ] + + for var test in tests { + test = [ + "version": 0, + "kind": "test", + "payload": test + ] + let json = try! JSONSerialization.data(withJSONObject: test) + json.withUnsafeBytes { json in + recordJSONHandler(json.baseAddress!, json.count, 0, context) + } + } + let now1970 = Date().timeIntervalSince1970 + for var (i, event) in events.enumerated() { + event["instant"] = [ + "absolute": i, + "since1970": now1970 + Double(i), + ] + event = [ + "version": 0, + "kind": "event", + "payload": event + ] + let json = try! JSONSerialization.data(withJSONObject: event) + json.withUnsafeBytes { json in + recordJSONHandler(json.baseAddress!, json.count, 0, context) + } + } + + completionHandler(EXIT_SUCCESS, 0, context) + } + + static let mock: Self = { + Self( + rawValue: .init( + name: StaticString("Mock Testing Library").constUTF8CString, + canonicalHint: StaticString("mock").constUTF8CString, + entryPoint: _mockRecordEntryPoint, + reserved: (0, 0, 0, 0, 0) + ) + ) + }() +} + +#if compiler(>=6.3) && hasFeature(CompileTimeValuesPreview) +#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 +#elseif 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 +#endif +private let _mockLibraryRecord: __TestContentRecord = ( + kind: 0x6D61696E, /* 'main' */ + reserved1: 0, + accessor: _mockLibraryRecordAccessor, + context: 0, + reserved2: 0 +) + +private func _mockLibraryRecordAccessor(_ outValue: UnsafeMutableRawPointer, _ type: UnsafeRawPointer, _ hint: UnsafeRawPointer?, _ reserved: UInt) -> CBool { +#if !hasFeature(Embedded) + // Make sure that the caller supplied the right Swift type. If a testing + // library is implemented in a language other than Swift, they can either: + // ignore this argument; or ask the Swift runtime for the type metadata + // pointer and compare it against the value `type.pointee` (`*type` in C). + guard type.load(as: Any.Type.self) == Library.self else { + return false + } +#endif + + // Check if the name of the testing library the caller wants is equivalent to + // "Swift Testing", ignoring case and punctuation. (If the caller did not + // specify a library name, the caller wants records for all libraries.) + let hint = hint.map { $0.load(as: UnsafePointer.self) } + if let hint, 0 != strcasecmp(hint, "mock") { + return false + } + + // Initialize the provided memory to the (ABI-stable) library structure. + _ = outValue.initializeMemory( + as: SWTLibrary.self, + to: Library.mock.rawValue + ) + + return true +} +#endif diff --git a/Tests/TestingTests/SwiftPMTests.swift b/Tests/TestingTests/SwiftPMTests.swift index 2d7001e8f..43547a76b 100644 --- a/Tests/TestingTests/SwiftPMTests.swift +++ b/Tests/TestingTests/SwiftPMTests.swift @@ -363,6 +363,7 @@ struct SwiftPMTests { ("--event-stream-output-path", "--event-stream-version", ABI.v0.versionNumber), ("--experimental-event-stream-output", "--experimental-event-stream-version", ABI.v0.versionNumber), ("--experimental-event-stream-output", "--experimental-event-stream-version", ABI.v6_3.versionNumber), + ("--experimental-event-stream-output", "--experimental-event-stream-version", ABI.ExperimentalVersion.versionNumber), ]) func eventStreamOutput(outputArgumentName: String, versionArgumentName: String, version: VersionNumber) async throws { let version = try #require(ABI.version(forVersionNumber: version)) @@ -382,6 +383,9 @@ struct SwiftPMTests { let test = Test(.tags(.blue)) {} let eventContext = Event.Context(test: test, testCase: nil, configuration: nil) + if V.includesExperimentalFields { + configuration.handleEvent(Event(.libraryDiscovered(.swiftTesting), testID: test.id, testCaseID: nil), in: eventContext) + } configuration.handleEvent(Event(.testDiscovered, testID: test.id, testCaseID: nil), in: eventContext) configuration.handleEvent(Event(.runStarted, testID: nil, testCaseID: nil), in: eventContext) do { From aec7758a4370c8391566f30cb494aa2d3d09cefe Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Sun, 7 Dec 2025 11:46:27 -0500 Subject: [PATCH 19/22] Make it BitwiseCopyable --- Sources/Testing/ABI/EntryPoints/Library.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Sources/Testing/ABI/EntryPoints/Library.swift b/Sources/Testing/ABI/EntryPoints/Library.swift index af0c84272..846f59a90 100644 --- a/Sources/Testing/ABI/EntryPoints/Library.swift +++ b/Sources/Testing/ABI/EntryPoints/Library.swift @@ -13,7 +13,7 @@ internal import _TestingInternals /// A type representing a testing library such as Swift Testing or XCTest. @_spi(Experimental) -public struct Library: Sendable { +public struct Library: Sendable, BitwiseCopyable { /// The underlying instance of ``SWTLibrary``. /// /// - Important: The in-memory layout of ``Library`` must _exactly_ match the @@ -133,6 +133,8 @@ extension Library: _DiscoverableAsTestContent { @_spi(Experimental) extension Library { + /// Perform a one-time check that the in-memory layout of ``Library`` matches + /// that of ``SWTLibrary``. private static let _validateMemoryLayout: Void = { assert(MemoryLayout.size == MemoryLayout.size, "Library.size (\(MemoryLayout.size)) != SWTLibrary.size (\(MemoryLayout.size)). Please file a bug report at https://github.com/swiftlang/swift-testing/issues/new") assert(MemoryLayout.stride == MemoryLayout.stride, "Library.stride (\(MemoryLayout.stride)) != SWTLibrary.stride (\(MemoryLayout.stride)). Please file a bug report at https://github.com/swiftlang/swift-testing/issues/new") From 986d01e50763a9459a02d537653689fcc443f8e1 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Sun, 7 Dec 2025 13:08:09 -0500 Subject: [PATCH 20/22] Assume a more complex return type than just an int (but for now, just return an int anyway) --- .../Testing/ABI/EntryPoints/EntryPoint.swift | 7 +- Sources/Testing/ABI/EntryPoints/Library.swift | 129 ++++++++++++------ Sources/_TestingInternals/include/Library.h | 3 +- Tests/TestingTests/LibraryTests.swift | 5 +- 4 files changed, 97 insertions(+), 47 deletions(-) diff --git a/Sources/Testing/ABI/EntryPoints/EntryPoint.swift b/Sources/Testing/ABI/EntryPoints/EntryPoint.swift index 73c7ed1dc..ea72a0c43 100644 --- a/Sources/Testing/ABI/EntryPoints/EntryPoint.swift +++ b/Sources/Testing/ABI/EntryPoints/EntryPoint.swift @@ -39,7 +39,8 @@ func entryPoint(passing args: __CommandLineArguments_v0?, eventHandler: Event.Ha let args = try args ?? parseCommandLineArguments(from: CommandLine.arguments) #if !SWT_NO_RUNTIME_LIBRARY_DISCOVERY - if let library = args.testingLibrary.flatMap(Library.init(withHint:)) { + if args.experimentalListLibraries != true, + let library = args.testingLibrary.flatMap(Library.init(withHint:)) { return await library.callEntryPoint(passing: args) } #endif @@ -100,7 +101,7 @@ func entryPoint(passing args: __CommandLineArguments_v0?, eventHandler: Event.Ha // of all tests.) var tests = [Test]() - if args.listTests ?? false { + if args.listTests == true { tests = await Array(Test.all) if args.verbosity > .min { @@ -119,7 +120,7 @@ func entryPoint(passing args: __CommandLineArguments_v0?, eventHandler: Event.Ha for test in tests { Event.post(.testDiscovered, for: (test, nil), configuration: configuration) } - } else if args.experimentalListLibraries ?? false { + } else if args.experimentalListLibraries == true { #if !SWT_NO_RUNTIME_LIBRARY_DISCOVERY let libraries = Library.all #else diff --git a/Sources/Testing/ABI/EntryPoints/Library.swift b/Sources/Testing/ABI/EntryPoints/Library.swift index 846f59a90..4f12f4d54 100644 --- a/Sources/Testing/ABI/EntryPoints/Library.swift +++ b/Sources/Testing/ABI/EntryPoints/Library.swift @@ -13,7 +13,7 @@ internal import _TestingInternals /// A type representing a testing library such as Swift Testing or XCTest. @_spi(Experimental) -public struct Library: Sendable, BitwiseCopyable { +public struct Library: Sendable { /// The underlying instance of ``SWTLibrary``. /// /// - Important: The in-memory layout of ``Library`` must _exactly_ match the @@ -41,47 +41,69 @@ public struct Library: Sendable, BitwiseCopyable { #if !SWT_NO_RUNTIME_LIBRARY_DISCOVERY /// Call the entry point function of this library. + /// + /// - Parameters: + /// - args: The arguments to pass to the testing library as its + /// configuration JSON. + /// - recordHandler: A callback to invoke once per record. + /// + /// - Returns: A process exit code such as `EXIT_SUCCESS`. + /// + /// - Warning: The signature of this function is subject to change as + /// `__CommandLineArguments_v0` is not a stable interface. @_spi(ForToolsIntegrationOnly) public func callEntryPoint( passing args: __CommandLineArguments_v0? = nil, - recordHandler: @escaping @Sendable ( - _ recordJSON: UnsafeRawBufferPointer - ) -> Void = { _ in } + recordHandler: (@Sendable (_ recordJSON: UnsafeRawBufferPointer) -> Void)? = nil ) async -> CInt { + do { + return try await _callEntryPoint(passing: args, recordHandler: recordHandler) + } catch { + // TODO: more advanced error recovery? + return EXIT_FAILURE + } + } + + /// The implementation of ``callEntryPoint(passing:recordHandler:)``. + /// + /// - Parameters: + /// - args: The arguments to pass to the testing library as its + /// configuration JSON. + /// - recordHandler: A callback to invoke once per record. + /// + /// - Returns: A process exit code such as `EXIT_SUCCESS`. + private func _callEntryPoint( + passing args: __CommandLineArguments_v0?, + recordHandler: (@Sendable (_ recordJSON: UnsafeRawBufferPointer) -> Void)? + ) async throws -> CInt { var recordHandler = recordHandler - let configurationJSON: [UInt8] - do { - let args = try args ?? parseCommandLineArguments(from: CommandLine.arguments) + let args = try args ?? parseCommandLineArguments(from: CommandLine.arguments) - // Event stream output - // Automatically write record JSON as JSON lines to the event stream if - // specified by the user. - if let eventStreamOutputPath = args.eventStreamOutputPath { - let file = try FileHandle(forWritingAtPath: eventStreamOutputPath) - recordHandler = { [oldRecordHandler = recordHandler] recordJSON in - JSON.asJSONLine(recordJSON) { recordJSON in - _ = try? file.withLock { - try file.write(recordJSON) - try file.write("\n") - } + // Event stream output + // Automatically write record JSON as JSON lines to the event stream if + // specified by the user. + if let eventStreamOutputPath = args.eventStreamOutputPath { + let file = try FileHandle(forWritingAtPath: eventStreamOutputPath) + recordHandler = { [oldRecordHandler = recordHandler] recordJSON in + JSON.asJSONLine(recordJSON) { recordJSON in + _ = try? file.withLock { + try file.write(recordJSON) + try file.write("\n") } - oldRecordHandler(recordJSON) } + oldRecordHandler?(recordJSON) } + } - configurationJSON = try JSON.withEncoding(of: args) { configurationJSON in - configurationJSON.withMemoryRebound(to: UInt8.self) { Array($0) } - } - } catch { - // TODO: more advanced error recovery? - return EXIT_FAILURE + let configurationJSON = try JSON.withEncoding(of: args) { configurationJSON in + configurationJSON.withMemoryRebound(to: UInt8.self) { Array($0) } } - return await withCheckedContinuation { continuation in + let resultJSON: [UInt8] = await withCheckedContinuation { continuation in struct Context { - var continuation: CheckedContinuation - var recordHandler: @Sendable (UnsafeRawBufferPointer) -> Void + var continuation: CheckedContinuation<[UInt8], Never> + var recordHandler: (@Sendable (UnsafeRawBufferPointer) -> Void)? } let context = Unmanaged.passRetained( Context( @@ -100,17 +122,28 @@ public struct Library: Sendable, BitwiseCopyable { return } let recordJSON = UnsafeRawBufferPointer(start: recordJSON, count: recordJSONByteCount) - context.recordHandler(recordJSON) + context.recordHandler?(recordJSON) }, - /* completionHandler: */ { exitCode, _, context in + /* completionHandler: */ { resultJSON, resultJSONByteCount, _, context in guard let context = Unmanaged.fromOpaque(context!).takeRetainedValue() as? Context else { return } - context.continuation.resume(returning: exitCode) + // TODO: interpret more complex results than a process exit code + let resultJSON = [UInt8](UnsafeRawBufferPointer(start: resultJSON, count: resultJSONByteCount)) + context.continuation.resume(returning: resultJSON) } ) } } + + do { + return try resultJSON.withUnsafeBytes { resultJSON in + try JSON.decode(CInt.self, from: resultJSON) + } + } catch { + // TODO: more advanced error recovery? + return EXIT_FAILURE + } } #endif } @@ -141,6 +174,13 @@ extension Library { assert(MemoryLayout.alignment == MemoryLayout.alignment, "Library.alignment (\(MemoryLayout.alignment)) != SWTLibrary.alignment (\(MemoryLayout.alignment)). Please file a bug report at https://github.com/swiftlang/swift-testing/issues/new") }() + /// Create an instance of this type representing the testing library with the + /// given hint. + /// + /// - Parameters: + /// - hint: The hint to match against such as `"swift-testing"`. + /// + /// If no matching testing library is found, this initializer returns `nil`. @_spi(ForToolsIntegrationOnly) public init?(withHint hint: String) { Self._validateMemoryLayout @@ -156,6 +196,7 @@ extension Library { } } + /// All testing libraries known to the system including Swift Testing. @_spi(ForToolsIntegrationOnly) public static var all: some Sequence { Self._validateMemoryLayout @@ -167,7 +208,7 @@ extension Library { // MARK: - Referring to Swift Testing directly extension Library { - /// TheABI entry point function for the testing library, thunked so that it + /// The ABI entry point function for the testing library, thunked so that it /// is compatible with the ``Library`` ABI. private static let _libraryRecordEntryPoint: SWTLibraryEntryPoint = { configurationJSON, configurationJSONByteCount, _, context, recordJSONHandler, completionHandler in #if !SWT_NO_RUNTIME_LIBRARY_DISCOVERY @@ -184,8 +225,11 @@ extension Library { recordJSONHandler(recordJSON.baseAddress!, recordJSON.count, 0, context) } } catch { - // TODO: more advanced error recovery? - return completionHandler(EXIT_FAILURE, 0, context) + // TODO: interpret more complex results than a process exit code + var resultJSON = "\(EXIT_FAILURE)" + return resultJSON.withUTF8 { resultJSON in + completionHandler(resultJSON.baseAddress!, resultJSON.count, 0, context) + } } // Avoid infinite recursion and double JSON output. (Other libraries don't @@ -194,17 +238,18 @@ extension Library { args.eventStreamOutputPath = nil // Create an async context and run tests within it. -#if !SWT_NO_UNSTRUCTURED_TASKS - Task.detached { [args] in + let run = { [args] in let context = UnsafeRawPointer(bitPattern: contextBitPattern)! let exitCode = await Testing.entryPoint(passing: args, eventHandler: eventHandler) - completionHandler(exitCode, 0, context) + var resultJSON = "\(exitCode)" + resultJSON.withUTF8 { resultJSON in + completionHandler(resultJSON.baseAddress!, resultJSON.count, 0, context) + } } +#if !SWT_NO_UNSTRUCTURED_TASKS + Task.detached { await run() } #else - let exitCode = Task.runInline { [args] in - await Testing.entryPoint(passing: args, eventHandler: eventHandler) - } - completionHandler(exitCode, 0, context) + Task.runInline { await run() } #endif #else // There is no way to call this function without pointer shenanigans because @@ -215,7 +260,7 @@ extension Library { } /// An instance of this type representing Swift Testing itself. - static let swiftTesting: Self = { + public static let swiftTesting: Self = { Self( rawValue: .init( name: StaticString("Swift Testing").constUTF8CString, diff --git a/Sources/_TestingInternals/include/Library.h b/Sources/_TestingInternals/include/Library.h index 93334f4f7..14edf3350 100644 --- a/Sources/_TestingInternals/include/Library.h +++ b/Sources/_TestingInternals/include/Library.h @@ -24,7 +24,8 @@ typedef void (* SWTLibraryEntryPointRecordJSONHandler)( ); typedef void (* SWTLibraryEntryPointCompletionHandler)( - int exitCode, + const void *resultJSON, + size_t resultJSONByteCount, uintptr_t reserved, const void *_Null_unspecified context ); diff --git a/Tests/TestingTests/LibraryTests.swift b/Tests/TestingTests/LibraryTests.swift index ebec2cf10..ec591c1e3 100644 --- a/Tests/TestingTests/LibraryTests.swift +++ b/Tests/TestingTests/LibraryTests.swift @@ -127,7 +127,10 @@ extension Library { } } - completionHandler(EXIT_SUCCESS, 0, context) + var resultJSON = "0" + resultJSON.withUTF8 { resultJSON in + completionHandler(resultJSON.baseAddress!, resultJSON.count, 0, context) + } } static let mock: Self = { From 6f26087712af810e7770c0f1518e38c6570d7014 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Sun, 7 Dec 2025 13:12:29 -0500 Subject: [PATCH 21/22] Don't recurse when Swift Testing is the library we're using --- Sources/Testing/ABI/EntryPoints/EntryPoint.swift | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Sources/Testing/ABI/EntryPoints/EntryPoint.swift b/Sources/Testing/ABI/EntryPoints/EntryPoint.swift index ea72a0c43..1588c5fc7 100644 --- a/Sources/Testing/ABI/EntryPoints/EntryPoint.swift +++ b/Sources/Testing/ABI/EntryPoints/EntryPoint.swift @@ -39,8 +39,12 @@ func entryPoint(passing args: __CommandLineArguments_v0?, eventHandler: Event.Ha let args = try args ?? parseCommandLineArguments(from: CommandLine.arguments) #if !SWT_NO_RUNTIME_LIBRARY_DISCOVERY + // If the user requested a different testing library, run it instead of + // Swift Testing. (If they requested Swift Testing, we're already here so + // there's no real need to recurse). if args.experimentalListLibraries != true, - let library = args.testingLibrary.flatMap(Library.init(withHint:)) { + let library = args.testingLibrary.flatMap(Library.init(withHint:)), + library.canonicalHint != "swift-testing" { return await library.callEntryPoint(passing: args) } #endif From 806578b4430147f1911bf8785cb5df70dc1e19b1 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Sun, 7 Dec 2025 13:20:21 -0500 Subject: [PATCH 22/22] Don't exit with EXIT_NO_TESTS_FOUND when listing libraries --- Sources/Testing/ABI/EntryPoints/EntryPoint.swift | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/Sources/Testing/ABI/EntryPoints/EntryPoint.swift b/Sources/Testing/ABI/EntryPoints/EntryPoint.swift index 1588c5fc7..80e020270 100644 --- a/Sources/Testing/ABI/EntryPoints/EntryPoint.swift +++ b/Sources/Testing/ABI/EntryPoints/EntryPoint.swift @@ -104,6 +104,7 @@ func entryPoint(passing args: __CommandLineArguments_v0?, eventHandler: Event.Ha // The set of matching tests (or, in the case of `swift test list`, the set // of all tests.) var tests = [Test]() + var libraries = [Library]() if args.listTests == true { tests = await Array(Test.all) @@ -126,18 +127,19 @@ func entryPoint(passing args: __CommandLineArguments_v0?, eventHandler: Event.Ha } } else if args.experimentalListLibraries == true { #if !SWT_NO_RUNTIME_LIBRARY_DISCOVERY - let libraries = Library.all + libraries = Array(Library.all) #else - let libraries = [Library.swiftTesting] + libraries = [Library.swiftTesting] #endif if args.verbosity > .min { for library in libraries { // Print the test ID to stdout (classical CLI behavior.) + let libraryDescription = "\(library.name) (swift test --experimental-testing-library \(library.canonicalHint))" #if SWT_TARGET_OS_APPLE && !SWT_NO_FILE_IO - try? FileHandle.stdout.write("\(library.name) (\(library.canonicalHint))\n") + try? FileHandle.stdout.write("\(libraryDescription)\n") #else - print("\(library.name) (\(library.canonicalHint))") + print(libraryDescription) #endif } } @@ -156,7 +158,7 @@ func entryPoint(passing args: __CommandLineArguments_v0?, eventHandler: Event.Ha // If there were no matching tests, exit with a dedicated exit code so that // the caller (assumed to be Swift Package Manager) can implement special // handling. - if tests.isEmpty { + if tests.isEmpty && libraries.isEmpty { exitCode.withLock { exitCode in if exitCode == EXIT_SUCCESS { exitCode = EXIT_NO_TESTS_FOUND