From 43105629672da1743371e79eaa52ffb11b5d81fa Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Fri, 10 Jul 2026 18:03:37 -0400 Subject: [PATCH 01/14] [WIP] Basic implementation of the harness. This PR provides a basic implementation of the harness that spawns child processes and consumes events they produce. --- Sources/Harness/Harness.swift | 44 ++++++- .../Testing/ABI/EntryPoints/EntryPoint.swift | 46 +++++++ Sources/Testing/ABI/EntryPoints/Grommet.swift | 25 ++++ .../ABI/EntryPoints/HarnessEntryPoint.swift | 67 ++++++++++ .../ABI/EntryPoints/LocalProcessGrommet.swift | 114 ++++++++++++++++++ Sources/Testing/ExitTests/ExitTest.swift | 6 +- .../Additions/CommandLineAdditions.swift | 2 +- 7 files changed, 296 insertions(+), 8 deletions(-) create mode 100644 Sources/Testing/ABI/EntryPoints/Grommet.swift create mode 100644 Sources/Testing/ABI/EntryPoints/HarnessEntryPoint.swift create mode 100644 Sources/Testing/ABI/EntryPoints/LocalProcessGrommet.swift diff --git a/Sources/Harness/Harness.swift b/Sources/Harness/Harness.swift index a1d3e3dd4..23efaa4d1 100644 --- a/Sources/Harness/Harness.swift +++ b/Sources/Harness/Harness.swift @@ -8,16 +8,52 @@ // See https://swift.org/CONTRIBUTORS.txt for Swift project authors // -@_spi(ForToolsIntegrationOnly) private import Testing -private import ArgumentParser +@_spi(ForToolsIntegrationOnly) import Testing +import ArgumentParser +import Foundation /// The harness' main command (i.e. its entry point). @main struct Harness: Sendable, AsyncParsableCommand { - fileprivate static let configuration = CommandConfiguration( + static let configuration = CommandConfiguration( commandName: "swift-testing-harness" ) + @Option(name: "--test-product-path") + var testProductPaths: [String] = [] + + @Option(name: "--swiftpm-testing-helper-path") + var _swiftPMTestingHelperPath: String? + + var swiftPMTestingHelperPath: String { + get throws { + if let swiftPMTestingHelperPath = _swiftPMTestingHelperPath { + return swiftPMTestingHelperPath + } + + let executablePath = try CommandLine.executablePath + let executableURL = URL(fileURLWithPath: executablePath, isDirectory: false) + let swiftPMTestingHelperURL = executableURL + .deletingLastPathComponent() // - progname + .deletingLastPathComponent() // - "testing" + .appendingPathComponent("pm", isDirectory: true) + .appendingPathComponent("swiftpm-testing-helper", isDirectory: false) + return swiftPMTestingHelperURL.path + } + } + mutating func run() async throws { - throw ValidationError("This tool is currently unimplemented.") + let swiftPMTestingHelperPath = try swiftPMTestingHelperPath + let grommets: [any Grommet] = try testProductPaths.map { testProductPath in + let testProductBundle = Bundle(path: testProductPath) + guard let testProductExecutablePath = testProductBundle?.executablePath else { + throw CocoaError(.fileReadNoSuchFile) + } + + return LocalProcessGrommet( + testProductExecutablePath: testProductExecutablePath, + swiftPMTestingHelperPath: swiftPMTestingHelperPath + ) + } + try await harnessEntryPoint(running: grommets) as Never } } diff --git a/Sources/Testing/ABI/EntryPoints/EntryPoint.swift b/Sources/Testing/ABI/EntryPoints/EntryPoint.swift index 102776238..103438924 100644 --- a/Sources/Testing/ABI/EntryPoints/EntryPoint.swift +++ b/Sources/Testing/ABI/EntryPoints/EntryPoint.swift @@ -310,6 +310,14 @@ public struct __CommandLineArguments_v0: Sendable { /// set the value of this property. var eventStreamVersionNumber: VersionNumber? +#if os(Windows) + /// The value of the `--__harness-event-stream-handle` argument. + var harnessEventStreamHANDLE: HANDLE? +#else + /// The value of the `--__harness-event-stream-file-descriptor` argument. + var harnessEventStreamFileDescriptor: CInt? +#endif + /// The value of the `--event-stream-version` or `--experimental-event-stream-version` /// argument, representing the version of the event stream schema to use when /// writing events to ``eventStreamOutput``. @@ -493,6 +501,25 @@ func parseCommandLineArguments(from args: [String]) throws -> __CommandLineArgum result.eventStreamVersionNumber = eventStreamVersion } } + + // Harness event stream file descriptor (file handle on Windows) + var hasHarnessEventStream = false +#if os(Windows) + if let handle = args.argumentValue(forLabel: "--__harness-event-stream-handle").flatMap(UInt.init).flatMap(HANDLE.init(bitPattern:)) { + result.harnessEventStreamHANDLE = handle + hasHarnessEventStream = true + } +#else + if let fd = args.argumentValue(forLabel: "--__harness-event-stream-file-descriptor").flatMap(CInt.init) { + result.harnessEventStreamFileDescriptor = fd + hasHarnessEventStream = true + } +#endif + if hasHarnessEventStream { + // The presence of a harness event stream implies suppression of the normal + // stderr output. + result.verbosity = .min + } #endif // XML output @@ -643,6 +670,25 @@ public func configurationForEntryPoint(from args: __CommandLineArguments_v0) thr oldEventHandler(event, context) } } + + // Harness event stream output +#if os(Windows) + let harnessFile = try args.harnessEventStreamHANDLE.map { try Allocated(FileHandle(unsafeWindowsHANDLE: $0, options: [.writeAccess])) } +#else + let harnessFile = try args.harnessEventStreamFileDescriptor.map { try Allocated(FileHandle(unsafePOSIXFileDescriptor: $0, options: [.writeAccess])) } +#endif + if let harnessFile { + let eventHandler = try eventHandlerForStreamingEvents(withVersionNumber: ABI.HarnessVersion.versionNumber, encodeAsJSONLines: true) { json in + _ = try? harnessFile.value.withLock { + try harnessFile.value.write(json) + try harnessFile.value.write("\n") + } + } + configuration.eventHandler = { [oldEventHandler = configuration.eventHandler] event, context in + eventHandler(event, context) + oldEventHandler(event, context) + } + } #endif #endif diff --git a/Sources/Testing/ABI/EntryPoints/Grommet.swift b/Sources/Testing/ABI/EntryPoints/Grommet.swift new file mode 100644 index 000000000..bf96f4adb --- /dev/null +++ b/Sources/Testing/ABI/EntryPoints/Grommet.swift @@ -0,0 +1,25 @@ +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2026 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 +// + +// FIXME: need an actual name for this protocol +package protocol Grommet: Sendable { + func run(_ eventHandler: @escaping @Sendable (borrowing Event, borrowing Event.Context) -> Void) async throws +} + +extension Runner: Grommet { + package func run(_ eventHandler: @escaping @Sendable (borrowing Event, borrowing Event.Context) -> Void) async throws { + var selfCopy = self + selfCopy.configuration.eventHandler = { [oldEventHandler = selfCopy.configuration.eventHandler] event, context in + eventHandler(event, context) + oldEventHandler(event, context) + } + await selfCopy.run() + } +} diff --git a/Sources/Testing/ABI/EntryPoints/HarnessEntryPoint.swift b/Sources/Testing/ABI/EntryPoints/HarnessEntryPoint.swift new file mode 100644 index 000000000..1bd322019 --- /dev/null +++ b/Sources/Testing/ABI/EntryPoints/HarnessEntryPoint.swift @@ -0,0 +1,67 @@ +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2026 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 _TestingInternals + +extension ABI { + package typealias HarnessVersion = ExperimentalVersion +} + +package func harnessEntryPoint( + running grommets: [any Grommet] +) async throws -> CInt { + var exitCodes = [CInt]() + + for grommet in grommets { + let exitCode = Atomic(EXIT_SUCCESS) + + func open(_ grommet: some Grommet) async throws { + try await grommet.run { event, eventContext in + switch event.kind { + case .testDiscovered: + _ = exitCode.compareExchange(expected: EXIT_SUCCESS, desired: EXIT_NO_TESTS_FOUND, ordering: .sequentiallyConsistent) + case let .issueRecorded(issue): + if issue.isFailure { + exitCode.store(EXIT_FAILURE, ordering: .sequentiallyConsistent) + } + default: + break + } + print(event) + } + } + + do { + try await open(grommet) + } catch { + // TODO: handle errors at this layer in an interesting way + exitCode.store(EXIT_FAILURE, ordering: .sequentiallyConsistent) + } + + exitCodes.append(exitCode.load(ordering: .sequentiallyConsistent)) + } + + let noTestsFound = exitCodes.allSatisfy { $0 == EXIT_NO_TESTS_FOUND } + if noTestsFound { + return EXIT_NO_TESTS_FOUND + } + let succeeded = exitCodes.allSatisfy { $0 == EXIT_SUCCESS } + if succeeded { + return EXIT_SUCCESS + } + return EXIT_FAILURE +} + +package func harnessEntryPoint( + running grommets: [any Grommet] +) async throws -> Never { + let exitCode: CInt = try await harnessEntryPoint(running: grommets) + exit(exitCode) +} diff --git a/Sources/Testing/ABI/EntryPoints/LocalProcessGrommet.swift b/Sources/Testing/ABI/EntryPoints/LocalProcessGrommet.swift new file mode 100644 index 000000000..f5000d798 --- /dev/null +++ b/Sources/Testing/ABI/EntryPoints/LocalProcessGrommet.swift @@ -0,0 +1,114 @@ +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2026 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 +// + +package struct LocalProcessGrommet: Grommet { + var testProductExecutablePath: String + var swiftPMTestingHelperPath: String + + package init(testProductExecutablePath: String, swiftPMTestingHelperPath: String) { + self.testProductExecutablePath = testProductExecutablePath + self.swiftPMTestingHelperPath = swiftPMTestingHelperPath + } + + package func run(_ eventHandler: @escaping @Sendable (borrowing Event, borrowing Event.Context) -> Void) async throws { + try await withThrowingTaskGroup(of: Void.self) { taskGroup in + var backChannelReadEnd: FileHandle! + var backChannelWriteEnd: FileHandle! + try FileHandle.makePipe(readEnd: &backChannelReadEnd, writeEnd: &backChannelWriteEnd) + + var arguments = [ + "--test-bundle-path", testProductExecutablePath, + "--testing-library", "swift-testing", + ] + CommandLine.arguments +#if os(Windows) + backChannelWriteEnd.withUnsafeWindowsHANDLE { handle in + guard let handle else { + return + } + + arguments += [ + "--__harness-event-stream-handle", String(describing: UInt(bitPattern: handle)), + ] + } +#else + backChannelWriteEnd.withUnsafePOSIXFileDescriptor { fd in + guard let fd else { + return + } + + arguments += [ + "--__harness-event-stream-file-descriptor", String(describing: fd), + ] + } +#endif + + let processID = try withUnsafePointer(to: backChannelWriteEnd) { backChannelWriteEnd in + try spawnExecutable( + atPath: swiftPMTestingHelperPath, + arguments: arguments, + environment: Environment.get(), + standardOutput: .stdout, + standardError: .stderr, + additionalFileHandles: [backChannelWriteEnd] + ) + } + backChannelWriteEnd.close() + + taskGroup.addTask { + _ = try await wait(for: processID) + } + taskGroup.addTask { [backChannelReadEnd] in + try await _processRecords(fromBackChannel: backChannelReadEnd!, eventHandler: eventHandler) + } + try await taskGroup.waitForAll() + } + + } + + private func _processRecords( + fromBackChannel backChannel: borrowing FileHandle, + eventHandler: @Sendable (borrowing Event, borrowing Event.Context) -> Void + ) async throws { + var context = ABI.Context() + + var terminator: UInt8? + repeat { + let recordJSON: [UInt8] + (recordJSON, terminator) = try backChannel.read(until: \.isASCIINewline) + + // Allow other tasks to run after we may have blocked for some time on + // I/O with the child process. + await Task.yield() + + if recordJSON.isEmpty { + continue + } + let record = try recordJSON.withUnsafeBufferPointer { recordJSON in + try JSON.decode(ABI.Record.self, from: .init(recordJSON)) + } + switch record.kind { + case let .test(encodedTest): + _ = Test(decoding: encodedTest, in: &context) + case let .event(encodedEvent): + guard let event = Event(decoding: encodedEvent) else { + try? FileHandle.stderr.write("Failed to decode \(encodedEvent)") + return + } + let eventContext = Event.Context( + test: encodedEvent.testID.flatMap(context.test(identifiedBy:)), + testCase: nil, + iteration: encodedEvent._iteration, + configuration: nil + ) + eventHandler(event, eventContext) + } + } while terminator != nil + } +} diff --git a/Sources/Testing/ExitTests/ExitTest.swift b/Sources/Testing/ExitTests/ExitTest.swift index 6542d724e..91cf9cd41 100644 --- a/Sources/Testing/ExitTests/ExitTest.swift +++ b/Sources/Testing/ExitTests/ExitTest.swift @@ -705,7 +705,7 @@ extension ExitTest { /// - Returns: A string representation of `fileHandle` that can be converted /// back to a (new) file handle with `_makeFileHandle()`, or `nil` if the /// file handle could not be converted to a string. - private static func _makeEnvironmentVariable(for fileHandle: borrowing FileHandle) -> String? { + static func makeEnvironmentVariable(for fileHandle: borrowing FileHandle) -> String? { #if SWT_TARGET_OS_APPLE || os(Linux) || os(FreeBSD) || os(OpenBSD) return fileHandle.withUnsafePOSIXFileDescriptor { fd in fd.map(String.init(describing:)) @@ -934,10 +934,10 @@ extension ExitTest { // Let the child process know how to find the back channel and // captured values channel by setting a known environment variable to // the corresponding file descriptor (HANDLE on Windows) for each. - if let backChannelEnvironmentVariable = _makeEnvironmentVariable(for: backChannelWriteEnd) { + if let backChannelEnvironmentVariable = Self.makeEnvironmentVariable(for: backChannelWriteEnd) { childEnvironment["SWT_BACKCHANNEL"] = backChannelEnvironmentVariable } - if let capturedValuesEnvironmentVariable = _makeEnvironmentVariable(for: capturedValuesReadEnd) { + if let capturedValuesEnvironmentVariable = Self.makeEnvironmentVariable(for: capturedValuesReadEnd) { childEnvironment["SWT_CAPTURED_VALUES"] = capturedValuesEnvironmentVariable } diff --git a/Sources/Testing/Support/Additions/CommandLineAdditions.swift b/Sources/Testing/Support/Additions/CommandLineAdditions.swift index fa51c9550..2783b6955 100644 --- a/Sources/Testing/Support/Additions/CommandLineAdditions.swift +++ b/Sources/Testing/Support/Additions/CommandLineAdditions.swift @@ -29,7 +29,7 @@ enum CommandLine { #if !SWT_NO_EXIT_TESTS extension CommandLine { /// The path to the current process' executable. - static var executablePath: String { + package static var executablePath: String { get throws { #if SWT_TARGET_OS_APPLE var result: String? From 3381b96306467cd8210e2063d1c63401e9fb6be8 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Mon, 13 Jul 2026 14:34:18 -0400 Subject: [PATCH 02/14] Import module --- Sources/Testing/ABI/EntryPoints/HarnessEntryPoint.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Sources/Testing/ABI/EntryPoints/HarnessEntryPoint.swift b/Sources/Testing/ABI/EntryPoints/HarnessEntryPoint.swift index 1bd322019..0196c9007 100644 --- a/Sources/Testing/ABI/EntryPoints/HarnessEntryPoint.swift +++ b/Sources/Testing/ABI/EntryPoints/HarnessEntryPoint.swift @@ -10,6 +10,10 @@ private import _TestingInternals +#if canImport(Synchronization) +private import Synchronization +#endif + extension ABI { package typealias HarnessVersion = ExperimentalVersion } From d06bddb0bfb175173194a90cc0f57466763d4a10 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Wed, 15 Jul 2026 16:59:15 -0400 Subject: [PATCH 03/14] Throw on invalid handle/fd --- Sources/Testing/ABI/EntryPoints/EntryPoint.swift | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/Sources/Testing/ABI/EntryPoints/EntryPoint.swift b/Sources/Testing/ABI/EntryPoints/EntryPoint.swift index 103438924..6d5bb2f8c 100644 --- a/Sources/Testing/ABI/EntryPoints/EntryPoint.swift +++ b/Sources/Testing/ABI/EntryPoints/EntryPoint.swift @@ -8,7 +8,7 @@ // See https://swift.org/CONTRIBUTORS.txt for Swift project authors // -private import _TestingInternals +internal import _TestingInternals #if canImport(Synchronization) private import Synchronization @@ -312,7 +312,7 @@ public struct __CommandLineArguments_v0: Sendable { #if os(Windows) /// The value of the `--__harness-event-stream-handle` argument. - var harnessEventStreamHANDLE: HANDLE? + nonisolated(unsafe) var harnessEventStreamHANDLE: HANDLE? #else /// The value of the `--__harness-event-stream-file-descriptor` argument. var harnessEventStreamFileDescriptor: CInt? @@ -505,12 +505,19 @@ func parseCommandLineArguments(from args: [String]) throws -> __CommandLineArgum // Harness event stream file descriptor (file handle on Windows) var hasHarnessEventStream = false #if os(Windows) - if let handle = args.argumentValue(forLabel: "--__harness-event-stream-handle").flatMap(UInt.init).flatMap(HANDLE.init(bitPattern:)) { + if let handleString = args.argumentValue(forLabel: "--__harness-event-stream-handle") { + guard let handle = UInt(handleString).flatMap(HANDLE.init(bitPattern:)), + handle != INVALID_HANDLE_VALUE else { + throw _EntryPointError.invalidArgument("--__harness-event-stream-handle", value: handleString) + } result.harnessEventStreamHANDLE = handle hasHarnessEventStream = true } #else - if let fd = args.argumentValue(forLabel: "--__harness-event-stream-file-descriptor").flatMap(CInt.init) { + if let fdString = args.argumentValue(forLabel: "--__harness-event-stream-file-descriptor") { + guard let fd = CInt(fdString) else { + throw _EntryPointError.invalidArgument("--__harness-event-stream-file-descriptor", value: fdString) + } result.harnessEventStreamFileDescriptor = fd hasHarnessEventStream = true } From 72bb5d193ff84a992648cd49c4639e4288befb6c Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Wed, 15 Jul 2026 18:48:44 -0400 Subject: [PATCH 04/14] Update .iteration name --- Sources/Testing/ABI/EntryPoints/LocalProcessGrommet.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Testing/ABI/EntryPoints/LocalProcessGrommet.swift b/Sources/Testing/ABI/EntryPoints/LocalProcessGrommet.swift index f5000d798..2a5dd6bd1 100644 --- a/Sources/Testing/ABI/EntryPoints/LocalProcessGrommet.swift +++ b/Sources/Testing/ABI/EntryPoints/LocalProcessGrommet.swift @@ -104,7 +104,7 @@ package struct LocalProcessGrommet: Grommet { let eventContext = Event.Context( test: encodedEvent.testID.flatMap(context.test(identifiedBy:)), testCase: nil, - iteration: encodedEvent._iteration, + iteration: encodedEvent.iteration, configuration: nil ) eventHandler(event, eventContext) From b3c5b4bbf4290964d2d5ac7c9fe9a746ee40f9d6 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Thu, 16 Jul 2026 10:46:01 -0400 Subject: [PATCH 05/14] More configurations --- Sources/Harness/Harness.swift | 21 ++++++++-- .../ABI/EntryPoints/LocalProcessGrommet.swift | 38 ++++++++++++++----- 2 files changed, 47 insertions(+), 12 deletions(-) diff --git a/Sources/Harness/Harness.swift b/Sources/Harness/Harness.swift index 23efaa4d1..bc38d4438 100644 --- a/Sources/Harness/Harness.swift +++ b/Sources/Harness/Harness.swift @@ -21,6 +21,7 @@ import Foundation @Option(name: "--test-product-path") var testProductPaths: [String] = [] +#if SWT_TARGET_OS_APPLE @Option(name: "--swiftpm-testing-helper-path") var _swiftPMTestingHelperPath: String? @@ -40,20 +41,34 @@ import Foundation return swiftPMTestingHelperURL.path } } +#endif mutating func run() async throws { +#if SWT_TARGET_OS_APPLE let swiftPMTestingHelperPath = try swiftPMTestingHelperPath - let grommets: [any Grommet] = try testProductPaths.map { testProductPath in +#else +#endif + let grommets: [any Grommet] +#if !SWT_NO_PROCESS_SPAWNING + grommets = try testProductPaths.map { testProductPath in +#if SWT_TARGET_OS_APPLE let testProductBundle = Bundle(path: testProductPath) - guard let testProductExecutablePath = testProductBundle?.executablePath else { + guard let testProductBinaryPath = testProductBundle?.executablePath else { throw CocoaError(.fileReadNoSuchFile) } return LocalProcessGrommet( - testProductExecutablePath: testProductExecutablePath, + testProductBinaryPath: testProductBinaryPath, swiftPMTestingHelperPath: swiftPMTestingHelperPath ) +#else + return LocalProcessGrommet(testProductPath: testProductBinaryPath) +#endif } +#else + grommets = [] +#endif + try await harnessEntryPoint(running: grommets) as Never } } diff --git a/Sources/Testing/ABI/EntryPoints/LocalProcessGrommet.swift b/Sources/Testing/ABI/EntryPoints/LocalProcessGrommet.swift index 2a5dd6bd1..4d7394e33 100644 --- a/Sources/Testing/ABI/EntryPoints/LocalProcessGrommet.swift +++ b/Sources/Testing/ABI/EntryPoints/LocalProcessGrommet.swift @@ -8,14 +8,23 @@ // See https://swift.org/CONTRIBUTORS.txt for Swift project authors // +#if !SWT_NO_PROCESS_SPAWNING package struct LocalProcessGrommet: Grommet { - var testProductExecutablePath: String - var swiftPMTestingHelperPath: String +#if SWT_TARGET_OS_APPLE + private var _testProductBinaryPath: String + private var _swiftPMTestingHelperPath: String - package init(testProductExecutablePath: String, swiftPMTestingHelperPath: String) { - self.testProductExecutablePath = testProductExecutablePath - self.swiftPMTestingHelperPath = swiftPMTestingHelperPath + package init(testProductBinaryPath: String, swiftPMTestingHelperPath: String) { + _testProductBinaryPath = testProductBinaryPath + _swiftPMTestingHelperPath = swiftPMTestingHelperPath } +#else + private var _testProductPath: String + + package init(testProductPath: String) { + _testProductPath = testProductPath + } +#endif package func run(_ eventHandler: @escaping @Sendable (borrowing Event, borrowing Event.Context) -> Void) async throws { try await withThrowingTaskGroup(of: Void.self) { taskGroup in @@ -23,10 +32,14 @@ package struct LocalProcessGrommet: Grommet { var backChannelWriteEnd: FileHandle! try FileHandle.makePipe(readEnd: &backChannelReadEnd, writeEnd: &backChannelWriteEnd) - var arguments = [ - "--test-bundle-path", testProductExecutablePath, + var arguments = [String]() +#if SWT_TARGET_OS_APPLE + arguments += ["--test-bundle-path", _testProductBinaryPath] +#endif + arguments += [ "--testing-library", "swift-testing", - ] + CommandLine.arguments + ] + arguments += CommandLine.arguments #if os(Windows) backChannelWriteEnd.withUnsafeWindowsHANDLE { handle in guard let handle else { @@ -49,9 +62,15 @@ package struct LocalProcessGrommet: Grommet { } #endif +#if SWT_TARGET_OS_APPLE + let executablePath = _swiftPMTestingHelperPath +#else + let executablePath = _testProductBinaryPath +#endif + let processID = try withUnsafePointer(to: backChannelWriteEnd) { backChannelWriteEnd in try spawnExecutable( - atPath: swiftPMTestingHelperPath, + atPath: executablePath, arguments: arguments, environment: Environment.get(), standardOutput: .stdout, @@ -112,3 +131,4 @@ package struct LocalProcessGrommet: Grommet { } while terminator != nil } } +#endif From fd21216167114c4168b641678fd01ee78aad15c6 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Mon, 20 Jul 2026 10:42:23 -0400 Subject: [PATCH 06/14] Fix typo --- Sources/Testing/ABI/EntryPoints/LocalProcessGrommet.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Testing/ABI/EntryPoints/LocalProcessGrommet.swift b/Sources/Testing/ABI/EntryPoints/LocalProcessGrommet.swift index 4d7394e33..47918f869 100644 --- a/Sources/Testing/ABI/EntryPoints/LocalProcessGrommet.swift +++ b/Sources/Testing/ABI/EntryPoints/LocalProcessGrommet.swift @@ -65,7 +65,7 @@ package struct LocalProcessGrommet: Grommet { #if SWT_TARGET_OS_APPLE let executablePath = _swiftPMTestingHelperPath #else - let executablePath = _testProductBinaryPath + let executablePath = _testProductPath #endif let processID = try withUnsafePointer(to: backChannelWriteEnd) { backChannelWriteEnd in From 7a0089f9de45c309bac26cf9da6d3813f5ac22b2 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Mon, 20 Jul 2026 10:48:02 -0400 Subject: [PATCH 07/14] Fix typo --- Sources/Harness/Harness.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Harness/Harness.swift b/Sources/Harness/Harness.swift index bc38d4438..762710472 100644 --- a/Sources/Harness/Harness.swift +++ b/Sources/Harness/Harness.swift @@ -62,7 +62,7 @@ import Foundation swiftPMTestingHelperPath: swiftPMTestingHelperPath ) #else - return LocalProcessGrommet(testProductPath: testProductBinaryPath) + return LocalProcessGrommet(testProductPath: testProductPath) #endif } #else From 9886b87daab18155b6ca9a3a7813349dc466dbc6 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Mon, 20 Jul 2026 11:00:32 -0400 Subject: [PATCH 08/14] Split out 'read-from-a-file' so we can do that directly in the harness and replay event streams --- Sources/Harness/Harness.swift | 21 ++++-- .../Testing/ABI/EntryPoints/FileGrommet.swift | 68 +++++++++++++++++++ .../ABI/EntryPoints/LocalProcessGrommet.swift | 49 ++----------- .../Additions/CommandLineAdditions.swift | 2 +- 4 files changed, 92 insertions(+), 48 deletions(-) create mode 100644 Sources/Testing/ABI/EntryPoints/FileGrommet.swift diff --git a/Sources/Harness/Harness.swift b/Sources/Harness/Harness.swift index 762710472..f1a8248bf 100644 --- a/Sources/Harness/Harness.swift +++ b/Sources/Harness/Harness.swift @@ -18,6 +18,7 @@ import Foundation commandName: "swift-testing-harness" ) +#if !SWT_NO_PROCESS_SPAWNING @Option(name: "--test-product-path") var testProductPaths: [String] = [] @@ -42,15 +43,22 @@ import Foundation } } #endif +#endif + +#if !SWT_NO_FILE_IO + @Option(name: "--event-stream-input-path") + var eventStreamInputPaths: [String] = [] +#endif mutating func run() async throws { + var grommets = [any Grommet]() + +#if !SWT_NO_PROCESS_SPAWNING #if SWT_TARGET_OS_APPLE let swiftPMTestingHelperPath = try swiftPMTestingHelperPath -#else #endif - let grommets: [any Grommet] #if !SWT_NO_PROCESS_SPAWNING - grommets = try testProductPaths.map { testProductPath in + grommets += try testProductPaths.map { testProductPath in #if SWT_TARGET_OS_APPLE let testProductBundle = Bundle(path: testProductPath) guard let testProductBinaryPath = testProductBundle?.executablePath else { @@ -65,8 +73,11 @@ import Foundation return LocalProcessGrommet(testProductPath: testProductPath) #endif } -#else - grommets = [] +#endif +#endif + +#if !SWT_NO_FILE_IO + grommets += try eventStreamInputPaths.map(FileGrommet.init(readingFromFileAtPath:)) #endif try await harnessEntryPoint(running: grommets) as Never diff --git a/Sources/Testing/ABI/EntryPoints/FileGrommet.swift b/Sources/Testing/ABI/EntryPoints/FileGrommet.swift new file mode 100644 index 000000000..1a2e69813 --- /dev/null +++ b/Sources/Testing/ABI/EntryPoints/FileGrommet.swift @@ -0,0 +1,68 @@ +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2026 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_FILE_IO +package final class FileGrommet: Grommet { + private let _file: FileHandle + + init(readingFrom file: consuming FileHandle) { + _file = file + } + + package convenience init(readingFromFileAtPath filePath: String) throws { + let file = try FileHandle(forReadingAtPath: filePath) + self.init(readingFrom: file) + } + + package func run(_ eventHandler: @escaping @Sendable (borrowing Event, borrowing Event.Context) -> Void) async throws { + try await _processRecords(fromBackChannel: _file, eventHandler: eventHandler) + } + + private func _processRecords( + fromBackChannel backChannel: borrowing FileHandle, + eventHandler: @Sendable (borrowing Event, borrowing Event.Context) -> Void + ) async throws { + var context = ABI.Context() + + var terminator: UInt8? + repeat { + let recordJSON: [UInt8] + (recordJSON, terminator) = try backChannel.read(until: \.isASCIINewline) + + // Allow other tasks to run after we may have blocked for some time on + // I/O with the child process. + await Task.yield() + + if recordJSON.isEmpty { + continue + } + let record = try recordJSON.withUnsafeBufferPointer { recordJSON in + try JSON.decode(ABI.Record.self, from: .init(recordJSON)) + } + switch record.kind { + case let .test(encodedTest): + _ = Test(decoding: encodedTest, in: &context) + case let .event(encodedEvent): + guard let event = Event(decoding: encodedEvent) else { + try? FileHandle.stderr.write("Failed to decode \(encodedEvent)") + return + } + let eventContext = Event.Context( + test: encodedEvent.testID.flatMap(context.test(identifiedBy:)), + testCase: nil, + iteration: encodedEvent.iteration, + configuration: nil + ) + eventHandler(event, eventContext) + } + } while terminator != nil + } +} +#endif diff --git a/Sources/Testing/ABI/EntryPoints/LocalProcessGrommet.swift b/Sources/Testing/ABI/EntryPoints/LocalProcessGrommet.swift index 47918f869..7a20e5e68 100644 --- a/Sources/Testing/ABI/EntryPoints/LocalProcessGrommet.swift +++ b/Sources/Testing/ABI/EntryPoints/LocalProcessGrommet.swift @@ -80,55 +80,20 @@ package struct LocalProcessGrommet: Grommet { } backChannelWriteEnd.close() + // Wait for the child process to terminate. taskGroup.addTask { _ = try await wait(for: processID) } - taskGroup.addTask { [backChannelReadEnd] in - try await _processRecords(fromBackChannel: backChannelReadEnd!, eventHandler: eventHandler) + + // Read events back out from the back channel. + let fileGrommet = FileGrommet(readingFrom: backChannelReadEnd!) + taskGroup.addTask { + try await fileGrommet.run(eventHandler) } + try await taskGroup.waitForAll() } } - - private func _processRecords( - fromBackChannel backChannel: borrowing FileHandle, - eventHandler: @Sendable (borrowing Event, borrowing Event.Context) -> Void - ) async throws { - var context = ABI.Context() - - var terminator: UInt8? - repeat { - let recordJSON: [UInt8] - (recordJSON, terminator) = try backChannel.read(until: \.isASCIINewline) - - // Allow other tasks to run after we may have blocked for some time on - // I/O with the child process. - await Task.yield() - - if recordJSON.isEmpty { - continue - } - let record = try recordJSON.withUnsafeBufferPointer { recordJSON in - try JSON.decode(ABI.Record.self, from: .init(recordJSON)) - } - switch record.kind { - case let .test(encodedTest): - _ = Test(decoding: encodedTest, in: &context) - case let .event(encodedEvent): - guard let event = Event(decoding: encodedEvent) else { - try? FileHandle.stderr.write("Failed to decode \(encodedEvent)") - return - } - let eventContext = Event.Context( - test: encodedEvent.testID.flatMap(context.test(identifiedBy:)), - testCase: nil, - iteration: encodedEvent.iteration, - configuration: nil - ) - eventHandler(event, eventContext) - } - } while terminator != nil - } } #endif diff --git a/Sources/Testing/Support/Additions/CommandLineAdditions.swift b/Sources/Testing/Support/Additions/CommandLineAdditions.swift index 2783b6955..5e19c437f 100644 --- a/Sources/Testing/Support/Additions/CommandLineAdditions.swift +++ b/Sources/Testing/Support/Additions/CommandLineAdditions.swift @@ -26,7 +26,7 @@ enum CommandLine { } #endif -#if !SWT_NO_EXIT_TESTS +#if !SWT_NO_PROCESS_SPAWNING extension CommandLine { /// The path to the current process' executable. package static var executablePath: String { From f37785338cdd664c2ce363327d206779bb6c56eb Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Mon, 20 Jul 2026 11:47:43 -0400 Subject: [PATCH 09/14] Refactoring --- .../Testing/ABI/EntryPoints/FileGrommet.swift | 58 +++++++++++-------- 1 file changed, 35 insertions(+), 23 deletions(-) diff --git a/Sources/Testing/ABI/EntryPoints/FileGrommet.swift b/Sources/Testing/ABI/EntryPoints/FileGrommet.swift index 1a2e69813..cee0ca5c1 100644 --- a/Sources/Testing/ABI/EntryPoints/FileGrommet.swift +++ b/Sources/Testing/ABI/EntryPoints/FileGrommet.swift @@ -22,19 +22,12 @@ package final class FileGrommet: Grommet { } package func run(_ eventHandler: @escaping @Sendable (borrowing Event, borrowing Event.Context) -> Void) async throws { - try await _processRecords(fromBackChannel: _file, eventHandler: eventHandler) - } - - private func _processRecords( - fromBackChannel backChannel: borrowing FileHandle, - eventHandler: @Sendable (borrowing Event, borrowing Event.Context) -> Void - ) async throws { var context = ABI.Context() var terminator: UInt8? repeat { let recordJSON: [UInt8] - (recordJSON, terminator) = try backChannel.read(until: \.isASCIINewline) + (recordJSON, terminator) = try _file.read(until: \.isASCIINewline) // Allow other tasks to run after we may have blocked for some time on // I/O with the child process. @@ -43,26 +36,45 @@ package final class FileGrommet: Grommet { if recordJSON.isEmpty { continue } - let record = try recordJSON.withUnsafeBufferPointer { recordJSON in - try JSON.decode(ABI.Record.self, from: .init(recordJSON)) - } - switch record.kind { - case let .test(encodedTest): - _ = Test(decoding: encodedTest, in: &context) - case let .event(encodedEvent): - guard let event = Event(decoding: encodedEvent) else { - try? FileHandle.stderr.write("Failed to decode \(encodedEvent)") + try recordJSON.withUnsafeBytes { recordJSON in + let versionNumber = try ABI.VersionNumber(fromRecordJSON: recordJSON) + guard let abi = ABI._version(forVersionNumber: versionNumber) else { + try? FileHandle.stderr.write("Failed to determine ABI version for JSON record with version number '\(versionNumber)'") return } - let eventContext = Event.Context( - test: encodedEvent.testID.flatMap(context.test(identifiedBy:)), - testCase: nil, - iteration: encodedEvent.iteration, - configuration: nil + try _processRecord( + recordJSON, + withABIVersion: abi, + in: &context, + eventHandler: eventHandler ) - eventHandler(event, eventContext) } } while terminator != nil } + + private func _processRecord( + _ recordJSON: UnsafeRawBufferPointer, + withABIVersion: V.Type, + in context: inout ABI.Context, + eventHandler: @Sendable (borrowing Event, borrowing Event.Context) -> Void + ) throws where V: ABI._Version { + let record = try JSON.decode(ABI.Record.self, from: recordJSON) + switch record.kind { + case let .test(encodedTest): + _ = Test(decoding: encodedTest, in: &context) + case let .event(encodedEvent): + guard let event = Event(decoding: encodedEvent) else { + try? FileHandle.stderr.write("Failed to decode \(encodedEvent)") + return + } + let eventContext = Event.Context( + test: encodedEvent.testID.flatMap(context.test(identifiedBy:)), + testCase: nil, + iteration: encodedEvent.iteration, + configuration: nil + ) + eventHandler(event, eventContext) + } + } } #endif From 0b702ab452335d65a5f6c848c48ad6ec34ec95f9 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Mon, 20 Jul 2026 11:50:55 -0400 Subject: [PATCH 10/14] Fix typo --- Sources/Testing/ABI/EntryPoints/FileGrommet.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Testing/ABI/EntryPoints/FileGrommet.swift b/Sources/Testing/ABI/EntryPoints/FileGrommet.swift index cee0ca5c1..09713ba7b 100644 --- a/Sources/Testing/ABI/EntryPoints/FileGrommet.swift +++ b/Sources/Testing/ABI/EntryPoints/FileGrommet.swift @@ -63,7 +63,7 @@ package final class FileGrommet: Grommet { case let .test(encodedTest): _ = Test(decoding: encodedTest, in: &context) case let .event(encodedEvent): - guard let event = Event(decoding: encodedEvent) else { + guard let event = Event(decoding: encodedEvent, in: &context) else { try? FileHandle.stderr.write("Failed to decode \(encodedEvent)") return } From f91fb5baf66a995c2d30e102e07d1be57d735d14 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Mon, 20 Jul 2026 12:10:24 -0400 Subject: [PATCH 11/14] Print name of each subtask as we start running it --- Sources/Testing/ABI/EntryPoints/FileGrommet.swift | 10 ++++++++-- Sources/Testing/ABI/EntryPoints/Grommet.swift | 5 +++++ .../Testing/ABI/EntryPoints/HarnessEntryPoint.swift | 5 +++++ .../Testing/ABI/EntryPoints/LocalProcessGrommet.swift | 8 ++++++++ 4 files changed, 26 insertions(+), 2 deletions(-) diff --git a/Sources/Testing/ABI/EntryPoints/FileGrommet.swift b/Sources/Testing/ABI/EntryPoints/FileGrommet.swift index 09713ba7b..281d1cafb 100644 --- a/Sources/Testing/ABI/EntryPoints/FileGrommet.swift +++ b/Sources/Testing/ABI/EntryPoints/FileGrommet.swift @@ -11,14 +11,20 @@ #if !SWT_NO_FILE_IO package final class FileGrommet: Grommet { private let _file: FileHandle + private let _filePath: String - init(readingFrom file: consuming FileHandle) { + init(readingFrom file: consuming FileHandle, atPath filePath: String? = nil) { _file = file + _filePath = filePath ?? "" } package convenience init(readingFromFileAtPath filePath: String) throws { let file = try FileHandle(forReadingAtPath: filePath) - self.init(readingFrom: file) + self.init(readingFrom: file, atPath: filePath) + } + + package var grommetName: String { + _filePath } package func run(_ eventHandler: @escaping @Sendable (borrowing Event, borrowing Event.Context) -> Void) async throws { diff --git a/Sources/Testing/ABI/EntryPoints/Grommet.swift b/Sources/Testing/ABI/EntryPoints/Grommet.swift index bf96f4adb..1c7b3cfff 100644 --- a/Sources/Testing/ABI/EntryPoints/Grommet.swift +++ b/Sources/Testing/ABI/EntryPoints/Grommet.swift @@ -10,10 +10,15 @@ // FIXME: need an actual name for this protocol package protocol Grommet: Sendable { + var grommetName: String { get } func run(_ eventHandler: @escaping @Sendable (borrowing Event, borrowing Event.Context) -> Void) async throws } extension Runner: Grommet { + package var grommetName: String { + "" + } + package func run(_ eventHandler: @escaping @Sendable (borrowing Event, borrowing Event.Context) -> Void) async throws { var selfCopy = self selfCopy.configuration.eventHandler = { [oldEventHandler = selfCopy.configuration.eventHandler] event, context in diff --git a/Sources/Testing/ABI/EntryPoints/HarnessEntryPoint.swift b/Sources/Testing/ABI/EntryPoints/HarnessEntryPoint.swift index 0196c9007..c87d46b79 100644 --- a/Sources/Testing/ABI/EntryPoints/HarnessEntryPoint.swift +++ b/Sources/Testing/ABI/EntryPoints/HarnessEntryPoint.swift @@ -23,10 +23,15 @@ package func harnessEntryPoint( ) async throws -> CInt { var exitCodes = [CInt]() + let grommetCount = grommets.count for grommet in grommets { let exitCode = Atomic(EXIT_SUCCESS) func open(_ grommet: some Grommet) async throws { + if grommetCount > 1 { + try? FileHandle.stderr.write("Running '\(grommet.grommetName)'...") + } + try await grommet.run { event, eventContext in switch event.kind { case .testDiscovered: diff --git a/Sources/Testing/ABI/EntryPoints/LocalProcessGrommet.swift b/Sources/Testing/ABI/EntryPoints/LocalProcessGrommet.swift index 7a20e5e68..ba54ce5ed 100644 --- a/Sources/Testing/ABI/EntryPoints/LocalProcessGrommet.swift +++ b/Sources/Testing/ABI/EntryPoints/LocalProcessGrommet.swift @@ -26,6 +26,14 @@ package struct LocalProcessGrommet: Grommet { } #endif + package var grommetName: String { +#if SWT_TARGET_OS_APPLE + _testProductBinaryPath +#else + _testProductPath +#endif + } + package func run(_ eventHandler: @escaping @Sendable (borrowing Event, borrowing Event.Context) -> Void) async throws { try await withThrowingTaskGroup(of: Void.self) { taskGroup in var backChannelReadEnd: FileHandle! From 551a9f635d6006536fccb72e8e9f17a9dbd5e595 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Mon, 20 Jul 2026 12:25:22 -0400 Subject: [PATCH 12/14] Newline --- Sources/Testing/ABI/EntryPoints/HarnessEntryPoint.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Testing/ABI/EntryPoints/HarnessEntryPoint.swift b/Sources/Testing/ABI/EntryPoints/HarnessEntryPoint.swift index c87d46b79..1b8b05670 100644 --- a/Sources/Testing/ABI/EntryPoints/HarnessEntryPoint.swift +++ b/Sources/Testing/ABI/EntryPoints/HarnessEntryPoint.swift @@ -29,7 +29,7 @@ package func harnessEntryPoint( func open(_ grommet: some Grommet) async throws { if grommetCount > 1 { - try? FileHandle.stderr.write("Running '\(grommet.grommetName)'...") + try? FileHandle.stderr.write("Running '\(grommet.grommetName)'...\n") } try await grommet.run { event, eventContext in From 09e3a07faa0b33796dfc673465b0da27d106f3dd Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Mon, 20 Jul 2026 14:08:05 -0400 Subject: [PATCH 13/14] Actually emit console output from harness --- .../Testing/ABI/EntryPoints/EntryPoint.swift | 12 ++++--- .../ABI/EntryPoints/HarnessEntryPoint.swift | 34 +++++++++++++++---- .../ABI/EntryPoints/LocalProcessGrommet.swift | 2 +- 3 files changed, 36 insertions(+), 12 deletions(-) diff --git a/Sources/Testing/ABI/EntryPoints/EntryPoint.swift b/Sources/Testing/ABI/EntryPoints/EntryPoint.swift index 6d5bb2f8c..ee84bcf4c 100644 --- a/Sources/Testing/ABI/EntryPoints/EntryPoint.swift +++ b/Sources/Testing/ABI/EntryPoints/EntryPoint.swift @@ -522,11 +522,6 @@ func parseCommandLineArguments(from args: [String]) throws -> __CommandLineArgum hasHarnessEventStream = true } #endif - if hasHarnessEventStream { - // The presence of a harness event stream implies suppression of the normal - // stderr output. - result.verbosity = .min - } #endif // XML output @@ -574,6 +569,13 @@ func parseCommandLineArguments(from args: [String]) throws -> __CommandLineArgum if args.contains("--quiet") || args.contains("-q") { result.quiet = true } +#if !SWT_NO_FILE_IO + if hasHarnessEventStream { + // The presence of a harness event stream implies suppression of the normal + // stderr output. + result.verbosity = .min + } +#endif // Filtering func filterValues(forArgumentsWithLabel label: String) -> [String] { diff --git a/Sources/Testing/ABI/EntryPoints/HarnessEntryPoint.swift b/Sources/Testing/ABI/EntryPoints/HarnessEntryPoint.swift index 1b8b05670..5547f300f 100644 --- a/Sources/Testing/ABI/EntryPoints/HarnessEntryPoint.swift +++ b/Sources/Testing/ABI/EntryPoints/HarnessEntryPoint.swift @@ -23,19 +23,36 @@ package func harnessEntryPoint( ) async throws -> CInt { var exitCodes = [CInt]() +#if !SWT_NO_FILE_IO + let eventRecorder = Event.ConsoleOutputRecorder(options: .for(.stderr)) { string in + try? FileHandle.stderr.write(string) + } +#endif + let grommetCount = grommets.count - for grommet in grommets { + for (i, grommet) in grommets.enumerated() { let exitCode = Atomic(EXIT_SUCCESS) func open(_ grommet: some Grommet) async throws { - if grommetCount > 1 { - try? FileHandle.stderr.write("Running '\(grommet.grommetName)'...\n") - } - try await grommet.run { event, eventContext in + var recordEvent = true + switch event.kind { case .testDiscovered: _ = exitCode.compareExchange(expected: EXIT_SUCCESS, desired: EXIT_NO_TESTS_FOUND, ordering: .sequentiallyConsistent) +#if !SWT_NO_FILE_IO + case .runStarted: + if i > 0 { + recordEvent = false + } + if grommetCount > 1 { + try? FileHandle.stderr.write("Running '\(grommet.grommetName)'...\n") + } + case .runEnded: + if i != grommets.count - 1 { + recordEvent = false + } +#endif case let .issueRecorded(issue): if issue.isFailure { exitCode.store(EXIT_FAILURE, ordering: .sequentiallyConsistent) @@ -43,7 +60,12 @@ package func harnessEntryPoint( default: break } - print(event) + +#if !SWT_NO_FILE_IO + if recordEvent { + eventRecorder.record(event, in: eventContext) + } +#endif } } diff --git a/Sources/Testing/ABI/EntryPoints/LocalProcessGrommet.swift b/Sources/Testing/ABI/EntryPoints/LocalProcessGrommet.swift index ba54ce5ed..8d5d7abb7 100644 --- a/Sources/Testing/ABI/EntryPoints/LocalProcessGrommet.swift +++ b/Sources/Testing/ABI/EntryPoints/LocalProcessGrommet.swift @@ -47,7 +47,7 @@ package struct LocalProcessGrommet: Grommet { arguments += [ "--testing-library", "swift-testing", ] - arguments += CommandLine.arguments + arguments += CommandLine.arguments.dropFirst() #if os(Windows) backChannelWriteEnd.withUnsafeWindowsHANDLE { handle in guard let handle else { From 4d3c71e9c9f718695812de40ccba3167bcccf84b Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Fri, 24 Jul 2026 13:52:21 -0400 Subject: [PATCH 14/14] Name the tasks --- Sources/Testing/ABI/EntryPoints/LocalProcessGrommet.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/Testing/ABI/EntryPoints/LocalProcessGrommet.swift b/Sources/Testing/ABI/EntryPoints/LocalProcessGrommet.swift index 8d5d7abb7..3474d15f0 100644 --- a/Sources/Testing/ABI/EntryPoints/LocalProcessGrommet.swift +++ b/Sources/Testing/ABI/EntryPoints/LocalProcessGrommet.swift @@ -89,13 +89,13 @@ package struct LocalProcessGrommet: Grommet { backChannelWriteEnd.close() // Wait for the child process to terminate. - taskGroup.addTask { + taskGroup.addTask(name: decorateTaskName("harness", withAction: "running test process")) { _ = try await wait(for: processID) } // Read events back out from the back channel. let fileGrommet = FileGrommet(readingFrom: backChannelReadEnd!) - taskGroup.addTask { + taskGroup.addTask(name: decorateTaskName("harness", withAction: "reading events from test process")) { try await fileGrommet.run(eventHandler) }