From 91f7889dcb4f8215832d9af813f71f48d73ee4b7 Mon Sep 17 00:00:00 2001 From: Stuart Montgomery Date: Tue, 6 May 2025 19:55:42 -0700 Subject: [PATCH 001/216] Work around compiler bug affecting macro decls with #if-guarded availability when building w/legacy driver (#1106) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This works around a Swift compiler bug which causes a failure validating the generated .swiftinterface of the `Testing` module due to it having macro declarations with `#if`-conditionalized `@available(...)` attributes _before_ any other `@`-attributes. The PR which recently landed to enable the Exit Tests feature (#324) revealed this compiler bug — specifically, that PR removed `@_spi` attributes which until then _preceded_ `#if SWT_NO_EXIT_TESTS`. The workaround is to move other attributes on the affected macro declarations up before the `#if`. The compiler bug is being fixed in https://github.com/swiftlang/swift/pull/81346. It only appears to happen when building with the legacy driver, and Android uses that driver still. An example CI failure log can be found here: > https://github.com/thebrowsercompany/swift-build/actions/runs/14823859186/job/41615678071#step:32:72 ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- Sources/Testing/Expectations/Expectation+Macro.swift | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/Sources/Testing/Expectations/Expectation+Macro.swift b/Sources/Testing/Expectations/Expectation+Macro.swift index d14920547..f85c7042b 100644 --- a/Sources/Testing/Expectations/Expectation+Macro.swift +++ b/Sources/Testing/Expectations/Expectation+Macro.swift @@ -514,11 +514,12 @@ public macro require( /// @Metadata { /// @Available(Swift, introduced: 6.2) /// } +@freestanding(expression) +@discardableResult #if SWT_NO_EXIT_TESTS @available(*, unavailable, message: "Exit tests are not available on this platform.") #endif -@discardableResult -@freestanding(expression) public macro expect( +public macro expect( processExitsWith expectedExitCondition: ExitTest.Condition, observing observedValues: [any PartialKeyPath & Sendable] = [], _ comment: @autoclosure () -> Comment? = nil, @@ -559,11 +560,12 @@ public macro require( /// @Metadata { /// @Available(Swift, introduced: 6.2) /// } +@freestanding(expression) +@discardableResult #if SWT_NO_EXIT_TESTS @available(*, unavailable, message: "Exit tests are not available on this platform.") #endif -@discardableResult -@freestanding(expression) public macro require( +public macro require( processExitsWith expectedExitCondition: ExitTest.Condition, observing observedValues: [any PartialKeyPath & Sendable] = [], _ comment: @autoclosure () -> Comment? = nil, From 3ad851e7a4ea978ed1010b05d8e3c7dc0f5349ae Mon Sep 17 00:00:00 2001 From: Stuart Montgomery Date: Wed, 7 May 2025 12:53:21 -0700 Subject: [PATCH 002/216] Simplify usages of withTaskGroup to infer ChildTaskResult type where possible (#1102) This adjusts usages of `withTaskGroup` and `withThrowingTaskGroup` to take advantage of [SE-0442: Allow TaskGroup's ChildTaskResult Type To Be Inferred](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0442-allow-taskgroup-childtaskresult-type-to-be-inferred.md) by inferring the child task result type. I successfully built this PR using a Swift 6.1 toolchain. A couple usages I _did_ need to leave explicitly specified, but most I was able to simplify. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- Sources/Testing/Running/Runner.swift | 4 ++-- Sources/Testing/Test+Discovery.swift | 4 ++-- Sources/Testing/Traits/TimeLimitTrait.swift | 2 +- Tests/TestingTests/Support/CartesianProductTests.swift | 2 +- Tests/TestingTests/Support/LockTests.swift | 2 +- Tests/TestingTests/Traits/TimeLimitTraitTests.swift | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Sources/Testing/Running/Runner.swift b/Sources/Testing/Running/Runner.swift index 8520d1aaf..bd1167b8e 100644 --- a/Sources/Testing/Running/Runner.swift +++ b/Sources/Testing/Running/Runner.swift @@ -156,7 +156,7 @@ extension Runner { in sequence: some Sequence, _ body: @Sendable @escaping (E) async throws -> Void ) async throws where E: Sendable { - try await withThrowingTaskGroup(of: Void.self) { taskGroup in + try await withThrowingTaskGroup { taskGroup in for element in sequence { // Each element gets its own subtask to run in. _ = taskGroup.addTaskUnlessCancelled { @@ -430,7 +430,7 @@ extension Runner { Event.post(.iterationEnded(iterationIndex), for: (nil, nil), configuration: runner.configuration) } - await withTaskGroup(of: Void.self) { [runner] taskGroup in + await withTaskGroup { [runner] taskGroup in _ = taskGroup.addTaskUnlessCancelled { try? await _runStep(atRootOf: runner.plan.stepGraph) } diff --git a/Sources/Testing/Test+Discovery.swift b/Sources/Testing/Test+Discovery.swift index 35f716525..5e9632d70 100644 --- a/Sources/Testing/Test+Discovery.swift +++ b/Sources/Testing/Test+Discovery.swift @@ -84,7 +84,7 @@ extension Test { // a task group and collate their results. if useNewMode { let generators = Generator.allTestContentRecords().lazy.compactMap { $0.load() } - await withTaskGroup(of: Self.self) { taskGroup in + await withTaskGroup { taskGroup in for generator in generators { taskGroup.addTask { await generator.rawValue() } } @@ -96,7 +96,7 @@ extension Test { // Perform legacy test discovery if needed. if useLegacyMode && result.isEmpty { let generators = Generator.allTypeMetadataBasedTestContentRecords().lazy.compactMap { $0.load() } - await withTaskGroup(of: Self.self) { taskGroup in + await withTaskGroup { taskGroup in for generator in generators { taskGroup.addTask { await generator.rawValue() } } diff --git a/Sources/Testing/Traits/TimeLimitTrait.swift b/Sources/Testing/Traits/TimeLimitTrait.swift index 4e84a1f92..54a200fb4 100644 --- a/Sources/Testing/Traits/TimeLimitTrait.swift +++ b/Sources/Testing/Traits/TimeLimitTrait.swift @@ -264,7 +264,7 @@ func withTimeLimit( _ body: @escaping @Sendable () async throws -> Void, timeoutHandler: @escaping @Sendable () -> Void ) async throws { - try await withThrowingTaskGroup(of: Void.self) { group in + try await withThrowingTaskGroup { group in group.addTask { // If sleep() returns instead of throwing a CancellationError, that means // the timeout was reached before this task could be cancelled, so call diff --git a/Tests/TestingTests/Support/CartesianProductTests.swift b/Tests/TestingTests/Support/CartesianProductTests.swift index b817b37f6..3cb4f6daf 100644 --- a/Tests/TestingTests/Support/CartesianProductTests.swift +++ b/Tests/TestingTests/Support/CartesianProductTests.swift @@ -96,7 +96,7 @@ struct CartesianProductTests { // Test that the product can be iterated multiple times concurrently. let (_, _, product) = computeCartesianProduct() let expectedSum = product.reduce(into: 0) { $0 &+= $1.1 } - await withTaskGroup(of: Int.self) { taskGroup in + await withTaskGroup { taskGroup in for _ in 0 ..< 10 { taskGroup.addTask { product.reduce(into: 0) { $0 &+= $1.1 } diff --git a/Tests/TestingTests/Support/LockTests.swift b/Tests/TestingTests/Support/LockTests.swift index 0113745e9..2a41e4c1d 100644 --- a/Tests/TestingTests/Support/LockTests.swift +++ b/Tests/TestingTests/Support/LockTests.swift @@ -36,7 +36,7 @@ struct LockTests { @Test("No lock") func noLock() async { let lock = LockedWith(rawValue: 0) - await withTaskGroup(of: Void.self) { taskGroup in + await withTaskGroup { taskGroup in for _ in 0 ..< 100_000 { taskGroup.addTask { lock.increment() diff --git a/Tests/TestingTests/Traits/TimeLimitTraitTests.swift b/Tests/TestingTests/Traits/TimeLimitTraitTests.swift index b29ccb93c..49412b6af 100644 --- a/Tests/TestingTests/Traits/TimeLimitTraitTests.swift +++ b/Tests/TestingTests/Traits/TimeLimitTraitTests.swift @@ -181,7 +181,7 @@ struct TimeLimitTraitTests { @Test("Cancelled tests can exit early (cancellation checking works)") func cancelledTestExitsEarly() async throws { let timeAwaited = await Test.Clock().measure { - await withTaskGroup(of: Void.self) { taskGroup in + await withTaskGroup { taskGroup in taskGroup.addTask { await Test { try await Test.Clock.sleep(for: .seconds(60) * 60) From 1932a1b4c0899987ea85a80c01b446de39f54737 Mon Sep 17 00:00:00 2001 From: Stuart Montgomery Date: Wed, 7 May 2025 15:37:57 -0700 Subject: [PATCH 003/216] Add missing Foundation imports to fix test build errors on iOS with MemberImportVisibility enabled (#1108) This fixes several instances of a build error when attempting to build this package for iOS, or any non-macOS Apple platform. Here's one example ``` error: instance method 'contains' is not available due to missing import of defining module 'Foundation' Tests/TestingTests/SwiftPMTests.swift:370:5: note: in expansion of macro 'expect' here #expect(testIDs.allSatisfy { $0.contains(".swift:") }) ``` This general kind of build error is being emitted because we adopted [SE-0444: Member import visibility](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0444-member-import-visibility.md) by enabling the `MemberImportVisibility` experimental feature in #1020. In that PR, I fixed several instances of missing imports, including some for `Foundation` in test files. But these errors are from usages of `String.contains()`, and it turns out there are multiple overloads of that function, with an older one in `Foundation` and a newer one directly in the stdlib `Swift` module. The latter has newer, iOS 13.0-aligned API availability, and when building our tests for macOS this issue was not noticed previously because SwiftPM artificially raises the deployment target of macOS test targets to match the testing frameworks included in Xcode (when the testing libraries are being used from the installed copy of Xcode). ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- Tests/TestingTests/ConfirmationTests.swift | 4 ++++ Tests/TestingTests/MiscellaneousTests.swift | 4 ++++ Tests/TestingTests/SwiftPMTests.swift | 4 ++++ Tests/TestingTests/Traits/TagListTests.swift | 4 ++++ Tests/TestingTests/Traits/TimeLimitTraitTests.swift | 4 ++++ 5 files changed, 20 insertions(+) diff --git a/Tests/TestingTests/ConfirmationTests.swift b/Tests/TestingTests/ConfirmationTests.swift index c4f076268..2551513eb 100644 --- a/Tests/TestingTests/ConfirmationTests.swift +++ b/Tests/TestingTests/ConfirmationTests.swift @@ -10,6 +10,10 @@ @testable @_spi(Experimental) @_spi(ForToolsIntegrationOnly) import Testing +#if canImport(Foundation) +private import Foundation +#endif + @Suite("Confirmation Tests") struct ConfirmationTests { @Test("Successful confirmations") diff --git a/Tests/TestingTests/MiscellaneousTests.swift b/Tests/TestingTests/MiscellaneousTests.swift index b4b12a217..9ae326afe 100644 --- a/Tests/TestingTests/MiscellaneousTests.swift +++ b/Tests/TestingTests/MiscellaneousTests.swift @@ -12,6 +12,10 @@ @_spi(Experimental) @_spi(ForToolsIntegrationOnly) import _TestDiscovery private import _TestingInternals +#if canImport(Foundation) +private import Foundation +#endif + @Test(/* name unspecified */ .hidden) @Sendable func freeSyncFunction() {} @Sendable func freeAsyncFunction() async {} diff --git a/Tests/TestingTests/SwiftPMTests.swift b/Tests/TestingTests/SwiftPMTests.swift index 6e7be0f15..eadde29a7 100644 --- a/Tests/TestingTests/SwiftPMTests.swift +++ b/Tests/TestingTests/SwiftPMTests.swift @@ -11,6 +11,10 @@ @testable @_spi(Experimental) @_spi(ForToolsIntegrationOnly) import Testing private import _TestingInternals +#if canImport(Foundation) +private import Foundation +#endif + private func configurationForEntryPoint(withArguments args: [String]) throws -> Configuration { let args = try parseCommandLineArguments(from: args) return try configurationForEntryPoint(from: args) diff --git a/Tests/TestingTests/Traits/TagListTests.swift b/Tests/TestingTests/Traits/TagListTests.swift index 1ec8d1248..81cba285c 100644 --- a/Tests/TestingTests/Traits/TagListTests.swift +++ b/Tests/TestingTests/Traits/TagListTests.swift @@ -11,6 +11,10 @@ @testable @_spi(Experimental) @_spi(ForToolsIntegrationOnly) import Testing private import _TestingInternals +#if canImport(Foundation) +private import Foundation +#endif + @Suite("Tag/Tag List Tests", .tags(.traitRelated)) struct TagListTests { @Test(".tags() factory method with one tag") diff --git a/Tests/TestingTests/Traits/TimeLimitTraitTests.swift b/Tests/TestingTests/Traits/TimeLimitTraitTests.swift index 49412b6af..7d427f259 100644 --- a/Tests/TestingTests/Traits/TimeLimitTraitTests.swift +++ b/Tests/TestingTests/Traits/TimeLimitTraitTests.swift @@ -10,6 +10,10 @@ @testable @_spi(Experimental) @_spi(ForToolsIntegrationOnly) import Testing +#if canImport(Foundation) +private import Foundation +#endif + @Suite("TimeLimitTrait Tests", .tags(.traitRelated)) struct TimeLimitTraitTests { @available(_clockAPI, *) From 72afbb418542654781a6b7853479c7e70a862b6f Mon Sep 17 00:00:00 2001 From: Andrew Kaster Date: Tue, 13 May 2025 10:20:51 -0600 Subject: [PATCH 004/216] Documentation: Update CMake.md to use the ABI entry point (#828) The SwiftPM entry point is unstable and the new ABIv0 entry point has already been added to the library. ### Motivation: Using the SwiftPM entry point when building tests from a CMake project as recommended in the documentation is outdated and unwise. ### Modifications: Replace the example with one using the new ABIv0 entry point. ### Result: CMake projects should stop relying on the SwiftPM entry point. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- Documentation/CMake.md | 31 ++++++++++++++++++++++++------- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/Documentation/CMake.md b/Documentation/CMake.md index e846f5641..37b9e1d4e 100644 --- a/Documentation/CMake.md +++ b/Documentation/CMake.md @@ -59,21 +59,38 @@ endif() ## Add an entry point You must include a source file in your test executable target with a -`@main` entry point. The following example uses the SwiftPM entry point: +`@main` entry point. The example main below requires the experimental +`Extern` feature. The function `swt_abiv0_getEntryPoint` is exported +from the swift-testing dylib. As such, its declaration could instead +be written in a C header file with its own `module.modulemap`, or +the runtime address could be obtained via +[`dlsym()`](https://pubs.opengroup.org/onlinepubs/9799919799/functions/dlsym.html) or +[`GetProcAddress()`](https://learn.microsoft.com/en-us/windows/win32/api/libloaderapi/nf-libloaderapi-getprocaddress). ```swift -import Testing +typealias EntryPoint = @convention(thin) @Sendable (_ configurationJSON: UnsafeRawBufferPointer?, _ recordHandler: @escaping @Sendable (_ recordJSON: UnsafeRawBufferPointer) -> Void) async throws -> Bool + +@_extern(c, "swt_abiv0_getEntryPoint") +func swt_abiv0_getEntryPoint() -> UnsafeRawPointer @main struct Runner { - static func main() async { - await Testing.__swiftPMEntryPoint() as Never + static func main() async throws { + nonisolated(unsafe) let configurationJSON: UnsafeRawBufferPointer? = nil + let recordHandler: @Sendable (UnsafeRawBufferPointer) -> Void = { _ in } + + let entryPoint = unsafeBitCast(swt_abiv0_getEntryPoint(), to: EntryPoint.self) + + if try await entryPoint(configurationJSON, recordHandler) { + exit(EXIT_SUCCESS) + } else { + exit(EXIT_FAILURE) + } } } ``` -> [!WARNING] -> The entry point is expected to change to an entry point designed for other -> build systems prior to the initial stable release of Swift Testing. +For more information on the input configuration and output records of the ABI entry +point, refer to the [ABI documentation](ABI/JSON.md). ## Integrate with CTest From 464c01aa0b61383921b11a4bce82e05835a34a65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Rodr=C3=ADguez=20Troiti=C3=B1o?= Date: Fri, 16 May 2025 08:43:56 -0700 Subject: [PATCH 005/216] [CMake] Fix usage of lowercase or in condition (#1117) CMake is a strange animal and some things are case sensitive, while others aren't. It seems that `if()` logical operator `OR` is case sensitive, and `or` was not cutting it. At least for CMake 3.26.4. If some CMake version allows `or`, it should also allow `OR`, so this change should not be a problem for those versions. Most of the time these pieces are not actually hit, because the build system sets `SwiftTesting_MACROS` to `NO`, so this is skipped, which might explain why it has not been a problem. --- Sources/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/CMakeLists.txt b/Sources/CMakeLists.txt index 8106eb2a3..c6575a310 100644 --- a/Sources/CMakeLists.txt +++ b/Sources/CMakeLists.txt @@ -66,7 +66,7 @@ if(SwiftTesting_MACRO STREQUAL "") if(NOT SwiftTesting_BuildMacrosAsExecutables) if(CMAKE_HOST_SYSTEM_NAME STREQUAL "Darwin") set(SwiftTesting_MACRO_PATH "${SwiftTesting_MACRO_INSTALL_PREFIX}/lib/swift/host/plugins/testing/libTestingMacros.dylib") - elseif(CMAKE_HOST_SYSTEM_NAME STREQUAL "Linux" or CMAKE_HOST_SYSTEM_NAME STREQUAL "FreeBSD") + elseif(CMAKE_HOST_SYSTEM_NAME STREQUAL "Linux" OR CMAKE_HOST_SYSTEM_NAME STREQUAL "FreeBSD") set(SwiftTesting_MACRO_PATH "${SwiftTesting_MACRO_INSTALL_PREFIX}/lib/swift/host/plugins/libTestingMacros.so") elseif(CMAKE_HOST_SYSTEM_NAME STREQUAL "Windows") set(SwiftTesting_MACRO_PATH "${SwiftTesting_MACRO_INSTALL_PREFIX}/bin/TestingMacros.dll") From 17c00b09b7d7a25233c379932b2a296a9f2514d9 Mon Sep 17 00:00:00 2001 From: Stuart Montgomery Date: Fri, 16 May 2025 11:47:31 -0500 Subject: [PATCH 006/216] Include total number of suites in "run ended" console message (#1116) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This enhances the console output message shown when a test run finishes by including the total number of suites which ran or skipped, after the total number of test functions. Example: ``` ✔ Test run with 456 tests in 62 suites passed after 3.389 seconds. ``` The data was already being collected to support this, in a property named `suiteCount`, but it was not being used anywhere. So this PR adopts that property to augment the current "test run ended" message. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- .../Event.HumanReadableOutputRecorder.swift | 5 ++- Tests/TestingTests/EventRecorderTests.swift | 39 +++++++++++++++---- 2 files changed, 34 insertions(+), 10 deletions(-) diff --git a/Sources/Testing/Events/Recorder/Event.HumanReadableOutputRecorder.swift b/Sources/Testing/Events/Recorder/Event.HumanReadableOutputRecorder.swift index 6abb71442..06d12de6e 100644 --- a/Sources/Testing/Events/Recorder/Event.HumanReadableOutputRecorder.swift +++ b/Sources/Testing/Events/Recorder/Event.HumanReadableOutputRecorder.swift @@ -542,6 +542,7 @@ extension Event.HumanReadableOutputRecorder { case .runEnded: let testCount = context.testCount + let suiteCount = context.suiteCount let issues = _issueCounts(in: context.testData) let runStartInstant = context.runStartInstant ?? instant let duration = runStartInstant.descriptionOfDuration(to: instant) @@ -550,14 +551,14 @@ extension Event.HumanReadableOutputRecorder { [ Message( symbol: .fail, - stringValue: "Test run with \(testCount.counting("test")) failed after \(duration)\(issues.description)." + stringValue: "Test run with \(testCount.counting("test")) in \(suiteCount.counting("suite")) failed after \(duration)\(issues.description)." ) ] } else { [ Message( symbol: .pass(knownIssueCount: issues.knownIssueCount), - stringValue: "Test run with \(testCount.counting("test")) passed after \(duration)\(issues.description)." + stringValue: "Test run with \(testCount.counting("test")) in \(suiteCount.counting("suite")) passed after \(duration)\(issues.description)." ) ] } diff --git a/Tests/TestingTests/EventRecorderTests.swift b/Tests/TestingTests/EventRecorderTests.swift index 18f70186a..d06e12c7c 100644 --- a/Tests/TestingTests/EventRecorderTests.swift +++ b/Tests/TestingTests/EventRecorderTests.swift @@ -322,20 +322,29 @@ struct EventRecorderTests { print(buffer, terminator: "") } + let testCount = Reference() + let suiteCount = Reference() + let issueCount = Reference() + let knownIssueCount = Reference() + let runFailureRegex = Regex { One(.anyGraphemeCluster) " Test run with " - OneOrMore(.digit) + Capture(as: testCount) { OneOrMore(.digit) } transform: { Int($0) } " test" Optionally("s") + " in " + Capture(as: suiteCount) { OneOrMore(.digit) } transform: { Int($0) } + " suite" + Optionally("s") " failed " ZeroOrMore(.any) " with " - Capture { OneOrMore(.digit) } transform: { Int($0) } + Capture(as: issueCount) { OneOrMore(.digit) } transform: { Int($0) } " issue" Optionally("s") " (including " - Capture { OneOrMore(.digit) } transform: { Int($0) } + Capture(as: knownIssueCount) { OneOrMore(.digit) } transform: { Int($0) } " known issue" Optionally("s") ")." @@ -346,8 +355,10 @@ struct EventRecorderTests { .compactMap(runFailureRegex.wholeMatch(in:)) .first ) - #expect(match.output.1 == 12) - #expect(match.output.2 == 5) + #expect(match[testCount] == 9) + #expect(match[suiteCount] == 2) + #expect(match[issueCount] == 12) + #expect(match[knownIssueCount] == 5) } @Test("Issue counts are summed correctly on run end for a test with only warning issues") @@ -369,16 +380,24 @@ struct EventRecorderTests { print(buffer, terminator: "") } + let testCount = Reference() + let suiteCount = Reference() + let warningCount = Reference() + let runFailureRegex = Regex { One(.anyGraphemeCluster) " Test run with " - OneOrMore(.digit) + Capture(as: testCount) { OneOrMore(.digit) } transform: { Int($0) } " test" Optionally("s") + " in " + Capture(as: suiteCount) { OneOrMore(.digit) } transform: { Int($0) } + " suite" + Optionally("s") " passed " ZeroOrMore(.any) " with " - Capture { OneOrMore(.digit) } transform: { Int($0) } + Capture(as: warningCount) { OneOrMore(.digit) } transform: { Int($0) } " warning" Optionally("s") "." @@ -390,7 +409,9 @@ struct EventRecorderTests { .first, "buffer: \(buffer)" ) - #expect(match.output.1 == 1) + #expect(match[testCount] == 1) + #expect(match[suiteCount] == 1) + #expect(match[warningCount] == 1) } #endif @@ -691,6 +712,8 @@ struct EventRecorderTests { func n(_ arg: Int) { #expect(arg > 0) } + + @Suite struct PredictableSubsuite {} } @Suite(.hidden) struct PredictablyFailingKnownIssueTests { From 40edfec8d306dc23a6f11ee96a92b7656b9a2ff9 Mon Sep 17 00:00:00 2001 From: Stuart Montgomery Date: Tue, 20 May 2025 15:16:07 -0500 Subject: [PATCH 007/216] Refinements to IssueHandlingTrait SPI (#1121) A few refinements to `IssueHandlingTrait`, which is still SPI. ### Motivation: Polish this SPI in anticipation of posting a pitch to promote it to public API soon. ### Modifications: - Expose a `handleIssue(_:)` instance method to allow more easily composing multiple issue handling traits and calling their underlying handler closure. This is conceptually similar to what was done for [ST-0010: Public API to evaluate ConditionTrait](https://github.com/swiftlang/swift-evolution/blob/main/proposals/testing/0010-evaluate-condition.md). - Refine the names of private decls. - Add `- Returns:` in DocC for places it's missing. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- .../Testing/Traits/IssueHandlingTrait.swift | 33 ++++++++++++++----- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/Sources/Testing/Traits/IssueHandlingTrait.swift b/Sources/Testing/Traits/IssueHandlingTrait.swift index d70d68e93..6a14132f1 100644 --- a/Sources/Testing/Traits/IssueHandlingTrait.swift +++ b/Sources/Testing/Traits/IssueHandlingTrait.swift @@ -26,20 +26,31 @@ /// - ``Trait/filterIssues(_:)`` @_spi(Experimental) public struct IssueHandlingTrait: TestTrait, SuiteTrait { - /// A function which transforms an issue and returns an optional replacement. + /// A function which handles an issue and returns an optional replacement. /// /// - Parameters: - /// - issue: The issue to transform. + /// - issue: The issue to handle. /// /// - Returns: An issue to replace `issue`, or else `nil` if the issue should /// not be recorded. - fileprivate typealias Transformer = @Sendable (_ issue: Issue) -> Issue? + fileprivate typealias Handler = @Sendable (_ issue: Issue) -> Issue? - /// This trait's transformer function. - private var _transformer: Transformer + /// This trait's handler function. + private var _handler: Handler - fileprivate init(transformer: @escaping Transformer) { - _transformer = transformer + fileprivate init(handler: @escaping Handler) { + _handler = handler + } + + /// Handle a specified issue. + /// + /// - Parameters: + /// - issue: The issue to handle. + /// + /// - Returns: An issue to replace `issue`, or else `nil` if the issue should + /// not be recorded. + public func handleIssue(_ issue: Issue) -> Issue? { + _handler(issue) } public var isRecursive: Bool { @@ -90,7 +101,7 @@ extension IssueHandlingTrait: TestScoping { // records new issues. This means only issue handling traits whose scope // is outside this one will be allowed to handle such issues. let newIssue = Configuration.withCurrent(oldConfiguration) { - _transformer(issue) + handleIssue(issue) } if let newIssue { @@ -113,6 +124,8 @@ extension Trait where Self == IssueHandlingTrait { /// this trait is applied to. It is passed a recorded issue, and returns /// an optional issue to replace the passed-in one. /// + /// - Returns: An instance of ``IssueHandlingTrait`` that transforms issues. + /// /// The `transformer` closure is called synchronously each time an issue is /// recorded by the test this trait is applied to. The closure is passed the /// recorded issue, and if it returns a non-`nil` value, that will be recorded @@ -131,7 +144,7 @@ extension Trait where Self == IssueHandlingTrait { /// record new issues, although they will only be handled by issue handling /// traits which precede this trait or were inherited from a containing suite. public static func transformIssues(_ transformer: @escaping @Sendable (Issue) -> Issue?) -> Self { - Self(transformer: transformer) + Self(handler: transformer) } /// Constructs a trait that filters issues recorded by a test. @@ -142,6 +155,8 @@ extension Trait where Self == IssueHandlingTrait { /// should return `true` if the issue should be included, or `false` if it /// should be suppressed. /// + /// - Returns: An instance of ``IssueHandlingTrait`` that filters issues. + /// /// The `isIncluded` closure is called synchronously each time an issue is /// recorded by the test this trait is applied to. The closure is passed the /// recorded issue, and if it returns `true`, the issue will be preserved in From 981aa1c64215e71b22c64fae0a8ec879e2d13501 Mon Sep 17 00:00:00 2001 From: Stuart Montgomery Date: Thu, 22 May 2025 15:14:19 -0500 Subject: [PATCH 008/216] Remove the 'triage-needed' label from New Issue templates (#1124) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This removes the `triage-needed` label from the GitHub New Issue templates in this repository which have it. I added this while revamping these templates in #962, but more recently we added a `triaged` label which is works the opposite way—it's applied after an issue has been triaged—so we don't need the older label anymore. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- .github/ISSUE_TEMPLATE/01-bug-report.yml | 2 +- .github/ISSUE_TEMPLATE/02-change-request.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/01-bug-report.yml b/.github/ISSUE_TEMPLATE/01-bug-report.yml index ec821ae82..9e7cc171f 100644 --- a/.github/ISSUE_TEMPLATE/01-bug-report.yml +++ b/.github/ISSUE_TEMPLATE/01-bug-report.yml @@ -9,7 +9,7 @@ name: 🪲 Report a bug description: > Report a deviation from expected or documented behavior. -labels: [bug, triage-needed] +labels: [bug] body: - type: markdown attributes: diff --git a/.github/ISSUE_TEMPLATE/02-change-request.yml b/.github/ISSUE_TEMPLATE/02-change-request.yml index 4647a4560..fc29f3c44 100644 --- a/.github/ISSUE_TEMPLATE/02-change-request.yml +++ b/.github/ISSUE_TEMPLATE/02-change-request.yml @@ -9,7 +9,7 @@ name: 🌟 Request a change description: > Request a feature, API, improvement, or other change. -labels: [enhancement, triage-needed] +labels: [enhancement] body: - type: markdown attributes: From ae713136615888862c8318d9bb8234cf554855c8 Mon Sep 17 00:00:00 2001 From: Stuart Montgomery Date: Fri, 23 May 2025 15:09:19 -0500 Subject: [PATCH 009/216] Output console message for test case ended events in verbose mode, with status and issue counts (#1125) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This outputs a console message when each test case of a parameterized test function ends, including its pass/fail status and a count of the number of issues which were recorded, when running in verbose mode (`swift test --verbose`). Here's an example: ``` ◇ Test contrivedExample(x:) started. ◇ Test case passing 1 argument x → 1 (Swift.Int) to contrivedExample(x:) started. ◇ Test case passing 1 argument x → 2 (Swift.Int) to contrivedExample(x:) started. ✔ Test case passing 1 argument x → 1 (Swift.Int) to contrivedExample(x:) passed after 0.001 seconds. ✘ Test contrivedExample(x:) recorded an issue with 1 argument x → 2 at EventRecorderTests.swift:759:3: Expectation failed: (x → 2) == 1 ↳ x: Swift.Int → 2 ↳ 1: Swift.Int → 1 ✘ Test case passing 1 argument x → 2 (Swift.Int) to contrivedExample(x:) failed after 0.001 seconds with 1 issue. ✘ Test contrivedExample(x:) with 2 test cases failed after 0.001 seconds with 1 issue. ``` > Note: This leverages #1000 which added more robust identification of test cases. Fixes #1021 Fixes rdar://146863942 ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- .../Event.HumanReadableOutputRecorder.swift | 97 ++++++++++++++----- .../CustomTestStringConvertible.swift | 12 +-- Tests/TestingTests/EventRecorderTests.swift | 19 ++-- 3 files changed, 93 insertions(+), 35 deletions(-) diff --git a/Sources/Testing/Events/Recorder/Event.HumanReadableOutputRecorder.swift b/Sources/Testing/Events/Recorder/Event.HumanReadableOutputRecorder.swift index 06d12de6e..a3d121e09 100644 --- a/Sources/Testing/Events/Recorder/Event.HumanReadableOutputRecorder.swift +++ b/Sources/Testing/Events/Recorder/Event.HumanReadableOutputRecorder.swift @@ -36,7 +36,7 @@ extension Event { /// A type that contains mutable context for /// ``Event/ConsoleOutputRecorder``. - private struct _Context { + fileprivate struct Context { /// The instant at which the run started. var runStartInstant: Test.Clock.Instant? @@ -51,6 +51,17 @@ extension Event { /// The number of test suites started or skipped during the run. var suiteCount = 0 + /// An enumeration describing the various keys which can be used in a test + /// data graph for an output recorder. + enum TestDataKey: Hashable { + /// A string key, typically containing one key from the key path + /// representation of a ``Test/ID`` instance. + case string(String) + + /// A test case ID. + case testCaseID(Test.Case.ID) + } + /// A type describing data tracked on a per-test basis. struct TestData { /// The instant at which the test started. @@ -62,18 +73,15 @@ extension Event { /// The number of known issues recorded for the test. var knownIssueCount = 0 - - /// The number of test cases for the test. - var testCasesCount = 0 } /// Data tracked on a per-test basis. - var testData = Graph() + var testData = Graph() } /// This event recorder's mutable context about events it has received, /// which may be used to inform how subsequent events are written. - private var _context = Locked(rawValue: _Context()) + private var _context = Locked(rawValue: Context()) /// Initialize a new human-readable event recorder. /// @@ -128,7 +136,9 @@ extension Event.HumanReadableOutputRecorder { /// - graph: The graph to walk while counting issues. /// /// - Returns: A tuple containing the number of issues recorded in `graph`. - private func _issueCounts(in graph: Graph?) -> (errorIssueCount: Int, warningIssueCount: Int, knownIssueCount: Int, totalIssueCount: Int, description: String) { + private func _issueCounts( + in graph: Graph? + ) -> (errorIssueCount: Int, warningIssueCount: Int, knownIssueCount: Int, totalIssueCount: Int, description: String) { guard let graph else { return (0, 0, 0, 0, "") } @@ -241,6 +251,7 @@ extension Event.HumanReadableOutputRecorder { 0 } let test = eventContext.test + let keyPath = eventContext.keyPath let testName = if let test { if let displayName = test.displayName { if verbosity > 0 { @@ -271,7 +282,7 @@ extension Event.HumanReadableOutputRecorder { case .testStarted: let test = test! - context.testData[test.id.keyPathRepresentation] = .init(startInstant: instant) + context.testData[keyPath] = .init(startInstant: instant) if test.isSuite { context.suiteCount += 1 } else { @@ -287,23 +298,17 @@ extension Event.HumanReadableOutputRecorder { } case let .issueRecorded(issue): - let id: [String] = if let test { - test.id.keyPathRepresentation - } else { - [] - } - var testData = context.testData[id] ?? .init(startInstant: instant) + var testData = context.testData[keyPath] ?? .init(startInstant: instant) if issue.isKnown { testData.knownIssueCount += 1 } else { let issueCount = testData.issueCount[issue.severity] ?? 0 testData.issueCount[issue.severity] = issueCount + 1 } - context.testData[id] = testData + context.testData[keyPath] = testData case .testCaseStarted: - let test = test! - context.testData[test.id.keyPathRepresentation]?.testCasesCount += 1 + context.testData[keyPath] = .init(startInstant: instant) default: // These events do not manipulate the context structure. @@ -384,13 +389,12 @@ extension Event.HumanReadableOutputRecorder { case .testEnded: let test = test! - let id = test.id - let testDataGraph = context.testData.subgraph(at: id.keyPathRepresentation) + let testDataGraph = context.testData.subgraph(at: keyPath) let testData = testDataGraph?.value ?? .init(startInstant: instant) let issues = _issueCounts(in: testDataGraph) let duration = testData.startInstant.descriptionOfDuration(to: instant) - let testCasesCount = if test.isParameterized { - " with \(testData.testCasesCount.counting("test case"))" + let testCasesCount = if test.isParameterized, let testDataGraph { + " with \(testDataGraph.children.count.counting("test case"))" } else { "" } @@ -517,15 +521,37 @@ extension Event.HumanReadableOutputRecorder { break } + let status = verbosity > 0 ? " started" : "" + return [ Message( symbol: .default, - stringValue: "Passing \(arguments.count.counting("argument")) \(testCase.labeledArguments(includingQualifiedTypeNames: verbosity > 0)) to \(testName)" + stringValue: "Test case passing \(arguments.count.counting("argument")) \(testCase.labeledArguments(includingQualifiedTypeNames: verbosity > 0)) to \(testName)\(status) started." ) ] case .testCaseEnded: - break + guard verbosity > 0, let testCase = eventContext.testCase, testCase.isParameterized, let arguments = testCase.arguments else { + break + } + + let testDataGraph = context.testData.subgraph(at: keyPath) + let testData = testDataGraph?.value ?? .init(startInstant: instant) + let issues = _issueCounts(in: testDataGraph) + let duration = testData.startInstant.descriptionOfDuration(to: instant) + + let message = if issues.errorIssueCount > 0 { + Message( + symbol: .fail, + stringValue: "Test case passing \(arguments.count.counting("argument")) \(testCase.labeledArguments(includingQualifiedTypeNames: verbosity > 0)) to \(testName) failed after \(duration)\(issues.description)." + ) + } else { + Message( + symbol: .pass(knownIssueCount: issues.knownIssueCount), + stringValue: "Test case passing \(arguments.count.counting("argument")) \(testCase.labeledArguments(includingQualifiedTypeNames: verbosity > 0)) to \(testName) passed after \(duration)\(issues.description)." + ) + } + return [message] case let .iterationEnded(index): guard let iterationStartInstant = context.iterationStartInstant else { @@ -568,6 +594,31 @@ extension Event.HumanReadableOutputRecorder { } } +extension Test.ID { + /// The key path in a test data graph representing this test ID. + fileprivate var keyPath: some Collection { + keyPathRepresentation.map { .string($0) } + } +} + +extension Event.Context { + /// The key path in a test data graph representing this event this context is + /// associated with, including its test and/or test case IDs. + fileprivate var keyPath: some Collection { + var keyPath = [Event.HumanReadableOutputRecorder.Context.TestDataKey]() + + if let test { + keyPath.append(contentsOf: test.id.keyPath) + + if let testCase { + keyPath.append(.testCaseID(testCase.id)) + } + } + + return keyPath + } +} + // MARK: - Codable extension Event.HumanReadableOutputRecorder.Message: Codable {} diff --git a/Sources/Testing/SourceAttribution/CustomTestStringConvertible.swift b/Sources/Testing/SourceAttribution/CustomTestStringConvertible.swift index 192dde5ad..6f0517468 100644 --- a/Sources/Testing/SourceAttribution/CustomTestStringConvertible.swift +++ b/Sources/Testing/SourceAttribution/CustomTestStringConvertible.swift @@ -42,9 +42,9 @@ /// the default description of a value may not be adequately descriptive: /// /// ``` -/// ◇ Passing argument food → .paella to isDelicious(_:) -/// ◇ Passing argument food → .oden to isDelicious(_:) -/// ◇ Passing argument food → .ragu to isDelicious(_:) +/// ◇ Test case passing 1 argument food → .paella to isDelicious(_:) started. +/// ◇ Test case passing 1 argument food → .oden to isDelicious(_:) started. +/// ◇ Test case passing 1 argument food → .ragu to isDelicious(_:) started. /// ``` /// /// By adopting ``CustomTestStringConvertible``, customized descriptions can be @@ -69,9 +69,9 @@ /// ``testDescription`` property: /// /// ``` -/// ◇ Passing argument food → paella valenciana to isDelicious(_:) -/// ◇ Passing argument food → おでん to isDelicious(_:) -/// ◇ Passing argument food → ragù alla bolognese to isDelicious(_:) +/// ◇ Test case passing 1 argument food → paella valenciana to isDelicious(_:) started. +/// ◇ Test case passing 1 argument food → おでん to isDelicious(_:) started. +/// ◇ Test case passing 1 argument food → ragù alla bolognese to isDelicious(_:) started. /// ``` /// /// ## See Also diff --git a/Tests/TestingTests/EventRecorderTests.swift b/Tests/TestingTests/EventRecorderTests.swift index d06e12c7c..ed7d765a0 100644 --- a/Tests/TestingTests/EventRecorderTests.swift +++ b/Tests/TestingTests/EventRecorderTests.swift @@ -94,6 +94,7 @@ struct EventRecorderTests { } @Test("Verbose output") + @available(_regexAPI, *) func verboseOutput() async throws { let stream = Stream() @@ -112,6 +113,14 @@ struct EventRecorderTests { #expect(buffer.contains(#"\#(Event.Symbol.details.unicodeCharacter) lhs: Swift.String → "987""#)) #expect(buffer.contains(#""Animal Crackers" (aka 'WrittenTests')"#)) #expect(buffer.contains(#""Not A Lobster" (aka 'actuallyCrab()')"#)) + do { + let regex = try Regex(".* Test case passing 1 argument i → 0 \\(Swift.Int\\) to multitudeOcelot\\(i:\\) passed after .*.") + #expect(try buffer.split(whereSeparator: \.isNewline).compactMap(regex.wholeMatch(in:)).first != nil) + } + do { + let regex = try Regex(".* Test case passing 1 argument i → 3 \\(Swift.Int\\) to multitudeOcelot\\(i:\\) failed after .* with 1 issue.") + #expect(try buffer.split(whereSeparator: \.isNewline).compactMap(regex.wholeMatch(in:)).first != nil) + } if testsWithSignificantIOAreEnabled { print(buffer, terminator: "") @@ -203,17 +212,15 @@ struct EventRecorderTests { await runTest(for: PredictablyFailingTests.self, configuration: configuration) let buffer = stream.buffer.rawValue - if testsWithSignificantIOAreEnabled { - print(buffer, terminator: "") - } - let aurgmentRegex = try Regex(expectedPattern) + let argumentRegex = try Regex(expectedPattern) #expect( (try buffer .split(whereSeparator: \.isNewline) - .compactMap(aurgmentRegex.wholeMatch(in:)) - .first) != nil + .compactMap(argumentRegex.wholeMatch(in:)) + .first) != nil, + "buffer: \(buffer)" ) } From 8a6ed780e591c242708e51476bdce0e021730888 Mon Sep 17 00:00:00 2001 From: Stuart Montgomery Date: Fri, 23 May 2025 15:12:31 -0500 Subject: [PATCH 010/216] Enable upcoming feature 'InferIsolatedConformances' and fix issues it reveals (#1126) This enables the `InferIsolatedConformances` upcoming Swift feature from [SE-0470: Global-actor isolated conformances](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0470-isolated-conformances.md) and fixes a couple of pre-existing issues it revealed. I confirmed these code changes still build successfully using a Swift 6.1 toolchain. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- Package.swift | 2 ++ Sources/Testing/Running/Configuration.TestFilter.swift | 2 +- Tests/TestingMacrosTests/TestSupport/Parse.swift | 2 +- cmake/modules/shared/CompilerSettings.cmake | 3 ++- 4 files changed, 6 insertions(+), 3 deletions(-) diff --git a/Package.swift b/Package.swift index 44116b6d1..3df51ca48 100644 --- a/Package.swift +++ b/Package.swift @@ -288,6 +288,8 @@ extension Array where Element == PackageDescription.SwiftSetting { // new-enough toolchain. .enableExperimentalFeature("AllowUnsafeAttribute"), + .enableUpcomingFeature("InferIsolatedConformances"), + // When building as a package, the macro plugin always builds as an // executable rather than a library. .define("SWT_NO_LIBRARY_MACRO_PLUGINS"), diff --git a/Sources/Testing/Running/Configuration.TestFilter.swift b/Sources/Testing/Running/Configuration.TestFilter.swift index 7ef1fb08c..92d811ba1 100644 --- a/Sources/Testing/Running/Configuration.TestFilter.swift +++ b/Sources/Testing/Running/Configuration.TestFilter.swift @@ -514,7 +514,7 @@ extension Configuration.TestFilter.Kind { /// A protocol representing a value which can be filtered using /// ``Configuration/TestFilter-swift.struct``. -private protocol _FilterableItem { +private protocol _FilterableItem: Sendable { /// The test this item represents. var test: Test { get } diff --git a/Tests/TestingMacrosTests/TestSupport/Parse.swift b/Tests/TestingMacrosTests/TestSupport/Parse.swift index 2b30df42e..453a631b2 100644 --- a/Tests/TestingMacrosTests/TestSupport/Parse.swift +++ b/Tests/TestingMacrosTests/TestSupport/Parse.swift @@ -19,7 +19,7 @@ import SwiftSyntaxBuilder import SwiftSyntaxMacros import SwiftSyntaxMacroExpansion -fileprivate let allMacros: [String: any Macro.Type] = [ +fileprivate let allMacros: [String: any (Macro & Sendable).Type] = [ "expect": ExpectMacro.self, "require": RequireMacro.self, "requireAmbiguous": AmbiguousRequireMacro.self, // different name needed only for unit testing diff --git a/cmake/modules/shared/CompilerSettings.cmake b/cmake/modules/shared/CompilerSettings.cmake index 0da4216c5..af8b56dfd 100644 --- a/cmake/modules/shared/CompilerSettings.cmake +++ b/cmake/modules/shared/CompilerSettings.cmake @@ -17,7 +17,8 @@ add_compile_options( add_compile_options( "SHELL:$<$:-Xfrontend -enable-upcoming-feature -Xfrontend ExistentialAny>" "SHELL:$<$:-Xfrontend -enable-upcoming-feature -Xfrontend InternalImportsByDefault>" - "SHELL:$<$:-Xfrontend -enable-upcoming-feature -Xfrontend MemberImportVisibility>") + "SHELL:$<$:-Xfrontend -enable-upcoming-feature -Xfrontend MemberImportVisibility>" + "SHELL:$<$:-Xfrontend -enable-upcoming-feature -Xfrontend InferIsolatedConformances>") # Platform-specific definitions. if(APPLE) From d5b5c59670720524cd8a86bf5c8b3f01887efed2 Mon Sep 17 00:00:00 2001 From: Stuart Montgomery Date: Mon, 26 May 2025 20:24:13 -0500 Subject: [PATCH 011/216] Revert "Work around a macOS CI failure (#1100)" (#1129) Revert the workaround added in #1100, since the Swift compiler issue has been resolved and newer `main` development snapshot toolchains have become available with the fix. This reverts commit a82d0a89dd2eb86d18056f03396055528ce56508. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- Tests/TestingTests/IssueTests.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/Tests/TestingTests/IssueTests.swift b/Tests/TestingTests/IssueTests.swift index cc0a7acf5..6ea1a5827 100644 --- a/Tests/TestingTests/IssueTests.swift +++ b/Tests/TestingTests/IssueTests.swift @@ -491,7 +491,6 @@ final class IssueTests: XCTestCase { }.run(configuration: .init()) } -#if !SWT_TARGET_OS_APPLE || SWT_FIXED_149299786 func testErrorCheckingWithExpect() async throws { let expectationFailed = expectation(description: "Expectation failed") expectationFailed.isInverted = true @@ -611,7 +610,6 @@ final class IssueTests: XCTestCase { await fulfillment(of: [expectationFailed], timeout: 0.0) } -#endif func testErrorCheckingWithExpect_mismatchedErrorDescription() async throws { let expectationFailed = expectation(description: "Expectation failed") From d6111205ce07f173dbc2519ddaf4eec800a76b94 Mon Sep 17 00:00:00 2001 From: Stuart Montgomery Date: Fri, 30 May 2025 14:33:37 -0500 Subject: [PATCH 012/216] Expand parameterized testing documentation to mention try/await support and showcase helper pattern (#1133) This expands a few places where we document parameterized testing APIs to mention `try`/`await` support and showcase a common pattern for sharing arguments between multiple tests. ### Modifications: - For each `@Test` macro which accepts arguments, mention that `try` and `await` are supported and that arguments are lazily evaluated. - In the "Implementing parameterized tests" article, add a new section titled "Pass the same arguments to multiple test functions" showcasing the pattern of extracting common arguments to a separate property. - Add a [`> Tip:` callout](https://www.swift.org/documentation/docc/other-formatting-options#Add-Notes-and-Other-Asides) within that new article section mentioning `try`/`await` support. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. Fixes rdar://130929060 --- Sources/Testing/Test+Macro.swift | 30 +++++++++++++----- .../Testing.docc/ParameterizedTesting.md | 31 +++++++++++++++++++ 2 files changed, 53 insertions(+), 8 deletions(-) diff --git a/Sources/Testing/Test+Macro.swift b/Sources/Testing/Test+Macro.swift index be0b5a91b..44e9d3d72 100644 --- a/Sources/Testing/Test+Macro.swift +++ b/Sources/Testing/Test+Macro.swift @@ -217,8 +217,10 @@ public macro Test( /// - collection: A collection of values to pass to the associated test /// function. /// -/// During testing, the associated test function is called once for each element -/// in `collection`. +/// You can prefix the expression you pass to `collection` with `try` or `await`. +/// The testing library evaluates the expression lazily only if it determines +/// that the associated test will run. During testing, the testing library calls +/// the associated test function once for each element in `collection`. /// /// @Comment { /// - Bug: The testing library should support variadic generics. @@ -270,7 +272,10 @@ extension Test { /// - collection1: A collection of values to pass to `testFunction`. /// - collection2: A second collection of values to pass to `testFunction`. /// -/// During testing, the associated test function is called once for each pair of +/// You can prefix the expressions you pass to `collection1` or `collection2` +/// with `try` or `await`. The testing library evaluates the expressions lazily +/// only if it determines that the associated test will run. During testing, the +/// testing library calls the associated test function once for each pair of /// elements in `collection1` and `collection2`. /// /// @Comment { @@ -298,7 +303,10 @@ public macro Test( /// - collection1: A collection of values to pass to `testFunction`. /// - collection2: A second collection of values to pass to `testFunction`. /// -/// During testing, the associated test function is called once for each pair of +/// You can prefix the expressions you pass to `collection1` or `collection2` +/// with `try` or `await`. The testing library evaluates the expressions lazily +/// only if it determines that the associated test will run. During testing, the +/// testing library calls the associated test function once for each pair of /// elements in `collection1` and `collection2`. /// /// @Comment { @@ -324,8 +332,11 @@ public macro Test( /// - zippedCollections: Two zipped collections of values to pass to /// `testFunction`. /// -/// During testing, the associated test function is called once for each element -/// in `zippedCollections`. +/// You can prefix the expression you pass to `zippedCollections` with `try` or +/// `await`. The testing library evaluates the expression lazily only if it +/// determines that the associated test will run. During testing, the testing +/// library calls the associated test function once for each element in +/// `zippedCollections`. /// /// @Comment { /// - Bug: The testing library should support variadic generics. @@ -352,8 +363,11 @@ public macro Test( /// - zippedCollections: Two zipped collections of values to pass to /// `testFunction`. /// -/// During testing, the associated test function is called once for each element -/// in `zippedCollections`. +/// You can prefix the expression you pass to `zippedCollections` with `try` or +/// `await`. The testing library evaluates the expression lazily only if it +/// determines that the associated test will run. During testing, the testing +/// library calls the associated test function once for each element in +/// `zippedCollections`. /// /// @Comment { /// - Bug: The testing library should support variadic generics. diff --git a/Sources/Testing/Testing.docc/ParameterizedTesting.md b/Sources/Testing/Testing.docc/ParameterizedTesting.md index c4310e8e6..2dada707e 100644 --- a/Sources/Testing/Testing.docc/ParameterizedTesting.md +++ b/Sources/Testing/Testing.docc/ParameterizedTesting.md @@ -101,6 +101,37 @@ func makeLargeOrder(count: Int) async throws { - Note: Very large ranges such as `0 ..< .max` may take an excessive amount of time to test, or may never complete due to resource constraints. +### Pass the same arguments to multiple test functions + +If you want to pass the same collection of arguments to two or more +parameterized test functions, you can extract the arguments to a separate +function or property and pass it to each `@Test` attribute. For example: + +```swift +extension Food { + static var bestSelling: [Food] { + get async throws { /* ... */ } + } +} + +@Test(arguments: try await Food.bestSelling) +func `Order entree`(food: Food) { + let foodTruck = FoodTruck() + #expect(foodTruck.order(food)) +} + +@Test(arguments: try await Food.bestSelling) +func `Package leftovers`(food: Food) throws { + let foodTruck = FoodTruck() + let container = try #require(foodTruck.container(fitting: food)) + try container.add(food) +} +``` + +> Tip: You can prefix expressions passed to `arguments:` with `try` or `await`. +> The testing library evaluates them lazily only if it determines that the +> associated test will run. + ### Test with more than one collection It's possible to test more than one collection. Consider the following test From ca81dffea6bd63dc7e26290fb595b7562ac730b9 Mon Sep 17 00:00:00 2001 From: Stuart Montgomery Date: Mon, 2 Jun 2025 14:41:30 -0500 Subject: [PATCH 013/216] Acknowledge unsafe API usages in code expanded from testing library macros (#1134) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Acknowledge unsafe API usages from various testing library macros such as `@Test`, `@Suite`, and `#expect(processExitsWith:)` which are revealed in modules which enable the new opt-in strict memory safety feature in Swift 6.2. ### Motivation: This fix allows clients of the testing library to enable [SE-0458: Opt-in Strict Memory Safety Checking](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0458-strict-memory-safety.md) if they wish and avoid diagnostics from the testing library macros in their modules. These warnings generally looked like this, before this fix: ``` ⚠️ @__swiftmacro_22MemorySafeTestingTests19exampleTestFunction33_F2EA1AA3013574E5644E5A4339F05086LL0F0fMp_.swift:23:14: warning: expression uses unsafe constructs but is not marked with 'unsafe' Testing.Test.__store($s22MemorySafeTestingTests19exampleTestFunction33_F2EA1AA3013574E5644E5A4339F05086LL0F0fMp_25generator1e3470c498e8fe35fMu_, into: outValue, asTypeAt: type) ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ``` ### Modifications: - Add test file to reproduce the new diagnostics. It's in a new module which opts-in to strict memory safety, and marked as a dependency of `TestingTests`. - Add `unsafe` keyword in the appropriate places in our macros to acknowledge the existing unsafe usages. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. Fixes: rdar://151238560 --- Package.swift | 27 ++++++++++++++++ Sources/Testing/ExitTests/ExitTest.swift | 3 ++ Sources/Testing/Test+Discovery.swift | 3 ++ Sources/TestingMacros/ConditionMacro.swift | 3 +- .../TestingMacros/SuiteDeclarationMacro.swift | 3 +- .../Support/EffectfulExpressionHandling.swift | 28 +++++++++-------- .../Support/TestContentGeneration.swift | 3 +- .../TestingMacros/TestDeclarationMacro.swift | 3 +- .../MemorySafeTestDecls.swift | 31 +++++++++++++++++++ 9 files changed, 87 insertions(+), 17 deletions(-) create mode 100644 Tests/_MemorySafeTestingTests/MemorySafeTestDecls.swift diff --git a/Package.swift b/Package.swift index 3df51ca48..13dbf61cf 100644 --- a/Package.swift +++ b/Package.swift @@ -127,10 +127,25 @@ let package = Package( "Testing", "_Testing_CoreGraphics", "_Testing_Foundation", + "MemorySafeTestingTests", ], swiftSettings: .packageSettings ), + // Use a plain `.target` instead of a `.testTarget` to avoid the unnecessary + // overhead of having a separate test target for this module. Conceptually, + // the content in this module is no different than content which would + // typically be placed in the `TestingTests` target, except this content + // needs the (module-wide) strict memory safety feature to be enabled. + .target( + name: "MemorySafeTestingTests", + dependencies: [ + "Testing", + ], + path: "Tests/_MemorySafeTestingTests", + swiftSettings: .packageSettings + .strictMemorySafety + ), + .macro( name: "TestingMacros", dependencies: [ @@ -355,6 +370,18 @@ extension Array where Element == PackageDescription.SwiftSetting { return result } + + /// Settings necessary to enable Strict Memory Safety, introduced in + /// [SE-0458: Opt-in Strict Memory Safety Checking](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0458-strict-memory-safety.md#swiftpm-integration). + static var strictMemorySafety: Self { +#if compiler(>=6.2) + // FIXME: Adopt official `.strictMemorySafety()` condition once the minimum + // supported toolchain is 6.2. + [.unsafeFlags(["-strict-memory-safety"])] +#else + [] +#endif + } } extension Array where Element == PackageDescription.CXXSetting { diff --git a/Sources/Testing/ExitTests/ExitTest.swift b/Sources/Testing/ExitTests/ExitTest.swift index 1e9c29c15..beda3eb4e 100644 --- a/Sources/Testing/ExitTests/ExitTest.swift +++ b/Sources/Testing/ExitTests/ExitTest.swift @@ -327,6 +327,9 @@ extension ExitTest { /// /// - Warning: This function is used to implement the /// `#expect(processExitsWith:)` macro. Do not use it directly. +#if compiler(>=6.2) + @safe +#endif public static func __store( _ id: (UInt64, UInt64, UInt64, UInt64), _ body: @escaping @Sendable (repeat each T) async throws -> Void, diff --git a/Sources/Testing/Test+Discovery.swift b/Sources/Testing/Test+Discovery.swift index 5e9632d70..71862943d 100644 --- a/Sources/Testing/Test+Discovery.swift +++ b/Sources/Testing/Test+Discovery.swift @@ -39,6 +39,9 @@ extension Test { /// /// - Warning: This function is used to implement the `@Test` macro. Do not /// use it directly. +#if compiler(>=6.2) + @safe +#endif public static func __store( _ generator: @escaping @Sendable () async -> Test, into outValue: UnsafeMutableRawPointer, diff --git a/Sources/TestingMacros/ConditionMacro.swift b/Sources/TestingMacros/ConditionMacro.swift index 49630cfc9..9f87dfbd3 100644 --- a/Sources/TestingMacros/ConditionMacro.swift +++ b/Sources/TestingMacros/ConditionMacro.swift @@ -496,10 +496,11 @@ extension ExitTestConditionMacro { var recordDecl: DeclSyntax? #if !SWT_NO_LEGACY_TEST_DISCOVERY let legacyEnumName = context.makeUniqueName("__🟡$") + let unsafeKeyword: TokenSyntax? = isUnsafeKeywordSupported ? .keyword(.unsafe, trailingTrivia: .space) : nil recordDecl = """ enum \(legacyEnumName): Testing.__TestContentRecordContainer { nonisolated static var __testContentRecord: Testing.__TestContentRecord { - \(enumName).testContentRecord + \(unsafeKeyword)\(enumName).testContentRecord } } """ diff --git a/Sources/TestingMacros/SuiteDeclarationMacro.swift b/Sources/TestingMacros/SuiteDeclarationMacro.swift index 60a276689..e44b0460a 100644 --- a/Sources/TestingMacros/SuiteDeclarationMacro.swift +++ b/Sources/TestingMacros/SuiteDeclarationMacro.swift @@ -169,12 +169,13 @@ public struct SuiteDeclarationMacro: MemberMacro, PeerMacro, Sendable { #if !SWT_NO_LEGACY_TEST_DISCOVERY // Emit a type that contains a reference to the test content record. let enumName = context.makeUniqueName("__🟡$") + let unsafeKeyword: TokenSyntax? = isUnsafeKeywordSupported ? .keyword(.unsafe, trailingTrivia: .space) : nil result.append( """ @available(*, deprecated, message: "This type is an implementation detail of the testing library. Do not use it directly.") enum \(enumName): Testing.__TestContentRecordContainer { nonisolated static var __testContentRecord: Testing.__TestContentRecord { - \(testContentRecordName) + \(unsafeKeyword)\(testContentRecordName) } } """ diff --git a/Sources/TestingMacros/Support/EffectfulExpressionHandling.swift b/Sources/TestingMacros/Support/EffectfulExpressionHandling.swift index 494d2fcfc..a0b84e737 100644 --- a/Sources/TestingMacros/Support/EffectfulExpressionHandling.swift +++ b/Sources/TestingMacros/Support/EffectfulExpressionHandling.swift @@ -86,6 +86,17 @@ extension BidirectionalCollection { // MARK: - Inserting effect keywords/thunks +/// Whether or not the `unsafe` expression keyword is supported. +var isUnsafeKeywordSupported: Bool { + // The 'unsafe' keyword was introduced in 6.2 as part of SE-0458. Older + // toolchains are not aware of it. +#if compiler(>=6.2) + true +#else + false +#endif +} + /// Make a function call expression to an effectful thunk function provided by /// the testing library. /// @@ -127,12 +138,7 @@ func applyEffectfulKeywords(_ effectfulKeywords: Set, to expr: some Exp let needAwait = effectfulKeywords.contains(.await) && !expr.is(AwaitExprSyntax.self) let needTry = effectfulKeywords.contains(.try) && !expr.is(TryExprSyntax.self) - // The 'unsafe' keyword was introduced in 6.2 as part of SE-0458. Older - // toolchains are not aware of it, so avoid emitting expressions involving - // that keyword when the macro has been built using an older toolchain. -#if compiler(>=6.2) - let needUnsafe = effectfulKeywords.contains(.unsafe) && !expr.is(UnsafeExprSyntax.self) -#endif + let needUnsafe = isUnsafeKeywordSupported && effectfulKeywords.contains(.unsafe) && !expr.is(UnsafeExprSyntax.self) // First, add thunk function calls. if needAwait { @@ -141,11 +147,9 @@ func applyEffectfulKeywords(_ effectfulKeywords: Set, to expr: some Exp if needTry { expr = _makeCallToEffectfulThunk(.identifier("__requiringTry"), passing: expr) } -#if compiler(>=6.2) if needUnsafe { expr = _makeCallToEffectfulThunk(.identifier("__requiringUnsafe"), passing: expr) } -#endif // Then add keyword expressions. (We do this separately so we end up writing // `try await __r(__r(self))` instead of `try __r(await __r(self))` which is @@ -153,7 +157,7 @@ func applyEffectfulKeywords(_ effectfulKeywords: Set, to expr: some Exp if needAwait { expr = ExprSyntax( AwaitExprSyntax( - awaitKeyword: .keyword(.await).with(\.trailingTrivia, .space), + awaitKeyword: .keyword(.await, trailingTrivia: .space), expression: expr ) ) @@ -161,21 +165,19 @@ func applyEffectfulKeywords(_ effectfulKeywords: Set, to expr: some Exp if needTry { expr = ExprSyntax( TryExprSyntax( - tryKeyword: .keyword(.try).with(\.trailingTrivia, .space), + tryKeyword: .keyword(.try, trailingTrivia: .space), expression: expr ) ) } -#if compiler(>=6.2) if needUnsafe { expr = ExprSyntax( UnsafeExprSyntax( - unsafeKeyword: .keyword(.unsafe).with(\.trailingTrivia, .space), + unsafeKeyword: .keyword(.unsafe, trailingTrivia: .space), expression: expr ) ) } -#endif expr.leadingTrivia = originalExpr.leadingTrivia expr.trailingTrivia = originalExpr.trailingTrivia diff --git a/Sources/TestingMacros/Support/TestContentGeneration.swift b/Sources/TestingMacros/Support/TestContentGeneration.swift index 9a2529cee..2999478de 100644 --- a/Sources/TestingMacros/Support/TestContentGeneration.swift +++ b/Sources/TestingMacros/Support/TestContentGeneration.swift @@ -63,12 +63,13 @@ func makeTestContentRecordDecl(named name: TokenSyntax, in typeName: TypeSyntax? IntegerLiteralExprSyntax(context, radix: .binary) } + let unsafeKeyword: TokenSyntax? = isUnsafeKeywordSupported ? .keyword(.unsafe, trailingTrivia: .space) : nil var result: DeclSyntax = """ @available(*, deprecated, message: "This property is an implementation detail of the testing library. Do not use it directly.") private nonisolated \(staticKeyword(for: typeName)) let \(name): Testing.__TestContentRecord = ( \(kindExpr), \(kind.commentRepresentation) 0, - \(accessorName), + \(unsafeKeyword)\(accessorName), \(contextExpr), 0 ) diff --git a/Sources/TestingMacros/TestDeclarationMacro.swift b/Sources/TestingMacros/TestDeclarationMacro.swift index 0b2d43f1e..58e8259ec 100644 --- a/Sources/TestingMacros/TestDeclarationMacro.swift +++ b/Sources/TestingMacros/TestDeclarationMacro.swift @@ -494,12 +494,13 @@ public struct TestDeclarationMacro: PeerMacro, Sendable { #if !SWT_NO_LEGACY_TEST_DISCOVERY // Emit a type that contains a reference to the test content record. let enumName = context.makeUniqueName(thunking: functionDecl, withPrefix: "__🟡$") + let unsafeKeyword: TokenSyntax? = isUnsafeKeywordSupported ? .keyword(.unsafe, trailingTrivia: .space) : nil result.append( """ @available(*, deprecated, message: "This type is an implementation detail of the testing library. Do not use it directly.") enum \(enumName): Testing.__TestContentRecordContainer { nonisolated static var __testContentRecord: Testing.__TestContentRecord { - \(testContentRecordName) + \(unsafeKeyword)\(testContentRecordName) } } """ diff --git a/Tests/_MemorySafeTestingTests/MemorySafeTestDecls.swift b/Tests/_MemorySafeTestingTests/MemorySafeTestDecls.swift new file mode 100644 index 000000000..baf02c026 --- /dev/null +++ b/Tests/_MemorySafeTestingTests/MemorySafeTestDecls.swift @@ -0,0 +1,31 @@ +// +// 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 compiler(>=6.2) + +@testable import Testing + +#if !hasFeature(StrictMemorySafety) +#error("This file requires strict memory safety to be enabled") +#endif + +@Test(.hidden) +func exampleTestFunction() {} + +@Suite(.hidden) +struct ExampleSuite { + @Test func example() {} +} + +func exampleExitTest() async { + await #expect(processExitsWith: .success) {} +} + +#endif From 603a568c376066365e573e5a6aa043c651c7edc8 Mon Sep 17 00:00:00 2001 From: Stuart Montgomery Date: Fri, 6 Jun 2025 15:29:21 -0500 Subject: [PATCH 014/216] Work around macOS build failures affecting test targets (#1139) This applies a workaround to fix macOS builds (including CI) which began failing due to a Swift compiler regression which is expected to be resolved by https://github.com/swiftlang/swift/pull/82034. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- Package.swift | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/Package.swift b/Package.swift index 13dbf61cf..cce384d7a 100644 --- a/Package.swift +++ b/Package.swift @@ -129,7 +129,7 @@ let package = Package( "_Testing_Foundation", "MemorySafeTestingTests", ], - swiftSettings: .packageSettings + swiftSettings: .packageSettings + .disableMandatoryOptimizationsSettings ), // Use a plain `.target` instead of a `.testTarget` to avoid the unnecessary @@ -234,7 +234,7 @@ package.targets.append(contentsOf: [ "Testing", "TestingMacros", ], - swiftSettings: .packageSettings + swiftSettings: .packageSettings + .disableMandatoryOptimizationsSettings ) ]) #endif @@ -290,7 +290,10 @@ extension Array where Element == PackageDescription.SwiftSetting { // This setting is enabled in the package, but not in the toolchain build // (via CMake). Enabling it is dependent on acceptance of the @section // proposal via Swift Evolution. - .enableExperimentalFeature("SymbolLinkageMarkers"), + // + // FIXME: Re-enable this once a CI blocker is resolved: + // https://github.com/swiftlang/swift-testing/issues/1138. +// .enableExperimentalFeature("SymbolLinkageMarkers"), // This setting is no longer needed when building with a 6.2 or later // toolchain now that SE-0458 has been accepted and implemented, but it is @@ -382,6 +385,20 @@ extension Array where Element == PackageDescription.SwiftSetting { [] #endif } + + /// Settings which disable Swift's mandatory optimizations pass. + /// + /// This is intended only to work around a build failure caused by a Swift + /// compiler regression which is expected to be resolved in + /// [swiftlang/swift#82034](https://github.com/swiftlang/swift/pull/82034). + /// + /// @Comment { + /// - Bug: This should be removed once the CI issue is resolved. + /// [swiftlang/swift-testin#1138](https://github.com/swiftlang/swift-testing/issues/1138). + /// } + static var disableMandatoryOptimizationsSettings: Self { + [.unsafeFlags(["-Xllvm", "-sil-disable-pass=mandatory-performance-optimizations"])] + } } extension Array where Element == PackageDescription.CXXSetting { From edb9a6a4ffe80d0517bb95fb04c2b87c69c3c3c6 Mon Sep 17 00:00:00 2001 From: Stuart Montgomery Date: Sat, 7 Jun 2025 06:46:17 -0500 Subject: [PATCH 015/216] IssueHandlingTrait refinements: Rename to 'compactMapIssues' and special-case system issues (#1136) Refinements to `IssueHandlingTrait` based on [pitch](https://forums.swift.org/t/pitch-issue-handling-traits/80019) feedback. ### Modifications: - Rename the `transformIssues(_:)` function to `compactMapIssues(_:)` to align better with `filterIssues(_:)`. - Ignore issues for which the value of `kind` is `.system`. - Prohibit returning an issue from the closure passed to `compactMapIssues(_:)` whose `kind` is `.system`. - Adjust documentation and tests. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- Sources/Testing/Testing.docc/Traits.md | 2 +- .../Testing/Traits/IssueHandlingTrait.swift | 38 +++++++--- .../Traits/IssueHandlingTraitTests.swift | 74 ++++++++++++++----- 3 files changed, 84 insertions(+), 30 deletions(-) diff --git a/Sources/Testing/Testing.docc/Traits.md b/Sources/Testing/Testing.docc/Traits.md index e6d19c1b9..46fa82b4d 100644 --- a/Sources/Testing/Testing.docc/Traits.md +++ b/Sources/Testing/Testing.docc/Traits.md @@ -51,7 +51,7 @@ types that customize the behavior of your tests. diff --git a/Sources/Testing/Traits/IssueHandlingTrait.swift b/Sources/Testing/Traits/IssueHandlingTrait.swift index 6a14132f1..e8142ff5a 100644 --- a/Sources/Testing/Traits/IssueHandlingTrait.swift +++ b/Sources/Testing/Traits/IssueHandlingTrait.swift @@ -15,14 +15,14 @@ /// modifying one or more of its properties, and returning the copy. You can /// observe recorded issues by returning them unmodified. Or you can suppress an /// issue by either filtering it using ``Trait/filterIssues(_:)`` or returning -/// `nil` from the closure passed to ``Trait/transformIssues(_:)``. +/// `nil` from the closure passed to ``Trait/compactMapIssues(_:)``. /// /// When an instance of this trait is applied to a suite, it is recursively /// inherited by all child suites and tests. /// /// To add this trait to a test, use one of the following functions: /// -/// - ``Trait/transformIssues(_:)`` +/// - ``Trait/compactMapIssues(_:)`` /// - ``Trait/filterIssues(_:)`` @_spi(Experimental) public struct IssueHandlingTrait: TestTrait, SuiteTrait { @@ -96,8 +96,14 @@ extension IssueHandlingTrait: TestScoping { return } + // Ignore system issues, as they are not expected to be caused by users. + if case .system = issue.kind { + oldConfiguration.eventHandler(event, context) + return + } + // Use the original configuration's event handler when invoking the - // transformer to avoid infinite recursion if the transformer itself + // handler closure to avoid infinite recursion if the handler itself // records new issues. This means only issue handling traits whose scope // is outside this one will be allowed to handle such issues. let newIssue = Configuration.withCurrent(oldConfiguration) { @@ -105,6 +111,11 @@ extension IssueHandlingTrait: TestScoping { } if let newIssue { + // Prohibit assigning the issue's kind to system. + if case .system = newIssue.kind { + preconditionFailure("Issue returned by issue handling closure cannot have kind 'system': \(newIssue)") + } + var event = event event.kind = .issueRecorded(newIssue) oldConfiguration.eventHandler(event, context) @@ -120,31 +131,35 @@ extension Trait where Self == IssueHandlingTrait { /// Constructs an trait that transforms issues recorded by a test. /// /// - Parameters: - /// - transformer: The closure called for each issue recorded by the test + /// - transform: A closure called for each issue recorded by the test /// this trait is applied to. It is passed a recorded issue, and returns /// an optional issue to replace the passed-in one. /// /// - Returns: An instance of ``IssueHandlingTrait`` that transforms issues. /// - /// The `transformer` closure is called synchronously each time an issue is + /// The `transform` closure is called synchronously each time an issue is /// recorded by the test this trait is applied to. The closure is passed the /// recorded issue, and if it returns a non-`nil` value, that will be recorded /// instead of the original. Otherwise, if the closure returns `nil`, the /// issue is suppressed and will not be included in the results. /// - /// The `transformer` closure may be called more than once if the test records + /// The `transform` closure may be called more than once if the test records /// multiple issues. If more than one instance of this trait is applied to a - /// test (including via inheritance from a containing suite), the `transformer` + /// test (including via inheritance from a containing suite), the `transform` /// closure for each instance will be called in right-to-left, innermost-to- /// outermost order, unless `nil` is returned, which will skip invoking the /// remaining traits' closures. /// - /// Within `transformer`, you may access the current test or test case (if any) + /// Within `transform`, you may access the current test or test case (if any) /// using ``Test/current`` ``Test/Case/current``, respectively. You may also /// record new issues, although they will only be handled by issue handling /// traits which precede this trait or were inherited from a containing suite. - public static func transformIssues(_ transformer: @escaping @Sendable (Issue) -> Issue?) -> Self { - Self(handler: transformer) + /// + /// - Note: `transform` will never be passed an issue for which the value of + /// ``Issue/kind`` is ``Issue/Kind/system``, and may not return such an + /// issue. + public static func compactMapIssues(_ transform: @escaping @Sendable (Issue) -> Issue?) -> Self { + Self(handler: transform) } /// Constructs a trait that filters issues recorded by a test. @@ -174,6 +189,9 @@ extension Trait where Self == IssueHandlingTrait { /// using ``Test/current`` ``Test/Case/current``, respectively. You may also /// record new issues, although they will only be handled by issue handling /// traits which precede this trait or were inherited from a containing suite. + /// + /// - Note: `isIncluded` will never be passed an issue for which the value of + /// ``Issue/kind`` is ``Issue/Kind/system``. public static func filterIssues(_ isIncluded: @escaping @Sendable (Issue) -> Bool) -> Self { Self { issue in isIncluded(issue) ? issue : nil diff --git a/Tests/TestingTests/Traits/IssueHandlingTraitTests.swift b/Tests/TestingTests/Traits/IssueHandlingTraitTests.swift index 4d749b07f..eb1aa1233 100644 --- a/Tests/TestingTests/Traits/IssueHandlingTraitTests.swift +++ b/Tests/TestingTests/Traits/IssueHandlingTraitTests.swift @@ -8,7 +8,7 @@ // See https://swift.org/CONTRIBUTORS.txt for Swift project authors // -@_spi(Experimental) @_spi(ForToolsIntegrationOnly) import Testing +@testable @_spi(Experimental) @_spi(ForToolsIntegrationOnly) import Testing @Suite("IssueHandlingTrait Tests") struct IssueHandlingTraitTests { @@ -23,7 +23,7 @@ struct IssueHandlingTraitTests { #expect(issue.comments == ["Foo", "Bar"]) } - let handler = IssueHandlingTrait.transformIssues { issue in + let handler = IssueHandlingTrait.compactMapIssues { issue in var issue = issue issue.comments.append("Bar") return issue @@ -34,8 +34,8 @@ struct IssueHandlingTraitTests { }.run(configuration: configuration) } - @Test("Suppressing an issue by returning `nil` from the transform closure") - func suppressIssueUsingTransformer() async throws { + @Test("Suppressing an issue by returning `nil` from the closure passed to compactMapIssues()") + func suppressIssueUsingCompactMapIssues() async throws { var configuration = Configuration() configuration.eventHandler = { event, context in if case .issueRecorded = event.kind { @@ -43,7 +43,7 @@ struct IssueHandlingTraitTests { } } - let handler = IssueHandlingTrait.transformIssues { _ in + let handler = IssueHandlingTrait.compactMapIssues { _ in // Return nil to suppress the issue. nil } @@ -81,10 +81,10 @@ struct IssueHandlingTraitTests { struct MyError: Error {} - try await confirmation("Transformer closure is called") { transformerCalled in - let transformer: @Sendable (Issue) -> Issue? = { issue in + try await confirmation("Issue handler closure is called") { issueHandlerCalled in + let transform: @Sendable (Issue) -> Issue? = { issue in defer { - transformerCalled() + issueHandlerCalled() } #expect(Test.Case.current == nil) @@ -96,7 +96,7 @@ struct IssueHandlingTraitTests { let test = Test( .enabled(if: try { throw MyError() }()), - .transformIssues(transformer) + .compactMapIssues(transform) ) {} // Use a detached task to intentionally clear task local values for the @@ -108,12 +108,12 @@ struct IssueHandlingTraitTests { } #endif - @Test("Accessing the current Test and Test.Case from a transformer closure") + @Test("Accessing the current Test and Test.Case from an issue handler closure") func currentTestAndCase() async throws { - await confirmation("Transformer closure is called") { transformerCalled in - let handler = IssueHandlingTrait.transformIssues { issue in + await confirmation("Issue handler closure is called") { issueHandlerCalled in + let handler = IssueHandlingTrait.compactMapIssues { issue in defer { - transformerCalled() + issueHandlerCalled() } #expect(Test.current?.name == "fixture()") #expect(Test.Case.current != nil) @@ -140,12 +140,12 @@ struct IssueHandlingTraitTests { #expect(issue.comments == ["Foo", "Bar", "Baz"]) } - let outerHandler = IssueHandlingTrait.transformIssues { issue in + let outerHandler = IssueHandlingTrait.compactMapIssues { issue in var issue = issue issue.comments.append("Baz") return issue } - let innerHandler = IssueHandlingTrait.transformIssues { issue in + let innerHandler = IssueHandlingTrait.compactMapIssues { issue in var issue = issue issue.comments.append("Bar") return issue @@ -156,7 +156,7 @@ struct IssueHandlingTraitTests { }.run(configuration: configuration) } - @Test("Secondary issue recorded from a transformer closure") + @Test("Secondary issue recorded from an issue handler closure") func issueRecordedFromClosure() async throws { await confirmation("Original issue recorded") { originalIssueRecorded in await confirmation("Secondary issue recorded") { secondaryIssueRecorded in @@ -175,14 +175,14 @@ struct IssueHandlingTraitTests { } } - let handler1 = IssueHandlingTrait.transformIssues { issue in + let handler1 = IssueHandlingTrait.compactMapIssues { issue in return issue } - let handler2 = IssueHandlingTrait.transformIssues { issue in + let handler2 = IssueHandlingTrait.compactMapIssues { issue in Issue.record("Something else") return issue } - let handler3 = IssueHandlingTrait.transformIssues { issue in + let handler3 = IssueHandlingTrait.compactMapIssues { issue in // The "Something else" issue should not be passed to this closure. #expect(issue.comments.contains("Foo")) return issue @@ -194,4 +194,40 @@ struct IssueHandlingTraitTests { } } } + + @Test("System issues are not passed to issue handler closures") + func ignoresSystemIssues() async throws { + var configuration = Configuration() + configuration.eventHandler = { event, context in + if case let .issueRecorded(issue) = event.kind, case .unconditional = issue.kind { + issue.record() + } + } + + let handler = IssueHandlingTrait.compactMapIssues { issue in + if case .system = issue.kind { + Issue.record("Unexpectedly received a system issue") + } + return nil + } + + await Test(handler) { + Issue(kind: .system).record() + }.run(configuration: configuration) + } + +#if !SWT_NO_EXIT_TESTS + @Test("Disallow assigning kind to .system") + func disallowAssigningSystemKind() async throws { + await #expect(processExitsWith: .failure) { + await Test(.compactMapIssues { issue in + var issue = issue + issue.kind = .system + return issue + }) { + Issue.record("A non-system issue") + }.run() + } + } +#endif } From 25b61ef4667a6d1dda30cb3420336621886529c4 Mon Sep 17 00:00:00 2001 From: Wouter Hennen <62355975+Wouter01@users.noreply.github.com> Date: Sun, 8 Jun 2025 03:47:01 +0200 Subject: [PATCH 016/216] Update outdated Suite macro documentation (#1142) --- Sources/Testing/Test+Macro.swift | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/Sources/Testing/Test+Macro.swift b/Sources/Testing/Test+Macro.swift index 44e9d3d72..f2fc415d6 100644 --- a/Sources/Testing/Test+Macro.swift +++ b/Sources/Testing/Test+Macro.swift @@ -53,9 +53,8 @@ public typealias __XCTestCompatibleSelector = Never /// - Parameters: /// - traits: Zero or more traits to apply to this test suite. /// -/// A test suite is a type that contains one or more test functions. Any -/// copyable type (that is, any type that is not marked `~Copyable`) may be a -/// test suite. +/// A test suite is a type that contains one or more test functions. +/// Any type may be a test suite. /// /// The use of the `@Suite` attribute is optional; types are recognized as test /// suites even if they do not have the `@Suite` attribute applied to them. @@ -81,9 +80,8 @@ public macro Suite( /// from the associated type's name. /// - traits: Zero or more traits to apply to this test suite. /// -/// A test suite is a type that contains one or more test functions. Any -/// copyable type (that is, any type that is not marked `~Copyable`) may be a -/// test suite. +/// A test suite is a type that contains one or more test functions. +/// Any type may be a test suite. /// /// The use of the `@Suite` attribute is optional; types are recognized as test /// suites even if they do not have the `@Suite` attribute applied to them. From b0a1efd5e7e9cbb33785cf7e7c0178cac3c1a791 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Mon, 9 Jun 2025 19:08:16 -0400 Subject: [PATCH 017/216] Disable inheritance of file descriptors created by Swift Testing by default. (#1145) This PR makes file descriptors created by Swift Testing `FD_CLOEXEC` by default (on Windows, `~HANDLE_FLAG_INHERIT`.) We then clear `FD_CLOEXEC` for specific file descriptors that should be inherited. On Darwin, this is effectively ignored because we use `POSIX_SPAWN_CLOEXEC_DEFAULT`, and on Windows we use `PROC_THREAD_ATTRIBUTE_HANDLE_LIST` which has [much the same effect](https://devblogs.microsoft.com/oldnewthing/20111216-00/?p=8873). (On non-Darwin POSIX platforms, there's no reliable way to ensure only one child process inherits a particular file descriptor.) This change is speculative. No additional unit tests are added because existing test coverage _should be_ sufficient; the reported issue is on a platform (Ubuntu 20.04) where we don't have any CI jobs. Resolves #1140. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- Sources/Testing/Attachments/Attachment.swift | 4 +- Sources/Testing/ExitTests/ExitTest.swift | 9 ++ Sources/Testing/Support/FileHandle.swift | 135 ++++++++++++++++++- Sources/_TestingInternals/include/Stubs.h | 20 +++ 4 files changed, 161 insertions(+), 7 deletions(-) diff --git a/Sources/Testing/Attachments/Attachment.swift b/Sources/Testing/Attachments/Attachment.swift index 366e288d1..e9b98fb7b 100644 --- a/Sources/Testing/Attachments/Attachment.swift +++ b/Sources/Testing/Attachments/Attachment.swift @@ -462,7 +462,7 @@ extension Attachment where AttachableValue: ~Copyable { // file exists at this path (note "x" in the mode string), an error will // be thrown and we'll try again by adding a suffix. let preferredPath = appendPathComponent(preferredName, to: directoryPath) - file = try FileHandle(atPath: preferredPath, mode: "wxb") + file = try FileHandle(atPath: preferredPath, mode: "wxeb") result = preferredPath } catch { // Split the extension(s) off the preferred name. The first component in @@ -478,7 +478,7 @@ extension Attachment where AttachableValue: ~Copyable { // Propagate any error *except* EEXIST, which would indicate that the // name was already in use (so we should try again with a new suffix.) do { - file = try FileHandle(atPath: preferredPath, mode: "wxb") + file = try FileHandle(atPath: preferredPath, mode: "wxeb") result = preferredPath break } catch let error as CError where error.rawValue == swt_EEXIST() { diff --git a/Sources/Testing/ExitTests/ExitTest.swift b/Sources/Testing/ExitTests/ExitTest.swift index beda3eb4e..9284310db 100644 --- a/Sources/Testing/ExitTests/ExitTest.swift +++ b/Sources/Testing/ExitTests/ExitTest.swift @@ -801,6 +801,15 @@ extension ExitTest { childEnvironment["SWT_EXPERIMENTAL_CAPTURED_VALUES"] = capturedValuesEnvironmentVariable } +#if !SWT_TARGET_OS_APPLE + // Set inherited those file handles that the child process needs. On + // Darwin, this is a no-op because we use POSIX_SPAWN_CLOEXEC_DEFAULT. + try stdoutWriteEnd?.setInherited(true) + try stderrWriteEnd?.setInherited(true) + try backChannelWriteEnd.setInherited(true) + try capturedValuesReadEnd.setInherited(true) +#endif + // Spawn the child process. let processID = try withUnsafePointer(to: backChannelWriteEnd) { backChannelWriteEnd in try withUnsafePointer(to: capturedValuesReadEnd) { capturedValuesReadEnd in diff --git a/Sources/Testing/Support/FileHandle.swift b/Sources/Testing/Support/FileHandle.swift index 2a2bfe967..4e3c17372 100644 --- a/Sources/Testing/Support/FileHandle.swift +++ b/Sources/Testing/Support/FileHandle.swift @@ -73,6 +73,13 @@ struct FileHandle: ~Copyable, Sendable { return } + // On Windows, "N" is used rather than "e" to signify that a file handle is + // not inherited. + var mode = mode + if let eIndex = mode.firstIndex(of: "e") { + mode.replaceSubrange(eIndex ... eIndex, with: "N") + } + // Windows deprecates fopen() as insecure, so call _wfopen_s() instead. let fileHandle = try path.withCString(encodedAs: UTF16.self) { path in try mode.withCString(encodedAs: UTF16.self) { mode in @@ -98,8 +105,13 @@ struct FileHandle: ~Copyable, Sendable { /// - path: The path to read from. /// /// - Throws: Any error preventing the stream from being opened. + /// + /// By default, the resulting file handle is not inherited by any child + /// processes (that is, `FD_CLOEXEC` is set on POSIX-like systems and + /// `HANDLE_FLAG_INHERIT` is cleared on Windows.) To make it inheritable, call + /// ``setInherited()``. init(forReadingAtPath path: String) throws { - try self.init(atPath: path, mode: "rb") + try self.init(atPath: path, mode: "reb") } /// Initialize an instance of this type to write to the given path. @@ -108,8 +120,13 @@ struct FileHandle: ~Copyable, Sendable { /// - path: The path to write to. /// /// - Throws: Any error preventing the stream from being opened. + /// + /// By default, the resulting file handle is not inherited by any child + /// processes (that is, `FD_CLOEXEC` is set on POSIX-like systems and + /// `HANDLE_FLAG_INHERIT` is cleared on Windows.) To make it inheritable, call + /// ``setInherited()``. init(forWritingAtPath path: String) throws { - try self.init(atPath: path, mode: "wb") + try self.init(atPath: path, mode: "web") } /// Initialize an instance of this type with an existing C file handle. @@ -445,6 +462,17 @@ extension FileHandle { #if !SWT_NO_PIPES // MARK: - Pipes +#if !SWT_TARGET_OS_APPLE && !os(Windows) && !SWT_NO_DYNAMIC_LINKING +/// Create a pipe with flags. +/// +/// This function declaration is provided because `pipe2()` is only declared if +/// `_GNU_SOURCE` is set, but setting it causes build errors due to conflicts +/// with Swift's Glibc module. +private let _pipe2 = symbol(named: "pipe2").map { + castCFunction(at: $0, to: (@convention(c) (UnsafeMutablePointer, CInt) -> CInt).self) +} +#endif + extension FileHandle { /// Make a pipe connecting two new file handles. /// @@ -461,15 +489,37 @@ extension FileHandle { /// - Bug: This function should return a tuple containing the file handles /// instead of returning them via `inout` arguments. Swift does not support /// tuples with move-only elements. ([104669935](rdar://104669935)) + /// + /// By default, the resulting file handles are not inherited by any child + /// processes (that is, `FD_CLOEXEC` is set on POSIX-like systems and + /// `HANDLE_FLAG_INHERIT` is cleared on Windows.) To make them inheritable, + /// call ``setInherited()``. static func makePipe(readEnd: inout FileHandle?, writeEnd: inout FileHandle?) throws { +#if !os(Windows) + var pipe2Called = false +#endif + var (fdReadEnd, fdWriteEnd) = try withUnsafeTemporaryAllocation(of: CInt.self, capacity: 2) { fds in #if os(Windows) - guard 0 == _pipe(fds.baseAddress, 0, _O_BINARY) else { + guard 0 == _pipe(fds.baseAddress, 0, _O_BINARY | _O_NOINHERIT) else { throw CError(rawValue: swt_errno()) } #else - guard 0 == pipe(fds.baseAddress!) else { - throw CError(rawValue: swt_errno()) +#if !SWT_TARGET_OS_APPLE && !os(Windows) && !SWT_NO_DYNAMIC_LINKING + if let _pipe2 { + guard 0 == _pipe2(fds.baseAddress!, O_CLOEXEC) else { + throw CError(rawValue: swt_errno()) + } + pipe2Called = true + } +#endif + + if !pipe2Called { + // pipe2() is not available. Use pipe() instead and simulate O_CLOEXEC + // to the best of our ability. + guard 0 == pipe(fds.baseAddress!) else { + throw CError(rawValue: swt_errno()) + } } #endif return (fds[0], fds[1]) @@ -479,6 +529,15 @@ extension FileHandle { Self._close(fdWriteEnd) } +#if !os(Windows) + if !pipe2Called { + // pipe2() is not available. Use pipe() instead and simulate O_CLOEXEC + // to the best of our ability. + try _setFileDescriptorInherited(fdReadEnd, false) + try _setFileDescriptorInherited(fdWriteEnd, false) + } +#endif + do { defer { fdReadEnd = -1 @@ -553,6 +612,72 @@ extension FileHandle { #endif } #endif + +#if SWT_TARGET_OS_APPLE || os(Linux) || os(FreeBSD) || os(OpenBSD) || os(Android) + /// Set whether or not the given file descriptor is inherited by child processes. + /// + /// - Parameters: + /// - fd: The file descriptor. + /// - inherited: Whether or not `fd` is inherited by child processes + /// (ignoring overriding functionality such as Apple's + /// `POSIX_SPAWN_CLOEXEC_DEFAULT` flag.) + /// + /// - Throws: Any error that occurred while setting the flag. + private static func _setFileDescriptorInherited(_ fd: CInt, _ inherited: Bool) throws { + switch swt_getfdflags(fd) { + case -1: + // An error occurred reading the flags for this file descriptor. + throw CError(rawValue: swt_errno()) + case let oldValue: + let newValue = if inherited { + oldValue & ~FD_CLOEXEC + } else { + oldValue | FD_CLOEXEC + } + if oldValue == newValue { + // No need to make a second syscall as nothing has changed. + return + } + if -1 == swt_setfdflags(fd, newValue) { + // An error occurred setting the flags for this file descriptor. + throw CError(rawValue: swt_errno()) + } + } + } +#endif + + /// Set whether or not this file handle is inherited by child processes. + /// + /// - Parameters: + /// - inherited: Whether or not this file handle is inherited by child + /// processes (ignoring overriding functionality such as Apple's + /// `POSIX_SPAWN_CLOEXEC_DEFAULT` flag.) + /// + /// - Throws: Any error that occurred while setting the flag. + func setInherited(_ inherited: Bool) throws { +#if SWT_TARGET_OS_APPLE || os(Linux) || os(FreeBSD) || os(OpenBSD) || os(Android) + try withUnsafePOSIXFileDescriptor { fd in + guard let fd else { + throw SystemError(description: "Cannot set whether a file handle is inherited unless it is backed by a file descriptor. Please file a bug report at https://github.com/swiftlang/swift-testing/issues/new") + } + try withLock { + try Self._setFileDescriptorInherited(fd, inherited) + } + } +#elseif os(Windows) + return try withUnsafeWindowsHANDLE { handle in + guard let handle else { + throw SystemError(description: "Cannot set whether a file handle is inherited unless it is backed by a Windows file handle. Please file a bug report at https://github.com/swiftlang/swift-testing/issues/new") + } + let newValue = inherited ? DWORD(HANDLE_FLAG_INHERIT) : 0 + guard SetHandleInformation(handle, DWORD(HANDLE_FLAG_INHERIT), newValue) else { + throw Win32Error(rawValue: GetLastError()) + } + } +#else +#warning("Platform-specific implementation missing: cannot set whether a file handle is inherited") +#endif + } } // MARK: - General path utilities diff --git a/Sources/_TestingInternals/include/Stubs.h b/Sources/_TestingInternals/include/Stubs.h index 8093a3722..171cca6a5 100644 --- a/Sources/_TestingInternals/include/Stubs.h +++ b/Sources/_TestingInternals/include/Stubs.h @@ -151,6 +151,26 @@ static int swt_EEXIST(void) { return EEXIST; } +#if defined(F_GETFD) +/// Call `fcntl(F_GETFD)`. +/// +/// This function is provided because `fcntl()` is a variadic function and +/// cannot be imported directly into Swift. +static int swt_getfdflags(int fd) { + return fcntl(fd, F_GETFD); +} +#endif + +#if defined(F_SETFD) +/// Call `fcntl(F_SETFD)`. +/// +/// This function is provided because `fcntl()` is a variadic function and +/// cannot be imported directly into Swift. +static int swt_setfdflags(int fd, int flags) { + return fcntl(fd, F_SETFD, flags); +} +#endif + SWT_ASSUME_NONNULL_END #endif From c0108ad3a3ba304448dc6162a4febd5a4630c0ca Mon Sep 17 00:00:00 2001 From: Stuart Montgomery Date: Tue, 10 Jun 2025 13:14:58 -0500 Subject: [PATCH 018/216] Declare Xcode 26 availability for all Swift 6.2 APIs (#1148) This declares Xcode 26.0 API availability for all Swift 6.2 declarations. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. Fixes rdar://151629765 --- .../Attachable+Encodable+NSSecureCoding.swift | 1 + .../Attachments/Attachable+Encodable.swift | 2 ++ .../Attachments/Attachable+NSSecureCoding.swift | 2 ++ .../Attachments/Attachment+URL.swift | 1 + .../Attachments/Data+Attachable.swift | 2 ++ Sources/Testing/Attachments/Attachable.swift | 4 ++++ Sources/Testing/Attachments/AttachableWrapper.swift | 3 +++ Sources/Testing/Attachments/Attachment.swift | 11 +++++++++++ Sources/Testing/ExitTests/ExitStatus.swift | 3 +++ Sources/Testing/ExitTests/ExitTest.Condition.swift | 6 ++++++ Sources/Testing/ExitTests/ExitTest.Result.swift | 4 ++++ Sources/Testing/ExitTests/ExitTest.swift | 2 ++ Sources/Testing/Expectations/Expectation+Macro.swift | 2 ++ Sources/Testing/Testing.docc/exit-testing.md | 1 + Sources/Testing/Traits/ConditionTrait.swift | 1 + 15 files changed, 45 insertions(+) diff --git a/Sources/Overlays/_Testing_Foundation/Attachments/Attachable+Encodable+NSSecureCoding.swift b/Sources/Overlays/_Testing_Foundation/Attachments/Attachable+Encodable+NSSecureCoding.swift index 46a1e11e6..d82c6a6c6 100644 --- a/Sources/Overlays/_Testing_Foundation/Attachments/Attachable+Encodable+NSSecureCoding.swift +++ b/Sources/Overlays/_Testing_Foundation/Attachments/Attachable+Encodable+NSSecureCoding.swift @@ -20,6 +20,7 @@ public import Foundation /// @Metadata { /// @Available(Swift, introduced: 6.2) +/// @Available(Xcode, introduced: 26.0) /// } extension Attachable where Self: Encodable & NSSecureCoding { @_documentation(visibility: private) diff --git a/Sources/Overlays/_Testing_Foundation/Attachments/Attachable+Encodable.swift b/Sources/Overlays/_Testing_Foundation/Attachments/Attachable+Encodable.swift index 683888801..747024de3 100644 --- a/Sources/Overlays/_Testing_Foundation/Attachments/Attachable+Encodable.swift +++ b/Sources/Overlays/_Testing_Foundation/Attachments/Attachable+Encodable.swift @@ -56,6 +56,7 @@ func withUnsafeBytes(encoding attachableValue: borrowing E, for attachment /// @Metadata { /// @Available(Swift, introduced: 6.2) +/// @Available(Xcode, introduced: 26.0) /// } extension Attachable where Self: Encodable { /// Encode this value into a buffer using either [`PropertyListEncoder`](https://developer.apple.com/documentation/foundation/propertylistencoder) @@ -92,6 +93,7 @@ extension Attachable where Self: Encodable { /// /// @Metadata { /// @Available(Swift, introduced: 6.2) + /// @Available(Xcode, introduced: 26.0) /// } public func withUnsafeBytes(for attachment: borrowing Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { try _Testing_Foundation.withUnsafeBytes(encoding: self, for: attachment, body) diff --git a/Sources/Overlays/_Testing_Foundation/Attachments/Attachable+NSSecureCoding.swift b/Sources/Overlays/_Testing_Foundation/Attachments/Attachable+NSSecureCoding.swift index 4acbf4960..95002be0a 100644 --- a/Sources/Overlays/_Testing_Foundation/Attachments/Attachable+NSSecureCoding.swift +++ b/Sources/Overlays/_Testing_Foundation/Attachments/Attachable+NSSecureCoding.swift @@ -18,6 +18,7 @@ public import Foundation /// @Metadata { /// @Available(Swift, introduced: 6.2) +/// @Available(Xcode, introduced: 26.0) /// } extension Attachable where Self: NSSecureCoding { /// Encode this object using [`NSKeyedArchiver`](https://developer.apple.com/documentation/foundation/nskeyedarchiver) @@ -52,6 +53,7 @@ extension Attachable where Self: NSSecureCoding { /// /// @Metadata { /// @Available(Swift, introduced: 6.2) + /// @Available(Xcode, introduced: 26.0) /// } public func withUnsafeBytes(for attachment: borrowing Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { let format = try EncodingFormat(for: attachment) diff --git a/Sources/Overlays/_Testing_Foundation/Attachments/Attachment+URL.swift b/Sources/Overlays/_Testing_Foundation/Attachments/Attachment+URL.swift index 83c3909be..a018363fc 100644 --- a/Sources/Overlays/_Testing_Foundation/Attachments/Attachment+URL.swift +++ b/Sources/Overlays/_Testing_Foundation/Attachments/Attachment+URL.swift @@ -53,6 +53,7 @@ extension Attachment where AttachableValue == _AttachableURLWrapper { /// /// @Metadata { /// @Available(Swift, introduced: 6.2) + /// @Available(Xcode, introduced: 26.0) /// } public init( contentsOf url: URL, diff --git a/Sources/Overlays/_Testing_Foundation/Attachments/Data+Attachable.swift b/Sources/Overlays/_Testing_Foundation/Attachments/Data+Attachable.swift index ce7b719a9..56f058da3 100644 --- a/Sources/Overlays/_Testing_Foundation/Attachments/Data+Attachable.swift +++ b/Sources/Overlays/_Testing_Foundation/Attachments/Data+Attachable.swift @@ -14,10 +14,12 @@ public import Foundation /// @Metadata { /// @Available(Swift, introduced: 6.2) +/// @Available(Xcode, introduced: 26.0) /// } extension Data: Attachable { /// @Metadata { /// @Available(Swift, introduced: 6.2) + /// @Available(Xcode, introduced: 26.0) /// } public func withUnsafeBytes(for attachment: borrowing Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { try withUnsafeBytes(body) diff --git a/Sources/Testing/Attachments/Attachable.swift b/Sources/Testing/Attachments/Attachable.swift index be466940b..9ec3ce8ad 100644 --- a/Sources/Testing/Attachments/Attachable.swift +++ b/Sources/Testing/Attachments/Attachable.swift @@ -29,6 +29,7 @@ /// /// @Metadata { /// @Available(Swift, introduced: 6.2) +/// @Available(Xcode, introduced: 26.0) /// } public protocol Attachable: ~Copyable { /// An estimate of the number of bytes of memory needed to store this value as @@ -48,6 +49,7 @@ public protocol Attachable: ~Copyable { /// /// @Metadata { /// @Available(Swift, introduced: 6.2) + /// @Available(Xcode, introduced: 26.0) /// } var estimatedAttachmentByteCount: Int? { get } @@ -74,6 +76,7 @@ public protocol Attachable: ~Copyable { /// /// @Metadata { /// @Available(Swift, introduced: 6.2) + /// @Available(Xcode, introduced: 26.0) /// } borrowing func withUnsafeBytes(for attachment: borrowing Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R @@ -94,6 +97,7 @@ public protocol Attachable: ~Copyable { /// /// @Metadata { /// @Available(Swift, introduced: 6.2) + /// @Available(Xcode, introduced: 26.0) /// } borrowing func preferredName(for attachment: borrowing Attachment, basedOn suggestedName: String) -> String } diff --git a/Sources/Testing/Attachments/AttachableWrapper.swift b/Sources/Testing/Attachments/AttachableWrapper.swift index 81df52d4d..d4b1cbe05 100644 --- a/Sources/Testing/Attachments/AttachableWrapper.swift +++ b/Sources/Testing/Attachments/AttachableWrapper.swift @@ -24,12 +24,14 @@ /// /// @Metadata { /// @Available(Swift, introduced: 6.2) +/// @Available(Xcode, introduced: 26.0) /// } public protocol AttachableWrapper: Attachable, ~Copyable { /// The type of the underlying value represented by this type. /// /// @Metadata { /// @Available(Swift, introduced: 6.2) + /// @Available(Xcode, introduced: 26.0) /// } associatedtype Wrapped @@ -37,6 +39,7 @@ public protocol AttachableWrapper: Attachable, ~Copyable { /// /// @Metadata { /// @Available(Swift, introduced: 6.2) + /// @Available(Xcode, introduced: 26.0) /// } var wrappedValue: Wrapped { get } } diff --git a/Sources/Testing/Attachments/Attachment.swift b/Sources/Testing/Attachments/Attachment.swift index e9b98fb7b..f8d242c8c 100644 --- a/Sources/Testing/Attachments/Attachment.swift +++ b/Sources/Testing/Attachments/Attachment.swift @@ -21,6 +21,7 @@ private import _TestingInternals /// /// @Metadata { /// @Available(Swift, introduced: 6.2) +/// @Available(Xcode, introduced: 26.0) /// } public struct Attachment: ~Copyable where AttachableValue: Attachable & ~Copyable { /// Storage for ``attachableValue-7dyjv``. @@ -57,6 +58,7 @@ public struct Attachment: ~Copyable where AttachableValue: Atta /// /// @Metadata { /// @Available(Swift, introduced: 6.2) + /// @Available(Xcode, introduced: 26.0) /// } public var preferredName: String { let suggestedName = if let _preferredName, !_preferredName.isEmpty { @@ -100,6 +102,7 @@ extension Attachment where AttachableValue: ~Copyable { /// /// @Metadata { /// @Available(Swift, introduced: 6.2) + /// @Available(Xcode, introduced: 26.0) /// } public init(_ attachableValue: consuming AttachableValue, named preferredName: String? = nil, sourceLocation: SourceLocation = #_sourceLocation) { self._attachableValue = attachableValue @@ -194,6 +197,7 @@ extension Attachment where AttachableValue: ~Copyable { extension Attachment: CustomStringConvertible { /// @Metadata { /// @Available(Swift, introduced: 6.2) + /// @Available(Xcode, introduced: 26.0) /// } public var description: String { #""\#(preferredName)": \#(String(describingForTest: attachableValue))"# @@ -207,6 +211,7 @@ extension Attachment where AttachableValue: ~Copyable { /// /// @Metadata { /// @Available(Swift, introduced: 6.2) + /// @Available(Xcode, introduced: 26.0) /// } @_disfavoredOverload public var attachableValue: AttachableValue { _read { @@ -229,6 +234,7 @@ extension Attachment where AttachableValue: AttachableWrapper & ~Copyable { /// /// @Metadata { /// @Available(Swift, introduced: 6.2) + /// @Available(Xcode, introduced: 26.0) /// } public var attachableValue: AttachableValue.Wrapped { _read { @@ -259,6 +265,7 @@ extension Attachment where AttachableValue: Sendable & Copyable { /// /// @Metadata { /// @Available(Swift, introduced: 6.2) + /// @Available(Xcode, introduced: 26.0) /// } @_documentation(visibility: private) public static func record(_ attachment: consuming Self, sourceLocation: SourceLocation = #_sourceLocation) { @@ -291,6 +298,7 @@ extension Attachment where AttachableValue: Sendable & Copyable { /// /// @Metadata { /// @Available(Swift, introduced: 6.2) + /// @Available(Xcode, introduced: 26.0) /// } @_documentation(visibility: private) public static func record(_ attachableValue: consuming AttachableValue, named preferredName: String? = nil, sourceLocation: SourceLocation = #_sourceLocation) { @@ -318,6 +326,7 @@ extension Attachment where AttachableValue: ~Copyable { /// /// @Metadata { /// @Available(Swift, introduced: 6.2) + /// @Available(Xcode, introduced: 26.0) /// } public static func record(_ attachment: consuming Self, sourceLocation: SourceLocation = #_sourceLocation) { do { @@ -361,6 +370,7 @@ extension Attachment where AttachableValue: ~Copyable { /// /// @Metadata { /// @Available(Swift, introduced: 6.2) + /// @Available(Xcode, introduced: 26.0) /// } public static func record(_ attachableValue: consuming AttachableValue, named preferredName: String? = nil, sourceLocation: SourceLocation = #_sourceLocation) { record(Self(attachableValue, named: preferredName, sourceLocation: sourceLocation), sourceLocation: sourceLocation) @@ -389,6 +399,7 @@ extension Attachment where AttachableValue: ~Copyable { /// /// @Metadata { /// @Available(Swift, introduced: 6.2) + /// @Available(Xcode, introduced: 26.0) /// } @inlinable public borrowing func withUnsafeBytes(_ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { try attachableValue.withUnsafeBytes(for: self, body) diff --git a/Sources/Testing/ExitTests/ExitStatus.swift b/Sources/Testing/ExitTests/ExitStatus.swift index 0dd6d86ab..69c583543 100644 --- a/Sources/Testing/ExitTests/ExitStatus.swift +++ b/Sources/Testing/ExitTests/ExitStatus.swift @@ -21,6 +21,7 @@ private import _TestingInternals /// /// @Metadata { /// @Available(Swift, introduced: 6.2) +/// @Available(Xcode, introduced: 26.0) /// } #if SWT_NO_PROCESS_SPAWNING @available(*, unavailable, message: "Exit tests are not available on this platform.") @@ -55,6 +56,7 @@ public enum ExitStatus: Sendable { /// /// @Metadata { /// @Available(Swift, introduced: 6.2) + /// @Available(Xcode, introduced: 26.0) /// } case exitCode(_ exitCode: CInt) @@ -81,6 +83,7 @@ public enum ExitStatus: Sendable { /// /// @Metadata { /// @Available(Swift, introduced: 6.2) + /// @Available(Xcode, introduced: 26.0) /// } case signal(_ signal: CInt) } diff --git a/Sources/Testing/ExitTests/ExitTest.Condition.swift b/Sources/Testing/ExitTests/ExitTest.Condition.swift index f737d8cf6..42d929077 100644 --- a/Sources/Testing/ExitTests/ExitTest.Condition.swift +++ b/Sources/Testing/ExitTests/ExitTest.Condition.swift @@ -35,6 +35,7 @@ extension ExitTest { /// /// @Metadata { /// @Available(Swift, introduced: 6.2) + /// @Available(Xcode, introduced: 26.0) /// } public struct Condition: Sendable { /// An enumeration describing the possible conditions for an exit test. @@ -66,6 +67,7 @@ extension ExitTest.Condition { /// /// @Metadata { /// @Available(Swift, introduced: 6.2) + /// @Available(Xcode, introduced: 26.0) /// } public static var success: Self { Self(_kind: .success) @@ -78,6 +80,7 @@ extension ExitTest.Condition { /// /// @Metadata { /// @Available(Swift, introduced: 6.2) + /// @Available(Xcode, introduced: 26.0) /// } public static var failure: Self { Self(_kind: .failure) @@ -91,6 +94,7 @@ extension ExitTest.Condition { /// /// @Metadata { /// @Available(Swift, introduced: 6.2) + /// @Available(Xcode, introduced: 26.0) /// } public init(_ exitStatus: ExitStatus) { self.init(_kind: .exitStatus(exitStatus)) @@ -126,6 +130,7 @@ extension ExitTest.Condition { /// /// @Metadata { /// @Available(Swift, introduced: 6.2) + /// @Available(Xcode, introduced: 26.0) /// } public static func exitCode(_ exitCode: CInt) -> Self { #if !SWT_NO_EXIT_TESTS @@ -158,6 +163,7 @@ extension ExitTest.Condition { /// /// @Metadata { /// @Available(Swift, introduced: 6.2) + /// @Available(Xcode, introduced: 26.0) /// } public static func signal(_ signal: CInt) -> Self { #if !SWT_NO_EXIT_TESTS diff --git a/Sources/Testing/ExitTests/ExitTest.Result.swift b/Sources/Testing/ExitTests/ExitTest.Result.swift index ef70a3789..f2c57e205 100644 --- a/Sources/Testing/ExitTests/ExitTest.Result.swift +++ b/Sources/Testing/ExitTests/ExitTest.Result.swift @@ -21,12 +21,14 @@ extension ExitTest { /// /// @Metadata { /// @Available(Swift, introduced: 6.2) + /// @Available(Xcode, introduced: 26.0) /// } public struct Result: Sendable { /// The exit status reported by the process hosting the exit test. /// /// @Metadata { /// @Available(Swift, introduced: 6.2) + /// @Available(Xcode, introduced: 26.0) /// } public var exitStatus: ExitStatus @@ -57,6 +59,7 @@ extension ExitTest { /// /// @Metadata { /// @Available(Swift, introduced: 6.2) + /// @Available(Xcode, introduced: 26.0) /// } public var standardOutputContent: [UInt8] = [] @@ -87,6 +90,7 @@ extension ExitTest { /// /// @Metadata { /// @Available(Swift, introduced: 6.2) + /// @Available(Xcode, introduced: 26.0) /// } public var standardErrorContent: [UInt8] = [] diff --git a/Sources/Testing/ExitTests/ExitTest.swift b/Sources/Testing/ExitTests/ExitTest.swift index 9284310db..65945b13b 100644 --- a/Sources/Testing/ExitTests/ExitTest.swift +++ b/Sources/Testing/ExitTests/ExitTest.swift @@ -32,6 +32,7 @@ private import _TestingInternals /// /// @Metadata { /// @Available(Swift, introduced: 6.2) +/// @Available(Xcode, introduced: 26.0) /// } #if SWT_NO_EXIT_TESTS @available(*, unavailable, message: "Exit tests are not available on this platform.") @@ -170,6 +171,7 @@ extension ExitTest { /// /// @Metadata { /// @Available(Swift, introduced: 6.2) + /// @Available(Xcode, introduced: 26.0) /// } public static var current: ExitTest? { _read { diff --git a/Sources/Testing/Expectations/Expectation+Macro.swift b/Sources/Testing/Expectations/Expectation+Macro.swift index f85c7042b..fcad6e377 100644 --- a/Sources/Testing/Expectations/Expectation+Macro.swift +++ b/Sources/Testing/Expectations/Expectation+Macro.swift @@ -513,6 +513,7 @@ public macro require( /// /// @Metadata { /// @Available(Swift, introduced: 6.2) +/// @Available(Xcode, introduced: 26.0) /// } @freestanding(expression) @discardableResult @@ -559,6 +560,7 @@ public macro expect( /// /// @Metadata { /// @Available(Swift, introduced: 6.2) +/// @Available(Xcode, introduced: 26.0) /// } @freestanding(expression) @discardableResult diff --git a/Sources/Testing/Testing.docc/exit-testing.md b/Sources/Testing/Testing.docc/exit-testing.md index 06ab53dc9..bb4fccafd 100644 --- a/Sources/Testing/Testing.docc/exit-testing.md +++ b/Sources/Testing/Testing.docc/exit-testing.md @@ -12,6 +12,7 @@ See https://swift.org/CONTRIBUTORS.txt for Swift project authors @Metadata { @Available(Swift, introduced: 6.2) + @Available(Xcode, introduced: 26.0) } Use exit tests to test functionality that might cause a test process to exit. diff --git a/Sources/Testing/Traits/ConditionTrait.swift b/Sources/Testing/Traits/ConditionTrait.swift index 079b64d8e..d60cf7cce 100644 --- a/Sources/Testing/Traits/ConditionTrait.swift +++ b/Sources/Testing/Traits/ConditionTrait.swift @@ -72,6 +72,7 @@ public struct ConditionTrait: TestTrait, SuiteTrait { /// /// @Metadata { /// @Available(Swift, introduced: 6.2) + /// @Available(Xcode, introduced: 26.0) /// } public func evaluate() async throws -> Bool { switch kind { From 32782178a6a210e1eb399ed9cac3c6efc581cba3 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Tue, 10 Jun 2025 16:18:24 -0400 Subject: [PATCH 019/216] Use `posix_spawn_file_actions_adddup2()` to clear `FD_CLOEXEC`. (#1147) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On [FreeBSD](https://man.freebsd.org/cgi/man.cgi?query=posix_spawn_file_actions_adddup2), [OpenBSD](https://github.com/openbsd/src/blob/master/lib/libc/gen/posix_spawn.c#L155), [Android](https://android.googlesource.com/platform/bionic/+/master/libc/bionic/spawn.cpp#103), and [Glibc ≥ 2.29](https://sourceware.org/bugzilla/show_bug.cgi?id=23640), `posix_spawn_file_actions_adddup2()` automatically clears `FD_CLOEXEC` if the file descriptors passed to it are equal. Relying on this behaviour eliminates a race condition when spawning child processes. This functionality is standardized in [POSIX.1-2024](https://pubs.opengroup.org/onlinepubs/9799919799/) thanks to [Austin Group Defect #411](https://www.austingroupbugs.net/view.php?id=411). Some older Linuxes (Amazon Linux 2 in particular) don't have this functionality, so we do a runtime check of the Glibc version. This PR is a follow-up to #1145. Resolves #1140. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- Sources/Testing/ExitTests/ExitTest.swift | 9 -- Sources/Testing/ExitTests/SpawnProcess.swift | 50 ++++++--- Sources/Testing/Support/FileHandle.swift | 110 ++++++------------- Sources/Testing/Support/Versions.swift | 24 ++++ Sources/_TestingInternals/include/Includes.h | 4 + 5 files changed, 99 insertions(+), 98 deletions(-) diff --git a/Sources/Testing/ExitTests/ExitTest.swift b/Sources/Testing/ExitTests/ExitTest.swift index 65945b13b..6272cea93 100644 --- a/Sources/Testing/ExitTests/ExitTest.swift +++ b/Sources/Testing/ExitTests/ExitTest.swift @@ -803,15 +803,6 @@ extension ExitTest { childEnvironment["SWT_EXPERIMENTAL_CAPTURED_VALUES"] = capturedValuesEnvironmentVariable } -#if !SWT_TARGET_OS_APPLE - // Set inherited those file handles that the child process needs. On - // Darwin, this is a no-op because we use POSIX_SPAWN_CLOEXEC_DEFAULT. - try stdoutWriteEnd?.setInherited(true) - try stderrWriteEnd?.setInherited(true) - try backChannelWriteEnd.setInherited(true) - try capturedValuesReadEnd.setInherited(true) -#endif - // Spawn the child process. let processID = try withUnsafePointer(to: backChannelWriteEnd) { backChannelWriteEnd in try withUnsafePointer(to: capturedValuesReadEnd) { capturedValuesReadEnd in diff --git a/Sources/Testing/ExitTests/SpawnProcess.swift b/Sources/Testing/ExitTests/SpawnProcess.swift index 8f8d95db6..647e62dd9 100644 --- a/Sources/Testing/ExitTests/SpawnProcess.swift +++ b/Sources/Testing/ExitTests/SpawnProcess.swift @@ -123,11 +123,27 @@ func spawnExecutable( guard let fd else { throw SystemError(description: "A child process cannot inherit a file handle without an associated file descriptor. Please file a bug report at https://github.com/swiftlang/swift-testing/issues/new") } - if let standardFD { + if let standardFD, standardFD != fd { _ = posix_spawn_file_actions_adddup2(fileActions, fd, standardFD) } else { #if SWT_TARGET_OS_APPLE _ = posix_spawn_file_actions_addinherit_np(fileActions, fd) +#else + // posix_spawn_file_actions_adddup2() will automatically clear + // FD_CLOEXEC after forking but before execing even if the old and + // new file descriptors are equal. This behavior is supported by + // Glibc ≥ 2.29, FreeBSD, OpenBSD, and Android (Bionic) and is + // standardized in POSIX.1-2024 (see https://pubs.opengroup.org/onlinepubs/9799919799/functions/posix_spawn_file_actions_adddup2.html + // and https://www.austingroupbugs.net/view.php?id=411). + _ = posix_spawn_file_actions_adddup2(fileActions, fd, fd) +#if canImport(Glibc) + if _slowPath(glibcVersion.major < 2 || (glibcVersion.major == 2 && glibcVersion.minor < 29)) { + // This system is using an older version of glibc that does not + // implement FD_CLOEXEC clearing in posix_spawn_file_actions_adddup2(), + // so we must clear it here in the parent process. + try setFD_CLOEXEC(false, onFileDescriptor: fd) + } +#endif #endif highestFD = max(highestFD, fd) } @@ -156,8 +172,6 @@ func spawnExecutable( #if !SWT_NO_DYNAMIC_LINKING // This platform doesn't have POSIX_SPAWN_CLOEXEC_DEFAULT, but we can at // least close all file descriptors higher than the highest inherited one. - // We are assuming here that the caller didn't set FD_CLOEXEC on any of - // these file descriptors. _ = _posix_spawn_file_actions_addclosefrom_np?(fileActions, highestFD + 1) #endif #elseif os(FreeBSD) @@ -216,36 +230,42 @@ func spawnExecutable( } #elseif os(Windows) return try _withStartupInfoEx(attributeCount: 1) { startupInfo in - func inherit(_ fileHandle: borrowing FileHandle, as outWindowsHANDLE: inout HANDLE?) throws { + func inherit(_ fileHandle: borrowing FileHandle) throws -> HANDLE? { try fileHandle.withUnsafeWindowsHANDLE { windowsHANDLE in guard let windowsHANDLE else { throw SystemError(description: "A child process cannot inherit a file handle without an associated Windows handle. Please file a bug report at https://github.com/swiftlang/swift-testing/issues/new") } - outWindowsHANDLE = windowsHANDLE + + // Ensure the file handle can be inherited by the child process. + guard SetHandleInformation(windowsHANDLE, DWORD(HANDLE_FLAG_INHERIT), DWORD(HANDLE_FLAG_INHERIT)) else { + throw Win32Error(rawValue: GetLastError()) + } + + return windowsHANDLE } } - func inherit(_ fileHandle: borrowing FileHandle?, as outWindowsHANDLE: inout HANDLE?) throws { + func inherit(_ fileHandle: borrowing FileHandle?) throws -> HANDLE? { if fileHandle != nil { - try inherit(fileHandle!, as: &outWindowsHANDLE) + return try inherit(fileHandle!) } else { - outWindowsHANDLE = nil + return nil } } // Forward standard I/O streams. - try inherit(standardInput, as: &startupInfo.pointee.StartupInfo.hStdInput) - try inherit(standardOutput, as: &startupInfo.pointee.StartupInfo.hStdOutput) - try inherit(standardError, as: &startupInfo.pointee.StartupInfo.hStdError) + startupInfo.pointee.StartupInfo.hStdInput = try inherit(standardInput) + startupInfo.pointee.StartupInfo.hStdOutput = try inherit(standardOutput) + startupInfo.pointee.StartupInfo.hStdError = try inherit(standardError) startupInfo.pointee.StartupInfo.dwFlags |= STARTF_USESTDHANDLES // Ensure standard I/O streams and any explicitly added file handles are // inherited by the child process. var inheritedHandles = [HANDLE?](repeating: nil, count: additionalFileHandles.count + 3) - try inherit(standardInput, as: &inheritedHandles[0]) - try inherit(standardOutput, as: &inheritedHandles[1]) - try inherit(standardError, as: &inheritedHandles[2]) + inheritedHandles[0] = startupInfo.pointee.StartupInfo.hStdInput + inheritedHandles[1] = startupInfo.pointee.StartupInfo.hStdOutput + inheritedHandles[2] = startupInfo.pointee.StartupInfo.hStdError for i in 0 ..< additionalFileHandles.count { - try inherit(additionalFileHandles[i].pointee, as: &inheritedHandles[i + 3]) + inheritedHandles[i + 3] = try inherit(additionalFileHandles[i].pointee) } inheritedHandles = inheritedHandles.compactMap(\.self) diff --git a/Sources/Testing/Support/FileHandle.swift b/Sources/Testing/Support/FileHandle.swift index 4e3c17372..1c5447460 100644 --- a/Sources/Testing/Support/FileHandle.swift +++ b/Sources/Testing/Support/FileHandle.swift @@ -108,8 +108,7 @@ struct FileHandle: ~Copyable, Sendable { /// /// By default, the resulting file handle is not inherited by any child /// processes (that is, `FD_CLOEXEC` is set on POSIX-like systems and - /// `HANDLE_FLAG_INHERIT` is cleared on Windows.) To make it inheritable, call - /// ``setInherited()``. + /// `HANDLE_FLAG_INHERIT` is cleared on Windows.). init(forReadingAtPath path: String) throws { try self.init(atPath: path, mode: "reb") } @@ -123,8 +122,7 @@ struct FileHandle: ~Copyable, Sendable { /// /// By default, the resulting file handle is not inherited by any child /// processes (that is, `FD_CLOEXEC` is set on POSIX-like systems and - /// `HANDLE_FLAG_INHERIT` is cleared on Windows.) To make it inheritable, call - /// ``setInherited()``. + /// `HANDLE_FLAG_INHERIT` is cleared on Windows.). init(forWritingAtPath path: String) throws { try self.init(atPath: path, mode: "web") } @@ -492,8 +490,7 @@ extension FileHandle { /// /// By default, the resulting file handles are not inherited by any child /// processes (that is, `FD_CLOEXEC` is set on POSIX-like systems and - /// `HANDLE_FLAG_INHERIT` is cleared on Windows.) To make them inheritable, - /// call ``setInherited()``. + /// `HANDLE_FLAG_INHERIT` is cleared on Windows.). static func makePipe(readEnd: inout FileHandle?, writeEnd: inout FileHandle?) throws { #if !os(Windows) var pipe2Called = false @@ -533,8 +530,8 @@ extension FileHandle { if !pipe2Called { // pipe2() is not available. Use pipe() instead and simulate O_CLOEXEC // to the best of our ability. - try _setFileDescriptorInherited(fdReadEnd, false) - try _setFileDescriptorInherited(fdWriteEnd, false) + try setFD_CLOEXEC(true, onFileDescriptor: fdReadEnd) + try setFD_CLOEXEC(true, onFileDescriptor: fdWriteEnd) } #endif @@ -612,72 +609,6 @@ extension FileHandle { #endif } #endif - -#if SWT_TARGET_OS_APPLE || os(Linux) || os(FreeBSD) || os(OpenBSD) || os(Android) - /// Set whether or not the given file descriptor is inherited by child processes. - /// - /// - Parameters: - /// - fd: The file descriptor. - /// - inherited: Whether or not `fd` is inherited by child processes - /// (ignoring overriding functionality such as Apple's - /// `POSIX_SPAWN_CLOEXEC_DEFAULT` flag.) - /// - /// - Throws: Any error that occurred while setting the flag. - private static func _setFileDescriptorInherited(_ fd: CInt, _ inherited: Bool) throws { - switch swt_getfdflags(fd) { - case -1: - // An error occurred reading the flags for this file descriptor. - throw CError(rawValue: swt_errno()) - case let oldValue: - let newValue = if inherited { - oldValue & ~FD_CLOEXEC - } else { - oldValue | FD_CLOEXEC - } - if oldValue == newValue { - // No need to make a second syscall as nothing has changed. - return - } - if -1 == swt_setfdflags(fd, newValue) { - // An error occurred setting the flags for this file descriptor. - throw CError(rawValue: swt_errno()) - } - } - } -#endif - - /// Set whether or not this file handle is inherited by child processes. - /// - /// - Parameters: - /// - inherited: Whether or not this file handle is inherited by child - /// processes (ignoring overriding functionality such as Apple's - /// `POSIX_SPAWN_CLOEXEC_DEFAULT` flag.) - /// - /// - Throws: Any error that occurred while setting the flag. - func setInherited(_ inherited: Bool) throws { -#if SWT_TARGET_OS_APPLE || os(Linux) || os(FreeBSD) || os(OpenBSD) || os(Android) - try withUnsafePOSIXFileDescriptor { fd in - guard let fd else { - throw SystemError(description: "Cannot set whether a file handle is inherited unless it is backed by a file descriptor. Please file a bug report at https://github.com/swiftlang/swift-testing/issues/new") - } - try withLock { - try Self._setFileDescriptorInherited(fd, inherited) - } - } -#elseif os(Windows) - return try withUnsafeWindowsHANDLE { handle in - guard let handle else { - throw SystemError(description: "Cannot set whether a file handle is inherited unless it is backed by a Windows file handle. Please file a bug report at https://github.com/swiftlang/swift-testing/issues/new") - } - let newValue = inherited ? DWORD(HANDLE_FLAG_INHERIT) : 0 - guard SetHandleInformation(handle, DWORD(HANDLE_FLAG_INHERIT), newValue) else { - throw Win32Error(rawValue: GetLastError()) - } - } -#else -#warning("Platform-specific implementation missing: cannot set whether a file handle is inherited") -#endif - } } // MARK: - General path utilities @@ -757,4 +688,35 @@ func canonicalizePath(_ path: String) -> String? { return nil #endif } + +#if SWT_TARGET_OS_APPLE || os(Linux) || os(FreeBSD) || os(OpenBSD) || os(Android) +/// Set the given file descriptor's `FD_CLOEXEC` flag. +/// +/// - Parameters: +/// - flag: The new value of `fd`'s `FD_CLOEXEC` flag. +/// - fd: The file descriptor. +/// +/// - Throws: Any error that occurred while setting the flag. +func setFD_CLOEXEC(_ flag: Bool, onFileDescriptor fd: CInt) throws { + switch swt_getfdflags(fd) { + case -1: + // An error occurred reading the flags for this file descriptor. + throw CError(rawValue: swt_errno()) + case let oldValue: + let newValue = if flag { + oldValue & ~FD_CLOEXEC + } else { + oldValue | FD_CLOEXEC + } + if oldValue == newValue { + // No need to make a second syscall as nothing has changed. + return + } + if -1 == swt_setfdflags(fd, newValue) { + // An error occurred setting the flags for this file descriptor. + throw CError(rawValue: swt_errno()) + } + } +} +#endif #endif diff --git a/Sources/Testing/Support/Versions.swift b/Sources/Testing/Support/Versions.swift index 1eb7f4e48..1229e80b0 100644 --- a/Sources/Testing/Support/Versions.swift +++ b/Sources/Testing/Support/Versions.swift @@ -153,6 +153,30 @@ let swiftStandardLibraryVersion: String = { return "unknown" }() +#if canImport(Glibc) +/// The (runtime, not compile-time) version of glibc in use on this system. +/// +/// This value is not part of the public interface of the testing library. +let glibcVersion: (major: Int, minor: Int) = { + // Default to the statically available version number if the function call + // fails for some reason. + var major = Int(clamping: __GLIBC__) + var minor = Int(clamping: __GLIBC_MINOR__) + + if let strVersion = gnu_get_libc_version() { + withUnsafeMutablePointer(to: &major) { major in + withUnsafeMutablePointer(to: &minor) { minor in + withVaList([major, minor]) { args in + _ = vsscanf(strVersion, "%zd.%zd", args) + } + } + } + } + + return (major, minor) +}() +#endif + // MARK: - sysctlbyname() Wrapper #if !SWT_NO_SYSCTL && SWT_TARGET_OS_APPLE diff --git a/Sources/_TestingInternals/include/Includes.h b/Sources/_TestingInternals/include/Includes.h index bfc87b001..1b95151cb 100644 --- a/Sources/_TestingInternals/include/Includes.h +++ b/Sources/_TestingInternals/include/Includes.h @@ -53,6 +53,10 @@ #include #endif +#if __has_include() +#include +#endif + #if __has_include() && !defined(__wasi__) #include #endif From 6e462ad31397980cfdaf25220f973d64565ce844 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Thu, 12 Jun 2025 17:50:51 -0400 Subject: [PATCH 020/216] Synthesize display names for de facto suites with raw identifiers. (#1105) This PR ensures that suite types that don't have the `@Suite` attribute but which _do_ have raw identifiers for names are correctly given display names the same way those with `@Suite` would be. This PR also ensures that we transform spaces in raw identifiers after they are demangled by the runtime--namely, the runtime replaces ASCII spaces (as typed by the user) with Unicode non-breaking spaces (which aren't otherwise valid in raw identifers) in order to avoid issues with existing uses of spaces in demangled names. We want to make sure that identifiers as presented to the user match what the user has typed, so we need to transform these spaces back. No changes in this area are needed for display names derived during macro expansion because we do the relevant work based on the source text which still has the original ASCII spaces. This PR also deletes the "`raw$`" hack that I put in place when originally implementing raw identifier support as the entire toolchain supports them now. Resolves #1104. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- .../Testing/Parameterization/TypeInfo.swift | 59 ++++++++++++++++--- Sources/Testing/Test.swift | 9 ++- .../Additions/TokenSyntaxAdditions.swift | 6 -- .../TestDeclarationMacroTests.swift | 12 ++-- Tests/TestingTests/MiscellaneousTests.swift | 19 ++++-- 5 files changed, 79 insertions(+), 26 deletions(-) diff --git a/Sources/Testing/Parameterization/TypeInfo.swift b/Sources/Testing/Parameterization/TypeInfo.swift index b0ed814b9..300004e16 100644 --- a/Sources/Testing/Parameterization/TypeInfo.swift +++ b/Sources/Testing/Parameterization/TypeInfo.swift @@ -142,6 +142,35 @@ func rawIdentifierAwareSplit(_ string: S, separator: Character, maxSplits: In } extension TypeInfo { + /// Replace any non-breaking spaces in the given string with normal spaces. + /// + /// - Parameters: + /// - rawIdentifier: The string to rewrite. + /// + /// - Returns: A copy of `rawIdentifier` with non-breaking spaces (`U+00A0`) + /// replaced with normal spaces (`U+0020`). + /// + /// When the Swift runtime demangles a raw identifier, it [replaces](https://github.com/swiftlang/swift/blob/d033eec1aa427f40dcc38679d43b83d9dbc06ae7/lib/Basic/Mangler.cpp#L250) + /// normal ASCII spaces with non-breaking spaces to maintain compatibility + /// with historical usages of spaces in mangled name forms. Non-breaking + /// spaces are not otherwise valid in raw identifiers, so this transformation + /// is reversible. + private static func _rewriteNonBreakingSpacesAsASCIISpaces(in rawIdentifier: some StringProtocol) -> String? { + let nbsp = "\u{00A0}" as UnicodeScalar + + // If there are no non-breaking spaces in the string, exit early to avoid + // any further allocations. + let unicodeScalars = rawIdentifier.unicodeScalars + guard unicodeScalars.contains(nbsp) else { + return nil + } + + // Replace non-breaking spaces, then construct a new string from the + // resulting sequence. + let result = unicodeScalars.lazy.map { $0 == nbsp ? " " : $0 } + return String(String.UnicodeScalarView(result)) + } + /// An in-memory cache of fully-qualified type name components. private static let _fullyQualifiedNameComponentsCache = Locked<[ObjectIdentifier: [String]]>() @@ -166,12 +195,21 @@ extension TypeInfo { components[0] = moduleName } - // If a type is private or embedded in a function, its fully qualified - // name may include "(unknown context at $xxxxxxxx)" as a component. Strip - // those out as they're uninteresting to us. - components = components.filter { !$0.starts(with: "(unknown context at") } - - return components.map(String.init) + return components.lazy + .filter { component in + // If a type is private or embedded in a function, its fully qualified + // name may include "(unknown context at $xxxxxxxx)" as a component. + // Strip those out as they're uninteresting to us. + !component.starts(with: "(unknown context at") + }.map { component in + // Replace non-breaking spaces with spaces. See the helper function's + // documentation for more information. + if let component = _rewriteNonBreakingSpacesAsASCIISpaces(in: component) { + component[...] + } else { + component + } + }.map(String.init) } /// The complete name of this type, with the names of all referenced types @@ -242,9 +280,14 @@ extension TypeInfo { public var unqualifiedName: String { switch _kind { case let .type(type): - String(describing: type) + // Replace non-breaking spaces with spaces. See the helper function's + // documentation for more information. + var result = String(describing: type) + result = Self._rewriteNonBreakingSpacesAsASCIISpaces(in: result) ?? result + + return result case let .nameOnly(_, unqualifiedName, _): - unqualifiedName + return unqualifiedName } } diff --git a/Sources/Testing/Test.swift b/Sources/Testing/Test.swift index 738daf72d..5f2ac2406 100644 --- a/Sources/Testing/Test.swift +++ b/Sources/Testing/Test.swift @@ -209,8 +209,13 @@ public struct Test: Sendable { containingTypeInfo: TypeInfo, isSynthesized: Bool = false ) { - self.name = containingTypeInfo.unqualifiedName - self.displayName = displayName + let name = containingTypeInfo.unqualifiedName + self.name = name + if let displayName { + self.displayName = displayName + } else if isSynthesized && name.count > 2 && name.first == "`" && name.last == "`" { + self.displayName = String(name.dropFirst().dropLast()) + } self.traits = traits self.sourceLocation = sourceLocation self.containingTypeInfo = containingTypeInfo diff --git a/Sources/TestingMacros/Support/Additions/TokenSyntaxAdditions.swift b/Sources/TestingMacros/Support/Additions/TokenSyntaxAdditions.swift index 447a18dee..26b9d1923 100644 --- a/Sources/TestingMacros/Support/Additions/TokenSyntaxAdditions.swift +++ b/Sources/TestingMacros/Support/Additions/TokenSyntaxAdditions.swift @@ -39,12 +39,6 @@ extension TokenSyntax { return textWithoutBackticks } - // TODO: remove this mock path once the toolchain fully supports raw IDs. - let mockPrefix = "__raw__$" - if backticksRemoved, textWithoutBackticks.starts(with: mockPrefix) { - return String(textWithoutBackticks.dropFirst(mockPrefix.count)) - } - return nil } } diff --git a/Tests/TestingMacrosTests/TestDeclarationMacroTests.swift b/Tests/TestingMacrosTests/TestDeclarationMacroTests.swift index 13ae3d180..6c04eb9eb 100644 --- a/Tests/TestingMacrosTests/TestDeclarationMacroTests.swift +++ b/Tests/TestingMacrosTests/TestDeclarationMacroTests.swift @@ -217,17 +217,17 @@ struct TestDeclarationMacroTests { ] ), - #"@Test("Goodbye world") func `__raw__$helloWorld`()"#: + #"@Test("Goodbye world") func `hello world`()"#: ( - message: "Attribute 'Test' specifies display name 'Goodbye world' for function with implicit display name 'helloWorld'", + message: "Attribute 'Test' specifies display name 'Goodbye world' for function with implicit display name 'hello world'", fixIts: [ ExpectedFixIt( message: "Remove 'Goodbye world'", changes: [.replace(oldSourceCode: #""Goodbye world""#, newSourceCode: "")] ), ExpectedFixIt( - message: "Rename '__raw__$helloWorld'", - changes: [.replace(oldSourceCode: "`__raw__$helloWorld`", newSourceCode: "\(EditorPlaceholderExprSyntax("name"))")] + message: "Rename 'hello world'", + changes: [.replace(oldSourceCode: "`hello world`", newSourceCode: "\(EditorPlaceholderExprSyntax("name"))")] ), ] ), @@ -281,10 +281,10 @@ struct TestDeclarationMacroTests { @Test("Raw function name components") func rawFunctionNameComponents() throws { let decl = """ - func `__raw__$hello`(`__raw__$world`: T, etc: U, `blah`: V) {} + func `hello there`(`world of mine`: T, etc: U, `blah`: V) {} """ as DeclSyntax let functionDecl = try #require(decl.as(FunctionDeclSyntax.self)) - #expect(functionDecl.completeName.trimmedDescription == "`hello`(`world`:etc:blah:)") + #expect(functionDecl.completeName.trimmedDescription == "`hello there`(`world of mine`:etc:blah:)") } @Test("Warning diagnostics emitted on API misuse", diff --git a/Tests/TestingTests/MiscellaneousTests.swift b/Tests/TestingTests/MiscellaneousTests.swift index 9ae326afe..b895f6c1b 100644 --- a/Tests/TestingTests/MiscellaneousTests.swift +++ b/Tests/TestingTests/MiscellaneousTests.swift @@ -297,15 +297,26 @@ struct MiscellaneousTests { #expect(testType.displayName == "Named Sendable test type") } - @Test func `__raw__$raw_identifier_provides_a_display_name`() throws { +#if compiler(>=6.2) && hasFeature(RawIdentifiers) + @Test func `Test with raw identifier gets a display name`() throws { let test = try #require(Test.current) - #expect(test.displayName == "raw_identifier_provides_a_display_name") - #expect(test.name == "`raw_identifier_provides_a_display_name`()") + #expect(test.displayName == "Test with raw identifier gets a display name") + #expect(test.name == "`Test with raw identifier gets a display name`()") let id = test.id #expect(id.moduleName == "TestingTests") - #expect(id.nameComponents == ["MiscellaneousTests", "`raw_identifier_provides_a_display_name`()"]) + #expect(id.nameComponents == ["MiscellaneousTests", "`Test with raw identifier gets a display name`()"]) } + @Test func `Suite type with raw identifier gets a display name`() throws { + struct `Suite With De Facto Display Name` {} + let typeInfo = TypeInfo(describing: `Suite With De Facto Display Name`.self) + let suite = Test(traits: [], sourceLocation: #_sourceLocation, containingTypeInfo: typeInfo, isSynthesized: true) + #expect(suite.name == "`Suite With De Facto Display Name`") + let displayName = try #require(suite.displayName) + #expect(displayName == "Suite With De Facto Display Name") + } +#endif + @Test("Free functions are runnable") func freeFunction() async throws { await Test(testFunction: freeSyncFunction).run() From 61a01cb30c7e862d4ff1689a470f61d88e9e680a Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Thu, 12 Jun 2025 19:34:37 -0400 Subject: [PATCH 021/216] Have `DiscoverableAsTestContent` enumeration produce `some Sequence` instead of `AnySequence`. (#1122) This PR changes how `DiscoverableAsTestContent` enumeration works so that we can return `some Sequence` instead of `AnySequence`. We do so by removing the `~Copyable` constraint on the protocol, which subsequently causes the compiler to get confused and crash trying to represent `some Sequence>` where `T: DiscoverableAsTestContent`. The only supported/allowed consumers of the `DiscoverableAsTestContent` protocol are Swift Testing and the experimental Playgrounds package, neither of which uses (or needs to use) a move-only type here. Earlier, `ExitTest` conformed to `DiscoverableAsTestContent`, but this was changed and is no longer necessary. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- Sources/Testing/Test+Discovery+Legacy.swift | 4 +-- .../DiscoverableAsTestContent.swift | 4 +-- .../_TestDiscovery/TestContentRecord.swift | 28 ++++++------------- 3 files changed, 12 insertions(+), 24 deletions(-) diff --git a/Sources/Testing/Test+Discovery+Legacy.swift b/Sources/Testing/Test+Discovery+Legacy.swift index 8ff878338..cafc55a4e 100644 --- a/Sources/Testing/Test+Discovery+Legacy.swift +++ b/Sources/Testing/Test+Discovery+Legacy.swift @@ -24,14 +24,14 @@ public protocol __TestContentRecordContainer { nonisolated static var __testContentRecord: __TestContentRecord { get } } -extension DiscoverableAsTestContent where Self: ~Copyable { +extension DiscoverableAsTestContent { /// Get all test content of this type known to Swift and found in the current /// process using the legacy discovery mechanism. /// /// - Returns: A sequence of instances of ``TestContentRecord``. Only test /// content records matching this ``TestContent`` type's requirements are /// included in the sequence. - static func allTypeMetadataBasedTestContentRecords() -> AnySequence> { + static func allTypeMetadataBasedTestContentRecords() -> some Sequence> { return allTypeMetadataBasedTestContentRecords { type, buffer in guard let type = type as? any __TestContentRecordContainer.Type else { return false diff --git a/Sources/_TestDiscovery/DiscoverableAsTestContent.swift b/Sources/_TestDiscovery/DiscoverableAsTestContent.swift index d4b15f8db..16369f82a 100644 --- a/Sources/_TestDiscovery/DiscoverableAsTestContent.swift +++ b/Sources/_TestDiscovery/DiscoverableAsTestContent.swift @@ -16,7 +16,7 @@ /// because they may be discovered within any isolation context or within /// multiple isolation contexts running concurrently. @_spi(Experimental) @_spi(ForToolsIntegrationOnly) -public protocol DiscoverableAsTestContent: Sendable, ~Copyable { +public protocol DiscoverableAsTestContent: Sendable { /// The value of the `kind` field in test content records associated with this /// type. /// @@ -49,7 +49,7 @@ public protocol DiscoverableAsTestContent: Sendable, ~Copyable { } #if !SWT_NO_LEGACY_TEST_DISCOVERY -extension DiscoverableAsTestContent where Self: ~Copyable { +extension DiscoverableAsTestContent { public static var _testContentTypeNameHint: String { "__🟡$" } diff --git a/Sources/_TestDiscovery/TestContentRecord.swift b/Sources/_TestDiscovery/TestContentRecord.swift index 9224fc2ea..df868c975 100644 --- a/Sources/_TestDiscovery/TestContentRecord.swift +++ b/Sources/_TestDiscovery/TestContentRecord.swift @@ -44,7 +44,7 @@ private typealias _TestContentRecord = ( reserved2: UInt ) -extension DiscoverableAsTestContent where Self: ~Copyable { +extension DiscoverableAsTestContent { /// Check that the layout of this structure in memory matches its expected /// layout in the test content section. /// @@ -64,7 +64,7 @@ extension DiscoverableAsTestContent where Self: ~Copyable { /// ``DiscoverableAsTestContent/allTestContentRecords()`` on a type that /// conforms to ``DiscoverableAsTestContent``. @_spi(Experimental) @_spi(ForToolsIntegrationOnly) -public struct TestContentRecord where T: DiscoverableAsTestContent & ~Copyable { +public struct TestContentRecord where T: DiscoverableAsTestContent { /// The base address of the image containing this instance, if known. /// /// The type of this pointer is platform-dependent: @@ -229,24 +229,19 @@ extension TestContentRecord: CustomStringConvertible { // MARK: - Enumeration of test content records -extension DiscoverableAsTestContent where Self: ~Copyable { +extension DiscoverableAsTestContent { /// Get all test content of this type known to Swift and found in the current /// process. /// /// - Returns: A sequence of instances of ``TestContentRecord``. Only test /// content records matching this ``TestContent`` type's requirements are /// included in the sequence. - /// - /// @Comment { - /// - Bug: This function returns an instance of `AnySequence` instead of an - /// opaque type due to a compiler crash. ([143080508](rdar://143080508)) - /// } - public static func allTestContentRecords() -> AnySequence> { + public static func allTestContentRecords() -> some Sequence> { validateMemoryLayout() let kind = testContentKind.rawValue - let result = SectionBounds.all(.testContent).lazy.flatMap { sb in + return SectionBounds.all(.testContent).lazy.flatMap { sb in sb.buffer.withMemoryRebound(to: _TestContentRecord.self) { records in (0 ..< records.count).lazy .map { (records.baseAddress! + $0) as UnsafePointer<_TestContentRecord> } @@ -254,7 +249,6 @@ extension DiscoverableAsTestContent where Self: ~Copyable { .map { TestContentRecord(imageAddress: sb.imageAddress, recordAddress: $0) } } } - return AnySequence(result) } } @@ -263,7 +257,7 @@ extension DiscoverableAsTestContent where Self: ~Copyable { private import _TestingInternals -extension DiscoverableAsTestContent where Self: ~Copyable { +extension DiscoverableAsTestContent { /// Get all test content of this type known to Swift and found in the current /// process using the legacy discovery mechanism. /// @@ -277,15 +271,10 @@ extension DiscoverableAsTestContent where Self: ~Copyable { /// - Returns: A sequence of instances of ``TestContentRecord``. Only test /// content records matching this ``TestContent`` type's requirements are /// included in the sequence. - /// - /// @Comment { - /// - Bug: This function returns an instance of `AnySequence` instead of an - /// opaque type due to a compiler crash. ([143080508](rdar://143080508)) - /// } @available(swift, deprecated: 100000.0, message: "Do not adopt this functionality in new code. It will be removed in a future release.") public static func allTypeMetadataBasedTestContentRecords( loadingWith loader: @escaping @Sendable (Any.Type, UnsafeMutableRawBufferPointer) -> Bool - ) -> AnySequence> { + ) -> some Sequence> { validateMemoryLayout() let typeNameHint = _testContentTypeNameHint @@ -300,7 +289,7 @@ extension DiscoverableAsTestContent where Self: ~Copyable { } } - let result = SectionBounds.all(.typeMetadata).lazy.flatMap { sb in + return SectionBounds.all(.typeMetadata).lazy.flatMap { sb in stride(from: 0, to: sb.buffer.count, by: SWTTypeMetadataRecordByteCount).lazy .map { sb.buffer.baseAddress! + $0 } .compactMap { swt_getType(fromTypeMetadataRecord: $0, ifNameContains: typeNameHint) } @@ -309,7 +298,6 @@ extension DiscoverableAsTestContent where Self: ~Copyable { .filter { $0.kind == kind } .map { TestContentRecord(imageAddress: sb.imageAddress, record: $0) } } - return AnySequence(result) } } #endif From eac4833c11eba5ba7fd926cae5acebd0643f53d9 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Thu, 12 Jun 2025 19:34:59 -0400 Subject: [PATCH 022/216] Improve the diagnostics for a bad exit test capture. (#1146) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR improves the diagnostics presented at compile time when an exit test captures an unsupported value. For example, given the following (bad) exit test: ```swift struct NonCodableValue {} let x = NonCodableValue() await #expect(processExitsWith: .success) { [x = x as NonCodableValue] in _ = x } ``` We currently get diagnostics of the form: > 🛑 Global function '__checkClosureCall(identifiedBy:encodingCapturedValues:processExitsWith:observing:performing:expression:comments:isRequired:isolation:sourceLocation:)' requires that 'NonCodableValue' conform to 'Decodable' > 🛑 Global function '__checkClosureCall(identifiedBy:encodingCapturedValues:processExitsWith:observing:performing:expression:comments:isRequired:isolation:sourceLocation:)' requires that 'NonCodableValue' conform to 'Encodable' > ⚠️ No 'async' operations occur within 'await' expression None of which actually tell the developer (clearly) what's wrong. With this PR, we instead get: > 🛑 Type of captured value 'x' must conform to 'Sendable' and 'Codable' (from macro '__capturedValue') Much better! The diagnostic is attributed to the temporary file containing the expansion of `#expect()` rather than to the original source file, but I've opened [an issue](https://github.com/swiftlang/swift-syntax/issues/3085) against swift-syntax with a fix in mind. Even with the misattribution, this diagnostic is still an improvement, yeah? > [!NOTE] > Exit test value capture remains an experimental feature. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- Sources/Testing/ExitTests/ExitTest.swift | 21 ++++++++ .../Expectations/Expectation+Macro.swift | 34 ++++++++++++ Sources/TestingMacros/CMakeLists.txt | 1 + Sources/TestingMacros/ConditionMacro.swift | 2 +- .../ExitTestCapturedValueMacro.swift | 54 +++++++++++++++++++ .../Support/ClosureCaptureListParsing.swift | 9 +++- .../Support/DiagnosticMessage.swift | 18 +++++++ Sources/TestingMacros/TestingMacrosMain.swift | 2 + Tests/TestingTests/ExitTestTests.swift | 24 +++++++++ 9 files changed, 162 insertions(+), 3 deletions(-) create mode 100644 Sources/TestingMacros/ExitTestCapturedValueMacro.swift diff --git a/Sources/Testing/ExitTests/ExitTest.swift b/Sources/Testing/ExitTests/ExitTest.swift index 6272cea93..c5579981e 100644 --- a/Sources/Testing/ExitTests/ExitTest.swift +++ b/Sources/Testing/ExitTests/ExitTest.swift @@ -365,6 +365,27 @@ extension ExitTest { outValue.initializeMemory(as: Record.self, to: record) return true } + + /// Attempt to store an invalid exit test into the given memory. + /// + /// This overload of `__store()` is provided to suppress diagnostics when a + /// value of an unsupported type is captured as an argument of `body`. It + /// always terminates the current process. + /// + /// - Warning: This function is used to implement the + /// `#expect(processExitsWith:)` macro. Do not use it directly. +#if compiler(>=6.2) + @safe +#endif + public static func __store( + _ id: (UInt64, UInt64, UInt64, UInt64), + _ body: T, + into outValue: UnsafeMutableRawPointer, + asTypeAt typeAddress: UnsafeRawPointer, + withHintAt hintAddress: UnsafeRawPointer? = nil + ) -> CBool { + fatalError("Unimplemented") + } } @_spi(ForToolsIntegrationOnly) diff --git a/Sources/Testing/Expectations/Expectation+Macro.swift b/Sources/Testing/Expectations/Expectation+Macro.swift index fcad6e377..973efde53 100644 --- a/Sources/Testing/Expectations/Expectation+Macro.swift +++ b/Sources/Testing/Expectations/Expectation+Macro.swift @@ -574,3 +574,37 @@ public macro require( sourceLocation: SourceLocation = #_sourceLocation, performing expression: @escaping @Sendable @convention(thin) () async throws -> Void ) -> ExitTest.Result = #externalMacro(module: "TestingMacros", type: "ExitTestRequireMacro") + +/// Capture a sendable and codable value to pass to an exit test. +/// +/// - Parameters: +/// - value: The captured value. +/// - name: The name of the capture list item corresponding to `value`. +/// +/// - Returns: `value` verbatim. +/// +/// - Warning: This macro is used to implement the `#expect(processExitsWith:)` +/// macro. Do not use it directly. +@freestanding(expression) +public macro __capturedValue( + _ value: T, + _ name: String +) -> T = #externalMacro(module: "TestingMacros", type: "ExitTestCapturedValueMacro") where T: Sendable & Codable + +/// Emit a compile-time diagnostic when an unsupported value is captured by an +/// exit test. +/// +/// - Parameters: +/// - value: The captured value. +/// - name: The name of the capture list item corresponding to `value`. +/// +/// - Returns: The result of a call to `fatalError()`. `value` is discarded at +/// compile time. +/// +/// - Warning: This macro is used to implement the `#expect(processExitsWith:)` +/// macro. Do not use it directly. +@freestanding(expression) +public macro __capturedValue( + _ value: borrowing T, + _ name: String +) -> Never = #externalMacro(module: "TestingMacros", type: "ExitTestBadCapturedValueMacro") where T: ~Copyable & ~Escapable diff --git a/Sources/TestingMacros/CMakeLists.txt b/Sources/TestingMacros/CMakeLists.txt index c9a579eaf..effa782a9 100644 --- a/Sources/TestingMacros/CMakeLists.txt +++ b/Sources/TestingMacros/CMakeLists.txt @@ -81,6 +81,7 @@ endif() target_sources(TestingMacros PRIVATE ConditionMacro.swift + ExitTestCapturedValueMacro.swift PragmaMacro.swift SourceLocationMacro.swift SuiteDeclarationMacro.swift diff --git a/Sources/TestingMacros/ConditionMacro.swift b/Sources/TestingMacros/ConditionMacro.swift index 9f87dfbd3..37cbc7339 100644 --- a/Sources/TestingMacros/ConditionMacro.swift +++ b/Sources/TestingMacros/ConditionMacro.swift @@ -551,7 +551,7 @@ extension ExitTestConditionMacro { label: "encodingCapturedValues", expression: TupleExprSyntax { for capturedValue in capturedValues { - LabeledExprSyntax(expression: capturedValue.expression.trimmed) + LabeledExprSyntax(expression: capturedValue.typeCheckedExpression) } } ) diff --git a/Sources/TestingMacros/ExitTestCapturedValueMacro.swift b/Sources/TestingMacros/ExitTestCapturedValueMacro.swift new file mode 100644 index 000000000..0038dac7c --- /dev/null +++ b/Sources/TestingMacros/ExitTestCapturedValueMacro.swift @@ -0,0 +1,54 @@ +// +// 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 +// + +public import SwiftSyntax +import SwiftSyntaxBuilder +public import SwiftSyntaxMacros + +/// The implementation of the `#__capturedValue()` macro when the value conforms +/// to the necessary protocols. +/// +/// This type is used to implement the `#__capturedValue()` macro. Do not use it +/// directly. +public struct ExitTestCapturedValueMacro: ExpressionMacro, Sendable { + public static func expansion( + of macro: some FreestandingMacroExpansionSyntax, + in context: some MacroExpansionContext + ) throws -> ExprSyntax { + let arguments = Array(macro.arguments) + let expr = arguments[0].expression + + // No additional processing is required as this expression's type meets our + // requirements. + + return expr + } +} + +/// The implementation of the `#__capturedValue()` macro when the value does +/// _not_ conform to the necessary protocols. +/// +/// This type is used to implement the `#__capturedValue()` macro. Do not use it +/// directly. +public struct ExitTestBadCapturedValueMacro: ExpressionMacro, Sendable { + public static func expansion( + of macro: some FreestandingMacroExpansionSyntax, + in context: some MacroExpansionContext + ) throws -> ExprSyntax { + let arguments = Array(macro.arguments) + let expr = arguments[0].expression + let nameExpr = arguments[1].expression.cast(StringLiteralExprSyntax.self) + + // Diagnose that the type of 'expr' is invalid. + context.diagnose(.capturedValueMustBeSendableAndCodable(expr, name: nameExpr)) + + return #"Swift.fatalError("Unsupported")"# + } +} diff --git a/Sources/TestingMacros/Support/ClosureCaptureListParsing.swift b/Sources/TestingMacros/Support/ClosureCaptureListParsing.swift index 41abe711c..a7dca88af 100644 --- a/Sources/TestingMacros/Support/ClosureCaptureListParsing.swift +++ b/Sources/TestingMacros/Support/ClosureCaptureListParsing.swift @@ -34,10 +34,15 @@ struct CapturedValueInfo { /// The type of the captured value. var type: TypeSyntax + /// The expression to assign to the captured value with type-checking applied. + var typeCheckedExpression: ExprSyntax { + #"#__capturedValue(\#(expression.trimmed), \#(literal: name.trimmedDescription))"# + } + init(_ capture: ClosureCaptureSyntax, in context: some MacroExpansionContext) { self.capture = capture - self.expression = "()" - self.type = "Swift.Void" + self.expression = #"Swift.fatalError("Unsupported")"# + self.type = "Swift.Never" // We don't support capture specifiers at this time. if let specifier = capture.specifier { diff --git a/Sources/TestingMacros/Support/DiagnosticMessage.swift b/Sources/TestingMacros/Support/DiagnosticMessage.swift index 36186ec4b..3a8957207 100644 --- a/Sources/TestingMacros/Support/DiagnosticMessage.swift +++ b/Sources/TestingMacros/Support/DiagnosticMessage.swift @@ -827,6 +827,24 @@ extension DiagnosticMessage { ) } + /// Create a diagnostic message stating that a captured value must conform to + /// `Sendable` and `Codable`. + /// + /// - Parameters: + /// - valueExpr: The captured value. + /// - nameExpr: The name of the capture list item corresponding to + /// `valueExpr`. + /// + /// - Returns: A diagnostic message. + static func capturedValueMustBeSendableAndCodable(_ valueExpr: ExprSyntax, name nameExpr: StringLiteralExprSyntax) -> Self { + let name = nameExpr.representedLiteralValue ?? valueExpr.trimmedDescription + return Self( + syntax: Syntax(valueExpr), + message: "Type of captured value '\(name)' must conform to 'Sendable' and 'Codable'", + severity: .error + ) + } + /// Create a diagnostic message stating that a capture clause cannot be used /// in an exit test. /// diff --git a/Sources/TestingMacros/TestingMacrosMain.swift b/Sources/TestingMacros/TestingMacrosMain.swift index 1894f4282..4e98115d0 100644 --- a/Sources/TestingMacros/TestingMacrosMain.swift +++ b/Sources/TestingMacros/TestingMacrosMain.swift @@ -28,6 +28,8 @@ struct TestingMacrosMain: CompilerPlugin { RequireThrowsNeverMacro.self, ExitTestExpectMacro.self, ExitTestRequireMacro.self, + ExitTestCapturedValueMacro.self, + ExitTestBadCapturedValueMacro.self, TagMacro.self, SourceLocationMacro.self, PragmaMacro.self, diff --git a/Tests/TestingTests/ExitTestTests.swift b/Tests/TestingTests/ExitTestTests.swift index 02be1a140..fb2d47f03 100644 --- a/Tests/TestingTests/ExitTestTests.swift +++ b/Tests/TestingTests/ExitTestTests.swift @@ -456,6 +456,30 @@ private import _TestingInternals #expect(instance.x == 123) } } + + @Test("Capturing #_sourceLocation") + func captureListPreservesSourceLocationMacro() async { + func sl(_ sl: SourceLocation = #_sourceLocation) -> SourceLocation { + sl + } + await #expect(processExitsWith: .success) { [sl = sl() as SourceLocation] in + #expect(sl.fileID == #fileID) + } + } + +#if false // intentionally fails to compile + struct NonCodableValue {} + + // We can't capture a value that isn't Codable. A unit test is not possible + // for this case as the type checker needs to get involved. + @Test("Capturing a move-only value") + func captureListWithMoveOnlyValue() async { + let x = NonCodableValue() + await #expect(processExitsWith: .success) { [x = x as NonCodableValue] in + _ = x + } + } +#endif #endif } From e7851c5f3ff9994b0e8d8680ffaa7e8e4712d5a8 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Thu, 12 Jun 2025 19:35:25 -0400 Subject: [PATCH 023/216] Use our `posix_spawn()` wrapper in the Foundation CIO rather than `Process`. (#1114) We already have custom `async`-friendly code for spawning child processes, so let's use it instead of relying on `Foundation.Process` when we need to call the platform's `zip` or `tar` tool. Currently we do this in the Foundation cross-import overlay (hence why we were using `Foundation.Process` at all). ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- Documentation/Porting.md | 3 + Package.swift | 47 ++++++++----- .../Attachments/Attachment+URL.swift | 70 ++++++++----------- Sources/Testing/ExitTests/SpawnProcess.swift | 40 +++++++++-- cmake/modules/shared/CompilerSettings.cmake | 1 + 5 files changed, 95 insertions(+), 66 deletions(-) diff --git a/Documentation/Porting.md b/Documentation/Porting.md index 6e83e0eb0..f7aecf97e 100644 --- a/Documentation/Porting.md +++ b/Documentation/Porting.md @@ -67,6 +67,9 @@ platform-specific attention. > conflicting requirements (for example, attempting to enable support for pipes > without also enabling support for file I/O.) You should be able to resolve > these issues by updating `Package.swift` and/or `CompilerSettings.cmake`. +> +> Don't forget to add your platform to the `BuildSettingCondition/whenApple(_:)` +> function in `Package.swift`. Most platform dependencies can be resolved through the use of platform-specific API. For example, Swift Testing uses the C11 standard [`timespec`](https://en.cppreference.com/w/c/chrono/timespec) diff --git a/Package.swift b/Package.swift index cce384d7a..4360aabdc 100644 --- a/Package.swift +++ b/Package.swift @@ -27,7 +27,7 @@ let buildingForDevelopment = (git?.currentTag == nil) /// to change in the future. /// /// - Bug: There is currently no way for us to tell if we are being asked to -/// build for an Embedded Swift target at the package manifest level. +/// build for an Embedded Swift target at the package manifest level. /// ([swift-syntax-#8431](https://github.com/swiftlang/swift-package-manager/issues/8431)) let buildingForEmbedded: Bool = { guard let envvar = Context.environment["SWT_EMBEDDED"] else { @@ -208,7 +208,7 @@ let package = Package( // The Foundation module only has Library Evolution enabled on Apple // platforms, and since this target's module publicly imports Foundation, // it can only enable Library Evolution itself on those platforms. - swiftSettings: .packageSettings + .enableLibraryEvolution(applePlatformsOnly: true) + swiftSettings: .packageSettings + .enableLibraryEvolution(.whenApple()) ), // Utility targets: These are utilities intended for use when developing @@ -244,11 +244,11 @@ extension BuildSettingCondition { /// Swift. /// /// - Parameters: - /// - nonEmbeddedCondition: The value to return if the target is not - /// Embedded Swift. If `nil`, the build condition evaluates to `false`. + /// - nonEmbeddedCondition: The value to return if the target is not + /// Embedded Swift. If `nil`, the build condition evaluates to `false`. /// /// - Returns: A build setting condition that evaluates to `true` for Embedded - /// Swift or is equal to `nonEmbeddedCondition` for non-Embedded Swift. + /// Swift or is equal to `nonEmbeddedCondition` for non-Embedded Swift. static func whenEmbedded(or nonEmbeddedCondition: @autoclosure () -> Self? = nil) -> Self? { if !buildingForEmbedded { if let nonEmbeddedCondition = nonEmbeddedCondition() { @@ -263,6 +263,21 @@ extension BuildSettingCondition { nil } } + + /// A build setting condition representing all Apple or non-Apple platforms. + /// + /// - Parameters: + /// - isApple: Whether or not the result represents Apple platforms. + /// + /// - Returns: A build setting condition that evaluates to `isApple` for Apple + /// platforms. + static func whenApple(_ isApple: Bool = true) -> Self { + if isApple { + .when(platforms: [.macOS, .iOS, .macCatalyst, .watchOS, .tvOS, .visionOS]) + } else { + .when(platforms: [.linux, .custom("freebsd"), .openbsd, .windows, .wasi, .android]) + } + } } extension Array where Element == PackageDescription.SwiftSetting { @@ -312,13 +327,14 @@ extension Array where Element == PackageDescription.SwiftSetting { // executable rather than a library. .define("SWT_NO_LIBRARY_MACRO_PLUGINS"), - .define("SWT_TARGET_OS_APPLE", .when(platforms: [.macOS, .iOS, .macCatalyst, .watchOS, .tvOS, .visionOS])), + .define("SWT_TARGET_OS_APPLE", .whenApple()), .define("SWT_NO_EXIT_TESTS", .whenEmbedded(or: .when(platforms: [.iOS, .watchOS, .tvOS, .visionOS, .wasi, .android]))), .define("SWT_NO_PROCESS_SPAWNING", .whenEmbedded(or: .when(platforms: [.iOS, .watchOS, .tvOS, .visionOS, .wasi, .android]))), - .define("SWT_NO_SNAPSHOT_TYPES", .whenEmbedded(or: .when(platforms: [.linux, .custom("freebsd"), .openbsd, .windows, .wasi, .android]))), + .define("SWT_NO_SNAPSHOT_TYPES", .whenEmbedded(or: .whenApple(false))), .define("SWT_NO_DYNAMIC_LINKING", .whenEmbedded(or: .when(platforms: [.wasi]))), .define("SWT_NO_PIPES", .whenEmbedded(or: .when(platforms: [.wasi]))), + .define("SWT_NO_FOUNDATION_FILE_COORDINATION", .whenEmbedded(or: .whenApple(false))), .define("SWT_NO_LEGACY_TEST_DISCOVERY", .whenEmbedded()), .define("SWT_NO_LIBDISPATCH", .whenEmbedded()), @@ -354,20 +370,16 @@ extension Array where Element == PackageDescription.SwiftSetting { ] } - /// Create a Swift setting which enables Library Evolution, optionally - /// constraining it to only Apple platforms. + /// Create a Swift setting which enables Library Evolution. /// /// - Parameters: - /// - applePlatformsOnly: Whether to constrain this setting to only Apple - /// platforms. - static func enableLibraryEvolution(applePlatformsOnly: Bool = false) -> Self { + /// - condition: A build setting condition to apply to this setting. + /// + /// - Returns: A Swift setting that enables Library Evolution. + static func enableLibraryEvolution(_ condition: BuildSettingCondition? = nil) -> Self { var result = [PackageDescription.SwiftSetting]() if buildingForDevelopment { - var condition: BuildSettingCondition? - if applePlatformsOnly { - condition = .when(platforms: [.macOS, .iOS, .macCatalyst, .watchOS, .tvOS, .visionOS]) - } result.append(.unsafeFlags(["-enable-library-evolution"], condition)) } @@ -410,9 +422,10 @@ extension Array where Element == PackageDescription.CXXSetting { result += [ .define("SWT_NO_EXIT_TESTS", .whenEmbedded(or: .when(platforms: [.iOS, .watchOS, .tvOS, .visionOS, .wasi, .android]))), .define("SWT_NO_PROCESS_SPAWNING", .whenEmbedded(or: .when(platforms: [.iOS, .watchOS, .tvOS, .visionOS, .wasi, .android]))), - .define("SWT_NO_SNAPSHOT_TYPES", .whenEmbedded(or: .when(platforms: [.linux, .custom("freebsd"), .openbsd, .windows, .wasi, .android]))), + .define("SWT_NO_SNAPSHOT_TYPES", .whenEmbedded(or: .whenApple(false))), .define("SWT_NO_DYNAMIC_LINKING", .whenEmbedded(or: .when(platforms: [.wasi]))), .define("SWT_NO_PIPES", .whenEmbedded(or: .when(platforms: [.wasi]))), + .define("SWT_NO_FOUNDATION_FILE_COORDINATION", .whenEmbedded(or: .whenApple(false))), .define("SWT_NO_LEGACY_TEST_DISCOVERY", .whenEmbedded()), .define("SWT_NO_LIBDISPATCH", .whenEmbedded()), diff --git a/Sources/Overlays/_Testing_Foundation/Attachments/Attachment+URL.swift b/Sources/Overlays/_Testing_Foundation/Attachments/Attachment+URL.swift index a018363fc..bb3668180 100644 --- a/Sources/Overlays/_Testing_Foundation/Attachments/Attachment+URL.swift +++ b/Sources/Overlays/_Testing_Foundation/Attachments/Attachment+URL.swift @@ -71,7 +71,7 @@ extension Attachment where AttachableValue == _AttachableURLWrapper { let url = url.resolvingSymlinksInPath() let isDirectory = try url.resourceValues(forKeys: [.isDirectoryKey]).isDirectory! -#if SWT_TARGET_OS_APPLE +#if SWT_TARGET_OS_APPLE && !SWT_NO_FOUNDATION_FILE_COORDINATION let data: Data = try await withCheckedThrowingContinuation { continuation in let fileCoordinator = NSFileCoordinator() let fileAccessIntent = NSFileAccessIntent.readingIntent(with: url, options: [.forUploading]) @@ -166,25 +166,31 @@ private func _compressContentsOfDirectory(at directoryURL: URL) async throws -> // knows how to write PKZIP archives, while Windows inherited FreeBSD's tar // tool in Windows 10 Build 17063 (per https://techcommunity.microsoft.com/blog/containers/tar-and-curl-come-to-windows/382409). // - // On Linux (which does not have FreeBSD's version of tar(1)), we can use - // zip(1) instead. + // On Linux and OpenBSD (which do not have FreeBSD's version of tar(1)), we + // can use zip(1) instead. This tool compresses paths relative to the current + // working directory, and posix_spawn_file_actions_addchdir_np() is not always + // available for us to call (not present on OpenBSD, requires glibc ≥ 2.28 on + // Linux), so we'll spawn a shell that calls cd before calling zip(1). // // OpenBSD's tar(1) does not support writing PKZIP archives, and /usr/bin/zip // tool is an optional install, so we check if it's present before trying to // execute it. +#if os(Linux) || os(OpenBSD) + let archiverPath = "/bin/sh" #if os(Linux) - let archiverPath = "/usr/bin/zip" -#elseif SWT_TARGET_OS_APPLE || os(FreeBSD) - let archiverPath = "/usr/bin/tar" -#elseif os(OpenBSD) - let archiverPath = "/usr/local/bin/zip" + let trueArchiverPath = "/usr/bin/zip" +#else + let trueArchiverPath = "/usr/local/bin/zip" var isDirectory = false - if !FileManager.default.fileExists(atPath: archiverPath, isDirectory: &isDirectory) || isDirectory { + if !FileManager.default.fileExists(atPath: trueArchiverPath, isDirectory: &isDirectory) || isDirectory { throw CocoaError(.fileNoSuchFile, userInfo: [ NSLocalizedDescriptionKey: "The 'zip' package is not installed.", - NSFilePathErrorKey: archiverPath + NSFilePathErrorKey: trueArchiverPath ]) } +#endif +#elseif SWT_TARGET_OS_APPLE || os(FreeBSD) + let archiverPath = "/usr/bin/tar" #elseif os(Windows) guard let archiverPath = _archiverPath else { throw CocoaError(.fileWriteUnknown, userInfo: [ @@ -197,20 +203,15 @@ private func _compressContentsOfDirectory(at directoryURL: URL) async throws -> throw CocoaError(.featureUnsupported, userInfo: [NSLocalizedDescriptionKey: "This platform does not support attaching directories to tests."]) #endif - try await withCheckedThrowingContinuation { continuation in - let process = Process() - - process.executableURL = URL(fileURLWithPath: archiverPath, isDirectory: false) - - let sourcePath = directoryURL.fileSystemPath - let destinationPath = temporaryURL.fileSystemPath + let sourcePath = directoryURL.fileSystemPath + let destinationPath = temporaryURL.fileSystemPath + let arguments = { #if os(Linux) || os(OpenBSD) // The zip command constructs relative paths from the current working // directory rather than from command-line arguments. - process.arguments = [destinationPath, "--recurse-paths", "."] - process.currentDirectoryURL = directoryURL + ["-c", #"cd "$0" && "$1" "$2" --recurse-paths ."#, sourcePath, trueArchiverPath, destinationPath] #elseif SWT_TARGET_OS_APPLE || os(FreeBSD) - process.arguments = ["--create", "--auto-compress", "--directory", sourcePath, "--file", destinationPath, "."] + ["--create", "--auto-compress", "--directory", sourcePath, "--file", destinationPath, "."] #elseif os(Windows) // The Windows version of bsdtar can handle relative paths for other archive // formats, but produces empty archives when inferring the zip format with @@ -219,30 +220,15 @@ private func _compressContentsOfDirectory(at directoryURL: URL) async throws -> // An alternative may be to use PowerShell's Compress-Archive command, // however that comes with a security risk as we'd be responsible for two // levels of command-line argument escaping. - process.arguments = ["--create", "--auto-compress", "--file", destinationPath, sourcePath] + ["--create", "--auto-compress", "--file", destinationPath, sourcePath] #endif + }() - process.standardOutput = nil - process.standardError = nil - - process.terminationHandler = { process in - let terminationReason = process.terminationReason - let terminationStatus = process.terminationStatus - if terminationReason == .exit && terminationStatus == EXIT_SUCCESS { - continuation.resume() - } else { - let error = CocoaError(.fileWriteUnknown, userInfo: [ - NSLocalizedDescriptionKey: "The directory at '\(sourcePath)' could not be compressed (\(terminationStatus)).", - ]) - continuation.resume(throwing: error) - } - } - - do { - try process.run() - } catch { - continuation.resume(throwing: error) - } + let exitStatus = try await spawnExecutableAtPathAndWait(archiverPath, arguments: arguments) + guard case .exitCode(EXIT_SUCCESS) = exitStatus else { + throw CocoaError(.fileWriteUnknown, userInfo: [ + NSLocalizedDescriptionKey: "The directory at '\(sourcePath)' could not be compressed (\(exitStatus)).", + ]) } return try Data(contentsOf: temporaryURL, options: [.mappedIfSafe]) diff --git a/Sources/Testing/ExitTests/SpawnProcess.swift b/Sources/Testing/ExitTests/SpawnProcess.swift index 647e62dd9..66143a7e0 100644 --- a/Sources/Testing/ExitTests/SpawnProcess.swift +++ b/Sources/Testing/ExitTests/SpawnProcess.swift @@ -38,7 +38,7 @@ private let _posix_spawn_file_actions_addclosefrom_np = symbol(named: "posix_spa } #endif -/// Spawn a process and wait for it to terminate. +/// Spawn a child process. /// /// - Parameters: /// - executablePath: The path to the executable to spawn. @@ -61,8 +61,7 @@ private let _posix_spawn_file_actions_addclosefrom_np = symbol(named: "posix_spa /// eventually pass this value to ``wait(for:)`` to avoid leaking system /// resources. /// -/// - Throws: Any error that prevented the process from spawning or its exit -/// condition from being read. +/// - Throws: Any error that prevented the process from spawning. func spawnExecutable( atPath executablePath: String, arguments: [String], @@ -83,8 +82,9 @@ func spawnExecutable( #if SWT_TARGET_OS_APPLE || os(Linux) || os(FreeBSD) || os(OpenBSD) return try withUnsafeTemporaryAllocation(of: P.self, capacity: 1) { fileActions in let fileActions = fileActions.baseAddress! - guard 0 == posix_spawn_file_actions_init(fileActions) else { - throw CError(rawValue: swt_errno()) + let fileActionsInitialized = posix_spawn_file_actions_init(fileActions) + guard 0 == fileActionsInitialized else { + throw CError(rawValue: fileActionsInitialized) } defer { _ = posix_spawn_file_actions_destroy(fileActions) @@ -92,8 +92,9 @@ func spawnExecutable( return try withUnsafeTemporaryAllocation(of: P.self, capacity: 1) { attrs in let attrs = attrs.baseAddress! - guard 0 == posix_spawnattr_init(attrs) else { - throw CError(rawValue: swt_errno()) + let attrsInitialized = posix_spawnattr_init(attrs) + guard 0 == attrsInitialized else { + throw CError(rawValue: attrsInitialized) } defer { _ = posix_spawnattr_destroy(attrs) @@ -416,4 +417,29 @@ private func _escapeCommandLine(_ arguments: [String]) -> String { }.joined(separator: " ") } #endif + +/// Spawn a child process and wait for it to terminate. +/// +/// - Parameters: +/// - executablePath: The path to the executable to spawn. +/// - arguments: The arguments to pass to the executable, not including the +/// executable path. +/// - environment: The environment block to pass to the executable. +/// +/// - Returns: The exit status of the spawned process. +/// +/// - Throws: Any error that prevented the process from spawning or its exit +/// condition from being read. +/// +/// This function is a convenience that spawns the given process and waits for +/// it to terminate. It is primarily for use by other targets in this package +/// such as its cross-import overlays. +package func spawnExecutableAtPathAndWait( + _ executablePath: String, + arguments: [String] = [], + environment: [String: String] = [:] +) async throws -> ExitStatus { + let processID = try spawnExecutable(atPath: executablePath, arguments: arguments, environment: environment) + return try await wait(for: processID) +} #endif diff --git a/cmake/modules/shared/CompilerSettings.cmake b/cmake/modules/shared/CompilerSettings.cmake index af8b56dfd..a667f5ba1 100644 --- a/cmake/modules/shared/CompilerSettings.cmake +++ b/cmake/modules/shared/CompilerSettings.cmake @@ -34,6 +34,7 @@ if(CMAKE_SYSTEM_NAME IN_LIST SWT_NO_PROCESS_SPAWNING_LIST) endif() if(NOT APPLE) add_compile_definitions("SWT_NO_SNAPSHOT_TYPES") + add_compile_definitions("SWT_NO_FOUNDATION_FILE_COORDINATION") endif() if(CMAKE_SYSTEM_NAME STREQUAL "WASI") add_compile_definitions("SWT_NO_DYNAMIC_LINKING") From f66ef03052a1e40d93af408ecc0795ff91c06421 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Thu, 12 Jun 2025 19:35:45 -0400 Subject: [PATCH 024/216] Add an unreachable marker function. (#1150) This PR adds a wrapper around `__builtin_unreachable()` (`Builtin.unreachable()` when building the Swift standard library) that we can use in place of `fatalError()`. The benefit is that the generated code size for unreachable paths is significantly reduced. For example, given the following function compiled with `-O`: ```swift @available(*, unavailable) func f() { fatalError("Unreachable") } ``` The compiler currently produces: ```asm sub sp, sp, #0x20 stp x29, x30, [sp, #0x10] add x29, sp, #0x10 mov w8, #0x1 ; =1 str w8, [sp, #0x8] mov w8, #0xc ; =12 str x8, [sp] adrp x0, 0 add x0, x0, #0x6a8 ; "Fatal error" adrp x5, 0 add x5, x5, #0x690 ; "UnreachableTest/S.swift" mov x3, #0x6e55 ; =28245 movk x3, #0x6572, lsl #16 movk x3, #0x6361, lsl #32 movk x3, #0x6168, lsl #48 mov x4, #0x6c62 ; =27746 movk x4, #0x65, lsl #16 movk x4, #0xeb00, lsl #48 mov w1, #0xb ; =11 mov w2, #0x2 ; =2 mov w6, #0x17 ; =23 mov w7, #0x2 ; =2 bl 0x100000680 ; symbol stub for: Swift._assertionFailure(_: Swift.StaticString, _: Swift.String, file: Swift.StaticString, line: Swift.UInt, flags: Swift.UInt32) -> Swift.Never brk #0x1 ``` But with this change: ```swift @available(*, unavailable) func f() { swt_unreachable() } ``` It instead compiles to simply: ```asm brk #0x1 ``` ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- .../ExitTests/ExitTest.CapturedValue.swift | 8 +++++--- .../Testing/ExitTests/ExitTest.Condition.swift | 6 +++--- Sources/Testing/Issues/Confirmation.swift | 8 +++++--- Sources/Testing/Traits/TimeLimitTrait.swift | 16 +++++++++------- Sources/_TestingInternals/include/Stubs.h | 9 +++++++++ Tests/TestingTests/ExitTestTests.swift | 2 +- 6 files changed, 32 insertions(+), 17 deletions(-) diff --git a/Sources/Testing/ExitTests/ExitTest.CapturedValue.swift b/Sources/Testing/ExitTests/ExitTest.CapturedValue.swift index 1d5c9b18a..867fdecc5 100644 --- a/Sources/Testing/ExitTests/ExitTest.CapturedValue.swift +++ b/Sources/Testing/ExitTests/ExitTest.CapturedValue.swift @@ -8,6 +8,8 @@ // See https://swift.org/CONTRIBUTORS.txt for Swift project authors // +private import _TestingInternals + @_spi(Experimental) @_spi(ForToolsIntegrationOnly) #if SWT_NO_EXIT_TESTS @available(*, unavailable, message: "Exit tests are not available on this platform.") @@ -82,7 +84,7 @@ extension ExitTest { } return nil #else - fatalError("Unsupported") + swt_unreachable() #endif } @@ -101,7 +103,7 @@ extension ExitTest { _kind = .typeOnly(type) } #else - fatalError("Unsupported") + swt_unreachable() #endif } } @@ -119,7 +121,7 @@ extension ExitTest { type } #else - fatalError("Unsupported") + swt_unreachable() #endif } } diff --git a/Sources/Testing/ExitTests/ExitTest.Condition.swift b/Sources/Testing/ExitTests/ExitTest.Condition.swift index 42d929077..0a23bf47c 100644 --- a/Sources/Testing/ExitTests/ExitTest.Condition.swift +++ b/Sources/Testing/ExitTests/ExitTest.Condition.swift @@ -136,7 +136,7 @@ extension ExitTest.Condition { #if !SWT_NO_EXIT_TESTS Self(.exitCode(exitCode)) #else - fatalError("Unsupported") + swt_unreachable() #endif } @@ -169,7 +169,7 @@ extension ExitTest.Condition { #if !SWT_NO_EXIT_TESTS Self(.signal(signal)) #else - fatalError("Unsupported") + swt_unreachable() #endif } } @@ -192,7 +192,7 @@ extension ExitTest.Condition: CustomStringConvertible { String(describing: exitStatus) } #else - fatalError("Unsupported") + swt_unreachable() #endif } } diff --git a/Sources/Testing/Issues/Confirmation.swift b/Sources/Testing/Issues/Confirmation.swift index 21baec505..33b0ec25a 100644 --- a/Sources/Testing/Issues/Confirmation.swift +++ b/Sources/Testing/Issues/Confirmation.swift @@ -8,6 +8,8 @@ // See https://swift.org/CONTRIBUTORS.txt for Swift project authors // +private import _TestingInternals + /// A type that can be used to confirm that an event occurs zero or more times. public struct Confirmation: Sendable { /// The number of times ``confirm(count:)`` has been called. @@ -202,7 +204,7 @@ public func confirmation( sourceLocation: SourceLocation = #_sourceLocation, _ body: (Confirmation) async throws -> R ) async rethrows -> R { - fatalError("Unsupported") + swt_unreachable() } /// An overload of ``confirmation(_:expectedCount:isolation:sourceLocation:_:)-l3il`` @@ -218,7 +220,7 @@ public func confirmation( sourceLocation: SourceLocation = #_sourceLocation, _ body: (Confirmation) async throws -> R ) async rethrows -> R { - fatalError("Unsupported") + swt_unreachable() } /// An overload of ``confirmation(_:expectedCount:isolation:sourceLocation:_:)-l3il`` @@ -234,5 +236,5 @@ public func confirmation( sourceLocation: SourceLocation = #_sourceLocation, _ body: (Confirmation) async throws -> R ) async rethrows -> R { - fatalError("Unsupported") + swt_unreachable() } diff --git a/Sources/Testing/Traits/TimeLimitTrait.swift b/Sources/Testing/Traits/TimeLimitTrait.swift index 54a200fb4..7b21209b6 100644 --- a/Sources/Testing/Traits/TimeLimitTrait.swift +++ b/Sources/Testing/Traits/TimeLimitTrait.swift @@ -8,6 +8,8 @@ // See https://swift.org/CONTRIBUTORS.txt for Swift project authors // +private import _TestingInternals + /// A type that defines a time limit to apply to a test. /// /// To add this trait to a test, use ``Trait/timeLimit(_:)-4kzjp``. @@ -126,7 +128,7 @@ extension TimeLimitTrait.Duration { /// This function is unavailable and is provided for diagnostic purposes only. @available(*, unavailable, message: "Time limit must be specified in minutes") public static func seconds(_ seconds: some BinaryInteger) -> Self { - fatalError("Unsupported") + swt_unreachable() } /// Construct a time limit duration given a number of seconds. @@ -134,7 +136,7 @@ extension TimeLimitTrait.Duration { /// This function is unavailable and is provided for diagnostic purposes only. @available(*, unavailable, message: "Time limit must be specified in minutes") public static func seconds(_ seconds: Double) -> Self { - fatalError("Unsupported") + swt_unreachable() } /// Construct a time limit duration given a number of milliseconds. @@ -142,7 +144,7 @@ extension TimeLimitTrait.Duration { /// This function is unavailable and is provided for diagnostic purposes only. @available(*, unavailable, message: "Time limit must be specified in minutes") public static func milliseconds(_ milliseconds: some BinaryInteger) -> Self { - fatalError("Unsupported") + swt_unreachable() } /// Construct a time limit duration given a number of milliseconds. @@ -150,7 +152,7 @@ extension TimeLimitTrait.Duration { /// This function is unavailable and is provided for diagnostic purposes only. @available(*, unavailable, message: "Time limit must be specified in minutes") public static func milliseconds(_ milliseconds: Double) -> Self { - fatalError("Unsupported") + swt_unreachable() } /// Construct a time limit duration given a number of microseconds. @@ -158,7 +160,7 @@ extension TimeLimitTrait.Duration { /// This function is unavailable and is provided for diagnostic purposes only. @available(*, unavailable, message: "Time limit must be specified in minutes") public static func microseconds(_ microseconds: some BinaryInteger) -> Self { - fatalError("Unsupported") + swt_unreachable() } /// Construct a time limit duration given a number of microseconds. @@ -166,7 +168,7 @@ extension TimeLimitTrait.Duration { /// This function is unavailable and is provided for diagnostic purposes only. @available(*, unavailable, message: "Time limit must be specified in minutes") public static func microseconds(_ microseconds: Double) -> Self { - fatalError("Unsupported") + swt_unreachable() } /// Construct a time limit duration given a number of nanoseconds. @@ -174,7 +176,7 @@ extension TimeLimitTrait.Duration { /// This function is unavailable and is provided for diagnostic purposes only. @available(*, unavailable, message: "Time limit must be specified in minutes") public static func nanoseconds(_ nanoseconds: some BinaryInteger) -> Self { - fatalError("Unsupported") + swt_unreachable() } } diff --git a/Sources/_TestingInternals/include/Stubs.h b/Sources/_TestingInternals/include/Stubs.h index 171cca6a5..636ea9aff 100644 --- a/Sources/_TestingInternals/include/Stubs.h +++ b/Sources/_TestingInternals/include/Stubs.h @@ -16,6 +16,15 @@ SWT_ASSUME_NONNULL_BEGIN +/// Mark a code path as unreachable. +/// +/// This function is necessary because Swift does not have an equivalent of +/// `__builtin_unreachable()`. +__attribute__((always_inline, noreturn)) +static inline void swt_unreachable(void) { + __builtin_unreachable(); +} + #if !SWT_NO_FILE_IO /// The C file handle type. /// diff --git a/Tests/TestingTests/ExitTestTests.swift b/Tests/TestingTests/ExitTestTests.swift index fb2d47f03..265a5cfaf 100644 --- a/Tests/TestingTests/ExitTestTests.swift +++ b/Tests/TestingTests/ExitTestTests.swift @@ -306,7 +306,7 @@ private import _TestingInternals await Test { try await #require(processExitsWith: .success) {} - fatalError("Unreachable") + Issue.record("#require(processExitsWith:) should have thrown an error") }.run(configuration: configuration) } } From e6061e66eb6557835272a48fabff3febd9803e88 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Fri, 13 Jun 2025 11:50:17 -0400 Subject: [PATCH 025/216] Remove `@convention(thin)` from exit test macro declarations. (#1153) `@convention(thin)` is not fully plumbed through the compiler and should not be in our API surface, but also because of the experimental value capturing feature the closures may be thick anyway. See also https://github.com/swiftlang/swift-evolution/pull/2884. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- Sources/Testing/Expectations/Expectation+Macro.swift | 4 ++-- Sources/Testing/Expectations/ExpectationChecking+Macro.swift | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/Testing/Expectations/Expectation+Macro.swift b/Sources/Testing/Expectations/Expectation+Macro.swift index 973efde53..df9d80058 100644 --- a/Sources/Testing/Expectations/Expectation+Macro.swift +++ b/Sources/Testing/Expectations/Expectation+Macro.swift @@ -525,7 +525,7 @@ public macro expect( observing observedValues: [any PartialKeyPath & Sendable] = [], _ comment: @autoclosure () -> Comment? = nil, sourceLocation: SourceLocation = #_sourceLocation, - performing expression: @escaping @Sendable @convention(thin) () async throws -> Void + performing expression: @escaping @Sendable () async throws -> Void ) -> ExitTest.Result? = #externalMacro(module: "TestingMacros", type: "ExitTestExpectMacro") /// Check that an expression causes the process to terminate in a given fashion @@ -572,7 +572,7 @@ public macro require( observing observedValues: [any PartialKeyPath & Sendable] = [], _ comment: @autoclosure () -> Comment? = nil, sourceLocation: SourceLocation = #_sourceLocation, - performing expression: @escaping @Sendable @convention(thin) () async throws -> Void + performing expression: @escaping @Sendable () async throws -> Void ) -> ExitTest.Result = #externalMacro(module: "TestingMacros", type: "ExitTestRequireMacro") /// Capture a sendable and codable value to pass to an exit test. diff --git a/Sources/Testing/Expectations/ExpectationChecking+Macro.swift b/Sources/Testing/Expectations/ExpectationChecking+Macro.swift index 6d3093f2a..3a190e679 100644 --- a/Sources/Testing/Expectations/ExpectationChecking+Macro.swift +++ b/Sources/Testing/Expectations/ExpectationChecking+Macro.swift @@ -1148,7 +1148,7 @@ public func __checkClosureCall( identifiedBy exitTestID: (UInt64, UInt64, UInt64, UInt64), processExitsWith expectedExitCondition: ExitTest.Condition, observing observedValues: [any PartialKeyPath & Sendable] = [], - performing _: @convention(thin) () -> Void, + performing _: @convention(c) () -> Void, expression: __Expression, comments: @autoclosure () -> [Comment], isRequired: Bool, @@ -1181,7 +1181,7 @@ public func __checkClosureCall( encodingCapturedValues capturedValues: (repeat each T), processExitsWith expectedExitCondition: ExitTest.Condition, observing observedValues: [any PartialKeyPath & Sendable] = [], - performing _: @convention(thin) () -> Void, + performing _: @convention(c) () -> Void, expression: __Expression, comments: @autoclosure () -> [Comment], isRequired: Bool, From 567285e5c39f98a36cb7ea35f08cfbef6753374f Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Fri, 13 Jun 2025 16:38:14 -0400 Subject: [PATCH 026/216] Infer the types of function/closure arguments when captured by an exit test. (#1130) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR adds the ability to infer the type of a parameter of a function or closure that encloses an exit test. For example, `x` here: ```swift func f(x: Int) async { await #expect(processExitsWith: .failure) { [x] in ... } } ``` This inference still fails if a parameter is shadowed by a variable with an incompatible type; we still need something like `decltype()` to solve for such cases. We emit a custom diagnostic of the form "🛑 Type of captured value 'x' is ambiguous" if the inferred type of the captured value doesn't match what the compiler thinks it is (see #1146). Still, being able to capture `@Test` function arguments with minimal ceremony is helpful: ```swift @Test(arguments: 0 ..< 100) func f(i: Int) async { await #expect(exitsWith: .failure) { [i] in ... } } ``` Also type inference for literals because "why not?" > [!NOTE] > Exit test value capture remains an experimental feature. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- Sources/TestingMacros/ConditionMacro.swift | 77 +++++++++++++++---- .../Support/ClosureCaptureListParsing.swift | 60 ++++++++++++++- .../Support/DiagnosticMessage.swift | 32 ++++++++ .../ConditionMacroTests.swift | 18 +++++ Tests/TestingTests/ExitTestTests.swift | 39 ++++++++++ 5 files changed, 210 insertions(+), 16 deletions(-) diff --git a/Sources/TestingMacros/ConditionMacro.swift b/Sources/TestingMacros/ConditionMacro.swift index 37cbc7339..0ef0970e9 100644 --- a/Sources/TestingMacros/ConditionMacro.swift +++ b/Sources/TestingMacros/ConditionMacro.swift @@ -437,21 +437,23 @@ extension ExitTestConditionMacro { var bodyArgumentExpr = arguments[trailingClosureIndex].expression bodyArgumentExpr = removeParentheses(from: bodyArgumentExpr) ?? bodyArgumentExpr - // Find any captured values and extract them from the trailing closure. - var capturedValues = [CapturedValueInfo]() - if ExitTestExpectMacro.isValueCapturingEnabled { - // The source file imports @_spi(Experimental), so allow value capturing. - if var closureExpr = bodyArgumentExpr.as(ClosureExprSyntax.self), - let captureList = closureExpr.signature?.capture?.items { - closureExpr.signature?.capture = ClosureCaptureClauseSyntax(items: [], trailingTrivia: .space) - capturedValues = captureList.map { CapturedValueInfo($0, in: context) } - bodyArgumentExpr = ExprSyntax(closureExpr) + // Before building the macro expansion, look for any problems and return + // early if found. + guard _diagnoseIssues(with: macro, body: bodyArgumentExpr, in: context) else { + if Self.isThrowing { + return #"{ () async throws -> Testing.ExitTest.Result in Swift.fatalError("Unreachable") }()"# + } else { + return #"{ () async -> Testing.ExitTest.Result in Swift.fatalError("Unreachable") }()"# } + } - } else if let closureExpr = bodyArgumentExpr.as(ClosureExprSyntax.self), - let captureClause = closureExpr.signature?.capture, - !captureClause.items.isEmpty { - context.diagnose(.captureClauseUnsupported(captureClause, in: closureExpr, inExitTest: macro)) + // Find any captured values and extract them from the trailing closure. + var capturedValues = [CapturedValueInfo]() + if var closureExpr = bodyArgumentExpr.as(ClosureExprSyntax.self), + let captureList = closureExpr.signature?.capture?.items { + closureExpr.signature?.capture = ClosureCaptureClauseSyntax(items: [], trailingTrivia: .space) + capturedValues = captureList.map { CapturedValueInfo($0, in: context) } + bodyArgumentExpr = ExprSyntax(closureExpr) } // Generate a unique identifier for this exit test. @@ -611,6 +613,55 @@ extension ExitTestConditionMacro { return ExprSyntax(tupleExpr) } } + + /// Diagnose issues with an exit test macro call. + /// + /// - Parameters: + /// - macro: The exit test macro call. + /// - bodyArgumentExpr: The exit test's body. + /// - context: The macro context in which the expression is being parsed. + /// + /// - Returns: Whether or not macro expansion should continue (i.e. stopping + /// if a fatal error was diagnosed.) + private static func _diagnoseIssues( + with macro: some FreestandingMacroExpansionSyntax, + body bodyArgumentExpr: ExprSyntax, + in context: some MacroExpansionContext + ) -> Bool { + var diagnostics = [DiagnosticMessage]() + + if let closureExpr = bodyArgumentExpr.as(ClosureExprSyntax.self), + let captureClause = closureExpr.signature?.capture, + !captureClause.items.isEmpty { + // Disallow capture lists if the experimental feature is not enabled. + if !ExitTestExpectMacro.isValueCapturingEnabled { + diagnostics.append(.captureClauseUnsupported(captureClause, in: closureExpr, inExitTest: macro)) + } + } + + // Disallow exit tests in generic types and functions as they cannot be + // correctly expanded due to the use of a nested type with static members. + for lexicalContext in context.lexicalContext { + if let lexicalContext = lexicalContext.asProtocol((any WithGenericParametersSyntax).self) { + if let genericClause = lexicalContext.genericParameterClause { + diagnostics.append(.expressionMacroUnsupported(macro, inGenericContextBecauseOf: genericClause, on: lexicalContext)) + } else if let whereClause = lexicalContext.genericWhereClause { + diagnostics.append(.expressionMacroUnsupported(macro, inGenericContextBecauseOf: whereClause, on: lexicalContext)) + } else if let functionDecl = lexicalContext.as(FunctionDeclSyntax.self) { + for parameter in functionDecl.signature.parameterClause.parameters { + if parameter.type.isSome { + diagnostics.append(.expressionMacroUnsupported(macro, inGenericContextBecauseOf: parameter, on: functionDecl)) + } + } + } + } + } + + for diagnostic in diagnostics { + context.diagnose(diagnostic) + } + return diagnostics.isEmpty + } } extension ExitTestExpectMacro { diff --git a/Sources/TestingMacros/Support/ClosureCaptureListParsing.swift b/Sources/TestingMacros/Support/ClosureCaptureListParsing.swift index a7dca88af..37e696865 100644 --- a/Sources/TestingMacros/Support/ClosureCaptureListParsing.swift +++ b/Sources/TestingMacros/Support/ClosureCaptureListParsing.swift @@ -52,8 +52,9 @@ struct CapturedValueInfo { // Potentially get the name of the type comprising the current lexical // context (i.e. whatever `Self` is.) + lazy var lexicalContext = context.lexicalContext lazy var typeNameOfLexicalContext = { - let lexicalContext = context.lexicalContext.drop { !$0.isProtocol((any DeclGroupSyntax).self) } + let lexicalContext = lexicalContext.drop { !$0.isProtocol((any DeclGroupSyntax).self) } return context.type(ofLexicalContext: lexicalContext) }() @@ -76,7 +77,19 @@ struct CapturedValueInfo { // Copying self. self.type = typeNameOfLexicalContext } else { - context.diagnose(.typeOfCaptureIsAmbiguous(capture, initializedWith: initializer)) + // Handle literals. Any other types are ambiguous. + switch self.expression.kind { + case .integerLiteralExpr: + self.type = TypeSyntax(IdentifierTypeSyntax(name: .identifier("IntegerLiteralType"))) + case .floatLiteralExpr: + self.type = TypeSyntax(IdentifierTypeSyntax(name: .identifier("FloatLiteralType"))) + case .booleanLiteralExpr: + self.type = TypeSyntax(IdentifierTypeSyntax(name: .identifier("BooleanLiteralType"))) + case .stringLiteralExpr, .simpleStringLiteralExpr: + self.type = TypeSyntax(IdentifierTypeSyntax(name: .identifier("StringLiteralType"))) + default: + context.diagnose(.typeOfCaptureIsAmbiguous(capture, initializedWith: initializer)) + } } } else if capture.name.tokenKind == .keyword(.self), @@ -84,10 +97,51 @@ struct CapturedValueInfo { // Capturing self. self.expression = "self" self.type = typeNameOfLexicalContext - + } else if let parameterType = Self._findTypeOfParameter(named: capture.name, in: lexicalContext) { + self.expression = ExprSyntax(DeclReferenceExprSyntax(baseName: capture.name.trimmed)) + self.type = parameterType } else { // Not enough contextual information to derive the type here. context.diagnose(.typeOfCaptureIsAmbiguous(capture)) } } + + /// Find a function or closure parameter in the given lexical context with a + /// given name and return its type. + /// + /// - Parameters: + /// - parameterName: The name of the parameter of interest. + /// - lexicalContext: The lexical context to examine. + /// + /// - Returns: The Swift type of first parameter found whose name matches, or + /// `nil` if none was found. The lexical context is searched in the order + /// provided which, by default, starts with the innermost scope. + private static func _findTypeOfParameter(named parameterName: TokenSyntax, in lexicalContext: [Syntax]) -> TypeSyntax? { + for lexicalContext in lexicalContext { + var parameterType: TypeSyntax? + if let functionDecl = lexicalContext.as(FunctionDeclSyntax.self) { + parameterType = functionDecl.signature.parameterClause.parameters + .first { ($0.secondName ?? $0.firstName).tokenKind == parameterName.tokenKind } + .map(\.type) + } else if let closureExpr = lexicalContext.as(ClosureExprSyntax.self) { + if case let .parameterClause(parameterClause) = closureExpr.signature?.parameterClause { + parameterType = parameterClause.parameters + .first { ($0.secondName ?? $0.firstName).tokenKind == parameterName.tokenKind } + .flatMap(\.type) + } + } else if lexicalContext.is(DeclSyntax.self) { + // If we've reached any other enclosing declaration, then any parameters + // beyond it won't be capturable and thus it isn't possible to infer + // types from them (any capture of `x`, for instance, must refer to some + // more-local variable with that name, not to a parameter named `x`.) + return nil + } + + if let parameterType { + return parameterType + } + } + + return nil + } } diff --git a/Sources/TestingMacros/Support/DiagnosticMessage.swift b/Sources/TestingMacros/Support/DiagnosticMessage.swift index 3a8957207..b7103bcc6 100644 --- a/Sources/TestingMacros/Support/DiagnosticMessage.swift +++ b/Sources/TestingMacros/Support/DiagnosticMessage.swift @@ -888,4 +888,36 @@ extension DiagnosticMessage { ] ) } + + /// Create a diagnostic message stating that an expression macro is not + /// supported in a generic context. + /// + /// - Parameters: + /// - macro: The invalid macro. + /// - genericClause: The child node on `genericDecl` that makes it generic. + /// - genericDecl: The generic declaration to which `genericClause` is + /// attached, possibly equal to `decl`. + /// + /// - Returns: A diagnostic message. + static func expressionMacroUnsupported(_ macro: some FreestandingMacroExpansionSyntax, inGenericContextBecauseOf genericClause: some SyntaxProtocol, on genericDecl: some SyntaxProtocol) -> Self { + if let functionDecl = genericDecl.as(FunctionDeclSyntax.self) { + return Self( + syntax: Syntax(macro), + message: "Cannot call macro '\(_macroName(macro))' within generic function '\(functionDecl.completeName)'", + severity: .error + ) + } else if let namedDecl = genericDecl.asProtocol((any NamedDeclSyntax).self) { + return Self( + syntax: Syntax(macro), + message: "Cannot call macro '\(_macroName(macro))' within generic \(_kindString(for: genericDecl)) '\(namedDecl.name.trimmed)'", + severity: .error + ) + } else { + return Self( + syntax: Syntax(macro), + message: "Cannot call macro '\(_macroName(macro))' within a generic \(_kindString(for: genericDecl))", + severity: .error + ) + } + } } diff --git a/Tests/TestingMacrosTests/ConditionMacroTests.swift b/Tests/TestingMacrosTests/ConditionMacroTests.swift index dc36af7cd..e5d8f05cb 100644 --- a/Tests/TestingMacrosTests/ConditionMacroTests.swift +++ b/Tests/TestingMacrosTests/ConditionMacroTests.swift @@ -436,6 +436,22 @@ struct ConditionMacroTests { #expect(diagnostic.message.contains("is redundant")) } + @Test("#expect(processExitsWith:) diagnostics", + arguments: [ + "func f() { #expectExitTest(processExitsWith: x) {} }": + "Cannot call macro ''#expectExitTest(processExitsWith:_:)'' within generic function 'f()'", + ] + ) + func exitTestDiagnostics(input: String, expectedMessage: String) throws { + let (_, diagnostics) = try parse(input) + + #expect(diagnostics.count > 0) + for diagnostic in diagnostics { + #expect(diagnostic.diagMessage.severity == .error) + #expect(diagnostic.message == expectedMessage) + } + } + #if ExperimentalExitTestValueCapture @Test("#expect(processExitsWith:) produces a diagnostic for a bad capture", arguments: [ @@ -445,6 +461,8 @@ struct ConditionMacroTests { "Type of captured value 'a' is ambiguous", "#expectExitTest(processExitsWith: x) { [a = b] in }": "Type of captured value 'a' is ambiguous", + "struct S { func f() { #expectExitTest(processExitsWith: x) { [a] in } } }": + "Cannot call macro ''#expectExitTest(processExitsWith:_:)'' within generic structure 'S'", ] ) func exitTestCaptureDiagnostics(input: String, expectedMessage: String) throws { diff --git a/Tests/TestingTests/ExitTestTests.swift b/Tests/TestingTests/ExitTestTests.swift index 265a5cfaf..16f7b0fec 100644 --- a/Tests/TestingTests/ExitTestTests.swift +++ b/Tests/TestingTests/ExitTestTests.swift @@ -457,6 +457,45 @@ private import _TestingInternals } } + @Test("Capturing a parameter to the test function") + func captureListWithParameter() async { + let i = Int.random(in: 0 ..< 1000) + + func f(j: Int) async { + await #expect(processExitsWith: .success) { [i = i as Int, j] in + #expect(i == j) + #expect(j >= 0) + #expect(j < 1000) + } + } + await f(j: i) + + await { (j: Int) in + _ = await #expect(processExitsWith: .success) { [i = i as Int, j] in + #expect(i == j) + #expect(j >= 0) + #expect(j < 1000) + } + }(i) + + // FAILS TO COMPILE: shadowing `i` with a variable of a different type will + // prevent correct expansion (we need an equivalent of decltype() for that.) +// let i = String(i) +// await #expect(processExitsWith: .success) { [i] in +// #expect(!i.isEmpty) +// } + } + + @Test("Capturing a literal expression") + func captureListWithLiterals() async { + await #expect(processExitsWith: .success) { [i = 0, f = 1.0, s = "", b = true] in + #expect(i == 0) + #expect(f == 1.0) + #expect(s == "") + #expect(b == true) + } + } + @Test("Capturing #_sourceLocation") func captureListPreservesSourceLocationMacro() async { func sl(_ sl: SourceLocation = #_sourceLocation) -> SourceLocation { From 8471b1f578bd0c5ca642d194e6422f850dc3c892 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Mon, 16 Jun 2025 12:42:29 -0400 Subject: [PATCH 027/216] Fix some typos in documentation. (#1156) This PR fixes some typos/minor errors in our documentation. Resolves #1118. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- Sources/Testing/ExitTests/ExitTest.Result.swift | 14 +++++++------- Sources/Testing/Testing.docc/exit-testing.md | 2 +- Sources/_TestDiscovery/TestContentRecord.swift | 4 ++-- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Sources/Testing/ExitTests/ExitTest.Result.swift b/Sources/Testing/ExitTests/ExitTest.Result.swift index f2c57e205..a427d4005 100644 --- a/Sources/Testing/ExitTests/ExitTest.Result.swift +++ b/Sources/Testing/ExitTests/ExitTest.Result.swift @@ -43,9 +43,9 @@ extension ExitTest { /// /// When checking the value of this property, keep in mind that the standard /// output stream is globally accessible, and any code running in an exit - /// test may write to it including including the operating system and any - /// third-party dependencies you have declared in your package. Rather than - /// comparing the value of this property with [`==`](https://developer.apple.com/documentation/swift/array/==(_:_:)), + /// test may write to it including the operating system and any third-party + /// dependencies you have declared in your package. Rather than comparing + /// the value of this property with [`==`](https://developer.apple.com/documentation/swift/array/==(_:_:)), /// use [`contains(_:)`](https://developer.apple.com/documentation/swift/collection/contains(_:)) /// to check if expected output is present. /// @@ -73,10 +73,10 @@ extension ExitTest { /// instead. /// /// When checking the value of this property, keep in mind that the standard - /// error stream is globally accessible, and any code running in an exit - /// test may write to it including including the operating system and any - /// third-party dependencies you have declared in your package. Rather than - /// comparing the value of this property with [`==`](https://developer.apple.com/documentation/swift/array/==(_:_:)), + /// output stream is globally accessible, and any code running in an exit + /// test may write to it including the operating system and any third-party + /// dependencies you have declared in your package. Rather than comparing + /// the value of this property with [`==`](https://developer.apple.com/documentation/swift/array/==(_:_:)), /// use [`contains(_:)`](https://developer.apple.com/documentation/swift/collection/contains(_:)) /// to check if expected output is present. /// diff --git a/Sources/Testing/Testing.docc/exit-testing.md b/Sources/Testing/Testing.docc/exit-testing.md index bb4fccafd..6ae81b980 100644 --- a/Sources/Testing/Testing.docc/exit-testing.md +++ b/Sources/Testing/Testing.docc/exit-testing.md @@ -153,4 +153,4 @@ extension Customer { The testing library always sets ``ExitTest/Result/exitStatus`` to the actual exit status of the child process (as reported by the system) even if you do not -pass it. +observe `\.exitStatus`. diff --git a/Sources/_TestDiscovery/TestContentRecord.swift b/Sources/_TestDiscovery/TestContentRecord.swift index df868c975..384113c1b 100644 --- a/Sources/_TestDiscovery/TestContentRecord.swift +++ b/Sources/_TestDiscovery/TestContentRecord.swift @@ -71,8 +71,8 @@ public struct TestContentRecord where T: DiscoverableAsTestContent { /// /// | Platform | Pointer Type | /// |-|-| - /// | macOS, iOS, watchOS, tvOS, visionOS | `UnsafePointer` | - /// | Linux, FreeBSD, Android | `UnsafePointer` | + /// | macOS, iOS, watchOS, tvOS, visionOS | `UnsafePointer` | + /// | Linux, FreeBSD, Android | `UnsafePointer` | /// | OpenBSD | `UnsafePointer` | /// | Windows | `HMODULE` | /// From 9015405745aa106179e5f8472691ae0e3b71b206 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Mon, 16 Jun 2025 15:35:05 -0400 Subject: [PATCH 028/216] Revert "Update outdated Suite macro documentation (#1142)" This reverts commit 25b61ef4667a6d1dda30cb3420336621886529c4. --- Sources/Testing/Test+Macro.swift | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/Sources/Testing/Test+Macro.swift b/Sources/Testing/Test+Macro.swift index f2fc415d6..44e9d3d72 100644 --- a/Sources/Testing/Test+Macro.swift +++ b/Sources/Testing/Test+Macro.swift @@ -53,8 +53,9 @@ public typealias __XCTestCompatibleSelector = Never /// - Parameters: /// - traits: Zero or more traits to apply to this test suite. /// -/// A test suite is a type that contains one or more test functions. -/// Any type may be a test suite. +/// A test suite is a type that contains one or more test functions. Any +/// copyable type (that is, any type that is not marked `~Copyable`) may be a +/// test suite. /// /// The use of the `@Suite` attribute is optional; types are recognized as test /// suites even if they do not have the `@Suite` attribute applied to them. @@ -80,8 +81,9 @@ public macro Suite( /// from the associated type's name. /// - traits: Zero or more traits to apply to this test suite. /// -/// A test suite is a type that contains one or more test functions. -/// Any type may be a test suite. +/// A test suite is a type that contains one or more test functions. Any +/// copyable type (that is, any type that is not marked `~Copyable`) may be a +/// test suite. /// /// The use of the `@Suite` attribute is optional; types are recognized as test /// suites even if they do not have the `@Suite` attribute applied to them. From 7c672abd0580d4fa4cb9a90acf5a95e10a703bb8 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Mon, 16 Jun 2025 15:37:26 -0400 Subject: [PATCH 029/216] Replace ~Copyable with ~Escapable in the relevant documentation about Suite types. --- Sources/Testing/Test+Macro.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/Testing/Test+Macro.swift b/Sources/Testing/Test+Macro.swift index 44e9d3d72..a8d9eaf51 100644 --- a/Sources/Testing/Test+Macro.swift +++ b/Sources/Testing/Test+Macro.swift @@ -54,7 +54,7 @@ public typealias __XCTestCompatibleSelector = Never /// - traits: Zero or more traits to apply to this test suite. /// /// A test suite is a type that contains one or more test functions. Any -/// copyable type (that is, any type that is not marked `~Copyable`) may be a +/// escapable type (that is, any type that is not marked `~Escapable`) may be a /// test suite. /// /// The use of the `@Suite` attribute is optional; types are recognized as test @@ -82,7 +82,7 @@ public macro Suite( /// - traits: Zero or more traits to apply to this test suite. /// /// A test suite is a type that contains one or more test functions. Any -/// copyable type (that is, any type that is not marked `~Copyable`) may be a +/// escapable type (that is, any type that is not marked `~Escapable`) may be a /// test suite. /// /// The use of the `@Suite` attribute is optional; types are recognized as test From eeeffd49695b44aff6c908c9e463f48cc7739250 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Wed, 18 Jun 2025 13:54:51 -0400 Subject: [PATCH 030/216] Suppress the complex expansion of expectations when we see effects in the lexical context. (#1161) This PR changes the behaviour of the testing library for expressions such as: ```swift try #expect(a == b) ``` Currently, we don't expand that expression correctly because we can't tell where the `try` keyword should be applied. It sometimes expands and sometimes doesn't. This PR detects the presence of those keywords (with a recent-enough toolchain) and, if found, disables the fancy expansion in favour of a simpler one that is less likely to fail to compile. More thorough support for effectful expressions in expectations is tracked by #840 which involves fully refactoring the implementation of the `#expect()` macro. See also #162 for some more context. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- .../Support/ConditionArgumentParsing.swift | 6 +- .../Support/EffectfulExpressionHandling.swift | 121 ++++++++++++------ 2 files changed, 87 insertions(+), 40 deletions(-) diff --git a/Sources/TestingMacros/Support/ConditionArgumentParsing.swift b/Sources/TestingMacros/Support/ConditionArgumentParsing.swift index e0ccda9a7..254f3d0aa 100644 --- a/Sources/TestingMacros/Support/ConditionArgumentParsing.swift +++ b/Sources/TestingMacros/Support/ConditionArgumentParsing.swift @@ -517,10 +517,8 @@ private func _parseCondition(from expr: ExprSyntax, for macro: some Freestanding /// - Returns: An instance of ``Condition`` describing `expr`. func parseCondition(from expr: ExprSyntax, for macro: some FreestandingMacroExpansionSyntax, in context: some MacroExpansionContext) -> Condition { // If the condition involves the `unsafe`, `try`, or `await` keywords, assume - // we cannot expand it. This check cannot handle expressions like - // `try #expect(a.b(c))` where `b()` is throwing because the `try` keyword is - // outside the macro expansion. SEE: rdar://109470248 - let effectKeywordsToApply = findEffectKeywords(in: expr, context: context) + // we cannot expand it. + let effectKeywordsToApply = findEffectKeywords(in: expr).union(findEffectKeywords(in: context)) guard effectKeywordsToApply.intersection([.unsafe, .try, .await]).isEmpty else { return Condition(expression: expr) } diff --git a/Sources/TestingMacros/Support/EffectfulExpressionHandling.swift b/Sources/TestingMacros/Support/EffectfulExpressionHandling.swift index a0b84e737..b093d1e77 100644 --- a/Sources/TestingMacros/Support/EffectfulExpressionHandling.swift +++ b/Sources/TestingMacros/Support/EffectfulExpressionHandling.swift @@ -14,6 +14,58 @@ import SwiftSyntaxMacros // MARK: - Finding effect keywords and expressions +/// Get the effect keyword corresponding to a given syntax node, if any. +/// +/// - Parameters: +/// - expr: The syntax node that may represent an effectful expression. +/// +/// - Returns: The effect keyword corresponding to `expr`, if any. +private func _effectKeyword(for expr: ExprSyntax) -> Keyword? { + switch expr.kind { + case .tryExpr: + return .try + case .awaitExpr: + return .await + case .consumeExpr: + return .consume + case .borrowExpr: + return .borrow + case .unsafeExpr: + return .unsafe + default: + return nil + } +} + +/// Determine how to descend further into a syntax node tree from a given node. +/// +/// - Parameters: +/// - node: The syntax node currently being walked. +/// +/// - Returns: Whether or not to descend into `node` and visit its children. +private func _continueKind(for node: Syntax) -> SyntaxVisitorContinueKind { + switch node.kind { + case .tryExpr, .awaitExpr, .consumeExpr, .borrowExpr, .unsafeExpr: + // If this node represents an effectful expression, look inside it for + // additional such expressions. + return .visitChildren + case .closureExpr, .functionDecl: + // Do not delve into closures or function declarations. + return .skipChildren + case .variableDecl: + // Delve into variable declarations. + return .visitChildren + default: + // Do not delve into declarations other than variables. + if node.isProtocol((any DeclSyntaxProtocol).self) { + return .skipChildren + } + } + + // Recurse into everything else. + return .visitChildren +} + /// A syntax visitor class that looks for effectful keywords in a given /// expression. private final class _EffectFinder: SyntaxAnyVisitor { @@ -21,32 +73,11 @@ private final class _EffectFinder: SyntaxAnyVisitor { var effectKeywords: Set = [] override func visitAny(_ node: Syntax) -> SyntaxVisitorContinueKind { - switch node.kind { - case .tryExpr: - effectKeywords.insert(.try) - case .awaitExpr: - effectKeywords.insert(.await) - case .consumeExpr: - effectKeywords.insert(.consume) - case .borrowExpr: - effectKeywords.insert(.borrow) - case .unsafeExpr: - effectKeywords.insert(.unsafe) - case .closureExpr, .functionDecl: - // Do not delve into closures or function declarations. - return .skipChildren - case .variableDecl: - // Delve into variable declarations. - return .visitChildren - default: - // Do not delve into declarations other than variables. - if node.isProtocol((any DeclSyntaxProtocol).self) { - return .skipChildren - } + if let expr = node.as(ExprSyntax.self), let keyword = _effectKeyword(for: expr) { + effectKeywords.insert(keyword) } - // Recurse into everything else. - return .visitChildren + return _continueKind(for: node) } } @@ -54,7 +85,6 @@ private final class _EffectFinder: SyntaxAnyVisitor { /// /// - Parameters: /// - node: The node to inspect. -/// - context: The macro context in which the expression is being parsed. /// /// - Returns: A set of effectful keywords such as `await` that are present in /// `node`. @@ -62,13 +92,27 @@ private final class _EffectFinder: SyntaxAnyVisitor { /// This function does not descend into function declarations or closure /// expressions because they represent distinct lexical contexts and their /// effects are uninteresting in the context of `node` unless they are called. -func findEffectKeywords(in node: some SyntaxProtocol, context: some MacroExpansionContext) -> Set { - // TODO: gather any effects from the lexical context once swift-syntax-#3037 and related PRs land +func findEffectKeywords(in node: some SyntaxProtocol) -> Set { let effectFinder = _EffectFinder(viewMode: .sourceAccurate) effectFinder.walk(node) return effectFinder.effectKeywords } +/// Find effectful keywords in a macro's lexical context. +/// +/// - Parameters: +/// - context: The macro context in which the expression is being parsed. +/// +/// - Returns: A set of effectful keywords such as `await` that are present in +/// `context` and would apply to an expression macro during its expansion. +func findEffectKeywords(in context: some MacroExpansionContext) -> Set { + let result = context.lexicalContext.reversed().lazy + .prefix { _continueKind(for: $0) == .visitChildren } + .compactMap { $0.as(ExprSyntax.self) } + .compactMap(_effectKeyword(for:)) + return Set(result) +} + extension BidirectionalCollection { /// The suffix of syntax nodes in this collection which are effectful /// expressions, such as those for `try` or `await`. @@ -128,10 +172,13 @@ private func _makeCallToEffectfulThunk(_ thunkName: TokenSyntax, passing expr: s /// - Parameters: /// - effectfulKeywords: The effectful keywords to apply. /// - expr: The expression to apply the keywords and thunk functions to. +/// - insertThunkCalls: Whether or not to also insert calls to thunks to +/// ensure the inserted keywords do not generate warnings. If you aren't +/// sure whether thunk calls are needed, pass `true`. /// /// - Returns: A copy of `expr` if no changes are needed, or an expression that /// adds the keywords in `effectfulKeywords` to `expr`. -func applyEffectfulKeywords(_ effectfulKeywords: Set, to expr: some ExprSyntaxProtocol) -> ExprSyntax { +func applyEffectfulKeywords(_ effectfulKeywords: Set, to expr: some ExprSyntaxProtocol, insertThunkCalls: Bool = true) -> ExprSyntax { let originalExpr = expr var expr = ExprSyntax(expr.trimmed) @@ -141,14 +188,16 @@ func applyEffectfulKeywords(_ effectfulKeywords: Set, to expr: some Exp let needUnsafe = isUnsafeKeywordSupported && effectfulKeywords.contains(.unsafe) && !expr.is(UnsafeExprSyntax.self) // First, add thunk function calls. - if needAwait { - expr = _makeCallToEffectfulThunk(.identifier("__requiringAwait"), passing: expr) - } - if needTry { - expr = _makeCallToEffectfulThunk(.identifier("__requiringTry"), passing: expr) - } - if needUnsafe { - expr = _makeCallToEffectfulThunk(.identifier("__requiringUnsafe"), passing: expr) + if insertThunkCalls { + if needAwait { + expr = _makeCallToEffectfulThunk(.identifier("__requiringAwait"), passing: expr) + } + if needTry { + expr = _makeCallToEffectfulThunk(.identifier("__requiringTry"), passing: expr) + } + if needUnsafe { + expr = _makeCallToEffectfulThunk(.identifier("__requiringUnsafe"), passing: expr) + } } // Then add keyword expressions. (We do this separately so we end up writing From 66701fb9370a7ee5554b3a8f21067869616c2099 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Wed, 18 Jun 2025 14:00:39 -0400 Subject: [PATCH 031/216] Improve type inference for exit test value captures. (#1163) This PR refactors the (new) type inference logic for exit test capture lists to use a syntax visitor, which allows for the types of more complex expressions to be inferred. For example, previously the type of this capture would not be inferred: ```swift [x = try await f() as Int] ``` Even though the type (`Int`) is clearly present, because the `AsExprSyntax` is nested in an `AwaitExprSyntax` and then a `TryExprSyntax`. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- .../TypeSyntaxProtocolAdditions.swift | 7 + .../Support/ClosureCaptureListParsing.swift | 203 ++++++++++++++---- .../ConditionMacroTests.swift | 4 + Tests/TestingTests/ExitTestTests.swift | 38 +++- 4 files changed, 207 insertions(+), 45 deletions(-) diff --git a/Sources/TestingMacros/Support/Additions/TypeSyntaxProtocolAdditions.swift b/Sources/TestingMacros/Support/Additions/TypeSyntaxProtocolAdditions.swift index e1bd346ed..e9bf03a02 100644 --- a/Sources/TestingMacros/Support/Additions/TypeSyntaxProtocolAdditions.swift +++ b/Sources/TestingMacros/Support/Additions/TypeSyntaxProtocolAdditions.swift @@ -36,6 +36,13 @@ extension TypeSyntaxProtocol { .contains(.keyword(.some)) } + /// Whether or not this type is `any T` or a type derived from such a type. + var isAny: Bool { + tokens(viewMode: .fixedUp).lazy + .map(\.tokenKind) + .contains(.keyword(.any)) + } + /// Check whether or not this type is named with the specified name and /// module. /// diff --git a/Sources/TestingMacros/Support/ClosureCaptureListParsing.swift b/Sources/TestingMacros/Support/ClosureCaptureListParsing.swift index 37e696865..0c36fbbf7 100644 --- a/Sources/TestingMacros/Support/ClosureCaptureListParsing.swift +++ b/Sources/TestingMacros/Support/ClosureCaptureListParsing.swift @@ -50,62 +50,177 @@ struct CapturedValueInfo { return } - // Potentially get the name of the type comprising the current lexical - // context (i.e. whatever `Self` is.) - lazy var lexicalContext = context.lexicalContext - lazy var typeNameOfLexicalContext = { - let lexicalContext = lexicalContext.drop { !$0.isProtocol((any DeclGroupSyntax).self) } - return context.type(ofLexicalContext: lexicalContext) - }() + if let (expr, type) = Self._inferExpressionAndType(of: capture, in: context) { + self.expression = expr + self.type = type + } else { + // Not enough contextual information to derive the type here. + context.diagnose(.typeOfCaptureIsAmbiguous(capture)) + } + } + /// Infer the captured expression and the type of a closure capture list item. + /// + /// - Parameters: + /// - capture: The closure capture list item to inspect. + /// - context: The macro context in which the expression is being parsed. + /// + /// - Returns: A tuple containing the expression and type of `capture`, or + /// `nil` if they could not be inferred. + private static func _inferExpressionAndType(of capture: ClosureCaptureSyntax, in context: some MacroExpansionContext) -> (ExprSyntax, TypeSyntax)? { if let initializer = capture.initializer { // Found an initializer clause. Extract the expression it captures. - self.expression = removeParentheses(from: initializer.value) ?? initializer.value + let finder = _ExprTypeFinder(in: context) + finder.walk(initializer.value) + if let inferredType = finder.inferredType { + return (initializer.value, inferredType) + } + } else if capture.name.tokenKind == .keyword(.self), + let typeNameOfLexicalContext = Self._inferSelf(from: context) { + // Capturing self. + return (ExprSyntax(DeclReferenceExprSyntax(baseName: .keyword(.self))), typeNameOfLexicalContext) + } else if let parameterType = Self._findTypeOfParameter(named: capture.name, in: context.lexicalContext) { + return (ExprSyntax(DeclReferenceExprSyntax(baseName: capture.name.trimmed)), parameterType) + } + + return nil + } + + private final class _ExprTypeFinder: SyntaxAnyVisitor where C: MacroExpansionContext { + var context: C + + /// The type that was inferred from the visited syntax tree, if any. + /// + /// This type has not been fixed up yet. Use ``inferredType`` for the final + /// derived type. + private var _inferredType: TypeSyntax? + + /// Whether or not the inferred type has been made optional by e.g. `try?`. + private var _needsOptionalApplied = false + + /// The type that was inferred from the visited syntax tree, if any. + var inferredType: TypeSyntax? { + _inferredType.flatMap { inferredType in + if inferredType.isSome || inferredType.isAny { + // `some` and `any` types are not concrete and cannot be inferred. + nil + } else if _needsOptionalApplied { + TypeSyntax(OptionalTypeSyntax(wrappedType: inferredType.trimmed)) + } else { + inferredType + } + } + } + + init(in context: C) { + self.context = context + super.init(viewMode: .sourceAccurate) + } + + override func visitAny(_ node: Syntax) -> SyntaxVisitorContinueKind { + if inferredType != nil { + // Another part of the syntax tree has already provided a type. Stop. + return .skipChildren + } - // Find the 'as' clause so we can determine the type of the captured value. - if let asExpr = self.expression.as(AsExprSyntax.self) { - self.type = if asExpr.questionOrExclamationMark?.tokenKind == .postfixQuestionMark { + switch node.kind { + case .asExpr: + let asExpr = node.cast(AsExprSyntax.self) + if let type = asExpr.type.as(IdentifierTypeSyntax.self), type.name.tokenKind == .keyword(.Self) { + // `Self` should resolve to the lexical context's type. + _inferredType = CapturedValueInfo._inferSelf(from: context) + } else if asExpr.questionOrExclamationMark?.tokenKind == .postfixQuestionMark { // If the caller is using as?, make the type optional. - TypeSyntax(OptionalTypeSyntax(wrappedType: asExpr.type.trimmed)) + _inferredType = TypeSyntax(OptionalTypeSyntax(wrappedType: asExpr.type.trimmed)) } else { - asExpr.type + _inferredType = asExpr.type } - } else if let selfExpr = self.expression.as(DeclReferenceExprSyntax.self), - selfExpr.baseName.tokenKind == .keyword(.self), - selfExpr.argumentNames == nil, - let typeNameOfLexicalContext { - // Copying self. - self.type = typeNameOfLexicalContext - } else { - // Handle literals. Any other types are ambiguous. - switch self.expression.kind { - case .integerLiteralExpr: - self.type = TypeSyntax(IdentifierTypeSyntax(name: .identifier("IntegerLiteralType"))) - case .floatLiteralExpr: - self.type = TypeSyntax(IdentifierTypeSyntax(name: .identifier("FloatLiteralType"))) - case .booleanLiteralExpr: - self.type = TypeSyntax(IdentifierTypeSyntax(name: .identifier("BooleanLiteralType"))) - case .stringLiteralExpr, .simpleStringLiteralExpr: - self.type = TypeSyntax(IdentifierTypeSyntax(name: .identifier("StringLiteralType"))) - default: - context.diagnose(.typeOfCaptureIsAmbiguous(capture, initializedWith: initializer)) + return .skipChildren + + case .awaitExpr, .unsafeExpr: + // These effect keywords do not affect the type of the expression. + return .visitChildren + + case .tryExpr: + let tryExpr = node.cast(TryExprSyntax.self) + if tryExpr.questionOrExclamationMark?.tokenKind == .postfixQuestionMark { + // The resulting type from the inner expression will be optionalized. + _needsOptionalApplied = true } - } + return .visitChildren - } else if capture.name.tokenKind == .keyword(.self), - let typeNameOfLexicalContext { - // Capturing self. - self.expression = "self" - self.type = typeNameOfLexicalContext - } else if let parameterType = Self._findTypeOfParameter(named: capture.name, in: lexicalContext) { - self.expression = ExprSyntax(DeclReferenceExprSyntax(baseName: capture.name.trimmed)) - self.type = parameterType - } else { - // Not enough contextual information to derive the type here. - context.diagnose(.typeOfCaptureIsAmbiguous(capture)) + case .tupleExpr: + // If the tuple contains exactly one element, it's just parentheses + // around that expression. + let tupleExpr = node.cast(TupleExprSyntax.self) + if tupleExpr.elements.count == 1 { + return .visitChildren + } + + // Otherwise, we need to try to compose the type as a tuple type from + // the types of all elements in the tuple expression. Note that tuples + // do not conform to Sendable or Codable, so our current use of this + // code in exit tests will still diagnose an error, but the error ("must + // conform") will be more useful than "couldn't infer". + let elements = tupleExpr.elements.compactMap { element in + let finder = Self(in: context) + finder.walk(element.expression) + return finder.inferredType.map { type in + TupleTypeElementSyntax(firstName: element.label?.trimmed, type: type.trimmed) + } + } + if elements.count == tupleExpr.elements.count { + _inferredType = TypeSyntax( + TupleTypeSyntax(elements: TupleTypeElementListSyntax { elements }) + ) + } + return .skipChildren + + case .declReferenceExpr: + // If the reference is to `self` without any arguments, its type can be + // inferred from the lexical context. + let expr = node.cast(DeclReferenceExprSyntax.self) + if expr.baseName.tokenKind == .keyword(.self), expr.argumentNames == nil { + _inferredType = CapturedValueInfo._inferSelf(from: context) + } + return .skipChildren + + case .integerLiteralExpr: + _inferredType = TypeSyntax(IdentifierTypeSyntax(name: .identifier("IntegerLiteralType"))) + return .skipChildren + + case .floatLiteralExpr: + _inferredType = TypeSyntax(IdentifierTypeSyntax(name: .identifier("FloatLiteralType"))) + return .skipChildren + + case .booleanLiteralExpr: + _inferredType = TypeSyntax(IdentifierTypeSyntax(name: .identifier("BooleanLiteralType"))) + return .skipChildren + + case .stringLiteralExpr, .simpleStringLiteralExpr: + _inferredType = TypeSyntax(IdentifierTypeSyntax(name: .identifier("StringLiteralType"))) + return .skipChildren + + default: + // We don't know how to infer a type from this syntax node, so do not + // proceed further. + return .skipChildren + } } } + /// Get the type of `self` inferred from the given context. + /// + /// - Parameters: + /// - context: The macro context in which the expression is being parsed. + /// + /// - Returns: The type in `lexicalContext` corresponding to `Self`, or `nil` + /// if it could not be determined. + private static func _inferSelf(from context: some MacroExpansionContext) -> TypeSyntax? { + let lexicalContext = context.lexicalContext.drop { !$0.isProtocol((any DeclGroupSyntax).self) } + return context.type(ofLexicalContext: lexicalContext) + } + /// Find a function or closure parameter in the given lexical context with a /// given name and return its type. /// diff --git a/Tests/TestingMacrosTests/ConditionMacroTests.swift b/Tests/TestingMacrosTests/ConditionMacroTests.swift index e5d8f05cb..2c3cc38d8 100644 --- a/Tests/TestingMacrosTests/ConditionMacroTests.swift +++ b/Tests/TestingMacrosTests/ConditionMacroTests.swift @@ -461,6 +461,10 @@ struct ConditionMacroTests { "Type of captured value 'a' is ambiguous", "#expectExitTest(processExitsWith: x) { [a = b] in }": "Type of captured value 'a' is ambiguous", + "#expectExitTest(processExitsWith: x) { [a = b as any T] in }": + "Type of captured value 'a' is ambiguous", + "#expectExitTest(processExitsWith: x) { [a = b as some T] in }": + "Type of captured value 'a' is ambiguous", "struct S { func f() { #expectExitTest(processExitsWith: x) { [a] in } } }": "Cannot call macro ''#expectExitTest(processExitsWith:_:)'' within generic structure 'S'", ] diff --git a/Tests/TestingTests/ExitTestTests.swift b/Tests/TestingTests/ExitTestTests.swift index 16f7b0fec..89aae3894 100644 --- a/Tests/TestingTests/ExitTestTests.swift +++ b/Tests/TestingTests/ExitTestTests.swift @@ -407,9 +407,10 @@ private import _TestingInternals @Test("self in capture list") func captureListWithSelf() async { - await #expect(processExitsWith: .success) { [self, x = self] in + await #expect(processExitsWith: .success) { [self, x = self, y = self as Self] in #expect(self.property == 456) #expect(x.property == 456) + #expect(y.property == 456) } } } @@ -506,6 +507,41 @@ private import _TestingInternals } } + @Test("Capturing an optional value") + func captureListWithOptionalValue() async throws { + await #expect(processExitsWith: .success) { [x = nil as Int?] in + #expect(x != 1) + } + await #expect(processExitsWith: .success) { [x = (0 as Any) as? String] in + #expect(x == nil) + } + } + + @Test("Capturing an effectful expression") + func captureListWithEffectfulExpression() async throws { + func f() async throws -> Int { 0 } + try await #require(processExitsWith: .success) { [f = try await f() as Int] in + #expect(f == 0) + } + try await #expect(processExitsWith: .success) { [f = f() as Int] in + #expect(f == 0) + } + } + +#if false // intentionally fails to compile + @Test("Capturing a tuple") + func captureListWithTuple() async throws { + // A tuple whose elements conform to Codable does not itself conform to + // Codable, so we cannot actually express this capture list in a way that + // works with #expect(). + await #expect(processExitsWith: .success) { [x = (0 as Int, 1 as Double, "2" as String)] in + #expect(x.0 == 0) + #expect(x.1 == 1) + #expect(x.2 == "2") + } + } +#endif + #if false // intentionally fails to compile struct NonCodableValue {} From e63d542c824859916234e2a1307697056e1712d1 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Wed, 18 Jun 2025 14:55:22 -0400 Subject: [PATCH 032/216] Diagnose when we incorrectly infer the type of a capture list item in an exit test. (#1152) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-up to #1130, split out for clarity. This PR adds a custom diagnostic at compile time if we incorrectly infer the type of a captured function argument or `self` in an exit test. For example: ```swift func f(_ x: Int) async { let x = String(x) // local type of 'x' is String, not Int await #expect(processExitsWith: ...) { [x] in ... } } ``` This improves our feedback to the developer when we encounter a pattern like that. The developer will now see: > 🛑 Type of captured value 'x' is ambiguous ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- .../Expectations/Expectation+Macro.swift | 29 +++++++++++++++++-- .../ExitTestCapturedValueMacro.swift | 24 +++++++++++++++ .../Support/ClosureCaptureListParsing.swift | 2 +- Sources/TestingMacros/TestingMacrosMain.swift | 1 + Tests/TestingTests/ExitTestTests.swift | 12 +++++--- 5 files changed, 61 insertions(+), 7 deletions(-) diff --git a/Sources/Testing/Expectations/Expectation+Macro.swift b/Sources/Testing/Expectations/Expectation+Macro.swift index df9d80058..8a6749a31 100644 --- a/Sources/Testing/Expectations/Expectation+Macro.swift +++ b/Sources/Testing/Expectations/Expectation+Macro.swift @@ -580,6 +580,7 @@ public macro require( /// - Parameters: /// - value: The captured value. /// - name: The name of the capture list item corresponding to `value`. +/// - expectedType: The type of `value`. /// /// - Returns: `value` verbatim. /// @@ -588,7 +589,8 @@ public macro require( @freestanding(expression) public macro __capturedValue( _ value: T, - _ name: String + _ name: String, + _ expectedType: T.Type ) -> T = #externalMacro(module: "TestingMacros", type: "ExitTestCapturedValueMacro") where T: Sendable & Codable /// Emit a compile-time diagnostic when an unsupported value is captured by an @@ -597,6 +599,7 @@ public macro __capturedValue( /// - Parameters: /// - value: The captured value. /// - name: The name of the capture list item corresponding to `value`. +/// - expectedType: The type of `value`. /// /// - Returns: The result of a call to `fatalError()`. `value` is discarded at /// compile time. @@ -606,5 +609,27 @@ public macro __capturedValue( @freestanding(expression) public macro __capturedValue( _ value: borrowing T, - _ name: String + _ name: String, + _ expectedType: T.Type ) -> Never = #externalMacro(module: "TestingMacros", type: "ExitTestBadCapturedValueMacro") where T: ~Copyable & ~Escapable + +/// Emit a compile-time diagnostic when a value is captured by an exit test but +/// we inferred the wrong type. +/// +/// - Parameters: +/// - value: The captured value. +/// - name: The name of the capture list item corresponding to `value`. +/// - expectedType: The _expected_ type of `value`, which will differ from the +/// _actual_ type of `value`. +/// +/// - Returns: The result of a call to `fatalError()`. `value` is discarded at +/// compile time. +/// +/// - Warning: This macro is used to implement the `#expect(processExitsWith:)` +/// macro. Do not use it directly. +@freestanding(expression) +public macro __capturedValue( + _ value: borrowing T, + _ name: String, + _ expectedType: U.Type +) -> T = #externalMacro(module: "TestingMacros", type: "ExitTestIncorrectlyCapturedValueMacro") where T: ~Copyable & ~Escapable, U: ~Copyable & ~Escapable diff --git a/Sources/TestingMacros/ExitTestCapturedValueMacro.swift b/Sources/TestingMacros/ExitTestCapturedValueMacro.swift index 0038dac7c..4bba47c79 100644 --- a/Sources/TestingMacros/ExitTestCapturedValueMacro.swift +++ b/Sources/TestingMacros/ExitTestCapturedValueMacro.swift @@ -8,6 +8,7 @@ // See https://swift.org/CONTRIBUTORS.txt for Swift project authors // +import SwiftParser public import SwiftSyntax import SwiftSyntaxBuilder public import SwiftSyntaxMacros @@ -52,3 +53,26 @@ public struct ExitTestBadCapturedValueMacro: ExpressionMacro, Sendable { return #"Swift.fatalError("Unsupported")"# } } + +/// The implementation of the `#__capturedValue()` macro when the type we +/// inferred for the value was incorrect. +/// +/// This type is used to implement the `#__capturedValue()` macro. Do not use it +/// directly. +public struct ExitTestIncorrectlyCapturedValueMacro: ExpressionMacro, Sendable { + public static func expansion( + of macro: some FreestandingMacroExpansionSyntax, + in context: some MacroExpansionContext + ) throws -> ExprSyntax { + let arguments = Array(macro.arguments) + let expr = arguments[0].expression + let nameExpr = arguments[1].expression.cast(StringLiteralExprSyntax.self) + + // Diagnose that the type of 'expr' is invalid. + let name = nameExpr.representedLiteralValue ?? expr.trimmedDescription + let capture = ClosureCaptureSyntax(name: .identifier(name)) + context.diagnose(.typeOfCaptureIsAmbiguous(capture)) + + return expr + } +} diff --git a/Sources/TestingMacros/Support/ClosureCaptureListParsing.swift b/Sources/TestingMacros/Support/ClosureCaptureListParsing.swift index 0c36fbbf7..80c216854 100644 --- a/Sources/TestingMacros/Support/ClosureCaptureListParsing.swift +++ b/Sources/TestingMacros/Support/ClosureCaptureListParsing.swift @@ -36,7 +36,7 @@ struct CapturedValueInfo { /// The expression to assign to the captured value with type-checking applied. var typeCheckedExpression: ExprSyntax { - #"#__capturedValue(\#(expression.trimmed), \#(literal: name.trimmedDescription))"# + #"#__capturedValue(\#(expression.trimmed), \#(literal: name.trimmedDescription), (\#(type.trimmed)).self)"# } init(_ capture: ClosureCaptureSyntax, in context: some MacroExpansionContext) { diff --git a/Sources/TestingMacros/TestingMacrosMain.swift b/Sources/TestingMacros/TestingMacrosMain.swift index 4e98115d0..074aeb86b 100644 --- a/Sources/TestingMacros/TestingMacrosMain.swift +++ b/Sources/TestingMacros/TestingMacrosMain.swift @@ -30,6 +30,7 @@ struct TestingMacrosMain: CompilerPlugin { ExitTestRequireMacro.self, ExitTestCapturedValueMacro.self, ExitTestBadCapturedValueMacro.self, + ExitTestIncorrectlyCapturedValueMacro.self, TagMacro.self, SourceLocationMacro.self, PragmaMacro.self, diff --git a/Tests/TestingTests/ExitTestTests.swift b/Tests/TestingTests/ExitTestTests.swift index 89aae3894..89d78cdde 100644 --- a/Tests/TestingTests/ExitTestTests.swift +++ b/Tests/TestingTests/ExitTestTests.swift @@ -479,12 +479,16 @@ private import _TestingInternals } }(i) +#if false // intentionally fails to compile // FAILS TO COMPILE: shadowing `i` with a variable of a different type will // prevent correct expansion (we need an equivalent of decltype() for that.) -// let i = String(i) -// await #expect(processExitsWith: .success) { [i] in -// #expect(!i.isEmpty) -// } + func g(i: Int) async { + let i = String(i) + await #expect(processExitsWith: .success) { [i] in + #expect(!i.isEmpty) + } + } +#endif } @Test("Capturing a literal expression") From 77de51d550fe406c15af4eb5ac3834580afa3eaf Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Tue, 24 Jun 2025 21:29:55 -0400 Subject: [PATCH 033/216] Work around a compiler regression affecting exit test value capturing. (#1171) --- .../ExpectationChecking+Macro.swift | 27 +++++++++++++++++++ Sources/TestingMacros/ConditionMacro.swift | 27 +++++++++++++++++++ .../Support/ClosureCaptureListParsing.swift | 2 +- 3 files changed, 55 insertions(+), 1 deletion(-) diff --git a/Sources/Testing/Expectations/ExpectationChecking+Macro.swift b/Sources/Testing/Expectations/ExpectationChecking+Macro.swift index 3a190e679..9dfc5ace7 100644 --- a/Sources/Testing/Expectations/ExpectationChecking+Macro.swift +++ b/Sources/Testing/Expectations/ExpectationChecking+Macro.swift @@ -1175,6 +1175,7 @@ public func __checkClosureCall( /// /// - Warning: This function is used to implement the `#expect()` and /// `#require()` macros. Do not call it directly. +#if SWT_FIXED_154221449 @_spi(Experimental) public func __checkClosureCall( identifiedBy exitTestID: (UInt64, UInt64, UInt64, UInt64), @@ -1199,6 +1200,32 @@ public func __checkClosureCall( sourceLocation: sourceLocation ) } +#else +@_spi(Experimental) +public func __checkClosureCall( + identifiedBy exitTestID: (UInt64, UInt64, UInt64, UInt64), + encodingCapturedValues capturedValues: repeat each T, + processExitsWith expectedExitCondition: ExitTest.Condition, + observing observedValues: [any PartialKeyPath & Sendable] = [], + performing _: @convention(c) () -> Void, + expression: __Expression, + comments: @autoclosure () -> [Comment], + isRequired: Bool, + isolation: isolated (any Actor)? = #isolation, + sourceLocation: SourceLocation +) async -> Result where repeat each T: Codable & Sendable { + await callExitTest( + identifiedBy: exitTestID, + encodingCapturedValues: Array(repeat each capturedValues), + processExitsWith: expectedExitCondition, + observing: observedValues, + expression: expression, + comments: comments(), + isRequired: isRequired, + sourceLocation: sourceLocation + ) +} +#endif #endif // MARK: - diff --git a/Sources/TestingMacros/ConditionMacro.swift b/Sources/TestingMacros/ConditionMacro.swift index 0ef0970e9..9d0c4f15f 100644 --- a/Sources/TestingMacros/ConditionMacro.swift +++ b/Sources/TestingMacros/ConditionMacro.swift @@ -98,7 +98,20 @@ extension ConditionMacro { if let trailingClosureIndex { // Assume that the comment, if present is the last argument in the // argument list prior to the trailing closure that has no label. +#if SWT_FIXED_154221449 commentIndex = macroArguments[.. 1 { // If there is no trailing closure argument and there is more than one // argument, then the comment is the last argument with no label (and also @@ -547,6 +560,7 @@ extension ExitTestConditionMacro { var leadingArguments = [ Argument(label: "identifiedBy", expression: idExpr), ] +#if SWT_FIXED_154221449 if !capturedValues.isEmpty { leadingArguments.append( Argument( @@ -559,6 +573,19 @@ extension ExitTestConditionMacro { ) ) } +#else + if let firstCapturedValue = capturedValues.first { + leadingArguments.append( + Argument( + label: "encodingCapturedValues", + expression: firstCapturedValue.typeCheckedExpression + ) + ) + leadingArguments += capturedValues.dropFirst() + .map(\.typeCheckedExpression) + .map { Argument(expression: $0) } + } +#endif arguments = leadingArguments + arguments // Replace the exit test body (as an argument to the macro) with a stub diff --git a/Sources/TestingMacros/Support/ClosureCaptureListParsing.swift b/Sources/TestingMacros/Support/ClosureCaptureListParsing.swift index 80c216854..08536aa69 100644 --- a/Sources/TestingMacros/Support/ClosureCaptureListParsing.swift +++ b/Sources/TestingMacros/Support/ClosureCaptureListParsing.swift @@ -36,7 +36,7 @@ struct CapturedValueInfo { /// The expression to assign to the captured value with type-checking applied. var typeCheckedExpression: ExprSyntax { - #"#__capturedValue(\#(expression.trimmed), \#(literal: name.trimmedDescription), (\#(type.trimmed)).self)"# + #"#__capturedValue(\#(expression.trimmed), \#(literal: name.trimmedDescription), \#(type.trimmed).self)"# } init(_ capture: ClosureCaptureSyntax, in context: some MacroExpansionContext) { From a2081545abdc24e58438736683a45c1c0da8ce9a Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Wed, 25 Jun 2025 12:10:36 -0400 Subject: [PATCH 034/216] Fix a miscompile when a test function has a raw identifier parameter label. (#1168) Resolves #1167. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- Sources/TestingMacros/TestDeclarationMacro.swift | 2 ++ Tests/TestingTests/MiscellaneousTests.swift | 5 +++++ 2 files changed, 7 insertions(+) diff --git a/Sources/TestingMacros/TestDeclarationMacro.swift b/Sources/TestingMacros/TestDeclarationMacro.swift index 58e8259ec..50ac690d2 100644 --- a/Sources/TestingMacros/TestDeclarationMacro.swift +++ b/Sources/TestingMacros/TestDeclarationMacro.swift @@ -160,6 +160,8 @@ public struct TestDeclarationMacro: PeerMacro, Sendable { for (label, parameter) in parametersWithLabels { if parameter.firstName.tokenKind == .wildcard { LabeledExprSyntax(expression: label) + } else if let rawIdentifier = parameter.firstName.rawIdentifier { + LabeledExprSyntax(label: "`\(rawIdentifier)`", expression: label) } else { LabeledExprSyntax(label: parameter.firstName.textWithoutBackticks, expression: label) } diff --git a/Tests/TestingTests/MiscellaneousTests.swift b/Tests/TestingTests/MiscellaneousTests.swift index b895f6c1b..6a65fb658 100644 --- a/Tests/TestingTests/MiscellaneousTests.swift +++ b/Tests/TestingTests/MiscellaneousTests.swift @@ -315,6 +315,11 @@ struct MiscellaneousTests { let displayName = try #require(suite.displayName) #expect(displayName == "Suite With De Facto Display Name") } + + @Test(arguments: [0]) + func `Test with raw identifier and raw identifier parameter labels can compile`(`argument name` i: Int) { + #expect(i == 0) + } #endif @Test("Free functions are runnable") From 3ee5ec0250b62e7f8b4905de0ff98b0b180698c0 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Wed, 25 Jun 2025 16:07:00 -0400 Subject: [PATCH 035/216] Lower the "ambiguous display name" diagnostic to a warning for some names. (#1175) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR modifies the behaviour of this compile-time macro diagnostic: > 🛑 "Attribute 'Test' specifies display name 'foo' for function with implicit display name 'bar' If `bar` (in the above context) is _not_ considered a raw identifier by the language, we emit a warning instead of an error. This will allow us to adjust display-name-from-backticked-name inference (see #1174) without introducing a source-breaking change for a declaration such as: ```swift @Test("subscript([K]) operator") func `subscript`() ``` (The above is a real-world test function in our own package that would be impacted.) Note that we don't actually have a code path that triggers this warning yet. #1174, if approved and merged, would introduce such a code path. Here's an example of what that would look like: Screenshot showing the warning diagnostic
presented for func subscript() ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- .../TestingMacros/Support/AttributeDiscovery.swift | 3 ++- .../TestingMacros/Support/DiagnosticMessage.swift | 12 +++++++++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/Sources/TestingMacros/Support/AttributeDiscovery.swift b/Sources/TestingMacros/Support/AttributeDiscovery.swift index a61989aef..3d95df294 100644 --- a/Sources/TestingMacros/Support/AttributeDiscovery.swift +++ b/Sources/TestingMacros/Support/AttributeDiscovery.swift @@ -144,8 +144,9 @@ struct AttributeInfo { let rawIdentifier = namedDecl.name.rawIdentifier { if let displayName, let displayNameArgument { context.diagnose(.declaration(namedDecl, hasExtraneousDisplayName: displayName, fromArgument: displayNameArgument, using: attribute)) + } else { + displayName = StringLiteralExprSyntax(content: rawIdentifier) } - displayName = StringLiteralExprSyntax(content: rawIdentifier) } // Remove leading "Self." expressions from the arguments of the attribute. diff --git a/Sources/TestingMacros/Support/DiagnosticMessage.swift b/Sources/TestingMacros/Support/DiagnosticMessage.swift index b7103bcc6..9f155b63a 100644 --- a/Sources/TestingMacros/Support/DiagnosticMessage.swift +++ b/Sources/TestingMacros/Support/DiagnosticMessage.swift @@ -657,10 +657,16 @@ struct DiagnosticMessage: SwiftDiagnostics.DiagnosticMessage { fromArgument argumentContainingDisplayName: LabeledExprListSyntax.Element, using attribute: AttributeSyntax ) -> Self { - Self( + // If the name of the ambiguously-named symbol should be derived from a raw + // identifier, this situation is an error. If the name is not raw but is + // still surrounded by backticks (e.g. "func `foo`()" or "struct `if`") then + // lower the severity to a warning. That way, existing code structured this + // way doesn't suddenly fail to build. + let severity: DiagnosticSeverity = (decl.name.rawIdentifier != nil) ? .error : .warning + return Self( syntax: Syntax(decl), - message: "Attribute \(_macroName(attribute)) specifies display name '\(displayNameFromAttribute.representedLiteralValue!)' for \(_kindString(for: decl)) with implicit display name '\(decl.name.rawIdentifier!)'", - severity: .error, + message: "Attribute \(_macroName(attribute)) specifies display name '\(displayNameFromAttribute.representedLiteralValue!)' for \(_kindString(for: decl)) with implicit display name '\(decl.name.textWithoutBackticks)'", + severity: severity, fixIts: [ FixIt( message: MacroExpansionFixItMessage("Remove '\(displayNameFromAttribute.representedLiteralValue!)'"), From d305f59feda08bc63652bfcac7a4a1d424012fc9 Mon Sep 17 00:00:00 2001 From: Stuart Montgomery Date: Wed, 25 Jun 2025 17:19:22 -0500 Subject: [PATCH 036/216] Revert portion of workaround landed in #1139 which disabled the mandatory perf optimization pass (#1172) This reverts most of the workaround I landed in #1139. A compiler fix in https://github.com/swiftlang/swift/pull/82034 has resolved the problem that necessitated it, and our macOS CI is using a new-enough toolchain which has that fix. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- Package.swift | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/Package.swift b/Package.swift index 4360aabdc..031968574 100644 --- a/Package.swift +++ b/Package.swift @@ -129,7 +129,7 @@ let package = Package( "_Testing_Foundation", "MemorySafeTestingTests", ], - swiftSettings: .packageSettings + .disableMandatoryOptimizationsSettings + swiftSettings: .packageSettings ), // Use a plain `.target` instead of a `.testTarget` to avoid the unnecessary @@ -234,7 +234,7 @@ package.targets.append(contentsOf: [ "Testing", "TestingMacros", ], - swiftSettings: .packageSettings + .disableMandatoryOptimizationsSettings + swiftSettings: .packageSettings ) ]) #endif @@ -397,20 +397,6 @@ extension Array where Element == PackageDescription.SwiftSetting { [] #endif } - - /// Settings which disable Swift's mandatory optimizations pass. - /// - /// This is intended only to work around a build failure caused by a Swift - /// compiler regression which is expected to be resolved in - /// [swiftlang/swift#82034](https://github.com/swiftlang/swift/pull/82034). - /// - /// @Comment { - /// - Bug: This should be removed once the CI issue is resolved. - /// [swiftlang/swift-testin#1138](https://github.com/swiftlang/swift-testing/issues/1138). - /// } - static var disableMandatoryOptimizationsSettings: Self { - [.unsafeFlags(["-Xllvm", "-sil-disable-pass=mandatory-performance-optimizations"])] - } } extension Array where Element == PackageDescription.CXXSetting { From e895fc8013bafc799d8a0f4a3f0143e4dde60b1e Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Thu, 26 Jun 2025 13:02:39 -0400 Subject: [PATCH 037/216] Add (hidden) synchronous overloads of `#expect(throws:)`. (#1178) This PR adds overloads of the `#expect(throws:)` and `#require(throws:)` macros that take synchronous closures. This is necessary due to a change in the compiler in Swift 6.2 that improves type checking on closures passed to macros. The change is a good thing, but it means that the compiler infers the type of closures passed to these macros as `async` even when they are synchronous and developers will now get warnings under some circumstances. This PR does not constitute an API change. The new overloads are identical to their `async` peers and there is no change in the underlying macro expansion logic at compile time or runtime. The PR serves solely to suppress new spurious warnings that were not emitted in Swift 6.1 or earlier. Resolves #1177. Resolves rdar://149299786. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- .../Expectations/Expectation+Macro.swift | 373 ++++++++++++++++++ 1 file changed, 373 insertions(+) diff --git a/Sources/Testing/Expectations/Expectation+Macro.swift b/Sources/Testing/Expectations/Expectation+Macro.swift index 8a6749a31..2f64aff3a 100644 --- a/Sources/Testing/Expectations/Expectation+Macro.swift +++ b/Sources/Testing/Expectations/Expectation+Macro.swift @@ -131,6 +131,74 @@ public macro require( // MARK: - Matching errors by type +#if compiler(>=6.2) +/// Check that an expression always throws an error of a given type. +/// +/// - Parameters: +/// - errorType: The type of error that is expected to be thrown. If +/// `expression` could throw _any_ error, or the specific type of thrown +/// error is unimportant, pass `(any Error).self`. +/// - comment: A comment describing the expectation. +/// - sourceLocation: The source location to which recorded expectations and +/// issues should be attributed. +/// - expression: The expression to be evaluated. +/// +/// - Returns: If the expectation passes, the instance of `errorType` that was +/// thrown by `expression`. If the expectation fails, the result is `nil`. +/// +/// Use this overload of `#expect()` when the expression `expression` _should_ +/// throw an error of a given type: +/// +/// ```swift +/// #expect(throws: EngineFailureError.self) { +/// FoodTruck.shared.engine.batteryLevel = 0 +/// try FoodTruck.shared.engine.start() +/// } +/// ``` +/// +/// If `expression` does not throw an error, or if it throws an error that is +/// not an instance of `errorType`, an ``Issue`` is recorded for the test that +/// is running in the current task. Any value returned by `expression` is +/// discarded. +/// +/// - Note: If you use this macro with a Swift compiler version lower than 6.1, +/// it doesn't return a value. +/// +/// If the thrown error need only equal another instance of [`Error`](https://developer.apple.com/documentation/swift/error), +/// use ``expect(throws:_:sourceLocation:performing:)-7du1h`` instead. +/// +/// ## Expressions that should never throw +/// +/// If the expression `expression` should _never_ throw any error, you can pass +/// [`Never.self`](https://developer.apple.com/documentation/swift/never): +/// +/// ```swift +/// #expect(throws: Never.self) { +/// FoodTruck.shared.engine.batteryLevel = 100 +/// try FoodTruck.shared.engine.start() +/// } +/// ``` +/// +/// If `expression` throws an error, an ``Issue`` is recorded for the test that +/// is running in the current task. Any value returned by `expression` is +/// discarded. +/// +/// Test functions can be annotated with `throws` and can throw errors which are +/// then recorded as issues when the test runs. If the intent is for a test to +/// fail when an error is thrown by `expression`, rather than to explicitly +/// check that an error is _not_ thrown by it, do not use this macro. Instead, +/// simply call the code in question and allow it to throw an error naturally. +@discardableResult +@freestanding(expression) +@_documentation(visibility: private) +public macro expect( + throws errorType: E.Type, + _ comment: @autoclosure () -> Comment? = nil, + sourceLocation: SourceLocation = #_sourceLocation, + performing expression: () throws -> R +) -> E? = #externalMacro(module: "TestingMacros", type: "ExpectMacro") where E: Error +#endif + /// Check that an expression always throws an error of a given type. /// /// - Parameters: @@ -195,6 +263,58 @@ public macro require( performing expression: () async throws -> R ) -> E? = #externalMacro(module: "TestingMacros", type: "ExpectMacro") where E: Error +#if compiler(>=6.2) +/// Check that an expression always throws an error of a given type, and throw +/// an error if it does not. +/// +/// - Parameters: +/// - errorType: The type of error that is expected to be thrown. If +/// `expression` could throw _any_ error, or the specific type of thrown +/// error is unimportant, pass `(any Error).self`. +/// - comment: A comment describing the expectation. +/// - sourceLocation: The source location to which recorded expectations and +/// issues should be attributed. +/// - expression: The expression to be evaluated. +/// +/// - Returns: The instance of `errorType` that was thrown by `expression`. +/// +/// - Throws: An instance of ``ExpectationFailedError`` if `expression` does not +/// throw a matching error. The error thrown by `expression` is not rethrown. +/// +/// Use this overload of `#require()` when the expression `expression` _should_ +/// throw an error of a given type: +/// +/// ```swift +/// try #require(throws: EngineFailureError.self) { +/// FoodTruck.shared.engine.batteryLevel = 0 +/// try FoodTruck.shared.engine.start() +/// } +/// ``` +/// +/// If `expression` does not throw an error, or if it throws an error that is +/// not an instance of `errorType`, an ``Issue`` is recorded for the test that +/// is running in the current task and an instance of ``ExpectationFailedError`` +/// is thrown. Any value returned by `expression` is discarded. +/// +/// - Note: If you use this macro with a Swift compiler version lower than 6.1, +/// it doesn't return a value. +/// +/// If the thrown error need only equal another instance of [`Error`](https://developer.apple.com/documentation/swift/error), +/// use ``require(throws:_:sourceLocation:performing:)-4djuw`` instead. +/// +/// If `expression` should _never_ throw, simply invoke the code without using +/// this macro. The test will then fail if an error is thrown. +@discardableResult +@freestanding(expression) +@_documentation(visibility: private) +public macro require( + throws errorType: E.Type, + _ comment: @autoclosure () -> Comment? = nil, + sourceLocation: SourceLocation = #_sourceLocation, + performing expression: () throws -> R +) -> E = #externalMacro(module: "TestingMacros", type: "RequireThrowsMacro") where E: Error +#endif + /// Check that an expression always throws an error of a given type, and throw /// an error if it does not. /// @@ -243,6 +363,28 @@ public macro require( performing expression: () async throws -> R ) -> E = #externalMacro(module: "TestingMacros", type: "RequireThrowsMacro") where E: Error +#if compiler(>=6.2) +/// Check that an expression never throws an error, and throw an error if it +/// does. +/// +/// - Parameters: +/// - comment: A comment describing the expectation. +/// - sourceLocation: The source location to which recorded expectations and +/// issues should be attributed. +/// - expression: The expression to be evaluated. +/// +/// - Throws: An instance of ``ExpectationFailedError`` if `expression` throws +/// any error. The error thrown by `expression` is not rethrown. +@freestanding(expression) +@_documentation(visibility: private) +public macro require( + throws _: Never.Type, + _ comment: @autoclosure () -> Comment? = nil, + sourceLocation: SourceLocation = #_sourceLocation, + performing expression: () throws -> R +) = #externalMacro(module: "TestingMacros", type: "RequireThrowsNeverMacro") +#endif + /// Check that an expression never throws an error, and throw an error if it /// does. /// @@ -265,6 +407,50 @@ public macro require( // MARK: - Matching instances of equatable errors +#if compiler(>=6.2) +/// Check that an expression always throws a specific error. +/// +/// - Parameters: +/// - error: The error that is expected to be thrown. +/// - comment: A comment describing the expectation. +/// - sourceLocation: The source location to which recorded expectations and +/// issues should be attributed. +/// - expression: The expression to be evaluated. +/// +/// - Returns: If the expectation passes, the instance of `E` that was thrown by +/// `expression` and is equal to `error`. If the expectation fails, the result +/// is `nil`. +/// +/// Use this overload of `#expect()` when the expression `expression` _should_ +/// throw a specific error: +/// +/// ```swift +/// #expect(throws: EngineFailureError.batteryDied) { +/// FoodTruck.shared.engine.batteryLevel = 0 +/// try FoodTruck.shared.engine.start() +/// } +/// ``` +/// +/// If `expression` does not throw an error, or if it throws an error that is +/// not equal to `error`, an ``Issue`` is recorded for the test that is running +/// in the current task. Any value returned by `expression` is discarded. +/// +/// - Note: If you use this macro with a Swift compiler version lower than 6.1, +/// it doesn't return a value. +/// +/// If the thrown error need only be an instance of a particular type, use +/// ``expect(throws:_:sourceLocation:performing:)-1hfms`` instead. +@discardableResult +@freestanding(expression) +@_documentation(visibility: private) +public macro expect( + throws error: E, + _ comment: @autoclosure () -> Comment? = nil, + sourceLocation: SourceLocation = #_sourceLocation, + performing expression: () throws -> R +) -> E? = #externalMacro(module: "TestingMacros", type: "ExpectMacro") where E: Error & Equatable +#endif + /// Check that an expression always throws a specific error. /// /// - Parameters: @@ -305,6 +491,54 @@ public macro require( performing expression: () async throws -> R ) -> E? = #externalMacro(module: "TestingMacros", type: "ExpectMacro") where E: Error & Equatable +#if compiler(>=6.2) +/// Check that an expression always throws a specific error, and throw an error +/// if it does not. +/// +/// - Parameters: +/// - error: The error that is expected to be thrown. +/// - comment: A comment describing the expectation. +/// - sourceLocation: The source location to which recorded expectations and +/// issues should be attributed. +/// - expression: The expression to be evaluated. + +/// - Returns: The instance of `E` that was thrown by `expression` and is equal +/// to `error`. +/// +/// - Throws: An instance of ``ExpectationFailedError`` if `expression` does not +/// throw a matching error. The error thrown by `expression` is not rethrown. +/// +/// Use this overload of `#require()` when the expression `expression` _should_ +/// throw a specific error: +/// +/// ```swift +/// try #require(throws: EngineFailureError.batteryDied) { +/// FoodTruck.shared.engine.batteryLevel = 0 +/// try FoodTruck.shared.engine.start() +/// } +/// ``` +/// +/// If `expression` does not throw an error, or if it throws an error that is +/// not equal to `error`, an ``Issue`` is recorded for the test that is running +/// in the current task and an instance of ``ExpectationFailedError`` is thrown. +/// Any value returned by `expression` is discarded. +/// +/// - Note: If you use this macro with a Swift compiler version lower than 6.1, +/// it doesn't return a value. +/// +/// If the thrown error need only be an instance of a particular type, use +/// ``require(throws:_:sourceLocation:performing:)-7n34r`` instead. +@discardableResult +@freestanding(expression) +@_documentation(visibility: private) +public macro require( + throws error: E, + _ comment: @autoclosure () -> Comment? = nil, + sourceLocation: SourceLocation = #_sourceLocation, + performing expression: () throws -> R +) -> E = #externalMacro(module: "TestingMacros", type: "RequireMacro") where E: Error & Equatable +#endif + /// Check that an expression always throws a specific error, and throw an error /// if it does not. /// @@ -351,6 +585,72 @@ public macro require( // MARK: - Arbitrary error matching +#if compiler(>=6.2) +/// Check that an expression always throws an error matching some condition. +/// +/// - Parameters: +/// - comment: A comment describing the expectation. +/// - sourceLocation: The source location to which recorded expectations and +/// issues should be attributed. +/// - expression: The expression to be evaluated. +/// - errorMatcher: A closure to invoke when `expression` throws an error that +/// indicates if it matched or not. +/// +/// - Returns: If the expectation passes, the error that was thrown by +/// `expression`. If the expectation fails, the result is `nil`. +/// +/// Use this overload of `#expect()` when the expression `expression` _should_ +/// throw an error, but the logic to determine if the error matches is complex: +/// +/// ```swift +/// #expect { +/// FoodTruck.shared.engine.batteryLevel = 0 +/// try FoodTruck.shared.engine.start() +/// } throws: { error in +/// return error == EngineFailureError.batteryDied +/// || error == EngineFailureError.stillCharging +/// } +/// ``` +/// +/// If `expression` does not throw an error, if it throws an error that is +/// not matched by `errorMatcher`, or if `errorMatcher` throws an error +/// (including the error passed to it), an ``Issue`` is recorded for the test +/// that is running in the current task. Any value returned by `expression` is +/// discarded. +/// +/// If the thrown error need only be an instance of a particular type, use +/// ``expect(throws:_:sourceLocation:performing:)-1hfms`` instead. If the thrown +/// error need only equal another instance of [`Error`](https://developer.apple.com/documentation/swift/error), +/// use ``expect(throws:_:sourceLocation:performing:)-7du1h`` instead. +/// +/// @Metadata { +/// @Available(Swift, introduced: 6.0) +/// @Available(Xcode, introduced: 16.0) +/// } +/// +/// @DeprecationSummary { +/// Examine the result of ``expect(throws:_:sourceLocation:performing:)-7du1h`` +/// or ``expect(throws:_:sourceLocation:performing:)-1hfms`` instead: +/// +/// ```swift +/// let error = #expect(throws: FoodTruckError.self) { +/// ... +/// } +/// #expect(error?.napkinCount == 0) +/// ``` +/// } +@available(swift, deprecated: 100000.0, message: "Examine the result of '#expect(throws:)' instead.") +@discardableResult +@freestanding(expression) +@_documentation(visibility: private) +public macro expect( + _ comment: @autoclosure () -> Comment? = nil, + sourceLocation: SourceLocation = #_sourceLocation, + performing expression: () throws -> R, + throws errorMatcher: (any Error) throws -> Bool +) -> (any Error)? = #externalMacro(module: "TestingMacros", type: "ExpectMacro") +#endif + /// Check that an expression always throws an error matching some condition. /// /// - Parameters: @@ -413,6 +713,79 @@ public macro require( throws errorMatcher: (any Error) async throws -> Bool ) -> (any Error)? = #externalMacro(module: "TestingMacros", type: "ExpectMacro") +#if compiler(>=6.2) +/// Check that an expression always throws an error matching some condition, and +/// throw an error if it does not. +/// +/// - Parameters: +/// - comment: A comment describing the expectation. +/// - sourceLocation: The source location to which recorded expectations and +/// issues should be attributed. +/// - expression: The expression to be evaluated. +/// - errorMatcher: A closure to invoke when `expression` throws an error that +/// indicates if it matched or not. +/// +/// - Returns: The error that was thrown by `expression`. +/// +/// - Throws: An instance of ``ExpectationFailedError`` if `expression` does not +/// throw a matching error. The error thrown by `expression` is not rethrown. +/// +/// Use this overload of `#require()` when the expression `expression` _should_ +/// throw an error, but the logic to determine if the error matches is complex: +/// +/// ```swift +/// #expect { +/// FoodTruck.shared.engine.batteryLevel = 0 +/// try FoodTruck.shared.engine.start() +/// } throws: { error in +/// return error == EngineFailureError.batteryDied +/// || error == EngineFailureError.stillCharging +/// } +/// ``` +/// +/// If `expression` does not throw an error, if it throws an error that is +/// not matched by `errorMatcher`, or if `errorMatcher` throws an error +/// (including the error passed to it), an ``Issue`` is recorded for the test +/// that is running in the current task and an instance of +/// ``ExpectationFailedError`` is thrown. Any value returned by `expression` is +/// discarded. +/// +/// If the thrown error need only be an instance of a particular type, use +/// ``require(throws:_:sourceLocation:performing:)-7n34r`` instead. If the thrown error need +/// only equal another instance of [`Error`](https://developer.apple.com/documentation/swift/error), +/// use ``require(throws:_:sourceLocation:performing:)-4djuw`` instead. +/// +/// If `expression` should _never_ throw, simply invoke the code without using +/// this macro. The test will then fail if an error is thrown. +/// +/// @Metadata { +/// @Available(Swift, introduced: 6.0) +/// @Available(Xcode, introduced: 16.0) +/// } +/// +/// @DeprecationSummary { +/// Examine the result of ``expect(throws:_:sourceLocation:performing:)-7du1h`` +/// or ``expect(throws:_:sourceLocation:performing:)-1hfms`` instead: +/// +/// ```swift +/// let error = try #require(throws: FoodTruckError.self) { +/// ... +/// } +/// #expect(error.napkinCount == 0) +/// ``` +/// } +@available(swift, deprecated: 100000.0, message: "Examine the result of '#require(throws:)' instead.") +@discardableResult +@freestanding(expression) +@_documentation(visibility: private) +public macro require( + _ comment: @autoclosure () -> Comment? = nil, + sourceLocation: SourceLocation = #_sourceLocation, + performing expression: () throws -> R, + throws errorMatcher: (any Error) throws -> Bool +) -> any Error = #externalMacro(module: "TestingMacros", type: "RequireMacro") +#endif + /// Check that an expression always throws an error matching some condition, and /// throw an error if it does not. /// From c17188868b0cd9cb83b53722c1928d82facdf9de Mon Sep 17 00:00:00 2001 From: Stuart Montgomery Date: Thu, 26 Jun 2025 14:03:03 -0500 Subject: [PATCH 038/216] Remove redundant word from console output for test case started events in verbose mode (#1180) This is a small fix for an oversight I made in #1125: the word `started` is printed twice at the end of the console message for `.testCaseStarted` events in verbose mode. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- .../Events/Recorder/Event.HumanReadableOutputRecorder.swift | 4 +--- Tests/TestingTests/EventRecorderTests.swift | 4 ++++ 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/Sources/Testing/Events/Recorder/Event.HumanReadableOutputRecorder.swift b/Sources/Testing/Events/Recorder/Event.HumanReadableOutputRecorder.swift index a3d121e09..248bb4aec 100644 --- a/Sources/Testing/Events/Recorder/Event.HumanReadableOutputRecorder.swift +++ b/Sources/Testing/Events/Recorder/Event.HumanReadableOutputRecorder.swift @@ -521,12 +521,10 @@ extension Event.HumanReadableOutputRecorder { break } - let status = verbosity > 0 ? " started" : "" - return [ Message( symbol: .default, - stringValue: "Test case passing \(arguments.count.counting("argument")) \(testCase.labeledArguments(includingQualifiedTypeNames: verbosity > 0)) to \(testName)\(status) started." + stringValue: "Test case passing \(arguments.count.counting("argument")) \(testCase.labeledArguments(includingQualifiedTypeNames: verbosity > 0)) to \(testName) started." ) ] diff --git a/Tests/TestingTests/EventRecorderTests.swift b/Tests/TestingTests/EventRecorderTests.swift index ed7d765a0..e3dc9ca99 100644 --- a/Tests/TestingTests/EventRecorderTests.swift +++ b/Tests/TestingTests/EventRecorderTests.swift @@ -113,6 +113,10 @@ struct EventRecorderTests { #expect(buffer.contains(#"\#(Event.Symbol.details.unicodeCharacter) lhs: Swift.String → "987""#)) #expect(buffer.contains(#""Animal Crackers" (aka 'WrittenTests')"#)) #expect(buffer.contains(#""Not A Lobster" (aka 'actuallyCrab()')"#)) + do { + let regex = try Regex(".* Test case passing 1 argument i → 0 \\(Swift.Int\\) to multitudeOcelot\\(i:\\) started.") + #expect(try buffer.split(whereSeparator: \.isNewline).compactMap(regex.wholeMatch(in:)).first != nil) + } do { let regex = try Regex(".* Test case passing 1 argument i → 0 \\(Swift.Int\\) to multitudeOcelot\\(i:\\) passed after .*.") #expect(try buffer.split(whereSeparator: \.isNewline).compactMap(regex.wholeMatch(in:)).first != nil) From 072692ca288edf3d540c645a6801bbccd9bf42ee Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Fri, 27 Jun 2025 18:55:11 -0400 Subject: [PATCH 039/216] Provide additional explanation for the Windows signal handling stuff. (#1182) --- Sources/Testing/ExitTests/ExitTest.swift | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/Sources/Testing/ExitTests/ExitTest.swift b/Sources/Testing/ExitTests/ExitTest.swift index c5579981e..aa7bf39ab 100644 --- a/Sources/Testing/ExitTests/ExitTest.swift +++ b/Sources/Testing/ExitTests/ExitTest.swift @@ -245,15 +245,30 @@ extension ExitTest { #if os(Windows) // Windows does not support signal handling to the degree UNIX-like systems // do. When a signal is raised in a Windows process, the default signal - // handler simply calls `exit()` and passes the constant value `3`. To allow - // us to handle signals on Windows, we install signal handlers for all + // handler simply calls `_exit()` and passes the constant value `3`. To + // allow us to handle signals on Windows, we install signal handlers for all // signals supported on Windows. These signal handlers exit with a specific // exit code that is unlikely to be encountered "in the wild" and which // encodes the caught signal. Corresponding code in the parent process looks // for these special exit codes and translates them back to signals. + // + // Microsoft's documentation for `_Exit()` and `_exit()` indicates they + // behave identically. Their documentation for abort() can be found at + // https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/abort?view=msvc-170 + // and states: "[...] abort calls _exit to terminate the process with exit + // code 3 [...]". + // + // The Wine project's implementation of raise() calls `_exit(3)` by default. + // See https://github.com/wine-mirror/wine/blob/master/dlls/msvcrt/except.c + // + // Finally, an official copy of the UCRT sources (not up to date) is hosted + // at https://www.nuget.org/packages/Microsoft.Windows.SDK.CRTSource . That + // repository doesn't have an official GitHub mirror, but you can manually + // navigate to misc/signal.cpp:481 to see the implementation of SIG_DFL + // (which, again, calls `_exit(3)` unconditionally.) for sig in [SIGINT, SIGILL, SIGFPE, SIGSEGV, SIGTERM, SIGBREAK, SIGABRT, SIGABRT_COMPAT] { _ = signal(sig) { sig in - _Exit(STATUS_SIGNAL_CAUGHT_BITS | sig) + _exit(STATUS_SIGNAL_CAUGHT_BITS | sig) } } #endif From 344089834e7f73a37d99b80f35ad33d400fc5fd7 Mon Sep 17 00:00:00 2001 From: Stuart Montgomery Date: Mon, 30 Jun 2025 16:16:43 -0500 Subject: [PATCH 040/216] Ensure that when .serialized is applied to a parameterized @Test func, its test cases are serialized (#1188) This fixes a regression introduced in the changes for #901. When `.serialized` is applied directly to a parameterized `@Test` function, not to a containing suite, its test cases are no longer serialized. This PR resolves and restores the original behavior by ensuring there is a non-`nil` scope provider returned for the test function. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. Fixes rdar://154529146 --- .../Testing/Traits/ParallelizationTrait.swift | 8 +++++++ .../TestSupport/TestingAdditions.swift | 21 +++++++++++++++++-- .../Traits/ParallelizationTraitTests.swift | 13 +++++++++--- 3 files changed, 37 insertions(+), 5 deletions(-) diff --git a/Sources/Testing/Traits/ParallelizationTrait.swift b/Sources/Testing/Traits/ParallelizationTrait.swift index 9eb1bd2a5..c91e01761 100644 --- a/Sources/Testing/Traits/ParallelizationTrait.swift +++ b/Sources/Testing/Traits/ParallelizationTrait.swift @@ -31,6 +31,14 @@ public struct ParallelizationTrait: TestTrait, SuiteTrait {} // MARK: - TestScoping extension ParallelizationTrait: TestScoping { + public func scopeProvider(for test: Test, testCase: Test.Case?) -> Self? { + // When applied to a test function, this trait should provide scope to the + // test function itself, not its individual test cases, since that allows + // Runner to correctly interpret the configuration setting to disable + // parallelization. + test.isSuite || testCase == nil ? self : nil + } + public func provideScope(for test: Test, testCase: Test.Case?, performing function: @Sendable () async throws -> Void) async throws { guard var configuration = Configuration.current else { throw SystemError(description: "There is no current Configuration when attempting to provide scope for test '\(test.name)'. Please file a bug report at https://github.com/swiftlang/swift-testing/issues/new") diff --git a/Tests/TestingTests/TestSupport/TestingAdditions.swift b/Tests/TestingTests/TestSupport/TestingAdditions.swift index 4648f96af..5c596785e 100644 --- a/Tests/TestingTests/TestSupport/TestingAdditions.swift +++ b/Tests/TestingTests/TestSupport/TestingAdditions.swift @@ -103,6 +103,25 @@ extension Runner { /// - fileID: The `#fileID` string whose module should be used to locate /// the test function to run. /// - configuration: The configuration to use for running. + init( + selecting testName: String, + inModuleOf fileID: String = #fileID, + configuration: Configuration = .init() + ) async { + let plan = await Runner.Plan(selecting: testName, inModuleOf: fileID, configuration: configuration) + self.init(plan: plan, configuration: configuration) + } +} + +extension Runner.Plan { + /// Initialize an instance of this type that selects the free test function + /// named `testName` in the module specified in `fileID`. + /// + /// - Parameters: + /// - testName: The name of the test function this instance should run. + /// - fileID: The `#fileID` string whose module should be used to locate + /// the test function to run. + /// - configuration: The configuration to use for running. init( selecting testName: String, inModuleOf fileID: String = #fileID, @@ -116,9 +135,7 @@ extension Runner { await self.init(configuration: configuration) } -} -extension Runner.Plan { /// Initialize an instance of this type with the specified suite type. /// /// - Parameters: diff --git a/Tests/TestingTests/Traits/ParallelizationTraitTests.swift b/Tests/TestingTests/Traits/ParallelizationTraitTests.swift index 776e5c320..6c4963dc5 100644 --- a/Tests/TestingTests/Traits/ParallelizationTraitTests.swift +++ b/Tests/TestingTests/Traits/ParallelizationTraitTests.swift @@ -12,8 +12,11 @@ @Suite("Parallelization Trait Tests", .tags(.traitRelated)) struct ParallelizationTraitTests { - @Test(".serialized trait serializes parameterized test") - func serializesParameterizedTestFunction() async { + @Test(".serialized trait serializes parameterized test", arguments: await [ + Runner.Plan(selecting: OuterSuite.self), + Runner.Plan(selecting: "globalParameterized(i:)"), + ]) + func serializesParameterizedTestFunction(plan: Runner.Plan) async { var configuration = Configuration() configuration.isParallelizationEnabled = true @@ -33,7 +36,6 @@ struct ParallelizationTraitTests { } } - let plan = await Runner.Plan(selecting: OuterSuite.self, configuration: configuration) let runner = Runner(plan: plan, configuration: configuration) await runner.run() @@ -59,3 +61,8 @@ private struct OuterSuite { } } } + +@Test(.hidden, .serialized, arguments: 0 ..< 10_000) +private func globalParameterized(i: Int) { + Issue.record("PARAMETERIZED\(i)") +} From db76ea7d388ac4f3af4b74754f20da1b664e7017 Mon Sep 17 00:00:00 2001 From: Luke Howard Date: Tue, 1 Jul 2025 10:32:38 +1000 Subject: [PATCH 041/216] order limits.h before stdlib.h to workaround for glibc _FORTIFY_SOURCE (#1184) `limits.h` must be included before `stdlib.h` when building with glibc and having `_FORTIFY_SOURCE` set to a non-zero value. When building with `_FORTIFY_SOURCE`, `realpath()` is inlined, and its definition depends on whether `limits.h` has been included or not (clearly, this is a terrible idea in terms of interacting with Clang modules and should probably be fixed upstream). If the definition differs from the one in SwiftGlibc, then _TestingInternals will not build. --- Sources/_TestingInternals/include/Includes.h | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/Sources/_TestingInternals/include/Includes.h b/Sources/_TestingInternals/include/Includes.h index 1b95151cb..869fcff2a 100644 --- a/Sources/_TestingInternals/include/Includes.h +++ b/Sources/_TestingInternals/include/Includes.h @@ -29,6 +29,12 @@ #include #include #include +/// limits.h must be included before stdlib.h with glibc, otherwise the +/// fortified realpath() in this module will differ from the one in SwiftGlibc. +/// glibc bug: https://sourceware.org/bugzilla/show_bug.cgi?id=30516 +#if __has_include() +#include +#endif /// Guard against including `signal.h` on WASI. The `signal.h` header file /// itself is available in wasi-libc, but it's just a stub that doesn't actually /// do anything. And also including it requires a special macro definition @@ -97,10 +103,6 @@ #include #endif -#if __has_include() -#include -#endif - #if __has_include() #include #endif From 79c22ad7b9c372499c305ca0f8fef78af2a907c5 Mon Sep 17 00:00:00 2001 From: Evan Wilde Date: Tue, 1 Jul 2025 19:06:01 -0700 Subject: [PATCH 042/216] FreeBSD: Gate GNU-only API (#1183) The FreeBSD builds are currently using the GlibC modulemap to import the C runtimes. FreeBSD does not have `gnu_get_libc_version` resulting in build failures. The use of this API was introduced in https://github.com/swiftlang/swift-testing/pull/1147 --- Sources/Testing/ExitTests/SpawnProcess.swift | 2 +- Sources/Testing/Support/Versions.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/Testing/ExitTests/SpawnProcess.swift b/Sources/Testing/ExitTests/SpawnProcess.swift index 66143a7e0..fe51a7086 100644 --- a/Sources/Testing/ExitTests/SpawnProcess.swift +++ b/Sources/Testing/ExitTests/SpawnProcess.swift @@ -137,7 +137,7 @@ func spawnExecutable( // standardized in POSIX.1-2024 (see https://pubs.opengroup.org/onlinepubs/9799919799/functions/posix_spawn_file_actions_adddup2.html // and https://www.austingroupbugs.net/view.php?id=411). _ = posix_spawn_file_actions_adddup2(fileActions, fd, fd) -#if canImport(Glibc) +#if canImport(Glibc) && !os(FreeBSD) && !os(OpenBSD) if _slowPath(glibcVersion.major < 2 || (glibcVersion.major == 2 && glibcVersion.minor < 29)) { // This system is using an older version of glibc that does not // implement FD_CLOEXEC clearing in posix_spawn_file_actions_adddup2(), diff --git a/Sources/Testing/Support/Versions.swift b/Sources/Testing/Support/Versions.swift index 1229e80b0..7f190ebb2 100644 --- a/Sources/Testing/Support/Versions.swift +++ b/Sources/Testing/Support/Versions.swift @@ -153,7 +153,7 @@ let swiftStandardLibraryVersion: String = { return "unknown" }() -#if canImport(Glibc) +#if canImport(Glibc) && !os(FreeBSD) && !os(OpenBSD) /// The (runtime, not compile-time) version of glibc in use on this system. /// /// This value is not part of the public interface of the testing library. From 84ed9521bf3af726c6537c979034787c9fc800b9 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Wed, 2 Jul 2025 13:26:05 -0400 Subject: [PATCH 043/216] Require `UTType` for image attachments. (#1192) This PR simplifies our image attachment code so that it always requires `UTType` instead of providing implementations for older Apple platforms. For more information about the `UTType` API, watch [this](https://developer.apple.com/videos/play/tech-talks/10696/) video. There is a kitty. Image attachments depend on `CGImage` and are Apple-specific at this time. Non-Apple image attachment support is a future direction. > [!NOTE] > Image attachments are an experimental feature. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- .../Attachment+AttachableAsCGImage.swift | 109 ++++++++---------- .../Attachments/_AttachableImageWrapper.swift | 45 ++------ Tests/TestingTests/AttachmentTests.swift | 17 +++ 3 files changed, 78 insertions(+), 93 deletions(-) diff --git a/Sources/Overlays/_Testing_CoreGraphics/Attachments/Attachment+AttachableAsCGImage.swift b/Sources/Overlays/_Testing_CoreGraphics/Attachments/Attachment+AttachableAsCGImage.swift index ed1e6a2ee..6bb6f3744 100644 --- a/Sources/Overlays/_Testing_CoreGraphics/Attachments/Attachment+AttachableAsCGImage.swift +++ b/Sources/Overlays/_Testing_CoreGraphics/Attachments/Attachment+AttachableAsCGImage.swift @@ -13,6 +13,8 @@ public import UniformTypeIdentifiers +@_spi(Experimental) +@available(_uttypesAPI, *) extension Attachment { /// Initialize an instance of this type that encloses the given image. /// @@ -23,46 +25,9 @@ extension Attachment { /// to a test report or to disk. If `nil`, the testing library attempts /// to derive a reasonable filename for the attached value. /// - contentType: The image format with which to encode `attachableValue`. - /// If this type does not conform to [`UTType.image`](https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/image), - /// the result is undefined. Pass `nil` to let the testing library decide - /// which image format to use. /// - encodingQuality: The encoding quality to use when encoding the image. - /// If the image format used for encoding (specified by the `contentType` - /// argument) does not support variable-quality encoding, the value of - /// this argument is ignored. - /// - sourceLocation: The source location of the call to this initializer. - /// This value is used when recording issues associated with the - /// attachment. - /// - /// This is the designated initializer for this type when attaching an image - /// that conforms to ``AttachableAsCGImage``. - fileprivate init( - attachableValue: T, - named preferredName: String?, - contentType: (any Sendable)?, - encodingQuality: Float, - sourceLocation: SourceLocation - ) where AttachableValue == _AttachableImageWrapper { - let imageWrapper = _AttachableImageWrapper(image: attachableValue, encodingQuality: encodingQuality, contentType: contentType) - self.init(imageWrapper, named: preferredName, sourceLocation: sourceLocation) - } - - /// Initialize an instance of this type that encloses the given image. - /// - /// - Parameters: - /// - attachableValue: The value that will be attached to the output of - /// the test run. - /// - preferredName: The preferred name of the attachment when writing it - /// to a test report or to disk. If `nil`, the testing library attempts - /// to derive a reasonable filename for the attached value. - /// - contentType: The image format with which to encode `attachableValue`. - /// If this type does not conform to [`UTType.image`](https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/image), - /// the result is undefined. Pass `nil` to let the testing library decide - /// which image format to use. - /// - encodingQuality: The encoding quality to use when encoding the image. - /// If the image format used for encoding (specified by the `contentType` - /// argument) does not support variable-quality encoding, the value of - /// this argument is ignored. + /// For the lowest supported quality, pass `0.0`. For the highest + /// supported quality, pass `1.0`. /// - sourceLocation: The source location of the call to this initializer. /// This value is used when recording issues associated with the /// attachment. @@ -71,46 +36,72 @@ extension Attachment { /// ``AttachableAsCGImage`` protocol and can be attached to a test: /// /// - [`CGImage`](https://developer.apple.com/documentation/coregraphics/cgimage) - @_spi(Experimental) - @available(_uttypesAPI, *) + /// + /// The testing library uses the image format specified by `contentType`. Pass + /// `nil` to let the testing library decide which image format to use. If you + /// pass `nil`, then the image format that the testing library uses depends on + /// the path extension you specify in `preferredName`, if any. If you do not + /// specify a path extension, or if the path extension you specify doesn't + /// correspond to an image format the operating system knows how to write, the + /// testing library selects an appropriate image format for you. + /// + /// If the target image format does not support variable-quality encoding, + /// the value of the `encodingQuality` argument is ignored. If `contentType` + /// is not `nil` and does not conform to [`UTType.image`](https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/image), + /// the result is undefined. public init( _ attachableValue: T, named preferredName: String? = nil, - as contentType: UTType?, + as contentType: UTType? = nil, encodingQuality: Float = 1.0, sourceLocation: SourceLocation = #_sourceLocation ) where AttachableValue == _AttachableImageWrapper { - self.init(attachableValue: attachableValue, named: preferredName, contentType: contentType, encodingQuality: encodingQuality, sourceLocation: sourceLocation) + let imageWrapper = _AttachableImageWrapper(image: attachableValue, encodingQuality: encodingQuality, contentType: contentType) + self.init(imageWrapper, named: preferredName, sourceLocation: sourceLocation) } - /// Initialize an instance of this type that encloses the given image. + /// Attach an image to the current test. /// /// - Parameters: - /// - attachableValue: The value that will be attached to the output of - /// the test run. - /// - preferredName: The preferred name of the attachment when writing it - /// to a test report or to disk. If `nil`, the testing library attempts - /// to derive a reasonable filename for the attached value. + /// - image: The value to attach. + /// - preferredName: The preferred name of the attachment when writing it to + /// a test report or to disk. If `nil`, the testing library attempts to + /// derive a reasonable filename for the attached value. + /// - contentType: The image format with which to encode `attachableValue`. /// - encodingQuality: The encoding quality to use when encoding the image. - /// If the image format used for encoding (specified by the `contentType` - /// argument) does not support variable-quality encoding, the value of - /// this argument is ignored. - /// - sourceLocation: The source location of the call to this initializer. - /// This value is used when recording issues associated with the - /// attachment. + /// For the lowest supported quality, pass `0.0`. For the highest + /// supported quality, pass `1.0`. + /// - sourceLocation: The source location of the call to this function. + /// + /// This function creates a new instance of ``Attachment`` wrapping `image` + /// and immediately attaches it to the current test. /// /// The following system-provided image types conform to the /// ``AttachableAsCGImage`` protocol and can be attached to a test: /// /// - [`CGImage`](https://developer.apple.com/documentation/coregraphics/cgimage) - @_spi(Experimental) - public init( - _ attachableValue: T, + /// + /// The testing library uses the image format specified by `contentType`. Pass + /// `nil` to let the testing library decide which image format to use. If you + /// pass `nil`, then the image format that the testing library uses depends on + /// the path extension you specify in `preferredName`, if any. If you do not + /// specify a path extension, or if the path extension you specify doesn't + /// correspond to an image format the operating system knows how to write, the + /// testing library selects an appropriate image format for you. + /// + /// If the target image format does not support variable-quality encoding, + /// the value of the `encodingQuality` argument is ignored. If `contentType` + /// is not `nil` and does not conform to [`UTType.image`](https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/image), + /// the result is undefined. + public static func record( + _ image: consuming T, named preferredName: String? = nil, + as contentType: UTType? = nil, encodingQuality: Float = 1.0, sourceLocation: SourceLocation = #_sourceLocation ) where AttachableValue == _AttachableImageWrapper { - self.init(attachableValue: attachableValue, named: preferredName, contentType: nil, encodingQuality: encodingQuality, sourceLocation: sourceLocation) + let attachment = Self(image, named: preferredName, as: contentType, encodingQuality: encodingQuality, sourceLocation: sourceLocation) + Self.record(attachment, sourceLocation: sourceLocation) } } #endif diff --git a/Sources/Overlays/_Testing_CoreGraphics/Attachments/_AttachableImageWrapper.swift b/Sources/Overlays/_Testing_CoreGraphics/Attachments/_AttachableImageWrapper.swift index 7aa1fd139..9b8ea6788 100644 --- a/Sources/Overlays/_Testing_CoreGraphics/Attachments/_AttachableImageWrapper.swift +++ b/Sources/Overlays/_Testing_CoreGraphics/Attachments/_AttachableImageWrapper.swift @@ -48,6 +48,7 @@ import UniformTypeIdentifiers /// /// - [`CGImage`](https://developer.apple.com/documentation/coregraphics/cgimage) @_spi(Experimental) +@available(_uttypesAPI, *) public struct _AttachableImageWrapper: Sendable where Image: AttachableAsCGImage { /// The underlying image. /// @@ -61,7 +62,7 @@ public struct _AttachableImageWrapper: Sendable where Image: AttachableAs var encodingQuality: Float /// Storage for ``contentType``. - private var _contentType: (any Sendable)? + private var _contentType: UTType? /// The content type to use when encoding the image. /// @@ -70,14 +71,9 @@ public struct _AttachableImageWrapper: Sendable where Image: AttachableAs /// /// If the value of this property does not conform to [`UTType.image`](https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/image), /// the result is undefined. - @available(_uttypesAPI, *) var contentType: UTType { get { - if let contentType = _contentType as? UTType { - return contentType - } else { - return encodingQuality < 1.0 ? .jpeg : .png - } + _contentType ?? .image } set { precondition( @@ -92,34 +88,17 @@ public struct _AttachableImageWrapper: Sendable where Image: AttachableAs /// type for `UTType.image`. /// /// This property is not part of the public interface of the testing library. - @available(_uttypesAPI, *) var computedContentType: UTType { - if let contentType = _contentType as? UTType, contentType != .image { - contentType - } else { - encodingQuality < 1.0 ? .jpeg : .png + if contentType == .image { + return encodingQuality < 1.0 ? .jpeg : .png } + return contentType } - /// The type identifier (as a `CFString`) corresponding to this instance's - /// ``computedContentType`` property. - /// - /// The value of this property is used by ImageIO when serializing an image. - /// - /// This property is not part of the public interface of the testing library. - /// It is used by ImageIO below. - var typeIdentifier: CFString { - if #available(_uttypesAPI, *) { - computedContentType.identifier as CFString - } else { - encodingQuality < 1.0 ? kUTTypeJPEG : kUTTypePNG - } - } - - init(image: Image, encodingQuality: Float, contentType: (any Sendable)?) { + init(image: Image, encodingQuality: Float, contentType: UTType?) { self.image = image._makeCopyForAttachment() self.encodingQuality = encodingQuality - if #available(_uttypesAPI, *), let contentType = contentType as? UTType { + if let contentType { self.contentType = contentType } } @@ -127,6 +106,7 @@ public struct _AttachableImageWrapper: Sendable where Image: AttachableAs // MARK: - +@available(_uttypesAPI, *) extension _AttachableImageWrapper: AttachableWrapper { public var wrappedValue: Image { image @@ -139,6 +119,7 @@ extension _AttachableImageWrapper: AttachableWrapper { let attachableCGImage = try image.attachableCGImage // Create the image destination. + let typeIdentifier = computedContentType.identifier as CFString guard let dest = CGImageDestinationCreateWithData(data as CFMutableData, typeIdentifier, 1, nil) else { throw ImageAttachmentError.couldNotCreateImageDestination } @@ -168,11 +149,7 @@ extension _AttachableImageWrapper: AttachableWrapper { } public borrowing func preferredName(for attachment: borrowing Attachment, basedOn suggestedName: String) -> String { - if #available(_uttypesAPI, *) { - return (suggestedName as NSString).appendingPathExtension(for: computedContentType) - } - - return suggestedName + (suggestedName as NSString).appendingPathExtension(for: computedContentType) } } #endif diff --git a/Tests/TestingTests/AttachmentTests.swift b/Tests/TestingTests/AttachmentTests.swift index be940371e..efa1eccfd 100644 --- a/Tests/TestingTests/AttachmentTests.swift +++ b/Tests/TestingTests/AttachmentTests.swift @@ -537,6 +537,23 @@ extension AttachmentTests { Attachment.record(attachment) } + @available(_uttypesAPI, *) + @Test func attachCGImageDirectly() async throws { + await confirmation("Attachment detected") { valueAttached in + var configuration = Configuration() + configuration.eventHandler = { event, _ in + if case .valueAttached = event.kind { + valueAttached() + } + } + + await Test { + let image = try Self.cgImage.get() + Attachment.record(image, named: "diamond.jpg") + }.run(configuration: configuration) + } + } + @available(_uttypesAPI, *) @Test(arguments: [Float(0.0).nextUp, 0.25, 0.5, 0.75, 1.0], [.png as UTType?, .jpeg, .gif, .image, nil]) func attachCGImage(quality: Float, type: UTType?) throws { From 49099a2c048c64883056f13eb3fef3ae7f4b6718 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Wed, 2 Jul 2025 14:13:07 -0400 Subject: [PATCH 044/216] Add a cross-import overlay with AppKit to allow attaching `NSImage`s. (#869) This PR adds on to the Core Graphics cross-import overlay added in #827 to allow attaching instances of `NSImage` to a test. `NSImage` is a more complicated animal because it is not `Sendable`, but we don't want to make a (potentially very expensive) deep copy of its data until absolutely necessary. So we check inside the image to see if its contained representations are known to be safely copyable (i.e. copies made with `NSCopying` do not share any mutable state with their originals.) If it looks safe to make a copy of the image by calling `copy()`, we do so; otherwise, we try to make a deep copy of the image. Due to how Swift implements polymorphism in protocol requirements, and because we don't really know what they're doing, subclasses of `NSImage` just get a call to `copy()` instead of deep introspection. `UIImage` support will be implemented in a separate PR. > [!NOTE] > Image attachments remain an experimental feature. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- Package.swift | 10 ++ .../NSImage+AttachableAsCGImage.swift | 95 +++++++++++++++ .../_Testing_AppKit/ReexportTesting.swift | 12 ++ .../Attachments/AttachableAsCGImage.swift | 2 + .../Attachment+AttachableAsCGImage.swift | 4 + .../Attachments/_AttachableImageWrapper.swift | 2 + Tests/TestingTests/AttachmentTests.swift | 108 ++++++++++++++++++ 7 files changed, 233 insertions(+) create mode 100644 Sources/Overlays/_Testing_AppKit/Attachments/NSImage+AttachableAsCGImage.swift create mode 100644 Sources/Overlays/_Testing_AppKit/ReexportTesting.swift diff --git a/Package.swift b/Package.swift index 031968574..5d08997e0 100644 --- a/Package.swift +++ b/Package.swift @@ -125,6 +125,7 @@ let package = Package( name: "TestingTests", dependencies: [ "Testing", + "_Testing_AppKit", "_Testing_CoreGraphics", "_Testing_Foundation", "MemorySafeTestingTests", @@ -190,6 +191,15 @@ let package = Package( ), // Cross-import overlays (not supported by Swift Package Manager) + .target( + name: "_Testing_AppKit", + dependencies: [ + "Testing", + "_Testing_CoreGraphics", + ], + path: "Sources/Overlays/_Testing_AppKit", + swiftSettings: .packageSettings + ), .target( name: "_Testing_CoreGraphics", dependencies: [ diff --git a/Sources/Overlays/_Testing_AppKit/Attachments/NSImage+AttachableAsCGImage.swift b/Sources/Overlays/_Testing_AppKit/Attachments/NSImage+AttachableAsCGImage.swift new file mode 100644 index 000000000..529dfc724 --- /dev/null +++ b/Sources/Overlays/_Testing_AppKit/Attachments/NSImage+AttachableAsCGImage.swift @@ -0,0 +1,95 @@ +// +// 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 +// + +#if SWT_TARGET_OS_APPLE && canImport(AppKit) +public import AppKit +@_spi(ForSwiftTestingOnly) @_spi(Experimental) public import _Testing_CoreGraphics + +@_spi(Experimental) +extension NSImage: AttachableAsCGImage { + public var attachableCGImage: CGImage { + get throws { + let ctm = AffineTransform(scale: _attachmentScaleFactor) as NSAffineTransform + guard let result = cgImage(forProposedRect: nil, context: nil, hints: [.ctm: ctm]) else { + throw ImageAttachmentError.couldNotCreateCGImage + } + return result + } + } + + public var _attachmentScaleFactor: CGFloat { + let maxRepWidth = representations.lazy + .map { CGFloat($0.pixelsWide) / $0.size.width } + .filter { $0 > 0.0 } + .max() + return maxRepWidth ?? 1.0 + } + + /// Get the base address of the loaded image containing `class`. + /// + /// - Parameters: + /// - class: The class to look for. + /// + /// - Returns: The base address of the image containing `class`, or `nil` if + /// no image was found (for instance, if the class is generic or dynamically + /// generated.) + /// + /// "Image" in this context refers to a binary/executable image. + private static func _baseAddressOfImage(containing `class`: AnyClass) -> UnsafeRawPointer? { + let classAsAddress = Unmanaged.passUnretained(`class` as AnyObject).toOpaque() + + var info = Dl_info() + guard 0 != dladdr(classAsAddress, &info) else { + return nil + } + return .init(info.dli_fbase) + } + + /// The base address of the image containing AppKit's symbols, if known. + private static nonisolated(unsafe) let _appKitBaseAddress = _baseAddressOfImage(containing: NSImageRep.self) + + public func _makeCopyForAttachment() -> Self { + // If this image is of an NSImage subclass, we cannot reliably make a deep + // copy of it because we don't know what its `init(data:)` implementation + // might do. Try to make a copy (using NSCopying), but if that doesn't work + // then just return `self` verbatim. + // + // Third-party NSImage subclasses are presumably rare in the wild, so + // hopefully this case doesn't pop up too often. + guard isMember(of: NSImage.self) else { + return self.copy() as? Self ?? self + } + + // Check whether the image contains any representations that we don't think + // are safe. If it does, then make a "safe" copy. + let allImageRepsAreSafe = representations.allSatisfy { imageRep in + // NSCustomImageRep includes an arbitrary rendering block that may not be + // concurrency-safe in Swift. + if imageRep is NSCustomImageRep { + return false + } + + // Treat all other classes declared in AppKit as safe. We can't reason + // about classes declared in other modules, so treat them all as if they + // are unsafe. + return Self._baseAddressOfImage(containing: type(of: imageRep)) == Self._appKitBaseAddress + } + if !allImageRepsAreSafe, let safeCopy = tiffRepresentation.flatMap(Self.init(data:)) { + // Create a "safe" copy of this image by flattening it to TIFF and then + // creating a new NSImage instance from it. + return safeCopy + } + + // This image appears to be safe to copy directly. (This call should never + // fail since we already know `self` is a direct instance of `NSImage`.) + return unsafeDowncast(self.copy() as AnyObject, to: Self.self) + } +} +#endif diff --git a/Sources/Overlays/_Testing_AppKit/ReexportTesting.swift b/Sources/Overlays/_Testing_AppKit/ReexportTesting.swift new file mode 100644 index 000000000..3716f1f01 --- /dev/null +++ b/Sources/Overlays/_Testing_AppKit/ReexportTesting.swift @@ -0,0 +1,12 @@ +// +// 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 +// + +@_exported public import Testing +@_exported public import _Testing_CoreGraphics diff --git a/Sources/Overlays/_Testing_CoreGraphics/Attachments/AttachableAsCGImage.swift b/Sources/Overlays/_Testing_CoreGraphics/Attachments/AttachableAsCGImage.swift index 14df843c6..08c94823c 100644 --- a/Sources/Overlays/_Testing_CoreGraphics/Attachments/AttachableAsCGImage.swift +++ b/Sources/Overlays/_Testing_CoreGraphics/Attachments/AttachableAsCGImage.swift @@ -24,6 +24,8 @@ private import ImageIO /// be attached to a test: /// /// - [`CGImage`](https://developer.apple.com/documentation/coregraphics/cgimage) +/// - [`NSImage`](https://developer.apple.com/documentation/appkit/nsimage) +/// (macOS) /// /// You do not generally need to add your own conformances to this protocol. If /// you have an image in another format that needs to be attached to a test, diff --git a/Sources/Overlays/_Testing_CoreGraphics/Attachments/Attachment+AttachableAsCGImage.swift b/Sources/Overlays/_Testing_CoreGraphics/Attachments/Attachment+AttachableAsCGImage.swift index 6bb6f3744..6c9a76dd5 100644 --- a/Sources/Overlays/_Testing_CoreGraphics/Attachments/Attachment+AttachableAsCGImage.swift +++ b/Sources/Overlays/_Testing_CoreGraphics/Attachments/Attachment+AttachableAsCGImage.swift @@ -36,6 +36,8 @@ extension Attachment { /// ``AttachableAsCGImage`` protocol and can be attached to a test: /// /// - [`CGImage`](https://developer.apple.com/documentation/coregraphics/cgimage) + /// - [`NSImage`](https://developer.apple.com/documentation/appkit/nsimage) + /// (macOS) /// /// The testing library uses the image format specified by `contentType`. Pass /// `nil` to let the testing library decide which image format to use. If you @@ -80,6 +82,8 @@ extension Attachment { /// ``AttachableAsCGImage`` protocol and can be attached to a test: /// /// - [`CGImage`](https://developer.apple.com/documentation/coregraphics/cgimage) + /// - [`NSImage`](https://developer.apple.com/documentation/appkit/nsimage) + /// (macOS) /// /// The testing library uses the image format specified by `contentType`. Pass /// `nil` to let the testing library decide which image format to use. If you diff --git a/Sources/Overlays/_Testing_CoreGraphics/Attachments/_AttachableImageWrapper.swift b/Sources/Overlays/_Testing_CoreGraphics/Attachments/_AttachableImageWrapper.swift index 9b8ea6788..f61b17e7d 100644 --- a/Sources/Overlays/_Testing_CoreGraphics/Attachments/_AttachableImageWrapper.swift +++ b/Sources/Overlays/_Testing_CoreGraphics/Attachments/_AttachableImageWrapper.swift @@ -47,6 +47,8 @@ import UniformTypeIdentifiers /// to the ``AttachableAsCGImage`` protocol and can be attached to a test: /// /// - [`CGImage`](https://developer.apple.com/documentation/coregraphics/cgimage) +/// - [`NSImage`](https://developer.apple.com/documentation/appkit/nsimage) +/// (macOS) @_spi(Experimental) @available(_uttypesAPI, *) public struct _AttachableImageWrapper: Sendable where Image: AttachableAsCGImage { diff --git a/Tests/TestingTests/AttachmentTests.swift b/Tests/TestingTests/AttachmentTests.swift index efa1eccfd..4b16d98ea 100644 --- a/Tests/TestingTests/AttachmentTests.swift +++ b/Tests/TestingTests/AttachmentTests.swift @@ -10,6 +10,10 @@ @testable @_spi(ForToolsIntegrationOnly) import Testing private import _TestingInternals +#if canImport(AppKit) +import AppKit +@_spi(Experimental) import _Testing_AppKit +#endif #if canImport(Foundation) import Foundation import _Testing_Foundation @@ -577,6 +581,71 @@ extension AttachmentTests { } } #endif + +#if canImport(AppKit) + static var nsImage: NSImage { + get throws { + let cgImage = try cgImage.get() + let size = CGSize(width: CGFloat(cgImage.width), height: CGFloat(cgImage.height)) + return NSImage(cgImage: cgImage, size: size) + } + } + + @available(_uttypesAPI, *) + @Test func attachNSImage() throws { + let image = try Self.nsImage + let attachment = Attachment(image, named: "diamond.jpg") + #expect(attachment.attachableValue.size == image.size) // NSImage makes a copy + try attachment.attachableValue.withUnsafeBytes(for: attachment) { buffer in + #expect(buffer.count > 32) + } + } + + @available(_uttypesAPI, *) + @Test func attachNSImageWithCustomRep() throws { + let image = NSImage(size: NSSize(width: 32.0, height: 32.0), flipped: false) { rect in + NSColor.red.setFill() + rect.fill() + return true + } + let attachment = Attachment(image, named: "diamond.jpg") + #expect(attachment.attachableValue.size == image.size) // NSImage makes a copy + try attachment.attachableValue.withUnsafeBytes(for: attachment) { buffer in + #expect(buffer.count > 32) + } + } + + @available(_uttypesAPI, *) + @Test func attachNSImageWithSubclassedNSImage() throws { + let image = MyImage(size: NSSize(width: 32.0, height: 32.0)) + image.addRepresentation(NSCustomImageRep(size: image.size, flipped: false) { rect in + NSColor.green.setFill() + rect.fill() + return true + }) + + let attachment = Attachment(image, named: "diamond.jpg") + #expect(attachment.attachableValue === image) + #expect(attachment.attachableValue.size == image.size) // NSImage makes a copy + try attachment.attachableValue.withUnsafeBytes(for: attachment) { buffer in + #expect(buffer.count > 32) + } + } + + @available(_uttypesAPI, *) + @Test func attachNSImageWithSubclassedRep() throws { + let image = NSImage(size: NSSize(width: 32.0, height: 32.0)) + image.addRepresentation(MyImageRep()) + + let attachment = Attachment(image, named: "diamond.jpg") + #expect(attachment.attachableValue.size == image.size) // NSImage makes a copy + let firstRep = try #require(attachment.attachableValue.representations.first) + #expect(!(firstRep is MyImageRep)) + try attachment.attachableValue.withUnsafeBytes(for: attachment) { buffer in + #expect(buffer.count > 32) + } + } +#endif #endif } } @@ -666,3 +735,42 @@ final class MyCodableAndSecureCodingAttachable: NSObject, Codable, NSSecureCodin } } #endif + +#if canImport(AppKit) +private final class MyImage: NSImage { + override init(size: NSSize) { + super.init(size: size) + } + + required init(pasteboardPropertyList propertyList: Any, ofType type: NSPasteboard.PasteboardType) { + fatalError("Unimplemented") + } + + required init(coder: NSCoder) { + fatalError("Unimplemented") + } + + override func copy(with zone: NSZone?) -> Any { + // Intentionally make a copy as NSImage instead of MyImage to exercise the + // cast-failed code path in the overlay. + NSImage() + } +} + +private final class MyImageRep: NSImageRep { + override init() { + super.init() + size = NSSize(width: 32.0, height: 32.0) + } + + required init?(coder: NSCoder) { + fatalError("Unimplemented") + } + + override func draw() -> Bool { + NSColor.blue.setFill() + NSRect(origin: .zero, size: size).fill() + return true + } +} +#endif From fab283ac7cdadd239140f6e4a9f30774ac0284b7 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Wed, 2 Jul 2025 15:41:58 -0400 Subject: [PATCH 045/216] Fixes to the AppKit/`NSImage` overlay. (#1196) The PR (#869) that just added the AppKit overlay for `NSImage` was a bit stale. This PR corrects a couple of bugs that snuck in due to its age and the resulting bit rot. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- Package.swift | 2 +- .../Attachments/NSImage+AttachableAsCGImage.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Package.swift b/Package.swift index 5d08997e0..6a86f6774 100644 --- a/Package.swift +++ b/Package.swift @@ -198,7 +198,7 @@ let package = Package( "_Testing_CoreGraphics", ], path: "Sources/Overlays/_Testing_AppKit", - swiftSettings: .packageSettings + swiftSettings: .packageSettings + .enableLibraryEvolution() ), .target( name: "_Testing_CoreGraphics", diff --git a/Sources/Overlays/_Testing_AppKit/Attachments/NSImage+AttachableAsCGImage.swift b/Sources/Overlays/_Testing_AppKit/Attachments/NSImage+AttachableAsCGImage.swift index 529dfc724..7e5c9363d 100644 --- a/Sources/Overlays/_Testing_AppKit/Attachments/NSImage+AttachableAsCGImage.swift +++ b/Sources/Overlays/_Testing_AppKit/Attachments/NSImage+AttachableAsCGImage.swift @@ -10,7 +10,7 @@ #if SWT_TARGET_OS_APPLE && canImport(AppKit) public import AppKit -@_spi(ForSwiftTestingOnly) @_spi(Experimental) public import _Testing_CoreGraphics +@_spi(Experimental) public import _Testing_CoreGraphics @_spi(Experimental) extension NSImage: AttachableAsCGImage { From 0022e294564f849c74e615f11df5c4c1cef13744 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Wed, 2 Jul 2025 16:03:21 -0400 Subject: [PATCH 046/216] Add a temporary package product that builds our cross-import overlays. (#1197) This PR adds a **temporary** product to our package that builds the cross-import overlay targets needed for image attachment support. This allows developers to try out this new code. This product will be removed from the package in a future update. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- Package.swift | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Package.swift b/Package.swift index 6a86f6774..dc481a163 100644 --- a/Package.swift +++ b/Package.swift @@ -84,6 +84,16 @@ let package = Package( ) #endif + result += [ + .library( + name: "_Testing_ExperimentalImageAttachments", + targets: [ + "_Testing_AppKit", + "_Testing_CoreGraphics", + ] + ) + ] + result.append( .library( name: "_TestDiscovery", From 64e1f9d8d358c5cb03addaab27fc7e7e5c503805 Mon Sep 17 00:00:00 2001 From: Stuart Montgomery Date: Thu, 3 Jul 2025 17:09:57 -0500 Subject: [PATCH 047/216] Apply the 'traitRelated' tag to more tests and suites (#1199) This is a small, test-only enhancement which applies the `.traitRelated` tag to more tests and suites which are related to specific traits or the trait subsystem. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- .../TestDeclarationMacroTests.swift | 4 ++++ .../TestSupport/TestingAdditions.swift | 16 ++++++++++++++++ Tests/TestingTests/PlanTests.swift | 4 ++-- Tests/TestingTests/SwiftPMTests.swift | 2 +- Tests/TestingTests/Test.SnapshotTests.swift | 2 +- .../Traits/IssueHandlingTraitTests.swift | 2 +- .../Traits/TestScopingTraitTests.swift | 2 +- 7 files changed, 26 insertions(+), 6 deletions(-) create mode 100644 Tests/TestingMacrosTests/TestSupport/TestingAdditions.swift diff --git a/Tests/TestingMacrosTests/TestDeclarationMacroTests.swift b/Tests/TestingMacrosTests/TestDeclarationMacroTests.swift index 6c04eb9eb..fd1fa6405 100644 --- a/Tests/TestingMacrosTests/TestDeclarationMacroTests.swift +++ b/Tests/TestingMacrosTests/TestDeclarationMacroTests.swift @@ -452,6 +452,7 @@ struct TestDeclarationMacroTests { } @Test("Valid tag expressions are allowed", + .tags(.traitRelated), arguments: [ #"@Test(.tags(.f)) func f() {}"#, #"@Test(Tag.List.tags(.f)) func f() {}"#, @@ -472,6 +473,7 @@ struct TestDeclarationMacroTests { } @Test("Invalid tag expressions are detected", + .tags(.traitRelated), arguments: [ "f()", ".f()", "loose", "WrongType.tag", "WrongType.f()", @@ -490,6 +492,7 @@ struct TestDeclarationMacroTests { } @Test("Valid bug identifiers are allowed", + .tags(.traitRelated), arguments: [ #"@Test(.bug(id: 12345)) func f() {}"#, #"@Test(.bug(id: "12345")) func f() {}"#, @@ -512,6 +515,7 @@ struct TestDeclarationMacroTests { } @Test("Invalid bug URLs are detected", + .tags(.traitRelated), arguments: [ "mailto: a@example.com", "example.com", ] diff --git a/Tests/TestingMacrosTests/TestSupport/TestingAdditions.swift b/Tests/TestingMacrosTests/TestSupport/TestingAdditions.swift new file mode 100644 index 000000000..68e0fe81b --- /dev/null +++ b/Tests/TestingMacrosTests/TestSupport/TestingAdditions.swift @@ -0,0 +1,16 @@ +// +// 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 +// + +import Testing + +extension Tag { + /// A tag indicating that a test is related to a trait. + @Tag static var traitRelated: Self +} diff --git a/Tests/TestingTests/PlanTests.swift b/Tests/TestingTests/PlanTests.swift index 0c57a7e03..1a8a8a8f1 100644 --- a/Tests/TestingTests/PlanTests.swift +++ b/Tests/TestingTests/PlanTests.swift @@ -369,7 +369,7 @@ struct PlanTests { #expect(!planTests.contains(testC)) } - @Test("Recursive trait application") + @Test("Recursive trait application", .tags(.traitRelated)) func recursiveTraitApplication() async throws { let outerTestType = try #require(await test(for: OuterTest.self)) // Intentionally omitting intermediate tests here... @@ -387,7 +387,7 @@ struct PlanTests { #expect(testWithTraitAdded.traits.contains { $0 is DummyRecursiveTrait }) } - @Test("Relative order of recursively applied traits") + @Test("Relative order of recursively applied traits", .tags(.traitRelated)) func recursiveTraitOrder() async throws { let testSuiteA = try #require(await test(for: RelativeTraitOrderingTests.A.self)) let testSuiteB = try #require(await test(for: RelativeTraitOrderingTests.A.B.self)) diff --git a/Tests/TestingTests/SwiftPMTests.swift b/Tests/TestingTests/SwiftPMTests.swift index eadde29a7..8d00b74d5 100644 --- a/Tests/TestingTests/SwiftPMTests.swift +++ b/Tests/TestingTests/SwiftPMTests.swift @@ -133,7 +133,7 @@ struct SwiftPMTests { #expect(planTests.contains(test2)) } - @Test(".hidden trait") + @Test(".hidden trait", .tags(.traitRelated)) func hidden() async throws { let configuration = try configurationForEntryPoint(withArguments: ["PATH"]) let test1 = Test(name: "hello") {} diff --git a/Tests/TestingTests/Test.SnapshotTests.swift b/Tests/TestingTests/Test.SnapshotTests.swift index 12a3f2467..a0c83be1f 100644 --- a/Tests/TestingTests/Test.SnapshotTests.swift +++ b/Tests/TestingTests/Test.SnapshotTests.swift @@ -98,7 +98,7 @@ struct Test_SnapshotTests { private static let bug: Bug = Bug.bug(id: 12345, "Lorem ipsum") @available(_clockAPI, *) - @Test("timeLimit property", _timeLimitIfAvailable(minutes: 999_999_999)) + @Test("timeLimit property", .tags(.traitRelated), _timeLimitIfAvailable(minutes: 999_999_999)) func timeLimit() async throws { let test = try #require(Test.current) let snapshot = Test.Snapshot(snapshotting: test) diff --git a/Tests/TestingTests/Traits/IssueHandlingTraitTests.swift b/Tests/TestingTests/Traits/IssueHandlingTraitTests.swift index eb1aa1233..c32d89111 100644 --- a/Tests/TestingTests/Traits/IssueHandlingTraitTests.swift +++ b/Tests/TestingTests/Traits/IssueHandlingTraitTests.swift @@ -10,7 +10,7 @@ @testable @_spi(Experimental) @_spi(ForToolsIntegrationOnly) import Testing -@Suite("IssueHandlingTrait Tests") +@Suite("IssueHandlingTrait Tests", .tags(.traitRelated)) struct IssueHandlingTraitTests { @Test("Transforming an issue by appending a comment") func addComment() async throws { diff --git a/Tests/TestingTests/Traits/TestScopingTraitTests.swift b/Tests/TestingTests/Traits/TestScopingTraitTests.swift index af63deb5e..2fcd7b260 100644 --- a/Tests/TestingTests/Traits/TestScopingTraitTests.swift +++ b/Tests/TestingTests/Traits/TestScopingTraitTests.swift @@ -10,7 +10,7 @@ @testable @_spi(Experimental) @_spi(ForToolsIntegrationOnly) import Testing -@Suite("TestScoping-conforming Trait Tests") +@Suite("TestScoping-conforming Trait Tests", .tags(.traitRelated)) struct TestScopingTraitTests { @Test("Execute code before and after a non-parameterized test.") func executeCodeBeforeAndAfterNonParameterizedTest() async { From 3c64d2bf6b4c145a13e83d4df091e705e87cbb97 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Mon, 7 Jul 2025 12:19:03 -0400 Subject: [PATCH 048/216] Adopt `SuspendingClock.systemEpoch`. (#1202) This PR adopts the new `systemEpoch` API added with [SE-0473](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0473-clock-epochs.md). This API is back-deployed, so we don't need availability annotations on Darwin. It is not available with the 6.1 toolchain though, so the old `unsafeBitCast()` calls remain if building from source with a 6.1 toolchain. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- Sources/Testing/Events/TimeValue.swift | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Sources/Testing/Events/TimeValue.swift b/Sources/Testing/Events/TimeValue.swift index 5dac1fe08..143aa7091 100644 --- a/Sources/Testing/Events/TimeValue.swift +++ b/Sources/Testing/Events/TimeValue.swift @@ -54,7 +54,11 @@ struct TimeValue: Sendable { @available(_clockAPI, *) init(_ instant: SuspendingClock.Instant) { +#if compiler(>=6.2) + self.init(SuspendingClock().systemEpoch.duration(to: instant)) +#else self.init(unsafeBitCast(instant, to: Duration.self)) +#endif } } @@ -110,7 +114,11 @@ extension Duration { @available(_clockAPI, *) extension SuspendingClock.Instant { init(_ timeValue: TimeValue) { +#if compiler(>=6.2) + self = SuspendingClock().systemEpoch.advanced(by: Duration(timeValue)) +#else self = unsafeBitCast(Duration(timeValue), to: SuspendingClock.Instant.self) +#endif } } From 5fd38dfd567ae9da300f25e518cf2843c12dbc46 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Mon, 7 Jul 2025 12:24:24 -0400 Subject: [PATCH 049/216] Disallow the `@Test` attribute on operator declarations. (#1205) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR prevents `@Test` from being applied to an operator declaration such as: ```swift @Test(arguments: ...) static func +(lhs: A, rhs: B) { ... } ``` Now, the following error will be emitted by the compiler: > 🛑 Attribute 'Test' cannot be applied to an operator Previously, applying `@Test` to an operator produced undefined/unstable effects. Resolves #1204. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- .../Additions/FunctionDeclSyntaxAdditions.swift | 10 ++++++++++ Sources/TestingMacros/Support/DiagnosticMessage.swift | 6 +++++- Sources/TestingMacros/TestDeclarationMacro.swift | 2 +- .../TestingMacrosTests/TestDeclarationMacroTests.swift | 2 ++ 4 files changed, 18 insertions(+), 2 deletions(-) diff --git a/Sources/TestingMacros/Support/Additions/FunctionDeclSyntaxAdditions.swift b/Sources/TestingMacros/Support/Additions/FunctionDeclSyntaxAdditions.swift index 9b9378283..9b55bc157 100644 --- a/Sources/TestingMacros/Support/Additions/FunctionDeclSyntaxAdditions.swift +++ b/Sources/TestingMacros/Support/Additions/FunctionDeclSyntaxAdditions.swift @@ -34,6 +34,16 @@ extension FunctionDeclSyntax { .contains(.keyword(.nonisolated)) } + /// Whether or not this function declares an operator. + var isOperator: Bool { + switch name.tokenKind { + case .binaryOperator, .prefixOperator, .postfixOperator: + true + default: + false + } + } + /// The name of this function including parentheses, parameter labels, and /// colons. var completeName: DeclReferenceExprSyntax { diff --git a/Sources/TestingMacros/Support/DiagnosticMessage.swift b/Sources/TestingMacros/Support/DiagnosticMessage.swift index 9f155b63a..71d5a7100 100644 --- a/Sources/TestingMacros/Support/DiagnosticMessage.swift +++ b/Sources/TestingMacros/Support/DiagnosticMessage.swift @@ -93,7 +93,11 @@ struct DiagnosticMessage: SwiftDiagnostics.DiagnosticMessage { let result: (value: String, article: String) switch node.kind { case .functionDecl: - result = ("function", "a") + if node.cast(FunctionDeclSyntax.self).isOperator { + result = ("operator", "an") + } else { + result = ("function", "a") + } case .classDecl: result = ("class", "a") case .structDecl: diff --git a/Sources/TestingMacros/TestDeclarationMacro.swift b/Sources/TestingMacros/TestDeclarationMacro.swift index 50ac690d2..ef156edd6 100644 --- a/Sources/TestingMacros/TestDeclarationMacro.swift +++ b/Sources/TestingMacros/TestDeclarationMacro.swift @@ -61,7 +61,7 @@ public struct TestDeclarationMacro: PeerMacro, Sendable { } // The @Test attribute is only supported on function declarations. - guard let function = declaration.as(FunctionDeclSyntax.self) else { + guard let function = declaration.as(FunctionDeclSyntax.self), !function.isOperator else { diagnostics.append(.attributeNotSupported(testAttribute, on: declaration)) return false } diff --git a/Tests/TestingMacrosTests/TestDeclarationMacroTests.swift b/Tests/TestingMacrosTests/TestDeclarationMacroTests.swift index fd1fa6405..47e2b5112 100644 --- a/Tests/TestingMacrosTests/TestDeclarationMacroTests.swift +++ b/Tests/TestingMacrosTests/TestDeclarationMacroTests.swift @@ -67,6 +67,8 @@ struct TestDeclarationMacroTests { "Attribute 'Test' cannot be applied to a structure", "@Test enum E {}": "Attribute 'Test' cannot be applied to an enumeration", + "@Test func +() {}": + "Attribute 'Test' cannot be applied to an operator", // Availability "@available(*, unavailable) @Suite struct S {}": From 3980e9f053a97da7a29a5076186d9f78d6eb9887 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Mon, 7 Jul 2025 12:37:04 -0400 Subject: [PATCH 050/216] Revert "Work around a compiler regression affecting exit test value capturing. (#1171)" This reverts commit 77de51d550fe406c15af4eb5ac3834580afa3eaf. --- .../ExpectationChecking+Macro.swift | 27 ------------------- Sources/TestingMacros/ConditionMacro.swift | 27 ------------------- .../Support/ClosureCaptureListParsing.swift | 2 +- 3 files changed, 1 insertion(+), 55 deletions(-) diff --git a/Sources/Testing/Expectations/ExpectationChecking+Macro.swift b/Sources/Testing/Expectations/ExpectationChecking+Macro.swift index 9dfc5ace7..3a190e679 100644 --- a/Sources/Testing/Expectations/ExpectationChecking+Macro.swift +++ b/Sources/Testing/Expectations/ExpectationChecking+Macro.swift @@ -1175,7 +1175,6 @@ public func __checkClosureCall( /// /// - Warning: This function is used to implement the `#expect()` and /// `#require()` macros. Do not call it directly. -#if SWT_FIXED_154221449 @_spi(Experimental) public func __checkClosureCall( identifiedBy exitTestID: (UInt64, UInt64, UInt64, UInt64), @@ -1200,32 +1199,6 @@ public func __checkClosureCall( sourceLocation: sourceLocation ) } -#else -@_spi(Experimental) -public func __checkClosureCall( - identifiedBy exitTestID: (UInt64, UInt64, UInt64, UInt64), - encodingCapturedValues capturedValues: repeat each T, - processExitsWith expectedExitCondition: ExitTest.Condition, - observing observedValues: [any PartialKeyPath & Sendable] = [], - performing _: @convention(c) () -> Void, - expression: __Expression, - comments: @autoclosure () -> [Comment], - isRequired: Bool, - isolation: isolated (any Actor)? = #isolation, - sourceLocation: SourceLocation -) async -> Result where repeat each T: Codable & Sendable { - await callExitTest( - identifiedBy: exitTestID, - encodingCapturedValues: Array(repeat each capturedValues), - processExitsWith: expectedExitCondition, - observing: observedValues, - expression: expression, - comments: comments(), - isRequired: isRequired, - sourceLocation: sourceLocation - ) -} -#endif #endif // MARK: - diff --git a/Sources/TestingMacros/ConditionMacro.swift b/Sources/TestingMacros/ConditionMacro.swift index 9d0c4f15f..0ef0970e9 100644 --- a/Sources/TestingMacros/ConditionMacro.swift +++ b/Sources/TestingMacros/ConditionMacro.swift @@ -98,20 +98,7 @@ extension ConditionMacro { if let trailingClosureIndex { // Assume that the comment, if present is the last argument in the // argument list prior to the trailing closure that has no label. -#if SWT_FIXED_154221449 commentIndex = macroArguments[.. 1 { // If there is no trailing closure argument and there is more than one // argument, then the comment is the last argument with no label (and also @@ -560,7 +547,6 @@ extension ExitTestConditionMacro { var leadingArguments = [ Argument(label: "identifiedBy", expression: idExpr), ] -#if SWT_FIXED_154221449 if !capturedValues.isEmpty { leadingArguments.append( Argument( @@ -573,19 +559,6 @@ extension ExitTestConditionMacro { ) ) } -#else - if let firstCapturedValue = capturedValues.first { - leadingArguments.append( - Argument( - label: "encodingCapturedValues", - expression: firstCapturedValue.typeCheckedExpression - ) - ) - leadingArguments += capturedValues.dropFirst() - .map(\.typeCheckedExpression) - .map { Argument(expression: $0) } - } -#endif arguments = leadingArguments + arguments // Replace the exit test body (as an argument to the macro) with a stub diff --git a/Sources/TestingMacros/Support/ClosureCaptureListParsing.swift b/Sources/TestingMacros/Support/ClosureCaptureListParsing.swift index 08536aa69..80c216854 100644 --- a/Sources/TestingMacros/Support/ClosureCaptureListParsing.swift +++ b/Sources/TestingMacros/Support/ClosureCaptureListParsing.swift @@ -36,7 +36,7 @@ struct CapturedValueInfo { /// The expression to assign to the captured value with type-checking applied. var typeCheckedExpression: ExprSyntax { - #"#__capturedValue(\#(expression.trimmed), \#(literal: name.trimmedDescription), \#(type.trimmed).self)"# + #"#__capturedValue(\#(expression.trimmed), \#(literal: name.trimmedDescription), (\#(type.trimmed)).self)"# } init(_ capture: ClosureCaptureSyntax, in context: some MacroExpansionContext) { From 74f2c74e406f242af1b9090c884f3b780f5e4395 Mon Sep 17 00:00:00 2001 From: Suzy Ratcliff Date: Mon, 7 Jul 2025 21:34:42 -0700 Subject: [PATCH 051/216] Add isFailure to ABI.EncodedIssue (#1211) This adds `isFailure` to `ABI.EncodedIssue` ### Motivation: Clients need to know if an issue is failing so that they know how to handle it. ### Modifications: This adds `isFailure` to `ABI.EncodedIssue` ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --------- Co-authored-by: Jonathan Grynspan --- Sources/Testing/ABI/Encoded/ABI.EncodedIssue.swift | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Sources/Testing/ABI/Encoded/ABI.EncodedIssue.swift b/Sources/Testing/ABI/Encoded/ABI.EncodedIssue.swift index 0ea218cc8..465be7aee 100644 --- a/Sources/Testing/ABI/Encoded/ABI.EncodedIssue.swift +++ b/Sources/Testing/ABI/Encoded/ABI.EncodedIssue.swift @@ -28,6 +28,11 @@ extension ABI { /// /// - Warning: Severity is not yet part of the JSON schema. var _severity: Severity + + /// If the issue is a failing issue. + /// + /// - Warning: Non-failing issues are not yet part of the JSON schema. + var _isFailure: Bool /// Whether or not this issue is known to occur. var isKnown: Bool @@ -50,6 +55,7 @@ extension ABI { case .warning: .warning case .error: .error } + _isFailure = issue.isFailure isKnown = issue.isKnown sourceLocation = issue.sourceLocation if let backtrace = issue.sourceContext.backtrace { From 404b142da2173e908a62a26a9ef567caa13ac526 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Tue, 8 Jul 2025 11:03:11 -0400 Subject: [PATCH 052/216] Bump the required compiler version for `systemEpoch` to 6.3. (#1213) There are some workflows including Xcode 26 betas where the compiler version is 6.2 but `systemEpoch` isn't available yet, so bump the requirement for it to 6.3. This is temporary and we can delete the check outright after 6.2 ships. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- Sources/Testing/Events/TimeValue.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/Testing/Events/TimeValue.swift b/Sources/Testing/Events/TimeValue.swift index 143aa7091..838eb381b 100644 --- a/Sources/Testing/Events/TimeValue.swift +++ b/Sources/Testing/Events/TimeValue.swift @@ -54,7 +54,7 @@ struct TimeValue: Sendable { @available(_clockAPI, *) init(_ instant: SuspendingClock.Instant) { -#if compiler(>=6.2) +#if compiler(>=6.3) self.init(SuspendingClock().systemEpoch.duration(to: instant)) #else self.init(unsafeBitCast(instant, to: Duration.self)) @@ -114,7 +114,7 @@ extension Duration { @available(_clockAPI, *) extension SuspendingClock.Instant { init(_ timeValue: TimeValue) { -#if compiler(>=6.2) +#if compiler(>=6.3) self = SuspendingClock().systemEpoch.advanced(by: Duration(timeValue)) #else self = unsafeBitCast(Duration(timeValue), to: SuspendingClock.Instant.self) From 3c8e4e8b4d73931f59da6dc955447fda3508da7e Mon Sep 17 00:00:00 2001 From: Stuart Montgomery Date: Tue, 8 Jul 2025 12:26:51 -0500 Subject: [PATCH 053/216] Clarify the number of supported argument collections in article which discusses combinatoric parameterized testing (#1210) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is a small adjustment to the documentation article [Implementing parameterized tests](https://swiftpackageindex.com/swiftlang/swift-testing/main/documentation/testing/parameterizedtesting), in the section [Test with more than one collection](https://swiftpackageindex.com/swiftlang/swift-testing/main/documentation/testing/parameterizedtesting#Test-with-more-than-one-collection). It currently has a sentence which ends: > …, elements from the second collection are passed as the second argument, **and so forth.** The "and so forth" has caused some confusion since it implies that you can pass more than two argument collections, but in reality only two are supported currently. Eventually this could potentially be expanded with further improvements to the testing library, but that is tracked separately and the docs should reflect its current capabilities. Fixes rdar://154647425 ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- Sources/Testing/Testing.docc/ParameterizedTesting.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/Testing/Testing.docc/ParameterizedTesting.md b/Sources/Testing/Testing.docc/ParameterizedTesting.md index 2dada707e..7aeaeead4 100644 --- a/Sources/Testing/Testing.docc/ParameterizedTesting.md +++ b/Sources/Testing/Testing.docc/ParameterizedTesting.md @@ -146,8 +146,8 @@ func makeLargeOrder(of food: Food, count: Int) async throws { ``` Elements from the first collection are passed as the first argument to the test -function, elements from the second collection are passed as the second argument, -and so forth. +function, and elements from the second collection are passed as the second +argument. Assuming there are five cases in the `Food` enumeration, this test function will, when run, be invoked 500 times (5 x 100) with every possible combination From f9f25bfbdb3feaa15f43c1c8cf575bdc9a11eb5b Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Tue, 8 Jul 2025 15:15:08 -0400 Subject: [PATCH 054/216] Reset the working directory of child processes we spawn on Windows. (#1212) This PR modifies the Windows implementation of `spawnProcess()` so that it sets the working directory of the new process to "C:\" (or thereabouts). This prevents a race condition on Windows because that system won't let you delete a directory if it's the working directory of any process. See [The Old New Thing](https://devblogs.microsoft.com/oldnewthing/20101109-00/?p=12323) for a very on-the-nose blog post. Note that we do not specify the value of the working directory in an exit test body. A test should generally not rely on it anyway because it is global state and any thread could change its value at any time. I haven't written a unit test for this change because it's unclear what I could write that would be easily verifiable, and because I don't know what state I might perturb outside such a test by calling `SetCurrentDirectory()`. Resolves #1209. (This is a speculative fix.) ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- Sources/Testing/ExitTests/SpawnProcess.swift | 30 +++++++++++++++--- Sources/Testing/Support/FileHandle.swift | 31 +++++++++++++++++++ .../Support/FileHandleTests.swift | 11 +++++++ 3 files changed, 68 insertions(+), 4 deletions(-) diff --git a/Sources/Testing/ExitTests/SpawnProcess.swift b/Sources/Testing/ExitTests/SpawnProcess.swift index fe51a7086..9f01a1d11 100644 --- a/Sources/Testing/ExitTests/SpawnProcess.swift +++ b/Sources/Testing/ExitTests/SpawnProcess.swift @@ -284,25 +284,47 @@ func spawnExecutable( let commandLine = _escapeCommandLine(CollectionOfOne(executablePath) + arguments) let environ = environment.map { "\($0.key)=\($0.value)" }.joined(separator: "\0") + "\0\0" + // CreateProcessW() may modify the command line argument, so we must make + // a mutable copy of it. (environ is also passed as a mutable raw pointer, + // but it is not documented as actually being mutated.) + let commandLineCopy = commandLine.withCString(encodedAs: UTF16.self) { _wcsdup($0) } + defer { + free(commandLineCopy) + } + + // On Windows, a process holds a reference to its current working + // directory, which prevents other processes from deleting it. This causes + // code to fail if it tries to set the working directory to a temporary + // path. SEE: https://github.com/swiftlang/swift-testing/issues/1209 + // + // This problem manifests for us when we spawn a child process without + // setting its working directory, which causes it to default to that of + // the parent process. To avoid this problem, we set the working directory + // of the new process to the root directory of the boot volume (which is + // unlikely to be deleted, one hopes). + // + // SEE: https://devblogs.microsoft.com/oldnewthing/20101109-00/?p=12323 + let workingDirectoryPath = rootDirectoryPath + var flags = DWORD(CREATE_NO_WINDOW | CREATE_UNICODE_ENVIRONMENT | EXTENDED_STARTUPINFO_PRESENT) #if DEBUG // Start the process suspended so we can attach a debugger if needed. flags |= DWORD(CREATE_SUSPENDED) #endif - return try commandLine.withCString(encodedAs: UTF16.self) { commandLine in - try environ.withCString(encodedAs: UTF16.self) { environ in + return try environ.withCString(encodedAs: UTF16.self) { environ in + try workingDirectoryPath.withCString(encodedAs: UTF16.self) { workingDirectoryPath in var processInfo = PROCESS_INFORMATION() guard CreateProcessW( nil, - .init(mutating: commandLine), + commandLineCopy, nil, nil, true, // bInheritHandles flags, .init(mutating: environ), - nil, + workingDirectoryPath, startupInfo.pointer(to: \.StartupInfo)!, &processInfo ) else { diff --git a/Sources/Testing/Support/FileHandle.swift b/Sources/Testing/Support/FileHandle.swift index 1c5447460..37774b91a 100644 --- a/Sources/Testing/Support/FileHandle.swift +++ b/Sources/Testing/Support/FileHandle.swift @@ -719,4 +719,35 @@ func setFD_CLOEXEC(_ flag: Bool, onFileDescriptor fd: CInt) throws { } } #endif + +/// The path to the root directory of the boot volume. +/// +/// On Windows, this string is usually of the form `"C:\"`. On UNIX-like +/// platforms, it is always equal to `"/"`. +let rootDirectoryPath: String = { +#if os(Windows) + var result: String? + + // The boot volume is, except in some legacy scenarios, the volume that + // contains the system Windows directory. For an explanation of the difference + // between the Windows directory and the _system_ Windows directory, see + // https://devblogs.microsoft.com/oldnewthing/20140723-00/?p=423 . + let count = GetSystemWindowsDirectoryW(nil, 0) + if count > 0 { + withUnsafeTemporaryAllocation(of: wchar_t.self, capacity: Int(count) + 1) { buffer in + _ = GetSystemWindowsDirectoryW(buffer.baseAddress!, UINT(buffer.count)) + let rStrip = PathCchStripToRoot(buffer.baseAddress!, buffer.count) + if rStrip == S_OK || rStrip == S_FALSE { + result = String.decodeCString(buffer.baseAddress!, as: UTF16.self)?.result + } + } + } + + // If we weren't able to get a path, fall back to "C:\" on the assumption that + // it's the common case and most likely correct. + return result ?? #"C:\"# +#else + return "/" +#endif +}() #endif diff --git a/Tests/TestingTests/Support/FileHandleTests.swift b/Tests/TestingTests/Support/FileHandleTests.swift index 4be633ad6..acca1dbea 100644 --- a/Tests/TestingTests/Support/FileHandleTests.swift +++ b/Tests/TestingTests/Support/FileHandleTests.swift @@ -201,6 +201,17 @@ struct FileHandleTests { #endif } #endif + + @Test("Root directory path is correct") + func rootDirectoryPathIsCorrect() throws { +#if os(Windows) + if let systemDrive = Environment.variable(named: "SYSTEMDRIVE") { + #expect(rootDirectoryPath.starts(with: systemDrive)) + } +#else + #expect(rootDirectoryPath == "/") +#endif + } } // MARK: - Fixtures From f4379f527f518f54aff9215d46cc659e08e7d949 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Tue, 8 Jul 2025 16:16:32 -0400 Subject: [PATCH 055/216] Use a custom `AttachableImageFormat` type instead of directly relying on `UTType`. (#1203) This PR creates a platform-agnostic type to represent image formats for image attachments instead of relying directly on `UTType`. The implementation still requires `UTType` on Apple platforms, but on non-Apple platforms we can use the same type to represent those platforms' platform-specific image format enums (e.g. on Windows, it can box `CLSID`.) This reduces the platform-specific API surface area for image attachments. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- .../Attachments/AttachableAsCGImage.swift | 3 + .../AttachableImageFormat+UTType.swift | 100 ++++++++++++++++++ .../Attachment+AttachableAsCGImage.swift | 36 ++----- .../Attachments/_AttachableImageWrapper.swift | 56 ++-------- .../Attachments/AttachableImageFormat.swift | 93 ++++++++++++++++ Tests/TestingTests/AttachmentTests.swift | 20 +++- 6 files changed, 232 insertions(+), 76 deletions(-) create mode 100644 Sources/Overlays/_Testing_CoreGraphics/Attachments/AttachableImageFormat+UTType.swift create mode 100644 Sources/Testing/Attachments/AttachableImageFormat.swift diff --git a/Sources/Overlays/_Testing_CoreGraphics/Attachments/AttachableAsCGImage.swift b/Sources/Overlays/_Testing_CoreGraphics/Attachments/AttachableAsCGImage.swift index 08c94823c..b470534df 100644 --- a/Sources/Overlays/_Testing_CoreGraphics/Attachments/AttachableAsCGImage.swift +++ b/Sources/Overlays/_Testing_CoreGraphics/Attachments/AttachableAsCGImage.swift @@ -31,6 +31,7 @@ private import ImageIO /// you have an image in another format that needs to be attached to a test, /// first convert it to an instance of one of the types above. @_spi(Experimental) +@available(_uttypesAPI, *) public protocol AttachableAsCGImage { /// An instance of `CGImage` representing this image. /// @@ -73,6 +74,7 @@ public protocol AttachableAsCGImage { func _makeCopyForAttachment() -> Self } +@available(_uttypesAPI, *) extension AttachableAsCGImage { public var _attachmentOrientation: UInt32 { CGImagePropertyOrientation.up.rawValue @@ -83,6 +85,7 @@ extension AttachableAsCGImage { } } +@available(_uttypesAPI, *) extension AttachableAsCGImage where Self: Sendable { public func _makeCopyForAttachment() -> Self { self diff --git a/Sources/Overlays/_Testing_CoreGraphics/Attachments/AttachableImageFormat+UTType.swift b/Sources/Overlays/_Testing_CoreGraphics/Attachments/AttachableImageFormat+UTType.swift new file mode 100644 index 000000000..41f7e5998 --- /dev/null +++ b/Sources/Overlays/_Testing_CoreGraphics/Attachments/AttachableImageFormat+UTType.swift @@ -0,0 +1,100 @@ +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024–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 SWT_TARGET_OS_APPLE && canImport(CoreGraphics) +@_spi(Experimental) public import Testing + +public import UniformTypeIdentifiers + +@available(_uttypesAPI, *) +extension AttachableImageFormat { + /// Get the content type to use when encoding the image, substituting a + /// concrete type for `UTType.image` in particular. + /// + /// - Parameters: + /// - imageFormat: The image format to use, or `nil` if the developer did + /// not specify one. + /// - preferredName: The preferred name of the image for which a type is + /// needed. + /// + /// - Returns: An instance of `UTType` referring to a concrete image type. + /// + /// This function is not part of the public interface of the testing library. + static func computeContentType(for imageFormat: Self?, withPreferredName preferredName: String) -> UTType { + guard let imageFormat else { + // The developer didn't specify a type. Substitute the generic `.image` + // and solve for that instead. + return computeContentType(for: Self(.image, encodingQuality: 1.0), withPreferredName: preferredName) + } + + switch imageFormat.kind { + case .png: + return .png + case .jpeg: + return .jpeg + case let .systemValue(contentType): + let contentType = contentType as! UTType + if contentType != .image { + // The developer explicitly specified a type. + return contentType + } + + // The developer didn't specify a concrete type, so try to derive one from + // the preferred name's path extension. + let pathExtension = (preferredName as NSString).pathExtension + if !pathExtension.isEmpty, + let contentType = UTType(filenameExtension: pathExtension, conformingTo: .image), + contentType.isDeclared { + return contentType + } + + // We couldn't derive a concrete type from the path extension, so pick + // between PNG and JPEG based on the encoding quality. + return imageFormat.encodingQuality < 1.0 ? .jpeg : .png + } + } + + /// The content type corresponding to this image format. + /// + /// The value of this property always conforms to [`UTType.image`](https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/image). + public var contentType: UTType { + switch kind { + case .png: + return .png + case .jpeg: + return .jpeg + case let .systemValue(contentType): + return contentType as! UTType + } + } + + /// Initialize an instance of this type with the given content type and + /// encoding quality. + /// + /// - Parameters: + /// - contentType: The image format to use when encoding images. + /// - encodingQuality: The encoding quality to use when encoding images. For + /// the lowest supported quality, pass `0.0`. For the highest supported + /// quality, pass `1.0`. + /// + /// If the target image format does not support variable-quality encoding, + /// the value of the `encodingQuality` argument is ignored. + /// + /// If `imageFormat` is not `nil` and does not conform to [`UTType.image`](https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/image), + /// the result is undefined. + public init(_ contentType: UTType, encodingQuality: Float = 1.0) { + precondition( + contentType.conforms(to: .image), + "An image cannot be attached as an instance of type '\(contentType.identifier)'. Use a type that conforms to 'public.image' instead." + ) + self.init(kind: .systemValue(contentType), encodingQuality: encodingQuality) + } +} +#endif diff --git a/Sources/Overlays/_Testing_CoreGraphics/Attachments/Attachment+AttachableAsCGImage.swift b/Sources/Overlays/_Testing_CoreGraphics/Attachments/Attachment+AttachableAsCGImage.swift index 6c9a76dd5..7e6836ef5 100644 --- a/Sources/Overlays/_Testing_CoreGraphics/Attachments/Attachment+AttachableAsCGImage.swift +++ b/Sources/Overlays/_Testing_CoreGraphics/Attachments/Attachment+AttachableAsCGImage.swift @@ -11,8 +11,6 @@ #if SWT_TARGET_OS_APPLE && canImport(CoreGraphics) @_spi(Experimental) public import Testing -public import UniformTypeIdentifiers - @_spi(Experimental) @available(_uttypesAPI, *) extension Attachment { @@ -24,10 +22,7 @@ extension Attachment { /// - preferredName: The preferred name of the attachment when writing it /// to a test report or to disk. If `nil`, the testing library attempts /// to derive a reasonable filename for the attached value. - /// - contentType: The image format with which to encode `attachableValue`. - /// - encodingQuality: The encoding quality to use when encoding the image. - /// For the lowest supported quality, pass `0.0`. For the highest - /// supported quality, pass `1.0`. + /// - imageFormat: The image format with which to encode `attachableValue`. /// - sourceLocation: The source location of the call to this initializer. /// This value is used when recording issues associated with the /// attachment. @@ -39,26 +34,20 @@ extension Attachment { /// - [`NSImage`](https://developer.apple.com/documentation/appkit/nsimage) /// (macOS) /// - /// The testing library uses the image format specified by `contentType`. Pass + /// The testing library uses the image format specified by `imageFormat`. Pass /// `nil` to let the testing library decide which image format to use. If you /// pass `nil`, then the image format that the testing library uses depends on /// the path extension you specify in `preferredName`, if any. If you do not /// specify a path extension, or if the path extension you specify doesn't /// correspond to an image format the operating system knows how to write, the /// testing library selects an appropriate image format for you. - /// - /// If the target image format does not support variable-quality encoding, - /// the value of the `encodingQuality` argument is ignored. If `contentType` - /// is not `nil` and does not conform to [`UTType.image`](https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/image), - /// the result is undefined. public init( _ attachableValue: T, named preferredName: String? = nil, - as contentType: UTType? = nil, - encodingQuality: Float = 1.0, + as imageFormat: AttachableImageFormat? = nil, sourceLocation: SourceLocation = #_sourceLocation ) where AttachableValue == _AttachableImageWrapper { - let imageWrapper = _AttachableImageWrapper(image: attachableValue, encodingQuality: encodingQuality, contentType: contentType) + let imageWrapper = _AttachableImageWrapper(image: attachableValue, imageFormat: imageFormat) self.init(imageWrapper, named: preferredName, sourceLocation: sourceLocation) } @@ -69,10 +58,7 @@ extension Attachment { /// - preferredName: The preferred name of the attachment when writing it to /// a test report or to disk. If `nil`, the testing library attempts to /// derive a reasonable filename for the attached value. - /// - contentType: The image format with which to encode `attachableValue`. - /// - encodingQuality: The encoding quality to use when encoding the image. - /// For the lowest supported quality, pass `0.0`. For the highest - /// supported quality, pass `1.0`. + /// - imageFormat: The image format with which to encode `attachableValue`. /// - sourceLocation: The source location of the call to this function. /// /// This function creates a new instance of ``Attachment`` wrapping `image` @@ -85,26 +71,20 @@ extension Attachment { /// - [`NSImage`](https://developer.apple.com/documentation/appkit/nsimage) /// (macOS) /// - /// The testing library uses the image format specified by `contentType`. Pass + /// The testing library uses the image format specified by `imageFormat`. Pass /// `nil` to let the testing library decide which image format to use. If you /// pass `nil`, then the image format that the testing library uses depends on /// the path extension you specify in `preferredName`, if any. If you do not /// specify a path extension, or if the path extension you specify doesn't /// correspond to an image format the operating system knows how to write, the /// testing library selects an appropriate image format for you. - /// - /// If the target image format does not support variable-quality encoding, - /// the value of the `encodingQuality` argument is ignored. If `contentType` - /// is not `nil` and does not conform to [`UTType.image`](https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/image), - /// the result is undefined. public static func record( _ image: consuming T, named preferredName: String? = nil, - as contentType: UTType? = nil, - encodingQuality: Float = 1.0, + as imageFormat: AttachableImageFormat? = nil, sourceLocation: SourceLocation = #_sourceLocation ) where AttachableValue == _AttachableImageWrapper { - let attachment = Self(image, named: preferredName, as: contentType, encodingQuality: encodingQuality, sourceLocation: sourceLocation) + let attachment = Self(image, named: preferredName, as: imageFormat, sourceLocation: sourceLocation) Self.record(attachment, sourceLocation: sourceLocation) } } diff --git a/Sources/Overlays/_Testing_CoreGraphics/Attachments/_AttachableImageWrapper.swift b/Sources/Overlays/_Testing_CoreGraphics/Attachments/_AttachableImageWrapper.swift index f61b17e7d..f109e3409 100644 --- a/Sources/Overlays/_Testing_CoreGraphics/Attachments/_AttachableImageWrapper.swift +++ b/Sources/Overlays/_Testing_CoreGraphics/Attachments/_AttachableImageWrapper.swift @@ -9,7 +9,7 @@ // #if SWT_TARGET_OS_APPLE && canImport(CoreGraphics) -public import Testing +@_spi(Experimental) public import Testing private import CoreGraphics private import ImageIO @@ -60,49 +60,12 @@ public struct _AttachableImageWrapper: Sendable where Image: AttachableAs /// instances of this type it creates hold "safe" `NSImage` instances. nonisolated(unsafe) var image: Image - /// The encoding quality to use when encoding the represented image. - var encodingQuality: Float + /// The image format to use when encoding the represented image. + var imageFormat: AttachableImageFormat? - /// Storage for ``contentType``. - private var _contentType: UTType? - - /// The content type to use when encoding the image. - /// - /// The testing library uses this property to determine which image format to - /// encode the associated image as when it is attached to a test. - /// - /// If the value of this property does not conform to [`UTType.image`](https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/image), - /// the result is undefined. - var contentType: UTType { - get { - _contentType ?? .image - } - set { - precondition( - newValue.conforms(to: .image), - "An image cannot be attached as an instance of type '\(newValue.identifier)'. Use a type that conforms to 'public.image' instead." - ) - _contentType = newValue - } - } - - /// The content type to use when encoding the image, substituting a concrete - /// type for `UTType.image`. - /// - /// This property is not part of the public interface of the testing library. - var computedContentType: UTType { - if contentType == .image { - return encodingQuality < 1.0 ? .jpeg : .png - } - return contentType - } - - init(image: Image, encodingQuality: Float, contentType: UTType?) { + init(image: Image, imageFormat: AttachableImageFormat?) { self.image = image._makeCopyForAttachment() - self.encodingQuality = encodingQuality - if let contentType { - self.contentType = contentType - } + self.imageFormat = imageFormat } } @@ -121,8 +84,8 @@ extension _AttachableImageWrapper: AttachableWrapper { let attachableCGImage = try image.attachableCGImage // Create the image destination. - let typeIdentifier = computedContentType.identifier as CFString - guard let dest = CGImageDestinationCreateWithData(data as CFMutableData, typeIdentifier, 1, nil) else { + let contentType = AttachableImageFormat.computeContentType(for: imageFormat, withPreferredName: attachment.preferredName) + guard let dest = CGImageDestinationCreateWithData(data as CFMutableData, contentType.identifier as CFString, 1, nil) else { throw ImageAttachmentError.couldNotCreateImageDestination } @@ -130,7 +93,7 @@ extension _AttachableImageWrapper: AttachableWrapper { let orientation = image._attachmentOrientation let scaleFactor = image._attachmentScaleFactor let properties: [CFString: Any] = [ - kCGImageDestinationLossyCompressionQuality: CGFloat(encodingQuality), + kCGImageDestinationLossyCompressionQuality: CGFloat(imageFormat?.encodingQuality ?? 1.0), kCGImagePropertyOrientation: orientation, kCGImagePropertyDPIWidth: 72.0 * scaleFactor, kCGImagePropertyDPIHeight: 72.0 * scaleFactor, @@ -151,7 +114,8 @@ extension _AttachableImageWrapper: AttachableWrapper { } public borrowing func preferredName(for attachment: borrowing Attachment, basedOn suggestedName: String) -> String { - (suggestedName as NSString).appendingPathExtension(for: computedContentType) + let contentType = AttachableImageFormat.computeContentType(for: imageFormat, withPreferredName: suggestedName) + return (suggestedName as NSString).appendingPathExtension(for: contentType) } } #endif diff --git a/Sources/Testing/Attachments/AttachableImageFormat.swift b/Sources/Testing/Attachments/AttachableImageFormat.swift new file mode 100644 index 000000000..afb74c80e --- /dev/null +++ b/Sources/Testing/Attachments/AttachableImageFormat.swift @@ -0,0 +1,93 @@ +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024–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 +// + +/// A type describing image formats supported by the system that can be used +/// when attaching an image to a test. +/// +/// When you attach an image to a test, you can pass an instance of this type to +/// ``Attachment/record(_:named:as:sourceLocation:)`` so that the testing +/// library knows the image format you'd like to use. If you don't pass an +/// instance of this type, the testing library infers which format to use based +/// on the attachment's preferred name. +/// +/// The PNG and JPEG image formats are always supported. The set of additional +/// supported image formats is platform-specific: +/// +/// - On Apple platforms, you can use [`CGImageDestinationCopyTypeIdentifiers()`](https://developer.apple.com/documentation/imageio/cgimagedestinationcopytypeidentifiers()) +/// from the [Image I/O framework](https://developer.apple.com/documentation/imageio) +/// to determine which formats are supported. +@_spi(Experimental) +@available(_uttypesAPI, *) +public struct AttachableImageFormat: Sendable { + /// An enumeration describing the various kinds of image format that can be + /// used with an attachment. + package enum Kind: Sendable { + /// The (widely-supported) PNG image format. + case png + + /// The (widely-supported) JPEG image format. + case jpeg + + /// A platform-specific image format. + /// + /// - Parameters: + /// - value: A platform-specific value representing the image format to + /// use. The platform-specific cross-import overlay or package is + /// responsible for exposing appropriate interfaces for this case. + /// + /// On Apple platforms, `value` should be an instance of `UTType`. + case systemValue(_ value: any Sendable) + } + + /// The kind of image format represented by this instance. + package var kind: Kind + + /// The encoding quality to use for this image format. + /// + /// The meaning of the value is format-specific with `0.0` being the lowest + /// supported encoding quality and `1.0` being the highest supported encoding + /// quality. The value of this property is ignored for image formats that do + /// not support variable encoding quality. + public internal(set) var encodingQuality: Float = 1.0 + + package init(kind: Kind, encodingQuality: Float) { + self.kind = kind + self.encodingQuality = min(max(0.0, encodingQuality), 1.0) + } +} + +// MARK: - + +@available(_uttypesAPI, *) +extension AttachableImageFormat { + /// The PNG image format. + public static var png: Self { + Self(kind: .png, encodingQuality: 1.0) + } + + /// The JPEG image format with maximum encoding quality. + public static var jpeg: Self { + Self(kind: .jpeg, encodingQuality: 1.0) + } + + /// The JPEG image format. + /// + /// - Parameters: + /// - encodingQuality: The encoding quality to use when serializing an + /// image. A value of `0.0` indicates the lowest supported encoding + /// quality and a value of `1.0` indicates the highest supported encoding + /// quality. + /// + /// - Returns: An instance of this type representing the JPEG image format + /// with the specified encoding quality. + public static func jpeg(withEncodingQuality encodingQuality: Float) -> Self { + Self(kind: .jpeg, encodingQuality: encodingQuality) + } +} diff --git a/Tests/TestingTests/AttachmentTests.swift b/Tests/TestingTests/AttachmentTests.swift index 4b16d98ea..bb1573bb9 100644 --- a/Tests/TestingTests/AttachmentTests.swift +++ b/Tests/TestingTests/AttachmentTests.swift @@ -562,7 +562,8 @@ extension AttachmentTests { @Test(arguments: [Float(0.0).nextUp, 0.25, 0.5, 0.75, 1.0], [.png as UTType?, .jpeg, .gif, .image, nil]) func attachCGImage(quality: Float, type: UTType?) throws { let image = try Self.cgImage.get() - let attachment = Attachment(image, named: "diamond", as: type, encodingQuality: quality) + let format = type.map { AttachableImageFormat($0, encodingQuality: quality) } + let attachment = Attachment(image, named: "diamond", as: format) #expect(attachment.attachableValue === image) try attachment.attachableValue.withUnsafeBytes(for: attachment) { buffer in #expect(buffer.count > 32) @@ -572,11 +573,26 @@ extension AttachmentTests { } } + @available(_uttypesAPI, *) + @Test(arguments: [AttachableImageFormat.png, .jpeg, .jpeg(withEncodingQuality: 0.5), .init(.tiff)]) + func attachCGImage(format: AttachableImageFormat) throws { + let image = try Self.cgImage.get() + let attachment = Attachment(image, named: "diamond", as: format) + #expect(attachment.attachableValue === image) + try attachment.attachableValue.withUnsafeBytes(for: attachment) { buffer in + #expect(buffer.count > 32) + } + if let ext = format.contentType.preferredFilenameExtension { + #expect(attachment.preferredName == ("diamond" as NSString).appendingPathExtension(ext)) + } + } + #if !SWT_NO_EXIT_TESTS @available(_uttypesAPI, *) @Test func cannotAttachCGImageWithNonImageType() async { await #expect(processExitsWith: .failure) { - let attachment = Attachment(try Self.cgImage.get(), named: "diamond", as: .mp3) + let format = AttachableImageFormat(.mp3) + let attachment = Attachment(try Self.cgImage.get(), named: "diamond", as: format) try attachment.attachableValue.withUnsafeBytes(for: attachment) { _ in } } } From ee1736d01c1c4a647c2d02ee2b3b70f22f023511 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Tue, 8 Jul 2025 16:38:23 -0400 Subject: [PATCH 056/216] Fix typo in comment --- .../Attachments/AttachableImageFormat+UTType.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Overlays/_Testing_CoreGraphics/Attachments/AttachableImageFormat+UTType.swift b/Sources/Overlays/_Testing_CoreGraphics/Attachments/AttachableImageFormat+UTType.swift index 41f7e5998..575e357cd 100644 --- a/Sources/Overlays/_Testing_CoreGraphics/Attachments/AttachableImageFormat+UTType.swift +++ b/Sources/Overlays/_Testing_CoreGraphics/Attachments/AttachableImageFormat+UTType.swift @@ -87,7 +87,7 @@ extension AttachableImageFormat { /// If the target image format does not support variable-quality encoding, /// the value of the `encodingQuality` argument is ignored. /// - /// If `imageFormat` is not `nil` and does not conform to [`UTType.image`](https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/image), + /// If `contentType` does not conform to [`UTType.image`](https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/image), /// the result is undefined. public init(_ contentType: UTType, encodingQuality: Float = 1.0) { precondition( From 743104e0436e8cfe688242cbd5d0e6a3c159b03d Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Wed, 9 Jul 2025 13:04:54 -0400 Subject: [PATCH 057/216] Remove a number of checks for compilers < 6.1. (#1214) This PR removes some old/dead code checking for compilers < 6.1. We do not support building with such compilers anymore. It also adopts the move-only-type-compatible `ObjectIdentifier.init` overload added with the 6.2 toolchain. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- CONTRIBUTING.md | 9 ++++----- Sources/Testing/Parameterization/TypeInfo.swift | 6 ++---- Sources/Testing/Support/Locked+Platform.swift | 4 ++-- Tests/TestingTests/Traits/ConditionTraitTests.swift | 4 ---- 4 files changed, 8 insertions(+), 15 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c63bd0ce5..44c16d305 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -48,11 +48,10 @@ and install a toolchain. #### Installing a toolchain -1. Download a toolchain. A recent **6.0 development snapshot** toolchain is - required to build the testing library. Visit - [swift.org](http://swift.org/install) and download the most recent toolchain - from the section titled **release/6.0** under **Development Snapshots** on - the page for your platform. +1. Download a toolchain. A recent **development snapshot** toolchain is required + to build the testing library. Visit [swift.org](https://swift.org/install), + select your platform, and download the most recent toolchain from the section + titled **release/6.x** under **Development Snapshots**. Be aware that development snapshot toolchains aren't intended for day-to-day development and may contain defects that affect the programs built with them. diff --git a/Sources/Testing/Parameterization/TypeInfo.swift b/Sources/Testing/Parameterization/TypeInfo.swift index 300004e16..0bceff744 100644 --- a/Sources/Testing/Parameterization/TypeInfo.swift +++ b/Sources/Testing/Parameterization/TypeInfo.swift @@ -308,11 +308,7 @@ extension TypeInfo { } switch _kind { case let .type(type): -#if compiler(>=6.1) return _mangledTypeName(type) -#else - return _mangledTypeName(unsafeBitCast(type, to: Any.Type.self)) -#endif case let .nameOnly(_, _, mangledName): return mangledName } @@ -412,6 +408,7 @@ extension TypeInfo: Hashable { } } +#if compiler(<6.2) // MARK: - ObjectIdentifier support extension ObjectIdentifier { @@ -426,6 +423,7 @@ extension ObjectIdentifier { self.init(unsafeBitCast(type, to: Any.Type.self)) } } +#endif // MARK: - Codable diff --git a/Sources/Testing/Support/Locked+Platform.swift b/Sources/Testing/Support/Locked+Platform.swift index 951e62da8..9e6f9db26 100644 --- a/Sources/Testing/Support/Locked+Platform.swift +++ b/Sources/Testing/Support/Locked+Platform.swift @@ -41,7 +41,7 @@ extension os_unfair_lock_s: Lockable { typealias pthread_mutex_t = _TestingInternals.pthread_mutex_t? #endif -#if SWT_TARGET_OS_APPLE || os(Linux) || os(Android) || (os(WASI) && compiler(>=6.1) && _runtime(_multithreaded)) || os(FreeBSD) || os(OpenBSD) +#if SWT_TARGET_OS_APPLE || os(Linux) || os(Android) || (os(WASI) && _runtime(_multithreaded)) || os(FreeBSD) || os(OpenBSD) extension pthread_mutex_t: Lockable { static func initializeLock(at lock: UnsafeMutablePointer) { _ = pthread_mutex_init(lock, nil) @@ -83,7 +83,7 @@ extension SRWLOCK: Lockable { #if SWT_TARGET_OS_APPLE && !SWT_NO_OS_UNFAIR_LOCK typealias DefaultLock = os_unfair_lock -#elseif SWT_TARGET_OS_APPLE || os(Linux) || os(Android) || (os(WASI) && compiler(>=6.1) && _runtime(_multithreaded)) || os(FreeBSD) || os(OpenBSD) +#elseif SWT_TARGET_OS_APPLE || os(Linux) || os(Android) || (os(WASI) && _runtime(_multithreaded)) || os(FreeBSD) || os(OpenBSD) typealias DefaultLock = pthread_mutex_t #elseif os(Windows) typealias DefaultLock = SRWLOCK diff --git a/Tests/TestingTests/Traits/ConditionTraitTests.swift b/Tests/TestingTests/Traits/ConditionTraitTests.swift index d957e425b..9747f6b3f 100644 --- a/Tests/TestingTests/Traits/ConditionTraitTests.swift +++ b/Tests/TestingTests/Traits/ConditionTraitTests.swift @@ -12,14 +12,12 @@ @Suite("Condition Trait Tests", .tags(.traitRelated)) struct ConditionTraitTests { - #if compiler(>=6.1) @Test( ".enabled trait", .enabled { true }, .bug("https://github.com/swiftlang/swift/issues/76409", "Verify the custom trait with closure causes @Test macro to fail is fixed") ) func enabledTraitClosure() throws {} - #endif @Test( ".enabled if trait", @@ -27,14 +25,12 @@ struct ConditionTraitTests { ) func enabledTraitIf() throws {} - #if compiler(>=6.1) @Test( ".disabled trait", .disabled { false }, .bug("https://github.com/swiftlang/swift/issues/76409", "Verify the custom trait with closure causes @Test macro to fail is fixed") ) func disabledTraitClosure() throws {} - #endif @Test( ".disabled if trait", From e245c544f463f9f1c09f0d801c3c8153cfa76ebc Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Wed, 9 Jul 2025 13:31:26 -0400 Subject: [PATCH 058/216] Add a cross-import overlay with CoreImage to allow attaching `CIImage`s. (#1195) This PR adds on to the Core Graphics cross-import overlay added in #827 to allow attaching instances of `CIImage` to a test. > [!NOTE] > Image attachments remain an experimental feature. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- Package.swift | 11 +++++++ .../_Testing_AppKit/ReexportTesting.swift | 4 +-- .../Attachments/AttachableAsCGImage.swift | 1 + .../Attachment+AttachableAsCGImage.swift | 2 ++ .../Attachments/_AttachableImageWrapper.swift | 1 + .../CIImage+AttachableAsCGImage.swift | 33 +++++++++++++++++++ .../_Testing_CoreImage/ReexportTesting.swift | 12 +++++++ Tests/TestingTests/AttachmentTests.swift | 16 +++++++++ 8 files changed, 78 insertions(+), 2 deletions(-) create mode 100644 Sources/Overlays/_Testing_CoreImage/Attachments/CIImage+AttachableAsCGImage.swift create mode 100644 Sources/Overlays/_Testing_CoreImage/ReexportTesting.swift diff --git a/Package.swift b/Package.swift index dc481a163..a6f7c6f36 100644 --- a/Package.swift +++ b/Package.swift @@ -90,6 +90,7 @@ let package = Package( targets: [ "_Testing_AppKit", "_Testing_CoreGraphics", + "_Testing_CoreImage", ] ) ] @@ -137,6 +138,7 @@ let package = Package( "Testing", "_Testing_AppKit", "_Testing_CoreGraphics", + "_Testing_CoreImage", "_Testing_Foundation", "MemorySafeTestingTests", ], @@ -218,6 +220,15 @@ let package = Package( path: "Sources/Overlays/_Testing_CoreGraphics", swiftSettings: .packageSettings + .enableLibraryEvolution() ), + .target( + name: "_Testing_CoreImage", + dependencies: [ + "Testing", + "_Testing_CoreGraphics", + ], + path: "Sources/Overlays/_Testing_CoreImage", + swiftSettings: .packageSettings + .enableLibraryEvolution() + ), .target( name: "_Testing_Foundation", dependencies: [ diff --git a/Sources/Overlays/_Testing_AppKit/ReexportTesting.swift b/Sources/Overlays/_Testing_AppKit/ReexportTesting.swift index 3716f1f01..ce80a70d9 100644 --- a/Sources/Overlays/_Testing_AppKit/ReexportTesting.swift +++ b/Sources/Overlays/_Testing_AppKit/ReexportTesting.swift @@ -8,5 +8,5 @@ // See https://swift.org/CONTRIBUTORS.txt for Swift project authors // -@_exported public import Testing -@_exported public import _Testing_CoreGraphics +@_exported @_spi(Experimental) @_spi(ForToolsIntegrationOnly) public import Testing +@_exported @_spi(Experimental) @_spi(ForToolsIntegrationOnly) public import _Testing_CoreGraphics diff --git a/Sources/Overlays/_Testing_CoreGraphics/Attachments/AttachableAsCGImage.swift b/Sources/Overlays/_Testing_CoreGraphics/Attachments/AttachableAsCGImage.swift index b470534df..689d06b66 100644 --- a/Sources/Overlays/_Testing_CoreGraphics/Attachments/AttachableAsCGImage.swift +++ b/Sources/Overlays/_Testing_CoreGraphics/Attachments/AttachableAsCGImage.swift @@ -24,6 +24,7 @@ private import ImageIO /// be attached to a test: /// /// - [`CGImage`](https://developer.apple.com/documentation/coregraphics/cgimage) +/// - [`CIImage`](https://developer.apple.com/documentation/coreimage/ciimage) /// - [`NSImage`](https://developer.apple.com/documentation/appkit/nsimage) /// (macOS) /// diff --git a/Sources/Overlays/_Testing_CoreGraphics/Attachments/Attachment+AttachableAsCGImage.swift b/Sources/Overlays/_Testing_CoreGraphics/Attachments/Attachment+AttachableAsCGImage.swift index 7e6836ef5..3835ddaf0 100644 --- a/Sources/Overlays/_Testing_CoreGraphics/Attachments/Attachment+AttachableAsCGImage.swift +++ b/Sources/Overlays/_Testing_CoreGraphics/Attachments/Attachment+AttachableAsCGImage.swift @@ -31,6 +31,7 @@ extension Attachment { /// ``AttachableAsCGImage`` protocol and can be attached to a test: /// /// - [`CGImage`](https://developer.apple.com/documentation/coregraphics/cgimage) + /// - [`CIImage`](https://developer.apple.com/documentation/coreimage/ciimage) /// - [`NSImage`](https://developer.apple.com/documentation/appkit/nsimage) /// (macOS) /// @@ -68,6 +69,7 @@ extension Attachment { /// ``AttachableAsCGImage`` protocol and can be attached to a test: /// /// - [`CGImage`](https://developer.apple.com/documentation/coregraphics/cgimage) + /// - [`CIImage`](https://developer.apple.com/documentation/coreimage/ciimage) /// - [`NSImage`](https://developer.apple.com/documentation/appkit/nsimage) /// (macOS) /// diff --git a/Sources/Overlays/_Testing_CoreGraphics/Attachments/_AttachableImageWrapper.swift b/Sources/Overlays/_Testing_CoreGraphics/Attachments/_AttachableImageWrapper.swift index f109e3409..6bb1e0d75 100644 --- a/Sources/Overlays/_Testing_CoreGraphics/Attachments/_AttachableImageWrapper.swift +++ b/Sources/Overlays/_Testing_CoreGraphics/Attachments/_AttachableImageWrapper.swift @@ -47,6 +47,7 @@ import UniformTypeIdentifiers /// to the ``AttachableAsCGImage`` protocol and can be attached to a test: /// /// - [`CGImage`](https://developer.apple.com/documentation/coregraphics/cgimage) +/// - [`CIImage`](https://developer.apple.com/documentation/coreimage/ciimage) /// - [`NSImage`](https://developer.apple.com/documentation/appkit/nsimage) /// (macOS) @_spi(Experimental) diff --git a/Sources/Overlays/_Testing_CoreImage/Attachments/CIImage+AttachableAsCGImage.swift b/Sources/Overlays/_Testing_CoreImage/Attachments/CIImage+AttachableAsCGImage.swift new file mode 100644 index 000000000..9a8278a83 --- /dev/null +++ b/Sources/Overlays/_Testing_CoreImage/Attachments/CIImage+AttachableAsCGImage.swift @@ -0,0 +1,33 @@ +// +// 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 +// + +#if SWT_TARGET_OS_APPLE && canImport(CoreImage) +public import CoreImage +@_spi(Experimental) public import _Testing_CoreGraphics + +@_spi(Experimental) +extension CIImage: AttachableAsCGImage { + public var attachableCGImage: CGImage { + get throws { + guard let result = CIContext().createCGImage(self, from: extent) else { + throw ImageAttachmentError.couldNotCreateCGImage + } + return result + } + } + + public func _makeCopyForAttachment() -> Self { + // CIImage is documented as thread-safe, but does not conform to Sendable. + // It conforms to NSCopying but does not actually copy itself, so there's no + // point in calling copy(). + self + } +} +#endif diff --git a/Sources/Overlays/_Testing_CoreImage/ReexportTesting.swift b/Sources/Overlays/_Testing_CoreImage/ReexportTesting.swift new file mode 100644 index 000000000..ce80a70d9 --- /dev/null +++ b/Sources/Overlays/_Testing_CoreImage/ReexportTesting.swift @@ -0,0 +1,12 @@ +// +// 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 +// + +@_exported @_spi(Experimental) @_spi(ForToolsIntegrationOnly) public import Testing +@_exported @_spi(Experimental) @_spi(ForToolsIntegrationOnly) public import _Testing_CoreGraphics diff --git a/Tests/TestingTests/AttachmentTests.swift b/Tests/TestingTests/AttachmentTests.swift index bb1573bb9..0f3f8a2ed 100644 --- a/Tests/TestingTests/AttachmentTests.swift +++ b/Tests/TestingTests/AttachmentTests.swift @@ -22,6 +22,10 @@ import _Testing_Foundation import CoreGraphics @_spi(Experimental) import _Testing_CoreGraphics #endif +#if canImport(CoreImage) +import CoreImage +@_spi(Experimental) import _Testing_CoreImage +#endif #if canImport(UniformTypeIdentifiers) import UniformTypeIdentifiers #endif @@ -598,6 +602,18 @@ extension AttachmentTests { } #endif +#if canImport(CoreImage) + @available(_uttypesAPI, *) + @Test func attachCIImage() throws { + let image = CIImage(cgImage: try Self.cgImage.get()) + let attachment = Attachment(image, named: "diamond.jpg") + #expect(attachment.attachableValue === image) + try attachment.attachableValue.withUnsafeBytes(for: attachment) { buffer in + #expect(buffer.count > 32) + } + } +#endif + #if canImport(AppKit) static var nsImage: NSImage { get throws { From 43fb3fb32bd71966ef3fa431810bf57e79750f67 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Wed, 9 Jul 2025 15:45:28 -0400 Subject: [PATCH 059/216] Add a cross-import overlay with UIKit to allow attaching `UIImage`s. (#1216) This PR adds on to the Core Graphics cross-import overlay added in #827 to allow attaching instances of `UIImage` to a test. > [!NOTE] > Image attachments remain an experimental feature. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- Package.swift | 12 ++++ .../UIImage+AttachableAsCGImage.swift | 67 +++++++++++++++++++ .../_Testing_UIKit/ReexportTesting.swift | 12 ++++ Tests/TestingTests/AttachmentTests.swift | 16 +++++ .../MemorySafeTestDecls.swift | 2 + 5 files changed, 109 insertions(+) create mode 100644 Sources/Overlays/_Testing_UIKit/Attachments/UIImage+AttachableAsCGImage.swift create mode 100644 Sources/Overlays/_Testing_UIKit/ReexportTesting.swift diff --git a/Package.swift b/Package.swift index a6f7c6f36..3405be19d 100644 --- a/Package.swift +++ b/Package.swift @@ -91,6 +91,7 @@ let package = Package( "_Testing_AppKit", "_Testing_CoreGraphics", "_Testing_CoreImage", + "_Testing_UIKit", ] ) ] @@ -140,6 +141,7 @@ let package = Package( "_Testing_CoreGraphics", "_Testing_CoreImage", "_Testing_Foundation", + "_Testing_UIKit", "MemorySafeTestingTests", ], swiftSettings: .packageSettings @@ -241,6 +243,16 @@ let package = Package( // it can only enable Library Evolution itself on those platforms. swiftSettings: .packageSettings + .enableLibraryEvolution(.whenApple()) ), + .target( + name: "_Testing_UIKit", + dependencies: [ + "Testing", + "_Testing_CoreGraphics", + "_Testing_CoreImage", + ], + path: "Sources/Overlays/_Testing_UIKit", + swiftSettings: .packageSettings + .enableLibraryEvolution() + ), // Utility targets: These are utilities intended for use when developing // this package, not for distribution. diff --git a/Sources/Overlays/_Testing_UIKit/Attachments/UIImage+AttachableAsCGImage.swift b/Sources/Overlays/_Testing_UIKit/Attachments/UIImage+AttachableAsCGImage.swift new file mode 100644 index 000000000..233f737a4 --- /dev/null +++ b/Sources/Overlays/_Testing_UIKit/Attachments/UIImage+AttachableAsCGImage.swift @@ -0,0 +1,67 @@ +// +// 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 +// + +#if SWT_TARGET_OS_APPLE && canImport(UIKit) +public import UIKit +@_spi(Experimental) public import _Testing_CoreGraphics +@_spi(Experimental) private import _Testing_CoreImage + +private import ImageIO +#if canImport(UIKitCore_Private) +private import UIKitCore_Private +#endif + +@_spi(Experimental) +extension UIImage: AttachableAsCGImage { + public var attachableCGImage: CGImage { + get throws { +#if canImport(UIKitCore_Private) + // _UIImageGetCGImageRepresentation() is an internal UIKit function that + // flattens any (most) UIImage instances to a CGImage. BUG: rdar://155449485 + if let cgImage = _UIImageGetCGImageRepresentation(self)?.takeUnretainedValue() { + return cgImage + } +#else + // NOTE: This API is marked to-be-deprecated so we'll need to eventually + // switch to UIGraphicsImageRenderer, but that type is not available on + // watchOS. BUG: rdar://155452406 + UIGraphicsBeginImageContextWithOptions(size, true, scale) + defer { + UIGraphicsEndImageContext() + } + draw(at: .zero) + if let cgImage = UIGraphicsGetImageFromCurrentImageContext()?.cgImage { + return cgImage + } +#endif + throw ImageAttachmentError.couldNotCreateCGImage + } + } + + public var _attachmentOrientation: UInt32 { + let result: CGImagePropertyOrientation = switch imageOrientation { + case .up: .up + case .down: .down + case .left: .left + case .right: .right + case .upMirrored: .upMirrored + case .downMirrored: .downMirrored + case .leftMirrored: .leftMirrored + case .rightMirrored: .rightMirrored + @unknown default: .up + } + return result.rawValue + } + + public var _attachmentScaleFactor: CGFloat { + scale + } +} +#endif diff --git a/Sources/Overlays/_Testing_UIKit/ReexportTesting.swift b/Sources/Overlays/_Testing_UIKit/ReexportTesting.swift new file mode 100644 index 000000000..ce80a70d9 --- /dev/null +++ b/Sources/Overlays/_Testing_UIKit/ReexportTesting.swift @@ -0,0 +1,12 @@ +// +// 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 +// + +@_exported @_spi(Experimental) @_spi(ForToolsIntegrationOnly) public import Testing +@_exported @_spi(Experimental) @_spi(ForToolsIntegrationOnly) public import _Testing_CoreGraphics diff --git a/Tests/TestingTests/AttachmentTests.swift b/Tests/TestingTests/AttachmentTests.swift index 0f3f8a2ed..6cc30e608 100644 --- a/Tests/TestingTests/AttachmentTests.swift +++ b/Tests/TestingTests/AttachmentTests.swift @@ -26,6 +26,10 @@ import CoreGraphics import CoreImage @_spi(Experimental) import _Testing_CoreImage #endif +#if canImport(UIKit) +import UIKit +@_spi(Experimental) import _Testing_UIKit +#endif #if canImport(UniformTypeIdentifiers) import UniformTypeIdentifiers #endif @@ -678,6 +682,18 @@ extension AttachmentTests { } } #endif + +#if canImport(UIKit) + @available(_uttypesAPI, *) + @Test func attachUIImage() throws { + let image = UIImage(cgImage: try Self.cgImage.get()) + let attachment = Attachment(image, named: "diamond.jpg") + #expect(attachment.attachableValue === image) + try attachment.attachableValue.withUnsafeBytes(for: attachment) { buffer in + #expect(buffer.count > 32) + } + } +#endif #endif } } diff --git a/Tests/_MemorySafeTestingTests/MemorySafeTestDecls.swift b/Tests/_MemorySafeTestingTests/MemorySafeTestDecls.swift index baf02c026..3eb19be5b 100644 --- a/Tests/_MemorySafeTestingTests/MemorySafeTestDecls.swift +++ b/Tests/_MemorySafeTestingTests/MemorySafeTestDecls.swift @@ -24,8 +24,10 @@ struct ExampleSuite { @Test func example() {} } +#if !SWT_NO_EXIT_TESTS func exampleExitTest() async { await #expect(processExitsWith: .success) {} } +#endif #endif From c3da189ac642516f80950950e56c9b817c6bf797 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Thu, 10 Jul 2025 14:20:06 -0400 Subject: [PATCH 060/216] Simplify `NSImageRep`-checking logic to use `Bundle`. (#1217) This PR simplifies the logic we use to check if subclasses of `NSImageRep` are likely to be thread-safe by using Foundation's `Bundle` instead of querying dyld. AppKit always links Foundation, so there's no need for us to roll our own logic for this check. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- .../NSImage+AttachableAsCGImage.swift | 57 +++++++------------ 1 file changed, 22 insertions(+), 35 deletions(-) diff --git a/Sources/Overlays/_Testing_AppKit/Attachments/NSImage+AttachableAsCGImage.swift b/Sources/Overlays/_Testing_AppKit/Attachments/NSImage+AttachableAsCGImage.swift index 7e5c9363d..d0830accf 100644 --- a/Sources/Overlays/_Testing_AppKit/Attachments/NSImage+AttachableAsCGImage.swift +++ b/Sources/Overlays/_Testing_AppKit/Attachments/NSImage+AttachableAsCGImage.swift @@ -12,6 +12,27 @@ public import AppKit @_spi(Experimental) public import _Testing_CoreGraphics +extension NSImageRep { + /// AppKit's bundle. + private static let _appKitBundle: Bundle = Bundle(for: NSImageRep.self) + + /// Whether or not this image rep's class is effectively thread-safe and can + /// be treated as if it conforms to `Sendable`. + fileprivate var isEffectivelySendable: Bool { + if isMember(of: NSImageRep.self) || isKind(of: NSCustomImageRep.self) { + // NSImageRep itself is an abstract class. NSCustomImageRep includes an + // arbitrary rendering block that may not be concurrency-safe in Swift. + return false + } + + // Treat all other classes declared in AppKit as safe. We can't reason about + // classes declared in other bundles, so treat them all as if they're unsafe. + return Bundle(for: Self.self) == Self._appKitBundle + } +} + +// MARK: - + @_spi(Experimental) extension NSImage: AttachableAsCGImage { public var attachableCGImage: CGImage { @@ -32,29 +53,6 @@ extension NSImage: AttachableAsCGImage { return maxRepWidth ?? 1.0 } - /// Get the base address of the loaded image containing `class`. - /// - /// - Parameters: - /// - class: The class to look for. - /// - /// - Returns: The base address of the image containing `class`, or `nil` if - /// no image was found (for instance, if the class is generic or dynamically - /// generated.) - /// - /// "Image" in this context refers to a binary/executable image. - private static func _baseAddressOfImage(containing `class`: AnyClass) -> UnsafeRawPointer? { - let classAsAddress = Unmanaged.passUnretained(`class` as AnyObject).toOpaque() - - var info = Dl_info() - guard 0 != dladdr(classAsAddress, &info) else { - return nil - } - return .init(info.dli_fbase) - } - - /// The base address of the image containing AppKit's symbols, if known. - private static nonisolated(unsafe) let _appKitBaseAddress = _baseAddressOfImage(containing: NSImageRep.self) - public func _makeCopyForAttachment() -> Self { // If this image is of an NSImage subclass, we cannot reliably make a deep // copy of it because we don't know what its `init(data:)` implementation @@ -69,18 +67,7 @@ extension NSImage: AttachableAsCGImage { // Check whether the image contains any representations that we don't think // are safe. If it does, then make a "safe" copy. - let allImageRepsAreSafe = representations.allSatisfy { imageRep in - // NSCustomImageRep includes an arbitrary rendering block that may not be - // concurrency-safe in Swift. - if imageRep is NSCustomImageRep { - return false - } - - // Treat all other classes declared in AppKit as safe. We can't reason - // about classes declared in other modules, so treat them all as if they - // are unsafe. - return Self._baseAddressOfImage(containing: type(of: imageRep)) == Self._appKitBaseAddress - } + let allImageRepsAreSafe = representations.allSatisfy(\.isEffectivelySendable) if !allImageRepsAreSafe, let safeCopy = tiffRepresentation.flatMap(Self.init(data:)) { // Create a "safe" copy of this image by flattening it to TIFF and then // creating a new NSImage instance from it. From f9fb1ec6272fb79543e25d6eb0ff64bad956c0fe Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Thu, 10 Jul 2025 14:20:22 -0400 Subject: [PATCH 061/216] Use a consistent definition for unreachable code in macro expansions. (#1166) This PR defines a single `ExprSyntax` instance we can use for unreachable code paths in macro expansions (where the compiler/swift-syntax requires us to produce an expression but we know we're going to emit an error anyway.) ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- Sources/Testing/ExitTests/ExitTest.swift | 2 +- Sources/TestingMacros/ConditionMacro.swift | 4 ++-- .../TestingMacros/ExitTestCapturedValueMacro.swift | 2 +- .../Additions/FunctionDeclSyntaxAdditions.swift | 12 ++++++++++++ .../Support/ClosureCaptureListParsing.swift | 2 +- Sources/TestingMacros/TagMacro.swift | 2 +- 6 files changed, 18 insertions(+), 6 deletions(-) diff --git a/Sources/Testing/ExitTests/ExitTest.swift b/Sources/Testing/ExitTests/ExitTest.swift index aa7bf39ab..57e38e3ec 100644 --- a/Sources/Testing/ExitTests/ExitTest.swift +++ b/Sources/Testing/ExitTests/ExitTest.swift @@ -399,7 +399,7 @@ extension ExitTest { asTypeAt typeAddress: UnsafeRawPointer, withHintAt hintAddress: UnsafeRawPointer? = nil ) -> CBool { - fatalError("Unimplemented") + swt_unreachable() } } diff --git a/Sources/TestingMacros/ConditionMacro.swift b/Sources/TestingMacros/ConditionMacro.swift index 9d0c4f15f..c176c96b5 100644 --- a/Sources/TestingMacros/ConditionMacro.swift +++ b/Sources/TestingMacros/ConditionMacro.swift @@ -454,9 +454,9 @@ extension ExitTestConditionMacro { // early if found. guard _diagnoseIssues(with: macro, body: bodyArgumentExpr, in: context) else { if Self.isThrowing { - return #"{ () async throws -> Testing.ExitTest.Result in Swift.fatalError("Unreachable") }()"# + return #"{ () async throws -> Testing.ExitTest.Result in \#(ExprSyntax.unreachable) }()"# } else { - return #"{ () async -> Testing.ExitTest.Result in Swift.fatalError("Unreachable") }()"# + return #"{ () async -> Testing.ExitTest.Result in \#(ExprSyntax.unreachable) }()"# } } diff --git a/Sources/TestingMacros/ExitTestCapturedValueMacro.swift b/Sources/TestingMacros/ExitTestCapturedValueMacro.swift index 4bba47c79..bd346bb3b 100644 --- a/Sources/TestingMacros/ExitTestCapturedValueMacro.swift +++ b/Sources/TestingMacros/ExitTestCapturedValueMacro.swift @@ -50,7 +50,7 @@ public struct ExitTestBadCapturedValueMacro: ExpressionMacro, Sendable { // Diagnose that the type of 'expr' is invalid. context.diagnose(.capturedValueMustBeSendableAndCodable(expr, name: nameExpr)) - return #"Swift.fatalError("Unsupported")"# + return .unreachable } } diff --git a/Sources/TestingMacros/Support/Additions/FunctionDeclSyntaxAdditions.swift b/Sources/TestingMacros/Support/Additions/FunctionDeclSyntaxAdditions.swift index 9b55bc157..fa390775a 100644 --- a/Sources/TestingMacros/Support/Additions/FunctionDeclSyntaxAdditions.swift +++ b/Sources/TestingMacros/Support/Additions/FunctionDeclSyntaxAdditions.swift @@ -188,3 +188,15 @@ extension FunctionParameterSyntax { return baseType.trimmedDescription } } + +// MARK: - + +extension ExprSyntax { + /// An expression representing an unreachable code path. + /// + /// Use this expression when a macro will emit an error diagnostic but the + /// compiler still requires us to produce a valid expression. + static var unreachable: Self { + #"Swift.fatalError("Unreachable")"# + } +} diff --git a/Sources/TestingMacros/Support/ClosureCaptureListParsing.swift b/Sources/TestingMacros/Support/ClosureCaptureListParsing.swift index 08536aa69..c4266c99f 100644 --- a/Sources/TestingMacros/Support/ClosureCaptureListParsing.swift +++ b/Sources/TestingMacros/Support/ClosureCaptureListParsing.swift @@ -41,7 +41,7 @@ struct CapturedValueInfo { init(_ capture: ClosureCaptureSyntax, in context: some MacroExpansionContext) { self.capture = capture - self.expression = #"Swift.fatalError("Unsupported")"# + self.expression = .unreachable self.type = "Swift.Never" // We don't support capture specifiers at this time. diff --git a/Sources/TestingMacros/TagMacro.swift b/Sources/TestingMacros/TagMacro.swift index 624f812cd..01932ef5d 100644 --- a/Sources/TestingMacros/TagMacro.swift +++ b/Sources/TestingMacros/TagMacro.swift @@ -22,7 +22,7 @@ public struct TagMacro: PeerMacro, AccessorMacro, Sendable { /// This property is used rather than simply returning the empty array in /// order to suppress a compiler diagnostic about not producing any accessors. private static var _fallbackAccessorDecls: [AccessorDeclSyntax] { - [#"get { Swift.fatalError("Unreachable") }"#] + [#"get { \#(ExprSyntax.unreachable) }"#] } public static func expansion( From 0c20a7210394180309774d3ef1a2fb7e2590aeb6 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Thu, 10 Jul 2025 16:20:50 -0400 Subject: [PATCH 062/216] Make `ExitTest._current` immutable. (#1218) This PR makes the private `ExitTest._current` stored property a `let` instead of a `var`. This has no practical effect but is more correct. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- Sources/Testing/ExitTests/ExitTest.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Testing/ExitTests/ExitTest.swift b/Sources/Testing/ExitTests/ExitTest.swift index 57e38e3ec..d69ced10e 100644 --- a/Sources/Testing/ExitTests/ExitTest.swift +++ b/Sources/Testing/ExitTests/ExitTest.swift @@ -153,7 +153,7 @@ extension ExitTest { /// /// A pointer is used for indirection because `ManagedBuffer` cannot yet hold /// move-only types. - private static nonisolated(unsafe) var _current: Locked> = { + private static nonisolated(unsafe) let _current: Locked> = { let current = UnsafeMutablePointer.allocate(capacity: 1) current.initialize(to: nil) return Locked(rawValue: current) From d2a238d4659792e3ce969b1466d05897e5f219ae Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Thu, 10 Jul 2025 17:52:00 -0400 Subject: [PATCH 063/216] Emit "barriers" into the stdout/stderr streams of an exit test. (#1049) This PR causes Swift Testing to write "barriers" (known sequences of bytes) to `stdout` and `stderr` in the child process created by an exit test. Then, in the parent, these values are used to splice off any leading or trailing output that wasn't generated by the exit test's body (such as content generated by the host process, XCTest/Xcode, etc.) This reduces the amount of extraneous data reported back to the exit test's parent process. Thanks to @briancroom for the suggestion. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- Sources/Testing/ExitTests/ExitTest.swift | 78 +++++++++++++++++++++++- Tests/TestingTests/ExitTestTests.swift | 2 + 2 files changed, 78 insertions(+), 2 deletions(-) diff --git a/Sources/Testing/ExitTests/ExitTest.swift b/Sources/Testing/ExitTests/ExitTest.swift index d69ced10e..9b2c1a77c 100644 --- a/Sources/Testing/ExitTests/ExitTest.swift +++ b/Sources/Testing/ExitTests/ExitTest.swift @@ -545,6 +545,73 @@ extension ABI { @_spi(ForToolsIntegrationOnly) extension ExitTest { + /// A barrier value to insert into the standard output and standard error + /// streams immediately before and after the body of an exit test runs in + /// order to distinguish output produced by the host process. + /// + /// The value of this property was randomly generated. It could conceivably + /// show up in actual output from an exit test, but the statistical likelihood + /// of that happening is negligible. + static var barrierValue: [UInt8] { + [ + 0x39, 0x74, 0x87, 0x6d, 0x96, 0xdd, 0xf6, 0x17, + 0x7f, 0x05, 0x61, 0x5d, 0x46, 0xeb, 0x37, 0x0c, + 0x90, 0x07, 0xca, 0xe5, 0xed, 0x0b, 0xc4, 0xc4, + 0x46, 0x36, 0xc5, 0xb8, 0x9c, 0xc7, 0x86, 0x57, + ] + } + + /// Remove the leading and trailing barrier values from the given array of + /// bytes along. + /// + /// - Parameters: + /// - buffer: The buffer to trim. + /// + /// - Returns: A copy of `buffer`. If a barrier value (equal to + /// ``barrierValue``) is present in `buffer`, it and everything before it + /// are trimmed from the beginning of the copy. If there is more than one + /// barrier value present, the last one and everything after it are trimmed + /// from the end of the copy. If no barrier value is present, `buffer` is + /// returned verbatim. + private static func _trimToBarrierValues(_ buffer: [UInt8]) -> [UInt8] { + let barrierValue = barrierValue + let firstBarrierByte = barrierValue[0] + + // If the buffer is too small to contain the barrier value, exit early. + guard buffer.count > barrierValue.count else { + return buffer + } + + // Find all the indices where the first byte of the barrier is present. + let splits = buffer.indices.filter { buffer[$0] == firstBarrierByte } + + // Trim off the leading barrier value. If we didn't find any barrier values, + // we do nothing. + let leadingIndex = splits.first { buffer[$0...].starts(with: barrierValue) } + guard let leadingIndex else { + return buffer + } + var trimmedBuffer = buffer[leadingIndex...].dropFirst(barrierValue.count) + + // If there's a trailing barrier value, trim it too. If it's at the same + // index as the leading barrier value, that means only one barrier value + // was present and we should assume it's the leading one. + let trailingIndex = splits.last { buffer[$0...].starts(with: barrierValue) } + if let trailingIndex, trailingIndex > leadingIndex { + trimmedBuffer = trimmedBuffer[.. Date: Mon, 14 Jul 2025 13:35:31 -0400 Subject: [PATCH 064/216] Add missing links to `UIImage` documentation. (#1223) Add missing links to `UIImage` documentation in the comment blobs that cover `CGImage`, `NSImage`, etc. This got omitted during a merge from main. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- .../Attachments/AttachableAsCGImage.swift | 2 ++ .../Attachments/Attachment+AttachableAsCGImage.swift | 4 ++++ .../Attachments/_AttachableImageWrapper.swift | 2 ++ 3 files changed, 8 insertions(+) diff --git a/Sources/Overlays/_Testing_CoreGraphics/Attachments/AttachableAsCGImage.swift b/Sources/Overlays/_Testing_CoreGraphics/Attachments/AttachableAsCGImage.swift index 689d06b66..5f5c7b2d7 100644 --- a/Sources/Overlays/_Testing_CoreGraphics/Attachments/AttachableAsCGImage.swift +++ b/Sources/Overlays/_Testing_CoreGraphics/Attachments/AttachableAsCGImage.swift @@ -27,6 +27,8 @@ private import ImageIO /// - [`CIImage`](https://developer.apple.com/documentation/coreimage/ciimage) /// - [`NSImage`](https://developer.apple.com/documentation/appkit/nsimage) /// (macOS) +/// - [`UIImage`](https://developer.apple.com/documentation/uikit/uiimage) +/// (iOS, watchOS, tvOS, visionOS, and Mac Catalyst) /// /// You do not generally need to add your own conformances to this protocol. If /// you have an image in another format that needs to be attached to a test, diff --git a/Sources/Overlays/_Testing_CoreGraphics/Attachments/Attachment+AttachableAsCGImage.swift b/Sources/Overlays/_Testing_CoreGraphics/Attachments/Attachment+AttachableAsCGImage.swift index 3835ddaf0..10c866e3a 100644 --- a/Sources/Overlays/_Testing_CoreGraphics/Attachments/Attachment+AttachableAsCGImage.swift +++ b/Sources/Overlays/_Testing_CoreGraphics/Attachments/Attachment+AttachableAsCGImage.swift @@ -34,6 +34,8 @@ extension Attachment { /// - [`CIImage`](https://developer.apple.com/documentation/coreimage/ciimage) /// - [`NSImage`](https://developer.apple.com/documentation/appkit/nsimage) /// (macOS) + /// - [`UIImage`](https://developer.apple.com/documentation/uikit/uiimage) + /// (iOS, watchOS, tvOS, visionOS, and Mac Catalyst) /// /// The testing library uses the image format specified by `imageFormat`. Pass /// `nil` to let the testing library decide which image format to use. If you @@ -72,6 +74,8 @@ extension Attachment { /// - [`CIImage`](https://developer.apple.com/documentation/coreimage/ciimage) /// - [`NSImage`](https://developer.apple.com/documentation/appkit/nsimage) /// (macOS) + /// - [`UIImage`](https://developer.apple.com/documentation/uikit/uiimage) + /// (iOS, watchOS, tvOS, visionOS, and Mac Catalyst) /// /// The testing library uses the image format specified by `imageFormat`. Pass /// `nil` to let the testing library decide which image format to use. If you diff --git a/Sources/Overlays/_Testing_CoreGraphics/Attachments/_AttachableImageWrapper.swift b/Sources/Overlays/_Testing_CoreGraphics/Attachments/_AttachableImageWrapper.swift index 6bb1e0d75..f17990455 100644 --- a/Sources/Overlays/_Testing_CoreGraphics/Attachments/_AttachableImageWrapper.swift +++ b/Sources/Overlays/_Testing_CoreGraphics/Attachments/_AttachableImageWrapper.swift @@ -50,6 +50,8 @@ import UniformTypeIdentifiers /// - [`CIImage`](https://developer.apple.com/documentation/coreimage/ciimage) /// - [`NSImage`](https://developer.apple.com/documentation/appkit/nsimage) /// (macOS) +/// - [`UIImage`](https://developer.apple.com/documentation/uikit/uiimage) +/// (iOS, watchOS, tvOS, visionOS, and Mac Catalyst) @_spi(Experimental) @available(_uttypesAPI, *) public struct _AttachableImageWrapper: Sendable where Image: AttachableAsCGImage { From cfa0c093552901ba001aaba18546289891734c26 Mon Sep 17 00:00:00 2001 From: Graham Lee Date: Wed, 16 Jul 2025 11:41:59 +0100 Subject: [PATCH 065/216] Clarify the outcome of applying a timeLimit trait to a suite. (#1225) Make it clearer that the time limit applies to each test or case in the suite, not to the overall runtime of the test suite. ### Motivation: I received feedback that a developer couldn't work out whether `.timeLimit(_:)` on a suite limits each test, or the whole suite. ### Modifications: - Add documentation that explains that the time limit applies to each test or case in a parameterized test, when you apply it to a suite. - Remove some passive-voice sentences in the same documents so that it's clearer what a developer does, and what the testing library does for them. ### Checklist: - [X] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [X] If public symbols are renamed or modified, DocC references should be updated. --- .../Testing.docc/LimitingExecutionTime.md | 25 ++++++++++++------- Sources/Testing/Traits/TimeLimitTrait.swift | 14 +++++++++++ 2 files changed, 30 insertions(+), 9 deletions(-) diff --git a/Sources/Testing/Testing.docc/LimitingExecutionTime.md b/Sources/Testing/Testing.docc/LimitingExecutionTime.md index 151b52028..9ea346230 100644 --- a/Sources/Testing/Testing.docc/LimitingExecutionTime.md +++ b/Sources/Testing/Testing.docc/LimitingExecutionTime.md @@ -40,8 +40,8 @@ hour (60 x 60 seconds) to execute, the task in which it's running is and the test fails with an issue of kind ``Issue/Kind-swift.enum/timeLimitExceeded(timeLimitComponents:)``. -- Note: If multiple time limit traits apply to a test, the shortest time limit - is used. +- Note: If multiple time limit traits apply to a test, the testing library uses + the shortest time limit. The testing library may adjust the specified time limit for performance reasons or to ensure tests have enough time to run. In particular, a granularity of (by @@ -49,13 +49,20 @@ default) one minute is applied to tests. The testing library can also be configured with a maximum time limit per test that overrides any applied time limit traits. -### Time limits applied to test suites +### Apply time limits to test suites -When a time limit is applied to a test suite, it's recursively applied to all -test functions and child test suites within that suite. +When you apply a time limit to a test suite, the testing library recursively +applies it to all test functions and child test suites within that suite. +The time limit applies to each test in the test suite and any child test suites, +or each test case for parameterized tests. -### Time limits applied to parameterized tests +For example, if a suite contains five tests and you apply a time limit trait +with a duration of one minute, then each test in the suite may run for up to +one minute. -When a time limit is applied to a parameterized test function, it's applied to -each invocation _separately_ so that if only some arguments cause failures, then -successful arguments aren't incorrectly marked as failing too. +### Apply time limits to parameterized tests + +When you apply a time limit to a parameterized test function, the testing +library applies it to each invocation _separately_ so that if only some +cases cause failures due to timeouts, then the testing library doesn't +incorrectly mark successful cases as failing. diff --git a/Sources/Testing/Traits/TimeLimitTrait.swift b/Sources/Testing/Traits/TimeLimitTrait.swift index 7b21209b6..e9216b242 100644 --- a/Sources/Testing/Traits/TimeLimitTrait.swift +++ b/Sources/Testing/Traits/TimeLimitTrait.swift @@ -83,6 +83,13 @@ extension Trait where Self == TimeLimitTrait { /// test cases individually. If a test has more than one time limit associated /// with it, the shortest one is used. A test run may also be configured with /// a maximum time limit per test case. + /// + /// If you apply this trait to a test suite, then it sets the time limit for + /// each test in the suite, or each test case in parameterized tests in the + /// suite. + /// For example, if a suite contains five tests and you apply a time limit trait + /// with a duration of one minute, then each test in the suite may run for up to + /// one minute. @_spi(Experimental) public static func timeLimit(_ timeLimit: Duration) -> Self { return Self(timeLimit: timeLimit) @@ -116,6 +123,13 @@ extension Trait where Self == TimeLimitTrait { /// If a test is parameterized, this time limit is applied to each of its /// test cases individually. If a test has more than one time limit associated /// with it, the testing library uses the shortest time limit. + /// + /// If you apply this trait to a test suite, then it sets the time limit for + /// each test in the suite, or each test case in parameterized tests in the + /// suite. + /// For example, if a suite contains five tests and you apply a time limit trait + /// with a duration of one minute, then each test in the suite may run for up to + /// one minute. public static func timeLimit(_ timeLimit: Self.Duration) -> Self { return Self(timeLimit: timeLimit.underlyingDuration) } From 3e955b7e9151205b8e27bebc1be1e1d1985d5548 Mon Sep 17 00:00:00 2001 From: Stuart Montgomery Date: Wed, 16 Jul 2025 17:03:02 -0500 Subject: [PATCH 066/216] Only clear exit test ID env var upon successful lookup (#1226) This changes the logic in `ExitTest.findInEnvironmentForEntryPoint()` to only clear the environment variable containing the ID of the exit test to run if an exit test is successfully located. ### Motivation: This ensures that if a tool integrates with the testing library and has both its own built-in copy of the library but also may call into the ABI entry point, both usage scenarios can successfully look up the exit test. Without this fix, if an earlier attempt to look up an exit test fails, the environment variable would be cleared which prevents a subsequent lookup attempt from succeeding. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- Sources/Testing/ExitTests/ExitTest.swift | 35 +++++++++++++++--------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/Sources/Testing/ExitTests/ExitTest.swift b/Sources/Testing/ExitTests/ExitTest.swift index 9b2c1a77c..374bd9e3c 100644 --- a/Sources/Testing/ExitTests/ExitTest.swift +++ b/Sources/Testing/ExitTests/ExitTest.swift @@ -233,6 +233,10 @@ extension ExitTest { #endif } + /// The name of the environment variable used to identify the exit test to + /// call in a spawned exit test process. + private static let _idEnvironmentVariableName = "SWT_EXIT_TEST_ID" + /// Call the exit test in the current process. /// /// This function invokes the closure originally passed to @@ -713,6 +717,17 @@ extension ExitTest { #endif } + /// The ID of the exit test to run, if any, specified in the environment. + static var environmentIDForEntryPoint: ID? { + guard var idString = Environment.variable(named: Self._idEnvironmentVariableName) else { + return nil + } + + return try? idString.withUTF8 { idBuffer in + try JSON.decode(ExitTest.ID.self, from: UnsafeRawBufferPointer(idBuffer)) + } + } + /// Find the exit test function specified in the environment of the current /// process, if any. /// @@ -723,21 +738,15 @@ extension ExitTest { /// `__swiftPMEntryPoint()` function. The effect of using it under other /// configurations is undefined. static func findInEnvironmentForEntryPoint() -> Self? { - // Find the ID of the exit test to run, if any, in the environment block. - var id: ExitTest.ID? - if var idString = Environment.variable(named: "SWT_EXIT_TEST_ID") { - // Clear the environment variable. It's an implementation detail and exit - // test code shouldn't be dependent on it. Use ExitTest.current if needed! - Environment.setVariable(nil, named: "SWT_EXIT_TEST_ID") - - id = try? idString.withUTF8 { idBuffer in - try JSON.decode(ExitTest.ID.self, from: UnsafeRawBufferPointer(idBuffer)) - } - } - guard let id, var result = find(identifiedBy: id) else { + guard let id = environmentIDForEntryPoint, var result = find(identifiedBy: id) else { return nil } + // Since an exit test was found, clear the environment variable. It's an + // implementation detail and exit test code shouldn't be dependent on it. + // Use ExitTest.current if needed! + Environment.setVariable(nil, named: Self._idEnvironmentVariableName) + // If an exit test was found, inject back channel handling into its body. // External tools authors should set up their own back channel mechanisms // and ensure they're installed before calling ExitTest.callAsFunction(). @@ -867,7 +876,7 @@ extension ExitTest { // Insert a specific variable that tells the child process which exit test // to run. try JSON.withEncoding(of: exitTest.id) { json in - childEnvironment["SWT_EXIT_TEST_ID"] = String(decoding: json, as: UTF8.self) + childEnvironment[Self._idEnvironmentVariableName] = String(decoding: json, as: UTF8.self) } typealias ResultUpdater = @Sendable (inout ExitTest.Result) -> Void From 824f6a4ec6195707056e1e61c03484e204f68dee Mon Sep 17 00:00:00 2001 From: Stuart Montgomery Date: Wed, 16 Jul 2025 18:57:41 -0500 Subject: [PATCH 067/216] Promote Issue Handling Traits to public API (#1198) --- Sources/Testing/Testing.docc/Traits.md | 4 +-- .../Testing/Traits/IssueHandlingTrait.swift | 36 ++++++++++++++++--- .../Traits/IssueHandlingTraitTests.swift | 36 ++++++++++++++++++- 3 files changed, 68 insertions(+), 8 deletions(-) diff --git a/Sources/Testing/Testing.docc/Traits.md b/Sources/Testing/Testing.docc/Traits.md index 46fa82b4d..d14eda999 100644 --- a/Sources/Testing/Testing.docc/Traits.md +++ b/Sources/Testing/Testing.docc/Traits.md @@ -48,12 +48,10 @@ types that customize the behavior of your tests. - ``Trait/bug(_:id:_:)-10yf5`` - ``Trait/bug(_:id:_:)-3vtpl`` - ### Creating custom traits @@ -67,8 +65,8 @@ types that customize the behavior of your tests. - ``Bug`` - ``Comment`` - ``ConditionTrait`` +- ``IssueHandlingTrait`` - ``ParallelizationTrait`` - ``Tag`` - ``Tag/List`` - ``TimeLimitTrait`` - diff --git a/Sources/Testing/Traits/IssueHandlingTrait.swift b/Sources/Testing/Traits/IssueHandlingTrait.swift index e8142ff5a..4d7d408d6 100644 --- a/Sources/Testing/Traits/IssueHandlingTrait.swift +++ b/Sources/Testing/Traits/IssueHandlingTrait.swift @@ -24,7 +24,10 @@ /// /// - ``Trait/compactMapIssues(_:)`` /// - ``Trait/filterIssues(_:)`` -@_spi(Experimental) +/// +/// @Metadata { +/// @Available(Swift, introduced: 6.2) +/// } public struct IssueHandlingTrait: TestTrait, SuiteTrait { /// A function which handles an issue and returns an optional replacement. /// @@ -49,6 +52,10 @@ public struct IssueHandlingTrait: TestTrait, SuiteTrait { /// /// - Returns: An issue to replace `issue`, or else `nil` if the issue should /// not be recorded. + /// + /// @Metadata { + /// @Available(Swift, introduced: 6.2) + /// } public func handleIssue(_ issue: Issue) -> Issue? { _handler(issue) } @@ -58,6 +65,9 @@ public struct IssueHandlingTrait: TestTrait, SuiteTrait { } } +/// @Metadata { +/// @Available(Swift, introduced: 6.2) +/// } extension IssueHandlingTrait: TestScoping { public func scopeProvider(for test: Test, testCase: Test.Case?) -> Self? { // Provide scope for tests at both the suite and test case levels, but not @@ -111,9 +121,20 @@ extension IssueHandlingTrait: TestScoping { } if let newIssue { - // Prohibit assigning the issue's kind to system. - if case .system = newIssue.kind { + // Validate the value of the returned issue's 'kind' property. + switch (issue.kind, newIssue.kind) { + case (_, .system): + // Prohibited by ST-0011. preconditionFailure("Issue returned by issue handling closure cannot have kind 'system': \(newIssue)") + case (.apiMisused, .apiMisused): + // This is permitted, but must be listed explicitly before the + // wildcard case below. + break + case (_, .apiMisused): + // Prohibited by ST-0011. + preconditionFailure("Issue returned by issue handling closure cannot have kind 'apiMisused' when the passed-in issue had a different kind: \(newIssue)") + default: + break } var event = event @@ -126,7 +147,6 @@ extension IssueHandlingTrait: TestScoping { } } -@_spi(Experimental) extension Trait where Self == IssueHandlingTrait { /// Constructs an trait that transforms issues recorded by a test. /// @@ -158,6 +178,10 @@ extension Trait where Self == IssueHandlingTrait { /// - Note: `transform` will never be passed an issue for which the value of /// ``Issue/kind`` is ``Issue/Kind/system``, and may not return such an /// issue. + /// + /// @Metadata { + /// @Available(Swift, introduced: 6.2) + /// } public static func compactMapIssues(_ transform: @escaping @Sendable (Issue) -> Issue?) -> Self { Self(handler: transform) } @@ -192,6 +216,10 @@ extension Trait where Self == IssueHandlingTrait { /// /// - Note: `isIncluded` will never be passed an issue for which the value of /// ``Issue/kind`` is ``Issue/Kind/system``. + /// + /// @Metadata { + /// @Available(Swift, introduced: 6.2) + /// } public static func filterIssues(_ isIncluded: @escaping @Sendable (Issue) -> Bool) -> Self { Self { issue in isIncluded(issue) ? issue : nil diff --git a/Tests/TestingTests/Traits/IssueHandlingTraitTests.swift b/Tests/TestingTests/Traits/IssueHandlingTraitTests.swift index c32d89111..2efb08dcc 100644 --- a/Tests/TestingTests/Traits/IssueHandlingTraitTests.swift +++ b/Tests/TestingTests/Traits/IssueHandlingTraitTests.swift @@ -8,7 +8,7 @@ // See https://swift.org/CONTRIBUTORS.txt for Swift project authors // -@testable @_spi(Experimental) @_spi(ForToolsIntegrationOnly) import Testing +@testable @_spi(ForToolsIntegrationOnly) import Testing @Suite("IssueHandlingTrait Tests", .tags(.traitRelated)) struct IssueHandlingTraitTests { @@ -216,6 +216,27 @@ struct IssueHandlingTraitTests { }.run(configuration: configuration) } + @Test("An API misused issue can be returned by issue handler closure when the original issue had that kind") + func returningAPIMisusedIssue() async throws { + var configuration = Configuration() + configuration.eventHandler = { event, context in + if case let .issueRecorded(issue) = event.kind, case .unconditional = issue.kind { + issue.record() + } + } + + let handler = IssueHandlingTrait.compactMapIssues { issue in + guard case .apiMisused = issue.kind else { + return Issue.record("Expected an issue of kind 'apiMisused': \(issue)") + } + return issue + } + + await Test(handler) { + Issue(kind: .apiMisused).record() + }.run(configuration: configuration) + } + #if !SWT_NO_EXIT_TESTS @Test("Disallow assigning kind to .system") func disallowAssigningSystemKind() async throws { @@ -229,5 +250,18 @@ struct IssueHandlingTraitTests { }.run() } } + + @Test("Disallow assigning kind to .apiMisused") + func disallowAssigningAPIMisusedKind() async throws { + await #expect(processExitsWith: .failure) { + await Test(.compactMapIssues { issue in + var issue = issue + issue.kind = .apiMisused + return issue + }) { + Issue.record("A non-system issue") + }.run() + } + } #endif } From ac4f8daae735808688603946f7d0d134fec489e4 Mon Sep 17 00:00:00 2001 From: Stuart Montgomery Date: Thu, 17 Jul 2025 13:09:53 -0500 Subject: [PATCH 068/216] Validate format of event stream version CLI argument when passed (#1231) This adds validation of the user-specified `--event-stream-version` (or `--experimental-event-stream-version`) command-line arguments when they are passed, to ensure they have a valid format. ### Motivation: Currently, these flags only accept integers like `0`, `1`, etc. But I am moving towards making the entry point and event stream versions align with the Swift version, rather than having an independent versioning scheme which users must look up or keep track of. Once this effort is finished, in order to use the event stream format included in (for example) Swift 6.3, a user would pass `--event-stream-version 6.3`. In https://github.com/swiftlang/swift-package-manager/pull/8944, I recently landed a SwiftPM change which will permit any arbitrary string for these arguments, and this means we need to begin validating the format of supported versions within the testing library. In a subsequent PR, I plan to introduce support for non-integer versions, at which point having some existing validation logic will be even more valuable. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- .../Testing/ABI/EntryPoints/EntryPoint.swift | 22 +++++++++++++------ Tests/TestingTests/SwiftPMTests.swift | 7 ++++++ 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/Sources/Testing/ABI/EntryPoints/EntryPoint.swift b/Sources/Testing/ABI/EntryPoints/EntryPoint.swift index 7a2e63003..733b4149d 100644 --- a/Sources/Testing/ABI/EntryPoints/EntryPoint.swift +++ b/Sources/Testing/ABI/EntryPoints/EntryPoint.swift @@ -255,15 +255,16 @@ public struct __CommandLineArguments_v0: Sendable { /// whichever occurs first. public var eventStreamOutputPath: String? - /// The version of the event stream schema to use when writing events to - /// ``eventStreamOutput``. + /// 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``. /// /// The corresponding stable schema is used to encode events to the event /// stream. ``ABI/Record`` is used if the value of this property is `0` or /// higher. /// /// If the value of this property is `nil`, the testing library assumes that - /// the newest available schema should be used. + /// the current supported (non-experimental) version should be used. public var eventStreamVersion: Int? /// The value(s) of the `--filter` argument. @@ -376,16 +377,23 @@ func parseCommandLineArguments(from args: [String]) throws -> __CommandLineArgum } } if let eventOutputVersionIndex, !isLastArgument(at: eventOutputVersionIndex) { - result.eventStreamVersion = Int(args[args.index(after: eventOutputVersionIndex)]) + let versionString = args[args.index(after: eventOutputVersionIndex)] + + // If the caller specified a version that could not be parsed, treat it as + // an invalid argument. + guard let eventStreamVersion = Int(versionString) else { + let argument = allowExperimental ? "--experimental-event-stream-version" : "--event-stream-version" + throw _EntryPointError.invalidArgument(argument, value: versionString) + } // If the caller specified an experimental ABI version, they must // explicitly use --experimental-event-stream-version, otherwise it's // treated as unsupported. - if let eventStreamVersion = result.eventStreamVersion, - eventStreamVersion > ABI.CurrentVersion.versionNumber, - !allowExperimental { + if eventStreamVersion > ABI.CurrentVersion.versionNumber, !allowExperimental { throw _EntryPointError.experimentalABIVersion(eventStreamVersion) } + + result.eventStreamVersion = eventStreamVersion } } #endif diff --git a/Tests/TestingTests/SwiftPMTests.swift b/Tests/TestingTests/SwiftPMTests.swift index 8d00b74d5..5008a271d 100644 --- a/Tests/TestingTests/SwiftPMTests.swift +++ b/Tests/TestingTests/SwiftPMTests.swift @@ -308,6 +308,13 @@ struct SwiftPMTests { _ = try configurationForEntryPoint(withArguments: ["PATH", "--event-stream-version", "\(experimentalVersion)"]) } } + + @Test("Invalid event stream version throws an invalid argument error") + func invalidEventStreamVersionThrows() { + #expect(throws: (any Error).self) { + _ = try configurationForEntryPoint(withArguments: ["PATH", "--event-stream-version", "xyz-invalid"]) + } + } #endif #endif From 004acdb667321057359718c38c6aa2168dc456ec Mon Sep 17 00:00:00 2001 From: Si Beaumont Date: Fri, 18 Jul 2025 00:25:27 +0100 Subject: [PATCH 069/216] Update docs for decoding stdout and stderr from exit tests (#1230) --- Sources/Testing/ExitTests/ExitTest.Result.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/Testing/ExitTests/ExitTest.Result.swift b/Sources/Testing/ExitTests/ExitTest.Result.swift index a427d4005..e31afa937 100644 --- a/Sources/Testing/ExitTests/ExitTest.Result.swift +++ b/Sources/Testing/ExitTests/ExitTest.Result.swift @@ -38,7 +38,7 @@ extension ExitTest { /// The value of this property may contain any arbitrary sequence of bytes, /// including sequences that are not valid UTF-8 and cannot be decoded by /// [`String.init(cString:)`](https://developer.apple.com/documentation/swift/string/init(cstring:)-6kr8s). - /// Consider using [`String.init(validatingCString:)`](https://developer.apple.com/documentation/swift/string/init(validatingcstring:)-992vo) + /// Consider using [`String.init(validating:as:)`](https://developer.apple.com/documentation/swift/string/init(validating:as:)-84qr9) /// instead. /// /// When checking the value of this property, keep in mind that the standard @@ -69,7 +69,7 @@ extension ExitTest { /// The value of this property may contain any arbitrary sequence of bytes, /// including sequences that are not valid UTF-8 and cannot be decoded by /// [`String.init(cString:)`](https://developer.apple.com/documentation/swift/string/init(cstring:)-6kr8s). - /// Consider using [`String.init(validatingCString:)`](https://developer.apple.com/documentation/swift/string/init(validatingcstring:)-992vo) + /// Consider using [`String.init(validating:as:)`](https://developer.apple.com/documentation/swift/string/init(validating:as:)-84qr9) /// instead. /// /// When checking the value of this property, keep in mind that the standard From 85175080f5ab9b8de61a455992df8ae2c0cd0200 Mon Sep 17 00:00:00 2001 From: Stuart Montgomery Date: Tue, 22 Jul 2025 12:34:28 -0500 Subject: [PATCH 070/216] Mention module name and presence in toolchain in README (#1233) This adds a mention of the testing library's module name (`import Testing`) and its presence in officially-supported Swift toolchains in the `README`. ### Motivation: Some users have been confused about whether they need to declare a package dependency on the `swift-testing` package to use Swift Testing. It's included in Swift toolchains for officially-supported platforms, so we generally do not recommend declaring such a package dependency, and doing so can lead to a degraded experience with integrated tools. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [X] If public symbols are renamed or modified, DocC references should be updated. --- README.md | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 8df465217..0d853ab78 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,8 @@ Swift expressions and operators, and captures the evaluated values so you can quickly understand what went wrong when a test fails. ```swift +import Testing + @Test func helloWorld() { let greeting = "Hello, world!" #expect(greeting == "Hello") // Expectation failed: (greeting → "Hello, world!") == "Hello" @@ -84,11 +86,19 @@ func mentionedContinents(videoName: String) async throws { ### Cross-platform support -Swift Testing works on all major platforms supported by Swift, including Apple -platforms, Linux, and Windows, so your tests can behave more consistently when -moving between platforms. It’s developed as open source and discussed on the -[Swift Forums](https://forums.swift.org/c/development/swift-testing/103) so the -very best ideas, from anywhere, can help shape the future of testing in Swift. +Swift Testing is included in officially-supported Swift toolchains, including +those for Apple platforms, Linux, and Windows. To use the library, import the +`Testing` module: + +```swift +import Testing +``` + +You don't need to declare a package dependency to use Swift Testing. It's +developed as open source and discussed on the +[Swift Forums](https://forums.swift.org/c/development/swift-testing/103) +so the very best ideas, from anywhere, can help shape the future of testing in +Swift. The table below describes the current level of support that Swift Testing has for various platforms: From 802d8f8bdd02edcc629b8e571165769250847a8c Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Wed, 23 Jul 2025 08:47:41 -0400 Subject: [PATCH 071/216] Enable exit test value capturing (#1165) --- Package.swift | 15 ---- Sources/Testing/Testing.docc/exit-testing.md | 69 +++++++++++++++---- Sources/TestingMacros/ConditionMacro.swift | 25 ------- .../Support/DiagnosticMessage.swift | 44 ------------ .../ConditionMacroTests.swift | 33 ++------- Tests/TestingTests/ExitTestTests.swift | 2 - 6 files changed, 59 insertions(+), 129 deletions(-) diff --git a/Package.swift b/Package.swift index 3405be19d..40d64e6c6 100644 --- a/Package.swift +++ b/Package.swift @@ -107,13 +107,6 @@ let package = Package( return result }(), - traits: [ - .trait( - name: "ExperimentalExitTestValueCapture", - description: "Enable experimental support for capturing values in exit tests" - ), - ], - dependencies: [ .package(url: "https://github.com/swiftlang/swift-syntax.git", from: "602.0.0-latest"), ], @@ -383,14 +376,6 @@ extension Array where Element == PackageDescription.SwiftSetting { .define("SWT_NO_LIBDISPATCH", .whenEmbedded()), ] - // Unconditionally enable 'ExperimentalExitTestValueCapture' when building - // for development. - if buildingForDevelopment { - result += [ - .define("ExperimentalExitTestValueCapture") - ] - } - return result } diff --git a/Sources/Testing/Testing.docc/exit-testing.md b/Sources/Testing/Testing.docc/exit-testing.md index 6ae81b980..fc00bf31d 100644 --- a/Sources/Testing/Testing.docc/exit-testing.md +++ b/Sources/Testing/Testing.docc/exit-testing.md @@ -67,21 +67,7 @@ The parent process doesn't call the body of the exit test. Instead, the child process treats the body of the exit test as its `main()` function and calls it directly. -- Note: Because the body acts as the `main()` function of a new process, it - can't capture any state originating in the parent process or from its lexical - context. For example, the following exit test will fail to compile because it - captures a variable declared outside the exit test itself: - - ```swift - @Test func `Customer won't eat food unless it's nutritious`() async { - let isNutritious = false - await #expect(processExitsWith: .failure) { - var food = ... - food.isNutritious = isNutritious // ❌ ERROR: trying to capture state here - Customer.current.eat(food) - } - } - ``` + If the body returns before the child process exits, the process exits as if `main()` returned normally. If the body throws an error, Swift handles it as if @@ -106,6 +92,59 @@ status of the child process against the expected exit condition you passed. If they match, the exit test passes; otherwise, it fails and the testing library records an issue. +### Capture state from the parent process + +To pass information from the parent process to the child process, you specify +the Swift values you want to pass in a [capture list](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/closures/#Capturing-Values) +on the exit test's body: + +```swift +@Test(arguments: Food.allJunkFood) +func `Customer won't eat food unless it's nutritious`(_ food: Food) async { + await #expect(processExitsWith: .failure) { [food] in + Customer.current.eat(food) + } +} +``` + +- Note: If you use this macro with a Swift compiler version lower than 6.3, it + doesn't support capturing state. + +If a captured value is an argument to the current function or is `self`, its +type is inferred at compile time. Otherwise, explicitly specify the type of the +value using the `as` operator: + +```swift +@Test func `Customer won't eat food unless it's nutritious`() async { + var food = ... + food.isNutritious = false + await #expect(processExitsWith: .failure) { [self, food = food as Food] in + self.prepare(food) + Customer.current.eat(food) + } +} +``` + +Every value you capture in an exit test must conform to [`Sendable`](https://developer.apple.com/documentation/swift/sendable) +and [`Codable`](https://developer.apple.com/documentation/swift/codable). Each +value is encoded by the parent process using [`encode(to:)`](https://developer.apple.com/documentation/swift/encodable/encode(to:)) +and is decoded by the child process [`init(from:)`](https://developer.apple.com/documentation/swift/decodable/init(from:)) +before being passed to the exit test body. + +If a captured value's type does not conform to both `Sendable` and `Codable`, or +if the value is not explicitly specified in the exit test body's capture list, +the compiler emits an error: + +```swift +@Test func `Customer won't eat food unless it's nutritious`() async { + var food = ... + food.isNutritious = false + await #expect(processExitsWith: .failure) { + Customer.current.eat(food) // ❌ ERROR: implicitly capturing 'food' + } +} +``` + ### Gather output from the child process The ``expect(processExitsWith:observing:_:sourceLocation:performing:)`` and diff --git a/Sources/TestingMacros/ConditionMacro.swift b/Sources/TestingMacros/ConditionMacro.swift index a4d38405f..c4b1db7e3 100644 --- a/Sources/TestingMacros/ConditionMacro.swift +++ b/Sources/TestingMacros/ConditionMacro.swift @@ -630,15 +630,6 @@ extension ExitTestConditionMacro { ) -> Bool { var diagnostics = [DiagnosticMessage]() - if let closureExpr = bodyArgumentExpr.as(ClosureExprSyntax.self), - let captureClause = closureExpr.signature?.capture, - !captureClause.items.isEmpty { - // Disallow capture lists if the experimental feature is not enabled. - if !ExitTestExpectMacro.isValueCapturingEnabled { - diagnostics.append(.captureClauseUnsupported(captureClause, in: closureExpr, inExitTest: macro)) - } - } - // Disallow exit tests in generic types and functions as they cannot be // correctly expanded due to the use of a nested type with static members. for lexicalContext in context.lexicalContext { @@ -664,22 +655,6 @@ extension ExitTestConditionMacro { } } -extension ExitTestExpectMacro { - /// Whether or not experimental value capturing via explicit capture lists is - /// enabled. - /// - /// This member is declared on ``ExitTestExpectMacro`` but also applies to - /// ``ExitTestRequireMacro``. - @TaskLocal - static var isValueCapturingEnabled: Bool = { -#if ExperimentalExitTestValueCapture - return true -#else - return false -#endif - }() -} - /// A type describing the expansion of the `#expect(processExitsWith:)` macro. /// /// This type checks for nested invocations of `#expect()` and `#require()` and diff --git a/Sources/TestingMacros/Support/DiagnosticMessage.swift b/Sources/TestingMacros/Support/DiagnosticMessage.swift index 71d5a7100..80c3d9e1f 100644 --- a/Sources/TestingMacros/Support/DiagnosticMessage.swift +++ b/Sources/TestingMacros/Support/DiagnosticMessage.swift @@ -855,50 +855,6 @@ extension DiagnosticMessage { ) } - /// Create a diagnostic message stating that a capture clause cannot be used - /// in an exit test. - /// - /// - Parameters: - /// - captureClause: The invalid capture clause. - /// - closure: The closure containing `captureClause`. - /// - exitTestMacro: The containing exit test macro invocation. - /// - /// - Returns: A diagnostic message. - static func captureClauseUnsupported(_ captureClause: ClosureCaptureClauseSyntax, in closure: ClosureExprSyntax, inExitTest exitTestMacro: some FreestandingMacroExpansionSyntax) -> Self { - let changes: [FixIt.Change] - if let signature = closure.signature, - Array(signature.with(\.capture, nil).tokens(viewMode: .sourceAccurate)).count == 1 { - // The only remaining token in the signature is `in`, so remove the whole - // signature tree instead of just the capture clause. - changes = [ - .replaceTrailingTrivia(token: closure.leftBrace, newTrivia: ""), - .replace( - oldNode: Syntax(signature), - newNode: Syntax("" as ExprSyntax) - ) - ] - } else { - changes = [ - .replace( - oldNode: Syntax(captureClause), - newNode: Syntax("" as ExprSyntax) - ) - ] - } - - return Self( - syntax: Syntax(captureClause), - message: "Cannot specify a capture clause in closure passed to \(_macroName(exitTestMacro))", - severity: .error, - fixIts: [ - FixIt( - message: MacroExpansionFixItMessage("Remove '\(captureClause.trimmed)'"), - changes: changes - ), - ] - ) - } - /// Create a diagnostic message stating that an expression macro is not /// supported in a generic context. /// diff --git a/Tests/TestingMacrosTests/ConditionMacroTests.swift b/Tests/TestingMacrosTests/ConditionMacroTests.swift index 2c3cc38d8..a451b6f29 100644 --- a/Tests/TestingMacrosTests/ConditionMacroTests.swift +++ b/Tests/TestingMacrosTests/ConditionMacroTests.swift @@ -452,7 +452,6 @@ struct ConditionMacroTests { } } -#if ExperimentalExitTestValueCapture @Test("#expect(processExitsWith:) produces a diagnostic for a bad capture", arguments: [ "#expectExitTest(processExitsWith: x) { [weak a] in }": @@ -470,34 +469,12 @@ struct ConditionMacroTests { ] ) func exitTestCaptureDiagnostics(input: String, expectedMessage: String) throws { - try ExitTestExpectMacro.$isValueCapturingEnabled.withValue(true) { - let (_, diagnostics) = try parse(input) - - #expect(diagnostics.count > 0) - for diagnostic in diagnostics { - #expect(diagnostic.diagMessage.severity == .error) - #expect(diagnostic.message == expectedMessage) - } - } - } -#endif + let (_, diagnostics) = try parse(input) - @Test( - "Capture list on an exit test produces a diagnostic", - arguments: [ - "#expectExitTest(processExitsWith: x) { [a] in }": - "Cannot specify a capture clause in closure passed to '#expectExitTest(processExitsWith:_:)'" - ] - ) - func exitTestCaptureListProducesDiagnostic(input: String, expectedMessage: String) throws { - try ExitTestExpectMacro.$isValueCapturingEnabled.withValue(false) { - let (_, diagnostics) = try parse(input) - - #expect(diagnostics.count > 0) - for diagnostic in diagnostics { - #expect(diagnostic.diagMessage.severity == .error) - #expect(diagnostic.message == expectedMessage) - } + #expect(diagnostics.count > 0) + for diagnostic in diagnostics { + #expect(diagnostic.diagMessage.severity == .error) + #expect(diagnostic.message == expectedMessage) } } diff --git a/Tests/TestingTests/ExitTestTests.swift b/Tests/TestingTests/ExitTestTests.swift index 59d048bdc..e0bc0b86c 100644 --- a/Tests/TestingTests/ExitTestTests.swift +++ b/Tests/TestingTests/ExitTestTests.swift @@ -383,7 +383,6 @@ private import _TestingInternals } } -#if ExperimentalExitTestValueCapture @Test("Capture list") func captureList() async { let i = 123 @@ -561,7 +560,6 @@ private import _TestingInternals } } #endif -#endif } // MARK: - Fixtures From 880c37b3245d9d63e6c654a2b0f0f2a5c95cee47 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Wed, 23 Jul 2025 20:38:57 -0400 Subject: [PATCH 072/216] Add FreeBSD and OpenBSD as experimentally supported in the readme. (#1236) This PR adds FreeBSD and OpenBSD to README.md as experimentally supported. (FreeBSD will be fully supported in an upcoming Swift release.) ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 0d853ab78..062ef843d 100644 --- a/README.md +++ b/README.md @@ -111,6 +111,8 @@ for various platforms: | **tvOS** | | | Supported | | **visionOS** | | | Supported | | **Ubuntu 22.04** | [![Build Status](https://ci.swift.org/buildStatus/icon?job=swift-testing-main-swift-6.1-linux)](https://ci.swift.org/job/swift-testing-main-swift-6.1-linux/) | [![Build Status](https://ci.swift.org/buildStatus/icon?job=swift-testing-main-swift-main-linux)](https://ci.swift.org/view/Swift%20Packages/job/swift-testing-main-swift-main-linux/) | Supported | +| **FreeBSD** | | | Experimental | +| **OpenBSD** | | | Experimental | | **Windows** | [![Build Status](https://ci-external.swift.org/buildStatus/icon?job=swift-testing-main-swift-6.1-windows)](https://ci-external.swift.org/view/all/job/swift-testing-main-swift-6.1-windows/) | [![Build Status](https://ci-external.swift.org/buildStatus/icon?job=swift-testing-main-swift-main-windows)](https://ci-external.swift.org/job/swift-testing-main-swift-main-windows/) | Supported | | **Wasm** | | | Experimental | From 0f809e4f7dc593fab76d1548ef3156e781a375e4 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Thu, 24 Jul 2025 15:59:59 -0400 Subject: [PATCH 073/216] Don't use `_getSuperclass()` from the Swift runtime. (#1237) To determine if a class is a subclass of another, we can use `T.self is U.Type` in an implicitly-opened existential context. Prior to the Swift 6 language mode, type existentials weren't openable, so we had to recursively call a runtime function to check this. That's no longer a concern for us. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- Sources/Testing/Parameterization/TypeInfo.swift | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/Sources/Testing/Parameterization/TypeInfo.swift b/Sources/Testing/Parameterization/TypeInfo.swift index 0bceff744..de377e6be 100644 --- a/Sources/Testing/Parameterization/TypeInfo.swift +++ b/Sources/Testing/Parameterization/TypeInfo.swift @@ -360,13 +360,10 @@ extension TypeInfo { /// - Returns: Whether `subclass` is a subclass of, or is equal to, /// `superclass`. func isClass(_ subclass: AnyClass, subclassOf superclass: AnyClass) -> Bool { - if subclass == superclass { - true - } else if let subclassImmediateSuperclass = _getSuperclass(subclass) { - isClass(subclassImmediateSuperclass, subclassOf: superclass) - } else { - false + func open(_: T.Type, _: U.Type) -> Bool where T: AnyObject, U: AnyObject { + T.self is U.Type } + return open(subclass, superclass) } // MARK: - CustomStringConvertible, CustomDebugStringConvertible, CustomTestStringConvertible From 5fecefc36cb5b09ce71353440cce915146c84e63 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Thu, 24 Jul 2025 17:59:26 -0400 Subject: [PATCH 074/216] Use the prefix `swift_testing_` for future exported symbols. (#1238) This PR updates our naming guidelines for exported/public C symbols. Right now, the only functions that would qualify are `swt_abiv0_getEntryPoint()` and `swt_copyABIEntryPoint_v0()` and their names must be preserved for binary compatibility with Xcode 16 through Xcode 26 anyway. Thus, this change is a documentation change only. Resolves #1064. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [ ] If public symbols are renamed or modified, DocC references should be updated. --- Documentation/StyleGuide.md | 17 +++++++++++++---- .../Testing/ABI/EntryPoints/ABIEntryPoint.swift | 4 ++++ 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/Documentation/StyleGuide.md b/Documentation/StyleGuide.md index 006cf9c51..b9c565e04 100644 --- a/Documentation/StyleGuide.md +++ b/Documentation/StyleGuide.md @@ -71,10 +71,10 @@ to the code called by the initialization expression causing the inferred type of its property to change unknowingly, which could break clients. Properties with lower access levels may have an inferred type. -Exported C and C++ symbols that are exported should be given the prefix `swt_` -and should otherwise be named using the same lowerCamelCase naming rules as in -Swift. Use the `SWT_EXTERN` macro to ensure that symbols are consistently -visible in C, C++, and Swift. For example: +C and C++ symbols that are used by the testing library should be given the +prefix `swt_` and should otherwise be named using the same lowerCamelCase naming +rules as in Swift. Use the `SWT_EXTERN` macro to ensure that symbols are +consistently visible in C, C++, and Swift. For example: ```c SWT_EXTERN bool swt_isDebugModeEnabled(void); @@ -82,6 +82,15 @@ SWT_EXTERN bool swt_isDebugModeEnabled(void); SWT_EXTERN void swt_setDebugModeEnabled(bool isEnabled); ``` +> [!NOTE] +> If a symbol is meant to be **publicly visible** and can be called by modules +> other than Swift Testing, use the prefix `swift_testing_` instead of `swt_` +> for consistency with the Swift standard library: +> +> ```c +> SWT_EXTERN void swift_testing_debugIfNeeded(void); +> ``` + C and C++ types should be given the prefix `SWT` and should otherwise be named using the same UpperCamelCase naming rules as in Swift. For example: diff --git a/Sources/Testing/ABI/EntryPoints/ABIEntryPoint.swift b/Sources/Testing/ABI/EntryPoints/ABIEntryPoint.swift index f3f50a1be..89f4bca93 100644 --- a/Sources/Testing/ABI/EntryPoints/ABIEntryPoint.swift +++ b/Sources/Testing/ABI/EntryPoints/ABIEntryPoint.swift @@ -67,6 +67,10 @@ extension ABI.v0 { /// /// - Returns: The value of ``ABI/v0/entryPoint-swift.type.property`` cast to an /// untyped pointer. +/// +/// - Note: This function's name is prefixed with `swt_` instead of +/// `swift_testing_` for binary compatibility reasons. Future ABI entry point +/// functions should use the `swift_testing_` prefix instead. @_cdecl("swt_abiv0_getEntryPoint") @usableFromInline func abiv0_getEntryPoint() -> UnsafeRawPointer { unsafeBitCast(ABI.v0.entryPoint, to: UnsafeRawPointer.self) From fdf27547987091340076cf16b4f1c55d092e0cf6 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Thu, 24 Jul 2025 21:02:02 -0400 Subject: [PATCH 075/216] Remove `swt_copyABIEntryPoint_v0()`. (#1240) --- .../ABI/EntryPoints/ABIEntryPoint.swift | 42 ------------ Tests/TestingTests/ABIEntryPointTests.swift | 64 ------------------- 2 files changed, 106 deletions(-) diff --git a/Sources/Testing/ABI/EntryPoints/ABIEntryPoint.swift b/Sources/Testing/ABI/EntryPoints/ABIEntryPoint.swift index 89f4bca93..f12a8ecf5 100644 --- a/Sources/Testing/ABI/EntryPoints/ABIEntryPoint.swift +++ b/Sources/Testing/ABI/EntryPoints/ABIEntryPoint.swift @@ -75,46 +75,4 @@ extension ABI.v0 { @usableFromInline func abiv0_getEntryPoint() -> UnsafeRawPointer { unsafeBitCast(ABI.v0.entryPoint, to: UnsafeRawPointer.self) } - -#if !SWT_NO_SNAPSHOT_TYPES -// MARK: - Xcode 16 compatibility - -extension ABI.Xcode16 { - /// An older signature for ``ABI/v0/EntryPoint-swift.typealias`` used by - /// Xcode 16. - /// - /// - Warning: This type will be removed in a future update. - @available(*, deprecated, message: "Use ABI.v0.EntryPoint instead.") - typealias EntryPoint = @Sendable ( - _ argumentsJSON: UnsafeRawBufferPointer?, - _ recordHandler: @escaping @Sendable (_ recordJSON: UnsafeRawBufferPointer) -> Void - ) async throws -> CInt -} - -/// An older signature for ``ABI/v0/entryPoint-swift.type.property`` used by -/// Xcode 16. -/// -/// - Warning: This function will be removed in a future update. -@available(*, deprecated, message: "Use ABI.v0.entryPoint (swt_abiv0_getEntryPoint()) instead.") -@_cdecl("swt_copyABIEntryPoint_v0") -@usableFromInline func copyABIEntryPoint_v0() -> UnsafeMutableRawPointer { - let result = UnsafeMutablePointer.allocate(capacity: 1) - result.initialize { configurationJSON, recordHandler in - var args = try configurationJSON.map { configurationJSON in - try JSON.decode(__CommandLineArguments_v0.self, from: configurationJSON) - } - if args?.eventStreamVersion == nil { - args?.eventStreamVersion = ABI.Xcode16.versionNumber - } - let eventHandler = try eventHandlerForStreamingEvents(version: args?.eventStreamVersion, encodeAsJSONLines: false, forwardingTo: recordHandler) - - var exitCode = await Testing.entryPoint(passing: args, eventHandler: eventHandler) - if exitCode == EXIT_NO_TESTS_FOUND { - exitCode = EXIT_SUCCESS - } - return exitCode - } - return .init(result) -} -#endif #endif diff --git a/Tests/TestingTests/ABIEntryPointTests.swift b/Tests/TestingTests/ABIEntryPointTests.swift index 9fcda9223..690889451 100644 --- a/Tests/TestingTests/ABIEntryPointTests.swift +++ b/Tests/TestingTests/ABIEntryPointTests.swift @@ -18,70 +18,6 @@ private import _TestingInternals @Suite("ABI entry point tests") struct ABIEntryPointTests { -#if !SWT_NO_SNAPSHOT_TYPES - @available(*, deprecated) - @Test func v0_experimental() async throws { - var arguments = __CommandLineArguments_v0() - arguments.filter = ["NonExistentTestThatMatchesNothingHopefully"] - arguments.eventStreamVersion = 0 - arguments.verbosity = .min - - let result = try await _invokeEntryPointV0Experimental(passing: arguments) { recordJSON in - let record = try! JSON.decode(ABI.Record.self, from: recordJSON) - _ = record.kind - } - - #expect(result == EXIT_SUCCESS) - } - - @available(*, deprecated) - @Test("v0 experimental entry point with a large number of filter arguments") - func v0_experimental_manyFilters() async throws { - var arguments = __CommandLineArguments_v0() - arguments.filter = (1...100).map { "NonExistentTestThatMatchesNothingHopefully_\($0)" } - arguments.eventStreamVersion = 0 - arguments.verbosity = .min - - let result = try await _invokeEntryPointV0Experimental(passing: arguments) - - #expect(result == EXIT_SUCCESS) - } - - @available(*, deprecated) - private func _invokeEntryPointV0Experimental( - passing arguments: __CommandLineArguments_v0, - recordHandler: @escaping @Sendable (_ recordJSON: UnsafeRawBufferPointer) -> Void = { _ in } - ) async throws -> CInt { -#if !SWT_NO_DYNAMIC_LINKING - // Get the ABI entry point by dynamically looking it up at runtime. - let copyABIEntryPoint_v0 = try withTestingLibraryImageAddress { testingLibrary in - try #require( - symbol(in: testingLibrary, named: "swt_copyABIEntryPoint_v0").map { - castCFunction(at: $0, to: (@convention(c) () -> UnsafeMutableRawPointer).self) - } - ) - } -#endif - let abiEntryPoint = copyABIEntryPoint_v0().assumingMemoryBound(to: ABI.Xcode16.EntryPoint.self) - defer { - abiEntryPoint.deinitialize(count: 1) - abiEntryPoint.deallocate() - } - - let argumentsJSON = try JSON.withEncoding(of: arguments) { argumentsJSON in - let result = UnsafeMutableRawBufferPointer.allocate(byteCount: argumentsJSON.count, alignment: 1) - result.copyMemory(from: argumentsJSON) - return result - } - defer { - argumentsJSON.deallocate() - } - - // Call the entry point function. - return try await abiEntryPoint.pointee(.init(argumentsJSON), recordHandler) - } -#endif - @Test func v0() async throws { var arguments = __CommandLineArguments_v0() arguments.filter = ["NonExistentTestThatMatchesNothingHopefully"] From 4df4ff0a263ddf02a4028c07b76b418b60fe4776 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Fri, 25 Jul 2025 13:00:10 -0400 Subject: [PATCH 076/216] Silence a warning on FreeBSD/OpenBSD when calling `pthread_set_name_np()`. (#1241) On FreeBSD and OpenBSD, `pthread_set_name_np()` returns `void`, so we don't need to suppress the result (assigning it to `_`) and the Swift compiler complains that we do. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- Sources/Testing/ExitTests/WaitFor.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/Testing/ExitTests/WaitFor.swift b/Sources/Testing/ExitTests/WaitFor.swift index 238ed835a..4569dddc9 100644 --- a/Sources/Testing/ExitTests/WaitFor.swift +++ b/Sources/Testing/ExitTests/WaitFor.swift @@ -167,9 +167,9 @@ private let _createWaitThread: Void = { _ = _pthread_setname_np?(pthread_self(), "SWT ExT monitor") #endif #elseif os(FreeBSD) - _ = pthread_set_name_np(pthread_self(), "SWT ex test monitor") + pthread_set_name_np(pthread_self(), "SWT ex test monitor") #elseif os(OpenBSD) - _ = pthread_set_name_np(pthread_self(), "SWT exit test monitor") + pthread_set_name_np(pthread_self(), "SWT exit test monitor") #else #warning("Platform-specific implementation missing: thread naming unavailable") #endif From b12203faab715c8f61955357f4b30fc3bb61cdb7 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Fri, 25 Jul 2025 15:52:00 -0400 Subject: [PATCH 077/216] Rename `"SWT_EXPERIMENTAL_CAPTURED_VALUES"`. (#1242) This feature has been approved and is no longer experimental, so rename the environment variable we use to shuffle data across process boundaries. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- Sources/Testing/ExitTests/ExitTest.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/Testing/ExitTests/ExitTest.swift b/Sources/Testing/ExitTests/ExitTest.swift index 374bd9e3c..737ff8463 100644 --- a/Sources/Testing/ExitTests/ExitTest.swift +++ b/Sources/Testing/ExitTests/ExitTest.swift @@ -919,7 +919,7 @@ extension ExitTest { childEnvironment["SWT_BACKCHANNEL"] = backChannelEnvironmentVariable } if let capturedValuesEnvironmentVariable = _makeEnvironmentVariable(for: capturedValuesReadEnd) { - childEnvironment["SWT_EXPERIMENTAL_CAPTURED_VALUES"] = capturedValuesEnvironmentVariable + childEnvironment["SWT_CAPTURED_VALUES"] = capturedValuesEnvironmentVariable } // Spawn the child process. @@ -1071,7 +1071,7 @@ extension ExitTest { private mutating func _decodeCapturedValuesForEntryPoint() throws { // Read the content of the captured values stream provided by the parent // process above. - guard let fileHandle = Self._makeFileHandle(forEnvironmentVariableNamed: "SWT_EXPERIMENTAL_CAPTURED_VALUES", mode: "rb") else { + guard let fileHandle = Self._makeFileHandle(forEnvironmentVariableNamed: "SWT_CAPTURED_VALUES", mode: "rb") else { return } let capturedValuesJSON = try fileHandle.readToEnd() From 4c5f9a4605ca28930421762c6f50cc6d286079fc Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Tue, 29 Jul 2025 13:47:15 -0400 Subject: [PATCH 078/216] Introduce a new `ABI.VersionNumber` type. (#1239) This PR introduces and plumbs through an internal `ABI.VersionNumber` type that represents the JSON event stream version that should be used during a test run. This type supports integers (such as the current version 0) and semantic versions (e.g. 6.3 or 6.3.0). Care is taken to ensure backwards compatibility for ABI versions -1 and 0 which do not accept a string in this position. The new type necessarily conforms to `Comparable` and `Codable`. I've adjusted `__CommandLineArguments_v0` to support this new type while maintaining backwards-compatibility with Xcode 16. For the moment, these are all implementation details that have no impact on our supported interface as there is no newer JSON schema than 0. We intend to make changes to the schema in the next Swift release (assumed to be 6.3) that will require a Swift Evolution review. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- Sources/Testing/ABI/ABI.Record.swift | 2 +- Sources/Testing/ABI/ABI.VersionNumber.swift | 130 ++++++++++++++++++ Sources/Testing/ABI/ABI.swift | 47 +++++-- .../Testing/ABI/Encoded/ABI.EncodedTest.swift | 2 +- .../ABI/EntryPoints/ABIEntryPoint.swift | 2 +- .../Testing/ABI/EntryPoints/EntryPoint.swift | 90 +++++++----- Sources/Testing/CMakeLists.txt | 1 + Sources/Testing/ExitTests/ExitTest.swift | 2 +- Tests/TestingTests/ABIEntryPointTests.swift | 64 ++++++++- Tests/TestingTests/EntryPointTests.swift | 6 +- Tests/TestingTests/SwiftPMTests.swift | 64 +++++++-- 11 files changed, 342 insertions(+), 68 deletions(-) create mode 100644 Sources/Testing/ABI/ABI.VersionNumber.swift diff --git a/Sources/Testing/ABI/ABI.Record.swift b/Sources/Testing/ABI/ABI.Record.swift index 74ac7f9aa..e03b5df6a 100644 --- a/Sources/Testing/ABI/ABI.Record.swift +++ b/Sources/Testing/ABI/ABI.Record.swift @@ -66,7 +66,7 @@ extension ABI.Record: Codable { init(from decoder: any Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) - let versionNumber = try container.decode(Int.self, forKey: .version) + let versionNumber = try container.decode(ABI.VersionNumber.self, forKey: .version) if versionNumber != V.versionNumber { throw DecodingError.dataCorrupted( DecodingError.Context( diff --git a/Sources/Testing/ABI/ABI.VersionNumber.swift b/Sources/Testing/ABI/ABI.VersionNumber.swift new file mode 100644 index 000000000..2235d3c1d --- /dev/null +++ b/Sources/Testing/ABI/ABI.VersionNumber.swift @@ -0,0 +1,130 @@ +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024–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 ABI { + /// A type describing an ABI version number. + /// + /// This type implements a subset of the [semantic versioning](https://semver.org) + /// specification (specifically parsing, displaying, and comparing + /// `` values we expect that Swift will need for the foreseeable + /// future.) + struct VersionNumber: Sendable { + /// The major version. + var majorComponent: Int8 + + /// The minor version. + var minorComponent: Int8 + + /// The patch, revision, or bug fix version. + var patchComponent: Int8 = 0 + } +} + +extension ABI.VersionNumber { + init(_ majorComponent: _const Int8, _ minorComponent: _const Int8, _ patchComponent: _const Int8 = 0) { + self.init(majorComponent: majorComponent, minorComponent: minorComponent, patchComponent: patchComponent) + } +} + +// MARK: - CustomStringConvertible + +extension ABI.VersionNumber: CustomStringConvertible { + /// Initialize an instance of this type by parsing the given string. + /// + /// - Parameters: + /// - string: The string to parse, such as `"0"` or `"6.3.0"`. + /// + /// @Comment { + /// - Bug: We are not able to reuse the logic from swift-syntax's + /// `VersionTupleSyntax` type here because we cannot link to swift-syntax + /// in this target. + /// } + /// + /// If `string` contains fewer than 3 numeric components, the missing + /// components are inferred to be `0` (for example, `"1.2"` is equivalent to + /// `"1.2.0"`.) If `string` contains more than 3 numeric components, the + /// additional components are ignored. + init?(_ string: String) { + // Split the string on "." (assuming it is of the form "1", "1.2", or + // "1.2.3") and parse the individual components as integers. + let components = string.split(separator: ".", omittingEmptySubsequences: false) + func componentValue(_ index: Int) -> Int8? { + components.count > index ? Int8(components[index]) : 0 + } + + guard let majorComponent = componentValue(0), + let minorComponent = componentValue(1), + let patchComponent = componentValue(2) else { + return nil + } + self.init(majorComponent: majorComponent, minorComponent: minorComponent, patchComponent: patchComponent) + } + + var description: String { + if majorComponent <= 0 && minorComponent == 0 && patchComponent == 0 { + // Version 0 and earlier are described as integers for compatibility with + // Swift 6.2 and earlier. + return String(describing: majorComponent) + } else if patchComponent == 0 { + return "\(majorComponent).\(minorComponent)" + } + return "\(majorComponent).\(minorComponent).\(patchComponent)" + } +} + +// MARK: - Equatable, Comparable + +extension ABI.VersionNumber: Equatable, Comparable { + static func <(lhs: Self, rhs: Self) -> Bool { + if lhs.majorComponent != rhs.majorComponent { + return lhs.majorComponent < rhs.majorComponent + } else if lhs.minorComponent != rhs.minorComponent { + return lhs.minorComponent < rhs.minorComponent + } else if lhs.patchComponent != rhs.patchComponent { + return lhs.patchComponent < rhs.patchComponent + } + return false + } +} + +// MARK: - Codable + +extension ABI.VersionNumber: Codable { + init(from decoder: any Decoder) throws { + let container = try decoder.singleValueContainer() + if let number = try? container.decode(Int8.self) { + // Allow for version numbers encoded as integers for compatibility with + // Swift 6.2 and earlier. + self.init(majorComponent: number, minorComponent: 0) + } else { + let string = try container.decode(String.self) + guard let result = Self(string) else { + throw DecodingError.dataCorrupted( + .init( + codingPath: decoder.codingPath, + debugDescription: "Unexpected string '\(string)' (expected an integer or a string of the form '1.2.3')" + ) + ) + } + self = result + } + } + + func encode(to encoder: any Encoder) throws { + var container = encoder.singleValueContainer() + if majorComponent <= 0 && minorComponent == 0 && patchComponent == 0 { + // Version 0 and earlier are encoded as integers for compatibility with + // Swift 6.2 and earlier. + try container.encode(majorComponent) + } else { + try container.encode("\(majorComponent).\(minorComponent).\(patchComponent)") + } + } +} diff --git a/Sources/Testing/ABI/ABI.swift b/Sources/Testing/ABI/ABI.swift index 3106d2a72..23b14297c 100644 --- a/Sources/Testing/ABI/ABI.swift +++ b/Sources/Testing/ABI/ABI.swift @@ -18,7 +18,7 @@ extension ABI { /// A protocol describing the types that represent different ABI versions. protocol Version: Sendable { /// The numeric representation of this ABI version. - static var versionNumber: Int { get } + static var versionNumber: VersionNumber { get } #if canImport(Foundation) && (!SWT_NO_FILE_IO || !SWT_NO_ABI_ENTRY_POINT) /// Create an event handler that encodes events as JSON and forwards them to @@ -44,6 +44,33 @@ extension ABI { /// The current supported ABI version (ignoring any experimental versions.) typealias CurrentVersion = v0 + +#if !hasFeature(Embedded) + /// Get the type representing a given ABI version. + /// + /// - Parameters: + /// - versionNumber: The ABI version number for which a concrete type is + /// needed. + /// + /// - 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)? { + switch versionNumber { + case ABI.v6_3.versionNumber...: + ABI.v6_3.self + case ABI.v0.versionNumber...: + ABI.v0.self +#if !SWT_NO_SNAPSHOT_TYPES + case ABI.Xcode16.versionNumber: + // Legacy support for Xcode 16. Support for this undocumented version will + // be removed in a future update. Do not use it. + ABI.Xcode16.self +#endif + default: + nil + } + } +#endif } // MARK: - Concrete ABI versions @@ -54,28 +81,28 @@ extension ABI { /// /// - Warning: This type will be removed in a future update. enum Xcode16: Sendable, Version { - static var versionNumber: Int { - -1 + static var versionNumber: VersionNumber { + VersionNumber(-1, 0) } } #endif /// A namespace and type for ABI version 0 symbols. public enum v0: Sendable, Version { - static var versionNumber: Int { - 0 + static var versionNumber: VersionNumber { + VersionNumber(0, 0) } } - /// A namespace and type for ABI version 1 symbols. + /// A namespace and type for ABI version 6.3 symbols. /// /// @Metadata { - /// @Available("Swift Testing ABI", introduced: 1) + /// @Available(Swift, introduced: 6.3) /// } @_spi(Experimental) - public enum v1: Sendable, Version { - static var versionNumber: Int { - 1 + public enum v6_3: Sendable, Version { + static var versionNumber: VersionNumber { + VersionNumber(6, 3) } } } diff --git a/Sources/Testing/ABI/Encoded/ABI.EncodedTest.swift b/Sources/Testing/ABI/Encoded/ABI.EncodedTest.swift index cda558f83..29bfa10c3 100644 --- a/Sources/Testing/ABI/Encoded/ABI.EncodedTest.swift +++ b/Sources/Testing/ABI/Encoded/ABI.EncodedTest.swift @@ -98,7 +98,7 @@ extension ABI { sourceLocation = test.sourceLocation id = ID(encoding: test.id) - if V.versionNumber >= ABI.v1.versionNumber { + if V.versionNumber >= ABI.v6_3.versionNumber { let tags = test.tags if !tags.isEmpty { _tags = tags.map(String.init(describing:)) diff --git a/Sources/Testing/ABI/EntryPoints/ABIEntryPoint.swift b/Sources/Testing/ABI/EntryPoints/ABIEntryPoint.swift index f12a8ecf5..2ff10c964 100644 --- a/Sources/Testing/ABI/EntryPoints/ABIEntryPoint.swift +++ b/Sources/Testing/ABI/EntryPoints/ABIEntryPoint.swift @@ -50,7 +50,7 @@ extension ABI.v0 { let args = try configurationJSON.map { configurationJSON in try JSON.decode(__CommandLineArguments_v0.self, from: configurationJSON) } - let eventHandler = try eventHandlerForStreamingEvents(version: args?.eventStreamVersion, encodeAsJSONLines: false, forwardingTo: recordHandler) + let eventHandler = try eventHandlerForStreamingEvents(withVersionNumber: args?.eventStreamVersionNumber, encodeAsJSONLines: false, forwardingTo: recordHandler) switch await Testing.entryPoint(passing: args, eventHandler: eventHandler) { case EXIT_SUCCESS, EXIT_NO_TESTS_FOUND: diff --git a/Sources/Testing/ABI/EntryPoints/EntryPoint.swift b/Sources/Testing/ABI/EntryPoints/EntryPoint.swift index 733b4149d..61f2f92cb 100644 --- a/Sources/Testing/ABI/EntryPoints/EntryPoint.swift +++ b/Sources/Testing/ABI/EntryPoints/EntryPoint.swift @@ -259,13 +259,36 @@ public struct __CommandLineArguments_v0: Sendable { /// argument, representing the version of the event stream schema to use when /// writing events to ``eventStreamOutput``. /// - /// The corresponding stable schema is used to encode events to the event - /// stream. ``ABI/Record`` is used if the value of this property is `0` or - /// higher. + /// This property is internal because its type is internal. External users of + /// this structure can use the ``eventStreamSchemaVersion`` property to get or + /// set the value of this property. + var eventStreamVersionNumber: ABI.VersionNumber? + + /// 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``. + /// + /// The value of this property is a 1- or 3-component version string such as + /// `"0"` or `"1.2.3"`. The corresponding stable schema is used to encode + /// events to the event stream. ``ABI/Record`` is used if the value of this + /// property is `"0.0.0"` or higher. The testing library compares components + /// individually, so `"1.2"` is less than `"1.20"`. /// /// If the value of this property is `nil`, the testing library assumes that /// the current supported (non-experimental) version should be used. - public var eventStreamVersion: Int? + public var eventStreamSchemaVersion: String? { + get { + eventStreamVersionNumber.map { String(describing: $0) } + } + set { + eventStreamVersionNumber = newValue.flatMap { newValue in + guard let newValue = ABI.VersionNumber(newValue) else { + preconditionFailure("Invalid event stream version number '\(newValue)'. Specify a version number of the form 'major.minor.patch'.") + } + return newValue + } + } + } /// The value(s) of the `--filter` argument. public var filter: [String]? @@ -310,7 +333,7 @@ extension __CommandLineArguments_v0: Codable { case _verbosity = "verbosity" case xunitOutput case eventStreamOutputPath - case eventStreamVersion + case eventStreamVersionNumber = "eventStreamVersion" case filter case skip case repetitions @@ -381,7 +404,7 @@ func parseCommandLineArguments(from args: [String]) throws -> __CommandLineArgum // If the caller specified a version that could not be parsed, treat it as // an invalid argument. - guard let eventStreamVersion = Int(versionString) else { + guard let eventStreamVersion = ABI.VersionNumber(versionString) else { let argument = allowExperimental ? "--experimental-event-stream-version" : "--event-stream-version" throw _EntryPointError.invalidArgument(argument, value: versionString) } @@ -393,7 +416,7 @@ func parseCommandLineArguments(from args: [String]) throws -> __CommandLineArgum throw _EntryPointError.experimentalABIVersion(eventStreamVersion) } - result.eventStreamVersion = eventStreamVersion + result.eventStreamVersionNumber = eventStreamVersion } } #endif @@ -526,10 +549,10 @@ public func configurationForEntryPoint(from args: __CommandLineArguments_v0) thr } #if canImport(Foundation) - // Event stream output (experimental) + // Event stream output if let eventStreamOutputPath = args.eventStreamOutputPath { let file = try FileHandle(forWritingAtPath: eventStreamOutputPath) - let eventHandler = try eventHandlerForStreamingEvents(version: args.eventStreamVersion, encodeAsJSONLines: true) { json in + let eventHandler = try eventHandlerForStreamingEvents(withVersionNumber: args.eventStreamVersionNumber, encodeAsJSONLines: true) { json in _ = try? file.withLock { try file.write(json) try file.write("\n") @@ -598,13 +621,13 @@ public func configurationForEntryPoint(from args: __CommandLineArguments_v0) thr if args.isWarningIssueRecordedEventEnabled == true { configuration.eventHandlingOptions.isWarningIssueRecordedEventEnabled = true } else { - switch args.eventStreamVersion { - case .some(...0): - // If the event stream version was explicitly specified to a value < 1, + switch args.eventStreamVersionNumber { + case .some(.. Void ) throws -> Event.Handler { - func eventHandler(for version: (some ABI.Version).Type) -> Event.Handler { - return version.eventHandler(encodeAsJSONLines: encodeAsJSONLines, forwardingTo: targetEventHandler) - } - - return switch versionNumber { - case nil: - eventHandler(for: ABI.CurrentVersion.self) -#if !SWT_NO_SNAPSHOT_TYPES - case ABI.Xcode16.versionNumber: - // Legacy support for Xcode 16. Support for this undocumented version will - // be removed in a future update. Do not use it. - eventHandler(for: ABI.Xcode16.self) -#endif - case ABI.v0.versionNumber: - eventHandler(for: ABI.v0.self) - case ABI.v1.versionNumber: - eventHandler(for: ABI.v1.self) - case let .some(unsupportedVersionNumber): - throw _EntryPointError.invalidArgument("--event-stream-version", value: "\(unsupportedVersionNumber)") + let versionNumber = versionNumber ?? ABI.CurrentVersion.versionNumber + guard let abi = ABI.version(forVersionNumber: versionNumber) else { + throw _EntryPointError.invalidArgument("--event-stream-version", value: "\(versionNumber)") } + return abi.eventHandler(encodeAsJSONLines: encodeAsJSONLines, forwardingTo: targetEventHandler) } #endif @@ -814,7 +822,7 @@ private enum _EntryPointError: Error { /// /// - Parameters: /// - versionNumber: The experimental ABI version number. - case experimentalABIVersion(_ versionNumber: Int) + case experimentalABIVersion(_ versionNumber: ABI.VersionNumber) } extension _EntryPointError: CustomStringConvertible { @@ -829,3 +837,17 @@ extension _EntryPointError: CustomStringConvertible { } } } + +// MARK: - Deprecated + +extension __CommandLineArguments_v0 { + @available(*, deprecated, message: "Use eventStreamSchemaVersion instead.") + public var eventStreamVersion: Int? { + get { + eventStreamVersionNumber.map(\.majorComponent).map(Int.init) + } + set { + eventStreamVersionNumber = newValue.map { ABI.VersionNumber(majorComponent: Int8(clamping: $0), minorComponent: 0) } + } + } +} diff --git a/Sources/Testing/CMakeLists.txt b/Sources/Testing/CMakeLists.txt index 5b84aeaf3..8ddaa2cb5 100644 --- a/Sources/Testing/CMakeLists.txt +++ b/Sources/Testing/CMakeLists.txt @@ -13,6 +13,7 @@ add_library(Testing ABI/ABI.Record.swift ABI/ABI.Record+Streaming.swift ABI/ABI.swift + ABI/ABI.VersionNumber.swift ABI/Encoded/ABI.EncodedAttachment.swift ABI/Encoded/ABI.EncodedBacktrace.swift ABI/Encoded/ABI.EncodedError.swift diff --git a/Sources/Testing/ExitTests/ExitTest.swift b/Sources/Testing/ExitTests/ExitTest.swift index 737ff8463..14f32d9d9 100644 --- a/Sources/Testing/ExitTests/ExitTest.swift +++ b/Sources/Testing/ExitTests/ExitTest.swift @@ -544,7 +544,7 @@ extension ABI { /// The back channel always uses the latest ABI version (even if experimental) /// since both the producer and consumer use this exact version of the testing /// library. - fileprivate typealias BackChannelVersion = v1 + fileprivate typealias BackChannelVersion = v6_3 } @_spi(ForToolsIntegrationOnly) diff --git a/Tests/TestingTests/ABIEntryPointTests.swift b/Tests/TestingTests/ABIEntryPointTests.swift index 690889451..a44942e2a 100644 --- a/Tests/TestingTests/ABIEntryPointTests.swift +++ b/Tests/TestingTests/ABIEntryPointTests.swift @@ -21,7 +21,7 @@ struct ABIEntryPointTests { @Test func v0() async throws { var arguments = __CommandLineArguments_v0() arguments.filter = ["NonExistentTestThatMatchesNothingHopefully"] - arguments.eventStreamVersion = 0 + arguments.eventStreamSchemaVersion = "0" arguments.verbosity = .min let result = try await _invokeEntryPointV0(passing: arguments) { recordJSON in @@ -36,7 +36,7 @@ struct ABIEntryPointTests { func v0_manyFilters() async throws { var arguments = __CommandLineArguments_v0() arguments.filter = (1...100).map { "NonExistentTestThatMatchesNothingHopefully_\($0)" } - arguments.eventStreamVersion = 0 + arguments.eventStreamSchemaVersion = "0" arguments.verbosity = .min let result = try await _invokeEntryPointV0(passing: arguments) @@ -48,7 +48,7 @@ struct ABIEntryPointTests { func v0_listingTestsOnly() async throws { var arguments = __CommandLineArguments_v0() arguments.listTests = true - arguments.eventStreamVersion = 0 + arguments.eventStreamSchemaVersion = "0" arguments.verbosity = .min try await confirmation("Test matched", expectedCount: 1...) { testMatched in @@ -105,7 +105,7 @@ struct ABIEntryPointTests { } @Test func decodeWrongRecordVersion() throws { - let record = ABI.Record(encoding: Test {}) + let record = ABI.Record(encoding: Test {}) let error = try JSON.withEncoding(of: record) { recordJSON in try #require(throws: DecodingError.self) { _ = try JSON.decode(ABI.Record.self, from: recordJSON) @@ -114,9 +114,63 @@ struct ABIEntryPointTests { guard case let .dataCorrupted(context) = error else { throw error } - #expect(context.debugDescription == "Unexpected record version 1 (expected 0).") + #expect(context.debugDescription == "Unexpected record version 6.3 (expected 0).") + } + + @Test func decodeVersionNumber() throws { + let version0 = try JSON.withEncoding(of: 0) { versionJSON in + try JSON.decode(ABI.VersionNumber.self, from: versionJSON) + } + #expect(version0 == ABI.VersionNumber(0, 0)) + + let version1_2_3 = try JSON.withEncoding(of: "1.2.3") { versionJSON in + try JSON.decode(ABI.VersionNumber.self, from: versionJSON) + } + #expect(version1_2_3.majorComponent == 1) + #expect(version1_2_3.minorComponent == 2) + #expect(version1_2_3.patchComponent == 3) + + #expect(throws: DecodingError.self) { + _ = try JSON.withEncoding(of: "not.valid") { versionJSON in + try JSON.decode(ABI.VersionNumber.self, from: versionJSON) + } + } } #endif + + @Test(arguments: [ + (ABI.VersionNumber(-1, 0), "-1"), + (ABI.VersionNumber(0, 0), "0"), + (ABI.VersionNumber(1, 0), "1.0"), + (ABI.VersionNumber(2, 0), "2.0"), + (ABI.VersionNumber("0.0.1"), "0.0.1"), + (ABI.VersionNumber("0.1.0"), "0.1"), + ]) func abiVersionStringConversion(version: ABI.VersionNumber?, expectedString: String) throws { + let version = try #require(version) + #expect(String(describing: version) == expectedString) + } + + @Test func badABIVersionString() { + let version = ABI.VersionNumber("not.valid") + #expect(version == nil) + } + + @Test func abiVersionComparisons() throws { + var versions = [ABI.VersionNumber]() + for major in 0 ..< 10 { + let version = try #require(ABI.VersionNumber("\(major)")) + versions.append(version) + for minor in 0 ..< 10 { + let version = try #require(ABI.VersionNumber("\(major).\(minor)")) + versions.append(version) + for patch in 0 ..< 10 { + let version = try #require(ABI.VersionNumber("\(major).\(minor).\(patch)")) + versions.append(version) + } + } + } + #expect(versions == versions.shuffled().sorted()) + } } #if !SWT_NO_DYNAMIC_LINKING diff --git a/Tests/TestingTests/EntryPointTests.swift b/Tests/TestingTests/EntryPointTests.swift index eae7d4b7e..a5ca1ce84 100644 --- a/Tests/TestingTests/EntryPointTests.swift +++ b/Tests/TestingTests/EntryPointTests.swift @@ -18,7 +18,7 @@ struct EntryPointTests { var arguments = __CommandLineArguments_v0() arguments.filter = ["_someHiddenTest"] arguments.includeHiddenTests = true - arguments.eventStreamVersion = 0 + arguments.eventStreamSchemaVersion = "0" arguments.verbosity = .min await confirmation("Test event started", expectedCount: 1) { testMatched in @@ -35,7 +35,7 @@ struct EntryPointTests { var arguments = __CommandLineArguments_v0() arguments.filter = ["_recordWarningIssue"] arguments.includeHiddenTests = true - arguments.eventStreamVersion = 0 + arguments.eventStreamSchemaVersion = "0" arguments.verbosity = .min let exitCode = await confirmation("Test matched", expectedCount: 1) { testMatched in @@ -55,7 +55,7 @@ struct EntryPointTests { var arguments = __CommandLineArguments_v0() arguments.filter = ["_recordWarningIssue"] arguments.includeHiddenTests = true - arguments.eventStreamVersion = 0 + arguments.eventStreamSchemaVersion = "0" arguments.isWarningIssueRecordedEventEnabled = true arguments.verbosity = .min diff --git a/Tests/TestingTests/SwiftPMTests.swift b/Tests/TestingTests/SwiftPMTests.swift index 5008a271d..64952fb4f 100644 --- a/Tests/TestingTests/SwiftPMTests.swift +++ b/Tests/TestingTests/SwiftPMTests.swift @@ -230,21 +230,60 @@ struct SwiftPMTests { #expect(args.parallel == false) } + @available(*, deprecated) + @Test("Deprecated eventStreamVersion property") + func deprecatedEventStreamVersionProperty() async throws { + var args = __CommandLineArguments_v0() + args.eventStreamVersion = 0 + #expect(args.eventStreamVersionNumber == ABI.VersionNumber(0, 0)) + #expect(args.eventStreamSchemaVersion == "0") + + args.eventStreamVersion = -1 + #expect(args.eventStreamVersionNumber == ABI.VersionNumber(-1, 0)) + #expect(args.eventStreamSchemaVersion == "-1") + + args.eventStreamVersion = 123 + #expect(args.eventStreamVersionNumber == ABI.VersionNumber(123, 0)) + #expect(args.eventStreamSchemaVersion == "123.0") + + args.eventStreamVersionNumber = ABI.VersionNumber(10, 20, 30) + #expect(args.eventStreamVersion == 10) + #expect(args.eventStreamSchemaVersion == "10.20.30") + + args.eventStreamSchemaVersion = "10.20.30" + #expect(args.eventStreamVersionNumber == ABI.VersionNumber(10, 20, 30)) + #expect(args.eventStreamVersion == 10) + +#if !SWT_NO_EXIT_TESTS + await #expect(processExitsWith: .failure) { + var args = __CommandLineArguments_v0() + args.eventStreamSchemaVersion = "invalidVersionString" + } +#endif + } + + @Test("New-but-not-experimental ABI version") + func newButNotExperimentalABIVersion() async throws { + let versionNumber = ABI.VersionNumber(0, 0, 1) + let version = try #require(ABI.version(forVersionNumber: versionNumber)) + #expect(version.versionNumber == ABI.v0.versionNumber) + } + + @Test("Unsupported ABI version") + func unsupportedABIVersion() async throws { + let versionNumber = ABI.VersionNumber(-100, 0) + #expect(ABI.version(forVersionNumber: versionNumber) == nil) + } + @Test("--event-stream-output-path argument (writes to a stream and can be read back)", arguments: [ ("--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.v1.versionNumber), + ("--experimental-event-stream-output", "--experimental-event-stream-version", ABI.v6_3.versionNumber), ]) - func eventStreamOutput(outputArgumentName: String, versionArgumentName: String, version: Int) async throws { - switch version { - case ABI.v0.versionNumber: - try await eventStreamOutput(outputArgumentName: outputArgumentName, versionArgumentName: versionArgumentName, version: ABI.v0.self) - case ABI.v1.versionNumber: - try await eventStreamOutput(outputArgumentName: outputArgumentName, versionArgumentName: versionArgumentName, version: ABI.v1.self) - default: - Issue.record("Unreachable event stream version \(version)") - } + func eventStreamOutput(outputArgumentName: String, versionArgumentName: String, version: ABI.VersionNumber) async throws { + let version = try #require(ABI.version(forVersionNumber: version)) + try await eventStreamOutput(outputArgumentName: outputArgumentName, versionArgumentName: versionArgumentName, version: version) } func eventStreamOutput(outputArgumentName: String, versionArgumentName: String, version: V.Type) async throws where V: ABI.Version { @@ -286,7 +325,7 @@ struct SwiftPMTests { } #expect(testRecords.count == 1) for testRecord in testRecords { - if version.versionNumber >= ABI.v1.versionNumber { + if version.versionNumber >= ABI.v6_3.versionNumber { #expect(testRecord._tags != nil) } else { #expect(testRecord._tags == nil) @@ -304,7 +343,8 @@ struct SwiftPMTests { @Test("Experimental ABI version requires --experimental-event-stream-version argument") func experimentalABIVersionNeedsExperimentalFlag() { #expect(throws: (any Error).self) { - let experimentalVersion = ABI.CurrentVersion.versionNumber + 1 + var experimentalVersion = ABI.CurrentVersion.versionNumber + experimentalVersion.minorComponent += 1 _ = try configurationForEntryPoint(withArguments: ["PATH", "--event-stream-version", "\(experimentalVersion)"]) } } From 8502f73b2d7e10b8d49f0b439a2f7397b059f369 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Tue, 29 Jul 2025 19:28:56 -0400 Subject: [PATCH 079/216] Add additional `canImport` checks to AttachmentTests.swift (#1244) This PR adds more `canImport` checks to `AttachmentTests` so that, if we're not building e.g. `_Testing_AppKit`, the file still compiles. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- Tests/TestingTests/AttachmentTests.swift | 26 ++++++++++++------------ 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/Tests/TestingTests/AttachmentTests.swift b/Tests/TestingTests/AttachmentTests.swift index 6cc30e608..f8ee5d0ba 100644 --- a/Tests/TestingTests/AttachmentTests.swift +++ b/Tests/TestingTests/AttachmentTests.swift @@ -10,23 +10,23 @@ @testable @_spi(ForToolsIntegrationOnly) import Testing private import _TestingInternals -#if canImport(AppKit) +#if canImport(AppKit) && canImport(_Testing_AppKit) import AppKit @_spi(Experimental) import _Testing_AppKit #endif -#if canImport(Foundation) +#if canImport(Foundation) && canImport(_Testing_Foundation) import Foundation import _Testing_Foundation #endif -#if canImport(CoreGraphics) +#if canImport(CoreGraphics) && canImport(_Testing_CoreGraphics) import CoreGraphics @_spi(Experimental) import _Testing_CoreGraphics #endif -#if canImport(CoreImage) +#if canImport(CoreImage) && canImport(_Testing_CoreImage) import CoreImage @_spi(Experimental) import _Testing_CoreImage #endif -#if canImport(UIKit) +#if canImport(UIKit) && canImport(_Testing_UIKit) import UIKit @_spi(Experimental) import _Testing_UIKit #endif @@ -259,7 +259,7 @@ struct AttachmentTests { } } -#if canImport(Foundation) +#if canImport(Foundation) && canImport(_Testing_Foundation) #if !SWT_NO_FILE_IO @Test func attachContentsOfFileURL() async throws { let data = try #require("".data(using: .utf8)) @@ -481,7 +481,7 @@ extension AttachmentTests { try test(value) } -#if canImport(Foundation) +#if canImport(Foundation) && canImport(_Testing_Foundation) @Test func data() throws { let value = try #require("abc123".data(using: .utf8)) try test(value) @@ -499,7 +499,7 @@ extension AttachmentTests { case couldNotCreateCGImage } -#if canImport(CoreGraphics) +#if canImport(CoreGraphics) && canImport(_Testing_CoreGraphics) static let cgImage = Result { let size = CGSize(width: 32.0, height: 32.0) let rgb = CGColorSpaceCreateDeviceRGB() @@ -606,7 +606,7 @@ extension AttachmentTests { } #endif -#if canImport(CoreImage) +#if canImport(CoreImage) && canImport(_Testing_CoreImage) @available(_uttypesAPI, *) @Test func attachCIImage() throws { let image = CIImage(cgImage: try Self.cgImage.get()) @@ -618,7 +618,7 @@ extension AttachmentTests { } #endif -#if canImport(AppKit) +#if canImport(AppKit) && canImport(_Testing_AppKit) static var nsImage: NSImage { get throws { let cgImage = try cgImage.get() @@ -683,7 +683,7 @@ extension AttachmentTests { } #endif -#if canImport(UIKit) +#if canImport(UIKit) && canImport(_Testing_UIKit) @available(_uttypesAPI, *) @Test func attachUIImage() throws { let image = UIImage(cgImage: try Self.cgImage.get()) @@ -742,7 +742,7 @@ struct MySendableAttachableWithDefaultByteCount: Attachable, Sendable { } } -#if canImport(Foundation) +#if canImport(Foundation) && canImport(_Testing_Foundation) struct MyCodableAttachable: Codable, Attachable, Sendable { var string: String } @@ -784,7 +784,7 @@ final class MyCodableAndSecureCodingAttachable: NSObject, Codable, NSSecureCodin } #endif -#if canImport(AppKit) +#if canImport(AppKit) && canImport(_Testing_AppKit) private final class MyImage: NSImage { override init(size: NSSize) { super.init(size: size) From 9edaba8cbbed6add10c138380cf4413eb713cbf0 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Tue, 29 Jul 2025 19:39:27 -0400 Subject: [PATCH 080/216] Work around a crash on Windows. Details TBD, but this test is crashing the test harness on my local install of Windows 11 for ARM64. I don't have any useful diagnostic info. --- Tests/TestingTests/SwiftPMTests.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Tests/TestingTests/SwiftPMTests.swift b/Tests/TestingTests/SwiftPMTests.swift index 64952fb4f..4a9b70502 100644 --- a/Tests/TestingTests/SwiftPMTests.swift +++ b/Tests/TestingTests/SwiftPMTests.swift @@ -272,7 +272,8 @@ struct SwiftPMTests { @Test("Unsupported ABI version") func unsupportedABIVersion() async throws { let versionNumber = ABI.VersionNumber(-100, 0) - #expect(ABI.version(forVersionNumber: versionNumber) == nil) + let versionTypeInfo = ABI.version(forVersionNumber: versionNumber).map(TypeInfo.init(describing:)) + #expect(versionTypeInfo == nil) } @Test("--event-stream-output-path argument (writes to a stream and can be read back)", From f0047e6fb754fa52b306d642ec8c1257c16d5eae Mon Sep 17 00:00:00 2001 From: Stuart Montgomery Date: Wed, 30 Jul 2025 07:51:08 -0500 Subject: [PATCH 081/216] Revert remaining workaround preventing the use of SymbolLinkageMarkers (#1234) --- Package.swift | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Package.swift b/Package.swift index 40d64e6c6..f3d5a1691 100644 --- a/Package.swift +++ b/Package.swift @@ -341,10 +341,7 @@ extension Array where Element == PackageDescription.SwiftSetting { // This setting is enabled in the package, but not in the toolchain build // (via CMake). Enabling it is dependent on acceptance of the @section // proposal via Swift Evolution. - // - // FIXME: Re-enable this once a CI blocker is resolved: - // https://github.com/swiftlang/swift-testing/issues/1138. -// .enableExperimentalFeature("SymbolLinkageMarkers"), + .enableExperimentalFeature("SymbolLinkageMarkers"), // This setting is no longer needed when building with a 6.2 or later // toolchain now that SE-0458 has been accepted and implemented, but it is From 0d85d8b32bc93afb30f52346b3612a86ebbb11f3 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Fri, 1 Aug 2025 18:52:35 -0400 Subject: [PATCH 082/216] Windows image attachments (#1245) --- Package.swift | 10 + .../AttachableAsGDIPlusImage.swift | 129 +++++++++ .../AttachableImageFormat+CLSID.swift | 266 ++++++++++++++++++ .../Attachment+AttachableAsGDIPlusImage.swift | 99 +++++++ .../_Testing_WinSDK/Attachments/GDI+.swift | 71 +++++ .../HBITMAP+AttachableAsGDIPlusImage.swift | 27 ++ .../HICON+AttachableAsGDIPlusImage.swift | 27 ++ ...ablePointer+AttachableAsGDIPlusImage.swift | 24 ++ .../Attachments/_AttachableImageWrapper.swift | 132 +++++++++ .../_Testing_WinSDK/ReexportTesting.swift | 11 + .../Attachments/AttachableImageFormat.swift | 3 +- Sources/Testing/Support/CError.swift | 10 +- Sources/Testing/Support/Environment.swift | 10 +- Sources/_TestingInternals/GDI+/include/GDI+.h | 71 +++++ Sources/_TestingInternals/include/Includes.h | 1 - Sources/_TestingInternals/include/Stubs.h | 18 ++ .../_TestingInternals/include/TestSupport.h | 6 + .../include/module.modulemap | 7 + Tests/TestingTests/AttachmentTests.swift | 88 ++++++ 19 files changed, 1000 insertions(+), 10 deletions(-) create mode 100644 Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsGDIPlusImage.swift create mode 100644 Sources/Overlays/_Testing_WinSDK/Attachments/AttachableImageFormat+CLSID.swift create mode 100644 Sources/Overlays/_Testing_WinSDK/Attachments/Attachment+AttachableAsGDIPlusImage.swift create mode 100644 Sources/Overlays/_Testing_WinSDK/Attachments/GDI+.swift create mode 100644 Sources/Overlays/_Testing_WinSDK/Attachments/HBITMAP+AttachableAsGDIPlusImage.swift create mode 100644 Sources/Overlays/_Testing_WinSDK/Attachments/HICON+AttachableAsGDIPlusImage.swift create mode 100644 Sources/Overlays/_Testing_WinSDK/Attachments/UnsafeMutablePointer+AttachableAsGDIPlusImage.swift create mode 100644 Sources/Overlays/_Testing_WinSDK/Attachments/_AttachableImageWrapper.swift create mode 100644 Sources/Overlays/_Testing_WinSDK/ReexportTesting.swift create mode 100644 Sources/_TestingInternals/GDI+/include/GDI+.h diff --git a/Package.swift b/Package.swift index f3d5a1691..58ba9552a 100644 --- a/Package.swift +++ b/Package.swift @@ -92,6 +92,7 @@ let package = Package( "_Testing_CoreGraphics", "_Testing_CoreImage", "_Testing_UIKit", + "_Testing_WinSDK", ] ) ] @@ -135,6 +136,7 @@ let package = Package( "_Testing_CoreImage", "_Testing_Foundation", "_Testing_UIKit", + "_Testing_WinSDK", "MemorySafeTestingTests", ], swiftSettings: .packageSettings @@ -246,6 +248,14 @@ let package = Package( path: "Sources/Overlays/_Testing_UIKit", swiftSettings: .packageSettings + .enableLibraryEvolution() ), + .target( + name: "_Testing_WinSDK", + dependencies: [ + "Testing", + ], + path: "Sources/Overlays/_Testing_WinSDK", + swiftSettings: .packageSettings + .enableLibraryEvolution() + [.interoperabilityMode(.Cxx)] + ), // Utility targets: These are utilities intended for use when developing // this package, not for distribution. diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsGDIPlusImage.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsGDIPlusImage.swift new file mode 100644 index 000000000..daaf88df1 --- /dev/null +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsGDIPlusImage.swift @@ -0,0 +1,129 @@ +// +// 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 +// + +#if os(Windows) +@_spi(Experimental) import Testing +private import _TestingInternals.GDIPlus + +internal import WinSDK + +/// A protocol describing images that can be converted to instances of +/// ``Testing/Attachment``. +/// +/// Instances of types conforming to this protocol do not themselves conform to +/// ``Testing/Attachable``. Instead, the testing library provides additional +/// initializers on ``Testing/Attachment`` that take instances of such types and +/// handle converting them to image data when needed. +/// +/// The following system-provided image types conform to this protocol and can +/// be attached to a test: +/// +/// - [`HBITMAP`](https://learn.microsoft.com/en-us/windows/win32/gdi/bitmaps) +/// - [`HICON`](https://learn.microsoft.com/en-us/windows/win32/menurc/icons) +/// +/// You do not generally need to add your own conformances to this protocol. If +/// you have an image in another format that needs to be attached to a test, +/// first convert it to an instance of one of the types above. +@_spi(Experimental) +public protocol _AttachableByAddressAsGDIPlusImage { + /// Create a GDI+ image representing an instance of this type at the given + /// address. + /// + /// - Parameters: + /// - imageAddress: The address of the instance of this type. + /// + /// - Returns: A pointer to a new GDI+ image representing this image. The + /// caller is responsible for deleting this image when done with it. + /// + /// - Throws: Any error that prevented the creation of the GDI+ image. + /// + /// - Note: This function returns a value of C++ type `Gdiplus::Image *`. That + /// type cannot be directly represented in Swift. If this function returns a + /// value of any other concrete type, the result is undefined. + /// + /// The testing library automatically calls `GdiplusStartup()` and + /// `GdiplusShutdown()` before and after calling this function. This function + /// can therefore assume that GDI+ is correctly configured on the current + /// thread when it is called. + /// + /// This function is not part of the public interface of the testing library. + /// It may be removed in a future update. + static func _copyAttachableGDIPlusImage(at imageAddress: UnsafeMutablePointer) throws -> OpaquePointer + + /// Clean up any resources at the given address. + /// + /// - Parameters: + /// - imageAddress: The address of the instance of this type. + /// + /// The implementation of this function cleans up any resources (such as + /// handles or COM objects) associated with this value. The testing library + /// automatically invokes this function as needed. + /// + /// This function is not responsible for deleting the image returned from + /// `_copyAttachableGDIPlusImage(at:)`. + /// + /// This function is not part of the public interface of the testing library. + /// It may be removed in a future update. + static func _cleanUpAttachment(at imageAddress: UnsafeMutablePointer) +} + +/// A protocol describing images that can be converted to instances of +/// ``Testing/Attachment``. +/// +/// Instances of types conforming to this protocol do not themselves conform to +/// ``Testing/Attachable``. Instead, the testing library provides additional +/// initializers on ``Testing/Attachment`` that take instances of such types and +/// handle converting them to image data when needed. +/// +/// The following system-provided image types conform to this protocol and can +/// be attached to a test: +/// +/// - [`HBITMAP`](https://learn.microsoft.com/en-us/windows/win32/gdi/bitmaps) +/// - [`HICON`](https://learn.microsoft.com/en-us/windows/win32/menurc/icons) +/// +/// You do not generally need to add your own conformances to this protocol. If +/// you have an image in another format that needs to be attached to a test, +/// first convert it to an instance of one of the types above. +@_spi(Experimental) +public protocol AttachableAsGDIPlusImage { + /// Create a GDI+ image representing this instance. + /// + /// - Returns: A pointer to a new GDI+ image representing this image. The + /// caller is responsible for deleting this image when done with it. + /// + /// - Throws: Any error that prevented the creation of the GDI+ image. + /// + /// - Note: This function returns a value of C++ type `Gdiplus::Image *`. That + /// type cannot be directly represented in Swift. If this function returns a + /// value of any other concrete type, the result is undefined. + /// + /// The testing library automatically calls `GdiplusStartup()` and + /// `GdiplusShutdown()` before and after calling this function. This function + /// can therefore assume that GDI+ is correctly configured on the current + /// thread when it is called. + /// + /// This function is not part of the public interface of the testing library. + /// It may be removed in a future update. + func _copyAttachableGDIPlusImage() throws -> OpaquePointer + + /// Clean up any resources associated with this instance. + /// + /// The implementation of this function cleans up any resources (such as + /// handles or COM objects) associated with this value. The testing library + /// automatically invokes this function as needed. + /// + /// This function is not responsible for deleting the image returned from + /// `_copyAttachableGDIPlusImage()`. + /// + /// This function is not part of the public interface of the testing library. + /// It may be removed in a future update. + func _cleanUpAttachment() +} +#endif diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableImageFormat+CLSID.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableImageFormat+CLSID.swift new file mode 100644 index 000000000..8985b2aeb --- /dev/null +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableImageFormat+CLSID.swift @@ -0,0 +1,266 @@ +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024–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 os(Windows) +@_spi(Experimental) import Testing +private import _TestingInternals.GDIPlus + +public import WinSDK + +extension AttachableImageFormat { + /// The set of `ImageCodecInfo` instances known to GDI+. + /// + /// If the testing library was unable to determine the set of image formats, + /// the value of this property is `nil`. + /// + /// - Note: The type of this property is a buffer pointer rather than an array + /// because the resulting buffer owns trailing untyped memory where path + /// extensions and other fields are stored. Do not deallocate this buffer. + private static nonisolated(unsafe) let _allCodecs: UnsafeBufferPointer = { + let result = try? withGDIPlus { + // Find out the size of the buffer needed. + var codecCount = UINT(0) + var byteCount = UINT(0) + let rGetSize = Gdiplus.GetImageEncodersSize(&codecCount, &byteCount) + guard rGetSize == Gdiplus.Ok else { + throw GDIPlusError.status(rGetSize) + } + + // Allocate a buffer of sufficient byte size, then bind the leading bytes + // to ImageCodecInfo. This leaves some number of trailing bytes unbound to + // any Swift type. + let result = UnsafeMutableRawBufferPointer.allocate( + byteCount: Int(byteCount), + alignment: MemoryLayout.alignment + ) + let codecBuffer = result + .prefix(MemoryLayout.stride * Int(codecCount)) + .bindMemory(to: Gdiplus.ImageCodecInfo.self) + + // Read the encoders list. + let rGetEncoders = Gdiplus.GetImageEncoders(codecCount, byteCount, codecBuffer.baseAddress!) + guard rGetEncoders == Gdiplus.Ok else { + result.deallocate() + throw GDIPlusError.status(rGetEncoders) + } + return UnsafeBufferPointer(codecBuffer) + } + return result ?? UnsafeBufferPointer(start: nil, count: 0) + }() + + /// Get the set of path extensions corresponding to the image format + /// represented by a GDI+ codec info structure. + /// + /// - Parameters: + /// - codec: The GDI+ codec info structure of interest. + /// + /// - Returns: An array of zero or more path extensions. The case of the + /// resulting strings is unspecified. + private static func _pathExtensions(for codec: Gdiplus.ImageCodecInfo) -> [String] { + guard let extensions = String.decodeCString(codec.FilenameExtension, as: UTF16.self)?.result else { + return [] + } + return extensions + .split(separator: ";") + .map { ext in + if ext.starts(with: "*.") { + ext.dropFirst(2) + } else { + ext[...] + } + }.map{ $0.lowercased() } // Vestiges of MS-DOS... + } + + /// Get the `CLSID` value corresponding to the same image format as the given + /// path extension. + /// + /// - Parameters: + /// - pathExtension: The path extension (as a wide C string) for which a + /// `CLSID` value is needed. + /// + /// - Returns: An instance of `CLSID` referring to a concrete image type, or + /// `nil` if one could not be determined. + private static func _computeCLSID(forPathExtension pathExtension: UnsafePointer) -> CLSID? { + _allCodecs.first { codec in + _pathExtensions(for: codec) + .contains { codecExtension in + codecExtension.withCString(encodedAs: UTF16.self) { codecExtension in + 0 == _wcsicmp(pathExtension, codecExtension) + } + } + }.map(\.Clsid) + } + + /// Get the `CLSID` value corresponding to the same image format as the given + /// path extension. + /// + /// - Parameters: + /// - pathExtension: The path extension for which a `CLSID` value is needed. + /// + /// - Returns: An instance of `CLSID` referring to a concrete image type, or + /// `nil` if one could not be determined. + private static func _computeCLSID(forPathExtension pathExtension: String) -> CLSID? { + pathExtension.withCString(encodedAs: UTF16.self) { pathExtension in + _computeCLSID(forPathExtension: pathExtension) + } + } + + /// Get the `CLSID` value corresponding to the same image format as the path + /// extension on the given attachment filename. + /// + /// - Parameters: + /// - preferredName: The preferred name of the image for which a `CLSID` + /// value is needed. + /// + /// - Returns: An instance of `CLSID` referring to a concrete image type, or + /// `nil` if one could not be determined. + private static func _computeCLSID(forPreferredName preferredName: String) -> CLSID? { + preferredName.withCString(encodedAs: UTF16.self) { (preferredName) -> CLSID? in + // Get the path extension on the preferred name, if any. + var dot: PCWSTR? + guard S_OK == PathCchFindExtension(preferredName, wcslen(preferredName) + 1, &dot), let dot, dot[0] != 0 else { + return nil + } + return _computeCLSID(forPathExtension: dot + 1) + } + } + + /// Get the `CLSID` value` to use when encoding the image. + /// + /// - Parameters: + /// - imageFormat: The image format to use, or `nil` if the developer did + /// not specify one. + /// - preferredName: The preferred name of the image for which a type is + /// needed. + /// + /// - Returns: An instance of `CLSID` referring to a concrete image type, or + /// `nil` if one could not be determined. + /// + /// This function is not part of the public interface of the testing library. + static func computeCLSID(for imageFormat: Self?, withPreferredName preferredName: String) -> CLSID { + if let clsid = imageFormat?.clsid { + return clsid + } + + // The developer didn't specify a CLSID, or we couldn't figure one out from + // context, so try to derive one from the preferred name's path extension. + if let inferredCLSID = _computeCLSID(forPreferredName: preferredName) { + return inferredCLSID + } + + // We couldn't derive a concrete type from the path extension, so default + // to PNG. Unlike Apple platforms, there's no abstract "image" type on + // Windows so we don't need to make any more decisions. + return _pngCLSID + } + + /// Append the path extension preferred by GDI+ for the given `CLSID` value + /// representing an image format to a suggested extension filename. + /// + /// - Parameters: + /// - clsid: The `CLSID` value representing the image format of interest. + /// - preferredName: The preferred name of the image for which a type is + /// needed. + /// + /// - Returns: A string containing the corresponding path extension, or `nil` + /// if none could be determined. + static func appendPathExtension(for clsid: CLSID, to preferredName: String) -> String { + // If there's already a CLSID associated with the filename, and it matches + // the one passed to us, no changes are needed. + if let existingCLSID = _computeCLSID(forPreferredName: preferredName), clsid == existingCLSID { + return preferredName + } + + let ext = _allCodecs + .first { $0.Clsid == clsid } + .flatMap { _pathExtensions(for: $0).first } + guard let ext else { + // Couldn't find a path extension for the given CLSID, so make no changes. + return preferredName + } + + return "\(preferredName).\(ext)" + } + + /// The `CLSID` value corresponding to the PNG image format. + /// + /// - Note: The named constant [`ImageFormatPNG`](https://learn.microsoft.com/en-us/windows/win32/gdiplus/-gdiplus-constant-image-file-format-constants) + /// is not the correct value and will cause `Image::Save()` to fail if + /// passed to it. + private static let _pngCLSID = _computeCLSID(forPathExtension: "png")! + + /// The `CLSID` value corresponding to the JPEG image format. + /// + /// - Note: The named constant [`ImageFormatJPEG`](https://learn.microsoft.com/en-us/windows/win32/gdiplus/-gdiplus-constant-image-file-format-constants) + /// is not the correct value and will cause `Image::Save()` to fail if + /// passed to it. + private static let _jpegCLSID = _computeCLSID(forPathExtension: "jpg")! + + /// The `CLSID` value corresponding to this image format. + public var clsid: CLSID { + switch kind { + case .png: + Self._pngCLSID + case .jpeg: + Self._jpegCLSID + case let .systemValue(clsid): + clsid as! CLSID + } + } + + /// Construct an instance of this type with the given `CLSID` value and + /// encoding quality. + /// + /// - Parameters: + /// - clsid: The `CLSID` value corresponding to the image format to use when + /// encoding images. + /// - encodingQuality: The encoding quality to use when encoding images. For + /// the lowest supported quality, pass `0.0`. For the highest supported + /// quality, pass `1.0`. + /// + /// If the target image format does not support variable-quality encoding, + /// the value of the `encodingQuality` argument is ignored. + /// + /// If `clsid` does not represent an image format supported by GDI+, the + /// result is undefined. For a list of image formats supported by GDI+, see + /// the [GetImageEncoders()](https://learn.microsoft.com/en-us/windows/win32/api/gdiplusimagecodec/nf-gdiplusimagecodec-getimageencoders) + /// function. + public init(_ clsid: CLSID, encodingQuality: Float = 1.0) { + self.init(kind: .systemValue(clsid), encodingQuality: encodingQuality) + } + + /// Construct an instance of this type with the given path extension and + /// encoding quality. + /// + /// - Parameters: + /// - pathExtension: A path extension corresponding to the image format to + /// use when encoding images. + /// - encodingQuality: The encoding quality to use when encoding images. For + /// the lowest supported quality, pass `0.0`. For the highest supported + /// quality, pass `1.0`. + /// + /// If the target image format does not support variable-quality encoding, + /// the value of the `encodingQuality` argument is ignored. + /// + /// If `pathExtension` does not correspond to an image format supported by + /// GDI+, this initializer returns `nil`. For a list of image formats + /// supported by GDI+, see the [GetImageEncoders()](https://learn.microsoft.com/en-us/windows/win32/api/gdiplusimagecodec/nf-gdiplusimagecodec-getimageencoders) + /// function. + public init?(pathExtension: String, encodingQuality: Float = 1.0) { + let pathExtension = pathExtension.drop { $0 == "." } + let clsid = Self._computeCLSID(forPathExtension: String(pathExtension)) + if let clsid { + self.init(clsid, encodingQuality: encodingQuality) + } else { + return nil + } + } +} +#endif diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/Attachment+AttachableAsGDIPlusImage.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/Attachment+AttachableAsGDIPlusImage.swift new file mode 100644 index 000000000..54b24d435 --- /dev/null +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/Attachment+AttachableAsGDIPlusImage.swift @@ -0,0 +1,99 @@ +// +// 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 +// + +#if os(Windows) +@_spi(Experimental) public import Testing + +@_spi(Experimental) +extension Attachment where AttachableValue: ~Copyable { + /// Initialize an instance of this type that encloses the given image. + /// + /// - Parameters: + /// - attachableValue: A pointer to the value that will be attached to the + /// output of the test run. + /// - preferredName: The preferred name of the attachment when writing it + /// to a test report or to disk. If `nil`, the testing library attempts + /// to derive a reasonable filename for the attached value. + /// - imageFormat: The image format with which to encode `attachableValue`. + /// - sourceLocation: The source location of the call to this initializer. + /// This value is used when recording issues associated with the + /// attachment. + /// + /// The following system-provided image types conform to the + /// ``AttachableAsGDIPlusImage`` protocol and can be attached to a test: + /// + /// - [`HBITMAP`](https://learn.microsoft.com/en-us/windows/win32/gdi/bitmaps) + /// - [`HICON`](https://learn.microsoft.com/en-us/windows/win32/menurc/icons) + /// + /// The testing library uses the image format specified by `imageFormat`. Pass + /// `nil` to let the testing library decide which image format to use. If you + /// pass `nil`, then the image format that the testing library uses depends on + /// the path extension you specify in `preferredName`, if any. If you do not + /// specify a path extension, or if the path extension you specify doesn't + /// correspond to an image format the operating system knows how to write, the + /// testing library selects an appropriate image format for you. + /// + /// - Important: The resulting instance of ``Attachment`` takes ownership of + /// `attachableValue` and frees its resources upon deinitialization. If you + /// do not want the testing library to take ownership of this value, call + /// ``Attachment/record(_:named:as:sourceLocation)`` instead of this + /// initializer, or make a copy of the resource before passing it to this + /// initializer. + @unsafe + public init( + _ attachableValue: consuming T, + named preferredName: String? = nil, + as imageFormat: AttachableImageFormat? = nil, + sourceLocation: SourceLocation = #_sourceLocation + ) where AttachableValue == _AttachableImageWrapper { + let imageWrapper = _AttachableImageWrapper(image: attachableValue, imageFormat: imageFormat, cleanUpWhenDone: true) + self.init(imageWrapper, named: preferredName, sourceLocation: sourceLocation) + } + + /// Attach an image to the current test. + /// + /// - Parameters: + /// - image: The value to attach. + /// - preferredName: The preferred name of the attachment when writing it + /// to a test report or to disk. If `nil`, the testing library attempts + /// to derive a reasonable filename for the attached value. + /// - imageFormat: The image format with which to encode `attachableValue`. + /// - sourceLocation: The source location of the call to this initializer. + /// This value is used when recording issues associated with the + /// attachment. + /// + /// This function creates a new instance of ``Attachment`` wrapping `image` + /// and immediately attaches it to the current test. + /// + /// The following system-provided image types conform to the + /// ``AttachableAsGDIPlusImage`` protocol and can be attached to a test: + /// + /// - [`HBITMAP`](https://learn.microsoft.com/en-us/windows/win32/gdi/bitmaps) + /// - [`HICON`](https://learn.microsoft.com/en-us/windows/win32/menurc/icons) + /// + /// The testing library uses the image format specified by `imageFormat`. Pass + /// `nil` to let the testing library decide which image format to use. If you + /// pass `nil`, then the image format that the testing library uses depends on + /// the path extension you specify in `preferredName`, if any. If you do not + /// specify a path extension, or if the path extension you specify doesn't + /// correspond to an image format the operating system knows how to write, the + /// testing library selects an appropriate image format for you. + public static func record( + _ image: borrowing T, + named preferredName: String? = nil, + as imageFormat: AttachableImageFormat? = nil, + sourceLocation: SourceLocation = #_sourceLocation + ) where AttachableValue == _AttachableImageWrapper { + let imageWrapper = _AttachableImageWrapper(image: copy image, imageFormat: imageFormat, cleanUpWhenDone: true) + let attachment = Self(imageWrapper, named: preferredName, sourceLocation: sourceLocation) + Self.record(attachment, sourceLocation: sourceLocation) + } +} +#endif diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/GDI+.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/GDI+.swift new file mode 100644 index 000000000..9535cedfd --- /dev/null +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/GDI+.swift @@ -0,0 +1,71 @@ +// +// 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 +// + +#if os(Windows) +@_spi(Experimental) import Testing +internal import _TestingInternals.GDIPlus + +internal import WinSDK + +/// A type describing errors that can be thrown by GDI+. +enum GDIPlusError: Error { + /// A GDI+ status code. + case status(Gdiplus.Status) + + /// The testing library failed to create an in-memory stream. + case streamCreationFailed(HRESULT) + + /// The testing library failed to get an in-memory stream's underlying buffer. + case globalFromStreamFailed(HRESULT) +} + +extension GDIPlusError: CustomStringConvertible { + var description: String { + switch self { + case let .status(status): + "Could not create the corresponding GDI+ image (Gdiplus.Status \(status.rawValue))." + case let .streamCreationFailed(result): + "Could not create an in-memory stream (HRESULT \(result))." + case let .globalFromStreamFailed(result): + "Could not access the buffer containing the encoded image (HRESULT \(result))." + } + } +} + +// MARK: - + +/// Call a function while GDI+ is set up on the current thread. +/// +/// - Parameters: +/// - body: The function to invoke. +/// +/// - Returns: Whatever is returned by `body`. +/// +/// - Throws: Whatever is thrown by `body`. +func withGDIPlus(_ body: () throws -> R) throws -> R { + // "Escape hatch" if the program being tested calls GdiplusStartup() itself in + // some way that is incompatible with our assumptions about it. + if Environment.flag(named: "SWT_GDIPLUS_STARTUP_ENABLED") == false { + return try body() + } + + var token = ULONG_PTR(0) + var input = Gdiplus.GdiplusStartupInput(nil, false, false) + let rStartup = swt_GdiplusStartup(&token, &input, nil) + guard rStartup == Gdiplus.Ok else { + throw GDIPlusError.status(rStartup) + } + defer { + swt_GdiplusShutdown(token) + } + + return try body() +} +#endif diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/HBITMAP+AttachableAsGDIPlusImage.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/HBITMAP+AttachableAsGDIPlusImage.swift new file mode 100644 index 000000000..466a992ec --- /dev/null +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/HBITMAP+AttachableAsGDIPlusImage.swift @@ -0,0 +1,27 @@ +// +// 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 +// + +#if os(Windows) +import Testing +private import _TestingInternals.GDIPlus + +public import WinSDK + +@_spi(Experimental) +extension HBITMAP__: _AttachableByAddressAsGDIPlusImage { + public static func _copyAttachableGDIPlusImage(at imageAddress: UnsafeMutablePointer) throws -> OpaquePointer { + swt_GdiplusImageFromHBITMAP(imageAddress, nil) + } + + public static func _cleanUpAttachment(at imageAddress: UnsafeMutablePointer) { + DeleteObject(imageAddress) + } +} +#endif diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/HICON+AttachableAsGDIPlusImage.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/HICON+AttachableAsGDIPlusImage.swift new file mode 100644 index 000000000..0269bee56 --- /dev/null +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/HICON+AttachableAsGDIPlusImage.swift @@ -0,0 +1,27 @@ +// +// 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 +// + +#if os(Windows) +import Testing +private import _TestingInternals.GDIPlus + +public import WinSDK + +@_spi(Experimental) +extension HICON__: _AttachableByAddressAsGDIPlusImage { + public static func _copyAttachableGDIPlusImage(at imageAddress: UnsafeMutablePointer) throws -> OpaquePointer { + swt_GdiplusImageFromHICON(imageAddress) + } + + public static func _cleanUpAttachment(at imageAddress: UnsafeMutablePointer) { + DeleteObject(imageAddress) + } +} +#endif diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/UnsafeMutablePointer+AttachableAsGDIPlusImage.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/UnsafeMutablePointer+AttachableAsGDIPlusImage.swift new file mode 100644 index 000000000..5a50ef1e7 --- /dev/null +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/UnsafeMutablePointer+AttachableAsGDIPlusImage.swift @@ -0,0 +1,24 @@ +// +// 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 +// + +#if os(Windows) +import Testing + +@_spi(Experimental) +extension UnsafeMutablePointer: AttachableAsGDIPlusImage where Pointee: _AttachableByAddressAsGDIPlusImage { + public func _copyAttachableGDIPlusImage() throws -> OpaquePointer { + try Pointee._copyAttachableGDIPlusImage(at: self) + } + + public func _cleanUpAttachment() { + Pointee._cleanUpAttachment(at: self) + } +} +#endif diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/_AttachableImageWrapper.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/_AttachableImageWrapper.swift new file mode 100644 index 000000000..9f23cb140 --- /dev/null +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/_AttachableImageWrapper.swift @@ -0,0 +1,132 @@ +// +// 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 os(Windows) +@_spi(Experimental) public import Testing +private import _TestingInternals.GDIPlus + +internal import WinSDK + +/// A wrapper type for image types such as `HBITMAP` and `HICON` that can be +/// attached indirectly. +/// +/// You do not need to use this type directly. Instead, initialize an instance +/// of ``Attachment`` using an instance of an image type that conforms to +/// ``AttachableAsGDIPlusImage``. The following system-provided image types +/// conform to the ``AttachableAsGDIPlusImage`` protocol and can be attached to +/// a test: +/// +/// - [`HBITMAP`](https://learn.microsoft.com/en-us/windows/win32/gdi/bitmaps) +/// - [`HICON`](https://learn.microsoft.com/en-us/windows/win32/menurc/icons) +@_spi(Experimental) +public struct _AttachableImageWrapper: ~Copyable where Image: AttachableAsGDIPlusImage { + /// The underlying image. + var image: Image + + /// The image format to use when encoding the represented image. + var imageFormat: AttachableImageFormat? + + /// Whether or not to call `_cleanUpAttachment(at:)` on `pointer` when this + /// instance is deinitialized. + /// + /// - Note: If cleanup is not performed, `pointer` is effectively being + /// borrowed from the calling context. + var cleanUpWhenDone: Bool + + init(image: Image, imageFormat: AttachableImageFormat?, cleanUpWhenDone: Bool) { + self.image = image + self.imageFormat = imageFormat + self.cleanUpWhenDone = cleanUpWhenDone + } + + deinit { + if cleanUpWhenDone { + image._cleanUpAttachment() + } + } +} + +@available(*, unavailable) +extension _AttachableImageWrapper: Sendable {} + +// MARK: - + +extension _AttachableImageWrapper: AttachableWrapper { + public var wrappedValue: Image { + image + } + + public func withUnsafeBytes(for attachment: borrowing Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { + // Create an in-memory stream to write the image data to. Note that Windows + // documentation recommends SHCreateMemStream() instead, but that function + // does not provide a mechanism to access the underlying memory directly. + var stream: UnsafeMutablePointer? + let rCreateStream = CreateStreamOnHGlobal(nil, true, &stream) + guard S_OK == rCreateStream, let stream else { + throw GDIPlusError.streamCreationFailed(rCreateStream) + } + defer { + stream.withMemoryRebound(to: IUnknown.self, capacity: 1) { stream in + _ = swt_IUnknown_Release(stream) + } + } + + try withGDIPlus { + // Get a GDI+ image from the attachment. + let image = try image._copyAttachableGDIPlusImage() + defer { + swt_GdiplusImageDelete(image) + } + + // Get the CLSID of the image encoder corresponding to the specified image + // format. + var clsid = AttachableImageFormat.computeCLSID(for: imageFormat, withPreferredName: attachment.preferredName) + + var encodingQuality = LONG((imageFormat?.encodingQuality ?? 1.0) * 100.0) + try withUnsafeMutableBytes(of: &encodingQuality) { encodingQuality in + var encoderParams = Gdiplus.EncoderParameters() + encoderParams.Count = 1 + encoderParams.Parameter.Guid = swt_GdiplusEncoderQuality() + encoderParams.Parameter.Type = ULONG(Gdiplus.EncoderParameterValueTypeLong.rawValue) + encoderParams.Parameter.NumberOfValues = 1 + encoderParams.Parameter.Value = encodingQuality.baseAddress + + // Save the image into the stream. + let rSave = swt_GdiplusImageSave(image, stream, &clsid, &encoderParams) + guard rSave == Gdiplus.Ok else { + throw GDIPlusError.status(rSave) + } + } + } + + // Extract the serialized image and pass it back to the caller. We hold the + // HGLOBAL locked while calling `body`, but nothing else should have a + // reference to it. + var global: HGLOBAL? + let rGetGlobal = GetHGlobalFromStream(stream, &global) + guard S_OK == rGetGlobal else { + throw GDIPlusError.globalFromStreamFailed(rGetGlobal) + } + guard let baseAddress = GlobalLock(global) else { + throw Win32Error(rawValue: GetLastError()) + } + defer { + GlobalUnlock(global) + } + let byteCount = GlobalSize(global) + return try body(UnsafeRawBufferPointer(start: baseAddress, count: Int(byteCount))) + } + + public borrowing func preferredName(for attachment: borrowing Attachment, basedOn suggestedName: String) -> String { + let clsid = AttachableImageFormat.computeCLSID(for: imageFormat, withPreferredName: suggestedName) + return AttachableImageFormat.appendPathExtension(for: clsid, to: suggestedName) + } +} +#endif diff --git a/Sources/Overlays/_Testing_WinSDK/ReexportTesting.swift b/Sources/Overlays/_Testing_WinSDK/ReexportTesting.swift new file mode 100644 index 000000000..48dff4164 --- /dev/null +++ b/Sources/Overlays/_Testing_WinSDK/ReexportTesting.swift @@ -0,0 +1,11 @@ +// +// 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 +// + +@_exported @_spi(Experimental) @_spi(ForToolsIntegrationOnly) public import Testing diff --git a/Sources/Testing/Attachments/AttachableImageFormat.swift b/Sources/Testing/Attachments/AttachableImageFormat.swift index afb74c80e..bf5df4f7f 100644 --- a/Sources/Testing/Attachments/AttachableImageFormat.swift +++ b/Sources/Testing/Attachments/AttachableImageFormat.swift @@ -42,7 +42,8 @@ public struct AttachableImageFormat: Sendable { /// use. The platform-specific cross-import overlay or package is /// responsible for exposing appropriate interfaces for this case. /// - /// On Apple platforms, `value` should be an instance of `UTType`. + /// On Apple platforms, `value` should be an instance of `UTType`. On + /// Windows, it should be an instance of `CLSID`. case systemValue(_ value: any Sendable) } diff --git a/Sources/Testing/Support/CError.swift b/Sources/Testing/Support/CError.swift index a8462fda4..b392191d1 100644 --- a/Sources/Testing/Support/CError.swift +++ b/Sources/Testing/Support/CError.swift @@ -27,8 +27,12 @@ struct CError: Error, RawRepresentable { /// [here](https://learn.microsoft.com/en-us/windows/win32/debug/system-error-codes). /// /// This type is not part of the public interface of the testing library. -struct Win32Error: Error, RawRepresentable { - var rawValue: DWORD +package struct Win32Error: Error, RawRepresentable { + package var rawValue: CUnsignedLong + + package init(rawValue: CUnsignedLong) { + self.rawValue = rawValue + } } #endif @@ -66,7 +70,7 @@ extension CError: CustomStringConvertible { #if os(Windows) extension Win32Error: CustomStringConvertible { - var description: String { + package var description: String { let (address, count) = withUnsafeTemporaryAllocation(of: LPWSTR?.self, capacity: 1) { buffer in // FormatMessageW() takes a wide-character buffer into which it writes the // error message... _unless_ you pass `FORMAT_MESSAGE_ALLOCATE_BUFFER` in diff --git a/Sources/Testing/Support/Environment.swift b/Sources/Testing/Support/Environment.swift index 2ab3710a4..4cddde9e0 100644 --- a/Sources/Testing/Support/Environment.swift +++ b/Sources/Testing/Support/Environment.swift @@ -15,7 +15,7 @@ private import _TestingInternals /// This type can be used to access the current process' environment variables. /// /// This type is not part of the public interface of the testing library. -enum Environment { +package enum Environment { #if SWT_NO_ENVIRONMENT_VARIABLES /// Storage for the simulated environment. /// @@ -92,7 +92,7 @@ enum Environment { /// Get all environment variables in the current process. /// /// - Returns: A copy of the current process' environment dictionary. - static func get() -> [String: String] { + package static func get() -> [String: String] { #if SWT_NO_ENVIRONMENT_VARIABLES simulatedEnvironment.rawValue #elseif SWT_TARGET_OS_APPLE @@ -140,7 +140,7 @@ enum Environment { /// /// - Returns: The value of the specified environment variable, or `nil` if it /// is not set for the current process. - static func variable(named name: String) -> String? { + package static func variable(named name: String) -> String? { #if SWT_NO_ENVIRONMENT_VARIABLES simulatedEnvironment.rawValue[name] #elseif SWT_TARGET_OS_APPLE && !SWT_NO_DYNAMIC_LINKING @@ -221,7 +221,7 @@ enum Environment { /// - String values beginning with the letters `"t"`, `"T"`, `"y"`, or `"Y"` /// are interpreted as `true`; and /// - All other non-`nil` string values are interpreted as `false`. - static func flag(named name: String) -> Bool? { + package static func flag(named name: String) -> Bool? { variable(named: name).map { if let signedValue = Int64($0) { return signedValue != 0 @@ -248,7 +248,7 @@ extension Environment { /// /// - Returns: Whether or not the environment variable was successfully set. @discardableResult - static func setVariable(_ value: String?, named name: String) -> Bool { + package static func setVariable(_ value: String?, named name: String) -> Bool { #if SWT_NO_ENVIRONMENT_VARIABLES simulatedEnvironment.withLock { environment in environment[name] = value diff --git a/Sources/_TestingInternals/GDI+/include/GDI+.h b/Sources/_TestingInternals/GDI+/include/GDI+.h new file mode 100644 index 000000000..ff3020b66 --- /dev/null +++ b/Sources/_TestingInternals/GDI+/include/GDI+.h @@ -0,0 +1,71 @@ +// +// 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_GDIPLUS_H) +#define SWT_GDIPLUS_H + +/// This header includes thunk functions for various GDI+ functions that the +/// Swift importer is currently unable to import. As such, I haven't documented +/// each function individually; refer to the GDI+ documentation for more +/// information about the thunked functions. + +#if defined(_WIN32) && defined(__cplusplus) +#include "../include/Defines.h" +#include "../include/Includes.h" + +#include + +SWT_ASSUME_NONNULL_BEGIN + +static inline Gdiplus::Status swt_GdiplusStartup( + ULONG_PTR *token, + const Gdiplus::GdiplusStartupInput *input, + Gdiplus::GdiplusStartupOutput *_Nullable output +) { + return Gdiplus::GdiplusStartup(token, input, output); +} + +static inline void swt_GdiplusShutdown(ULONG_PTR token) { + Gdiplus::GdiplusShutdown(token); +} + +static inline Gdiplus::Image *swt_GdiplusImageFromHBITMAP(HBITMAP bitmap, HPALETTE _Nullable palette) { + return Gdiplus::Bitmap::FromHBITMAP(bitmap, palette); +} + +static inline Gdiplus::Image *swt_GdiplusImageFromHICON(HICON icon) { + return Gdiplus::Bitmap::FromHICON(icon); +} + +static inline Gdiplus::Image *swt_GdiplusImageClone(Gdiplus::Image *image) { + return image->Clone(); +} + +static inline void swt_GdiplusImageDelete(Gdiplus::Image *image) { + delete image; +} + +static inline Gdiplus::Status swt_GdiplusImageSave( + Gdiplus::Image *image, + IStream *stream, + const CLSID *format, + const Gdiplus::EncoderParameters *_Nullable encoderParams +) { + return image->Save(stream, format, encoderParams); +} + +static inline GUID swt_GdiplusEncoderQuality(void) { + return Gdiplus::EncoderQuality; +} + +SWT_ASSUME_NONNULL_END + +#endif +#endif diff --git a/Sources/_TestingInternals/include/Includes.h b/Sources/_TestingInternals/include/Includes.h index 869fcff2a..3f0433cb4 100644 --- a/Sources/_TestingInternals/include/Includes.h +++ b/Sources/_TestingInternals/include/Includes.h @@ -153,7 +153,6 @@ #endif #if defined(_WIN32) -#define WIN32_LEAN_AND_MEAN #define NOMINMAX #include #include diff --git a/Sources/_TestingInternals/include/Stubs.h b/Sources/_TestingInternals/include/Stubs.h index 636ea9aff..ae641de0d 100644 --- a/Sources/_TestingInternals/include/Stubs.h +++ b/Sources/_TestingInternals/include/Stubs.h @@ -108,6 +108,24 @@ static DWORD_PTR swt_PROC_THREAD_ATTRIBUTE_HANDLE_LIST(void) { static const IMAGE_SECTION_HEADER *_Null_unspecified swt_IMAGE_FIRST_SECTION(const IMAGE_NT_HEADERS *ntHeader) { return IMAGE_FIRST_SECTION(ntHeader); } + +#if defined(__cplusplus) +/// Add a reference to (retain) a COM object. +/// +/// This function is provided because `IUnknown::AddRef()` is a virtual member +/// function and cannot be imported directly into Swift. +static inline ULONG swt_IUnknown_AddRef(IUnknown *object) { + return object->AddRef(); +} + +/// Release a COM object. +/// +/// This function is provided because `IUnknown::Release()` is a virtual member +/// function and cannot be imported directly into Swift. +static inline ULONG swt_IUnknown_Release(IUnknown *object) { + return object->Release(); +} +#endif #endif #if defined(__linux__) || defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__ANDROID__) diff --git a/Sources/_TestingInternals/include/TestSupport.h b/Sources/_TestingInternals/include/TestSupport.h index 37d42692e..2d6229ed5 100644 --- a/Sources/_TestingInternals/include/TestSupport.h +++ b/Sources/_TestingInternals/include/TestSupport.h @@ -37,6 +37,12 @@ static inline bool swt_pointersNotEqual4(const char *a, const char *b, const cha return a != b && b != c && c != d; } +#if defined(_WIN32) +static inline LPCSTR swt_IDI_SHIELD(void) { + return IDI_SHIELD; +} +#endif + SWT_ASSUME_NONNULL_END #endif diff --git a/Sources/_TestingInternals/include/module.modulemap b/Sources/_TestingInternals/include/module.modulemap index e05a32552..12a23c81d 100644 --- a/Sources/_TestingInternals/include/module.modulemap +++ b/Sources/_TestingInternals/include/module.modulemap @@ -11,4 +11,11 @@ module _TestingInternals { umbrella "." export * + + explicit module GDIPlus { + header "../GDI+/include/GDI+.h" + export * + + link "gdiplus.lib" + } } diff --git a/Tests/TestingTests/AttachmentTests.swift b/Tests/TestingTests/AttachmentTests.swift index f8ee5d0ba..d1556ff87 100644 --- a/Tests/TestingTests/AttachmentTests.swift +++ b/Tests/TestingTests/AttachmentTests.swift @@ -33,6 +33,10 @@ import UIKit #if canImport(UniformTypeIdentifiers) import UniformTypeIdentifiers #endif +#if canImport(WinSDK) && canImport(_Testing_WinSDK) +import WinSDK +@testable @_spi(Experimental) import _Testing_WinSDK +#endif @Suite("Attachment Tests") struct AttachmentTests { @@ -692,8 +696,92 @@ extension AttachmentTests { try attachment.attachableValue.withUnsafeBytes(for: attachment) { buffer in #expect(buffer.count > 32) } + Attachment.record(attachment) } #endif +#endif + +#if canImport(WinSDK) && canImport(_Testing_WinSDK) + private func copyHICON() throws -> HICON { + try #require(LoadIconA(nil, swt_IDI_SHIELD())) + } + + @MainActor @Test func attachHICON() throws { + let icon = try copyHICON() + defer { + DeleteObject(icon) + } + + let attachment = Attachment(icon, named: "diamond.jpeg") + try attachment.withUnsafeBytes { buffer in + #expect(buffer.count > 32) + } + } + + private func copyHBITMAP() throws -> HBITMAP { + let (width, height) = (GetSystemMetrics(SM_CXICON), GetSystemMetrics(SM_CYICON)) + + let icon = try copyHICON() + defer { + DeleteObject(icon) + } + + let screenDC = try #require(GetDC(nil)) + defer { + ReleaseDC(nil, screenDC) + } + + let dc = try #require(CreateCompatibleDC(nil)) + defer { + DeleteDC(dc) + } + + let bitmap = try #require(CreateCompatibleBitmap(screenDC, width, height)) + let oldSelectedObject = SelectObject(dc, bitmap) + defer { + _ = SelectObject(dc, oldSelectedObject) + } + DrawIcon(dc, 0, 0, icon) + + return bitmap + } + + @MainActor @Test func attachHBITMAP() throws { + let bitmap = try copyHBITMAP() + let attachment = Attachment(bitmap, named: "diamond.png") + try attachment.withUnsafeBytes { buffer in + #expect(buffer.count > 32) + } + } + + @MainActor @Test func attachHBITMAPAsJPEG() throws { + let bitmap1 = try copyHBITMAP() + let hiFi = Attachment(bitmap1, named: "diamond", as: .jpeg(withEncodingQuality: 1.0)) + let bitmap2 = try copyHBITMAP() + let loFi = Attachment(bitmap2, named: "diamond", as: .jpeg(withEncodingQuality: 0.1)) + try hiFi.withUnsafeBytes { hiFi in + try loFi.withUnsafeBytes { loFi in + #expect(hiFi.count > loFi.count) + } + } + Attachment.record(loFi) + } + + @MainActor @Test func pathExtensionAndCLSID() throws { + let pngCLSID = AttachableImageFormat.png.clsid + let pngFilename = AttachableImageFormat.appendPathExtension(for: pngCLSID, to: "example") + #expect(pngFilename == "example.png") + + let jpegCLSID = AttachableImageFormat.jpeg.clsid + let jpegFilename = AttachableImageFormat.appendPathExtension(for: jpegCLSID, to: "example") + #expect(jpegFilename == "example.jpg") + + let pngjpegFilename = AttachableImageFormat.appendPathExtension(for: jpegCLSID, to: "example.png") + #expect(pngjpegFilename == "example.png.jpg") + + let jpgjpegFilename = AttachableImageFormat.appendPathExtension(for: jpegCLSID, to: "example.jpeg") + #expect(jpgjpegFilename == "example.jpeg") + } #endif } } From aaa60ed6506d3f21cf77ca4cca828bf4ef3d25d7 Mon Sep 17 00:00:00 2001 From: Stuart Montgomery Date: Mon, 4 Aug 2025 15:47:42 -0500 Subject: [PATCH 083/216] Fix two broken Forum category links in this project's New Issue template chooser (#1248) This fixes two broken Forum category links in this project's [New Issue](https://github.com/swiftlang/swift-testing/issues/new/choose) template chooser. The location of the "Development > Swift Testing" subcategory changed around the time of the formation of the [Testing Workgroup](https://swift.org/testing-workgroup/). I also took this opportunity to switch to the [Using Swift](https://forums.swift.org/c/swift-users/15) category for the "Ask a question" link, since that's generally more appropriate for general usage questions. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- .github/ISSUE_TEMPLATE/config.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 3add3b3e3..3d01084aa 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -9,7 +9,7 @@ blank_issues_enabled: true contact_links: - name: 🌐 Discuss an idea - url: https://forums.swift.org/c/related-projects/swift-testing + url: https://forums.swift.org/c/development/swift-testing/103 about: > Share an idea with the Swift Testing community. - name: 📄 Formally propose a change @@ -18,10 +18,10 @@ contact_links: Formally propose an addition, removal, or change to the APIs or features of Swift Testing. - name: 🙋 Ask a question - url: https://forums.swift.org/c/related-projects/swift-testing + url: https://forums.swift.org/c/swift-users/15 about: > - Ask a question about or get help with Swift Testing. Beginner questions - welcome! + Ask a question or get help by starting a new Forum topic with the 'swift-testing' tag. + Beginner questions welcome! - name: 🪲 Report an issue with Swift Package Manager url: https://github.com/swiftlang/swift-package-manager/issues/new/choose about: > From 64856c7e1beb699e5497190a1ebc5d6b87457e4b Mon Sep 17 00:00:00 2001 From: Suzy Ratcliff Date: Tue, 5 Aug 2025 11:02:07 -0700 Subject: [PATCH 084/216] Remove experimental spi from issue severity (#1247) Remove experimental spi from issue severity ### Motivation: The Issue Severity proposal was approved. https://github.com/swiftlang/swift-evolution/blob/main/proposals/testing/0013-issue-severity-warning.md ### Modifications: Removed experimental spi from issue severity and updated comments. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- Sources/Testing/Issues/Issue+Recording.swift | 18 +++++++++++++----- Sources/Testing/Issues/Issue.swift | 20 ++++++++++++++++---- Sources/Testing/Running/Configuration.swift | 8 +------- 3 files changed, 30 insertions(+), 16 deletions(-) diff --git a/Sources/Testing/Issues/Issue+Recording.swift b/Sources/Testing/Issues/Issue+Recording.swift index b79e94269..d99323777 100644 --- a/Sources/Testing/Issues/Issue+Recording.swift +++ b/Sources/Testing/Issues/Issue+Recording.swift @@ -50,7 +50,7 @@ extension Issue { return self } - /// Record an issue when a running test fails unexpectedly. + /// Records an issue that a test encounters while it's running. /// /// - Parameters: /// - comment: A comment describing the expectation. @@ -62,6 +62,9 @@ extension Issue { /// Use this function if, while running a test, an issue occurs that cannot be /// represented as an expectation (using the ``expect(_:_:sourceLocation:)`` /// or ``require(_:_:sourceLocation:)-5l63q`` macros.) + @_disfavoredOverload + @_documentation(visibility: private) + @available(*, deprecated, message: "Use record(_:severity:sourceLocation:) instead.") @discardableResult public static func record( _ comment: Comment? = nil, sourceLocation: SourceLocation = #_sourceLocation @@ -69,11 +72,13 @@ extension Issue { record(comment, severity: .error, sourceLocation: sourceLocation) } - /// Record an issue when a running test fails unexpectedly. + /// Records an issue that a test encounters while it's running. /// /// - Parameters: /// - comment: A comment describing the expectation. - /// - severity: The severity of the issue. + /// - severity: The severity level of the issue. The testing library marks the + /// test as failed if the severity is greater than ``Issue/Severity/warning``. + /// The default is ``Issue/Severity/error``. /// - sourceLocation: The source location to which the issue should be /// attributed. /// @@ -82,10 +87,13 @@ extension Issue { /// Use this function if, while running a test, an issue occurs that cannot be /// represented as an expectation (using the ``expect(_:_:sourceLocation:)`` /// or ``require(_:_:sourceLocation:)-5l63q`` macros.) - @_spi(Experimental) + /// + /// @Metadata { + /// @Available(Swift, introduced: 6.3) + /// } @discardableResult public static func record( _ comment: Comment? = nil, - severity: Severity, + severity: Severity = .error, sourceLocation: SourceLocation = #_sourceLocation ) -> Self { let sourceContext = SourceContext(backtrace: .current(), sourceLocation: sourceLocation) diff --git a/Sources/Testing/Issues/Issue.swift b/Sources/Testing/Issues/Issue.swift index 9a2555177..9d2e9fc90 100644 --- a/Sources/Testing/Issues/Issue.swift +++ b/Sources/Testing/Issues/Issue.swift @@ -84,7 +84,10 @@ public struct Issue: Sendable { /// /// - ``warning`` /// - ``error`` - @_spi(Experimental) + /// + /// @Metadata { + /// @Available(Swift, introduced: 6.3) + /// } public enum Severity: Sendable { /// The severity level for an issue which should be noted but is not /// necessarily an error. @@ -101,7 +104,10 @@ public struct Issue: Sendable { } /// The severity of this issue. - @_spi(Experimental) + /// + /// @Metadata { + /// @Available(Swift, introduced: 6.3) + /// } public var severity: Severity /// Whether or not this issue should cause the test it's associated with to be @@ -114,7 +120,10 @@ public struct Issue: Sendable { /// /// Use this property to determine if an issue should be considered a failure, instead of /// directly comparing the value of the ``severity`` property. - @_spi(Experimental) + /// + /// @Metadata { + /// @Available(Swift, introduced: 6.3) + /// } public var isFailure: Bool { return !self.isKnown && self.severity >= .error } @@ -324,7 +333,10 @@ extension Issue { public var kind: Kind.Snapshot /// The severity of this issue. - @_spi(Experimental) + /// + /// @Metadata { + /// @Available(Swift, introduced: 6.3) + /// } public var severity: Severity /// Any comments provided by the developer and associated with this issue. diff --git a/Sources/Testing/Running/Configuration.swift b/Sources/Testing/Running/Configuration.swift index bca788ec7..12b8827de 100644 --- a/Sources/Testing/Running/Configuration.swift +++ b/Sources/Testing/Running/Configuration.swift @@ -184,13 +184,7 @@ public struct Configuration: Sendable { /// Whether or not events of the kind ``Event/Kind-swift.enum/issueRecorded(_:)`` /// containing issues with warning (or lower) severity should be delivered /// to the event handler of the configuration these options are applied to. - /// - /// By default, events matching this criteria are not delivered to event - /// handlers since this is an experimental feature. - /// - /// - Warning: Warning issues are not yet an approved feature. - @_spi(Experimental) - public var isWarningIssueRecordedEventEnabled: Bool = false + public var isWarningIssueRecordedEventEnabled: Bool = true /// Whether or not events of the kind /// ``Event/Kind-swift.enum/expectationChecked(_:)`` should be delivered to From c60f1306842faf2d10439323e52f289d3fffbe34 Mon Sep 17 00:00:00 2001 From: Suzy Ratcliff Date: Tue, 5 Aug 2025 13:14:06 -0700 Subject: [PATCH 085/216] Fix test: issueCountSummingAtRunEnd which was broken by https://github.com/swiftlang/swift-testing/pull/1247 (#1250) Fix test: issueCountSummingAtRunEnd which was broken by https://github.com/swiftlang/swift-testing/pull/1247 Enabling warnings by default broke the test: issueCountSummingAtRunEnd. This fixes that. ### Modifications: I updated the expected output to contain warnings. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- Tests/TestingTests/EventRecorderTests.swift | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Tests/TestingTests/EventRecorderTests.swift b/Tests/TestingTests/EventRecorderTests.swift index e3dc9ca99..d489b2f1e 100644 --- a/Tests/TestingTests/EventRecorderTests.swift +++ b/Tests/TestingTests/EventRecorderTests.swift @@ -336,6 +336,7 @@ struct EventRecorderTests { let testCount = Reference() let suiteCount = Reference() let issueCount = Reference() + let warningCount = Reference() let knownIssueCount = Reference() let runFailureRegex = Regex { @@ -355,6 +356,8 @@ struct EventRecorderTests { " issue" Optionally("s") " (including " + Capture(as: warningCount) { OneOrMore(.digit) } transform: { Int($0) } + " warnings and " Capture(as: knownIssueCount) { OneOrMore(.digit) } transform: { Int($0) } " known issue" Optionally("s") @@ -368,8 +371,9 @@ struct EventRecorderTests { ) #expect(match[testCount] == 9) #expect(match[suiteCount] == 2) - #expect(match[issueCount] == 12) - #expect(match[knownIssueCount] == 5) + #expect(match[issueCount] == 16) + #expect(match[warningCount] == 3) + #expect(match[knownIssueCount] == 6) } @Test("Issue counts are summed correctly on run end for a test with only warning issues") From 54f919e0e581c5d5e3caaf309a8fd81b2c4e21dc Mon Sep 17 00:00:00 2001 From: Stuart Montgomery Date: Wed, 6 Aug 2025 10:25:30 -0500 Subject: [PATCH 086/216] Declare Xcode 26 availability for IssueHandlingTrait (#1251) This declares Xcode 26 availability for `IssueHandlingTrait`, which was proposed in [ST-0011: Issue Handling Traits](https://github.com/swiftlang/swift-evolution/blob/main/proposals/testing/0011-issue-handling-traits.md) and included in Swift 6.2 in #1228. This feature first appeared in Xcode 26 Beta 5, as noted[^1] in the [Release Notes](https://developer.apple.com/documentation/xcode-release-notes/xcode-26-release-notes). [^1]: Search for "ST-0011" on that page to find the reference. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- Sources/Testing/Traits/IssueHandlingTrait.swift | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Sources/Testing/Traits/IssueHandlingTrait.swift b/Sources/Testing/Traits/IssueHandlingTrait.swift index 4d7d408d6..21a9adaac 100644 --- a/Sources/Testing/Traits/IssueHandlingTrait.swift +++ b/Sources/Testing/Traits/IssueHandlingTrait.swift @@ -27,6 +27,7 @@ /// /// @Metadata { /// @Available(Swift, introduced: 6.2) +/// @Available(Xcode, introduced: 26.0) /// } public struct IssueHandlingTrait: TestTrait, SuiteTrait { /// A function which handles an issue and returns an optional replacement. @@ -55,6 +56,7 @@ public struct IssueHandlingTrait: TestTrait, SuiteTrait { /// /// @Metadata { /// @Available(Swift, introduced: 6.2) + /// @Available(Xcode, introduced: 26.0) /// } public func handleIssue(_ issue: Issue) -> Issue? { _handler(issue) @@ -67,6 +69,7 @@ public struct IssueHandlingTrait: TestTrait, SuiteTrait { /// @Metadata { /// @Available(Swift, introduced: 6.2) +/// @Available(Xcode, introduced: 26.0) /// } extension IssueHandlingTrait: TestScoping { public func scopeProvider(for test: Test, testCase: Test.Case?) -> Self? { @@ -181,6 +184,7 @@ extension Trait where Self == IssueHandlingTrait { /// /// @Metadata { /// @Available(Swift, introduced: 6.2) + /// @Available(Xcode, introduced: 26.0) /// } public static func compactMapIssues(_ transform: @escaping @Sendable (Issue) -> Issue?) -> Self { Self(handler: transform) @@ -219,6 +223,7 @@ extension Trait where Self == IssueHandlingTrait { /// /// @Metadata { /// @Available(Swift, introduced: 6.2) + /// @Available(Xcode, introduced: 26.0) /// } public static func filterIssues(_ isIncluded: @escaping @Sendable (Issue) -> Bool) -> Self { Self { issue in From 67e11d57011d9680cbd2cd4007cf84431227aab9 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Thu, 7 Aug 2025 16:47:15 -0400 Subject: [PATCH 087/216] Various `VersionNumber` changes of merit. (#1246) This PR does things to `ABI.VersionNumber`: - Renames it to `VersionNumber` as we do have some use cases that aren't related to JSON schema versioning. I initially didn't want to make this type a general version number type, but it's just too useful not to do so. Alas. - Changes the type of the `swiftStandardLibraryVersion` global variable to `VersionNumber?`. - Changes the type of the `glibcVersion` global variable to `VersionNumber`. - Adds `swiftCompilerVersion` representing the version of the Swift compiler used to compile Swift Testing. We need this value when computing the JSON schema version (see next bullet.) - Clamps the range of supported JSON schema versions to the Swift compiler version _unless_ we've explicitly defined a schema version higher than it: | Compiler | Highest Defined Schema | Requested | Result | |-|-|-|-| | 1.0 | 1.0 | 1.0 | 1.0 | | 2.0 | 1.0 | 1.0 | 1.0 | | 1.0 | 2.0 | 1.0 | 1.0 | | 1.0 | 1.0 | 2.0 | `nil` | | 2.0 | 2.0 | 1.0 | 1.0 | | 2.0 | 1.0 | 2.0 | 1.0 | | 1.0 | 2.0 | 2.0 | 2.0 | | 2.0 | 2.0 | 2.0 | 2.0 | The reasoning here is that, when we're built with a given compiler version, we presumably know about all JSON schema versions up to and including the one aligned with that compiler, so if you ask for the schema version aligned with the compiler, it's equivalent to whatever we support that's less than or equal to the compiler version. But if you ask for something greater than the compiler version, and we haven't defined it, we don't know anything about it and can't provide it. This reasoning breaks down somewhat if you build an old version of the Swift Testing package with a new compiler, but in general we don't support that sort of configuration for very long (and we can't predict the future anyway.) ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- Sources/Testing/ABI/ABI.Record.swift | 2 +- Sources/Testing/ABI/ABI.swift | 22 +++++++- .../Testing/ABI/Encoded/ABI.EncodedTest.swift | 2 +- .../Testing/ABI/EntryPoints/EntryPoint.swift | 12 ++--- Sources/Testing/CMakeLists.txt | 2 +- .../Event.HumanReadableOutputRecorder.swift | 8 ++- Sources/Testing/ExitTests/SpawnProcess.swift | 2 +- .../VersionNumber.swift} | 52 +++++++++++-------- Sources/Testing/Support/Versions.swift | 46 ++++++++++++---- Sources/_TestingInternals/include/Versions.h | 14 +++++ Tests/TestingTests/ABIEntryPointTests.swift | 32 ++++++------ Tests/TestingTests/SwiftPMTests.swift | 26 +++++++--- 12 files changed, 153 insertions(+), 67 deletions(-) rename Sources/Testing/{ABI/ABI.VersionNumber.swift => Support/VersionNumber.swift} (72%) diff --git a/Sources/Testing/ABI/ABI.Record.swift b/Sources/Testing/ABI/ABI.Record.swift index e03b5df6a..ef7bc6937 100644 --- a/Sources/Testing/ABI/ABI.Record.swift +++ b/Sources/Testing/ABI/ABI.Record.swift @@ -66,7 +66,7 @@ extension ABI.Record: Codable { init(from decoder: any Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) - let versionNumber = try container.decode(ABI.VersionNumber.self, forKey: .version) + let versionNumber = try container.decode(VersionNumber.self, forKey: .version) if versionNumber != V.versionNumber { throw DecodingError.dataCorrupted( DecodingError.Context( diff --git a/Sources/Testing/ABI/ABI.swift b/Sources/Testing/ABI/ABI.swift index 23b14297c..1707953a0 100644 --- a/Sources/Testing/ABI/ABI.swift +++ b/Sources/Testing/ABI/ABI.swift @@ -45,6 +45,9 @@ extension ABI { /// The current supported ABI version (ignoring any experimental versions.) typealias CurrentVersion = v0 + /// The highest supported ABI version (including any experimental versions.) + typealias HighestVersion = v6_3 + #if !hasFeature(Embedded) /// Get the type representing a given ABI version. /// @@ -55,7 +58,24 @@ 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)? { - switch versionNumber { + 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, + // then we assume we don't know what they're talking about and return nil. + // + // Note that it is possible for the Swift compiler version to be lower + // than the highest defined ABI version (e.g. if you use a 6.2 toolchain + // to build this package's release/6.3 branch with a 6.3 ABI defined.) + // + // Note also that building an old version of Swift Testing with a newer + // compiler may produce incorrect results here. We don't generally support + // that configuration though. + if versionNumber > swiftCompilerVersion { + return nil + } + } + + return switch versionNumber { case ABI.v6_3.versionNumber...: ABI.v6_3.self case ABI.v0.versionNumber...: diff --git a/Sources/Testing/ABI/Encoded/ABI.EncodedTest.swift b/Sources/Testing/ABI/Encoded/ABI.EncodedTest.swift index 29bfa10c3..11c309e83 100644 --- a/Sources/Testing/ABI/Encoded/ABI.EncodedTest.swift +++ b/Sources/Testing/ABI/Encoded/ABI.EncodedTest.swift @@ -78,7 +78,7 @@ extension ABI { /// - Warning: Tags are not yet part of the JSON schema. /// /// @Metadata { - /// @Available("Swift Testing ABI", introduced: 1) + /// @Available("Swift Testing ABI", introduced: 6.3) /// } var _tags: [String]? diff --git a/Sources/Testing/ABI/EntryPoints/EntryPoint.swift b/Sources/Testing/ABI/EntryPoints/EntryPoint.swift index 61f2f92cb..ed16a9841 100644 --- a/Sources/Testing/ABI/EntryPoints/EntryPoint.swift +++ b/Sources/Testing/ABI/EntryPoints/EntryPoint.swift @@ -262,7 +262,7 @@ public struct __CommandLineArguments_v0: Sendable { /// This property is internal because its type is internal. External users of /// this structure can use the ``eventStreamSchemaVersion`` property to get or /// set the value of this property. - var eventStreamVersionNumber: ABI.VersionNumber? + var eventStreamVersionNumber: VersionNumber? /// The value of the `--event-stream-version` or `--experimental-event-stream-version` /// argument, representing the version of the event stream schema to use when @@ -282,7 +282,7 @@ public struct __CommandLineArguments_v0: Sendable { } set { eventStreamVersionNumber = newValue.flatMap { newValue in - guard let newValue = ABI.VersionNumber(newValue) else { + guard let newValue = VersionNumber(newValue) else { preconditionFailure("Invalid event stream version number '\(newValue)'. Specify a version number of the form 'major.minor.patch'.") } return newValue @@ -404,7 +404,7 @@ func parseCommandLineArguments(from args: [String]) throws -> __CommandLineArgum // If the caller specified a version that could not be parsed, treat it as // an invalid argument. - guard let eventStreamVersion = ABI.VersionNumber(versionString) else { + guard let eventStreamVersion = VersionNumber(versionString) else { let argument = allowExperimental ? "--experimental-event-stream-version" : "--event-stream-version" throw _EntryPointError.invalidArgument(argument, value: versionString) } @@ -652,7 +652,7 @@ public func configurationForEntryPoint(from args: __CommandLineArguments_v0) thr /// /// - Throws: If `version` is not a supported ABI version. func eventHandlerForStreamingEvents( - withVersionNumber versionNumber: ABI.VersionNumber?, + withVersionNumber versionNumber: VersionNumber?, encodeAsJSONLines: Bool, forwardingTo targetEventHandler: @escaping @Sendable (UnsafeRawBufferPointer) -> Void ) throws -> Event.Handler { @@ -822,7 +822,7 @@ private enum _EntryPointError: Error { /// /// - Parameters: /// - versionNumber: The experimental ABI version number. - case experimentalABIVersion(_ versionNumber: ABI.VersionNumber) + case experimentalABIVersion(_ versionNumber: VersionNumber) } extension _EntryPointError: CustomStringConvertible { @@ -847,7 +847,7 @@ extension __CommandLineArguments_v0 { eventStreamVersionNumber.map(\.majorComponent).map(Int.init) } set { - eventStreamVersionNumber = newValue.map { ABI.VersionNumber(majorComponent: Int8(clamping: $0), minorComponent: 0) } + eventStreamVersionNumber = newValue.map { VersionNumber(majorComponent: .init(clamping: $0), minorComponent: 0) } } } } diff --git a/Sources/Testing/CMakeLists.txt b/Sources/Testing/CMakeLists.txt index 8ddaa2cb5..f970f43bb 100644 --- a/Sources/Testing/CMakeLists.txt +++ b/Sources/Testing/CMakeLists.txt @@ -13,7 +13,6 @@ add_library(Testing ABI/ABI.Record.swift ABI/ABI.Record+Streaming.swift ABI/ABI.swift - ABI/ABI.VersionNumber.swift ABI/Encoded/ABI.EncodedAttachment.swift ABI/Encoded/ABI.EncodedBacktrace.swift ABI/Encoded/ABI.EncodedError.swift @@ -84,6 +83,7 @@ add_library(Testing Support/JSON.swift Support/Locked.swift Support/Locked+Platform.swift + Support/VersionNumber.swift Support/Versions.swift Discovery+Macro.swift Test.ID.Selection.swift diff --git a/Sources/Testing/Events/Recorder/Event.HumanReadableOutputRecorder.swift b/Sources/Testing/Events/Recorder/Event.HumanReadableOutputRecorder.swift index 248bb4aec..434487e27 100644 --- a/Sources/Testing/Events/Recorder/Event.HumanReadableOutputRecorder.swift +++ b/Sources/Testing/Events/Recorder/Event.HumanReadableOutputRecorder.swift @@ -342,7 +342,13 @@ extension Event.HumanReadableOutputRecorder { case .runStarted: var comments = [Comment]() if verbosity > 0 { - comments.append("Swift Version: \(swiftStandardLibraryVersion)") + if let swiftStandardLibraryVersion { + comments.append("Swift Standard Library Version: \(swiftStandardLibraryVersion)") + } + comments.append("Swift Compiler Version: \(swiftCompilerVersion)") +#if canImport(Glibc) && !os(FreeBSD) && !os(OpenBSD) + comments.append("GNU C Library Version: \(glibcVersion)") +#endif } comments.append("Testing Library Version: \(testingLibraryVersion)") if let targetTriple { diff --git a/Sources/Testing/ExitTests/SpawnProcess.swift b/Sources/Testing/ExitTests/SpawnProcess.swift index 9f01a1d11..24303f632 100644 --- a/Sources/Testing/ExitTests/SpawnProcess.swift +++ b/Sources/Testing/ExitTests/SpawnProcess.swift @@ -138,7 +138,7 @@ func spawnExecutable( // and https://www.austingroupbugs.net/view.php?id=411). _ = posix_spawn_file_actions_adddup2(fileActions, fd, fd) #if canImport(Glibc) && !os(FreeBSD) && !os(OpenBSD) - if _slowPath(glibcVersion.major < 2 || (glibcVersion.major == 2 && glibcVersion.minor < 29)) { + if _slowPath(glibcVersion < VersionNumber(2, 29)) { // This system is using an older version of glibc that does not // implement FD_CLOEXEC clearing in posix_spawn_file_actions_adddup2(), // so we must clear it here in the parent process. diff --git a/Sources/Testing/ABI/ABI.VersionNumber.swift b/Sources/Testing/Support/VersionNumber.swift similarity index 72% rename from Sources/Testing/ABI/ABI.VersionNumber.swift rename to Sources/Testing/Support/VersionNumber.swift index 2235d3c1d..4dfa52994 100644 --- a/Sources/Testing/ABI/ABI.VersionNumber.swift +++ b/Sources/Testing/Support/VersionNumber.swift @@ -8,34 +8,42 @@ // See https://swift.org/CONTRIBUTORS.txt for Swift project authors // -extension ABI { - /// A type describing an ABI version number. +private import _TestingInternals + +/// A type describing an ABI version number. +/// +/// This type implements a subset of the [semantic versioning](https://semver.org) +/// specification (specifically parsing, displaying, and comparing +/// `` values we expect that the testing library will need for the +/// foreseeable future.) +struct VersionNumber: Sendable { + /// The integer type used to store a component. /// - /// This type implements a subset of the [semantic versioning](https://semver.org) - /// specification (specifically parsing, displaying, and comparing - /// `` values we expect that Swift will need for the foreseeable - /// future.) - struct VersionNumber: Sendable { - /// The major version. - var majorComponent: Int8 + /// The testing library does not generally need to deal with version numbers + /// whose components exceed the width of this type. If we need to deal with + /// larger version number components in the future, we can increase the width + /// of this type accordingly. + typealias Component = Int8 - /// The minor version. - var minorComponent: Int8 + /// The major version. + var majorComponent: Component - /// The patch, revision, or bug fix version. - var patchComponent: Int8 = 0 - } + /// The minor version. + var minorComponent: Component + + /// The patch, revision, or bug fix version. + var patchComponent: Component = 0 } -extension ABI.VersionNumber { - init(_ majorComponent: _const Int8, _ minorComponent: _const Int8, _ patchComponent: _const Int8 = 0) { +extension VersionNumber { + init(_ majorComponent: _const Component, _ minorComponent: _const Component, _ patchComponent: _const Component = 0) { self.init(majorComponent: majorComponent, minorComponent: minorComponent, patchComponent: patchComponent) } } // MARK: - CustomStringConvertible -extension ABI.VersionNumber: CustomStringConvertible { +extension VersionNumber: CustomStringConvertible { /// Initialize an instance of this type by parsing the given string. /// /// - Parameters: @@ -55,8 +63,8 @@ extension ABI.VersionNumber: CustomStringConvertible { // Split the string on "." (assuming it is of the form "1", "1.2", or // "1.2.3") and parse the individual components as integers. let components = string.split(separator: ".", omittingEmptySubsequences: false) - func componentValue(_ index: Int) -> Int8? { - components.count > index ? Int8(components[index]) : 0 + func componentValue(_ index: Int) -> Component? { + components.count > index ? Component(components[index]) : 0 } guard let majorComponent = componentValue(0), @@ -81,7 +89,7 @@ extension ABI.VersionNumber: CustomStringConvertible { // MARK: - Equatable, Comparable -extension ABI.VersionNumber: Equatable, Comparable { +extension VersionNumber: Equatable, Comparable { static func <(lhs: Self, rhs: Self) -> Bool { if lhs.majorComponent != rhs.majorComponent { return lhs.majorComponent < rhs.majorComponent @@ -96,10 +104,10 @@ extension ABI.VersionNumber: Equatable, Comparable { // MARK: - Codable -extension ABI.VersionNumber: Codable { +extension VersionNumber: Codable { init(from decoder: any Decoder) throws { let container = try decoder.singleValueContainer() - if let number = try? container.decode(Int8.self) { + if let number = try? container.decode(Component.self) { // Allow for version numbers encoded as integers for compatibility with // Swift 6.2 and earlier. self.init(majorComponent: number, minorComponent: 0) diff --git a/Sources/Testing/Support/Versions.swift b/Sources/Testing/Support/Versions.swift index 7f190ebb2..62193d6c4 100644 --- a/Sources/Testing/Support/Versions.swift +++ b/Sources/Testing/Support/Versions.swift @@ -9,6 +9,7 @@ // private import _TestingInternals +private import SwiftShims /// A human-readable string describing the current operating system's version. /// @@ -141,23 +142,50 @@ var targetTriple: String? { /// A human-readable string describing the Swift Standard Library's version. /// -/// This value's format is platform-specific and is not meant to be -/// machine-readable. It is added to the output of a test run when using -/// an event writer. +/// This value is unavailable on some earlier Apple runtime targets. On those +/// targets, this property has a value of `5.0.0`. /// /// This value is not part of the public interface of the testing library. -let swiftStandardLibraryVersion: String = { - if #available(_swiftVersionAPI, *) { - return String(describing: _SwiftStdlibVersion.current) +let swiftStandardLibraryVersion: VersionNumber? = { + guard #available(_swiftVersionAPI, *) else { + return VersionNumber(5, 0) } - return "unknown" + let packedValue = _SwiftStdlibVersion.current._value + return VersionNumber( + majorComponent: .init((packedValue & 0xFFFF0000) >> 16), + minorComponent: .init((packedValue & 0x0000FF00) >> 8), + patchComponent: .init((packedValue & 0x000000FF) >> 0) + ) }() +/// The version of the Swift compiler used to build the testing library. +/// +/// This value is determined at compile time by the Swift compiler. For more +/// information, see [Version.h](https://github.com/swiftlang/swift/blob/main/include/swift/Basic/Version.h) +/// and [ClangImporter.cpp](https://github.com/swiftlang/swift/blob/main/lib/ClangImporter/ClangImporter.cpp) +/// in the Swift repository. +/// +/// This value is not part of the public interface of the testing library. +var swiftCompilerVersion: VersionNumber { + let packedValue = swt_getSwiftCompilerVersion() + if packedValue == 0, let swiftStandardLibraryVersion { + // The compiler did not supply its version. This is currently expected on + // non-Darwin targets in particular. Substitute the stdlib version (which + // should generally be aligned on non-Darwin targets.) + return swiftStandardLibraryVersion + } + return VersionNumber( + majorComponent: .init((packedValue % 1_000_000_000_000_000) / 1_000_000_000_000), + minorComponent: .init((packedValue % 1_000_000_000_000) / 1_000_000_000), + patchComponent: .init((packedValue % 1_000_000_000) / 1_000_000) + ) +} + #if canImport(Glibc) && !os(FreeBSD) && !os(OpenBSD) /// The (runtime, not compile-time) version of glibc in use on this system. /// /// This value is not part of the public interface of the testing library. -let glibcVersion: (major: Int, minor: Int) = { +let glibcVersion: VersionNumber = { // Default to the statically available version number if the function call // fails for some reason. var major = Int(clamping: __GLIBC__) @@ -173,7 +201,7 @@ let glibcVersion: (major: Int, minor: Int) = { } } - return (major, minor) + return VersionNumber(majorComponent: .init(clamping: major), minorComponent: .init(clamping: minor)) }() #endif diff --git a/Sources/_TestingInternals/include/Versions.h b/Sources/_TestingInternals/include/Versions.h index 1be02ba33..ed523b86f 100644 --- a/Sources/_TestingInternals/include/Versions.h +++ b/Sources/_TestingInternals/include/Versions.h @@ -16,6 +16,20 @@ SWT_ASSUME_NONNULL_BEGIN +/// Get the version of the compiler used to build the testing library. +/// +/// - Returns: An integer containing the packed major, minor, and patch +/// components of the compiler version. For more information, see +/// [ClangImporter.cpp](https://github.com/swiftlang/swift/blob/36246a2c8e9501cd29a75f34c9631a8f4e2e1e9b/lib/ClangImporter/ClangImporter.cpp#L647) +/// in the Swift repository. +static inline uint64_t swt_getSwiftCompilerVersion(void) { +#if defined(__SWIFT_COMPILER_VERSION) + return __SWIFT_COMPILER_VERSION; +#else + return 0; +#endif +} + /// Get the human-readable version of the testing library. /// /// - Returns: A human-readable string describing the version of the testing diff --git a/Tests/TestingTests/ABIEntryPointTests.swift b/Tests/TestingTests/ABIEntryPointTests.swift index a44942e2a..76af1b83e 100644 --- a/Tests/TestingTests/ABIEntryPointTests.swift +++ b/Tests/TestingTests/ABIEntryPointTests.swift @@ -119,12 +119,12 @@ struct ABIEntryPointTests { @Test func decodeVersionNumber() throws { let version0 = try JSON.withEncoding(of: 0) { versionJSON in - try JSON.decode(ABI.VersionNumber.self, from: versionJSON) + try JSON.decode(VersionNumber.self, from: versionJSON) } - #expect(version0 == ABI.VersionNumber(0, 0)) + #expect(version0 == VersionNumber(0, 0)) let version1_2_3 = try JSON.withEncoding(of: "1.2.3") { versionJSON in - try JSON.decode(ABI.VersionNumber.self, from: versionJSON) + try JSON.decode(VersionNumber.self, from: versionJSON) } #expect(version1_2_3.majorComponent == 1) #expect(version1_2_3.minorComponent == 2) @@ -132,39 +132,39 @@ struct ABIEntryPointTests { #expect(throws: DecodingError.self) { _ = try JSON.withEncoding(of: "not.valid") { versionJSON in - try JSON.decode(ABI.VersionNumber.self, from: versionJSON) + try JSON.decode(VersionNumber.self, from: versionJSON) } } } #endif @Test(arguments: [ - (ABI.VersionNumber(-1, 0), "-1"), - (ABI.VersionNumber(0, 0), "0"), - (ABI.VersionNumber(1, 0), "1.0"), - (ABI.VersionNumber(2, 0), "2.0"), - (ABI.VersionNumber("0.0.1"), "0.0.1"), - (ABI.VersionNumber("0.1.0"), "0.1"), - ]) func abiVersionStringConversion(version: ABI.VersionNumber?, expectedString: String) throws { + (VersionNumber(-1, 0), "-1"), + (VersionNumber(0, 0), "0"), + (VersionNumber(1, 0), "1.0"), + (VersionNumber(2, 0), "2.0"), + (VersionNumber("0.0.1"), "0.0.1"), + (VersionNumber("0.1.0"), "0.1"), + ]) func abiVersionStringConversion(version: VersionNumber?, expectedString: String) throws { let version = try #require(version) #expect(String(describing: version) == expectedString) } @Test func badABIVersionString() { - let version = ABI.VersionNumber("not.valid") + let version = VersionNumber("not.valid") #expect(version == nil) } @Test func abiVersionComparisons() throws { - var versions = [ABI.VersionNumber]() + var versions = [VersionNumber]() for major in 0 ..< 10 { - let version = try #require(ABI.VersionNumber("\(major)")) + let version = try #require(VersionNumber("\(major)")) versions.append(version) for minor in 0 ..< 10 { - let version = try #require(ABI.VersionNumber("\(major).\(minor)")) + let version = try #require(VersionNumber("\(major).\(minor)")) versions.append(version) for patch in 0 ..< 10 { - let version = try #require(ABI.VersionNumber("\(major).\(minor).\(patch)")) + let version = try #require(VersionNumber("\(major).\(minor).\(patch)")) versions.append(version) } } diff --git a/Tests/TestingTests/SwiftPMTests.swift b/Tests/TestingTests/SwiftPMTests.swift index 4a9b70502..3cca1ad55 100644 --- a/Tests/TestingTests/SwiftPMTests.swift +++ b/Tests/TestingTests/SwiftPMTests.swift @@ -235,23 +235,23 @@ struct SwiftPMTests { func deprecatedEventStreamVersionProperty() async throws { var args = __CommandLineArguments_v0() args.eventStreamVersion = 0 - #expect(args.eventStreamVersionNumber == ABI.VersionNumber(0, 0)) + #expect(args.eventStreamVersionNumber == VersionNumber(0, 0)) #expect(args.eventStreamSchemaVersion == "0") args.eventStreamVersion = -1 - #expect(args.eventStreamVersionNumber == ABI.VersionNumber(-1, 0)) + #expect(args.eventStreamVersionNumber == VersionNumber(-1, 0)) #expect(args.eventStreamSchemaVersion == "-1") args.eventStreamVersion = 123 - #expect(args.eventStreamVersionNumber == ABI.VersionNumber(123, 0)) + #expect(args.eventStreamVersionNumber == VersionNumber(123, 0)) #expect(args.eventStreamSchemaVersion == "123.0") - args.eventStreamVersionNumber = ABI.VersionNumber(10, 20, 30) + args.eventStreamVersionNumber = VersionNumber(10, 20, 30) #expect(args.eventStreamVersion == 10) #expect(args.eventStreamSchemaVersion == "10.20.30") args.eventStreamSchemaVersion = "10.20.30" - #expect(args.eventStreamVersionNumber == ABI.VersionNumber(10, 20, 30)) + #expect(args.eventStreamVersionNumber == VersionNumber(10, 20, 30)) #expect(args.eventStreamVersion == 10) #if !SWT_NO_EXIT_TESTS @@ -264,14 +264,24 @@ struct SwiftPMTests { @Test("New-but-not-experimental ABI version") func newButNotExperimentalABIVersion() async throws { - let versionNumber = ABI.VersionNumber(0, 0, 1) + var versionNumber = ABI.CurrentVersion.versionNumber + versionNumber.patchComponent += 1 let version = try #require(ABI.version(forVersionNumber: versionNumber)) #expect(version.versionNumber == ABI.v0.versionNumber) } @Test("Unsupported ABI version") func unsupportedABIVersion() async throws { - let versionNumber = ABI.VersionNumber(-100, 0) + let versionNumber = VersionNumber(-100, 0) + let versionTypeInfo = ABI.version(forVersionNumber: versionNumber).map(TypeInfo.init(describing:)) + #expect(versionTypeInfo == nil) + } + + @Test("Future ABI version (should be nil)") + func futureABIVersion() async throws { + #expect(swiftCompilerVersion >= VersionNumber(6, 0)) + #expect(swiftCompilerVersion < VersionNumber(8, 0), "Swift 8.0 is here! Please update this test.") + let versionNumber = VersionNumber(8, 0) let versionTypeInfo = ABI.version(forVersionNumber: versionNumber).map(TypeInfo.init(describing:)) #expect(versionTypeInfo == nil) } @@ -282,7 +292,7 @@ struct SwiftPMTests { ("--experimental-event-stream-output", "--experimental-event-stream-version", ABI.v0.versionNumber), ("--experimental-event-stream-output", "--experimental-event-stream-version", ABI.v6_3.versionNumber), ]) - func eventStreamOutput(outputArgumentName: String, versionArgumentName: String, version: ABI.VersionNumber) async throws { + func eventStreamOutput(outputArgumentName: String, versionArgumentName: String, version: VersionNumber) async throws { let version = try #require(ABI.version(forVersionNumber: version)) try await eventStreamOutput(outputArgumentName: outputArgumentName, versionArgumentName: versionArgumentName, version: version) } From 8ed3dffccc295d55ab8f14b481119b7d93070ad6 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Fri, 8 Aug 2025 14:41:54 -0400 Subject: [PATCH 088/216] Rewrite Windows image attachments to use WIC instead of GDI+. (#1254) This PR shifts our support for Windows image attachments onto WIC (Windows Imaging Component) instead of GDI+, which is an older API that supports fewer types of image object. This change also removes the dependency on C++ interop from the WinSDK overlay module, which could have prevented it from being used by modules that _don't_ have C++ interop enabled. Yes, `obj.pointee.lpVtbl.pointee.Release(obj)` is how you spell it. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- Package.swift | 2 +- .../Attachment+AttachableAsCGImage.swift | 22 +- .../AttachableAsGDIPlusImage.swift | 129 ---------- .../Attachments/AttachableAsIWICBitmap.swift | 197 +++++++++++++++ .../AttachableImageFormat+CLSID.swift | 231 ++++++++++-------- ...> Attachment+AttachableAsIWICBitmap.swift} | 38 +-- .../_Testing_WinSDK/Attachments/GDI+.swift | 71 ------ .../HBITMAP+AttachableAsGDIPlusImage.swift | 27 -- .../HBITMAP+AttachableAsIWICBitmap.swift | 42 ++++ .../HICON+AttachableAsGDIPlusImage.swift | 27 -- .../HICON+AttachableAsIWICBitmap.swift | 41 ++++ .../IWICBitmap+AttachableAsIWICBitmap.swift | 35 +++ .../Attachments/ImageAttachmentError.swift | 51 ++++ ...ablePointer+AttachableAsGDIPlusImage.swift | 24 -- ...utablePointer+AttachableAsIWICBitmap.swift | 30 +++ .../Attachments/_AttachableImageWrapper.swift | 142 ++++++----- .../Support/Additions/GUIDAdditions.swift | 37 +++ .../Additions/IPropertyBag2Additions.swift | 40 +++ .../IWICImagingFactoryAdditions.swift | 40 +++ Sources/_TestingInternals/GDI+/include/GDI+.h | 71 ------ Sources/_TestingInternals/include/Stubs.h | 18 -- .../include/module.modulemap | 7 - Tests/TestingTests/AttachmentTests.swift | 57 ++++- 23 files changed, 814 insertions(+), 565 deletions(-) delete mode 100644 Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsGDIPlusImage.swift create mode 100644 Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsIWICBitmap.swift rename Sources/Overlays/_Testing_WinSDK/Attachments/{Attachment+AttachableAsGDIPlusImage.swift => Attachment+AttachableAsIWICBitmap.swift} (75%) delete mode 100644 Sources/Overlays/_Testing_WinSDK/Attachments/GDI+.swift delete mode 100644 Sources/Overlays/_Testing_WinSDK/Attachments/HBITMAP+AttachableAsGDIPlusImage.swift create mode 100644 Sources/Overlays/_Testing_WinSDK/Attachments/HBITMAP+AttachableAsIWICBitmap.swift delete mode 100644 Sources/Overlays/_Testing_WinSDK/Attachments/HICON+AttachableAsGDIPlusImage.swift create mode 100644 Sources/Overlays/_Testing_WinSDK/Attachments/HICON+AttachableAsIWICBitmap.swift create mode 100644 Sources/Overlays/_Testing_WinSDK/Attachments/IWICBitmap+AttachableAsIWICBitmap.swift create mode 100644 Sources/Overlays/_Testing_WinSDK/Attachments/ImageAttachmentError.swift delete mode 100644 Sources/Overlays/_Testing_WinSDK/Attachments/UnsafeMutablePointer+AttachableAsGDIPlusImage.swift create mode 100644 Sources/Overlays/_Testing_WinSDK/Attachments/UnsafeMutablePointer+AttachableAsIWICBitmap.swift create mode 100644 Sources/Overlays/_Testing_WinSDK/Support/Additions/GUIDAdditions.swift create mode 100644 Sources/Overlays/_Testing_WinSDK/Support/Additions/IPropertyBag2Additions.swift create mode 100644 Sources/Overlays/_Testing_WinSDK/Support/Additions/IWICImagingFactoryAdditions.swift delete mode 100644 Sources/_TestingInternals/GDI+/include/GDI+.h diff --git a/Package.swift b/Package.swift index 58ba9552a..80db6076c 100644 --- a/Package.swift +++ b/Package.swift @@ -254,7 +254,7 @@ let package = Package( "Testing", ], path: "Sources/Overlays/_Testing_WinSDK", - swiftSettings: .packageSettings + .enableLibraryEvolution() + [.interoperabilityMode(.Cxx)] + swiftSettings: .packageSettings + .enableLibraryEvolution() ), // Utility targets: These are utilities intended for use when developing diff --git a/Sources/Overlays/_Testing_CoreGraphics/Attachments/Attachment+AttachableAsCGImage.swift b/Sources/Overlays/_Testing_CoreGraphics/Attachments/Attachment+AttachableAsCGImage.swift index 10c866e3a..866240e64 100644 --- a/Sources/Overlays/_Testing_CoreGraphics/Attachments/Attachment+AttachableAsCGImage.swift +++ b/Sources/Overlays/_Testing_CoreGraphics/Attachments/Attachment+AttachableAsCGImage.swift @@ -17,12 +17,11 @@ extension Attachment { /// Initialize an instance of this type that encloses the given image. /// /// - Parameters: - /// - attachableValue: The value that will be attached to the output of - /// the test run. + /// - image: The value that will be attached to the output of the test run. /// - preferredName: The preferred name of the attachment when writing it /// to a test report or to disk. If `nil`, the testing library attempts /// to derive a reasonable filename for the attached value. - /// - imageFormat: The image format with which to encode `attachableValue`. + /// - imageFormat: The image format with which to encode `image`. /// - sourceLocation: The source location of the call to this initializer. /// This value is used when recording issues associated with the /// attachment. @@ -45,12 +44,12 @@ extension Attachment { /// correspond to an image format the operating system knows how to write, the /// testing library selects an appropriate image format for you. public init( - _ attachableValue: T, + _ image: T, named preferredName: String? = nil, as imageFormat: AttachableImageFormat? = nil, sourceLocation: SourceLocation = #_sourceLocation ) where AttachableValue == _AttachableImageWrapper { - let imageWrapper = _AttachableImageWrapper(image: attachableValue, imageFormat: imageFormat) + let imageWrapper = _AttachableImageWrapper(image: image, imageFormat: imageFormat) self.init(imageWrapper, named: preferredName, sourceLocation: sourceLocation) } @@ -61,7 +60,7 @@ extension Attachment { /// - preferredName: The preferred name of the attachment when writing it to /// a test report or to disk. If `nil`, the testing library attempts to /// derive a reasonable filename for the attached value. - /// - imageFormat: The image format with which to encode `attachableValue`. + /// - imageFormat: The image format with which to encode `image`. /// - sourceLocation: The source location of the call to this function. /// /// This function creates a new instance of ``Attachment`` wrapping `image` @@ -94,4 +93,15 @@ extension Attachment { Self.record(attachment, sourceLocation: sourceLocation) } } + +@_spi(Experimental) // STOP: not part of ST-0014 +@available(_uttypesAPI, *) +extension Attachment where AttachableValue: AttachableWrapper, AttachableValue.Wrapped: AttachableAsCGImage { + /// The image format to use when encoding the represented image. + @_disfavoredOverload + public var imageFormat: AttachableImageFormat? { + // FIXME: no way to express `where AttachableValue == _AttachableImageWrapper` on a property + (attachableValue as? _AttachableImageWrapper)?.imageFormat + } +} #endif diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsGDIPlusImage.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsGDIPlusImage.swift deleted file mode 100644 index daaf88df1..000000000 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsGDIPlusImage.swift +++ /dev/null @@ -1,129 +0,0 @@ -// -// 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 -// - -#if os(Windows) -@_spi(Experimental) import Testing -private import _TestingInternals.GDIPlus - -internal import WinSDK - -/// A protocol describing images that can be converted to instances of -/// ``Testing/Attachment``. -/// -/// Instances of types conforming to this protocol do not themselves conform to -/// ``Testing/Attachable``. Instead, the testing library provides additional -/// initializers on ``Testing/Attachment`` that take instances of such types and -/// handle converting them to image data when needed. -/// -/// The following system-provided image types conform to this protocol and can -/// be attached to a test: -/// -/// - [`HBITMAP`](https://learn.microsoft.com/en-us/windows/win32/gdi/bitmaps) -/// - [`HICON`](https://learn.microsoft.com/en-us/windows/win32/menurc/icons) -/// -/// You do not generally need to add your own conformances to this protocol. If -/// you have an image in another format that needs to be attached to a test, -/// first convert it to an instance of one of the types above. -@_spi(Experimental) -public protocol _AttachableByAddressAsGDIPlusImage { - /// Create a GDI+ image representing an instance of this type at the given - /// address. - /// - /// - Parameters: - /// - imageAddress: The address of the instance of this type. - /// - /// - Returns: A pointer to a new GDI+ image representing this image. The - /// caller is responsible for deleting this image when done with it. - /// - /// - Throws: Any error that prevented the creation of the GDI+ image. - /// - /// - Note: This function returns a value of C++ type `Gdiplus::Image *`. That - /// type cannot be directly represented in Swift. If this function returns a - /// value of any other concrete type, the result is undefined. - /// - /// The testing library automatically calls `GdiplusStartup()` and - /// `GdiplusShutdown()` before and after calling this function. This function - /// can therefore assume that GDI+ is correctly configured on the current - /// thread when it is called. - /// - /// This function is not part of the public interface of the testing library. - /// It may be removed in a future update. - static func _copyAttachableGDIPlusImage(at imageAddress: UnsafeMutablePointer) throws -> OpaquePointer - - /// Clean up any resources at the given address. - /// - /// - Parameters: - /// - imageAddress: The address of the instance of this type. - /// - /// The implementation of this function cleans up any resources (such as - /// handles or COM objects) associated with this value. The testing library - /// automatically invokes this function as needed. - /// - /// This function is not responsible for deleting the image returned from - /// `_copyAttachableGDIPlusImage(at:)`. - /// - /// This function is not part of the public interface of the testing library. - /// It may be removed in a future update. - static func _cleanUpAttachment(at imageAddress: UnsafeMutablePointer) -} - -/// A protocol describing images that can be converted to instances of -/// ``Testing/Attachment``. -/// -/// Instances of types conforming to this protocol do not themselves conform to -/// ``Testing/Attachable``. Instead, the testing library provides additional -/// initializers on ``Testing/Attachment`` that take instances of such types and -/// handle converting them to image data when needed. -/// -/// The following system-provided image types conform to this protocol and can -/// be attached to a test: -/// -/// - [`HBITMAP`](https://learn.microsoft.com/en-us/windows/win32/gdi/bitmaps) -/// - [`HICON`](https://learn.microsoft.com/en-us/windows/win32/menurc/icons) -/// -/// You do not generally need to add your own conformances to this protocol. If -/// you have an image in another format that needs to be attached to a test, -/// first convert it to an instance of one of the types above. -@_spi(Experimental) -public protocol AttachableAsGDIPlusImage { - /// Create a GDI+ image representing this instance. - /// - /// - Returns: A pointer to a new GDI+ image representing this image. The - /// caller is responsible for deleting this image when done with it. - /// - /// - Throws: Any error that prevented the creation of the GDI+ image. - /// - /// - Note: This function returns a value of C++ type `Gdiplus::Image *`. That - /// type cannot be directly represented in Swift. If this function returns a - /// value of any other concrete type, the result is undefined. - /// - /// The testing library automatically calls `GdiplusStartup()` and - /// `GdiplusShutdown()` before and after calling this function. This function - /// can therefore assume that GDI+ is correctly configured on the current - /// thread when it is called. - /// - /// This function is not part of the public interface of the testing library. - /// It may be removed in a future update. - func _copyAttachableGDIPlusImage() throws -> OpaquePointer - - /// Clean up any resources associated with this instance. - /// - /// The implementation of this function cleans up any resources (such as - /// handles or COM objects) associated with this value. The testing library - /// automatically invokes this function as needed. - /// - /// This function is not responsible for deleting the image returned from - /// `_copyAttachableGDIPlusImage()`. - /// - /// This function is not part of the public interface of the testing library. - /// It may be removed in a future update. - func _cleanUpAttachment() -} -#endif diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsIWICBitmap.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsIWICBitmap.swift new file mode 100644 index 000000000..416048650 --- /dev/null +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsIWICBitmap.swift @@ -0,0 +1,197 @@ +// +// 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 +// + +#if os(Windows) +@_spi(Experimental) import Testing + +public import WinSDK + +/// A protocol describing images that can be converted to instances of +/// ``Testing/Attachment``. +/// +/// Instances of types conforming to this protocol do not themselves conform to +/// ``Testing/Attachable``. Instead, the testing library provides additional +/// initializers on ``Testing/Attachment`` that take instances of such types and +/// handle converting them to image data when needed. +/// +/// The following system-provided image types conform to this protocol and can +/// be attached to a test: +/// +/// - [`HBITMAP`](https://learn.microsoft.com/en-us/windows/win32/gdi/bitmaps) +/// - [`HICON`](https://learn.microsoft.com/en-us/windows/win32/menurc/icons) +/// - [`IWICBitmap`](https://learn.microsoft.com/en-us/windows/win32/api/wincodec/nn-wincodec-iwicbitmap) +/// +/// You do not generally need to add your own conformances to this protocol. If +/// you have an image in another format that needs to be attached to a test, +/// first convert it to an instance of one of the types above. +@_spi(Experimental) +public protocol _AttachableByAddressAsIWICBitmap { + /// Create a WIC bitmap representing an instance of this type at the given + /// address. + /// + /// - Parameters: + /// - imageAddress: The address of the instance of this type. + /// - factory: A WIC imaging factory that can be used to create additional + /// WIC objects. + /// + /// - Returns: A pointer to a new WIC bitmap representing this image. The + /// caller is responsible for releasing this image when done with it. + /// + /// - Throws: Any error that prevented the creation of the WIC bitmap. + /// + /// This function is not part of the public interface of the testing library. + /// It may be removed in a future update. + static func _copyAttachableIWICBitmap( + from imageAddress: UnsafeMutablePointer, + using factory: UnsafeMutablePointer + ) throws -> UnsafeMutablePointer + + /// Make a copy of the instance of this type at the given address. + /// + /// - Parameters: + /// - imageAddress: The address of the instance of this type that should be + /// copied. + /// + /// - Returns: A copy of `imageAddress`, or `imageAddress` if this type does + /// not support a copying operation. + /// + /// - Throws: Any error that prevented copying the value at `imageAddress`. + /// + /// The testing library uses this function to take ownership of image + /// resources that test authors pass to it. If possible, make a copy of or add + /// a reference to the value at `imageAddress`. If this type does not support + /// making copies, return `imageAddress` verbatim. + /// + /// This function is not part of the public interface of the testing library. + /// It may be removed in a future update. + static func _copyAttachableValue(at imageAddress: UnsafeMutablePointer) throws -> UnsafeMutablePointer + + /// Manually deinitialize any resources at the given address. + /// + /// - Parameters: + /// - imageAddress: The address of the instance of this type. + /// + /// The implementation of this function is responsible for balancing a + /// previous call to `_copyAttachableValue(at:)` by cleaning up any resources + /// (such as handles or COM objects) associated with the value at + /// `imageAddress`. The testing library automatically invokes this function as + /// needed. If `_copyAttachableValue(at:)` threw an error, the testing library + /// does not call this function. + /// + /// This function is not responsible for releasing the image returned from + /// `_copyAttachableIWICBitmap(from:using:)`. + /// + /// This function is not part of the public interface of the testing library. + /// It may be removed in a future update. + static func _deinitializeAttachableValue(at imageAddress: UnsafeMutablePointer) +} + +/// A protocol describing images that can be converted to instances of +/// ``Testing/Attachment``. +/// +/// Instances of types conforming to this protocol do not themselves conform to +/// ``Testing/Attachable``. Instead, the testing library provides additional +/// initializers on ``Testing/Attachment`` that take instances of such types and +/// handle converting them to image data when needed. +/// +/// The following system-provided image types conform to this protocol and can +/// be attached to a test: +/// +/// - [`HBITMAP`](https://learn.microsoft.com/en-us/windows/win32/gdi/bitmaps) +/// - [`HICON`](https://learn.microsoft.com/en-us/windows/win32/menurc/icons) +/// - [`IWICBitmap`](https://learn.microsoft.com/en-us/windows/win32/api/wincodec/nn-wincodec-iwicbitmap) +/// +/// You do not generally need to add your own conformances to this protocol. If +/// you have an image in another format that needs to be attached to a test, +/// first convert it to an instance of one of the types above. +@_spi(Experimental) +public protocol AttachableAsIWICBitmap { + /// Create a WIC bitmap representing an instance of this type. + /// + /// - Parameters: + /// - factory: A WIC imaging factory that can be used to create additional + /// WIC objects. + /// + /// - Returns: A pointer to a new WIC bitmap representing this image. The + /// caller is responsible for releasing this image when done with it. + /// + /// - Throws: Any error that prevented the creation of the WIC bitmap. + /// + /// This function is not part of the public interface of the testing library. + /// It may be removed in a future update. + borrowing func _copyAttachableIWICBitmap( + using factory: UnsafeMutablePointer + ) throws -> UnsafeMutablePointer + + /// Make a copy of this instance. + /// + /// - Returns: A copy of `self`, or `self` if this type does not support a + /// copying operation. + /// + /// - Throws: Any error that prevented copying this value. + /// + /// The testing library uses this function to take ownership of image + /// resources that test authors pass to it. If possible, make a copy of or add + /// a reference to `self`. If this type does not support making copies, return + /// `self` verbatim. + /// + /// This function is not part of the public interface of the testing library. + /// It may be removed in a future update. + func _copyAttachableValue() throws -> Self + + /// Manually deinitialize any resources associated with this image. + /// + /// The implementation of this function cleans up any resources (such as + /// handles or COM objects) associated with this image. The testing library + /// automatically invokes this function as needed. + /// + /// This function is not responsible for releasing the image returned from + /// `_copyAttachableIWICBitmap(using:)`. + /// + /// This function is not part of the public interface of the testing library. + /// It may be removed in a future update. + func _deinitializeAttachableValue() +} + +extension AttachableAsIWICBitmap { + /// Create a WIC bitmap representing an instance of this type and return it as + /// an instance of `IWICBitmapSource`. + /// + /// - Parameters: + /// - factory: A WIC imaging factory that can be used to create additional + /// WIC objects. + /// + /// - Returns: A pointer to a new WIC bitmap representing this image. The + /// caller is responsible for releasing this image when done with it. + /// + /// - Throws: Any error that prevented the creation of the WIC bitmap. + /// + /// This function is a convenience over `_copyAttachableIWICBitmap(using:)` + /// that casts the result of that function to `IWICBitmapSource` (as needed + /// by WIC when it encodes the image.) + borrowing func copyAttachableIWICBitmapSource( + using factory: UnsafeMutablePointer + ) throws -> UnsafeMutablePointer { + let bitmap = try _copyAttachableIWICBitmap(using: factory) + defer { + _ = bitmap.pointee.lpVtbl.pointee.Release(bitmap) + } + + return try withUnsafePointer(to: IID_IWICBitmapSource) { IID_IWICBitmapSource in + var bitmapSource: UnsafeMutableRawPointer? + let rQuery = bitmap.pointee.lpVtbl.pointee.QueryInterface(bitmap, IID_IWICBitmapSource, &bitmapSource) + guard rQuery == S_OK, let bitmapSource else { + throw ImageAttachmentError.queryInterfaceFailed(IWICBitmapSource.self, rQuery) + } + return bitmapSource.assumingMemoryBound(to: IWICBitmapSource.self) + } + } +} +#endif diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableImageFormat+CLSID.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableImageFormat+CLSID.swift index 8985b2aeb..2fe25abb9 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableImageFormat+CLSID.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableImageFormat+CLSID.swift @@ -10,101 +10,141 @@ #if os(Windows) @_spi(Experimental) import Testing -private import _TestingInternals.GDIPlus public import WinSDK extension AttachableImageFormat { - /// The set of `ImageCodecInfo` instances known to GDI+. - /// - /// If the testing library was unable to determine the set of image formats, - /// the value of this property is `nil`. - /// - /// - Note: The type of this property is a buffer pointer rather than an array - /// because the resulting buffer owns trailing untyped memory where path - /// extensions and other fields are stored. Do not deallocate this buffer. - private static nonisolated(unsafe) let _allCodecs: UnsafeBufferPointer = { - let result = try? withGDIPlus { - // Find out the size of the buffer needed. - var codecCount = UINT(0) - var byteCount = UINT(0) - let rGetSize = Gdiplus.GetImageEncodersSize(&codecCount, &byteCount) - guard rGetSize == Gdiplus.Ok else { - throw GDIPlusError.status(rGetSize) + private static let _encoderPathExtensionsByCLSID = Result<[UInt128: [String]], any Error> { + var result = [UInt128: [String]]() + + // Create an imaging factory. + let factory = try IWICImagingFactory.create() + defer { + _ = factory.pointee.lpVtbl.pointee.Release(factory) + } + + // Create a COM enumerator over the encoders known to WIC. + var enumerator: UnsafeMutablePointer? + let rCreate = factory.pointee.lpVtbl.pointee.CreateComponentEnumerator( + factory, + DWORD(bitPattern: WICEncoder.rawValue), + DWORD(bitPattern: WICComponentEnumerateDefault.rawValue), + &enumerator + ) + guard rCreate == S_OK, let enumerator else { + throw ImageAttachmentError.comObjectCreationFailed(IEnumUnknown.self, rCreate) + } + defer { + _ = enumerator.pointee.lpVtbl.pointee.Release(enumerator) + } + + // Loop through the iterator and extract the path extensions and CLSID of + // each encoder we find. + while true { + var nextObject: UnsafeMutablePointer? + guard S_OK == enumerator.pointee.lpVtbl.pointee.Next(enumerator, 1, &nextObject, nil), let nextObject else { + // End of loop. + break + } + defer { + _ = nextObject.pointee.lpVtbl.pointee.Release(nextObject) } - // Allocate a buffer of sufficient byte size, then bind the leading bytes - // to ImageCodecInfo. This leaves some number of trailing bytes unbound to - // any Swift type. - let result = UnsafeMutableRawBufferPointer.allocate( - byteCount: Int(byteCount), - alignment: MemoryLayout.alignment - ) - let codecBuffer = result - .prefix(MemoryLayout.stride * Int(codecCount)) - .bindMemory(to: Gdiplus.ImageCodecInfo.self) + // Cast the enumerated object to the correct/expected type. + let info = try withUnsafePointer(to: IID_IWICBitmapEncoderInfo) { IID_IWICBitmapEncoderInfo in + var info: UnsafeMutableRawPointer? + let rQuery = nextObject.pointee.lpVtbl.pointee.QueryInterface(nextObject, IID_IWICBitmapEncoderInfo, &info) + guard rQuery == S_OK, let info else { + throw ImageAttachmentError.queryInterfaceFailed(IWICBitmapEncoderInfo.self, rQuery) + } + return info.assumingMemoryBound(to: IWICBitmapEncoderInfo.self) + } + defer { + _ = info.pointee.lpVtbl.pointee.Release(info) + } - // Read the encoders list. - let rGetEncoders = Gdiplus.GetImageEncoders(codecCount, byteCount, codecBuffer.baseAddress!) - guard rGetEncoders == Gdiplus.Ok else { - result.deallocate() - throw GDIPlusError.status(rGetEncoders) + var clsid = CLSID() + guard S_OK == info.pointee.lpVtbl.pointee.GetCLSID(info, &clsid) else { + continue } - return UnsafeBufferPointer(codecBuffer) + let extensions = _pathExtensions(for: info) + result[UInt128(clsid)] = extensions } - return result ?? UnsafeBufferPointer(start: nil, count: 0) - }() + + return result + } /// Get the set of path extensions corresponding to the image format - /// represented by a GDI+ codec info structure. + /// represented by a WIC bitmap encoder info object. /// /// - Parameters: - /// - codec: The GDI+ codec info structure of interest. + /// - info: The WIC bitmap encoder info object of interest. /// /// - Returns: An array of zero or more path extensions. The case of the /// resulting strings is unspecified. - private static func _pathExtensions(for codec: Gdiplus.ImageCodecInfo) -> [String] { - guard let extensions = String.decodeCString(codec.FilenameExtension, as: UTF16.self)?.result else { + private static func _pathExtensions(for info: UnsafeMutablePointer) -> [String] { + // Figure out the size of the buffer we need. (Microsoft does not specify if + // the size is in wide characters or bytes.) + var charCount = UINT(0) + var rGet = info.pointee.lpVtbl.pointee.GetFileExtensions(info, 0, nil, &charCount) + guard rGet == S_OK else { + return [] + } + + // Allocate the necessary buffer and populate it. + let buffer = UnsafeMutableBufferPointer.allocate(capacity: Int(charCount)) + defer { + buffer.deallocate() + } + rGet = info.pointee.lpVtbl.pointee.GetFileExtensions(info, UINT(buffer.count), buffer.baseAddress!, &charCount) + guard rGet == S_OK else { + return [] + } + + // Convert the buffer to a Swift string for further manipulation. + guard let extensions = String.decodeCString(buffer.baseAddress!, as: UTF16.self)?.result else { return [] } + return extensions - .split(separator: ";") + .split(separator: ",") .map { ext in - if ext.starts(with: "*.") { - ext.dropFirst(2) + if ext.starts(with: ".") { + ext.dropFirst(1) } else { ext[...] } - }.map{ $0.lowercased() } // Vestiges of MS-DOS... + }.map(String.init) } - /// Get the `CLSID` value corresponding to the same image format as the given - /// path extension. + /// Get the `CLSID` value of the WIC image encoder corresponding to the same + /// image format as the given path extension. /// /// - Parameters: /// - pathExtension: The path extension (as a wide C string) for which a /// `CLSID` value is needed. /// - /// - Returns: An instance of `CLSID` referring to a concrete image type, or + /// - Returns: An instance of `CLSID` referring to a a WIC image encoder, or /// `nil` if one could not be determined. private static func _computeCLSID(forPathExtension pathExtension: UnsafePointer) -> CLSID? { - _allCodecs.first { codec in - _pathExtensions(for: codec) - .contains { codecExtension in - codecExtension.withCString(encodedAs: UTF16.self) { codecExtension in - 0 == _wcsicmp(pathExtension, codecExtension) + let encoderPathExtensionsByCLSID = (try? _encoderPathExtensionsByCLSID.get()) ?? [:] + return encoderPathExtensionsByCLSID + .first { _, extensions in + extensions.contains { encoderExt in + encoderExt.withCString(encodedAs: UTF16.self) { encoderExt in + 0 == _wcsicmp(pathExtension, encoderExt) } } - }.map(\.Clsid) + }.map { CLSID($0.key) } } - /// Get the `CLSID` value corresponding to the same image format as the given - /// path extension. + /// Get the `CLSID` value of the WIC image encoder corresponding to the same + /// image format as the given path extension. /// /// - Parameters: /// - pathExtension: The path extension for which a `CLSID` value is needed. /// - /// - Returns: An instance of `CLSID` referring to a concrete image type, or + /// - Returns: An instance of `CLSID` referring to a a WIC image encoder, or /// `nil` if one could not be determined. private static func _computeCLSID(forPathExtension pathExtension: String) -> CLSID? { pathExtension.withCString(encodedAs: UTF16.self) { pathExtension in @@ -112,14 +152,14 @@ extension AttachableImageFormat { } } - /// Get the `CLSID` value corresponding to the same image format as the path - /// extension on the given attachment filename. + /// Get the `CLSID` value of the WIC image encoder corresponding to the same + /// image format as the path extension on the given attachment filename. /// /// - Parameters: /// - preferredName: The preferred name of the image for which a `CLSID` /// value is needed. /// - /// - Returns: An instance of `CLSID` referring to a concrete image type, or + /// - Returns: An instance of `CLSID` referring to a a WIC image encoder, or /// `nil` if one could not be determined. private static func _computeCLSID(forPreferredName preferredName: String) -> CLSID? { preferredName.withCString(encodedAs: UTF16.self) { (preferredName) -> CLSID? in @@ -132,7 +172,8 @@ extension AttachableImageFormat { } } - /// Get the `CLSID` value` to use when encoding the image. + /// Get the `CLSID` value of the WIC image encoder to use when encoding an + /// image. /// /// - Parameters: /// - imageFormat: The image format to use, or `nil` if the developer did @@ -140,8 +181,9 @@ extension AttachableImageFormat { /// - preferredName: The preferred name of the image for which a type is /// needed. /// - /// - Returns: An instance of `CLSID` referring to a concrete image type, or - /// `nil` if one could not be determined. + /// - Returns: An instance of `CLSID` referring to a a WIC image encoder. If + /// none could be derived from `imageFormat` or `preferredName`, the PNG + /// encoder is used. /// /// This function is not part of the public interface of the testing library. static func computeCLSID(for imageFormat: Self?, withPreferredName preferredName: String) -> CLSID { @@ -158,19 +200,19 @@ extension AttachableImageFormat { // We couldn't derive a concrete type from the path extension, so default // to PNG. Unlike Apple platforms, there's no abstract "image" type on // Windows so we don't need to make any more decisions. - return _pngCLSID + return CLSID_WICPngEncoder } - /// Append the path extension preferred by GDI+ for the given `CLSID` value - /// representing an image format to a suggested extension filename. + /// Append the path extension preferred by WIC for the image format + /// corresponding to the given `CLSID` value or the given filename. /// /// - Parameters: /// - clsid: The `CLSID` value representing the image format of interest. /// - preferredName: The preferred name of the image for which a type is /// needed. /// - /// - Returns: A string containing the corresponding path extension, or `nil` - /// if none could be determined. + /// - Returns: A copy of `preferredName`, possibly modified to include a path + /// extension appropriate for `CLSID`. static func appendPathExtension(for clsid: CLSID, to preferredName: String) -> String { // If there's already a CLSID associated with the filename, and it matches // the one passed to us, no changes are needed. @@ -178,38 +220,24 @@ extension AttachableImageFormat { return preferredName } - let ext = _allCodecs - .first { $0.Clsid == clsid } - .flatMap { _pathExtensions(for: $0).first } - guard let ext else { - // Couldn't find a path extension for the given CLSID, so make no changes. - return preferredName + // Find the preferred path extension for the encoder with the given CLSID. + let encoderPathExtensionsByCLSID = (try? _encoderPathExtensionsByCLSID.get()) ?? [:] + if let ext = encoderPathExtensionsByCLSID[UInt128(clsid)]?.first { + return "\(preferredName).\(ext)" } - return "\(preferredName).\(ext)" + // Couldn't find anything better. Return the preferred name unmodified. + return preferredName } - /// The `CLSID` value corresponding to the PNG image format. - /// - /// - Note: The named constant [`ImageFormatPNG`](https://learn.microsoft.com/en-us/windows/win32/gdiplus/-gdiplus-constant-image-file-format-constants) - /// is not the correct value and will cause `Image::Save()` to fail if - /// passed to it. - private static let _pngCLSID = _computeCLSID(forPathExtension: "png")! - - /// The `CLSID` value corresponding to the JPEG image format. - /// - /// - Note: The named constant [`ImageFormatJPEG`](https://learn.microsoft.com/en-us/windows/win32/gdiplus/-gdiplus-constant-image-file-format-constants) - /// is not the correct value and will cause `Image::Save()` to fail if - /// passed to it. - private static let _jpegCLSID = _computeCLSID(forPathExtension: "jpg")! - - /// The `CLSID` value corresponding to this image format. + /// The `CLSID` value corresponding to the WIC image encoder for this image + /// format. public var clsid: CLSID { switch kind { case .png: - Self._pngCLSID + CLSID_WICPngEncoder case .jpeg: - Self._jpegCLSID + CLSID_WICJpegEncoder case let .systemValue(clsid): clsid as! CLSID } @@ -219,19 +247,19 @@ extension AttachableImageFormat { /// encoding quality. /// /// - Parameters: - /// - clsid: The `CLSID` value corresponding to the image format to use when - /// encoding images. + /// - clsid: The `CLSID` value corresponding to a WIC image encoder to use + /// when encoding images. /// - encodingQuality: The encoding quality to use when encoding images. For /// the lowest supported quality, pass `0.0`. For the highest supported /// quality, pass `1.0`. /// - /// If the target image format does not support variable-quality encoding, + /// If the target image encoder does not support variable-quality encoding, /// the value of the `encodingQuality` argument is ignored. /// - /// If `clsid` does not represent an image format supported by GDI+, the - /// result is undefined. For a list of image formats supported by GDI+, see - /// the [GetImageEncoders()](https://learn.microsoft.com/en-us/windows/win32/api/gdiplusimagecodec/nf-gdiplusimagecodec-getimageencoders) - /// function. + /// If `clsid` does not represent an image encoder type supported by WIC, the + /// result is undefined. For a list of image encoders supported by WIC, see + /// the documentation for the [IWICBitmapEncoder](https://learn.microsoft.com/en-us/windows/win32/api/wincodec/nn-wincodec-iwicbitmapencoder) + /// class. public init(_ clsid: CLSID, encodingQuality: Float = 1.0) { self.init(kind: .systemValue(clsid), encodingQuality: encodingQuality) } @@ -249,12 +277,13 @@ extension AttachableImageFormat { /// If the target image format does not support variable-quality encoding, /// the value of the `encodingQuality` argument is ignored. /// - /// If `pathExtension` does not correspond to an image format supported by - /// GDI+, this initializer returns `nil`. For a list of image formats - /// supported by GDI+, see the [GetImageEncoders()](https://learn.microsoft.com/en-us/windows/win32/api/gdiplusimagecodec/nf-gdiplusimagecodec-getimageencoders) - /// function. + /// If `pathExtension` does not correspond to an image format that WIC can use + /// to encode images, this initializer returns `nil`. For a list of image + /// encoders supported by WIC, see the documentation for the [IWICBitmapEncoder](https://learn.microsoft.com/en-us/windows/win32/api/wincodec/nn-wincodec-iwicbitmapencoder) + /// class. public init?(pathExtension: String, encodingQuality: Float = 1.0) { let pathExtension = pathExtension.drop { $0 == "." } + let clsid = Self._computeCLSID(forPathExtension: String(pathExtension)) if let clsid { self.init(clsid, encodingQuality: encodingQuality) diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/Attachment+AttachableAsGDIPlusImage.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/Attachment+AttachableAsIWICBitmap.swift similarity index 75% rename from Sources/Overlays/_Testing_WinSDK/Attachments/Attachment+AttachableAsGDIPlusImage.swift rename to Sources/Overlays/_Testing_WinSDK/Attachments/Attachment+AttachableAsIWICBitmap.swift index 54b24d435..b8ff6552e 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/Attachment+AttachableAsGDIPlusImage.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/Attachment+AttachableAsIWICBitmap.swift @@ -16,21 +16,22 @@ extension Attachment where AttachableValue: ~Copyable { /// Initialize an instance of this type that encloses the given image. /// /// - Parameters: - /// - attachableValue: A pointer to the value that will be attached to the - /// output of the test run. + /// - image: A pointer to the value that will be attached to the output of + /// the test run. /// - preferredName: The preferred name of the attachment when writing it /// to a test report or to disk. If `nil`, the testing library attempts /// to derive a reasonable filename for the attached value. - /// - imageFormat: The image format with which to encode `attachableValue`. + /// - imageFormat: The image format with which to encode `image`. /// - sourceLocation: The source location of the call to this initializer. /// This value is used when recording issues associated with the /// attachment. /// /// The following system-provided image types conform to the - /// ``AttachableAsGDIPlusImage`` protocol and can be attached to a test: + /// ``AttachableAsIWICBitmap`` protocol and can be attached to a test: /// /// - [`HBITMAP`](https://learn.microsoft.com/en-us/windows/win32/gdi/bitmaps) /// - [`HICON`](https://learn.microsoft.com/en-us/windows/win32/menurc/icons) + /// - [`IWICBitmap`](https://learn.microsoft.com/en-us/windows/win32/api/wincodec/nn-wincodec-iwicbitmap) /// /// The testing library uses the image format specified by `imageFormat`. Pass /// `nil` to let the testing library decide which image format to use. If you @@ -39,21 +40,13 @@ extension Attachment where AttachableValue: ~Copyable { /// specify a path extension, or if the path extension you specify doesn't /// correspond to an image format the operating system knows how to write, the /// testing library selects an appropriate image format for you. - /// - /// - Important: The resulting instance of ``Attachment`` takes ownership of - /// `attachableValue` and frees its resources upon deinitialization. If you - /// do not want the testing library to take ownership of this value, call - /// ``Attachment/record(_:named:as:sourceLocation)`` instead of this - /// initializer, or make a copy of the resource before passing it to this - /// initializer. - @unsafe public init( - _ attachableValue: consuming T, + _ image: borrowing T, named preferredName: String? = nil, as imageFormat: AttachableImageFormat? = nil, sourceLocation: SourceLocation = #_sourceLocation ) where AttachableValue == _AttachableImageWrapper { - let imageWrapper = _AttachableImageWrapper(image: attachableValue, imageFormat: imageFormat, cleanUpWhenDone: true) + let imageWrapper = _AttachableImageWrapper(image: image, imageFormat: imageFormat) self.init(imageWrapper, named: preferredName, sourceLocation: sourceLocation) } @@ -64,7 +57,7 @@ extension Attachment where AttachableValue: ~Copyable { /// - preferredName: The preferred name of the attachment when writing it /// to a test report or to disk. If `nil`, the testing library attempts /// to derive a reasonable filename for the attached value. - /// - imageFormat: The image format with which to encode `attachableValue`. + /// - imageFormat: The image format with which to encode `image`. /// - sourceLocation: The source location of the call to this initializer. /// This value is used when recording issues associated with the /// attachment. @@ -73,10 +66,11 @@ extension Attachment where AttachableValue: ~Copyable { /// and immediately attaches it to the current test. /// /// The following system-provided image types conform to the - /// ``AttachableAsGDIPlusImage`` protocol and can be attached to a test: + /// ``AttachableAsIWICBitmap`` protocol and can be attached to a test: /// /// - [`HBITMAP`](https://learn.microsoft.com/en-us/windows/win32/gdi/bitmaps) /// - [`HICON`](https://learn.microsoft.com/en-us/windows/win32/menurc/icons) + /// - [`IWICBitmap`](https://learn.microsoft.com/en-us/windows/win32/api/wincodec/nn-wincodec-iwicbitmap) /// /// The testing library uses the image format specified by `imageFormat`. Pass /// `nil` to let the testing library decide which image format to use. If you @@ -91,9 +85,19 @@ extension Attachment where AttachableValue: ~Copyable { as imageFormat: AttachableImageFormat? = nil, sourceLocation: SourceLocation = #_sourceLocation ) where AttachableValue == _AttachableImageWrapper { - let imageWrapper = _AttachableImageWrapper(image: copy image, imageFormat: imageFormat, cleanUpWhenDone: true) + let imageWrapper = _AttachableImageWrapper(image: image, imageFormat: imageFormat) let attachment = Self(imageWrapper, named: preferredName, sourceLocation: sourceLocation) Self.record(attachment, sourceLocation: sourceLocation) } } + +@_spi(Experimental) +extension Attachment where AttachableValue: AttachableWrapper, AttachableValue.Wrapped: AttachableAsIWICBitmap { + /// The image format to use when encoding the represented image. + @_disfavoredOverload + public var imageFormat: AttachableImageFormat? { + // FIXME: no way to express `where AttachableValue == _AttachableImageWrapper` on a property + (attachableValue as? _AttachableImageWrapper)?.imageFormat + } +} #endif diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/GDI+.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/GDI+.swift deleted file mode 100644 index 9535cedfd..000000000 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/GDI+.swift +++ /dev/null @@ -1,71 +0,0 @@ -// -// 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 -// - -#if os(Windows) -@_spi(Experimental) import Testing -internal import _TestingInternals.GDIPlus - -internal import WinSDK - -/// A type describing errors that can be thrown by GDI+. -enum GDIPlusError: Error { - /// A GDI+ status code. - case status(Gdiplus.Status) - - /// The testing library failed to create an in-memory stream. - case streamCreationFailed(HRESULT) - - /// The testing library failed to get an in-memory stream's underlying buffer. - case globalFromStreamFailed(HRESULT) -} - -extension GDIPlusError: CustomStringConvertible { - var description: String { - switch self { - case let .status(status): - "Could not create the corresponding GDI+ image (Gdiplus.Status \(status.rawValue))." - case let .streamCreationFailed(result): - "Could not create an in-memory stream (HRESULT \(result))." - case let .globalFromStreamFailed(result): - "Could not access the buffer containing the encoded image (HRESULT \(result))." - } - } -} - -// MARK: - - -/// Call a function while GDI+ is set up on the current thread. -/// -/// - Parameters: -/// - body: The function to invoke. -/// -/// - Returns: Whatever is returned by `body`. -/// -/// - Throws: Whatever is thrown by `body`. -func withGDIPlus(_ body: () throws -> R) throws -> R { - // "Escape hatch" if the program being tested calls GdiplusStartup() itself in - // some way that is incompatible with our assumptions about it. - if Environment.flag(named: "SWT_GDIPLUS_STARTUP_ENABLED") == false { - return try body() - } - - var token = ULONG_PTR(0) - var input = Gdiplus.GdiplusStartupInput(nil, false, false) - let rStartup = swt_GdiplusStartup(&token, &input, nil) - guard rStartup == Gdiplus.Ok else { - throw GDIPlusError.status(rStartup) - } - defer { - swt_GdiplusShutdown(token) - } - - return try body() -} -#endif diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/HBITMAP+AttachableAsGDIPlusImage.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/HBITMAP+AttachableAsGDIPlusImage.swift deleted file mode 100644 index 466a992ec..000000000 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/HBITMAP+AttachableAsGDIPlusImage.swift +++ /dev/null @@ -1,27 +0,0 @@ -// -// 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 -// - -#if os(Windows) -import Testing -private import _TestingInternals.GDIPlus - -public import WinSDK - -@_spi(Experimental) -extension HBITMAP__: _AttachableByAddressAsGDIPlusImage { - public static func _copyAttachableGDIPlusImage(at imageAddress: UnsafeMutablePointer) throws -> OpaquePointer { - swt_GdiplusImageFromHBITMAP(imageAddress, nil) - } - - public static func _cleanUpAttachment(at imageAddress: UnsafeMutablePointer) { - DeleteObject(imageAddress) - } -} -#endif diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/HBITMAP+AttachableAsIWICBitmap.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/HBITMAP+AttachableAsIWICBitmap.swift new file mode 100644 index 000000000..7223e3cf2 --- /dev/null +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/HBITMAP+AttachableAsIWICBitmap.swift @@ -0,0 +1,42 @@ +// +// 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 +// + +#if os(Windows) +import Testing + +public import WinSDK + +@_spi(Experimental) +extension HBITMAP__: _AttachableByAddressAsIWICBitmap { + public static func _copyAttachableIWICBitmap( + from imageAddress: UnsafeMutablePointer, + using factory: UnsafeMutablePointer + ) throws -> UnsafeMutablePointer { + var bitmap: UnsafeMutablePointer! + let rCreate = factory.pointee.lpVtbl.pointee.CreateBitmapFromHBITMAP(factory, imageAddress, nil, WICBitmapUsePremultipliedAlpha, &bitmap) + guard rCreate == S_OK, let bitmap else { + throw ImageAttachmentError.comObjectCreationFailed(IWICBitmap.self, rCreate) + } + return bitmap + } + + public static func _copyAttachableValue(at imageAddress: UnsafeMutablePointer) throws -> UnsafeMutablePointer { + let result: HBITMAP? = CopyImage(imageAddress, UINT(IMAGE_BITMAP), 0, 0, 0).assumingMemoryBound(to: Self.self) + guard let result else { + throw Win32Error(rawValue: GetLastError()) + } + return result + } + + public static func _deinitializeAttachableValue(at imageAddress: UnsafeMutablePointer) { + DeleteObject(imageAddress) + } +} +#endif diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/HICON+AttachableAsGDIPlusImage.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/HICON+AttachableAsGDIPlusImage.swift deleted file mode 100644 index 0269bee56..000000000 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/HICON+AttachableAsGDIPlusImage.swift +++ /dev/null @@ -1,27 +0,0 @@ -// -// 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 -// - -#if os(Windows) -import Testing -private import _TestingInternals.GDIPlus - -public import WinSDK - -@_spi(Experimental) -extension HICON__: _AttachableByAddressAsGDIPlusImage { - public static func _copyAttachableGDIPlusImage(at imageAddress: UnsafeMutablePointer) throws -> OpaquePointer { - swt_GdiplusImageFromHICON(imageAddress) - } - - public static func _cleanUpAttachment(at imageAddress: UnsafeMutablePointer) { - DeleteObject(imageAddress) - } -} -#endif diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/HICON+AttachableAsIWICBitmap.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/HICON+AttachableAsIWICBitmap.swift new file mode 100644 index 000000000..e06b51388 --- /dev/null +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/HICON+AttachableAsIWICBitmap.swift @@ -0,0 +1,41 @@ +// +// 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 +// + +#if os(Windows) +import Testing + +public import WinSDK + +@_spi(Experimental) +extension HICON__: _AttachableByAddressAsIWICBitmap { + public static func _copyAttachableIWICBitmap( + from imageAddress: UnsafeMutablePointer, + using factory: UnsafeMutablePointer + ) throws -> UnsafeMutablePointer { + var bitmap: UnsafeMutablePointer! + let rCreate = factory.pointee.lpVtbl.pointee.CreateBitmapFromHICON(factory, imageAddress, &bitmap) + guard rCreate == S_OK, let bitmap else { + throw ImageAttachmentError.comObjectCreationFailed(IWICBitmap.self, rCreate) + } + return bitmap + } + + public static func _copyAttachableValue(at imageAddress: UnsafeMutablePointer) throws -> UnsafeMutablePointer { + guard let result = CopyIcon(imageAddress) else { + throw Win32Error(rawValue: GetLastError()) + } + return result + } + + public static func _deinitializeAttachableValue(at imageAddress: UnsafeMutablePointer) { + DestroyIcon(imageAddress) + } +} +#endif diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/IWICBitmap+AttachableAsIWICBitmap.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/IWICBitmap+AttachableAsIWICBitmap.swift new file mode 100644 index 000000000..a7419e3fd --- /dev/null +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/IWICBitmap+AttachableAsIWICBitmap.swift @@ -0,0 +1,35 @@ +// +// 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 +// + +#if os(Windows) +import Testing + +public import WinSDK + +@_spi(Experimental) +extension IWICBitmap: _AttachableByAddressAsIWICBitmap { + public static func _copyAttachableIWICBitmap( + from imageAddress: UnsafeMutablePointer, + using factory: UnsafeMutablePointer + ) throws -> UnsafeMutablePointer { + _ = imageAddress.pointee.lpVtbl.pointee.AddRef(imageAddress) + return imageAddress + } + + public static func _copyAttachableValue(at imageAddress: UnsafeMutablePointer) throws -> UnsafeMutablePointer { + _ = imageAddress.pointee.lpVtbl.pointee.AddRef(imageAddress) + return imageAddress + } + + public static func _deinitializeAttachableValue(at imageAddress: UnsafeMutablePointer) { + _ = imageAddress.pointee.lpVtbl.pointee.Release(imageAddress) + } +} +#endif diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/ImageAttachmentError.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/ImageAttachmentError.swift new file mode 100644 index 000000000..52a51b708 --- /dev/null +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/ImageAttachmentError.swift @@ -0,0 +1,51 @@ +// +// 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 os(Windows) +@_spi(Experimental) import Testing + +internal import WinSDK + +/// A type describing errors that can be thrown by WIC or COM while attaching an +/// image. +enum ImageAttachmentError: Error { + /// A call to `QueryInterface()` failed. + case queryInterfaceFailed(Any.Type, HRESULT) + + /// The testing library failed to create a WIC object. + case comObjectCreationFailed(Any.Type, HRESULT) + + /// An image could not be written. + case imageWritingFailed(HRESULT) + + /// The testing library failed to get an in-memory stream's underlying buffer. + case globalFromStreamFailed(HRESULT) + + /// A property could not be written to a property bag. + case propertyBagWritingFailed(String, HRESULT) +} + +extension ImageAttachmentError: CustomStringConvertible { + var description: String { + switch self { + case let .queryInterfaceFailed(type, result): + "Could not cast a COM object to type '\(type)' (HRESULT \(result))." + case let .comObjectCreationFailed(type, result): + "Could not create a COM object of type '\(type)' (HRESULT \(result))." + case let .imageWritingFailed(result): + "Could not write the image (HRESULT \(result))." + case let .globalFromStreamFailed(result): + "Could not access the buffer containing the encoded image (HRESULT \(result))." + case let .propertyBagWritingFailed(name, result): + "Could not set the property '\(name)' (HRESULT \(result))." + } + } +} +#endif diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/UnsafeMutablePointer+AttachableAsGDIPlusImage.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/UnsafeMutablePointer+AttachableAsGDIPlusImage.swift deleted file mode 100644 index 5a50ef1e7..000000000 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/UnsafeMutablePointer+AttachableAsGDIPlusImage.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// 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 -// - -#if os(Windows) -import Testing - -@_spi(Experimental) -extension UnsafeMutablePointer: AttachableAsGDIPlusImage where Pointee: _AttachableByAddressAsGDIPlusImage { - public func _copyAttachableGDIPlusImage() throws -> OpaquePointer { - try Pointee._copyAttachableGDIPlusImage(at: self) - } - - public func _cleanUpAttachment() { - Pointee._cleanUpAttachment(at: self) - } -} -#endif diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/UnsafeMutablePointer+AttachableAsIWICBitmap.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/UnsafeMutablePointer+AttachableAsIWICBitmap.swift new file mode 100644 index 000000000..1111fd178 --- /dev/null +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/UnsafeMutablePointer+AttachableAsIWICBitmap.swift @@ -0,0 +1,30 @@ +// +// 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 +// + +#if os(Windows) +import Testing + +public import WinSDK + +@_spi(Experimental) +extension UnsafeMutablePointer: AttachableAsIWICBitmap where Pointee: _AttachableByAddressAsIWICBitmap { + public func _copyAttachableIWICBitmap(using factory: UnsafeMutablePointer) throws -> UnsafeMutablePointer { + try Pointee._copyAttachableIWICBitmap(from: self, using: factory) + } + + public func _copyAttachableValue() throws -> Self { + try Pointee._copyAttachableValue(at: self) + } + + public consuming func _deinitializeAttachableValue() { + Pointee._deinitializeAttachableValue(at: self) + } +} +#endif diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/_AttachableImageWrapper.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/_AttachableImageWrapper.swift index 9f23cb140..72afbb575 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/_AttachableImageWrapper.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/_AttachableImageWrapper.swift @@ -10,7 +10,6 @@ #if os(Windows) @_spi(Experimental) public import Testing -private import _TestingInternals.GDIPlus internal import WinSDK @@ -19,91 +18,124 @@ internal import WinSDK /// /// You do not need to use this type directly. Instead, initialize an instance /// of ``Attachment`` using an instance of an image type that conforms to -/// ``AttachableAsGDIPlusImage``. The following system-provided image types -/// conform to the ``AttachableAsGDIPlusImage`` protocol and can be attached to -/// a test: +/// ``AttachableAsIWICBitmap``. The following system-provided image types +/// conform to the ``AttachableAsIWICBitmap`` protocol and can be attached to a +/// test: /// /// - [`HBITMAP`](https://learn.microsoft.com/en-us/windows/win32/gdi/bitmaps) /// - [`HICON`](https://learn.microsoft.com/en-us/windows/win32/menurc/icons) +/// - [`IWICBitmap`](https://learn.microsoft.com/en-us/windows/win32/api/wincodec/nn-wincodec-iwicbitmap) @_spi(Experimental) -public struct _AttachableImageWrapper: ~Copyable where Image: AttachableAsGDIPlusImage { +public final class _AttachableImageWrapper: Sendable where Image: AttachableAsIWICBitmap { /// The underlying image. - var image: Image + nonisolated(unsafe) let image: Result /// The image format to use when encoding the represented image. - var imageFormat: AttachableImageFormat? + let imageFormat: AttachableImageFormat? - /// Whether or not to call `_cleanUpAttachment(at:)` on `pointer` when this - /// instance is deinitialized. - /// - /// - Note: If cleanup is not performed, `pointer` is effectively being - /// borrowed from the calling context. - var cleanUpWhenDone: Bool - - init(image: Image, imageFormat: AttachableImageFormat?, cleanUpWhenDone: Bool) { - self.image = image + init(image: borrowing Image, imageFormat: AttachableImageFormat?) { + self.image = Result { [image = copy image] in + try image._copyAttachableValue() + } self.imageFormat = imageFormat - self.cleanUpWhenDone = cleanUpWhenDone } deinit { - if cleanUpWhenDone { - image._cleanUpAttachment() + if let image = try? image.get() { + image._deinitializeAttachableValue() } } } -@available(*, unavailable) -extension _AttachableImageWrapper: Sendable {} - // MARK: - extension _AttachableImageWrapper: AttachableWrapper { - public var wrappedValue: Image { - image + public var wrappedValue: Image? { + try? image.get() } - public func withUnsafeBytes(for attachment: borrowing Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { + public func withUnsafeBytes(for attachment: borrowing Attachment<_AttachableImageWrapper>, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { // Create an in-memory stream to write the image data to. Note that Windows // documentation recommends SHCreateMemStream() instead, but that function // does not provide a mechanism to access the underlying memory directly. var stream: UnsafeMutablePointer? let rCreateStream = CreateStreamOnHGlobal(nil, true, &stream) guard S_OK == rCreateStream, let stream else { - throw GDIPlusError.streamCreationFailed(rCreateStream) + throw ImageAttachmentError.comObjectCreationFailed(IStream.self, rCreateStream) } defer { - stream.withMemoryRebound(to: IUnknown.self, capacity: 1) { stream in - _ = swt_IUnknown_Release(stream) - } + _ = stream.pointee.lpVtbl.pointee.Release(stream) } - try withGDIPlus { - // Get a GDI+ image from the attachment. - let image = try image._copyAttachableGDIPlusImage() - defer { - swt_GdiplusImageDelete(image) - } + // Get an imaging factory to create the WIC bitmap and encoder. + let factory = try IWICImagingFactory.create() + defer { + _ = factory.pointee.lpVtbl.pointee.Release(factory) + } + + // Create the bitmap and downcast it to an IWICBitmapSource for later use. + let bitmap = try image.get().copyAttachableIWICBitmapSource(using: factory) + defer { + _ = bitmap.pointee.lpVtbl.pointee.Release(bitmap) + } - // Get the CLSID of the image encoder corresponding to the specified image - // format. - var clsid = AttachableImageFormat.computeCLSID(for: imageFormat, withPreferredName: attachment.preferredName) - - var encodingQuality = LONG((imageFormat?.encodingQuality ?? 1.0) * 100.0) - try withUnsafeMutableBytes(of: &encodingQuality) { encodingQuality in - var encoderParams = Gdiplus.EncoderParameters() - encoderParams.Count = 1 - encoderParams.Parameter.Guid = swt_GdiplusEncoderQuality() - encoderParams.Parameter.Type = ULONG(Gdiplus.EncoderParameterValueTypeLong.rawValue) - encoderParams.Parameter.NumberOfValues = 1 - encoderParams.Parameter.Value = encodingQuality.baseAddress - - // Save the image into the stream. - let rSave = swt_GdiplusImageSave(image, stream, &clsid, &encoderParams) - guard rSave == Gdiplus.Ok else { - throw GDIPlusError.status(rSave) - } + // Create the encoder. + let encoder = try withUnsafePointer(to: IID_IWICBitmapEncoder) { [preferredName = attachment.preferredName] IID_IWICBitmapEncoder in + var encoderCLSID = AttachableImageFormat.computeCLSID(for: imageFormat, withPreferredName: preferredName) + var encoder: UnsafeMutableRawPointer? + let rCreate = CoCreateInstance( + &encoderCLSID, + nil, + DWORD(bitPattern: CLSCTX_INPROC_SERVER.rawValue), + IID_IWICBitmapEncoder, + &encoder + ) + guard rCreate == S_OK, let encoder = encoder?.assumingMemoryBound(to: IWICBitmapEncoder.self) else { + throw ImageAttachmentError.comObjectCreationFailed(IWICBitmapEncoder.self, rCreate) } + return encoder + } + defer { + _ = encoder.pointee.lpVtbl.pointee.Release(encoder) + } + _ = encoder.pointee.lpVtbl.pointee.Initialize(encoder, stream, WICBitmapEncoderNoCache) + + // Create the frame into which the bitmap will be composited. + var frame: UnsafeMutablePointer? + var propertyBag: UnsafeMutablePointer? + let rCreateFrame = encoder.pointee.lpVtbl.pointee.CreateNewFrame(encoder, &frame, &propertyBag) + guard rCreateFrame == S_OK, let frame, let propertyBag else { + throw ImageAttachmentError.comObjectCreationFailed(IWICBitmapFrameEncode.self, rCreateFrame) + } + defer { + _ = frame.pointee.lpVtbl.pointee.Release(frame) + _ = propertyBag.pointee.lpVtbl.pointee.Release(propertyBag) + } + + // Set properties. The only property we currently set is image quality. + if let encodingQuality = imageFormat?.encodingQuality { + try propertyBag.write(encodingQuality, named: "ImageQuality") + } + _ = frame.pointee.lpVtbl.pointee.Initialize(frame, propertyBag) + + // Write the image! + let rWrite = frame.pointee.lpVtbl.pointee.WriteSource(frame, bitmap, nil) + guard rWrite == S_OK else { + throw ImageAttachmentError.imageWritingFailed(rWrite) + } + + // Commit changes through the various layers. + var rCommit = frame.pointee.lpVtbl.pointee.Commit(frame) + guard rCommit == S_OK else { + throw ImageAttachmentError.imageWritingFailed(rCommit) + } + rCommit = encoder.pointee.lpVtbl.pointee.Commit(encoder) + guard rCommit == S_OK else { + throw ImageAttachmentError.imageWritingFailed(rCommit) + } + rCommit = stream.pointee.lpVtbl.pointee.Commit(stream, DWORD(bitPattern: STGC_DEFAULT.rawValue)) + guard rCommit == S_OK else { + throw ImageAttachmentError.imageWritingFailed(rCommit) } // Extract the serialized image and pass it back to the caller. We hold the @@ -112,7 +144,7 @@ extension _AttachableImageWrapper: AttachableWrapper { var global: HGLOBAL? let rGetGlobal = GetHGlobalFromStream(stream, &global) guard S_OK == rGetGlobal else { - throw GDIPlusError.globalFromStreamFailed(rGetGlobal) + throw ImageAttachmentError.globalFromStreamFailed(rGetGlobal) } guard let baseAddress = GlobalLock(global) else { throw Win32Error(rawValue: GetLastError()) @@ -124,7 +156,7 @@ extension _AttachableImageWrapper: AttachableWrapper { return try body(UnsafeRawBufferPointer(start: baseAddress, count: Int(byteCount))) } - public borrowing func preferredName(for attachment: borrowing Attachment, basedOn suggestedName: String) -> String { + public borrowing func preferredName(for attachment: borrowing Attachment<_AttachableImageWrapper>, basedOn suggestedName: String) -> String { let clsid = AttachableImageFormat.computeCLSID(for: imageFormat, withPreferredName: suggestedName) return AttachableImageFormat.appendPathExtension(for: clsid, to: suggestedName) } diff --git a/Sources/Overlays/_Testing_WinSDK/Support/Additions/GUIDAdditions.swift b/Sources/Overlays/_Testing_WinSDK/Support/Additions/GUIDAdditions.swift new file mode 100644 index 000000000..c58eca577 --- /dev/null +++ b/Sources/Overlays/_Testing_WinSDK/Support/Additions/GUIDAdditions.swift @@ -0,0 +1,37 @@ +// +// 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 os(Windows) +internal import WinSDK + +extension UInt128 { + init(_ guid: GUID) { + self = withUnsafeBytes(of: guid) { buffer in + buffer.baseAddress!.loadUnaligned(as: Self.self) + } + } +} + +extension GUID { + init(_ uint128Value: UInt128) { + self = withUnsafeBytes(of: uint128Value) { buffer in + buffer.baseAddress!.loadUnaligned(as: Self.self) + } + } + + static func ==(lhs: Self, rhs: Self) -> Bool { + withUnsafeBytes(of: lhs) { lhs in + withUnsafeBytes(of: rhs) { rhs in + lhs.elementsEqual(rhs) + } + } + } +} +#endif diff --git a/Sources/Overlays/_Testing_WinSDK/Support/Additions/IPropertyBag2Additions.swift b/Sources/Overlays/_Testing_WinSDK/Support/Additions/IPropertyBag2Additions.swift new file mode 100644 index 000000000..307e25778 --- /dev/null +++ b/Sources/Overlays/_Testing_WinSDK/Support/Additions/IPropertyBag2Additions.swift @@ -0,0 +1,40 @@ +// +// 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 os(Windows) +internal import WinSDK + +extension UnsafeMutablePointer { + /// Write a floating-point value to this property bag with the given name, + /// + /// - Parameters: + /// - value: The value to write. + /// - propertyName: The name of the property. + /// + /// - Throws: If any error occurred writing the property. + func write(_ value: Float, named propertyName: String) throws { + let rWrite = propertyName.withCString(encodedAs: UTF16.self) { propertyName in + var option = PROPBAG2() + option.pstrName = .init(mutating: propertyName) + + return withUnsafeTemporaryAllocation(of: VARIANT.self, capacity: 1) { variant in + let variant = variant.baseAddress! + VariantInit(variant) + variant.pointee.vt = .init(VT_R4.rawValue) + variant.pointee.fltVal = value + return self.pointee.lpVtbl.pointee.Write(self, 1, &option, variant) + } + } + guard rWrite == S_OK else { + throw ImageAttachmentError.propertyBagWritingFailed(propertyName, rWrite) + } + } +} +#endif diff --git a/Sources/Overlays/_Testing_WinSDK/Support/Additions/IWICImagingFactoryAdditions.swift b/Sources/Overlays/_Testing_WinSDK/Support/Additions/IWICImagingFactoryAdditions.swift new file mode 100644 index 000000000..dc11ab0fc --- /dev/null +++ b/Sources/Overlays/_Testing_WinSDK/Support/Additions/IWICImagingFactoryAdditions.swift @@ -0,0 +1,40 @@ +// +// 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 os(Windows) +internal import WinSDK + +extension IWICImagingFactory { + /// Create an imaging factory. + /// + /// - Returns: A pointer to a new instance of this type. The caller is + /// responsible for releasing this object when done with it. + /// + /// - Throws: Any error that occurred while creating the object. + static func create() throws -> UnsafeMutablePointer { + try withUnsafePointer(to: CLSID_WICImagingFactory) { CLSID_WICImagingFactory in + try withUnsafePointer(to: IID_IWICImagingFactory) { IID_IWICImagingFactory in + var factory: UnsafeMutableRawPointer? + let rCreate = CoCreateInstance( + CLSID_WICImagingFactory, + nil, + DWORD(bitPattern: CLSCTX_INPROC_SERVER.rawValue), + IID_IWICImagingFactory, + &factory + ) + guard rCreate == S_OK, let factory = factory?.assumingMemoryBound(to: Self.self) else { + throw ImageAttachmentError.comObjectCreationFailed(Self.self, rCreate) + } + return factory + } + } + } +} +#endif diff --git a/Sources/_TestingInternals/GDI+/include/GDI+.h b/Sources/_TestingInternals/GDI+/include/GDI+.h deleted file mode 100644 index ff3020b66..000000000 --- a/Sources/_TestingInternals/GDI+/include/GDI+.h +++ /dev/null @@ -1,71 +0,0 @@ -// -// 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_GDIPLUS_H) -#define SWT_GDIPLUS_H - -/// This header includes thunk functions for various GDI+ functions that the -/// Swift importer is currently unable to import. As such, I haven't documented -/// each function individually; refer to the GDI+ documentation for more -/// information about the thunked functions. - -#if defined(_WIN32) && defined(__cplusplus) -#include "../include/Defines.h" -#include "../include/Includes.h" - -#include - -SWT_ASSUME_NONNULL_BEGIN - -static inline Gdiplus::Status swt_GdiplusStartup( - ULONG_PTR *token, - const Gdiplus::GdiplusStartupInput *input, - Gdiplus::GdiplusStartupOutput *_Nullable output -) { - return Gdiplus::GdiplusStartup(token, input, output); -} - -static inline void swt_GdiplusShutdown(ULONG_PTR token) { - Gdiplus::GdiplusShutdown(token); -} - -static inline Gdiplus::Image *swt_GdiplusImageFromHBITMAP(HBITMAP bitmap, HPALETTE _Nullable palette) { - return Gdiplus::Bitmap::FromHBITMAP(bitmap, palette); -} - -static inline Gdiplus::Image *swt_GdiplusImageFromHICON(HICON icon) { - return Gdiplus::Bitmap::FromHICON(icon); -} - -static inline Gdiplus::Image *swt_GdiplusImageClone(Gdiplus::Image *image) { - return image->Clone(); -} - -static inline void swt_GdiplusImageDelete(Gdiplus::Image *image) { - delete image; -} - -static inline Gdiplus::Status swt_GdiplusImageSave( - Gdiplus::Image *image, - IStream *stream, - const CLSID *format, - const Gdiplus::EncoderParameters *_Nullable encoderParams -) { - return image->Save(stream, format, encoderParams); -} - -static inline GUID swt_GdiplusEncoderQuality(void) { - return Gdiplus::EncoderQuality; -} - -SWT_ASSUME_NONNULL_END - -#endif -#endif diff --git a/Sources/_TestingInternals/include/Stubs.h b/Sources/_TestingInternals/include/Stubs.h index ae641de0d..636ea9aff 100644 --- a/Sources/_TestingInternals/include/Stubs.h +++ b/Sources/_TestingInternals/include/Stubs.h @@ -108,24 +108,6 @@ static DWORD_PTR swt_PROC_THREAD_ATTRIBUTE_HANDLE_LIST(void) { static const IMAGE_SECTION_HEADER *_Null_unspecified swt_IMAGE_FIRST_SECTION(const IMAGE_NT_HEADERS *ntHeader) { return IMAGE_FIRST_SECTION(ntHeader); } - -#if defined(__cplusplus) -/// Add a reference to (retain) a COM object. -/// -/// This function is provided because `IUnknown::AddRef()` is a virtual member -/// function and cannot be imported directly into Swift. -static inline ULONG swt_IUnknown_AddRef(IUnknown *object) { - return object->AddRef(); -} - -/// Release a COM object. -/// -/// This function is provided because `IUnknown::Release()` is a virtual member -/// function and cannot be imported directly into Swift. -static inline ULONG swt_IUnknown_Release(IUnknown *object) { - return object->Release(); -} -#endif #endif #if defined(__linux__) || defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__ANDROID__) diff --git a/Sources/_TestingInternals/include/module.modulemap b/Sources/_TestingInternals/include/module.modulemap index 12a23c81d..e05a32552 100644 --- a/Sources/_TestingInternals/include/module.modulemap +++ b/Sources/_TestingInternals/include/module.modulemap @@ -11,11 +11,4 @@ module _TestingInternals { umbrella "." export * - - explicit module GDIPlus { - header "../GDI+/include/GDI+.h" - export * - - link "gdiplus.lib" - } } diff --git a/Tests/TestingTests/AttachmentTests.swift b/Tests/TestingTests/AttachmentTests.swift index d1556ff87..8604e05c3 100644 --- a/Tests/TestingTests/AttachmentTests.swift +++ b/Tests/TestingTests/AttachmentTests.swift @@ -709,7 +709,7 @@ extension AttachmentTests { @MainActor @Test func attachHICON() throws { let icon = try copyHICON() defer { - DeleteObject(icon) + DestroyIcon(icon) } let attachment = Attachment(icon, named: "diamond.jpeg") @@ -723,7 +723,7 @@ extension AttachmentTests { let icon = try copyHICON() defer { - DeleteObject(icon) + DestroyIcon(icon) } let screenDC = try #require(GetDC(nil)) @@ -748,17 +748,25 @@ extension AttachmentTests { @MainActor @Test func attachHBITMAP() throws { let bitmap = try copyHBITMAP() + defer { + DeleteObject(bitmap) + } + let attachment = Attachment(bitmap, named: "diamond.png") try attachment.withUnsafeBytes { buffer in #expect(buffer.count > 32) } + Attachment.record(attachment) } @MainActor @Test func attachHBITMAPAsJPEG() throws { - let bitmap1 = try copyHBITMAP() - let hiFi = Attachment(bitmap1, named: "diamond", as: .jpeg(withEncodingQuality: 1.0)) - let bitmap2 = try copyHBITMAP() - let loFi = Attachment(bitmap2, named: "diamond", as: .jpeg(withEncodingQuality: 0.1)) + let bitmap = try copyHBITMAP() + defer { + DeleteObject(bitmap) + } + let hiFi = Attachment(bitmap, named: "hifi", as: .jpeg(withEncodingQuality: 1.0)) + let loFi = Attachment(bitmap, named: "lofi", as: .jpeg(withEncodingQuality: 0.1)) + try hiFi.withUnsafeBytes { hiFi in try loFi.withUnsafeBytes { loFi in #expect(hiFi.count > loFi.count) @@ -767,20 +775,47 @@ extension AttachmentTests { Attachment.record(loFi) } - @MainActor @Test func pathExtensionAndCLSID() throws { + @MainActor @Test func attachIWICBitmap() throws { + let factory = try IWICImagingFactory.create() + defer { + _ = factory.pointee.lpVtbl.pointee.Release(factory) + } + + let bitmap = try copyHBITMAP() + defer { + DeleteObject(bitmap) + } + + var wicBitmap: UnsafeMutablePointer? + let rCreate = factory.pointee.lpVtbl.pointee.CreateBitmapFromHBITMAP(factory, bitmap, nil, WICBitmapUsePremultipliedAlpha, &wicBitmap) + guard rCreate == S_OK, let wicBitmap else { + throw ImageAttachmentError.comObjectCreationFailed(IWICBitmap.self, rCreate) + } + defer { + _ = wicBitmap.pointee.lpVtbl.pointee.Release(wicBitmap) + } + + let attachment = Attachment(wicBitmap, named: "diamond.png") + try attachment.withUnsafeBytes { buffer in + #expect(buffer.count > 32) + } + Attachment.record(attachment) + } + + @MainActor @Test func pathExtensionAndCLSID() { let pngCLSID = AttachableImageFormat.png.clsid let pngFilename = AttachableImageFormat.appendPathExtension(for: pngCLSID, to: "example") #expect(pngFilename == "example.png") let jpegCLSID = AttachableImageFormat.jpeg.clsid let jpegFilename = AttachableImageFormat.appendPathExtension(for: jpegCLSID, to: "example") - #expect(jpegFilename == "example.jpg") + #expect(jpegFilename == "example.jpeg") let pngjpegFilename = AttachableImageFormat.appendPathExtension(for: jpegCLSID, to: "example.png") - #expect(pngjpegFilename == "example.png.jpg") + #expect(pngjpegFilename == "example.png.jpeg") - let jpgjpegFilename = AttachableImageFormat.appendPathExtension(for: jpegCLSID, to: "example.jpeg") - #expect(jpgjpegFilename == "example.jpeg") + let jpgjpegFilename = AttachableImageFormat.appendPathExtension(for: jpegCLSID, to: "example.jpg") + #expect(jpgjpegFilename == "example.jpg") } #endif } From be62a4fc03f52ba214f0842219f3acb144873cfe Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Fri, 8 Aug 2025 17:34:41 -0400 Subject: [PATCH 089/216] Fix typo copying HBITMAP --- .../Attachments/HBITMAP+AttachableAsIWICBitmap.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/HBITMAP+AttachableAsIWICBitmap.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/HBITMAP+AttachableAsIWICBitmap.swift index 7223e3cf2..9b8ebcd57 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/HBITMAP+AttachableAsIWICBitmap.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/HBITMAP+AttachableAsIWICBitmap.swift @@ -28,8 +28,7 @@ extension HBITMAP__: _AttachableByAddressAsIWICBitmap { } public static func _copyAttachableValue(at imageAddress: UnsafeMutablePointer) throws -> UnsafeMutablePointer { - let result: HBITMAP? = CopyImage(imageAddress, UINT(IMAGE_BITMAP), 0, 0, 0).assumingMemoryBound(to: Self.self) - guard let result else { + guard let result = CopyImage(imageAddress, UINT(IMAGE_BITMAP), 0, 0, 0)?.assumingMemoryBound(to: Self.self) else { throw Win32Error(rawValue: GetLastError()) } return result From cb090d3b29b886d3ec4a5751a874956a5469dee8 Mon Sep 17 00:00:00 2001 From: Suzy Ratcliff Date: Fri, 8 Aug 2025 15:51:00 -0700 Subject: [PATCH 090/216] Update docc comments for Issue.record (#1257) Update docc comments for Issue.record ### Motivation: Docc comments were not updated when we added a severity to Issue.record. This fixes that. ### Modifications: Add severity parameter to the record docc comments. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --------- Co-authored-by: Stuart Montgomery --- Sources/Testing/Issues/Issue.swift | 4 ++-- Sources/Testing/Support/CustomIssueRepresentable.swift | 6 +++--- Sources/Testing/Testing.docc/MigratingFromXCTest.md | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Sources/Testing/Issues/Issue.swift b/Sources/Testing/Issues/Issue.swift index 9d2e9fc90..beeca101e 100644 --- a/Sources/Testing/Issues/Issue.swift +++ b/Sources/Testing/Issues/Issue.swift @@ -13,7 +13,7 @@ public struct Issue: Sendable { /// Kinds of issues which may be recorded. public enum Kind: Sendable { /// An issue which occurred unconditionally, for example by using - /// ``Issue/record(_:sourceLocation:)``. + /// ``Issue/record(_:severity:sourceLocation:)``. case unconditional /// An issue due to a failed expectation, such as those produced by @@ -409,7 +409,7 @@ extension Issue.Kind { @_spi(ForToolsIntegrationOnly) public enum Snapshot: Sendable, Codable { /// An issue which occurred unconditionally, for example by using - /// ``Issue/record(_:sourceLocation:)``. + /// ``Issue/record(_:severity:sourceLocation:)``. case unconditional /// An issue due to a failed expectation, such as those produced by diff --git a/Sources/Testing/Support/CustomIssueRepresentable.swift b/Sources/Testing/Support/CustomIssueRepresentable.swift index d76739d1f..77f7b23b0 100644 --- a/Sources/Testing/Support/CustomIssueRepresentable.swift +++ b/Sources/Testing/Support/CustomIssueRepresentable.swift @@ -12,7 +12,7 @@ /// record themselves as test issues. /// /// When a type conforms to this protocol, values of that type can be passed to -/// ``Issue/record(_:_:)``. The testing library then calls the +/// ``Issue/record(_:severity:sourceLocation:)``. The testing library then calls the /// ``customize(_:)`` function and passes it an instance of ``Issue`` that will /// be used to represent the value. The function can then reconfigure or replace /// the issue as needed. @@ -43,7 +43,7 @@ protocol CustomIssueRepresentable: Error { /// /// This type is not part of the public interface of the testing library. /// External callers should generally record issues by throwing their own errors -/// or by calling ``Issue/record(_:sourceLocation:)``. +/// or by calling ``Issue/record(_:severity:sourceLocation:)``. struct SystemError: Error, CustomStringConvertible, CustomIssueRepresentable { var description: String @@ -62,7 +62,7 @@ struct SystemError: Error, CustomStringConvertible, CustomIssueRepresentable { /// /// This type is not part of the public interface of the testing library. /// External callers should generally record issues by throwing their own errors -/// or by calling ``Issue/record(_:sourceLocation:)``. +/// or by calling ``Issue/record(_:severity:sourceLocation:)``. struct APIMisuseError: Error, CustomStringConvertible, CustomIssueRepresentable { var description: String diff --git a/Sources/Testing/Testing.docc/MigratingFromXCTest.md b/Sources/Testing/Testing.docc/MigratingFromXCTest.md index 60744ba7a..81434b8c3 100644 --- a/Sources/Testing/Testing.docc/MigratingFromXCTest.md +++ b/Sources/Testing/Testing.docc/MigratingFromXCTest.md @@ -277,7 +277,7 @@ XCTest has a function, [`XCTFail()`](https://developer.apple.com/documentation/x that causes a test to fail immediately and unconditionally. This function is useful when the syntax of the language prevents the use of an `XCTAssert()` function. To record an unconditional issue using the testing library, use the -``Issue/record(_:sourceLocation:)`` function: +``Issue/record(_:severity:sourceLocation:)`` function: @Row { @Column { From f4736a6054b0d66ce866e09a9eb8d2cf21618956 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Mon, 11 Aug 2025 07:57:35 -0400 Subject: [PATCH 091/216] Remove optionality from `_AttachableImageWrapper.wrappedValue` on Windows. (#1258) This PR makes `_AttachableImageWrapper.wrappedValue` non-optional on Windows (as it is on Darwin.) As currently implemented, it allows for failures when calling `CopyImage()` and `CopyIcon()`, but in practice the only way these can fail is due to heap exhaustion[^msDocs]. Swift treats allocation failures as almost universally fatal, so we should do the same. [^msDocs]: Microsoft's documentation for these functions does not list any other failure modes. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- .../Attachments/AttachableAsIWICBitmap.swift | 8 ++------ .../HBITMAP+AttachableAsIWICBitmap.swift | 10 +++++----- .../HICON+AttachableAsIWICBitmap.swift | 10 +++++----- .../IWICBitmap+AttachableAsIWICBitmap.swift | 2 +- .../Attachments/ImageAttachmentError.swift | 5 ++--- ...feMutablePointer+AttachableAsIWICBitmap.swift | 4 ++-- .../Attachments/_AttachableImageWrapper.swift | 16 ++++++---------- 7 files changed, 23 insertions(+), 32 deletions(-) diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsIWICBitmap.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsIWICBitmap.swift index 416048650..8375b4f7e 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsIWICBitmap.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsIWICBitmap.swift @@ -62,8 +62,6 @@ public protocol _AttachableByAddressAsIWICBitmap { /// - Returns: A copy of `imageAddress`, or `imageAddress` if this type does /// not support a copying operation. /// - /// - Throws: Any error that prevented copying the value at `imageAddress`. - /// /// The testing library uses this function to take ownership of image /// resources that test authors pass to it. If possible, make a copy of or add /// a reference to the value at `imageAddress`. If this type does not support @@ -71,7 +69,7 @@ public protocol _AttachableByAddressAsIWICBitmap { /// /// This function is not part of the public interface of the testing library. /// It may be removed in a future update. - static func _copyAttachableValue(at imageAddress: UnsafeMutablePointer) throws -> UnsafeMutablePointer + static func _copyAttachableValue(at imageAddress: UnsafeMutablePointer) -> UnsafeMutablePointer /// Manually deinitialize any resources at the given address. /// @@ -135,8 +133,6 @@ public protocol AttachableAsIWICBitmap { /// - Returns: A copy of `self`, or `self` if this type does not support a /// copying operation. /// - /// - Throws: Any error that prevented copying this value. - /// /// The testing library uses this function to take ownership of image /// resources that test authors pass to it. If possible, make a copy of or add /// a reference to `self`. If this type does not support making copies, return @@ -144,7 +140,7 @@ public protocol AttachableAsIWICBitmap { /// /// This function is not part of the public interface of the testing library. /// It may be removed in a future update. - func _copyAttachableValue() throws -> Self + func _copyAttachableValue() -> Self /// Manually deinitialize any resources associated with this image. /// diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/HBITMAP+AttachableAsIWICBitmap.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/HBITMAP+AttachableAsIWICBitmap.swift index 9b8ebcd57..296a971e6 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/HBITMAP+AttachableAsIWICBitmap.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/HBITMAP+AttachableAsIWICBitmap.swift @@ -27,11 +27,11 @@ extension HBITMAP__: _AttachableByAddressAsIWICBitmap { return bitmap } - public static func _copyAttachableValue(at imageAddress: UnsafeMutablePointer) throws -> UnsafeMutablePointer { - guard let result = CopyImage(imageAddress, UINT(IMAGE_BITMAP), 0, 0, 0)?.assumingMemoryBound(to: Self.self) else { - throw Win32Error(rawValue: GetLastError()) - } - return result + public static func _copyAttachableValue(at imageAddress: UnsafeMutablePointer) -> UnsafeMutablePointer { + // The only reasonable failure mode for `CopyImage()` is allocation failure, + // and Swift treats allocation failures as fatal. Hence, we do not check for + // `nil` on return. + CopyImage(imageAddress, UINT(IMAGE_BITMAP), 0, 0, 0).assumingMemoryBound(to: Self.self) } public static func _deinitializeAttachableValue(at imageAddress: UnsafeMutablePointer) { diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/HICON+AttachableAsIWICBitmap.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/HICON+AttachableAsIWICBitmap.swift index e06b51388..caeb2b5f6 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/HICON+AttachableAsIWICBitmap.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/HICON+AttachableAsIWICBitmap.swift @@ -27,11 +27,11 @@ extension HICON__: _AttachableByAddressAsIWICBitmap { return bitmap } - public static func _copyAttachableValue(at imageAddress: UnsafeMutablePointer) throws -> UnsafeMutablePointer { - guard let result = CopyIcon(imageAddress) else { - throw Win32Error(rawValue: GetLastError()) - } - return result + public static func _copyAttachableValue(at imageAddress: UnsafeMutablePointer) -> UnsafeMutablePointer { + // The only reasonable failure mode for `CopyIcon()` is allocation failure, + // and Swift treats allocation failures as fatal. Hence, we do not check for + // `nil` on return. + CopyIcon(imageAddress) } public static func _deinitializeAttachableValue(at imageAddress: UnsafeMutablePointer) { diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/IWICBitmap+AttachableAsIWICBitmap.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/IWICBitmap+AttachableAsIWICBitmap.swift index a7419e3fd..f5fcd4c26 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/IWICBitmap+AttachableAsIWICBitmap.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/IWICBitmap+AttachableAsIWICBitmap.swift @@ -23,7 +23,7 @@ extension IWICBitmap: _AttachableByAddressAsIWICBitmap { return imageAddress } - public static func _copyAttachableValue(at imageAddress: UnsafeMutablePointer) throws -> UnsafeMutablePointer { + public static func _copyAttachableValue(at imageAddress: UnsafeMutablePointer) -> UnsafeMutablePointer { _ = imageAddress.pointee.lpVtbl.pointee.AddRef(imageAddress) return imageAddress } diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/ImageAttachmentError.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/ImageAttachmentError.swift index 52a51b708..1b37df4a9 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/ImageAttachmentError.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/ImageAttachmentError.swift @@ -13,13 +13,12 @@ internal import WinSDK -/// A type describing errors that can be thrown by WIC or COM while attaching an -/// image. +/// A type representing an error that can occur when attaching an image. enum ImageAttachmentError: Error { /// A call to `QueryInterface()` failed. case queryInterfaceFailed(Any.Type, HRESULT) - /// The testing library failed to create a WIC object. + /// The testing library failed to create a COM object. case comObjectCreationFailed(Any.Type, HRESULT) /// An image could not be written. diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/UnsafeMutablePointer+AttachableAsIWICBitmap.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/UnsafeMutablePointer+AttachableAsIWICBitmap.swift index 1111fd178..c957cf40a 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/UnsafeMutablePointer+AttachableAsIWICBitmap.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/UnsafeMutablePointer+AttachableAsIWICBitmap.swift @@ -19,8 +19,8 @@ extension UnsafeMutablePointer: AttachableAsIWICBitmap where Pointee: _Attachabl try Pointee._copyAttachableIWICBitmap(from: self, using: factory) } - public func _copyAttachableValue() throws -> Self { - try Pointee._copyAttachableValue(at: self) + public func _copyAttachableValue() -> Self { + Pointee._copyAttachableValue(at: self) } public consuming func _deinitializeAttachableValue() { diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/_AttachableImageWrapper.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/_AttachableImageWrapper.swift index 72afbb575..09fe7554e 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/_AttachableImageWrapper.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/_AttachableImageWrapper.swift @@ -28,30 +28,26 @@ internal import WinSDK @_spi(Experimental) public final class _AttachableImageWrapper: Sendable where Image: AttachableAsIWICBitmap { /// The underlying image. - nonisolated(unsafe) let image: Result + nonisolated(unsafe) let image: Image /// The image format to use when encoding the represented image. let imageFormat: AttachableImageFormat? init(image: borrowing Image, imageFormat: AttachableImageFormat?) { - self.image = Result { [image = copy image] in - try image._copyAttachableValue() - } + self.image = image._copyAttachableValue() self.imageFormat = imageFormat } deinit { - if let image = try? image.get() { - image._deinitializeAttachableValue() - } + image._deinitializeAttachableValue() } } // MARK: - extension _AttachableImageWrapper: AttachableWrapper { - public var wrappedValue: Image? { - try? image.get() + public var wrappedValue: Image { + image } public func withUnsafeBytes(for attachment: borrowing Attachment<_AttachableImageWrapper>, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { @@ -74,7 +70,7 @@ extension _AttachableImageWrapper: AttachableWrapper { } // Create the bitmap and downcast it to an IWICBitmapSource for later use. - let bitmap = try image.get().copyAttachableIWICBitmapSource(using: factory) + let bitmap = try image.copyAttachableIWICBitmapSource(using: factory) defer { _ = bitmap.pointee.lpVtbl.pointee.Release(bitmap) } From 068f415ba1486c528389a8adf12b415a1f22d8dc Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Mon, 11 Aug 2025 12:24:45 -0400 Subject: [PATCH 092/216] Harmonize `_AttachableImageWrapper` and `AttachableAs___` across Darwin and Windows. This PR makes some changes to harmonize the `_AttachableImageWrapper` wrapper type and the `AttachableAsCGImage`/`AttachableAsIWICBitmap` protocols across Darwin and Windows: - `_AttachableImageWrapper` is now a class on both platforms, not just Windows. - The `_makeCopyForAttachment()` function is now named `_copyAttachableValue()` (which is a more accurate name already used on Windows.) - A default implementation of `_copyAttachableValue()` and of `_deinitializeAttachableValue()` is provided for `Sendable` types on Windows. --- .../Attachments/NSImage+AttachableAsCGImage.swift | 2 +- .../Attachments/AttachableAsCGImage.swift | 11 ++++++----- .../Attachments/_AttachableImageWrapper.swift | 12 ++++++------ .../Attachments/CIImage+AttachableAsCGImage.swift | 2 +- .../Attachments/AttachableAsIWICBitmap.swift | 14 ++++++++++++++ 5 files changed, 28 insertions(+), 13 deletions(-) diff --git a/Sources/Overlays/_Testing_AppKit/Attachments/NSImage+AttachableAsCGImage.swift b/Sources/Overlays/_Testing_AppKit/Attachments/NSImage+AttachableAsCGImage.swift index d0830accf..d62ef71cc 100644 --- a/Sources/Overlays/_Testing_AppKit/Attachments/NSImage+AttachableAsCGImage.swift +++ b/Sources/Overlays/_Testing_AppKit/Attachments/NSImage+AttachableAsCGImage.swift @@ -53,7 +53,7 @@ extension NSImage: AttachableAsCGImage { return maxRepWidth ?? 1.0 } - public func _makeCopyForAttachment() -> Self { + public func _copyAttachableValue() -> Self { // If this image is of an NSImage subclass, we cannot reliably make a deep // copy of it because we don't know what its `init(data:)` implementation // might do. Try to make a copy (using NSCopying), but if that doesn't work diff --git a/Sources/Overlays/_Testing_CoreGraphics/Attachments/AttachableAsCGImage.swift b/Sources/Overlays/_Testing_CoreGraphics/Attachments/AttachableAsCGImage.swift index 5f5c7b2d7..8ca2a0ae4 100644 --- a/Sources/Overlays/_Testing_CoreGraphics/Attachments/AttachableAsCGImage.swift +++ b/Sources/Overlays/_Testing_CoreGraphics/Attachments/AttachableAsCGImage.swift @@ -65,16 +65,17 @@ public protocol AttachableAsCGImage { /// /// - Returns: A copy of `self`, or `self` if no copy is needed. /// - /// Several system image types do not conform to `Sendable`; use this - /// function to make copies of such images that will not be shared outside - /// of an attachment and so can be generally safely stored. + /// The testing library uses this function to take ownership of image + /// resources that test authors pass to it. If possible, make a copy of or add + /// a reference to `self`. If this type does not support making copies, return + /// `self` verbatim. /// /// The default implementation of this function when `Self` conforms to /// `Sendable` simply returns `self`. /// /// This function is not part of the public interface of the testing library. /// It may be removed in a future update. - func _makeCopyForAttachment() -> Self + func _copyAttachableValue() -> Self } @available(_uttypesAPI, *) @@ -90,7 +91,7 @@ extension AttachableAsCGImage { @available(_uttypesAPI, *) extension AttachableAsCGImage where Self: Sendable { - public func _makeCopyForAttachment() -> Self { + public func _copyAttachableValue() -> Self { self } } diff --git a/Sources/Overlays/_Testing_CoreGraphics/Attachments/_AttachableImageWrapper.swift b/Sources/Overlays/_Testing_CoreGraphics/Attachments/_AttachableImageWrapper.swift index f17990455..61904936f 100644 --- a/Sources/Overlays/_Testing_CoreGraphics/Attachments/_AttachableImageWrapper.swift +++ b/Sources/Overlays/_Testing_CoreGraphics/Attachments/_AttachableImageWrapper.swift @@ -54,20 +54,20 @@ import UniformTypeIdentifiers /// (iOS, watchOS, tvOS, visionOS, and Mac Catalyst) @_spi(Experimental) @available(_uttypesAPI, *) -public struct _AttachableImageWrapper: Sendable where Image: AttachableAsCGImage { +public final class _AttachableImageWrapper: Sendable where Image: AttachableAsCGImage { /// The underlying image. /// /// `CGImage` and `UIImage` are sendable, but `NSImage` is not. `NSImage` /// instances can be created from closures that are run at rendering time. /// The AppKit cross-import overlay is responsible for ensuring that any /// instances of this type it creates hold "safe" `NSImage` instances. - nonisolated(unsafe) var image: Image + nonisolated(unsafe) let image: Image /// The image format to use when encoding the represented image. - var imageFormat: AttachableImageFormat? + let imageFormat: AttachableImageFormat? init(image: Image, imageFormat: AttachableImageFormat?) { - self.image = image._makeCopyForAttachment() + self.image = image._copyAttachableValue() self.imageFormat = imageFormat } } @@ -80,7 +80,7 @@ extension _AttachableImageWrapper: AttachableWrapper { image } - public func withUnsafeBytes(for attachment: borrowing Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { + public func withUnsafeBytes(for attachment: borrowing Attachment<_AttachableImageWrapper>, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { let data = NSMutableData() // Convert the image to a CGImage. @@ -116,7 +116,7 @@ extension _AttachableImageWrapper: AttachableWrapper { } } - public borrowing func preferredName(for attachment: borrowing Attachment, basedOn suggestedName: String) -> String { + public borrowing func preferredName(for attachment: borrowing Attachment<_AttachableImageWrapper>, basedOn suggestedName: String) -> String { let contentType = AttachableImageFormat.computeContentType(for: imageFormat, withPreferredName: suggestedName) return (suggestedName as NSString).appendingPathExtension(for: contentType) } diff --git a/Sources/Overlays/_Testing_CoreImage/Attachments/CIImage+AttachableAsCGImage.swift b/Sources/Overlays/_Testing_CoreImage/Attachments/CIImage+AttachableAsCGImage.swift index 9a8278a83..7614dc633 100644 --- a/Sources/Overlays/_Testing_CoreImage/Attachments/CIImage+AttachableAsCGImage.swift +++ b/Sources/Overlays/_Testing_CoreImage/Attachments/CIImage+AttachableAsCGImage.swift @@ -23,7 +23,7 @@ extension CIImage: AttachableAsCGImage { } } - public func _makeCopyForAttachment() -> Self { + public func _copyAttachableValue() -> Self { // CIImage is documented as thread-safe, but does not conform to Sendable. // It conforms to NSCopying but does not actually copy itself, so there's no // point in calling copy(). diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsIWICBitmap.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsIWICBitmap.swift index 8375b4f7e..b17b82417 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsIWICBitmap.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsIWICBitmap.swift @@ -138,6 +138,9 @@ public protocol AttachableAsIWICBitmap { /// a reference to `self`. If this type does not support making copies, return /// `self` verbatim. /// + /// The default implementation of this function when `Self` conforms to + /// `Sendable` simply returns `self`. + /// /// This function is not part of the public interface of the testing library. /// It may be removed in a future update. func _copyAttachableValue() -> Self @@ -151,6 +154,9 @@ public protocol AttachableAsIWICBitmap { /// This function is not responsible for releasing the image returned from /// `_copyAttachableIWICBitmap(using:)`. /// + /// The default implementation of this function when `Self` conforms to + /// `Sendable` does nothing. + /// /// This function is not part of the public interface of the testing library. /// It may be removed in a future update. func _deinitializeAttachableValue() @@ -190,4 +196,12 @@ extension AttachableAsIWICBitmap { } } } + +extension AttachableAsIWICBitmap where Self: Sendable { + public func _copyAttachableValue() -> Self { + self + } + + public func _deinitializeAttachableValue() {} +} #endif From 4bb02ded63d9571f0faeb82d1257f33d6226e22a Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Mon, 11 Aug 2025 12:49:09 -0400 Subject: [PATCH 093/216] Revert "Harmonize `_AttachableImageWrapper` and `AttachableAs___` across Darwin and Windows." This reverts commit 068f415ba1486c528389a8adf12b415a1f22d8dc. --- .../Attachments/NSImage+AttachableAsCGImage.swift | 2 +- .../Attachments/AttachableAsCGImage.swift | 11 +++++------ .../Attachments/_AttachableImageWrapper.swift | 12 ++++++------ .../Attachments/CIImage+AttachableAsCGImage.swift | 2 +- .../Attachments/AttachableAsIWICBitmap.swift | 14 -------------- 5 files changed, 13 insertions(+), 28 deletions(-) diff --git a/Sources/Overlays/_Testing_AppKit/Attachments/NSImage+AttachableAsCGImage.swift b/Sources/Overlays/_Testing_AppKit/Attachments/NSImage+AttachableAsCGImage.swift index d62ef71cc..d0830accf 100644 --- a/Sources/Overlays/_Testing_AppKit/Attachments/NSImage+AttachableAsCGImage.swift +++ b/Sources/Overlays/_Testing_AppKit/Attachments/NSImage+AttachableAsCGImage.swift @@ -53,7 +53,7 @@ extension NSImage: AttachableAsCGImage { return maxRepWidth ?? 1.0 } - public func _copyAttachableValue() -> Self { + public func _makeCopyForAttachment() -> Self { // If this image is of an NSImage subclass, we cannot reliably make a deep // copy of it because we don't know what its `init(data:)` implementation // might do. Try to make a copy (using NSCopying), but if that doesn't work diff --git a/Sources/Overlays/_Testing_CoreGraphics/Attachments/AttachableAsCGImage.swift b/Sources/Overlays/_Testing_CoreGraphics/Attachments/AttachableAsCGImage.swift index 8ca2a0ae4..5f5c7b2d7 100644 --- a/Sources/Overlays/_Testing_CoreGraphics/Attachments/AttachableAsCGImage.swift +++ b/Sources/Overlays/_Testing_CoreGraphics/Attachments/AttachableAsCGImage.swift @@ -65,17 +65,16 @@ public protocol AttachableAsCGImage { /// /// - Returns: A copy of `self`, or `self` if no copy is needed. /// - /// The testing library uses this function to take ownership of image - /// resources that test authors pass to it. If possible, make a copy of or add - /// a reference to `self`. If this type does not support making copies, return - /// `self` verbatim. + /// Several system image types do not conform to `Sendable`; use this + /// function to make copies of such images that will not be shared outside + /// of an attachment and so can be generally safely stored. /// /// The default implementation of this function when `Self` conforms to /// `Sendable` simply returns `self`. /// /// This function is not part of the public interface of the testing library. /// It may be removed in a future update. - func _copyAttachableValue() -> Self + func _makeCopyForAttachment() -> Self } @available(_uttypesAPI, *) @@ -91,7 +90,7 @@ extension AttachableAsCGImage { @available(_uttypesAPI, *) extension AttachableAsCGImage where Self: Sendable { - public func _copyAttachableValue() -> Self { + public func _makeCopyForAttachment() -> Self { self } } diff --git a/Sources/Overlays/_Testing_CoreGraphics/Attachments/_AttachableImageWrapper.swift b/Sources/Overlays/_Testing_CoreGraphics/Attachments/_AttachableImageWrapper.swift index 61904936f..f17990455 100644 --- a/Sources/Overlays/_Testing_CoreGraphics/Attachments/_AttachableImageWrapper.swift +++ b/Sources/Overlays/_Testing_CoreGraphics/Attachments/_AttachableImageWrapper.swift @@ -54,20 +54,20 @@ import UniformTypeIdentifiers /// (iOS, watchOS, tvOS, visionOS, and Mac Catalyst) @_spi(Experimental) @available(_uttypesAPI, *) -public final class _AttachableImageWrapper: Sendable where Image: AttachableAsCGImage { +public struct _AttachableImageWrapper: Sendable where Image: AttachableAsCGImage { /// The underlying image. /// /// `CGImage` and `UIImage` are sendable, but `NSImage` is not. `NSImage` /// instances can be created from closures that are run at rendering time. /// The AppKit cross-import overlay is responsible for ensuring that any /// instances of this type it creates hold "safe" `NSImage` instances. - nonisolated(unsafe) let image: Image + nonisolated(unsafe) var image: Image /// The image format to use when encoding the represented image. - let imageFormat: AttachableImageFormat? + var imageFormat: AttachableImageFormat? init(image: Image, imageFormat: AttachableImageFormat?) { - self.image = image._copyAttachableValue() + self.image = image._makeCopyForAttachment() self.imageFormat = imageFormat } } @@ -80,7 +80,7 @@ extension _AttachableImageWrapper: AttachableWrapper { image } - public func withUnsafeBytes(for attachment: borrowing Attachment<_AttachableImageWrapper>, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { + public func withUnsafeBytes(for attachment: borrowing Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { let data = NSMutableData() // Convert the image to a CGImage. @@ -116,7 +116,7 @@ extension _AttachableImageWrapper: AttachableWrapper { } } - public borrowing func preferredName(for attachment: borrowing Attachment<_AttachableImageWrapper>, basedOn suggestedName: String) -> String { + public borrowing func preferredName(for attachment: borrowing Attachment, basedOn suggestedName: String) -> String { let contentType = AttachableImageFormat.computeContentType(for: imageFormat, withPreferredName: suggestedName) return (suggestedName as NSString).appendingPathExtension(for: contentType) } diff --git a/Sources/Overlays/_Testing_CoreImage/Attachments/CIImage+AttachableAsCGImage.swift b/Sources/Overlays/_Testing_CoreImage/Attachments/CIImage+AttachableAsCGImage.swift index 7614dc633..9a8278a83 100644 --- a/Sources/Overlays/_Testing_CoreImage/Attachments/CIImage+AttachableAsCGImage.swift +++ b/Sources/Overlays/_Testing_CoreImage/Attachments/CIImage+AttachableAsCGImage.swift @@ -23,7 +23,7 @@ extension CIImage: AttachableAsCGImage { } } - public func _copyAttachableValue() -> Self { + public func _makeCopyForAttachment() -> Self { // CIImage is documented as thread-safe, but does not conform to Sendable. // It conforms to NSCopying but does not actually copy itself, so there's no // point in calling copy(). diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsIWICBitmap.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsIWICBitmap.swift index b17b82417..8375b4f7e 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsIWICBitmap.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsIWICBitmap.swift @@ -138,9 +138,6 @@ public protocol AttachableAsIWICBitmap { /// a reference to `self`. If this type does not support making copies, return /// `self` verbatim. /// - /// The default implementation of this function when `Self` conforms to - /// `Sendable` simply returns `self`. - /// /// This function is not part of the public interface of the testing library. /// It may be removed in a future update. func _copyAttachableValue() -> Self @@ -154,9 +151,6 @@ public protocol AttachableAsIWICBitmap { /// This function is not responsible for releasing the image returned from /// `_copyAttachableIWICBitmap(using:)`. /// - /// The default implementation of this function when `Self` conforms to - /// `Sendable` does nothing. - /// /// This function is not part of the public interface of the testing library. /// It may be removed in a future update. func _deinitializeAttachableValue() @@ -196,12 +190,4 @@ extension AttachableAsIWICBitmap { } } } - -extension AttachableAsIWICBitmap where Self: Sendable { - public func _copyAttachableValue() -> Self { - self - } - - public func _deinitializeAttachableValue() {} -} #endif From adcec53754a46178e56937ecacd1a8b0c608e034 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Mon, 11 Aug 2025 15:15:23 -0400 Subject: [PATCH 094/216] Harmonize `_AttachableImageWrapper` and `AttachableAs___` across Darwin and Windows. (#1261) This PR makes some changes to harmonize the `_AttachableImageWrapper` wrapper type and the `AttachableAsCGImage`/`AttachableAsIWICBitmap` protocols across Darwin and Windows: - `_AttachableImageWrapper` is now a class on both platforms, not just Windows. - The `_makeCopyForAttachment()` function is now named `_copyAttachableValue()` (which is a more accurate name already used on Windows.) - A default implementation of `_copyAttachableValue()` and of `_deinitializeAttachableValue()` is provided for `Sendable` types on Windows. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- .../Attachments/NSImage+AttachableAsCGImage.swift | 2 +- .../Attachments/AttachableAsCGImage.swift | 11 ++++++----- .../Attachments/_AttachableImageWrapper.swift | 12 ++++++------ .../Attachments/CIImage+AttachableAsCGImage.swift | 2 +- .../Attachments/AttachableAsIWICBitmap.swift | 14 ++++++++++++++ .../Attachments/AttachableImageFormat+CLSID.swift | 2 +- 6 files changed, 29 insertions(+), 14 deletions(-) diff --git a/Sources/Overlays/_Testing_AppKit/Attachments/NSImage+AttachableAsCGImage.swift b/Sources/Overlays/_Testing_AppKit/Attachments/NSImage+AttachableAsCGImage.swift index d0830accf..d62ef71cc 100644 --- a/Sources/Overlays/_Testing_AppKit/Attachments/NSImage+AttachableAsCGImage.swift +++ b/Sources/Overlays/_Testing_AppKit/Attachments/NSImage+AttachableAsCGImage.swift @@ -53,7 +53,7 @@ extension NSImage: AttachableAsCGImage { return maxRepWidth ?? 1.0 } - public func _makeCopyForAttachment() -> Self { + public func _copyAttachableValue() -> Self { // If this image is of an NSImage subclass, we cannot reliably make a deep // copy of it because we don't know what its `init(data:)` implementation // might do. Try to make a copy (using NSCopying), but if that doesn't work diff --git a/Sources/Overlays/_Testing_CoreGraphics/Attachments/AttachableAsCGImage.swift b/Sources/Overlays/_Testing_CoreGraphics/Attachments/AttachableAsCGImage.swift index 5f5c7b2d7..8ca2a0ae4 100644 --- a/Sources/Overlays/_Testing_CoreGraphics/Attachments/AttachableAsCGImage.swift +++ b/Sources/Overlays/_Testing_CoreGraphics/Attachments/AttachableAsCGImage.swift @@ -65,16 +65,17 @@ public protocol AttachableAsCGImage { /// /// - Returns: A copy of `self`, or `self` if no copy is needed. /// - /// Several system image types do not conform to `Sendable`; use this - /// function to make copies of such images that will not be shared outside - /// of an attachment and so can be generally safely stored. + /// The testing library uses this function to take ownership of image + /// resources that test authors pass to it. If possible, make a copy of or add + /// a reference to `self`. If this type does not support making copies, return + /// `self` verbatim. /// /// The default implementation of this function when `Self` conforms to /// `Sendable` simply returns `self`. /// /// This function is not part of the public interface of the testing library. /// It may be removed in a future update. - func _makeCopyForAttachment() -> Self + func _copyAttachableValue() -> Self } @available(_uttypesAPI, *) @@ -90,7 +91,7 @@ extension AttachableAsCGImage { @available(_uttypesAPI, *) extension AttachableAsCGImage where Self: Sendable { - public func _makeCopyForAttachment() -> Self { + public func _copyAttachableValue() -> Self { self } } diff --git a/Sources/Overlays/_Testing_CoreGraphics/Attachments/_AttachableImageWrapper.swift b/Sources/Overlays/_Testing_CoreGraphics/Attachments/_AttachableImageWrapper.swift index f17990455..61904936f 100644 --- a/Sources/Overlays/_Testing_CoreGraphics/Attachments/_AttachableImageWrapper.swift +++ b/Sources/Overlays/_Testing_CoreGraphics/Attachments/_AttachableImageWrapper.swift @@ -54,20 +54,20 @@ import UniformTypeIdentifiers /// (iOS, watchOS, tvOS, visionOS, and Mac Catalyst) @_spi(Experimental) @available(_uttypesAPI, *) -public struct _AttachableImageWrapper: Sendable where Image: AttachableAsCGImage { +public final class _AttachableImageWrapper: Sendable where Image: AttachableAsCGImage { /// The underlying image. /// /// `CGImage` and `UIImage` are sendable, but `NSImage` is not. `NSImage` /// instances can be created from closures that are run at rendering time. /// The AppKit cross-import overlay is responsible for ensuring that any /// instances of this type it creates hold "safe" `NSImage` instances. - nonisolated(unsafe) var image: Image + nonisolated(unsafe) let image: Image /// The image format to use when encoding the represented image. - var imageFormat: AttachableImageFormat? + let imageFormat: AttachableImageFormat? init(image: Image, imageFormat: AttachableImageFormat?) { - self.image = image._makeCopyForAttachment() + self.image = image._copyAttachableValue() self.imageFormat = imageFormat } } @@ -80,7 +80,7 @@ extension _AttachableImageWrapper: AttachableWrapper { image } - public func withUnsafeBytes(for attachment: borrowing Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { + public func withUnsafeBytes(for attachment: borrowing Attachment<_AttachableImageWrapper>, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { let data = NSMutableData() // Convert the image to a CGImage. @@ -116,7 +116,7 @@ extension _AttachableImageWrapper: AttachableWrapper { } } - public borrowing func preferredName(for attachment: borrowing Attachment, basedOn suggestedName: String) -> String { + public borrowing func preferredName(for attachment: borrowing Attachment<_AttachableImageWrapper>, basedOn suggestedName: String) -> String { let contentType = AttachableImageFormat.computeContentType(for: imageFormat, withPreferredName: suggestedName) return (suggestedName as NSString).appendingPathExtension(for: contentType) } diff --git a/Sources/Overlays/_Testing_CoreImage/Attachments/CIImage+AttachableAsCGImage.swift b/Sources/Overlays/_Testing_CoreImage/Attachments/CIImage+AttachableAsCGImage.swift index 9a8278a83..7614dc633 100644 --- a/Sources/Overlays/_Testing_CoreImage/Attachments/CIImage+AttachableAsCGImage.swift +++ b/Sources/Overlays/_Testing_CoreImage/Attachments/CIImage+AttachableAsCGImage.swift @@ -23,7 +23,7 @@ extension CIImage: AttachableAsCGImage { } } - public func _makeCopyForAttachment() -> Self { + public func _copyAttachableValue() -> Self { // CIImage is documented as thread-safe, but does not conform to Sendable. // It conforms to NSCopying but does not actually copy itself, so there's no // point in calling copy(). diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsIWICBitmap.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsIWICBitmap.swift index 8375b4f7e..b17b82417 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsIWICBitmap.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsIWICBitmap.swift @@ -138,6 +138,9 @@ public protocol AttachableAsIWICBitmap { /// a reference to `self`. If this type does not support making copies, return /// `self` verbatim. /// + /// The default implementation of this function when `Self` conforms to + /// `Sendable` simply returns `self`. + /// /// This function is not part of the public interface of the testing library. /// It may be removed in a future update. func _copyAttachableValue() -> Self @@ -151,6 +154,9 @@ public protocol AttachableAsIWICBitmap { /// This function is not responsible for releasing the image returned from /// `_copyAttachableIWICBitmap(using:)`. /// + /// The default implementation of this function when `Self` conforms to + /// `Sendable` does nothing. + /// /// This function is not part of the public interface of the testing library. /// It may be removed in a future update. func _deinitializeAttachableValue() @@ -190,4 +196,12 @@ extension AttachableAsIWICBitmap { } } } + +extension AttachableAsIWICBitmap where Self: Sendable { + public func _copyAttachableValue() -> Self { + self + } + + public func _deinitializeAttachableValue() {} +} #endif diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableImageFormat+CLSID.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableImageFormat+CLSID.swift index 2fe25abb9..bf7e88366 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableImageFormat+CLSID.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableImageFormat+CLSID.swift @@ -112,7 +112,7 @@ extension AttachableImageFormat { if ext.starts(with: ".") { ext.dropFirst(1) } else { - ext[...] + ext } }.map(String.init) } From 893b4e8df44480124a54d61714663d3188963594 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Tue, 12 Aug 2025 13:33:17 -0400 Subject: [PATCH 095/216] Remove a workaround for a `URL.path` bug on Windows. (#1263) This PR removes a workaround for a bug in Foundation's `URL` on Windows where the `path` property would not be formatted correctly. See https://github.com/swiftlang/swift-foundation/pull/1038. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- .../Attachments/Attachment+URL.swift | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/Sources/Overlays/_Testing_Foundation/Attachments/Attachment+URL.swift b/Sources/Overlays/_Testing_Foundation/Attachments/Attachment+URL.swift index bb3668180..25c84959f 100644 --- a/Sources/Overlays/_Testing_Foundation/Attachments/Attachment+URL.swift +++ b/Sources/Overlays/_Testing_Foundation/Attachments/Attachment+URL.swift @@ -17,21 +17,6 @@ private import WinSDK #endif #if !SWT_NO_FILE_IO -extension URL { - /// The file system path of the URL, equivalent to `path`. - var fileSystemPath: String { -#if os(Windows) - // BUG: `path` includes a leading slash which makes it invalid on Windows. - // SEE: https://github.com/swiftlang/swift-foundation/pull/964 - let path = path - if path.starts(with: /\/[A-Za-z]:\//) { - return String(path.dropFirst()) - } -#endif - return path - } -} - extension Attachment where AttachableValue == _AttachableURLWrapper { #if SWT_TARGET_OS_APPLE /// An operation queue to use for asynchronously reading data from disk. @@ -203,8 +188,8 @@ private func _compressContentsOfDirectory(at directoryURL: URL) async throws -> throw CocoaError(.featureUnsupported, userInfo: [NSLocalizedDescriptionKey: "This platform does not support attaching directories to tests."]) #endif - let sourcePath = directoryURL.fileSystemPath - let destinationPath = temporaryURL.fileSystemPath + let sourcePath = directoryURL.path + let destinationPath = temporaryURL.path let arguments = { #if os(Linux) || os(OpenBSD) // The zip command constructs relative paths from the current working From a68a681c8adcd35be1b2b350a49cd0cf7031d084 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Tue, 12 Aug 2025 13:43:16 -0400 Subject: [PATCH 096/216] Enhance documentation for URL attachments. (#1262) Enhance documentation for [`Attachment.init(contentsOf:named:sourceLocation:)`](https://developer.apple.com/documentation/testing/attachment/init(contentsof:named:sourcelocation:)). In particular, provide info on what kind of URLs can be passed in, what the initializer awaits on, and how to use this (since there's intentionally no equivalent `record()` overload.) Resolves #1255. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --------- Co-authored-by: Graham Lee --- .../Attachments/Attachment+URL.swift | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/Sources/Overlays/_Testing_Foundation/Attachments/Attachment+URL.swift b/Sources/Overlays/_Testing_Foundation/Attachments/Attachment+URL.swift index 25c84959f..3ca05b8d1 100644 --- a/Sources/Overlays/_Testing_Foundation/Attachments/Attachment+URL.swift +++ b/Sources/Overlays/_Testing_Foundation/Attachments/Attachment+URL.swift @@ -36,6 +36,26 @@ extension Attachment where AttachableValue == _AttachableURLWrapper { /// /// - Throws: Any error that occurs attempting to read from `url`. /// + /// Use this initializer to create an instance of ``Attachment`` that + /// represents a local file or directory: + /// + /// ```swift + /// let url = try await FoodTruck.saveMenu(as: .pdf) + /// let attachment = try await Attachment(contentsOf: url) + /// Attachment.record(attachment) + /// ``` + /// + /// When you call this initializer and pass it the URL of a file, it reads or + /// maps the contents of that file into memory. When you call this initializer + /// and pass it the URL of a directory, it creates a temporary zip file of the + /// directory before reading or mapping it into memory. These operations may + /// take some time, so this initializer suspends the calling task until they + /// are complete. + /// + /// - Important: This initializer supports creating attachments from file URLs + /// only. If you pass it a URL other than a file URL, such as an HTTPS URL, + /// the testing library throws an error. + /// /// @Metadata { /// @Available(Swift, introduced: 6.2) /// @Available(Xcode, introduced: 26.0) From 75fde1dae74376441687ec94b84ce1acea30f4c2 Mon Sep 17 00:00:00 2001 From: Stuart Montgomery Date: Wed, 13 Aug 2025 17:55:11 -0500 Subject: [PATCH 097/216] Emit a diagnostic if a display name string is empty (#1256) --- ...EditorPlaceholderExprSyntaxAdditions.swift | 31 ++++++++++++++-- .../Support/AttributeDiscovery.swift | 11 ++++++ .../Support/DiagnosticMessage.swift | 35 +++++++++++++++++++ .../TestDeclarationMacroTests.swift | 8 +++++ 4 files changed, 83 insertions(+), 2 deletions(-) diff --git a/Sources/TestingMacros/Support/Additions/EditorPlaceholderExprSyntaxAdditions.swift b/Sources/TestingMacros/Support/Additions/EditorPlaceholderExprSyntaxAdditions.swift index 9a0d31ab3..360b5260e 100644 --- a/Sources/TestingMacros/Support/Additions/EditorPlaceholderExprSyntaxAdditions.swift +++ b/Sources/TestingMacros/Support/Additions/EditorPlaceholderExprSyntaxAdditions.swift @@ -9,6 +9,7 @@ // import SwiftSyntax +import SwiftSyntaxBuilder extension EditorPlaceholderExprSyntax { /// Initialize an instance of this type with the given placeholder string and @@ -39,7 +40,7 @@ extension EditorPlaceholderExprSyntax { // Manually concatenate the string to avoid it being interpreted as a // placeholder when editing this file. - self.init(placeholder: .identifier("<#\(placeholderContent)#" + ">")) + self.init(placeholder: .identifier(_editorPlaceholder(containing: placeholderContent))) } /// Initialize an instance of this type with the given type, using that as the @@ -62,6 +63,32 @@ extension TypeSyntax { /// /// - Returns: A new `TypeSyntax` instance representing a placeholder. static func placeholder(_ placeholder: String) -> Self { - return Self(IdentifierTypeSyntax(name: .identifier("<#\(placeholder)#" + ">"))) + Self(IdentifierTypeSyntax(name: .identifier(_editorPlaceholder(containing: placeholder)))) } } + +extension StringLiteralExprSyntax { + /// Construct a string literal expression syntax node containing an editor + /// placeholder string. + /// + /// - Parameters + /// - placeholder: The placeholder string, not including surrounding angle + /// brackets or pound characters. + init(placeholder: String) { + self.init(content: _editorPlaceholder(containing: placeholder)) + } +} + +/// Format a source editor placeholder string with the specified content. +/// +/// - Parameters: +/// - content: The placeholder string, not including surrounding angle +/// brackets or pound characters +/// +/// - Returns: A fully-formatted formatted editor placeholder string, including +/// necessary surrounding punctuation. +private func _editorPlaceholder(containing content: String) -> String { + // Manually concatenate the string to avoid it being interpreted as a + // placeholder when editing this file. + "<#\(content)#" + ">" +} diff --git a/Sources/TestingMacros/Support/AttributeDiscovery.swift b/Sources/TestingMacros/Support/AttributeDiscovery.swift index 3d95df294..fdf4b7e93 100644 --- a/Sources/TestingMacros/Support/AttributeDiscovery.swift +++ b/Sources/TestingMacros/Support/AttributeDiscovery.swift @@ -11,6 +11,7 @@ import SwiftSyntax import SwiftSyntaxBuilder import SwiftSyntaxMacros +import SwiftParser /// A syntax rewriter that removes leading `Self.` tokens from member access /// expressions in a syntax tree. @@ -149,6 +150,16 @@ struct AttributeInfo { } } + // If there was a display name but it's completely empty, emit a diagnostic + // since this can cause confusion isn't generally recommended. Note that + // this is only possible for string literal display names; the compiler + // enforces that raw identifiers must be non-empty. + if let namedDecl = declaration.asProtocol((any NamedDeclSyntax).self), + let displayName, let displayNameArgument, + displayName.representedLiteralValue?.isEmpty == true { + context.diagnose(.declaration(namedDecl, hasEmptyDisplayName: displayName, fromArgument: displayNameArgument, using: attribute)) + } + // Remove leading "Self." expressions from the arguments of the attribute. // See _SelfRemover for more information. Rewriting a syntax tree discards // location information from the copy, so only invoke the rewriter if the diff --git a/Sources/TestingMacros/Support/DiagnosticMessage.swift b/Sources/TestingMacros/Support/DiagnosticMessage.swift index 80c3d9e1f..7d7ee1f31 100644 --- a/Sources/TestingMacros/Support/DiagnosticMessage.swift +++ b/Sources/TestingMacros/Support/DiagnosticMessage.swift @@ -643,6 +643,41 @@ struct DiagnosticMessage: SwiftDiagnostics.DiagnosticMessage { ) } + /// Create a diagnostic message stating that a string literal expression + /// passed as the display name to a `@Test` or `@Suite` attribute is empty + /// but should not be. + /// + /// - Parameters: + /// - decl: The declaration that has an empty display name. + /// - displayNameExpr: The display name string literal expression. + /// - argumentContainingDisplayName: The argument node containing the node + /// `displayNameExpr`. + /// - attribute: The `@Test` or `@Suite` attribute. + /// + /// - Returns: A diagnostic message. + static func declaration( + _ decl: some NamedDeclSyntax, + hasEmptyDisplayName displayNameExpr: StringLiteralExprSyntax, + fromArgument argumentContainingDisplayName: LabeledExprListSyntax.Element, + using attribute: AttributeSyntax + ) -> Self { + Self( + syntax: Syntax(displayNameExpr), + message: "Attribute \(_macroName(attribute)) specifies an empty display name for this \(_kindString(for: decl))", + severity: .error, + fixIts: [ + FixIt( + message: MacroExpansionFixItMessage("Remove display name argument"), + changes: [.replace(oldNode: Syntax(argumentContainingDisplayName), newNode: Syntax("" as ExprSyntax))] + ), + FixIt( + message: MacroExpansionFixItMessage("Add display name"), + changes: [.replace(oldNode: Syntax(argumentContainingDisplayName), newNode: Syntax(StringLiteralExprSyntax(placeholder: "display name")))] + ), + ] + ) + } + /// Create a diagnostic message stating that a declaration has two display /// names. /// diff --git a/Tests/TestingMacrosTests/TestDeclarationMacroTests.swift b/Tests/TestingMacrosTests/TestDeclarationMacroTests.swift index 47e2b5112..9ac9c6264 100644 --- a/Tests/TestingMacrosTests/TestDeclarationMacroTests.swift +++ b/Tests/TestingMacrosTests/TestDeclarationMacroTests.swift @@ -149,6 +149,14 @@ struct TestDeclarationMacroTests { "Attribute 'Test' cannot be applied to a function within structure 'S' because its conformance to 'Escapable' has been suppressed", "struct S: ~(Escapable) { @Test func f() {} }": "Attribute 'Test' cannot be applied to a function within structure 'S' because its conformance to 'Escapable' has been suppressed", + + // empty display name string literal + #"@Test("") func f() {}"#: + "Attribute 'Test' specifies an empty display name for this function", + ##"@Test(#""#) func f() {}"##: + "Attribute 'Test' specifies an empty display name for this function", + #"@Suite("") struct S {}"#: + "Attribute 'Suite' specifies an empty display name for this structure", ] ) func apiMisuseErrors(input: String, expectedMessage: String) throws { From 9faef9bf88dd7d59cdd49367c59acb026555735f Mon Sep 17 00:00:00 2001 From: Jerry Chen Date: Thu, 14 Aug 2025 07:41:54 -0700 Subject: [PATCH 098/216] Test that the fixups are generated if display name is empty (#1267) No functional change: adds new test cases for the change in #1256 Moved the tests previously added to `apiMisuseErrors` -> `apiMisuseErrorsIncludingFixIts`, and include the expected fixits It would probably be ok to leave them in `apiMisuseErrors` as well, but I think it would be kinda redundant since we also test the expected message along with the fixits already. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- .../TestDeclarationMacroTests.swift | 64 ++++++++++++++++--- 1 file changed, 56 insertions(+), 8 deletions(-) diff --git a/Tests/TestingMacrosTests/TestDeclarationMacroTests.swift b/Tests/TestingMacrosTests/TestDeclarationMacroTests.swift index 9ac9c6264..b65a6a62e 100644 --- a/Tests/TestingMacrosTests/TestDeclarationMacroTests.swift +++ b/Tests/TestingMacrosTests/TestDeclarationMacroTests.swift @@ -149,14 +149,6 @@ struct TestDeclarationMacroTests { "Attribute 'Test' cannot be applied to a function within structure 'S' because its conformance to 'Escapable' has been suppressed", "struct S: ~(Escapable) { @Test func f() {} }": "Attribute 'Test' cannot be applied to a function within structure 'S' because its conformance to 'Escapable' has been suppressed", - - // empty display name string literal - #"@Test("") func f() {}"#: - "Attribute 'Test' specifies an empty display name for this function", - ##"@Test(#""#) func f() {}"##: - "Attribute 'Test' specifies an empty display name for this function", - #"@Suite("") struct S {}"#: - "Attribute 'Suite' specifies an empty display name for this structure", ] ) func apiMisuseErrors(input: String, expectedMessage: String) throws { @@ -241,6 +233,62 @@ struct TestDeclarationMacroTests { ), ] ), + + // empty display name string literal + #"@Test("") func f() {}"#: + ( + message: "Attribute 'Test' specifies an empty display name for this function", + fixIts: [ + ExpectedFixIt( + message: "Remove display name argument", + changes: [ + .replace(oldSourceCode: #""""#, newSourceCode: "") + ]), + ExpectedFixIt( + message: "Add display name", + changes: [ + .replace( + oldSourceCode: #""""#, + newSourceCode: #""\#(EditorPlaceholderExprSyntax("display name"))""#) + ]) + ] + ), + ##"@Test(#""#) func f() {}"##: + ( + message: "Attribute 'Test' specifies an empty display name for this function", + fixIts: [ + ExpectedFixIt( + message: "Remove display name argument", + changes: [ + .replace(oldSourceCode: ##"#""#"##, newSourceCode: "") + ]), + ExpectedFixIt( + message: "Add display name", + changes: [ + .replace( + oldSourceCode: ##"#""#"##, + newSourceCode: #""\#(EditorPlaceholderExprSyntax("display name"))""#) + ]) + ] + ), + #"@Suite("") struct S {}"#: + ( + message: "Attribute 'Suite' specifies an empty display name for this structure", + fixIts: [ + ExpectedFixIt( + message: "Remove display name argument", + changes: [ + .replace(oldSourceCode: #""""#, newSourceCode: "") + ]), + ExpectedFixIt( + message: "Add display name", + changes: [ + .replace( + oldSourceCode: #""""#, + newSourceCode: #""\#(EditorPlaceholderExprSyntax("display name"))""#) + ]) + ] + ) ] } From 2b45ed11714c9e75258793b87c830e38f6ab607c Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Thu, 14 Aug 2025 10:54:36 -0400 Subject: [PATCH 099/216] Allow attaching an `IWICBitmapSource` instance directly. (#1266) This PR adjusts the experimental WIC bitmap attachment support I recently added so that a test author can attach an instance of `IWICBitmapSource` (which is the parent type of the already-supported `IWICBitmap`.) Protocols and conformances are adjusted to match. This PR then explicitly adds conformances to every COM class in WIC that subclasses `IWICBitmapSource` since we can't see COM class inheritance in Swift (yet?) I could have left the protocols as-is, but then you'd have to convert an `IWICBitmapSource` to an `IWICBitmap` (by allocating a new COM object) then cast it back to an `IWICBitmapSource` (by calling `QueryInterface()` and juggling the refcount) in order to actually attach it. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- ...ift => AttachableAsIWICBitmapSource.swift} | 67 +++------ ...chment+AttachableAsIWICBitmapSource.swift} | 12 +- ...BITMAP+AttachableAsIWICBitmapSource.swift} | 10 +- ... HICON+AttachableAsIWICBitmapSource.swift} | 10 +- .../IWICBitmap+AttachableAsIWICBitmap.swift | 35 ----- ...pSource+AttachableAsIWICBitmapSource.swift | 131 ++++++++++++++++++ ...ointer+AttachableAsIWICBitmapSource.swift} | 6 +- .../Attachments/_AttachableImageWrapper.swift | 13 +- Tests/TestingTests/AttachmentTests.swift | 20 ++- 9 files changed, 194 insertions(+), 110 deletions(-) rename Sources/Overlays/_Testing_WinSDK/Attachments/{AttachableAsIWICBitmap.swift => AttachableAsIWICBitmapSource.swift} (73%) rename Sources/Overlays/_Testing_WinSDK/Attachments/{Attachment+AttachableAsIWICBitmap.swift => Attachment+AttachableAsIWICBitmapSource.swift} (88%) rename Sources/Overlays/_Testing_WinSDK/Attachments/{HBITMAP+AttachableAsIWICBitmap.swift => HBITMAP+AttachableAsIWICBitmapSource.swift} (83%) rename Sources/Overlays/_Testing_WinSDK/Attachments/{HICON+AttachableAsIWICBitmap.swift => HICON+AttachableAsIWICBitmapSource.swift} (81%) delete mode 100644 Sources/Overlays/_Testing_WinSDK/Attachments/IWICBitmap+AttachableAsIWICBitmap.swift create mode 100644 Sources/Overlays/_Testing_WinSDK/Attachments/IWICBitmapSource+AttachableAsIWICBitmapSource.swift rename Sources/Overlays/_Testing_WinSDK/Attachments/{UnsafeMutablePointer+AttachableAsIWICBitmap.swift => UnsafeMutablePointer+AttachableAsIWICBitmapSource.swift} (64%) diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsIWICBitmap.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsIWICBitmapSource.swift similarity index 73% rename from Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsIWICBitmap.swift rename to Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsIWICBitmapSource.swift index b17b82417..b3c37d27f 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsIWICBitmap.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsIWICBitmapSource.swift @@ -26,32 +26,33 @@ public import WinSDK /// /// - [`HBITMAP`](https://learn.microsoft.com/en-us/windows/win32/gdi/bitmaps) /// - [`HICON`](https://learn.microsoft.com/en-us/windows/win32/menurc/icons) -/// - [`IWICBitmap`](https://learn.microsoft.com/en-us/windows/win32/api/wincodec/nn-wincodec-iwicbitmap) +/// - [`IWICBitmapSource`](https://learn.microsoft.com/en-us/windows/win32/api/wincodec/nn-wincodec-iwicbitmapsource) +/// (including its subclasses declared by Windows Imaging Component) /// /// You do not generally need to add your own conformances to this protocol. If /// you have an image in another format that needs to be attached to a test, /// first convert it to an instance of one of the types above. @_spi(Experimental) -public protocol _AttachableByAddressAsIWICBitmap { - /// Create a WIC bitmap representing an instance of this type at the given - /// address. +public protocol _AttachableByAddressAsIWICBitmapSource { + /// Create a WIC bitmap source representing an instance of this type at the + /// given address. /// /// - Parameters: /// - imageAddress: The address of the instance of this type. /// - factory: A WIC imaging factory that can be used to create additional /// WIC objects. /// - /// - Returns: A pointer to a new WIC bitmap representing this image. The - /// caller is responsible for releasing this image when done with it. + /// - Returns: A pointer to a new WIC bitmap source representing this image. + /// The caller is responsible for releasing this image when done with it. /// /// - Throws: Any error that prevented the creation of the WIC bitmap. /// /// This function is not part of the public interface of the testing library. /// It may be removed in a future update. - static func _copyAttachableIWICBitmap( + static func _copyAttachableIWICBitmapSource( from imageAddress: UnsafeMutablePointer, using factory: UnsafeMutablePointer - ) throws -> UnsafeMutablePointer + ) throws -> UnsafeMutablePointer /// Make a copy of the instance of this type at the given address. /// @@ -84,7 +85,7 @@ public protocol _AttachableByAddressAsIWICBitmap { /// does not call this function. /// /// This function is not responsible for releasing the image returned from - /// `_copyAttachableIWICBitmap(from:using:)`. + /// `_copyAttachableIWICBitmapSource(from:using:)`. /// /// This function is not part of the public interface of the testing library. /// It may be removed in a future update. @@ -104,13 +105,14 @@ public protocol _AttachableByAddressAsIWICBitmap { /// /// - [`HBITMAP`](https://learn.microsoft.com/en-us/windows/win32/gdi/bitmaps) /// - [`HICON`](https://learn.microsoft.com/en-us/windows/win32/menurc/icons) -/// - [`IWICBitmap`](https://learn.microsoft.com/en-us/windows/win32/api/wincodec/nn-wincodec-iwicbitmap) +/// - [`IWICBitmapSource`](https://learn.microsoft.com/en-us/windows/win32/api/wincodec/nn-wincodec-iwicbitmapsource) +/// (including its subclasses declared by Windows Imaging Component) /// /// You do not generally need to add your own conformances to this protocol. If /// you have an image in another format that needs to be attached to a test, /// first convert it to an instance of one of the types above. @_spi(Experimental) -public protocol AttachableAsIWICBitmap { +public protocol AttachableAsIWICBitmapSource { /// Create a WIC bitmap representing an instance of this type. /// /// - Parameters: @@ -124,9 +126,9 @@ public protocol AttachableAsIWICBitmap { /// /// This function is not part of the public interface of the testing library. /// It may be removed in a future update. - borrowing func _copyAttachableIWICBitmap( + borrowing func _copyAttachableIWICBitmapSource( using factory: UnsafeMutablePointer - ) throws -> UnsafeMutablePointer + ) throws -> UnsafeMutablePointer /// Make a copy of this instance. /// @@ -152,7 +154,7 @@ public protocol AttachableAsIWICBitmap { /// automatically invokes this function as needed. /// /// This function is not responsible for releasing the image returned from - /// `_copyAttachableIWICBitmap(using:)`. + /// `_copyAttachableIWICBitmapSource(using:)`. /// /// The default implementation of this function when `Self` conforms to /// `Sendable` does nothing. @@ -162,42 +164,7 @@ public protocol AttachableAsIWICBitmap { func _deinitializeAttachableValue() } -extension AttachableAsIWICBitmap { - /// Create a WIC bitmap representing an instance of this type and return it as - /// an instance of `IWICBitmapSource`. - /// - /// - Parameters: - /// - factory: A WIC imaging factory that can be used to create additional - /// WIC objects. - /// - /// - Returns: A pointer to a new WIC bitmap representing this image. The - /// caller is responsible for releasing this image when done with it. - /// - /// - Throws: Any error that prevented the creation of the WIC bitmap. - /// - /// This function is a convenience over `_copyAttachableIWICBitmap(using:)` - /// that casts the result of that function to `IWICBitmapSource` (as needed - /// by WIC when it encodes the image.) - borrowing func copyAttachableIWICBitmapSource( - using factory: UnsafeMutablePointer - ) throws -> UnsafeMutablePointer { - let bitmap = try _copyAttachableIWICBitmap(using: factory) - defer { - _ = bitmap.pointee.lpVtbl.pointee.Release(bitmap) - } - - return try withUnsafePointer(to: IID_IWICBitmapSource) { IID_IWICBitmapSource in - var bitmapSource: UnsafeMutableRawPointer? - let rQuery = bitmap.pointee.lpVtbl.pointee.QueryInterface(bitmap, IID_IWICBitmapSource, &bitmapSource) - guard rQuery == S_OK, let bitmapSource else { - throw ImageAttachmentError.queryInterfaceFailed(IWICBitmapSource.self, rQuery) - } - return bitmapSource.assumingMemoryBound(to: IWICBitmapSource.self) - } - } -} - -extension AttachableAsIWICBitmap where Self: Sendable { +extension AttachableAsIWICBitmapSource where Self: Sendable { public func _copyAttachableValue() -> Self { self } diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/Attachment+AttachableAsIWICBitmap.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/Attachment+AttachableAsIWICBitmapSource.swift similarity index 88% rename from Sources/Overlays/_Testing_WinSDK/Attachments/Attachment+AttachableAsIWICBitmap.swift rename to Sources/Overlays/_Testing_WinSDK/Attachments/Attachment+AttachableAsIWICBitmapSource.swift index b8ff6552e..5597ab1f4 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/Attachment+AttachableAsIWICBitmap.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/Attachment+AttachableAsIWICBitmapSource.swift @@ -27,11 +27,12 @@ extension Attachment where AttachableValue: ~Copyable { /// attachment. /// /// The following system-provided image types conform to the - /// ``AttachableAsIWICBitmap`` protocol and can be attached to a test: + /// ``AttachableAsIWICBitmapSource`` protocol and can be attached to a test: /// /// - [`HBITMAP`](https://learn.microsoft.com/en-us/windows/win32/gdi/bitmaps) /// - [`HICON`](https://learn.microsoft.com/en-us/windows/win32/menurc/icons) - /// - [`IWICBitmap`](https://learn.microsoft.com/en-us/windows/win32/api/wincodec/nn-wincodec-iwicbitmap) + /// - [`IWICBitmapSource`](https://learn.microsoft.com/en-us/windows/win32/api/wincodec/nn-wincodec-iwicbitmapsource) + /// (including its subclasses declared by Windows Imaging Component) /// /// The testing library uses the image format specified by `imageFormat`. Pass /// `nil` to let the testing library decide which image format to use. If you @@ -66,11 +67,12 @@ extension Attachment where AttachableValue: ~Copyable { /// and immediately attaches it to the current test. /// /// The following system-provided image types conform to the - /// ``AttachableAsIWICBitmap`` protocol and can be attached to a test: + /// ``AttachableAsIWICBitmapSource`` protocol and can be attached to a test: /// /// - [`HBITMAP`](https://learn.microsoft.com/en-us/windows/win32/gdi/bitmaps) /// - [`HICON`](https://learn.microsoft.com/en-us/windows/win32/menurc/icons) - /// - [`IWICBitmap`](https://learn.microsoft.com/en-us/windows/win32/api/wincodec/nn-wincodec-iwicbitmap) + /// - [`IWICBitmapSource`](https://learn.microsoft.com/en-us/windows/win32/api/wincodec/nn-wincodec-iwicbitmapsource) + /// (including its subclasses declared by Windows Imaging Component) /// /// The testing library uses the image format specified by `imageFormat`. Pass /// `nil` to let the testing library decide which image format to use. If you @@ -92,7 +94,7 @@ extension Attachment where AttachableValue: ~Copyable { } @_spi(Experimental) -extension Attachment where AttachableValue: AttachableWrapper, AttachableValue.Wrapped: AttachableAsIWICBitmap { +extension Attachment where AttachableValue: AttachableWrapper, AttachableValue.Wrapped: AttachableAsIWICBitmapSource { /// The image format to use when encoding the represented image. @_disfavoredOverload public var imageFormat: AttachableImageFormat? { diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/HBITMAP+AttachableAsIWICBitmap.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/HBITMAP+AttachableAsIWICBitmapSource.swift similarity index 83% rename from Sources/Overlays/_Testing_WinSDK/Attachments/HBITMAP+AttachableAsIWICBitmap.swift rename to Sources/Overlays/_Testing_WinSDK/Attachments/HBITMAP+AttachableAsIWICBitmapSource.swift index 296a971e6..0d11fd0bc 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/HBITMAP+AttachableAsIWICBitmap.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/HBITMAP+AttachableAsIWICBitmapSource.swift @@ -14,17 +14,17 @@ import Testing public import WinSDK @_spi(Experimental) -extension HBITMAP__: _AttachableByAddressAsIWICBitmap { - public static func _copyAttachableIWICBitmap( +extension HBITMAP__: _AttachableByAddressAsIWICBitmapSource { + public static func _copyAttachableIWICBitmapSource( from imageAddress: UnsafeMutablePointer, using factory: UnsafeMutablePointer - ) throws -> UnsafeMutablePointer { - var bitmap: UnsafeMutablePointer! + ) throws -> UnsafeMutablePointer { + var bitmap: UnsafeMutablePointer? let rCreate = factory.pointee.lpVtbl.pointee.CreateBitmapFromHBITMAP(factory, imageAddress, nil, WICBitmapUsePremultipliedAlpha, &bitmap) guard rCreate == S_OK, let bitmap else { throw ImageAttachmentError.comObjectCreationFailed(IWICBitmap.self, rCreate) } - return bitmap + return try bitmap.cast(to: IWICBitmapSource.self) } public static func _copyAttachableValue(at imageAddress: UnsafeMutablePointer) -> UnsafeMutablePointer { diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/HICON+AttachableAsIWICBitmap.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/HICON+AttachableAsIWICBitmapSource.swift similarity index 81% rename from Sources/Overlays/_Testing_WinSDK/Attachments/HICON+AttachableAsIWICBitmap.swift rename to Sources/Overlays/_Testing_WinSDK/Attachments/HICON+AttachableAsIWICBitmapSource.swift index caeb2b5f6..a25e8f371 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/HICON+AttachableAsIWICBitmap.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/HICON+AttachableAsIWICBitmapSource.swift @@ -14,17 +14,17 @@ import Testing public import WinSDK @_spi(Experimental) -extension HICON__: _AttachableByAddressAsIWICBitmap { - public static func _copyAttachableIWICBitmap( +extension HICON__: _AttachableByAddressAsIWICBitmapSource { + public static func _copyAttachableIWICBitmapSource( from imageAddress: UnsafeMutablePointer, using factory: UnsafeMutablePointer - ) throws -> UnsafeMutablePointer { - var bitmap: UnsafeMutablePointer! + ) throws -> UnsafeMutablePointer { + var bitmap: UnsafeMutablePointer? let rCreate = factory.pointee.lpVtbl.pointee.CreateBitmapFromHICON(factory, imageAddress, &bitmap) guard rCreate == S_OK, let bitmap else { throw ImageAttachmentError.comObjectCreationFailed(IWICBitmap.self, rCreate) } - return bitmap + return try bitmap.cast(to: IWICBitmapSource.self) } public static func _copyAttachableValue(at imageAddress: UnsafeMutablePointer) -> UnsafeMutablePointer { diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/IWICBitmap+AttachableAsIWICBitmap.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/IWICBitmap+AttachableAsIWICBitmap.swift deleted file mode 100644 index f5fcd4c26..000000000 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/IWICBitmap+AttachableAsIWICBitmap.swift +++ /dev/null @@ -1,35 +0,0 @@ -// -// 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 -// - -#if os(Windows) -import Testing - -public import WinSDK - -@_spi(Experimental) -extension IWICBitmap: _AttachableByAddressAsIWICBitmap { - public static func _copyAttachableIWICBitmap( - from imageAddress: UnsafeMutablePointer, - using factory: UnsafeMutablePointer - ) throws -> UnsafeMutablePointer { - _ = imageAddress.pointee.lpVtbl.pointee.AddRef(imageAddress) - return imageAddress - } - - public static func _copyAttachableValue(at imageAddress: UnsafeMutablePointer) -> UnsafeMutablePointer { - _ = imageAddress.pointee.lpVtbl.pointee.AddRef(imageAddress) - return imageAddress - } - - public static func _deinitializeAttachableValue(at imageAddress: UnsafeMutablePointer) { - _ = imageAddress.pointee.lpVtbl.pointee.Release(imageAddress) - } -} -#endif diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/IWICBitmapSource+AttachableAsIWICBitmapSource.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/IWICBitmapSource+AttachableAsIWICBitmapSource.swift new file mode 100644 index 000000000..401e6f480 --- /dev/null +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/IWICBitmapSource+AttachableAsIWICBitmapSource.swift @@ -0,0 +1,131 @@ +// +// 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 +// + +#if os(Windows) +import Testing + +public import WinSDK + +/// - Important: The casts in this file to `IUnknown` are safe insofar as we use +/// them to access fixed members of the COM vtable. The casts would become +/// unsafe if we allowed the resulting pointers to escape _and_ if any of the +/// types we use them on have multiple non-virtual inheritance to `IUnknown`. + +/// A protocol that identifies a type as a COM subclass of `IWICBitmapSource`. +/// +/// Because COM class inheritance is not visible in Swift, we must manually +/// apply conformances to this protocol to each COM type that inherits from +/// `IWICBitmapSource`. +/// +/// Because this protocol is not `public`, we must also explicitly restate +/// conformance to the public protocol `_AttachableByAddressAsIWICBitmapSource` +/// even though this protocol refines that one. This protocol refines +/// `_AttachableByAddressAsIWICBitmapSource` because otherwise the compiler will +/// not allow us to declare `public` members in its extension that provides the +/// implementation of `_AttachableByAddressAsIWICBitmapSource` below. +/// +/// This protocol is not part of the public interface of the testing library. It +/// allows us to reuse code across all subclasses of `IWICBitmapSource`. +protocol IWICBitmapSourceProtocol: _AttachableByAddressAsIWICBitmapSource {} + +@_spi(Experimental) +extension IWICBitmapSource: _AttachableByAddressAsIWICBitmapSource, IWICBitmapSourceProtocol {} + +@_spi(Experimental) +extension IWICBitmap: _AttachableByAddressAsIWICBitmapSource, IWICBitmapSourceProtocol {} + +@_spi(Experimental) +extension IWICBitmapClipper: _AttachableByAddressAsIWICBitmapSource, IWICBitmapSourceProtocol {} + +@_spi(Experimental) +extension IWICBitmapFlipRotator: _AttachableByAddressAsIWICBitmapSource, IWICBitmapSourceProtocol {} + +@_spi(Experimental) +extension IWICBitmapFrameDecode: _AttachableByAddressAsIWICBitmapSource, IWICBitmapSourceProtocol {} + +@_spi(Experimental) +extension IWICBitmapScaler: _AttachableByAddressAsIWICBitmapSource, IWICBitmapSourceProtocol {} + +@_spi(Experimental) +extension IWICColorTransform: _AttachableByAddressAsIWICBitmapSource, IWICBitmapSourceProtocol {} + +@_spi(Experimental) +extension IWICFormatConverter: _AttachableByAddressAsIWICBitmapSource, IWICBitmapSourceProtocol {} + +@_spi(Experimental) +extension IWICPlanarFormatConverter: _AttachableByAddressAsIWICBitmapSource, IWICBitmapSourceProtocol {} + +// MARK: - Upcasting conveniences + +extension UnsafeMutablePointer where Pointee: IWICBitmapSourceProtocol { + /// Upcast this WIC bitmap to a WIC bitmap source (its parent type). + /// + /// - Returns: `self`, cast to the parent type via `QueryInterface()`. The + /// caller is responsible for releasing the resulting object. + /// + /// - Throws: Any error that occurs while calling `QueryInterface()`. In + /// practice, this function is not expected to throw an error as it should + /// always be possible to cast a valid instance of `IWICBitmap` to + /// `IWICBitmapSource`. + /// + /// - Important: This function consumes a reference to `self` even if the cast + /// fails. + consuming func cast(to _: IWICBitmapSource.Type) throws -> UnsafeMutablePointer { + try self.withMemoryRebound(to: IUnknown.self, capacity: 1) { `self` in + defer { + _ = self.pointee.lpVtbl.pointee.Release(self) + } + + return try withUnsafePointer(to: IID_IWICBitmapSource) { IID_IWICBitmapSource in + var bitmapSource: UnsafeMutableRawPointer? + let rQuery = self.pointee.lpVtbl.pointee.QueryInterface(self, IID_IWICBitmapSource, &bitmapSource) + guard rQuery == S_OK, let bitmapSource else { + throw ImageAttachmentError.queryInterfaceFailed(IWICBitmapSource.self, rQuery) + } + return bitmapSource.assumingMemoryBound(to: IWICBitmapSource.self) + } + } + } +} + +// MARK: - _AttachableByAddressAsIWICBitmapSource implementation + +extension IWICBitmapSourceProtocol { + public static func _copyAttachableIWICBitmapSource( + from imageAddress: UnsafeMutablePointer, + using factory: UnsafeMutablePointer + ) throws -> UnsafeMutablePointer { + try _copyAttachableValue(at: imageAddress).cast(to: IWICBitmapSource.self) + } + + public static func _copyAttachableValue(at imageAddress: UnsafeMutablePointer) -> UnsafeMutablePointer { + imageAddress.withMemoryRebound(to: IUnknown.self, capacity: 1) { imageAddress in + _ = imageAddress.pointee.lpVtbl.pointee.AddRef(imageAddress) + } + return imageAddress + } + + public static func _deinitializeAttachableValue(at imageAddress: UnsafeMutablePointer) { + imageAddress.withMemoryRebound(to: IUnknown.self, capacity: 1) { imageAddress in + _ = imageAddress.pointee.lpVtbl.pointee.Release(imageAddress) + } + } +} + +extension IWICBitmapSource { + public static func _copyAttachableIWICBitmapSource( + from imageAddress: UnsafeMutablePointer, + using factory: UnsafeMutablePointer + ) throws -> UnsafeMutablePointer { + _ = imageAddress.pointee.lpVtbl.pointee.AddRef(imageAddress) + return imageAddress + } +} +#endif diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/UnsafeMutablePointer+AttachableAsIWICBitmap.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/UnsafeMutablePointer+AttachableAsIWICBitmapSource.swift similarity index 64% rename from Sources/Overlays/_Testing_WinSDK/Attachments/UnsafeMutablePointer+AttachableAsIWICBitmap.swift rename to Sources/Overlays/_Testing_WinSDK/Attachments/UnsafeMutablePointer+AttachableAsIWICBitmapSource.swift index c957cf40a..0bfa354f4 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/UnsafeMutablePointer+AttachableAsIWICBitmap.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/UnsafeMutablePointer+AttachableAsIWICBitmapSource.swift @@ -14,9 +14,9 @@ import Testing public import WinSDK @_spi(Experimental) -extension UnsafeMutablePointer: AttachableAsIWICBitmap where Pointee: _AttachableByAddressAsIWICBitmap { - public func _copyAttachableIWICBitmap(using factory: UnsafeMutablePointer) throws -> UnsafeMutablePointer { - try Pointee._copyAttachableIWICBitmap(from: self, using: factory) +extension UnsafeMutablePointer: AttachableAsIWICBitmapSource where Pointee: _AttachableByAddressAsIWICBitmapSource { + public func _copyAttachableIWICBitmapSource(using factory: UnsafeMutablePointer) throws -> UnsafeMutablePointer { + try Pointee._copyAttachableIWICBitmapSource(from: self, using: factory) } public func _copyAttachableValue() -> Self { diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/_AttachableImageWrapper.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/_AttachableImageWrapper.swift index 09fe7554e..63931c378 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/_AttachableImageWrapper.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/_AttachableImageWrapper.swift @@ -18,15 +18,16 @@ internal import WinSDK /// /// You do not need to use this type directly. Instead, initialize an instance /// of ``Attachment`` using an instance of an image type that conforms to -/// ``AttachableAsIWICBitmap``. The following system-provided image types -/// conform to the ``AttachableAsIWICBitmap`` protocol and can be attached to a -/// test: +/// ``AttachableAsIWICBitmapSource``. The following system-provided image types +/// conform to the ``AttachableAsIWICBitmapSource`` protocol and can be attached +/// to a test: /// /// - [`HBITMAP`](https://learn.microsoft.com/en-us/windows/win32/gdi/bitmaps) /// - [`HICON`](https://learn.microsoft.com/en-us/windows/win32/menurc/icons) -/// - [`IWICBitmap`](https://learn.microsoft.com/en-us/windows/win32/api/wincodec/nn-wincodec-iwicbitmap) +/// - [`IWICBitmapSource`](https://learn.microsoft.com/en-us/windows/win32/api/wincodec/nn-wincodec-iwicbitmapsource) +/// (including its subclasses declared by Windows Imaging Component) @_spi(Experimental) -public final class _AttachableImageWrapper: Sendable where Image: AttachableAsIWICBitmap { +public final class _AttachableImageWrapper: Sendable where Image: AttachableAsIWICBitmapSource { /// The underlying image. nonisolated(unsafe) let image: Image @@ -70,7 +71,7 @@ extension _AttachableImageWrapper: AttachableWrapper { } // Create the bitmap and downcast it to an IWICBitmapSource for later use. - let bitmap = try image.copyAttachableIWICBitmapSource(using: factory) + let bitmap = try image._copyAttachableIWICBitmapSource(using: factory) defer { _ = bitmap.pointee.lpVtbl.pointee.Release(bitmap) } diff --git a/Tests/TestingTests/AttachmentTests.swift b/Tests/TestingTests/AttachmentTests.swift index 8604e05c3..e26dbe595 100644 --- a/Tests/TestingTests/AttachmentTests.swift +++ b/Tests/TestingTests/AttachmentTests.swift @@ -775,7 +775,7 @@ extension AttachmentTests { Attachment.record(loFi) } - @MainActor @Test func attachIWICBitmap() throws { + private func copyIWICBitmap() throws -> UnsafeMutablePointer { let factory = try IWICImagingFactory.create() defer { _ = factory.pointee.lpVtbl.pointee.Release(factory) @@ -791,6 +791,11 @@ extension AttachmentTests { guard rCreate == S_OK, let wicBitmap else { throw ImageAttachmentError.comObjectCreationFailed(IWICBitmap.self, rCreate) } + return wicBitmap + } + + @MainActor @Test func attachIWICBitmap() throws { + let wicBitmap = try copyIWICBitmap() defer { _ = wicBitmap.pointee.lpVtbl.pointee.Release(wicBitmap) } @@ -802,6 +807,19 @@ extension AttachmentTests { Attachment.record(attachment) } + @MainActor @Test func attachIWICBitmapSource() throws { + let wicBitmapSource = try copyIWICBitmap().cast(to: IWICBitmapSource.self) + defer { + _ = wicBitmapSource.pointee.lpVtbl.pointee.Release(wicBitmapSource) + } + + let attachment = Attachment(wicBitmapSource, named: "diamond.png") + try attachment.withUnsafeBytes { buffer in + #expect(buffer.count > 32) + } + Attachment.record(attachment) + } + @MainActor @Test func pathExtensionAndCLSID() { let pngCLSID = AttachableImageFormat.png.clsid let pngFilename = AttachableImageFormat.appendPathExtension(for: pngCLSID, to: "example") From 16030001a1d08e59ce6ffafc4cae9eefa6229978 Mon Sep 17 00:00:00 2001 From: Kelvin Bui <150134371+tienquocbui@users.noreply.github.com> Date: Thu, 14 Aug 2025 23:20:00 +0700 Subject: [PATCH 100/216] Add experimental AdvancedConsoleOutputRecorder skeleton framework (#1253) Add experimental AdvancedConsoleOutputRecorder skeleton framework ### Motivation: The current console output for `swift test` is a static log, which presents challenges for developers in understanding test progress and diagnosing failures, especially in large, parallel test suites. This PR introduces the foundational "skeleton" for a new, advanced console reporter to address these issues. The recorder is marked as experimental and must be explicitly enabled via the `SWT_ENABLE_EXPERIMENTAL_CONSOLE_OUTPUT` environment variable, ensuring it doesn't affect existing functionality. ### Modifications: - **Added `Event.AdvancedConsoleOutputRecorder.swift`**: New experimental console output recorder skeleton with: - Basic structure and minimal configuration options - Currently delegates to `Event.ConsoleOutputRecorder` for actual output - Foundation framework ready for future development of advanced features The implementation provides the foundation structure following established patterns in the codebase, ready for future development of advanced console features. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- .../Testing/ABI/EntryPoints/EntryPoint.swift | 29 +++++++-- Sources/Testing/CMakeLists.txt | 1 + .../Event.AdvancedConsoleOutputRecorder.swift | 63 +++++++++++++++++++ 3 files changed, 87 insertions(+), 6 deletions(-) create mode 100644 Sources/Testing/Events/Recorder/Event.AdvancedConsoleOutputRecorder.swift diff --git a/Sources/Testing/ABI/EntryPoints/EntryPoint.swift b/Sources/Testing/ABI/EntryPoints/EntryPoint.swift index ed16a9841..4fe85ea73 100644 --- a/Sources/Testing/ABI/EntryPoints/EntryPoint.swift +++ b/Sources/Testing/ABI/EntryPoints/EntryPoint.swift @@ -54,12 +54,29 @@ func entryPoint(passing args: __CommandLineArguments_v0?, eventHandler: Event.Ha #if !SWT_NO_FILE_IO // Configure the event recorder to write events to stderr. if configuration.verbosity > .min { - let eventRecorder = Event.ConsoleOutputRecorder(options: .for(.stderr)) { string in - try? FileHandle.stderr.write(string) - } - configuration.eventHandler = { [oldEventHandler = configuration.eventHandler] event, context in - eventRecorder.record(event, in: context) - oldEventHandler(event, context) + // Check for experimental console output flag + if Environment.flag(named: "SWT_ENABLE_EXPERIMENTAL_CONSOLE_OUTPUT") == true { + // Use experimental AdvancedConsoleOutputRecorder + var advancedOptions = Event.AdvancedConsoleOutputRecorder.Options() + advancedOptions.base = .for(.stderr) + + let eventRecorder = Event.AdvancedConsoleOutputRecorder(options: advancedOptions) { string in + try? FileHandle.stderr.write(string) + } + + configuration.eventHandler = { [oldEventHandler = configuration.eventHandler] event, context in + eventRecorder.record(event, in: context) + oldEventHandler(event, context) + } + } else { + // Use the standard console output recorder (default behavior) + let eventRecorder = Event.ConsoleOutputRecorder(options: .for(.stderr)) { string in + try? FileHandle.stderr.write(string) + } + configuration.eventHandler = { [oldEventHandler = configuration.eventHandler] event, context in + eventRecorder.record(event, in: context) + oldEventHandler(event, context) + } } } #endif diff --git a/Sources/Testing/CMakeLists.txt b/Sources/Testing/CMakeLists.txt index f970f43bb..36437e167 100644 --- a/Sources/Testing/CMakeLists.txt +++ b/Sources/Testing/CMakeLists.txt @@ -26,6 +26,7 @@ add_library(Testing Attachments/Attachment.swift Events/Clock.swift Events/Event.swift + Events/Recorder/Event.AdvancedConsoleOutputRecorder.swift Events/Recorder/Event.ConsoleOutputRecorder.swift Events/Recorder/Event.HumanReadableOutputRecorder.swift Events/Recorder/Event.JUnitXMLRecorder.swift diff --git a/Sources/Testing/Events/Recorder/Event.AdvancedConsoleOutputRecorder.swift b/Sources/Testing/Events/Recorder/Event.AdvancedConsoleOutputRecorder.swift new file mode 100644 index 000000000..784929993 --- /dev/null +++ b/Sources/Testing/Events/Recorder/Event.AdvancedConsoleOutputRecorder.swift @@ -0,0 +1,63 @@ +// +// 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 Event { + /// An experimental console output recorder that provides enhanced test result + /// display capabilities. + /// + /// This recorder is currently experimental and must be enabled via the + /// `SWT_ENABLE_EXPERIMENTAL_CONSOLE_OUTPUT` environment variable. + struct AdvancedConsoleOutputRecorder: Sendable { + /// Configuration options for the advanced console output recorder. + struct Options: Sendable { + /// Base console output recorder options to inherit from. + var base: Event.ConsoleOutputRecorder.Options + + init() { + self.base = Event.ConsoleOutputRecorder.Options() + } + } + + /// The options for this recorder. + let options: Options + + /// The write function for this recorder. + let write: @Sendable (String) -> Void + + /// The fallback console recorder for standard output. + private let _fallbackRecorder: Event.ConsoleOutputRecorder + + /// Initialize the advanced console output recorder. + /// + /// - Parameters: + /// - options: Configuration options for the recorder. + /// - write: A closure that writes output to its destination. + init(options: Options = Options(), writingUsing write: @escaping @Sendable (String) -> Void) { + self.options = options + self.write = write + self._fallbackRecorder = Event.ConsoleOutputRecorder(options: options.base, writingUsing: write) + } + } +} + +extension Event.AdvancedConsoleOutputRecorder { + /// Record an event by processing it and generating appropriate output. + /// + /// Currently this is a skeleton implementation that delegates to + /// ``Event/ConsoleOutputRecorder``. + /// + /// - Parameters: + /// - event: The event to record. + /// - eventContext: The context associated with the event. + func record(_ event: borrowing Event, in eventContext: borrowing Event.Context) { + // Skeleton implementation: delegate to ConsoleOutputRecorder + _fallbackRecorder.record(event, in: eventContext) + } +} From 0b48b608a822b5c2ed7b9eddf7b4f270375dc7ea Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Fri, 15 Aug 2025 13:09:57 -0400 Subject: [PATCH 101/216] Revise Windows image attachments to match the draft API proposal. (#1268) This PR tweaks the Windows image attachments API to match what is proposed in https://github.com/swiftlang/swift-evolution/pull/2940. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- .../AttachableImageFormat+UTType.swift | 34 +++++++++++++++++++ .../AttachableAsIWICBitmapSource.swift | 25 +++++++++++++- .../AttachableImageFormat+CLSID.swift | 18 +++++----- ...achment+AttachableAsIWICBitmapSource.swift | 6 ++-- ...Pointer+AttachableAsIWICBitmapSource.swift | 8 +++++ .../Attachments/AttachableImageFormat.swift | 2 ++ Tests/TestingTests/AttachmentTests.swift | 8 +++++ 7 files changed, 89 insertions(+), 12 deletions(-) diff --git a/Sources/Overlays/_Testing_CoreGraphics/Attachments/AttachableImageFormat+UTType.swift b/Sources/Overlays/_Testing_CoreGraphics/Attachments/AttachableImageFormat+UTType.swift index 575e357cd..173265807 100644 --- a/Sources/Overlays/_Testing_CoreGraphics/Attachments/AttachableImageFormat+UTType.swift +++ b/Sources/Overlays/_Testing_CoreGraphics/Attachments/AttachableImageFormat+UTType.swift @@ -97,4 +97,38 @@ extension AttachableImageFormat { self.init(kind: .systemValue(contentType), encodingQuality: encodingQuality) } } + +@available(_uttypesAPI, *) +@_spi(Experimental) // STOP: not part of ST-0014 +extension AttachableImageFormat { + /// Construct an instance of this type with the given path extension and + /// encoding quality. + /// + /// - Parameters: + /// - pathExtension: A path extension corresponding to the image format to + /// use when encoding images. + /// - encodingQuality: The encoding quality to use when encoding images. For + /// the lowest supported quality, pass `0.0`. For the highest supported + /// quality, pass `1.0`. + /// + /// If the target image format does not support variable-quality encoding, + /// the value of the `encodingQuality` argument is ignored. + /// + /// If `pathExtension` does not correspond to a recognized image format, this + /// initializer returns `nil`: + /// + /// - On Apple platforms, the content type corresponding to `pathExtension` + /// must conform to [`UTType.image`](https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/image). + /// - On Windows, there must be a corresponding subclass of [`IWICBitmapEncoder`](https://learn.microsoft.com/en-us/windows/win32/api/wincodec/nn-wincodec-iwicbitmapencoder) + /// registered with Windows Imaging Component. + public init?(pathExtension: String, encodingQuality: Float = 1.0) { + let pathExtension = pathExtension.drop { $0 == "." } + + guard let contentType = UTType(filenameExtension: String(pathExtension), conformingTo: .image) else { + return nil + } + + self.init(contentType, encodingQuality: encodingQuality) + } +} #endif diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsIWICBitmapSource.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsIWICBitmapSource.swift index b3c37d27f..cf23df63a 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsIWICBitmapSource.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsIWICBitmapSource.swift @@ -113,6 +113,14 @@ public protocol _AttachableByAddressAsIWICBitmapSource { /// first convert it to an instance of one of the types above. @_spi(Experimental) public protocol AttachableAsIWICBitmapSource { + /// Create a WIC bitmap source representing an instance of this type. + /// + /// - Returns: A pointer to a new WIC bitmap source representing this image. + /// The caller is responsible for releasing this image when done with it. + /// + /// - Throws: Any error that prevented the creation of the WIC bitmap source. + func copyAttachableIWICBitmapSource() throws -> UnsafeMutablePointer + /// Create a WIC bitmap representing an instance of this type. /// /// - Parameters: @@ -124,9 +132,16 @@ public protocol AttachableAsIWICBitmapSource { /// /// - Throws: Any error that prevented the creation of the WIC bitmap. /// + /// The default implementation of this function ignores `factory` and calls + /// ``copyAttachableIWICBitmapSource()``. If your implementation of + /// ``copyAttachableIWICBitmapSource()`` needs to create a WIC imaging factory + /// in order to return a result, it is more efficient to implement this + /// function too so that the testing library can pass the WIC imaging factory + /// it creates. + /// /// This function is not part of the public interface of the testing library. /// It may be removed in a future update. - borrowing func _copyAttachableIWICBitmapSource( + func _copyAttachableIWICBitmapSource( using factory: UnsafeMutablePointer ) throws -> UnsafeMutablePointer @@ -164,6 +179,14 @@ public protocol AttachableAsIWICBitmapSource { func _deinitializeAttachableValue() } +extension AttachableAsIWICBitmapSource { + public func _copyAttachableIWICBitmapSource( + using factory: UnsafeMutablePointer + ) throws -> UnsafeMutablePointer { + try copyAttachableIWICBitmapSource() + } +} + extension AttachableAsIWICBitmapSource where Self: Sendable { public func _copyAttachableValue() -> Self { self diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableImageFormat+CLSID.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableImageFormat+CLSID.swift index bf7e88366..009015e13 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableImageFormat+CLSID.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableImageFormat+CLSID.swift @@ -277,19 +277,21 @@ extension AttachableImageFormat { /// If the target image format does not support variable-quality encoding, /// the value of the `encodingQuality` argument is ignored. /// - /// If `pathExtension` does not correspond to an image format that WIC can use - /// to encode images, this initializer returns `nil`. For a list of image - /// encoders supported by WIC, see the documentation for the [IWICBitmapEncoder](https://learn.microsoft.com/en-us/windows/win32/api/wincodec/nn-wincodec-iwicbitmapencoder) - /// class. + /// If `pathExtension` does not correspond to a recognized image format, this + /// initializer returns `nil`: + /// + /// - On Apple platforms, the content type corresponding to `pathExtension` + /// must conform to [`UTType.image`](https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/image). + /// - On Windows, there must be a corresponding subclass of [`IWICBitmapEncoder`](https://learn.microsoft.com/en-us/windows/win32/api/wincodec/nn-wincodec-iwicbitmapencoder) + /// registered with Windows Imaging Component. public init?(pathExtension: String, encodingQuality: Float = 1.0) { let pathExtension = pathExtension.drop { $0 == "." } - let clsid = Self._computeCLSID(forPathExtension: String(pathExtension)) - if let clsid { - self.init(clsid, encodingQuality: encodingQuality) - } else { + guard let clsid = Self._computeCLSID(forPathExtension: String(pathExtension)) else { return nil } + + self.init(clsid, encodingQuality: encodingQuality) } } #endif diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/Attachment+AttachableAsIWICBitmapSource.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/Attachment+AttachableAsIWICBitmapSource.swift index 5597ab1f4..ec0bb016b 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/Attachment+AttachableAsIWICBitmapSource.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/Attachment+AttachableAsIWICBitmapSource.swift @@ -12,7 +12,7 @@ @_spi(Experimental) public import Testing @_spi(Experimental) -extension Attachment where AttachableValue: ~Copyable { +extension Attachment { /// Initialize an instance of this type that encloses the given image. /// /// - Parameters: @@ -42,7 +42,7 @@ extension Attachment where AttachableValue: ~Copyable { /// correspond to an image format the operating system knows how to write, the /// testing library selects an appropriate image format for you. public init( - _ image: borrowing T, + _ image: T, named preferredName: String? = nil, as imageFormat: AttachableImageFormat? = nil, sourceLocation: SourceLocation = #_sourceLocation @@ -82,7 +82,7 @@ extension Attachment where AttachableValue: ~Copyable { /// correspond to an image format the operating system knows how to write, the /// testing library selects an appropriate image format for you. public static func record( - _ image: borrowing T, + _ image: T, named preferredName: String? = nil, as imageFormat: AttachableImageFormat? = nil, sourceLocation: SourceLocation = #_sourceLocation diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/UnsafeMutablePointer+AttachableAsIWICBitmapSource.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/UnsafeMutablePointer+AttachableAsIWICBitmapSource.swift index 0bfa354f4..6d34a6e90 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/UnsafeMutablePointer+AttachableAsIWICBitmapSource.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/UnsafeMutablePointer+AttachableAsIWICBitmapSource.swift @@ -15,6 +15,14 @@ public import WinSDK @_spi(Experimental) extension UnsafeMutablePointer: AttachableAsIWICBitmapSource where Pointee: _AttachableByAddressAsIWICBitmapSource { + public func copyAttachableIWICBitmapSource() throws -> UnsafeMutablePointer { + let factory = try IWICImagingFactory.create() + defer { + _ = factory.pointee.lpVtbl.pointee.Release(factory) + } + return try _copyAttachableIWICBitmapSource(using: factory) + } + public func _copyAttachableIWICBitmapSource(using factory: UnsafeMutablePointer) throws -> UnsafeMutablePointer { try Pointee._copyAttachableIWICBitmapSource(from: self, using: factory) } diff --git a/Sources/Testing/Attachments/AttachableImageFormat.swift b/Sources/Testing/Attachments/AttachableImageFormat.swift index bf5df4f7f..4798e42b1 100644 --- a/Sources/Testing/Attachments/AttachableImageFormat.swift +++ b/Sources/Testing/Attachments/AttachableImageFormat.swift @@ -23,6 +23,8 @@ /// - On Apple platforms, you can use [`CGImageDestinationCopyTypeIdentifiers()`](https://developer.apple.com/documentation/imageio/cgimagedestinationcopytypeidentifiers()) /// from the [Image I/O framework](https://developer.apple.com/documentation/imageio) /// to determine which formats are supported. +/// - On Windows, you can use [`IWICImagingFactory.CreateComponentEnumerator()`](https://learn.microsoft.com/en-us/windows/win32/api/wincodec/nf-wincodec-iwicimagingfactory-createcomponentenumerator) +/// to enumerate the available image encoders. @_spi(Experimental) @available(_uttypesAPI, *) public struct AttachableImageFormat: Sendable { diff --git a/Tests/TestingTests/AttachmentTests.swift b/Tests/TestingTests/AttachmentTests.swift index e26dbe595..84db8fe38 100644 --- a/Tests/TestingTests/AttachmentTests.swift +++ b/Tests/TestingTests/AttachmentTests.swift @@ -836,6 +836,14 @@ extension AttachmentTests { #expect(jpgjpegFilename == "example.jpg") } #endif + +#if (canImport(CoreGraphics) && canImport(_Testing_CoreGraphics)) || (canImport(WinSDK) && canImport(_Testing_WinSDK)) + @available(_uttypesAPI, *) + @Test func imageFormatFromPathExtension() { + let format = AttachableImageFormat(pathExtension: "png") + #expect(format != nil) + } +#endif } } From e801af770a54ca0b21842279d929ba871a51c093 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Mon, 18 Aug 2025 11:20:27 -0400 Subject: [PATCH 102/216] Propagate issue severity from exit test bodies. (#1272) This PR ensures that an issue with `.warning` severity is recorded correctly from within the body of an exit test. For example: ```swift await #expect(processExitsWith: .success) { Issue.record("TODO: implement exit test body", severity: .warning) } ``` ### Checklist: - [ ] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [ ] If public symbols are renamed or modified, DocC references should be updated. --- Sources/Testing/ExitTests/ExitTest.swift | 8 +++++++- Tests/TestingTests/ExitTestTests.swift | 20 ++++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/Sources/Testing/ExitTests/ExitTest.swift b/Sources/Testing/ExitTests/ExitTest.swift index 14f32d9d9..c1d6e9fe1 100644 --- a/Sources/Testing/ExitTests/ExitTest.swift +++ b/Sources/Testing/ExitTests/ExitTest.swift @@ -1046,11 +1046,17 @@ extension ExitTest { // TODO: improve fidelity of issue kind reporting (especially those without associated values) .unconditional } + let severity: Issue.Severity = switch issue._severity { + case .warning: + .warning + case .error: + .error + } let sourceContext = SourceContext( backtrace: nil, // `issue._backtrace` will have the wrong address space. sourceLocation: issue.sourceLocation ) - var issueCopy = Issue(kind: issueKind, comments: comments, sourceContext: sourceContext) + var issueCopy = Issue(kind: issueKind, severity: severity, comments: comments, sourceContext: sourceContext) if issue.isKnown { // The known issue comment, if there was one, is already included in // the `comments` array above. diff --git a/Tests/TestingTests/ExitTestTests.swift b/Tests/TestingTests/ExitTestTests.swift index e0bc0b86c..d86489e13 100644 --- a/Tests/TestingTests/ExitTestTests.swift +++ b/Tests/TestingTests/ExitTestTests.swift @@ -383,6 +383,26 @@ private import _TestingInternals } } + @Test("Issue severity") + func issueSeverity() async { + await confirmation("Recorded issue had warning severity") { wasWarning in + var configuration = Configuration() + configuration.eventHandler = { event, _ in + if case let .issueRecorded(issue) = event.kind, issue.severity == .warning { + wasWarning() + } + } + + // Mock an exit test where the process exits successfully. + configuration.exitTestHandler = ExitTest.handlerForEntryPoint() + await Test { + await #expect(processExitsWith: .success) { + Issue.record("Issue recorded", severity: .warning) + } + }.run(configuration: configuration) + } + } + @Test("Capture list") func captureList() async { let i = 123 From 317916fd33b4479351265715649e6c8c1fa7d40f Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Mon, 18 Aug 2025 12:22:07 -0400 Subject: [PATCH 103/216] Add @jerryjrchen as a code owner. (#1273) Add Jerry to `CODEOWNERS``. --- CODEOWNERS | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index e2590577e..0c6a4fc9a 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1,11 +1,11 @@ # # This source file is part of the Swift.org open source project # -# Copyright (c) 2023 Apple Inc. and the Swift project authors +# Copyright (c) 2023–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 # -* @stmontgomery @grynspan @briancroom @suzannaratcliff +* @stmontgomery @grynspan @briancroom @suzannaratcliff @jerryjrchen From 8cbb4917ee20328cc75045c5d5835138765f1039 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Mon, 18 Aug 2025 14:28:12 -0400 Subject: [PATCH 104/216] Reduce duplicated code between Darwin's and Windows' image attachments. (#1270) This PR cleans up the implementations of image attachments on Darwin and Windows so that we have less code duplication between the two. `_AttachableImageWrapper` is partially lowered to the main library (excepting the platform-specific bits) and `ImageAttachmentError` is lowered in its entirety. There are some adjustments to the (internal/package) interface of `_AttachableImageWrapper` to accomodate it not being able to specify conformance to `AttachableAsCGImage` or `AttachableAsIWICBitmapSource`. Namely, whatever code initializes an instance of it is responsible for copying `image` and providing a deinitializer function as needed. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- .../Attachments/AttachableAsCGImage.swift | 17 +++---- .../Attachment+AttachableAsCGImage.swift | 49 +++++++++--------- .../Attachments/ImageAttachmentError.swift | 34 ------------- ...hableImageWrapper+AttachableWrapper.swift} | 50 ++----------------- .../AttachableAsIWICBitmapSource.swift | 39 ++++++++------- .../AttachableImageFormat+CLSID.swift | 5 +- ...achment+AttachableAsIWICBitmapSource.swift | 46 +++++++++-------- ...HBITMAP+AttachableAsIWICBitmapSource.swift | 3 +- .../HICON+AttachableAsIWICBitmapSource.swift | 3 +- ...pSource+AttachableAsIWICBitmapSource.swift | 3 +- ...Pointer+AttachableAsIWICBitmapSource.swift | 3 +- ...hableImageWrapper+AttachableWrapper.swift} | 44 ++-------------- .../{ => Images}/AttachableImageFormat.swift | 0 .../Images}/ImageAttachmentError.swift | 46 ++++++++++++----- .../Images/_AttachableImageWrapper.swift | 49 ++++++++++++++++++ Sources/Testing/CMakeLists.txt | 3 ++ 16 files changed, 176 insertions(+), 218 deletions(-) delete mode 100644 Sources/Overlays/_Testing_CoreGraphics/Attachments/ImageAttachmentError.swift rename Sources/Overlays/_Testing_CoreGraphics/Attachments/{_AttachableImageWrapper.swift => _AttachableImageWrapper+AttachableWrapper.swift} (66%) rename Sources/Overlays/_Testing_WinSDK/Attachments/{_AttachableImageWrapper.swift => _AttachableImageWrapper+AttachableWrapper.swift} (76%) rename Sources/Testing/Attachments/{ => Images}/AttachableImageFormat.swift (100%) rename Sources/{Overlays/_Testing_WinSDK/Attachments => Testing/Attachments/Images}/ImageAttachmentError.swift (54%) create mode 100644 Sources/Testing/Attachments/Images/_AttachableImageWrapper.swift diff --git a/Sources/Overlays/_Testing_CoreGraphics/Attachments/AttachableAsCGImage.swift b/Sources/Overlays/_Testing_CoreGraphics/Attachments/AttachableAsCGImage.swift index 8ca2a0ae4..e51742dc3 100644 --- a/Sources/Overlays/_Testing_CoreGraphics/Attachments/AttachableAsCGImage.swift +++ b/Sources/Overlays/_Testing_CoreGraphics/Attachments/AttachableAsCGImage.swift @@ -20,22 +20,21 @@ private import ImageIO /// initializers on ``Testing/Attachment`` that take instances of such types and /// handle converting them to image data when needed. /// -/// The following system-provided image types conform to this protocol and can -/// be attached to a test: +/// You can attach instances of the following system-provided image types to a +/// test: /// -/// - [`CGImage`](https://developer.apple.com/documentation/coregraphics/cgimage) -/// - [`CIImage`](https://developer.apple.com/documentation/coreimage/ciimage) -/// - [`NSImage`](https://developer.apple.com/documentation/appkit/nsimage) -/// (macOS) -/// - [`UIImage`](https://developer.apple.com/documentation/uikit/uiimage) -/// (iOS, watchOS, tvOS, visionOS, and Mac Catalyst) +/// | Platform | Supported Types | +/// |-|-| +/// | macOS | [`CGImage`](https://developer.apple.com/documentation/coregraphics/cgimage), [`CIImage`](https://developer.apple.com/documentation/coreimage/ciimage), [`NSImage`](https://developer.apple.com/documentation/appkit/nsimage) | +/// | iOS, watchOS, tvOS, and visionOS | [`CGImage`](https://developer.apple.com/documentation/coregraphics/cgimage), [`CIImage`](https://developer.apple.com/documentation/coreimage/ciimage), [`UIImage`](https://developer.apple.com/documentation/uikit/uiimage) | +/// | Windows | [`HBITMAP`](https://learn.microsoft.com/en-us/windows/win32/gdi/bitmaps), [`HICON`](https://learn.microsoft.com/en-us/windows/win32/menurc/icons), [`IWICBitmapSource`](https://learn.microsoft.com/en-us/windows/win32/api/wincodec/nn-wincodec-iwicbitmapsource) (including its subclasses declared by Windows Imaging Component) | /// /// You do not generally need to add your own conformances to this protocol. If /// you have an image in another format that needs to be attached to a test, /// first convert it to an instance of one of the types above. @_spi(Experimental) @available(_uttypesAPI, *) -public protocol AttachableAsCGImage { +public protocol AttachableAsCGImage: SendableMetatype { /// An instance of `CGImage` representing this image. /// /// - Throws: Any error that prevents the creation of an image. diff --git a/Sources/Overlays/_Testing_CoreGraphics/Attachments/Attachment+AttachableAsCGImage.swift b/Sources/Overlays/_Testing_CoreGraphics/Attachments/Attachment+AttachableAsCGImage.swift index 866240e64..b3349915b 100644 --- a/Sources/Overlays/_Testing_CoreGraphics/Attachments/Attachment+AttachableAsCGImage.swift +++ b/Sources/Overlays/_Testing_CoreGraphics/Attachments/Attachment+AttachableAsCGImage.swift @@ -26,15 +26,14 @@ extension Attachment { /// This value is used when recording issues associated with the /// attachment. /// - /// The following system-provided image types conform to the - /// ``AttachableAsCGImage`` protocol and can be attached to a test: + /// You can attach instances of the following system-provided image types to a + /// test: /// - /// - [`CGImage`](https://developer.apple.com/documentation/coregraphics/cgimage) - /// - [`CIImage`](https://developer.apple.com/documentation/coreimage/ciimage) - /// - [`NSImage`](https://developer.apple.com/documentation/appkit/nsimage) - /// (macOS) - /// - [`UIImage`](https://developer.apple.com/documentation/uikit/uiimage) - /// (iOS, watchOS, tvOS, visionOS, and Mac Catalyst) + /// | Platform | Supported Types | + /// |-|-| + /// | macOS | [`CGImage`](https://developer.apple.com/documentation/coregraphics/cgimage), [`CIImage`](https://developer.apple.com/documentation/coreimage/ciimage), [`NSImage`](https://developer.apple.com/documentation/appkit/nsimage) | + /// | iOS, watchOS, tvOS, and visionOS | [`CGImage`](https://developer.apple.com/documentation/coregraphics/cgimage), [`CIImage`](https://developer.apple.com/documentation/coreimage/ciimage), [`UIImage`](https://developer.apple.com/documentation/uikit/uiimage) | + /// | Windows | [`HBITMAP`](https://learn.microsoft.com/en-us/windows/win32/gdi/bitmaps), [`HICON`](https://learn.microsoft.com/en-us/windows/win32/menurc/icons), [`IWICBitmapSource`](https://learn.microsoft.com/en-us/windows/win32/api/wincodec/nn-wincodec-iwicbitmapsource) (including its subclasses declared by Windows Imaging Component) | /// /// The testing library uses the image format specified by `imageFormat`. Pass /// `nil` to let the testing library decide which image format to use. If you @@ -48,8 +47,12 @@ extension Attachment { named preferredName: String? = nil, as imageFormat: AttachableImageFormat? = nil, sourceLocation: SourceLocation = #_sourceLocation - ) where AttachableValue == _AttachableImageWrapper { - let imageWrapper = _AttachableImageWrapper(image: image, imageFormat: imageFormat) + ) where T: AttachableAsCGImage, AttachableValue == _AttachableImageWrapper { + let imageWrapper = _AttachableImageWrapper( + image: image._copyAttachableValue(), + imageFormat: imageFormat, + deinitializingWith: { _ in } + ) self.init(imageWrapper, named: preferredName, sourceLocation: sourceLocation) } @@ -64,17 +67,14 @@ extension Attachment { /// - sourceLocation: The source location of the call to this function. /// /// This function creates a new instance of ``Attachment`` wrapping `image` - /// and immediately attaches it to the current test. + /// and immediately attaches it to the current test. You can attach instances + /// of the following system-provided image types to a test: /// - /// The following system-provided image types conform to the - /// ``AttachableAsCGImage`` protocol and can be attached to a test: - /// - /// - [`CGImage`](https://developer.apple.com/documentation/coregraphics/cgimage) - /// - [`CIImage`](https://developer.apple.com/documentation/coreimage/ciimage) - /// - [`NSImage`](https://developer.apple.com/documentation/appkit/nsimage) - /// (macOS) - /// - [`UIImage`](https://developer.apple.com/documentation/uikit/uiimage) - /// (iOS, watchOS, tvOS, visionOS, and Mac Catalyst) + /// | Platform | Supported Types | + /// |-|-| + /// | macOS | [`CGImage`](https://developer.apple.com/documentation/coregraphics/cgimage), [`CIImage`](https://developer.apple.com/documentation/coreimage/ciimage), [`NSImage`](https://developer.apple.com/documentation/appkit/nsimage) | + /// | iOS, watchOS, tvOS, and visionOS | [`CGImage`](https://developer.apple.com/documentation/coregraphics/cgimage), [`CIImage`](https://developer.apple.com/documentation/coreimage/ciimage), [`UIImage`](https://developer.apple.com/documentation/uikit/uiimage) | + /// | Windows | [`HBITMAP`](https://learn.microsoft.com/en-us/windows/win32/gdi/bitmaps), [`HICON`](https://learn.microsoft.com/en-us/windows/win32/menurc/icons), [`IWICBitmapSource`](https://learn.microsoft.com/en-us/windows/win32/api/wincodec/nn-wincodec-iwicbitmapsource) (including its subclasses declared by Windows Imaging Component) | /// /// The testing library uses the image format specified by `imageFormat`. Pass /// `nil` to let the testing library decide which image format to use. If you @@ -88,19 +88,20 @@ extension Attachment { named preferredName: String? = nil, as imageFormat: AttachableImageFormat? = nil, sourceLocation: SourceLocation = #_sourceLocation - ) where AttachableValue == _AttachableImageWrapper { + ) where T: AttachableAsCGImage, AttachableValue == _AttachableImageWrapper { let attachment = Self(image, named: preferredName, as: imageFormat, sourceLocation: sourceLocation) Self.record(attachment, sourceLocation: sourceLocation) } } +// MARK: - + @_spi(Experimental) // STOP: not part of ST-0014 @available(_uttypesAPI, *) extension Attachment where AttachableValue: AttachableWrapper, AttachableValue.Wrapped: AttachableAsCGImage { /// The image format to use when encoding the represented image. - @_disfavoredOverload - public var imageFormat: AttachableImageFormat? { - // FIXME: no way to express `where AttachableValue == _AttachableImageWrapper` on a property + @_disfavoredOverload public var imageFormat: AttachableImageFormat? { + // FIXME: no way to express `where AttachableValue == _AttachableImageWrapper` on a property (see rdar://47559973) (attachableValue as? _AttachableImageWrapper)?.imageFormat } } diff --git a/Sources/Overlays/_Testing_CoreGraphics/Attachments/ImageAttachmentError.swift b/Sources/Overlays/_Testing_CoreGraphics/Attachments/ImageAttachmentError.swift deleted file mode 100644 index f957888b7..000000000 --- a/Sources/Overlays/_Testing_CoreGraphics/Attachments/ImageAttachmentError.swift +++ /dev/null @@ -1,34 +0,0 @@ -// -// 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 -// - -#if SWT_TARGET_OS_APPLE && canImport(CoreGraphics) -/// A type representing an error that can occur when attaching an image. -package enum ImageAttachmentError: Error, CustomStringConvertible { - /// The image could not be converted to an instance of `CGImage`. - case couldNotCreateCGImage - - /// The image destination could not be created. - case couldNotCreateImageDestination - - /// The image could not be converted. - case couldNotConvertImage - - public var description: String { - switch self { - case .couldNotCreateCGImage: - "Could not create the corresponding Core Graphics image." - case .couldNotCreateImageDestination: - "Could not create the Core Graphics image destination to encode this image." - case .couldNotConvertImage: - "Could not convert the image to the specified format." - } - } -} -#endif diff --git a/Sources/Overlays/_Testing_CoreGraphics/Attachments/_AttachableImageWrapper.swift b/Sources/Overlays/_Testing_CoreGraphics/Attachments/_AttachableImageWrapper+AttachableWrapper.swift similarity index 66% rename from Sources/Overlays/_Testing_CoreGraphics/Attachments/_AttachableImageWrapper.swift rename to Sources/Overlays/_Testing_CoreGraphics/Attachments/_AttachableImageWrapper+AttachableWrapper.swift index 61904936f..bb0d42b23 100644 --- a/Sources/Overlays/_Testing_CoreGraphics/Attachments/_AttachableImageWrapper.swift +++ b/Sources/Overlays/_Testing_CoreGraphics/Attachments/_AttachableImageWrapper+AttachableWrapper.swift @@ -13,7 +13,7 @@ private import CoreGraphics private import ImageIO -import UniformTypeIdentifiers +private import UniformTypeIdentifiers /// ## Why can't images directly conform to Attachable? /// @@ -38,53 +38,13 @@ import UniformTypeIdentifiers /// (And no, the language does not let us write `where T: Self` anywhere /// useful.) -/// A wrapper type for image types such as `CGImage` and `NSImage` that can be -/// attached indirectly. -/// -/// You do not need to use this type directly. Instead, initialize an instance -/// of ``Attachment`` using an instance of an image type that conforms to -/// ``AttachableAsCGImage``. The following system-provided image types conform -/// to the ``AttachableAsCGImage`` protocol and can be attached to a test: -/// -/// - [`CGImage`](https://developer.apple.com/documentation/coregraphics/cgimage) -/// - [`CIImage`](https://developer.apple.com/documentation/coreimage/ciimage) -/// - [`NSImage`](https://developer.apple.com/documentation/appkit/nsimage) -/// (macOS) -/// - [`UIImage`](https://developer.apple.com/documentation/uikit/uiimage) -/// (iOS, watchOS, tvOS, visionOS, and Mac Catalyst) -@_spi(Experimental) @available(_uttypesAPI, *) -public final class _AttachableImageWrapper: Sendable where Image: AttachableAsCGImage { - /// The underlying image. - /// - /// `CGImage` and `UIImage` are sendable, but `NSImage` is not. `NSImage` - /// instances can be created from closures that are run at rendering time. - /// The AppKit cross-import overlay is responsible for ensuring that any - /// instances of this type it creates hold "safe" `NSImage` instances. - nonisolated(unsafe) let image: Image - - /// The image format to use when encoding the represented image. - let imageFormat: AttachableImageFormat? - - init(image: Image, imageFormat: AttachableImageFormat?) { - self.image = image._copyAttachableValue() - self.imageFormat = imageFormat - } -} - -// MARK: - - -@available(_uttypesAPI, *) -extension _AttachableImageWrapper: AttachableWrapper { - public var wrappedValue: Image { - image - } - +extension _AttachableImageWrapper: Attachable, AttachableWrapper where Image: AttachableAsCGImage { public func withUnsafeBytes(for attachment: borrowing Attachment<_AttachableImageWrapper>, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { let data = NSMutableData() // Convert the image to a CGImage. - let attachableCGImage = try image.attachableCGImage + let attachableCGImage = try wrappedValue.attachableCGImage // Create the image destination. let contentType = AttachableImageFormat.computeContentType(for: imageFormat, withPreferredName: attachment.preferredName) @@ -93,8 +53,8 @@ extension _AttachableImageWrapper: AttachableWrapper { } // Configure the properties of the image conversion operation. - let orientation = image._attachmentOrientation - let scaleFactor = image._attachmentScaleFactor + let orientation = wrappedValue._attachmentOrientation + let scaleFactor = wrappedValue._attachmentScaleFactor let properties: [CFString: Any] = [ kCGImageDestinationLossyCompressionQuality: CGFloat(imageFormat?.encodingQuality ?? 1.0), kCGImagePropertyOrientation: orientation, diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsIWICBitmapSource.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsIWICBitmapSource.swift index cf23df63a..fdcad1809 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsIWICBitmapSource.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsIWICBitmapSource.swift @@ -9,8 +9,7 @@ // #if os(Windows) -@_spi(Experimental) import Testing - +private import Testing public import WinSDK /// A protocol describing images that can be converted to instances of @@ -21,13 +20,14 @@ public import WinSDK /// initializers on ``Testing/Attachment`` that take instances of such types and /// handle converting them to image data when needed. /// -/// The following system-provided image types conform to this protocol and can -/// be attached to a test: +/// You can attach instances of the following system-provided image types to a +/// test: /// -/// - [`HBITMAP`](https://learn.microsoft.com/en-us/windows/win32/gdi/bitmaps) -/// - [`HICON`](https://learn.microsoft.com/en-us/windows/win32/menurc/icons) -/// - [`IWICBitmapSource`](https://learn.microsoft.com/en-us/windows/win32/api/wincodec/nn-wincodec-iwicbitmapsource) -/// (including its subclasses declared by Windows Imaging Component) +/// | Platform | Supported Types | +/// |-|-| +/// | macOS | [`CGImage`](https://developer.apple.com/documentation/coregraphics/cgimage), [`CIImage`](https://developer.apple.com/documentation/coreimage/ciimage), [`NSImage`](https://developer.apple.com/documentation/appkit/nsimage) | +/// | iOS, watchOS, tvOS, and visionOS | [`CGImage`](https://developer.apple.com/documentation/coregraphics/cgimage), [`CIImage`](https://developer.apple.com/documentation/coreimage/ciimage), [`UIImage`](https://developer.apple.com/documentation/uikit/uiimage) | +/// | Windows | [`HBITMAP`](https://learn.microsoft.com/en-us/windows/win32/gdi/bitmaps), [`HICON`](https://learn.microsoft.com/en-us/windows/win32/menurc/icons), [`IWICBitmapSource`](https://learn.microsoft.com/en-us/windows/win32/api/wincodec/nn-wincodec-iwicbitmapsource) (including its subclasses declared by Windows Imaging Component) | /// /// You do not generally need to add your own conformances to this protocol. If /// you have an image in another format that needs to be attached to a test, @@ -93,26 +93,27 @@ public protocol _AttachableByAddressAsIWICBitmapSource { } /// A protocol describing images that can be converted to instances of -/// ``Testing/Attachment``. +/// [`Attachment`](https://developer.apple.com/documentation/testing/attachment). /// /// Instances of types conforming to this protocol do not themselves conform to -/// ``Testing/Attachable``. Instead, the testing library provides additional -/// initializers on ``Testing/Attachment`` that take instances of such types and -/// handle converting them to image data when needed. +/// [`Attachable`](https://developer.apple.com/documentation/testing/attachable). +/// Instead, the testing library provides additional initializers on [`Attachment`](https://developer.apple.com/documentation/testing/attachment) +/// that take instances of such types and handle converting them to image data when needed. /// -/// The following system-provided image types conform to this protocol and can -/// be attached to a test: +/// You can attach instances of the following system-provided image types to a +/// test: /// -/// - [`HBITMAP`](https://learn.microsoft.com/en-us/windows/win32/gdi/bitmaps) -/// - [`HICON`](https://learn.microsoft.com/en-us/windows/win32/menurc/icons) -/// - [`IWICBitmapSource`](https://learn.microsoft.com/en-us/windows/win32/api/wincodec/nn-wincodec-iwicbitmapsource) -/// (including its subclasses declared by Windows Imaging Component) +/// | Platform | Supported Types | +/// |-|-| +/// | macOS | [`CGImage`](https://developer.apple.com/documentation/coregraphics/cgimage), [`CIImage`](https://developer.apple.com/documentation/coreimage/ciimage), [`NSImage`](https://developer.apple.com/documentation/appkit/nsimage) | +/// | iOS, watchOS, tvOS, and visionOS | [`CGImage`](https://developer.apple.com/documentation/coregraphics/cgimage), [`CIImage`](https://developer.apple.com/documentation/coreimage/ciimage), [`UIImage`](https://developer.apple.com/documentation/uikit/uiimage) | +/// | Windows | [`HBITMAP`](https://learn.microsoft.com/en-us/windows/win32/gdi/bitmaps), [`HICON`](https://learn.microsoft.com/en-us/windows/win32/menurc/icons), [`IWICBitmapSource`](https://learn.microsoft.com/en-us/windows/win32/api/wincodec/nn-wincodec-iwicbitmapsource) (including its subclasses declared by Windows Imaging Component) | /// /// You do not generally need to add your own conformances to this protocol. If /// you have an image in another format that needs to be attached to a test, /// first convert it to an instance of one of the types above. @_spi(Experimental) -public protocol AttachableAsIWICBitmapSource { +public protocol AttachableAsIWICBitmapSource: SendableMetatype { /// Create a WIC bitmap source representing an instance of this type. /// /// - Returns: A pointer to a new WIC bitmap source representing this image. diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableImageFormat+CLSID.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableImageFormat+CLSID.swift index 009015e13..1881fa036 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableImageFormat+CLSID.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableImageFormat+CLSID.swift @@ -9,8 +9,7 @@ // #if os(Windows) -@_spi(Experimental) import Testing - +@_spi(Experimental) public import Testing public import WinSDK extension AttachableImageFormat { @@ -258,7 +257,7 @@ extension AttachableImageFormat { /// /// If `clsid` does not represent an image encoder type supported by WIC, the /// result is undefined. For a list of image encoders supported by WIC, see - /// the documentation for the [IWICBitmapEncoder](https://learn.microsoft.com/en-us/windows/win32/api/wincodec/nn-wincodec-iwicbitmapencoder) + /// the documentation for the [`IWICBitmapEncoder`](https://learn.microsoft.com/en-us/windows/win32/api/wincodec/nn-wincodec-iwicbitmapencoder) /// class. public init(_ clsid: CLSID, encodingQuality: Float = 1.0) { self.init(kind: .systemValue(clsid), encodingQuality: encodingQuality) diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/Attachment+AttachableAsIWICBitmapSource.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/Attachment+AttachableAsIWICBitmapSource.swift index ec0bb016b..8068e7a8e 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/Attachment+AttachableAsIWICBitmapSource.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/Attachment+AttachableAsIWICBitmapSource.swift @@ -26,13 +26,14 @@ extension Attachment { /// This value is used when recording issues associated with the /// attachment. /// - /// The following system-provided image types conform to the - /// ``AttachableAsIWICBitmapSource`` protocol and can be attached to a test: + /// You can attach instances of the following system-provided image types to a + /// test: /// - /// - [`HBITMAP`](https://learn.microsoft.com/en-us/windows/win32/gdi/bitmaps) - /// - [`HICON`](https://learn.microsoft.com/en-us/windows/win32/menurc/icons) - /// - [`IWICBitmapSource`](https://learn.microsoft.com/en-us/windows/win32/api/wincodec/nn-wincodec-iwicbitmapsource) - /// (including its subclasses declared by Windows Imaging Component) + /// | Platform | Supported Types | + /// |-|-| + /// | macOS | [`CGImage`](https://developer.apple.com/documentation/coregraphics/cgimage), [`CIImage`](https://developer.apple.com/documentation/coreimage/ciimage), [`NSImage`](https://developer.apple.com/documentation/appkit/nsimage) | + /// | iOS, watchOS, tvOS, and visionOS | [`CGImage`](https://developer.apple.com/documentation/coregraphics/cgimage), [`CIImage`](https://developer.apple.com/documentation/coreimage/ciimage), [`UIImage`](https://developer.apple.com/documentation/uikit/uiimage) | + /// | Windows | [`HBITMAP`](https://learn.microsoft.com/en-us/windows/win32/gdi/bitmaps), [`HICON`](https://learn.microsoft.com/en-us/windows/win32/menurc/icons), [`IWICBitmapSource`](https://learn.microsoft.com/en-us/windows/win32/api/wincodec/nn-wincodec-iwicbitmapsource) (including its subclasses declared by Windows Imaging Component) | /// /// The testing library uses the image format specified by `imageFormat`. Pass /// `nil` to let the testing library decide which image format to use. If you @@ -46,8 +47,12 @@ extension Attachment { named preferredName: String? = nil, as imageFormat: AttachableImageFormat? = nil, sourceLocation: SourceLocation = #_sourceLocation - ) where AttachableValue == _AttachableImageWrapper { - let imageWrapper = _AttachableImageWrapper(image: image, imageFormat: imageFormat) + ) where T: AttachableAsIWICBitmapSource, AttachableValue == _AttachableImageWrapper { + let imageWrapper = _AttachableImageWrapper( + image: image._copyAttachableValue(), + imageFormat: imageFormat, + deinitializingWith: { $0._deinitializeAttachableValue() } + ) self.init(imageWrapper, named: preferredName, sourceLocation: sourceLocation) } @@ -64,15 +69,14 @@ extension Attachment { /// attachment. /// /// This function creates a new instance of ``Attachment`` wrapping `image` - /// and immediately attaches it to the current test. + /// and immediately attaches it to the current test. You can attach instances + /// of the following system-provided image types to a test: /// - /// The following system-provided image types conform to the - /// ``AttachableAsIWICBitmapSource`` protocol and can be attached to a test: - /// - /// - [`HBITMAP`](https://learn.microsoft.com/en-us/windows/win32/gdi/bitmaps) - /// - [`HICON`](https://learn.microsoft.com/en-us/windows/win32/menurc/icons) - /// - [`IWICBitmapSource`](https://learn.microsoft.com/en-us/windows/win32/api/wincodec/nn-wincodec-iwicbitmapsource) - /// (including its subclasses declared by Windows Imaging Component) + /// | Platform | Supported Types | + /// |-|-| + /// | macOS | [`CGImage`](https://developer.apple.com/documentation/coregraphics/cgimage), [`CIImage`](https://developer.apple.com/documentation/coreimage/ciimage), [`NSImage`](https://developer.apple.com/documentation/appkit/nsimage) | + /// | iOS, watchOS, tvOS, and visionOS | [`CGImage`](https://developer.apple.com/documentation/coregraphics/cgimage), [`CIImage`](https://developer.apple.com/documentation/coreimage/ciimage), [`UIImage`](https://developer.apple.com/documentation/uikit/uiimage) | + /// | Windows | [`HBITMAP`](https://learn.microsoft.com/en-us/windows/win32/gdi/bitmaps), [`HICON`](https://learn.microsoft.com/en-us/windows/win32/menurc/icons), [`IWICBitmapSource`](https://learn.microsoft.com/en-us/windows/win32/api/wincodec/nn-wincodec-iwicbitmapsource) (including its subclasses declared by Windows Imaging Component) | /// /// The testing library uses the image format specified by `imageFormat`. Pass /// `nil` to let the testing library decide which image format to use. If you @@ -86,9 +90,8 @@ extension Attachment { named preferredName: String? = nil, as imageFormat: AttachableImageFormat? = nil, sourceLocation: SourceLocation = #_sourceLocation - ) where AttachableValue == _AttachableImageWrapper { - let imageWrapper = _AttachableImageWrapper(image: image, imageFormat: imageFormat) - let attachment = Self(imageWrapper, named: preferredName, sourceLocation: sourceLocation) + ) where T: AttachableAsIWICBitmapSource, AttachableValue == _AttachableImageWrapper { + let attachment = Self(image, named: preferredName, as: imageFormat, sourceLocation: sourceLocation) Self.record(attachment, sourceLocation: sourceLocation) } } @@ -96,9 +99,8 @@ extension Attachment { @_spi(Experimental) extension Attachment where AttachableValue: AttachableWrapper, AttachableValue.Wrapped: AttachableAsIWICBitmapSource { /// The image format to use when encoding the represented image. - @_disfavoredOverload - public var imageFormat: AttachableImageFormat? { - // FIXME: no way to express `where AttachableValue == _AttachableImageWrapper` on a property + @_disfavoredOverload public var imageFormat: AttachableImageFormat? { + // FIXME: no way to express `where AttachableValue == _AttachableImageWrapper` on a property (see rdar://47559973) (attachableValue as? _AttachableImageWrapper)?.imageFormat } } diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/HBITMAP+AttachableAsIWICBitmapSource.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/HBITMAP+AttachableAsIWICBitmapSource.swift index 0d11fd0bc..4baef8d36 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/HBITMAP+AttachableAsIWICBitmapSource.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/HBITMAP+AttachableAsIWICBitmapSource.swift @@ -9,8 +9,7 @@ // #if os(Windows) -import Testing - +private import Testing public import WinSDK @_spi(Experimental) diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/HICON+AttachableAsIWICBitmapSource.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/HICON+AttachableAsIWICBitmapSource.swift index a25e8f371..36f4fcc9e 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/HICON+AttachableAsIWICBitmapSource.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/HICON+AttachableAsIWICBitmapSource.swift @@ -9,8 +9,7 @@ // #if os(Windows) -import Testing - +private import Testing public import WinSDK @_spi(Experimental) diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/IWICBitmapSource+AttachableAsIWICBitmapSource.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/IWICBitmapSource+AttachableAsIWICBitmapSource.swift index 401e6f480..8ff6d5430 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/IWICBitmapSource+AttachableAsIWICBitmapSource.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/IWICBitmapSource+AttachableAsIWICBitmapSource.swift @@ -9,8 +9,7 @@ // #if os(Windows) -import Testing - +private import Testing public import WinSDK /// - Important: The casts in this file to `IUnknown` are safe insofar as we use diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/UnsafeMutablePointer+AttachableAsIWICBitmapSource.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/UnsafeMutablePointer+AttachableAsIWICBitmapSource.swift index 6d34a6e90..297e1f25a 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/UnsafeMutablePointer+AttachableAsIWICBitmapSource.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/UnsafeMutablePointer+AttachableAsIWICBitmapSource.swift @@ -9,8 +9,7 @@ // #if os(Windows) -import Testing - +private import Testing public import WinSDK @_spi(Experimental) diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/_AttachableImageWrapper.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/_AttachableImageWrapper+AttachableWrapper.swift similarity index 76% rename from Sources/Overlays/_Testing_WinSDK/Attachments/_AttachableImageWrapper.swift rename to Sources/Overlays/_Testing_WinSDK/Attachments/_AttachableImageWrapper+AttachableWrapper.swift index 63931c378..7cfe181b8 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/_AttachableImageWrapper.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/_AttachableImageWrapper+AttachableWrapper.swift @@ -10,47 +10,9 @@ #if os(Windows) @_spi(Experimental) public import Testing +private import WinSDK -internal import WinSDK - -/// A wrapper type for image types such as `HBITMAP` and `HICON` that can be -/// attached indirectly. -/// -/// You do not need to use this type directly. Instead, initialize an instance -/// of ``Attachment`` using an instance of an image type that conforms to -/// ``AttachableAsIWICBitmapSource``. The following system-provided image types -/// conform to the ``AttachableAsIWICBitmapSource`` protocol and can be attached -/// to a test: -/// -/// - [`HBITMAP`](https://learn.microsoft.com/en-us/windows/win32/gdi/bitmaps) -/// - [`HICON`](https://learn.microsoft.com/en-us/windows/win32/menurc/icons) -/// - [`IWICBitmapSource`](https://learn.microsoft.com/en-us/windows/win32/api/wincodec/nn-wincodec-iwicbitmapsource) -/// (including its subclasses declared by Windows Imaging Component) -@_spi(Experimental) -public final class _AttachableImageWrapper: Sendable where Image: AttachableAsIWICBitmapSource { - /// The underlying image. - nonisolated(unsafe) let image: Image - - /// The image format to use when encoding the represented image. - let imageFormat: AttachableImageFormat? - - init(image: borrowing Image, imageFormat: AttachableImageFormat?) { - self.image = image._copyAttachableValue() - self.imageFormat = imageFormat - } - - deinit { - image._deinitializeAttachableValue() - } -} - -// MARK: - - -extension _AttachableImageWrapper: AttachableWrapper { - public var wrappedValue: Image { - image - } - +extension _AttachableImageWrapper: Attachable, AttachableWrapper where Image: AttachableAsIWICBitmapSource { public func withUnsafeBytes(for attachment: borrowing Attachment<_AttachableImageWrapper>, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { // Create an in-memory stream to write the image data to. Note that Windows // documentation recommends SHCreateMemStream() instead, but that function @@ -71,7 +33,7 @@ extension _AttachableImageWrapper: AttachableWrapper { } // Create the bitmap and downcast it to an IWICBitmapSource for later use. - let bitmap = try image._copyAttachableIWICBitmapSource(using: factory) + let bitmap = try wrappedValue._copyAttachableIWICBitmapSource(using: factory) defer { _ = bitmap.pointee.lpVtbl.pointee.Release(bitmap) } diff --git a/Sources/Testing/Attachments/AttachableImageFormat.swift b/Sources/Testing/Attachments/Images/AttachableImageFormat.swift similarity index 100% rename from Sources/Testing/Attachments/AttachableImageFormat.swift rename to Sources/Testing/Attachments/Images/AttachableImageFormat.swift diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/ImageAttachmentError.swift b/Sources/Testing/Attachments/Images/ImageAttachmentError.swift similarity index 54% rename from Sources/Overlays/_Testing_WinSDK/Attachments/ImageAttachmentError.swift rename to Sources/Testing/Attachments/Images/ImageAttachmentError.swift index 1b37df4a9..7cd6f8180 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/ImageAttachmentError.swift +++ b/Sources/Testing/Attachments/Images/ImageAttachmentError.swift @@ -1,38 +1,56 @@ // // This source file is part of the Swift.org open source project // -// Copyright (c) 2025 Apple Inc. and the Swift project authors +// 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 // -#if os(Windows) -@_spi(Experimental) import Testing - -internal import WinSDK +private import _TestingInternals /// A type representing an error that can occur when attaching an image. -enum ImageAttachmentError: Error { +package enum ImageAttachmentError: Error { +#if SWT_TARGET_OS_APPLE + /// The image could not be converted to an instance of `CGImage`. + case couldNotCreateCGImage + + /// The image destination could not be created. + case couldNotCreateImageDestination + + /// The image could not be converted. + case couldNotConvertImage +#elseif os(Windows) /// A call to `QueryInterface()` failed. - case queryInterfaceFailed(Any.Type, HRESULT) + case queryInterfaceFailed(Any.Type, Int32) /// The testing library failed to create a COM object. - case comObjectCreationFailed(Any.Type, HRESULT) + case comObjectCreationFailed(Any.Type, Int32) /// An image could not be written. - case imageWritingFailed(HRESULT) + case imageWritingFailed(Int32) /// The testing library failed to get an in-memory stream's underlying buffer. - case globalFromStreamFailed(HRESULT) + case globalFromStreamFailed(Int32) /// A property could not be written to a property bag. - case propertyBagWritingFailed(String, HRESULT) + case propertyBagWritingFailed(String, Int32) +#endif } extension ImageAttachmentError: CustomStringConvertible { - var description: String { + package var description: String { +#if SWT_TARGET_OS_APPLE + switch self { + case .couldNotCreateCGImage: + "Could not create the corresponding Core Graphics image." + case .couldNotCreateImageDestination: + "Could not create the Core Graphics image destination to encode this image." + case .couldNotConvertImage: + "Could not convert the image to the specified format." + } +#elseif os(Windows) switch self { case let .queryInterfaceFailed(type, result): "Could not cast a COM object to type '\(type)' (HRESULT \(result))." @@ -45,6 +63,8 @@ extension ImageAttachmentError: CustomStringConvertible { case let .propertyBagWritingFailed(name, result): "Could not set the property '\(name)' (HRESULT \(result))." } +#else + swt_unreachable() +#endif } } -#endif diff --git a/Sources/Testing/Attachments/Images/_AttachableImageWrapper.swift b/Sources/Testing/Attachments/Images/_AttachableImageWrapper.swift new file mode 100644 index 000000000..71623538a --- /dev/null +++ b/Sources/Testing/Attachments/Images/_AttachableImageWrapper.swift @@ -0,0 +1,49 @@ +// +// 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 +// + +/// A wrapper type for images that can be indirectly attached to a test. +/// +/// You can attach instances of the following system-provided image types to a +/// test: +/// +/// | Platform | Supported Types | +/// |-|-| +/// | macOS | [`CGImage`](https://developer.apple.com/documentation/coregraphics/cgimage), [`CIImage`](https://developer.apple.com/documentation/coreimage/ciimage), [`NSImage`](https://developer.apple.com/documentation/appkit/nsimage) | +/// | iOS, watchOS, tvOS, and visionOS | [`CGImage`](https://developer.apple.com/documentation/coregraphics/cgimage), [`CIImage`](https://developer.apple.com/documentation/coreimage/ciimage), [`UIImage`](https://developer.apple.com/documentation/uikit/uiimage) | +/// | Windows | [`HBITMAP`](https://learn.microsoft.com/en-us/windows/win32/gdi/bitmaps), [`HICON`](https://learn.microsoft.com/en-us/windows/win32/menurc/icons), [`IWICBitmapSource`](https://learn.microsoft.com/en-us/windows/win32/api/wincodec/nn-wincodec-iwicbitmapsource) (including its subclasses declared by Windows Imaging Component) | +@_spi(Experimental) +@available(_uttypesAPI, *) +public final class _AttachableImageWrapper: Sendable { + /// The underlying image. + private nonisolated(unsafe) let _image: Image + + /// The image format to use when encoding the represented image. + package let imageFormat: AttachableImageFormat? + + /// A deinitializer function to call to clean up `image`. + private let _deinit: @Sendable (consuming Image) -> Void + + package init(image: Image, imageFormat: AttachableImageFormat?, deinitializingWith `deinit`: @escaping @Sendable (consuming Image) -> Void) { + self._image = image + self.imageFormat = imageFormat + self._deinit = `deinit` + } + + deinit { + _deinit(_image) + } +} + +@available(_uttypesAPI, *) +extension _AttachableImageWrapper { + public var wrappedValue: Image { + _image + } +} diff --git a/Sources/Testing/CMakeLists.txt b/Sources/Testing/CMakeLists.txt index 36437e167..531d27cfc 100644 --- a/Sources/Testing/CMakeLists.txt +++ b/Sources/Testing/CMakeLists.txt @@ -21,6 +21,9 @@ add_library(Testing ABI/Encoded/ABI.EncodedIssue.swift ABI/Encoded/ABI.EncodedMessage.swift ABI/Encoded/ABI.EncodedTest.swift + Attachments/Images/_AttachableImageWrapper.swift + Attachments/Images/AttachableImageFormat.swift + Attachments/Images/ImageAttachmentError.swift Attachments/Attachable.swift Attachments/AttachableWrapper.swift Attachments/Attachment.swift From e28b2b00b647138fb449edaad2ef45584eba2caf Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Tue, 19 Aug 2025 10:40:48 -0400 Subject: [PATCH 105/216] Commit to main--missing @_spi attributes on some symbols breaking the build --- .../Attachments/_AttachableImageWrapper+AttachableWrapper.swift | 1 + .../Attachments/_AttachableImageWrapper+AttachableWrapper.swift | 1 + 2 files changed, 2 insertions(+) diff --git a/Sources/Overlays/_Testing_CoreGraphics/Attachments/_AttachableImageWrapper+AttachableWrapper.swift b/Sources/Overlays/_Testing_CoreGraphics/Attachments/_AttachableImageWrapper+AttachableWrapper.swift index bb0d42b23..9976e769b 100644 --- a/Sources/Overlays/_Testing_CoreGraphics/Attachments/_AttachableImageWrapper+AttachableWrapper.swift +++ b/Sources/Overlays/_Testing_CoreGraphics/Attachments/_AttachableImageWrapper+AttachableWrapper.swift @@ -39,6 +39,7 @@ private import UniformTypeIdentifiers /// useful.) @available(_uttypesAPI, *) +@_spi(Experimental) extension _AttachableImageWrapper: Attachable, AttachableWrapper where Image: AttachableAsCGImage { public func withUnsafeBytes(for attachment: borrowing Attachment<_AttachableImageWrapper>, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { let data = NSMutableData() diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/_AttachableImageWrapper+AttachableWrapper.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/_AttachableImageWrapper+AttachableWrapper.swift index 7cfe181b8..dd5fc1da3 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/_AttachableImageWrapper+AttachableWrapper.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/_AttachableImageWrapper+AttachableWrapper.swift @@ -12,6 +12,7 @@ @_spi(Experimental) public import Testing private import WinSDK +@_spi(Experimental) extension _AttachableImageWrapper: Attachable, AttachableWrapper where Image: AttachableAsIWICBitmapSource { public func withUnsafeBytes(for attachment: borrowing Attachment<_AttachableImageWrapper>, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { // Create an in-memory stream to write the image data to. Note that Windows From bf030fda56e13cfd11a27f838d8b9582c243d997 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Tue, 19 Aug 2025 17:20:42 -0400 Subject: [PATCH 106/216] Update README.md to point to 6.2 jobs. (#1275) This PR updates our README.md to point to 6.2 as we are no longer targetting 6.1 on our main branch. release/6.2 continues to build for Swift 6.1. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 062ef843d..77be2953e 100644 --- a/README.md +++ b/README.md @@ -103,17 +103,17 @@ Swift. The table below describes the current level of support that Swift Testing has for various platforms: -| **Platform** | **CI Status (6.1)** | **CI Status (main)** | **Support Status** | +| **Platform** | **CI Status (6.2)** | **CI Status (main)** | **Support Status** | |---|:-:|:-:|---| -| **macOS** | [![Build Status](https://ci.swift.org/buildStatus/icon?job=swift-testing-main-swift-6.1-macos)](https://ci.swift.org/job/swift-testing-main-swift-6.1-macos/) | [![Build Status](https://ci.swift.org/buildStatus/icon?job=swift-testing-main-swift-main-macos)](https://ci.swift.org/view/Swift%20Packages/job/swift-testing-main-swift-main-macos/) | Supported | +| **macOS** | [![Build Status](https://ci.swift.org/buildStatus/icon?job=swift-testing-main-swift-6.2-macos)](https://ci.swift.org/job/swift-testing-main-swift-6.2-macos/) | [![Build Status](https://ci.swift.org/buildStatus/icon?job=swift-testing-main-swift-main-macos)](https://ci.swift.org/view/Swift%20Packages/job/swift-testing-main-swift-main-macos/) | Supported | | **iOS** | | | Supported | | **watchOS** | | | Supported | | **tvOS** | | | Supported | | **visionOS** | | | Supported | -| **Ubuntu 22.04** | [![Build Status](https://ci.swift.org/buildStatus/icon?job=swift-testing-main-swift-6.1-linux)](https://ci.swift.org/job/swift-testing-main-swift-6.1-linux/) | [![Build Status](https://ci.swift.org/buildStatus/icon?job=swift-testing-main-swift-main-linux)](https://ci.swift.org/view/Swift%20Packages/job/swift-testing-main-swift-main-linux/) | Supported | +| **Ubuntu 22.04** | [![Build Status](https://ci.swift.org/buildStatus/icon?job=swift-testing-main-swift-6.2-linux)](https://ci.swift.org/job/swift-testing-main-swift-6.2-linux/) | [![Build Status](https://ci.swift.org/buildStatus/icon?job=swift-testing-main-swift-main-linux)](https://ci.swift.org/view/Swift%20Packages/job/swift-testing-main-swift-main-linux/) | Supported | | **FreeBSD** | | | Experimental | | **OpenBSD** | | | Experimental | -| **Windows** | [![Build Status](https://ci-external.swift.org/buildStatus/icon?job=swift-testing-main-swift-6.1-windows)](https://ci-external.swift.org/view/all/job/swift-testing-main-swift-6.1-windows/) | [![Build Status](https://ci-external.swift.org/buildStatus/icon?job=swift-testing-main-swift-main-windows)](https://ci-external.swift.org/job/swift-testing-main-swift-main-windows/) | Supported | +| **Windows** | [![Build Status](https://ci-external.swift.org/buildStatus/icon?job=swift-testing-main-swift-6.2-windows)](https://ci-external.swift.org/view/all/job/swift-testing-main-swift-6.2-windows/) | [![Build Status](https://ci-external.swift.org/buildStatus/icon?job=swift-testing-main-swift-main-windows)](https://ci-external.swift.org/job/swift-testing-main-swift-main-windows/) | Supported | | **Wasm** | | | Experimental | ### Works with XCTest From 887871ba22f26dc87e49174d7cdbabdae7d2b796 Mon Sep 17 00:00:00 2001 From: Kelvin Bui <150134371+tienquocbui@users.noreply.github.com> Date: Fri, 22 Aug 2025 09:56:19 +0700 Subject: [PATCH 107/216] Refactor AdvancedConsoleOutputRecorder to use ABI.EncodedEvent architecture (#1281) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### Key changes implemented: * Event → ABI.EncodedEvent conversion: Using ABI.EncodedEvent(encoding: event, in: eventContext, messages: messages) for forward compatibility * Extensible context storage system: Added Locked struct containing test storage dictionary, designed for easy addition of result data and other event information in future PRs * Test discovery handling: Populates context storage during testDiscovered events for later lookup by test ID string ABI event processing: Added _processABIEvent() method with placeholder switch statement ready for failure summary implementation * Still delegates to fallback recorder to maintain existing functionality while demonstrating ABI conversion flow * Uses Event.HumanReadableOutputRecorder to generate messages that are passed to ABI encoding ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- .../Testing/ABI/EntryPoints/EntryPoint.swift | 4 +- .../Event.AdvancedConsoleOutputRecorder.swift | 79 ++++++++++++++++++- 2 files changed, 77 insertions(+), 6 deletions(-) diff --git a/Sources/Testing/ABI/EntryPoints/EntryPoint.swift b/Sources/Testing/ABI/EntryPoints/EntryPoint.swift index 4fe85ea73..a2e7dddbe 100644 --- a/Sources/Testing/ABI/EntryPoints/EntryPoint.swift +++ b/Sources/Testing/ABI/EntryPoints/EntryPoint.swift @@ -57,10 +57,10 @@ func entryPoint(passing args: __CommandLineArguments_v0?, eventHandler: Event.Ha // Check for experimental console output flag if Environment.flag(named: "SWT_ENABLE_EXPERIMENTAL_CONSOLE_OUTPUT") == true { // Use experimental AdvancedConsoleOutputRecorder - var advancedOptions = Event.AdvancedConsoleOutputRecorder.Options() + var advancedOptions = Event.AdvancedConsoleOutputRecorder.Options() advancedOptions.base = .for(.stderr) - let eventRecorder = Event.AdvancedConsoleOutputRecorder(options: advancedOptions) { string in + let eventRecorder = Event.AdvancedConsoleOutputRecorder(options: advancedOptions) { string in try? FileHandle.stderr.write(string) } diff --git a/Sources/Testing/Events/Recorder/Event.AdvancedConsoleOutputRecorder.swift b/Sources/Testing/Events/Recorder/Event.AdvancedConsoleOutputRecorder.swift index 784929993..2f537cb3d 100644 --- a/Sources/Testing/Events/Recorder/Event.AdvancedConsoleOutputRecorder.swift +++ b/Sources/Testing/Events/Recorder/Event.AdvancedConsoleOutputRecorder.swift @@ -14,7 +14,7 @@ extension Event { /// /// This recorder is currently experimental and must be enabled via the /// `SWT_ENABLE_EXPERIMENTAL_CONSOLE_OUTPUT` environment variable. - struct AdvancedConsoleOutputRecorder: Sendable { + struct AdvancedConsoleOutputRecorder: Sendable { /// Configuration options for the advanced console output recorder. struct Options: Sendable { /// Base console output recorder options to inherit from. @@ -25,6 +25,15 @@ extension Event { } } + /// Context for storing data across events during test execution. + private struct _Context: Sendable { + /// Storage for test information, keyed by test ID string value. + /// This is needed because ABI.EncodedEvent doesn't contain full test context. + var testStorage: [String: ABI.EncodedTest] = [:] + + // Future storage for result data and other event information can be added here + } + /// The options for this recorder. let options: Options @@ -34,6 +43,12 @@ extension Event { /// The fallback console recorder for standard output. private let _fallbackRecorder: Event.ConsoleOutputRecorder + /// Context storage for test information and results. + private let _context: Locked<_Context> + + /// Human-readable output recorder for generating messages. + private let _humanReadableRecorder: Event.HumanReadableOutputRecorder + /// Initialize the advanced console output recorder. /// /// - Parameters: @@ -43,6 +58,8 @@ extension Event { self.options = options self.write = write self._fallbackRecorder = Event.ConsoleOutputRecorder(options: options.base, writingUsing: write) + self._context = Locked(rawValue: _Context()) + self._humanReadableRecorder = Event.HumanReadableOutputRecorder() } } } @@ -50,14 +67,68 @@ extension Event { extension Event.AdvancedConsoleOutputRecorder { /// Record an event by processing it and generating appropriate output. /// - /// Currently this is a skeleton implementation that delegates to - /// ``Event/ConsoleOutputRecorder``. + /// This implementation converts the Event to ABI.EncodedEvent for internal processing, + /// following the ABI-based architecture for future separation into a harness process. /// /// - Parameters: /// - event: The event to record. /// - eventContext: The context associated with the event. func record(_ event: borrowing Event, in eventContext: borrowing Event.Context) { - // Skeleton implementation: delegate to ConsoleOutputRecorder + // Handle test discovery to populate our test storage + if case .testDiscovered = event.kind, let test = eventContext.test { + let encodedTest = ABI.EncodedTest(encoding: test) + _context.withLock { context in + context.testStorage[encodedTest.id.stringValue] = encodedTest + } + } + + // Generate human-readable messages for the event + let messages = _humanReadableRecorder.record(event, in: eventContext) + + // Convert Event to ABI.EncodedEvent + if let encodedEvent = ABI.EncodedEvent(encoding: event, in: eventContext, messages: messages) { + // Process the ABI event + _processABIEvent(encodedEvent) + } + + // For now, still delegate to the fallback recorder to maintain existing functionality _fallbackRecorder.record(event, in: eventContext) } + + /// Process an ABI.EncodedEvent for advanced console output. + /// + /// This is where the enhanced console logic will be implemented in future PRs. + /// Currently this is a placeholder that demonstrates the ABI conversion. + /// + /// - Parameters: + /// - encodedEvent: The ABI-encoded event to process. + private func _processABIEvent(_ encodedEvent: ABI.EncodedEvent) { + // TODO: Implement enhanced console output logic here + // This will be expanded in subsequent PRs for: + // - Failure summary display + // - Progress bar functionality + // - Hierarchical test result display + + // For now, we just demonstrate that we can access the ABI event data + switch encodedEvent.kind { + case .runStarted: + // Could implement run start logic here + break + case .testStarted: + // Could implement test start logic here + break + case .issueRecorded: + // Could implement issue recording logic here + break + case .testEnded: + // Could implement test end logic here + break + case .runEnded: + // Could implement run end logic here + break + default: + // Handle other event types + break + } + } } From cff2b1120c958121979a9954b918d393396ed3ce Mon Sep 17 00:00:00 2001 From: 3405691582 Date: Thu, 21 Aug 2025 23:02:57 -0400 Subject: [PATCH 108/216] Tests link with libutil for openpty on OpenBSD. (#1285) One of the tests refers to openpty, which requires linking with libutil on OpenBSD. ### Motivation: swift test on the project has a link failure on OpenBSD. ### Modifications: Add the libutil linker flag in Package.swift ### Checklist: - [X] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [X] If public symbols are renamed or modified, DocC references should be updated. ***(not required).*** --- Package.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index 80db6076c..a1e16b1e8 100644 --- a/Package.swift +++ b/Package.swift @@ -139,7 +139,10 @@ let package = Package( "_Testing_WinSDK", "MemorySafeTestingTests", ], - swiftSettings: .packageSettings + swiftSettings: .packageSettings, + linkerSettings: [ + .linkedLibrary("util", .when(platforms: [.openbsd])) + ] ), // Use a plain `.target` instead of a `.testTarget` to avoid the unnecessary From fc6556a642fe938086177d400fad4b8a60ecf951 Mon Sep 17 00:00:00 2001 From: Jerry Chen Date: Thu, 21 Aug 2025 20:35:22 -0700 Subject: [PATCH 109/216] Include severity and isFailure for Issues in the published JSON event stream (#1279) ### Motivation: - Proposed and accepted in ST-0013 [^1], so we can move ahead and include this in the event stream. - This will _not_ be available in event stream version 0 as this is considered a breaking change [^2] ### Modifications: - Promote existing _severity/_isFailure fields -> severity/isFailure without the underscores This effectively makes them part of the official JSON ABI. The underscored names will no longer be accessible, but they were always experimental so this is not considered a blocker. - Only emit severity/isFailure when using event stream version >= 6.3 This removes access to the underscored names from earlier versions, but again, these were experimental. - Remove hidden `__CommandLineArguments_v0` argument `isWarningIssueRecordedEventEnabled` This was only for internal testing use, but is obsolete because we test issue severity by specifying event stream version >= 6.3. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. [^1]: https://github.com/swiftlang/swift-evolution/blob/main/proposals/testing/0013-issue-severity-warning.md [^2]: https://github.com/swiftlang/swift-evolution/blob/main/proposals/testing/0013-issue-severity-warning.md#integration-with-supporting-tools Fixes rdar://158621033 --- .../ABI/Encoded/ABI.EncodedIssue.swift | 35 ++++++++---- .../Testing/ABI/EntryPoints/EntryPoint.swift | 33 ++++-------- Sources/Testing/ExitTests/ExitTest.swift | 5 +- Tests/TestingTests/EntryPointTests.swift | 11 ++-- Tests/TestingTests/SwiftPMTests.swift | 54 ++++++++++++++++--- 5 files changed, 92 insertions(+), 46 deletions(-) diff --git a/Sources/Testing/ABI/Encoded/ABI.EncodedIssue.swift b/Sources/Testing/ABI/Encoded/ABI.EncodedIssue.swift index 465be7aee..c1e3c12fd 100644 --- a/Sources/Testing/ABI/Encoded/ABI.EncodedIssue.swift +++ b/Sources/Testing/ABI/Encoded/ABI.EncodedIssue.swift @@ -26,13 +26,21 @@ extension ABI { /// The severity of this issue. /// - /// - Warning: Severity is not yet part of the JSON schema. - var _severity: Severity - + /// Prior to 6.3, this is nil. + /// + /// @Metadata { + /// @Available(Swift, introduced: 6.3) + /// } + var severity: Severity? + /// If the issue is a failing issue. /// - /// - Warning: Non-failing issues are not yet part of the JSON schema. - var _isFailure: Bool + /// Prior to 6.3, this is nil. + /// + /// @Metadata { + /// @Available(Swift, introduced: 6.3) + /// } + var isFailure: Bool? /// Whether or not this issue is known to occur. var isKnown: Bool @@ -51,13 +59,20 @@ extension ABI { var _error: EncodedError? init(encoding issue: borrowing Issue, in eventContext: borrowing Event.Context) { - _severity = switch issue.severity { - case .warning: .warning - case .error: .error - } - _isFailure = issue.isFailure + // >= v0 isKnown = issue.isKnown sourceLocation = issue.sourceLocation + + // >= v6.3 + if V.versionNumber >= ABI.v6_3.versionNumber { + severity = switch issue.severity { + case .warning: .warning + case .error: .error + } + isFailure = issue.isFailure + } + + // Experimental if let backtrace = issue.sourceContext.backtrace { _backtrace = EncodedBacktrace(encoding: backtrace, in: eventContext) } diff --git a/Sources/Testing/ABI/EntryPoints/EntryPoint.swift b/Sources/Testing/ABI/EntryPoints/EntryPoint.swift index a2e7dddbe..a97d33c9e 100644 --- a/Sources/Testing/ABI/EntryPoints/EntryPoint.swift +++ b/Sources/Testing/ABI/EntryPoints/EntryPoint.swift @@ -59,11 +59,11 @@ func entryPoint(passing args: __CommandLineArguments_v0?, eventHandler: Event.Ha // Use experimental AdvancedConsoleOutputRecorder var advancedOptions = Event.AdvancedConsoleOutputRecorder.Options() advancedOptions.base = .for(.stderr) - + let eventRecorder = Event.AdvancedConsoleOutputRecorder(options: advancedOptions) { string in try? FileHandle.stderr.write(string) } - + configuration.eventHandler = { [oldEventHandler = configuration.eventHandler] event, context in eventRecorder.record(event, in: context) oldEventHandler(event, context) @@ -328,13 +328,6 @@ public struct __CommandLineArguments_v0: Sendable { /// The value of the `--attachments-path` argument. public var attachmentsPath: String? - - /// Whether or not the experimental warning issue severity feature should be - /// enabled. - /// - /// This property is intended for use in testing the testing library itself. - /// It is not parsed as a command-line argument. - var isWarningIssueRecordedEventEnabled: Bool? } extension __CommandLineArguments_v0: Codable { @@ -635,19 +628,15 @@ public func configurationForEntryPoint(from args: __CommandLineArguments_v0) thr #endif // Warning issues (experimental). - if args.isWarningIssueRecordedEventEnabled == true { - configuration.eventHandlingOptions.isWarningIssueRecordedEventEnabled = true - } else { - switch args.eventStreamVersionNumber { - case .some(..= 6.3 arguments.eventStreamSchemaVersion = "0" arguments.verbosity = .min @@ -50,13 +51,13 @@ struct EntryPointTests { #expect(exitCode == EXIT_SUCCESS) } - @Test("Entry point with WarningIssues feature enabled propagates warning issues and exits with success if all issues have severity < .error") + + @Test("Entry point using event stream version 6.3 propagates warning issues and exits with success if all issues have severity < .error") func warningIssuesEnabled() async throws { var arguments = __CommandLineArguments_v0() arguments.filter = ["_recordWarningIssue"] arguments.includeHiddenTests = true - arguments.eventStreamSchemaVersion = "0" - arguments.isWarningIssueRecordedEventEnabled = true + arguments.eventStreamSchemaVersion = "6.3" arguments.verbosity = .min let exitCode = await confirmation("Warning issue recorded", expectedCount: 1) { issueRecorded in diff --git a/Tests/TestingTests/SwiftPMTests.swift b/Tests/TestingTests/SwiftPMTests.swift index 3cca1ad55..672e34a03 100644 --- a/Tests/TestingTests/SwiftPMTests.swift +++ b/Tests/TestingTests/SwiftPMTests.swift @@ -20,6 +20,18 @@ private func configurationForEntryPoint(withArguments args: [String]) throws -> return try configurationForEntryPoint(from: args) } +/// Reads event stream output from the provided file matching event stream +/// version `V`. +private func decodedEventStreamRecords(fromPath filePath: String) throws -> [ABI.Record] { + try FileHandle(forReadingAtPath: filePath).readToEnd() + .split(whereSeparator: \.isASCIINewline) + .map { line in + try line.withUnsafeBytes { line in + return try JSON.decode(ABI.Record.self, from: line) + } + } +} + @Suite("Swift Package Manager Integration Tests") struct SwiftPMTests { @Test("Command line arguments are available") @@ -286,6 +298,40 @@ struct SwiftPMTests { #expect(versionTypeInfo == nil) } + @Test("Severity and isFailure fields included in version 6.3") + func validateEventStreamContents() async throws { + let tempDirPath = try temporaryDirectory() + let temporaryFilePath = appendPathComponent("\(UInt64.random(in: 0 ..< .max))", to: tempDirPath) + defer { + _ = remove(temporaryFilePath) + } + + do { + let test = Test { + Issue.record("Test warning", severity: .warning) + } + + let configuration = try configurationForEntryPoint(withArguments: + ["PATH", "--event-stream-output-path", temporaryFilePath, "--experimental-event-stream-version", "6.3"] + ) + + await test.run(configuration: configuration) + } + + let issueEventRecords = try decodedEventStreamRecords(fromPath: temporaryFilePath) + .compactMap { (record: ABI.Record) in + if case let .event(event) = record.kind, event.kind == .issueRecorded { + return event + } + return nil + } + + let issue = try #require(issueEventRecords.first?.issue) + #expect(issueEventRecords.count == 1) + #expect(issue.isFailure == false) + #expect(issue.severity == .warning) + } + @Test("--event-stream-output-path argument (writes to a stream and can be read back)", arguments: [ ("--event-stream-output-path", "--event-stream-version", ABI.v0.versionNumber), @@ -320,13 +366,7 @@ struct SwiftPMTests { configuration.handleEvent(Event(.runEnded, testID: nil, testCaseID: nil), in: eventContext) } - let decodedRecords = try FileHandle(forReadingAtPath: temporaryFilePath).readToEnd() - .split(whereSeparator: \.isASCIINewline) - .map { line in - try line.withUnsafeBytes { line in - try JSON.decode(ABI.Record.self, from: line) - } - } + let decodedRecords: [ABI.Record] = try decodedEventStreamRecords(fromPath: temporaryFilePath) let testRecords = decodedRecords.compactMap { record in if case let .test(test) = record.kind { From 7f0110d844c51c14542672ae2deab92d819f90fa Mon Sep 17 00:00:00 2001 From: Stuart Montgomery Date: Fri, 22 Aug 2025 15:40:33 -0500 Subject: [PATCH 110/216] Update swift-syntax package dependency to 603 (#1286) This updates our package dependency on swift-syntax to `603.0.0-latest` (replacing `602-0.0-latest`). I also took the opportunity to document the workaround we use to ensure we're always using the latest "prerelease" tag. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- Package.swift | 10 +++++++++- Sources/TestingMacros/CMakeLists.txt | 2 +- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/Package.swift b/Package.swift index a1e16b1e8..f9f302e08 100644 --- a/Package.swift +++ b/Package.swift @@ -109,7 +109,15 @@ let package = Package( }(), dependencies: [ - .package(url: "https://github.com/swiftlang/swift-syntax.git", from: "602.0.0-latest"), + // swift-syntax periodically publishes a new tag with a suffix of the format + // "-prerelease-YYYY-MM-DD". We always want to use the most recent tag + // associated with a particular Swift version, without needing to hardcode + // an exact tag and manually keep it up-to-date. Specifying the suffix + // "-latest" on this dependency is a workaround which causes Swift package + // manager to use the lexicographically highest-sorted tag with the + // specified semantic version, meaning the most recent "prerelease" tag will + // always be used. + .package(url: "https://github.com/swiftlang/swift-syntax.git", from: "603.0.0-latest"), ], targets: [ diff --git a/Sources/TestingMacros/CMakeLists.txt b/Sources/TestingMacros/CMakeLists.txt index effa782a9..46be5e388 100644 --- a/Sources/TestingMacros/CMakeLists.txt +++ b/Sources/TestingMacros/CMakeLists.txt @@ -31,7 +31,7 @@ if(SwiftTesting_BuildMacrosAsExecutables) set(FETCHCONTENT_BASE_DIR ${CMAKE_BINARY_DIR}/_d) FetchContent_Declare(SwiftSyntax GIT_REPOSITORY https://github.com/swiftlang/swift-syntax - GIT_TAG 340f8400262d494c7c659cd838223990195d7fed) # 602.0.0-prerelease-2025-04-10 + GIT_TAG 07bf225e198119c23b2b9a0a3432bdb534498873) # 603.0.0-prerelease-2025-08-11 FetchContent_MakeAvailable(SwiftSyntax) endif() From a8a3fcb0336919454871c8aa838b9f579aa68514 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Tue, 26 Aug 2025 09:27:56 -0400 Subject: [PATCH 111/216] Ensure attachments created in exit tests are forwarded to the parent. (#1282) This PR ensures that when an exit test records an attachment, it is forwarded to the parent process. It introduces a local/private dependency on `Data` in `EncodedAttachment` so that we can use its base64 encoding/decoding and its ability to map files from disk. If Foundation is not available, it falls back to encoding/decoding `[UInt8]` and reading files into memory with `fread()` (the old-fashioned way). Resolves rdar://149242118. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- .../ABI/Encoded/ABI.EncodedAttachment.swift | 123 +++++++++++++++++- Sources/Testing/ExitTests/ExitTest.swift | 13 +- Tests/TestingTests/ExitTestTests.swift | 27 ++++ 3 files changed, 159 insertions(+), 4 deletions(-) diff --git a/Sources/Testing/ABI/Encoded/ABI.EncodedAttachment.swift b/Sources/Testing/ABI/Encoded/ABI.EncodedAttachment.swift index 7668f778a..9275da1ed 100644 --- a/Sources/Testing/ABI/Encoded/ABI.EncodedAttachment.swift +++ b/Sources/Testing/ABI/Encoded/ABI.EncodedAttachment.swift @@ -8,6 +8,10 @@ // See https://swift.org/CONTRIBUTORS.txt for Swift project authors // +#if canImport(Foundation) +private import Foundation +#endif + extension ABI { /// A type implementing the JSON encoding of ``Attachment`` for the ABI entry /// point and event stream output. @@ -15,14 +19,50 @@ extension ABI { /// 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: Attachments are not yet part of the JSON schema. struct EncodedAttachment: Sendable where V: ABI.Version { /// The path where the attachment was written. var path: String? + /// The preferred name of the attachment. + /// + /// - Warning: Attachments' preferred names are not yet part of the JSON + /// schema. + var _preferredName: String? + + /// The raw content of the attachment, if available. + /// + /// The value of this property is set if the attachment was not first saved + /// to a file. It may also be `nil` if an error occurred while trying to get + /// the original attachment's serialized representation. + /// + /// - Warning: Inline attachment content is not yet part of the JSON schema. + var _bytes: Bytes? + + /// The source location where this attachment was created. + /// + /// - Warning: Attachment source locations are not yet part of the JSON + /// schema. + var _sourceLocation: SourceLocation? + init(encoding attachment: borrowing Attachment, in eventContext: borrowing Event.Context) { path = attachment.fileSystemPath + + if V.versionNumber >= ABI.v6_3.versionNumber { + _preferredName = attachment.preferredName + + if path == nil { + _bytes = try? attachment.withUnsafeBytes { bytes in + return Bytes(rawValue: [UInt8](bytes)) + } + } + + _sourceLocation = attachment.sourceLocation + } + } + + /// A structure representing the bytes of an attachment. + struct Bytes: Sendable, RawRepresentable { + var rawValue: [UInt8] } } } @@ -30,3 +70,82 @@ extension ABI { // MARK: - Codable extension ABI.EncodedAttachment: Codable {} + +extension ABI.EncodedAttachment.Bytes: Codable { + func encode(to encoder: any Encoder) throws { +#if canImport(Foundation) + // If possible, encode this structure as Base64 data. + try rawValue.withUnsafeBytes { rawValue in + let data = Data(bytesNoCopy: .init(mutating: rawValue.baseAddress!), count: rawValue.count, deallocator: .none) + var container = encoder.singleValueContainer() + try container.encode(data) + } +#else + // Otherwise, it's an array of integers. + var container = encoder.singleValueContainer() + try container.encode(rawValue) +#endif + } + + init(from decoder: any Decoder) throws { + let container = try decoder.singleValueContainer() + +#if canImport(Foundation) + // If possible, decode a whole Foundation Data object. + if let data = try? container.decode(Data.self) { + self.init(rawValue: [UInt8](data)) + return + } +#endif + + // Fall back to trying to decode an array of integers. + let bytes = try container.decode([UInt8].self) + self.init(rawValue: bytes) + } +} + +// MARK: - Attachable + +extension ABI.EncodedAttachment: Attachable { + var estimatedAttachmentByteCount: Int? { + _bytes?.rawValue.count + } + + /// An error type that is thrown when ``ABI/EncodedAttachment`` cannot satisfy + /// a request for the underlying attachment's bytes. + fileprivate struct BytesUnavailableError: Error {} + + borrowing func withUnsafeBytes(for attachment: borrowing Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { + if let bytes = _bytes?.rawValue { + return try bytes.withUnsafeBytes(body) + } + +#if !SWT_NO_FILE_IO + guard let path else { + throw BytesUnavailableError() + } +#if canImport(Foundation) + // Leverage Foundation's file-mapping logic since we're using Data anyway. + let url = URL(fileURLWithPath: path, isDirectory: false) + let bytes = try Data(contentsOf: url, options: [.mappedIfSafe]) +#else + let fileHandle = try FileHandle(forReadingAtPath: path) + let bytes = try fileHandle.readToEnd() +#endif + return try bytes.withUnsafeBytes(body) +#else + // Cannot read the attachment from disk on this platform. + throw BytesUnavailableError() +#endif + } + + borrowing func preferredName(for attachment: borrowing Attachment, basedOn suggestedName: String) -> String { + _preferredName ?? suggestedName + } +} + +extension ABI.EncodedAttachment.BytesUnavailableError: CustomStringConvertible { + var description: String { + "The attachment's content could not be deserialized." + } +} diff --git a/Sources/Testing/ExitTests/ExitTest.swift b/Sources/Testing/ExitTests/ExitTest.swift index 904d3a40a..5eb006b03 100644 --- a/Sources/Testing/ExitTests/ExitTest.swift +++ b/Sources/Testing/ExitTests/ExitTest.swift @@ -767,8 +767,12 @@ extension ExitTest { } } configuration.eventHandler = { event, eventContext in - if case .issueRecorded = event.kind { + switch event.kind { + case .issueRecorded, .valueAttached: eventHandler(event, eventContext) + default: + // Don't forward other kinds of event. + break } } @@ -1034,8 +1038,11 @@ extension ExitTest { /// - Throws: Any error encountered attempting to decode or process the JSON. private static func _processRecord(_ recordJSON: UnsafeRawBufferPointer, fromBackChannel backChannel: borrowing FileHandle) throws { let record = try JSON.decode(ABI.Record.self, from: recordJSON) + guard case let .event(event) = record.kind else { + return + } - if case let .event(event) = record.kind, let issue = event.issue { + if let issue = event.issue { // Translate the issue back into a "real" issue and record it // in the parent process. This translation is, of course, lossy // due to the process boundary, but we make a best effort. @@ -1064,6 +1071,8 @@ extension ExitTest { issueCopy.knownIssueContext = Issue.KnownIssueContext() } issueCopy.record() + } else if let attachment = event.attachment { + Attachment.record(attachment, sourceLocation: attachment._sourceLocation!) } } diff --git a/Tests/TestingTests/ExitTestTests.swift b/Tests/TestingTests/ExitTestTests.swift index d86489e13..e68e26c60 100644 --- a/Tests/TestingTests/ExitTestTests.swift +++ b/Tests/TestingTests/ExitTestTests.swift @@ -198,6 +198,33 @@ private import _TestingInternals } } + private static let attachmentPayload = [UInt8](0...255) + + @Test("Exit test forwards attachments") func forwardsAttachments() async { + await confirmation("Value attached") { valueAttached in + var configuration = Configuration() + configuration.eventHandler = { event, _ in + guard case let .valueAttached(attachment) = event.kind else { + return + } + #expect(throws: Never.self) { + try attachment.withUnsafeBytes { bytes in + #expect(Array(bytes) == Self.attachmentPayload) + } + } + #expect(attachment.preferredName == "my attachment.bytes") + valueAttached() + } + configuration.exitTestHandler = ExitTest.handlerForEntryPoint() + + await Test { + await #expect(processExitsWith: .success) { + Attachment.record(Self.attachmentPayload, named: "my attachment.bytes") + } + }.run(configuration: configuration) + } + } + #if !os(Linux) @Test("Exit test reports > 8 bits of the exit code") func fullWidthExitCode() async { From 78f6ced4e218bfb993e0e98ba0ca35ca2db64e13 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Wed, 27 Aug 2025 14:48:43 -0400 Subject: [PATCH 112/216] Promote Darwin image attachments to API. (#1274) This PR promotes image attachments on Apple platforms to API (pending approval of ST-0014). ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- Sources/Overlays/CMakeLists.txt | 6 ++++- .../NSImage+AttachableAsCGImage.swift | 11 +++++--- .../Overlays/_Testing_AppKit/CMakeLists.txt | 23 ++++++++++++++++ .../_Testing_AppKit/ReexportTesting.swift | 6 ++--- .../Attachments/AttachableAsCGImage.swift | 21 ++++++++++----- .../AttachableImageFormat+UTType.swift | 10 ++++++- .../Attachment+AttachableAsCGImage.swift | 19 +++++++++++--- .../CGImage+AttachableAsCGImage.swift | 9 +++++-- ...chableImageWrapper+AttachableWrapper.swift | 5 ++-- .../_Testing_CoreGraphics/CMakeLists.txt | 26 +++++++++++++++++++ .../ReexportTesting.swift | 4 +-- .../CIImage+AttachableAsCGImage.swift | 11 +++++--- .../_Testing_CoreImage/CMakeLists.txt | 23 ++++++++++++++++ .../_Testing_CoreImage/ReexportTesting.swift | 6 ++--- .../UIImage+AttachableAsCGImage.swift | 12 ++++++--- .../Overlays/_Testing_UIKit/CMakeLists.txt | 23 ++++++++++++++++ .../_Testing_UIKit/ReexportTesting.swift | 6 ++--- .../Images/AttachableImageFormat.swift | 25 ++++++++++++++++-- .../Images/ImageAttachmentError.swift | 2 +- .../Images/_AttachableImageWrapper.swift | 3 ++- Sources/Testing/Testing.docc/Attachments.md | 14 ++++++++-- .../AppKit.swiftoverlay | 3 +++ .../CoreGraphics.swiftoverlay | 3 +++ .../CoreImage.swiftoverlay | 3 +++ .../UIKit.swiftoverlay | 3 +++ Tests/TestingTests/AttachmentTests.swift | 8 +++--- 26 files changed, 237 insertions(+), 48 deletions(-) create mode 100644 Sources/Overlays/_Testing_AppKit/CMakeLists.txt create mode 100644 Sources/Overlays/_Testing_CoreGraphics/CMakeLists.txt create mode 100644 Sources/Overlays/_Testing_CoreImage/CMakeLists.txt create mode 100644 Sources/Overlays/_Testing_UIKit/CMakeLists.txt create mode 100644 Sources/Testing/Testing.swiftcrossimport/AppKit.swiftoverlay create mode 100644 Sources/Testing/Testing.swiftcrossimport/CoreGraphics.swiftoverlay create mode 100644 Sources/Testing/Testing.swiftcrossimport/CoreImage.swiftoverlay create mode 100644 Sources/Testing/Testing.swiftcrossimport/UIKit.swiftoverlay diff --git a/Sources/Overlays/CMakeLists.txt b/Sources/Overlays/CMakeLists.txt index 2120d680f..5629e8229 100644 --- a/Sources/Overlays/CMakeLists.txt +++ b/Sources/Overlays/CMakeLists.txt @@ -1,9 +1,13 @@ # This source file is part of the Swift.org open source project # -# Copyright (c) 2024 Apple Inc. and the Swift project authors +# Copyright (c) 2024–2025 Apple Inc. and the Swift project authors # Licensed under Apache License v2.0 with Runtime Library Exception # # See http://swift.org/LICENSE.txt for license information # See http://swift.org/CONTRIBUTORS.txt for Swift project authors +add_subdirectory(_Testing_AppKit) +add_subdirectory(_Testing_CoreGraphics) +add_subdirectory(_Testing_CoreImage) add_subdirectory(_Testing_Foundation) +add_subdirectory(_Testing_UIKit) diff --git a/Sources/Overlays/_Testing_AppKit/Attachments/NSImage+AttachableAsCGImage.swift b/Sources/Overlays/_Testing_AppKit/Attachments/NSImage+AttachableAsCGImage.swift index d62ef71cc..ef601f46f 100644 --- a/Sources/Overlays/_Testing_AppKit/Attachments/NSImage+AttachableAsCGImage.swift +++ b/Sources/Overlays/_Testing_AppKit/Attachments/NSImage+AttachableAsCGImage.swift @@ -1,7 +1,7 @@ // // This source file is part of the Swift.org open source project // -// Copyright (c) 2024 Apple Inc. and the Swift project authors +// Copyright (c) 2024–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 @@ -10,7 +10,7 @@ #if SWT_TARGET_OS_APPLE && canImport(AppKit) public import AppKit -@_spi(Experimental) public import _Testing_CoreGraphics +public import _Testing_CoreGraphics extension NSImageRep { /// AppKit's bundle. @@ -33,8 +33,13 @@ extension NSImageRep { // MARK: - -@_spi(Experimental) +/// @Metadata { +/// @Available(Swift, introduced: 6.3) +/// } extension NSImage: AttachableAsCGImage { + /// @Metadata { + /// @Available(Swift, introduced: 6.3) + /// } public var attachableCGImage: CGImage { get throws { let ctm = AffineTransform(scale: _attachmentScaleFactor) as NSAffineTransform diff --git a/Sources/Overlays/_Testing_AppKit/CMakeLists.txt b/Sources/Overlays/_Testing_AppKit/CMakeLists.txt new file mode 100644 index 000000000..75a463433 --- /dev/null +++ b/Sources/Overlays/_Testing_AppKit/CMakeLists.txt @@ -0,0 +1,23 @@ +# This source file is part of the Swift.org open source project +# +# Copyright (c) 2024–2025 Apple Inc. and the Swift project authors +# Licensed under Apache License v2.0 with Runtime Library Exception +# +# See http://swift.org/LICENSE.txt for license information +# See http://swift.org/CONTRIBUTORS.txt for Swift project authors + +if (CMAKE_SYSTEM_NAME STREQUAL "Darwin") + add_library(_Testing_AppKit + Attachments/NSImage+AttachableAsCGImage.swift + ReexportTesting.swift) + + target_link_libraries(_Testing_AppKit PUBLIC + Testing + _Testing_CoreGraphics) + + target_compile_options(_Testing_AppKit PRIVATE + -enable-library-evolution + -emit-module-interface -emit-module-interface-path $/_Testing_AppKit.swiftinterface) + + _swift_testing_install_target(_Testing_AppKit) +endif() diff --git a/Sources/Overlays/_Testing_AppKit/ReexportTesting.swift b/Sources/Overlays/_Testing_AppKit/ReexportTesting.swift index ce80a70d9..2e76ecd44 100644 --- a/Sources/Overlays/_Testing_AppKit/ReexportTesting.swift +++ b/Sources/Overlays/_Testing_AppKit/ReexportTesting.swift @@ -1,12 +1,12 @@ // // This source file is part of the Swift.org open source project // -// Copyright (c) 2024 Apple Inc. and the Swift project authors +// Copyright (c) 2024–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 // -@_exported @_spi(Experimental) @_spi(ForToolsIntegrationOnly) public import Testing -@_exported @_spi(Experimental) @_spi(ForToolsIntegrationOnly) public import _Testing_CoreGraphics +@_exported public import Testing +@_exported public import _Testing_CoreGraphics diff --git a/Sources/Overlays/_Testing_CoreGraphics/Attachments/AttachableAsCGImage.swift b/Sources/Overlays/_Testing_CoreGraphics/Attachments/AttachableAsCGImage.swift index e51742dc3..31427c9d7 100644 --- a/Sources/Overlays/_Testing_CoreGraphics/Attachments/AttachableAsCGImage.swift +++ b/Sources/Overlays/_Testing_CoreGraphics/Attachments/AttachableAsCGImage.swift @@ -1,7 +1,7 @@ // // This source file is part of the Swift.org open source project // -// Copyright (c) 2024 Apple Inc. and the Swift project authors +// Copyright (c) 2024–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 @@ -13,12 +13,12 @@ public import CoreGraphics private import ImageIO /// A protocol describing images that can be converted to instances of -/// ``Testing/Attachment``. +/// [`Attachment`](https://developer.apple.com/documentation/testing/attachment). /// /// Instances of types conforming to this protocol do not themselves conform to -/// ``Testing/Attachable``. Instead, the testing library provides additional -/// initializers on ``Testing/Attachment`` that take instances of such types and -/// handle converting them to image data when needed. +/// [`Attachable`](https://developer.apple.com/documentation/testing/attachable). +/// Instead, the testing library provides additional initializers on [`Attachment`](https://developer.apple.com/documentation/testing/attachment) +/// that take instances of such types and handle converting them to image data when needed. /// /// You can attach instances of the following system-provided image types to a /// test: @@ -27,17 +27,26 @@ private import ImageIO /// |-|-| /// | macOS | [`CGImage`](https://developer.apple.com/documentation/coregraphics/cgimage), [`CIImage`](https://developer.apple.com/documentation/coreimage/ciimage), [`NSImage`](https://developer.apple.com/documentation/appkit/nsimage) | /// | iOS, watchOS, tvOS, and visionOS | [`CGImage`](https://developer.apple.com/documentation/coregraphics/cgimage), [`CIImage`](https://developer.apple.com/documentation/coreimage/ciimage), [`UIImage`](https://developer.apple.com/documentation/uikit/uiimage) | +/// @Comment { /// | Windows | [`HBITMAP`](https://learn.microsoft.com/en-us/windows/win32/gdi/bitmaps), [`HICON`](https://learn.microsoft.com/en-us/windows/win32/menurc/icons), [`IWICBitmapSource`](https://learn.microsoft.com/en-us/windows/win32/api/wincodec/nn-wincodec-iwicbitmapsource) (including its subclasses declared by Windows Imaging Component) | +/// } /// /// You do not generally need to add your own conformances to this protocol. If /// you have an image in another format that needs to be attached to a test, /// first convert it to an instance of one of the types above. -@_spi(Experimental) +/// +/// @Metadata { +/// @Available(Swift, introduced: 6.3) +/// } @available(_uttypesAPI, *) public protocol AttachableAsCGImage: SendableMetatype { /// An instance of `CGImage` representing this image. /// /// - Throws: Any error that prevents the creation of an image. + /// + /// @Metadata { + /// @Available(Swift, introduced: 6.3) + /// } var attachableCGImage: CGImage { get throws } /// The orientation of the image. diff --git a/Sources/Overlays/_Testing_CoreGraphics/Attachments/AttachableImageFormat+UTType.swift b/Sources/Overlays/_Testing_CoreGraphics/Attachments/AttachableImageFormat+UTType.swift index 173265807..4a0146c72 100644 --- a/Sources/Overlays/_Testing_CoreGraphics/Attachments/AttachableImageFormat+UTType.swift +++ b/Sources/Overlays/_Testing_CoreGraphics/Attachments/AttachableImageFormat+UTType.swift @@ -9,7 +9,7 @@ // #if SWT_TARGET_OS_APPLE && canImport(CoreGraphics) -@_spi(Experimental) public import Testing +public import Testing public import UniformTypeIdentifiers @@ -64,6 +64,10 @@ extension AttachableImageFormat { /// The content type corresponding to this image format. /// /// The value of this property always conforms to [`UTType.image`](https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/image). + /// + /// @Metadata { + /// @Available(Swift, introduced: 6.3) + /// } public var contentType: UTType { switch kind { case .png: @@ -89,6 +93,10 @@ extension AttachableImageFormat { /// /// If `contentType` does not conform to [`UTType.image`](https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/image), /// the result is undefined. + /// + /// @Metadata { + /// @Available(Swift, introduced: 6.3) + /// } public init(_ contentType: UTType, encodingQuality: Float = 1.0) { precondition( contentType.conforms(to: .image), diff --git a/Sources/Overlays/_Testing_CoreGraphics/Attachments/Attachment+AttachableAsCGImage.swift b/Sources/Overlays/_Testing_CoreGraphics/Attachments/Attachment+AttachableAsCGImage.swift index b3349915b..65d90b11a 100644 --- a/Sources/Overlays/_Testing_CoreGraphics/Attachments/Attachment+AttachableAsCGImage.swift +++ b/Sources/Overlays/_Testing_CoreGraphics/Attachments/Attachment+AttachableAsCGImage.swift @@ -1,7 +1,7 @@ // // This source file is part of the Swift.org open source project // -// Copyright (c) 2024 Apple Inc. and the Swift project authors +// Copyright (c) 2024–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 @@ -9,9 +9,8 @@ // #if SWT_TARGET_OS_APPLE && canImport(CoreGraphics) -@_spi(Experimental) public import Testing +public import Testing -@_spi(Experimental) @available(_uttypesAPI, *) extension Attachment { /// Initialize an instance of this type that encloses the given image. @@ -33,7 +32,9 @@ extension Attachment { /// |-|-| /// | macOS | [`CGImage`](https://developer.apple.com/documentation/coregraphics/cgimage), [`CIImage`](https://developer.apple.com/documentation/coreimage/ciimage), [`NSImage`](https://developer.apple.com/documentation/appkit/nsimage) | /// | iOS, watchOS, tvOS, and visionOS | [`CGImage`](https://developer.apple.com/documentation/coregraphics/cgimage), [`CIImage`](https://developer.apple.com/documentation/coreimage/ciimage), [`UIImage`](https://developer.apple.com/documentation/uikit/uiimage) | + /// @Comment { /// | Windows | [`HBITMAP`](https://learn.microsoft.com/en-us/windows/win32/gdi/bitmaps), [`HICON`](https://learn.microsoft.com/en-us/windows/win32/menurc/icons), [`IWICBitmapSource`](https://learn.microsoft.com/en-us/windows/win32/api/wincodec/nn-wincodec-iwicbitmapsource) (including its subclasses declared by Windows Imaging Component) | + /// } /// /// The testing library uses the image format specified by `imageFormat`. Pass /// `nil` to let the testing library decide which image format to use. If you @@ -42,6 +43,10 @@ extension Attachment { /// specify a path extension, or if the path extension you specify doesn't /// correspond to an image format the operating system knows how to write, the /// testing library selects an appropriate image format for you. + /// + /// @Metadata { + /// @Available(Swift, introduced: 6.3) + /// } public init( _ image: T, named preferredName: String? = nil, @@ -74,7 +79,9 @@ extension Attachment { /// |-|-| /// | macOS | [`CGImage`](https://developer.apple.com/documentation/coregraphics/cgimage), [`CIImage`](https://developer.apple.com/documentation/coreimage/ciimage), [`NSImage`](https://developer.apple.com/documentation/appkit/nsimage) | /// | iOS, watchOS, tvOS, and visionOS | [`CGImage`](https://developer.apple.com/documentation/coregraphics/cgimage), [`CIImage`](https://developer.apple.com/documentation/coreimage/ciimage), [`UIImage`](https://developer.apple.com/documentation/uikit/uiimage) | + /// @Comment { /// | Windows | [`HBITMAP`](https://learn.microsoft.com/en-us/windows/win32/gdi/bitmaps), [`HICON`](https://learn.microsoft.com/en-us/windows/win32/menurc/icons), [`IWICBitmapSource`](https://learn.microsoft.com/en-us/windows/win32/api/wincodec/nn-wincodec-iwicbitmapsource) (including its subclasses declared by Windows Imaging Component) | + /// } /// /// The testing library uses the image format specified by `imageFormat`. Pass /// `nil` to let the testing library decide which image format to use. If you @@ -83,8 +90,12 @@ extension Attachment { /// specify a path extension, or if the path extension you specify doesn't /// correspond to an image format the operating system knows how to write, the /// testing library selects an appropriate image format for you. + /// + /// @Metadata { + /// @Available(Swift, introduced: 6.3) + /// } public static func record( - _ image: consuming T, + _ image: T, named preferredName: String? = nil, as imageFormat: AttachableImageFormat? = nil, sourceLocation: SourceLocation = #_sourceLocation diff --git a/Sources/Overlays/_Testing_CoreGraphics/Attachments/CGImage+AttachableAsCGImage.swift b/Sources/Overlays/_Testing_CoreGraphics/Attachments/CGImage+AttachableAsCGImage.swift index 944798d39..c4a4fd630 100644 --- a/Sources/Overlays/_Testing_CoreGraphics/Attachments/CGImage+AttachableAsCGImage.swift +++ b/Sources/Overlays/_Testing_CoreGraphics/Attachments/CGImage+AttachableAsCGImage.swift @@ -1,7 +1,7 @@ // // This source file is part of the Swift.org open source project // -// Copyright (c) 2024 Apple Inc. and the Swift project authors +// Copyright (c) 2024–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 @@ -11,8 +11,13 @@ #if SWT_TARGET_OS_APPLE && canImport(CoreGraphics) public import CoreGraphics -@_spi(Experimental) +/// @Metadata { +/// @Available(Swift, introduced: 6.3) +/// } extension CGImage: AttachableAsCGImage { + /// @Metadata { + /// @Available(Swift, introduced: 6.3) + /// } public var attachableCGImage: CGImage { self } diff --git a/Sources/Overlays/_Testing_CoreGraphics/Attachments/_AttachableImageWrapper+AttachableWrapper.swift b/Sources/Overlays/_Testing_CoreGraphics/Attachments/_AttachableImageWrapper+AttachableWrapper.swift index 9976e769b..3281de11a 100644 --- a/Sources/Overlays/_Testing_CoreGraphics/Attachments/_AttachableImageWrapper+AttachableWrapper.swift +++ b/Sources/Overlays/_Testing_CoreGraphics/Attachments/_AttachableImageWrapper+AttachableWrapper.swift @@ -1,7 +1,7 @@ // // This source file is part of the Swift.org open source project // -// Copyright (c) 2024 Apple Inc. and the Swift project authors +// Copyright (c) 2024–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 @@ -9,7 +9,7 @@ // #if SWT_TARGET_OS_APPLE && canImport(CoreGraphics) -@_spi(Experimental) public import Testing +public import Testing private import CoreGraphics private import ImageIO @@ -39,7 +39,6 @@ private import UniformTypeIdentifiers /// useful.) @available(_uttypesAPI, *) -@_spi(Experimental) extension _AttachableImageWrapper: Attachable, AttachableWrapper where Image: AttachableAsCGImage { public func withUnsafeBytes(for attachment: borrowing Attachment<_AttachableImageWrapper>, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { let data = NSMutableData() diff --git a/Sources/Overlays/_Testing_CoreGraphics/CMakeLists.txt b/Sources/Overlays/_Testing_CoreGraphics/CMakeLists.txt new file mode 100644 index 000000000..335068a0b --- /dev/null +++ b/Sources/Overlays/_Testing_CoreGraphics/CMakeLists.txt @@ -0,0 +1,26 @@ +# This source file is part of the Swift.org open source project +# +# Copyright (c) 2024–2025 Apple Inc. and the Swift project authors +# Licensed under Apache License v2.0 with Runtime Library Exception +# +# See http://swift.org/LICENSE.txt for license information +# See http://swift.org/CONTRIBUTORS.txt for Swift project authors + +if(APPLE) + add_library(_Testing_CoreGraphics + Attachments/_AttachableImageWrapper+AttachableWrapper.swift + Attachments/AttachableAsCGImage.swift + Attachments/AttachableImageFormat+UTType.swift + Attachments/Attachment+AttachableAsCGImage.swift + Attachments/CGImage+AttachableAsCGImage.swift + ReexportTesting.swift) + + target_link_libraries(_Testing_CoreGraphics PUBLIC + Testing) + + target_compile_options(_Testing_CoreGraphics PRIVATE + -enable-library-evolution + -emit-module-interface -emit-module-interface-path $/_Testing_CoreGraphics.swiftinterface) + + _swift_testing_install_target(_Testing_CoreGraphics) +endif() diff --git a/Sources/Overlays/_Testing_CoreGraphics/ReexportTesting.swift b/Sources/Overlays/_Testing_CoreGraphics/ReexportTesting.swift index 5b28faa77..be2275d11 100644 --- a/Sources/Overlays/_Testing_CoreGraphics/ReexportTesting.swift +++ b/Sources/Overlays/_Testing_CoreGraphics/ReexportTesting.swift @@ -1,11 +1,11 @@ // // This source file is part of the Swift.org open source project // -// Copyright (c) 2024 Apple Inc. and the Swift project authors +// Copyright (c) 2024–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 // -@_exported @_spi(Experimental) @_spi(ForToolsIntegrationOnly) public import Testing +@_exported public import Testing diff --git a/Sources/Overlays/_Testing_CoreImage/Attachments/CIImage+AttachableAsCGImage.swift b/Sources/Overlays/_Testing_CoreImage/Attachments/CIImage+AttachableAsCGImage.swift index 7614dc633..581de2c7c 100644 --- a/Sources/Overlays/_Testing_CoreImage/Attachments/CIImage+AttachableAsCGImage.swift +++ b/Sources/Overlays/_Testing_CoreImage/Attachments/CIImage+AttachableAsCGImage.swift @@ -1,7 +1,7 @@ // // This source file is part of the Swift.org open source project // -// Copyright (c) 2024 Apple Inc. and the Swift project authors +// Copyright (c) 2024–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 @@ -10,10 +10,15 @@ #if SWT_TARGET_OS_APPLE && canImport(CoreImage) public import CoreImage -@_spi(Experimental) public import _Testing_CoreGraphics +public import _Testing_CoreGraphics -@_spi(Experimental) +/// @Metadata { +/// @Available(Swift, introduced: 6.3) +/// } extension CIImage: AttachableAsCGImage { + /// @Metadata { + /// @Available(Swift, introduced: 6.3) + /// } public var attachableCGImage: CGImage { get throws { guard let result = CIContext().createCGImage(self, from: extent) else { diff --git a/Sources/Overlays/_Testing_CoreImage/CMakeLists.txt b/Sources/Overlays/_Testing_CoreImage/CMakeLists.txt new file mode 100644 index 000000000..baa50537e --- /dev/null +++ b/Sources/Overlays/_Testing_CoreImage/CMakeLists.txt @@ -0,0 +1,23 @@ +# This source file is part of the Swift.org open source project +# +# Copyright (c) 2024–2025 Apple Inc. and the Swift project authors +# Licensed under Apache License v2.0 with Runtime Library Exception +# +# See http://swift.org/LICENSE.txt for license information +# See http://swift.org/CONTRIBUTORS.txt for Swift project authors + +if(APPLE) + add_library(_Testing_CoreImage + Attachments/CIImage+AttachableAsCGImage.swift + ReexportTesting.swift) + + target_link_libraries(_Testing_CoreImage PUBLIC + Testing + _Testing_CoreGraphics) + + target_compile_options(_Testing_CoreImage PRIVATE + -enable-library-evolution + -emit-module-interface -emit-module-interface-path $/_Testing_CoreImage.swiftinterface) + + _swift_testing_install_target(_Testing_CoreImage) +endif() diff --git a/Sources/Overlays/_Testing_CoreImage/ReexportTesting.swift b/Sources/Overlays/_Testing_CoreImage/ReexportTesting.swift index ce80a70d9..2e76ecd44 100644 --- a/Sources/Overlays/_Testing_CoreImage/ReexportTesting.swift +++ b/Sources/Overlays/_Testing_CoreImage/ReexportTesting.swift @@ -1,12 +1,12 @@ // // This source file is part of the Swift.org open source project // -// Copyright (c) 2024 Apple Inc. and the Swift project authors +// Copyright (c) 2024–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 // -@_exported @_spi(Experimental) @_spi(ForToolsIntegrationOnly) public import Testing -@_exported @_spi(Experimental) @_spi(ForToolsIntegrationOnly) public import _Testing_CoreGraphics +@_exported public import Testing +@_exported public import _Testing_CoreGraphics diff --git a/Sources/Overlays/_Testing_UIKit/Attachments/UIImage+AttachableAsCGImage.swift b/Sources/Overlays/_Testing_UIKit/Attachments/UIImage+AttachableAsCGImage.swift index 233f737a4..3766b3095 100644 --- a/Sources/Overlays/_Testing_UIKit/Attachments/UIImage+AttachableAsCGImage.swift +++ b/Sources/Overlays/_Testing_UIKit/Attachments/UIImage+AttachableAsCGImage.swift @@ -1,7 +1,7 @@ // // This source file is part of the Swift.org open source project // -// Copyright (c) 2024 Apple Inc. and the Swift project authors +// Copyright (c) 2024–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 @@ -10,16 +10,20 @@ #if SWT_TARGET_OS_APPLE && canImport(UIKit) public import UIKit -@_spi(Experimental) public import _Testing_CoreGraphics -@_spi(Experimental) private import _Testing_CoreImage +public import _Testing_CoreGraphics private import ImageIO #if canImport(UIKitCore_Private) private import UIKitCore_Private #endif -@_spi(Experimental) +/// @Metadata { +/// @Available(Swift, introduced: 6.3) +/// } extension UIImage: AttachableAsCGImage { + /// @Metadata { + /// @Available(Swift, introduced: 6.3) + /// } public var attachableCGImage: CGImage { get throws { #if canImport(UIKitCore_Private) diff --git a/Sources/Overlays/_Testing_UIKit/CMakeLists.txt b/Sources/Overlays/_Testing_UIKit/CMakeLists.txt new file mode 100644 index 000000000..5976e2e86 --- /dev/null +++ b/Sources/Overlays/_Testing_UIKit/CMakeLists.txt @@ -0,0 +1,23 @@ +# This source file is part of the Swift.org open source project +# +# Copyright (c) 2024–2025 Apple Inc. and the Swift project authors +# Licensed under Apache License v2.0 with Runtime Library Exception +# +# See http://swift.org/LICENSE.txt for license information +# See http://swift.org/CONTRIBUTORS.txt for Swift project authors + +if(APPLE) + add_library(_Testing_UIKit + Attachments/UIImage+AttachableAsCGImage.swift + ReexportTesting.swift) + + target_link_libraries(_Testing_UIKit PUBLIC + Testing + _Testing_CoreGraphics) + + target_compile_options(_Testing_UIKit PRIVATE + -enable-library-evolution + -emit-module-interface -emit-module-interface-path $/_Testing_UIKit.swiftinterface) + + _swift_testing_install_target(_Testing_UIKit) +endif() diff --git a/Sources/Overlays/_Testing_UIKit/ReexportTesting.swift b/Sources/Overlays/_Testing_UIKit/ReexportTesting.swift index ce80a70d9..2e76ecd44 100644 --- a/Sources/Overlays/_Testing_UIKit/ReexportTesting.swift +++ b/Sources/Overlays/_Testing_UIKit/ReexportTesting.swift @@ -1,12 +1,12 @@ // // This source file is part of the Swift.org open source project // -// Copyright (c) 2024 Apple Inc. and the Swift project authors +// Copyright (c) 2024–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 // -@_exported @_spi(Experimental) @_spi(ForToolsIntegrationOnly) public import Testing -@_exported @_spi(Experimental) @_spi(ForToolsIntegrationOnly) public import _Testing_CoreGraphics +@_exported public import Testing +@_exported public import _Testing_CoreGraphics diff --git a/Sources/Testing/Attachments/Images/AttachableImageFormat.swift b/Sources/Testing/Attachments/Images/AttachableImageFormat.swift index 4798e42b1..9bf6b50f8 100644 --- a/Sources/Testing/Attachments/Images/AttachableImageFormat.swift +++ b/Sources/Testing/Attachments/Images/AttachableImageFormat.swift @@ -12,7 +12,7 @@ /// when attaching an image to a test. /// /// When you attach an image to a test, you can pass an instance of this type to -/// ``Attachment/record(_:named:as:sourceLocation:)`` so that the testing +/// `Attachment.record(_:named:as:sourceLocation:)` so that the testing /// library knows the image format you'd like to use. If you don't pass an /// instance of this type, the testing library infers which format to use based /// on the attachment's preferred name. @@ -23,9 +23,14 @@ /// - On Apple platforms, you can use [`CGImageDestinationCopyTypeIdentifiers()`](https://developer.apple.com/documentation/imageio/cgimagedestinationcopytypeidentifiers()) /// from the [Image I/O framework](https://developer.apple.com/documentation/imageio) /// to determine which formats are supported. +/// @Comment { /// - On Windows, you can use [`IWICImagingFactory.CreateComponentEnumerator()`](https://learn.microsoft.com/en-us/windows/win32/api/wincodec/nf-wincodec-iwicimagingfactory-createcomponentenumerator) /// to enumerate the available image encoders. -@_spi(Experimental) +/// } +/// +/// @Metadata { +/// @Available(Swift, introduced: 6.3) +/// } @available(_uttypesAPI, *) public struct AttachableImageFormat: Sendable { /// An enumeration describing the various kinds of image format that can be @@ -58,6 +63,10 @@ public struct AttachableImageFormat: Sendable { /// supported encoding quality and `1.0` being the highest supported encoding /// quality. The value of this property is ignored for image formats that do /// not support variable encoding quality. + /// + /// @Metadata { + /// @Available(Swift, introduced: 6.3) + /// } public internal(set) var encodingQuality: Float = 1.0 package init(kind: Kind, encodingQuality: Float) { @@ -71,11 +80,19 @@ public struct AttachableImageFormat: Sendable { @available(_uttypesAPI, *) extension AttachableImageFormat { /// The PNG image format. + /// + /// @Metadata { + /// @Available(Swift, introduced: 6.3) + /// } public static var png: Self { Self(kind: .png, encodingQuality: 1.0) } /// The JPEG image format with maximum encoding quality. + /// + /// @Metadata { + /// @Available(Swift, introduced: 6.3) + /// } public static var jpeg: Self { Self(kind: .jpeg, encodingQuality: 1.0) } @@ -90,6 +107,10 @@ extension AttachableImageFormat { /// /// - Returns: An instance of this type representing the JPEG image format /// with the specified encoding quality. + /// + /// @Metadata { + /// @Available(Swift, introduced: 6.3) + /// } public static func jpeg(withEncodingQuality encodingQuality: Float) -> Self { Self(kind: .jpeg, encodingQuality: encodingQuality) } diff --git a/Sources/Testing/Attachments/Images/ImageAttachmentError.swift b/Sources/Testing/Attachments/Images/ImageAttachmentError.swift index 7cd6f8180..de421bfd9 100644 --- a/Sources/Testing/Attachments/Images/ImageAttachmentError.swift +++ b/Sources/Testing/Attachments/Images/ImageAttachmentError.swift @@ -1,7 +1,7 @@ // // This source file is part of the Swift.org open source project // -// Copyright (c) 2024 Apple Inc. and the Swift project authors +// Copyright (c) 2024–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 diff --git a/Sources/Testing/Attachments/Images/_AttachableImageWrapper.swift b/Sources/Testing/Attachments/Images/_AttachableImageWrapper.swift index 71623538a..25e102677 100644 --- a/Sources/Testing/Attachments/Images/_AttachableImageWrapper.swift +++ b/Sources/Testing/Attachments/Images/_AttachableImageWrapper.swift @@ -17,8 +17,9 @@ /// |-|-| /// | macOS | [`CGImage`](https://developer.apple.com/documentation/coregraphics/cgimage), [`CIImage`](https://developer.apple.com/documentation/coreimage/ciimage), [`NSImage`](https://developer.apple.com/documentation/appkit/nsimage) | /// | iOS, watchOS, tvOS, and visionOS | [`CGImage`](https://developer.apple.com/documentation/coregraphics/cgimage), [`CIImage`](https://developer.apple.com/documentation/coreimage/ciimage), [`UIImage`](https://developer.apple.com/documentation/uikit/uiimage) | +/// @Comment { /// | Windows | [`HBITMAP`](https://learn.microsoft.com/en-us/windows/win32/gdi/bitmaps), [`HICON`](https://learn.microsoft.com/en-us/windows/win32/menurc/icons), [`IWICBitmapSource`](https://learn.microsoft.com/en-us/windows/win32/api/wincodec/nn-wincodec-iwicbitmapsource) (including its subclasses declared by Windows Imaging Component) | -@_spi(Experimental) +/// } @available(_uttypesAPI, *) public final class _AttachableImageWrapper: Sendable { /// The underlying image. diff --git a/Sources/Testing/Testing.docc/Attachments.md b/Sources/Testing/Testing.docc/Attachments.md index 0da40c201..e05fc0270 100644 --- a/Sources/Testing/Testing.docc/Attachments.md +++ b/Sources/Testing/Testing.docc/Attachments.md @@ -14,7 +14,7 @@ Attach values to tests to help diagnose issues and gather feedback. ## Overview -Attach values such as strings and files to tests. Implement the ``Attachable`` +Attach values such as strings and files to tests. Implement the ``Attachable`` protocol to create your own attachable types. ## Topics @@ -25,8 +25,18 @@ protocol to create your own attachable types. - ``Attachable`` - ``AttachableWrapper`` + + +### Attaching images to tests + +- ``AttachableImageFormat`` + diff --git a/Sources/Testing/Testing.swiftcrossimport/AppKit.swiftoverlay b/Sources/Testing/Testing.swiftcrossimport/AppKit.swiftoverlay new file mode 100644 index 000000000..cc9998ba3 --- /dev/null +++ b/Sources/Testing/Testing.swiftcrossimport/AppKit.swiftoverlay @@ -0,0 +1,3 @@ +version: 1 +modules: +- name: _Testing_AppKit diff --git a/Sources/Testing/Testing.swiftcrossimport/CoreGraphics.swiftoverlay b/Sources/Testing/Testing.swiftcrossimport/CoreGraphics.swiftoverlay new file mode 100644 index 000000000..656012089 --- /dev/null +++ b/Sources/Testing/Testing.swiftcrossimport/CoreGraphics.swiftoverlay @@ -0,0 +1,3 @@ +version: 1 +modules: +- name: _Testing_CoreGraphics diff --git a/Sources/Testing/Testing.swiftcrossimport/CoreImage.swiftoverlay b/Sources/Testing/Testing.swiftcrossimport/CoreImage.swiftoverlay new file mode 100644 index 000000000..cdea5109b --- /dev/null +++ b/Sources/Testing/Testing.swiftcrossimport/CoreImage.swiftoverlay @@ -0,0 +1,3 @@ +version: 1 +modules: +- name: _Testing_CoreImage diff --git a/Sources/Testing/Testing.swiftcrossimport/UIKit.swiftoverlay b/Sources/Testing/Testing.swiftcrossimport/UIKit.swiftoverlay new file mode 100644 index 000000000..b3c35caed --- /dev/null +++ b/Sources/Testing/Testing.swiftcrossimport/UIKit.swiftoverlay @@ -0,0 +1,3 @@ +version: 1 +modules: +- name: _Testing_UIKit diff --git a/Tests/TestingTests/AttachmentTests.swift b/Tests/TestingTests/AttachmentTests.swift index 84db8fe38..69793d215 100644 --- a/Tests/TestingTests/AttachmentTests.swift +++ b/Tests/TestingTests/AttachmentTests.swift @@ -1,7 +1,7 @@ // // This source file is part of the Swift.org open source project // -// Copyright (c) 2023 Apple Inc. and the Swift project authors +// Copyright (c) 2023–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 @@ -12,7 +12,7 @@ private import _TestingInternals #if canImport(AppKit) && canImport(_Testing_AppKit) import AppKit -@_spi(Experimental) import _Testing_AppKit +import _Testing_AppKit #endif #if canImport(Foundation) && canImport(_Testing_Foundation) import Foundation @@ -24,11 +24,11 @@ import CoreGraphics #endif #if canImport(CoreImage) && canImport(_Testing_CoreImage) import CoreImage -@_spi(Experimental) import _Testing_CoreImage +import _Testing_CoreImage #endif #if canImport(UIKit) && canImport(_Testing_UIKit) import UIKit -@_spi(Experimental) import _Testing_UIKit +import _Testing_UIKit #endif #if canImport(UniformTypeIdentifiers) import UniformTypeIdentifiers From 64c8ce6b14fab21dfcddb9b40d86fbc22ccb79e5 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Wed, 27 Aug 2025 15:14:31 -0400 Subject: [PATCH 113/216] Add experimental SPI to cancel a running test. (#1284) This PR introduces `Test.cancel()` and `Test.Case.cancel()` which cancel the current test/suite and the current test case, respectively. For example: ```swift @Test(arguments: [Food.burger, .fries, .iceCream]) func `Food truck is well-stocked`(_ food: Food) throws { if food == .iceCream && Season.current == .winter { try Test.Case.cancel("It's too cold for ice cream.") } // ... } ``` These functions work by cancelling the child task associated with the current test or test case, then throwing an error to end local execution early. Compare `XCTSkip()` which, in Swift, is just a thrown error that the XCTest harness special-cases, or `XCTSkip()` in Objective-C which actually throws an exception to force the caller to exit early. Resolves #120. Resolves #1289. Resolves rdar://159150449. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- .../ABI/Encoded/ABI.EncodedAttachment.swift | 8 - .../ABI/Encoded/ABI.EncodedEvent.swift | 49 ++- Sources/Testing/CMakeLists.txt | 1 + Sources/Testing/Events/Event.swift | 52 ++++ .../Event.HumanReadableOutputRecorder.swift | 75 +++-- Sources/Testing/ExitTests/ExitTest.swift | 23 +- Sources/Testing/Issues/Issue+Recording.swift | 18 +- Sources/Testing/Running/Runner.Plan.swift | 68 +++- .../Testing/Running/Runner.RuntimeState.swift | 8 +- Sources/Testing/Running/Runner.swift | 32 +- Sources/Testing/Test+Cancellation.swift | 293 ++++++++++++++++++ .../Testing.docc/EnablingAndDisabling.md | 17 + .../Testing.docc/MigratingFromXCTest.md | 39 +++ .../TestingTests/TestCancellationTests.swift | 228 ++++++++++++++ .../TestSupport/TestingAdditions.swift | 14 +- 15 files changed, 839 insertions(+), 86 deletions(-) create mode 100644 Sources/Testing/Test+Cancellation.swift create mode 100644 Tests/TestingTests/TestCancellationTests.swift diff --git a/Sources/Testing/ABI/Encoded/ABI.EncodedAttachment.swift b/Sources/Testing/ABI/Encoded/ABI.EncodedAttachment.swift index 9275da1ed..a2400f9bf 100644 --- a/Sources/Testing/ABI/Encoded/ABI.EncodedAttachment.swift +++ b/Sources/Testing/ABI/Encoded/ABI.EncodedAttachment.swift @@ -38,12 +38,6 @@ extension ABI { /// - Warning: Inline attachment content is not yet part of the JSON schema. var _bytes: Bytes? - /// The source location where this attachment was created. - /// - /// - Warning: Attachment source locations are not yet part of the JSON - /// schema. - var _sourceLocation: SourceLocation? - init(encoding attachment: borrowing Attachment, in eventContext: borrowing Event.Context) { path = attachment.fileSystemPath @@ -55,8 +49,6 @@ extension ABI { return Bytes(rawValue: [UInt8](bytes)) } } - - _sourceLocation = attachment.sourceLocation } } diff --git a/Sources/Testing/ABI/Encoded/ABI.EncodedEvent.swift b/Sources/Testing/ABI/Encoded/ABI.EncodedEvent.swift index 73e7db2ac..a78f86368 100644 --- a/Sources/Testing/ABI/Encoded/ABI.EncodedEvent.swift +++ b/Sources/Testing/ABI/Encoded/ABI.EncodedEvent.swift @@ -29,8 +29,10 @@ extension ABI { case issueRecorded case valueAttached case testCaseEnded + case testCaseCancelled = "_testCaseCancelled" case testEnded case testSkipped + case testCancelled = "_testCancelled" case runEnded } @@ -64,6 +66,38 @@ extension ABI { /// - Warning: Test cases are not yet part of the JSON schema. var _testCase: EncodedTestCase? + /// The comments the test author provided for this event, if any. + /// + /// The value of this property contains the comments related to the primary + /// user action that caused this event to be generated. + /// + /// Some kinds of events have additional associated comments. For example, + /// when using ``withKnownIssue(_:isIntermittent:sourceLocation:_:)``, there + /// can be separate comments for the "underlying" issue versus the known + /// issue matcher, and either can be `nil`. In such cases, the secondary + /// comment(s) are represented via a distinct property depending on the kind + /// of that event. + /// + /// - Warning: Comments at this level are not yet part of the JSON schema. + var _comments: [String]? + + /// A source location associated with this event, if any. + /// + /// The value of this property represents the source location most closely + /// related to the primary user action that caused this event to be + /// generated. + /// + /// Some kinds of events have additional associated source locations. For + /// example, when using ``withKnownIssue(_:isIntermittent:sourceLocation:_:)``, + /// there can be separate source locations for the "underlying" issue versus + /// the known issue matcher. In such cases, the secondary source location(s) + /// are represented via a distinct property depending on the kind of that + /// event. + /// + /// - Warning: Source locations at this level of the JSON schema are not yet + /// part of said JSON schema. + var _sourceLocation: SourceLocation? + init?(encoding event: borrowing Event, in eventContext: borrowing Event.Context, messages: borrowing [Event.HumanReadableOutputRecorder.Message]) { switch event.kind { case .runStarted: @@ -78,18 +112,31 @@ extension ABI { case let .issueRecorded(recordedIssue): kind = .issueRecorded issue = EncodedIssue(encoding: recordedIssue, in: eventContext) + _comments = recordedIssue.comments.map(\.rawValue) + _sourceLocation = recordedIssue.sourceLocation case let .valueAttached(attachment): kind = .valueAttached self.attachment = EncodedAttachment(encoding: attachment, in: eventContext) + _sourceLocation = attachment.sourceLocation case .testCaseEnded: if eventContext.test?.isParameterized == false { return nil } kind = .testCaseEnded + case let .testCaseCancelled(skipInfo): + kind = .testCaseCancelled + _comments = Array(skipInfo.comment).map(\.rawValue) + _sourceLocation = skipInfo.sourceLocation case .testEnded: kind = .testEnded - case .testSkipped: + case let .testSkipped(skipInfo): kind = .testSkipped + _comments = Array(skipInfo.comment).map(\.rawValue) + _sourceLocation = skipInfo.sourceLocation + case let .testCancelled(skipInfo): + kind = .testCancelled + _comments = Array(skipInfo.comment).map(\.rawValue) + _sourceLocation = skipInfo.sourceLocation case .runEnded: kind = .runEnded default: diff --git a/Sources/Testing/CMakeLists.txt b/Sources/Testing/CMakeLists.txt index 531d27cfc..d2cd21ab9 100644 --- a/Sources/Testing/CMakeLists.txt +++ b/Sources/Testing/CMakeLists.txt @@ -93,6 +93,7 @@ add_library(Testing Test.ID.Selection.swift Test.ID.swift Test.swift + Test+Cancellation.swift Test+Discovery.swift Test+Discovery+Legacy.swift Test+Macro.swift diff --git a/Sources/Testing/Events/Event.swift b/Sources/Testing/Events/Event.swift index 0be14ae88..d8daa3e89 100644 --- a/Sources/Testing/Events/Event.swift +++ b/Sources/Testing/Events/Event.swift @@ -74,6 +74,18 @@ public struct Event: Sendable { /// that was passed to the event handler along with this event. case testCaseEnded + /// A test case was cancelled. + /// + /// - Parameters: + /// - skipInfo: A ``SkipInfo`` with details about the cancelled test case. + /// + /// This event is generated by a call to ``Test/Case/cancel(_:sourceLocation:)``. + /// + /// The test case that was cancelled is contained in the ``Event/Context`` + /// instance that was passed to the event handler along with this event. + @_spi(Experimental) + indirect case testCaseCancelled(_ skipInfo: SkipInfo) + /// An expectation was checked with `#expect()` or `#require()`. /// /// - Parameters: @@ -121,6 +133,18 @@ public struct Event: Sendable { /// available from this event's ``Event/testID`` property. indirect case testSkipped(_ skipInfo: SkipInfo) + /// A test was cancelled. + /// + /// - Parameters: + /// - skipInfo: A ``SkipInfo`` with details about the cancelled test. + /// + /// This event is generated by a call to ``Test/cancel(_:sourceLocation:)``. + /// + /// The test that was cancelled is contained in the ``Event/Context`` + /// instance that was passed to the event handler along with this event. + @_spi(Experimental) + indirect case testCancelled(_ skipInfo: SkipInfo) + /// A step in the runner plan ended. /// /// - Parameters: @@ -395,6 +419,18 @@ extension Event.Kind { /// A test case ended. case testCaseEnded + /// A test case was cancelled. + /// + /// - Parameters: + /// - skipInfo: A ``SkipInfo`` with details about the cancelled test case. + /// + /// This event is generated by a call to ``Test/Case/cancel(_:sourceLocation:)``. + /// + /// The test case that was cancelled is contained in the ``Event/Context`` + /// instance that was passed to the event handler along with this event. + @_spi(Experimental) + indirect case testCaseCancelled(_ skipInfo: SkipInfo) + /// An expectation was checked with `#expect()` or `#require()`. /// /// - Parameters: @@ -431,6 +467,18 @@ extension Event.Kind { /// - skipInfo: A ``SkipInfo`` containing details about this skipped test. indirect case testSkipped(_ skipInfo: SkipInfo) + /// A test was cancelled. + /// + /// - Parameters: + /// - skipInfo: A ``SkipInfo`` with details about the cancelled test. + /// + /// This event is generated by a call to ``Test/cancel(_:sourceLocation:)``. + /// + /// The test that was cancelled is contained in the ``Event/Context`` + /// instance that was passed to the event handler along with this event. + @_spi(Experimental) + indirect case testCancelled(_ skipInfo: SkipInfo) + /// A step in the runner plan ended. /// /// - Parameters: @@ -479,6 +527,8 @@ extension Event.Kind { self = .testCaseStarted case .testCaseEnded: self = .testCaseEnded + case let .testCaseCancelled(skipInfo): + self = .testCaseCancelled(skipInfo) case let .expectationChecked(expectation): let expectationSnapshot = Expectation.Snapshot(snapshotting: expectation) self = Snapshot.expectationChecked(expectationSnapshot) @@ -490,6 +540,8 @@ extension Event.Kind { self = .testEnded case let .testSkipped(skipInfo): self = .testSkipped(skipInfo) + case let .testCancelled(skipInfo): + self = .testCancelled(skipInfo) case .planStepEnded: self = .planStepEnded case let .iterationEnded(index): diff --git a/Sources/Testing/Events/Recorder/Event.HumanReadableOutputRecorder.swift b/Sources/Testing/Events/Recorder/Event.HumanReadableOutputRecorder.swift index 434487e27..7497ad4cd 100644 --- a/Sources/Testing/Events/Recorder/Event.HumanReadableOutputRecorder.swift +++ b/Sources/Testing/Events/Recorder/Event.HumanReadableOutputRecorder.swift @@ -73,6 +73,9 @@ extension Event { /// The number of known issues recorded for the test. var knownIssueCount = 0 + + /// Information about the cancellation of this test or test case. + var cancellationInfo: SkipInfo? } /// Data tracked on a per-test basis. @@ -251,6 +254,7 @@ extension Event.HumanReadableOutputRecorder { 0 } let test = eventContext.test + let testCase = eventContext.testCase let keyPath = eventContext.keyPath let testName = if let test { if let displayName = test.displayName { @@ -310,6 +314,9 @@ extension Event.HumanReadableOutputRecorder { case .testCaseStarted: context.testData[keyPath] = .init(startInstant: instant) + case let .testCancelled(skipInfo), let .testCaseCancelled(skipInfo): + context.testData[keyPath]?.cancellationInfo = skipInfo + default: // These events do not manipulate the context structure. break @@ -404,21 +411,29 @@ extension Event.HumanReadableOutputRecorder { } else { "" } - return if issues.errorIssueCount > 0 { - CollectionOfOne( - Message( - symbol: .fail, - stringValue: "\(_capitalizedTitle(for: test)) \(testName)\(testCasesCount) failed after \(duration)\(issues.description)." - ) - ) + _formattedComments(for: test) + var cancellationComment = "." + let (symbol, verbed): (Event.Symbol, String) + if issues.errorIssueCount > 0 { + (symbol, verbed) = (.fail, "failed") + } else if !test.isParameterized, let cancellationInfo = testData.cancellationInfo { + if let comment = cancellationInfo.comment { + cancellationComment = ": \"\(comment.rawValue)\"" + } + (symbol, verbed) = (.skip, "was cancelled") } else { - [ - Message( - symbol: .pass(knownIssueCount: issues.knownIssueCount), - stringValue: "\(_capitalizedTitle(for: test)) \(testName)\(testCasesCount) passed after \(duration)\(issues.description)." - ) - ] + (symbol, verbed) = (.pass(knownIssueCount: issues.knownIssueCount), "passed") + } + + var result = [ + Message( + symbol: symbol, + stringValue: "\(_capitalizedTitle(for: test)) \(testName)\(testCasesCount) \(verbed) after \(duration)\(issues.description)\(cancellationComment)" + ) + ] + if issues.errorIssueCount > 0 { + result += _formattedComments(for: test) } + return result case let .testSkipped(skipInfo): let test = test! @@ -443,7 +458,7 @@ extension Event.HumanReadableOutputRecorder { } else { 0 } - let labeledArguments = if let testCase = eventContext.testCase { + let labeledArguments = if let testCase { testCase.labeledArguments() } else { "" @@ -523,7 +538,7 @@ extension Event.HumanReadableOutputRecorder { return result case .testCaseStarted: - guard let testCase = eventContext.testCase, testCase.isParameterized, let arguments = testCase.arguments else { + guard let testCase, testCase.isParameterized, let arguments = testCase.arguments else { break } @@ -535,7 +550,7 @@ extension Event.HumanReadableOutputRecorder { ] case .testCaseEnded: - guard verbosity > 0, let testCase = eventContext.testCase, testCase.isParameterized, let arguments = testCase.arguments else { + guard verbosity > 0, let test, let testCase, testCase.isParameterized, let arguments = testCase.arguments else { break } @@ -544,18 +559,28 @@ extension Event.HumanReadableOutputRecorder { let issues = _issueCounts(in: testDataGraph) let duration = testData.startInstant.descriptionOfDuration(to: instant) - let message = if issues.errorIssueCount > 0 { - Message( - symbol: .fail, - stringValue: "Test case passing \(arguments.count.counting("argument")) \(testCase.labeledArguments(includingQualifiedTypeNames: verbosity > 0)) to \(testName) failed after \(duration)\(issues.description)." - ) + var cancellationComment = "." + let (symbol, verbed): (Event.Symbol, String) + if issues.errorIssueCount > 0 { + (symbol, verbed) = (.fail, "failed") + } else if !test.isParameterized, let cancellationInfo = testData.cancellationInfo { + if let comment = cancellationInfo.comment { + cancellationComment = ": \"\(comment.rawValue)\"" + } + (symbol, verbed) = (.skip, "was cancelled") } else { + (symbol, verbed) = (.pass(knownIssueCount: issues.knownIssueCount), "passed") + } + return [ Message( - symbol: .pass(knownIssueCount: issues.knownIssueCount), - stringValue: "Test case passing \(arguments.count.counting("argument")) \(testCase.labeledArguments(includingQualifiedTypeNames: verbosity > 0)) to \(testName) passed after \(duration)\(issues.description)." + symbol: symbol, + stringValue: "Test case passing \(arguments.count.counting("argument")) \(testCase.labeledArguments(includingQualifiedTypeNames: verbosity > 0)) to \(testName) \(verbed) after \(duration)\(issues.description)\(cancellationComment)" ) - } - return [message] + ] + + case .testCancelled, .testCaseCancelled: + // Handled in .testEnded and .testCaseEnded + break case let .iterationEnded(index): guard let iterationStartInstant = context.iterationStartInstant else { diff --git a/Sources/Testing/ExitTests/ExitTest.swift b/Sources/Testing/ExitTests/ExitTest.swift index 5eb006b03..c82430eb5 100644 --- a/Sources/Testing/ExitTests/ExitTest.swift +++ b/Sources/Testing/ExitTests/ExitTest.swift @@ -291,9 +291,10 @@ extension ExitTest { current.pointee = self.unsafeCopy() } - do { + let error = await Issue.withErrorRecording(at: nil) { try await body(&self) - } catch { + } + if let error { _errorInMain(error) } @@ -768,7 +769,7 @@ extension ExitTest { } configuration.eventHandler = { event, eventContext in switch event.kind { - case .issueRecorded, .valueAttached: + case .issueRecorded, .valueAttached, .testCancelled, .testCaseCancelled: eventHandler(event, eventContext) default: // Don't forward other kinds of event. @@ -1042,11 +1043,15 @@ extension ExitTest { return } + lazy var comments: [Comment] = event._comments?.map(Comment.init(rawValue:)) ?? [] + lazy var sourceContext = SourceContext( + backtrace: nil, // A backtrace from the child process will have the wrong address space. + sourceLocation: event._sourceLocation + ) if let issue = event.issue { // Translate the issue back into a "real" issue and record it // in the parent process. This translation is, of course, lossy // due to the process boundary, but we make a best effort. - let comments: [Comment] = event.messages.map(\.text).map(Comment.init(rawValue:)) let issueKind: Issue.Kind = if let error = issue._error { .errorCaught(error) } else { @@ -1060,10 +1065,6 @@ extension ExitTest { // Prior to 6.3, all Issues are errors .error } - let sourceContext = SourceContext( - backtrace: nil, // `issue._backtrace` will have the wrong address space. - sourceLocation: issue.sourceLocation - ) var issueCopy = Issue(kind: issueKind, severity: severity, comments: comments, sourceContext: sourceContext) if issue.isKnown { // The known issue comment, if there was one, is already included in @@ -1072,7 +1073,11 @@ extension ExitTest { } issueCopy.record() } else if let attachment = event.attachment { - Attachment.record(attachment, sourceLocation: attachment._sourceLocation!) + Attachment.record(attachment, sourceLocation: event._sourceLocation!) + } else if case .testCancelled = event.kind { + _ = try? Test.cancel(comments: comments, sourceContext: sourceContext) + } else if case .testCaseCancelled = event.kind { + _ = try? Test.Case.cancel(comments: comments, sourceContext: sourceContext) } } diff --git a/Sources/Testing/Issues/Issue+Recording.swift b/Sources/Testing/Issues/Issue+Recording.swift index d99323777..50cdbbc1f 100644 --- a/Sources/Testing/Issues/Issue+Recording.swift +++ b/Sources/Testing/Issues/Issue+Recording.swift @@ -159,7 +159,8 @@ extension Issue { /// allowing it to propagate to the caller. /// /// - Parameters: - /// - sourceLocation: The source location to attribute any caught error to. + /// - sourceLocation: The source location to attribute any caught error to, + /// if available. /// - configuration: The test configuration to use when recording an issue. /// The default value is ``Configuration/current``. /// - body: A closure that might throw an error. @@ -168,7 +169,7 @@ extension Issue { /// caught, otherwise `nil`. @discardableResult static func withErrorRecording( - at sourceLocation: SourceLocation, + at sourceLocation: SourceLocation?, configuration: Configuration? = nil, _ body: () throws -> Void ) -> (any Error)? { @@ -185,6 +186,10 @@ extension Issue { // This error is thrown by expectation checking functions to indicate a // condition evaluated to `false`. Those functions record their own issue, // so we don't need to record another one redundantly. + } catch is SkipInfo, + is CancellationError where Task.isCancelled { + // This error represents control flow rather than an issue, so we suppress + // it here. } catch { let issue = Issue(for: error, sourceLocation: sourceLocation) issue.record(configuration: configuration) @@ -198,7 +203,8 @@ extension Issue { /// issue instead of allowing it to propagate to the caller. /// /// - Parameters: - /// - sourceLocation: The source location to attribute any caught error to. + /// - sourceLocation: The source location to attribute any caught error to, + /// if available. /// - configuration: The test configuration to use when recording an issue. /// The default value is ``Configuration/current``. /// - isolation: The actor to which `body` is isolated, if any. @@ -208,7 +214,7 @@ extension Issue { /// caught, otherwise `nil`. @discardableResult static func withErrorRecording( - at sourceLocation: SourceLocation, + at sourceLocation: SourceLocation?, configuration: Configuration? = nil, isolation: isolated (any Actor)? = #isolation, _ body: () async throws -> Void @@ -226,6 +232,10 @@ extension Issue { // This error is thrown by expectation checking functions to indicate a // condition evaluated to `false`. Those functions record their own issue, // so we don't need to record another one redundantly. + } catch is SkipInfo, + is CancellationError where Task.isCancelled { + // This error represents control flow rather than an issue, so we suppress + // it here. } catch { let issue = Issue(for: error, sourceLocation: sourceLocation) issue.record(configuration: configuration) diff --git a/Sources/Testing/Running/Runner.Plan.swift b/Sources/Testing/Running/Runner.Plan.swift index c89fdecb5..a1a17d51b 100644 --- a/Sources/Testing/Running/Runner.Plan.swift +++ b/Sources/Testing/Running/Runner.Plan.swift @@ -193,6 +193,57 @@ extension Runner.Plan { synthesizeSuites(in: &graph, sourceLocation: &sourceLocation) } + /// The basic "run" action. + private static let _runAction = Action.run(options: .init()) + + /// Determine what action to perform for a given test by preparing its traits. + /// + /// - Parameters: + /// - test: The test whose action will be determined. + /// + /// - Returns: A tuple containing the action to take for `test` as well as any + /// error that was thrown during trait evaluation. If more than one error + /// was thrown, the first-caught error is returned. + private static func _determineAction(for test: Test) async -> (Action, (any Error)?) { + // We use a task group here with a single child task so that, if the trait + // code calls Test.cancel() we don't end up cancelling the entire test run. + // We could also model this as an unstructured task except that they aren't + // available in the "task-to-thread" concurrency model. + // + // FIXME: Parallelize this work. Calling `prepare(...)` on all traits and + // evaluating all test arguments should be safely parallelizable. + await withTaskGroup(returning: (Action, (any Error)?).self) { taskGroup in + taskGroup.addTask { + var action = _runAction + var firstCaughtError: (any Error)? + + await Test.withCurrent(test) { + for trait in test.traits { + do { + try await trait.prepare(for: test) + } catch let error as SkipInfo { + action = .skip(error) + break + } catch is CancellationError where Task.isCancelled { + // Synthesize skip info for this cancellation error. + let sourceContext = SourceContext(backtrace: .current(), sourceLocation: nil) + let skipInfo = SkipInfo(comment: nil, sourceContext: sourceContext) + action = .skip(skipInfo) + break + } catch { + // Only preserve the first caught error + firstCaughtError = firstCaughtError ?? error + } + } + } + + return (action, firstCaughtError) + } + + return await taskGroup.first { _ in true }! + } + } + /// Construct a graph of runner plan steps for the specified tests. /// /// - Parameters: @@ -211,7 +262,7 @@ extension Runner.Plan { // Convert the list of test into a graph of steps. The actions for these // steps will all be .run() *unless* an error was thrown while examining // them, in which case it will be .recordIssue(). - let runAction = Action.run(options: .init()) + let runAction = _runAction var testGraph = Graph() var actionGraph = Graph(value: runAction) for test in tests { @@ -251,9 +302,6 @@ extension Runner.Plan { _recursivelyApplyTraits(to: &testGraph) // For each test value, determine the appropriate action for it. - // - // FIXME: Parallelize this work. Calling `prepare(...)` on all traits and - // evaluating all test arguments should be safely parallelizable. testGraph = await testGraph.mapValues { keyPath, test in // Skip any nil test, which implies this node is just a placeholder and // not actual test content. @@ -269,17 +317,7 @@ extension Runner.Plan { // But if any throw another kind of error, keep track of the first error // but continue walking, because if any subsequent traits throw a // `SkipInfo`, the error should not be recorded. - for trait in test.traits { - do { - try await trait.prepare(for: test) - } catch let error as SkipInfo { - action = .skip(error) - break - } catch { - // Only preserve the first caught error - firstCaughtError = firstCaughtError ?? error - } - } + (action, firstCaughtError) = await _determineAction(for: test) // If no trait specified that the test should be skipped, but one did // throw an error, then the action is to record an issue for that error. diff --git a/Sources/Testing/Running/Runner.RuntimeState.swift b/Sources/Testing/Running/Runner.RuntimeState.swift index 9ae299412..e88cea60b 100644 --- a/Sources/Testing/Running/Runner.RuntimeState.swift +++ b/Sources/Testing/Running/Runner.RuntimeState.swift @@ -206,7 +206,9 @@ extension Test { static func withCurrent(_ test: Self, perform body: () async throws -> R) async rethrows -> R { var runtimeState = Runner.RuntimeState.current ?? .init() runtimeState.test = test - return try await Runner.RuntimeState.$current.withValue(runtimeState, operation: body) + return try await Runner.RuntimeState.$current.withValue(runtimeState) { + try await test.withCancellationHandling(body) + } } } @@ -239,7 +241,9 @@ extension Test.Case { static func withCurrent(_ testCase: Self, perform body: () async throws -> R) async rethrows -> R { var runtimeState = Runner.RuntimeState.current ?? .init() runtimeState.testCase = testCase - return try await Runner.RuntimeState.$current.withValue(runtimeState, operation: body) + return try await Runner.RuntimeState.$current.withValue(runtimeState) { + try await testCase.withCancellationHandling(body) + } } } diff --git a/Sources/Testing/Running/Runner.swift b/Sources/Testing/Running/Runner.swift index bd1167b8e..d5a844fba 100644 --- a/Sources/Testing/Running/Runner.swift +++ b/Sources/Testing/Running/Runner.swift @@ -155,11 +155,11 @@ extension Runner { private static func _forEach( in sequence: some Sequence, _ body: @Sendable @escaping (E) async throws -> Void - ) async throws where E: Sendable { + ) async rethrows where E: Sendable { try await withThrowingTaskGroup { taskGroup in for element in sequence { // Each element gets its own subtask to run in. - _ = taskGroup.addTaskUnlessCancelled { + taskGroup.addTask { try await body(element) } @@ -190,9 +190,6 @@ extension Runner { /// /// - ``Runner/run()`` private static func _runStep(atRootOf stepGraph: Graph) async throws { - // Exit early if the task has already been cancelled. - try Task.checkCancellation() - // Whether to send a `.testEnded` event at the end of running this step. // Some steps' actions may not require a final event to be sent — for // example, a skip event only sends `.testSkipped`. @@ -243,10 +240,13 @@ extension Runner { if let step = stepGraph.value, case .run = step.action { await Test.withCurrent(step.test) { _ = await Issue.withErrorRecording(at: step.test.sourceLocation, configuration: configuration) { + // Exit early if the task has already been cancelled. + try Task.checkCancellation() + try await _applyScopingTraits(for: step.test, testCase: nil) { // Run the test function at this step (if one is present.) if let testCases = step.test.testCases { - try await _runTestCases(testCases, within: step) + await _runTestCases(testCases, within: step) } // Run the children of this test (i.e. the tests in this suite.) @@ -326,20 +326,17 @@ extension Runner { /// - testCases: The test cases to be run. /// - step: The runner plan step associated with this test case. /// - /// - Throws: Whatever is thrown from a test case's body. Thrown errors are - /// normally reported as test failures. - /// /// If parallelization is supported and enabled, the generated test cases will /// be run in parallel using a task group. - private static func _runTestCases(_ testCases: some Sequence, within step: Plan.Step) async throws { + private static func _runTestCases(_ testCases: some Sequence, within step: Plan.Step) async { // Apply the configuration's test case filter. let testCaseFilter = _configuration.testCaseFilter let testCases = testCases.lazy.filter { testCase in testCaseFilter(testCase, step.test) } - try await _forEach(in: testCases) { testCase in - try await _runTestCase(testCase, within: step) + await _forEach(in: testCases) { testCase in + await _runTestCase(testCase, within: step) } } @@ -349,15 +346,9 @@ extension Runner { /// - testCase: The test case to run. /// - step: The runner plan step associated with this test case. /// - /// - Throws: Whatever is thrown from the test case's body. Thrown errors - /// are normally reported as test failures. - /// /// This function sets ``Test/Case/current``, then invokes the test case's /// body closure. - private static func _runTestCase(_ testCase: Test.Case, within step: Plan.Step) async throws { - // Exit early if the task has already been cancelled. - try Task.checkCancellation() - + private static func _runTestCase(_ testCase: Test.Case, within step: Plan.Step) async { let configuration = _configuration Event.post(.testCaseStarted, for: (step.test, testCase), configuration: configuration) @@ -368,6 +359,9 @@ extension Runner { await Test.Case.withCurrent(testCase) { let sourceLocation = step.test.sourceLocation await Issue.withErrorRecording(at: sourceLocation, configuration: configuration) { + // Exit early if the task has already been cancelled. + try Task.checkCancellation() + try await withTimeLimit(for: step.test, configuration: configuration) { try await _applyScopingTraits(for: step.test, testCase: testCase) { try await testCase.body() diff --git a/Sources/Testing/Test+Cancellation.swift b/Sources/Testing/Test+Cancellation.swift new file mode 100644 index 000000000..b1dfce93f --- /dev/null +++ b/Sources/Testing/Test+Cancellation.swift @@ -0,0 +1,293 @@ +// +// 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 +// + +/// A protocol describing cancellable tests and test cases. +/// +/// This protocol is used to abstract away the common implementation of test and +/// test case cancellation. +protocol TestCancellable: Sendable { + /// Cancel the current instance of this type. + /// + /// - Parameters: + /// - comments: Comments describing why you are cancelling the test/case. + /// - sourceContext: The source context to which the testing library will + /// attribute the cancellation. + /// + /// - Throws: An error indicating that the current instance of this type has + /// been cancelled. + /// + /// Note that the public ``Test/cancel(_:sourceLocation:)`` function has a + /// different signature and accepts a source location rather than a source + /// context value. + static func cancel(comments: [Comment], sourceContext: @autoclosure () -> SourceContext) throws -> Never + + /// Make an instance of ``Event/Kind`` appropriate for an instance of this + /// type. + /// + /// - Parameters: + /// - skipInfo: The ``SkipInfo`` structure describing the cancellation. + /// + /// - Returns: An instance of ``Event/Kind`` that describes the cancellation. + static func makeCancelledEventKind(with skipInfo: SkipInfo) -> Event.Kind +} + +// MARK: - Tracking the current task + +/// A structure describing a reference to a task that is associated with some +/// ``TestCancellable`` value. +private struct _TaskReference: Sendable { + /// The unsafe underlying reference to the associated task. + private nonisolated(unsafe) var _unsafeCurrentTask = Locked() + + init() { + let unsafeCurrentTask = withUnsafeCurrentTask { $0 } + _unsafeCurrentTask = Locked(rawValue: unsafeCurrentTask) + } + + /// Take this instance's reference to its associated task. + /// + /// - Returns: An `UnsafeCurrentTask` instance, or `nil` if it was already + /// taken or if it was never available. + /// + /// This function consumes the reference to the task. After the first call, + /// subsequent calls on the same instance return `nil`. + func takeUnsafeCurrentTask() -> UnsafeCurrentTask? { + _unsafeCurrentTask.withLock { unsafeCurrentTask in + let result = unsafeCurrentTask + unsafeCurrentTask = nil + return result + } + } +} + +/// A dictionary of tracked tasks, keyed by types that conform to +/// ``TestCancellable``. +@TaskLocal +private var _currentTaskReferences = [ObjectIdentifier: _TaskReference]() + +extension TestCancellable { + /// Call a function while the ``unsafeCurrentTask`` property of this instance + /// is set to the current task. + /// + /// - Parameters: + /// - body: The function to invoke. + /// + /// - Returns: Whatever is returned by `body`. + /// + /// - Throws: Whatever is thrown by `body`. + /// + /// This function sets up a task cancellation handler and calls `body`. If + /// the current task, test, or test case is cancelled, it records a + /// corresponding cancellation event. + func withCancellationHandling(_ body: () async throws -> R) async rethrows -> R { + var currentTaskReferences = _currentTaskReferences + currentTaskReferences[ObjectIdentifier(Self.self)] = _TaskReference() + return try await $_currentTaskReferences.withValue(currentTaskReferences) { + try await withTaskCancellationHandler { + try await body() + } onCancel: { + // The current task was cancelled, so cancel the test case or test + // associated with it. + _ = try? Self.cancel( + comments: [], + sourceContext: SourceContext(backtrace: .current(), sourceLocation: nil) + ) + } + } + } +} + +// MARK: - + +/// The common implementation of cancellation for ``Test`` and ``Test/Case``. +/// +/// - Parameters: +/// - cancellableValue: The test or test case to cancel, or `nil` if neither +/// is set and we need fallback handling. +/// - testAndTestCase: The test and test case to use when posting an event. +/// - comments: Comments describing why you are cancelling the test/case. +/// - sourceContext: The source context to which the testing library will +/// attribute the cancellation. +/// +/// - Throws: An instance of ``SkipInfo`` describing the cancellation. +private func _cancel(_ cancellableValue: T?, for testAndTestCase: (Test?, Test.Case?), comments: [Comment], sourceContext: @autoclosure () -> SourceContext) throws -> Never where T: TestCancellable { + var skipInfo = SkipInfo(comment: comments.first, sourceContext: .init(backtrace: nil, sourceLocation: nil)) + + if cancellableValue != nil { + // If the current test case is still running, cancel its task and clear its + // task property (which signals that it has been cancelled.) + let task = _currentTaskReferences[ObjectIdentifier(T.self)]?.takeUnsafeCurrentTask() + task?.cancel() + + // If we just cancelled the current test case's task, post a corresponding + // event with the relevant skip info. + if task != nil { + skipInfo.sourceContext = sourceContext() + Event.post(T.makeCancelledEventKind(with: skipInfo), for: testAndTestCase) + } + } else { + // The current task isn't associated with a test/case, so just cancel the + // task. + withUnsafeCurrentTask { task in + task?.cancel() + } + + if ExitTest.current != nil { + // This code is running in an exit test. We don't have a "current test" or + // "current test case" in the child process, so we'll let the parent + // process sort that out. + skipInfo.sourceContext = sourceContext() + Event.post(T.makeCancelledEventKind(with: skipInfo), for: (nil, nil)) + } else { + // Record an API misuse issue for trying to cancel the current test/case + // outside of any useful context. + let comments = ["Attempted to cancel the current test or test case, but one is not associated with the current task."] + comments + let issue = Issue(kind: .apiMisused, comments: comments, sourceContext: sourceContext()) + issue.record() + } + } + + throw skipInfo +} + +// MARK: - Test cancellation + +extension Test: TestCancellable { + /// Cancel the current test. + /// + /// - Parameters: + /// - comment: A comment describing why you are cancelling the test. + /// - sourceLocation: The source location to which the testing library will + /// attribute the cancellation. + /// + /// - Throws: An error indicating that the current test case has been + /// cancelled. + /// + /// The testing library runs each test in its own task. When you call this + /// function, the testing library cancels the task associated with the current + /// test: + /// + /// ```swift + /// @Test func `Food truck is well-stocked`() throws { + /// guard businessHours.contains(.now) else { + /// try Test.cancel("We're off the clock.") + /// } + /// // ... + /// } + /// ``` + /// + /// If the current test is parameterized, all of its pending and running test + /// cases are cancelled. If the current test is a suite, all of its pending + /// and running tests are cancelled. If you have already cancelled the current + /// test or if it has already finished running, this function throws an error + /// but does not attempt to cancel the test a second time. + /// + /// @Comment { + /// TODO: Document the interaction between an exit test and test + /// cancellation. In particular, the error thrown by this function isn't + /// thrown into the parent process and task cancellation doesn't propagate + /// (because the exit test _de facto_ runs in a detached task.) + /// } + /// + /// - Important: If the current task is not associated with a test (for + /// example, because it was created with [`Task.detached(name:priority:operation:)`](https://developer.apple.com/documentation/swift/task/detached(name:priority:operation:)-795w1)) + /// this function records an issue and cancels the current task. + /// + /// To cancel the current test case but leave other test cases of the current + /// test alone, call ``Test/Case/cancel(_:sourceLocation:)`` instead. + @_spi(Experimental) + public static func cancel(_ comment: Comment? = nil, sourceLocation: SourceLocation = #_sourceLocation) throws -> Never { + try Self.cancel( + comments: Array(comment), + sourceContext: SourceContext(backtrace: .current(), sourceLocation: sourceLocation) + ) + } + + static func cancel(comments: [Comment], sourceContext: @autoclosure () -> SourceContext) throws -> Never { + let test = Test.current + try _cancel(test, for: (test, nil), comments: comments, sourceContext: sourceContext()) + } + + static func makeCancelledEventKind(with skipInfo: SkipInfo) -> Event.Kind { + .testCancelled(skipInfo) + } +} + +// MARK: - Test case cancellation + +extension Test.Case: TestCancellable { + /// Cancel the current test case. + /// + /// - Parameters: + /// - comment: A comment describing why you are cancelling the test case. + /// - sourceLocation: The source location to which the testing library will + /// attribute the cancellation. + /// + /// - Throws: An error indicating that the current test case has been + /// cancelled. + /// + /// The testing library runs each test case of a test in its own task. When + /// you call this function, the testing library cancels the task associated + /// with the current test case: + /// + /// ```swift + /// @Test(arguments: [Food.burger, .fries, .iceCream]) + /// func `Food truck is well-stocked`(_ food: Food) throws { + /// if food == .iceCream && Season.current == .winter { + /// try Test.Case.cancel("It's too cold for ice cream.") + /// } + /// // ... + /// } + /// ``` + /// + /// If the current test is parameterized, the test's other test cases continue + /// running. If the current test case has already been cancelled, this + /// function throws an error but does not attempt to cancel the test case a + /// second time. + /// + /// @Comment { + /// TODO: Document the interaction between an exit test and test + /// cancellation. In particular, the error thrown by this function isn't + /// thrown into the parent process and task cancellation doesn't propagate + /// (because the exit test _de facto_ runs in a detached task.) + /// } + /// + /// - Important: If the current task is not associated with a test case (for + /// example, because it was created with [`Task.detached(name:priority:operation:)`](https://developer.apple.com/documentation/swift/task/detached(name:priority:operation:)-795w1)) + /// this function records an issue and cancels the current task. + /// + /// To cancel all test cases in the current test, call + /// ``Test/cancel(_:sourceLocation:)`` instead. + @_spi(Experimental) + public static func cancel(_ comment: Comment? = nil, sourceLocation: SourceLocation = #_sourceLocation) throws -> Never { + try Self.cancel( + comments: Array(comment), + sourceContext: SourceContext(backtrace: .current(), sourceLocation: sourceLocation) + ) + } + + static func cancel(comments: [Comment], sourceContext: @autoclosure () -> SourceContext) throws -> Never { + let test = Test.current + let testCase = Test.Case.current + let sourceContext = sourceContext() // evaluated twice, avoid laziness + + do { + // Cancel the current test case (if it's nil, that's the API misuse path.) + try _cancel(testCase, for: (test, testCase), comments: comments, sourceContext: sourceContext) + } catch _ where test?.isParameterized == false { + // The current test is not parameterized, so cancel the whole test too. + try _cancel(test, for: (test, nil), comments: comments, sourceContext: sourceContext) + } + } + + static func makeCancelledEventKind(with skipInfo: SkipInfo) -> Event.Kind { + .testCaseCancelled(skipInfo) + } +} diff --git a/Sources/Testing/Testing.docc/EnablingAndDisabling.md b/Sources/Testing/Testing.docc/EnablingAndDisabling.md index 9fab8eeab..7e3f31bde 100644 --- a/Sources/Testing/Testing.docc/EnablingAndDisabling.md +++ b/Sources/Testing/Testing.docc/EnablingAndDisabling.md @@ -120,3 +120,20 @@ func allIngredientsAvailable(for food: Food) -> Bool { ... } ) func makeSundae() async throws { ... } ``` + + diff --git a/Sources/Testing/Testing.docc/MigratingFromXCTest.md b/Sources/Testing/Testing.docc/MigratingFromXCTest.md index 81434b8c3..fcf1f529d 100644 --- a/Sources/Testing/Testing.docc/MigratingFromXCTest.md +++ b/Sources/Testing/Testing.docc/MigratingFromXCTest.md @@ -556,6 +556,45 @@ test function with an instance of this trait type to control whether it runs: } } + + ### Annotate known issues A test may have a known issue that sometimes or always prevents it from passing. diff --git a/Tests/TestingTests/TestCancellationTests.swift b/Tests/TestingTests/TestCancellationTests.swift new file mode 100644 index 000000000..3d7fb819e --- /dev/null +++ b/Tests/TestingTests/TestCancellationTests.swift @@ -0,0 +1,228 @@ +// +// 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 +// + +@testable @_spi(Experimental) @_spi(ForToolsIntegrationOnly) import Testing + +@Suite(.serialized) struct `Test cancellation tests` { + func testCancellation(testCancelled: Int = 0, testSkipped: Int = 0, testCaseCancelled: Int = 0, issueRecorded: Int = 0, _ body: @Sendable (Configuration) async -> Void) async { + await confirmation("Test cancelled", expectedCount: testCancelled) { testCancelled in + await confirmation("Test skipped", expectedCount: testSkipped) { testSkipped in + await confirmation("Test case cancelled", expectedCount: testCaseCancelled) { testCaseCancelled in + await confirmation("Issue recorded", expectedCount: issueRecorded) { [issueRecordedCount = issueRecorded] issueRecorded in + var configuration = Configuration() + configuration.eventHandler = { event, _ in + switch event.kind { + case .testCancelled: + testCancelled() + case .testSkipped: + testSkipped() + case .testCaseCancelled: + testCaseCancelled() + case let .issueRecorded(issue): + if issueRecordedCount == 0 { + issue.record() + } + issueRecorded() + default: + break + } + } +#if !SWT_NO_EXIT_TESTS + configuration.exitTestHandler = ExitTest.handlerForEntryPoint() +#endif + await body(configuration) + } + } + } + } + } + + @Test func `Cancelling a test`() async { + await testCancellation(testCancelled: 1, testCaseCancelled: 1) { configuration in + await Test { + try Test.cancel("Cancelled test") + }.run(configuration: configuration) + } + } + + @Test func `Cancelling a non-parameterized test via Test.Case.cancel()`() async { + await testCancellation(testCancelled: 1, testCaseCancelled: 1) { configuration in + await Test { + try Test.Case.cancel("Cancelled test") + }.run(configuration: configuration) + } + } + + @Test func `Cancelling a test case in a parameterized test`() async { + await testCancellation(testCaseCancelled: 5, issueRecorded: 5) { configuration in + await Test(arguments: 0 ..< 10) { i in + if (i % 2) == 0 { + try Test.Case.cancel("\(i) is even!") + } + Issue.record("\(i) records an issue!") + }.run(configuration: configuration) + } + } + + @Test func `Cancelling an entire parameterized test`() async { + await testCancellation(testCancelled: 1, testCaseCancelled: 10) { configuration in + // .serialized to ensure that none of the cases complete before the first + // one cancels the test. + await Test(.serialized, arguments: 0 ..< 10) { i in + if i == 0 { + try Test.cancel("\(i) cancelled the test") + } + Issue.record("\(i) records an issue!") + }.run(configuration: configuration) + } + } + + @Test func `Cancelling a test by cancelling its task (throwing)`() async { + await testCancellation(testCancelled: 1, testCaseCancelled: 1) { configuration in + await Test { + withUnsafeCurrentTask { $0?.cancel() } + try Task.checkCancellation() + }.run(configuration: configuration) + } + } + + @Test func `Cancelling a test by cancelling its task (returning)`() async { + await testCancellation(testCancelled: 1, testCaseCancelled: 1) { configuration in + await Test { + withUnsafeCurrentTask { $0?.cancel() } + }.run(configuration: configuration) + } + } + + @Test func `Throwing CancellationError without cancelling the test task`() async { + await testCancellation(issueRecorded: 1) { configuration in + await Test { + throw CancellationError() + }.run(configuration: configuration) + } + } + + @Test func `Throwing CancellationError while evaluating traits without cancelling the test task`() async { + await testCancellation(issueRecorded: 1) { configuration in + await Test(CancelledTrait(throwsWithoutCancelling: true)) { + }.run(configuration: configuration) + } + } + + @Test func `Cancelling a test while evaluating traits skips the test`() async { + await testCancellation(testSkipped: 1) { configuration in + await Test(CancelledTrait()) { + Issue.record("Recorded an issue!") + }.run(configuration: configuration) + } + } + + @Test func `Cancelling the current task while evaluating traits skips the test`() async { + await testCancellation(testSkipped: 1) { configuration in + await Test(CancelledTrait(cancelsTask: true)) { + Issue.record("Recorded an issue!") + }.run(configuration: configuration) + } + } + +#if !SWT_NO_EXIT_TESTS + @Test func `Cancelling the current test from within an exit test`() async { + await testCancellation(testCancelled: 1, testCaseCancelled: 1) { configuration in + await Test { + await #expect(processExitsWith: .success) { + try Test.cancel("Cancelled test") + } + #expect(Task.isCancelled) + try Task.checkCancellation() + }.run(configuration: configuration) + } + } + + @Test func `Cancelling the current test case from within an exit test`() async { + await testCancellation(testCancelled: 1, testCaseCancelled: 1) { configuration in + await Test { + await #expect(processExitsWith: .success) { + try Test.Case.cancel("Cancelled test") + } + #expect(Task.isCancelled) + try Task.checkCancellation() + }.run(configuration: configuration) + } + } + + @Test func `Cancelling the current task in an exit test doesn't cancel the test`() async { + await testCancellation(testCancelled: 0, testCaseCancelled: 0) { configuration in + await Test { + await #expect(processExitsWith: .success) { + withUnsafeCurrentTask { $0?.cancel() } + } + #expect(!Task.isCancelled) + try Task.checkCancellation() + }.run(configuration: configuration) + } + } +#endif +} + +#if canImport(XCTest) +import XCTest + +final class TestCancellationTests: XCTestCase { + func testCancellationFromBackgroundTask() async { + let testCancelled = expectation(description: "Test cancelled") + testCancelled.isInverted = true + + let issueRecorded = expectation(description: "Issue recorded") + + var configuration = Configuration() + configuration.eventHandler = { event, _ in + if case .testCancelled = event.kind { + testCancelled.fulfill() + } else if case .issueRecorded = event.kind { + issueRecorded.fulfill() + } + } + + await Test { + await Task.detached { + _ = try? Test.cancel("Cancelled test") + }.value + }.run(configuration: configuration) + + await fulfillment(of: [testCancelled, issueRecorded], timeout: 0.0) + } +} +#endif + +// MARK: - Fixtures + +struct CancelledTrait: TestTrait { + var throwsWithoutCancelling = false + var cancelsTask = false + + func prepare(for test: Test) async throws { + if throwsWithoutCancelling { + throw CancellationError() + } + if cancelsTask { + withUnsafeCurrentTask { $0?.cancel() } + try Task.checkCancellation() + } + try Test.cancel("Cancelled from trait") + } +} + +#if !SWT_NO_SNAPSHOT_TYPES +struct `Shows as skipped in Xcode 16` { + @Test func `Cancelled test`() throws { + try Test.cancel("This test should appear cancelled/skipped") + } +} +#endif diff --git a/Tests/TestingTests/TestSupport/TestingAdditions.swift b/Tests/TestingTests/TestSupport/TestingAdditions.swift index 5c596785e..30d53ce7f 100644 --- a/Tests/TestingTests/TestSupport/TestingAdditions.swift +++ b/Tests/TestingTests/TestSupport/TestingAdditions.swift @@ -187,7 +187,9 @@ extension Test { init( _ traits: any TestTrait..., arguments collection: C, - parameters: [Parameter] = [], + parameters: [Parameter] = [ + Parameter(index: 0, firstName: "x", type: C.Element.self), + ], sourceLocation: SourceLocation = #_sourceLocation, column: Int = #column, name: String = #function, @@ -216,7 +218,10 @@ extension Test { init( _ traits: any TestTrait..., arguments collection1: C1, _ collection2: C2, - parameters: [Parameter] = [], + parameters: [Parameter] = [ + Parameter(index: 0, firstName: "x", type: C1.Element.self), + Parameter(index: 1, firstName: "y", type: C2.Element.self), + ], sourceLocation: SourceLocation = #_sourceLocation, name: String = #function, testFunction: @escaping @Sendable (C1.Element, C2.Element) async throws -> Void @@ -239,7 +244,10 @@ extension Test { init( _ traits: any TestTrait..., arguments zippedCollections: Zip2Sequence, - parameters: [Parameter] = [], + parameters: [Parameter] = [ + Parameter(index: 0, firstName: "x", type: C1.Element.self), + Parameter(index: 1, firstName: "y", type: C2.Element.self), + ], sourceLocation: SourceLocation = #_sourceLocation, name: String = #function, testFunction: @escaping @Sendable ((C1.Element, C2.Element)) async throws -> Void From d645e480a6758c27d61413afdc968793e4e3e91b Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Wed, 27 Aug 2025 19:55:33 -0400 Subject: [PATCH 114/216] Correct bug in cancellation code when building on platforms without exit tests --- Sources/Testing/Test+Cancellation.swift | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Sources/Testing/Test+Cancellation.swift b/Sources/Testing/Test+Cancellation.swift index b1dfce93f..5b3b3b530 100644 --- a/Sources/Testing/Test+Cancellation.swift +++ b/Sources/Testing/Test+Cancellation.swift @@ -139,7 +139,11 @@ private func _cancel(_ cancellableValue: T?, for testAndTestCase: (Test?, Tes task?.cancel() } - if ExitTest.current != nil { + var inExitTest = false +#if !SWT_NO_EXIT_TESTS + inExitTest = (ExitTest.current != nil) +#endif + if inExitTest { // This code is running in an exit test. We don't have a "current test" or // "current test case" in the child process, so we'll let the parent // process sort that out. From b731280b575ef574b7ed1b1e07e68944ffcc0c97 Mon Sep 17 00:00:00 2001 From: Stuart Montgomery Date: Thu, 28 Aug 2025 09:45:43 -0500 Subject: [PATCH 115/216] Clarify note about supported target types in "Defining test functions" article (#1291) --- Sources/Testing/Testing.docc/DefiningTests.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Sources/Testing/Testing.docc/DefiningTests.md b/Sources/Testing/Testing.docc/DefiningTests.md index 6b8cc4f00..237a64b4e 100644 --- a/Sources/Testing/Testing.docc/DefiningTests.md +++ b/Sources/Testing/Testing.docc/DefiningTests.md @@ -25,8 +25,9 @@ contains the test: import Testing ``` -- Note: Only import the testing library into a test target. Importing the - testing library into an application, library, or binary target isn't +- Note: Only import the testing library into a test target or library meant for + test targets. Importing the testing library into a target intended for + distribution such as an application, app library, or executable target isn't supported or recommended. Test functions aren't stripped from binaries when building for release, so logic and fixtures of a test may be visible to anyone who inspects a build product that contains a test function. From d0917ae3b6f17424b9549772036de4b22f20b850 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Thu, 28 Aug 2025 16:31:14 -0400 Subject: [PATCH 116/216] Silence warnings about CMakeList.txt when building the package. (#1293) This PR silences warnings from SwiftPM about untracked files named "CMakeList.txt" in the new overlay targets. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- Package.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Package.swift b/Package.swift index f9f302e08..35331296f 100644 --- a/Package.swift +++ b/Package.swift @@ -218,6 +218,7 @@ let package = Package( "_Testing_CoreGraphics", ], path: "Sources/Overlays/_Testing_AppKit", + exclude: ["CMakeLists.txt"], swiftSettings: .packageSettings + .enableLibraryEvolution() ), .target( @@ -226,6 +227,7 @@ let package = Package( "Testing", ], path: "Sources/Overlays/_Testing_CoreGraphics", + exclude: ["CMakeLists.txt"], swiftSettings: .packageSettings + .enableLibraryEvolution() ), .target( @@ -235,6 +237,7 @@ let package = Package( "_Testing_CoreGraphics", ], path: "Sources/Overlays/_Testing_CoreImage", + exclude: ["CMakeLists.txt"], swiftSettings: .packageSettings + .enableLibraryEvolution() ), .target( @@ -257,6 +260,7 @@ let package = Package( "_Testing_CoreImage", ], path: "Sources/Overlays/_Testing_UIKit", + exclude: ["CMakeLists.txt"], swiftSettings: .packageSettings + .enableLibraryEvolution() ), .target( From 45e8d695de88d210b93318d44c8a1cac34d90db8 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Tue, 2 Sep 2025 13:50:11 -0400 Subject: [PATCH 117/216] Rename `clsid` to `encoderCLSID` and add label to corresponding `init()`. (#1295) This PR adjusts the (experimental) extensions to `AttachableImageFormat` used for Windows support so that the `CLSID` value we use is explicitly defined in the API (not just documentation) as the _encoder_ CLSID. The Windows SDK also specifies _decoder_ CLSIDs and _container GUIDs_ which could be mistakenly passed here but which won't work correctly. Also some tweaks to documentation on the Apple side to match changes on the Windows side. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- .../AttachableImageFormat+UTType.swift | 3 + .../AttachableImageFormat+CLSID.swift | 59 +++++++++++-------- ...chableImageWrapper+AttachableWrapper.swift | 4 +- Tests/TestingTests/AttachmentTests.swift | 4 +- 4 files changed, 42 insertions(+), 28 deletions(-) diff --git a/Sources/Overlays/_Testing_CoreGraphics/Attachments/AttachableImageFormat+UTType.swift b/Sources/Overlays/_Testing_CoreGraphics/Attachments/AttachableImageFormat+UTType.swift index 4a0146c72..f77945687 100644 --- a/Sources/Overlays/_Testing_CoreGraphics/Attachments/AttachableImageFormat+UTType.swift +++ b/Sources/Overlays/_Testing_CoreGraphics/Attachments/AttachableImageFormat+UTType.swift @@ -63,6 +63,9 @@ extension AttachableImageFormat { /// The content type corresponding to this image format. /// + /// For example, if this image format equals ``png``, the value of this + /// property equals [`UTType.png`](https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/png). + /// /// The value of this property always conforms to [`UTType.image`](https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/image). /// /// @Metadata { diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableImageFormat+CLSID.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableImageFormat+CLSID.swift index 1881fa036..dcdec67f2 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableImageFormat+CLSID.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableImageFormat+CLSID.swift @@ -125,7 +125,7 @@ extension AttachableImageFormat { /// /// - Returns: An instance of `CLSID` referring to a a WIC image encoder, or /// `nil` if one could not be determined. - private static func _computeCLSID(forPathExtension pathExtension: UnsafePointer) -> CLSID? { + private static func _computeEncoderCLSID(forPathExtension pathExtension: UnsafePointer) -> CLSID? { let encoderPathExtensionsByCLSID = (try? _encoderPathExtensionsByCLSID.get()) ?? [:] return encoderPathExtensionsByCLSID .first { _, extensions in @@ -145,9 +145,9 @@ extension AttachableImageFormat { /// /// - Returns: An instance of `CLSID` referring to a a WIC image encoder, or /// `nil` if one could not be determined. - private static func _computeCLSID(forPathExtension pathExtension: String) -> CLSID? { + private static func _computeEncoderCLSID(forPathExtension pathExtension: String) -> CLSID? { pathExtension.withCString(encodedAs: UTF16.self) { pathExtension in - _computeCLSID(forPathExtension: pathExtension) + _computeEncoderCLSID(forPathExtension: pathExtension) } } @@ -160,14 +160,14 @@ extension AttachableImageFormat { /// /// - Returns: An instance of `CLSID` referring to a a WIC image encoder, or /// `nil` if one could not be determined. - private static func _computeCLSID(forPreferredName preferredName: String) -> CLSID? { + private static func _computeEncoderCLSID(forPreferredName preferredName: String) -> CLSID? { preferredName.withCString(encodedAs: UTF16.self) { (preferredName) -> CLSID? in // Get the path extension on the preferred name, if any. var dot: PCWSTR? guard S_OK == PathCchFindExtension(preferredName, wcslen(preferredName) + 1, &dot), let dot, dot[0] != 0 else { return nil } - return _computeCLSID(forPathExtension: dot + 1) + return _computeEncoderCLSID(forPathExtension: dot + 1) } } @@ -185,14 +185,14 @@ extension AttachableImageFormat { /// encoder is used. /// /// This function is not part of the public interface of the testing library. - static func computeCLSID(for imageFormat: Self?, withPreferredName preferredName: String) -> CLSID { - if let clsid = imageFormat?.clsid { + static func computeEncoderCLSID(for imageFormat: Self?, withPreferredName preferredName: String) -> CLSID { + if let clsid = imageFormat?.encoderCLSID { return clsid } // The developer didn't specify a CLSID, or we couldn't figure one out from // context, so try to derive one from the preferred name's path extension. - if let inferredCLSID = _computeCLSID(forPreferredName: preferredName) { + if let inferredCLSID = _computeEncoderCLSID(forPreferredName: preferredName) { return inferredCLSID } @@ -215,7 +215,7 @@ extension AttachableImageFormat { static func appendPathExtension(for clsid: CLSID, to preferredName: String) -> String { // If there's already a CLSID associated with the filename, and it matches // the one passed to us, no changes are needed. - if let existingCLSID = _computeCLSID(forPreferredName: preferredName), clsid == existingCLSID { + if let existingCLSID = _computeEncoderCLSID(forPreferredName: preferredName), clsid == existingCLSID { return preferredName } @@ -229,9 +229,12 @@ extension AttachableImageFormat { return preferredName } - /// The `CLSID` value corresponding to the WIC image encoder for this image - /// format. - public var clsid: CLSID { + /// The `CLSID` value of the Windows Imaging Component (WIC) encoder class + /// that corresponds to this image format. + /// + /// For example, if this image format equals ``png``, the value of this + /// property equals [`CLSID_WICPngEncoder`](https://learn.microsoft.com/en-us/windows/win32/wic/-wic-guids-clsids#wic-guids-and-clsids). + public var encoderCLSID: CLSID { switch kind { case .png: CLSID_WICPngEncoder @@ -242,12 +245,12 @@ extension AttachableImageFormat { } } - /// Construct an instance of this type with the given `CLSID` value and - /// encoding quality. + /// Construct an instance of this type with the `CLSID` value of a Windows + /// Imaging Component (WIC) encoder class and the desired encoding quality. /// /// - Parameters: - /// - clsid: The `CLSID` value corresponding to a WIC image encoder to use - /// when encoding images. + /// - encoderCLSID: The `CLSID` value of the Windows Imaging Component + /// encoder class to use when encoding images. /// - encodingQuality: The encoding quality to use when encoding images. For /// the lowest supported quality, pass `0.0`. For the highest supported /// quality, pass `1.0`. @@ -255,12 +258,18 @@ extension AttachableImageFormat { /// If the target image encoder does not support variable-quality encoding, /// the value of the `encodingQuality` argument is ignored. /// - /// If `clsid` does not represent an image encoder type supported by WIC, the - /// result is undefined. For a list of image encoders supported by WIC, see - /// the documentation for the [`IWICBitmapEncoder`](https://learn.microsoft.com/en-us/windows/win32/api/wincodec/nn-wincodec-iwicbitmapencoder) + /// If `clsid` does not represent an image encoder class supported by WIC, the + /// result is undefined. For a list of image encoder classes supported by WIC, + /// see the documentation for the [`IWICBitmapEncoder`](https://learn.microsoft.com/en-us/windows/win32/api/wincodec/nn-wincodec-iwicbitmapencoder) /// class. - public init(_ clsid: CLSID, encodingQuality: Float = 1.0) { - self.init(kind: .systemValue(clsid), encodingQuality: encodingQuality) + public init(encoderCLSID: CLSID, encodingQuality: Float = 1.0) { + if encoderCLSID == CLSID_WICPngEncoder { + self = .png + } else if encoderCLSID == CLSID_WICJpegEncoder { + self = .jpeg + } else { + self.init(kind: .systemValue(encoderCLSID), encodingQuality: encodingQuality) + } } /// Construct an instance of this type with the given path extension and @@ -286,11 +295,13 @@ extension AttachableImageFormat { public init?(pathExtension: String, encodingQuality: Float = 1.0) { let pathExtension = pathExtension.drop { $0 == "." } - guard let clsid = Self._computeCLSID(forPathExtension: String(pathExtension)) else { + let encoderCLSID = pathExtension.withCString(encodedAs: UTF16.self) { pathExtension in + Self._computeEncoderCLSID(forPathExtension: pathExtension) + } + guard let encoderCLSID else { return nil } - - self.init(clsid, encodingQuality: encodingQuality) + self.init(encoderCLSID: encoderCLSID, encodingQuality: encodingQuality) } } #endif diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/_AttachableImageWrapper+AttachableWrapper.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/_AttachableImageWrapper+AttachableWrapper.swift index dd5fc1da3..d80c2eb01 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/_AttachableImageWrapper+AttachableWrapper.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/_AttachableImageWrapper+AttachableWrapper.swift @@ -41,7 +41,7 @@ extension _AttachableImageWrapper: Attachable, AttachableWrapper where Image: At // Create the encoder. let encoder = try withUnsafePointer(to: IID_IWICBitmapEncoder) { [preferredName = attachment.preferredName] IID_IWICBitmapEncoder in - var encoderCLSID = AttachableImageFormat.computeCLSID(for: imageFormat, withPreferredName: preferredName) + var encoderCLSID = AttachableImageFormat.computeEncoderCLSID(for: imageFormat, withPreferredName: preferredName) var encoder: UnsafeMutableRawPointer? let rCreate = CoCreateInstance( &encoderCLSID, @@ -117,7 +117,7 @@ extension _AttachableImageWrapper: Attachable, AttachableWrapper where Image: At } public borrowing func preferredName(for attachment: borrowing Attachment<_AttachableImageWrapper>, basedOn suggestedName: String) -> String { - let clsid = AttachableImageFormat.computeCLSID(for: imageFormat, withPreferredName: suggestedName) + let clsid = AttachableImageFormat.computeEncoderCLSID(for: imageFormat, withPreferredName: suggestedName) return AttachableImageFormat.appendPathExtension(for: clsid, to: suggestedName) } } diff --git a/Tests/TestingTests/AttachmentTests.swift b/Tests/TestingTests/AttachmentTests.swift index 69793d215..faf12ac5b 100644 --- a/Tests/TestingTests/AttachmentTests.swift +++ b/Tests/TestingTests/AttachmentTests.swift @@ -821,11 +821,11 @@ extension AttachmentTests { } @MainActor @Test func pathExtensionAndCLSID() { - let pngCLSID = AttachableImageFormat.png.clsid + let pngCLSID = AttachableImageFormat.png.encoderCLSID let pngFilename = AttachableImageFormat.appendPathExtension(for: pngCLSID, to: "example") #expect(pngFilename == "example.png") - let jpegCLSID = AttachableImageFormat.jpeg.clsid + let jpegCLSID = AttachableImageFormat.jpeg.encoderCLSID let jpegFilename = AttachableImageFormat.appendPathExtension(for: jpegCLSID, to: "example") #expect(jpegFilename == "example.jpeg") From 844bccbe0b0eecb15e399902efc06f149126c604 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Tue, 2 Sep 2025 16:51:04 -0400 Subject: [PATCH 118/216] Extend test/task cancellation support to test case evaluation This PR extends the special-casing of `SkipInfo` and `CancellationError` added in #1284 to also cover test case evaluation during test planning. If either error is thrown from `evaluateTestCases()`, we treat it as test cancellation the same way we do for trait evaluation. --- Sources/Testing/Issues/Issue+Recording.swift | 6 +- Sources/Testing/Running/Runner.Plan.swift | 94 ++++++++++--------- Sources/Testing/Running/SkipInfo.swift | 24 +++++ .../TestingTests/TestCancellationTests.swift | 25 +++++ .../TestSupport/TestingAdditions.swift | 17 ++++ 5 files changed, 117 insertions(+), 49 deletions(-) diff --git a/Sources/Testing/Issues/Issue+Recording.swift b/Sources/Testing/Issues/Issue+Recording.swift index 50cdbbc1f..72f4c65a4 100644 --- a/Sources/Testing/Issues/Issue+Recording.swift +++ b/Sources/Testing/Issues/Issue+Recording.swift @@ -186,8 +186,7 @@ extension Issue { // This error is thrown by expectation checking functions to indicate a // condition evaluated to `false`. Those functions record their own issue, // so we don't need to record another one redundantly. - } catch is SkipInfo, - is CancellationError where Task.isCancelled { + } catch let error where SkipInfo(error) != nil { // This error represents control flow rather than an issue, so we suppress // it here. } catch { @@ -232,8 +231,7 @@ extension Issue { // This error is thrown by expectation checking functions to indicate a // condition evaluated to `false`. Those functions record their own issue, // so we don't need to record another one redundantly. - } catch is SkipInfo, - is CancellationError where Task.isCancelled { + } catch let error where SkipInfo(error) != nil { // This error represents control flow rather than an issue, so we suppress // it here. } catch { diff --git a/Sources/Testing/Running/Runner.Plan.swift b/Sources/Testing/Running/Runner.Plan.swift index a1a17d51b..4bef38942 100644 --- a/Sources/Testing/Running/Runner.Plan.swift +++ b/Sources/Testing/Running/Runner.Plan.swift @@ -201,10 +201,10 @@ extension Runner.Plan { /// - Parameters: /// - test: The test whose action will be determined. /// - /// - Returns: A tuple containing the action to take for `test` as well as any - /// error that was thrown during trait evaluation. If more than one error - /// was thrown, the first-caught error is returned. - private static func _determineAction(for test: Test) async -> (Action, (any Error)?) { + /// - Returns:The action to take for `test`. + private static func _determineAction(for test: inout Test) async -> Action { + let result: Action + // We use a task group here with a single child task so that, if the trait // code calls Test.cancel() we don't end up cancelling the entire test run. // We could also model this as an unstructured task except that they aren't @@ -212,36 +212,64 @@ extension Runner.Plan { // // FIXME: Parallelize this work. Calling `prepare(...)` on all traits and // evaluating all test arguments should be safely parallelizable. - await withTaskGroup(returning: (Action, (any Error)?).self) { taskGroup in + (test, result) = await withTaskGroup(returning: (Test, Action).self) { [test] taskGroup in taskGroup.addTask { + var test = test var action = _runAction - var firstCaughtError: (any Error)? await Test.withCurrent(test) { - for trait in test.traits { + do { + var firstCaughtError: (any Error)? + + for trait in test.traits { + do { + try await trait.prepare(for: test) + } catch { + if let skipInfo = SkipInfo(error) { + action = .skip(skipInfo) + break + } else { + // Only preserve the first caught error + firstCaughtError = firstCaughtError ?? error + } + } + } + + // If no trait specified that the test should be skipped, but one + // did throw an error, then the action is to record an issue for + // that error. + if case .run = action, let error = firstCaughtError { + action = .recordIssue(Issue(for: error)) + } + } + + // If the test is still planned to run (i.e. nothing thus far has + // caused it to be skipped), evaluate its test cases now. + // + // The argument expressions of each test are captured in closures so + // they can be evaluated lazily only once it is determined that the + // test will run, to avoid unnecessary work. But now is the + // appropriate time to evaluate them. + if case .run = action { do { - try await trait.prepare(for: test) - } catch let error as SkipInfo { - action = .skip(error) - break - } catch is CancellationError where Task.isCancelled { - // Synthesize skip info for this cancellation error. - let sourceContext = SourceContext(backtrace: .current(), sourceLocation: nil) - let skipInfo = SkipInfo(comment: nil, sourceContext: sourceContext) - action = .skip(skipInfo) - break + try await test.evaluateTestCases() } catch { - // Only preserve the first caught error - firstCaughtError = firstCaughtError ?? error + if let skipInfo = SkipInfo(error) { + action = .skip(skipInfo) + } else { + action = .recordIssue(Issue(for: error)) + } } } } - return (action, firstCaughtError) + return (test, action) } return await taskGroup.first { _ in true }! } + + return result } /// Construct a graph of runner plan steps for the specified tests. @@ -309,36 +337,12 @@ extension Runner.Plan { return nil } - var action = runAction - var firstCaughtError: (any Error)? - // Walk all the traits and tell each to prepare to run the test. // If any throw a `SkipInfo` error at this stage, stop walking further. // But if any throw another kind of error, keep track of the first error // but continue walking, because if any subsequent traits throw a // `SkipInfo`, the error should not be recorded. - (action, firstCaughtError) = await _determineAction(for: test) - - // If no trait specified that the test should be skipped, but one did - // throw an error, then the action is to record an issue for that error. - if case .run = action, let error = firstCaughtError { - action = .recordIssue(Issue(for: error)) - } - - // If the test is still planned to run (i.e. nothing thus far has caused - // it to be skipped), evaluate its test cases now. - // - // The argument expressions of each test are captured in closures so they - // can be evaluated lazily only once it is determined that the test will - // run, to avoid unnecessary work. But now is the appropriate time to - // evaluate them. - if case .run = action { - do { - try await test.evaluateTestCases() - } catch { - action = .recordIssue(Issue(for: error)) - } - } + var action = await _determineAction(for: &test) // If the test is parameterized but has no cases, mark it as skipped. if case .run = action, let testCases = test.testCases, testCases.first(where: { _ in true }) == nil { diff --git a/Sources/Testing/Running/SkipInfo.swift b/Sources/Testing/Running/SkipInfo.swift index 0c5a6923d..a5f32ebca 100644 --- a/Sources/Testing/Running/SkipInfo.swift +++ b/Sources/Testing/Running/SkipInfo.swift @@ -54,6 +54,30 @@ extension SkipInfo: Equatable, Hashable {} extension SkipInfo: Codable {} +// MARK: - + +extension SkipInfo { + /// Initialize an instance of this type from an arbitrary error. + /// + /// - Parameters: + /// - error: The error to convert to an instance of this type. + /// + /// If `error` does not represent a skip or cancellation event, this + /// initializer returns `nil`. + init?(_ error: any Error) { + if let skipInfo = error as? Self { + self = skipInfo + } else if error is CancellationError, Task.isCancelled { + // Synthesize skip info for this cancellation error. + let backtrace = Backtrace(forFirstThrowOf: error) ?? .current() + let sourceContext = SourceContext(backtrace: backtrace, sourceLocation: nil) + self.init(comment: nil, sourceContext: sourceContext) + } else { + return nil + } + } +} + // MARK: - Deprecated extension SkipInfo { diff --git a/Tests/TestingTests/TestCancellationTests.swift b/Tests/TestingTests/TestCancellationTests.swift index 3d7fb819e..03ab1126f 100644 --- a/Tests/TestingTests/TestCancellationTests.swift +++ b/Tests/TestingTests/TestCancellationTests.swift @@ -132,6 +132,22 @@ } } + @Test func `Cancelling a test while evaluating test cases skips the test`() async { + await testCancellation(testSkipped: 1) { configuration in + await Test(arguments: { try await cancelledTestCases(cancelsTask: false) }) { _ in + Issue.record("Recorded an issue!") + }.run(configuration: configuration) + } + } + + @Test func `Cancelling the current task while evaluating test cases skips the test`() async { + await testCancellation(testSkipped: 1) { configuration in + await Test(arguments: { try await cancelledTestCases(cancelsTask: true) }) { _ in + Issue.record("Recorded an issue!") + }.run(configuration: configuration) + } + } + #if !SWT_NO_EXIT_TESTS @Test func `Cancelling the current test from within an exit test`() async { await testCancellation(testCancelled: 1, testCaseCancelled: 1) { configuration in @@ -219,6 +235,15 @@ struct CancelledTrait: TestTrait { } } +func cancelledTestCases(cancelsTask: Bool) async throws -> EmptyCollection { + if cancelsTask { + withUnsafeCurrentTask { $0?.cancel() } + try Task.checkCancellation() + } + try Test.cancel("Cancelled from trait") +} + + #if !SWT_NO_SNAPSHOT_TYPES struct `Shows as skipped in Xcode 16` { @Test func `Cancelled test`() throws { diff --git a/Tests/TestingTests/TestSupport/TestingAdditions.swift b/Tests/TestingTests/TestSupport/TestingAdditions.swift index 30d53ce7f..05bb05dc8 100644 --- a/Tests/TestingTests/TestSupport/TestingAdditions.swift +++ b/Tests/TestingTests/TestSupport/TestingAdditions.swift @@ -199,6 +199,23 @@ extension Test { self.init(name: name, displayName: name, traits: traits, sourceLocation: sourceLocation, containingTypeInfo: nil, testCases: caseGenerator, parameters: parameters) } + init( + _ traits: any TestTrait..., + arguments collection: @escaping @Sendable () async throws -> C, + parameters: [Parameter] = [ + Parameter(index: 0, firstName: "x", type: C.Element.self), + ], + sourceLocation: SourceLocation = #_sourceLocation, + column: Int = #column, + name: String = #function, + testFunction: @escaping @Sendable (C.Element) async throws -> Void + ) where C: Collection & Sendable, C.Element: Sendable { + let caseGenerator = { @Sendable in + Case.Generator(arguments: try await collection(), parameters: parameters, testFunction: testFunction) + } + self.init(name: name, displayName: name, traits: traits, sourceLocation: sourceLocation, containingTypeInfo: nil, testCases: caseGenerator, parameters: parameters) + } + /// Initialize an instance of this type with a function or closure to call, /// parameterized over two collections of values. /// From 440ecedcb81cf646dda83086c76762bca563b188 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Tue, 2 Sep 2025 17:04:32 -0400 Subject: [PATCH 119/216] Revert "Extend test/task cancellation support to test case evaluation" This reverts commit 844bccbe0b0eecb15e399902efc06f149126c604. --- Sources/Testing/Issues/Issue+Recording.swift | 6 +- Sources/Testing/Running/Runner.Plan.swift | 94 +++++++++---------- Sources/Testing/Running/SkipInfo.swift | 24 ----- .../TestingTests/TestCancellationTests.swift | 25 ----- .../TestSupport/TestingAdditions.swift | 17 ---- 5 files changed, 49 insertions(+), 117 deletions(-) diff --git a/Sources/Testing/Issues/Issue+Recording.swift b/Sources/Testing/Issues/Issue+Recording.swift index 72f4c65a4..50cdbbc1f 100644 --- a/Sources/Testing/Issues/Issue+Recording.swift +++ b/Sources/Testing/Issues/Issue+Recording.swift @@ -186,7 +186,8 @@ extension Issue { // This error is thrown by expectation checking functions to indicate a // condition evaluated to `false`. Those functions record their own issue, // so we don't need to record another one redundantly. - } catch let error where SkipInfo(error) != nil { + } catch is SkipInfo, + is CancellationError where Task.isCancelled { // This error represents control flow rather than an issue, so we suppress // it here. } catch { @@ -231,7 +232,8 @@ extension Issue { // This error is thrown by expectation checking functions to indicate a // condition evaluated to `false`. Those functions record their own issue, // so we don't need to record another one redundantly. - } catch let error where SkipInfo(error) != nil { + } catch is SkipInfo, + is CancellationError where Task.isCancelled { // This error represents control flow rather than an issue, so we suppress // it here. } catch { diff --git a/Sources/Testing/Running/Runner.Plan.swift b/Sources/Testing/Running/Runner.Plan.swift index 4bef38942..a1a17d51b 100644 --- a/Sources/Testing/Running/Runner.Plan.swift +++ b/Sources/Testing/Running/Runner.Plan.swift @@ -201,10 +201,10 @@ extension Runner.Plan { /// - Parameters: /// - test: The test whose action will be determined. /// - /// - Returns:The action to take for `test`. - private static func _determineAction(for test: inout Test) async -> Action { - let result: Action - + /// - Returns: A tuple containing the action to take for `test` as well as any + /// error that was thrown during trait evaluation. If more than one error + /// was thrown, the first-caught error is returned. + private static func _determineAction(for test: Test) async -> (Action, (any Error)?) { // We use a task group here with a single child task so that, if the trait // code calls Test.cancel() we don't end up cancelling the entire test run. // We could also model this as an unstructured task except that they aren't @@ -212,64 +212,36 @@ extension Runner.Plan { // // FIXME: Parallelize this work. Calling `prepare(...)` on all traits and // evaluating all test arguments should be safely parallelizable. - (test, result) = await withTaskGroup(returning: (Test, Action).self) { [test] taskGroup in + await withTaskGroup(returning: (Action, (any Error)?).self) { taskGroup in taskGroup.addTask { - var test = test var action = _runAction + var firstCaughtError: (any Error)? await Test.withCurrent(test) { - do { - var firstCaughtError: (any Error)? - - for trait in test.traits { - do { - try await trait.prepare(for: test) - } catch { - if let skipInfo = SkipInfo(error) { - action = .skip(skipInfo) - break - } else { - // Only preserve the first caught error - firstCaughtError = firstCaughtError ?? error - } - } - } - - // If no trait specified that the test should be skipped, but one - // did throw an error, then the action is to record an issue for - // that error. - if case .run = action, let error = firstCaughtError { - action = .recordIssue(Issue(for: error)) - } - } - - // If the test is still planned to run (i.e. nothing thus far has - // caused it to be skipped), evaluate its test cases now. - // - // The argument expressions of each test are captured in closures so - // they can be evaluated lazily only once it is determined that the - // test will run, to avoid unnecessary work. But now is the - // appropriate time to evaluate them. - if case .run = action { + for trait in test.traits { do { - try await test.evaluateTestCases() + try await trait.prepare(for: test) + } catch let error as SkipInfo { + action = .skip(error) + break + } catch is CancellationError where Task.isCancelled { + // Synthesize skip info for this cancellation error. + let sourceContext = SourceContext(backtrace: .current(), sourceLocation: nil) + let skipInfo = SkipInfo(comment: nil, sourceContext: sourceContext) + action = .skip(skipInfo) + break } catch { - if let skipInfo = SkipInfo(error) { - action = .skip(skipInfo) - } else { - action = .recordIssue(Issue(for: error)) - } + // Only preserve the first caught error + firstCaughtError = firstCaughtError ?? error } } } - return (test, action) + return (action, firstCaughtError) } return await taskGroup.first { _ in true }! } - - return result } /// Construct a graph of runner plan steps for the specified tests. @@ -337,12 +309,36 @@ extension Runner.Plan { return nil } + var action = runAction + var firstCaughtError: (any Error)? + // Walk all the traits and tell each to prepare to run the test. // If any throw a `SkipInfo` error at this stage, stop walking further. // But if any throw another kind of error, keep track of the first error // but continue walking, because if any subsequent traits throw a // `SkipInfo`, the error should not be recorded. - var action = await _determineAction(for: &test) + (action, firstCaughtError) = await _determineAction(for: test) + + // If no trait specified that the test should be skipped, but one did + // throw an error, then the action is to record an issue for that error. + if case .run = action, let error = firstCaughtError { + action = .recordIssue(Issue(for: error)) + } + + // If the test is still planned to run (i.e. nothing thus far has caused + // it to be skipped), evaluate its test cases now. + // + // The argument expressions of each test are captured in closures so they + // can be evaluated lazily only once it is determined that the test will + // run, to avoid unnecessary work. But now is the appropriate time to + // evaluate them. + if case .run = action { + do { + try await test.evaluateTestCases() + } catch { + action = .recordIssue(Issue(for: error)) + } + } // If the test is parameterized but has no cases, mark it as skipped. if case .run = action, let testCases = test.testCases, testCases.first(where: { _ in true }) == nil { diff --git a/Sources/Testing/Running/SkipInfo.swift b/Sources/Testing/Running/SkipInfo.swift index a5f32ebca..0c5a6923d 100644 --- a/Sources/Testing/Running/SkipInfo.swift +++ b/Sources/Testing/Running/SkipInfo.swift @@ -54,30 +54,6 @@ extension SkipInfo: Equatable, Hashable {} extension SkipInfo: Codable {} -// MARK: - - -extension SkipInfo { - /// Initialize an instance of this type from an arbitrary error. - /// - /// - Parameters: - /// - error: The error to convert to an instance of this type. - /// - /// If `error` does not represent a skip or cancellation event, this - /// initializer returns `nil`. - init?(_ error: any Error) { - if let skipInfo = error as? Self { - self = skipInfo - } else if error is CancellationError, Task.isCancelled { - // Synthesize skip info for this cancellation error. - let backtrace = Backtrace(forFirstThrowOf: error) ?? .current() - let sourceContext = SourceContext(backtrace: backtrace, sourceLocation: nil) - self.init(comment: nil, sourceContext: sourceContext) - } else { - return nil - } - } -} - // MARK: - Deprecated extension SkipInfo { diff --git a/Tests/TestingTests/TestCancellationTests.swift b/Tests/TestingTests/TestCancellationTests.swift index 03ab1126f..3d7fb819e 100644 --- a/Tests/TestingTests/TestCancellationTests.swift +++ b/Tests/TestingTests/TestCancellationTests.swift @@ -132,22 +132,6 @@ } } - @Test func `Cancelling a test while evaluating test cases skips the test`() async { - await testCancellation(testSkipped: 1) { configuration in - await Test(arguments: { try await cancelledTestCases(cancelsTask: false) }) { _ in - Issue.record("Recorded an issue!") - }.run(configuration: configuration) - } - } - - @Test func `Cancelling the current task while evaluating test cases skips the test`() async { - await testCancellation(testSkipped: 1) { configuration in - await Test(arguments: { try await cancelledTestCases(cancelsTask: true) }) { _ in - Issue.record("Recorded an issue!") - }.run(configuration: configuration) - } - } - #if !SWT_NO_EXIT_TESTS @Test func `Cancelling the current test from within an exit test`() async { await testCancellation(testCancelled: 1, testCaseCancelled: 1) { configuration in @@ -235,15 +219,6 @@ struct CancelledTrait: TestTrait { } } -func cancelledTestCases(cancelsTask: Bool) async throws -> EmptyCollection { - if cancelsTask { - withUnsafeCurrentTask { $0?.cancel() } - try Task.checkCancellation() - } - try Test.cancel("Cancelled from trait") -} - - #if !SWT_NO_SNAPSHOT_TYPES struct `Shows as skipped in Xcode 16` { @Test func `Cancelled test`() throws { diff --git a/Tests/TestingTests/TestSupport/TestingAdditions.swift b/Tests/TestingTests/TestSupport/TestingAdditions.swift index 05bb05dc8..30d53ce7f 100644 --- a/Tests/TestingTests/TestSupport/TestingAdditions.swift +++ b/Tests/TestingTests/TestSupport/TestingAdditions.swift @@ -199,23 +199,6 @@ extension Test { self.init(name: name, displayName: name, traits: traits, sourceLocation: sourceLocation, containingTypeInfo: nil, testCases: caseGenerator, parameters: parameters) } - init( - _ traits: any TestTrait..., - arguments collection: @escaping @Sendable () async throws -> C, - parameters: [Parameter] = [ - Parameter(index: 0, firstName: "x", type: C.Element.self), - ], - sourceLocation: SourceLocation = #_sourceLocation, - column: Int = #column, - name: String = #function, - testFunction: @escaping @Sendable (C.Element) async throws -> Void - ) where C: Collection & Sendable, C.Element: Sendable { - let caseGenerator = { @Sendable in - Case.Generator(arguments: try await collection(), parameters: parameters, testFunction: testFunction) - } - self.init(name: name, displayName: name, traits: traits, sourceLocation: sourceLocation, containingTypeInfo: nil, testCases: caseGenerator, parameters: parameters) - } - /// Initialize an instance of this type with a function or closure to call, /// parameterized over two collections of values. /// From 5089e36f3325d8bb54f9ed5a21d352b12ad5204e Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Tue, 2 Sep 2025 17:19:44 -0400 Subject: [PATCH 120/216] Extend test/task cancellation support to test case evaluation. (#1297) This PR extends the special-casing of `SkipInfo` and `CancellationError` added in #1284 to also cover test case evaluation during test planning. If either error is thrown from `evaluateTestCases()`, we treat it as test cancellation the same way we do for trait evaluation. Example: ```swift func websites() async throws -> [Website] { guard let www = Web() else { try Test.cancel("The Web doesn't exist, is this 1992?") } return try await www.downloadEverything() } @Test(arguments: try await websites()) func browseWeb(at website: Website) { ... } ``` ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- Sources/Testing/Issues/Issue+Recording.swift | 6 +- Sources/Testing/Running/Runner.Plan.swift | 94 ++++++++++--------- Sources/Testing/Running/SkipInfo.swift | 24 +++++ .../TestingTests/TestCancellationTests.swift | 25 +++++ .../TestSupport/TestingAdditions.swift | 17 ++++ 5 files changed, 117 insertions(+), 49 deletions(-) diff --git a/Sources/Testing/Issues/Issue+Recording.swift b/Sources/Testing/Issues/Issue+Recording.swift index 50cdbbc1f..72f4c65a4 100644 --- a/Sources/Testing/Issues/Issue+Recording.swift +++ b/Sources/Testing/Issues/Issue+Recording.swift @@ -186,8 +186,7 @@ extension Issue { // This error is thrown by expectation checking functions to indicate a // condition evaluated to `false`. Those functions record their own issue, // so we don't need to record another one redundantly. - } catch is SkipInfo, - is CancellationError where Task.isCancelled { + } catch let error where SkipInfo(error) != nil { // This error represents control flow rather than an issue, so we suppress // it here. } catch { @@ -232,8 +231,7 @@ extension Issue { // This error is thrown by expectation checking functions to indicate a // condition evaluated to `false`. Those functions record their own issue, // so we don't need to record another one redundantly. - } catch is SkipInfo, - is CancellationError where Task.isCancelled { + } catch let error where SkipInfo(error) != nil { // This error represents control flow rather than an issue, so we suppress // it here. } catch { diff --git a/Sources/Testing/Running/Runner.Plan.swift b/Sources/Testing/Running/Runner.Plan.swift index a1a17d51b..4bef38942 100644 --- a/Sources/Testing/Running/Runner.Plan.swift +++ b/Sources/Testing/Running/Runner.Plan.swift @@ -201,10 +201,10 @@ extension Runner.Plan { /// - Parameters: /// - test: The test whose action will be determined. /// - /// - Returns: A tuple containing the action to take for `test` as well as any - /// error that was thrown during trait evaluation. If more than one error - /// was thrown, the first-caught error is returned. - private static func _determineAction(for test: Test) async -> (Action, (any Error)?) { + /// - Returns:The action to take for `test`. + private static func _determineAction(for test: inout Test) async -> Action { + let result: Action + // We use a task group here with a single child task so that, if the trait // code calls Test.cancel() we don't end up cancelling the entire test run. // We could also model this as an unstructured task except that they aren't @@ -212,36 +212,64 @@ extension Runner.Plan { // // FIXME: Parallelize this work. Calling `prepare(...)` on all traits and // evaluating all test arguments should be safely parallelizable. - await withTaskGroup(returning: (Action, (any Error)?).self) { taskGroup in + (test, result) = await withTaskGroup(returning: (Test, Action).self) { [test] taskGroup in taskGroup.addTask { + var test = test var action = _runAction - var firstCaughtError: (any Error)? await Test.withCurrent(test) { - for trait in test.traits { + do { + var firstCaughtError: (any Error)? + + for trait in test.traits { + do { + try await trait.prepare(for: test) + } catch { + if let skipInfo = SkipInfo(error) { + action = .skip(skipInfo) + break + } else { + // Only preserve the first caught error + firstCaughtError = firstCaughtError ?? error + } + } + } + + // If no trait specified that the test should be skipped, but one + // did throw an error, then the action is to record an issue for + // that error. + if case .run = action, let error = firstCaughtError { + action = .recordIssue(Issue(for: error)) + } + } + + // If the test is still planned to run (i.e. nothing thus far has + // caused it to be skipped), evaluate its test cases now. + // + // The argument expressions of each test are captured in closures so + // they can be evaluated lazily only once it is determined that the + // test will run, to avoid unnecessary work. But now is the + // appropriate time to evaluate them. + if case .run = action { do { - try await trait.prepare(for: test) - } catch let error as SkipInfo { - action = .skip(error) - break - } catch is CancellationError where Task.isCancelled { - // Synthesize skip info for this cancellation error. - let sourceContext = SourceContext(backtrace: .current(), sourceLocation: nil) - let skipInfo = SkipInfo(comment: nil, sourceContext: sourceContext) - action = .skip(skipInfo) - break + try await test.evaluateTestCases() } catch { - // Only preserve the first caught error - firstCaughtError = firstCaughtError ?? error + if let skipInfo = SkipInfo(error) { + action = .skip(skipInfo) + } else { + action = .recordIssue(Issue(for: error)) + } } } } - return (action, firstCaughtError) + return (test, action) } return await taskGroup.first { _ in true }! } + + return result } /// Construct a graph of runner plan steps for the specified tests. @@ -309,36 +337,12 @@ extension Runner.Plan { return nil } - var action = runAction - var firstCaughtError: (any Error)? - // Walk all the traits and tell each to prepare to run the test. // If any throw a `SkipInfo` error at this stage, stop walking further. // But if any throw another kind of error, keep track of the first error // but continue walking, because if any subsequent traits throw a // `SkipInfo`, the error should not be recorded. - (action, firstCaughtError) = await _determineAction(for: test) - - // If no trait specified that the test should be skipped, but one did - // throw an error, then the action is to record an issue for that error. - if case .run = action, let error = firstCaughtError { - action = .recordIssue(Issue(for: error)) - } - - // If the test is still planned to run (i.e. nothing thus far has caused - // it to be skipped), evaluate its test cases now. - // - // The argument expressions of each test are captured in closures so they - // can be evaluated lazily only once it is determined that the test will - // run, to avoid unnecessary work. But now is the appropriate time to - // evaluate them. - if case .run = action { - do { - try await test.evaluateTestCases() - } catch { - action = .recordIssue(Issue(for: error)) - } - } + var action = await _determineAction(for: &test) // If the test is parameterized but has no cases, mark it as skipped. if case .run = action, let testCases = test.testCases, testCases.first(where: { _ in true }) == nil { diff --git a/Sources/Testing/Running/SkipInfo.swift b/Sources/Testing/Running/SkipInfo.swift index 0c5a6923d..a5f32ebca 100644 --- a/Sources/Testing/Running/SkipInfo.swift +++ b/Sources/Testing/Running/SkipInfo.swift @@ -54,6 +54,30 @@ extension SkipInfo: Equatable, Hashable {} extension SkipInfo: Codable {} +// MARK: - + +extension SkipInfo { + /// Initialize an instance of this type from an arbitrary error. + /// + /// - Parameters: + /// - error: The error to convert to an instance of this type. + /// + /// If `error` does not represent a skip or cancellation event, this + /// initializer returns `nil`. + init?(_ error: any Error) { + if let skipInfo = error as? Self { + self = skipInfo + } else if error is CancellationError, Task.isCancelled { + // Synthesize skip info for this cancellation error. + let backtrace = Backtrace(forFirstThrowOf: error) ?? .current() + let sourceContext = SourceContext(backtrace: backtrace, sourceLocation: nil) + self.init(comment: nil, sourceContext: sourceContext) + } else { + return nil + } + } +} + // MARK: - Deprecated extension SkipInfo { diff --git a/Tests/TestingTests/TestCancellationTests.swift b/Tests/TestingTests/TestCancellationTests.swift index 3d7fb819e..03ab1126f 100644 --- a/Tests/TestingTests/TestCancellationTests.swift +++ b/Tests/TestingTests/TestCancellationTests.swift @@ -132,6 +132,22 @@ } } + @Test func `Cancelling a test while evaluating test cases skips the test`() async { + await testCancellation(testSkipped: 1) { configuration in + await Test(arguments: { try await cancelledTestCases(cancelsTask: false) }) { _ in + Issue.record("Recorded an issue!") + }.run(configuration: configuration) + } + } + + @Test func `Cancelling the current task while evaluating test cases skips the test`() async { + await testCancellation(testSkipped: 1) { configuration in + await Test(arguments: { try await cancelledTestCases(cancelsTask: true) }) { _ in + Issue.record("Recorded an issue!") + }.run(configuration: configuration) + } + } + #if !SWT_NO_EXIT_TESTS @Test func `Cancelling the current test from within an exit test`() async { await testCancellation(testCancelled: 1, testCaseCancelled: 1) { configuration in @@ -219,6 +235,15 @@ struct CancelledTrait: TestTrait { } } +func cancelledTestCases(cancelsTask: Bool) async throws -> EmptyCollection { + if cancelsTask { + withUnsafeCurrentTask { $0?.cancel() } + try Task.checkCancellation() + } + try Test.cancel("Cancelled from trait") +} + + #if !SWT_NO_SNAPSHOT_TYPES struct `Shows as skipped in Xcode 16` { @Test func `Cancelled test`() throws { diff --git a/Tests/TestingTests/TestSupport/TestingAdditions.swift b/Tests/TestingTests/TestSupport/TestingAdditions.swift index 30d53ce7f..05bb05dc8 100644 --- a/Tests/TestingTests/TestSupport/TestingAdditions.swift +++ b/Tests/TestingTests/TestSupport/TestingAdditions.swift @@ -199,6 +199,23 @@ extension Test { self.init(name: name, displayName: name, traits: traits, sourceLocation: sourceLocation, containingTypeInfo: nil, testCases: caseGenerator, parameters: parameters) } + init( + _ traits: any TestTrait..., + arguments collection: @escaping @Sendable () async throws -> C, + parameters: [Parameter] = [ + Parameter(index: 0, firstName: "x", type: C.Element.self), + ], + sourceLocation: SourceLocation = #_sourceLocation, + column: Int = #column, + name: String = #function, + testFunction: @escaping @Sendable (C.Element) async throws -> Void + ) where C: Collection & Sendable, C.Element: Sendable { + let caseGenerator = { @Sendable in + Case.Generator(arguments: try await collection(), parameters: parameters, testFunction: testFunction) + } + self.init(name: name, displayName: name, traits: traits, sourceLocation: sourceLocation, containingTypeInfo: nil, testCases: caseGenerator, parameters: parameters) + } + /// Initialize an instance of this type with a function or closure to call, /// parameterized over two collections of values. /// From 8500bd0e26080e42720dbb6b3a7374fc6b305f80 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Wed, 3 Sep 2025 17:03:58 -0400 Subject: [PATCH 121/216] Propagate `SkipInfo` to child tasks when a test is cancelled. (#1298) This PR ensures that, when a test is cancelled, its test cases report the same `SkipInfo` structure as they are recursively cancelled. For instance: ```swift @Test(arguments: [a ... z]) func foo(arg: T) throws { try Test.cancel("Message") } ``` Prior to this change, the `.testCaseCancelled` events would be posted without the user-supplied comment `"Message"` and each one would need to construct a source context by capturing a backtrace from deep within Swift Testing and the stdlib. With this change in place, said events post with the same comment and source context as is associated with the initial `.testCancelled` event. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- Sources/Testing/ExitTests/ExitTest.swift | 5 +- Sources/Testing/Running/SkipInfo.swift | 2 +- Sources/Testing/Test+Cancellation.swift | 89 ++++++++++--------- Sources/Testing/Traits/ConditionTrait.swift | 4 +- .../TestingTests/TestCancellationTests.swift | 26 +++++- 5 files changed, 78 insertions(+), 48 deletions(-) diff --git a/Sources/Testing/ExitTests/ExitTest.swift b/Sources/Testing/ExitTests/ExitTest.swift index c82430eb5..6f93a470c 100644 --- a/Sources/Testing/ExitTests/ExitTest.swift +++ b/Sources/Testing/ExitTests/ExitTest.swift @@ -1048,6 +1048,7 @@ extension ExitTest { backtrace: nil, // A backtrace from the child process will have the wrong address space. sourceLocation: event._sourceLocation ) + lazy var skipInfo = SkipInfo(comment: comments.first, sourceContext: sourceContext) if let issue = event.issue { // Translate the issue back into a "real" issue and record it // in the parent process. This translation is, of course, lossy @@ -1075,9 +1076,9 @@ extension ExitTest { } else if let attachment = event.attachment { Attachment.record(attachment, sourceLocation: event._sourceLocation!) } else if case .testCancelled = event.kind { - _ = try? Test.cancel(comments: comments, sourceContext: sourceContext) + _ = try? Test.cancel(with: skipInfo) } else if case .testCaseCancelled = event.kind { - _ = try? Test.Case.cancel(comments: comments, sourceContext: sourceContext) + _ = try? Test.Case.cancel(with: skipInfo) } } diff --git a/Sources/Testing/Running/SkipInfo.swift b/Sources/Testing/Running/SkipInfo.swift index a5f32ebca..687cf8434 100644 --- a/Sources/Testing/Running/SkipInfo.swift +++ b/Sources/Testing/Running/SkipInfo.swift @@ -69,7 +69,7 @@ extension SkipInfo { self = skipInfo } else if error is CancellationError, Task.isCancelled { // Synthesize skip info for this cancellation error. - let backtrace = Backtrace(forFirstThrowOf: error) ?? .current() + let backtrace = Backtrace(forFirstThrowOf: error) let sourceContext = SourceContext(backtrace: backtrace, sourceLocation: nil) self.init(comment: nil, sourceContext: sourceContext) } else { diff --git a/Sources/Testing/Test+Cancellation.swift b/Sources/Testing/Test+Cancellation.swift index 5b3b3b530..ed8738a64 100644 --- a/Sources/Testing/Test+Cancellation.swift +++ b/Sources/Testing/Test+Cancellation.swift @@ -16,9 +16,7 @@ protocol TestCancellable: Sendable { /// Cancel the current instance of this type. /// /// - Parameters: - /// - comments: Comments describing why you are cancelling the test/case. - /// - sourceContext: The source context to which the testing library will - /// attribute the cancellation. + /// - skipInfo: Information about the cancellation event. /// /// - Throws: An error indicating that the current instance of this type has /// been cancelled. @@ -26,7 +24,7 @@ protocol TestCancellable: Sendable { /// Note that the public ``Test/cancel(_:sourceLocation:)`` function has a /// different signature and accepts a source location rather than a source /// context value. - static func cancel(comments: [Comment], sourceContext: @autoclosure () -> SourceContext) throws -> Never + static func cancel(with skipInfo: SkipInfo) throws -> Never /// Make an instance of ``Event/Kind`` appropriate for an instance of this /// type. @@ -47,8 +45,17 @@ private struct _TaskReference: Sendable { private nonisolated(unsafe) var _unsafeCurrentTask = Locked() init() { - let unsafeCurrentTask = withUnsafeCurrentTask { $0 } - _unsafeCurrentTask = Locked(rawValue: unsafeCurrentTask) + // WARNING! Normally, allowing an instance of `UnsafeCurrentTask` to escape + // its scope is dangerous because it could be used unsafely after the task + // ends. However, because we take care not to allow the task object to + // escape the task (by only storing it in a task-local value), we can ensure + // these unsafe scenarios won't occur. + // + // TODO: when our deployment targets allow, we should switch to calling the + // `async` overload of `withUnsafeCurrentTask()` from the body of + // `withCancellationHandling(_:)`. That will allow us to use the task object + // in a safely scoped fashion. + _unsafeCurrentTask = withUnsafeCurrentTask { Locked(rawValue: $0) } } /// Take this instance's reference to its associated task. @@ -69,8 +76,14 @@ private struct _TaskReference: Sendable { /// A dictionary of tracked tasks, keyed by types that conform to /// ``TestCancellable``. -@TaskLocal -private var _currentTaskReferences = [ObjectIdentifier: _TaskReference]() +@TaskLocal private var _currentTaskReferences = [ObjectIdentifier: _TaskReference]() + +/// The instance of ``SkipInfo`` to propagate to children of the current task. +/// +/// We set this value while calling `UnsafeCurrentTask.cancel()` so that its +/// value is available in tracked child tasks when their cancellation handlers +/// are called (in ``TestCancellable/withCancellationHandling(_:)`` below). +@TaskLocal private var _currentSkipInfo: SkipInfo? extension TestCancellable { /// Call a function while the ``unsafeCurrentTask`` property of this instance @@ -95,10 +108,9 @@ extension TestCancellable { } onCancel: { // The current task was cancelled, so cancel the test case or test // associated with it. - _ = try? Self.cancel( - comments: [], - sourceContext: SourceContext(backtrace: .current(), sourceLocation: nil) - ) + + let skipInfo = _currentSkipInfo ?? SkipInfo(sourceContext: SourceContext(backtrace: .current(), sourceLocation: nil)) + _ = try? Self.cancel(with: skipInfo) } } } @@ -112,24 +124,21 @@ extension TestCancellable { /// - cancellableValue: The test or test case to cancel, or `nil` if neither /// is set and we need fallback handling. /// - testAndTestCase: The test and test case to use when posting an event. -/// - comments: Comments describing why you are cancelling the test/case. -/// - sourceContext: The source context to which the testing library will -/// attribute the cancellation. +/// - skipInfo: Information about the cancellation event. /// /// - Throws: An instance of ``SkipInfo`` describing the cancellation. -private func _cancel(_ cancellableValue: T?, for testAndTestCase: (Test?, Test.Case?), comments: [Comment], sourceContext: @autoclosure () -> SourceContext) throws -> Never where T: TestCancellable { - var skipInfo = SkipInfo(comment: comments.first, sourceContext: .init(backtrace: nil, sourceLocation: nil)) - +private func _cancel(_ cancellableValue: T?, for testAndTestCase: (Test?, Test.Case?), skipInfo: SkipInfo) throws -> Never where T: TestCancellable { if cancellableValue != nil { - // If the current test case is still running, cancel its task and clear its - // task property (which signals that it has been cancelled.) + // If the current test case is still running, take its task property (which + // signals to subsequent callers that it has been cancelled.) let task = _currentTaskReferences[ObjectIdentifier(T.self)]?.takeUnsafeCurrentTask() - task?.cancel() // If we just cancelled the current test case's task, post a corresponding // event with the relevant skip info. - if task != nil { - skipInfo.sourceContext = sourceContext() + if let task { + $_currentSkipInfo.withValue(skipInfo) { + task.cancel() + } Event.post(T.makeCancelledEventKind(with: skipInfo), for: testAndTestCase) } } else { @@ -147,13 +156,18 @@ private func _cancel(_ cancellableValue: T?, for testAndTestCase: (Test?, Tes // This code is running in an exit test. We don't have a "current test" or // "current test case" in the child process, so we'll let the parent // process sort that out. - skipInfo.sourceContext = sourceContext() Event.post(T.makeCancelledEventKind(with: skipInfo), for: (nil, nil)) } else { // Record an API misuse issue for trying to cancel the current test/case // outside of any useful context. - let comments = ["Attempted to cancel the current test or test case, but one is not associated with the current task."] + comments - let issue = Issue(kind: .apiMisused, comments: comments, sourceContext: sourceContext()) + let issue = Issue( + kind: .apiMisused, + comments: [ + "Attempted to cancel the current test or test case, but one is not associated with the current task.", + skipInfo.comment, + ].compactMap(\.self), + sourceContext: skipInfo.sourceContext + ) issue.record() } } @@ -208,15 +222,13 @@ extension Test: TestCancellable { /// test alone, call ``Test/Case/cancel(_:sourceLocation:)`` instead. @_spi(Experimental) public static func cancel(_ comment: Comment? = nil, sourceLocation: SourceLocation = #_sourceLocation) throws -> Never { - try Self.cancel( - comments: Array(comment), - sourceContext: SourceContext(backtrace: .current(), sourceLocation: sourceLocation) - ) + let skipInfo = SkipInfo(comment: comment, sourceContext: SourceContext(backtrace: nil, sourceLocation: sourceLocation)) + try Self.cancel(with: skipInfo) } - static func cancel(comments: [Comment], sourceContext: @autoclosure () -> SourceContext) throws -> Never { + static func cancel(with skipInfo: SkipInfo) throws -> Never { let test = Test.current - try _cancel(test, for: (test, nil), comments: comments, sourceContext: sourceContext()) + try _cancel(test, for: (test, nil), skipInfo: skipInfo) } static func makeCancelledEventKind(with skipInfo: SkipInfo) -> Event.Kind { @@ -271,23 +283,20 @@ extension Test.Case: TestCancellable { /// ``Test/cancel(_:sourceLocation:)`` instead. @_spi(Experimental) public static func cancel(_ comment: Comment? = nil, sourceLocation: SourceLocation = #_sourceLocation) throws -> Never { - try Self.cancel( - comments: Array(comment), - sourceContext: SourceContext(backtrace: .current(), sourceLocation: sourceLocation) - ) + let skipInfo = SkipInfo(comment: comment, sourceContext: SourceContext(backtrace: nil, sourceLocation: sourceLocation)) + try Self.cancel(with: skipInfo) } - static func cancel(comments: [Comment], sourceContext: @autoclosure () -> SourceContext) throws -> Never { + static func cancel(with skipInfo: SkipInfo) throws -> Never { let test = Test.current let testCase = Test.Case.current - let sourceContext = sourceContext() // evaluated twice, avoid laziness do { // Cancel the current test case (if it's nil, that's the API misuse path.) - try _cancel(testCase, for: (test, testCase), comments: comments, sourceContext: sourceContext) + try _cancel(testCase, for: (test, testCase), skipInfo: skipInfo) } catch _ where test?.isParameterized == false { // The current test is not parameterized, so cancel the whole test too. - try _cancel(test, for: (test, nil), comments: comments, sourceContext: sourceContext) + try _cancel(test, for: (test, nil), skipInfo: skipInfo) } } diff --git a/Sources/Testing/Traits/ConditionTrait.swift b/Sources/Testing/Traits/ConditionTrait.swift index d60cf7cce..2bd776c91 100644 --- a/Sources/Testing/Traits/ConditionTrait.swift +++ b/Sources/Testing/Traits/ConditionTrait.swift @@ -85,15 +85,13 @@ public struct ConditionTrait: TestTrait, SuiteTrait { public func prepare(for test: Test) async throws { let isEnabled = try await evaluate() - if !isEnabled { // We don't need to consider including a backtrace here because it will // primarily contain frames in the testing library, not user code. If an // error was thrown by a condition evaluated above, the caller _should_ // attempt to get the backtrace of the caught error when creating an issue // for it, however. - let sourceContext = SourceContext(backtrace: nil, sourceLocation: sourceLocation) - throw SkipInfo(comment: comments.first, sourceContext: sourceContext) + try Test.cancel(comments.first, sourceLocation: sourceLocation) } } diff --git a/Tests/TestingTests/TestCancellationTests.swift b/Tests/TestingTests/TestCancellationTests.swift index 03ab1126f..a4f95fe56 100644 --- a/Tests/TestingTests/TestCancellationTests.swift +++ b/Tests/TestingTests/TestCancellationTests.swift @@ -11,13 +11,20 @@ @testable @_spi(Experimental) @_spi(ForToolsIntegrationOnly) import Testing @Suite(.serialized) struct `Test cancellation tests` { - func testCancellation(testCancelled: Int = 0, testSkipped: Int = 0, testCaseCancelled: Int = 0, issueRecorded: Int = 0, _ body: @Sendable (Configuration) async -> Void) async { + func testCancellation( + testCancelled: Int = 0, + testSkipped: Int = 0, + testCaseCancelled: Int = 0, + issueRecorded: Int = 0, + _ body: @Sendable (Configuration) async -> Void, + eventHandler: @escaping @Sendable (borrowing Event, borrowing Event.Context) -> Void = { _, _ in } + ) async { await confirmation("Test cancelled", expectedCount: testCancelled) { testCancelled in await confirmation("Test skipped", expectedCount: testSkipped) { testSkipped in await confirmation("Test case cancelled", expectedCount: testCaseCancelled) { testCaseCancelled in await confirmation("Issue recorded", expectedCount: issueRecorded) { [issueRecordedCount = issueRecorded] issueRecorded in var configuration = Configuration() - configuration.eventHandler = { event, _ in + configuration.eventHandler = { event, eventContext in switch event.kind { case .testCancelled: testCancelled() @@ -33,6 +40,7 @@ default: break } + eventHandler(event, eventContext) } #if !SWT_NO_EXIT_TESTS configuration.exitTestHandler = ExitTest.handlerForEntryPoint() @@ -84,6 +92,20 @@ } } + @Test func `Cancelling a test propagates its SkipInfo to its test cases`() async { + let sourceLocation = #_sourceLocation + await testCancellation(testCancelled: 1, testCaseCancelled: 1) { configuration in + await Test { + try Test.cancel("Cancelled test", sourceLocation: sourceLocation) + }.run(configuration: configuration) + } eventHandler: { event, _ in + if case let .testCaseCancelled(skipInfo) = event.kind { + #expect(skipInfo.comment?.rawValue == "Cancelled test") + #expect(skipInfo.sourceContext.sourceLocation == sourceLocation) + } + } + } + @Test func `Cancelling a test by cancelling its task (throwing)`() async { await testCancellation(testCancelled: 1, testCaseCancelled: 1) { configuration in await Test { From 6b3a8f3b0281bcd18d36554be922be3a62cc4bb5 Mon Sep 17 00:00:00 2001 From: Stuart Montgomery Date: Fri, 5 Sep 2025 21:42:02 -0500 Subject: [PATCH 122/216] Define an experimental event stream version and use it to conditionalize relevant content (#1287) This PR formally defines an "experimental" event stream version (named `ABI.ExperimentalVersion`) which represents content that is considered experimental. It then adopts the new experimental version in several places to conditionalize the inclusion of fields on event stream models which are not yet officially included in any defined, supported version. Finally, it uses this new version everywhere that intentionally always uses the _highest experimental_ version, such as the exit test back channel and the [recently-added](#1253) experimental console output recorder. ### Motivation: The event stream now has an established versioning system (as of #956) and can easily conditionalize content based on version. As a general rule, we prefer to exclude content which has not gone through Swift Evolution review when delivering data to event stream consumers which expect to receive content from a supported version. The fields this PR conditionalizes have underscore prefixes and have code-level documentation indicating their unofficial-ness, but the fact that they are included at all in supported/non-experimental version streams could lead to misuse or unintentional breakages in the future if the names or semantics of these fields change. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- Sources/Testing/ABI/ABI.swift | 44 ++++++++++++++++++- .../ABI/Encoded/ABI.EncodedEvent.swift | 8 +++- .../ABI/Encoded/ABI.EncodedIssue.swift | 14 +++--- .../Testing/ABI/Encoded/ABI.EncodedTest.swift | 17 +++---- .../Testing/ABI/EntryPoints/EntryPoint.swift | 4 +- Sources/Testing/ExitTests/ExitTest.swift | 7 ++- Tests/TestingTests/SwiftPMTests.swift | 2 +- 7 files changed, 70 insertions(+), 26 deletions(-) diff --git a/Sources/Testing/ABI/ABI.swift b/Sources/Testing/ABI/ABI.swift index 1707953a0..7a33970fc 100644 --- a/Sources/Testing/ABI/ABI.swift +++ b/Sources/Testing/ABI/ABI.swift @@ -45,7 +45,8 @@ extension ABI { /// The current supported ABI version (ignoring any experimental versions.) typealias CurrentVersion = v0 - /// The highest supported ABI version (including any experimental versions.) + /// The highest defined and supported ABI version (including any experimental + /// versions.) typealias HighestVersion = v6_3 #if !hasFeature(Embedded) @@ -93,6 +94,39 @@ extension ABI { #endif } +/// The value of the environment variable flag which enables experimental event +/// stream fields, if any. +private let _shouldIncludeExperimentalFlags = Environment.flag(named: "SWT_EXPERIMENTAL_EVENT_STREAM_FIELDS_ENABLED") + +extension ABI.Version { + /// Whether or not experimental fields should be included when using this + /// ABI version. + /// + /// The value of this property is `true` if any of the following conditions + /// are satisfied: + /// + /// - The version number is less than 6.3. This is to preserve compatibility + /// with existing clients before the inclusion of experimental fields became + /// opt-in starting in 6.3. + /// - The version number is greater than or equal to 6.3 and the environment + /// variable flag `SWT_EXPERIMENTAL_EVENT_STREAM_FIELDS_ENABLED` is set to a + /// true value. + /// - The version number is greater than or equal to that of ``ABI/ExperimentalVersion``. + /// + /// Otherwise, the value of this property is `false`. + static var includesExperimentalFields: Bool { + switch versionNumber { + case ABI.ExperimentalVersion.versionNumber...: + true + case ABI.v6_3.versionNumber...: + _shouldIncludeExperimentalFlags == true + default: + // Maintain behavior for pre-6.3 versions. + true + } + } +} + // MARK: - Concrete ABI versions extension ABI { @@ -125,6 +159,14 @@ extension ABI { VersionNumber(6, 3) } } + + /// A namespace and type representing the ABI version whose symbols are + /// considered experimental. + enum ExperimentalVersion: Sendable, Version { + static var versionNumber: VersionNumber { + VersionNumber(99, 0) + } + } } /// A namespace for ABI version 0 symbols. diff --git a/Sources/Testing/ABI/Encoded/ABI.EncodedEvent.swift b/Sources/Testing/ABI/Encoded/ABI.EncodedEvent.swift index a78f86368..523d7845c 100644 --- a/Sources/Testing/ABI/Encoded/ABI.EncodedEvent.swift +++ b/Sources/Testing/ABI/Encoded/ABI.EncodedEvent.swift @@ -145,8 +145,12 @@ extension ABI { instant = EncodedInstant(encoding: event.instant) self.messages = messages.map(EncodedMessage.init) testID = event.testID.map(EncodedTest.ID.init) - if eventContext.test?.isParameterized == true { - _testCase = eventContext.testCase.map(EncodedTestCase.init) + + // Experimental fields + if V.includesExperimentalFields { + if eventContext.test?.isParameterized == true { + _testCase = eventContext.testCase.map(EncodedTestCase.init) + } } } } diff --git a/Sources/Testing/ABI/Encoded/ABI.EncodedIssue.swift b/Sources/Testing/ABI/Encoded/ABI.EncodedIssue.swift index c1e3c12fd..c593a68a5 100644 --- a/Sources/Testing/ABI/Encoded/ABI.EncodedIssue.swift +++ b/Sources/Testing/ABI/Encoded/ABI.EncodedIssue.swift @@ -72,12 +72,14 @@ extension ABI { isFailure = issue.isFailure } - // Experimental - if let backtrace = issue.sourceContext.backtrace { - _backtrace = EncodedBacktrace(encoding: backtrace, in: eventContext) - } - if let error = issue.error { - _error = EncodedError(encoding: error, in: eventContext) + // Experimental fields + if V.includesExperimentalFields { + if let backtrace = issue.sourceContext.backtrace { + _backtrace = EncodedBacktrace(encoding: backtrace, in: eventContext) + } + if let error = issue.error { + _error = EncodedError(encoding: error, in: eventContext) + } } } } diff --git a/Sources/Testing/ABI/Encoded/ABI.EncodedTest.swift b/Sources/Testing/ABI/Encoded/ABI.EncodedTest.swift index 11c309e83..43a1b615b 100644 --- a/Sources/Testing/ABI/Encoded/ABI.EncodedTest.swift +++ b/Sources/Testing/ABI/Encoded/ABI.EncodedTest.swift @@ -76,10 +76,6 @@ extension ABI { /// The tags associated with the test. /// /// - Warning: Tags are not yet part of the JSON schema. - /// - /// @Metadata { - /// @Available("Swift Testing ABI", introduced: 6.3) - /// } var _tags: [String]? init(encoding test: borrowing Test) { @@ -87,18 +83,19 @@ extension ABI { kind = .suite } else { kind = .function - let testIsParameterized = test.isParameterized - isParameterized = testIsParameterized - if testIsParameterized { - _testCases = test.uncheckedTestCases?.map(EncodedTestCase.init(encoding:)) - } + isParameterized = test.isParameterized } name = test.name displayName = test.displayName sourceLocation = test.sourceLocation id = ID(encoding: test.id) - if V.versionNumber >= ABI.v6_3.versionNumber { + // Experimental fields + if V.includesExperimentalFields { + if isParameterized == true { + _testCases = test.uncheckedTestCases?.map(EncodedTestCase.init(encoding:)) + } + let tags = test.tags if !tags.isEmpty { _tags = tags.map(String.init(describing:)) diff --git a/Sources/Testing/ABI/EntryPoints/EntryPoint.swift b/Sources/Testing/ABI/EntryPoints/EntryPoint.swift index a97d33c9e..727d91632 100644 --- a/Sources/Testing/ABI/EntryPoints/EntryPoint.swift +++ b/Sources/Testing/ABI/EntryPoints/EntryPoint.swift @@ -57,10 +57,10 @@ func entryPoint(passing args: __CommandLineArguments_v0?, eventHandler: Event.Ha // Check for experimental console output flag if Environment.flag(named: "SWT_ENABLE_EXPERIMENTAL_CONSOLE_OUTPUT") == true { // Use experimental AdvancedConsoleOutputRecorder - var advancedOptions = Event.AdvancedConsoleOutputRecorder.Options() + var advancedOptions = Event.AdvancedConsoleOutputRecorder.Options() advancedOptions.base = .for(.stderr) - let eventRecorder = Event.AdvancedConsoleOutputRecorder(options: advancedOptions) { string in + let eventRecorder = Event.AdvancedConsoleOutputRecorder(options: advancedOptions) { string in try? FileHandle.stderr.write(string) } diff --git a/Sources/Testing/ExitTests/ExitTest.swift b/Sources/Testing/ExitTests/ExitTest.swift index 6f93a470c..ff8d805ae 100644 --- a/Sources/Testing/ExitTests/ExitTest.swift +++ b/Sources/Testing/ExitTests/ExitTest.swift @@ -542,10 +542,9 @@ extension ABI { /// The ABI version to use for encoding and decoding events sent over the back /// channel. /// - /// The back channel always uses the latest ABI version (even if experimental) - /// since both the producer and consumer use this exact version of the testing - /// library. - fileprivate typealias BackChannelVersion = v6_3 + /// The back channel always uses the experimental ABI version since both the + /// producer and consumer use this exact version of the testing library. + fileprivate typealias BackChannelVersion = ExperimentalVersion } @_spi(ForToolsIntegrationOnly) diff --git a/Tests/TestingTests/SwiftPMTests.swift b/Tests/TestingTests/SwiftPMTests.swift index 672e34a03..e61c3b237 100644 --- a/Tests/TestingTests/SwiftPMTests.swift +++ b/Tests/TestingTests/SwiftPMTests.swift @@ -376,7 +376,7 @@ struct SwiftPMTests { } #expect(testRecords.count == 1) for testRecord in testRecords { - if version.versionNumber >= ABI.v6_3.versionNumber { + if version.includesExperimentalFields { #expect(testRecord._tags != nil) } else { #expect(testRecord._tags == nil) From c169ca3f251e5e96f1a74b7527e61c2be5162466 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Mon, 8 Sep 2025 12:42:35 -0400 Subject: [PATCH 123/216] Add a document describing our environment variable usage. (#1300) Adds a document describing our environment variable usage for reference by developers working on Swift Testing. This document is not an API contract: the set of environment variables Swift Testing uses may change at any time. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --------- Co-authored-by: Joseph Heck --- Documentation/EnvironmentVariables.md | 66 +++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 Documentation/EnvironmentVariables.md diff --git a/Documentation/EnvironmentVariables.md b/Documentation/EnvironmentVariables.md new file mode 100644 index 000000000..a73ffdc65 --- /dev/null +++ b/Documentation/EnvironmentVariables.md @@ -0,0 +1,66 @@ + + +# Environment variables in Swift Testing + +This document lists the environment variables that Swift Testing currently uses. +This list is meant for use by developers working on Swift Testing. + +Those environment variables marked with `*` are defined by components outside +Swift Testing. In general, environment variables that Swift Testing defines have +names prefixed with `SWT_`. + +> [!WARNING] +> This document is not an API contract. The set of environment variables Swift +> Testing uses may change at any time. + +## Console output + +| Variable Name | Value Type | Notes | +|-|:-:|-| +| `COLORTERM`\* | `String` | Used to determine if the current terminal supports 24-bit color. Common across UNIX-like platforms. | +| `NO_COLOR`[\*](https://no-color.org) | `Any?` | If set to any value, disables color output regardless of terminal capabilities. | +| `SWT_ENABLE_EXPERIMENTAL_CONSOLE_OUTPUT` | `Bool` | Used to enable or disable experimental console output. | +| `SWT_SF_SYMBOLS_ENABLED` | `Bool` | Used to explicitly enable or disable SF Symbols support on macOS. | +| `TERM`[\*](https://pubs.opengroup.org/onlinepubs/9799919799/basedefs/V1_chap08.html) | `String` | Used to determine if the current terminal supports 4- or 8-bit color. Common across UNIX-like platforms. | + +## Error handling + +| Variable Name | Value Type | Notes | +|-|:-:|-| +| `SWT_FOUNDATION_ERROR_BACKTRACING_ENABLED` | `Bool` | Used to explicitly enable or disable error backtrace capturing when an instance of `NSError` or `CFError` is created on Apple platforms. | +| `SWT_SWIFT_ERROR_BACKTRACING_ENABLED` | `Bool` | Used to explicitly enable or disable error backtrace capturing when a Swift error is thrown. | + +## Event streams + +| Variable Name | Value Type | Notes | +|-|:-:|-| +| `SWT_EXPERIMENTAL_EVENT_STREAM_FIELDS_ENABLED` | `Bool` | Used to explicitly enable or disable experimental fields in the JSON event stream. | +| `SWT_PRETTY_PRINT_JSON` | `Bool` | Used to enable pretty-printed JSON output to the event stream (for debugging purposes). | + +## Exit tests + +| Variable Name | Value Type | Notes | +|-|:-:|-| +| `SWT_BACKCHANNEL` | `CInt`/`HANDLE` | A file descriptor (handle on Windows) to which the exit test's events are written. | +| `SWT_CAPTURED_VALUES` | `CInt`/`HANDLE` | A file descriptor (handle on Windows) containing captured values passed to the exit test. | +| `SWT_CLOSEFROM` | `CInt` | Used on OpenBSD to emulate `posix_spawn_file_actions_addclosefrom_np()`. | +| `SWT_EXIT_TEST_ID` | `String` (JSON) | Specifies which exit test to run. | +| `XCTestBundlePath`\* | `String` | Used on Apple platforms to determine if Xcode is hosting the test run. | + +## Miscellaneous + +| Variable Name | Value Type | Notes | +|-|:-:|-| +| `CFFIXED_USER_HOME`\* | `String` | Used on Apple platforms to determine the user's home directory. | +| `HOME`[\*](https://pubs.opengroup.org/onlinepubs/9799919799/basedefs/V1_chap08.html) | `String` | Used to determine the user's home directory. | +| `SIMULATOR_RUNTIME_BUILD_VERSION`\* | `String` | Used when running in the iOS (etc.) Simulator to determine the simulator's version. | +| `SIMULATOR_RUNTIME_VERSION`\* | `String` | Used when running in the iOS (etc.) Simulator to determine the simulator's version. | +| `SWT_USE_LEGACY_TEST_DISCOVERY` | `Bool` | Used to explicitly enable or disable legacy test discovery. | From 944d9e6ca593e6a5d151a138f834b57f5a93486a Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Mon, 8 Sep 2025 17:32:35 -0400 Subject: [PATCH 124/216] Lower more experimental JSON fields. (#1301) This PR suppresses more experimental JSON fields if they aren't supposed to be included in the current JSON event stream version: - All experimental event kinds. - Attachments' preferred names and bytes/contents. - Hoisted comments and source locations for all event kinds. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- Sources/Testing/ABI/ABI.Record.swift | 4 +++ .../ABI/Encoded/ABI.EncodedAttachment.swift | 2 +- .../ABI/Encoded/ABI.EncodedEvent.swift | 30 +++++++++++-------- 3 files changed, 23 insertions(+), 13 deletions(-) diff --git a/Sources/Testing/ABI/ABI.Record.swift b/Sources/Testing/ABI/ABI.Record.swift index ef7bc6937..40a8d4bc3 100644 --- a/Sources/Testing/ABI/ABI.Record.swift +++ b/Sources/Testing/ABI/ABI.Record.swift @@ -36,6 +36,10 @@ extension ABI { guard let event = EncodedEvent(encoding: event, in: eventContext, messages: messages) else { return nil } + if !V.includesExperimentalFields && event.kind.rawValue.first == "_" { + // Don't encode experimental event kinds. + return nil + } kind = .event(event) } } diff --git a/Sources/Testing/ABI/Encoded/ABI.EncodedAttachment.swift b/Sources/Testing/ABI/Encoded/ABI.EncodedAttachment.swift index a2400f9bf..013e129f6 100644 --- a/Sources/Testing/ABI/Encoded/ABI.EncodedAttachment.swift +++ b/Sources/Testing/ABI/Encoded/ABI.EncodedAttachment.swift @@ -41,7 +41,7 @@ extension ABI { init(encoding attachment: borrowing Attachment, in eventContext: borrowing Event.Context) { path = attachment.fileSystemPath - if V.versionNumber >= ABI.v6_3.versionNumber { + if V.includesExperimentalFields { _preferredName = attachment.preferredName if path == nil { diff --git a/Sources/Testing/ABI/Encoded/ABI.EncodedEvent.swift b/Sources/Testing/ABI/Encoded/ABI.EncodedEvent.swift index 523d7845c..3bfd6ff36 100644 --- a/Sources/Testing/ABI/Encoded/ABI.EncodedEvent.swift +++ b/Sources/Testing/ABI/Encoded/ABI.EncodedEvent.swift @@ -112,31 +112,22 @@ extension ABI { case let .issueRecorded(recordedIssue): kind = .issueRecorded issue = EncodedIssue(encoding: recordedIssue, in: eventContext) - _comments = recordedIssue.comments.map(\.rawValue) - _sourceLocation = recordedIssue.sourceLocation case let .valueAttached(attachment): kind = .valueAttached self.attachment = EncodedAttachment(encoding: attachment, in: eventContext) - _sourceLocation = attachment.sourceLocation case .testCaseEnded: if eventContext.test?.isParameterized == false { return nil } kind = .testCaseEnded - case let .testCaseCancelled(skipInfo): + case .testCaseCancelled: kind = .testCaseCancelled - _comments = Array(skipInfo.comment).map(\.rawValue) - _sourceLocation = skipInfo.sourceLocation case .testEnded: kind = .testEnded - case let .testSkipped(skipInfo): + case .testSkipped: kind = .testSkipped - _comments = Array(skipInfo.comment).map(\.rawValue) - _sourceLocation = skipInfo.sourceLocation - case let .testCancelled(skipInfo): + case .testCancelled: kind = .testCancelled - _comments = Array(skipInfo.comment).map(\.rawValue) - _sourceLocation = skipInfo.sourceLocation case .runEnded: kind = .runEnded default: @@ -148,6 +139,21 @@ extension ABI { // Experimental fields if V.includesExperimentalFields { + switch event.kind { + case let .issueRecorded(recordedIssue): + _comments = recordedIssue.comments.map(\.rawValue) + _sourceLocation = recordedIssue.sourceLocation + case let .valueAttached(attachment): + _sourceLocation = attachment.sourceLocation + case let .testCaseCancelled(skipInfo), + let .testSkipped(skipInfo), + let .testCancelled(skipInfo): + _comments = Array(skipInfo.comment).map(\.rawValue) + _sourceLocation = skipInfo.sourceLocation + default: + break + } + if eventContext.test?.isParameterized == true { _testCase = eventContext.testCase.map(EncodedTestCase.init) } From 4fc4f3f2e929a0ce57a5347fb21c0493119b8b9d Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Mon, 8 Sep 2025 17:49:08 -0400 Subject: [PATCH 125/216] Suppress warning building WASI --- Sources/Testing/Test+Cancellation.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Testing/Test+Cancellation.swift b/Sources/Testing/Test+Cancellation.swift index ed8738a64..7b8f9e88d 100644 --- a/Sources/Testing/Test+Cancellation.swift +++ b/Sources/Testing/Test+Cancellation.swift @@ -152,7 +152,7 @@ private func _cancel(_ cancellableValue: T?, for testAndTestCase: (Test?, Tes #if !SWT_NO_EXIT_TESTS inExitTest = (ExitTest.current != nil) #endif - if inExitTest { + if Bool(inExitTest) { // This code is running in an exit test. We don't have a "current test" or // "current test case" in the child process, so we'll let the parent // process sort that out. From 03ec4840fe952ba419e46b1bfaf13714a55593d9 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Mon, 8 Sep 2025 17:53:56 -0400 Subject: [PATCH 126/216] Improve `ExitStatus.description` for signals. (#1302) This PR uses the `sys_signame` array present on many UNIX-like systems to derive a better description for values of type `ExitStatus` and `ExitTest.Condition`. (On Linux, the equivalent `sigabbrev_np()` is used. Windows and WASI don't have an equivalent API.) Before: ```swift let s = ExitStatus.signal(SIGABRT) print(s) // ".signal(12345)" ``` After: ```swift let s = ExitStatus.signal(SIGABRT) print(s) // ".signal(SIGABRT)" ``` ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- Sources/Testing/ExitTests/ExitStatus.swift | 44 ++++++++++++++++++++-- Tests/TestingTests/ExitTestTests.swift | 9 +++++ 2 files changed, 50 insertions(+), 3 deletions(-) diff --git a/Sources/Testing/ExitTests/ExitStatus.swift b/Sources/Testing/ExitTests/ExitStatus.swift index 69c583543..d4a95e14d 100644 --- a/Sources/Testing/ExitTests/ExitStatus.swift +++ b/Sources/Testing/ExitTests/ExitStatus.swift @@ -96,7 +96,19 @@ public enum ExitStatus: Sendable { extension ExitStatus: Equatable {} // MARK: - CustomStringConvertible -@_spi(Experimental) + +#if os(Linux) && !SWT_NO_DYNAMIC_LINKING +/// Get the short name of a signal constant. +/// +/// This symbol is provided because the underlying function was added to glibc +/// relatively recently and may not be available on all targets. Checking +/// `__GLIBC_PREREQ()` is insufficient because `_GNU_SOURCE` may not be defined +/// at the point string.h is first included. +private let _sigabbrev_np = symbol(named: "sigabbrev_np").map { + castCFunction(at: $0, to: (@convention(c) (CInt) -> UnsafePointer?).self) +} +#endif + #if SWT_NO_PROCESS_SPAWNING @available(*, unavailable, message: "Exit tests are not available on this platform.") #endif @@ -104,9 +116,35 @@ extension ExitStatus: CustomStringConvertible { public var description: String { switch self { case let .exitCode(exitCode): - ".exitCode(\(exitCode))" + return ".exitCode(\(exitCode))" case let .signal(signal): - ".signal(\(signal))" + var signalName: String? + +#if SWT_TARGET_OS_APPLE || os(FreeBSD) || os(OpenBSD) || os(Android) + // These platforms define sys_signame with a size, which is imported + // into Swift as a tuple. + withUnsafeBytes(of: sys_signame) { sys_signame in + sys_signame.withMemoryRebound(to: UnsafePointer.self) { sys_signame in + if signal > 0 && signal < sys_signame.count { + signalName = String(validatingCString: sys_signame[Int(signal)])?.uppercased() + } + } + } +#elseif os(Linux) +#if !SWT_NO_DYNAMIC_LINKING + signalName = _sigabbrev_np?(signal).flatMap(String.init(validatingCString:)) +#endif +#elseif os(Windows) || os(WASI) + // These platforms do not have API to get the programmatic name of a + // signal constant. +#else +#warning("Platform-specific implementation missing: signal names unavailable") +#endif + + if let signalName { + return ".signal(SIG\(signalName) → \(signal))" + } + return ".signal(\(signal))" } } } diff --git a/Tests/TestingTests/ExitTestTests.swift b/Tests/TestingTests/ExitTestTests.swift index e68e26c60..1801a21b4 100644 --- a/Tests/TestingTests/ExitTestTests.swift +++ b/Tests/TestingTests/ExitTestTests.swift @@ -13,6 +13,15 @@ private import _TestingInternals #if !SWT_NO_EXIT_TESTS @Suite("Exit test tests") struct ExitTestTests { + @Test("Signal names are reported (where supported)") func signalName() { + let exitStatus = ExitStatus.signal(SIGABRT) +#if SWT_TARGET_OS_APPLE || os(Linux) || os(FreeBSD) || os(OpenBSD) || os(Android) + #expect(String(describing: exitStatus) == ".signal(SIGABRT → \(SIGABRT))") +#else + #expect(String(describing: exitStatus) == ".signal(\(SIGABRT))") +#endif + } + @Test("Exit tests (passing)") func passing() async { await #expect(processExitsWith: .failure) { exit(EXIT_FAILURE) From 3366f65c266637c1fe2ec7cca5a23cae9399b8fc Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Tue, 9 Sep 2025 12:08:30 -0400 Subject: [PATCH 127/216] Add CMake directions for the WinSDK overlay (#1296) Add CMake scripting/etc. for the WinSDK cross-import overlay. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- Package.swift | 1 + Sources/Overlays/CMakeLists.txt | 1 + .../AttachableImageFormat+CLSID.swift | 5 +-- ...pSource+AttachableAsIWICBitmapSource.swift | 2 ++ ...chableImageWrapper+AttachableWrapper.swift | 4 +-- .../Overlays/_Testing_WinSDK/CMakeLists.txt | 32 +++++++++++++++++++ .../IWICImagingFactoryAdditions.swift | 2 +- .../Images/ImageAttachmentError.swift | 10 +++--- .../WinSDK.swiftoverlay | 3 ++ 9 files changed, 50 insertions(+), 10 deletions(-) create mode 100644 Sources/Overlays/_Testing_WinSDK/CMakeLists.txt create mode 100644 Sources/Testing/Testing.swiftcrossimport/WinSDK.swiftoverlay diff --git a/Package.swift b/Package.swift index 35331296f..5206b268e 100644 --- a/Package.swift +++ b/Package.swift @@ -269,6 +269,7 @@ let package = Package( "Testing", ], path: "Sources/Overlays/_Testing_WinSDK", + exclude: ["CMakeLists.txt"], swiftSettings: .packageSettings + .enableLibraryEvolution() ), diff --git a/Sources/Overlays/CMakeLists.txt b/Sources/Overlays/CMakeLists.txt index 5629e8229..d4a0e75f2 100644 --- a/Sources/Overlays/CMakeLists.txt +++ b/Sources/Overlays/CMakeLists.txt @@ -11,3 +11,4 @@ add_subdirectory(_Testing_CoreGraphics) add_subdirectory(_Testing_CoreImage) add_subdirectory(_Testing_Foundation) add_subdirectory(_Testing_UIKit) +add_subdirectory(_Testing_WinSDK) diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableImageFormat+CLSID.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableImageFormat+CLSID.swift index dcdec67f2..2928d7af5 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableImageFormat+CLSID.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableImageFormat+CLSID.swift @@ -12,6 +12,7 @@ @_spi(Experimental) public import Testing public import WinSDK +@_spi(Experimental) extension AttachableImageFormat { private static let _encoderPathExtensionsByCLSID = Result<[UInt128: [String]], any Error> { var result = [UInt128: [String]]() @@ -26,8 +27,8 @@ extension AttachableImageFormat { var enumerator: UnsafeMutablePointer? let rCreate = factory.pointee.lpVtbl.pointee.CreateComponentEnumerator( factory, - DWORD(bitPattern: WICEncoder.rawValue), - DWORD(bitPattern: WICComponentEnumerateDefault.rawValue), + DWORD(WICEncoder.rawValue), + DWORD(WICComponentEnumerateDefault.rawValue), &enumerator ) guard rCreate == S_OK, let enumerator else { diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/IWICBitmapSource+AttachableAsIWICBitmapSource.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/IWICBitmapSource+AttachableAsIWICBitmapSource.swift index 8ff6d5430..55c2ec4c2 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/IWICBitmapSource+AttachableAsIWICBitmapSource.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/IWICBitmapSource+AttachableAsIWICBitmapSource.swift @@ -96,6 +96,7 @@ extension UnsafeMutablePointer where Pointee: IWICBitmapSourceProtocol { // MARK: - _AttachableByAddressAsIWICBitmapSource implementation +@_spi(Experimental) extension IWICBitmapSourceProtocol { public static func _copyAttachableIWICBitmapSource( from imageAddress: UnsafeMutablePointer, @@ -119,6 +120,7 @@ extension IWICBitmapSourceProtocol { } extension IWICBitmapSource { + @_spi(Experimental) public static func _copyAttachableIWICBitmapSource( from imageAddress: UnsafeMutablePointer, using factory: UnsafeMutablePointer diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/_AttachableImageWrapper+AttachableWrapper.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/_AttachableImageWrapper+AttachableWrapper.swift index d80c2eb01..ecf4602e4 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/_AttachableImageWrapper+AttachableWrapper.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/_AttachableImageWrapper+AttachableWrapper.swift @@ -46,7 +46,7 @@ extension _AttachableImageWrapper: Attachable, AttachableWrapper where Image: At let rCreate = CoCreateInstance( &encoderCLSID, nil, - DWORD(bitPattern: CLSCTX_INPROC_SERVER.rawValue), + DWORD(CLSCTX_INPROC_SERVER.rawValue), IID_IWICBitmapEncoder, &encoder ) @@ -93,7 +93,7 @@ extension _AttachableImageWrapper: Attachable, AttachableWrapper where Image: At guard rCommit == S_OK else { throw ImageAttachmentError.imageWritingFailed(rCommit) } - rCommit = stream.pointee.lpVtbl.pointee.Commit(stream, DWORD(bitPattern: STGC_DEFAULT.rawValue)) + rCommit = stream.pointee.lpVtbl.pointee.Commit(stream, DWORD(STGC_DEFAULT.rawValue)) guard rCommit == S_OK else { throw ImageAttachmentError.imageWritingFailed(rCommit) } diff --git a/Sources/Overlays/_Testing_WinSDK/CMakeLists.txt b/Sources/Overlays/_Testing_WinSDK/CMakeLists.txt new file mode 100644 index 000000000..4dcbc706e --- /dev/null +++ b/Sources/Overlays/_Testing_WinSDK/CMakeLists.txt @@ -0,0 +1,32 @@ +# This source file is part of the Swift.org open source project +# +# Copyright (c) 2024–2025 Apple Inc. and the Swift project authors +# Licensed under Apache License v2.0 with Runtime Library Exception +# +# See http://swift.org/LICENSE.txt for license information +# See http://swift.org/CONTRIBUTORS.txt for Swift project authors + +if (CMAKE_SYSTEM_NAME STREQUAL "Windows") + add_library(_Testing_WinSDK + Attachments/_AttachableImageWrapper+AttachableWrapper.swift + Attachments/AttachableAsIWICBitmapSource.swift + Attachments/AttachableImageFormat+CLSID.swift + Attachments/Attachment+AttachableAsIWICBitmapSource.swift + Attachments/HBITMAP+AttachableAsIWICBitmapSource.swift + Attachments/HICON+AttachableAsIWICBitmapSource.swift + Attachments/IWICBitmapSource+AttachableAsIWICBitmapSource.swift + Attachments/UnsafeMutablePointer+AttachableAsIWICBitmapSource.swift + Support/Additions/GUIDAdditions.swift + Support/Additions/IPropertyBag2Additions.swift + Support/Additions/IWICImagingFactoryAdditions.swift + ReexportTesting.swift) + + target_link_libraries(_Testing_WinSDK PUBLIC + Testing) + + target_compile_options(_Testing_WinSDK PRIVATE + -enable-library-evolution + -emit-module-interface -emit-module-interface-path $/_Testing_WinSDK.swiftinterface) + + _swift_testing_install_target(_Testing_WinSDK) +endif() diff --git a/Sources/Overlays/_Testing_WinSDK/Support/Additions/IWICImagingFactoryAdditions.swift b/Sources/Overlays/_Testing_WinSDK/Support/Additions/IWICImagingFactoryAdditions.swift index dc11ab0fc..acacc80b1 100644 --- a/Sources/Overlays/_Testing_WinSDK/Support/Additions/IWICImagingFactoryAdditions.swift +++ b/Sources/Overlays/_Testing_WinSDK/Support/Additions/IWICImagingFactoryAdditions.swift @@ -25,7 +25,7 @@ extension IWICImagingFactory { let rCreate = CoCreateInstance( CLSID_WICImagingFactory, nil, - DWORD(bitPattern: CLSCTX_INPROC_SERVER.rawValue), + DWORD(CLSCTX_INPROC_SERVER.rawValue), IID_IWICImagingFactory, &factory ) diff --git a/Sources/Testing/Attachments/Images/ImageAttachmentError.swift b/Sources/Testing/Attachments/Images/ImageAttachmentError.swift index de421bfd9..1bd90b641 100644 --- a/Sources/Testing/Attachments/Images/ImageAttachmentError.swift +++ b/Sources/Testing/Attachments/Images/ImageAttachmentError.swift @@ -23,19 +23,19 @@ package enum ImageAttachmentError: Error { case couldNotConvertImage #elseif os(Windows) /// A call to `QueryInterface()` failed. - case queryInterfaceFailed(Any.Type, Int32) + case queryInterfaceFailed(Any.Type, CLong) /// The testing library failed to create a COM object. - case comObjectCreationFailed(Any.Type, Int32) + case comObjectCreationFailed(Any.Type, CLong) /// An image could not be written. - case imageWritingFailed(Int32) + case imageWritingFailed(CLong) /// The testing library failed to get an in-memory stream's underlying buffer. - case globalFromStreamFailed(Int32) + case globalFromStreamFailed(CLong) /// A property could not be written to a property bag. - case propertyBagWritingFailed(String, Int32) + case propertyBagWritingFailed(String, CLong) #endif } diff --git a/Sources/Testing/Testing.swiftcrossimport/WinSDK.swiftoverlay b/Sources/Testing/Testing.swiftcrossimport/WinSDK.swiftoverlay new file mode 100644 index 000000000..fdaa23701 --- /dev/null +++ b/Sources/Testing/Testing.swiftcrossimport/WinSDK.swiftoverlay @@ -0,0 +1,3 @@ +version: 1 +modules: +- name: _Testing_WinSDK From 389484526a17f7bb78508ce04851ff1d1a9e2e4e Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Tue, 9 Sep 2025 17:50:53 -0400 Subject: [PATCH 128/216] Work around a compile-time error on the 6.2 toolchain (Windows only). (#1306) Disable a test that is failing to build on Windows with the 6.2 toolchain. Works around https://github.com/swiftlang/swift/issues/84184. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- Tests/TestingTests/AttachmentTests.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Tests/TestingTests/AttachmentTests.swift b/Tests/TestingTests/AttachmentTests.swift index faf12ac5b..ae95d7c65 100644 --- a/Tests/TestingTests/AttachmentTests.swift +++ b/Tests/TestingTests/AttachmentTests.swift @@ -53,12 +53,14 @@ struct AttachmentTests { #expect(attachment.description.contains("MySendableAttachable(")) } +#if compiler(>=6.3) || !os(Windows) // WORKAROUND: swift-#84184 @Test func moveOnlyDescription() { let attachableValue = MyAttachable(string: "") let attachment = Attachment(attachableValue, named: "AttachmentTests.saveValue.html") #expect(attachment.description.contains(#""\#(attachment.preferredName)""#)) #expect(attachment.description.contains("'MyAttachable'")) } +#endif #if !SWT_NO_FILE_IO func compare(_ attachableValue: borrowing MySendableAttachable, toContentsOfFileAtPath filePath: String) throws { From 9e7fc29411ccdc319bdc5c32ca6e7c2630214db7 Mon Sep 17 00:00:00 2001 From: Mishal Shah Date: Tue, 9 Sep 2025 23:08:20 -0700 Subject: [PATCH 129/216] [CI] Add support for GitHub Actions --- .github/workflows/pull_request.yml | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 .github/workflows/pull_request.yml diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml new file mode 100644 index 000000000..e6a6a26a9 --- /dev/null +++ b/.github/workflows/pull_request.yml @@ -0,0 +1,23 @@ +name: Pull request + +on: + pull_request: + types: [opened, reopened, synchronize] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + tests: + name: Test + uses: swiftlang/github-workflows/.github/workflows/swift_package_test.yml@main + with: + linux_os_versions: '["amazonlinux2", "bookworm", "noble", "jammy", "rhel-ubi9"]' + linux_swift_versions: '["6.1", "nightly-main", "nightly-6.2"]' + windows_swift_versions: '["6.1", "nightly-main", "nightly-6.2"]' + soundness: + name: Soundness + uses: swiftlang/github-workflows/.github/workflows/soundness.yml@main + with: + license_header_check_project_name: "Swift" From 354d256badef7839f0016db1727c3439fe28273d Mon Sep 17 00:00:00 2001 From: Mishal Shah Date: Wed, 10 Sep 2025 00:48:37 -0700 Subject: [PATCH 130/216] Update pull_request.yml to include macOS and wasm --- .github/workflows/pull_request.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index e6a6a26a9..2194fcb00 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -13,9 +13,10 @@ jobs: name: Test uses: swiftlang/github-workflows/.github/workflows/swift_package_test.yml@main with: - linux_os_versions: '["amazonlinux2", "bookworm", "noble", "jammy", "rhel-ubi9"]' - linux_swift_versions: '["6.1", "nightly-main", "nightly-6.2"]' - windows_swift_versions: '["6.1", "nightly-main", "nightly-6.2"]' + linux_swift_versions: '["nightly-main", "nightly-6.2"]' + windows_swift_versions: '["nightly-main", "nightly-6.2"]' + enable_macos_checks: true + enable_wasm_sdk_build: true soundness: name: Soundness uses: swiftlang/github-workflows/.github/workflows/soundness.yml@main From 5fb6744e2ba5e3beddd3e3e8a5d825c566f49642 Mon Sep 17 00:00:00 2001 From: Mishal Shah Date: Wed, 10 Sep 2025 01:11:03 -0700 Subject: [PATCH 131/216] Disable broken CI checks for now. --- .github/workflows/pull_request.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 2194fcb00..0ce8ad2e5 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -16,9 +16,14 @@ jobs: linux_swift_versions: '["nightly-main", "nightly-6.2"]' windows_swift_versions: '["nightly-main", "nightly-6.2"]' enable_macos_checks: true + macos_exclude_xcode_versions: "[{\"xcode_version\": \"16.2\"}, {\"xcode_version\": \"16.3\"}]" enable_wasm_sdk_build: true soundness: name: Soundness uses: swiftlang/github-workflows/.github/workflows/soundness.yml@main with: license_header_check_project_name: "Swift" + license_header_check_enabled: false + docs_check_enabled: false + unacceptable_language_check_enabled: false + format_check_enabled: false From b56289ab96e1ee589e04a57cd631e207c9ae30fa Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Wed, 10 Sep 2025 12:32:45 -0400 Subject: [PATCH 132/216] Add task names to structured tasks we create. (#1305) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR sets a task name for every child task created by Swift Testing. These names are useful for debugging. Example result: Screenshot 2025-09-10 at 11 49 42 AM Resolves #1303. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --------- Co-authored-by: Stuart Montgomery --- Sources/Testing/CMakeLists.txt | 1 + .../Event.HumanReadableOutputRecorder.swift | 34 ++++++++++------ Sources/Testing/ExitTests/ExitTest.swift | 8 ++-- Sources/Testing/Running/Runner.Plan.swift | 9 ++++- Sources/Testing/Running/Runner.swift | 40 ++++++++++++++++--- .../Support/Additions/TaskAdditions.swift | 27 +++++++++++++ Sources/Testing/Test+Discovery.swift | 12 ++++-- Sources/Testing/Traits/TimeLimitTrait.swift | 6 ++- 8 files changed, 107 insertions(+), 30 deletions(-) create mode 100644 Sources/Testing/Support/Additions/TaskAdditions.swift diff --git a/Sources/Testing/CMakeLists.txt b/Sources/Testing/CMakeLists.txt index d2cd21ab9..f895996be 100644 --- a/Sources/Testing/CMakeLists.txt +++ b/Sources/Testing/CMakeLists.txt @@ -76,6 +76,7 @@ add_library(Testing Support/Additions/CommandLineAdditions.swift Support/Additions/NumericAdditions.swift Support/Additions/ResultAdditions.swift + Support/Additions/TaskAdditions.swift Support/Additions/WinSDKAdditions.swift Support/CartesianProduct.swift Support/CError.swift diff --git a/Sources/Testing/Events/Recorder/Event.HumanReadableOutputRecorder.swift b/Sources/Testing/Events/Recorder/Event.HumanReadableOutputRecorder.swift index 7497ad4cd..32d1ce770 100644 --- a/Sources/Testing/Events/Recorder/Event.HumanReadableOutputRecorder.swift +++ b/Sources/Testing/Events/Recorder/Event.HumanReadableOutputRecorder.swift @@ -187,6 +187,26 @@ private func _capitalizedTitle(for test: Test?) -> String { test?.isSuite == true ? "Suite" : "Test" } +extension Test { + /// The name to use for this test in a human-readable context such as console + /// output. + /// + /// - Parameters: + /// - verbosity: The verbosity with which to describe this test. + /// + /// - Returns: The name of this test, suitable for display to the user. + func humanReadableName(withVerbosity verbosity: Int = 0) -> String { + switch displayName { + case let .some(displayName) where verbosity > 0: + #""\#(displayName)" (aka '\#(name)')"# + case let .some(displayName): + #""\#(displayName)""# + default: + name + } + } +} + extension Test.Case { /// The arguments of this test case, formatted for presentation, prefixed by /// their corresponding parameter label when available. @@ -256,19 +276,7 @@ extension Event.HumanReadableOutputRecorder { let test = eventContext.test let testCase = eventContext.testCase let keyPath = eventContext.keyPath - let testName = if let test { - if let displayName = test.displayName { - if verbosity > 0 { - "\"\(displayName)\" (aka '\(test.name)')" - } else { - "\"\(displayName)\"" - } - } else { - test.name - } - } else { - "«unknown»" - } + let testName = test?.humanReadableName(withVerbosity: verbosity) ?? "«unknown»" let instant = event.instant let iterationCount = eventContext.configuration?.repetitionPolicy.maximumIterationCount diff --git a/Sources/Testing/ExitTests/ExitTest.swift b/Sources/Testing/ExitTests/ExitTest.swift index ff8d805ae..c1de79091 100644 --- a/Sources/Testing/ExitTests/ExitTest.swift +++ b/Sources/Testing/ExitTests/ExitTest.swift @@ -955,7 +955,7 @@ extension ExitTest { capturedValuesWriteEnd.close() // Await termination of the child process. - taskGroup.addTask { + taskGroup.addTask(name: decorateTaskName("exit test", withAction: "awaiting termination")) { let exitStatus = try await wait(for: processID) return { $0.exitStatus = exitStatus } } @@ -963,14 +963,14 @@ extension ExitTest { // Read back the stdout and stderr streams. if let stdoutReadEnd { stdoutWriteEnd?.close() - taskGroup.addTask { + taskGroup.addTask(name: decorateTaskName("exit test", withAction: "reading stdout")) { let standardOutputContent = try Self._trimToBarrierValues(stdoutReadEnd.readToEnd()) return { $0.standardOutputContent = standardOutputContent } } } if let stderrReadEnd { stderrWriteEnd?.close() - taskGroup.addTask { + taskGroup.addTask(name: decorateTaskName("exit test", withAction: "reading stderr")) { let standardErrorContent = try Self._trimToBarrierValues(stderrReadEnd.readToEnd()) return { $0.standardErrorContent = standardErrorContent } } @@ -979,7 +979,7 @@ extension ExitTest { // Read back all data written to the back channel by the child process // and process it as a (minimal) event stream. backChannelWriteEnd.close() - taskGroup.addTask { + taskGroup.addTask(name: decorateTaskName("exit test", withAction: "processing events")) { Self._processRecords(fromBackChannel: backChannelReadEnd) return nil } diff --git a/Sources/Testing/Running/Runner.Plan.swift b/Sources/Testing/Running/Runner.Plan.swift index 4bef38942..92827ad36 100644 --- a/Sources/Testing/Running/Runner.Plan.swift +++ b/Sources/Testing/Running/Runner.Plan.swift @@ -213,7 +213,14 @@ extension Runner.Plan { // FIXME: Parallelize this work. Calling `prepare(...)` on all traits and // evaluating all test arguments should be safely parallelizable. (test, result) = await withTaskGroup(returning: (Test, Action).self) { [test] taskGroup in - taskGroup.addTask { + let testName = test.humanReadableName() + let (taskName, taskAction) = if test.isSuite { + ("suite \(testName)", "evaluating traits") + } else { + // TODO: split the task group's single task into two serially-run subtasks + ("test \(testName)", "evaluating traits and test cases") + } + taskGroup.addTask(name: decorateTaskName(taskName, withAction: taskAction)) { var test = test var action = _runAction diff --git a/Sources/Testing/Running/Runner.swift b/Sources/Testing/Running/Runner.swift index d5a844fba..1cedf6182 100644 --- a/Sources/Testing/Running/Runner.swift +++ b/Sources/Testing/Running/Runner.swift @@ -149,17 +149,21 @@ extension Runner { /// /// - Parameters: /// - sequence: The sequence to enumerate. + /// - taskNamer: A function to invoke for each element in `sequence`. The + /// result of this function is used to name each child task. /// - body: The function to invoke. /// /// - Throws: Whatever is thrown by `body`. private static func _forEach( in sequence: some Sequence, - _ body: @Sendable @escaping (E) async throws -> Void + namingTasksWith taskNamer: (borrowing E) -> (taskName: String, action: String?)?, + _ body: @Sendable @escaping (borrowing E) async throws -> Void ) async rethrows where E: Sendable { try await withThrowingTaskGroup { taskGroup in for element in sequence { // Each element gets its own subtask to run in. - taskGroup.addTask { + let taskName = taskNamer(element) + taskGroup.addTask(name: decorateTaskName(taskName?.taskName, withAction: taskName?.action)) { try await body(element) } @@ -314,8 +318,19 @@ extension Runner { } } + // Figure out how to name child tasks. + func taskNamer(_ childGraph: Graph) -> (String, String?)? { + childGraph.value.map { step in + let testName = step.test.humanReadableName() + if step.test.isSuite { + return ("suite \(testName)", "running") + } + return ("test \(testName)", nil) // test cases have " - running" suffix + } + } + // Run the child nodes. - try await _forEach(in: childGraphs) { _, childGraph in + try await _forEach(in: childGraphs.lazy.map(\.value), namingTasksWith: taskNamer) { childGraph in try await _runStep(atRootOf: childGraph) } } @@ -335,7 +350,15 @@ extension Runner { testCaseFilter(testCase, step.test) } - await _forEach(in: testCases) { testCase in + // Figure out how to name child tasks. + let testName = "test \(step.test.humanReadableName())" + let taskNamer: (Int, Test.Case) -> (String, String?)? = if step.test.isParameterized { + { i, _ in (testName, "running test case #\(i + 1)") } + } else { + { _, _ in (testName, "running") } + } + + await _forEach(in: testCases.enumerated(), namingTasksWith: taskNamer) { _, testCase in await _runTestCase(testCase, within: step) } } @@ -418,14 +441,19 @@ extension Runner { } let repetitionPolicy = runner.configuration.repetitionPolicy - for iterationIndex in 0 ..< repetitionPolicy.maximumIterationCount { + let iterationCount = repetitionPolicy.maximumIterationCount + for iterationIndex in 0 ..< iterationCount { Event.post(.iterationStarted(iterationIndex), for: (nil, nil), configuration: runner.configuration) defer { Event.post(.iterationEnded(iterationIndex), for: (nil, nil), configuration: runner.configuration) } await withTaskGroup { [runner] taskGroup in - _ = taskGroup.addTaskUnlessCancelled { + var taskAction: String? + if iterationCount > 1 { + taskAction = "running iteration #\(iterationIndex + 1)" + } + _ = taskGroup.addTaskUnlessCancelled(name: decorateTaskName("test run", withAction: taskAction)) { try? await _runStep(atRootOf: runner.plan.stepGraph) } await taskGroup.waitForAll() diff --git a/Sources/Testing/Support/Additions/TaskAdditions.swift b/Sources/Testing/Support/Additions/TaskAdditions.swift new file mode 100644 index 000000000..1ec0c3079 --- /dev/null +++ b/Sources/Testing/Support/Additions/TaskAdditions.swift @@ -0,0 +1,27 @@ +// +// 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 +// + +/// Make a (decorated) task name from the given undecorated task name. +/// +/// - Parameters: +/// - taskName: The undecorated task name to modify. +/// +/// - Returns: A copy of `taskName` with a common prefix applied, or `nil` if +/// `taskName` was `nil`. +func decorateTaskName(_ taskName: String?, withAction action: String?) -> String? { + let prefix = "[Swift Testing]" + return taskName.map { taskName in +#if DEBUG + precondition(!taskName.hasPrefix(prefix), "Applied prefix '\(prefix)' to task name '\(taskName)' twice. Please file a bug report at https://github.com/swiftlang/swift-testing/issues/new") +#endif + let action = action.map { " - \($0)" } ?? "" + return "\(prefix) \(taskName)\(action)" + } +} diff --git a/Sources/Testing/Test+Discovery.swift b/Sources/Testing/Test+Discovery.swift index 71862943d..87dafe7a4 100644 --- a/Sources/Testing/Test+Discovery.swift +++ b/Sources/Testing/Test+Discovery.swift @@ -88,8 +88,10 @@ extension Test { if useNewMode { let generators = Generator.allTestContentRecords().lazy.compactMap { $0.load() } await withTaskGroup { taskGroup in - for generator in generators { - taskGroup.addTask { await generator.rawValue() } + for (i, generator) in generators.enumerated() { + taskGroup.addTask(name: decorateTaskName("test discovery", withAction: "loading test #\(i)")) { + await generator.rawValue() + } } result = await taskGroup.reduce(into: result) { $0.insert($1) } } @@ -100,8 +102,10 @@ extension Test { if useLegacyMode && result.isEmpty { let generators = Generator.allTypeMetadataBasedTestContentRecords().lazy.compactMap { $0.load() } await withTaskGroup { taskGroup in - for generator in generators { - taskGroup.addTask { await generator.rawValue() } + for (i, generator) in generators.enumerated() { + taskGroup.addTask(name: decorateTaskName("type-based test discovery", withAction: "loading test #\(i)")) { + await generator.rawValue() + } } result = await taskGroup.reduce(into: result) { $0.insert($1) } } diff --git a/Sources/Testing/Traits/TimeLimitTrait.swift b/Sources/Testing/Traits/TimeLimitTrait.swift index e9216b242..10f3fc31a 100644 --- a/Sources/Testing/Traits/TimeLimitTrait.swift +++ b/Sources/Testing/Traits/TimeLimitTrait.swift @@ -264,6 +264,7 @@ struct TimeoutError: Error, CustomStringConvertible { /// /// - Parameters: /// - timeLimit: The amount of time until the closure times out. +/// - taskName: The name of the child task that runs `body`, if any. /// - body: The function to invoke. /// - timeoutHandler: A function to invoke if `body` times out. /// @@ -277,18 +278,19 @@ struct TimeoutError: Error, CustomStringConvertible { @available(_clockAPI, *) func withTimeLimit( _ timeLimit: Duration, + taskName: String? = nil, _ body: @escaping @Sendable () async throws -> Void, timeoutHandler: @escaping @Sendable () -> Void ) async throws { try await withThrowingTaskGroup { group in - group.addTask { + group.addTask(name: decorateTaskName(taskName, withAction: "waiting for timeout")) { // If sleep() returns instead of throwing a CancellationError, that means // the timeout was reached before this task could be cancelled, so call // the timeout handler. try await Test.Clock.sleep(for: timeLimit) timeoutHandler() } - group.addTask(operation: body) + group.addTask(name: decorateTaskName(taskName, withAction: "running"), operation: body) defer { group.cancelAll() From d96b282733a74cdd29da672dd8eac29230770984 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Thu, 11 Sep 2025 13:44:37 -0400 Subject: [PATCH 133/216] Disable the API breakage check in GitHub Actions. (#1311) Disable the API breakage check in GitHub Actions. We are not targetting the 6.1 toolchain, so we aren't worried about API breakage when built against it. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- .github/workflows/pull_request.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 0ce8ad2e5..306ab13bd 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -27,3 +27,4 @@ jobs: docs_check_enabled: false unacceptable_language_check_enabled: false format_check_enabled: false + api_breakage_check_enabled: false From f5c9286897897a7c17bc3b971431621465f4f9cb Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Thu, 11 Sep 2025 14:23:34 -0400 Subject: [PATCH 134/216] Factor out the commit hash from the library version. (#1310) This PR captures the current commit hash in a different variable from the library version. There is no user-facing impact. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- Package.swift | 21 ++++++++---- Sources/Testing/Support/Versions.swift | 23 +++++++++++-- Sources/_TestingInternals/CMakeLists.txt | 1 + Sources/_TestingInternals/Versions.cpp | 13 ++++++++ Sources/_TestingInternals/include/Versions.h | 9 +++++ cmake/modules/GitCommit.cmake | 35 ++++++++++++++++++++ cmake/modules/LibraryVersion.cmake | 28 ---------------- 7 files changed, 92 insertions(+), 38 deletions(-) create mode 100644 cmake/modules/GitCommit.cmake diff --git a/Package.swift b/Package.swift index 5206b268e..840541887 100644 --- a/Package.swift +++ b/Package.swift @@ -468,16 +468,23 @@ extension Array where Element == PackageDescription.CXXSetting { .define("SWT_NO_LIBDISPATCH", .whenEmbedded()), ] - // Capture the testing library's version as a C++ string constant. + // Capture the testing library's commit info as C++ constants. if let git { - let testingLibraryVersion = if let tag = git.currentTag { - tag - } else if git.hasUncommittedChanges { - "\(git.currentCommit) (modified)" + if let tag = git.currentTag { + result.append(.define("SWT_TESTING_LIBRARY_VERSION", to: #""\#(tag)""#)) } else { - git.currentCommit + result.append(.define("SWT_TESTING_LIBRARY_VERSION", to: "0")) } - result.append(.define("SWT_TESTING_LIBRARY_VERSION", to: #""\#(testingLibraryVersion)""#)) + + result.append(.define("SWT_TESTING_LIBRARY_COMMIT_HASH", to: #""\#(git.currentCommit)""#)) + if git.hasUncommittedChanges { + result.append(.define("SWT_TESTING_LIBRARY_COMMIT_MODIFIED", to: "1")) + } + } else if let gitHubSHA = Context.environment["GITHUB_SHA"] { + // When building in GitHub Actions, the git command may fail to get us the + // commit hash, so check if GitHub shared it with us instead. + result.append(.define("SWT_TESTING_LIBRARY_VERSION", to: "0")) + result.append(.define("SWT_TESTING_LIBRARY_COMMIT_HASH", to: #""\#(gitHubSHA)""#)) } return result diff --git a/Sources/Testing/Support/Versions.swift b/Sources/Testing/Support/Versions.swift index 62193d6c4..76c5aeaf6 100644 --- a/Sources/Testing/Support/Versions.swift +++ b/Sources/Testing/Support/Versions.swift @@ -129,9 +129,26 @@ let simulatorVersion: String = { /// an event writer. /// /// This value is not part of the public interface of the testing library. -var testingLibraryVersion: String { - swt_getTestingLibraryVersion().flatMap(String.init(validatingCString:)) ?? "unknown" -} +let testingLibraryVersion: String = { + var result = swt_getTestingLibraryVersion().flatMap(String.init(validatingCString:)) ?? "unknown" + + // Get details of the git commit used when compiling the testing library. + var commitHash: UnsafePointer? + var commitModified = CBool(false) + swt_getTestingLibraryCommit(&commitHash, &commitModified) + + if let commitHash = commitHash.flatMap(String.init(validatingCString:)) { + // Truncate to 15 characters of the hash to match `swift --version`. + let commitHash = commitHash.prefix(15) + if commitModified { + result = "\(result) (\(commitHash) - modified)" + } else { + result = "\(result) (\(commitHash))" + } + } + + return result +}() /// Get the LLVM target triple used to build the testing library, if available. /// diff --git a/Sources/_TestingInternals/CMakeLists.txt b/Sources/_TestingInternals/CMakeLists.txt index 16713ab27..2b385cb1b 100644 --- a/Sources/_TestingInternals/CMakeLists.txt +++ b/Sources/_TestingInternals/CMakeLists.txt @@ -8,6 +8,7 @@ set(CMAKE_CXX_SCAN_FOR_MODULES 0) +include(GitCommit) include(LibraryVersion) include(TargetTriple) add_library(_TestingInternals STATIC diff --git a/Sources/_TestingInternals/Versions.cpp b/Sources/_TestingInternals/Versions.cpp index 97eace99e..8e85f3d06 100644 --- a/Sources/_TestingInternals/Versions.cpp +++ b/Sources/_TestingInternals/Versions.cpp @@ -19,6 +19,19 @@ const char *swt_getTestingLibraryVersion(void) { #endif } +void swt_getTestingLibraryCommit(const char *_Nullable *_Nonnull outHash, bool *outModified) { +#if defined(SWT_TESTING_LIBRARY_COMMIT_HASH) + *outHash = SWT_TESTING_LIBRARY_COMMIT_HASH; +#else + *outHash = nullptr; +#endif +#if defined(SWT_TESTING_LIBRARY_COMMIT_MODIFIED) + *outModified = (SWT_TESTING_LIBRARY_COMMIT_MODIFIED != 0); +#else + *outModified = false; +#endif +} + const char *swt_getTargetTriple(void) { #if defined(SWT_TARGET_TRIPLE) return SWT_TARGET_TRIPLE; diff --git a/Sources/_TestingInternals/include/Versions.h b/Sources/_TestingInternals/include/Versions.h index ed523b86f..b188a4d66 100644 --- a/Sources/_TestingInternals/include/Versions.h +++ b/Sources/_TestingInternals/include/Versions.h @@ -38,6 +38,15 @@ static inline uint64_t swt_getSwiftCompilerVersion(void) { /// other conditions. Do not attempt to parse it. SWT_EXTERN const char *_Nullable swt_getTestingLibraryVersion(void); +/// Get details of the source control (git) commit from which the testing +/// library was built. +/// +/// - Parameters: +/// - outHash: On return, set to a pointer to a string containing the commit +/// hash from which the testing library was built. +/// - outModified: On return, whether or not there were uncommitted changes. +SWT_EXTERN void swt_getTestingLibraryCommit(const char *_Nullable *_Nonnull outHash, bool *outModified); + /// Get the LLVM target triple used to build the testing library. /// /// - Returns: A string containing the LLVM target triple used to build the diff --git a/cmake/modules/GitCommit.cmake b/cmake/modules/GitCommit.cmake new file mode 100644 index 000000000..86b0b4733 --- /dev/null +++ b/cmake/modules/GitCommit.cmake @@ -0,0 +1,35 @@ +# This source file is part of the Swift.org open source project +# +# Copyright (c) 2024–2025 Apple Inc. and the Swift project authors +# Licensed under Apache License v2.0 with Runtime Library Exception +# +# See http://swift.org/LICENSE.txt for license information +# See http://swift.org/CONTRIBUTORS.txt for Swift project authors + +find_package(Git QUIET) +if(Git_FOUND) + # Get the commit hash corresponding to the current build. + execute_process( + COMMAND ${GIT_EXECUTABLE} rev-parse --verify HEAD + WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} + OUTPUT_VARIABLE SWT_TESTING_LIBRARY_COMMIT_HASH + OUTPUT_STRIP_TRAILING_WHITESPACE + ERROR_QUIET) + + # Check if there are local changes. + execute_process( + COMMAND ${GIT_EXECUTABLE} status -s + WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} + OUTPUT_VARIABLE SWT_TESTING_LIBRARY_COMMIT_MODIFIED + OUTPUT_STRIP_TRAILING_WHITESPACE) +endif() + +if(SWT_TESTING_LIBRARY_COMMIT_HASH) + message(STATUS "Swift Testing commit hash: ${SWT_TESTING_LIBRARY_COMMIT_HASH}") + add_compile_definitions( + "$<$:SWT_TESTING_LIBRARY_COMMIT_HASH=\"${SWT_TESTING_LIBRARY_COMMIT_HASH}\">") + if(SWT_TESTING_LIBRARY_COMMIT_MODIFIED) + add_compile_definitions( + "$<$:SWT_TESTING_LIBRARY_COMMIT_MODIFIED=1>") + endif() +endif() diff --git a/cmake/modules/LibraryVersion.cmake b/cmake/modules/LibraryVersion.cmake index 259ead608..32901365a 100644 --- a/cmake/modules/LibraryVersion.cmake +++ b/cmake/modules/LibraryVersion.cmake @@ -10,34 +10,6 @@ # remember to remove -dev. set(SWT_TESTING_LIBRARY_VERSION "6.3-dev") -find_package(Git QUIET) -if(Git_FOUND) - # Get the commit hash corresponding to the current build. Limit length to 15 - # to match `swift --version` output format. - execute_process( - COMMAND ${GIT_EXECUTABLE} rev-parse --short=15 --verify HEAD - WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} - OUTPUT_VARIABLE GIT_VERSION - OUTPUT_STRIP_TRAILING_WHITESPACE - ERROR_QUIET) - - # Check if there are local changes. - execute_process( - COMMAND ${GIT_EXECUTABLE} status -s - WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} - OUTPUT_VARIABLE GIT_STATUS - OUTPUT_STRIP_TRAILING_WHITESPACE) - if(GIT_STATUS) - set(GIT_VERSION "${GIT_VERSION} - modified") - endif() -endif() - -# Combine the hard-coded Swift version with available Git information. -if(GIT_VERSION) -set(SWT_TESTING_LIBRARY_VERSION "${SWT_TESTING_LIBRARY_VERSION} (${GIT_VERSION})") -endif() - -# All done! message(STATUS "Swift Testing version: ${SWT_TESTING_LIBRARY_VERSION}") add_compile_definitions( "$<$:SWT_TESTING_LIBRARY_VERSION=\"${SWT_TESTING_LIBRARY_VERSION}\">") From 77f84d3e09a44a45063daa9fb3f845757942af2c Mon Sep 17 00:00:00 2001 From: Stuart Montgomery Date: Thu, 11 Sep 2025 17:59:35 -0500 Subject: [PATCH 135/216] Enable license header soundness check (#1312) --- .editorconfig | 10 ++++++++++ .github/workflows/pull_request.yml | 1 - .license_header_template | 9 +++++++++ .licenseignore | 4 ++++ CMakeLists.txt | 4 ++-- Dockerfile | 16 +++++++++------- Sources/CMakeLists.txt | 4 ++-- Sources/Overlays/CMakeLists.txt | 4 ++-- .../Overlays/_Testing_AppKit/CMakeLists.txt | 4 ++-- .../_Testing_CoreGraphics/CMakeLists.txt | 4 ++-- .../Overlays/_Testing_CoreImage/CMakeLists.txt | 4 ++-- .../_Testing_Foundation/CMakeLists.txt | 4 ++-- Sources/Overlays/_Testing_UIKit/CMakeLists.txt | 4 ++-- .../Overlays/_Testing_WinSDK/CMakeLists.txt | 4 ++-- Sources/Testing/CMakeLists.txt | 4 ++-- Sources/TestingMacros/CMakeLists.txt | 4 ++-- Sources/_TestDiscovery/CMakeLists.txt | 4 ++-- Sources/_TestingInternals/CMakeLists.txt | 4 ++-- Tests/TestingTests/ObjCInteropTests.swift | 18 +++++++++--------- cmake/modules/GitCommit.cmake | 16 +++++++++------- cmake/modules/LibraryVersion.cmake | 16 +++++++++------- cmake/modules/PlatformInfo.cmake | 16 +++++++++------- cmake/modules/SwiftModuleInstallation.cmake | 16 +++++++++------- cmake/modules/TargetTriple.cmake | 16 +++++++++------- .../shared/AvailabilityDefinitions.cmake | 16 +++++++++------- cmake/modules/shared/CompilerSettings.cmake | 16 +++++++++------- 26 files changed, 130 insertions(+), 92 deletions(-) create mode 100644 .license_header_template create mode 100644 .licenseignore diff --git a/.editorconfig b/.editorconfig index f9e5f815a..30a99e77b 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,3 +1,13 @@ +## +## This source file is part of the Swift.org open source project +## +## Copyright (c) 2024-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 +## + # EditorConfig documentation: https://editorconfig.org root = true diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 306ab13bd..c0049b554 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -23,7 +23,6 @@ jobs: uses: swiftlang/github-workflows/.github/workflows/soundness.yml@main with: license_header_check_project_name: "Swift" - license_header_check_enabled: false docs_check_enabled: false unacceptable_language_check_enabled: false format_check_enabled: false diff --git a/.license_header_template b/.license_header_template new file mode 100644 index 000000000..d99c3c0f0 --- /dev/null +++ b/.license_header_template @@ -0,0 +1,9 @@ +@@ +@@ This source file is part of the Swift.org open source project +@@ +@@ Copyright (c) YEARS 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 +@@ diff --git a/.licenseignore b/.licenseignore new file mode 100644 index 000000000..b73b15c97 --- /dev/null +++ b/.licenseignore @@ -0,0 +1,4 @@ +Package.swift +**/*.xctestplan +**/*.xcscheme +**/*.swiftoverlay diff --git a/CMakeLists.txt b/CMakeLists.txt index 38aeda617..2f2828d49 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3,8 +3,8 @@ # Copyright (c) 2024 Apple Inc. and the Swift project authors # Licensed under Apache License v2.0 with Runtime Library Exception # -# See http://swift.org/LICENSE.txt for license information -# See http://swift.org/CONTRIBUTORS.txt for Swift project authors +# See https://swift.org/LICENSE.txt for license information +# See https://swift.org/CONTRIBUTORS.txt for Swift project authors cmake_minimum_required(VERSION 3.19.6...3.29) diff --git a/Dockerfile b/Dockerfile index d18db2add..1647b0808 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,10 +1,12 @@ -# 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 +## +## 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 +## FROM swiftlang/swift:nightly-main-jammy diff --git a/Sources/CMakeLists.txt b/Sources/CMakeLists.txt index c6575a310..09e5e9fd6 100644 --- a/Sources/CMakeLists.txt +++ b/Sources/CMakeLists.txt @@ -3,8 +3,8 @@ # Copyright (c) 2024 Apple Inc. and the Swift project authors # Licensed under Apache License v2.0 with Runtime Library Exception # -# See http://swift.org/LICENSE.txt for license information -# See http://swift.org/CONTRIBUTORS.txt for Swift project authors +# See https://swift.org/LICENSE.txt for license information +# See https://swift.org/CONTRIBUTORS.txt for Swift project authors set(SwiftTesting_MACRO "" CACHE STRING "Path to SwiftTesting macro plugin, or '' for automatically building it") diff --git a/Sources/Overlays/CMakeLists.txt b/Sources/Overlays/CMakeLists.txt index d4a0e75f2..434b4d3ec 100644 --- a/Sources/Overlays/CMakeLists.txt +++ b/Sources/Overlays/CMakeLists.txt @@ -3,8 +3,8 @@ # Copyright (c) 2024–2025 Apple Inc. and the Swift project authors # Licensed under Apache License v2.0 with Runtime Library Exception # -# See http://swift.org/LICENSE.txt for license information -# See http://swift.org/CONTRIBUTORS.txt for Swift project authors +# See https://swift.org/LICENSE.txt for license information +# See https://swift.org/CONTRIBUTORS.txt for Swift project authors add_subdirectory(_Testing_AppKit) add_subdirectory(_Testing_CoreGraphics) diff --git a/Sources/Overlays/_Testing_AppKit/CMakeLists.txt b/Sources/Overlays/_Testing_AppKit/CMakeLists.txt index 75a463433..e864509f4 100644 --- a/Sources/Overlays/_Testing_AppKit/CMakeLists.txt +++ b/Sources/Overlays/_Testing_AppKit/CMakeLists.txt @@ -3,8 +3,8 @@ # Copyright (c) 2024–2025 Apple Inc. and the Swift project authors # Licensed under Apache License v2.0 with Runtime Library Exception # -# See http://swift.org/LICENSE.txt for license information -# See http://swift.org/CONTRIBUTORS.txt for Swift project authors +# See https://swift.org/LICENSE.txt for license information +# See https://swift.org/CONTRIBUTORS.txt for Swift project authors if (CMAKE_SYSTEM_NAME STREQUAL "Darwin") add_library(_Testing_AppKit diff --git a/Sources/Overlays/_Testing_CoreGraphics/CMakeLists.txt b/Sources/Overlays/_Testing_CoreGraphics/CMakeLists.txt index 335068a0b..fcd1d3459 100644 --- a/Sources/Overlays/_Testing_CoreGraphics/CMakeLists.txt +++ b/Sources/Overlays/_Testing_CoreGraphics/CMakeLists.txt @@ -3,8 +3,8 @@ # Copyright (c) 2024–2025 Apple Inc. and the Swift project authors # Licensed under Apache License v2.0 with Runtime Library Exception # -# See http://swift.org/LICENSE.txt for license information -# See http://swift.org/CONTRIBUTORS.txt for Swift project authors +# See https://swift.org/LICENSE.txt for license information +# See https://swift.org/CONTRIBUTORS.txt for Swift project authors if(APPLE) add_library(_Testing_CoreGraphics diff --git a/Sources/Overlays/_Testing_CoreImage/CMakeLists.txt b/Sources/Overlays/_Testing_CoreImage/CMakeLists.txt index baa50537e..8c8076b8b 100644 --- a/Sources/Overlays/_Testing_CoreImage/CMakeLists.txt +++ b/Sources/Overlays/_Testing_CoreImage/CMakeLists.txt @@ -3,8 +3,8 @@ # Copyright (c) 2024–2025 Apple Inc. and the Swift project authors # Licensed under Apache License v2.0 with Runtime Library Exception # -# See http://swift.org/LICENSE.txt for license information -# See http://swift.org/CONTRIBUTORS.txt for Swift project authors +# See https://swift.org/LICENSE.txt for license information +# See https://swift.org/CONTRIBUTORS.txt for Swift project authors if(APPLE) add_library(_Testing_CoreImage diff --git a/Sources/Overlays/_Testing_Foundation/CMakeLists.txt b/Sources/Overlays/_Testing_Foundation/CMakeLists.txt index 9343960ab..7da6f773d 100644 --- a/Sources/Overlays/_Testing_Foundation/CMakeLists.txt +++ b/Sources/Overlays/_Testing_Foundation/CMakeLists.txt @@ -3,8 +3,8 @@ # Copyright (c) 2024 Apple Inc. and the Swift project authors # Licensed under Apache License v2.0 with Runtime Library Exception # -# See http://swift.org/LICENSE.txt for license information -# See http://swift.org/CONTRIBUTORS.txt for Swift project authors +# See https://swift.org/LICENSE.txt for license information +# See https://swift.org/CONTRIBUTORS.txt for Swift project authors add_library(_Testing_Foundation Attachments/_AttachableURLWrapper.swift diff --git a/Sources/Overlays/_Testing_UIKit/CMakeLists.txt b/Sources/Overlays/_Testing_UIKit/CMakeLists.txt index 5976e2e86..e6f4ae9d5 100644 --- a/Sources/Overlays/_Testing_UIKit/CMakeLists.txt +++ b/Sources/Overlays/_Testing_UIKit/CMakeLists.txt @@ -3,8 +3,8 @@ # Copyright (c) 2024–2025 Apple Inc. and the Swift project authors # Licensed under Apache License v2.0 with Runtime Library Exception # -# See http://swift.org/LICENSE.txt for license information -# See http://swift.org/CONTRIBUTORS.txt for Swift project authors +# See https://swift.org/LICENSE.txt for license information +# See https://swift.org/CONTRIBUTORS.txt for Swift project authors if(APPLE) add_library(_Testing_UIKit diff --git a/Sources/Overlays/_Testing_WinSDK/CMakeLists.txt b/Sources/Overlays/_Testing_WinSDK/CMakeLists.txt index 4dcbc706e..a1df6bd60 100644 --- a/Sources/Overlays/_Testing_WinSDK/CMakeLists.txt +++ b/Sources/Overlays/_Testing_WinSDK/CMakeLists.txt @@ -3,8 +3,8 @@ # Copyright (c) 2024–2025 Apple Inc. and the Swift project authors # Licensed under Apache License v2.0 with Runtime Library Exception # -# See http://swift.org/LICENSE.txt for license information -# See http://swift.org/CONTRIBUTORS.txt for Swift project authors +# See https://swift.org/LICENSE.txt for license information +# See https://swift.org/CONTRIBUTORS.txt for Swift project authors if (CMAKE_SYSTEM_NAME STREQUAL "Windows") add_library(_Testing_WinSDK diff --git a/Sources/Testing/CMakeLists.txt b/Sources/Testing/CMakeLists.txt index f895996be..8fef166c7 100644 --- a/Sources/Testing/CMakeLists.txt +++ b/Sources/Testing/CMakeLists.txt @@ -3,8 +3,8 @@ # Copyright (c) 2024 Apple Inc. and the Swift project authors # Licensed under Apache License v2.0 with Runtime Library Exception # -# See http://swift.org/LICENSE.txt for license information -# See http://swift.org/CONTRIBUTORS.txt for Swift project authors +# See https://swift.org/LICENSE.txt for license information +# See https://swift.org/CONTRIBUTORS.txt for Swift project authors add_library(Testing ABI/EntryPoints/ABIEntryPoint.swift diff --git a/Sources/TestingMacros/CMakeLists.txt b/Sources/TestingMacros/CMakeLists.txt index 46be5e388..9de696920 100644 --- a/Sources/TestingMacros/CMakeLists.txt +++ b/Sources/TestingMacros/CMakeLists.txt @@ -3,8 +3,8 @@ # Copyright (c) 2024 Apple Inc. and the Swift project authors # Licensed under Apache License v2.0 with Runtime Library Exception # -# See http://swift.org/LICENSE.txt for license information -# See http://swift.org/CONTRIBUTORS.txt for Swift project authors +# See https://swift.org/LICENSE.txt for license information +# See https://swift.org/CONTRIBUTORS.txt for Swift project authors cmake_minimum_required(VERSION 3.19.6...3.29) diff --git a/Sources/_TestDiscovery/CMakeLists.txt b/Sources/_TestDiscovery/CMakeLists.txt index 7d6059792..7da7e0a8c 100644 --- a/Sources/_TestDiscovery/CMakeLists.txt +++ b/Sources/_TestDiscovery/CMakeLists.txt @@ -3,8 +3,8 @@ # Copyright (c) 2023–2025 Apple Inc. and the Swift project authors # Licensed under Apache License v2.0 with Runtime Library Exception # -# See http://swift.org/LICENSE.txt for license information -# See http://swift.org/CONTRIBUTORS.txt for Swift project authors +# See https://swift.org/LICENSE.txt for license information +# See https://swift.org/CONTRIBUTORS.txt for Swift project authors add_library(_TestDiscovery STATIC Additions/WinSDKAdditions.swift diff --git a/Sources/_TestingInternals/CMakeLists.txt b/Sources/_TestingInternals/CMakeLists.txt index 2b385cb1b..90ad424e9 100644 --- a/Sources/_TestingInternals/CMakeLists.txt +++ b/Sources/_TestingInternals/CMakeLists.txt @@ -3,8 +3,8 @@ # Copyright (c) 2024 Apple Inc. and the Swift project authors # Licensed under Apache License v2.0 with Runtime Library Exception # -# See http://swift.org/LICENSE.txt for license information -# See http://swift.org/CONTRIBUTORS.txt for Swift project authors +# See https://swift.org/LICENSE.txt for license information +# See https://swift.org/CONTRIBUTORS.txt for Swift project authors set(CMAKE_CXX_SCAN_FOR_MODULES 0) diff --git a/Tests/TestingTests/ObjCInteropTests.swift b/Tests/TestingTests/ObjCInteropTests.swift index be12e520d..9c6e42a49 100644 --- a/Tests/TestingTests/ObjCInteropTests.swift +++ b/Tests/TestingTests/ObjCInteropTests.swift @@ -1,12 +1,12 @@ -/* - 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 - */ +// +// 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 canImport(XCTest) import XCTest diff --git a/cmake/modules/GitCommit.cmake b/cmake/modules/GitCommit.cmake index 86b0b4733..1e5a286cf 100644 --- a/cmake/modules/GitCommit.cmake +++ b/cmake/modules/GitCommit.cmake @@ -1,10 +1,12 @@ -# This source file is part of the Swift.org open source project -# -# Copyright (c) 2024–2025 Apple Inc. and the Swift project authors -# Licensed under Apache License v2.0 with Runtime Library Exception -# -# See http://swift.org/LICENSE.txt for license information -# See http://swift.org/CONTRIBUTORS.txt for Swift project authors +## +## This source file is part of the Swift.org open source project +## +## Copyright (c) 2024–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 +## find_package(Git QUIET) if(Git_FOUND) diff --git a/cmake/modules/LibraryVersion.cmake b/cmake/modules/LibraryVersion.cmake index 32901365a..72e42e4cb 100644 --- a/cmake/modules/LibraryVersion.cmake +++ b/cmake/modules/LibraryVersion.cmake @@ -1,10 +1,12 @@ -# 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 http://swift.org/LICENSE.txt for license information -# See http://swift.org/CONTRIBUTORS.txt for Swift project authors +## +## 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 +## # The current version of the Swift Testing release. For release branches, # remember to remove -dev. diff --git a/cmake/modules/PlatformInfo.cmake b/cmake/modules/PlatformInfo.cmake index 94c60ef28..13eb736c5 100644 --- a/cmake/modules/PlatformInfo.cmake +++ b/cmake/modules/PlatformInfo.cmake @@ -1,10 +1,12 @@ -# 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 http://swift.org/LICENSE.txt for license information -# See http://swift.org/CONTRIBUTORS.txt for Swift project authors +## +## 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 +## set(print_target_info_invocation "${CMAKE_Swift_COMPILER}" -print-target-info) if(CMAKE_Swift_COMPILER_TARGET) diff --git a/cmake/modules/SwiftModuleInstallation.cmake b/cmake/modules/SwiftModuleInstallation.cmake index f9bade57d..6947bb1cd 100644 --- a/cmake/modules/SwiftModuleInstallation.cmake +++ b/cmake/modules/SwiftModuleInstallation.cmake @@ -1,10 +1,12 @@ -# 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 http://swift.org/LICENSE.txt for license information -# See http://swift.org/CONTRIBUTORS.txt for Swift project authors +## +## 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 +## function(_swift_testing_install_target module) install(TARGETS ${module} diff --git a/cmake/modules/TargetTriple.cmake b/cmake/modules/TargetTriple.cmake index e087cc47c..39d17bc2a 100644 --- a/cmake/modules/TargetTriple.cmake +++ b/cmake/modules/TargetTriple.cmake @@ -1,10 +1,12 @@ -# 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 http://swift.org/LICENSE.txt for license information -# See http://swift.org/CONTRIBUTORS.txt for Swift project authors +## +## 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 +## # Ask the Swift compiler what target triple it will be compiling with today. set(SWT_TARGET_INFO_COMMAND "${CMAKE_Swift_COMPILER}" -print-target-info) diff --git a/cmake/modules/shared/AvailabilityDefinitions.cmake b/cmake/modules/shared/AvailabilityDefinitions.cmake index 2124a32be..472b2b929 100644 --- a/cmake/modules/shared/AvailabilityDefinitions.cmake +++ b/cmake/modules/shared/AvailabilityDefinitions.cmake @@ -1,10 +1,12 @@ -# 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 http://swift.org/LICENSE.txt for license information -# See http://swift.org/CONTRIBUTORS.txt for Swift project authors +## +## 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 +## # Settings which define commonly-used OS availability macros. add_compile_options( diff --git a/cmake/modules/shared/CompilerSettings.cmake b/cmake/modules/shared/CompilerSettings.cmake index a667f5ba1..ae9ee8fce 100644 --- a/cmake/modules/shared/CompilerSettings.cmake +++ b/cmake/modules/shared/CompilerSettings.cmake @@ -1,10 +1,12 @@ -# 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 http://swift.org/LICENSE.txt for license information -# See http://swift.org/CONTRIBUTORS.txt for Swift project authors +## +## 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 +## # Settings intended to be applied to every Swift target in this project. # Analogous to project-level build settings in an Xcode project. From 1be6b35e5275a89bdb3e29b09812de961297db7d Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Sun, 14 Sep 2025 11:42:16 -0400 Subject: [PATCH 136/216] Move the package version to VERSION.txt. (#1304) This change moves the definition of Swift Testing's version to a new file, VERSION.txt. This file doesn't represent a standard, _per se_, but it is common for open-source projects to list their version info in such a file. And by putting our version info in this file, we can share it between package builds and CMake builds. Currently using C23's `#embed` which is supported in clang as a non-standard extension. C++26 will add support for `#embed`. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- Package.swift | 7 ------ Sources/_TestingInternals/CMakeLists.txt | 1 - Sources/_TestingInternals/Versions.cpp | 28 +++++++++++++++++++++++- VERSION.txt | 1 + cmake/modules/LibraryVersion.cmake | 11 +++------- 5 files changed, 31 insertions(+), 17 deletions(-) create mode 100644 VERSION.txt diff --git a/Package.swift b/Package.swift index 840541887..e387f1453 100644 --- a/Package.swift +++ b/Package.swift @@ -470,12 +470,6 @@ extension Array where Element == PackageDescription.CXXSetting { // Capture the testing library's commit info as C++ constants. if let git { - if let tag = git.currentTag { - result.append(.define("SWT_TESTING_LIBRARY_VERSION", to: #""\#(tag)""#)) - } else { - result.append(.define("SWT_TESTING_LIBRARY_VERSION", to: "0")) - } - result.append(.define("SWT_TESTING_LIBRARY_COMMIT_HASH", to: #""\#(git.currentCommit)""#)) if git.hasUncommittedChanges { result.append(.define("SWT_TESTING_LIBRARY_COMMIT_MODIFIED", to: "1")) @@ -483,7 +477,6 @@ extension Array where Element == PackageDescription.CXXSetting { } else if let gitHubSHA = Context.environment["GITHUB_SHA"] { // When building in GitHub Actions, the git command may fail to get us the // commit hash, so check if GitHub shared it with us instead. - result.append(.define("SWT_TESTING_LIBRARY_VERSION", to: "0")) result.append(.define("SWT_TESTING_LIBRARY_COMMIT_HASH", to: #""\#(gitHubSHA)""#)) } diff --git a/Sources/_TestingInternals/CMakeLists.txt b/Sources/_TestingInternals/CMakeLists.txt index 90ad424e9..a951c7d4b 100644 --- a/Sources/_TestingInternals/CMakeLists.txt +++ b/Sources/_TestingInternals/CMakeLists.txt @@ -9,7 +9,6 @@ set(CMAKE_CXX_SCAN_FOR_MODULES 0) include(GitCommit) -include(LibraryVersion) include(TargetTriple) add_library(_TestingInternals STATIC Discovery.cpp diff --git a/Sources/_TestingInternals/Versions.cpp b/Sources/_TestingInternals/Versions.cpp index 8e85f3d06..bd8f2314a 100644 --- a/Sources/_TestingInternals/Versions.cpp +++ b/Sources/_TestingInternals/Versions.cpp @@ -10,11 +10,37 @@ #include "Versions.h" +#include +#include +#include + const char *swt_getTestingLibraryVersion(void) { #if defined(SWT_TESTING_LIBRARY_VERSION) + // The current environment explicitly specifies a version string to return. return SWT_TESTING_LIBRARY_VERSION; +#elif __has_embed("../../VERSION.txt") + static constinit auto version = [] () constexpr { + // Read the version from version.txt at the root of the package's repo. + char version[] = { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wc23-extensions" +#embed "../../VERSION.txt" +#pragma clang diagnostic pop + }; + + // Copy the first line from the C string into a C array so that we can + // return it from this closure. + std::array result {}; + auto i = std::find_if(std::begin(version), std::end(version), [] (char c) { + return c == '\r' || c == '\n'; + }); + std::copy(std::begin(version), i, result.begin()); + return result; + }(); + + return version.data(); #else -#warning SWT_TESTING_LIBRARY_VERSION not defined: testing library version is unavailable +#warning SWT_TESTING_LIBRARY_VERSION not defined and VERSION.txt not found: testing library version is unavailable return nullptr; #endif } diff --git a/VERSION.txt b/VERSION.txt new file mode 100644 index 000000000..a2b88412f --- /dev/null +++ b/VERSION.txt @@ -0,0 +1 @@ +6.3-dev diff --git a/cmake/modules/LibraryVersion.cmake b/cmake/modules/LibraryVersion.cmake index 72e42e4cb..c5c1d6405 100644 --- a/cmake/modules/LibraryVersion.cmake +++ b/cmake/modules/LibraryVersion.cmake @@ -1,17 +1,12 @@ ## ## This source file is part of the Swift.org open source project ## -## Copyright (c) 2024 Apple Inc. and the Swift project authors +## Copyright (c) 2024–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 ## -# The current version of the Swift Testing release. For release branches, -# remember to remove -dev. -set(SWT_TESTING_LIBRARY_VERSION "6.3-dev") - -message(STATUS "Swift Testing version: ${SWT_TESTING_LIBRARY_VERSION}") -add_compile_definitions( - "$<$:SWT_TESTING_LIBRARY_VERSION=\"${SWT_TESTING_LIBRARY_VERSION}\">") +# The library version is now tracked in VERSION.txt at the root directory of the +# repository. This file will be removed in a future commit. From d875f178ad45f77458fe0674a22dc49056410a90 Mon Sep 17 00:00:00 2001 From: Stuart Montgomery Date: Mon, 15 Sep 2025 15:47:01 -0500 Subject: [PATCH 137/216] Enable the unacceptable language soundness check and resolve identified issues (#1313) This enables the (currently-disabled) unacceptable language soundness check via GitHub Actions and resolves all the issues that running the script identified. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- .github/workflows/pull_request.yml | 1 - Dockerfile | 2 +- Sources/Testing/ExitTests/ExitTest.swift | 2 +- Sources/Testing/ExitTests/SpawnProcess.swift | 2 +- Sources/Testing/Testing.docc/LimitingExecutionTime.md | 2 +- Tests/TestingTests/IssueTests.swift | 2 +- Tests/TestingTests/Support/CartesianProductTests.swift | 2 +- 7 files changed, 6 insertions(+), 7 deletions(-) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index c0049b554..bb435c6b4 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -24,6 +24,5 @@ jobs: with: license_header_check_project_name: "Swift" docs_check_enabled: false - unacceptable_language_check_enabled: false format_check_enabled: false api_breakage_check_enabled: false diff --git a/Dockerfile b/Dockerfile index 1647b0808..5776af6e7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,7 +11,7 @@ FROM swiftlang/swift:nightly-main-jammy # Set up the current build user in the same way done in the Swift.org CI system: -# https://github.com/swiftlang/swift-docker/blob/main/swift-ci/master/ubuntu/22.04/Dockerfile +# https://github.com/swiftlang/swift-docker/blob/main/swift-ci/main/ubuntu/22.04/Dockerfile RUN groupadd -g 998 build-user && \ useradd -m -r -u 998 -g build-user build-user diff --git a/Sources/Testing/ExitTests/ExitTest.swift b/Sources/Testing/ExitTests/ExitTest.swift index c1de79091..32e930e0d 100644 --- a/Sources/Testing/ExitTests/ExitTest.swift +++ b/Sources/Testing/ExitTests/ExitTest.swift @@ -263,7 +263,7 @@ extension ExitTest { // code 3 [...]". // // The Wine project's implementation of raise() calls `_exit(3)` by default. - // See https://github.com/wine-mirror/wine/blob/master/dlls/msvcrt/except.c + // See https://github.com/wine-mirror/wine/blob/master/dlls/msvcrt/except.c (ignore-unacceptable-language) // // Finally, an official copy of the UCRT sources (not up to date) is hosted // at https://www.nuget.org/packages/Microsoft.Windows.SDK.CRTSource . That diff --git a/Sources/Testing/ExitTests/SpawnProcess.swift b/Sources/Testing/ExitTests/SpawnProcess.swift index 24303f632..d1f600e2d 100644 --- a/Sources/Testing/ExitTests/SpawnProcess.swift +++ b/Sources/Testing/ExitTests/SpawnProcess.swift @@ -224,7 +224,7 @@ func spawnExecutable( } #if SWT_TARGET_OS_APPLE && DEBUG // Resume the process. - _ = kill(pid, SIGCONT) + _ = kill(pid, SIGCONT) // ignore-unacceptable-language #endif return pid } diff --git a/Sources/Testing/Testing.docc/LimitingExecutionTime.md b/Sources/Testing/Testing.docc/LimitingExecutionTime.md index 9ea346230..f86091fe4 100644 --- a/Sources/Testing/Testing.docc/LimitingExecutionTime.md +++ b/Sources/Testing/Testing.docc/LimitingExecutionTime.md @@ -18,7 +18,7 @@ Some tests may naturally run slowly: they may require significant system resources to complete, may rely on downloaded data from a server, or may otherwise be dependent on external factors. -If a test may hang indefinitely or may consume too many system resources to +If a test might stall indefinitely or might consume too many system resources to complete effectively, consider setting a time limit for it so that it's marked as failing if it runs for an excessive amount of time. Use the ``Trait/timeLimit(_:)-4kzjp`` trait as an upper bound: diff --git a/Tests/TestingTests/IssueTests.swift b/Tests/TestingTests/IssueTests.swift index 6ea1a5827..6abd384fe 100644 --- a/Tests/TestingTests/IssueTests.swift +++ b/Tests/TestingTests/IssueTests.swift @@ -484,7 +484,7 @@ final class IssueTests: XCTestCase { } func testCastAsAnyProtocol() async { - // Sanity check that we parse types cleanly. + // Check that we parse types cleanly. await Test { #expect((1 as Any) is any Numeric) _ = try #require((1 as Any) as? any Numeric) diff --git a/Tests/TestingTests/Support/CartesianProductTests.swift b/Tests/TestingTests/Support/CartesianProductTests.swift index 3cb4f6daf..304fa329e 100644 --- a/Tests/TestingTests/Support/CartesianProductTests.swift +++ b/Tests/TestingTests/Support/CartesianProductTests.swift @@ -37,7 +37,7 @@ struct CartesianProductTests { @Test("First element is correct") func firstElement() throws { - // Sanity-check the first element is correct. (This value is also tested in + // Check that the first element is correct. (This value is also tested in // testCompleteEquality().) let (c1, c2, product) = computeCartesianProduct() let first = try #require(product.first(where: { _ in true })) From 368a94db77e2be0328ea9e6b947137a6ec11ea20 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Mon, 15 Sep 2025 17:09:13 -0400 Subject: [PATCH 138/216] Avoid eagerly serializing move-only attachments. (#1317) This PR adjusts the interface of `Attachment` so that it always conforms to `Copyable` even if the value it wraps does not. This allows us to avoid eagerly serializing attachments that are not `Copyable` but _are_ `Sendable`. We must still eagerly serialize non-sendable attachments. This change will allow us to support "attachment lifetime" (API to be designed, see XCTest's API [here](https://developer.apple.com/documentation/xctest/xctattachment/lifetime-swift.enum)) more efficiently and for more attachable values. If you're paying the cost of serialization for attachments you don't end up keeping, that's a waste of\ resources. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- Sources/Testing/Attachments/Attachment.swift | 186 +++++++++---------- Tests/TestingTests/AttachmentTests.swift | 2 +- 2 files changed, 84 insertions(+), 104 deletions(-) diff --git a/Sources/Testing/Attachments/Attachment.swift b/Sources/Testing/Attachments/Attachment.swift index f8d242c8c..b665b99fe 100644 --- a/Sources/Testing/Attachments/Attachment.swift +++ b/Sources/Testing/Attachments/Attachment.swift @@ -23,9 +23,23 @@ private import _TestingInternals /// @Available(Swift, introduced: 6.2) /// @Available(Xcode, introduced: 26.0) /// } -public struct Attachment: ~Copyable where AttachableValue: Attachable & ~Copyable { +public struct Attachment where AttachableValue: Attachable & ~Copyable { + /// A class that stores an attachment's (potentially move-only) attachable + /// value. + /// + /// We use a class to store the attachable value so that ``Attachment`` can + /// conform to `Copyable` even if `AttachableValue` doesn't. + fileprivate final class Storage { + /// Storage for ``Attachment/attachableValue-7dyjv``. + let attachableValue: AttachableValue + + init(_ attachableValue: consuming AttachableValue) { + self.attachableValue = attachableValue + } + } + /// Storage for ``attachableValue-7dyjv``. - fileprivate var _attachableValue: AttachableValue + private var _storage: Storage /// The path to which the this attachment was written, if any. /// @@ -80,12 +94,11 @@ public struct Attachment: ~Copyable where AttachableValue: Atta var sourceLocation: SourceLocation } -extension Attachment: Copyable where AttachableValue: Copyable {} extension Attachment: Sendable where AttachableValue: Sendable {} +extension Attachment.Storage: Sendable where AttachableValue: Sendable {} // MARK: - Initializing an attachment -#if !SWT_NO_LAZY_ATTACHMENTS extension Attachment where AttachableValue: ~Copyable { /// Initialize an instance of this type that encloses the given attachable /// value. @@ -105,29 +118,12 @@ extension Attachment where AttachableValue: ~Copyable { /// @Available(Xcode, introduced: 26.0) /// } public init(_ attachableValue: consuming AttachableValue, named preferredName: String? = nil, sourceLocation: SourceLocation = #_sourceLocation) { - self._attachableValue = attachableValue + self._storage = Storage(attachableValue) self._preferredName = preferredName self.sourceLocation = sourceLocation } } -@_spi(ForToolsIntegrationOnly) -extension Attachment where AttachableValue == AnyAttachable { - /// Create a type-erased attachment from an instance of ``Attachment``. - /// - /// - Parameters: - /// - attachment: The attachment to type-erase. - fileprivate init(_ attachment: Attachment) { - self.init( - _attachableValue: AnyAttachable(wrappedValue: attachment.attachableValue), - fileSystemPath: attachment.fileSystemPath, - _preferredName: attachment._preferredName, - sourceLocation: attachment.sourceLocation - ) - } -} -#endif - /// A type-erased wrapper type that represents any attachable value. /// /// This type is not generally visible to developers. It is used when posting @@ -140,47 +136,45 @@ extension Attachment where AttachableValue == AnyAttachable { /// `Event.Kind.valueAttached(_:)`, otherwise it would be declared private. /// } @_spi(ForToolsIntegrationOnly) -public struct AnyAttachable: AttachableWrapper, Copyable, Sendable { -#if !SWT_NO_LAZY_ATTACHMENTS - public typealias Wrapped = any Attachable & Sendable /* & Copyable rdar://137614425 */ -#else - public typealias Wrapped = [UInt8] -#endif +public struct AnyAttachable: AttachableWrapper, Sendable, Copyable { + public struct Wrapped: Sendable {} - public var wrappedValue: Wrapped + public var wrappedValue: Wrapped { + Wrapped() + } - init(wrappedValue: Wrapped) { - self.wrappedValue = wrappedValue + init(_ attachment: Attachment) where A: Attachable & Sendable & ~Copyable { + _estimatedAttachmentByteCount = { attachment.attachableValue.estimatedAttachmentByteCount } + _withUnsafeBytes = { try attachment.withUnsafeBytes($0) } + _preferredName = { attachment.attachableValue.preferredName(for: attachment, basedOn: $0) } } + /// The implementation of ``estimatedAttachmentByteCount`` borrowed from the + /// original attachment. + private var _estimatedAttachmentByteCount: @Sendable () -> Int? + public var estimatedAttachmentByteCount: Int? { - wrappedValue.estimatedAttachmentByteCount + _estimatedAttachmentByteCount() } + /// The implementation of ``withUnsafeBytes(for:_:)`` borrowed from the + /// original attachment. + private var _withUnsafeBytes: @Sendable ((UnsafeRawBufferPointer) throws -> Void) throws -> Void + public func withUnsafeBytes(for attachment: borrowing Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { - func open(_ wrappedValue: T, for attachment: borrowing Attachment) throws -> R where T: Attachable & Sendable & Copyable { - let temporaryAttachment = Attachment( - _attachableValue: wrappedValue, - fileSystemPath: attachment.fileSystemPath, - _preferredName: attachment._preferredName, - sourceLocation: attachment.sourceLocation - ) - return try temporaryAttachment.withUnsafeBytes(body) + var result: R! + try _withUnsafeBytes { bytes in + result = try body(bytes) } - return try open(wrappedValue, for: attachment) + return result } + /// The implementation of ``preferredName(for:basedOn:)`` borrowed from the + /// original attachment. + private var _preferredName: @Sendable (String) -> String + public borrowing func preferredName(for attachment: borrowing Attachment, basedOn suggestedName: String) -> String { - func open(_ wrappedValue: T, for attachment: borrowing Attachment) -> String where T: Attachable & Sendable & Copyable { - let temporaryAttachment = Attachment( - _attachableValue: wrappedValue, - fileSystemPath: attachment.fileSystemPath, - _preferredName: attachment._preferredName, - sourceLocation: attachment.sourceLocation - ) - return temporaryAttachment.preferredName - } - return open(wrappedValue, for: attachment) + _preferredName(suggestedName) } } @@ -215,7 +209,7 @@ extension Attachment where AttachableValue: ~Copyable { /// } @_disfavoredOverload public var attachableValue: AttachableValue { _read { - yield _attachableValue + yield _storage.attachableValue } } } @@ -245,23 +239,20 @@ extension Attachment where AttachableValue: AttachableWrapper & ~Copyable { // MARK: - Attaching an attachment to a test (etc.) -#if !SWT_NO_LAZY_ATTACHMENTS -extension Attachment where AttachableValue: Sendable & Copyable { +extension Attachment where AttachableValue: Sendable & ~Copyable { /// Attach an attachment to the current test. /// /// - Parameters: /// - attachment: The attachment to attach. /// - sourceLocation: The source location of the call to this function. /// - /// When attaching a value of a type that does not conform to both - /// [`Sendable`](https://developer.apple.com/documentation/swift/sendable) and - /// [`Copyable`](https://developer.apple.com/documentation/swift/copyable), - /// the testing library encodes it as data immediately. If the value cannot be - /// encoded and an error is thrown, that error is recorded as an issue in the - /// current test and the attachment is not written to the test report or to - /// disk. - /// - /// An attachment can only be attached once. + /// When `attachableValue` is an instance of a type that does not conform to + /// the [`Sendable`](https://developer.apple.com/documentation/swift/sendable) + /// protocol, the testing library encodes it as data immediately. If + /// `attachableValue` throws an error when the testing library attempts to + /// encode it, the testing library records that error as an issue in the + /// current test and does not write the attachment to the test report or to + /// persistent storage. /// /// @Metadata { /// @Available(Swift, introduced: 6.2) @@ -269,8 +260,12 @@ extension Attachment where AttachableValue: Sendable & Copyable { /// } @_documentation(visibility: private) public static func record(_ attachment: consuming Self, sourceLocation: SourceLocation = #_sourceLocation) { - var attachmentCopy = Attachment(attachment) - attachmentCopy.sourceLocation = sourceLocation + var attachmentCopy = Attachment( + AnyAttachable(copy attachment), + named: attachment._preferredName, + sourceLocation: sourceLocation + ) + attachmentCopy.fileSystemPath = attachment.fileSystemPath Event.post(.valueAttached(attachmentCopy)) } @@ -283,19 +278,17 @@ extension Attachment where AttachableValue: Sendable & Copyable { /// derive a reasonable filename for the attached value. /// - sourceLocation: The source location of the call to this function. /// - /// When attaching a value of a type that does not conform to both - /// [`Sendable`](https://developer.apple.com/documentation/swift/sendable) and - /// [`Copyable`](https://developer.apple.com/documentation/swift/copyable), - /// the testing library encodes it as data immediately. If the value cannot be - /// encoded and an error is thrown, that error is recorded as an issue in the - /// current test and the attachment is not written to the test report or to - /// disk. + /// When `attachableValue` is an instance of a type that does not conform to + /// the [`Sendable`](https://developer.apple.com/documentation/swift/sendable) + /// protocol, the testing library encodes it as data immediately. If + /// `attachableValue` throws an error when the testing library attempts to + /// encode it, the testing library records that error as an issue in the + /// current test and does not write the attachment to the test report or to + /// persistent storage. /// /// This function creates a new instance of ``Attachment`` and immediately /// attaches it to the current test. /// - /// An attachment can only be attached once. - /// /// @Metadata { /// @Available(Swift, introduced: 6.2) /// @Available(Xcode, introduced: 26.0) @@ -305,7 +298,6 @@ extension Attachment where AttachableValue: Sendable & Copyable { record(Self(attachableValue, named: preferredName, sourceLocation: sourceLocation), sourceLocation: sourceLocation) } } -#endif extension Attachment where AttachableValue: ~Copyable { /// Attach an attachment to the current test. @@ -314,15 +306,13 @@ extension Attachment where AttachableValue: ~Copyable { /// - attachment: The attachment to attach. /// - sourceLocation: The source location of the call to this function. /// - /// When attaching a value of a type that does not conform to both - /// [`Sendable`](https://developer.apple.com/documentation/swift/sendable) and - /// [`Copyable`](https://developer.apple.com/documentation/swift/copyable), - /// the testing library encodes it as data immediately. If the value cannot be - /// encoded and an error is thrown, that error is recorded as an issue in the - /// current test and the attachment is not written to the test report or to - /// disk. - /// - /// An attachment can only be attached once. + /// When `attachableValue` is an instance of a type that does not conform to + /// the [`Sendable`](https://developer.apple.com/documentation/swift/sendable) + /// protocol, the testing library encodes it as data immediately. If + /// `attachableValue` throws an error when the testing library attempts to + /// encode it, the testing library records that error as an issue in the + /// current test and does not write the attachment to the test report or to + /// persistent storage. /// /// @Metadata { /// @Available(Swift, introduced: 6.2) @@ -330,16 +320,8 @@ extension Attachment where AttachableValue: ~Copyable { /// } public static func record(_ attachment: consuming Self, sourceLocation: SourceLocation = #_sourceLocation) { do { - let attachmentCopy = try attachment.withUnsafeBytes { buffer in - let attachableWrapper = AnyAttachable(wrappedValue: Array(buffer)) - return Attachment( - _attachableValue: attachableWrapper, - fileSystemPath: attachment.fileSystemPath, - _preferredName: attachment.preferredName, // invokes preferredName(for:basedOn:) - sourceLocation: sourceLocation - ) - } - Event.post(.valueAttached(attachmentCopy)) + let bufferCopy = try attachment.withUnsafeBytes { Array($0) } + Attachment.record(bufferCopy, sourceLocation: sourceLocation) } catch { let sourceContext = SourceContext(backtrace: .current(), sourceLocation: sourceLocation) Issue(kind: .valueAttachmentFailed(error), comments: [], sourceContext: sourceContext).record() @@ -355,19 +337,17 @@ extension Attachment where AttachableValue: ~Copyable { /// derive a reasonable filename for the attached value. /// - sourceLocation: The source location of the call to this function. /// - /// When attaching a value of a type that does not conform to both - /// [`Sendable`](https://developer.apple.com/documentation/swift/sendable) and - /// [`Copyable`](https://developer.apple.com/documentation/swift/copyable), - /// the testing library encodes it as data immediately. If the value cannot be - /// encoded and an error is thrown, that error is recorded as an issue in the - /// current test and the attachment is not written to the test report or to - /// disk. + /// When `attachableValue` is an instance of a type that does not conform to + /// the [`Sendable`](https://developer.apple.com/documentation/swift/sendable) + /// protocol, the testing library encodes it as data immediately. If + /// `attachableValue` throws an error when the testing library attempts to + /// encode it, the testing library records that error as an issue in the + /// current test and does not write the attachment to the test report or to + /// persistent storage. /// /// This function creates a new instance of ``Attachment`` and immediately /// attaches it to the current test. /// - /// An attachment can only be attached once. - /// /// @Metadata { /// @Available(Swift, introduced: 6.2) /// @Available(Xcode, introduced: 26.0) diff --git a/Tests/TestingTests/AttachmentTests.swift b/Tests/TestingTests/AttachmentTests.swift index ae95d7c65..b634c5007 100644 --- a/Tests/TestingTests/AttachmentTests.swift +++ b/Tests/TestingTests/AttachmentTests.swift @@ -227,7 +227,7 @@ struct AttachmentTests { return } - #expect(attachment.attachableValue is MySendableAttachable) + #expect((attachment.attachableValue as Any) is AnyAttachable.Wrapped) #expect(attachment.sourceLocation.fileID == #fileID) valueAttached() } From 279629ff91b3155c6a5d78cd72c04aff37b392ff Mon Sep 17 00:00:00 2001 From: 3405691582 Date: Mon, 15 Sep 2025 23:15:59 -0400 Subject: [PATCH 139/216] OpenBSD needs execinfo as well. (#1318) Add OpenBSD to the CMake clause that adds libexecinfo. ### Motivation: `swift test` fails on some projects with `ld: error: undefined reference due to --no-allow-shlib-undefined: backtrace`; `backtrace` is provided by `libexecinfo` on OpenBSD. ### Modifications: Add OpenBSD to the CMake clause that adds libexecinfo. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- Sources/Testing/CMakeLists.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Sources/Testing/CMakeLists.txt b/Sources/Testing/CMakeLists.txt index 8fef166c7..a88dd4084 100644 --- a/Sources/Testing/CMakeLists.txt +++ b/Sources/Testing/CMakeLists.txt @@ -124,7 +124,8 @@ if(NOT APPLE) endif() target_link_libraries(Testing PUBLIC Foundation) - if (CMAKE_SYSTEM_NAME STREQUAL "FreeBSD") + if (CMAKE_SYSTEM_NAME STREQUAL "FreeBSD" OR + CMAKE_SYSTEM_NAME STREQUAL "OpenBSD") target_link_libraries(Testing PUBLIC execinfo) endif() endif() From 2e4f1a59c3422638d2b2e04352633341ace15028 Mon Sep 17 00:00:00 2001 From: Mishal Shah Date: Wed, 17 Sep 2025 08:14:59 -0700 Subject: [PATCH 140/216] [CI] Add Amazon Linux 2 in PR testing (#1322) --- .github/workflows/pull_request.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index bb435c6b4..925965906 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -14,6 +14,7 @@ jobs: uses: swiftlang/github-workflows/.github/workflows/swift_package_test.yml@main with: linux_swift_versions: '["nightly-main", "nightly-6.2"]' + linux_os_versions: '["amazonlinux2", "jammy"]' windows_swift_versions: '["nightly-main", "nightly-6.2"]' enable_macos_checks: true macos_exclude_xcode_versions: "[{\"xcode_version\": \"16.2\"}, {\"xcode_version\": \"16.3\"}]" From ae900b15d210d8cac9fccc49e64e7fa933403c78 Mon Sep 17 00:00:00 2001 From: Stuart Montgomery Date: Wed, 17 Sep 2025 10:21:14 -0500 Subject: [PATCH 141/216] Enable the Docs soundness check (#1314) Enable the (currently-disabled) Docs soundness check via GitHub Actions. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- .github/workflows/pull_request.yml | 2 +- Sources/Testing/Testing.docc/Documentation.md | 6 ++---- Sources/Testing/Testing.docc/MigratingFromXCTest.md | 8 +++++--- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 925965906..288a012a1 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -24,6 +24,6 @@ jobs: uses: swiftlang/github-workflows/.github/workflows/soundness.yml@main with: license_header_check_project_name: "Swift" - docs_check_enabled: false + docs_check_container_image: "swift:6.2-noble" format_check_enabled: false api_breakage_check_enabled: false diff --git a/Sources/Testing/Testing.docc/Documentation.md b/Sources/Testing/Testing.docc/Documentation.md index cc4001889..fb4ecc347 100644 --- a/Sources/Testing/Testing.docc/Documentation.md +++ b/Sources/Testing/Testing.docc/Documentation.md @@ -35,10 +35,8 @@ their problems. #### Related videos -@Links(visualStyle: compactGrid) { - - - - -} +- [Meet Swift Testing](https://developer.apple.com/videos/play/wwdc2024/10179) +- [Go further with Swift Testing](https://developer.apple.com/videos/play/wwdc2024/10195) ## Topics diff --git a/Sources/Testing/Testing.docc/MigratingFromXCTest.md b/Sources/Testing/Testing.docc/MigratingFromXCTest.md index fcf1f529d..6b1d9f381 100644 --- a/Sources/Testing/Testing.docc/MigratingFromXCTest.md +++ b/Sources/Testing/Testing.docc/MigratingFromXCTest.md @@ -556,9 +556,11 @@ test function with an instance of this trait type to control whether it runs: } } - ### Annotate known issues From 357f3cfe6b16e90db181dc014233982447d1711f Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Wed, 17 Sep 2025 12:27:34 -0400 Subject: [PATCH 142/216] Revert "[CI] Add Amazon Linux 2 in PR testing (#1322)" This reverts commit 2e4f1a59c3422638d2b2e04352633341ace15028. --- .github/workflows/pull_request.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 288a012a1..8c8480260 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -14,7 +14,6 @@ jobs: uses: swiftlang/github-workflows/.github/workflows/swift_package_test.yml@main with: linux_swift_versions: '["nightly-main", "nightly-6.2"]' - linux_os_versions: '["amazonlinux2", "jammy"]' windows_swift_versions: '["nightly-main", "nightly-6.2"]' enable_macos_checks: true macos_exclude_xcode_versions: "[{\"xcode_version\": \"16.2\"}, {\"xcode_version\": \"16.3\"}]" From 05b46397beaf2dfad30fb853faa725fc4089cdc3 Mon Sep 17 00:00:00 2001 From: 3405691582 Date: Wed, 17 Sep 2025 13:39:19 -0400 Subject: [PATCH 143/216] Work around lack of embed in clang on OpenBSD and Amazon Linux 2. (#1320) Work around lack of embed in OpenBSD's clang. ### Motivation: For various reasons, the Swift toolchain for OpenBSD relies on using the platform's native clang, which is 16. clang 19 is the most recent version that will not emit an error with the new __has_embed features in C23. Since swift-testing is experimentally supported by OpenBSD and thus to make swift-testing build again on the platform, work around the issue with a platform-specific command-line specified macro override in swiftpm and in cmake. Furthermore, we can use cmake trickery to subsitute the version file contents instead of using embed. This may not be possible to do with swiftpm, but I don't know for sure. Resolves rdar://160591315. ### Checklist: - [X] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [X] If public symbols are renamed or modified, DocC references should be updated. --------- Co-authored-by: Jonathan Grynspan Co-authored-by: Jonathan Grynspan --- Sources/_TestingInternals/Versions.cpp | 34 +++++++++++++-------- cmake/modules/shared/CompilerSettings.cmake | 6 ++++ 2 files changed, 27 insertions(+), 13 deletions(-) diff --git a/Sources/_TestingInternals/Versions.cpp b/Sources/_TestingInternals/Versions.cpp index bd8f2314a..8ca708b26 100644 --- a/Sources/_TestingInternals/Versions.cpp +++ b/Sources/_TestingInternals/Versions.cpp @@ -13,36 +13,44 @@ #include #include #include +#include const char *swt_getTestingLibraryVersion(void) { #if defined(SWT_TESTING_LIBRARY_VERSION) // The current environment explicitly specifies a version string to return. + // All CMake builds should take this path (see CompilerSettings.cmake.) return SWT_TESTING_LIBRARY_VERSION; -#elif __has_embed("../../VERSION.txt") - static constinit auto version = [] () constexpr { - // Read the version from version.txt at the root of the package's repo. - char version[] = { +#elif __clang_major__ >= 17 && defined(__has_embed) +#if __has_embed("../../VERSION.txt") + // Read the version from version.txt at the root of the package's repo. + static char version[] = { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wc23-extensions" -#embed "../../VERSION.txt" +#embed "../../VERSION.txt" suffix(, '\0') #pragma clang diagnostic pop - }; + }; - // Copy the first line from the C string into a C array so that we can - // return it from this closure. - std::array result {}; + // Zero out the newline character and anything after it. + static std::once_flag once; + std::call_once(once, [] { auto i = std::find_if(std::begin(version), std::end(version), [] (char c) { return c == '\r' || c == '\n'; }); - std::copy(std::begin(version), i, result.begin()); - return result; - }(); + std::fill(i, std::end(version), '\0'); + }); - return version.data(); + return version; #else #warning SWT_TESTING_LIBRARY_VERSION not defined and VERSION.txt not found: testing library version is unavailable return nullptr; #endif +#elif defined(__OpenBSD__) + // OpenBSD's version of clang doesn't support __has_embed or #embed. + return nullptr; +#else +#warning SWT_TESTING_LIBRARY_VERSION not defined and could not read from VERSION.txt at compile time: testing library version is unavailable + return nullptr; +#endif } void swt_getTestingLibraryCommit(const char *_Nullable *_Nonnull outHash, bool *outModified) { diff --git a/cmake/modules/shared/CompilerSettings.cmake b/cmake/modules/shared/CompilerSettings.cmake index ae9ee8fce..df3bb826a 100644 --- a/cmake/modules/shared/CompilerSettings.cmake +++ b/cmake/modules/shared/CompilerSettings.cmake @@ -42,3 +42,9 @@ if(CMAKE_SYSTEM_NAME STREQUAL "WASI") add_compile_definitions("SWT_NO_DYNAMIC_LINKING") add_compile_definitions("SWT_NO_PIPES") endif() + +file(STRINGS "../VERSION.txt" SWT_TESTING_LIBRARY_VERSION LIMIT_COUNT 1) +if(SWT_TESTING_LIBRARY_VERSION) + message(STATUS "Swift Testing version: ${SWT_TESTING_LIBRARY_VERSION}") + add_compile_definitions("$<$:SWT_TESTING_LIBRARY_VERSION=\"${SWT_TESTING_LIBRARY_VERSION}\">") +endif() From 9a1fc35fd8c89a414091c7fe5423d03c0cb7abe5 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Wed, 17 Sep 2025 16:04:25 -0400 Subject: [PATCH 144/216] Fix the path to VERSION.txt used in CMake. (#1327) Fixes a bug introduced in #1320. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- CMakeLists.txt | 2 ++ Sources/TestingMacros/CMakeLists.txt | 2 ++ cmake/modules/shared/CompilerSettings.cmake | 2 +- 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 2f2828d49..3714e8c87 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -19,6 +19,8 @@ if(POLICY CMP0157) endif() endif() +set(SWT_SOURCE_ROOT_DIR ${CMAKE_SOURCE_DIR}) + project(SwiftTesting LANGUAGES CXX Swift) diff --git a/Sources/TestingMacros/CMakeLists.txt b/Sources/TestingMacros/CMakeLists.txt index 9de696920..458bd4170 100644 --- a/Sources/TestingMacros/CMakeLists.txt +++ b/Sources/TestingMacros/CMakeLists.txt @@ -12,6 +12,8 @@ if(POLICY CMP0157) cmake_policy(SET CMP0157 NEW) endif() +set(SWT_SOURCE_ROOT_DIR ${CMAKE_SOURCE_DIR}/../..) + project(TestingMacros LANGUAGES Swift) diff --git a/cmake/modules/shared/CompilerSettings.cmake b/cmake/modules/shared/CompilerSettings.cmake index df3bb826a..e5a4fcbf6 100644 --- a/cmake/modules/shared/CompilerSettings.cmake +++ b/cmake/modules/shared/CompilerSettings.cmake @@ -43,7 +43,7 @@ if(CMAKE_SYSTEM_NAME STREQUAL "WASI") add_compile_definitions("SWT_NO_PIPES") endif() -file(STRINGS "../VERSION.txt" SWT_TESTING_LIBRARY_VERSION LIMIT_COUNT 1) +file(STRINGS "${SWT_SOURCE_ROOT_DIR}/VERSION.txt" SWT_TESTING_LIBRARY_VERSION LIMIT_COUNT 1) if(SWT_TESTING_LIBRARY_VERSION) message(STATUS "Swift Testing version: ${SWT_TESTING_LIBRARY_VERSION}") add_compile_definitions("$<$:SWT_TESTING_LIBRARY_VERSION=\"${SWT_TESTING_LIBRARY_VERSION}\">") From 41121f189cc57d9b200d3e93d9a141a1da4ec138 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Wed, 17 Sep 2025 16:06:09 -0400 Subject: [PATCH 145/216] Remove a workaround for varargs not compiling on WASI. (#1323) This PR removes a workaround on WASI where we were avoiding a call to `withVaList()` because it miscompiled there. That problem was fixed by https://github.com/swiftlang/swift/pull/84029. It was cherry-picked to 6.2, but does not appear to be fixed in 6.2 CI, so the change here only applies to 6.3 onward. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- Sources/Testing/Events/TimeValue.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Testing/Events/TimeValue.swift b/Sources/Testing/Events/TimeValue.swift index 838eb381b..ddc454f7b 100644 --- a/Sources/Testing/Events/TimeValue.swift +++ b/Sources/Testing/Events/TimeValue.swift @@ -81,7 +81,7 @@ extension TimeValue: Codable {} extension TimeValue: CustomStringConvertible { var description: String { -#if os(WASI) +#if os(WASI) && compiler(<6.3) // BUG: https://github.com/swiftlang/swift/issues/72398 return String(describing: Duration(self)) #else From 0d27f722e29f51717c0f14faea024cc05109466f Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Wed, 17 Sep 2025 16:22:24 -0400 Subject: [PATCH 146/216] Add Amazon Linux 2 in PR testing (take 2) (#1326) Restores the changes from #1322 that were reverted in #1324. Resolves #1325. --- .github/workflows/pull_request.yml | 1 + Sources/Testing/ExitTests/ExitTest.swift | 2 +- Sources/Testing/Support/FileHandle.swift | 4 ++-- Tests/TestingTests/ExitTestTests.swift | 17 ++++++++++++----- 4 files changed, 16 insertions(+), 8 deletions(-) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 8c8480260..288a012a1 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -14,6 +14,7 @@ jobs: uses: swiftlang/github-workflows/.github/workflows/swift_package_test.yml@main with: linux_swift_versions: '["nightly-main", "nightly-6.2"]' + linux_os_versions: '["amazonlinux2", "jammy"]' windows_swift_versions: '["nightly-main", "nightly-6.2"]' enable_macos_checks: true macos_exclude_xcode_versions: "[{\"xcode_version\": \"16.2\"}, {\"xcode_version\": \"16.3\"}]" diff --git a/Sources/Testing/ExitTests/ExitTest.swift b/Sources/Testing/ExitTests/ExitTest.swift index 32e930e0d..b9ce39496 100644 --- a/Sources/Testing/ExitTests/ExitTest.swift +++ b/Sources/Testing/ExitTests/ExitTest.swift @@ -525,11 +525,11 @@ func callExitTest( } // Plumb the exit test's result through the general expectation machinery. + let expression = __Expression(String(describingForTest: expectedExitCondition)) return __checkValue( expectedExitCondition.isApproximatelyEqual(to: result.exitStatus), expression: expression, expressionWithCapturedRuntimeValues: expression.capturingRuntimeValues(result.exitStatus), - mismatchedExitConditionDescription: String(describingForTest: expectedExitCondition), comments: comments(), isRequired: isRequired, sourceLocation: sourceLocation diff --git a/Sources/Testing/Support/FileHandle.swift b/Sources/Testing/Support/FileHandle.swift index 37774b91a..d038db101 100644 --- a/Sources/Testing/Support/FileHandle.swift +++ b/Sources/Testing/Support/FileHandle.swift @@ -704,9 +704,9 @@ func setFD_CLOEXEC(_ flag: Bool, onFileDescriptor fd: CInt) throws { throw CError(rawValue: swt_errno()) case let oldValue: let newValue = if flag { - oldValue & ~FD_CLOEXEC - } else { oldValue | FD_CLOEXEC + } else { + oldValue & ~FD_CLOEXEC } if oldValue == newValue { // No need to make a second syscall as nothing has changed. diff --git a/Tests/TestingTests/ExitTestTests.swift b/Tests/TestingTests/ExitTestTests.swift index 1801a21b4..615ffcb74 100644 --- a/Tests/TestingTests/ExitTestTests.swift +++ b/Tests/TestingTests/ExitTestTests.swift @@ -14,12 +14,19 @@ private import _TestingInternals #if !SWT_NO_EXIT_TESTS @Suite("Exit test tests") struct ExitTestTests { @Test("Signal names are reported (where supported)") func signalName() { - let exitStatus = ExitStatus.signal(SIGABRT) -#if SWT_TARGET_OS_APPLE || os(Linux) || os(FreeBSD) || os(OpenBSD) || os(Android) - #expect(String(describing: exitStatus) == ".signal(SIGABRT → \(SIGABRT))") -#else - #expect(String(describing: exitStatus) == ".signal(\(SIGABRT))") + var hasSignalNames = false +#if SWT_TARGET_OS_APPLE || os(FreeBSD) || os(OpenBSD) || os(Android) + hasSignalNames = true +#elseif os(Linux) && !SWT_NO_DYNAMIC_LINKING + hasSignalNames = (symbol(named: "sigabbrev_np") != nil) #endif + + let exitStatus = ExitStatus.signal(SIGABRT) + if Bool(hasSignalNames) { + #expect(String(describing: exitStatus) == ".signal(SIGABRT → \(SIGABRT))") + } else { + #expect(String(describing: exitStatus) == ".signal(\(SIGABRT))") + } } @Test("Exit tests (passing)") func passing() async { From 2c66d9e33d7169d2c060136ebd15064a2ac36fe6 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Thu, 18 Sep 2025 11:28:06 -0400 Subject: [PATCH 147/216] Drop remaining Swift 6.1 compiler support. (#1328) This PR drops the remaining bits in the package that build with the Swift 6.1 compiler. Swift 6.2 has been released and our main branch no longer supports Swift 6.1 toolchains (and hasn't for a while now.) ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- Package.swift | 27 ++----------------- Sources/Testing/ExitTests/ExitTest.swift | 10 ++----- .../Expectations/Expectation+Macro.swift | 14 ---------- .../Testing/Parameterization/TypeInfo.swift | 17 ------------ Sources/Testing/Test+Discovery.swift | 5 +--- Sources/TestingMacros/ConditionMacro.swift | 3 +-- .../TestingMacros/SuiteDeclarationMacro.swift | 3 +-- .../Support/EffectfulExpressionHandling.swift | 14 +--------- .../Support/TestContentGeneration.swift | 3 +-- .../TestingMacros/TestDeclarationMacro.swift | 3 +-- Tests/TestingTests/AttachmentTests.swift | 6 +---- Tests/TestingTests/MiscellaneousTests.swift | 2 -- .../MemorySafeTestDecls.swift | 4 --- 13 files changed, 11 insertions(+), 100 deletions(-) diff --git a/Package.swift b/Package.swift index e387f1453..0f4b1648b 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version: 6.1 +// swift-tools-version: 6.2 // // This source file is part of the Swift.org open source project @@ -164,7 +164,7 @@ let package = Package( "Testing", ], path: "Tests/_MemorySafeTestingTests", - swiftSettings: .packageSettings + .strictMemorySafety + swiftSettings: .packageSettings + [.strictMemorySafety()] ), .macro( @@ -369,17 +369,6 @@ extension Array where Element == PackageDescription.SwiftSetting { // proposal via Swift Evolution. .enableExperimentalFeature("SymbolLinkageMarkers"), - // This setting is no longer needed when building with a 6.2 or later - // toolchain now that SE-0458 has been accepted and implemented, but it is - // needed in order to preserve support for building with 6.1 development - // snapshot toolchains. (Production 6.1 toolchains can build the testing - // library even without this setting since this experimental feature is - // _suppressible_.) This setting can be removed once the minimum supported - // toolchain for building the testing library is ≥ 6.2. It is not needed - // in the CMake settings since that is expected to build using a - // new-enough toolchain. - .enableExperimentalFeature("AllowUnsafeAttribute"), - .enableUpcomingFeature("InferIsolatedConformances"), // When building as a package, the macro plugin always builds as an @@ -436,18 +425,6 @@ extension Array where Element == PackageDescription.SwiftSetting { return result } - - /// Settings necessary to enable Strict Memory Safety, introduced in - /// [SE-0458: Opt-in Strict Memory Safety Checking](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0458-strict-memory-safety.md#swiftpm-integration). - static var strictMemorySafety: Self { -#if compiler(>=6.2) - // FIXME: Adopt official `.strictMemorySafety()` condition once the minimum - // supported toolchain is 6.2. - [.unsafeFlags(["-strict-memory-safety"])] -#else - [] -#endif - } } extension Array where Element == PackageDescription.CXXSetting { diff --git a/Sources/Testing/ExitTests/ExitTest.swift b/Sources/Testing/ExitTests/ExitTest.swift index b9ce39496..4cabda8b3 100644 --- a/Sources/Testing/ExitTests/ExitTest.swift +++ b/Sources/Testing/ExitTests/ExitTest.swift @@ -349,10 +349,7 @@ extension ExitTest { /// /// - Warning: This function is used to implement the /// `#expect(processExitsWith:)` macro. Do not use it directly. -#if compiler(>=6.2) - @safe -#endif - public static func __store( + @safe public static func __store( _ id: (UInt64, UInt64, UInt64, UInt64), _ body: @escaping @Sendable (repeat each T) async throws -> Void, into outValue: UnsafeMutableRawPointer, @@ -394,10 +391,7 @@ extension ExitTest { /// /// - Warning: This function is used to implement the /// `#expect(processExitsWith:)` macro. Do not use it directly. -#if compiler(>=6.2) - @safe -#endif - public static func __store( + @safe public static func __store( _ id: (UInt64, UInt64, UInt64, UInt64), _ body: T, into outValue: UnsafeMutableRawPointer, diff --git a/Sources/Testing/Expectations/Expectation+Macro.swift b/Sources/Testing/Expectations/Expectation+Macro.swift index 2f64aff3a..17127d058 100644 --- a/Sources/Testing/Expectations/Expectation+Macro.swift +++ b/Sources/Testing/Expectations/Expectation+Macro.swift @@ -131,7 +131,6 @@ public macro require( // MARK: - Matching errors by type -#if compiler(>=6.2) /// Check that an expression always throws an error of a given type. /// /// - Parameters: @@ -197,7 +196,6 @@ public macro expect( sourceLocation: SourceLocation = #_sourceLocation, performing expression: () throws -> R ) -> E? = #externalMacro(module: "TestingMacros", type: "ExpectMacro") where E: Error -#endif /// Check that an expression always throws an error of a given type. /// @@ -263,7 +261,6 @@ public macro expect( performing expression: () async throws -> R ) -> E? = #externalMacro(module: "TestingMacros", type: "ExpectMacro") where E: Error -#if compiler(>=6.2) /// Check that an expression always throws an error of a given type, and throw /// an error if it does not. /// @@ -313,7 +310,6 @@ public macro require( sourceLocation: SourceLocation = #_sourceLocation, performing expression: () throws -> R ) -> E = #externalMacro(module: "TestingMacros", type: "RequireThrowsMacro") where E: Error -#endif /// Check that an expression always throws an error of a given type, and throw /// an error if it does not. @@ -363,7 +359,6 @@ public macro require( performing expression: () async throws -> R ) -> E = #externalMacro(module: "TestingMacros", type: "RequireThrowsMacro") where E: Error -#if compiler(>=6.2) /// Check that an expression never throws an error, and throw an error if it /// does. /// @@ -383,7 +378,6 @@ public macro require( sourceLocation: SourceLocation = #_sourceLocation, performing expression: () throws -> R ) = #externalMacro(module: "TestingMacros", type: "RequireThrowsNeverMacro") -#endif /// Check that an expression never throws an error, and throw an error if it /// does. @@ -407,7 +401,6 @@ public macro require( // MARK: - Matching instances of equatable errors -#if compiler(>=6.2) /// Check that an expression always throws a specific error. /// /// - Parameters: @@ -449,7 +442,6 @@ public macro expect( sourceLocation: SourceLocation = #_sourceLocation, performing expression: () throws -> R ) -> E? = #externalMacro(module: "TestingMacros", type: "ExpectMacro") where E: Error & Equatable -#endif /// Check that an expression always throws a specific error. /// @@ -491,7 +483,6 @@ public macro expect( performing expression: () async throws -> R ) -> E? = #externalMacro(module: "TestingMacros", type: "ExpectMacro") where E: Error & Equatable -#if compiler(>=6.2) /// Check that an expression always throws a specific error, and throw an error /// if it does not. /// @@ -537,7 +528,6 @@ public macro require( sourceLocation: SourceLocation = #_sourceLocation, performing expression: () throws -> R ) -> E = #externalMacro(module: "TestingMacros", type: "RequireMacro") where E: Error & Equatable -#endif /// Check that an expression always throws a specific error, and throw an error /// if it does not. @@ -585,7 +575,6 @@ public macro require( // MARK: - Arbitrary error matching -#if compiler(>=6.2) /// Check that an expression always throws an error matching some condition. /// /// - Parameters: @@ -649,7 +638,6 @@ public macro expect( performing expression: () throws -> R, throws errorMatcher: (any Error) throws -> Bool ) -> (any Error)? = #externalMacro(module: "TestingMacros", type: "ExpectMacro") -#endif /// Check that an expression always throws an error matching some condition. /// @@ -713,7 +701,6 @@ public macro expect( throws errorMatcher: (any Error) async throws -> Bool ) -> (any Error)? = #externalMacro(module: "TestingMacros", type: "ExpectMacro") -#if compiler(>=6.2) /// Check that an expression always throws an error matching some condition, and /// throw an error if it does not. /// @@ -784,7 +771,6 @@ public macro require( performing expression: () throws -> R, throws errorMatcher: (any Error) throws -> Bool ) -> any Error = #externalMacro(module: "TestingMacros", type: "RequireMacro") -#endif /// Check that an expression always throws an error matching some condition, and /// throw an error if it does not. diff --git a/Sources/Testing/Parameterization/TypeInfo.swift b/Sources/Testing/Parameterization/TypeInfo.swift index de377e6be..0ebda0a82 100644 --- a/Sources/Testing/Parameterization/TypeInfo.swift +++ b/Sources/Testing/Parameterization/TypeInfo.swift @@ -405,23 +405,6 @@ extension TypeInfo: Hashable { } } -#if compiler(<6.2) -// MARK: - ObjectIdentifier support - -extension ObjectIdentifier { - /// Initialize an instance of this type from a type reference. - /// - /// - Parameters: - /// - type: The type to initialize this instance from. - /// - /// - Bug: The standard library should support this conversion. - /// ([134276458](rdar://134276458), [134415960](rdar://134415960)) - fileprivate init(_ type: any ~Copyable.Type) { - self.init(unsafeBitCast(type, to: Any.Type.self)) - } -} -#endif - // MARK: - Codable extension TypeInfo: Codable { diff --git a/Sources/Testing/Test+Discovery.swift b/Sources/Testing/Test+Discovery.swift index 87dafe7a4..d7d3b8ea7 100644 --- a/Sources/Testing/Test+Discovery.swift +++ b/Sources/Testing/Test+Discovery.swift @@ -39,10 +39,7 @@ extension Test { /// /// - Warning: This function is used to implement the `@Test` macro. Do not /// use it directly. -#if compiler(>=6.2) - @safe -#endif - public static func __store( + @safe public static func __store( _ generator: @escaping @Sendable () async -> Test, into outValue: UnsafeMutableRawPointer, asTypeAt typeAddress: UnsafeRawPointer diff --git a/Sources/TestingMacros/ConditionMacro.swift b/Sources/TestingMacros/ConditionMacro.swift index c4b1db7e3..6ba8ff124 100644 --- a/Sources/TestingMacros/ConditionMacro.swift +++ b/Sources/TestingMacros/ConditionMacro.swift @@ -498,11 +498,10 @@ extension ExitTestConditionMacro { var recordDecl: DeclSyntax? #if !SWT_NO_LEGACY_TEST_DISCOVERY let legacyEnumName = context.makeUniqueName("__🟡$") - let unsafeKeyword: TokenSyntax? = isUnsafeKeywordSupported ? .keyword(.unsafe, trailingTrivia: .space) : nil recordDecl = """ enum \(legacyEnumName): Testing.__TestContentRecordContainer { nonisolated static var __testContentRecord: Testing.__TestContentRecord { - \(unsafeKeyword)\(enumName).testContentRecord + unsafe \(enumName).testContentRecord } } """ diff --git a/Sources/TestingMacros/SuiteDeclarationMacro.swift b/Sources/TestingMacros/SuiteDeclarationMacro.swift index e44b0460a..4bee4c30f 100644 --- a/Sources/TestingMacros/SuiteDeclarationMacro.swift +++ b/Sources/TestingMacros/SuiteDeclarationMacro.swift @@ -169,13 +169,12 @@ public struct SuiteDeclarationMacro: MemberMacro, PeerMacro, Sendable { #if !SWT_NO_LEGACY_TEST_DISCOVERY // Emit a type that contains a reference to the test content record. let enumName = context.makeUniqueName("__🟡$") - let unsafeKeyword: TokenSyntax? = isUnsafeKeywordSupported ? .keyword(.unsafe, trailingTrivia: .space) : nil result.append( """ @available(*, deprecated, message: "This type is an implementation detail of the testing library. Do not use it directly.") enum \(enumName): Testing.__TestContentRecordContainer { nonisolated static var __testContentRecord: Testing.__TestContentRecord { - \(unsafeKeyword)\(testContentRecordName) + unsafe \(testContentRecordName) } } """ diff --git a/Sources/TestingMacros/Support/EffectfulExpressionHandling.swift b/Sources/TestingMacros/Support/EffectfulExpressionHandling.swift index b093d1e77..28e0af56d 100644 --- a/Sources/TestingMacros/Support/EffectfulExpressionHandling.swift +++ b/Sources/TestingMacros/Support/EffectfulExpressionHandling.swift @@ -130,17 +130,6 @@ extension BidirectionalCollection { // MARK: - Inserting effect keywords/thunks -/// Whether or not the `unsafe` expression keyword is supported. -var isUnsafeKeywordSupported: Bool { - // The 'unsafe' keyword was introduced in 6.2 as part of SE-0458. Older - // toolchains are not aware of it. -#if compiler(>=6.2) - true -#else - false -#endif -} - /// Make a function call expression to an effectful thunk function provided by /// the testing library. /// @@ -184,8 +173,7 @@ func applyEffectfulKeywords(_ effectfulKeywords: Set, to expr: some Exp let needAwait = effectfulKeywords.contains(.await) && !expr.is(AwaitExprSyntax.self) let needTry = effectfulKeywords.contains(.try) && !expr.is(TryExprSyntax.self) - - let needUnsafe = isUnsafeKeywordSupported && effectfulKeywords.contains(.unsafe) && !expr.is(UnsafeExprSyntax.self) + let needUnsafe = effectfulKeywords.contains(.unsafe) && !expr.is(UnsafeExprSyntax.self) // First, add thunk function calls. if insertThunkCalls { diff --git a/Sources/TestingMacros/Support/TestContentGeneration.swift b/Sources/TestingMacros/Support/TestContentGeneration.swift index 2999478de..05214d1b8 100644 --- a/Sources/TestingMacros/Support/TestContentGeneration.swift +++ b/Sources/TestingMacros/Support/TestContentGeneration.swift @@ -63,13 +63,12 @@ func makeTestContentRecordDecl(named name: TokenSyntax, in typeName: TypeSyntax? IntegerLiteralExprSyntax(context, radix: .binary) } - let unsafeKeyword: TokenSyntax? = isUnsafeKeywordSupported ? .keyword(.unsafe, trailingTrivia: .space) : nil var result: DeclSyntax = """ @available(*, deprecated, message: "This property is an implementation detail of the testing library. Do not use it directly.") private nonisolated \(staticKeyword(for: typeName)) let \(name): Testing.__TestContentRecord = ( \(kindExpr), \(kind.commentRepresentation) 0, - \(unsafeKeyword)\(accessorName), + unsafe \(accessorName), \(contextExpr), 0 ) diff --git a/Sources/TestingMacros/TestDeclarationMacro.swift b/Sources/TestingMacros/TestDeclarationMacro.swift index ef156edd6..b67bf3360 100644 --- a/Sources/TestingMacros/TestDeclarationMacro.swift +++ b/Sources/TestingMacros/TestDeclarationMacro.swift @@ -496,13 +496,12 @@ public struct TestDeclarationMacro: PeerMacro, Sendable { #if !SWT_NO_LEGACY_TEST_DISCOVERY // Emit a type that contains a reference to the test content record. let enumName = context.makeUniqueName(thunking: functionDecl, withPrefix: "__🟡$") - let unsafeKeyword: TokenSyntax? = isUnsafeKeywordSupported ? .keyword(.unsafe, trailingTrivia: .space) : nil result.append( """ @available(*, deprecated, message: "This type is an implementation detail of the testing library. Do not use it directly.") enum \(enumName): Testing.__TestContentRecordContainer { nonisolated static var __testContentRecord: Testing.__TestContentRecord { - \(unsafeKeyword)\(testContentRecordName) + unsafe \(testContentRecordName) } } """ diff --git a/Tests/TestingTests/AttachmentTests.swift b/Tests/TestingTests/AttachmentTests.swift index b634c5007..7695dd634 100644 --- a/Tests/TestingTests/AttachmentTests.swift +++ b/Tests/TestingTests/AttachmentTests.swift @@ -177,16 +177,12 @@ struct AttachmentTests { } valueAttached() - // BUG: We could use #expect(throws: Never.self) here, but the Swift 6.1 - // compiler crashes trying to expand the macro (rdar://138997009) - do { + #expect(throws: Never.self) { let filePath = try #require(attachment.fileSystemPath) defer { remove(filePath) } try compare(attachableValue, toContentsOfFileAtPath: filePath) - } catch { - Issue.record(error) } } diff --git a/Tests/TestingTests/MiscellaneousTests.swift b/Tests/TestingTests/MiscellaneousTests.swift index 6a65fb658..84d1ce493 100644 --- a/Tests/TestingTests/MiscellaneousTests.swift +++ b/Tests/TestingTests/MiscellaneousTests.swift @@ -297,7 +297,6 @@ struct MiscellaneousTests { #expect(testType.displayName == "Named Sendable test type") } -#if compiler(>=6.2) && hasFeature(RawIdentifiers) @Test func `Test with raw identifier gets a display name`() throws { let test = try #require(Test.current) #expect(test.displayName == "Test with raw identifier gets a display name") @@ -320,7 +319,6 @@ struct MiscellaneousTests { func `Test with raw identifier and raw identifier parameter labels can compile`(`argument name` i: Int) { #expect(i == 0) } -#endif @Test("Free functions are runnable") func freeFunction() async throws { diff --git a/Tests/_MemorySafeTestingTests/MemorySafeTestDecls.swift b/Tests/_MemorySafeTestingTests/MemorySafeTestDecls.swift index 3eb19be5b..206c41488 100644 --- a/Tests/_MemorySafeTestingTests/MemorySafeTestDecls.swift +++ b/Tests/_MemorySafeTestingTests/MemorySafeTestDecls.swift @@ -8,8 +8,6 @@ // See https://swift.org/CONTRIBUTORS.txt for Swift project authors // -#if compiler(>=6.2) - @testable import Testing #if !hasFeature(StrictMemorySafety) @@ -29,5 +27,3 @@ func exampleExitTest() async { await #expect(processExitsWith: .success) {} } #endif - -#endif From e4e0b88295d3fead4bd24f290aa796cb8259fd05 Mon Sep 17 00:00:00 2001 From: Kelvin Bui <150134371+tienquocbui@users.noreply.github.com> Date: Fri, 19 Sep 2025 17:53:40 +0200 Subject: [PATCH 148/216] Implement hierarchical console output with comprehensive test result display (#1290) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement hierarchical console output with comprehensive test result display ### Modifications: - Implemented a tree structure using Unicode box-drawing characters (┌─, ├─, ╰─, │) that properly displays the relationship between modules, test suites, and individual tests with ASCII fallback - **Test Result Summary**: - Right-aligned duration formatting in consistent `x.xxs` format - Updated summary format: "X tests completed in Y.ZZs (pass: A, fail: B, skip: C)" - Concise failure summaries in hierarchy for quick scanning - **Detailed Failure Section**: - Complete hierarchical path display for failed tests - Comprehensive failure analysis with test count: "FAILED TEST DETAILS (3)" - Detailed expectation failure messages with actual vs expected values - Source location information with file and line numbers - Progress tracking with error counters [1/3], [2/3], [3/3] - Support for multiple expectations per test with individual analysis - **Unit Tests**: Added automated tests following established patterns to validate output formatting, hierarchy generation, and failure handling ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- .../Event.AdvancedConsoleOutputRecorder.swift | 930 +++++++++++++++++- .../AdvancedConsoleOutputRecorderTests.swift | 239 +++++ 2 files changed, 1127 insertions(+), 42 deletions(-) create mode 100644 Tests/TestingTests/AdvancedConsoleOutputRecorderTests.swift diff --git a/Sources/Testing/Events/Recorder/Event.AdvancedConsoleOutputRecorder.swift b/Sources/Testing/Events/Recorder/Event.AdvancedConsoleOutputRecorder.swift index 2f537cb3d..83bf12ef9 100644 --- a/Sources/Testing/Events/Recorder/Event.AdvancedConsoleOutputRecorder.swift +++ b/Sources/Testing/Events/Recorder/Event.AdvancedConsoleOutputRecorder.swift @@ -15,13 +15,33 @@ extension Event { /// This recorder is currently experimental and must be enabled via the /// `SWT_ENABLE_EXPERIMENTAL_CONSOLE_OUTPUT` environment variable. struct AdvancedConsoleOutputRecorder: Sendable { + /// Configuration for box-drawing character rendering strategy. + enum BoxDrawingMode: Sendable { + /// Use Unicode box-drawing characters (┌─, ├─, ╰─, │). + case unicode + /// Use Windows Code Page 437 box-drawing characters (┌─, ├─, └─, │). + case windows437 + /// Use ASCII fallback characters (--, |-, `-, |). + case ascii + } + /// Configuration options for the advanced console output recorder. struct Options: Sendable { /// Base console output recorder options to inherit from. var base: Event.ConsoleOutputRecorder.Options + /// Box-drawing character mode override. + /// + /// When `nil` (default), the mode is automatically determined based on platform: + /// - macOS/Linux: Unicode if ANSI enabled, otherwise ASCII + /// - Windows: Code Page 437 if ANSI enabled, otherwise ASCII + /// + /// Set to a specific mode to override the automatic selection. + var boxDrawingMode: BoxDrawingMode? + init() { self.base = Event.ConsoleOutputRecorder.Options() + self.boxDrawingMode = nil // Use automatic selection } } @@ -31,7 +51,89 @@ extension Event { /// This is needed because ABI.EncodedEvent doesn't contain full test context. var testStorage: [String: ABI.EncodedTest] = [:] - // Future storage for result data and other event information can be added here + /// Hierarchical test tree structure using Graph for efficient operations. + /// Key path represents the hierarchy (e.g., ["TestingTests", "ClockAPITests", "testMethod"]) + /// Value contains the test node data for that specific node. + var testTree: Graph = Graph() + + /// Consolidated test data for each test, keyed by test ID string. + /// Contains all runtime information gathered during test execution. + var testData: [String: _TestData] = [:] + + /// The instant when the test run was started. + /// Used to calculate total run duration. + var runStartTime: ABI.EncodedInstant? + + /// The instant when the test run was completed. + /// Used to calculate total run duration. + var runEndTime: ABI.EncodedInstant? + + /// The number of tests that passed during this run. + var totalPassed: Int = 0 + + /// The number of tests that failed during this run. + var totalFailed: Int = 0 + + /// The number of tests that were skipped during this run. + var totalSkipped: Int = 0 + } + + /// Consolidated data for a single test, combining result, timing, and issues. + private struct _TestData: Sendable { + /// The final result of the test execution (passed, failed, or skipped). + /// This is determined after all events for the test have been processed. + var result: _TestResult? + + /// The instant when the test started executing. + /// Used to calculate individual test duration. + var startTime: ABI.EncodedInstant? + + /// The instant when the test finished executing. + /// Used to calculate individual test duration. + var endTime: ABI.EncodedInstant? + + /// All issues recorded during the test execution. + /// Includes failures, warnings, and other diagnostic information. + var issues: [ABI.EncodedIssue] = [] + + /// Detailed messages for each issue, preserving the order and association. + /// Each inner array contains all messages for a single issue. + var issueMessages: [[ABI.EncodedMessage]] = [] + } + + /// Represents a node in the test hierarchy tree. + /// Graph handles the parent-child relationships, so this only stores node-specific data. + private struct _HierarchyNode: Sendable { + /// The unique identifier for this test or test suite. + let testID: String + + /// The base name of the test or suite without display formatting. + let name: String + + /// The human-readable display name for the test or suite, if different from name. + let displayName: String? + + /// Whether this node represents a test suite (true) or individual test (false). + let isSuite: Bool + + init(testID: String, name: String, displayName: String?, isSuite: Bool) { + self.testID = testID + self.name = name + self.displayName = displayName + self.isSuite = isSuite + } + } + + /// Represents the result of a test execution. + private enum _TestResult: Sendable { + /// The test executed successfully without any failures. + case passed + + /// The test failed due to one or more assertion failures or errors. + case failed + + /// The test was skipped and did not execute. + case skipped } /// The options for this recorder. @@ -64,71 +166,815 @@ extension Event { } } +// MARK: - 3-Tiered Fallback Support + extension Event.AdvancedConsoleOutputRecorder { - /// Record an event by processing it and generating appropriate output. + /// Determine the appropriate box-drawing mode based on platform and configuration. + private var _boxDrawingMode: BoxDrawingMode { + // Use explicit override if provided + if let explicitMode = options.boxDrawingMode { + return explicitMode + } + + // Otherwise, use platform-appropriate defaults +#if os(Windows) + // On Windows, prefer Code Page 437 characters if ANSI is enabled, otherwise ASCII + return options.base.useANSIEscapeCodes ? .windows437 : .ascii +#else + // On macOS/Linux, prefer Unicode if ANSI is enabled, otherwise ASCII + return options.base.useANSIEscapeCodes ? .unicode : .ascii +#endif + } + + /// Get the appropriate tree drawing character with 3-tiered fallback. /// - /// This implementation converts the Event to ABI.EncodedEvent for internal processing, - /// following the ABI-based architecture for future separation into a harness process. + /// Implements the fallback strategy: + /// 1. Default (macOS/Linux): Unicode characters (┌─, ├─, ╰─, │) + /// 2. Windows fallback: Code Page 437 characters (┌─, ├─, └─, │) + /// 3. Final fallback: ASCII characters (--, |-, `-, |) + /// + /// - Parameters: + /// - unicode: The Unicode box-drawing character to use. + /// - windows437: The Windows Code Page 437 character to use. + /// - ascii: The ASCII fallback character(s) to use. + /// + /// - Returns: The appropriate character based on platform and terminal capabilities. + private func _treeCharacter(unicode: String, windows437: String, ascii: String) -> String { + switch _boxDrawingMode { + case .unicode: + return unicode + case .windows437: + return windows437 + case .ascii: + return ascii + } + } + + /// Get the tree branch character (├─). + private var _treeBranch: String { + _treeCharacter(unicode: "├─ ", windows437: "├─ ", ascii: "|- ") + } + + /// Get the tree last branch character (╰─ or └─). + private var _treeLastBranch: String { + _treeCharacter(unicode: "╰─ ", windows437: "└─ ", ascii: "`- ") + } + + /// Get the tree vertical line character (│). + private var _treeVertical: String { + _treeCharacter(unicode: "│", windows437: "│", ascii: "|") + } +} + +extension Event.AdvancedConsoleOutputRecorder { + /// Record an event and its context. /// /// - Parameters: /// - event: The event to record. - /// - eventContext: The context associated with the event. - func record(_ event: borrowing Event, in eventContext: borrowing Event.Context) { - // Handle test discovery to populate our test storage - if case .testDiscovered = event.kind, let test = eventContext.test { + /// - eventContext: Contextual information about the event. + public func record(_ event: borrowing Event, in eventContext: borrowing Event.Context) { + // Extract values before entering lock to avoid borrowing issues + let eventKind = event.kind + let testValue = eventContext.test + + // Handle test discovery for hierarchy building + if case .testDiscovered = eventKind, let test = testValue { let encodedTest = ABI.EncodedTest(encoding: test) + _context.withLock { context in - context.testStorage[encodedTest.id.stringValue] = encodedTest + _buildTestHierarchy(encodedTest, in: &context) } } - // Generate human-readable messages for the event + // Generate detailed messages using HumanReadableOutputRecorder let messages = _humanReadableRecorder.record(event, in: eventContext) - // Convert Event to ABI.EncodedEvent + // Convert Event to ABI.EncodedEvent for processing (if needed) if let encodedEvent = ABI.EncodedEvent(encoding: event, in: eventContext, messages: messages) { - // Process the ABI event _processABIEvent(encodedEvent) } - // For now, still delegate to the fallback recorder to maintain existing functionality - _fallbackRecorder.record(event, in: eventContext) + // Only output specific messages during the run, suppress most standard output + // The hierarchical summary will be shown at the end + switch eventKind { + case .runStarted: + let symbol = Event.Symbol.default.stringValue(options: _fallbackRecorder.options) + write("\(symbol) Test run started.\n") + + case .runEnded: + // The hierarchical summary is generated in _processABIEvent for runEnded + break + + default: + // Suppress other standard messages to avoid duplicate output + // The hierarchy will show all the details at the end + break + } + } + + /// Build the test hierarchy from discovered tests. + /// + /// - Parameters: + /// - encodedTest: The test to add to the hierarchy. + /// - context: The mutable context to update. + private func _buildTestHierarchy(_ encodedTest: ABI.EncodedTest, in context: inout _Context) { + let testID = encodedTest.id.stringValue + let isSuite = encodedTest.kind == .suite + + // Create hierarchy node + let hierarchyNode = _HierarchyNode( + testID: testID, + name: encodedTest.name, + displayName: encodedTest.displayName, + isSuite: isSuite + ) + + // Parse the test ID to extract the key path for Graph + let keyPath = _parseTestIDToKeyPath(testID) + + // Insert the node into the Graph at the appropriate key path + context.testTree[keyPath] = hierarchyNode + + // Create intermediate nodes (modules and suites) if they don't exist + for i in 1.. ["TestingTests", "ClockAPITests", "testMethod()"] + /// - "TestingTests" -> ["TestingTests"] + /// + /// - Parameters: + /// - testID: The test ID to parse. + /// - Returns: An array of key path components. + private func _parseTestIDToKeyPath(_ testID: String) -> [String] { + // Use backtick-aware split for proper handling of raw identifiers + let components = rawIdentifierAwareSplit(testID, separator: "/").map(String.init) + var logicalPath: [String] = [] + + for component in components { + // Skip source location components (filename should be the last component) + if component.hasSuffix(".swift:") { + break + } + logicalPath.append(component) + } + + // Convert the first component from dot notation to separate components + // e.g., "TestingTests.ClockAPITests" -> ["TestingTests", "ClockAPITests"] + var keyPath: [String] = [] + + if let firstComponent = logicalPath.first { + let moduleParts = rawIdentifierAwareSplit(firstComponent, separator: ".").map(String.init) + keyPath.append(contentsOf: moduleParts) + + // Add any additional path components (for nested suites) + keyPath.append(contentsOf: logicalPath.dropFirst()) + } + + return keyPath.isEmpty ? [testID] : keyPath + } + + /// Extract all root nodes (module-level nodes) from the Graph. + /// + /// - Parameters: + /// - testTree: The Graph to extract root nodes from. + /// - Returns: Array of key paths for root nodes (modules). + private func _rootNodes(from testTree: Graph) -> [[String]] { + var rootNodes: [[String]] = [] + var moduleNames: Set = [] + + // Find all unique module names (first component of key paths) + testTree.forEach { keyPath, node in + if node != nil && !keyPath.isEmpty { + let moduleName = keyPath[0] + moduleNames.insert(moduleName) + } + } + + // Convert module names to single-component key paths + for moduleName in moduleNames.sorted() { + rootNodes.append([moduleName]) + } + + return rootNodes + } + + /// Find a hierarchy node from a test ID by searching the Graph. + /// + /// - Parameters: + /// - testID: The test ID to search for. + /// - testTree: The Graph to search in. + /// - Returns: The hierarchy node if found. + private func _nodeFromTestID(_ testID: String, in testTree: Graph) -> _HierarchyNode? { + var foundNode: _HierarchyNode? + + testTree.forEach { keyPath, node in + if node?.testID == testID { + foundNode = node + } + } + + return foundNode + } + + /// Find all child key paths for a given parent key path in the Graph. + /// + /// - Parameters: + /// - parentKeyPath: The parent key path. + /// - testTree: The Graph to search in. + /// - Returns: Array of child key paths sorted alphabetically. + private func _childKeyPaths(for parentKeyPath: [String], in testTree: Graph) -> [[String]] { + var childKeyPaths: [[String]] = [] + + testTree.forEach { keyPath, node in + if keyPath.count == parentKeyPath.count + 1 && + keyPath.prefix(parentKeyPath.count).elementsEqual(parentKeyPath) && + node != nil { + childKeyPaths.append(keyPath) + } + } + + return childKeyPaths.sorted { $0.last ?? "" < $1.last ?? "" } + } + + /// Find the key path for a given test ID in the Graph. + /// + /// - Parameters: + /// - testID: The test ID to search for. + /// - testTree: The Graph to search in. + /// - Returns: The key path if found, nil otherwise. + private func _findKeyPathForTestID(_ testID: String, in testTree: Graph) -> [String]? { + var foundKeyPath: [String]? + + testTree.forEach { keyPath, node in + if node?.testID == testID { + foundKeyPath = keyPath + } + } + + return foundKeyPath } /// Process an ABI.EncodedEvent for advanced console output. /// - /// This is where the enhanced console logic will be implemented in future PRs. - /// Currently this is a placeholder that demonstrates the ABI conversion. + /// This implements the enhanced console logic for hierarchical display and failure summary. /// /// - Parameters: /// - encodedEvent: The ABI-encoded event to process. private func _processABIEvent(_ encodedEvent: ABI.EncodedEvent) { - // TODO: Implement enhanced console output logic here - // This will be expanded in subsequent PRs for: - // - Failure summary display - // - Progress bar functionality - // - Hierarchical test result display - - // For now, we just demonstrate that we can access the ABI event data - switch encodedEvent.kind { - case .runStarted: - // Could implement run start logic here - break - case .testStarted: - // Could implement test start logic here - break - case .issueRecorded: - // Could implement issue recording logic here - break - case .testEnded: - // Could implement test end logic here - break - case .runEnded: - // Could implement run end logic here - break - default: - // Handle other event types - break + _context.withLock { context in + switch encodedEvent.kind { + case .runStarted: + context.runStartTime = encodedEvent.instant + + case .testStarted: + // Track test start time + if let testID = encodedEvent.testID?.stringValue { + var testData = context.testData[testID] ?? _TestData() + testData.startTime = encodedEvent.instant + context.testData[testID] = testData + } + + case .issueRecorded: + // Record issues for failure summary + if let testID = encodedEvent.testID?.stringValue, + let issue = encodedEvent.issue { + var testData = context.testData[testID] ?? _TestData() + testData.issues.append(issue) + testData.issueMessages.append(encodedEvent.messages) + context.testData[testID] = testData + } + + case .testEnded: + // Track test end time and determine result + if let testID = encodedEvent.testID?.stringValue { + var testData = context.testData[testID] ?? _TestData() + testData.endTime = encodedEvent.instant + + // Determine test result based on issues + let hasFailures = testData.issues.contains { !$0.isKnown && ($0.isFailure ?? true) } + let result: _TestResult = hasFailures ? .failed : .passed + testData.result = result + context.testData[testID] = testData + + // Update statistics + switch result { + case .passed: + context.totalPassed += 1 + case .failed: + context.totalFailed += 1 + case .skipped: + context.totalSkipped += 1 + } + } + + case .testSkipped: + // Mark test as skipped + if let testID = encodedEvent.testID?.stringValue { + var testData = context.testData[testID] ?? _TestData() + testData.result = .skipped + context.testData[testID] = testData + context.totalSkipped += 1 + } + + case .runEnded: + context.runEndTime = encodedEvent.instant + // Generate hierarchical summary + _generateHierarchicalSummary(context: context) + + default: + // Handle other event types + break + } + } + } + + /// Generate the final hierarchical summary when the run completes. + /// + /// - Parameters: + /// - context: The context containing all hierarchy and results data. + private func _generateHierarchicalSummary(context: _Context) { + var output = "\n" + + // Hierarchical Test Results + output += "══════════════════════════════════════ HIERARCHICAL TEST RESULTS ══════════════════════════════════════\n" + output += "\n" + + // Render the test hierarchy tree using Graph + let rootNodes = _rootNodes(from: context.testTree) + + if rootNodes.isEmpty { + // Show test results as flat list if no hierarchy + let allTests = context.testData.sorted { $0.key < $1.key } + for (testID, testData) in allTests { + let statusIcon = _statusIcon(for: testData.result ?? .passed) + let testName = _nodeFromTestID(testID, in: context.testTree)?.displayName ?? _nodeFromTestID(testID, in: context.testTree)?.name ?? testID + output += "\(statusIcon) \(testName)\n" + } + } else { + // Render the test hierarchy tree + for (index, rootKeyPath) in rootNodes.enumerated() { + if let rootNode = context.testTree[rootKeyPath] { + output += _renderHierarchyNode(rootNode, keyPath: rootKeyPath, context: context, prefix: "", isLast: true) + + // Add blank line between top-level modules (treat as separate trees) + if index < rootNodes.count - 1 { + output += "\n" + } + } + } + } + + output += "\n" + + // Test run summary + let totalTests = context.totalPassed + context.totalFailed + context.totalSkipped + + // Calculate total run duration + var totalDuration = "" + if let startTime = context.runStartTime, let endTime = context.runEndTime { + totalDuration = _formatDuration(endTime.absolute - startTime.absolute) } + + // Format: [total] tests completed in [duration] ([pass symbol] pass: [number], [failed symbol] fail: [number], ...) + let passIcon = _statusIcon(for: .passed) + let failIcon = _statusIcon(for: .failed) + let skipIcon = _statusIcon(for: .skipped) + + var summaryParts: [String] = [] + if context.totalPassed > 0 { + summaryParts.append("\(passIcon) pass: \(context.totalPassed)") + } + if context.totalFailed > 0 { + summaryParts.append("\(failIcon) fail: \(context.totalFailed)") + } + if context.totalSkipped > 0 { + summaryParts.append("\(skipIcon) skip: \(context.totalSkipped)") + } + + let summaryDetails = summaryParts.joined(separator: ", ") + let durationText = totalDuration.isEmpty ? "" : " in \(totalDuration)" + output += "\(totalTests) test\(totalTests == 1 ? "" : "s") completed\(durationText) (\(summaryDetails))\n" + output += "\n" + + // Failed Test Details (only if there are failures) + let failedTests = context.testData.filter { $0.value.result == .failed } + if !failedTests.isEmpty { + output += "══════════════════════════════════════ FAILED TEST DETAILS (\(failedTests.count)) ══════════════════════════════════════\n" + output += "\n" + + // Iterate through all tests that recorded one or more failures + for (testIndex, testEntry) in failedTests.enumerated() { + let (testID, testData) = testEntry + let testNumber = testIndex + 1 + let totalFailedTests = failedTests.count + + // Get the fully qualified test name by traversing up the hierarchy + let fullyQualifiedName = _getFullyQualifiedTestNameWithFile(testID: testID, context: context) + + let failureIcon = _statusIcon(for: .failed) + output += "\(failureIcon) \(fullyQualifiedName)\n" + + // Show detailed issue information with enhanced formatting + if !testData.issues.isEmpty { + for (issueIndex, issue) in testData.issues.enumerated() { + // 1. Error Message - Get detailed error description + let issueDescription = _formatDetailedIssueDescription(issue, issueIndex: issueIndex, testData: testData) + + if !issueDescription.isEmpty { + let errorLines = issueDescription.split(separator: "\n", omittingEmptySubsequences: false) + for line in errorLines { + output += " \(line)\n" + } + } + + // 2. Location + if let sourceLocation = issue.sourceLocation { + output += "\n" + output += " Location: \(sourceLocation.fileName):\(sourceLocation.line):\(sourceLocation.column)\n" + } + + // 3. Statistics - Error counter in lower right + let errorCounter = "[\(testNumber)/\(totalFailedTests)]" + let paddingLength = max(0, 100 - errorCounter.count) + output += "\n" + output += "\(String(repeating: " ", count: paddingLength))\(errorCounter)\n" + + // Add spacing between issues (except for the last one) + if issueIndex < testData.issues.count - 1 { + output += "\n" + } + } + } + + // Add spacing between tests (except for the last one) + if testIndex < failedTests.count - 1 { + output += "\n" + } + } + } + + write(output) + } + + /// Render a hierarchy node with proper indentation and tree drawing characters. + /// + /// - Parameters: + /// - node: The node to render. + /// - context: The hierarchy context. + /// - prefix: The prefix for indentation and tree drawing. + /// - isLast: Whether this is the last child at its level. + /// - Returns: The rendered string for this node and its children. + private func _renderHierarchyNode(_ node: _HierarchyNode, keyPath: [String], context: _Context, prefix: String, isLast: Bool) -> String { + var output = "" + + if node.isSuite { + // Suite header + let treePrefix: String + if prefix.isEmpty { + // Top-level modules: no tree prefix, flush left (treat as separate trees) + treePrefix = "" + } else { + // Nested suites: use standard tree characters + treePrefix = isLast ? _treeLastBranch : _treeBranch + } + + let suiteName = node.displayName ?? node.name + output += "\(prefix)\(treePrefix)\(suiteName)\n" + + // Render children with updated prefix + let childPrefix: String + if prefix.isEmpty { + // Top-level modules: children start with 3 spaces (no vertical line needed) + childPrefix = " " + } else { + // Nested case: continue vertical line unless this is the last node + childPrefix = prefix + (isLast ? " " : "\(_treeVertical) ") + } + + let childKeyPaths = _childKeyPaths(for: keyPath, in: context.testTree) + for (childIndex, childKeyPath) in childKeyPaths.enumerated() { + let isLastChild = childIndex == childKeyPaths.count - 1 + if let childNode = context.testTree[childKeyPath] { + output += _renderHierarchyNode(childNode, keyPath: childKeyPath, context: context, prefix: childPrefix, isLast: isLastChild) + + // Add spacing between child nodes when the next sibling is a suite + // Continue the tree structure with vertical line + if childIndex < childKeyPaths.count - 1 { + // Check if the next sibling is a suite + let nextChildKeyPath = childKeyPaths[childIndex + 1] + if let nextChildNode = context.testTree[nextChildKeyPath], nextChildNode.isSuite { + // Use the correct spacing prefix + let spacingPrefix: String + if prefix.isEmpty { + // Top-level modules: use 3 spaces + vertical line + spacingPrefix = " \(_treeVertical)" + } else { + // Nested case: use the child prefix + spacingPrefix = childPrefix + } + output += "\(spacingPrefix)\n" // Add the vertical line continuation + } + } + } + } + } else { + // Test case line + let treePrefix = isLast ? _treeLastBranch : _treeBranch + let statusIcon = _statusIcon(for: context.testData[node.testID]?.result ?? .passed) + let testName = node.displayName ?? node.name + + // Calculate duration + var duration = "" + if let startTime = context.testData[node.testID]?.startTime, + let endTime = context.testData[node.testID]?.endTime { + duration = _formatDuration(endTime.absolute - startTime.absolute) + } + + // Format with right-aligned duration + let testLine = "\(statusIcon) \(testName)" + let fullPrefix = "\(prefix)\(treePrefix)" + let paddedTestLine = _padWithDuration(testLine, duration: duration, existingPrefix: fullPrefix) + output += "\(fullPrefix)\(paddedTestLine)\n" + + // Show concise issue summary for quick overview + if let issues = context.testData[node.testID]?.issues, !issues.isEmpty { + let issuePrefix = prefix + (isLast ? " " : "\(_treeVertical) ") + for (issueIndex, issue) in issues.enumerated() { + let isLastIssue = issueIndex == issues.count - 1 + let issueTreePrefix = isLastIssue ? _treeLastBranch : _treeBranch + + // Show "Expectation failed" with the actual error details + let fullDescription = _formatDetailedIssueDescription(issue, issueIndex: issueIndex, testData: context.testData[node.testID]!) + let conciseDescription = fullDescription.split(separator: "\n").first.map(String.init) ?? "Expected condition was not met" + output += "\(issuePrefix)\(issueTreePrefix)Expectation failed: \(conciseDescription)\n" + + // Add concise source location + if let sourceLocation = issue.sourceLocation { + let locationPrefix = issuePrefix + (isLastIssue ? " " : "\(_treeVertical) ") + output += "\(locationPrefix)at \(sourceLocation.fileName):\(sourceLocation.line)\n" + } + } + } + } + + return output + } + + /// Format a detailed description of an issue for the Failed Test Details section. + /// + /// - Parameters: + /// - issue: The encoded issue to format. + /// - issueIndex: The index of the issue in the testData.issues array. + /// - testData: The test data containing the stored messages. + /// - Returns: A detailed description of what failed. + private func _formatDetailedIssueDescription(_ issue: ABI.EncodedIssue, issueIndex: Int, testData: _TestData) -> String { + // Get the corresponding messages for this issue + guard issueIndex < testData.issueMessages.count else { + // Fallback to error description if available + if let error = issue._error { + return error.description + } + return "Issue recorded" + } + + let messages = testData.issueMessages[issueIndex] + + // Look for detailed messages (difference, details) that contain the actual failure information + var detailedMessages: [String] = [] + + for message in messages { + switch message.symbol { + case .difference, .details: + // These contain the detailed expectation failure information + detailedMessages.append(message.text) + case .fail: + // Primary failure message - use if no detailed messages available + if detailedMessages.isEmpty { + detailedMessages.append(message.text) + } + default: + break + } + } + + if !detailedMessages.isEmpty { + let fullMessage = detailedMessages.joined(separator: "\n") + // Truncate very long messages to prevent layout issues + if fullMessage.count > 200 { + let truncated = String(fullMessage.prefix(200)) + return truncated + "..." + } + return fullMessage + } + + // Final fallback + if let error = issue._error { + let errorDesc = error.description + // Truncate very long error descriptions + if errorDesc.count > 200 { + return String(errorDesc.prefix(200)) + "..." + } + return errorDesc + } + return "Issue recorded" + } + + /// Determine the status icon for a test result. + /// + /// - Parameters: + /// - result: The test result. + /// - Returns: The appropriate symbol string. + private func _statusIcon(for result: _TestResult) -> String { + switch result { + case .passed: + return Event.Symbol.pass(knownIssueCount: 0).stringValue(options: options.base) + case .failed: + return Event.Symbol.fail.stringValue(options: options.base) + case .skipped: + return Event.Symbol.skip.stringValue(options: options.base) + } + } + + /// Format a duration in seconds with exactly 2 decimal places. + /// + /// - Parameter duration: The duration to format. + /// - Returns: A formatted duration string (e.g., "1.80s", "0.05s"). + private func _formatDuration(_ duration: Double) -> String { + // Always format to exactly 2 decimal places + let wholePart = Int(duration) + let fractionalPart = Int((duration - Double(wholePart)) * 100 + 0.5) // Round to nearest hundredth + + // Handle rounding overflow (e.g., 0.999 -> 1.00) + if fractionalPart >= 100 { + return "\(wholePart + 1).00s" + } else { + let fractionalString = fractionalPart < 10 ? "0\(fractionalPart)" : "\(fractionalPart)" + return "\(wholePart).\(fractionalString)s" + } + } + + /// Pad a test line with right-aligned duration. + /// + /// - Parameters: + /// - testLine: The test line to pad. + /// - duration: The duration string. + /// - existingPrefix: Any prefix that will be added before this line. + /// - Returns: The padded test line with right-aligned duration. + private func _padWithDuration(_ testLine: String, duration: String, existingPrefix: String = "") -> String { + if duration.isEmpty { + return testLine + } + + // Get terminal width dynamically, fall back to 120 if unavailable + let targetWidth = _terminalWidth() + let rightPart = "(\(duration))" + + // Calculate visible character count (excluding ANSI escape codes) + let visiblePrefixLength = _visibleCharacterCount(existingPrefix) + let visibleLeftLength = _visibleCharacterCount(testLine) + let totalRightLength = rightPart.count + + // Ensure minimum spacing between content and duration + let minimumSpacing = 3 + let totalUsedWidth = visiblePrefixLength + visibleLeftLength + totalRightLength + minimumSpacing + + if totalUsedWidth < targetWidth { + let paddingLength = targetWidth - visiblePrefixLength - visibleLeftLength - totalRightLength + return "\(testLine)\(String(repeating: " ", count: paddingLength))\(rightPart)" + } else { + return "\(testLine) \(rightPart)" + } + } + + /// Determine the current terminal width, with fallback to reasonable default. + /// + /// - Returns: Terminal width in characters, defaults to 120 if unavailable. + private func _terminalWidth() -> Int { + // Try to get terminal width from environment variable + if let columnsEnv = Environment.variable(named: "COLUMNS"), + let columns = Int(columnsEnv), columns > 0 { + return columns + } + + // Fallback to a reasonable default width + // Modern terminals are typically 120+ characters wide + return 120 + } + + /// Calculate the visible character count, excluding ANSI escape sequences. + /// + /// - Parameters: + /// - string: The string to count visible characters in. + /// - Returns: The number of visible characters. + private func _visibleCharacterCount(_ string: String) -> Int { + var visibleCount = 0 + var inEscapeSequence = false + var i = string.startIndex + + while i < string.endIndex { + let char = string[i] + + if char == "\u{1B}" { // ESC character + inEscapeSequence = true + } else if inEscapeSequence && (char == "m" || char == "K") { + // End of ANSI escape sequence + inEscapeSequence = false + } else if !inEscapeSequence { + visibleCount += 1 + } + + i = string.index(after: i) + } + + return visibleCount + } + + /// Get the fully qualified test name for a given test ID. + /// + /// This function traverses the hierarchy to build the full test name. + /// + /// - Parameters: + /// - testID: The ID of the test. + /// - context: The context containing the test hierarchy. + /// - Returns: The fully qualified test name. + private func _getFullyQualifiedTestName(testID: String, context: _Context) -> String { + guard let keyPath = _findKeyPathForTestID(testID, in: context.testTree) else { return testID } + + var nameParts: [String] = [] + + // Build the hierarchy path by traversing from root to leaf + for i in 1...keyPath.count { + let currentKeyPath = Array(keyPath.prefix(i)) + if let node = context.testTree[currentKeyPath] { + let displayName = node.displayName ?? node.name + nameParts.append(displayName) + } + } + + return nameParts.joined(separator: "/") + } + + /// Get the fully qualified test name for a given test ID, including the file name. + /// + /// This function traverses the hierarchy to build the full test name in the format: + /// ModuleName/FileName/"SuiteName"/"TestName" + /// + /// - Parameters: + /// - testID: The ID of the test. + /// - context: The context containing the test hierarchy. + /// - Returns: The fully qualified test name with file name included. + private func _getFullyQualifiedTestNameWithFile(testID: String, context: _Context) -> String { + guard let keyPath = _findKeyPathForTestID(testID, in: context.testTree) else { return testID } + + // Get the source file name from the first issue + var fileName = "" + if let issues = context.testData[testID]?.issues, + let firstIssue = issues.first, + let sourceLocation = firstIssue.sourceLocation { + fileName = sourceLocation.fileName + } + + var nameParts: [String] = [] + + // Build the hierarchy path by traversing from root to leaf + for i in 1...keyPath.count { + let currentKeyPath = Array(keyPath.prefix(i)) + if let node = context.testTree[currentKeyPath] { + let displayName = node.displayName ?? node.name + + // For non-module nodes (suites and tests), wrap in quotes + if i > 1 { + nameParts.append("\"\(displayName)\"") + } else { + // Module name - no quotes + nameParts.append(displayName) + } + } + } + + // Insert file name after module name if we have it + if !fileName.isEmpty && nameParts.count > 0 { + nameParts.insert(fileName, at: 1) + } + + return nameParts.joined(separator: "/") } } diff --git a/Tests/TestingTests/AdvancedConsoleOutputRecorderTests.swift b/Tests/TestingTests/AdvancedConsoleOutputRecorderTests.swift new file mode 100644 index 000000000..dc8dd5260 --- /dev/null +++ b/Tests/TestingTests/AdvancedConsoleOutputRecorderTests.swift @@ -0,0 +1,239 @@ +// +// 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 +// + +@testable @_spi(Experimental) @_spi(ForToolsIntegrationOnly) import Testing + +@Suite("Advanced Console Output Recorder Tests") +struct AdvancedConsoleOutputRecorderTests { + final class Stream: TextOutputStream, Sendable { + let buffer = Locked(rawValue: "") + + @Sendable func write(_ string: String) { + buffer.withLock { + $0.append(string) + } + } + } + + @Test("Recorder initialization with default options") + func recorderInitialization() { + let stream = Stream() + let recorder = Event.AdvancedConsoleOutputRecorder(writingUsing: stream.write) + + // Verify the recorder was created successfully and has expected defaults + #expect(recorder.options.base.useANSIEscapeCodes == false) // Default for non-TTY + } + + @Test("Recorder initialization with custom options") + func recorderInitializationWithCustomOptions() { + let stream = Stream() + var options = Event.AdvancedConsoleOutputRecorder.Options() + options.base.useANSIEscapeCodes = true + + let recorder = Event.AdvancedConsoleOutputRecorder( + options: options, + writingUsing: stream.write + ) + + // Verify the custom options were applied + #expect(recorder.options.base.useANSIEscapeCodes == true) + } + + @Test("Basic event recording produces output") + func basicEventRecording() async { + let stream = Stream() + + var configuration = Configuration() + let eventRecorder = Event.AdvancedConsoleOutputRecorder(writingUsing: stream.write) + configuration.eventHandler = { event, context in + eventRecorder.record(event, in: context) + } + + // Run a simple test to generate events + await Test(name: "Sample Test") { + #expect(Bool(true)) + }.run(configuration: configuration) + + let buffer = stream.buffer.rawValue + // Verify that the hierarchical output was generated + #expect(buffer.contains("HIERARCHICAL TEST RESULTS")) + #expect(buffer.contains("Test run started")) + } + + @Test("Hierarchical output structure is generated") + func hierarchicalOutputStructure() async { + let stream = Stream() + + var configuration = Configuration() + let eventRecorder = Event.AdvancedConsoleOutputRecorder(writingUsing: stream.write) + configuration.eventHandler = { event, context in + eventRecorder.record(event, in: context) + } + + // Run tests that will create a hierarchy + await runTest(for: HierarchicalTestSuite.self, configuration: configuration) + + let buffer = stream.buffer.rawValue + + // Verify hierarchical output headers are generated + #expect(buffer.contains("HIERARCHICAL TEST RESULTS")) + #expect(buffer.contains("completed")) + + // Should contain tree structure characters (Unicode or ASCII fallback) + #expect(buffer.contains("├─") || buffer.contains("╰─") || buffer.contains("┌─") || + buffer.contains("|-") || buffer.contains("`-") || buffer.contains(".-")) + } + + @Test("Failed test details are properly formatted") + func failedTestDetails() async { + let stream = Stream() + + var configuration = Configuration() + let eventRecorder = Event.AdvancedConsoleOutputRecorder(writingUsing: stream.write) + configuration.eventHandler = { event, context in + eventRecorder.record(event, in: context) + } + + // Run tests with failures + await runTest(for: FailingTestSuite.self, configuration: configuration) + + let buffer = stream.buffer.rawValue + + // Verify failure details section is generated + #expect(buffer.contains("FAILED TEST DETAILS")) + + // Should show test hierarchy in failure details + #expect(buffer.contains("FailingTestSuite")) + } + + @Test("Test statistics are correctly calculated") + func testStatisticsCalculation() async { + let stream = Stream() + + var configuration = Configuration() + let eventRecorder = Event.AdvancedConsoleOutputRecorder(writingUsing: stream.write) + configuration.eventHandler = { event, context in + eventRecorder.record(event, in: context) + } + + // Run mixed passing and failing tests + await runTest(for: MixedTestSuite.self, configuration: configuration) + + let buffer = stream.buffer.rawValue + + // Verify that statistics are correctly calculated and displayed + #expect(buffer.contains("completed")) + #expect(buffer.contains("pass:") || buffer.contains("fail:")) + } + + @Test("Duration formatting is consistent") + func durationFormatting() async { + let stream = Stream() + + var configuration = Configuration() + let eventRecorder = Event.AdvancedConsoleOutputRecorder(writingUsing: stream.write) + configuration.eventHandler = { event, context in + eventRecorder.record(event, in: context) + } + + // Run a simple test to generate timing + await Test(name: "Timed Test") { + #expect(Bool(true)) + }.run(configuration: configuration) + + let buffer = stream.buffer.rawValue + + // Should not crash and should generate some output with timing + #expect(!buffer.isEmpty) + #expect(buffer.contains("s")) // Duration formatting should include 's' suffix + } + + @Test("Event consolidation works correctly") + func eventConsolidation() async { + let stream = Stream() + + var configuration = Configuration() + let eventRecorder = Event.AdvancedConsoleOutputRecorder(writingUsing: stream.write) + configuration.eventHandler = { event, context in + eventRecorder.record(event, in: context) + } + + // Run tests to verify the consolidated data structure works + await runTest(for: SimpleTestSuite.self, configuration: configuration) + + let buffer = stream.buffer.rawValue + + // Basic verification that the recorder processes events without crashing + #expect(!buffer.isEmpty) + #expect(buffer.contains("HIERARCHICAL TEST RESULTS")) + } +} + +// MARK: - Test Suites for Testing + +@Suite(.hidden) +struct HierarchicalTestSuite { + @Test(.hidden) + func passingTest() { + #expect(Bool(true)) + } + + @Test(.hidden) + func anotherPassingTest() { + #expect(1 + 1 == 2) + } + + @Suite(.hidden) + struct NestedSuite { + @Test(.hidden) + func nestedTest() { + #expect("hello".count == 5) + } + } +} + +@Suite(.hidden) +struct FailingTestSuite { + @Test(.hidden) + func failingTest() { + #expect(Bool(false), "This test is designed to fail") + } + + @Test(.hidden) + func passingTest() { + #expect(Bool(true)) + } +} + +@Suite(.hidden) +struct MixedTestSuite { + @Test(.hidden) + func test1() { + #expect(Bool(true)) + } + + @Test(.hidden) + func test2() { + #expect(Bool(false), "Intentional failure") + } + + @Test(.hidden) + func test3() { + #expect(1 == 1) + } +} + +@Suite(.hidden) +struct SimpleTestSuite { + @Test(.hidden) + func simpleTest() { + #expect(Bool(true)) + } +} From d8b63e61dae16ee8e2e1ebc6f6689f1b076fe636 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Mon, 22 Sep 2025 12:42:14 -0400 Subject: [PATCH 149/216] Exclude Xcode 16.4 from GitHub Actions. (#1331) We don't support building with Xcode 16.4 which only has Swift 6.1. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- .github/workflows/pull_request.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 288a012a1..41abeb44d 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -17,7 +17,7 @@ jobs: linux_os_versions: '["amazonlinux2", "jammy"]' windows_swift_versions: '["nightly-main", "nightly-6.2"]' enable_macos_checks: true - macos_exclude_xcode_versions: "[{\"xcode_version\": \"16.2\"}, {\"xcode_version\": \"16.3\"}]" + macos_exclude_xcode_versions: "[{\"xcode_version\": \"16.2\"}, {\"xcode_version\": \"16.3\"}, {\"xcode_version\": \"16.4\"}]" enable_wasm_sdk_build: true soundness: name: Soundness From a6d370e05fbadf4a3b2d5172190a6411c5b4a9f2 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Mon, 22 Sep 2025 18:07:17 -0400 Subject: [PATCH 150/216] Merge more of the image attachments codebase duplicated between Darwin and Windows. (#1329) This PR eliminates a bunch of duplicated code between Darwin and Windows that's used to support image attachments. We're able to do this because `_AttachableImageWrapper` is now a class and can be included in conforms-to generic constraints, so we can write `where AttachableValue: _AttachableImageWrapper & AttachableWrapper`. It was previously a structure and structures can't be used in generic constraints this way. There's a new public, underscored protocol introduced here, `_AttachableAsImage`, that serves as a "base" protocol for `AttachableAsCGImage` and `AttachableAsIWICBitmapSource`. I intend to promote this protocol to API, but it will need a Swift Evolution proposal first. I'm going to include it in a future "image attachments refinement" proposal that will cover a few other things. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- .../Attachments/AttachableAsCGImage.swift | 20 +--- .../_Testing_CoreGraphics/CMakeLists.txt | 1 - .../AttachableAsIWICBitmapSource.swift | 43 +------ ...achment+AttachableAsIWICBitmapSource.swift | 107 ------------------ ...Pointer+AttachableAsIWICBitmapSource.swift | 4 +- .../Overlays/_Testing_WinSDK/CMakeLists.txt | 1 - .../Attachment+_AttachableAsImage.swift} | 16 +-- .../Images/_AttachableAsImage.swift | 54 +++++++++ .../Images/_AttachableImageWrapper.swift | 12 +- Sources/Testing/CMakeLists.txt | 2 + 10 files changed, 70 insertions(+), 190 deletions(-) delete mode 100644 Sources/Overlays/_Testing_WinSDK/Attachments/Attachment+AttachableAsIWICBitmapSource.swift rename Sources/{Overlays/_Testing_CoreGraphics/Attachments/Attachment+AttachableAsCGImage.swift => Testing/Attachments/Images/Attachment+_AttachableAsImage.swift} (93%) create mode 100644 Sources/Testing/Attachments/Images/_AttachableAsImage.swift diff --git a/Sources/Overlays/_Testing_CoreGraphics/Attachments/AttachableAsCGImage.swift b/Sources/Overlays/_Testing_CoreGraphics/Attachments/AttachableAsCGImage.swift index 31427c9d7..96b93bad5 100644 --- a/Sources/Overlays/_Testing_CoreGraphics/Attachments/AttachableAsCGImage.swift +++ b/Sources/Overlays/_Testing_CoreGraphics/Attachments/AttachableAsCGImage.swift @@ -39,7 +39,7 @@ private import ImageIO /// @Available(Swift, introduced: 6.3) /// } @available(_uttypesAPI, *) -public protocol AttachableAsCGImage: SendableMetatype { +public protocol AttachableAsCGImage: _AttachableAsImage, SendableMetatype { /// An instance of `CGImage` representing this image. /// /// - Throws: Any error that prevents the creation of an image. @@ -68,22 +68,6 @@ public protocol AttachableAsCGImage: SendableMetatype { /// This property is not part of the public interface of the testing /// library. It may be removed in a future update. var _attachmentScaleFactor: CGFloat { get } - - /// Make a copy of this instance to pass to an attachment. - /// - /// - Returns: A copy of `self`, or `self` if no copy is needed. - /// - /// The testing library uses this function to take ownership of image - /// resources that test authors pass to it. If possible, make a copy of or add - /// a reference to `self`. If this type does not support making copies, return - /// `self` verbatim. - /// - /// The default implementation of this function when `Self` conforms to - /// `Sendable` simply returns `self`. - /// - /// This function is not part of the public interface of the testing library. - /// It may be removed in a future update. - func _copyAttachableValue() -> Self } @available(_uttypesAPI, *) @@ -95,6 +79,8 @@ extension AttachableAsCGImage { public var _attachmentScaleFactor: CGFloat { 1.0 } + + public func _deinitializeAttachableValue() {} } @available(_uttypesAPI, *) diff --git a/Sources/Overlays/_Testing_CoreGraphics/CMakeLists.txt b/Sources/Overlays/_Testing_CoreGraphics/CMakeLists.txt index fcd1d3459..567428150 100644 --- a/Sources/Overlays/_Testing_CoreGraphics/CMakeLists.txt +++ b/Sources/Overlays/_Testing_CoreGraphics/CMakeLists.txt @@ -11,7 +11,6 @@ if(APPLE) Attachments/_AttachableImageWrapper+AttachableWrapper.swift Attachments/AttachableAsCGImage.swift Attachments/AttachableImageFormat+UTType.swift - Attachments/Attachment+AttachableAsCGImage.swift Attachments/CGImage+AttachableAsCGImage.swift ReexportTesting.swift) diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsIWICBitmapSource.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsIWICBitmapSource.swift index fdcad1809..60d2e28b8 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsIWICBitmapSource.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsIWICBitmapSource.swift @@ -113,7 +113,7 @@ public protocol _AttachableByAddressAsIWICBitmapSource { /// you have an image in another format that needs to be attached to a test, /// first convert it to an instance of one of the types above. @_spi(Experimental) -public protocol AttachableAsIWICBitmapSource: SendableMetatype { +public protocol AttachableAsIWICBitmapSource: _AttachableAsImage, SendableMetatype { /// Create a WIC bitmap source representing an instance of this type. /// /// - Returns: A pointer to a new WIC bitmap source representing this image. @@ -145,39 +145,6 @@ public protocol AttachableAsIWICBitmapSource: SendableMetatype { func _copyAttachableIWICBitmapSource( using factory: UnsafeMutablePointer ) throws -> UnsafeMutablePointer - - /// Make a copy of this instance. - /// - /// - Returns: A copy of `self`, or `self` if this type does not support a - /// copying operation. - /// - /// The testing library uses this function to take ownership of image - /// resources that test authors pass to it. If possible, make a copy of or add - /// a reference to `self`. If this type does not support making copies, return - /// `self` verbatim. - /// - /// The default implementation of this function when `Self` conforms to - /// `Sendable` simply returns `self`. - /// - /// This function is not part of the public interface of the testing library. - /// It may be removed in a future update. - func _copyAttachableValue() -> Self - - /// Manually deinitialize any resources associated with this image. - /// - /// The implementation of this function cleans up any resources (such as - /// handles or COM objects) associated with this image. The testing library - /// automatically invokes this function as needed. - /// - /// This function is not responsible for releasing the image returned from - /// `_copyAttachableIWICBitmapSource(using:)`. - /// - /// The default implementation of this function when `Self` conforms to - /// `Sendable` does nothing. - /// - /// This function is not part of the public interface of the testing library. - /// It may be removed in a future update. - func _deinitializeAttachableValue() } extension AttachableAsIWICBitmapSource { @@ -187,12 +154,4 @@ extension AttachableAsIWICBitmapSource { try copyAttachableIWICBitmapSource() } } - -extension AttachableAsIWICBitmapSource where Self: Sendable { - public func _copyAttachableValue() -> Self { - self - } - - public func _deinitializeAttachableValue() {} -} #endif diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/Attachment+AttachableAsIWICBitmapSource.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/Attachment+AttachableAsIWICBitmapSource.swift deleted file mode 100644 index 8068e7a8e..000000000 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/Attachment+AttachableAsIWICBitmapSource.swift +++ /dev/null @@ -1,107 +0,0 @@ -// -// 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 -// - -#if os(Windows) -@_spi(Experimental) public import Testing - -@_spi(Experimental) -extension Attachment { - /// Initialize an instance of this type that encloses the given image. - /// - /// - Parameters: - /// - image: A pointer to the value that will be attached to the output of - /// the test run. - /// - preferredName: The preferred name of the attachment when writing it - /// to a test report or to disk. If `nil`, the testing library attempts - /// to derive a reasonable filename for the attached value. - /// - imageFormat: The image format with which to encode `image`. - /// - sourceLocation: The source location of the call to this initializer. - /// This value is used when recording issues associated with the - /// attachment. - /// - /// You can attach instances of the following system-provided image types to a - /// test: - /// - /// | Platform | Supported Types | - /// |-|-| - /// | macOS | [`CGImage`](https://developer.apple.com/documentation/coregraphics/cgimage), [`CIImage`](https://developer.apple.com/documentation/coreimage/ciimage), [`NSImage`](https://developer.apple.com/documentation/appkit/nsimage) | - /// | iOS, watchOS, tvOS, and visionOS | [`CGImage`](https://developer.apple.com/documentation/coregraphics/cgimage), [`CIImage`](https://developer.apple.com/documentation/coreimage/ciimage), [`UIImage`](https://developer.apple.com/documentation/uikit/uiimage) | - /// | Windows | [`HBITMAP`](https://learn.microsoft.com/en-us/windows/win32/gdi/bitmaps), [`HICON`](https://learn.microsoft.com/en-us/windows/win32/menurc/icons), [`IWICBitmapSource`](https://learn.microsoft.com/en-us/windows/win32/api/wincodec/nn-wincodec-iwicbitmapsource) (including its subclasses declared by Windows Imaging Component) | - /// - /// The testing library uses the image format specified by `imageFormat`. Pass - /// `nil` to let the testing library decide which image format to use. If you - /// pass `nil`, then the image format that the testing library uses depends on - /// the path extension you specify in `preferredName`, if any. If you do not - /// specify a path extension, or if the path extension you specify doesn't - /// correspond to an image format the operating system knows how to write, the - /// testing library selects an appropriate image format for you. - public init( - _ image: T, - named preferredName: String? = nil, - as imageFormat: AttachableImageFormat? = nil, - sourceLocation: SourceLocation = #_sourceLocation - ) where T: AttachableAsIWICBitmapSource, AttachableValue == _AttachableImageWrapper { - let imageWrapper = _AttachableImageWrapper( - image: image._copyAttachableValue(), - imageFormat: imageFormat, - deinitializingWith: { $0._deinitializeAttachableValue() } - ) - self.init(imageWrapper, named: preferredName, sourceLocation: sourceLocation) - } - - /// Attach an image to the current test. - /// - /// - Parameters: - /// - image: The value to attach. - /// - preferredName: The preferred name of the attachment when writing it - /// to a test report or to disk. If `nil`, the testing library attempts - /// to derive a reasonable filename for the attached value. - /// - imageFormat: The image format with which to encode `image`. - /// - sourceLocation: The source location of the call to this initializer. - /// This value is used when recording issues associated with the - /// attachment. - /// - /// This function creates a new instance of ``Attachment`` wrapping `image` - /// and immediately attaches it to the current test. You can attach instances - /// of the following system-provided image types to a test: - /// - /// | Platform | Supported Types | - /// |-|-| - /// | macOS | [`CGImage`](https://developer.apple.com/documentation/coregraphics/cgimage), [`CIImage`](https://developer.apple.com/documentation/coreimage/ciimage), [`NSImage`](https://developer.apple.com/documentation/appkit/nsimage) | - /// | iOS, watchOS, tvOS, and visionOS | [`CGImage`](https://developer.apple.com/documentation/coregraphics/cgimage), [`CIImage`](https://developer.apple.com/documentation/coreimage/ciimage), [`UIImage`](https://developer.apple.com/documentation/uikit/uiimage) | - /// | Windows | [`HBITMAP`](https://learn.microsoft.com/en-us/windows/win32/gdi/bitmaps), [`HICON`](https://learn.microsoft.com/en-us/windows/win32/menurc/icons), [`IWICBitmapSource`](https://learn.microsoft.com/en-us/windows/win32/api/wincodec/nn-wincodec-iwicbitmapsource) (including its subclasses declared by Windows Imaging Component) | - /// - /// The testing library uses the image format specified by `imageFormat`. Pass - /// `nil` to let the testing library decide which image format to use. If you - /// pass `nil`, then the image format that the testing library uses depends on - /// the path extension you specify in `preferredName`, if any. If you do not - /// specify a path extension, or if the path extension you specify doesn't - /// correspond to an image format the operating system knows how to write, the - /// testing library selects an appropriate image format for you. - public static func record( - _ image: T, - named preferredName: String? = nil, - as imageFormat: AttachableImageFormat? = nil, - sourceLocation: SourceLocation = #_sourceLocation - ) where T: AttachableAsIWICBitmapSource, AttachableValue == _AttachableImageWrapper { - let attachment = Self(image, named: preferredName, as: imageFormat, sourceLocation: sourceLocation) - Self.record(attachment, sourceLocation: sourceLocation) - } -} - -@_spi(Experimental) -extension Attachment where AttachableValue: AttachableWrapper, AttachableValue.Wrapped: AttachableAsIWICBitmapSource { - /// The image format to use when encoding the represented image. - @_disfavoredOverload public var imageFormat: AttachableImageFormat? { - // FIXME: no way to express `where AttachableValue == _AttachableImageWrapper` on a property (see rdar://47559973) - (attachableValue as? _AttachableImageWrapper)?.imageFormat - } -} -#endif diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/UnsafeMutablePointer+AttachableAsIWICBitmapSource.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/UnsafeMutablePointer+AttachableAsIWICBitmapSource.swift index 297e1f25a..a8b0a6312 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/UnsafeMutablePointer+AttachableAsIWICBitmapSource.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/UnsafeMutablePointer+AttachableAsIWICBitmapSource.swift @@ -13,7 +13,7 @@ private import Testing public import WinSDK @_spi(Experimental) -extension UnsafeMutablePointer: AttachableAsIWICBitmapSource where Pointee: _AttachableByAddressAsIWICBitmapSource { +extension UnsafeMutablePointer: _AttachableAsImage, AttachableAsIWICBitmapSource where Pointee: _AttachableByAddressAsIWICBitmapSource { public func copyAttachableIWICBitmapSource() throws -> UnsafeMutablePointer { let factory = try IWICImagingFactory.create() defer { @@ -30,7 +30,7 @@ extension UnsafeMutablePointer: AttachableAsIWICBitmapSource where Pointee: _Att Pointee._copyAttachableValue(at: self) } - public consuming func _deinitializeAttachableValue() { + public func _deinitializeAttachableValue() { Pointee._deinitializeAttachableValue(at: self) } } diff --git a/Sources/Overlays/_Testing_WinSDK/CMakeLists.txt b/Sources/Overlays/_Testing_WinSDK/CMakeLists.txt index a1df6bd60..1b56f0a8d 100644 --- a/Sources/Overlays/_Testing_WinSDK/CMakeLists.txt +++ b/Sources/Overlays/_Testing_WinSDK/CMakeLists.txt @@ -11,7 +11,6 @@ if (CMAKE_SYSTEM_NAME STREQUAL "Windows") Attachments/_AttachableImageWrapper+AttachableWrapper.swift Attachments/AttachableAsIWICBitmapSource.swift Attachments/AttachableImageFormat+CLSID.swift - Attachments/Attachment+AttachableAsIWICBitmapSource.swift Attachments/HBITMAP+AttachableAsIWICBitmapSource.swift Attachments/HICON+AttachableAsIWICBitmapSource.swift Attachments/IWICBitmapSource+AttachableAsIWICBitmapSource.swift diff --git a/Sources/Overlays/_Testing_CoreGraphics/Attachments/Attachment+AttachableAsCGImage.swift b/Sources/Testing/Attachments/Images/Attachment+_AttachableAsImage.swift similarity index 93% rename from Sources/Overlays/_Testing_CoreGraphics/Attachments/Attachment+AttachableAsCGImage.swift rename to Sources/Testing/Attachments/Images/Attachment+_AttachableAsImage.swift index 65d90b11a..6ac3ccc29 100644 --- a/Sources/Overlays/_Testing_CoreGraphics/Attachments/Attachment+AttachableAsCGImage.swift +++ b/Sources/Testing/Attachments/Images/Attachment+_AttachableAsImage.swift @@ -8,9 +8,6 @@ // See https://swift.org/CONTRIBUTORS.txt for Swift project authors // -#if SWT_TARGET_OS_APPLE && canImport(CoreGraphics) -public import Testing - @available(_uttypesAPI, *) extension Attachment { /// Initialize an instance of this type that encloses the given image. @@ -52,12 +49,8 @@ extension Attachment { named preferredName: String? = nil, as imageFormat: AttachableImageFormat? = nil, sourceLocation: SourceLocation = #_sourceLocation - ) where T: AttachableAsCGImage, AttachableValue == _AttachableImageWrapper { - let imageWrapper = _AttachableImageWrapper( - image: image._copyAttachableValue(), - imageFormat: imageFormat, - deinitializingWith: { _ in } - ) + ) where AttachableValue: _AttachableImageWrapper & AttachableWrapper { + let imageWrapper = AttachableValue(image: image, imageFormat: imageFormat) self.init(imageWrapper, named: preferredName, sourceLocation: sourceLocation) } @@ -99,7 +92,7 @@ extension Attachment { named preferredName: String? = nil, as imageFormat: AttachableImageFormat? = nil, sourceLocation: SourceLocation = #_sourceLocation - ) where T: AttachableAsCGImage, AttachableValue == _AttachableImageWrapper { + ) where AttachableValue: _AttachableImageWrapper & AttachableWrapper { let attachment = Self(image, named: preferredName, as: imageFormat, sourceLocation: sourceLocation) Self.record(attachment, sourceLocation: sourceLocation) } @@ -109,11 +102,10 @@ extension Attachment { @_spi(Experimental) // STOP: not part of ST-0014 @available(_uttypesAPI, *) -extension Attachment where AttachableValue: AttachableWrapper, AttachableValue.Wrapped: AttachableAsCGImage { +extension Attachment where AttachableValue: AttachableWrapper, AttachableValue.Wrapped: _AttachableAsImage { /// The image format to use when encoding the represented image. @_disfavoredOverload public var imageFormat: AttachableImageFormat? { // FIXME: no way to express `where AttachableValue == _AttachableImageWrapper` on a property (see rdar://47559973) (attachableValue as? _AttachableImageWrapper)?.imageFormat } } -#endif diff --git a/Sources/Testing/Attachments/Images/_AttachableAsImage.swift b/Sources/Testing/Attachments/Images/_AttachableAsImage.swift new file mode 100644 index 000000000..6954600eb --- /dev/null +++ b/Sources/Testing/Attachments/Images/_AttachableAsImage.swift @@ -0,0 +1,54 @@ +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024–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 +// + +/// A protocol describing images that can be converted to instances of +/// [`Attachment`](https://developer.apple.com/documentation/testing/attachment). +/// +/// This protocol acts as an abstract, platform-independent base protocol for +/// ``AttachableAsCGImage`` and ``AttachableAsIWICBitmapSource``. +/// +/// @Comment { +/// A future Swift Evolution proposal will promote this protocol to API so +/// that we don't need to underscore its name. +/// } +@available(_uttypesAPI, *) +public protocol _AttachableAsImage: SendableMetatype { + /// Make a copy of this instance to pass to an attachment. + /// + /// - Returns: A copy of `self`, or `self` if no copy is needed. + /// + /// The testing library uses this function to take ownership of image + /// resources that test authors pass to it. If possible, make a copy of or add + /// a reference to `self`. If this type does not support making copies, return + /// `self` verbatim. + /// + /// The default implementation of this function when `Self` conforms to + /// `Sendable` simply returns `self`. + /// + /// This function is not part of the public interface of the testing library. + /// It may be removed in a future update. + func _copyAttachableValue() -> Self + + /// Manually deinitialize any resources associated with this image. + /// + /// The implementation of this function cleans up any resources (such as + /// handles or COM objects) associated with this image. The testing library + /// automatically invokes this function as needed. + /// + /// This function is not responsible for releasing the image returned from + /// `_copyAttachableIWICBitmapSource(using:)`. + /// + /// The default implementation of this function when `Self` conforms to + /// `Sendable` does nothing. + /// + /// This function is not part of the public interface of the testing library. + /// It may be removed in a future update. + func _deinitializeAttachableValue() +} diff --git a/Sources/Testing/Attachments/Images/_AttachableImageWrapper.swift b/Sources/Testing/Attachments/Images/_AttachableImageWrapper.swift index 25e102677..4fe2e84a6 100644 --- a/Sources/Testing/Attachments/Images/_AttachableImageWrapper.swift +++ b/Sources/Testing/Attachments/Images/_AttachableImageWrapper.swift @@ -21,24 +21,20 @@ /// | Windows | [`HBITMAP`](https://learn.microsoft.com/en-us/windows/win32/gdi/bitmaps), [`HICON`](https://learn.microsoft.com/en-us/windows/win32/menurc/icons), [`IWICBitmapSource`](https://learn.microsoft.com/en-us/windows/win32/api/wincodec/nn-wincodec-iwicbitmapsource) (including its subclasses declared by Windows Imaging Component) | /// } @available(_uttypesAPI, *) -public final class _AttachableImageWrapper: Sendable { +public final class _AttachableImageWrapper: Sendable where Image: _AttachableAsImage { /// The underlying image. private nonisolated(unsafe) let _image: Image /// The image format to use when encoding the represented image. package let imageFormat: AttachableImageFormat? - /// A deinitializer function to call to clean up `image`. - private let _deinit: @Sendable (consuming Image) -> Void - - package init(image: Image, imageFormat: AttachableImageFormat?, deinitializingWith `deinit`: @escaping @Sendable (consuming Image) -> Void) { - self._image = image + init(image: Image, imageFormat: AttachableImageFormat?) { + self._image = image._copyAttachableValue() self.imageFormat = imageFormat - self._deinit = `deinit` } deinit { - _deinit(_image) + _image._deinitializeAttachableValue() } } diff --git a/Sources/Testing/CMakeLists.txt b/Sources/Testing/CMakeLists.txt index a88dd4084..9776f70d3 100644 --- a/Sources/Testing/CMakeLists.txt +++ b/Sources/Testing/CMakeLists.txt @@ -21,8 +21,10 @@ add_library(Testing ABI/Encoded/ABI.EncodedIssue.swift ABI/Encoded/ABI.EncodedMessage.swift ABI/Encoded/ABI.EncodedTest.swift + Attachments/Images/_AttachableAsImage.swift Attachments/Images/_AttachableImageWrapper.swift Attachments/Images/AttachableImageFormat.swift + Attachments/Images/Attachment+_AttachableAsImage.swift Attachments/Images/ImageAttachmentError.swift Attachments/Attachable.swift Attachments/AttachableWrapper.swift From 3a8299423225832853cd441fb1c2c37c61b60db0 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Tue, 23 Sep 2025 08:45:34 -0400 Subject: [PATCH 151/216] Change the semantics of `Test.cancel()`. (#1332) This PR changes the semantics of `Test.cancel()` such that it only cancels the current test case if the current test is parameterized. This PR also removes `Test.Case.cancel()`, but we may opt to add `Test.Case.cancelAll()` or some such in the future to re-add an interface that cancels an entire parameterized test. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- Sources/Testing/ExitTests/ExitTest.swift | 4 +- .../Testing/Running/Runner.RuntimeState.swift | 1 + Sources/Testing/Test+Cancellation.swift | 145 ++++++------------ .../TestingTests/TestCancellationTests.swift | 35 +---- 4 files changed, 52 insertions(+), 133 deletions(-) diff --git a/Sources/Testing/ExitTests/ExitTest.swift b/Sources/Testing/ExitTests/ExitTest.swift index 4cabda8b3..ef37c4bd9 100644 --- a/Sources/Testing/ExitTests/ExitTest.swift +++ b/Sources/Testing/ExitTests/ExitTest.swift @@ -762,7 +762,7 @@ extension ExitTest { } configuration.eventHandler = { event, eventContext in switch event.kind { - case .issueRecorded, .valueAttached, .testCancelled, .testCaseCancelled: + case .issueRecorded, .valueAttached, .testCancelled: eventHandler(event, eventContext) default: // Don't forward other kinds of event. @@ -1070,8 +1070,6 @@ extension ExitTest { Attachment.record(attachment, sourceLocation: event._sourceLocation!) } else if case .testCancelled = event.kind { _ = try? Test.cancel(with: skipInfo) - } else if case .testCaseCancelled = event.kind { - _ = try? Test.Case.cancel(with: skipInfo) } } diff --git a/Sources/Testing/Running/Runner.RuntimeState.swift b/Sources/Testing/Running/Runner.RuntimeState.swift index e88cea60b..6826be7a4 100644 --- a/Sources/Testing/Running/Runner.RuntimeState.swift +++ b/Sources/Testing/Running/Runner.RuntimeState.swift @@ -206,6 +206,7 @@ extension Test { static func withCurrent(_ test: Self, perform body: () async throws -> R) async rethrows -> R { var runtimeState = Runner.RuntimeState.current ?? .init() runtimeState.test = test + runtimeState.testCase = nil return try await Runner.RuntimeState.$current.withValue(runtimeState) { try await test.withCancellationHandling(body) } diff --git a/Sources/Testing/Test+Cancellation.swift b/Sources/Testing/Test+Cancellation.swift index 7b8f9e88d..5a4b425c7 100644 --- a/Sources/Testing/Test+Cancellation.swift +++ b/Sources/Testing/Test+Cancellation.swift @@ -13,19 +13,6 @@ /// This protocol is used to abstract away the common implementation of test and /// test case cancellation. protocol TestCancellable: Sendable { - /// Cancel the current instance of this type. - /// - /// - Parameters: - /// - skipInfo: Information about the cancellation event. - /// - /// - Throws: An error indicating that the current instance of this type has - /// been cancelled. - /// - /// Note that the public ``Test/cancel(_:sourceLocation:)`` function has a - /// different signature and accepts a source location rather than a source - /// context value. - static func cancel(with skipInfo: SkipInfo) throws -> Never - /// Make an instance of ``Event/Kind`` appropriate for an instance of this /// type. /// @@ -108,9 +95,8 @@ extension TestCancellable { } onCancel: { // The current task was cancelled, so cancel the test case or test // associated with it. - let skipInfo = _currentSkipInfo ?? SkipInfo(sourceContext: SourceContext(backtrace: .current(), sourceLocation: nil)) - _ = try? Self.cancel(with: skipInfo) + _ = try? Test.cancel(with: skipInfo) } } } @@ -125,9 +111,7 @@ extension TestCancellable { /// is set and we need fallback handling. /// - testAndTestCase: The test and test case to use when posting an event. /// - skipInfo: Information about the cancellation event. -/// -/// - Throws: An instance of ``SkipInfo`` describing the cancellation. -private func _cancel(_ cancellableValue: T?, for testAndTestCase: (Test?, Test.Case?), skipInfo: SkipInfo) throws -> Never where T: TestCancellable { +private func _cancel(_ cancellableValue: T?, for testAndTestCase: (Test?, Test.Case?), skipInfo: SkipInfo) where T: TestCancellable { if cancellableValue != nil { // If the current test case is still running, take its task property (which // signals to subsequent callers that it has been cancelled.) @@ -171,26 +155,25 @@ private func _cancel(_ cancellableValue: T?, for testAndTestCase: (Test?, Tes issue.record() } } - - throw skipInfo } // MARK: - Test cancellation extension Test: TestCancellable { - /// Cancel the current test. + /// Cancel the current test or test case. /// /// - Parameters: - /// - comment: A comment describing why you are cancelling the test. + /// - comment: A comment describing why you are cancelling the test or test + /// case. /// - sourceLocation: The source location to which the testing library will /// attribute the cancellation. /// - /// - Throws: An error indicating that the current test case has been + /// - Throws: An error indicating that the current test or test case has been /// cancelled. /// - /// The testing library runs each test in its own task. When you call this - /// function, the testing library cancels the task associated with the current - /// test: + /// The testing library runs each test and each test case in its own task. + /// When you call this function, the testing library cancels the task + /// associated with the current test: /// /// ```swift /// @Test func `Food truck is well-stocked`() throws { @@ -201,11 +184,17 @@ extension Test: TestCancellable { /// } /// ``` /// - /// If the current test is parameterized, all of its pending and running test - /// cases are cancelled. If the current test is a suite, all of its pending - /// and running tests are cancelled. If you have already cancelled the current - /// test or if it has already finished running, this function throws an error - /// but does not attempt to cancel the test a second time. + /// If the current test is a parameterized test function, this function + /// instead cancels the current test case. Other test cases in the test + /// function are not affected. + /// + /// If the current test is a suite, the testing library cancels all of its + /// pending and running tests. + /// + /// If you have already cancelled the current test or if it has already + /// finished running, this function throws an error to indicate that the + /// current test has been cancelled, but does not attempt to cancel the test a + /// second time. /// /// @Comment { /// TODO: Document the interaction between an exit test and test @@ -217,89 +206,53 @@ extension Test: TestCancellable { /// - Important: If the current task is not associated with a test (for /// example, because it was created with [`Task.detached(name:priority:operation:)`](https://developer.apple.com/documentation/swift/task/detached(name:priority:operation:)-795w1)) /// this function records an issue and cancels the current task. - /// - /// To cancel the current test case but leave other test cases of the current - /// test alone, call ``Test/Case/cancel(_:sourceLocation:)`` instead. @_spi(Experimental) public static func cancel(_ comment: Comment? = nil, sourceLocation: SourceLocation = #_sourceLocation) throws -> Never { let skipInfo = SkipInfo(comment: comment, sourceContext: SourceContext(backtrace: nil, sourceLocation: sourceLocation)) try Self.cancel(with: skipInfo) } - static func cancel(with skipInfo: SkipInfo) throws -> Never { - let test = Test.current - try _cancel(test, for: (test, nil), skipInfo: skipInfo) - } - - static func makeCancelledEventKind(with skipInfo: SkipInfo) -> Event.Kind { - .testCancelled(skipInfo) - } -} - -// MARK: - Test case cancellation - -extension Test.Case: TestCancellable { - /// Cancel the current test case. + /// Cancel the current test or test case. /// /// - Parameters: - /// - comment: A comment describing why you are cancelling the test case. - /// - sourceLocation: The source location to which the testing library will - /// attribute the cancellation. + /// - skipInfo: Information about the cancellation event. /// - /// - Throws: An error indicating that the current test case has been + /// - Throws: An error indicating that the current test or test case has been /// cancelled. /// - /// The testing library runs each test case of a test in its own task. When - /// you call this function, the testing library cancels the task associated - /// with the current test case: - /// - /// ```swift - /// @Test(arguments: [Food.burger, .fries, .iceCream]) - /// func `Food truck is well-stocked`(_ food: Food) throws { - /// if food == .iceCream && Season.current == .winter { - /// try Test.Case.cancel("It's too cold for ice cream.") - /// } - /// // ... - /// } - /// ``` - /// - /// If the current test is parameterized, the test's other test cases continue - /// running. If the current test case has already been cancelled, this - /// function throws an error but does not attempt to cancel the test case a - /// second time. - /// - /// @Comment { - /// TODO: Document the interaction between an exit test and test - /// cancellation. In particular, the error thrown by this function isn't - /// thrown into the parent process and task cancellation doesn't propagate - /// (because the exit test _de facto_ runs in a detached task.) - /// } - /// - /// - Important: If the current task is not associated with a test case (for - /// example, because it was created with [`Task.detached(name:priority:operation:)`](https://developer.apple.com/documentation/swift/task/detached(name:priority:operation:)-795w1)) - /// this function records an issue and cancels the current task. - /// - /// To cancel all test cases in the current test, call - /// ``Test/cancel(_:sourceLocation:)`` instead. - @_spi(Experimental) - public static func cancel(_ comment: Comment? = nil, sourceLocation: SourceLocation = #_sourceLocation) throws -> Never { - let skipInfo = SkipInfo(comment: comment, sourceContext: SourceContext(backtrace: nil, sourceLocation: sourceLocation)) - try Self.cancel(with: skipInfo) - } - + /// Note that the public ``Test/cancel(_:sourceLocation:)`` function has a + /// different signature and accepts a source location rather than an instance + /// of ``SkipInfo``. static func cancel(with skipInfo: SkipInfo) throws -> Never { let test = Test.current let testCase = Test.Case.current - do { - // Cancel the current test case (if it's nil, that's the API misuse path.) - try _cancel(testCase, for: (test, testCase), skipInfo: skipInfo) - } catch _ where test?.isParameterized == false { - // The current test is not parameterized, so cancel the whole test too. - try _cancel(test, for: (test, nil), skipInfo: skipInfo) + if let testCase { + // Cancel the current test case. + _cancel(testCase, for: (test, testCase), skipInfo: skipInfo) } + + if let test { + if !test.isParameterized { + // The current test is not parameterized, so cancel the whole test too. + _cancel(test, for: (test, nil), skipInfo: skipInfo) + } + } else { + // There is no current test (this is the API misuse path.) + _cancel(test, for: (test, nil), skipInfo: skipInfo) + } + + throw skipInfo } + static func makeCancelledEventKind(with skipInfo: SkipInfo) -> Event.Kind { + .testCancelled(skipInfo) + } +} + +// MARK: - Test case cancellation + +extension Test.Case: TestCancellable { static func makeCancelledEventKind(with skipInfo: SkipInfo) -> Event.Kind { .testCaseCancelled(skipInfo) } diff --git a/Tests/TestingTests/TestCancellationTests.swift b/Tests/TestingTests/TestCancellationTests.swift index a4f95fe56..06c1375a5 100644 --- a/Tests/TestingTests/TestCancellationTests.swift +++ b/Tests/TestingTests/TestCancellationTests.swift @@ -60,32 +60,11 @@ } } - @Test func `Cancelling a non-parameterized test via Test.Case.cancel()`() async { - await testCancellation(testCancelled: 1, testCaseCancelled: 1) { configuration in - await Test { - try Test.Case.cancel("Cancelled test") - }.run(configuration: configuration) - } - } - @Test func `Cancelling a test case in a parameterized test`() async { await testCancellation(testCaseCancelled: 5, issueRecorded: 5) { configuration in await Test(arguments: 0 ..< 10) { i in if (i % 2) == 0 { - try Test.Case.cancel("\(i) is even!") - } - Issue.record("\(i) records an issue!") - }.run(configuration: configuration) - } - } - - @Test func `Cancelling an entire parameterized test`() async { - await testCancellation(testCancelled: 1, testCaseCancelled: 10) { configuration in - // .serialized to ensure that none of the cases complete before the first - // one cancels the test. - await Test(.serialized, arguments: 0 ..< 10) { i in - if i == 0 { - try Test.cancel("\(i) cancelled the test") + try Test.cancel("\(i) is even!") } Issue.record("\(i) records an issue!") }.run(configuration: configuration) @@ -183,18 +162,6 @@ } } - @Test func `Cancelling the current test case from within an exit test`() async { - await testCancellation(testCancelled: 1, testCaseCancelled: 1) { configuration in - await Test { - await #expect(processExitsWith: .success) { - try Test.Case.cancel("Cancelled test") - } - #expect(Task.isCancelled) - try Task.checkCancellation() - }.run(configuration: configuration) - } - } - @Test func `Cancelling the current task in an exit test doesn't cancel the test`() async { await testCancellation(testCancelled: 0, testCaseCancelled: 0) { configuration in await Test { From 894fc62620cbc0fd0f95c8f73b5a57f47b28c3ed Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Tue, 23 Sep 2025 12:01:33 -0400 Subject: [PATCH 152/216] Mark image attachments as unavailable on non-Darwin/non-Windows. (#1330) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR explicitly marks the image attachments API bits in the core Swift Testing library as unavailable on e.g. Linux. Developers who attempt to use this API on Linux will get diagnostics such as: > 🛑 'AttachableImageFormat' is unavailable: Image attachments are not available on this platform. Blocked on #1329. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- .github/workflows/pull_request.yml | 2 +- Package.swift | 1 + .../Attachments/Images/AttachableImageFormat.swift | 6 ++++++ .../Images/Attachment+_AttachableAsImage.swift | 6 ++++++ .../Attachments/Images/ImageAttachmentError.swift | 6 ++++++ .../Attachments/Images/_AttachableAsImage.swift | 11 +++++++++++ .../Attachments/Images/_AttachableImageWrapper.swift | 6 ++++++ cmake/modules/shared/CompilerSettings.cmake | 3 +++ 8 files changed, 40 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 41abeb44d..1435fa472 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -24,6 +24,6 @@ jobs: uses: swiftlang/github-workflows/.github/workflows/soundness.yml@main with: license_header_check_project_name: "Swift" - docs_check_container_image: "swift:6.2-noble" + docs_check_enabled: false format_check_enabled: false api_breakage_check_enabled: false diff --git a/Package.swift b/Package.swift index 0f4b1648b..e910a67b8 100644 --- a/Package.swift +++ b/Package.swift @@ -383,6 +383,7 @@ extension Array where Element == PackageDescription.SwiftSetting { .define("SWT_NO_DYNAMIC_LINKING", .whenEmbedded(or: .when(platforms: [.wasi]))), .define("SWT_NO_PIPES", .whenEmbedded(or: .when(platforms: [.wasi]))), .define("SWT_NO_FOUNDATION_FILE_COORDINATION", .whenEmbedded(or: .whenApple(false))), + .define("SWT_NO_IMAGE_ATTACHMENTS", .whenEmbedded(or: .when(platforms: [.linux, .custom("freebsd"), .openbsd, .wasi, .android]))), .define("SWT_NO_LEGACY_TEST_DISCOVERY", .whenEmbedded()), .define("SWT_NO_LIBDISPATCH", .whenEmbedded()), diff --git a/Sources/Testing/Attachments/Images/AttachableImageFormat.swift b/Sources/Testing/Attachments/Images/AttachableImageFormat.swift index 9bf6b50f8..a5f71a01d 100644 --- a/Sources/Testing/Attachments/Images/AttachableImageFormat.swift +++ b/Sources/Testing/Attachments/Images/AttachableImageFormat.swift @@ -31,6 +31,9 @@ /// @Metadata { /// @Available(Swift, introduced: 6.3) /// } +#if SWT_NO_IMAGE_ATTACHMENTS +@available(*, unavailable, message: "Image attachments are not available on this platform.") +#endif @available(_uttypesAPI, *) public struct AttachableImageFormat: Sendable { /// An enumeration describing the various kinds of image format that can be @@ -77,6 +80,9 @@ public struct AttachableImageFormat: Sendable { // MARK: - +#if SWT_NO_IMAGE_ATTACHMENTS +@available(*, unavailable, message: "Image attachments are not available on this platform.") +#endif @available(_uttypesAPI, *) extension AttachableImageFormat { /// The PNG image format. diff --git a/Sources/Testing/Attachments/Images/Attachment+_AttachableAsImage.swift b/Sources/Testing/Attachments/Images/Attachment+_AttachableAsImage.swift index 6ac3ccc29..a6bda8ad9 100644 --- a/Sources/Testing/Attachments/Images/Attachment+_AttachableAsImage.swift +++ b/Sources/Testing/Attachments/Images/Attachment+_AttachableAsImage.swift @@ -8,6 +8,9 @@ // See https://swift.org/CONTRIBUTORS.txt for Swift project authors // +#if SWT_NO_IMAGE_ATTACHMENTS +@available(*, unavailable, message: "Image attachments are not available on this platform.") +#endif @available(_uttypesAPI, *) extension Attachment { /// Initialize an instance of this type that encloses the given image. @@ -101,6 +104,9 @@ extension Attachment { // MARK: - @_spi(Experimental) // STOP: not part of ST-0014 +#if SWT_NO_IMAGE_ATTACHMENTS +@available(*, unavailable, message: "Image attachments are not available on this platform.") +#endif @available(_uttypesAPI, *) extension Attachment where AttachableValue: AttachableWrapper, AttachableValue.Wrapped: _AttachableAsImage { /// The image format to use when encoding the represented image. diff --git a/Sources/Testing/Attachments/Images/ImageAttachmentError.swift b/Sources/Testing/Attachments/Images/ImageAttachmentError.swift index 1bd90b641..47b0ba91e 100644 --- a/Sources/Testing/Attachments/Images/ImageAttachmentError.swift +++ b/Sources/Testing/Attachments/Images/ImageAttachmentError.swift @@ -11,6 +11,9 @@ private import _TestingInternals /// A type representing an error that can occur when attaching an image. +#if SWT_NO_IMAGE_ATTACHMENTS +@available(*, unavailable, message: "Image attachments are not available on this platform.") +#endif package enum ImageAttachmentError: Error { #if SWT_TARGET_OS_APPLE /// The image could not be converted to an instance of `CGImage`. @@ -39,6 +42,9 @@ package enum ImageAttachmentError: Error { #endif } +#if SWT_NO_IMAGE_ATTACHMENTS +@available(*, unavailable, message: "Image attachments are not available on this platform.") +#endif extension ImageAttachmentError: CustomStringConvertible { package var description: String { #if SWT_TARGET_OS_APPLE diff --git a/Sources/Testing/Attachments/Images/_AttachableAsImage.swift b/Sources/Testing/Attachments/Images/_AttachableAsImage.swift index 6954600eb..4ee2f66cb 100644 --- a/Sources/Testing/Attachments/Images/_AttachableAsImage.swift +++ b/Sources/Testing/Attachments/Images/_AttachableAsImage.swift @@ -8,6 +8,14 @@ // See https://swift.org/CONTRIBUTORS.txt for Swift project authors // +#if SWT_TARGET_OS_APPLE +// Image attachments on Apple platforms conform to AttachableAsCGImage. +#elseif os(Windows) +// Image attachments on Windows platforms conform to AttachableAsIWICBitmapSource. +#elseif !SWT_NO_IMAGE_ATTACHMENTS +#error("Platform-specific misconfiguration: support for image attachments requires a platform-specific implementation") +#endif + /// A protocol describing images that can be converted to instances of /// [`Attachment`](https://developer.apple.com/documentation/testing/attachment). /// @@ -18,6 +26,9 @@ /// A future Swift Evolution proposal will promote this protocol to API so /// that we don't need to underscore its name. /// } +#if SWT_NO_IMAGE_ATTACHMENTS +@available(*, unavailable, message: "Image attachments are not available on this platform.") +#endif @available(_uttypesAPI, *) public protocol _AttachableAsImage: SendableMetatype { /// Make a copy of this instance to pass to an attachment. diff --git a/Sources/Testing/Attachments/Images/_AttachableImageWrapper.swift b/Sources/Testing/Attachments/Images/_AttachableImageWrapper.swift index 4fe2e84a6..4ce3576d9 100644 --- a/Sources/Testing/Attachments/Images/_AttachableImageWrapper.swift +++ b/Sources/Testing/Attachments/Images/_AttachableImageWrapper.swift @@ -20,6 +20,9 @@ /// @Comment { /// | Windows | [`HBITMAP`](https://learn.microsoft.com/en-us/windows/win32/gdi/bitmaps), [`HICON`](https://learn.microsoft.com/en-us/windows/win32/menurc/icons), [`IWICBitmapSource`](https://learn.microsoft.com/en-us/windows/win32/api/wincodec/nn-wincodec-iwicbitmapsource) (including its subclasses declared by Windows Imaging Component) | /// } +#if SWT_NO_IMAGE_ATTACHMENTS +@available(*, unavailable, message: "Image attachments are not available on this platform.") +#endif @available(_uttypesAPI, *) public final class _AttachableImageWrapper: Sendable where Image: _AttachableAsImage { /// The underlying image. @@ -38,6 +41,9 @@ public final class _AttachableImageWrapper: Sendable where Image: _Attach } } +#if SWT_NO_IMAGE_ATTACHMENTS +@available(*, unavailable, message: "Image attachments are not available on this platform.") +#endif @available(_uttypesAPI, *) extension _AttachableImageWrapper { public var wrappedValue: Image { diff --git a/cmake/modules/shared/CompilerSettings.cmake b/cmake/modules/shared/CompilerSettings.cmake index e5a4fcbf6..b3c0fe3aa 100644 --- a/cmake/modules/shared/CompilerSettings.cmake +++ b/cmake/modules/shared/CompilerSettings.cmake @@ -42,6 +42,9 @@ if(CMAKE_SYSTEM_NAME STREQUAL "WASI") add_compile_definitions("SWT_NO_DYNAMIC_LINKING") add_compile_definitions("SWT_NO_PIPES") endif() +if (NOT (APPLE OR CMAKE_SYSTEM_NAME STREQUAL "Windows")) + add_compile_definitions("SWT_NO_IMAGE_ATTACHMENTS") +endif() file(STRINGS "${SWT_SOURCE_ROOT_DIR}/VERSION.txt" SWT_TESTING_LIBRARY_VERSION LIMIT_COUNT 1) if(SWT_TESTING_LIBRARY_VERSION) From 0a38abb3d7071ac7ab8a58ad5d24ae6e3b339312 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Wed, 24 Sep 2025 08:50:17 -0400 Subject: [PATCH 153/216] Promote `SourceLocation._filePath` to proper, non-underscored SPI. (#1334) This PR introduces an `@_spi var filePath` property on `SourceLocation` with the intent of replacing the existing underscored/unsupported `_filePath` property. Because Xcode 16 through 26 directly encodes and decodes instances of `SourceLocation`, we can't just stop encoding/decoding the `_filePath` key and need to keep it there for at least a Swift release or two, until Apple's fork of Swift Testing has been updated to include the new encoding key. At that point we can remove the old one. (We'll need to introduce an `EncodedSourceLocation` structure at that point and keep including the underscored key for the v0 JSON stream, but that's not a "today" problem.) ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- .../SourceAttribution/SourceLocation.swift | 63 ++++++++++++++++--- Tests/TestingTests/SourceLocationTests.swift | 12 +++- 2 files changed, 67 insertions(+), 8 deletions(-) diff --git a/Sources/Testing/SourceAttribution/SourceLocation.swift b/Sources/Testing/SourceAttribution/SourceLocation.swift index 3aca54d2f..f5af81543 100644 --- a/Sources/Testing/SourceAttribution/SourceLocation.swift +++ b/Sources/Testing/SourceAttribution/SourceLocation.swift @@ -71,11 +71,8 @@ public struct SourceLocation: Sendable { } /// The path to the source file. - /// - /// - Warning: This property is provided temporarily to aid in integrating the - /// testing library with existing tools such as Swift Package Manager. It - /// will be removed in a future release. - public var _filePath: String + @_spi(Experimental) + public var filePath: String /// The line in the source file. /// @@ -118,7 +115,7 @@ public struct SourceLocation: Sendable { precondition(column > 0, "SourceLocation.column must be greater than 0 (was \(column))") self.fileID = fileID - self._filePath = filePath + self.filePath = filePath self.line = line self.column = column } @@ -167,4 +164,56 @@ extension SourceLocation: CustomStringConvertible, CustomDebugStringConvertible // MARK: - Codable -extension SourceLocation: Codable {} +extension SourceLocation: Codable { + private enum _CodingKeys: String, CodingKey { + case fileID + case filePath + case line + case column + + /// A backwards-compatible synonym of ``filePath``. + case _filePath + } + + public func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: _CodingKeys.self) + try container.encode(fileID, forKey: .fileID) + try container.encode(line, forKey: .line) + try container.encode(column, forKey: .column) + + // For backwards-compatibility, we must always encode "_filePath". + try container.encode(filePath, forKey: ._filePath) + try container.encode(filePath, forKey: .filePath) + } + + public init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: _CodingKeys.self) + fileID = try container.decode(String.self, forKey: .fileID) + line = try container.decode(Int.self, forKey: .line) + column = try container.decode(Int.self, forKey: .column) + + // For simplicity's sake, we won't be picky about which key contains the + // file path. + filePath = try container.decodeIfPresent(String.self, forKey: .filePath) + ?? container.decode(String.self, forKey: ._filePath) + } +} + +// MARK: - Deprecated + +extension SourceLocation { + /// The path to the source file. + /// + /// - Warning: This property is provided temporarily to aid in integrating the + /// testing library with existing tools such as Swift Package Manager. It + /// will be removed in a future release. + @available(swift, deprecated: 100000.0, renamed: "filePath") + public var _filePath: String { + get { + filePath + } + set { + filePath = newValue + } + } +} diff --git a/Tests/TestingTests/SourceLocationTests.swift b/Tests/TestingTests/SourceLocationTests.swift index 4145687b8..2f9dc9b7e 100644 --- a/Tests/TestingTests/SourceLocationTests.swift +++ b/Tests/TestingTests/SourceLocationTests.swift @@ -121,8 +121,18 @@ struct SourceLocationTests { } #endif - @Test("SourceLocation._filePath property") + @Test("SourceLocation.filePath property") func sourceLocationFilePath() { + var sourceLocation = #_sourceLocation + #expect(sourceLocation.filePath == #filePath) + + sourceLocation.filePath = "A" + #expect(sourceLocation.filePath == "A") + } + + @available(swift, deprecated: 100000.0) + @Test("SourceLocation._filePath property") + func sourceLocation_FilePath() { var sourceLocation = #_sourceLocation #expect(sourceLocation._filePath == #filePath) From 87f4168fc4e75fa2a8b41978908d078be645873d Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Wed, 24 Sep 2025 14:41:17 -0400 Subject: [PATCH 154/216] Fix a symbol marked `@_spi` that's needed for exit test value capturing. (#1335) I missed removing an `@_spi` attribute when enabling exit test value capturing for [ST-0012](https://github.com/swiftlang/swift-evolution/blob/main/proposals/testing/0012-exit-test-value-capturing.md). ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- Sources/Testing/Expectations/ExpectationChecking+Macro.swift | 1 - Tests/TestingTests/ExitTestTests.swift | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/Sources/Testing/Expectations/ExpectationChecking+Macro.swift b/Sources/Testing/Expectations/ExpectationChecking+Macro.swift index 3a190e679..b47dbd910 100644 --- a/Sources/Testing/Expectations/ExpectationChecking+Macro.swift +++ b/Sources/Testing/Expectations/ExpectationChecking+Macro.swift @@ -1175,7 +1175,6 @@ public func __checkClosureCall( /// /// - Warning: This function is used to implement the `#expect()` and /// `#require()` macros. Do not call it directly. -@_spi(Experimental) public func __checkClosureCall( identifiedBy exitTestID: (UInt64, UInt64, UInt64, UInt64), encodingCapturedValues capturedValues: (repeat each T), diff --git a/Tests/TestingTests/ExitTestTests.swift b/Tests/TestingTests/ExitTestTests.swift index 615ffcb74..5bcb2a05d 100644 --- a/Tests/TestingTests/ExitTestTests.swift +++ b/Tests/TestingTests/ExitTestTests.swift @@ -8,7 +8,7 @@ // See https://swift.org/CONTRIBUTORS.txt for Swift project authors // -@testable @_spi(Experimental) @_spi(ForToolsIntegrationOnly) import Testing +@testable @_spi(ForToolsIntegrationOnly) import Testing private import _TestingInternals #if !SWT_NO_EXIT_TESTS From ee0ba8cbf1cca0c4966b86d3706c11abfae59e19 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Thu, 25 Sep 2025 11:35:55 -0400 Subject: [PATCH 155/216] Remove `@_spi(Experimental)` from `ExitTest.CapturedValue`. (#1337) These symbols should be `@_spi(ForToolsIntegrationOnly)` but are no longer considered experimental. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- Sources/Testing/ExitTests/ExitTest.CapturedValue.swift | 2 +- Sources/Testing/ExitTests/ExitTest.Condition.swift | 1 - Sources/Testing/ExitTests/ExitTest.swift | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Sources/Testing/ExitTests/ExitTest.CapturedValue.swift b/Sources/Testing/ExitTests/ExitTest.CapturedValue.swift index 867fdecc5..c311978a8 100644 --- a/Sources/Testing/ExitTests/ExitTest.CapturedValue.swift +++ b/Sources/Testing/ExitTests/ExitTest.CapturedValue.swift @@ -10,7 +10,7 @@ private import _TestingInternals -@_spi(Experimental) @_spi(ForToolsIntegrationOnly) +@_spi(ForToolsIntegrationOnly) #if SWT_NO_EXIT_TESTS @available(*, unavailable, message: "Exit tests are not available on this platform.") #endif diff --git a/Sources/Testing/ExitTests/ExitTest.Condition.swift b/Sources/Testing/ExitTests/ExitTest.Condition.swift index 0a23bf47c..fd3f3680c 100644 --- a/Sources/Testing/ExitTests/ExitTest.Condition.swift +++ b/Sources/Testing/ExitTests/ExitTest.Condition.swift @@ -176,7 +176,6 @@ extension ExitTest.Condition { // MARK: - CustomStringConvertible -@_spi(Experimental) #if SWT_NO_EXIT_TESTS @available(*, unavailable, message: "Exit tests are not available on this platform.") #endif diff --git a/Sources/Testing/ExitTests/ExitTest.swift b/Sources/Testing/ExitTests/ExitTest.swift index ef37c4bd9..cec43a6ce 100644 --- a/Sources/Testing/ExitTests/ExitTest.swift +++ b/Sources/Testing/ExitTests/ExitTest.swift @@ -128,7 +128,7 @@ public struct ExitTest: Sendable, ~Copyable { /// /// The order of values in this array must be the same between the parent and /// child processes. - @_spi(Experimental) @_spi(ForToolsIntegrationOnly) + @_spi(ForToolsIntegrationOnly) public var capturedValues = [CapturedValue]() /// Make a copy of this instance. From cbafbcd800d8cecf3f20083ed8584f2875363a46 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Thu, 25 Sep 2025 14:53:09 -0400 Subject: [PATCH 156/216] Use generics to declare suite types in macro expansions. (#1338) This PR adopts generics instead of existentials when declaring suite types in the expansions of `@Suite` and `@Test` and in the interface of the internal `TypeInfo` helper type. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- .../Testing/Parameterization/TypeInfo.swift | 19 ++++++++-- Sources/Testing/Test+Macro.swift | 36 +++++++++---------- .../TestingMacros/TestDeclarationMacro.swift | 2 +- Tests/TestingTests/MiscellaneousTests.swift | 2 +- Tests/TestingTests/SwiftPMTests.swift | 4 +-- Tests/TestingTests/TypeInfoTests.swift | 7 ++++ 6 files changed, 45 insertions(+), 25 deletions(-) diff --git a/Sources/Testing/Parameterization/TypeInfo.swift b/Sources/Testing/Parameterization/TypeInfo.swift index 0ebda0a82..50026ec17 100644 --- a/Sources/Testing/Parameterization/TypeInfo.swift +++ b/Sources/Testing/Parameterization/TypeInfo.swift @@ -79,7 +79,7 @@ public struct TypeInfo: Sendable { /// /// - Parameters: /// - type: The type which this instance should describe. - init(describing type: any ~Copyable.Type) { + init(describing type: (some ~Copyable).Type) { _kind = .type(type) } @@ -88,8 +88,21 @@ public struct TypeInfo: Sendable { /// /// - Parameters: /// - value: The value whose type this instance should describe. - init(describingTypeOf value: Any) { - self.init(describing: Swift.type(of: value)) + init(describingTypeOf value: some Any) { +#if !hasFeature(Embedded) + let value = value as Any +#endif + let type = Swift.type(of: value) + self.init(describing: type) + } + + /// Initialize an instance of this type describing the type of the specified + /// value. + /// + /// - Parameters: + /// - value: The value whose type this instance should describe. + init(describingTypeOf value: borrowing T) where T: ~Copyable { + self.init(describing: T.self) } } diff --git a/Sources/Testing/Test+Macro.swift b/Sources/Testing/Test+Macro.swift index a8d9eaf51..023ee5d17 100644 --- a/Sources/Testing/Test+Macro.swift +++ b/Sources/Testing/Test+Macro.swift @@ -157,16 +157,16 @@ extension Test { /// /// - Warning: This function is used to implement the `@Test` macro. Do not /// call it directly. - public static func __function( + public static func __function( named testFunctionName: String, - in containingType: (any ~Copyable.Type)?, + in containingType: S.Type?, xcTestCompatibleSelector: __XCTestCompatibleSelector?, displayName: String? = nil, traits: [any TestTrait], sourceLocation: SourceLocation, parameters: [__Parameter] = [], testFunction: @escaping @Sendable () async throws -> Void - ) -> Self { + ) -> Self where S: ~Copyable { // Don't use Optional.map here due to a miscompile/crash. Expand out to an // if expression instead. SEE: rdar://134280902 let containingTypeInfo: TypeInfo? = if let containingType { @@ -241,9 +241,9 @@ extension Test { /// /// - Warning: This function is used to implement the `@Test` macro. Do not /// call it directly. - public static func __function( + public static func __function( named testFunctionName: String, - in containingType: (any ~Copyable.Type)?, + in containingType: S.Type?, xcTestCompatibleSelector: __XCTestCompatibleSelector?, displayName: String? = nil, traits: [any TestTrait], @@ -251,7 +251,7 @@ extension Test { sourceLocation: SourceLocation, parameters paramTuples: [__Parameter], testFunction: @escaping @Sendable (C.Element) async throws -> Void - ) -> Self where C: Collection & Sendable, C.Element: Sendable { + ) -> Self where S: ~Copyable, C: Collection & Sendable, C.Element: Sendable { let containingTypeInfo: TypeInfo? = if let containingType { TypeInfo(describing: containingType) } else { @@ -388,9 +388,9 @@ extension Test { /// /// - Warning: This function is used to implement the `@Test` macro. Do not /// call it directly. - public static func __function( + public static func __function( named testFunctionName: String, - in containingType: (any ~Copyable.Type)?, + in containingType: S.Type?, xcTestCompatibleSelector: __XCTestCompatibleSelector?, displayName: String? = nil, traits: [any TestTrait], @@ -398,7 +398,7 @@ extension Test { sourceLocation: SourceLocation, parameters paramTuples: [__Parameter], testFunction: @escaping @Sendable (C1.Element, C2.Element) async throws -> Void - ) -> Self where C1: Collection & Sendable, C1.Element: Sendable, C2: Collection & Sendable, C2.Element: Sendable { + ) -> Self where S: ~Copyable, C1: Collection & Sendable, C1.Element: Sendable, C2: Collection & Sendable, C2.Element: Sendable { let containingTypeInfo: TypeInfo? = if let containingType { TypeInfo(describing: containingType) } else { @@ -416,9 +416,9 @@ extension Test { /// /// - Warning: This function is used to implement the `@Test` macro. Do not /// call it directly. - public static func __function( + public static func __function( named testFunctionName: String, - in containingType: (any ~Copyable.Type)?, + in containingType: S.Type?, xcTestCompatibleSelector: __XCTestCompatibleSelector?, displayName: String? = nil, traits: [any TestTrait], @@ -426,7 +426,7 @@ extension Test { sourceLocation: SourceLocation, parameters paramTuples: [__Parameter], testFunction: @escaping @Sendable ((E1, E2)) async throws -> Void - ) -> Self where C: Collection & Sendable, C.Element == (E1, E2), E1: Sendable, E2: Sendable { + ) -> Self where S: ~Copyable, C: Collection & Sendable, C.Element == (E1, E2), E1: Sendable, E2: Sendable { let containingTypeInfo: TypeInfo? = if let containingType { TypeInfo(describing: containingType) } else { @@ -447,9 +447,9 @@ extension Test { /// /// - Warning: This function is used to implement the `@Test` macro. Do not /// call it directly. - public static func __function( + public static func __function( named testFunctionName: String, - in containingType: (any ~Copyable.Type)?, + in containingType: S.Type?, xcTestCompatibleSelector: __XCTestCompatibleSelector?, displayName: String? = nil, traits: [any TestTrait], @@ -457,7 +457,7 @@ extension Test { sourceLocation: SourceLocation, parameters paramTuples: [__Parameter], testFunction: @escaping @Sendable ((Key, Value)) async throws -> Void - ) -> Self where Key: Sendable, Value: Sendable { + ) -> Self where S: ~Copyable, Key: Sendable, Value: Sendable { let containingTypeInfo: TypeInfo? = if let containingType { TypeInfo(describing: containingType) } else { @@ -472,9 +472,9 @@ extension Test { /// /// - Warning: This function is used to implement the `@Test` macro. Do not /// call it directly. - public static func __function( + public static func __function( named testFunctionName: String, - in containingType: (any ~Copyable.Type)?, + in containingType: S.Type?, xcTestCompatibleSelector: __XCTestCompatibleSelector?, displayName: String? = nil, traits: [any TestTrait], @@ -482,7 +482,7 @@ extension Test { sourceLocation: SourceLocation, parameters paramTuples: [__Parameter], testFunction: @escaping @Sendable (C1.Element, C2.Element) async throws -> Void - ) -> Self where C1: Collection & Sendable, C1.Element: Sendable, C2: Collection & Sendable, C2.Element: Sendable { + ) -> Self where S: ~Copyable, C1: Collection & Sendable, C1.Element: Sendable, C2: Collection & Sendable, C2.Element: Sendable { let containingTypeInfo: TypeInfo? = if let containingType { TypeInfo(describing: containingType) } else { diff --git a/Sources/TestingMacros/TestDeclarationMacro.swift b/Sources/TestingMacros/TestDeclarationMacro.swift index b67bf3360..8007c3aaf 100644 --- a/Sources/TestingMacros/TestDeclarationMacro.swift +++ b/Sources/TestingMacros/TestDeclarationMacro.swift @@ -382,7 +382,7 @@ public struct TestDeclarationMacro: PeerMacro, Sendable { // Get the name of the type containing the function for passing to the test // factory function later. - let typeNameExpr: ExprSyntax = typeName.map { "\($0).self" } ?? "nil" + let typeNameExpr: ExprSyntax = typeName.map { "\($0).self" } ?? "nil as Swift.Never.Type?" if typeName != nil, let genericGuardDecl = makeGenericGuardDecl(guardingAgainst: functionDecl, in: context) { result.append(genericGuardDecl) diff --git a/Tests/TestingTests/MiscellaneousTests.swift b/Tests/TestingTests/MiscellaneousTests.swift index 84d1ce493..d47811f90 100644 --- a/Tests/TestingTests/MiscellaneousTests.swift +++ b/Tests/TestingTests/MiscellaneousTests.swift @@ -560,7 +560,7 @@ struct MiscellaneousTests { let line = 12345 let column = 67890 let sourceLocation = SourceLocation(fileID: fileID, filePath: filePath, line: line, column: column) - let testFunction = Test.__function(named: "myTestFunction()", in: nil, xcTestCompatibleSelector: nil, displayName: nil, traits: [], sourceLocation: sourceLocation) {} + let testFunction = Test.__function(named: "myTestFunction()", in: nil as Never.Type?, xcTestCompatibleSelector: nil, displayName: nil, traits: [], sourceLocation: sourceLocation) {} #expect(String(describing: testFunction.id) == "Module.myTestFunction()/Y.swift:12345:67890") } diff --git a/Tests/TestingTests/SwiftPMTests.swift b/Tests/TestingTests/SwiftPMTests.swift index e61c3b237..4543d3932 100644 --- a/Tests/TestingTests/SwiftPMTests.swift +++ b/Tests/TestingTests/SwiftPMTests.swift @@ -285,7 +285,7 @@ struct SwiftPMTests { @Test("Unsupported ABI version") func unsupportedABIVersion() async throws { let versionNumber = VersionNumber(-100, 0) - let versionTypeInfo = ABI.version(forVersionNumber: versionNumber).map(TypeInfo.init(describing:)) + let versionTypeInfo = ABI.version(forVersionNumber: versionNumber).map {TypeInfo(describing: $0) } #expect(versionTypeInfo == nil) } @@ -294,7 +294,7 @@ struct SwiftPMTests { #expect(swiftCompilerVersion >= VersionNumber(6, 0)) #expect(swiftCompilerVersion < VersionNumber(8, 0), "Swift 8.0 is here! Please update this test.") let versionNumber = VersionNumber(8, 0) - let versionTypeInfo = ABI.version(forVersionNumber: versionNumber).map(TypeInfo.init(describing:)) + let versionTypeInfo = ABI.version(forVersionNumber: versionNumber).map {TypeInfo(describing: $0) } #expect(versionTypeInfo == nil) } diff --git a/Tests/TestingTests/TypeInfoTests.swift b/Tests/TestingTests/TypeInfoTests.swift index b2a79f1ab..2063a7684 100644 --- a/Tests/TestingTests/TypeInfoTests.swift +++ b/Tests/TestingTests/TypeInfoTests.swift @@ -122,6 +122,11 @@ struct TypeInfoTests { #expect(!TypeInfo(describing: String.self).isSwiftEnumeration) #expect(TypeInfo(describing: SomeEnum.self).isSwiftEnumeration) } + + @Test func typeOfMoveOnlyValueIsInferred() { + let value = MoveOnlyType() + #expect(TypeInfo(describingTypeOf: value).unqualifiedName == "MoveOnlyType") + } } // MARK: - Fixtures @@ -131,3 +136,5 @@ extension String { } private enum SomeEnum {} + +private struct MoveOnlyType: ~Copyable {} From 192dc75971cafd5f275c27f883cbc8b1390b8d6e Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Thu, 25 Sep 2025 14:53:21 -0400 Subject: [PATCH 157/216] Disable backtrace capturing in Embedded Swift. (#1339) This PR disables the bulk of the backtrace-capturing-on-error logic when building for Embedded Swift. At least for the moment, we can't deal with error existentials in Embedded Swift, so this code won't function correctly even if it successfully compiles. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- .../Testing/SourceAttribution/Backtrace.swift | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/Sources/Testing/SourceAttribution/Backtrace.swift b/Sources/Testing/SourceAttribution/Backtrace.swift index 97815755e..60d08fede 100644 --- a/Sources/Testing/SourceAttribution/Backtrace.swift +++ b/Sources/Testing/SourceAttribution/Backtrace.swift @@ -119,6 +119,7 @@ extension Backtrace: Codable { // MARK: - Backtraces for thrown errors extension Backtrace { +#if !hasFeature(Embedded) // MARK: - Error cache keys /// A type used as a cache key that uniquely identifies error existential @@ -321,6 +322,7 @@ extension Backtrace { } forward(errorType) } +#endif /// Whether or not Foundation provides a function that triggers the capture of /// backtaces when instances of `NSError` or `CFError` are created. @@ -336,7 +338,7 @@ extension Backtrace { /// - Note: The underlying Foundation function is called (if present) the /// first time the value of this property is read. static let isFoundationCaptureEnabled = { -#if _runtime(_ObjC) && !SWT_NO_DYNAMIC_LINKING +#if !hasFeature(Embedded) && _runtime(_ObjC) && !SWT_NO_DYNAMIC_LINKING if Environment.flag(named: "SWT_FOUNDATION_ERROR_BACKTRACING_ENABLED") == true { let _CFErrorSetCallStackCaptureEnabled = symbol(named: "_CFErrorSetCallStackCaptureEnabled").map { castCFunction(at: $0, to: (@convention(c) (DarwinBoolean) -> DarwinBoolean).self) @@ -348,6 +350,7 @@ extension Backtrace { return false }() +#if !hasFeature(Embedded) /// The implementation of ``Backtrace/startCachingForThrownErrors()``, run /// only once. /// @@ -373,6 +376,7 @@ extension Backtrace { } } }() +#endif /// Configure the Swift runtime to allow capturing backtraces when errors are /// thrown. @@ -381,7 +385,9 @@ extension Backtrace { /// developer-supplied code to ensure that thrown errors' backtraces are /// always captured. static func startCachingForThrownErrors() { +#if !hasFeature(Embedded) __SWIFT_TESTING_IS_CAPTURING_A_BACKTRACE_FOR_A_THROWN_ERROR__ +#endif } /// Flush stale entries from the error-mapping cache. @@ -389,9 +395,11 @@ extension Backtrace { /// Call this function periodically to ensure that errors do not continue to /// take up space in the cache after they have been deinitialized. static func flushThrownErrorCache() { +#if !hasFeature(Embedded) _errorMappingCache.withLock { cache in cache = cache.filter { $0.value.errorObject != nil } } +#endif } /// Initialize an instance of this type with the previously-cached backtrace @@ -411,6 +419,7 @@ extension Backtrace { /// initializer cannot be made an instance method or property of `Error` /// because doing so will cause Swift-native errors to be unboxed into /// existential containers with different addresses. +#if !hasFeature(Embedded) @inline(never) init?(forFirstThrowOf error: any Error, checkFoundation: Bool = true) { if checkFoundation && Self.isFoundationCaptureEnabled, @@ -430,4 +439,9 @@ extension Backtrace { return nil } } +#else + init?(forFirstThrowOf error: some Error, checkFoundation: Bool = true) { + return nil + } +#endif } From 08728d782286ee2f8e23251e788c4a34038648b9 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Thu, 25 Sep 2025 15:32:40 -0400 Subject: [PATCH 158/216] Mark exit tests unavailable in Embedded Swift. (#1341) For the moment at least, we don't support exit tests in Embedded Swift. We can investigate supporting them in the future. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- Sources/Testing/ExitTests/ExitTest.CapturedValue.swift | 1 + Sources/Testing/ExitTests/ExitTest.Condition.swift | 4 ++++ Sources/Testing/ExitTests/ExitTest.Result.swift | 1 + Sources/Testing/ExitTests/ExitTest.swift | 1 + Sources/Testing/Expectations/Expectation+Macro.swift | 2 ++ 5 files changed, 9 insertions(+) diff --git a/Sources/Testing/ExitTests/ExitTest.CapturedValue.swift b/Sources/Testing/ExitTests/ExitTest.CapturedValue.swift index c311978a8..556fc0cf6 100644 --- a/Sources/Testing/ExitTests/ExitTest.CapturedValue.swift +++ b/Sources/Testing/ExitTests/ExitTest.CapturedValue.swift @@ -12,6 +12,7 @@ private import _TestingInternals @_spi(ForToolsIntegrationOnly) #if SWT_NO_EXIT_TESTS +@_unavailableInEmbedded @available(*, unavailable, message: "Exit tests are not available on this platform.") #endif extension ExitTest { diff --git a/Sources/Testing/ExitTests/ExitTest.Condition.swift b/Sources/Testing/ExitTests/ExitTest.Condition.swift index fd3f3680c..edd94193b 100644 --- a/Sources/Testing/ExitTests/ExitTest.Condition.swift +++ b/Sources/Testing/ExitTests/ExitTest.Condition.swift @@ -11,6 +11,7 @@ private import _TestingInternals #if SWT_NO_EXIT_TESTS +@_unavailableInEmbedded @available(*, unavailable, message: "Exit tests are not available on this platform.") #endif extension ExitTest { @@ -58,6 +59,7 @@ extension ExitTest { // MARK: - #if SWT_NO_EXIT_TESTS +@_unavailableInEmbedded @available(*, unavailable, message: "Exit tests are not available on this platform.") #endif extension ExitTest.Condition { @@ -177,6 +179,7 @@ extension ExitTest.Condition { // MARK: - CustomStringConvertible #if SWT_NO_EXIT_TESTS +@_unavailableInEmbedded @available(*, unavailable, message: "Exit tests are not available on this platform.") #endif extension ExitTest.Condition: CustomStringConvertible { @@ -199,6 +202,7 @@ extension ExitTest.Condition: CustomStringConvertible { // MARK: - Comparison #if SWT_NO_EXIT_TESTS +@_unavailableInEmbedded @available(*, unavailable, message: "Exit tests are not available on this platform.") #endif extension ExitTest.Condition { diff --git a/Sources/Testing/ExitTests/ExitTest.Result.swift b/Sources/Testing/ExitTests/ExitTest.Result.swift index e31afa937..53d816c85 100644 --- a/Sources/Testing/ExitTests/ExitTest.Result.swift +++ b/Sources/Testing/ExitTests/ExitTest.Result.swift @@ -9,6 +9,7 @@ // #if SWT_NO_EXIT_TESTS +@_unavailableInEmbedded @available(*, unavailable, message: "Exit tests are not available on this platform.") #endif extension ExitTest { diff --git a/Sources/Testing/ExitTests/ExitTest.swift b/Sources/Testing/ExitTests/ExitTest.swift index cec43a6ce..56096906f 100644 --- a/Sources/Testing/ExitTests/ExitTest.swift +++ b/Sources/Testing/ExitTests/ExitTest.swift @@ -35,6 +35,7 @@ private import _TestingInternals /// @Available(Xcode, introduced: 26.0) /// } #if SWT_NO_EXIT_TESTS +@_unavailableInEmbedded @available(*, unavailable, message: "Exit tests are not available on this platform.") #endif public struct ExitTest: Sendable, ~Copyable { diff --git a/Sources/Testing/Expectations/Expectation+Macro.swift b/Sources/Testing/Expectations/Expectation+Macro.swift index 17127d058..ea007f667 100644 --- a/Sources/Testing/Expectations/Expectation+Macro.swift +++ b/Sources/Testing/Expectations/Expectation+Macro.swift @@ -877,6 +877,7 @@ public macro require( @freestanding(expression) @discardableResult #if SWT_NO_EXIT_TESTS +@_unavailableInEmbedded @available(*, unavailable, message: "Exit tests are not available on this platform.") #endif public macro expect( @@ -924,6 +925,7 @@ public macro expect( @freestanding(expression) @discardableResult #if SWT_NO_EXIT_TESTS +@_unavailableInEmbedded @available(*, unavailable, message: "Exit tests are not available on this platform.") #endif public macro require( From 992cd2732531683ad12545a0335dbcaeab21dee0 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Thu, 25 Sep 2025 15:32:52 -0400 Subject: [PATCH 159/216] Remove a stray `throws` in the declaration of `__requiringUnsafe()`. (#1340) This function is not meant to handle `try`, just `unsafe`. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- Sources/Testing/Test+Macro.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Testing/Test+Macro.swift b/Sources/Testing/Test+Macro.swift index 023ee5d17..78464cee8 100644 --- a/Sources/Testing/Test+Macro.swift +++ b/Sources/Testing/Test+Macro.swift @@ -556,7 +556,7 @@ extension Test { /// /// - Warning: This function is used to implement the `@Test` macro. Do not use /// it directly. -@unsafe @inlinable public func __requiringUnsafe(_ value: consuming T) throws -> T where T: ~Copyable { +@unsafe @inlinable public func __requiringUnsafe(_ value: consuming T) -> T where T: ~Copyable { value } From 3d5dd841e41bb6fa32bffdd5c58589f45c46a313 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Tue, 30 Sep 2025 13:05:03 -0400 Subject: [PATCH 160/216] Adopt `_CFErrorCopyCallStackReturnAddresses()`. (#1344) This PR adopts a Core Foundation function, new in macOS 15.4/etc. and added for us to use, that captures backtraces for `NSError` and `CFError` instances at initialization time and stores them in a sidecar location for consumption by Swift Testing and/or XCTest. Upstreaming from Apple's fork (rdar://139841808, 508230a + dd7bae2) ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- .../Testing/SourceAttribution/Backtrace.swift | 42 ++++++++++++++++--- 1 file changed, 36 insertions(+), 6 deletions(-) diff --git a/Sources/Testing/SourceAttribution/Backtrace.swift b/Sources/Testing/SourceAttribution/Backtrace.swift index 60d08fede..fd7972cc4 100644 --- a/Sources/Testing/SourceAttribution/Backtrace.swift +++ b/Sources/Testing/SourceAttribution/Backtrace.swift @@ -324,6 +324,24 @@ extension Backtrace { } #endif +#if !hasFeature(Embedded) && SWT_TARGET_OS_APPLE && !SWT_NO_DYNAMIC_LINKING + /// A function provided by Core Foundation that copies the captured backtrace + /// from storage inside `CFError` or `NSError`. + /// + /// - Parameters: + /// - error: The error whose backtrace is desired. + /// + /// - Returns: The backtrace (as an instance of `NSArray`) captured by `error` + /// when it was created, or `nil` if none was captured. The caller is + /// responsible for releasing this object when done. + /// + /// This function was added in an internal Foundation PR and is not available + /// on older systems. + private static let _CFErrorCopyCallStackReturnAddresses = symbol(named: "_CFErrorCopyCallStackReturnAddresses").map { + castCFunction(at: $0, to: (@convention(c) (_ error: any Error) -> Unmanaged?).self) + } +#endif + /// Whether or not Foundation provides a function that triggers the capture of /// backtaces when instances of `NSError` or `CFError` are created. /// @@ -339,7 +357,11 @@ extension Backtrace { /// first time the value of this property is read. static let isFoundationCaptureEnabled = { #if !hasFeature(Embedded) && _runtime(_ObjC) && !SWT_NO_DYNAMIC_LINKING - if Environment.flag(named: "SWT_FOUNDATION_ERROR_BACKTRACING_ENABLED") == true { + // Check the environment variable; if it isn't set, enable if and only if + // the Core Foundation getter function is implemented. + let foundationBacktracesEnabled = Environment.flag(named: "SWT_FOUNDATION_ERROR_BACKTRACING_ENABLED") + ?? (_CFErrorCopyCallStackReturnAddresses != nil) + if foundationBacktracesEnabled { let _CFErrorSetCallStackCaptureEnabled = symbol(named: "_CFErrorSetCallStackCaptureEnabled").map { castCFunction(at: $0, to: (@convention(c) (DarwinBoolean) -> DarwinBoolean).self) } @@ -422,11 +444,19 @@ extension Backtrace { #if !hasFeature(Embedded) @inline(never) init?(forFirstThrowOf error: any Error, checkFoundation: Bool = true) { - if checkFoundation && Self.isFoundationCaptureEnabled, - let userInfo = error._userInfo as? [String: Any], - let addresses = userInfo["NSCallStackReturnAddresses"] as? [Address], !addresses.isEmpty { - self.init(addresses: addresses) - return + if checkFoundation && Self.isFoundationCaptureEnabled { +#if !hasFeature(Embedded) && SWT_TARGET_OS_APPLE && !SWT_NO_DYNAMIC_LINKING + if let addresses = Self._CFErrorCopyCallStackReturnAddresses?(error)?.takeRetainedValue() as? [Address] { + self.init(addresses: addresses) + return + } +#endif + + if let userInfo = error._userInfo as? [String: Any], + let addresses = userInfo["NSCallStackReturnAddresses"] as? [Address], !addresses.isEmpty { + self.init(addresses: addresses) + return + } } let entry = Self._errorMappingCache.withLock { cache in From 0b47e51a5ad1fd3aa6d8a2cfc53f7a86d21e3de3 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Tue, 30 Sep 2025 13:31:26 -0400 Subject: [PATCH 161/216] Treat `@_unavailableInEmbedded` like other availability attributes during macro expansion. (#1342) This PR adds support for mapping `@_unavailableInEmbedded` to a condition trait like we do with `@available()` etc. Example: ```swift @_unavailableInEmbedded @Test func `stuff that doesn't work in Embedded Swift`() { ... } ``` Which expands to include a trait of the form `.__unavailableInEmbedded(sourceLocation: ...)` of type `ConditionTrait`. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- .../Testing/Traits/ConditionTrait+Macro.swift | 23 +++++++++++++++++++ .../FunctionDeclSyntaxAdditions.swift | 10 ++------ .../WithAttributesSyntaxAdditions.swift | 14 +++++------ .../Support/AvailabilityGuards.swift | 16 +++++++++++++ Tests/TestingTests/RunnerTests.swift | 23 +++++++++++++++++++ 5 files changed, 71 insertions(+), 15 deletions(-) diff --git a/Sources/Testing/Traits/ConditionTrait+Macro.swift b/Sources/Testing/Traits/ConditionTrait+Macro.swift index dbddcfc1f..fd489b9e6 100644 --- a/Sources/Testing/Traits/ConditionTrait+Macro.swift +++ b/Sources/Testing/Traits/ConditionTrait+Macro.swift @@ -124,4 +124,27 @@ extension Trait where Self == ConditionTrait { sourceLocation: sourceLocation ) } + + /// Create a trait controlling availability of a test based on an + /// `@_unavailableInEmbedded` attribute applied to it. + /// + /// - Parameters: + /// - sourceLocation: The source location of the test. + /// + /// - Returns: A trait. + /// + /// - Warning: This function is used to implement the `@Test` macro. Do not + /// call it directly. + public static func __unavailableInEmbedded(sourceLocation: SourceLocation) -> Self { +#if hasFeature(Embedded) + let isEmbedded = true +#else + let isEmbedded = false +#endif + return Self( + kind: .unconditional(!isEmbedded), + comments: ["Marked @_unavailableInEmbedded"], + sourceLocation: sourceLocation + ) + } } diff --git a/Sources/TestingMacros/Support/Additions/FunctionDeclSyntaxAdditions.swift b/Sources/TestingMacros/Support/Additions/FunctionDeclSyntaxAdditions.swift index fa390775a..8065d299e 100644 --- a/Sources/TestingMacros/Support/Additions/FunctionDeclSyntaxAdditions.swift +++ b/Sources/TestingMacros/Support/Additions/FunctionDeclSyntaxAdditions.swift @@ -87,14 +87,8 @@ extension FunctionDeclSyntax { var xcTestCompatibleSelector: ObjCSelectorPieceListSyntax? { // First, look for an @objc attribute with an explicit selector, and use // that if found. - let objcAttribute = attributes.lazy - .compactMap { - if case let .attribute(attribute) = $0 { - return attribute - } - return nil - }.first { $0.attributeNameText == "objc" } - if let objcAttribute, case let .objCName(objCName) = objcAttribute.arguments { + if let objcAttribute = attributes(named: "objc", inModuleNamed: "Swift").first, + case let .objCName(objCName) = objcAttribute.arguments { if true == objCName.first?.name?.textWithoutBackticks.hasPrefix("test") { return objCName } diff --git a/Sources/TestingMacros/Support/Additions/WithAttributesSyntaxAdditions.swift b/Sources/TestingMacros/Support/Additions/WithAttributesSyntaxAdditions.swift index 52d85bbd4..aa778a00c 100644 --- a/Sources/TestingMacros/Support/Additions/WithAttributesSyntaxAdditions.swift +++ b/Sources/TestingMacros/Support/Additions/WithAttributesSyntaxAdditions.swift @@ -105,13 +105,13 @@ extension WithAttributesSyntax { /// The first `@available(*, noasync)` or `@_unavailableFromAsync` attribute /// on this instance, if any. var noasyncAttribute: AttributeSyntax? { - availability(when: .noasync).first?.attribute ?? attributes.lazy - .compactMap { attribute in - if case let .attribute(attribute) = attribute { - return attribute - } - return nil - }.first { $0.attributeNameText == "_unavailableFromAsync" } + availability(when: .noasync).first?.attribute + ?? attributes(named: "_unavailableFromAsync", inModuleNamed: "Swift").first + } + + /// The first `@_unavailableInEmbedded` attribute on this instance, if any. + var unavailableInEmbeddedAttribute: AttributeSyntax? { + attributes(named: "_unavailableInEmbedded", inModuleNamed: "Swift").first } /// Find all attributes on this node, if any, with the given name. diff --git a/Sources/TestingMacros/Support/AvailabilityGuards.swift b/Sources/TestingMacros/Support/AvailabilityGuards.swift index e9f4ba762..deb3a0f8b 100644 --- a/Sources/TestingMacros/Support/AvailabilityGuards.swift +++ b/Sources/TestingMacros/Support/AvailabilityGuards.swift @@ -169,6 +169,11 @@ func createAvailabilityTraitExprs( _createAvailabilityTraitExpr(from: availability, when: .obsoleted, in: context) } + if let attribute = decl.unavailableInEmbeddedAttribute { + let sourceLocationExpr = createSourceLocationExpr(of: attribute, context: context) + result += [".__unavailableInEmbedded(sourceLocation: \(sourceLocationExpr))"] + } + return result } @@ -290,5 +295,16 @@ func createSyntaxNode( } } + // Handle Embedded Swift. + if decl.unavailableInEmbeddedAttribute != nil { + result = """ + #if !hasFeature(Embedded) + \(result) + #else + \(exitStatement) + #endif + """ + } + return result } diff --git a/Tests/TestingTests/RunnerTests.swift b/Tests/TestingTests/RunnerTests.swift index 335f8be37..c254e5ba9 100644 --- a/Tests/TestingTests/RunnerTests.swift +++ b/Tests/TestingTests/RunnerTests.swift @@ -819,6 +819,29 @@ final class RunnerTests: XCTestCase { await fulfillment(of: [testStarted], timeout: 0.0) } + @Suite(.hidden) struct UnavailableInEmbeddedTests { + @Test(.hidden) + @_unavailableInEmbedded + func embedded() {} + } + + func testUnavailableInEmbeddedAttribute() async throws { + let testStarted = expectation(description: "Test started") +#if !hasFeature(Embedded) + testStarted.expectedFulfillmentCount = 3 +#else + testStarted.isInverted = true +#endif + var configuration = Configuration() + configuration.eventHandler = { event, _ in + if case .testStarted = event.kind { + testStarted.fulfill() + } + } + await runTest(for: UnavailableInEmbeddedTests.self, configuration: configuration) + await fulfillment(of: [testStarted], timeout: 0.0) + } + #if !SWT_NO_GLOBAL_ACTORS @TaskLocal static var isMainActorIsolationEnforced = false From b4d1d05d07542dfc63a91aefff5cfdbab4f3cd40 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Tue, 30 Sep 2025 16:33:33 -0400 Subject: [PATCH 162/216] Provide opt-out condition for `sys_signame[]`. (#1345) This PR adds `SWT_NO_SYS_SIGNAME` which can be set in environments where the [`sys_signame[]`](https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man3/sys_signame.3.html) array is not available. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- Sources/Testing/ExitTests/ExitStatus.swift | 2 ++ Tests/TestingTests/ExitTestTests.swift | 2 ++ 2 files changed, 4 insertions(+) diff --git a/Sources/Testing/ExitTests/ExitStatus.swift b/Sources/Testing/ExitTests/ExitStatus.swift index d4a95e14d..21fa2335e 100644 --- a/Sources/Testing/ExitTests/ExitStatus.swift +++ b/Sources/Testing/ExitTests/ExitStatus.swift @@ -121,6 +121,7 @@ extension ExitStatus: CustomStringConvertible { var signalName: String? #if SWT_TARGET_OS_APPLE || os(FreeBSD) || os(OpenBSD) || os(Android) +#if !SWT_NO_SYS_SIGNAME // These platforms define sys_signame with a size, which is imported // into Swift as a tuple. withUnsafeBytes(of: sys_signame) { sys_signame in @@ -130,6 +131,7 @@ extension ExitStatus: CustomStringConvertible { } } } +#endif #elseif os(Linux) #if !SWT_NO_DYNAMIC_LINKING signalName = _sigabbrev_np?(signal).flatMap(String.init(validatingCString:)) diff --git a/Tests/TestingTests/ExitTestTests.swift b/Tests/TestingTests/ExitTestTests.swift index 5bcb2a05d..5be229266 100644 --- a/Tests/TestingTests/ExitTestTests.swift +++ b/Tests/TestingTests/ExitTestTests.swift @@ -16,7 +16,9 @@ private import _TestingInternals @Test("Signal names are reported (where supported)") func signalName() { var hasSignalNames = false #if SWT_TARGET_OS_APPLE || os(FreeBSD) || os(OpenBSD) || os(Android) +#if !SWT_NO_SYS_SIGNAME hasSignalNames = true +#endif #elseif os(Linux) && !SWT_NO_DYNAMIC_LINKING hasSignalNames = (symbol(named: "sigabbrev_np") != nil) #endif From c996b0aeb4ea8c13b6c42062298790fb06f29fb8 Mon Sep 17 00:00:00 2001 From: Stuart Montgomery Date: Wed, 1 Oct 2025 09:15:25 -0500 Subject: [PATCH 163/216] AdvancedConsoleOutputRecorderTests is missing an import of Foundation (#1346) Import `Foundation` in `AdvancedConsoleOutputRecorderTests`. This is required due to `MemberImportVisibility` although in practice this problem only shows up when building for non-macOS Apple platforms, such as iOS, due to the differing deployment target versions we use for non-macOS platforms. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- Tests/TestingTests/AdvancedConsoleOutputRecorderTests.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Tests/TestingTests/AdvancedConsoleOutputRecorderTests.swift b/Tests/TestingTests/AdvancedConsoleOutputRecorderTests.swift index dc8dd5260..2eafb6601 100644 --- a/Tests/TestingTests/AdvancedConsoleOutputRecorderTests.swift +++ b/Tests/TestingTests/AdvancedConsoleOutputRecorderTests.swift @@ -8,7 +8,9 @@ // See https://swift.org/CONTRIBUTORS.txt for Swift project authors // +#if canImport(Foundation) @testable @_spi(Experimental) @_spi(ForToolsIntegrationOnly) import Testing +import Foundation @Suite("Advanced Console Output Recorder Tests") struct AdvancedConsoleOutputRecorderTests { @@ -237,3 +239,4 @@ struct SimpleTestSuite { #expect(Bool(true)) } } +#endif From f276d250c6d3d25bf6bcd13bba67539abbe0254b Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Fri, 3 Oct 2025 20:25:22 -0400 Subject: [PATCH 164/216] Normalize the order in which we specify our Linux/FreeBSD/OpenBSD checks. (#1349) This PR adjusts some of our `#if os(...)` checks to always specify Linux, FreeBSD, OpenBSD, and Android in a consistent order. This makes it easier to find such code in the repo. I've also found a couple of spots where OpenBSD was missing and have fixed them. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- .../Event.HumanReadableOutputRecorder.swift | 2 +- Sources/Testing/ExitTests/SpawnProcess.swift | 2 +- Sources/Testing/ExitTests/WaitFor.swift | 4 ---- Sources/Testing/Support/Locked+Platform.swift | 5 +++-- Sources/Testing/Support/Versions.swift | 2 +- Tests/TestingTests/ABIEntryPointTests.swift | 13 +++++++------ 6 files changed, 13 insertions(+), 15 deletions(-) diff --git a/Sources/Testing/Events/Recorder/Event.HumanReadableOutputRecorder.swift b/Sources/Testing/Events/Recorder/Event.HumanReadableOutputRecorder.swift index 32d1ce770..9f0ac21b1 100644 --- a/Sources/Testing/Events/Recorder/Event.HumanReadableOutputRecorder.swift +++ b/Sources/Testing/Events/Recorder/Event.HumanReadableOutputRecorder.swift @@ -361,7 +361,7 @@ extension Event.HumanReadableOutputRecorder { comments.append("Swift Standard Library Version: \(swiftStandardLibraryVersion)") } comments.append("Swift Compiler Version: \(swiftCompilerVersion)") -#if canImport(Glibc) && !os(FreeBSD) && !os(OpenBSD) +#if os(Linux) && canImport(Glibc) comments.append("GNU C Library Version: \(glibcVersion)") #endif } diff --git a/Sources/Testing/ExitTests/SpawnProcess.swift b/Sources/Testing/ExitTests/SpawnProcess.swift index d1f600e2d..d82d7b663 100644 --- a/Sources/Testing/ExitTests/SpawnProcess.swift +++ b/Sources/Testing/ExitTests/SpawnProcess.swift @@ -137,7 +137,7 @@ func spawnExecutable( // standardized in POSIX.1-2024 (see https://pubs.opengroup.org/onlinepubs/9799919799/functions/posix_spawn_file_actions_adddup2.html // and https://www.austingroupbugs.net/view.php?id=411). _ = posix_spawn_file_actions_adddup2(fileActions, fd, fd) -#if canImport(Glibc) && !os(FreeBSD) && !os(OpenBSD) +#if os(Linux) && canImport(Glibc) if _slowPath(glibcVersion < VersionNumber(2, 29)) { // This system is using an older version of glibc that does not // implement FD_CLOEXEC clearing in posix_spawn_file_actions_adddup2(), diff --git a/Sources/Testing/ExitTests/WaitFor.swift b/Sources/Testing/ExitTests/WaitFor.swift index 4569dddc9..8c6ad52f3 100644 --- a/Sources/Testing/ExitTests/WaitFor.swift +++ b/Sources/Testing/ExitTests/WaitFor.swift @@ -85,11 +85,7 @@ private let _childProcessContinuations = LockedWith.allocate(capacity: 1) -#else let result = UnsafeMutablePointer.allocate(capacity: 1) -#endif _ = pthread_cond_init(result, nil) return result }() diff --git a/Sources/Testing/Support/Locked+Platform.swift b/Sources/Testing/Support/Locked+Platform.swift index 9e6f9db26..a2ba82ac2 100644 --- a/Sources/Testing/Support/Locked+Platform.swift +++ b/Sources/Testing/Support/Locked+Platform.swift @@ -39,9 +39,10 @@ extension os_unfair_lock_s: Lockable { #if os(FreeBSD) || os(OpenBSD) typealias pthread_mutex_t = _TestingInternals.pthread_mutex_t? +typealias pthread_cond_t = _TestingInternals.pthread_cond_t? #endif -#if SWT_TARGET_OS_APPLE || os(Linux) || os(Android) || (os(WASI) && _runtime(_multithreaded)) || os(FreeBSD) || os(OpenBSD) +#if SWT_TARGET_OS_APPLE || os(Linux) || os(FreeBSD) || os(OpenBSD) || os(Android) || (os(WASI) && _runtime(_multithreaded)) extension pthread_mutex_t: Lockable { static func initializeLock(at lock: UnsafeMutablePointer) { _ = pthread_mutex_init(lock, nil) @@ -83,7 +84,7 @@ extension SRWLOCK: Lockable { #if SWT_TARGET_OS_APPLE && !SWT_NO_OS_UNFAIR_LOCK typealias DefaultLock = os_unfair_lock -#elseif SWT_TARGET_OS_APPLE || os(Linux) || os(Android) || (os(WASI) && _runtime(_multithreaded)) || os(FreeBSD) || os(OpenBSD) +#elseif SWT_TARGET_OS_APPLE || os(Linux) || os(FreeBSD) || os(OpenBSD) || os(Android) || (os(WASI) && _runtime(_multithreaded)) typealias DefaultLock = pthread_mutex_t #elseif os(Windows) typealias DefaultLock = SRWLOCK diff --git a/Sources/Testing/Support/Versions.swift b/Sources/Testing/Support/Versions.swift index 76c5aeaf6..b671b302b 100644 --- a/Sources/Testing/Support/Versions.swift +++ b/Sources/Testing/Support/Versions.swift @@ -198,7 +198,7 @@ var swiftCompilerVersion: VersionNumber { ) } -#if canImport(Glibc) && !os(FreeBSD) && !os(OpenBSD) +#if os(Linux) && canImport(Glibc) /// The (runtime, not compile-time) version of glibc in use on this system. /// /// This value is not part of the public interface of the testing library. diff --git a/Tests/TestingTests/ABIEntryPointTests.swift b/Tests/TestingTests/ABIEntryPointTests.swift index 76af1b83e..15b9cc879 100644 --- a/Tests/TestingTests/ABIEntryPointTests.swift +++ b/Tests/TestingTests/ABIEntryPointTests.swift @@ -67,12 +67,12 @@ struct ABIEntryPointTests { passing arguments: __CommandLineArguments_v0, recordHandler: @escaping @Sendable (_ recordJSON: UnsafeRawBufferPointer) -> Void = { _ in } ) async throws -> Bool { -#if !os(Linux) && !os(FreeBSD) && !os(Android) && !SWT_NO_DYNAMIC_LINKING +#if !(os(Linux) || os(FreeBSD) || os(OpenBSD) || os(Android)) && !SWT_NO_DYNAMIC_LINKING // Get the ABI entry point by dynamically looking it up at runtime. // - // NOTE: The standard Linux linker does not allow exporting symbols from - // executables, so dlsym() does not let us find this function on that - // platform when built as an executable rather than a dynamic library. + // NOTE: The standard linkers on these platforms do not export symbols from + // executables, so dlsym() does not let us find this function on these + // platforms when built as an executable rather than a dynamic library. let abiv0_getEntryPoint = try withTestingLibraryImageAddress { testingLibrary in try #require( symbol(in: testingLibrary, named: "swt_abiv0_getEntryPoint").map { @@ -187,8 +187,9 @@ private func withTestingLibraryImageAddress(_ body: (ImageAddress?) throws -> defer { dlclose(testingLibraryAddress) } -#elseif os(Linux) || os(FreeBSD) || os(Android) - // When using glibc, dladdr() is only available if __USE_GNU is specified. +#elseif os(Linux) || os(FreeBSD) || os(OpenBSD) || os(Android) + // We can't dynamically look up a function linked into the test executable on + // ELF-based platforms. #elseif os(Windows) let flags = DWORD(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS) try addressInTestingLibrary.withMemoryRebound(to: wchar_t.self, capacity: MemoryLayout.stride / MemoryLayout.stride) { addressInTestingLibrary in From 1ffbf6d2614063187ae5eeaa88aa08d6174be772 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Fri, 3 Oct 2025 23:06:56 -0400 Subject: [PATCH 165/216] Get exit tests functioning on OpenBSD. (#1352) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR adjusts the implementation of exit tests on OpenBSD so that they correctly spawn child processes there. Without this change, exit tests all abnormally terminate with exit code 127. `ktrace` and `kdump` report: ``` 63170 swift-testingPackageTes CALL sigaction(SIGHUP,0x932dd7e5ca0,0) 63170 swift-testingPackageTes STRU struct sigaction { handler=SIG_DFL, mask=0<>, flags=0<> } 63170 swift-testingPackageTes RET sigaction 0 63170 swift-testingPackageTes CALL sigaction(SIGINT,0x932dd7e5ca0,0) 63170 swift-testingPackageTes STRU struct sigaction { handler=SIG_DFL, mask=0<>, flags=0<> } 63170 swift-testingPackageTes RET sigaction 0 [...] 63170 swift-testingPackageTes CALL sigaction(SIGKILL,0x932dd7e5ca0,0) 63170 swift-testingPackageTes RET sigaction -1 errno 22 Invalid argument 63170 swift-testingPackageTes CALL exit(127) ``` OpenBSD is more strict in its handling of `posix_spawnattr_setsigdefault()` than our other target platforms. Specifically, the child process will self-terminate after forking but before execing if either `SIGKILL` or `SIGSTOP` is in the signal mask passed to that function. Other platforms silently ignore these signals (since their handlers cannot be set per POSIX anyway.) With this change, exit tests function correctly and Swift Testing's test suite passes with zero issues on OpenBSD. 🥳 openbsd ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- Sources/Testing/ExitTests/SpawnProcess.swift | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Sources/Testing/ExitTests/SpawnProcess.swift b/Sources/Testing/ExitTests/SpawnProcess.swift index d82d7b663..6114566f1 100644 --- a/Sources/Testing/ExitTests/SpawnProcess.swift +++ b/Sources/Testing/ExitTests/SpawnProcess.swift @@ -113,6 +113,13 @@ func spawnExecutable( withUnsafeTemporaryAllocation(of: sigset_t.self, capacity: 1) { allSignals in let allSignals = allSignals.baseAddress! sigfillset(allSignals) +#if os(OpenBSD) + // On OpenBSD, attempting to set the signal handler for SIGKILL or + // SIGSTOP will cause the child process of a call to posix_spawn() to + // exit abnormally with exit code 127. See https://man.openbsd.org/sigaction.2#ERRORS + sigdelset(allSignals, SIGKILL) + sigdelset(allSignals, SIGSTOP) +#endif posix_spawnattr_setsigdefault(attrs, allSignals); flags |= CShort(POSIX_SPAWN_SETSIGDEF) } From 13319e109eeaecd7fac963816ef947e1d90a2717 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Fri, 3 Oct 2025 23:45:45 -0400 Subject: [PATCH 166/216] Adopt `Mutex`. (#1351) This PR adopts `Mutex` on all platforms except Darwin (where we still need to back-deploy further than `Mutex` is available.) Resolves #538. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- Sources/Testing/CMakeLists.txt | 1 - Sources/Testing/ExitTests/WaitFor.swift | 43 +++++- Sources/Testing/Support/Locked+Platform.swift | 97 ------------- Sources/Testing/Support/Locked.swift | 135 +++++++----------- Tests/TestingTests/Support/LockTests.swift | 38 +---- 5 files changed, 97 insertions(+), 217 deletions(-) delete mode 100644 Sources/Testing/Support/Locked+Platform.swift diff --git a/Sources/Testing/CMakeLists.txt b/Sources/Testing/CMakeLists.txt index 9776f70d3..68fec3b13 100644 --- a/Sources/Testing/CMakeLists.txt +++ b/Sources/Testing/CMakeLists.txt @@ -89,7 +89,6 @@ add_library(Testing Support/Graph.swift Support/JSON.swift Support/Locked.swift - Support/Locked+Platform.swift Support/VersionNumber.swift Support/Versions.swift Discovery+Macro.swift diff --git a/Sources/Testing/ExitTests/WaitFor.swift b/Sources/Testing/ExitTests/WaitFor.swift index 8c6ad52f3..f0326ff3c 100644 --- a/Sources/Testing/ExitTests/WaitFor.swift +++ b/Sources/Testing/ExitTests/WaitFor.swift @@ -80,7 +80,42 @@ func wait(for pid: consuming pid_t) async throws -> ExitStatus { } #elseif SWT_TARGET_OS_APPLE || os(Linux) || os(FreeBSD) || os(OpenBSD) /// A mapping of awaited child PIDs to their corresponding Swift continuations. -private let _childProcessContinuations = LockedWith]>() +private nonisolated(unsafe) let _childProcessContinuations = { + let result = ManagedBuffer<[pid_t: CheckedContinuation], pthread_mutex_t>.create( + minimumCapacity: 1, + makingHeaderWith: { _ in [:] } + ) + + result.withUnsafeMutablePointers { _, lock in + _ = pthread_mutex_init(lock, nil) + } + + return result +}() + +/// Access the value in `_childProcessContinuations` while guarded by its lock. +/// +/// - Parameters: +/// - body: A closure to invoke while the lock is held. +/// +/// - Returns: Whatever is returned by `body`. +/// +/// - Throws: Whatever is thrown by `body`. +private func _withLockedChildProcessContinuations( + _ body: ( + _ childProcessContinuations: inout [pid_t: CheckedContinuation], + _ lock: UnsafeMutablePointer + ) throws -> R +) rethrows -> R { + try _childProcessContinuations.withUnsafeMutablePointers { childProcessContinuations, lock in + _ = pthread_mutex_lock(lock) + defer { + _ = pthread_mutex_unlock(lock) + } + + return try body(&childProcessContinuations.pointee, lock) + } +} /// A condition variable used to suspend the waiter thread created by /// `_createWaitThread()` when there are no child processes to await. @@ -112,7 +147,7 @@ private let _createWaitThread: Void = { var siginfo = siginfo_t() if 0 == waitid(P_ALL, 0, &siginfo, WEXITED | WNOWAIT) { if case let pid = siginfo.si_pid, pid != 0 { - let continuation = _childProcessContinuations.withLock { childProcessContinuations in + let continuation = _withLockedChildProcessContinuations { childProcessContinuations, _ in childProcessContinuations.removeValue(forKey: pid) } @@ -133,7 +168,7 @@ private let _createWaitThread: Void = { // newly-scheduled waiter process. (If this condition is spuriously // woken, we'll just loop again, which is fine.) Note that we read errno // outside the lock in case acquiring the lock perturbs it. - _childProcessContinuations.withUnsafeUnderlyingLock { lock, childProcessContinuations in + _withLockedChildProcessContinuations { childProcessContinuations, lock in if childProcessContinuations.isEmpty { _ = pthread_cond_wait(_waitThreadNoChildrenCondition, lock) } @@ -205,7 +240,7 @@ func wait(for pid: consuming pid_t) async throws -> ExitStatus { _createWaitThread return try await withCheckedThrowingContinuation { continuation in - _childProcessContinuations.withLock { childProcessContinuations in + _withLockedChildProcessContinuations { childProcessContinuations, _ in // We don't need to worry about a race condition here because waitid() // does not clear the wait/zombie state of the child process. If it sees // the child process has terminated and manages to acquire the lock before diff --git a/Sources/Testing/Support/Locked+Platform.swift b/Sources/Testing/Support/Locked+Platform.swift deleted file mode 100644 index a2ba82ac2..000000000 --- a/Sources/Testing/Support/Locked+Platform.swift +++ /dev/null @@ -1,97 +0,0 @@ -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2023–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 -// - -internal import _TestingInternals - -extension Never: Lockable { - static func initializeLock(at lock: UnsafeMutablePointer) {} - static func deinitializeLock(at lock: UnsafeMutablePointer) {} - static func unsafelyAcquireLock(at lock: UnsafeMutablePointer) {} - static func unsafelyRelinquishLock(at lock: UnsafeMutablePointer) {} -} - -#if SWT_TARGET_OS_APPLE && !SWT_NO_OS_UNFAIR_LOCK -extension os_unfair_lock_s: Lockable { - static func initializeLock(at lock: UnsafeMutablePointer) { - lock.initialize(to: .init()) - } - - static func deinitializeLock(at lock: UnsafeMutablePointer) { - // No deinitialization needed. - } - - static func unsafelyAcquireLock(at lock: UnsafeMutablePointer) { - os_unfair_lock_lock(lock) - } - - static func unsafelyRelinquishLock(at lock: UnsafeMutablePointer) { - os_unfair_lock_unlock(lock) - } -} -#endif - -#if os(FreeBSD) || os(OpenBSD) -typealias pthread_mutex_t = _TestingInternals.pthread_mutex_t? -typealias pthread_cond_t = _TestingInternals.pthread_cond_t? -#endif - -#if SWT_TARGET_OS_APPLE || os(Linux) || os(FreeBSD) || os(OpenBSD) || os(Android) || (os(WASI) && _runtime(_multithreaded)) -extension pthread_mutex_t: Lockable { - static func initializeLock(at lock: UnsafeMutablePointer) { - _ = pthread_mutex_init(lock, nil) - } - - static func deinitializeLock(at lock: UnsafeMutablePointer) { - _ = pthread_mutex_destroy(lock) - } - - static func unsafelyAcquireLock(at lock: UnsafeMutablePointer) { - _ = pthread_mutex_lock(lock) - } - - static func unsafelyRelinquishLock(at lock: UnsafeMutablePointer) { - _ = pthread_mutex_unlock(lock) - } -} -#endif - -#if os(Windows) -extension SRWLOCK: Lockable { - static func initializeLock(at lock: UnsafeMutablePointer) { - InitializeSRWLock(lock) - } - - static func deinitializeLock(at lock: UnsafeMutablePointer) { - // No deinitialization needed. - } - - static func unsafelyAcquireLock(at lock: UnsafeMutablePointer) { - AcquireSRWLockExclusive(lock) - } - - static func unsafelyRelinquishLock(at lock: UnsafeMutablePointer) { - ReleaseSRWLockExclusive(lock) - } -} -#endif - -#if SWT_TARGET_OS_APPLE && !SWT_NO_OS_UNFAIR_LOCK -typealias DefaultLock = os_unfair_lock -#elseif SWT_TARGET_OS_APPLE || os(Linux) || os(FreeBSD) || os(OpenBSD) || os(Android) || (os(WASI) && _runtime(_multithreaded)) -typealias DefaultLock = pthread_mutex_t -#elseif os(Windows) -typealias DefaultLock = SRWLOCK -#elseif os(WASI) -// No locks on WASI without multithreaded runtime. -typealias DefaultLock = Never -#else -#warning("Platform-specific implementation missing: locking unavailable") -typealias DefaultLock = Never -#endif diff --git a/Sources/Testing/Support/Locked.swift b/Sources/Testing/Support/Locked.swift index d1db8ef1f..ae2448b35 100644 --- a/Sources/Testing/Support/Locked.swift +++ b/Sources/Testing/Support/Locked.swift @@ -8,38 +8,8 @@ // See https://swift.org/CONTRIBUTORS.txt for Swift project authors // -internal import _TestingInternals - -/// A protocol defining a type, generally platform-specific, that satisfies the -/// requirements of a lock or mutex. -protocol Lockable { - /// Initialize the lock at the given address. - /// - /// - Parameters: - /// - lock: A pointer to uninitialized memory that should be initialized as - /// an instance of this type. - static func initializeLock(at lock: UnsafeMutablePointer) - - /// Deinitialize the lock at the given address. - /// - /// - Parameters: - /// - lock: A pointer to initialized memory that should be deinitialized. - static func deinitializeLock(at lock: UnsafeMutablePointer) - - /// Acquire the lock at the given address. - /// - /// - Parameters: - /// - lock: The address of the lock to acquire. - static func unsafelyAcquireLock(at lock: UnsafeMutablePointer) - - /// Relinquish the lock at the given address. - /// - /// - Parameters: - /// - lock: The address of the lock to relinquish. - static func unsafelyRelinquishLock(at lock: UnsafeMutablePointer) -} - -// MARK: - +private import _TestingInternals +private import Synchronization /// A type that wraps a value requiring access from a synchronous caller during /// concurrent execution. @@ -52,30 +22,48 @@ protocol Lockable { /// concurrency tools. /// /// This type is not part of the public interface of the testing library. -struct LockedWith: RawRepresentable where L: Lockable { - /// A type providing heap-allocated storage for an instance of ``Locked``. - private final class _Storage: ManagedBuffer { - deinit { - withUnsafeMutablePointerToElements { lock in - L.deinitializeLock(at: lock) - } +struct Locked { + /// A type providing storage for the underlying lock and wrapped value. +#if SWT_TARGET_OS_APPLE && canImport(os) + private typealias _Storage = ManagedBuffer +#else + private final class _Storage { + let mutex: Mutex + + init(_ rawValue: consuming sending T) { + mutex = Mutex(rawValue) } } +#endif /// Storage for the underlying lock and wrapped value. - private nonisolated(unsafe) var _storage: ManagedBuffer + private nonisolated(unsafe) var _storage: _Storage +} + +extension Locked: Sendable where T: Sendable {} +extension Locked: RawRepresentable { init(rawValue: T) { - _storage = _Storage.create(minimumCapacity: 1, makingHeaderWith: { _ in rawValue }) +#if SWT_TARGET_OS_APPLE && canImport(os) + _storage = .create(minimumCapacity: 1, makingHeaderWith: { _ in rawValue }) _storage.withUnsafeMutablePointerToElements { lock in - L.initializeLock(at: lock) + lock.initialize(to: .init()) } +#else + nonisolated(unsafe) let rawValue = rawValue + _storage = _Storage(rawValue) +#endif } var rawValue: T { - withLock { $0 } + withLock { rawValue in + nonisolated(unsafe) let rawValue = rawValue + return rawValue + } } +} +extension Locked { /// Acquire the lock and invoke a function while it is held. /// /// - Parameters: @@ -88,55 +76,27 @@ struct LockedWith: RawRepresentable where L: Lockable { /// This function can be used to synchronize access to shared data from a /// synchronous caller. Wherever possible, use actor isolation or other Swift /// concurrency tools. - nonmutating func withLock(_ body: (inout T) throws -> R) rethrows -> R where R: ~Copyable { - try _storage.withUnsafeMutablePointers { rawValue, lock in - L.unsafelyAcquireLock(at: lock) + func withLock(_ body: (inout T) throws -> sending R) rethrows -> sending R where R: ~Copyable { +#if SWT_TARGET_OS_APPLE && canImport(os) + nonisolated(unsafe) let result = try _storage.withUnsafeMutablePointers { rawValue, lock in + os_unfair_lock_lock(lock) defer { - L.unsafelyRelinquishLock(at: lock) + os_unfair_lock_unlock(lock) } return try body(&rawValue.pointee) } - } - - /// Acquire the lock and invoke a function while it is held, yielding both the - /// protected value and a reference to the underlying lock guarding it. - /// - /// - Parameters: - /// - body: A closure to invoke while the lock is held. - /// - /// - Returns: Whatever is returned by `body`. - /// - /// - Throws: Whatever is thrown by `body`. - /// - /// This function is equivalent to ``withLock(_:)`` except that the closure - /// passed to it also takes a reference to the underlying lock guarding this - /// instance's wrapped value. This function can be used when platform-specific - /// functionality such as a `pthread_cond_t` is needed. Because the caller has - /// direct access to the lock and is able to unlock and re-lock it, it is - /// unsafe to modify the protected value. - /// - /// - Warning: Callers that unlock the lock _must_ lock it again before the - /// closure returns. If the lock is not acquired when `body` returns, the - /// effect is undefined. - nonmutating func withUnsafeUnderlyingLock(_ body: (UnsafeMutablePointer, T) throws -> R) rethrows -> R where R: ~Copyable { - try withLock { value in - try _storage.withUnsafeMutablePointerToElements { lock in - try body(lock, value) - } + return result +#else + try _storage.mutex.withLock { rawValue in + try body(&rawValue) } +#endif } } -extension LockedWith: Sendable where T: Sendable {} - -/// A type that wraps a value requiring access from a synchronous caller during -/// concurrent execution and which uses the default platform-specific lock type -/// for the current platform. -typealias Locked = LockedWith - // MARK: - Additions -extension LockedWith where T: AdditiveArithmetic { +extension Locked where T: AdditiveArithmetic & Sendable { /// Add something to the current wrapped value of this instance. /// /// - Parameters: @@ -152,7 +112,7 @@ extension LockedWith where T: AdditiveArithmetic { } } -extension LockedWith where T: Numeric { +extension Locked where T: Numeric & Sendable { /// Increment the current wrapped value of this instance. /// /// - Returns: The sum of ``rawValue`` and `1`. @@ -172,7 +132,7 @@ extension LockedWith where T: Numeric { } } -extension LockedWith { +extension Locked { /// Initialize an instance of this type with a raw value of `nil`. init() where T == V? { self.init(rawValue: nil) @@ -188,3 +148,10 @@ extension LockedWith { self.init(rawValue: []) } } + +// MARK: - POSIX conveniences + +#if os(FreeBSD) || os(OpenBSD) +typealias pthread_mutex_t = _TestingInternals.pthread_mutex_t? +typealias pthread_cond_t = _TestingInternals.pthread_cond_t? +#endif diff --git a/Tests/TestingTests/Support/LockTests.swift b/Tests/TestingTests/Support/LockTests.swift index 2a41e4c1d..486143e1e 100644 --- a/Tests/TestingTests/Support/LockTests.swift +++ b/Tests/TestingTests/Support/LockTests.swift @@ -13,7 +13,9 @@ private import _TestingInternals @Suite("Locked Tests") struct LockTests { - func testLock(_ lock: LockedWith) { + @Test("Locking and unlocking") + func locking() { + let lock = Locked(rawValue: 0) #expect(lock.rawValue == 0) lock.withLock { value in value = 1 @@ -21,21 +23,9 @@ struct LockTests { #expect(lock.rawValue == 1) } - @Test("Platform-default lock") - func locking() { - testLock(Locked(rawValue: 0)) - } - -#if SWT_TARGET_OS_APPLE && !SWT_NO_OS_UNFAIR_LOCK - @Test("pthread_mutex_t (Darwin alternate)") - func lockingWith_pthread_mutex_t() { - testLock(LockedWith(rawValue: 0)) - } -#endif - - @Test("No lock") - func noLock() async { - let lock = LockedWith(rawValue: 0) + @Test("Repeatedly accessing a lock") + func lockRepeatedly() async { + let lock = Locked(rawValue: 0) await withTaskGroup { taskGroup in for _ in 0 ..< 100_000 { taskGroup.addTask { @@ -43,20 +33,6 @@ struct LockTests { } } } - #expect(lock.rawValue != 100_000) - } - - @Test("Get the underlying lock") - func underlyingLock() { - let lock = Locked(rawValue: 0) - testLock(lock) - lock.withUnsafeUnderlyingLock { underlyingLock, _ in - DefaultLock.unsafelyRelinquishLock(at: underlyingLock) - lock.withLock { value in - value += 1000 - } - DefaultLock.unsafelyAcquireLock(at: underlyingLock) - } - #expect(lock.rawValue == 1001) + #expect(lock.rawValue == 100_000) } } From d7836420db9744e4bfa39e30dc2a6af56f9cf487 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Fri, 3 Oct 2025 23:52:50 -0400 Subject: [PATCH 167/216] Fix typo on the BSDs --- Sources/Testing/Support/Locked.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Testing/Support/Locked.swift b/Sources/Testing/Support/Locked.swift index ae2448b35..fac062adb 100644 --- a/Sources/Testing/Support/Locked.swift +++ b/Sources/Testing/Support/Locked.swift @@ -8,7 +8,7 @@ // See https://swift.org/CONTRIBUTORS.txt for Swift project authors // -private import _TestingInternals +internal import _TestingInternals private import Synchronization /// A type that wraps a value requiring access from a synchronous caller during From 56506bf81f8d0b484be711539806e1adba02afe7 Mon Sep 17 00:00:00 2001 From: Hamish Knight Date: Sat, 4 Oct 2025 19:34:53 +0100 Subject: [PATCH 168/216] Revert "Adopt `Mutex`." (#1353) Reverts #1351, this is failing to build in CI (https://ci.swift.org/job/oss-swift-pr-test-ubuntu-22_04/9662/) --- Sources/Testing/CMakeLists.txt | 1 + Sources/Testing/ExitTests/WaitFor.swift | 43 +----- Sources/Testing/Support/Locked+Platform.swift | 97 +++++++++++++ Sources/Testing/Support/Locked.swift | 133 +++++++++++------- Tests/TestingTests/Support/LockTests.swift | 38 ++++- 5 files changed, 216 insertions(+), 96 deletions(-) create mode 100644 Sources/Testing/Support/Locked+Platform.swift diff --git a/Sources/Testing/CMakeLists.txt b/Sources/Testing/CMakeLists.txt index 68fec3b13..9776f70d3 100644 --- a/Sources/Testing/CMakeLists.txt +++ b/Sources/Testing/CMakeLists.txt @@ -89,6 +89,7 @@ add_library(Testing Support/Graph.swift Support/JSON.swift Support/Locked.swift + Support/Locked+Platform.swift Support/VersionNumber.swift Support/Versions.swift Discovery+Macro.swift diff --git a/Sources/Testing/ExitTests/WaitFor.swift b/Sources/Testing/ExitTests/WaitFor.swift index f0326ff3c..8c6ad52f3 100644 --- a/Sources/Testing/ExitTests/WaitFor.swift +++ b/Sources/Testing/ExitTests/WaitFor.swift @@ -80,42 +80,7 @@ func wait(for pid: consuming pid_t) async throws -> ExitStatus { } #elseif SWT_TARGET_OS_APPLE || os(Linux) || os(FreeBSD) || os(OpenBSD) /// A mapping of awaited child PIDs to their corresponding Swift continuations. -private nonisolated(unsafe) let _childProcessContinuations = { - let result = ManagedBuffer<[pid_t: CheckedContinuation], pthread_mutex_t>.create( - minimumCapacity: 1, - makingHeaderWith: { _ in [:] } - ) - - result.withUnsafeMutablePointers { _, lock in - _ = pthread_mutex_init(lock, nil) - } - - return result -}() - -/// Access the value in `_childProcessContinuations` while guarded by its lock. -/// -/// - Parameters: -/// - body: A closure to invoke while the lock is held. -/// -/// - Returns: Whatever is returned by `body`. -/// -/// - Throws: Whatever is thrown by `body`. -private func _withLockedChildProcessContinuations( - _ body: ( - _ childProcessContinuations: inout [pid_t: CheckedContinuation], - _ lock: UnsafeMutablePointer - ) throws -> R -) rethrows -> R { - try _childProcessContinuations.withUnsafeMutablePointers { childProcessContinuations, lock in - _ = pthread_mutex_lock(lock) - defer { - _ = pthread_mutex_unlock(lock) - } - - return try body(&childProcessContinuations.pointee, lock) - } -} +private let _childProcessContinuations = LockedWith]>() /// A condition variable used to suspend the waiter thread created by /// `_createWaitThread()` when there are no child processes to await. @@ -147,7 +112,7 @@ private let _createWaitThread: Void = { var siginfo = siginfo_t() if 0 == waitid(P_ALL, 0, &siginfo, WEXITED | WNOWAIT) { if case let pid = siginfo.si_pid, pid != 0 { - let continuation = _withLockedChildProcessContinuations { childProcessContinuations, _ in + let continuation = _childProcessContinuations.withLock { childProcessContinuations in childProcessContinuations.removeValue(forKey: pid) } @@ -168,7 +133,7 @@ private let _createWaitThread: Void = { // newly-scheduled waiter process. (If this condition is spuriously // woken, we'll just loop again, which is fine.) Note that we read errno // outside the lock in case acquiring the lock perturbs it. - _withLockedChildProcessContinuations { childProcessContinuations, lock in + _childProcessContinuations.withUnsafeUnderlyingLock { lock, childProcessContinuations in if childProcessContinuations.isEmpty { _ = pthread_cond_wait(_waitThreadNoChildrenCondition, lock) } @@ -240,7 +205,7 @@ func wait(for pid: consuming pid_t) async throws -> ExitStatus { _createWaitThread return try await withCheckedThrowingContinuation { continuation in - _withLockedChildProcessContinuations { childProcessContinuations, _ in + _childProcessContinuations.withLock { childProcessContinuations in // We don't need to worry about a race condition here because waitid() // does not clear the wait/zombie state of the child process. If it sees // the child process has terminated and manages to acquire the lock before diff --git a/Sources/Testing/Support/Locked+Platform.swift b/Sources/Testing/Support/Locked+Platform.swift new file mode 100644 index 000000000..a2ba82ac2 --- /dev/null +++ b/Sources/Testing/Support/Locked+Platform.swift @@ -0,0 +1,97 @@ +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2023–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 +// + +internal import _TestingInternals + +extension Never: Lockable { + static func initializeLock(at lock: UnsafeMutablePointer) {} + static func deinitializeLock(at lock: UnsafeMutablePointer) {} + static func unsafelyAcquireLock(at lock: UnsafeMutablePointer) {} + static func unsafelyRelinquishLock(at lock: UnsafeMutablePointer) {} +} + +#if SWT_TARGET_OS_APPLE && !SWT_NO_OS_UNFAIR_LOCK +extension os_unfair_lock_s: Lockable { + static func initializeLock(at lock: UnsafeMutablePointer) { + lock.initialize(to: .init()) + } + + static func deinitializeLock(at lock: UnsafeMutablePointer) { + // No deinitialization needed. + } + + static func unsafelyAcquireLock(at lock: UnsafeMutablePointer) { + os_unfair_lock_lock(lock) + } + + static func unsafelyRelinquishLock(at lock: UnsafeMutablePointer) { + os_unfair_lock_unlock(lock) + } +} +#endif + +#if os(FreeBSD) || os(OpenBSD) +typealias pthread_mutex_t = _TestingInternals.pthread_mutex_t? +typealias pthread_cond_t = _TestingInternals.pthread_cond_t? +#endif + +#if SWT_TARGET_OS_APPLE || os(Linux) || os(FreeBSD) || os(OpenBSD) || os(Android) || (os(WASI) && _runtime(_multithreaded)) +extension pthread_mutex_t: Lockable { + static func initializeLock(at lock: UnsafeMutablePointer) { + _ = pthread_mutex_init(lock, nil) + } + + static func deinitializeLock(at lock: UnsafeMutablePointer) { + _ = pthread_mutex_destroy(lock) + } + + static func unsafelyAcquireLock(at lock: UnsafeMutablePointer) { + _ = pthread_mutex_lock(lock) + } + + static func unsafelyRelinquishLock(at lock: UnsafeMutablePointer) { + _ = pthread_mutex_unlock(lock) + } +} +#endif + +#if os(Windows) +extension SRWLOCK: Lockable { + static func initializeLock(at lock: UnsafeMutablePointer) { + InitializeSRWLock(lock) + } + + static func deinitializeLock(at lock: UnsafeMutablePointer) { + // No deinitialization needed. + } + + static func unsafelyAcquireLock(at lock: UnsafeMutablePointer) { + AcquireSRWLockExclusive(lock) + } + + static func unsafelyRelinquishLock(at lock: UnsafeMutablePointer) { + ReleaseSRWLockExclusive(lock) + } +} +#endif + +#if SWT_TARGET_OS_APPLE && !SWT_NO_OS_UNFAIR_LOCK +typealias DefaultLock = os_unfair_lock +#elseif SWT_TARGET_OS_APPLE || os(Linux) || os(FreeBSD) || os(OpenBSD) || os(Android) || (os(WASI) && _runtime(_multithreaded)) +typealias DefaultLock = pthread_mutex_t +#elseif os(Windows) +typealias DefaultLock = SRWLOCK +#elseif os(WASI) +// No locks on WASI without multithreaded runtime. +typealias DefaultLock = Never +#else +#warning("Platform-specific implementation missing: locking unavailable") +typealias DefaultLock = Never +#endif diff --git a/Sources/Testing/Support/Locked.swift b/Sources/Testing/Support/Locked.swift index fac062adb..d1db8ef1f 100644 --- a/Sources/Testing/Support/Locked.swift +++ b/Sources/Testing/Support/Locked.swift @@ -9,7 +9,37 @@ // internal import _TestingInternals -private import Synchronization + +/// A protocol defining a type, generally platform-specific, that satisfies the +/// requirements of a lock or mutex. +protocol Lockable { + /// Initialize the lock at the given address. + /// + /// - Parameters: + /// - lock: A pointer to uninitialized memory that should be initialized as + /// an instance of this type. + static func initializeLock(at lock: UnsafeMutablePointer) + + /// Deinitialize the lock at the given address. + /// + /// - Parameters: + /// - lock: A pointer to initialized memory that should be deinitialized. + static func deinitializeLock(at lock: UnsafeMutablePointer) + + /// Acquire the lock at the given address. + /// + /// - Parameters: + /// - lock: The address of the lock to acquire. + static func unsafelyAcquireLock(at lock: UnsafeMutablePointer) + + /// Relinquish the lock at the given address. + /// + /// - Parameters: + /// - lock: The address of the lock to relinquish. + static func unsafelyRelinquishLock(at lock: UnsafeMutablePointer) +} + +// MARK: - /// A type that wraps a value requiring access from a synchronous caller during /// concurrent execution. @@ -22,48 +52,30 @@ private import Synchronization /// concurrency tools. /// /// This type is not part of the public interface of the testing library. -struct Locked { - /// A type providing storage for the underlying lock and wrapped value. -#if SWT_TARGET_OS_APPLE && canImport(os) - private typealias _Storage = ManagedBuffer -#else - private final class _Storage { - let mutex: Mutex - - init(_ rawValue: consuming sending T) { - mutex = Mutex(rawValue) +struct LockedWith: RawRepresentable where L: Lockable { + /// A type providing heap-allocated storage for an instance of ``Locked``. + private final class _Storage: ManagedBuffer { + deinit { + withUnsafeMutablePointerToElements { lock in + L.deinitializeLock(at: lock) + } } } -#endif /// Storage for the underlying lock and wrapped value. - private nonisolated(unsafe) var _storage: _Storage -} - -extension Locked: Sendable where T: Sendable {} + private nonisolated(unsafe) var _storage: ManagedBuffer -extension Locked: RawRepresentable { init(rawValue: T) { -#if SWT_TARGET_OS_APPLE && canImport(os) - _storage = .create(minimumCapacity: 1, makingHeaderWith: { _ in rawValue }) + _storage = _Storage.create(minimumCapacity: 1, makingHeaderWith: { _ in rawValue }) _storage.withUnsafeMutablePointerToElements { lock in - lock.initialize(to: .init()) + L.initializeLock(at: lock) } -#else - nonisolated(unsafe) let rawValue = rawValue - _storage = _Storage(rawValue) -#endif } var rawValue: T { - withLock { rawValue in - nonisolated(unsafe) let rawValue = rawValue - return rawValue - } + withLock { $0 } } -} -extension Locked { /// Acquire the lock and invoke a function while it is held. /// /// - Parameters: @@ -76,27 +88,55 @@ extension Locked { /// This function can be used to synchronize access to shared data from a /// synchronous caller. Wherever possible, use actor isolation or other Swift /// concurrency tools. - func withLock(_ body: (inout T) throws -> sending R) rethrows -> sending R where R: ~Copyable { -#if SWT_TARGET_OS_APPLE && canImport(os) - nonisolated(unsafe) let result = try _storage.withUnsafeMutablePointers { rawValue, lock in - os_unfair_lock_lock(lock) + nonmutating func withLock(_ body: (inout T) throws -> R) rethrows -> R where R: ~Copyable { + try _storage.withUnsafeMutablePointers { rawValue, lock in + L.unsafelyAcquireLock(at: lock) defer { - os_unfair_lock_unlock(lock) + L.unsafelyRelinquishLock(at: lock) } return try body(&rawValue.pointee) } - return result -#else - try _storage.mutex.withLock { rawValue in - try body(&rawValue) + } + + /// Acquire the lock and invoke a function while it is held, yielding both the + /// protected value and a reference to the underlying lock guarding it. + /// + /// - Parameters: + /// - body: A closure to invoke while the lock is held. + /// + /// - Returns: Whatever is returned by `body`. + /// + /// - Throws: Whatever is thrown by `body`. + /// + /// This function is equivalent to ``withLock(_:)`` except that the closure + /// passed to it also takes a reference to the underlying lock guarding this + /// instance's wrapped value. This function can be used when platform-specific + /// functionality such as a `pthread_cond_t` is needed. Because the caller has + /// direct access to the lock and is able to unlock and re-lock it, it is + /// unsafe to modify the protected value. + /// + /// - Warning: Callers that unlock the lock _must_ lock it again before the + /// closure returns. If the lock is not acquired when `body` returns, the + /// effect is undefined. + nonmutating func withUnsafeUnderlyingLock(_ body: (UnsafeMutablePointer, T) throws -> R) rethrows -> R where R: ~Copyable { + try withLock { value in + try _storage.withUnsafeMutablePointerToElements { lock in + try body(lock, value) + } } -#endif } } +extension LockedWith: Sendable where T: Sendable {} + +/// A type that wraps a value requiring access from a synchronous caller during +/// concurrent execution and which uses the default platform-specific lock type +/// for the current platform. +typealias Locked = LockedWith + // MARK: - Additions -extension Locked where T: AdditiveArithmetic & Sendable { +extension LockedWith where T: AdditiveArithmetic { /// Add something to the current wrapped value of this instance. /// /// - Parameters: @@ -112,7 +152,7 @@ extension Locked where T: AdditiveArithmetic & Sendable { } } -extension Locked where T: Numeric & Sendable { +extension LockedWith where T: Numeric { /// Increment the current wrapped value of this instance. /// /// - Returns: The sum of ``rawValue`` and `1`. @@ -132,7 +172,7 @@ extension Locked where T: Numeric & Sendable { } } -extension Locked { +extension LockedWith { /// Initialize an instance of this type with a raw value of `nil`. init() where T == V? { self.init(rawValue: nil) @@ -148,10 +188,3 @@ extension Locked { self.init(rawValue: []) } } - -// MARK: - POSIX conveniences - -#if os(FreeBSD) || os(OpenBSD) -typealias pthread_mutex_t = _TestingInternals.pthread_mutex_t? -typealias pthread_cond_t = _TestingInternals.pthread_cond_t? -#endif diff --git a/Tests/TestingTests/Support/LockTests.swift b/Tests/TestingTests/Support/LockTests.swift index 486143e1e..2a41e4c1d 100644 --- a/Tests/TestingTests/Support/LockTests.swift +++ b/Tests/TestingTests/Support/LockTests.swift @@ -13,9 +13,7 @@ private import _TestingInternals @Suite("Locked Tests") struct LockTests { - @Test("Locking and unlocking") - func locking() { - let lock = Locked(rawValue: 0) + func testLock(_ lock: LockedWith) { #expect(lock.rawValue == 0) lock.withLock { value in value = 1 @@ -23,9 +21,21 @@ struct LockTests { #expect(lock.rawValue == 1) } - @Test("Repeatedly accessing a lock") - func lockRepeatedly() async { - let lock = Locked(rawValue: 0) + @Test("Platform-default lock") + func locking() { + testLock(Locked(rawValue: 0)) + } + +#if SWT_TARGET_OS_APPLE && !SWT_NO_OS_UNFAIR_LOCK + @Test("pthread_mutex_t (Darwin alternate)") + func lockingWith_pthread_mutex_t() { + testLock(LockedWith(rawValue: 0)) + } +#endif + + @Test("No lock") + func noLock() async { + let lock = LockedWith(rawValue: 0) await withTaskGroup { taskGroup in for _ in 0 ..< 100_000 { taskGroup.addTask { @@ -33,6 +43,20 @@ struct LockTests { } } } - #expect(lock.rawValue == 100_000) + #expect(lock.rawValue != 100_000) + } + + @Test("Get the underlying lock") + func underlyingLock() { + let lock = Locked(rawValue: 0) + testLock(lock) + lock.withUnsafeUnderlyingLock { underlyingLock, _ in + DefaultLock.unsafelyRelinquishLock(at: underlyingLock) + lock.withLock { value in + value += 1000 + } + DefaultLock.unsafelyAcquireLock(at: underlyingLock) + } + #expect(lock.rawValue == 1001) } } From c6512ffe0f018768098082d3bd3209cd78473a9c Mon Sep 17 00:00:00 2001 From: Kelvin Bui <150134371+tienquocbui@users.noreply.github.com> Date: Mon, 6 Oct 2025 09:57:14 -0400 Subject: [PATCH 169/216] Refactor AdvancedConsoleOutputRecorder to remove fallback recorder dependency (#1354) --- .../Recorder/Event.AdvancedConsoleOutputRecorder.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/Testing/Events/Recorder/Event.AdvancedConsoleOutputRecorder.swift b/Sources/Testing/Events/Recorder/Event.AdvancedConsoleOutputRecorder.swift index 83bf12ef9..c00a9101a 100644 --- a/Sources/Testing/Events/Recorder/Event.AdvancedConsoleOutputRecorder.swift +++ b/Sources/Testing/Events/Recorder/Event.AdvancedConsoleOutputRecorder.swift @@ -142,8 +142,8 @@ extension Event { /// The write function for this recorder. let write: @Sendable (String) -> Void - /// The fallback console recorder for standard output. - private let _fallbackRecorder: Event.ConsoleOutputRecorder + /// The base console output options. + private let _baseOptions: Event.ConsoleOutputRecorder.Options /// Context storage for test information and results. private let _context: Locked<_Context> @@ -159,7 +159,7 @@ extension Event { init(options: Options = Options(), writingUsing write: @escaping @Sendable (String) -> Void) { self.options = options self.write = write - self._fallbackRecorder = Event.ConsoleOutputRecorder(options: options.base, writingUsing: write) + self._baseOptions = options.base self._context = Locked(rawValue: _Context()) self._humanReadableRecorder = Event.HumanReadableOutputRecorder() } @@ -258,7 +258,7 @@ extension Event.AdvancedConsoleOutputRecorder { // The hierarchical summary will be shown at the end switch eventKind { case .runStarted: - let symbol = Event.Symbol.default.stringValue(options: _fallbackRecorder.options) + let symbol = Event.Symbol.default.stringValue(options: _baseOptions) write("\(symbol) Test run started.\n") case .runEnded: From 81dd324e76217bcd03e0123a8e1e1b05290e03d5 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Mon, 6 Oct 2025 14:24:32 -0400 Subject: [PATCH 170/216] Remove some uses of `@unchecked Sendable` and `AnySequence`. (#1358) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR removes some outstanding uses of `@unchecked Sendable` and of `AnySequence`. Ironically we still need `Test.testCases` to type-erase with `AnySequence` _in its implementation only_ for now because if we change it from `some Sequence` to `any Sequence` we get this diagnostic: > 🛑 Runtime support for parameterized protocol types is only available in macOS > 13.0.0 or newer ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- Sources/Testing/Test.swift | 21 +++++++------------ .../_TestDiscovery/TestContentRecord.swift | 4 ++-- 2 files changed, 9 insertions(+), 16 deletions(-) diff --git a/Sources/Testing/Test.swift b/Sources/Testing/Test.swift index 5f2ac2406..52b41137d 100644 --- a/Sources/Testing/Test.swift +++ b/Sources/Testing/Test.swift @@ -69,26 +69,19 @@ public struct Test: Sendable { public nonisolated(unsafe) var xcTestCompatibleSelector: __XCTestCompatibleSelector? /// An enumeration describing the evaluation state of a test's cases. - /// - /// This use of `@unchecked Sendable` and of `AnySequence` in this type's - /// cases is necessary because it is not currently possible to express - /// `Sequence & Sendable` as an existential (`any`) - /// ([96960993](rdar://96960993)). It is also not possible to have a value of - /// an underlying generic sequence type without specifying its generic - /// parameters. - fileprivate enum TestCasesState: @unchecked Sendable { + fileprivate enum TestCasesState: Sendable { /// The test's cases have not yet been evaluated. /// /// - Parameters: /// - function: The function to call to evaluate the test's cases. The /// result is a sequence of test cases. - case unevaluated(_ function: @Sendable () async throws -> AnySequence) + case unevaluated(_ function: @Sendable () async throws -> any Sequence & Sendable) /// The test's cases have been evaluated. /// /// - Parameters: /// - testCases: The test's cases. - case evaluated(_ testCases: AnySequence) + case evaluated(_ testCases: any Sequence & Sendable) /// An error was thrown when the testing library attempted to evaluate the /// test's cases. @@ -124,7 +117,7 @@ public struct Test: Sendable { // attempt to run it, and thus never access this property. preconditionFailure("Attempting to access test cases with invalid state. Please file a bug report at https://github.com/swiftlang/swift-testing/issues/new and include this information: \(String(reflecting: testCasesState))") } - return testCases + return AnySequence(testCases) } } @@ -139,7 +132,7 @@ public struct Test: Sendable { var uncheckedTestCases: (some Sequence)? { testCasesState.flatMap { testCasesState in if case let .evaluated(testCases) = testCasesState { - return testCases + return AnySequence(testCases) } return nil } @@ -239,7 +232,7 @@ public struct Test: Sendable { self.sourceLocation = sourceLocation self.containingTypeInfo = containingTypeInfo self.xcTestCompatibleSelector = xcTestCompatibleSelector - self.testCasesState = .unevaluated { .init(try await testCases()) } + self.testCasesState = .unevaluated { try await testCases() } self.parameters = parameters } @@ -260,7 +253,7 @@ public struct Test: Sendable { self.sourceLocation = sourceLocation self.containingTypeInfo = containingTypeInfo self.xcTestCompatibleSelector = xcTestCompatibleSelector - self.testCasesState = .evaluated(.init(testCases)) + self.testCasesState = .evaluated(testCases) self.parameters = parameters } } diff --git a/Sources/_TestDiscovery/TestContentRecord.swift b/Sources/_TestDiscovery/TestContentRecord.swift index 384113c1b..b830026e2 100644 --- a/Sources/_TestDiscovery/TestContentRecord.swift +++ b/Sources/_TestDiscovery/TestContentRecord.swift @@ -85,7 +85,7 @@ public struct TestContentRecord where T: DiscoverableAsTestContent { public private(set) nonisolated(unsafe) var imageAddress: UnsafeRawPointer? /// A type defining storage for the underlying test content record. - private enum _RecordStorage: @unchecked Sendable { + private enum _RecordStorage { /// The test content record is stored by address. case atAddress(UnsafePointer<_TestContentRecord>) @@ -94,7 +94,7 @@ public struct TestContentRecord where T: DiscoverableAsTestContent { } /// Storage for `_record`. - private var _recordStorage: _RecordStorage + private nonisolated(unsafe) var _recordStorage: _RecordStorage /// The underlying test content record. private var _record: _TestContentRecord { From 533538b0fbd884e74375add42895014758310511 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Mon, 6 Oct 2025 15:43:31 -0400 Subject: [PATCH 171/216] Attachment lifetimes (#1319) This PR introduces a new experimental trait, `.savingAttachments(if:)`, that can be used to control whether a test's attachments are saved or not. XCTest has API around the [`XCTAttachment.Lifetime`](https://developer.apple.com/documentation/xctest/xctattachment/lifetime-swift.enum) enumeration that developers can use to control whether attachments are saved to a test report in Xcode. This enumeration has two cases: ```objc /* * Attachment will be kept regardless of the outcome of the test. */ XCTAttachmentLifetimeKeepAlways = 0, /* * Attachment will only be kept when the test fails, and deleted otherwise. */ XCTAttachmentLifetimeDeleteOnSuccess = 1 ``` I've opted to implement something a bit more granular. A developer can specify `.savingAttachments(if: .testFails)` and `.savingAttachments(if: .testPasses)` or can call some custom function of their own design like `runningInCI` or `hasPlentyOfFloppyDiskSpace`. The default behaviour if this trait is not used is to always save attachments, which is equivalent to `XCTAttachmentLifetimeKeepAlways`. `XCTAttachmentLifetimeDeleteOnSuccess` is, in effect, equivalent to `.savingAttachments(if: .testFails)`, but I hope reads a bit more clearly in context. Here's a usage example: ```swift @Test(.savingAttachments(if: .testFails)) func `best test ever`() { Attachment.record("...") // only saves to the test report or to disk if the // next line is uncommented. // Issue.record("sadness") } ``` I've taken the opportunity to update existing documentation for `Attachment` and `Attachable` to try to use more consistent language: a test records an attachment and then the testing library saves it (somewhere). I'm sure I've missed some spots, so please point them out if you see them. Resolves rdar://138921461. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- Sources/Testing/Attachments/Attachable.swift | 31 +- .../Attachments/AttachableWrapper.swift | 5 +- Sources/Testing/Attachments/Attachment.swift | 122 ++++--- Sources/Testing/CMakeLists.txt | 1 + Sources/Testing/Events/Event.swift | 10 + .../Running/Configuration+EventHandling.swift | 12 - .../Testing/Running/Runner.RuntimeState.swift | 14 +- .../Traits/AttachmentSavingTrait.swift | 336 ++++++++++++++++++ .../Traits/AttachmentSavingTraitTests.swift | 158 ++++++++ 9 files changed, 601 insertions(+), 88 deletions(-) create mode 100644 Sources/Testing/Traits/AttachmentSavingTrait.swift create mode 100644 Tests/TestingTests/Traits/AttachmentSavingTraitTests.swift diff --git a/Sources/Testing/Attachments/Attachable.swift b/Sources/Testing/Attachments/Attachable.swift index 9ec3ce8ad..8e2c06420 100644 --- a/Sources/Testing/Attachments/Attachable.swift +++ b/Sources/Testing/Attachments/Attachable.swift @@ -8,14 +8,15 @@ // See https://swift.org/CONTRIBUTORS.txt for Swift project authors // -/// A protocol describing a type that can be attached to a test report or -/// written to disk when a test is run. +private import _TestingInternals + +/// A protocol describing a type whose instances can be recorded and saved as +/// part of a test run. /// /// To attach an attachable value to a test, pass it to ``Attachment/record(_:named:sourceLocation:)``. /// To further configure an attachable value before you attach it, use it to /// initialize an instance of ``Attachment`` and set its properties before -/// passing it to ``Attachment/record(_:sourceLocation:)``. An attachable -/// value can only be attached to a test once. +/// passing it to ``Attachment/record(_:sourceLocation:)``. /// /// The testing library provides default conformances to this protocol for a /// variety of standard library types. Most user-defined types do not need to @@ -36,8 +37,8 @@ public protocol Attachable: ~Copyable { /// an attachment. /// /// The testing library uses this property to determine if an attachment - /// should be held in memory or should be immediately persisted to storage. - /// Larger attachments are more likely to be persisted, but the algorithm the + /// should be held in memory or should be immediately saved. Larger + /// attachments are more likely to be saved immediately, but the algorithm the /// testing library uses is an implementation detail and is subject to change. /// /// The value of this property is approximately equal to the number of bytes @@ -66,13 +67,12 @@ public protocol Attachable: ~Copyable { /// - Throws: Whatever is thrown by `body`, or any error that prevented the /// creation of the buffer. /// - /// The testing library uses this function when writing an attachment to a - /// test report or to a file on disk. The format of the buffer is - /// implementation-defined, but should be "idiomatic" for this type: for - /// example, if this type represents an image, it would be appropriate for - /// the buffer to contain an image in PNG format, JPEG format, etc., but it - /// would not be idiomatic for the buffer to contain a textual description of - /// the image. + /// The testing library uses this function when saving an attachment. The + /// format of the buffer is implementation-defined, but should be "idiomatic" + /// for this type: for example, if this type represents an image, it would be + /// appropriate for the buffer to contain an image in PNG format, JPEG format, + /// etc., but it would not be idiomatic for the buffer to contain a textual + /// description of the image. /// /// @Metadata { /// @Available(Swift, introduced: 6.2) @@ -91,9 +91,8 @@ public protocol Attachable: ~Copyable { /// - Returns: The preferred name for `attachment`. /// /// The testing library uses this function to determine the best name to use - /// when adding `attachment` to a test report or persisting it to storage. The - /// default implementation of this function returns `suggestedName` without - /// any changes. + /// when saving `attachment`. The default implementation of this function + /// returns `suggestedName` without any changes. /// /// @Metadata { /// @Available(Swift, introduced: 6.2) diff --git a/Sources/Testing/Attachments/AttachableWrapper.swift b/Sources/Testing/Attachments/AttachableWrapper.swift index d4b1cbe05..85d7ae9dc 100644 --- a/Sources/Testing/Attachments/AttachableWrapper.swift +++ b/Sources/Testing/Attachments/AttachableWrapper.swift @@ -8,9 +8,8 @@ // See https://swift.org/CONTRIBUTORS.txt for Swift project authors // -/// A protocol describing a type that can be attached to a test report or -/// written to disk when a test is run and which contains another value that it -/// stands in for. +/// A protocol describing a type whose instances can be recorded and saved as +/// part of a test run and which contains another value that it stands in for. /// /// To attach an attachable value to a test, pass it to ``Attachment/record(_:named:sourceLocation:)``. /// To further configure an attachable value before you attach it, use it to diff --git a/Sources/Testing/Attachments/Attachment.swift b/Sources/Testing/Attachments/Attachment.swift index b665b99fe..a17130176 100644 --- a/Sources/Testing/Attachments/Attachment.swift +++ b/Sources/Testing/Attachments/Attachment.swift @@ -13,11 +13,24 @@ private import _TestingInternals /// A type describing values that can be attached to the output of a test run /// and inspected later by the user. /// -/// Attachments are included in test reports in Xcode or written to disk when -/// tests are run at the command line. To create an attachment, you need a value -/// of some type that conforms to ``Attachable``. Initialize an instance of -/// ``Attachment`` with that value and, optionally, a preferred filename to use -/// when writing to disk. +/// To create an attachment, you need a value of some type that conforms to +/// ``Attachable``. Initialize an instance of ``Attachment`` with that value +/// and, optionally, a preferred filename to use when saving the attachment. To +/// record the attachment, call ``Attachment/record(_:sourceLocation:)``. +/// Alternatively, pass your attachable value directly to ``Attachment/record(_:named:sourceLocation:)``. +/// +/// By default, the testing library saves your attachments as soon as you call +/// ``Attachment/record(_:sourceLocation:)`` or +/// ``Attachment/record(_:named:sourceLocation:)``. You can access saved +/// attachments after your tests finish running: +/// +/// - When using Xcode, you can access attachments from the test report. +/// - When using Visual Studio Code, the testing library saves attachments to +/// `.build/attachments` by default. Visual Studio Code reports the paths to +/// individual attachments in its Tests Results panel. +/// - When using Swift Package Manager's `swift test` command, you can pass the +/// `--attachments-path` option. The testing library saves attachments to the +/// specified directory. /// /// @Metadata { /// @Available(Swift, introduced: 6.2) @@ -41,16 +54,17 @@ public struct Attachment where AttachableValue: Attachable & ~C /// Storage for ``attachableValue-7dyjv``. private var _storage: Storage - /// The path to which the this attachment was written, if any. + /// The path to which the this attachment was saved, if any. /// /// If a developer sets the ``Configuration/attachmentsPath`` property of the /// current configuration before running tests, or if a developer passes /// `--attachments-path` on the command line, then attachments will be - /// automatically written to disk when they are attached and the value of this - /// property will describe the path where they were written. + /// automatically saved when they are attached and the value of this property + /// will describe the paths where they were saved. A developer can use the + /// ``AttachmentSavingTrait`` trait type to defer or skip saving attachments. /// - /// If no destination path is set, or if an error occurred while writing this - /// attachment to disk, the value of this property is `nil`. + /// If no destination path is set, or if an error occurred while saving this + /// attachment, the value of this property is `nil`. @_spi(ForToolsIntegrationOnly) public var fileSystemPath: String? @@ -62,8 +76,7 @@ public struct Attachment where AttachableValue: Attachable & ~C /// Storage for ``preferredName``. fileprivate var _preferredName: String? - /// A filename to use when writing this attachment to a test report or to a - /// file on disk. + /// A filename to use when saving this attachment. /// /// The value of this property is used as a hint to the testing library. The /// testing library may substitute a different filename as needed. If the @@ -106,9 +119,9 @@ extension Attachment where AttachableValue: ~Copyable { /// - Parameters: /// - attachableValue: The value that will be attached to the output of the /// test run. - /// - preferredName: The preferred name of the attachment when writing it to - /// a test report or to disk. If `nil`, the testing library attempts to - /// derive a reasonable filename for the attached value. + /// - preferredName: The preferred name of the attachment to use when saving + /// it. If `nil`, the testing library attempts to generate a reasonable + /// filename for the attached value. /// - sourceLocation: The source location of the call to this initializer. /// This value is used when recording issues associated with the /// attachment. @@ -248,11 +261,11 @@ extension Attachment where AttachableValue: Sendable & ~Copyable { /// /// When `attachableValue` is an instance of a type that does not conform to /// the [`Sendable`](https://developer.apple.com/documentation/swift/sendable) - /// protocol, the testing library encodes it as data immediately. If - /// `attachableValue` throws an error when the testing library attempts to - /// encode it, the testing library records that error as an issue in the - /// current test and does not write the attachment to the test report or to - /// persistent storage. + /// protocol, the testing library calls its ``Attachable/withUnsafeBytes(for:_:)`` + /// immediately and records a copy of the resulting buffer instead. If + /// `attachableValue` throws an error when the testing library calls its + /// ``Attachable/withUnsafeBytes(for:_:)`` function, the testing library + /// records that error as an issue in the current test. /// /// @Metadata { /// @Available(Swift, introduced: 6.2) @@ -273,18 +286,18 @@ extension Attachment where AttachableValue: Sendable & ~Copyable { /// /// - Parameters: /// - attachableValue: The value to attach. - /// - preferredName: The preferred name of the attachment when writing it to - /// a test report or to disk. If `nil`, the testing library attempts to - /// derive a reasonable filename for the attached value. + /// - preferredName: The preferred name of the attachment to use when saving + /// it. If `nil`, the testing library attempts to generate a reasonable + /// filename for the attached value. /// - sourceLocation: The source location of the call to this function. /// /// When `attachableValue` is an instance of a type that does not conform to /// the [`Sendable`](https://developer.apple.com/documentation/swift/sendable) - /// protocol, the testing library encodes it as data immediately. If - /// `attachableValue` throws an error when the testing library attempts to - /// encode it, the testing library records that error as an issue in the - /// current test and does not write the attachment to the test report or to - /// persistent storage. + /// protocol, the testing library calls its ``Attachable/withUnsafeBytes(for:_:)`` + /// immediately and records a copy of the resulting buffer instead. If + /// `attachableValue` throws an error when the testing library calls its + /// ``Attachable/withUnsafeBytes(for:_:)`` function, the testing library + /// records that error as an issue in the current test. /// /// This function creates a new instance of ``Attachment`` and immediately /// attaches it to the current test. @@ -308,11 +321,11 @@ extension Attachment where AttachableValue: ~Copyable { /// /// When `attachableValue` is an instance of a type that does not conform to /// the [`Sendable`](https://developer.apple.com/documentation/swift/sendable) - /// protocol, the testing library encodes it as data immediately. If - /// `attachableValue` throws an error when the testing library attempts to - /// encode it, the testing library records that error as an issue in the - /// current test and does not write the attachment to the test report or to - /// persistent storage. + /// protocol, the testing library calls its ``Attachable/withUnsafeBytes(for:_:)`` + /// immediately and records a copy of the resulting buffer instead. If + /// `attachableValue` throws an error when the testing library calls its + /// ``Attachable/withUnsafeBytes(for:_:)`` function, the testing library + /// records that error as an issue in the current test. /// /// @Metadata { /// @Available(Swift, introduced: 6.2) @@ -332,18 +345,18 @@ extension Attachment where AttachableValue: ~Copyable { /// /// - Parameters: /// - attachableValue: The value to attach. - /// - preferredName: The preferred name of the attachment when writing it to - /// a test report or to disk. If `nil`, the testing library attempts to - /// derive a reasonable filename for the attached value. + /// - preferredName: The preferred name of the attachment to use when saving + /// it. If `nil`, the testing library attempts to generate a reasonable + /// filename for the attached value. /// - sourceLocation: The source location of the call to this function. /// /// When `attachableValue` is an instance of a type that does not conform to /// the [`Sendable`](https://developer.apple.com/documentation/swift/sendable) - /// protocol, the testing library encodes it as data immediately. If - /// `attachableValue` throws an error when the testing library attempts to - /// encode it, the testing library records that error as an issue in the - /// current test and does not write the attachment to the test report or to - /// persistent storage. + /// protocol, the testing library calls its ``Attachable/withUnsafeBytes(for:_:)`` + /// immediately and records a copy of the resulting buffer instead. If + /// `attachableValue` throws an error when the testing library calls its + /// ``Attachable/withUnsafeBytes(for:_:)`` function, the testing library + /// records that error as an issue in the current test. /// /// This function creates a new instance of ``Attachment`` and immediately /// attaches it to the current test. @@ -372,10 +385,9 @@ extension Attachment where AttachableValue: ~Copyable { /// - Throws: Whatever is thrown by `body`, or any error that prevented the /// creation of the buffer. /// - /// The testing library uses this function when writing an attachment to a - /// test report or to a file on disk. This function calls the - /// ``Attachable/withUnsafeBytes(for:_:)`` function on this attachment's - /// ``attachableValue-2tnj5`` property. + /// The testing library uses this function when saving an attachment. This + /// function calls the ``Attachable/withUnsafeBytes(for:_:)`` function on this + /// attachment's ``attachableValue-2tnj5`` property. /// /// @Metadata { /// @Available(Swift, introduced: 6.2) @@ -404,16 +416,16 @@ extension Attachment where AttachableValue: ~Copyable { /// is derived from the value of the ``Attachment/preferredName`` property. /// /// If you pass `--attachments-path` to `swift test`, the testing library - /// automatically uses this function to persist attachments to the directory - /// you specify. + /// automatically uses this function to save attachments to the directory you + /// specify. /// /// This function does not get or set the value of the attachment's /// ``fileSystemPath`` property. The caller is responsible for setting the /// value of this property if needed. /// - /// This function is provided as a convenience to allow tools authors to write - /// attachments to persistent storage the same way that Swift Package Manager - /// does. You are not required to use this function. + /// This function is provided as a convenience to allow tools authors to save + /// attachments the same way that Swift Package Manager does. You are not + /// required to use this function. @_spi(ForToolsIntegrationOnly) public borrowing func write(toFileInDirectoryAtPath directoryPath: String) throws -> String { try write( @@ -505,9 +517,9 @@ extension Configuration { /// - Returns: Whether or not to continue handling the event. /// /// This function is called automatically by ``handleEvent(_:in:)``. You do - /// not need to call it elsewhere. It automatically persists the attachment + /// not need to call it elsewhere. It automatically saves the attachment /// associated with `event` and modifies `event` to include the path where the - /// attachment was stored. + /// attachment was saved. func handleValueAttachedEvent(_ event: inout Event, in eventContext: borrowing Event.Context) -> Bool { guard let attachmentsPath else { // If there is no path to which attachments should be written, there's @@ -519,9 +531,9 @@ extension Configuration { preconditionFailure("Passed the wrong kind of event to \(#function) (expected valueAttached, got \(event.kind)). Please file a bug report at https://github.com/swiftlang/swift-testing/issues/new") } if attachment.fileSystemPath != nil { - // Somebody already persisted this attachment. This isn't necessarily a - // logic error in the testing library, but it probably means we shouldn't - // persist it again. Suppress the event. + // Somebody already saved this attachment. This isn't necessarily a logic + // error in the testing library, but it probably means we shouldn't save + // it again. Suppress the event. return false } diff --git a/Sources/Testing/CMakeLists.txt b/Sources/Testing/CMakeLists.txt index 9776f70d3..751bf1f64 100644 --- a/Sources/Testing/CMakeLists.txt +++ b/Sources/Testing/CMakeLists.txt @@ -100,6 +100,7 @@ add_library(Testing Test+Discovery.swift Test+Discovery+Legacy.swift Test+Macro.swift + Traits/AttachmentSavingTrait.swift Traits/Bug.swift Traits/Comment.swift Traits/Comment+Macro.swift diff --git a/Sources/Testing/Events/Event.swift b/Sources/Testing/Events/Event.swift index d8daa3e89..c6285f4f4 100644 --- a/Sources/Testing/Events/Event.swift +++ b/Sources/Testing/Events/Event.swift @@ -191,6 +191,16 @@ public struct Event: Sendable { /// The instant at which the event occurred. public var instant: Test.Clock.Instant +#if DEBUG + /// Whether or not this event was deferred. + /// + /// A deferred event is handled significantly later than when was posted. + /// + /// We currently use this property in our tests, but do not expose it as API + /// or SPI. We can expose it in the future if tools need it. + var wasDeferred: Bool = false +#endif + /// Initialize an instance of this type. /// /// - Parameters: diff --git a/Sources/Testing/Running/Configuration+EventHandling.swift b/Sources/Testing/Running/Configuration+EventHandling.swift index e3c189f8b..68eb2ab91 100644 --- a/Sources/Testing/Running/Configuration+EventHandling.swift +++ b/Sources/Testing/Running/Configuration+EventHandling.swift @@ -23,18 +23,6 @@ extension Configuration { var contextCopy = copy context contextCopy.configuration = self contextCopy.configuration?.eventHandler = { _, _ in } - -#if !SWT_NO_FILE_IO - if case .valueAttached = event.kind { - var eventCopy = copy event - guard handleValueAttachedEvent(&eventCopy, in: contextCopy) else { - // The attachment could not be handled, so suppress this event. - return - } - return eventHandler(eventCopy, contextCopy) - } -#endif - return eventHandler(event, contextCopy) } } diff --git a/Sources/Testing/Running/Runner.RuntimeState.swift b/Sources/Testing/Running/Runner.RuntimeState.swift index 6826be7a4..2d22002c5 100644 --- a/Sources/Testing/Running/Runner.RuntimeState.swift +++ b/Sources/Testing/Running/Runner.RuntimeState.swift @@ -47,9 +47,19 @@ extension Runner { return } - configuration.eventHandler = { [eventHandler = configuration.eventHandler] event, context in + configuration.eventHandler = { [oldEventHandler = configuration.eventHandler] event, context in +#if !SWT_NO_FILE_IO + var event = copy event + if case .valueAttached = event.kind { + guard let configuration = context.configuration, + configuration.handleValueAttachedEvent(&event, in: context) else { + // The attachment could not be handled, so suppress this event. + return + } + } +#endif RuntimeState.$current.withValue(existingRuntimeState) { - eventHandler(event, context) + oldEventHandler(event, context) } } } diff --git a/Sources/Testing/Traits/AttachmentSavingTrait.swift b/Sources/Testing/Traits/AttachmentSavingTrait.swift new file mode 100644 index 000000000..eed8085d5 --- /dev/null +++ b/Sources/Testing/Traits/AttachmentSavingTrait.swift @@ -0,0 +1,336 @@ +// +// 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 +// + +/// A type that defines a condition which must be satisfied for the testing +/// library to save attachments recorded by a test. +/// +/// To add this trait to a test, use one of the following functions: +/// +/// - ``Trait/savingAttachments(if:)`` +/// +/// By default, the testing library saves your attachments as soon as you call +/// ``Attachment/record(_:named:sourceLocation:)``. You can access saved +/// attachments after your tests finish running: +/// +/// - When using Xcode, you can access attachments from the test report. +/// - When using Visual Studio Code, the testing library saves attachments to +/// `.build/attachments` by default. Visual Studio Code reports the paths to +/// individual attachments in its Tests Results panel. +/// - When using Swift Package Manager's `swift test` command, you can pass the +/// `--attachments-path` option. The testing library saves attachments to the +/// specified directory. +/// +/// If you add an instance of this trait type to a test, any attachments that +/// test records are stored in memory until the test finishes running. The +/// testing library then evaluates the instance's condition and, if the +/// condition is met, saves the attachments. +@_spi(Experimental) +public struct AttachmentSavingTrait: TestTrait, SuiteTrait { + /// A type that describes the conditions under which the testing library + /// will save attachments. + /// + /// You can pass instances of this type to ``Trait/savingAttachments(if:)``. + public struct Condition: Sendable { + /// The testing library saves attachments if the test passes. + public static var testPasses: Self { + Self { !$0.hasFailed } + } + + /// The testing library saves attachments if the test fails. + public static var testFails: Self { + Self { $0.hasFailed } + } + + /// The testing library saves attachments if the test records a matching + /// issue. + /// + /// - Parameters: + /// - issueMatcher: A function to invoke when an issue occurs that is used + /// to determine if the testing library should save attachments for the + /// current test. + /// + /// - Returns: An instance of ``AttachmentSavingTrait/Condition`` that + /// evaluates `issueMatcher`. + public static func testRecordsIssue( + matching issueMatcher: @escaping @Sendable (_ issue: Issue) async throws -> Bool + ) -> Self { + Self(inspectsIssues: true) { context in + for issue in context.issues { + if try await issueMatcher(issue) { + return true + } + } + return false + } + } + + /// Whether or not this condition needs to inspect individual issues (which + /// implies a slower path.) + fileprivate var inspectsIssues = false + + /// The condition function. + fileprivate var body: @Sendable (borrowing Context) async throws -> Bool + } + + /// This instance's condition. + var condition: Condition + + /// The source location where this trait is specified. + var sourceLocation: SourceLocation + + public var isRecursive: Bool { + true + } +} + +// MARK: - TestScoping + +extension AttachmentSavingTrait: TestScoping { + /// A type representing the per-test-case context for this trait. + /// + /// An instance of this type is created for each scope this trait provides. + /// When the scope ends, the context is then passed to the trait's condition + /// function for evaluation. + fileprivate struct Context: Sendable { + /// The set of events that were deferred for later conditional handling. + var deferredEvents = [Event]() + + /// Whether or not the current test case has recorded a failing issue. + var hasFailed = false + + /// All issues recorded within the scope of the current test case. + var issues = [Issue]() + } + + public func scopeProvider(for test: Test, testCase: Test.Case?) -> Self? { + // This function should apply directly to test cases only. It doesn't make + // sense to apply it to suites or test functions since they don't run their + // own code. + // + // NOTE: this trait can't reliably affect attachments recorded when other + // traits are evaluated (we may need a new scope in the TestScoping protocol + // for that.) + testCase != nil ? self : nil + } + + public func provideScope(for test: Test, testCase: Test.Case?, performing function: @Sendable () async throws -> Void) async throws { + guard var configuration = Configuration.current else { + throw SystemError(description: "There is no current Configuration when attempting to provide scope for test '\(test.name)'. Please file a bug report at https://github.com/swiftlang/swift-testing/issues/new") + } + let oldConfiguration = configuration + + let context = Locked(rawValue: Context()) + configuration.eventHandler = { event, eventContext in + var eventDeferred = false + defer { + if !eventDeferred { + oldConfiguration.eventHandler(event, eventContext) + } + } + + // Guard against events generated in unstructured tasks or outside a test + // function body (where testCase shouldn't be nil). + guard eventContext.test == test && eventContext.testCase != nil else { + return + } + + switch event.kind { + case .valueAttached: + // Defer this event until the current test or test case ends. + eventDeferred = true + context.withLock { context in + context.deferredEvents.append(event) + } + + case let .issueRecorded(issue): + if condition.inspectsIssues { + context.withLock { context in + if issue.isFailure { + context.hasFailed = true + } + context.issues.append(issue) + } + } else if issue.isFailure { + context.withLock { context in + context.hasFailed = true + } + } + + default: + break + } + } + + // TODO: adopt async defer if/when we get it + let result: Result + do { + result = try await .success(Configuration.withCurrent(configuration, perform: function)) + } catch { + result = .failure(error) + } + await _handleDeferredEvents(in: context.rawValue, for: test, testCase: testCase, configuration: oldConfiguration) + return try result.get() + } + + /// Handle any deferred events for a given test and test case. + /// + /// - Parameters: + /// - context: A context structure containing the deferred events to handle. + /// - test: The test for which events were recorded. + /// - testCase The test case for which events were recorded, if any. + /// - configuration: The configuration to pass events to. + private func _handleDeferredEvents(in context: consuming Context, for test: Test, testCase: Test.Case?, configuration: Configuration) async { + if context.deferredEvents.isEmpty { + // Never mind... + return + } + + await Issue.withErrorRecording(at: sourceLocation, configuration: configuration) { + // Evaluate the condition. + guard try await condition.body(context) else { + return + } + + // Finally issue the attachment-recorded events that we deferred. + let eventContext = Event.Context(test: test, testCase: testCase, configuration: configuration) + for event in context.deferredEvents { +#if DEBUG + var event = event + event.wasDeferred = true +#endif + configuration.eventHandler(event, eventContext) + } + } + } +} + +// MARK: - + +@_spi(Experimental) +extension Trait where Self == AttachmentSavingTrait { + /// Constructs a trait that tells the testing library to only save attachments + /// if a given condition is met. + /// + /// - Parameters: + /// - condition: A condition which, when met, means that the testing library + /// should save attachments that the current test has recorded. If the + /// condition is not met, the testing library discards the test's + /// attachments when the test ends. + /// - sourceLocation: The source location of the trait. + /// + /// - Returns: An instance of ``AttachmentSavingTrait`` that evaluates the + /// closure you provide. + /// + /// By default, the testing library saves your attachments as soon as you call + /// ``Attachment/record(_:named:sourceLocation:)``. You can access saved + /// attachments after your tests finish running: + /// + /// - When using Xcode, you can access attachments from the test report. + /// - When using Visual Studio Code, the testing library saves attachments to + /// `.build/attachments` by default. Visual Studio Code reports the paths to + /// individual attachments in its Tests Results panel. + /// - When using Swift Package Manager's `swift test` command, you can pass + /// the `--attachments-path` option. The testing library saves attachments + /// to the specified directory. + /// + /// If you add this trait to a test, any attachments that test records are + /// stored in memory until the test finishes running. The testing library then + /// evaluates `condition` and, if the condition is met, saves the attachments. + public static func savingAttachments( + if condition: Self.Condition, + sourceLocation: SourceLocation = #_sourceLocation + ) -> Self { + Self(condition: condition, sourceLocation: sourceLocation) + } + + /// Constructs a trait that tells the testing library to only save attachments + /// if a given condition is met. + /// + /// - Parameters: + /// - condition: A closure that contains the trait's custom condition logic. + /// If this closure returns `true`, the trait tells the testing library to + /// save attachments that the current test has recorded. If this closure + /// returns `false`, the testing library discards the test's attachments + /// when the test ends. If this closure throws an error, the testing + /// library records that error as an issue and discards the test's + /// attachments. + /// - sourceLocation: The source location of the trait. + /// + /// - Returns: An instance of ``AttachmentSavingTrait`` that evaluates the + /// closure you provide. + /// + /// By default, the testing library saves your attachments as soon as you call + /// ``Attachment/record(_:named:sourceLocation:)``. You can access saved + /// attachments after your tests finish running: + /// + /// - When using Xcode, you can access attachments from the test report. + /// - When using Visual Studio Code, the testing library saves attachments + ///  to `.build/attachments` by default. Visual Studio Code reports the paths + ///  to individual attachments in its Tests Results panel. + /// - When using Swift Package Manager's `swift test` command, you can pass + /// the `--attachments-path` option. The testing library saves attachments + /// to the specified directory. + /// + /// If you add this trait to a test, any attachments that test records are + /// stored in memory until the test finishes running. The testing library then + /// evaluates `condition` and, if the condition is met, saves the attachments. + public static func savingAttachments( + if condition: @autoclosure @escaping @Sendable () throws -> Bool, + sourceLocation: SourceLocation = #_sourceLocation + ) -> Self { + let condition = Self.Condition { _ in try condition() } + return savingAttachments(if: condition, sourceLocation: sourceLocation) + } + + /// Constructs a trait that tells the testing library to only save attachments + /// if a given condition is met. + /// + /// - Parameters: + /// - condition: A closure that contains the trait's custom condition logic. + /// If this closure returns `true`, the trait tells the testing library to + /// save attachments that the current test has recorded. If this closure + /// returns `false`, the testing library discards the test's attachments + /// when the test ends. If this closure throws an error, the testing + /// library records that error as an issue and discards the test's + /// attachments. + /// - sourceLocation: The source location of the trait. + /// + /// - Returns: An instance of ``AttachmentSavingTrait`` that evaluates the + /// closure you provide. + /// + /// By default, the testing library saves your attachments as soon as you call + /// ``Attachment/record(_:named:sourceLocation:)``. You can access saved + /// attachments after your tests finish running: + /// + /// - When using Xcode, you can access attachments from the test report. + /// - When using Visual Studio Code, the testing library saves attachments + ///  to `.build/attachments` by default. Visual Studio Code reports the paths + ///  to individual attachments in its Tests Results panel. + /// - When using Swift Package Manager's `swift test` command, you can pass + /// the `--attachments-path` option. The testing library saves attachments + /// to the specified directory. + /// + /// If you add this trait to a test, any attachments that test records are + /// stored in memory until the test finishes running. The testing library then + /// evaluates `condition` and, if the condition is met, saves the attachments. + /// + /// @Comment { + /// - Bug: `condition` cannot be `async` without making this function + /// `async` even though `condition` is not evaluated locally. + /// ([103037177](rdar://103037177)) + /// } + public static func savingAttachments( + if condition: @escaping @Sendable () async throws -> Bool, + sourceLocation: SourceLocation = #_sourceLocation + ) -> Self { + let condition = Self.Condition { _ in try await condition() } + return savingAttachments(if: condition, sourceLocation: sourceLocation) + } +} diff --git a/Tests/TestingTests/Traits/AttachmentSavingTraitTests.swift b/Tests/TestingTests/Traits/AttachmentSavingTraitTests.swift new file mode 100644 index 000000000..1ea63eeda --- /dev/null +++ b/Tests/TestingTests/Traits/AttachmentSavingTraitTests.swift @@ -0,0 +1,158 @@ +// +// 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 +// + +@testable @_spi(Experimental) @_spi(ForToolsIntegrationOnly) import Testing + +struct `AttachmentSavingTrait tests` { + func runAttachmentSavingTests(with trait: AttachmentSavingTrait?, expectedCount: Int, expectedIssueCount: Int = Self.issueCountFromTestBodies, expectedPreferredName: String?) async throws { + let traitToApply = trait as (any SuiteTrait)? ?? Self.currentAttachmentSavingTrait + try await Self.$currentAttachmentSavingTrait.withValue(traitToApply) { + try await confirmation("Issue recorded", expectedCount: expectedIssueCount) { issueRecorded in + try await confirmation("Attachment detected", expectedCount: expectedCount) { valueAttached in + var configuration = Configuration() + configuration.attachmentsPath = try temporaryDirectory() + configuration.eventHandler = { event, _ in + switch event.kind { + case .issueRecorded: + issueRecorded() + case let .valueAttached(attachment): +#if DEBUG + if trait != nil { + #expect(event.wasDeferred) + } +#endif + if let expectedPreferredName { + #expect(attachment.preferredName == expectedPreferredName) + } + valueAttached() + default: + break + } + } + + await runTest(for: FixtureSuite.self, configuration: configuration) + } + } + } + } + + @Test func `Saving attachments without conditions`() async throws { + try await runAttachmentSavingTests( + with: nil, + expectedCount: Self.totalTestCaseCount, + expectedPreferredName: nil + ) + } + + @Test func `Saving attachments only on test pass`() async throws { + try await runAttachmentSavingTests( + with: .savingAttachments(if: .testPasses), + expectedCount: Self.passingTestCaseCount, + expectedPreferredName: "PASSING TEST" + ) + } + + @Test func `Saving attachments with warning issue`() async throws { + try await runAttachmentSavingTests( + with: .savingAttachments(if: .testRecordsIssue { $0.severity == .warning }), + expectedCount: Self.warningTestCaseCount, + expectedPreferredName: "PASSING TEST" + ) + } + + @Test func `Saving attachments only on test failure`() async throws { + try await runAttachmentSavingTests( + with: .savingAttachments(if: .testFails), + expectedCount: Self.failingTestCaseCount, + expectedPreferredName: "FAILING TEST" + ) + } + + @Test func `Saving attachments with custom condition`() async throws { + try await runAttachmentSavingTests( + with: .savingAttachments(if: true), + expectedCount: Self.totalTestCaseCount, + expectedPreferredName: nil + ) + + try await runAttachmentSavingTests( + with: .savingAttachments(if: false), + expectedCount: 0, + expectedPreferredName: nil + ) + } + + @Test func `Saving attachments with custom async condition`() async throws { + @Sendable func conditionFunction() async -> Bool { + true + } + + try await runAttachmentSavingTests( + with: .savingAttachments(if: conditionFunction), + expectedCount: Self.totalTestCaseCount, + expectedPreferredName: nil + ) + } + + @Test func `Saving attachments but the condition throws`() async throws { + @Sendable func conditionFunction() throws -> Bool { + throw MyError() + } + + try await runAttachmentSavingTests( + with: .savingAttachments(if: conditionFunction), + expectedCount: 0, + expectedIssueCount: Self.issueCountFromTestBodies + Self.totalTestCaseCount /* thrown from conditionFunction */, + expectedPreferredName: nil + ) + } +} + +// MARK: - Fixtures + +extension `AttachmentSavingTrait tests` { + static let totalTestCaseCount = passingTestCaseCount + failingTestCaseCount + static let passingTestCaseCount = 1 + 5 + warningTestCaseCount + static let warningTestCaseCount = 1 + static let failingTestCaseCount = 1 + 7 + static let issueCountFromTestBodies = warningTestCaseCount + failingTestCaseCount + + @TaskLocal + static var currentAttachmentSavingTrait: any SuiteTrait = Comment(rawValue: "") + + @Suite(.hidden, currentAttachmentSavingTrait) + struct FixtureSuite { + @Test(.hidden) func `Records an attachment (passing)`() { + Attachment.record("", named: "PASSING TEST") + } + + @Test(.hidden) func `Records an attachment (warning)`() { + Attachment.record("", named: "PASSING TEST") + Issue.record("", severity: .warning) + } + + @Test(.hidden) func `Records an attachment (failing)`() { + Attachment.record("", named: "FAILING TEST") + Issue.record("") + } + + @Test(.hidden, arguments: 0 ..< 5) + func `Records an attachment (passing, parameterized)`(i: Int) async { + Attachment.record("\(i)", named: "PASSING TEST") + } + + @Test(.hidden, arguments: 0 ..< 7) // intentionally different count + func `Records an attachment (failing, parameterized)`(i: Int) async { + Attachment.record("\(i)", named: "FAILING TEST") + Issue.record("\(i)") + } + } +} + From 75d5929078de3707c7229d26e05e046afa044cf4 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Mon, 6 Oct 2025 16:38:51 -0400 Subject: [PATCH 172/216] Support command-line arguments specified as `--foo=bar`. (#1357) SwiftPM and `swift test` use Swift Argument Parser which allows developers to specify arguments of the form `--foo=bar`. Our bare-bones argument parser doesn't currently recognize that pattern, which means that the developer could write `--foo=bar` but get the wrong behavior. This PR adds support for that pattern by changing how we parse command-line arguments to allow for both `--foo bar` and `--foo=bar`. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- .../Testing/ABI/EntryPoints/EntryPoint.swift | 95 ++++++++++++------- Tests/TestingTests/SwiftPMTests.swift | 28 ++++++ 2 files changed, 89 insertions(+), 34 deletions(-) diff --git a/Sources/Testing/ABI/EntryPoints/EntryPoint.swift b/Sources/Testing/ABI/EntryPoints/EntryPoint.swift index 727d91632..f4f1a751c 100644 --- a/Sources/Testing/ABI/EntryPoints/EntryPoint.swift +++ b/Sources/Testing/ABI/EntryPoints/EntryPoint.swift @@ -352,6 +352,45 @@ extension __CommandLineArguments_v0: Codable { } } +extension RandomAccessCollection { + /// Get the value of the command line argument with the given name. + /// + /// - Parameters: + /// - label: The label or name of the argument, e.g. `"--attachments-path"`. + /// - index: The index where `label` should be found, or `nil` to search the + /// entire collection. + /// + /// - Returns: The value of the argument named by `label` at `index`. If no + /// value is available, or if `index` is not `nil` and the argument at + /// `index` is not named `label`, returns `nil`. + /// + /// This function handles arguments of the form `--label value` and + /// `--label=value`. Other argument syntaxes are not supported. + fileprivate func argumentValue(forLabel label: String, at index: Index? = nil) -> String? { + guard let index else { + return indices.lazy + .compactMap { argumentValue(forLabel: label, at: $0) } + .first + } + + let element = self[index] + if element == label { + let nextIndex = self.index(after: index) + if nextIndex < endIndex { + return self[nextIndex] + } + } else { + // Find an element equal to something like "--foo=bar" and split it. + let prefix = "\(label)=" + if element.hasPrefix(prefix), let equalsIndex = element.firstIndex(of: "=") { + return String(element[equalsIndex...].dropFirst()) + } + } + + return nil + } +} + /// Initialize this instance given a sequence of command-line arguments passed /// from Swift Package Manager. /// @@ -366,10 +405,6 @@ func parseCommandLineArguments(from args: [String]) throws -> __CommandLineArgum // Do not consider the executable path AKA argv[0]. let args = args.dropFirst() - func isLastArgument(at index: [String].Index) -> Bool { - args.index(after: index) >= args.endIndex - } - #if !SWT_NO_FILE_IO #if canImport(Foundation) // Configuration for the test run passed in as a JSON file (experimental) @@ -379,9 +414,7 @@ func parseCommandLineArguments(from args: [String]) throws -> __CommandLineArgum // NOTE: While the output event stream is opened later, it is necessary to // open the configuration file early (here) in order to correctly construct // the resulting __CommandLineArguments_v0 instance. - if let configurationIndex = args.firstIndex(of: "--configuration-path") ?? args.firstIndex(of: "--experimental-configuration-path"), - !isLastArgument(at: configurationIndex) { - let path = args[args.index(after: configurationIndex)] + if let path = args.argumentValue(forLabel: "--configuration-path") ?? args.argumentValue(forLabel: "--experimental-configuration-path") { let file = try FileHandle(forReadingAtPath: path) let configurationJSON = try file.readToEnd() result = try configurationJSON.withUnsafeBufferPointer { configurationJSON in @@ -394,24 +427,22 @@ func parseCommandLineArguments(from args: [String]) throws -> __CommandLineArgum } // Event stream output - if let eventOutputIndex = args.firstIndex(of: "--event-stream-output-path") ?? args.firstIndex(of: "--experimental-event-stream-output"), - !isLastArgument(at: eventOutputIndex) { - result.eventStreamOutputPath = args[args.index(after: eventOutputIndex)] + if let path = args.argumentValue(forLabel: "--event-stream-output-path") ?? args.argumentValue(forLabel: "--experimental-event-stream-output") { + result.eventStreamOutputPath = path } + // Event stream version do { - var eventOutputVersionIndex: Array.Index? + var versionString: String? var allowExperimental = false - eventOutputVersionIndex = args.firstIndex(of: "--event-stream-version") - if eventOutputVersionIndex == nil { - eventOutputVersionIndex = args.firstIndex(of: "--experimental-event-stream-version") - if eventOutputVersionIndex != nil { + versionString = args.argumentValue(forLabel: "--event-stream-version") + if versionString == nil { + versionString = args.argumentValue(forLabel: "--experimental-event-stream-version") + if versionString != nil { allowExperimental = true } } - if let eventOutputVersionIndex, !isLastArgument(at: eventOutputVersionIndex) { - let versionString = args[args.index(after: eventOutputVersionIndex)] - + if let versionString { // If the caller specified a version that could not be parsed, treat it as // an invalid argument. guard let eventStreamVersion = VersionNumber(versionString) else { @@ -432,14 +463,13 @@ func parseCommandLineArguments(from args: [String]) throws -> __CommandLineArgum #endif // XML output - if let xunitOutputIndex = args.firstIndex(of: "--xunit-output"), !isLastArgument(at: xunitOutputIndex) { - result.xunitOutput = args[args.index(after: xunitOutputIndex)] + if let xunitOutputPath = args.argumentValue(forLabel: "--xunit-output") { + result.xunitOutput = xunitOutputPath } // Attachment output - if let attachmentsPathIndex = args.firstIndex(of: "--attachments-path") ?? args.firstIndex(of: "--experimental-attachments-path"), - !isLastArgument(at: attachmentsPathIndex) { - result.attachmentsPath = args[args.index(after: attachmentsPathIndex)] + if let attachmentsPath = args.argumentValue(forLabel: "--attachments-path") ?? args.argumentValue(forLabel: "--experimental-attachments-path") { + result.attachmentsPath = attachmentsPath } #endif @@ -457,13 +487,12 @@ func parseCommandLineArguments(from args: [String]) throws -> __CommandLineArgum } // Whether or not to symbolicate backtraces in the event stream. - if let symbolicateBacktracesIndex = args.firstIndex(of: "--symbolicate-backtraces"), !isLastArgument(at: symbolicateBacktracesIndex) { - result.symbolicateBacktraces = args[args.index(after: symbolicateBacktracesIndex)] + if let symbolicateBacktraces = args.argumentValue(forLabel: "--symbolicate-backtraces") { + result.symbolicateBacktraces = symbolicateBacktraces } // Verbosity - if let verbosityIndex = args.firstIndex(of: "--verbosity"), !isLastArgument(at: verbosityIndex), - let verbosity = Int(args[args.index(after: verbosityIndex)]) { + if let verbosity = args.argumentValue(forLabel: "--verbosity").flatMap(Int.init) { result.verbosity = verbosity } if args.contains("--verbose") || args.contains("-v") { @@ -478,9 +507,7 @@ func parseCommandLineArguments(from args: [String]) throws -> __CommandLineArgum // Filtering func filterValues(forArgumentsWithLabel label: String) -> [String] { - args.indices.lazy - .filter { args[$0] == label && $0 < args.endIndex } - .map { args[args.index(after: $0)] } + args.indices.compactMap { args.argumentValue(forLabel: label, at: $0) } } let filter = filterValues(forArgumentsWithLabel: "--filter") if !filter.isEmpty { @@ -492,11 +519,11 @@ func parseCommandLineArguments(from args: [String]) throws -> __CommandLineArgum } // Set up the iteration policy for the test run. - if let repetitionsIndex = args.firstIndex(of: "--repetitions"), !isLastArgument(at: repetitionsIndex) { - result.repetitions = Int(args[args.index(after: repetitionsIndex)]) + if let repetitions = args.argumentValue(forLabel: "--repetitions").flatMap(Int.init) { + result.repetitions = repetitions } - if let repeatUntilIndex = args.firstIndex(of: "--repeat-until"), !isLastArgument(at: repeatUntilIndex) { - result.repeatUntil = args[args.index(after: repeatUntilIndex)] + if let repeatUntil = args.argumentValue(forLabel: "--repeat-until") { + result.repeatUntil = repeatUntil } return result diff --git a/Tests/TestingTests/SwiftPMTests.swift b/Tests/TestingTests/SwiftPMTests.swift index 4543d3932..4668fbb25 100644 --- a/Tests/TestingTests/SwiftPMTests.swift +++ b/Tests/TestingTests/SwiftPMTests.swift @@ -145,6 +145,13 @@ struct SwiftPMTests { #expect(planTests.contains(test2)) } + @Test("--filter or --skip argument as last argument") + @available(_regexAPI, *) + func filterOrSkipAsLast() async throws { + _ = try configurationForEntryPoint(withArguments: ["PATH", "--filter"]) + _ = try configurationForEntryPoint(withArguments: ["PATH", "--skip"]) + } + @Test(".hidden trait", .tags(.traitRelated)) func hidden() async throws { let configuration = try configurationForEntryPoint(withArguments: ["PATH"]) @@ -492,4 +499,25 @@ struct SwiftPMTests { let args = try parseCommandLineArguments(from: ["PATH", "--verbosity", "12345"]) #expect(args.verbosity == 12345) } + + @Test("--foo=bar form") + func equalsSignForm() throws { + // We can split the string and parse the result correctly. + do { + let args = try parseCommandLineArguments(from: ["PATH", "--verbosity=12345"]) + #expect(args.verbosity == 12345) + } + + // We don't overrun the string and correctly handle empty values. + do { + let args = try parseCommandLineArguments(from: ["PATH", "--xunit-output="]) + #expect(args.xunitOutput == "") + } + + // We split at the first equals-sign. + do { + let args = try parseCommandLineArguments(from: ["PATH", "--xunit-output=abc=123"]) + #expect(args.xunitOutput == "abc=123") + } + } } From ffda98aaf56773fd931f06670ca12b9eb3d027b3 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Wed, 8 Oct 2025 17:37:46 -0400 Subject: [PATCH 173/216] Add support for experimental "custom availability domains" feature. (#1362) This PR updates our parsing of the `@available` attribute during expansion of the `@Test` and `@Suite` macros to support custom availability domains of the form: ```swift @available(Foo) @Test func f() {} ``` This change means the macro expansion will include `if #available(Foo)` instead of `if #available(Foo, *)` in this case. Resolves rdar://160323829. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- .../WithAttributesSyntaxAdditions.swift | 21 ++++++++++++++----- .../Support/AvailabilityGuards.swift | 11 +++++++--- .../TestDeclarationMacroTests.swift | 7 ++++++- 3 files changed, 30 insertions(+), 9 deletions(-) diff --git a/Sources/TestingMacros/Support/Additions/WithAttributesSyntaxAdditions.swift b/Sources/TestingMacros/Support/Additions/WithAttributesSyntaxAdditions.swift index aa778a00c..3c42b6fb3 100644 --- a/Sources/TestingMacros/Support/Additions/WithAttributesSyntaxAdditions.swift +++ b/Sources/TestingMacros/Support/Additions/WithAttributesSyntaxAdditions.swift @@ -61,16 +61,17 @@ extension WithAttributesSyntax { }.first var lastPlatformName: TokenSyntax? = nil - var asteriskEncountered = false + var wildcardEncountered = false + let hasWildcard = entries.contains(where: \.isWildcard) return entries.compactMap { entry in switch entry { case let .availabilityVersionRestriction(restriction) where whenKeyword == .introduced: - return Availability(attribute: attribute, platformName: restriction.platform, version: restriction.version, message: message) + return Availability(attribute: attribute, platformName: restriction.platform, version: restriction.version, mayNeedTrailingWildcard: hasWildcard, message: message) case let .token(token): if case .identifier = token.tokenKind { lastPlatformName = token - } else if case let .binaryOperator(op) = token.tokenKind, op == "*" { - asteriskEncountered = true + } else if entry.isWildcard { + wildcardEncountered = true // It is syntactically valid to specify a platform name without a // version in an availability declaration, and it's used to resolve // a custom availability definition specified via the @@ -81,7 +82,7 @@ extension WithAttributesSyntax { return Availability(attribute: attribute, platformName: lastPlatformName, version: nil, message: message) } } else if case let .keyword(keyword) = token.tokenKind, keyword == whenKeyword { - if asteriskEncountered { + if wildcardEncountered { // Match the "always this availability" construct, i.e. // `@available(*, deprecated)` and `@available(*, unavailable)`. return Availability(attribute: attribute, platformName: lastPlatformName, version: nil, message: message) @@ -144,3 +145,13 @@ extension AttributeSyntax { .joined() } } + +extension AvailabilityArgumentSyntax.Argument { + var isWildcard: Bool { + if case let .token(token) = self, + case let .binaryOperator(op) = token.tokenKind, op == "*" { + return true + } + return false + } +} diff --git a/Sources/TestingMacros/Support/AvailabilityGuards.swift b/Sources/TestingMacros/Support/AvailabilityGuards.swift index deb3a0f8b..93451170c 100644 --- a/Sources/TestingMacros/Support/AvailabilityGuards.swift +++ b/Sources/TestingMacros/Support/AvailabilityGuards.swift @@ -24,6 +24,10 @@ struct Availability { /// The platform version, such as 1.2.3, if any. var version: VersionTupleSyntax? + /// Whether or not this availability attribute may need a trailing wildcard + /// (`*`) when it is expanded into `@available()` or `#available()`. + var mayNeedTrailingWildcard = true + /// The `message` argument to the attribute, if any. var message: SimpleStringLiteralExprSyntax? @@ -70,13 +74,14 @@ private func _createAvailabilityTraitExpr( "(\(literal: components.major), \(literal: components.minor), \(literal: components.patch))" } ?? "nil" let message = availability.message.map(\.trimmed).map(ExprSyntax.init) ?? "nil" + let trailingWildcard = availability.mayNeedTrailingWildcard ? ", *" : "" let sourceLocationExpr = createSourceLocationExpr(of: availability.attribute, context: context) switch (whenKeyword, availability.isSwift) { case (.introduced, false): return """ .__available(\(literal: availability.platformName!.textWithoutBackticks), introduced: \(version), message: \(message), sourceLocation: \(sourceLocationExpr)) { - if #available(\(availability.platformVersion!), *) { + if #available(\(availability.platformVersion!)\(raw: trailingWildcard)) { return true } return false @@ -207,8 +212,8 @@ func createSyntaxNode( do { let availableExprs: [ExprSyntax] = decl.availability(when: .introduced).lazy .filter { !$0.isSwift } - .compactMap(\.platformVersion) - .map { "#available(\($0), *)" } + .compactMap { ($0.platformVersion, $0.mayNeedTrailingWildcard ? ", *" : "") } + .map { "#available(\($0.0)\(raw: $0.1))" } if !availableExprs.isEmpty { let conditionList = ConditionElementListSyntax { for availableExpr in availableExprs { diff --git a/Tests/TestingMacrosTests/TestDeclarationMacroTests.swift b/Tests/TestingMacrosTests/TestDeclarationMacroTests.swift index b65a6a62e..aec6d2c10 100644 --- a/Tests/TestingMacrosTests/TestDeclarationMacroTests.swift +++ b/Tests/TestingMacrosTests/TestDeclarationMacroTests.swift @@ -415,7 +415,12 @@ struct TestDeclarationMacroTests { [ #"#if os(moofOS)"#, #".__available("moofOS", obsoleted: nil, message: "Moof!", "#, - ] + ], + #"@available(customAvailabilityDomain) @Test func f() {}"#: + [ + #".__available("customAvailabilityDomain", introduced: nil, "#, + #"guard #available (customAvailabilityDomain) else"#, + ], ] ) func availabilityAttributeCapture(input: String, expectedOutputs: [String]) throws { From d276e4523bb0934d20bce62bb777f8fdda183492 Mon Sep 17 00:00:00 2001 From: Stuart Montgomery Date: Thu, 9 Oct 2025 07:13:18 -0500 Subject: [PATCH 174/216] Add GitHub Action workflows which build main branch (post-merge) (#1361) --- .github/workflows/main_using_main.yml | 22 ++++++++++++++++++++++ .github/workflows/main_using_release.yml | 22 ++++++++++++++++++++++ .github/workflows/pull_request.yml | 2 +- 3 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/main_using_main.yml create mode 100644 .github/workflows/main_using_release.yml diff --git a/.github/workflows/main_using_main.yml b/.github/workflows/main_using_main.yml new file mode 100644 index 000000000..963a6728c --- /dev/null +++ b/.github/workflows/main_using_main.yml @@ -0,0 +1,22 @@ +name: main branch, main toolchain + +on: + push: + branches: + - 'main' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + tests: + name: Test + uses: swiftlang/github-workflows/.github/workflows/swift_package_test.yml@main + with: + linux_swift_versions: '["nightly-main"]' + linux_os_versions: '["amazonlinux2", "jammy"]' + windows_swift_versions: '["nightly-main"]' + enable_macos_checks: true + macos_exclude_xcode_versions: '[{"xcode_version": "16.2"}, {"xcode_version": "16.3"}, {"xcode_version": "16.4"}]' + enable_wasm_sdk_build: true diff --git a/.github/workflows/main_using_release.yml b/.github/workflows/main_using_release.yml new file mode 100644 index 000000000..151e94874 --- /dev/null +++ b/.github/workflows/main_using_release.yml @@ -0,0 +1,22 @@ +name: main branch, 6.2 toolchain + +on: + push: + branches: + - 'main' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + tests: + name: Test + uses: swiftlang/github-workflows/.github/workflows/swift_package_test.yml@main + with: + linux_swift_versions: '["nightly-6.2"]' + linux_os_versions: '["amazonlinux2", "jammy"]' + windows_swift_versions: '["nightly-6.2"]' + enable_macos_checks: true + macos_exclude_xcode_versions: '[{"xcode_version": "16.2"}, {"xcode_version": "16.3"}, {"xcode_version": "16.4"}]' + enable_wasm_sdk_build: true diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 1435fa472..19725c9da 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -17,7 +17,7 @@ jobs: linux_os_versions: '["amazonlinux2", "jammy"]' windows_swift_versions: '["nightly-main", "nightly-6.2"]' enable_macos_checks: true - macos_exclude_xcode_versions: "[{\"xcode_version\": \"16.2\"}, {\"xcode_version\": \"16.3\"}, {\"xcode_version\": \"16.4\"}]" + macos_exclude_xcode_versions: '[{"xcode_version": "16.2"}, {"xcode_version": "16.3"}, {"xcode_version": "16.4"}]' enable_wasm_sdk_build: true soundness: name: Soundness From 08f39d64bc77b504d4a266027ad176f51b6544c5 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Thu, 9 Oct 2025 15:13:18 -0400 Subject: [PATCH 175/216] Promote Windows image attachments to API. (#1333) Remove `@_spi` and adjust documentation as needed now that [ST-0015](https://forums.swift.org/t/st-0015-image-attachments-in-swift-testing-windows/82241) has been approved. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- .../Attachments/AttachableAsCGImage.swift | 2 -- .../AttachableImageFormat+UTType.swift | 8 +++---- .../AttachableAsIWICBitmapSource.swift | 10 +++++++-- .../AttachableImageFormat+CLSID.swift | 21 +++++++++++++++++-- ...HBITMAP+AttachableAsIWICBitmapSource.swift | 1 - .../HICON+AttachableAsIWICBitmapSource.swift | 1 - ...pSource+AttachableAsIWICBitmapSource.swift | 19 ----------------- ...Pointer+AttachableAsIWICBitmapSource.swift | 7 ++++++- ...chableImageWrapper+AttachableWrapper.swift | 3 +-- .../_Testing_WinSDK/ReexportTesting.swift | 2 +- .../Images/AttachableImageFormat.swift | 4 ++-- .../Attachment+_AttachableAsImage.swift | 6 ++---- .../Images/ImageAttachmentError.swift | 2 ++ .../Images/_AttachableAsImage.swift | 1 + .../Images/_AttachableImageWrapper.swift | 4 ++-- Sources/Testing/Testing.docc/Attachments.md | 5 +++-- 16 files changed, 51 insertions(+), 45 deletions(-) diff --git a/Sources/Overlays/_Testing_CoreGraphics/Attachments/AttachableAsCGImage.swift b/Sources/Overlays/_Testing_CoreGraphics/Attachments/AttachableAsCGImage.swift index 96b93bad5..bae0b38f5 100644 --- a/Sources/Overlays/_Testing_CoreGraphics/Attachments/AttachableAsCGImage.swift +++ b/Sources/Overlays/_Testing_CoreGraphics/Attachments/AttachableAsCGImage.swift @@ -27,9 +27,7 @@ private import ImageIO /// |-|-| /// | macOS | [`CGImage`](https://developer.apple.com/documentation/coregraphics/cgimage), [`CIImage`](https://developer.apple.com/documentation/coreimage/ciimage), [`NSImage`](https://developer.apple.com/documentation/appkit/nsimage) | /// | iOS, watchOS, tvOS, and visionOS | [`CGImage`](https://developer.apple.com/documentation/coregraphics/cgimage), [`CIImage`](https://developer.apple.com/documentation/coreimage/ciimage), [`UIImage`](https://developer.apple.com/documentation/uikit/uiimage) | -/// @Comment { /// | Windows | [`HBITMAP`](https://learn.microsoft.com/en-us/windows/win32/gdi/bitmaps), [`HICON`](https://learn.microsoft.com/en-us/windows/win32/menurc/icons), [`IWICBitmapSource`](https://learn.microsoft.com/en-us/windows/win32/api/wincodec/nn-wincodec-iwicbitmapsource) (including its subclasses declared by Windows Imaging Component) | -/// } /// /// You do not generally need to add your own conformances to this protocol. If /// you have an image in another format that needs to be attached to a test, diff --git a/Sources/Overlays/_Testing_CoreGraphics/Attachments/AttachableImageFormat+UTType.swift b/Sources/Overlays/_Testing_CoreGraphics/Attachments/AttachableImageFormat+UTType.swift index f77945687..a427ceec9 100644 --- a/Sources/Overlays/_Testing_CoreGraphics/Attachments/AttachableImageFormat+UTType.swift +++ b/Sources/Overlays/_Testing_CoreGraphics/Attachments/AttachableImageFormat+UTType.swift @@ -107,11 +107,7 @@ extension AttachableImageFormat { ) self.init(kind: .systemValue(contentType), encodingQuality: encodingQuality) } -} -@available(_uttypesAPI, *) -@_spi(Experimental) // STOP: not part of ST-0014 -extension AttachableImageFormat { /// Construct an instance of this type with the given path extension and /// encoding quality. /// @@ -132,6 +128,10 @@ extension AttachableImageFormat { /// must conform to [`UTType.image`](https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/image). /// - On Windows, there must be a corresponding subclass of [`IWICBitmapEncoder`](https://learn.microsoft.com/en-us/windows/win32/api/wincodec/nn-wincodec-iwicbitmapencoder) /// registered with Windows Imaging Component. + /// + /// @Metadata { + /// @Available(Swift, introduced: 6.3) + /// } public init?(pathExtension: String, encodingQuality: Float = 1.0) { let pathExtension = pathExtension.drop { $0 == "." } diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsIWICBitmapSource.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsIWICBitmapSource.swift index 60d2e28b8..94dfe4299 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsIWICBitmapSource.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsIWICBitmapSource.swift @@ -32,7 +32,6 @@ public import WinSDK /// You do not generally need to add your own conformances to this protocol. If /// you have an image in another format that needs to be attached to a test, /// first convert it to an instance of one of the types above. -@_spi(Experimental) public protocol _AttachableByAddressAsIWICBitmapSource { /// Create a WIC bitmap source representing an instance of this type at the /// given address. @@ -112,7 +111,10 @@ public protocol _AttachableByAddressAsIWICBitmapSource { /// You do not generally need to add your own conformances to this protocol. If /// you have an image in another format that needs to be attached to a test, /// first convert it to an instance of one of the types above. -@_spi(Experimental) +/// +/// @Metadata { +/// @Available(Swift, introduced: 6.3) +/// } public protocol AttachableAsIWICBitmapSource: _AttachableAsImage, SendableMetatype { /// Create a WIC bitmap source representing an instance of this type. /// @@ -120,6 +122,10 @@ public protocol AttachableAsIWICBitmapSource: _AttachableAsImage, SendableMetaty /// The caller is responsible for releasing this image when done with it. /// /// - Throws: Any error that prevented the creation of the WIC bitmap source. + /// + /// @Metadata { + /// @Available(Swift, introduced: 6.3) + /// } func copyAttachableIWICBitmapSource() throws -> UnsafeMutablePointer /// Create a WIC bitmap representing an instance of this type. diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableImageFormat+CLSID.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableImageFormat+CLSID.swift index 2928d7af5..37e7c8f9f 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableImageFormat+CLSID.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableImageFormat+CLSID.swift @@ -9,10 +9,9 @@ // #if os(Windows) -@_spi(Experimental) public import Testing +public import Testing public import WinSDK -@_spi(Experimental) extension AttachableImageFormat { private static let _encoderPathExtensionsByCLSID = Result<[UInt128: [String]], any Error> { var result = [UInt128: [String]]() @@ -235,6 +234,13 @@ extension AttachableImageFormat { /// /// For example, if this image format equals ``png``, the value of this /// property equals [`CLSID_WICPngEncoder`](https://learn.microsoft.com/en-us/windows/win32/wic/-wic-guids-clsids#wic-guids-and-clsids). + /// + /// @Metadata { + /// @Available(Swift, introduced: 6.3) + /// } +#if compiler(>=6.3) && !SWT_FIXED_84466 + @_spi(_) +#endif public var encoderCLSID: CLSID { switch kind { case .png: @@ -263,6 +269,13 @@ extension AttachableImageFormat { /// result is undefined. For a list of image encoder classes supported by WIC, /// see the documentation for the [`IWICBitmapEncoder`](https://learn.microsoft.com/en-us/windows/win32/api/wincodec/nn-wincodec-iwicbitmapencoder) /// class. + /// + /// @Metadata { + /// @Available(Swift, introduced: 6.3) + /// } +#if compiler(>=6.3) && !SWT_FIXED_84466 + @_spi(_) +#endif public init(encoderCLSID: CLSID, encodingQuality: Float = 1.0) { if encoderCLSID == CLSID_WICPngEncoder { self = .png @@ -293,6 +306,10 @@ extension AttachableImageFormat { /// must conform to [`UTType.image`](https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/image). /// - On Windows, there must be a corresponding subclass of [`IWICBitmapEncoder`](https://learn.microsoft.com/en-us/windows/win32/api/wincodec/nn-wincodec-iwicbitmapencoder) /// registered with Windows Imaging Component. + /// + /// @Metadata { + /// @Available(Swift, introduced: 6.3) + /// } public init?(pathExtension: String, encodingQuality: Float = 1.0) { let pathExtension = pathExtension.drop { $0 == "." } diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/HBITMAP+AttachableAsIWICBitmapSource.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/HBITMAP+AttachableAsIWICBitmapSource.swift index 4baef8d36..baccf2663 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/HBITMAP+AttachableAsIWICBitmapSource.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/HBITMAP+AttachableAsIWICBitmapSource.swift @@ -12,7 +12,6 @@ private import Testing public import WinSDK -@_spi(Experimental) extension HBITMAP__: _AttachableByAddressAsIWICBitmapSource { public static func _copyAttachableIWICBitmapSource( from imageAddress: UnsafeMutablePointer, diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/HICON+AttachableAsIWICBitmapSource.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/HICON+AttachableAsIWICBitmapSource.swift index 36f4fcc9e..8884d713a 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/HICON+AttachableAsIWICBitmapSource.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/HICON+AttachableAsIWICBitmapSource.swift @@ -12,7 +12,6 @@ private import Testing public import WinSDK -@_spi(Experimental) extension HICON__: _AttachableByAddressAsIWICBitmapSource { public static func _copyAttachableIWICBitmapSource( from imageAddress: UnsafeMutablePointer, diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/IWICBitmapSource+AttachableAsIWICBitmapSource.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/IWICBitmapSource+AttachableAsIWICBitmapSource.swift index 55c2ec4c2..d900baa46 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/IWICBitmapSource+AttachableAsIWICBitmapSource.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/IWICBitmapSource+AttachableAsIWICBitmapSource.swift @@ -34,31 +34,14 @@ public import WinSDK /// allows us to reuse code across all subclasses of `IWICBitmapSource`. protocol IWICBitmapSourceProtocol: _AttachableByAddressAsIWICBitmapSource {} -@_spi(Experimental) extension IWICBitmapSource: _AttachableByAddressAsIWICBitmapSource, IWICBitmapSourceProtocol {} - -@_spi(Experimental) extension IWICBitmap: _AttachableByAddressAsIWICBitmapSource, IWICBitmapSourceProtocol {} - -@_spi(Experimental) extension IWICBitmapClipper: _AttachableByAddressAsIWICBitmapSource, IWICBitmapSourceProtocol {} - -@_spi(Experimental) extension IWICBitmapFlipRotator: _AttachableByAddressAsIWICBitmapSource, IWICBitmapSourceProtocol {} - -@_spi(Experimental) extension IWICBitmapFrameDecode: _AttachableByAddressAsIWICBitmapSource, IWICBitmapSourceProtocol {} - -@_spi(Experimental) extension IWICBitmapScaler: _AttachableByAddressAsIWICBitmapSource, IWICBitmapSourceProtocol {} - -@_spi(Experimental) extension IWICColorTransform: _AttachableByAddressAsIWICBitmapSource, IWICBitmapSourceProtocol {} - -@_spi(Experimental) extension IWICFormatConverter: _AttachableByAddressAsIWICBitmapSource, IWICBitmapSourceProtocol {} - -@_spi(Experimental) extension IWICPlanarFormatConverter: _AttachableByAddressAsIWICBitmapSource, IWICBitmapSourceProtocol {} // MARK: - Upcasting conveniences @@ -96,7 +79,6 @@ extension UnsafeMutablePointer where Pointee: IWICBitmapSourceProtocol { // MARK: - _AttachableByAddressAsIWICBitmapSource implementation -@_spi(Experimental) extension IWICBitmapSourceProtocol { public static func _copyAttachableIWICBitmapSource( from imageAddress: UnsafeMutablePointer, @@ -120,7 +102,6 @@ extension IWICBitmapSourceProtocol { } extension IWICBitmapSource { - @_spi(Experimental) public static func _copyAttachableIWICBitmapSource( from imageAddress: UnsafeMutablePointer, using factory: UnsafeMutablePointer diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/UnsafeMutablePointer+AttachableAsIWICBitmapSource.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/UnsafeMutablePointer+AttachableAsIWICBitmapSource.swift index a8b0a6312..448c2151e 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/UnsafeMutablePointer+AttachableAsIWICBitmapSource.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/UnsafeMutablePointer+AttachableAsIWICBitmapSource.swift @@ -12,8 +12,13 @@ private import Testing public import WinSDK -@_spi(Experimental) +/// @Metadata { +/// @Available(Swift, introduced: 6.3) +/// } extension UnsafeMutablePointer: _AttachableAsImage, AttachableAsIWICBitmapSource where Pointee: _AttachableByAddressAsIWICBitmapSource { + /// @Metadata { + /// @Available(Swift, introduced: 6.3) + /// } public func copyAttachableIWICBitmapSource() throws -> UnsafeMutablePointer { let factory = try IWICImagingFactory.create() defer { diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/_AttachableImageWrapper+AttachableWrapper.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/_AttachableImageWrapper+AttachableWrapper.swift index ecf4602e4..33026fc9a 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/_AttachableImageWrapper+AttachableWrapper.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/_AttachableImageWrapper+AttachableWrapper.swift @@ -9,10 +9,9 @@ // #if os(Windows) -@_spi(Experimental) public import Testing +public import Testing private import WinSDK -@_spi(Experimental) extension _AttachableImageWrapper: Attachable, AttachableWrapper where Image: AttachableAsIWICBitmapSource { public func withUnsafeBytes(for attachment: borrowing Attachment<_AttachableImageWrapper>, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { // Create an in-memory stream to write the image data to. Note that Windows diff --git a/Sources/Overlays/_Testing_WinSDK/ReexportTesting.swift b/Sources/Overlays/_Testing_WinSDK/ReexportTesting.swift index 48dff4164..da5b41a1b 100644 --- a/Sources/Overlays/_Testing_WinSDK/ReexportTesting.swift +++ b/Sources/Overlays/_Testing_WinSDK/ReexportTesting.swift @@ -8,4 +8,4 @@ // See https://swift.org/CONTRIBUTORS.txt for Swift project authors // -@_exported @_spi(Experimental) @_spi(ForToolsIntegrationOnly) public import Testing +@_exported public import Testing diff --git a/Sources/Testing/Attachments/Images/AttachableImageFormat.swift b/Sources/Testing/Attachments/Images/AttachableImageFormat.swift index a5f71a01d..350ef849e 100644 --- a/Sources/Testing/Attachments/Images/AttachableImageFormat.swift +++ b/Sources/Testing/Attachments/Images/AttachableImageFormat.swift @@ -23,15 +23,14 @@ /// - On Apple platforms, you can use [`CGImageDestinationCopyTypeIdentifiers()`](https://developer.apple.com/documentation/imageio/cgimagedestinationcopytypeidentifiers()) /// from the [Image I/O framework](https://developer.apple.com/documentation/imageio) /// to determine which formats are supported. -/// @Comment { /// - On Windows, you can use [`IWICImagingFactory.CreateComponentEnumerator()`](https://learn.microsoft.com/en-us/windows/win32/api/wincodec/nf-wincodec-iwicimagingfactory-createcomponentenumerator) /// to enumerate the available image encoders. -/// } /// /// @Metadata { /// @Available(Swift, introduced: 6.3) /// } #if SWT_NO_IMAGE_ATTACHMENTS +@_unavailableInEmbedded @available(*, unavailable, message: "Image attachments are not available on this platform.") #endif @available(_uttypesAPI, *) @@ -81,6 +80,7 @@ public struct AttachableImageFormat: Sendable { // MARK: - #if SWT_NO_IMAGE_ATTACHMENTS +@_unavailableInEmbedded @available(*, unavailable, message: "Image attachments are not available on this platform.") #endif @available(_uttypesAPI, *) diff --git a/Sources/Testing/Attachments/Images/Attachment+_AttachableAsImage.swift b/Sources/Testing/Attachments/Images/Attachment+_AttachableAsImage.swift index a6bda8ad9..dde24f6d7 100644 --- a/Sources/Testing/Attachments/Images/Attachment+_AttachableAsImage.swift +++ b/Sources/Testing/Attachments/Images/Attachment+_AttachableAsImage.swift @@ -9,6 +9,7 @@ // #if SWT_NO_IMAGE_ATTACHMENTS +@_unavailableInEmbedded @available(*, unavailable, message: "Image attachments are not available on this platform.") #endif @available(_uttypesAPI, *) @@ -32,9 +33,7 @@ extension Attachment { /// |-|-| /// | macOS | [`CGImage`](https://developer.apple.com/documentation/coregraphics/cgimage), [`CIImage`](https://developer.apple.com/documentation/coreimage/ciimage), [`NSImage`](https://developer.apple.com/documentation/appkit/nsimage) | /// | iOS, watchOS, tvOS, and visionOS | [`CGImage`](https://developer.apple.com/documentation/coregraphics/cgimage), [`CIImage`](https://developer.apple.com/documentation/coreimage/ciimage), [`UIImage`](https://developer.apple.com/documentation/uikit/uiimage) | - /// @Comment { /// | Windows | [`HBITMAP`](https://learn.microsoft.com/en-us/windows/win32/gdi/bitmaps), [`HICON`](https://learn.microsoft.com/en-us/windows/win32/menurc/icons), [`IWICBitmapSource`](https://learn.microsoft.com/en-us/windows/win32/api/wincodec/nn-wincodec-iwicbitmapsource) (including its subclasses declared by Windows Imaging Component) | - /// } /// /// The testing library uses the image format specified by `imageFormat`. Pass /// `nil` to let the testing library decide which image format to use. If you @@ -75,9 +74,7 @@ extension Attachment { /// |-|-| /// | macOS | [`CGImage`](https://developer.apple.com/documentation/coregraphics/cgimage), [`CIImage`](https://developer.apple.com/documentation/coreimage/ciimage), [`NSImage`](https://developer.apple.com/documentation/appkit/nsimage) | /// | iOS, watchOS, tvOS, and visionOS | [`CGImage`](https://developer.apple.com/documentation/coregraphics/cgimage), [`CIImage`](https://developer.apple.com/documentation/coreimage/ciimage), [`UIImage`](https://developer.apple.com/documentation/uikit/uiimage) | - /// @Comment { /// | Windows | [`HBITMAP`](https://learn.microsoft.com/en-us/windows/win32/gdi/bitmaps), [`HICON`](https://learn.microsoft.com/en-us/windows/win32/menurc/icons), [`IWICBitmapSource`](https://learn.microsoft.com/en-us/windows/win32/api/wincodec/nn-wincodec-iwicbitmapsource) (including its subclasses declared by Windows Imaging Component) | - /// } /// /// The testing library uses the image format specified by `imageFormat`. Pass /// `nil` to let the testing library decide which image format to use. If you @@ -105,6 +102,7 @@ extension Attachment { @_spi(Experimental) // STOP: not part of ST-0014 #if SWT_NO_IMAGE_ATTACHMENTS +@_unavailableInEmbedded @available(*, unavailable, message: "Image attachments are not available on this platform.") #endif @available(_uttypesAPI, *) diff --git a/Sources/Testing/Attachments/Images/ImageAttachmentError.swift b/Sources/Testing/Attachments/Images/ImageAttachmentError.swift index 47b0ba91e..e172abd8c 100644 --- a/Sources/Testing/Attachments/Images/ImageAttachmentError.swift +++ b/Sources/Testing/Attachments/Images/ImageAttachmentError.swift @@ -12,6 +12,7 @@ private import _TestingInternals /// A type representing an error that can occur when attaching an image. #if SWT_NO_IMAGE_ATTACHMENTS +@_unavailableInEmbedded @available(*, unavailable, message: "Image attachments are not available on this platform.") #endif package enum ImageAttachmentError: Error { @@ -43,6 +44,7 @@ package enum ImageAttachmentError: Error { } #if SWT_NO_IMAGE_ATTACHMENTS +@_unavailableInEmbedded @available(*, unavailable, message: "Image attachments are not available on this platform.") #endif extension ImageAttachmentError: CustomStringConvertible { diff --git a/Sources/Testing/Attachments/Images/_AttachableAsImage.swift b/Sources/Testing/Attachments/Images/_AttachableAsImage.swift index 4ee2f66cb..ca8efacf2 100644 --- a/Sources/Testing/Attachments/Images/_AttachableAsImage.swift +++ b/Sources/Testing/Attachments/Images/_AttachableAsImage.swift @@ -27,6 +27,7 @@ /// that we don't need to underscore its name. /// } #if SWT_NO_IMAGE_ATTACHMENTS +@_unavailableInEmbedded @available(*, unavailable, message: "Image attachments are not available on this platform.") #endif @available(_uttypesAPI, *) diff --git a/Sources/Testing/Attachments/Images/_AttachableImageWrapper.swift b/Sources/Testing/Attachments/Images/_AttachableImageWrapper.swift index 4ce3576d9..dfde12587 100644 --- a/Sources/Testing/Attachments/Images/_AttachableImageWrapper.swift +++ b/Sources/Testing/Attachments/Images/_AttachableImageWrapper.swift @@ -17,10 +17,9 @@ /// |-|-| /// | macOS | [`CGImage`](https://developer.apple.com/documentation/coregraphics/cgimage), [`CIImage`](https://developer.apple.com/documentation/coreimage/ciimage), [`NSImage`](https://developer.apple.com/documentation/appkit/nsimage) | /// | iOS, watchOS, tvOS, and visionOS | [`CGImage`](https://developer.apple.com/documentation/coregraphics/cgimage), [`CIImage`](https://developer.apple.com/documentation/coreimage/ciimage), [`UIImage`](https://developer.apple.com/documentation/uikit/uiimage) | -/// @Comment { /// | Windows | [`HBITMAP`](https://learn.microsoft.com/en-us/windows/win32/gdi/bitmaps), [`HICON`](https://learn.microsoft.com/en-us/windows/win32/menurc/icons), [`IWICBitmapSource`](https://learn.microsoft.com/en-us/windows/win32/api/wincodec/nn-wincodec-iwicbitmapsource) (including its subclasses declared by Windows Imaging Component) | -/// } #if SWT_NO_IMAGE_ATTACHMENTS +@_unavailableInEmbedded @available(*, unavailable, message: "Image attachments are not available on this platform.") #endif @available(_uttypesAPI, *) @@ -42,6 +41,7 @@ public final class _AttachableImageWrapper: Sendable where Image: _Attach } #if SWT_NO_IMAGE_ATTACHMENTS +@_unavailableInEmbedded @available(*, unavailable, message: "Image attachments are not available on this platform.") #endif @available(_uttypesAPI, *) diff --git a/Sources/Testing/Testing.docc/Attachments.md b/Sources/Testing/Testing.docc/Attachments.md index e05fc0270..aa754cc76 100644 --- a/Sources/Testing/Testing.docc/Attachments.md +++ b/Sources/Testing/Testing.docc/Attachments.md @@ -34,9 +34,10 @@ protocol to create your own attachable types. ### Attaching images to tests -- ``AttachableImageFormat`` +- ``AttachableImageFormat`` - ``Attachment/init(_:named:as:sourceLocation:)`` - ``Attachment/record(_:named:as:sourceLocation:)`` ---> From fa1e095b316b9925dd5cc7fafc5d67825a07cd52 Mon Sep 17 00:00:00 2001 From: Stuart Montgomery Date: Thu, 9 Oct 2025 14:36:54 -0500 Subject: [PATCH 176/216] Adjust README to show status badges from GitHub Actions workflows (#1364) This adjusts `README.md` to begin showing CI status badges from the GitHub Actions workflows I configured in #1361. View [rendered README document](https://github.com/stmontgomery/swift-testing/blob/github-actions-badges/README.md). ### Motivation: This will help ensure our CI status badges remain consistent with the CI status shown on Pull Requests. ### Modifications: - Remove the CI status column on the "Supported platforms" table from the README. - This is because the new workflows do not offer granular, per-platform status badges (see [GitHub's documentation](https://docs.github.com/en/actions/how-tos/monitor-workflows/add-a-status-badge)). - Add a new section to the README showing this status. - Drive-by improvement: Move "Windows" platform up a few rows so that all the Supported platforms are listed first, followed by all Experimental platforms. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- README.md | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 77be2953e..1a7e6416e 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,9 @@ See https://swift.org/CONTRIBUTORS.txt for Swift project authors Swift Testing is a package with expressive and intuitive APIs that make testing your Swift code a breeze. +[![CI status badge for main branch using main toolchain](https://github.com/swiftlang/swift-testing/actions/workflows/main_using_main.yml/badge.svg?branch=main&event=push)](https://github.com/swiftlang/swift-testing/actions/workflows/main_using_main.yml) +[![CI status badge for main branch using 6.2 toolchain](https://github.com/swiftlang/swift-testing/actions/workflows/main_using_release.yml/badge.svg?branch=main&event=push)](https://github.com/swiftlang/swift-testing/actions/workflows/main_using_release.yml) + ## Feature overview ### Clear, expressive API @@ -103,18 +106,14 @@ Swift. The table below describes the current level of support that Swift Testing has for various platforms: -| **Platform** | **CI Status (6.2)** | **CI Status (main)** | **Support Status** | -|---|:-:|:-:|---| -| **macOS** | [![Build Status](https://ci.swift.org/buildStatus/icon?job=swift-testing-main-swift-6.2-macos)](https://ci.swift.org/job/swift-testing-main-swift-6.2-macos/) | [![Build Status](https://ci.swift.org/buildStatus/icon?job=swift-testing-main-swift-main-macos)](https://ci.swift.org/view/Swift%20Packages/job/swift-testing-main-swift-main-macos/) | Supported | -| **iOS** | | | Supported | -| **watchOS** | | | Supported | -| **tvOS** | | | Supported | -| **visionOS** | | | Supported | -| **Ubuntu 22.04** | [![Build Status](https://ci.swift.org/buildStatus/icon?job=swift-testing-main-swift-6.2-linux)](https://ci.swift.org/job/swift-testing-main-swift-6.2-linux/) | [![Build Status](https://ci.swift.org/buildStatus/icon?job=swift-testing-main-swift-main-linux)](https://ci.swift.org/view/Swift%20Packages/job/swift-testing-main-swift-main-linux/) | Supported | -| **FreeBSD** | | | Experimental | -| **OpenBSD** | | | Experimental | -| **Windows** | [![Build Status](https://ci-external.swift.org/buildStatus/icon?job=swift-testing-main-swift-6.2-windows)](https://ci-external.swift.org/view/all/job/swift-testing-main-swift-6.2-windows/) | [![Build Status](https://ci-external.swift.org/buildStatus/icon?job=swift-testing-main-swift-main-windows)](https://ci-external.swift.org/job/swift-testing-main-swift-main-windows/) | Supported | -| **Wasm** | | | Experimental | +| **Platform** | **Support Status** | +|-|-| +| Apple platforms | Supported | +| Linux | Supported | +| Windows | Supported | +| FreeBSD, OpenBSD | Experimental | +| Wasm | Experimental | +| Android | Experimental | ### Works with XCTest From 85cbeb3254c44bc4dc0ce5ddcc1f63c477f146bf Mon Sep 17 00:00:00 2001 From: Melissa Kilby Date: Sun, 12 Oct 2025 11:03:30 -0700 Subject: [PATCH 177/216] chore: restrict GitHub workflow permissions - future-proof (#1365) --- .github/workflows/main_using_main.yml | 3 +++ .github/workflows/main_using_release.yml | 3 +++ .github/workflows/pull_request.yml | 3 +++ 3 files changed, 9 insertions(+) diff --git a/.github/workflows/main_using_main.yml b/.github/workflows/main_using_main.yml index 963a6728c..dbf27c0f4 100644 --- a/.github/workflows/main_using_main.yml +++ b/.github/workflows/main_using_main.yml @@ -1,5 +1,8 @@ name: main branch, main toolchain +permissions: + contents: read + on: push: branches: diff --git a/.github/workflows/main_using_release.yml b/.github/workflows/main_using_release.yml index 151e94874..b861be1f8 100644 --- a/.github/workflows/main_using_release.yml +++ b/.github/workflows/main_using_release.yml @@ -1,5 +1,8 @@ name: main branch, 6.2 toolchain +permissions: + contents: read + on: push: branches: diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 19725c9da..084fd1d87 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -1,5 +1,8 @@ name: Pull request +permissions: + contents: read + on: pull_request: types: [opened, reopened, synchronize] From 90e08ac60db5da902bcabc5d1030afaa1d6df58c Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Tue, 14 Oct 2025 17:12:51 -0400 Subject: [PATCH 178/216] Adopt `Mutex` (take 2) (#1363) This PR adopts `Mutex` on all platforms except Darwin (where we still need to back-deploy further than `Mutex` is available.) Blocked by https://github.com/swiftlang/swift/pull/84771. Reapplies #1351. Resolves #538. Resolves rdar://131832797. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- Sources/Testing/CMakeLists.txt | 1 - Sources/Testing/ExitTests/WaitFor.swift | 43 +++++- Sources/Testing/Support/Locked+Platform.swift | 97 ------------- Sources/Testing/Support/Locked.swift | 133 +++++++----------- Tests/TestingTests/Support/LockTests.swift | 38 +---- 5 files changed, 96 insertions(+), 216 deletions(-) delete mode 100644 Sources/Testing/Support/Locked+Platform.swift diff --git a/Sources/Testing/CMakeLists.txt b/Sources/Testing/CMakeLists.txt index 751bf1f64..fca4fcae4 100644 --- a/Sources/Testing/CMakeLists.txt +++ b/Sources/Testing/CMakeLists.txt @@ -89,7 +89,6 @@ add_library(Testing Support/Graph.swift Support/JSON.swift Support/Locked.swift - Support/Locked+Platform.swift Support/VersionNumber.swift Support/Versions.swift Discovery+Macro.swift diff --git a/Sources/Testing/ExitTests/WaitFor.swift b/Sources/Testing/ExitTests/WaitFor.swift index 8c6ad52f3..f0326ff3c 100644 --- a/Sources/Testing/ExitTests/WaitFor.swift +++ b/Sources/Testing/ExitTests/WaitFor.swift @@ -80,7 +80,42 @@ func wait(for pid: consuming pid_t) async throws -> ExitStatus { } #elseif SWT_TARGET_OS_APPLE || os(Linux) || os(FreeBSD) || os(OpenBSD) /// A mapping of awaited child PIDs to their corresponding Swift continuations. -private let _childProcessContinuations = LockedWith]>() +private nonisolated(unsafe) let _childProcessContinuations = { + let result = ManagedBuffer<[pid_t: CheckedContinuation], pthread_mutex_t>.create( + minimumCapacity: 1, + makingHeaderWith: { _ in [:] } + ) + + result.withUnsafeMutablePointers { _, lock in + _ = pthread_mutex_init(lock, nil) + } + + return result +}() + +/// Access the value in `_childProcessContinuations` while guarded by its lock. +/// +/// - Parameters: +/// - body: A closure to invoke while the lock is held. +/// +/// - Returns: Whatever is returned by `body`. +/// +/// - Throws: Whatever is thrown by `body`. +private func _withLockedChildProcessContinuations( + _ body: ( + _ childProcessContinuations: inout [pid_t: CheckedContinuation], + _ lock: UnsafeMutablePointer + ) throws -> R +) rethrows -> R { + try _childProcessContinuations.withUnsafeMutablePointers { childProcessContinuations, lock in + _ = pthread_mutex_lock(lock) + defer { + _ = pthread_mutex_unlock(lock) + } + + return try body(&childProcessContinuations.pointee, lock) + } +} /// A condition variable used to suspend the waiter thread created by /// `_createWaitThread()` when there are no child processes to await. @@ -112,7 +147,7 @@ private let _createWaitThread: Void = { var siginfo = siginfo_t() if 0 == waitid(P_ALL, 0, &siginfo, WEXITED | WNOWAIT) { if case let pid = siginfo.si_pid, pid != 0 { - let continuation = _childProcessContinuations.withLock { childProcessContinuations in + let continuation = _withLockedChildProcessContinuations { childProcessContinuations, _ in childProcessContinuations.removeValue(forKey: pid) } @@ -133,7 +168,7 @@ private let _createWaitThread: Void = { // newly-scheduled waiter process. (If this condition is spuriously // woken, we'll just loop again, which is fine.) Note that we read errno // outside the lock in case acquiring the lock perturbs it. - _childProcessContinuations.withUnsafeUnderlyingLock { lock, childProcessContinuations in + _withLockedChildProcessContinuations { childProcessContinuations, lock in if childProcessContinuations.isEmpty { _ = pthread_cond_wait(_waitThreadNoChildrenCondition, lock) } @@ -205,7 +240,7 @@ func wait(for pid: consuming pid_t) async throws -> ExitStatus { _createWaitThread return try await withCheckedThrowingContinuation { continuation in - _childProcessContinuations.withLock { childProcessContinuations in + _withLockedChildProcessContinuations { childProcessContinuations, _ in // We don't need to worry about a race condition here because waitid() // does not clear the wait/zombie state of the child process. If it sees // the child process has terminated and manages to acquire the lock before diff --git a/Sources/Testing/Support/Locked+Platform.swift b/Sources/Testing/Support/Locked+Platform.swift deleted file mode 100644 index a2ba82ac2..000000000 --- a/Sources/Testing/Support/Locked+Platform.swift +++ /dev/null @@ -1,97 +0,0 @@ -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2023–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 -// - -internal import _TestingInternals - -extension Never: Lockable { - static func initializeLock(at lock: UnsafeMutablePointer) {} - static func deinitializeLock(at lock: UnsafeMutablePointer) {} - static func unsafelyAcquireLock(at lock: UnsafeMutablePointer) {} - static func unsafelyRelinquishLock(at lock: UnsafeMutablePointer) {} -} - -#if SWT_TARGET_OS_APPLE && !SWT_NO_OS_UNFAIR_LOCK -extension os_unfair_lock_s: Lockable { - static func initializeLock(at lock: UnsafeMutablePointer) { - lock.initialize(to: .init()) - } - - static func deinitializeLock(at lock: UnsafeMutablePointer) { - // No deinitialization needed. - } - - static func unsafelyAcquireLock(at lock: UnsafeMutablePointer) { - os_unfair_lock_lock(lock) - } - - static func unsafelyRelinquishLock(at lock: UnsafeMutablePointer) { - os_unfair_lock_unlock(lock) - } -} -#endif - -#if os(FreeBSD) || os(OpenBSD) -typealias pthread_mutex_t = _TestingInternals.pthread_mutex_t? -typealias pthread_cond_t = _TestingInternals.pthread_cond_t? -#endif - -#if SWT_TARGET_OS_APPLE || os(Linux) || os(FreeBSD) || os(OpenBSD) || os(Android) || (os(WASI) && _runtime(_multithreaded)) -extension pthread_mutex_t: Lockable { - static func initializeLock(at lock: UnsafeMutablePointer) { - _ = pthread_mutex_init(lock, nil) - } - - static func deinitializeLock(at lock: UnsafeMutablePointer) { - _ = pthread_mutex_destroy(lock) - } - - static func unsafelyAcquireLock(at lock: UnsafeMutablePointer) { - _ = pthread_mutex_lock(lock) - } - - static func unsafelyRelinquishLock(at lock: UnsafeMutablePointer) { - _ = pthread_mutex_unlock(lock) - } -} -#endif - -#if os(Windows) -extension SRWLOCK: Lockable { - static func initializeLock(at lock: UnsafeMutablePointer) { - InitializeSRWLock(lock) - } - - static func deinitializeLock(at lock: UnsafeMutablePointer) { - // No deinitialization needed. - } - - static func unsafelyAcquireLock(at lock: UnsafeMutablePointer) { - AcquireSRWLockExclusive(lock) - } - - static func unsafelyRelinquishLock(at lock: UnsafeMutablePointer) { - ReleaseSRWLockExclusive(lock) - } -} -#endif - -#if SWT_TARGET_OS_APPLE && !SWT_NO_OS_UNFAIR_LOCK -typealias DefaultLock = os_unfair_lock -#elseif SWT_TARGET_OS_APPLE || os(Linux) || os(FreeBSD) || os(OpenBSD) || os(Android) || (os(WASI) && _runtime(_multithreaded)) -typealias DefaultLock = pthread_mutex_t -#elseif os(Windows) -typealias DefaultLock = SRWLOCK -#elseif os(WASI) -// No locks on WASI without multithreaded runtime. -typealias DefaultLock = Never -#else -#warning("Platform-specific implementation missing: locking unavailable") -typealias DefaultLock = Never -#endif diff --git a/Sources/Testing/Support/Locked.swift b/Sources/Testing/Support/Locked.swift index d1db8ef1f..fac062adb 100644 --- a/Sources/Testing/Support/Locked.swift +++ b/Sources/Testing/Support/Locked.swift @@ -9,37 +9,7 @@ // internal import _TestingInternals - -/// A protocol defining a type, generally platform-specific, that satisfies the -/// requirements of a lock or mutex. -protocol Lockable { - /// Initialize the lock at the given address. - /// - /// - Parameters: - /// - lock: A pointer to uninitialized memory that should be initialized as - /// an instance of this type. - static func initializeLock(at lock: UnsafeMutablePointer) - - /// Deinitialize the lock at the given address. - /// - /// - Parameters: - /// - lock: A pointer to initialized memory that should be deinitialized. - static func deinitializeLock(at lock: UnsafeMutablePointer) - - /// Acquire the lock at the given address. - /// - /// - Parameters: - /// - lock: The address of the lock to acquire. - static func unsafelyAcquireLock(at lock: UnsafeMutablePointer) - - /// Relinquish the lock at the given address. - /// - /// - Parameters: - /// - lock: The address of the lock to relinquish. - static func unsafelyRelinquishLock(at lock: UnsafeMutablePointer) -} - -// MARK: - +private import Synchronization /// A type that wraps a value requiring access from a synchronous caller during /// concurrent execution. @@ -52,30 +22,48 @@ protocol Lockable { /// concurrency tools. /// /// This type is not part of the public interface of the testing library. -struct LockedWith: RawRepresentable where L: Lockable { - /// A type providing heap-allocated storage for an instance of ``Locked``. - private final class _Storage: ManagedBuffer { - deinit { - withUnsafeMutablePointerToElements { lock in - L.deinitializeLock(at: lock) - } +struct Locked { + /// A type providing storage for the underlying lock and wrapped value. +#if SWT_TARGET_OS_APPLE && canImport(os) + private typealias _Storage = ManagedBuffer +#else + private final class _Storage { + let mutex: Mutex + + init(_ rawValue: consuming sending T) { + mutex = Mutex(rawValue) } } +#endif /// Storage for the underlying lock and wrapped value. - private nonisolated(unsafe) var _storage: ManagedBuffer + private nonisolated(unsafe) var _storage: _Storage +} + +extension Locked: Sendable where T: Sendable {} +extension Locked: RawRepresentable { init(rawValue: T) { - _storage = _Storage.create(minimumCapacity: 1, makingHeaderWith: { _ in rawValue }) +#if SWT_TARGET_OS_APPLE && canImport(os) + _storage = .create(minimumCapacity: 1, makingHeaderWith: { _ in rawValue }) _storage.withUnsafeMutablePointerToElements { lock in - L.initializeLock(at: lock) + lock.initialize(to: .init()) } +#else + nonisolated(unsafe) let rawValue = rawValue + _storage = _Storage(rawValue) +#endif } var rawValue: T { - withLock { $0 } + withLock { rawValue in + nonisolated(unsafe) let rawValue = rawValue + return rawValue + } } +} +extension Locked { /// Acquire the lock and invoke a function while it is held. /// /// - Parameters: @@ -88,55 +76,27 @@ struct LockedWith: RawRepresentable where L: Lockable { /// This function can be used to synchronize access to shared data from a /// synchronous caller. Wherever possible, use actor isolation or other Swift /// concurrency tools. - nonmutating func withLock(_ body: (inout T) throws -> R) rethrows -> R where R: ~Copyable { - try _storage.withUnsafeMutablePointers { rawValue, lock in - L.unsafelyAcquireLock(at: lock) + func withLock(_ body: (inout T) throws -> sending R) rethrows -> sending R where R: ~Copyable { +#if SWT_TARGET_OS_APPLE && canImport(os) + nonisolated(unsafe) let result = try _storage.withUnsafeMutablePointers { rawValue, lock in + os_unfair_lock_lock(lock) defer { - L.unsafelyRelinquishLock(at: lock) + os_unfair_lock_unlock(lock) } return try body(&rawValue.pointee) } - } - - /// Acquire the lock and invoke a function while it is held, yielding both the - /// protected value and a reference to the underlying lock guarding it. - /// - /// - Parameters: - /// - body: A closure to invoke while the lock is held. - /// - /// - Returns: Whatever is returned by `body`. - /// - /// - Throws: Whatever is thrown by `body`. - /// - /// This function is equivalent to ``withLock(_:)`` except that the closure - /// passed to it also takes a reference to the underlying lock guarding this - /// instance's wrapped value. This function can be used when platform-specific - /// functionality such as a `pthread_cond_t` is needed. Because the caller has - /// direct access to the lock and is able to unlock and re-lock it, it is - /// unsafe to modify the protected value. - /// - /// - Warning: Callers that unlock the lock _must_ lock it again before the - /// closure returns. If the lock is not acquired when `body` returns, the - /// effect is undefined. - nonmutating func withUnsafeUnderlyingLock(_ body: (UnsafeMutablePointer, T) throws -> R) rethrows -> R where R: ~Copyable { - try withLock { value in - try _storage.withUnsafeMutablePointerToElements { lock in - try body(lock, value) - } + return result +#else + try _storage.mutex.withLock { rawValue in + try body(&rawValue) } +#endif } } -extension LockedWith: Sendable where T: Sendable {} - -/// A type that wraps a value requiring access from a synchronous caller during -/// concurrent execution and which uses the default platform-specific lock type -/// for the current platform. -typealias Locked = LockedWith - // MARK: - Additions -extension LockedWith where T: AdditiveArithmetic { +extension Locked where T: AdditiveArithmetic & Sendable { /// Add something to the current wrapped value of this instance. /// /// - Parameters: @@ -152,7 +112,7 @@ extension LockedWith where T: AdditiveArithmetic { } } -extension LockedWith where T: Numeric { +extension Locked where T: Numeric & Sendable { /// Increment the current wrapped value of this instance. /// /// - Returns: The sum of ``rawValue`` and `1`. @@ -172,7 +132,7 @@ extension LockedWith where T: Numeric { } } -extension LockedWith { +extension Locked { /// Initialize an instance of this type with a raw value of `nil`. init() where T == V? { self.init(rawValue: nil) @@ -188,3 +148,10 @@ extension LockedWith { self.init(rawValue: []) } } + +// MARK: - POSIX conveniences + +#if os(FreeBSD) || os(OpenBSD) +typealias pthread_mutex_t = _TestingInternals.pthread_mutex_t? +typealias pthread_cond_t = _TestingInternals.pthread_cond_t? +#endif diff --git a/Tests/TestingTests/Support/LockTests.swift b/Tests/TestingTests/Support/LockTests.swift index 2a41e4c1d..486143e1e 100644 --- a/Tests/TestingTests/Support/LockTests.swift +++ b/Tests/TestingTests/Support/LockTests.swift @@ -13,7 +13,9 @@ private import _TestingInternals @Suite("Locked Tests") struct LockTests { - func testLock(_ lock: LockedWith) { + @Test("Locking and unlocking") + func locking() { + let lock = Locked(rawValue: 0) #expect(lock.rawValue == 0) lock.withLock { value in value = 1 @@ -21,21 +23,9 @@ struct LockTests { #expect(lock.rawValue == 1) } - @Test("Platform-default lock") - func locking() { - testLock(Locked(rawValue: 0)) - } - -#if SWT_TARGET_OS_APPLE && !SWT_NO_OS_UNFAIR_LOCK - @Test("pthread_mutex_t (Darwin alternate)") - func lockingWith_pthread_mutex_t() { - testLock(LockedWith(rawValue: 0)) - } -#endif - - @Test("No lock") - func noLock() async { - let lock = LockedWith(rawValue: 0) + @Test("Repeatedly accessing a lock") + func lockRepeatedly() async { + let lock = Locked(rawValue: 0) await withTaskGroup { taskGroup in for _ in 0 ..< 100_000 { taskGroup.addTask { @@ -43,20 +33,6 @@ struct LockTests { } } } - #expect(lock.rawValue != 100_000) - } - - @Test("Get the underlying lock") - func underlyingLock() { - let lock = Locked(rawValue: 0) - testLock(lock) - lock.withUnsafeUnderlyingLock { underlyingLock, _ in - DefaultLock.unsafelyRelinquishLock(at: underlyingLock) - lock.withLock { value in - value += 1000 - } - DefaultLock.unsafelyAcquireLock(at: underlyingLock) - } - #expect(lock.rawValue == 1001) + #expect(lock.rawValue == 100_000) } } From f817ce5863fbd6ba3af57fb7837d8ef0f42336c4 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Wed, 15 Oct 2025 11:21:51 -0400 Subject: [PATCH 179/216] Dynamically look up `backtrace(3)` on Android. (#1356) The `backtrace()` function on Android was added with API level 33, but at least some external Android builds target earlier Android NDKs, so we'll dynamically look up the function. If it's not present or dynamic loading isn't available, we just produce an empty backtrace. See: https://cs.android.com/android/_/android/platform/bionic/+/731631f300090436d7f5df80d50b6275c8c60a93:libc/include/execinfo.h;l=52 Resolves #1135. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- .../Testing/SourceAttribution/Backtrace.swift | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/Sources/Testing/SourceAttribution/Backtrace.swift b/Sources/Testing/SourceAttribution/Backtrace.swift index fd7972cc4..6f6fcaabb 100644 --- a/Sources/Testing/SourceAttribution/Backtrace.swift +++ b/Sources/Testing/SourceAttribution/Backtrace.swift @@ -40,6 +40,16 @@ public struct Backtrace: Sendable { self.addresses = addresses.map { Address(UInt(bitPattern: $0)) } } +#if os(Android) && !SWT_NO_DYNAMIC_LINKING + /// The `backtrace()` function. + /// + /// This function was added to Android with API level 33, which is higher than + /// our minimum deployment target, so we look it up dynamically at runtime. + private static let _backtrace = symbol(named: "backtrace").map { + castCFunction(at: $0, to: (@convention(c) (UnsafeMutablePointer, CInt) -> CInt).self) + } +#endif + /// Get the current backtrace. /// /// - Parameters: @@ -66,9 +76,9 @@ public struct Backtrace: Sendable { initializedCount = .init(clamping: backtrace(addresses.baseAddress!, .init(clamping: addresses.count))) } #elseif os(Android) - initializedCount = addresses.withMemoryRebound(to: UnsafeMutableRawPointer.self) { addresses in - .init(clamping: backtrace(addresses.baseAddress!, .init(clamping: addresses.count))) - } +#if !SWT_NO_DYNAMIC_LINKING + initializedCount = .init(clamping: _backtrace?(addresses.baseAddress!, .init(clamping: addresses.count))) +#endif #elseif os(Linux) || os(FreeBSD) || os(OpenBSD) initializedCount = .init(clamping: backtrace(addresses.baseAddress!, .init(clamping: addresses.count))) #elseif os(Windows) From c1be7ba2768ff7fe3ef034f4158c5e7e5e49badc Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Thu, 16 Oct 2025 18:03:32 -0400 Subject: [PATCH 180/216] Fix accidentally dropping image encoding quality on the floor on Windows. (#1368) This fixes a bug in `AttachableImageFormat.init(encoderCLSID:encodingQuality:)` where the `encodingQuality` argument would not be preserved. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- .../Attachments/AttachableImageFormat+CLSID.swift | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableImageFormat+CLSID.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableImageFormat+CLSID.swift index 37e7c8f9f..72d0d8d3d 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableImageFormat+CLSID.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableImageFormat+CLSID.swift @@ -277,13 +277,14 @@ extension AttachableImageFormat { @_spi(_) #endif public init(encoderCLSID: CLSID, encodingQuality: Float = 1.0) { - if encoderCLSID == CLSID_WICPngEncoder { - self = .png + let kind: Kind = if encoderCLSID == CLSID_WICPngEncoder { + .png } else if encoderCLSID == CLSID_WICJpegEncoder { - self = .jpeg + .jpeg } else { - self.init(kind: .systemValue(encoderCLSID), encodingQuality: encodingQuality) + .systemValue(encoderCLSID) } + self.init(kind: kind, encodingQuality: encodingQuality) } /// Construct an instance of this type with the given path extension and From 5ac9500a5d858868e518a56fa0197be65b759ef4 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Thu, 16 Oct 2025 18:57:31 -0400 Subject: [PATCH 181/216] Fix backtrace() call on Android being optional --- Sources/Testing/SourceAttribution/Backtrace.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Sources/Testing/SourceAttribution/Backtrace.swift b/Sources/Testing/SourceAttribution/Backtrace.swift index 6f6fcaabb..552e16d68 100644 --- a/Sources/Testing/SourceAttribution/Backtrace.swift +++ b/Sources/Testing/SourceAttribution/Backtrace.swift @@ -77,7 +77,9 @@ public struct Backtrace: Sendable { } #elseif os(Android) #if !SWT_NO_DYNAMIC_LINKING - initializedCount = .init(clamping: _backtrace?(addresses.baseAddress!, .init(clamping: addresses.count))) + if let _backtrace { + initializedCount = .init(clamping: _backtrace(addresses.baseAddress!, .init(clamping: addresses.count))) + } #endif #elseif os(Linux) || os(FreeBSD) || os(OpenBSD) initializedCount = .init(clamping: backtrace(addresses.baseAddress!, .init(clamping: addresses.count))) From e03702312a996a82d6edab496a248f0090ce6ab3 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Mon, 20 Oct 2025 15:21:16 -0400 Subject: [PATCH 182/216] Make `Attachment` always conform to `CustomStringConvertible`. (#1367) This PR ensures that `Attachment` conforms to `CustomStringConvertible` even if the value it wraps is non-copyable. We accomplish this by creating a new helper function that is able to box an arbitrary `~Copyable` value in `Any` if its type is actually `Copyable`. (This requires a newer runtime on Apple platforms, so we fall back to a generic (`~Copyable`) implementation on older macOS/iOS/etc.) We then call this helper function in `Attachment.description` and if it returns a value, we can stringify it. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- Package.swift | 1 + Sources/Testing/Attachments/Attachment.swift | 16 +++---- Sources/Testing/CMakeLists.txt | 1 + .../Support/Additions/CopyableAdditions.swift | 47 +++++++++++++++++++ .../shared/AvailabilityDefinitions.cmake | 1 + 5 files changed, 56 insertions(+), 10 deletions(-) create mode 100644 Sources/Testing/Support/Additions/CopyableAdditions.swift diff --git a/Package.swift b/Package.swift index e910a67b8..2788502c3 100644 --- a/Package.swift +++ b/Package.swift @@ -406,6 +406,7 @@ extension Array where Element == PackageDescription.SwiftSetting { .enableExperimentalFeature("AvailabilityMacro=_regexAPI:macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0"), .enableExperimentalFeature("AvailabilityMacro=_swiftVersionAPI:macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0"), .enableExperimentalFeature("AvailabilityMacro=_typedThrowsAPI:macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0"), + .enableExperimentalFeature("AvailabilityMacro=_castingWithNonCopyableGenerics:macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0"), .enableExperimentalFeature("AvailabilityMacro=_distantFuture:macOS 99.0, iOS 99.0, watchOS 99.0, tvOS 99.0, visionOS 99.0"), ] diff --git a/Sources/Testing/Attachments/Attachment.swift b/Sources/Testing/Attachments/Attachment.swift index a17130176..1313b0d41 100644 --- a/Sources/Testing/Attachments/Attachment.swift +++ b/Sources/Testing/Attachments/Attachment.swift @@ -193,21 +193,17 @@ public struct AnyAttachable: AttachableWrapper, Sendable, Copyable { // MARK: - Describing an attachment -extension Attachment where AttachableValue: ~Copyable { - @_documentation(visibility: private) - public var description: String { - let typeInfo = TypeInfo(describing: AttachableValue.self) - return #""\#(preferredName)": instance of '\#(typeInfo.unqualifiedName)'"# - } -} - -extension Attachment: CustomStringConvertible { +extension Attachment: CustomStringConvertible where AttachableValue: ~Copyable { /// @Metadata { /// @Available(Swift, introduced: 6.2) /// @Available(Xcode, introduced: 26.0) /// } public var description: String { - #""\#(preferredName)": \#(String(describingForTest: attachableValue))"# + if #available(_castingWithNonCopyableGenerics, *), let attachableValue = boxCopyableValue(attachableValue) { + return #""\#(preferredName)": \#(String(describingForTest: attachableValue))"# + } + let typeInfo = TypeInfo(describing: AttachableValue.self) + return #""\#(preferredName)": instance of '\#(typeInfo.unqualifiedName)'"# } } diff --git a/Sources/Testing/CMakeLists.txt b/Sources/Testing/CMakeLists.txt index fca4fcae4..bebf05eb9 100644 --- a/Sources/Testing/CMakeLists.txt +++ b/Sources/Testing/CMakeLists.txt @@ -76,6 +76,7 @@ add_library(Testing Support/Additions/ArrayAdditions.swift Support/Additions/CollectionDifferenceAdditions.swift Support/Additions/CommandLineAdditions.swift + Support/Additions/CopyableAdditions.swift Support/Additions/NumericAdditions.swift Support/Additions/ResultAdditions.swift Support/Additions/TaskAdditions.swift diff --git a/Sources/Testing/Support/Additions/CopyableAdditions.swift b/Sources/Testing/Support/Additions/CopyableAdditions.swift new file mode 100644 index 000000000..ea192b1ef --- /dev/null +++ b/Sources/Testing/Support/Additions/CopyableAdditions.swift @@ -0,0 +1,47 @@ +// +// 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 !hasFeature(Embedded) +/// A helper protocol for ``boxCopyableValue(_:)``. +private protocol _CopyablePointer { + /// Load the value at this address into an existential box. + /// + /// - Returns: The value at this address. + func load() -> Any +} + +extension UnsafePointer: _CopyablePointer where Pointee: Copyable { + func load() -> Any { + pointee + } +} +#endif + +/// Copy a value to an existential box if its type conforms to `Copyable`. +/// +/// - Parameters: +/// - value: The value to copy. +/// +/// - Returns: A copy of `value` in an existential box, or `nil` if the type of +/// `value` does not conform to `Copyable`. +/// +/// When using Embedded Swift, this function always returns `nil`. +#if !hasFeature(Embedded) +@available(_castingWithNonCopyableGenerics, *) +func boxCopyableValue(_ value: borrowing some ~Copyable) -> Any? { + withUnsafePointer(to: value) { address in + return (address as? any _CopyablePointer)?.load() + } +} +#else +func boxCopyableValue(_ value: borrowing some ~Copyable) -> Void? { + nil +} +#endif diff --git a/cmake/modules/shared/AvailabilityDefinitions.cmake b/cmake/modules/shared/AvailabilityDefinitions.cmake index 472b2b929..e6b716657 100644 --- a/cmake/modules/shared/AvailabilityDefinitions.cmake +++ b/cmake/modules/shared/AvailabilityDefinitions.cmake @@ -17,4 +17,5 @@ add_compile_options( "SHELL:$<$:-Xfrontend -define-availability -Xfrontend \"_regexAPI:macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0\">" "SHELL:$<$:-Xfrontend -define-availability -Xfrontend \"_swiftVersionAPI:macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0\">" "SHELL:$<$:-Xfrontend -define-availability -Xfrontend \"_typedThrowsAPI:macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0\">" + "SHELL:$<$:-Xfrontend -define-availability -Xfrontend \"_castingWithNonCopyableGenerics:macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0\">" "SHELL:$<$:-Xfrontend -define-availability -Xfrontend \"_distantFuture:macOS 99.0, iOS 99.0, watchOS 99.0, tvOS 99.0, visionOS 99.0\">") From 38067d67c07b0d24dddbdb1824084c99b5dc8e4d Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Mon, 20 Oct 2025 18:27:51 -0400 Subject: [PATCH 183/216] Fix typo in Porting.md --- Documentation/Porting.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Documentation/Porting.md b/Documentation/Porting.md index f7aecf97e..4773e2823 100644 --- a/Documentation/Porting.md +++ b/Documentation/Porting.md @@ -172,8 +172,10 @@ to load that information: + } + let sb = SectionBounds( + imageAddress: UnsafeRawPointer(bitPattern: UInt(refNum)), -+ start: handle.pointee!, -+ size: GetHandleSize(handle) ++ buffer: UnsafeRawBufferPointer( ++ start: handle.pointee, ++ count: GetHandleSize(handle) ++ ) + ) + result.append(sb) + } while noErr == GetNextResourceFile(refNum, &refNum)) From 6a4a3a944bb3531ad735a70899f9ab2d2394fe87 Mon Sep 17 00:00:00 2001 From: Jerry Chen Date: Thu, 23 Oct 2025 12:27:10 -0700 Subject: [PATCH 184/216] Create fallback event handler library for Swift Testing and XCTest interop (#1369) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This library will eventually be visible in the toolchain, and either testing library (Swift Testing or XCTest) will be able to use it to pass along unhandled issues to a test runner from the other framework, enabling interoperability. ### Modifications: No tests are included in this change because the interop library must make it into the toolchain first before we can write tests against it. Tests will be coming in the near future! ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. - [x] Toolchain build succeeds - [x] Expected symbols are visible in the toolchain build* Ubuntu: ``` ~/Downloads/usr ❯ nm lib/swift/linux/lib_TestingInterop.so | rg handler • 1:0000000000002140 b $s15_TestingInterop21_fallbackEventHandler33_AF82E98923290C49994CD1CA5A7BD647LL15Synchronization6AtomicVySVSgGvp 2:0000000000000b50 T $s15_TestingInterop38_swift_testing_getFallbackEventHandlerySPys4Int8VG_SVSiSVSgtYbXCSgyF 3:0000000000000b70 T $s15_TestingInterop42_swift_testing_installFallbackEventHandlerySbySPys4Int8VG_SVSiSVSgtYbXCF 57:0000000000000b40 T _swift_testing_getFallbackEventHandler 58:0000000000000b60 T _swift_testing_installFallbackEventHandler ``` macOS: ``` ~/Downloads/Library ❯ nm Developer/Toolchains/swift-PR-84971-2110.xctoolchain/usr/lib/swift/macosx/testing/lib_TestingInterop.dylib | rg handler • 1:0000000000003bbc t _$s15_TestingInterop21_fallbackEventHandler33_AF82E98923290C49994CD1CA5A7BD647LL_WZ 2:0000000000008010 b _$s15_TestingInterop21_fallbackEventHandler33_AF82E98923290C49994CD1CA5A7BD647LL_Wz 3:0000000000008018 b _$s15_TestingInterop21_fallbackEventHandler33_AF82E98923290C49994CD1CA5A7BD647LLs13ManagedBufferCyySPys4Int8VG_SVSiSVSgtYbXCSgSo16os_unfair_lock_sVGvp 4:0000000000003c50 T _$s15_TestingInterop38_swift_testing_getFallbackEventHandlerySPys4Int8VG_SVSiSVSgtYbXCSgyF 5:0000000000003d24 T _$s15_TestingInterop42_swift_testing_installFallbackEventHandlerySbySPys4Int8VG_SVSiSVSgtYbXCF 46:0000000000003bf0 T __swift_testing_getFallbackEventHandler 47:0000000000003cb0 T __swift_testing_installFallbackEventHandler ``` *Windows does not have the interop lib yet since it requires changes to https://github.com/swiftlang/swift-installer-scripts. This will be done in a follow-up :) --- Sources/CMakeLists.txt | 1 + Sources/_TestingInterop/CMakeLists.txt | 24 ++++ .../FallbackEventHandler.swift | 106 ++++++++++++++++++ 3 files changed, 131 insertions(+) create mode 100644 Sources/_TestingInterop/CMakeLists.txt create mode 100644 Sources/_TestingInterop/FallbackEventHandler.swift diff --git a/Sources/CMakeLists.txt b/Sources/CMakeLists.txt index 09e5e9fd6..4fc0847b7 100644 --- a/Sources/CMakeLists.txt +++ b/Sources/CMakeLists.txt @@ -104,6 +104,7 @@ endif() include(AvailabilityDefinitions) include(CompilerSettings) add_subdirectory(_TestDiscovery) +add_subdirectory(_TestingInterop) add_subdirectory(_TestingInternals) add_subdirectory(Overlays) add_subdirectory(Testing) diff --git a/Sources/_TestingInterop/CMakeLists.txt b/Sources/_TestingInterop/CMakeLists.txt new file mode 100644 index 000000000..adbf7037b --- /dev/null +++ b/Sources/_TestingInterop/CMakeLists.txt @@ -0,0 +1,24 @@ +# 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 http://swift.org/LICENSE.txt for license information +# See http://swift.org/CONTRIBUTORS.txt for Swift project authors + +add_library(_TestingInterop + FallbackEventHandler.swift) + +target_link_libraries(_TestingInterop PRIVATE + _TestingInternals) +if(NOT BUILD_SHARED_LIBS) + # When building a static library, tell clients to autolink the internal + # libraries. + target_compile_options(_TestingInterop PRIVATE + "SHELL:-Xfrontend -public-autolink-library -Xfrontend _TestingInternals") +endif() +target_compile_options(_TestingInterop PRIVATE + -enable-library-evolution + -emit-module-interface -emit-module-interface-path $/_TestingInterop.swiftinterface) + +_swift_testing_install_target(_TestingInterop) diff --git a/Sources/_TestingInterop/FallbackEventHandler.swift b/Sources/_TestingInterop/FallbackEventHandler.swift new file mode 100644 index 000000000..2e33cd04d --- /dev/null +++ b/Sources/_TestingInterop/FallbackEventHandler.swift @@ -0,0 +1,106 @@ +// +// 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 !SWT_NO_INTEROP +#if SWT_TARGET_OS_APPLE && !SWT_NO_OS_UNFAIR_LOCK +private import _TestingInternals +#else +private import Synchronization +#endif + +#if SWT_TARGET_OS_APPLE && !SWT_NO_OS_UNFAIR_LOCK +/// The installed event handler. +private nonisolated(unsafe) let _fallbackEventHandler = { + let result = ManagedBuffer.create( + minimumCapacity: 1, + makingHeaderWith: { _ in nil } + ) + result.withUnsafeMutablePointerToHeader { $0.initialize(to: nil) } + return result +}() +#else +/// The installed event handler. +private nonisolated(unsafe) let _fallbackEventHandler = Atomic(nil) +#endif + +/// A type describing a fallback event handler that testing API can invoke as an +/// alternate method of reporting test events to the current test runner. +/// +/// For example, an `XCTAssert` failure in the body of a Swift Testing test +/// cannot record issues directly with the Swift Testing runner. Instead, the +/// framework packages the assertion failure as a JSON `Event` and invokes this +/// handler to report the failure. +/// +/// - Parameters: +/// - recordJSONSchemaVersionNumber: The JSON schema version used to encode +/// the event record. +/// - recordJSONBaseAddress: A pointer to the first byte of the encoded event. +/// - recordJSONByteCount: The size of the encoded event in bytes. +/// - reserved: Reserved for future use. +@usableFromInline +package typealias FallbackEventHandler = @Sendable @convention(c) ( + _ recordJSONSchemaVersionNumber: UnsafePointer, + _ recordJSONBaseAddress: UnsafeRawPointer, + _ recordJSONByteCount: Int, + _ reserved: UnsafeRawPointer? +) -> Void + +/// Get the current fallback event handler. +/// +/// - Returns: The currently-set handler function, if any. +@_cdecl("_swift_testing_getFallbackEventHandler") +@usableFromInline +package func _swift_testing_getFallbackEventHandler() -> FallbackEventHandler? { +#if SWT_TARGET_OS_APPLE && !SWT_NO_OS_UNFAIR_LOCK + return _fallbackEventHandler.withUnsafeMutablePointers { fallbackEventHandler, lock in + os_unfair_lock_lock(lock) + defer { + os_unfair_lock_unlock(lock) + } + return fallbackEventHandler.pointee + } +#else + return _fallbackEventHandler.load(ordering: .sequentiallyConsistent).flatMap { fallbackEventHandler in + unsafeBitCast(fallbackEventHandler, to: FallbackEventHandler?.self) + } +#endif +} + +/// Set the current fallback event handler if one has not already been set. +/// +/// - Parameters: +/// - handler: The handler function to set. +/// +/// - Returns: Whether or not `handler` was installed. +/// +/// The fallback event handler can only be installed once per process, typically +/// by the first testing library to run. If this function has already been +/// called and the handler set, it does not replace the previous handler. +@_cdecl("_swift_testing_installFallbackEventHandler") +@usableFromInline +package func _swift_testing_installFallbackEventHandler(_ handler: FallbackEventHandler) -> CBool { +#if SWT_TARGET_OS_APPLE && !SWT_NO_OS_UNFAIR_LOCK + return _fallbackEventHandler.withUnsafeMutablePointers { fallbackEventHandler, lock in + os_unfair_lock_lock(lock) + defer { + os_unfair_lock_unlock(lock) + } + guard fallbackEventHandler.pointee == nil else { + return false + } + fallbackEventHandler.pointee = handler + return true + } +#else + let handler = unsafeBitCast(handler, to: UnsafeRawPointer.self) + return _fallbackEventHandler.compareExchange(expected: nil, desired: handler, ordering: .sequentiallyConsistent).exchanged +#endif +} +#endif From 6d7b81e54d9b9494cdf1431be4def533f9b40b3e Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Thu, 23 Oct 2025 16:06:35 -0400 Subject: [PATCH 185/216] Enable iOS GitHub Actions workflow. (#1376) Enable iOS GitHub Actions workflow added in https://github.com/swiftlang/github-workflows/pull/169. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- .github/workflows/pull_request.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 084fd1d87..45d14cb3e 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -21,6 +21,8 @@ jobs: windows_swift_versions: '["nightly-main", "nightly-6.2"]' enable_macos_checks: true macos_exclude_xcode_versions: '[{"xcode_version": "16.2"}, {"xcode_version": "16.3"}, {"xcode_version": "16.4"}]' + enable_ios_checks: true + ios_host_exclude_xcode_versions: '[{"xcode_version": "16.2"}, {"xcode_version": "16.3"}, {"xcode_version": "16.4"}]' enable_wasm_sdk_build: true soundness: name: Soundness From 4889f152d9843f46c103f9e1077cb4af10485d80 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Fri, 24 Oct 2025 11:30:38 -0400 Subject: [PATCH 186/216] Add `@_preInverseGenerics` to `Attachment: CustomStringConvertible`. (#1378) To avoid a crash in Xcode 26, we need `Attachment`'s conformance to `CustomStringConvertible` to not take `~Copyable` into account. Yay. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- Sources/Testing/Attachments/Attachment.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/Testing/Attachments/Attachment.swift b/Sources/Testing/Attachments/Attachment.swift index 1313b0d41..b22911919 100644 --- a/Sources/Testing/Attachments/Attachment.swift +++ b/Sources/Testing/Attachments/Attachment.swift @@ -193,6 +193,7 @@ public struct AnyAttachable: AttachableWrapper, Sendable, Copyable { // MARK: - Describing an attachment +@_preInverseGenerics extension Attachment: CustomStringConvertible where AttachableValue: ~Copyable { /// @Metadata { /// @Available(Swift, introduced: 6.2) From db3ec852ab4f14b1195220cf1fe245dd8fb8d866 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Mon, 27 Oct 2025 16:14:45 -0400 Subject: [PATCH 187/216] Enable Android build in CI (no tests, only build). (#1382) Enable the Android CI workflow added in https://github.com/swiftlang/github-workflows/pull/172. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- .github/workflows/pull_request.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 45d14cb3e..086b2226e 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -24,6 +24,7 @@ jobs: enable_ios_checks: true ios_host_exclude_xcode_versions: '[{"xcode_version": "16.2"}, {"xcode_version": "16.3"}, {"xcode_version": "16.4"}]' enable_wasm_sdk_build: true + enable_android_sdk_build: true soundness: name: Soundness uses: swiftlang/github-workflows/.github/workflows/soundness.yml@main From b1789fd8ef9e588c533af8e34ac48b0c8e5cfa4e Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Mon, 27 Oct 2025 19:35:53 -0400 Subject: [PATCH 188/216] Ensure `NonisolatedNonsendingByDefault` doesn't break exit tests. (#1383) When `NonisolatedNonsendingByDefault` is enabled, overload resolution of `ExitTest.__store()` picks the overload that takes any old `T` instead of taking a function. This overload exists only to suppress certain unhelpful compiler diagnostics and its implementation immediately aborts, which causes the described failure. Adding `nonisolated(nonsending)` or `@concurrent` to the "good" overload doesn't appear to satisfy the type checker, so mark the "bad" overload as explicitly disfavoured instead. I am unable to add a unit test for this case due to https://github.com/swiftlang/swift-package-manager/issues/9293. Resolves #1375. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- Sources/Testing/ExitTests/ExitTest.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/Testing/ExitTests/ExitTest.swift b/Sources/Testing/ExitTests/ExitTest.swift index 56096906f..da4da4c91 100644 --- a/Sources/Testing/ExitTests/ExitTest.swift +++ b/Sources/Testing/ExitTests/ExitTest.swift @@ -392,6 +392,7 @@ extension ExitTest { /// /// - Warning: This function is used to implement the /// `#expect(processExitsWith:)` macro. Do not use it directly. + @_disfavoredOverload @safe public static func __store( _ id: (UInt64, UInt64, UInt64, UInt64), _ body: T, From 664d24f58a1d94ff807d847937bcf84d6afac483 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Wed, 29 Oct 2025 12:57:05 -0400 Subject: [PATCH 189/216] Avoid `unsafeBitCast()` in the new interop target. (#1384) This PR uses `Unmanaged` to avoid calling `unsafeBitCast()` in the new `_TestingInterop` target on platforms that can use `Atomic`. This improves type safety (yay!) ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- Package.swift | 21 +++++++++ .../FallbackEventHandler.swift | 45 ++++++++++++++----- 2 files changed, 56 insertions(+), 10 deletions(-) diff --git a/Package.swift b/Package.swift index 2788502c3..a82241527 100644 --- a/Package.swift +++ b/Package.swift @@ -105,6 +105,17 @@ let package = Package( ) ) +#if DEBUG + // Build _TestingInterop for debugging/testing purposes only. It is + // important that clients do not link to this product/target. + result += [ + .library( + name: "_TestingInterop_DO_NOT_USE", + targets: ["_TestingInterop_DO_NOT_USE"] + ) + ] +#endif + return result }(), @@ -209,6 +220,16 @@ let package = Package( cxxSettings: .packageSettings, swiftSettings: .packageSettings + .enableLibraryEvolution() ), + .target( + // Build _TestingInterop for debugging/testing purposes only. It is + // important that clients do not link to this product/target. + name: "_TestingInterop_DO_NOT_USE", + dependencies: ["_TestingInternals",], + path: "Sources/_TestingInterop", + exclude: ["CMakeLists.txt"], + cxxSettings: .packageSettings, + swiftSettings: .packageSettings + ), // Cross-import overlays (not supported by Swift Package Manager) .target( diff --git a/Sources/_TestingInterop/FallbackEventHandler.swift b/Sources/_TestingInterop/FallbackEventHandler.swift index 2e33cd04d..bd4286315 100644 --- a/Sources/_TestingInterop/FallbackEventHandler.swift +++ b/Sources/_TestingInterop/FallbackEventHandler.swift @@ -9,13 +9,13 @@ // #if !SWT_NO_INTEROP -#if SWT_TARGET_OS_APPLE && !SWT_NO_OS_UNFAIR_LOCK +#if SWT_TARGET_OS_APPLE && !SWT_NO_OS_UNFAIR_LOCK && !hasFeature(Embedded) private import _TestingInternals #else private import Synchronization #endif -#if SWT_TARGET_OS_APPLE && !SWT_NO_OS_UNFAIR_LOCK +#if SWT_TARGET_OS_APPLE && !SWT_NO_OS_UNFAIR_LOCK && !hasFeature(Embedded) /// The installed event handler. private nonisolated(unsafe) let _fallbackEventHandler = { let result = ManagedBuffer.create( @@ -26,8 +26,17 @@ private nonisolated(unsafe) let _fallbackEventHandler = { return result }() #else +/// `Atomic`-compatible storage for ``FallbackEventHandler``. +private final class _FallbackEventHandlerStorage: Sendable, RawRepresentable { + let rawValue: FallbackEventHandler + + init(rawValue: FallbackEventHandler) { + self.rawValue = rawValue + } +} + /// The installed event handler. -private nonisolated(unsafe) let _fallbackEventHandler = Atomic(nil) +private let _fallbackEventHandler = Atomic?>(nil) #endif /// A type describing a fallback event handler that testing API can invoke as an @@ -58,7 +67,7 @@ package typealias FallbackEventHandler = @Sendable @convention(c) ( @_cdecl("_swift_testing_getFallbackEventHandler") @usableFromInline package func _swift_testing_getFallbackEventHandler() -> FallbackEventHandler? { -#if SWT_TARGET_OS_APPLE && !SWT_NO_OS_UNFAIR_LOCK +#if SWT_TARGET_OS_APPLE && !SWT_NO_OS_UNFAIR_LOCK && !hasFeature(Embedded) return _fallbackEventHandler.withUnsafeMutablePointers { fallbackEventHandler, lock in os_unfair_lock_lock(lock) defer { @@ -67,8 +76,14 @@ package func _swift_testing_getFallbackEventHandler() -> FallbackEventHandler? { return fallbackEventHandler.pointee } #else - return _fallbackEventHandler.load(ordering: .sequentiallyConsistent).flatMap { fallbackEventHandler in - unsafeBitCast(fallbackEventHandler, to: FallbackEventHandler?.self) + // If we had a setter, this load would present a race condition because + // another thread could store a new value in between the load and the call to + // `takeUnretainedValue()`, resulting in a use-after-free on this thread. We + // would need a full lock in order to avoid that problem. However, because we + // instead have a one-time installation function, we can be sure that the + // loaded value (if non-nil) will never be replaced with another value. + return _fallbackEventHandler.load(ordering: .sequentiallyConsistent).map { fallbackEventHandler in + fallbackEventHandler.takeUnretainedValue().rawValue } #endif } @@ -86,8 +101,10 @@ package func _swift_testing_getFallbackEventHandler() -> FallbackEventHandler? { @_cdecl("_swift_testing_installFallbackEventHandler") @usableFromInline package func _swift_testing_installFallbackEventHandler(_ handler: FallbackEventHandler) -> CBool { -#if SWT_TARGET_OS_APPLE && !SWT_NO_OS_UNFAIR_LOCK - return _fallbackEventHandler.withUnsafeMutablePointers { fallbackEventHandler, lock in + var result = false + +#if SWT_TARGET_OS_APPLE && !SWT_NO_OS_UNFAIR_LOCK && !hasFeature(Embedded) + result = _fallbackEventHandler.withUnsafeMutablePointers { fallbackEventHandler, lock in os_unfair_lock_lock(lock) defer { os_unfair_lock_unlock(lock) @@ -99,8 +116,16 @@ package func _swift_testing_installFallbackEventHandler(_ handler: FallbackEvent return true } #else - let handler = unsafeBitCast(handler, to: UnsafeRawPointer.self) - return _fallbackEventHandler.compareExchange(expected: nil, desired: handler, ordering: .sequentiallyConsistent).exchanged + let handler = Unmanaged.passRetained(_FallbackEventHandlerStorage(rawValue: handler)) + defer { + if !result { + handler.release() + } + } + + result = _fallbackEventHandler.compareExchange(expected: nil, desired: handler, ordering: .sequentiallyConsistent).exchanged #endif + + return result } #endif From ec463f93efaab31a16611e8065ea399d8eca2688 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Mon, 3 Nov 2025 15:49:07 -0500 Subject: [PATCH 190/216] Disfavor overloads of `__checkFunctionCall()` with variadic generics. (#1389) This works around rdar://122011759 which appears to have been exacerbated by https://github.com/swiftlang/swift/pull/84907. For example: ```swift let file = try #require(fmemopen(nil, 1, "wb+")) ``` The third parameter of `fmemopen()` is of type `const char *` (AKA `UnsafePointer`) and we're passing the string literal `"wb+"` which is implicitly cast to a C string by the compiler. However, overload resolution of the expansion function used by `#require()` is now preferring an overload that uses variadic generics, and that overload is impacted by a compiler issue (rdar://122011759) that causes it to pass a garbage pointer as the temporary C string. Previously, the type checker would pick an overload with a concrete, finite set of generic arguments (`` instead of ``.) ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- Sources/Testing/Expectations/ExpectationChecking+Macro.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Sources/Testing/Expectations/ExpectationChecking+Macro.swift b/Sources/Testing/Expectations/ExpectationChecking+Macro.swift index b47dbd910..ed81d1f59 100644 --- a/Sources/Testing/Expectations/ExpectationChecking+Macro.swift +++ b/Sources/Testing/Expectations/ExpectationChecking+Macro.swift @@ -198,6 +198,7 @@ private func _callBinaryOperator( /// /// - Warning: This function is used to implement the `#expect()` and /// `#require()` macros. Do not call it directly. +@_disfavoredOverload public func __checkFunctionCall( _ lhs: T, calling functionCall: (T, repeat each U) throws -> Bool, _ arguments: repeat each U, expression: __Expression, @@ -367,6 +368,7 @@ public func __checkInoutFunctionCall( /// /// - Warning: This function is used to implement the `#expect()` and /// `#require()` macros. Do not call it directly. +@_disfavoredOverload public func __checkFunctionCall( _ lhs: T, calling functionCall: (T, repeat each U) throws -> R?, _ arguments: repeat each U, expression: __Expression, From 3dabdba2ce1729e4f57e8446ef844b2b0634ec0b Mon Sep 17 00:00:00 2001 From: Stuart Montgomery Date: Tue, 4 Nov 2025 19:53:31 -0600 Subject: [PATCH 191/216] Update the comment and bug ID explaining why a workaround in ConfirmationTests is still needed (#1391) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This has no functional change, it just updates the comment and tracking bug ID explaining why a certain workaround in `ConfirmationTests.swift` for existential composition of parameterized protocol types is still necessary. ### Motivation: I tried removing this workaround recently, since it should no longer be necessary, but discovered that while the syntax is now valid in Swift 6.2, there's a new blocker—the runtime crash tracked by the new bug ID. It's helpful to document this so we know why the workaround is still needed. Note that even if/when this remaining issue is resolved, this capability will require a certain minimum OS version on Apple platforms, since the compiler fix had a runtime component. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- Tests/TestingTests/ConfirmationTests.swift | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Tests/TestingTests/ConfirmationTests.swift b/Tests/TestingTests/ConfirmationTests.swift index 2551513eb..454572edc 100644 --- a/Tests/TestingTests/ConfirmationTests.swift +++ b/Tests/TestingTests/ConfirmationTests.swift @@ -168,8 +168,10 @@ struct UnsuccessfulConfirmationTests { // MARK: - /// Needed since we don't have generic test functions, so we need a concrete -/// argument type for `confirmedOutOfRange(_:)`, but we can't write -/// `any RangeExpression & Sendable`. ([96960993](rdar://96960993)) +/// argument type for `confirmedOutOfRange(_:)`. Although we can now write +/// `any RangeExpression & Sequence & Sendable` as of Swift 6.2 +/// (per [swiftlang/swift#76705](https://github.com/swiftlang/swift/pull/76705)), +/// attempting to form an array of such values crashes at runtime. ([163980446](rdar://163980446)) protocol ExpectedCount: RangeExpression, Sequence, Sendable where Bound == Int, Element == Int {} extension ClosedRange: ExpectedCount {} extension PartialRangeFrom: ExpectedCount {} From c668222c45671255d2fa942fe43db774459e421d Mon Sep 17 00:00:00 2001 From: Stuart Montgomery Date: Wed, 5 Nov 2025 09:41:55 -0600 Subject: [PATCH 192/216] Update CODEOWNERS file (#1393) This updates the `CODEOWNERS` file to remove @suzannaratcliff as a code owner to better reflect her current focus areas. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- CODEOWNERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CODEOWNERS b/CODEOWNERS index 0c6a4fc9a..24d168f0e 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -8,4 +8,4 @@ # See https://swift.org/CONTRIBUTORS.txt for Swift project authors # -* @stmontgomery @grynspan @briancroom @suzannaratcliff @jerryjrchen +* @stmontgomery @grynspan @briancroom @jerryjrchen From 0cbe3ec5f8802664b39a182528306575c780342a Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Wed, 5 Nov 2025 21:25:06 +0000 Subject: [PATCH 193/216] Apply changes from ST-0017. (#1359) This PR applies the changes from [ST-0017](https://github.com/swiftlang/swift-evolution/pull/2985). It merges `AttachableAsCGImage` and `AttachableAsIWICBitmapSource` into a single `AttachableAsImage` protocol and it adjusts the interfaces of `AttachableImageFormat` and `Attachment where AttachableValue: AttachableAsImage`. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- ....swift => NSImage+AttachableAsImage.swift} | 8 +- .../Overlays/_Testing_AppKit/CMakeLists.txt | 2 +- .../Attachments/AttachableAsCGImage.swift | 93 ++++----- .../AttachableImageFormat+UTType.swift | 111 +++++------ ....swift => CGImage+AttachableAsImage.swift} | 4 +- ...chableImageWrapper+AttachableWrapper.swift | 92 ++++----- .../_Testing_CoreGraphics/CMakeLists.txt | 4 +- ....swift => CIImage+AttachableAsImage.swift} | 4 +- .../_Testing_CoreImage/CMakeLists.txt | 2 +- ....swift => UIImage+AttachableAsImage.swift} | 13 +- .../Overlays/_Testing_UIKit/CMakeLists.txt | 2 +- .../AttachableAsIWICBitmapSource.swift | 181 +++++++++++------- .../AttachableImageFormat+CLSID.swift | 130 +++++++------ ...HBITMAP+AttachableAsIWICBitmapSource.swift | 1 - .../HICON+AttachableAsIWICBitmapSource.swift | 1 - ...pSource+AttachableAsIWICBitmapSource.swift | 1 - ...Pointer+AttachableAsIWICBitmapSource.swift | 18 +- ...chableImageWrapper+AttachableWrapper.swift | 134 +++---------- .../Support/Additions/GUIDAdditions.swift | 46 +++-- .../Images/AttachableAsImage.swift | 131 +++++++++++++ .../Images/AttachableImageFormat.swift | 69 ++++++- ...ift => Attachment+AttachableAsImage.swift} | 9 +- .../Images/_AttachableAsImage.swift | 66 ------- .../Images/_AttachableImageWrapper.swift | 19 +- Sources/Testing/CMakeLists.txt | 4 +- Sources/Testing/Testing.docc/Attachments.md | 5 +- Tests/TestingTests/AttachmentTests.swift | 58 +++++- 27 files changed, 660 insertions(+), 548 deletions(-) rename Sources/Overlays/_Testing_AppKit/Attachments/{NSImage+AttachableAsCGImage.swift => NSImage+AttachableAsImage.swift} (92%) rename Sources/Overlays/_Testing_CoreGraphics/Attachments/{CGImage+AttachableAsCGImage.swift => CGImage+AttachableAsImage.swift} (84%) rename Sources/Overlays/_Testing_CoreImage/Attachments/{CIImage+AttachableAsCGImage.swift => CIImage+AttachableAsImage.swift} (90%) rename Sources/Overlays/_Testing_UIKit/Attachments/{UIImage+AttachableAsCGImage.swift => UIImage+AttachableAsImage.swift} (85%) create mode 100644 Sources/Testing/Attachments/Images/AttachableAsImage.swift rename Sources/Testing/Attachments/Images/{Attachment+_AttachableAsImage.swift => Attachment+AttachableAsImage.swift} (97%) delete mode 100644 Sources/Testing/Attachments/Images/_AttachableAsImage.swift diff --git a/Sources/Overlays/_Testing_AppKit/Attachments/NSImage+AttachableAsCGImage.swift b/Sources/Overlays/_Testing_AppKit/Attachments/NSImage+AttachableAsImage.swift similarity index 92% rename from Sources/Overlays/_Testing_AppKit/Attachments/NSImage+AttachableAsCGImage.swift rename to Sources/Overlays/_Testing_AppKit/Attachments/NSImage+AttachableAsImage.swift index ef601f46f..0db26feb6 100644 --- a/Sources/Overlays/_Testing_AppKit/Attachments/NSImage+AttachableAsCGImage.swift +++ b/Sources/Overlays/_Testing_AppKit/Attachments/NSImage+AttachableAsImage.swift @@ -36,13 +36,13 @@ extension NSImageRep { /// @Metadata { /// @Available(Swift, introduced: 6.3) /// } -extension NSImage: AttachableAsCGImage { +extension NSImage: AttachableAsImage, AttachableAsCGImage { /// @Metadata { /// @Available(Swift, introduced: 6.3) /// } - public var attachableCGImage: CGImage { + package var attachableCGImage: CGImage { get throws { - let ctm = AffineTransform(scale: _attachmentScaleFactor) as NSAffineTransform + let ctm = AffineTransform(scale: attachmentScaleFactor) as NSAffineTransform guard let result = cgImage(forProposedRect: nil, context: nil, hints: [.ctm: ctm]) else { throw ImageAttachmentError.couldNotCreateCGImage } @@ -50,7 +50,7 @@ extension NSImage: AttachableAsCGImage { } } - public var _attachmentScaleFactor: CGFloat { + package var attachmentScaleFactor: CGFloat { let maxRepWidth = representations.lazy .map { CGFloat($0.pixelsWide) / $0.size.width } .filter { $0 > 0.0 } diff --git a/Sources/Overlays/_Testing_AppKit/CMakeLists.txt b/Sources/Overlays/_Testing_AppKit/CMakeLists.txt index e864509f4..4ce37cfb7 100644 --- a/Sources/Overlays/_Testing_AppKit/CMakeLists.txt +++ b/Sources/Overlays/_Testing_AppKit/CMakeLists.txt @@ -8,7 +8,7 @@ if (CMAKE_SYSTEM_NAME STREQUAL "Darwin") add_library(_Testing_AppKit - Attachments/NSImage+AttachableAsCGImage.swift + Attachments/NSImage+AttachableAsImage.swift ReexportTesting.swift) target_link_libraries(_Testing_AppKit PUBLIC diff --git a/Sources/Overlays/_Testing_CoreGraphics/Attachments/AttachableAsCGImage.swift b/Sources/Overlays/_Testing_CoreGraphics/Attachments/AttachableAsCGImage.swift index bae0b38f5..4bb060377 100644 --- a/Sources/Overlays/_Testing_CoreGraphics/Attachments/AttachableAsCGImage.swift +++ b/Sources/Overlays/_Testing_CoreGraphics/Attachments/AttachableAsCGImage.swift @@ -9,42 +9,21 @@ // #if SWT_TARGET_OS_APPLE && canImport(CoreGraphics) -public import CoreGraphics -private import ImageIO +package import CoreGraphics +package import ImageIO +private import UniformTypeIdentifiers /// A protocol describing images that can be converted to instances of -/// [`Attachment`](https://developer.apple.com/documentation/testing/attachment). +/// [`Attachment`](https://developer.apple.com/documentation/testing/attachment) +/// and which can be represented as instances of [`CGImage`](https://developer.apple.com/documentation/coregraphics/cgimage). /// -/// Instances of types conforming to this protocol do not themselves conform to -/// [`Attachable`](https://developer.apple.com/documentation/testing/attachable). -/// Instead, the testing library provides additional initializers on [`Attachment`](https://developer.apple.com/documentation/testing/attachment) -/// that take instances of such types and handle converting them to image data when needed. -/// -/// You can attach instances of the following system-provided image types to a -/// test: -/// -/// | Platform | Supported Types | -/// |-|-| -/// | macOS | [`CGImage`](https://developer.apple.com/documentation/coregraphics/cgimage), [`CIImage`](https://developer.apple.com/documentation/coreimage/ciimage), [`NSImage`](https://developer.apple.com/documentation/appkit/nsimage) | -/// | iOS, watchOS, tvOS, and visionOS | [`CGImage`](https://developer.apple.com/documentation/coregraphics/cgimage), [`CIImage`](https://developer.apple.com/documentation/coreimage/ciimage), [`UIImage`](https://developer.apple.com/documentation/uikit/uiimage) | -/// | Windows | [`HBITMAP`](https://learn.microsoft.com/en-us/windows/win32/gdi/bitmaps), [`HICON`](https://learn.microsoft.com/en-us/windows/win32/menurc/icons), [`IWICBitmapSource`](https://learn.microsoft.com/en-us/windows/win32/api/wincodec/nn-wincodec-iwicbitmapsource) (including its subclasses declared by Windows Imaging Component) | -/// -/// You do not generally need to add your own conformances to this protocol. If -/// you have an image in another format that needs to be attached to a test, -/// first convert it to an instance of one of the types above. -/// -/// @Metadata { -/// @Available(Swift, introduced: 6.3) -/// } +/// This protocol is not part of the public interface of the testing library. It +/// encapsulates Apple-specific logic for image attachments. @available(_uttypesAPI, *) -public protocol AttachableAsCGImage: _AttachableAsImage, SendableMetatype { +package protocol AttachableAsCGImage: AttachableAsImage { /// An instance of `CGImage` representing this image. /// /// - Throws: Any error that prevents the creation of an image. - /// - /// @Metadata { - /// @Available(Swift, introduced: 6.3) - /// } var attachableCGImage: CGImage { get throws } /// The orientation of the image. @@ -53,9 +32,9 @@ public protocol AttachableAsCGImage: _AttachableAsImage, SendableMetatype { /// `CGImagePropertyOrientation`. The default value of this property is /// `.up`. /// - /// This property is not part of the public interface of the testing - /// library. It may be removed in a future update. - var _attachmentOrientation: UInt32 { get } + /// This property is not part of the public interface of the testing library. + /// It may be removed in a future update. + var attachmentOrientation: CGImagePropertyOrientation { get } /// The scale factor of the image. /// @@ -63,28 +42,54 @@ public protocol AttachableAsCGImage: _AttachableAsImage, SendableMetatype { /// originates from a Retina Display screenshot or similar. The default value /// of this property is `1.0`. /// - /// This property is not part of the public interface of the testing - /// library. It may be removed in a future update. - var _attachmentScaleFactor: CGFloat { get } + /// This property is not part of the public interface of the testing library. + /// It may be removed in a future update. + var attachmentScaleFactor: CGFloat { get } } @available(_uttypesAPI, *) extension AttachableAsCGImage { - public var _attachmentOrientation: UInt32 { - CGImagePropertyOrientation.up.rawValue + package var attachmentOrientation: CGImagePropertyOrientation { + .up } - public var _attachmentScaleFactor: CGFloat { + package var attachmentScaleFactor: CGFloat { 1.0 } - public func _deinitializeAttachableValue() {} -} + public func withUnsafeBytes(as imageFormat: AttachableImageFormat, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { + let data = NSMutableData() -@available(_uttypesAPI, *) -extension AttachableAsCGImage where Self: Sendable { - public func _copyAttachableValue() -> Self { - self + // Convert the image to a CGImage. + let attachableCGImage = try attachableCGImage + + // Create the image destination. + guard let dest = CGImageDestinationCreateWithData(data as CFMutableData, imageFormat.contentType.identifier as CFString, 1, nil) else { + throw ImageAttachmentError.couldNotCreateImageDestination + } + + // Configure the properties of the image conversion operation. + let orientation = attachmentOrientation + let scaleFactor = attachmentScaleFactor + let properties: [CFString: Any] = [ + kCGImageDestinationLossyCompressionQuality: CGFloat(imageFormat.encodingQuality), + kCGImagePropertyOrientation: orientation, + kCGImagePropertyDPIWidth: 72.0 * scaleFactor, + kCGImagePropertyDPIHeight: 72.0 * scaleFactor, + ] + + // Perform the image conversion. + CGImageDestinationAddImage(dest, attachableCGImage, properties as CFDictionary) + guard CGImageDestinationFinalize(dest) else { + throw ImageAttachmentError.couldNotConvertImage + } + + // Pass the bits of the image out to the body. Note that we have an + // NSMutableData here so we have to use slightly different API than we would + // with an instance of Data. + return try withExtendedLifetime(data) { + try body(UnsafeRawBufferPointer(start: data.bytes, count: data.length)) + } } } #endif diff --git a/Sources/Overlays/_Testing_CoreGraphics/Attachments/AttachableImageFormat+UTType.swift b/Sources/Overlays/_Testing_CoreGraphics/Attachments/AttachableImageFormat+UTType.swift index a427ceec9..43c1346e4 100644 --- a/Sources/Overlays/_Testing_CoreGraphics/Attachments/AttachableImageFormat+UTType.swift +++ b/Sources/Overlays/_Testing_CoreGraphics/Attachments/AttachableImageFormat+UTType.swift @@ -9,58 +9,10 @@ // #if SWT_TARGET_OS_APPLE && canImport(CoreGraphics) -public import Testing - public import UniformTypeIdentifiers @available(_uttypesAPI, *) extension AttachableImageFormat { - /// Get the content type to use when encoding the image, substituting a - /// concrete type for `UTType.image` in particular. - /// - /// - Parameters: - /// - imageFormat: The image format to use, or `nil` if the developer did - /// not specify one. - /// - preferredName: The preferred name of the image for which a type is - /// needed. - /// - /// - Returns: An instance of `UTType` referring to a concrete image type. - /// - /// This function is not part of the public interface of the testing library. - static func computeContentType(for imageFormat: Self?, withPreferredName preferredName: String) -> UTType { - guard let imageFormat else { - // The developer didn't specify a type. Substitute the generic `.image` - // and solve for that instead. - return computeContentType(for: Self(.image, encodingQuality: 1.0), withPreferredName: preferredName) - } - - switch imageFormat.kind { - case .png: - return .png - case .jpeg: - return .jpeg - case let .systemValue(contentType): - let contentType = contentType as! UTType - if contentType != .image { - // The developer explicitly specified a type. - return contentType - } - - // The developer didn't specify a concrete type, so try to derive one from - // the preferred name's path extension. - let pathExtension = (preferredName as NSString).pathExtension - if !pathExtension.isEmpty, - let contentType = UTType(filenameExtension: pathExtension, conformingTo: .image), - contentType.isDeclared { - return contentType - } - - // We couldn't derive a concrete type from the path extension, so pick - // between PNG and JPEG based on the encoding quality. - return imageFormat.encodingQuality < 1.0 ? .jpeg : .png - } - } - /// The content type corresponding to this image format. /// /// For example, if this image format equals ``png``, the value of this @@ -72,14 +24,7 @@ extension AttachableImageFormat { /// @Available(Swift, introduced: 6.3) /// } public var contentType: UTType { - switch kind { - case .png: - return .png - case .jpeg: - return .jpeg - case let .systemValue(contentType): - return contentType as! UTType - } + kind.contentType } /// Initialize an instance of this type with the given content type and @@ -100,12 +45,19 @@ extension AttachableImageFormat { /// @Metadata { /// @Available(Swift, introduced: 6.3) /// } - public init(_ contentType: UTType, encodingQuality: Float = 1.0) { - precondition( - contentType.conforms(to: .image), - "An image cannot be attached as an instance of type '\(contentType.identifier)'. Use a type that conforms to 'public.image' instead." - ) - self.init(kind: .systemValue(contentType), encodingQuality: encodingQuality) + public init(contentType: UTType, encodingQuality: Float = 1.0) { + switch contentType { + case .png: + self.init(kind: .png, encodingQuality: encodingQuality) + case .jpeg: + self.init(kind: .jpeg, encodingQuality: encodingQuality) + default: + precondition( + contentType.conforms(to: .image), + "An image cannot be attached as an instance of type '\(contentType.identifier)'. Use a type that conforms to 'public.image' instead." + ) + self.init(kind: .systemValue(contentType), encodingQuality: encodingQuality) + } } /// Construct an instance of this type with the given path extension and @@ -135,11 +87,42 @@ extension AttachableImageFormat { public init?(pathExtension: String, encodingQuality: Float = 1.0) { let pathExtension = pathExtension.drop { $0 == "." } - guard let contentType = UTType(filenameExtension: String(pathExtension), conformingTo: .image) else { + guard let contentType = UTType(filenameExtension: String(pathExtension), conformingTo: .image), + contentType.isDeclared else { return nil } - self.init(contentType, encodingQuality: encodingQuality) + self.init(contentType: contentType, encodingQuality: encodingQuality) + } +} + +// MARK: - CustomStringConvertible, CustomDebugStringConvertible + +@available(_uttypesAPI, *) +extension AttachableImageFormat.Kind: CustomStringConvertible, CustomDebugStringConvertible { + /// The content type corresponding to this image format. + fileprivate var contentType: UTType { + switch self { + case .png: + return .png + case .jpeg: + return .jpeg + case let .systemValue(contentType): + return contentType as! UTType + } + } + + package var description: String { + let contentType = contentType + return contentType.localizedDescription ?? contentType.identifier + } + + package var debugDescription: String { + let contentType = contentType + if let localizedDescription = contentType.localizedDescription { + return "\(localizedDescription) (\(contentType.identifier))" + } + return contentType.identifier } } #endif diff --git a/Sources/Overlays/_Testing_CoreGraphics/Attachments/CGImage+AttachableAsCGImage.swift b/Sources/Overlays/_Testing_CoreGraphics/Attachments/CGImage+AttachableAsImage.swift similarity index 84% rename from Sources/Overlays/_Testing_CoreGraphics/Attachments/CGImage+AttachableAsCGImage.swift rename to Sources/Overlays/_Testing_CoreGraphics/Attachments/CGImage+AttachableAsImage.swift index c4a4fd630..dedff803b 100644 --- a/Sources/Overlays/_Testing_CoreGraphics/Attachments/CGImage+AttachableAsCGImage.swift +++ b/Sources/Overlays/_Testing_CoreGraphics/Attachments/CGImage+AttachableAsImage.swift @@ -14,11 +14,11 @@ public import CoreGraphics /// @Metadata { /// @Available(Swift, introduced: 6.3) /// } -extension CGImage: AttachableAsCGImage { +extension CGImage: AttachableAsImage, AttachableAsCGImage { /// @Metadata { /// @Available(Swift, introduced: 6.3) /// } - public var attachableCGImage: CGImage { + package var attachableCGImage: CGImage { self } } diff --git a/Sources/Overlays/_Testing_CoreGraphics/Attachments/_AttachableImageWrapper+AttachableWrapper.swift b/Sources/Overlays/_Testing_CoreGraphics/Attachments/_AttachableImageWrapper+AttachableWrapper.swift index 3281de11a..16c165ae1 100644 --- a/Sources/Overlays/_Testing_CoreGraphics/Attachments/_AttachableImageWrapper+AttachableWrapper.swift +++ b/Sources/Overlays/_Testing_CoreGraphics/Attachments/_AttachableImageWrapper+AttachableWrapper.swift @@ -9,76 +9,52 @@ // #if SWT_TARGET_OS_APPLE && canImport(CoreGraphics) -public import Testing private import CoreGraphics -private import ImageIO private import UniformTypeIdentifiers -/// ## Why can't images directly conform to Attachable? -/// -/// Three reasons: -/// -/// 1. Several image classes are not marked `Sendable`, which means that as far -/// as Swift is concerned, they cannot be safely passed to Swift Testing's -/// event handler (primarily because `Event` is `Sendable`.) So we would have -/// to eagerly serialize them, which is unnecessarily expensive if we know -/// they're actually concurrency-safe. -/// 2. We would have no place to store metadata such as the encoding quality -/// (although in the future we may introduce a "metadata" associated type to -/// `Attachable` that could store that info.) -/// 3. `Attachable` has a requirement with `Self` in non-parameter, non-return -/// position. As far as Swift is concerned, a non-final class cannot satisfy -/// such a requirement, and all image types we care about are non-final -/// classes. Thus, the compiler will steadfastly refuse to allow non-final -/// classes to conform to the `Attachable` protocol. We could get around this -/// by changing the signature of `withUnsafeBytes()` so that the -/// generic parameter to `Attachment` is not `Self`, but that would defeat -/// much of the purpose of making `Attachment` generic in the first place. -/// (And no, the language does not let us write `where T: Self` anywhere -/// useful.) - @available(_uttypesAPI, *) -extension _AttachableImageWrapper: Attachable, AttachableWrapper where Image: AttachableAsCGImage { - public func withUnsafeBytes(for attachment: borrowing Attachment<_AttachableImageWrapper>, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { - let data = NSMutableData() - - // Convert the image to a CGImage. - let attachableCGImage = try wrappedValue.attachableCGImage - - // Create the image destination. - let contentType = AttachableImageFormat.computeContentType(for: imageFormat, withPreferredName: attachment.preferredName) - guard let dest = CGImageDestinationCreateWithData(data as CFMutableData, contentType.identifier as CFString, 1, nil) else { - throw ImageAttachmentError.couldNotCreateImageDestination +extension _AttachableImageWrapper: Attachable, AttachableWrapper where Image: AttachableAsImage { + /// Get the image format to use when encoding an image, substituting a + /// concrete type for `UTType.image` in particular. + /// + /// - Parameters: + /// - preferredName: The preferred name of the image for which a type is + /// needed. + /// + /// - Returns: An instance of ``AttachableImageFormat`` referring to a + /// concrete image type. + /// + /// This function is not part of the public interface of the testing library. + private func _imageFormat(forPreferredName preferredName: String) -> AttachableImageFormat { + if let imageFormat, case let contentType = imageFormat.contentType, contentType != .image { + // The developer explicitly specified a type. + return imageFormat } - // Configure the properties of the image conversion operation. - let orientation = wrappedValue._attachmentOrientation - let scaleFactor = wrappedValue._attachmentScaleFactor - let properties: [CFString: Any] = [ - kCGImageDestinationLossyCompressionQuality: CGFloat(imageFormat?.encodingQuality ?? 1.0), - kCGImagePropertyOrientation: orientation, - kCGImagePropertyDPIWidth: 72.0 * scaleFactor, - kCGImagePropertyDPIHeight: 72.0 * scaleFactor, - ] - - // Perform the image conversion. - CGImageDestinationAddImage(dest, attachableCGImage, properties as CFDictionary) - guard CGImageDestinationFinalize(dest) else { - throw ImageAttachmentError.couldNotConvertImage + // The developer didn't specify a concrete type, so try to derive one from + // the preferred name's path extension. + let pathExtension = (preferredName as NSString).pathExtension + if !pathExtension.isEmpty, + let contentType = UTType(filenameExtension: pathExtension, conformingTo: .image), + contentType.isDeclared { + return AttachableImageFormat(contentType: contentType) } - // Pass the bits of the image out to the body. Note that we have an - // NSMutableData here so we have to use slightly different API than we would - // with an instance of Data. - return try withExtendedLifetime(data) { - try body(UnsafeRawBufferPointer(start: data.bytes, count: data.length)) - } + // We couldn't derive a concrete type from the path extension, so pick + // between PNG and JPEG based on the encoding quality. + let encodingQuality = imageFormat?.encodingQuality ?? 1.0 + return encodingQuality < 1.0 ? .jpeg : .png + } + + public func withUnsafeBytes(for attachment: borrowing Attachment<_AttachableImageWrapper>, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { + let imageFormat = _imageFormat(forPreferredName: attachment.preferredName) + return try wrappedValue.withUnsafeBytes(as: imageFormat, body) } public borrowing func preferredName(for attachment: borrowing Attachment<_AttachableImageWrapper>, basedOn suggestedName: String) -> String { - let contentType = AttachableImageFormat.computeContentType(for: imageFormat, withPreferredName: suggestedName) - return (suggestedName as NSString).appendingPathExtension(for: contentType) + let imageFormat = _imageFormat(forPreferredName: suggestedName) + return (suggestedName as NSString).appendingPathExtension(for: imageFormat.contentType) } } #endif diff --git a/Sources/Overlays/_Testing_CoreGraphics/CMakeLists.txt b/Sources/Overlays/_Testing_CoreGraphics/CMakeLists.txt index 567428150..e408fa517 100644 --- a/Sources/Overlays/_Testing_CoreGraphics/CMakeLists.txt +++ b/Sources/Overlays/_Testing_CoreGraphics/CMakeLists.txt @@ -8,10 +8,10 @@ if(APPLE) add_library(_Testing_CoreGraphics - Attachments/_AttachableImageWrapper+AttachableWrapper.swift Attachments/AttachableAsCGImage.swift + Attachments/_AttachableImageWrapper+AttachableWrapper.swift Attachments/AttachableImageFormat+UTType.swift - Attachments/CGImage+AttachableAsCGImage.swift + Attachments/CGImage+AttachableAsImage.swift ReexportTesting.swift) target_link_libraries(_Testing_CoreGraphics PUBLIC diff --git a/Sources/Overlays/_Testing_CoreImage/Attachments/CIImage+AttachableAsCGImage.swift b/Sources/Overlays/_Testing_CoreImage/Attachments/CIImage+AttachableAsImage.swift similarity index 90% rename from Sources/Overlays/_Testing_CoreImage/Attachments/CIImage+AttachableAsCGImage.swift rename to Sources/Overlays/_Testing_CoreImage/Attachments/CIImage+AttachableAsImage.swift index 581de2c7c..130e7e831 100644 --- a/Sources/Overlays/_Testing_CoreImage/Attachments/CIImage+AttachableAsCGImage.swift +++ b/Sources/Overlays/_Testing_CoreImage/Attachments/CIImage+AttachableAsImage.swift @@ -15,11 +15,11 @@ public import _Testing_CoreGraphics /// @Metadata { /// @Available(Swift, introduced: 6.3) /// } -extension CIImage: AttachableAsCGImage { +extension CIImage: AttachableAsImage, AttachableAsCGImage { /// @Metadata { /// @Available(Swift, introduced: 6.3) /// } - public var attachableCGImage: CGImage { + package var attachableCGImage: CGImage { get throws { guard let result = CIContext().createCGImage(self, from: extent) else { throw ImageAttachmentError.couldNotCreateCGImage diff --git a/Sources/Overlays/_Testing_CoreImage/CMakeLists.txt b/Sources/Overlays/_Testing_CoreImage/CMakeLists.txt index 8c8076b8b..0295fedc7 100644 --- a/Sources/Overlays/_Testing_CoreImage/CMakeLists.txt +++ b/Sources/Overlays/_Testing_CoreImage/CMakeLists.txt @@ -8,7 +8,7 @@ if(APPLE) add_library(_Testing_CoreImage - Attachments/CIImage+AttachableAsCGImage.swift + Attachments/CIImage+AttachableAsImage.swift ReexportTesting.swift) target_link_libraries(_Testing_CoreImage PUBLIC diff --git a/Sources/Overlays/_Testing_UIKit/Attachments/UIImage+AttachableAsCGImage.swift b/Sources/Overlays/_Testing_UIKit/Attachments/UIImage+AttachableAsImage.swift similarity index 85% rename from Sources/Overlays/_Testing_UIKit/Attachments/UIImage+AttachableAsCGImage.swift rename to Sources/Overlays/_Testing_UIKit/Attachments/UIImage+AttachableAsImage.swift index 3766b3095..6e9ca6838 100644 --- a/Sources/Overlays/_Testing_UIKit/Attachments/UIImage+AttachableAsCGImage.swift +++ b/Sources/Overlays/_Testing_UIKit/Attachments/UIImage+AttachableAsImage.swift @@ -12,7 +12,7 @@ public import UIKit public import _Testing_CoreGraphics -private import ImageIO +package import ImageIO #if canImport(UIKitCore_Private) private import UIKitCore_Private #endif @@ -20,11 +20,11 @@ private import UIKitCore_Private /// @Metadata { /// @Available(Swift, introduced: 6.3) /// } -extension UIImage: AttachableAsCGImage { +extension UIImage: AttachableAsImage, AttachableAsCGImage { /// @Metadata { /// @Available(Swift, introduced: 6.3) /// } - public var attachableCGImage: CGImage { + package var attachableCGImage: CGImage { get throws { #if canImport(UIKitCore_Private) // _UIImageGetCGImageRepresentation() is an internal UIKit function that @@ -49,8 +49,8 @@ extension UIImage: AttachableAsCGImage { } } - public var _attachmentOrientation: UInt32 { - let result: CGImagePropertyOrientation = switch imageOrientation { + package var attachmentOrientation: CGImagePropertyOrientation { + switch imageOrientation { case .up: .up case .down: .down case .left: .left @@ -61,10 +61,9 @@ extension UIImage: AttachableAsCGImage { case .rightMirrored: .rightMirrored @unknown default: .up } - return result.rawValue } - public var _attachmentScaleFactor: CGFloat { + package var attachmentScaleFactor: CGFloat { scale } } diff --git a/Sources/Overlays/_Testing_UIKit/CMakeLists.txt b/Sources/Overlays/_Testing_UIKit/CMakeLists.txt index e6f4ae9d5..908824704 100644 --- a/Sources/Overlays/_Testing_UIKit/CMakeLists.txt +++ b/Sources/Overlays/_Testing_UIKit/CMakeLists.txt @@ -8,7 +8,7 @@ if(APPLE) add_library(_Testing_UIKit - Attachments/UIImage+AttachableAsCGImage.swift + Attachments/UIImage+AttachableAsImage.swift ReexportTesting.swift) target_link_libraries(_Testing_UIKit PUBLIC diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsIWICBitmapSource.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsIWICBitmapSource.swift index 94dfe4299..e459986e7 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsIWICBitmapSource.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsIWICBitmapSource.swift @@ -13,25 +13,11 @@ private import Testing public import WinSDK /// A protocol describing images that can be converted to instances of -/// ``Testing/Attachment``. +/// [`Attachment`](https://developer.apple.com/documentation/testing/attachment) +/// and which can be represented as instances of [`IWICBitmapSource`](https://learn.microsoft.com/en-us/windows/win32/api/wincodec/nn-wincodec-iwicbitmapsource) +/// by address. /// -/// Instances of types conforming to this protocol do not themselves conform to -/// ``Testing/Attachable``. Instead, the testing library provides additional -/// initializers on ``Testing/Attachment`` that take instances of such types and -/// handle converting them to image data when needed. -/// -/// You can attach instances of the following system-provided image types to a -/// test: -/// -/// | Platform | Supported Types | -/// |-|-| -/// | macOS | [`CGImage`](https://developer.apple.com/documentation/coregraphics/cgimage), [`CIImage`](https://developer.apple.com/documentation/coreimage/ciimage), [`NSImage`](https://developer.apple.com/documentation/appkit/nsimage) | -/// | iOS, watchOS, tvOS, and visionOS | [`CGImage`](https://developer.apple.com/documentation/coregraphics/cgimage), [`CIImage`](https://developer.apple.com/documentation/coreimage/ciimage), [`UIImage`](https://developer.apple.com/documentation/uikit/uiimage) | -/// | Windows | [`HBITMAP`](https://learn.microsoft.com/en-us/windows/win32/gdi/bitmaps), [`HICON`](https://learn.microsoft.com/en-us/windows/win32/menurc/icons), [`IWICBitmapSource`](https://learn.microsoft.com/en-us/windows/win32/api/wincodec/nn-wincodec-iwicbitmapsource) (including its subclasses declared by Windows Imaging Component) | -/// -/// You do not generally need to add your own conformances to this protocol. If -/// you have an image in another format that needs to be attached to a test, -/// first convert it to an instance of one of the types above. +/// This protocol is not part of the public interface of the testing library. public protocol _AttachableByAddressAsIWICBitmapSource { /// Create a WIC bitmap source representing an instance of this type at the /// given address. @@ -92,42 +78,12 @@ public protocol _AttachableByAddressAsIWICBitmapSource { } /// A protocol describing images that can be converted to instances of -/// [`Attachment`](https://developer.apple.com/documentation/testing/attachment). -/// -/// Instances of types conforming to this protocol do not themselves conform to -/// [`Attachable`](https://developer.apple.com/documentation/testing/attachable). -/// Instead, the testing library provides additional initializers on [`Attachment`](https://developer.apple.com/documentation/testing/attachment) -/// that take instances of such types and handle converting them to image data when needed. -/// -/// You can attach instances of the following system-provided image types to a -/// test: -/// -/// | Platform | Supported Types | -/// |-|-| -/// | macOS | [`CGImage`](https://developer.apple.com/documentation/coregraphics/cgimage), [`CIImage`](https://developer.apple.com/documentation/coreimage/ciimage), [`NSImage`](https://developer.apple.com/documentation/appkit/nsimage) | -/// | iOS, watchOS, tvOS, and visionOS | [`CGImage`](https://developer.apple.com/documentation/coregraphics/cgimage), [`CIImage`](https://developer.apple.com/documentation/coreimage/ciimage), [`UIImage`](https://developer.apple.com/documentation/uikit/uiimage) | -/// | Windows | [`HBITMAP`](https://learn.microsoft.com/en-us/windows/win32/gdi/bitmaps), [`HICON`](https://learn.microsoft.com/en-us/windows/win32/menurc/icons), [`IWICBitmapSource`](https://learn.microsoft.com/en-us/windows/win32/api/wincodec/nn-wincodec-iwicbitmapsource) (including its subclasses declared by Windows Imaging Component) | -/// -/// You do not generally need to add your own conformances to this protocol. If -/// you have an image in another format that needs to be attached to a test, -/// first convert it to an instance of one of the types above. +/// [`Attachment`](https://developer.apple.com/documentation/testing/attachment) +/// and which can be represented as instances of [`IWICBitmapSource`](https://learn.microsoft.com/en-us/windows/win32/api/wincodec/nn-wincodec-iwicbitmapsource). /// -/// @Metadata { -/// @Available(Swift, introduced: 6.3) -/// } -public protocol AttachableAsIWICBitmapSource: _AttachableAsImage, SendableMetatype { - /// Create a WIC bitmap source representing an instance of this type. - /// - /// - Returns: A pointer to a new WIC bitmap source representing this image. - /// The caller is responsible for releasing this image when done with it. - /// - /// - Throws: Any error that prevented the creation of the WIC bitmap source. - /// - /// @Metadata { - /// @Available(Swift, introduced: 6.3) - /// } - func copyAttachableIWICBitmapSource() throws -> UnsafeMutablePointer - +/// This protocol is not part of the public interface of the testing library. It +/// encapsulates Windows-specific logic for image attachments. +package protocol AttachableAsIWICBitmapSource: AttachableAsImage { /// Create a WIC bitmap representing an instance of this type. /// /// - Parameters: @@ -138,26 +94,115 @@ public protocol AttachableAsIWICBitmapSource: _AttachableAsImage, SendableMetaty /// caller is responsible for releasing this image when done with it. /// /// - Throws: Any error that prevented the creation of the WIC bitmap. - /// - /// The default implementation of this function ignores `factory` and calls - /// ``copyAttachableIWICBitmapSource()``. If your implementation of - /// ``copyAttachableIWICBitmapSource()`` needs to create a WIC imaging factory - /// in order to return a result, it is more efficient to implement this - /// function too so that the testing library can pass the WIC imaging factory - /// it creates. - /// - /// This function is not part of the public interface of the testing library. - /// It may be removed in a future update. - func _copyAttachableIWICBitmapSource( + func copyAttachableIWICBitmapSource( using factory: UnsafeMutablePointer ) throws -> UnsafeMutablePointer } extension AttachableAsIWICBitmapSource { - public func _copyAttachableIWICBitmapSource( - using factory: UnsafeMutablePointer - ) throws -> UnsafeMutablePointer { - try copyAttachableIWICBitmapSource() + public func withUnsafeBytes(as imageFormat: AttachableImageFormat, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { + // Create an in-memory stream to write the image data to. Note that Windows + // documentation recommends SHCreateMemStream() instead, but that function + // does not provide a mechanism to access the underlying memory directly. + var stream: UnsafeMutablePointer? + let rCreateStream = CreateStreamOnHGlobal(nil, true, &stream) + guard S_OK == rCreateStream, let stream else { + throw ImageAttachmentError.comObjectCreationFailed(IStream.self, rCreateStream) + } + defer { + _ = stream.pointee.lpVtbl.pointee.Release(stream) + } + + // Get an imaging factory to create the WIC bitmap and encoder. + let factory = try IWICImagingFactory.create() + defer { + _ = factory.pointee.lpVtbl.pointee.Release(factory) + } + + // Create the bitmap and downcast it to an IWICBitmapSource for later use. + let bitmap = try copyAttachableIWICBitmapSource(using: factory) + defer { + _ = bitmap.pointee.lpVtbl.pointee.Release(bitmap) + } + + // Create the encoder. + let encoder = try withUnsafePointer(to: IID_IWICBitmapEncoder) { IID_IWICBitmapEncoder in + var encoderCLSID = imageFormat.encoderCLSID + var encoder: UnsafeMutableRawPointer? + let rCreate = CoCreateInstance( + &encoderCLSID, + nil, + DWORD(CLSCTX_INPROC_SERVER.rawValue), + IID_IWICBitmapEncoder, + &encoder + ) + guard rCreate == S_OK, let encoder = encoder?.assumingMemoryBound(to: IWICBitmapEncoder.self) else { + throw ImageAttachmentError.comObjectCreationFailed(IWICBitmapEncoder.self, rCreate) + } + return encoder + } + defer { + _ = encoder.pointee.lpVtbl.pointee.Release(encoder) + } + _ = encoder.pointee.lpVtbl.pointee.Initialize(encoder, stream, WICBitmapEncoderNoCache) + + // Create the frame into which the bitmap will be composited. + var frame: UnsafeMutablePointer? + var propertyBag: UnsafeMutablePointer? + let rCreateFrame = encoder.pointee.lpVtbl.pointee.CreateNewFrame(encoder, &frame, &propertyBag) + guard rCreateFrame == S_OK, let frame, let propertyBag else { + throw ImageAttachmentError.comObjectCreationFailed(IWICBitmapFrameEncode.self, rCreateFrame) + } + defer { + _ = frame.pointee.lpVtbl.pointee.Release(frame) + _ = propertyBag.pointee.lpVtbl.pointee.Release(propertyBag) + } + + // Set properties. The only property we currently set is image quality. + do { + try propertyBag.write(imageFormat.encodingQuality, named: "ImageQuality") + } catch ImageAttachmentError.propertyBagWritingFailed(_, HRESULT(bitPattern: 0x80004005)) { + // E_FAIL: This property is not supported for the current encoder/format. + // Eat this error silently as it's not useful to the test author. + } + _ = frame.pointee.lpVtbl.pointee.Initialize(frame, propertyBag) + + // Write the image! + let rWrite = frame.pointee.lpVtbl.pointee.WriteSource(frame, bitmap, nil) + guard rWrite == S_OK else { + throw ImageAttachmentError.imageWritingFailed(rWrite) + } + + // Commit changes through the various layers. + var rCommit = frame.pointee.lpVtbl.pointee.Commit(frame) + guard rCommit == S_OK else { + throw ImageAttachmentError.imageWritingFailed(rCommit) + } + rCommit = encoder.pointee.lpVtbl.pointee.Commit(encoder) + guard rCommit == S_OK else { + throw ImageAttachmentError.imageWritingFailed(rCommit) + } + rCommit = stream.pointee.lpVtbl.pointee.Commit(stream, DWORD(STGC_DEFAULT.rawValue)) + guard rCommit == S_OK else { + throw ImageAttachmentError.imageWritingFailed(rCommit) + } + + // Extract the serialized image and pass it back to the caller. We hold the + // HGLOBAL locked while calling `body`, but nothing else should have a + // reference to it. + var global: HGLOBAL? + let rGetGlobal = GetHGlobalFromStream(stream, &global) + guard S_OK == rGetGlobal else { + throw ImageAttachmentError.globalFromStreamFailed(rGetGlobal) + } + guard let baseAddress = GlobalLock(global) else { + throw Win32Error(rawValue: GetLastError()) + } + defer { + GlobalUnlock(global) + } + let byteCount = GlobalSize(global) + return try body(UnsafeRawBufferPointer(start: baseAddress, count: Int(byteCount))) } } #endif diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableImageFormat+CLSID.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableImageFormat+CLSID.swift index 72d0d8d3d..deaf032c4 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableImageFormat+CLSID.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableImageFormat+CLSID.swift @@ -9,12 +9,11 @@ // #if os(Windows) -public import Testing public import WinSDK extension AttachableImageFormat { - private static let _encoderPathExtensionsByCLSID = Result<[UInt128: [String]], any Error> { - var result = [UInt128: [String]]() + private static let _encoderPathExtensionsByCLSID = Result { + var result = [CLSID.Wrapper: [String]]() // Create an imaging factory. let factory = try IWICImagingFactory.create() @@ -67,7 +66,7 @@ extension AttachableImageFormat { continue } let extensions = _pathExtensions(for: info) - result[UInt128(clsid)] = extensions + result[CLSID.Wrapper(clsid)] = extensions } return result @@ -134,21 +133,7 @@ extension AttachableImageFormat { 0 == _wcsicmp(pathExtension, encoderExt) } } - }.map { CLSID($0.key) } - } - - /// Get the `CLSID` value of the WIC image encoder corresponding to the same - /// image format as the given path extension. - /// - /// - Parameters: - /// - pathExtension: The path extension for which a `CLSID` value is needed. - /// - /// - Returns: An instance of `CLSID` referring to a a WIC image encoder, or - /// `nil` if one could not be determined. - private static func _computeEncoderCLSID(forPathExtension pathExtension: String) -> CLSID? { - pathExtension.withCString(encodedAs: UTF16.self) { pathExtension in - _computeEncoderCLSID(forPathExtension: pathExtension) - } + }.map { $0.key.rawValue } } /// Get the `CLSID` value of the WIC image encoder corresponding to the same @@ -160,7 +145,7 @@ extension AttachableImageFormat { /// /// - Returns: An instance of `CLSID` referring to a a WIC image encoder, or /// `nil` if one could not be determined. - private static func _computeEncoderCLSID(forPreferredName preferredName: String) -> CLSID? { + static func computeEncoderCLSID(forPreferredName preferredName: String) -> CLSID? { preferredName.withCString(encodedAs: UTF16.self) { (preferredName) -> CLSID? in // Get the path extension on the preferred name, if any. var dot: PCWSTR? @@ -171,37 +156,6 @@ extension AttachableImageFormat { } } - /// Get the `CLSID` value of the WIC image encoder to use when encoding an - /// image. - /// - /// - Parameters: - /// - imageFormat: The image format to use, or `nil` if the developer did - /// not specify one. - /// - preferredName: The preferred name of the image for which a type is - /// needed. - /// - /// - Returns: An instance of `CLSID` referring to a a WIC image encoder. If - /// none could be derived from `imageFormat` or `preferredName`, the PNG - /// encoder is used. - /// - /// This function is not part of the public interface of the testing library. - static func computeEncoderCLSID(for imageFormat: Self?, withPreferredName preferredName: String) -> CLSID { - if let clsid = imageFormat?.encoderCLSID { - return clsid - } - - // The developer didn't specify a CLSID, or we couldn't figure one out from - // context, so try to derive one from the preferred name's path extension. - if let inferredCLSID = _computeEncoderCLSID(forPreferredName: preferredName) { - return inferredCLSID - } - - // We couldn't derive a concrete type from the path extension, so default - // to PNG. Unlike Apple platforms, there's no abstract "image" type on - // Windows so we don't need to make any more decisions. - return CLSID_WICPngEncoder - } - /// Append the path extension preferred by WIC for the image format /// corresponding to the given `CLSID` value or the given filename. /// @@ -215,13 +169,13 @@ extension AttachableImageFormat { static func appendPathExtension(for clsid: CLSID, to preferredName: String) -> String { // If there's already a CLSID associated with the filename, and it matches // the one passed to us, no changes are needed. - if let existingCLSID = _computeEncoderCLSID(forPreferredName: preferredName), clsid == existingCLSID { + if let existingCLSID = computeEncoderCLSID(forPreferredName: preferredName), CLSID.Wrapper(clsid) == CLSID.Wrapper(existingCLSID) { return preferredName } // Find the preferred path extension for the encoder with the given CLSID. let encoderPathExtensionsByCLSID = (try? _encoderPathExtensionsByCLSID.get()) ?? [:] - if let ext = encoderPathExtensionsByCLSID[UInt128(clsid)]?.first { + if let ext = encoderPathExtensionsByCLSID[CLSID.Wrapper(clsid)]?.first { return "\(preferredName).\(ext)" } @@ -242,14 +196,7 @@ extension AttachableImageFormat { @_spi(_) #endif public var encoderCLSID: CLSID { - switch kind { - case .png: - CLSID_WICPngEncoder - case .jpeg: - CLSID_WICJpegEncoder - case let .systemValue(clsid): - clsid as! CLSID - } + kind.encoderCLSID } /// Construct an instance of this type with the `CLSID` value of a Windows @@ -277,9 +224,10 @@ extension AttachableImageFormat { @_spi(_) #endif public init(encoderCLSID: CLSID, encodingQuality: Float = 1.0) { - let kind: Kind = if encoderCLSID == CLSID_WICPngEncoder { + let encoderCLSID = CLSID.Wrapper(encoderCLSID) + let kind: Kind = if encoderCLSID == CLSID.Wrapper(CLSID_WICPngEncoder) { .png - } else if encoderCLSID == CLSID_WICJpegEncoder { + } else if encoderCLSID == CLSID.Wrapper(CLSID_WICJpegEncoder) { .jpeg } else { .systemValue(encoderCLSID) @@ -323,4 +271,60 @@ extension AttachableImageFormat { self.init(encoderCLSID: encoderCLSID, encodingQuality: encodingQuality) } } + +// MARK: - CustomStringConvertible, CustomDebugStringConvertible + +extension AttachableImageFormat.Kind: CustomStringConvertible, CustomDebugStringConvertible { + /// The `CLSID` value of the Windows Imaging Component (WIC) encoder class + /// that corresponds to this image format. + fileprivate var encoderCLSID: CLSID { + switch self { + case .png: + CLSID_WICPngEncoder + case .jpeg: + CLSID_WICJpegEncoder + case let .systemValue(clsid): + (clsid as! CLSID.Wrapper).rawValue + } + } + + /// Get a description of the given `CLSID` value. + /// + /// - Parameters: + /// - clsid: The `CLSID` value to describe. + /// + /// - Returns: A description of `clsid`. + private static func _description(of clsid: CLSID) -> String { + var clsid = clsid + var buffer: RPC_WSTR? + if RPC_S_OK == UuidToStringW(&clsid, &buffer) { + defer { + RpcStringFreeW(&buffer) + } + if let result = String.decodeCString(buffer, as: UTF16.self)?.result { + return result + } + } + return String(describing: clsid) + } + + package var description: String { + let clsid = encoderCLSID + let encoderPathExtensionsByCLSID = (try? AttachableImageFormat._encoderPathExtensionsByCLSID.get()) ?? [:] + if let ext = encoderPathExtensionsByCLSID[CLSID.Wrapper(clsid)]?.first { + return "\(ext.uppercased()) format" + } + return Self._description(of: clsid) + } + + package var debugDescription: String { + let clsid = encoderCLSID + let clsidDescription = Self._description(of: clsid) + let encoderPathExtensionsByCLSID = (try? AttachableImageFormat._encoderPathExtensionsByCLSID.get()) ?? [:] + if let ext = encoderPathExtensionsByCLSID[CLSID.Wrapper(clsid)]?.first { + return "\(ext.uppercased()) format (\(clsidDescription))" + } + return clsidDescription + } +} #endif diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/HBITMAP+AttachableAsIWICBitmapSource.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/HBITMAP+AttachableAsIWICBitmapSource.swift index baccf2663..0982234cc 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/HBITMAP+AttachableAsIWICBitmapSource.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/HBITMAP+AttachableAsIWICBitmapSource.swift @@ -9,7 +9,6 @@ // #if os(Windows) -private import Testing public import WinSDK extension HBITMAP__: _AttachableByAddressAsIWICBitmapSource { diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/HICON+AttachableAsIWICBitmapSource.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/HICON+AttachableAsIWICBitmapSource.swift index 8884d713a..4e6addfa3 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/HICON+AttachableAsIWICBitmapSource.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/HICON+AttachableAsIWICBitmapSource.swift @@ -9,7 +9,6 @@ // #if os(Windows) -private import Testing public import WinSDK extension HICON__: _AttachableByAddressAsIWICBitmapSource { diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/IWICBitmapSource+AttachableAsIWICBitmapSource.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/IWICBitmapSource+AttachableAsIWICBitmapSource.swift index d900baa46..733b72bb7 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/IWICBitmapSource+AttachableAsIWICBitmapSource.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/IWICBitmapSource+AttachableAsIWICBitmapSource.swift @@ -9,7 +9,6 @@ // #if os(Windows) -private import Testing public import WinSDK /// - Important: The casts in this file to `IUnknown` are safe insofar as we use diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/UnsafeMutablePointer+AttachableAsIWICBitmapSource.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/UnsafeMutablePointer+AttachableAsIWICBitmapSource.swift index 448c2151e..a7487c1cd 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/UnsafeMutablePointer+AttachableAsIWICBitmapSource.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/UnsafeMutablePointer+AttachableAsIWICBitmapSource.swift @@ -9,25 +9,13 @@ // #if os(Windows) -private import Testing -public import WinSDK +package import WinSDK /// @Metadata { /// @Available(Swift, introduced: 6.3) /// } -extension UnsafeMutablePointer: _AttachableAsImage, AttachableAsIWICBitmapSource where Pointee: _AttachableByAddressAsIWICBitmapSource { - /// @Metadata { - /// @Available(Swift, introduced: 6.3) - /// } - public func copyAttachableIWICBitmapSource() throws -> UnsafeMutablePointer { - let factory = try IWICImagingFactory.create() - defer { - _ = factory.pointee.lpVtbl.pointee.Release(factory) - } - return try _copyAttachableIWICBitmapSource(using: factory) - } - - public func _copyAttachableIWICBitmapSource(using factory: UnsafeMutablePointer) throws -> UnsafeMutablePointer { +extension UnsafeMutablePointer: AttachableAsImage, AttachableAsIWICBitmapSource where Pointee: _AttachableByAddressAsIWICBitmapSource { + package func copyAttachableIWICBitmapSource(using factory: UnsafeMutablePointer) throws -> UnsafeMutablePointer { try Pointee._copyAttachableIWICBitmapSource(from: self, using: factory) } diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/_AttachableImageWrapper+AttachableWrapper.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/_AttachableImageWrapper+AttachableWrapper.swift index 33026fc9a..3437f2deb 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/_AttachableImageWrapper+AttachableWrapper.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/_AttachableImageWrapper+AttachableWrapper.swift @@ -9,115 +9,43 @@ // #if os(Windows) -public import Testing private import WinSDK -extension _AttachableImageWrapper: Attachable, AttachableWrapper where Image: AttachableAsIWICBitmapSource { - public func withUnsafeBytes(for attachment: borrowing Attachment<_AttachableImageWrapper>, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { - // Create an in-memory stream to write the image data to. Note that Windows - // documentation recommends SHCreateMemStream() instead, but that function - // does not provide a mechanism to access the underlying memory directly. - var stream: UnsafeMutablePointer? - let rCreateStream = CreateStreamOnHGlobal(nil, true, &stream) - guard S_OK == rCreateStream, let stream else { - throw ImageAttachmentError.comObjectCreationFailed(IStream.self, rCreateStream) - } - defer { - _ = stream.pointee.lpVtbl.pointee.Release(stream) - } - - // Get an imaging factory to create the WIC bitmap and encoder. - let factory = try IWICImagingFactory.create() - defer { - _ = factory.pointee.lpVtbl.pointee.Release(factory) - } - - // Create the bitmap and downcast it to an IWICBitmapSource for later use. - let bitmap = try wrappedValue._copyAttachableIWICBitmapSource(using: factory) - defer { - _ = bitmap.pointee.lpVtbl.pointee.Release(bitmap) - } - - // Create the encoder. - let encoder = try withUnsafePointer(to: IID_IWICBitmapEncoder) { [preferredName = attachment.preferredName] IID_IWICBitmapEncoder in - var encoderCLSID = AttachableImageFormat.computeEncoderCLSID(for: imageFormat, withPreferredName: preferredName) - var encoder: UnsafeMutableRawPointer? - let rCreate = CoCreateInstance( - &encoderCLSID, - nil, - DWORD(CLSCTX_INPROC_SERVER.rawValue), - IID_IWICBitmapEncoder, - &encoder - ) - guard rCreate == S_OK, let encoder = encoder?.assumingMemoryBound(to: IWICBitmapEncoder.self) else { - throw ImageAttachmentError.comObjectCreationFailed(IWICBitmapEncoder.self, rCreate) - } - return encoder - } - defer { - _ = encoder.pointee.lpVtbl.pointee.Release(encoder) - } - _ = encoder.pointee.lpVtbl.pointee.Initialize(encoder, stream, WICBitmapEncoderNoCache) - - // Create the frame into which the bitmap will be composited. - var frame: UnsafeMutablePointer? - var propertyBag: UnsafeMutablePointer? - let rCreateFrame = encoder.pointee.lpVtbl.pointee.CreateNewFrame(encoder, &frame, &propertyBag) - guard rCreateFrame == S_OK, let frame, let propertyBag else { - throw ImageAttachmentError.comObjectCreationFailed(IWICBitmapFrameEncode.self, rCreateFrame) - } - defer { - _ = frame.pointee.lpVtbl.pointee.Release(frame) - _ = propertyBag.pointee.lpVtbl.pointee.Release(propertyBag) - } - - // Set properties. The only property we currently set is image quality. - if let encodingQuality = imageFormat?.encodingQuality { - try propertyBag.write(encodingQuality, named: "ImageQuality") - } - _ = frame.pointee.lpVtbl.pointee.Initialize(frame, propertyBag) - - // Write the image! - let rWrite = frame.pointee.lpVtbl.pointee.WriteSource(frame, bitmap, nil) - guard rWrite == S_OK else { - throw ImageAttachmentError.imageWritingFailed(rWrite) - } - - // Commit changes through the various layers. - var rCommit = frame.pointee.lpVtbl.pointee.Commit(frame) - guard rCommit == S_OK else { - throw ImageAttachmentError.imageWritingFailed(rCommit) - } - rCommit = encoder.pointee.lpVtbl.pointee.Commit(encoder) - guard rCommit == S_OK else { - throw ImageAttachmentError.imageWritingFailed(rCommit) - } - rCommit = stream.pointee.lpVtbl.pointee.Commit(stream, DWORD(STGC_DEFAULT.rawValue)) - guard rCommit == S_OK else { - throw ImageAttachmentError.imageWritingFailed(rCommit) - } +extension _AttachableImageWrapper: Attachable, AttachableWrapper where Image: AttachableAsImage { + /// Get the image format to use when encoding an image. + /// + /// - Parameters: + /// - preferredName: The preferred name of the image for which a type is + /// needed. + /// + /// - Returns: An instance of ``AttachableImageFormat`` referring to a + /// concrete image type. + /// + /// This function is not part of the public interface of the testing library. + private func _imageFormat(forPreferredName preferredName: String) -> AttachableImageFormat { + if let imageFormat { + // The developer explicitly specified a type. + return imageFormat + } + + if let clsid = AttachableImageFormat.computeEncoderCLSID(forPreferredName: preferredName) { + return AttachableImageFormat(encoderCLSID: clsid) + } + + // We couldn't derive a concrete type from the path extension, so pick + // between PNG and JPEG based on the encoding quality. + let encodingQuality = imageFormat?.encodingQuality ?? 1.0 + return encodingQuality < 1.0 ? .jpeg : .png + } - // Extract the serialized image and pass it back to the caller. We hold the - // HGLOBAL locked while calling `body`, but nothing else should have a - // reference to it. - var global: HGLOBAL? - let rGetGlobal = GetHGlobalFromStream(stream, &global) - guard S_OK == rGetGlobal else { - throw ImageAttachmentError.globalFromStreamFailed(rGetGlobal) - } - guard let baseAddress = GlobalLock(global) else { - throw Win32Error(rawValue: GetLastError()) - } - defer { - GlobalUnlock(global) - } - let byteCount = GlobalSize(global) - return try body(UnsafeRawBufferPointer(start: baseAddress, count: Int(byteCount))) + public func withUnsafeBytes(for attachment: borrowing Attachment<_AttachableImageWrapper>, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { + let imageFormat = _imageFormat(forPreferredName: attachment.preferredName) + return try wrappedValue.withUnsafeBytes(as: imageFormat, body) } public borrowing func preferredName(for attachment: borrowing Attachment<_AttachableImageWrapper>, basedOn suggestedName: String) -> String { - let clsid = AttachableImageFormat.computeEncoderCLSID(for: imageFormat, withPreferredName: suggestedName) - return AttachableImageFormat.appendPathExtension(for: clsid, to: suggestedName) + let imageFormat = _imageFormat(forPreferredName: suggestedName) + return AttachableImageFormat.appendPathExtension(for: imageFormat.encoderCLSID, to: suggestedName) } } #endif diff --git a/Sources/Overlays/_Testing_WinSDK/Support/Additions/GUIDAdditions.swift b/Sources/Overlays/_Testing_WinSDK/Support/Additions/GUIDAdditions.swift index c58eca577..06985ed4c 100644 --- a/Sources/Overlays/_Testing_WinSDK/Support/Additions/GUIDAdditions.swift +++ b/Sources/Overlays/_Testing_WinSDK/Support/Additions/GUIDAdditions.swift @@ -11,27 +11,45 @@ #if os(Windows) internal import WinSDK -extension UInt128 { - init(_ guid: GUID) { - self = withUnsafeBytes(of: guid) { buffer in - buffer.baseAddress!.loadUnaligned(as: Self.self) - } +extension GUID { + /// A type that wraps `GUID` instances and conforms to various Swift + /// protocols. + /// + /// - Bug: This type will become obsolete once we can use the `Equatable` and + /// `Hashable` conformances added to the WinSDK module in Swift 6.3. +#if compiler(>=6.3.1) && DEBUG + @available(*, deprecated, message: "GUID.Wrapper is no longer needed and can be removed.") +#endif + struct Wrapper: Sendable, RawRepresentable { + var rawValue: GUID } } -extension GUID { - init(_ uint128Value: UInt128) { - self = withUnsafeBytes(of: uint128Value) { buffer in - buffer.baseAddress!.loadUnaligned(as: Self.self) +// MARK: - + +extension GUID.Wrapper: Equatable, Hashable, CustomStringConvertible { + init(_ rawValue: GUID) { + self.init(rawValue: rawValue) + } + +#if compiler(<6.3.1) + private var _uint128Value: UInt128 { + withUnsafeBytes(of: rawValue) { buffer in + buffer.baseAddress!.loadUnaligned(as: UInt128.self) } } static func ==(lhs: Self, rhs: Self) -> Bool { - withUnsafeBytes(of: lhs) { lhs in - withUnsafeBytes(of: rhs) { rhs in - lhs.elementsEqual(rhs) - } - } + lhs._uint128Value == rhs._uint128Value + } + + func hash(into hasher: inout Hasher) { + hasher.combine(_uint128Value) } + + var description: String { + String(describing: rawValue) + } +#endif } #endif diff --git a/Sources/Testing/Attachments/Images/AttachableAsImage.swift b/Sources/Testing/Attachments/Images/AttachableAsImage.swift new file mode 100644 index 000000000..dbb64c06c --- /dev/null +++ b/Sources/Testing/Attachments/Images/AttachableAsImage.swift @@ -0,0 +1,131 @@ +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024–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 SWT_TARGET_OS_APPLE || os(Windows) +// These platforms support image attachments. +#elseif !SWT_NO_IMAGE_ATTACHMENTS +#error("Platform-specific misconfiguration: support for image attachments requires a platform-specific implementation") +#endif + +/// ## Why can't images directly conform to Attachable? +/// +/// Three reasons: +/// +/// 1. Several image classes are not marked `Sendable`, which means that as far +/// as Swift is concerned, they cannot be safely passed to Swift Testing's +/// event handler (primarily because `Event` is `Sendable`.) So we would have +/// to eagerly serialize them, which is unnecessarily expensive if we know +/// they're actually concurrency-safe. +/// 2. We would have no place to store metadata such as the encoding quality +/// (although in the future we may introduce a "metadata" associated type to +/// `Attachable` that could store that info.) +/// 3. `Attachable` has a requirement with `Self` in non-parameter, non-return +/// position. As far as Swift is concerned, a non-final class cannot satisfy +/// such a requirement, and all image types we care about are non-final +/// classes. Thus, the compiler will steadfastly refuse to allow non-final +/// classes to conform to the `Attachable` protocol. We could get around this +/// by changing the signature of `withUnsafeBytes()` so that the +/// generic parameter to `Attachment` is not `Self`, but that would defeat +/// much of the purpose of making `Attachment` generic in the first place. +/// (And no, the language does not let us write `where T: Self` anywhere +/// useful.) + +/// A protocol describing images that can be converted to instances of +/// [`Attachment`](https://developer.apple.com/documentation/testing/attachment). +/// +/// Instances of types conforming to this protocol do not themselves conform to +/// [`Attachable`](https://developer.apple.com/documentation/testing/attachable). +/// Instead, the testing library provides additional initializers on [`Attachment`](https://developer.apple.com/documentation/testing/attachment) +/// that take instances of such types and handle converting them to image data when needed. +/// +/// You can attach instances of the following system-provided image types to a +/// test: +/// +/// | Platform | Supported Types | +/// |-|-| +/// | macOS | [`CGImage`](https://developer.apple.com/documentation/coregraphics/cgimage), [`CIImage`](https://developer.apple.com/documentation/coreimage/ciimage), [`NSImage`](https://developer.apple.com/documentation/appkit/nsimage) | +/// | iOS, watchOS, tvOS, and visionOS | [`CGImage`](https://developer.apple.com/documentation/coregraphics/cgimage), [`CIImage`](https://developer.apple.com/documentation/coreimage/ciimage), [`UIImage`](https://developer.apple.com/documentation/uikit/uiimage) | +/// @Comment { +/// | Windows | [`HBITMAP`](https://learn.microsoft.com/en-us/windows/win32/gdi/bitmaps), [`HICON`](https://learn.microsoft.com/en-us/windows/win32/menurc/icons), [`IWICBitmapSource`](https://learn.microsoft.com/en-us/windows/win32/api/wincodec/nn-wincodec-iwicbitmapsource) (including its subclasses declared by Windows Imaging Component) | +/// } +/// +/// You do not generally need to add your own conformances to this protocol. If +/// you have an image in another format that needs to be attached to a test, +/// first convert it to an instance of one of the types above. +/// +/// @Metadata { +/// @Available(Swift, introduced: 6.3) +/// } +#if SWT_NO_IMAGE_ATTACHMENTS +@available(*, unavailable, message: "Image attachments are not available on this platform.") +#endif +@available(_uttypesAPI, *) +public protocol AttachableAsImage { + /// Encode a representation of this image in a given image format. + /// + /// - Parameters: + /// - imageFormat: The image format to use when encoding this image. + /// - body: A function to call. A temporary buffer containing a data + /// representation of this instance is passed to it. + /// + /// - Returns: Whatever is returned by `body`. + /// + /// - Throws: Whatever is thrown by `body`, or any error that prevented the + /// creation of the buffer. + /// + /// The testing library uses this function when saving an image as an + /// attachment. The implementation should use `imageFormat` to determine what + /// encoder to use. + borrowing func withUnsafeBytes(as imageFormat: AttachableImageFormat, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R + + /// Make a copy of this instance to pass to an attachment. + /// + /// - Returns: A copy of `self`, or `self` if no copy is needed. + /// + /// The testing library uses this function to take ownership of image + /// resources that test authors pass to it. If possible, make a copy of or add + /// a reference to `self`. If this type does not support making copies, return + /// `self` verbatim. + /// + /// The default implementation of this function when `Self` conforms to + /// `Sendable` simply returns `self`. + /// + /// This function is not part of the public interface of the testing library. + /// It may be removed in a future update. + func _copyAttachableValue() -> Self + + /// Manually deinitialize any resources associated with this image. + /// + /// The implementation of this function cleans up any resources (such as + /// handles or COM objects) associated with this image. The testing library + /// automatically invokes this function as needed. + /// + /// This function is not responsible for releasing the image returned from + /// `_copyAttachableIWICBitmapSource(using:)`. + /// + /// The default implementation of this function when `Self` conforms to + /// `Sendable` does nothing. + /// + /// This function is not part of the public interface of the testing library. + /// It may be removed in a future update. + func _deinitializeAttachableValue() +} + +#if SWT_NO_IMAGE_ATTACHMENTS +@available(*, unavailable, message: "Image attachments are not available on this platform.") +#endif +@available(_uttypesAPI, *) +extension AttachableAsImage { + public func _copyAttachableValue() -> Self { + self + } + + public func _deinitializeAttachableValue() {} +} diff --git a/Sources/Testing/Attachments/Images/AttachableImageFormat.swift b/Sources/Testing/Attachments/Images/AttachableImageFormat.swift index 350ef849e..d0fb16507 100644 --- a/Sources/Testing/Attachments/Images/AttachableImageFormat.swift +++ b/Sources/Testing/Attachments/Images/AttachableImageFormat.swift @@ -12,7 +12,7 @@ /// when attaching an image to a test. /// /// When you attach an image to a test, you can pass an instance of this type to -/// `Attachment.record(_:named:as:sourceLocation:)` so that the testing +/// ``Attachment/record(_:named:as:sourceLocation:)`` so that the testing /// library knows the image format you'd like to use. If you don't pass an /// instance of this type, the testing library infers which format to use based /// on the attachment's preferred name. @@ -53,7 +53,7 @@ public struct AttachableImageFormat: Sendable { /// /// On Apple platforms, `value` should be an instance of `UTType`. On /// Windows, it should be an instance of `CLSID`. - case systemValue(_ value: any Sendable) + case systemValue(_ value: any Sendable & Equatable & Hashable) } /// The kind of image format represented by this instance. @@ -69,7 +69,7 @@ public struct AttachableImageFormat: Sendable { /// @Metadata { /// @Available(Swift, introduced: 6.3) /// } - public internal(set) var encodingQuality: Float = 1.0 + public private(set) var encodingQuality: Float = 1.0 package init(kind: Kind, encodingQuality: Float) { self.kind = kind @@ -77,6 +77,69 @@ public struct AttachableImageFormat: Sendable { } } +// MARK: - Equatable, Hashable + +#if SWT_NO_IMAGE_ATTACHMENTS +@_unavailableInEmbedded +@available(*, unavailable, message: "Image attachments are not available on this platform.") +#endif +@available(_uttypesAPI, *) +extension AttachableImageFormat: Equatable, Hashable {} + +#if SWT_NO_IMAGE_ATTACHMENTS +@_unavailableInEmbedded +@available(*, unavailable, message: "Image attachments are not available on this platform.") +#endif +@available(_uttypesAPI, *) +extension AttachableImageFormat.Kind: Equatable, Hashable { + public static func ==(lhs: Self, rhs: Self) -> Bool { + switch (lhs, rhs) { + case (.png, .png), (.jpeg, .jpeg): + return true + case let (.systemValue(lhs), .systemValue(rhs)): + func open(_ lhs: T) -> Bool where T: Equatable { + lhs == (rhs as? T) + } + return open(lhs) + default: + return false + } + } + + public func hash(into hasher: inout Hasher) { + switch self { + case .png: + hasher.combine("png") + case .jpeg: + hasher.combine("jpeg") + case let .systemValue(systemValue): + hasher.combine(systemValue) + } + } +} + +// MARK: - CustomStringConvertible, CustomDebugStringConvertible + +#if SWT_NO_IMAGE_ATTACHMENTS +@_unavailableInEmbedded +@available(*, unavailable, message: "Image attachments are not available on this platform.") +#endif +@available(_uttypesAPI, *) +extension AttachableImageFormat: CustomStringConvertible, CustomDebugStringConvertible { + public var description: String { + let kindDescription = String(describing: kind) + if encodingQuality < 1.0 { + return "\(kindDescription) at \(Int(encodingQuality * 100.0))% quality" + } + return kindDescription + } + + public var debugDescription: String { + let kindDescription = String(reflecting: kind) + return "\(kindDescription) at quality \(encodingQuality)" + } +} + // MARK: - #if SWT_NO_IMAGE_ATTACHMENTS diff --git a/Sources/Testing/Attachments/Images/Attachment+_AttachableAsImage.swift b/Sources/Testing/Attachments/Images/Attachment+AttachableAsImage.swift similarity index 97% rename from Sources/Testing/Attachments/Images/Attachment+_AttachableAsImage.swift rename to Sources/Testing/Attachments/Images/Attachment+AttachableAsImage.swift index dde24f6d7..045b5938e 100644 --- a/Sources/Testing/Attachments/Images/Attachment+_AttachableAsImage.swift +++ b/Sources/Testing/Attachments/Images/Attachment+AttachableAsImage.swift @@ -100,14 +100,17 @@ extension Attachment { // MARK: - -@_spi(Experimental) // STOP: not part of ST-0014 #if SWT_NO_IMAGE_ATTACHMENTS @_unavailableInEmbedded @available(*, unavailable, message: "Image attachments are not available on this platform.") #endif @available(_uttypesAPI, *) -extension Attachment where AttachableValue: AttachableWrapper, AttachableValue.Wrapped: _AttachableAsImage { - /// The image format to use when encoding the represented image. +extension Attachment where AttachableValue: AttachableWrapper, AttachableValue.Wrapped: AttachableAsImage { + /// The image format to use when encoding the represented image, if specified. + /// + /// @Metadata { + /// @Available(Swift, introduced: 6.3) + /// } @_disfavoredOverload public var imageFormat: AttachableImageFormat? { // FIXME: no way to express `where AttachableValue == _AttachableImageWrapper` on a property (see rdar://47559973) (attachableValue as? _AttachableImageWrapper)?.imageFormat diff --git a/Sources/Testing/Attachments/Images/_AttachableAsImage.swift b/Sources/Testing/Attachments/Images/_AttachableAsImage.swift deleted file mode 100644 index ca8efacf2..000000000 --- a/Sources/Testing/Attachments/Images/_AttachableAsImage.swift +++ /dev/null @@ -1,66 +0,0 @@ -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2024–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 SWT_TARGET_OS_APPLE -// Image attachments on Apple platforms conform to AttachableAsCGImage. -#elseif os(Windows) -// Image attachments on Windows platforms conform to AttachableAsIWICBitmapSource. -#elseif !SWT_NO_IMAGE_ATTACHMENTS -#error("Platform-specific misconfiguration: support for image attachments requires a platform-specific implementation") -#endif - -/// A protocol describing images that can be converted to instances of -/// [`Attachment`](https://developer.apple.com/documentation/testing/attachment). -/// -/// This protocol acts as an abstract, platform-independent base protocol for -/// ``AttachableAsCGImage`` and ``AttachableAsIWICBitmapSource``. -/// -/// @Comment { -/// A future Swift Evolution proposal will promote this protocol to API so -/// that we don't need to underscore its name. -/// } -#if SWT_NO_IMAGE_ATTACHMENTS -@_unavailableInEmbedded -@available(*, unavailable, message: "Image attachments are not available on this platform.") -#endif -@available(_uttypesAPI, *) -public protocol _AttachableAsImage: SendableMetatype { - /// Make a copy of this instance to pass to an attachment. - /// - /// - Returns: A copy of `self`, or `self` if no copy is needed. - /// - /// The testing library uses this function to take ownership of image - /// resources that test authors pass to it. If possible, make a copy of or add - /// a reference to `self`. If this type does not support making copies, return - /// `self` verbatim. - /// - /// The default implementation of this function when `Self` conforms to - /// `Sendable` simply returns `self`. - /// - /// This function is not part of the public interface of the testing library. - /// It may be removed in a future update. - func _copyAttachableValue() -> Self - - /// Manually deinitialize any resources associated with this image. - /// - /// The implementation of this function cleans up any resources (such as - /// handles or COM objects) associated with this image. The testing library - /// automatically invokes this function as needed. - /// - /// This function is not responsible for releasing the image returned from - /// `_copyAttachableIWICBitmapSource(using:)`. - /// - /// The default implementation of this function when `Self` conforms to - /// `Sendable` does nothing. - /// - /// This function is not part of the public interface of the testing library. - /// It may be removed in a future update. - func _deinitializeAttachableValue() -} diff --git a/Sources/Testing/Attachments/Images/_AttachableImageWrapper.swift b/Sources/Testing/Attachments/Images/_AttachableImageWrapper.swift index dfde12587..7065c03f7 100644 --- a/Sources/Testing/Attachments/Images/_AttachableImageWrapper.swift +++ b/Sources/Testing/Attachments/Images/_AttachableImageWrapper.swift @@ -23,30 +23,19 @@ @available(*, unavailable, message: "Image attachments are not available on this platform.") #endif @available(_uttypesAPI, *) -public final class _AttachableImageWrapper: Sendable where Image: _AttachableAsImage { +public final class _AttachableImageWrapper: Sendable where Image: AttachableAsImage { /// The underlying image. - private nonisolated(unsafe) let _image: Image + public nonisolated(unsafe) let wrappedValue: Image /// The image format to use when encoding the represented image. package let imageFormat: AttachableImageFormat? init(image: Image, imageFormat: AttachableImageFormat?) { - self._image = image._copyAttachableValue() + self.wrappedValue = image._copyAttachableValue() self.imageFormat = imageFormat } deinit { - _image._deinitializeAttachableValue() - } -} - -#if SWT_NO_IMAGE_ATTACHMENTS -@_unavailableInEmbedded -@available(*, unavailable, message: "Image attachments are not available on this platform.") -#endif -@available(_uttypesAPI, *) -extension _AttachableImageWrapper { - public var wrappedValue: Image { - _image + wrappedValue._deinitializeAttachableValue() } } diff --git a/Sources/Testing/CMakeLists.txt b/Sources/Testing/CMakeLists.txt index bebf05eb9..b7836e3e1 100644 --- a/Sources/Testing/CMakeLists.txt +++ b/Sources/Testing/CMakeLists.txt @@ -21,10 +21,10 @@ add_library(Testing ABI/Encoded/ABI.EncodedIssue.swift ABI/Encoded/ABI.EncodedMessage.swift ABI/Encoded/ABI.EncodedTest.swift - Attachments/Images/_AttachableAsImage.swift + Attachments/Images/AttachableAsImage.swift Attachments/Images/_AttachableImageWrapper.swift Attachments/Images/AttachableImageFormat.swift - Attachments/Images/Attachment+_AttachableAsImage.swift + Attachments/Images/Attachment+AttachableAsImage.swift Attachments/Images/ImageAttachmentError.swift Attachments/Attachable.swift Attachments/AttachableWrapper.swift diff --git a/Sources/Testing/Testing.docc/Attachments.md b/Sources/Testing/Testing.docc/Attachments.md index aa754cc76..b84a50e13 100644 --- a/Sources/Testing/Testing.docc/Attachments.md +++ b/Sources/Testing/Testing.docc/Attachments.md @@ -34,10 +34,7 @@ protocol to create your own attachable types. ### Attaching images to tests - +- ``AttachableAsImage`` - ``AttachableImageFormat`` - ``Attachment/init(_:named:as:sourceLocation:)`` - ``Attachment/record(_:named:as:sourceLocation:)`` diff --git a/Tests/TestingTests/AttachmentTests.swift b/Tests/TestingTests/AttachmentTests.swift index 7695dd634..b457fc271 100644 --- a/Tests/TestingTests/AttachmentTests.swift +++ b/Tests/TestingTests/AttachmentTests.swift @@ -572,7 +572,7 @@ extension AttachmentTests { @Test(arguments: [Float(0.0).nextUp, 0.25, 0.5, 0.75, 1.0], [.png as UTType?, .jpeg, .gif, .image, nil]) func attachCGImage(quality: Float, type: UTType?) throws { let image = try Self.cgImage.get() - let format = type.map { AttachableImageFormat($0, encodingQuality: quality) } + let format = type.map { AttachableImageFormat(contentType: $0, encodingQuality: quality) } let attachment = Attachment(image, named: "diamond", as: format) #expect(attachment.attachableValue === image) try attachment.attachableValue.withUnsafeBytes(for: attachment) { buffer in @@ -584,7 +584,7 @@ extension AttachmentTests { } @available(_uttypesAPI, *) - @Test(arguments: [AttachableImageFormat.png, .jpeg, .jpeg(withEncodingQuality: 0.5), .init(.tiff)]) + @Test(arguments: [AttachableImageFormat.png, .jpeg, .jpeg(withEncodingQuality: 0.5), .init(contentType: .tiff)]) func attachCGImage(format: AttachableImageFormat) throws { let image = try Self.cgImage.get() let attachment = Attachment(image, named: "diamond", as: format) @@ -601,7 +601,7 @@ extension AttachmentTests { @available(_uttypesAPI, *) @Test func cannotAttachCGImageWithNonImageType() async { await #expect(processExitsWith: .failure) { - let format = AttachableImageFormat(.mp3) + let format = AttachableImageFormat(contentType: .mp3) let attachment = Attachment(try Self.cgImage.get(), named: "diamond", as: format) try attachment.attachableValue.withUnsafeBytes(for: attachment) { _ in } } @@ -840,6 +840,58 @@ extension AttachmentTests { @Test func imageFormatFromPathExtension() { let format = AttachableImageFormat(pathExtension: "png") #expect(format != nil) + #expect(format == .png) + + let badFormat = AttachableImageFormat(pathExtension: "no-such-image-format") + #expect(badFormat == nil) + } + + @available(_uttypesAPI, *) + @Test func imageFormatEquatableConformance() { + let format1 = AttachableImageFormat.png + let format2 = AttachableImageFormat.jpeg +#if canImport(CoreGraphics) && canImport(_Testing_CoreGraphics) + let format3 = AttachableImageFormat(contentType: .tiff) +#elseif canImport(WinSDK) && canImport(_Testing_WinSDK) + let format3 = AttachableImageFormat(encoderCLSID: CLSID_WICTiffEncoder) +#endif + #expect(format1 == format1) + #expect(format2 == format2) + #expect(format3 == format3) + #expect(format1 != format2) + #expect(format2 != format3) + #expect(format1 != format3) + + #expect(format1.hashValue == format1.hashValue) + #expect(format2.hashValue == format2.hashValue) + #expect(format3.hashValue == format3.hashValue) + #expect(format1.hashValue != format2.hashValue) + #expect(format2.hashValue != format3.hashValue) + #expect(format1.hashValue != format3.hashValue) + } + + @available(_uttypesAPI, *) + @Test func imageFormatStringification() { + let format: AttachableImageFormat = AttachableImageFormat.png +#if canImport(CoreGraphics) && canImport(_Testing_CoreGraphics) + #expect(String(describing: format) == UTType.png.localizedDescription!) + #expect(String(reflecting: format) == "\(UTType.png.localizedDescription!) (\(UTType.png.identifier)) at quality 1.0") +#elseif canImport(WinSDK) && canImport(_Testing_WinSDK) + #expect(String(describing: format) == "PNG format") + #expect(String(reflecting: format) == "PNG format (27949969-876a-41d7-9447-568f6a35a4dc) at quality 1.0") +#endif + } + + @available(_uttypesAPI, *) + @Test func imageFormatStringificationWithQuality() { + let format: AttachableImageFormat = AttachableImageFormat.jpeg(withEncodingQuality: 0.5) +#if canImport(CoreGraphics) && canImport(_Testing_CoreGraphics) + #expect(String(describing: format) == "\(UTType.jpeg.localizedDescription!) at 50% quality") + #expect(String(reflecting: format) == "\(UTType.jpeg.localizedDescription!) (\(UTType.jpeg.identifier)) at quality 0.5") +#elseif canImport(WinSDK) && canImport(_Testing_WinSDK) + #expect(String(describing: format) == "JPEG format at 50% quality") + #expect(String(reflecting: format) == "JPEG format (1a34f5c1-4a5a-46dc-b644-1f4567e7a676) at quality 0.5") +#endif } #endif } From 38a775805adea923fb12a74d19d8f943b8ae1f63 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Wed, 5 Nov 2025 17:55:57 -0500 Subject: [PATCH 194/216] Remove `@Comment` around a documentation line for Windows image attachments. This feature is now accepted/implemented/public, so remove the stray `@Comment` annotation. --- Sources/Testing/Attachments/Images/AttachableAsImage.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/Sources/Testing/Attachments/Images/AttachableAsImage.swift b/Sources/Testing/Attachments/Images/AttachableAsImage.swift index dbb64c06c..5091ec9ae 100644 --- a/Sources/Testing/Attachments/Images/AttachableAsImage.swift +++ b/Sources/Testing/Attachments/Images/AttachableAsImage.swift @@ -52,9 +52,7 @@ /// |-|-| /// | macOS | [`CGImage`](https://developer.apple.com/documentation/coregraphics/cgimage), [`CIImage`](https://developer.apple.com/documentation/coreimage/ciimage), [`NSImage`](https://developer.apple.com/documentation/appkit/nsimage) | /// | iOS, watchOS, tvOS, and visionOS | [`CGImage`](https://developer.apple.com/documentation/coregraphics/cgimage), [`CIImage`](https://developer.apple.com/documentation/coreimage/ciimage), [`UIImage`](https://developer.apple.com/documentation/uikit/uiimage) | -/// @Comment { /// | Windows | [`HBITMAP`](https://learn.microsoft.com/en-us/windows/win32/gdi/bitmaps), [`HICON`](https://learn.microsoft.com/en-us/windows/win32/menurc/icons), [`IWICBitmapSource`](https://learn.microsoft.com/en-us/windows/win32/api/wincodec/nn-wincodec-iwicbitmapsource) (including its subclasses declared by Windows Imaging Component) | -/// } /// /// You do not generally need to add your own conformances to this protocol. If /// you have an image in another format that needs to be attached to a test, From 55ff51e85d702bb3fa4836da0c9e52a06ca11a13 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Wed, 5 Nov 2025 22:37:09 -0500 Subject: [PATCH 195/216] Capture the current working directory early on OpenBSD. (#1388) This PR adds some OpenBSD-specific code to capture the current working directory as early as possible. We then use that directory when argv[0] appears to be a relative path so that the following incantation works correctly: ```sh ./.build/debug/myPackageTests.xctest --testing-library swift-testing ``` This logic is not necessary on other platforms because they all provide a way to get the path to the current executable. OpenBSD has no such API. Resolves #1348. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- .../Additions/CommandLineAdditions.swift | 12 +++++-- Sources/_TestingInternals/CMakeLists.txt | 1 + Sources/_TestingInternals/ExecutablePath.cpp | 35 +++++++++++++++++++ .../include/ExecutablePath.h | 31 ++++++++++++++++ Tests/TestingTests/ExitTestTests.swift | 8 +++++ 5 files changed, 84 insertions(+), 3 deletions(-) create mode 100644 Sources/_TestingInternals/ExecutablePath.cpp create mode 100644 Sources/_TestingInternals/include/ExecutablePath.h diff --git a/Sources/Testing/Support/Additions/CommandLineAdditions.swift b/Sources/Testing/Support/Additions/CommandLineAdditions.swift index 57d9851a8..63decd1ad 100644 --- a/Sources/Testing/Support/Additions/CommandLineAdditions.swift +++ b/Sources/Testing/Support/Additions/CommandLineAdditions.swift @@ -57,11 +57,17 @@ extension CommandLine { } #elseif os(OpenBSD) // OpenBSD does not have API to get a path to the running executable. Use - // arguments[0]. We do a basic sniff test for a path-like string, but - // otherwise return argv[0] verbatim. - guard let argv0 = arguments.first, argv0.contains("/") else { + // arguments[0]. We do a basic sniff test for a path-like string, and + // prepend the early CWD if it looks like a relative path, but otherwise + // return argv[0] verbatim. + guard var argv0 = arguments.first, argv0.contains("/") else { throw CError(rawValue: ENOEXEC) } + if argv0.first != "/", + let earlyCWD = swt_getEarlyCWD().flatMap(String.init(validatingCString:)), + !earlyCWD.isEmpty { + argv0 = "\(earlyCWD)/\(argv0)" + } return argv0 #elseif os(Windows) var result: String? diff --git a/Sources/_TestingInternals/CMakeLists.txt b/Sources/_TestingInternals/CMakeLists.txt index a951c7d4b..b2bc5b6b1 100644 --- a/Sources/_TestingInternals/CMakeLists.txt +++ b/Sources/_TestingInternals/CMakeLists.txt @@ -12,6 +12,7 @@ include(GitCommit) include(TargetTriple) add_library(_TestingInternals STATIC Discovery.cpp + ExecutablePath.cpp Versions.cpp WillThrow.cpp) target_include_directories(_TestingInternals PUBLIC diff --git a/Sources/_TestingInternals/ExecutablePath.cpp b/Sources/_TestingInternals/ExecutablePath.cpp new file mode 100644 index 000000000..0d23653f6 --- /dev/null +++ b/Sources/_TestingInternals/ExecutablePath.cpp @@ -0,0 +1,35 @@ +// +// 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 +// + +#include "ExecutablePath.h" + +#include + +#if defined(__OpenBSD__) +static std::atomic earlyCWD { nullptr }; + +/// At process start (before `main()` is called), capture the current working +/// directory. +/// +/// This function is necessary on OpenBSD so that we can (as correctly as +/// possible) resolve the executable path when the first argument is a relative +/// path (which can occur when manually invoking the test executable.) +__attribute__((__constructor__(101), __used__)) +static void swt_captureEarlyCWD(void) { + static char buffer[PATH_MAX * 2]; + if (getcwd(buffer, sizeof(buffer))) { + earlyCWD.store(buffer); + } +} + +const char *swt_getEarlyCWD(void) { + return earlyCWD.load(); +} +#endif diff --git a/Sources/_TestingInternals/include/ExecutablePath.h b/Sources/_TestingInternals/include/ExecutablePath.h new file mode 100644 index 000000000..22f3acc62 --- /dev/null +++ b/Sources/_TestingInternals/include/ExecutablePath.h @@ -0,0 +1,31 @@ +// +// 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_EXECUTABLE_PATH_H) +#define SWT_EXECUTABLE_PATH_H + +#include "Defines.h" +#include "Includes.h" + +SWT_ASSUME_NONNULL_BEGIN + +#if defined(__OpenBSD__) +/// Get the current working directory as it was set shortly after the process +/// started and before `main()` has been called. +/// +/// This function is necessary on OpenBSD so that we can (as correctly as +/// possible) resolve the executable path when the first argument is a relative +/// path (which can occur when manually invoking the test executable.) +SWT_EXTERN const char *_Nullable swt_getEarlyCWD(void); +#endif + +SWT_ASSUME_NONNULL_END + +#endif diff --git a/Tests/TestingTests/ExitTestTests.swift b/Tests/TestingTests/ExitTestTests.swift index 5be229266..6edabc305 100644 --- a/Tests/TestingTests/ExitTestTests.swift +++ b/Tests/TestingTests/ExitTestTests.swift @@ -625,6 +625,14 @@ private import _TestingInternals } } #endif + +#if os(OpenBSD) + @Test("Changing the CWD doesn't break exit tests") + func changeCWD() async throws { + try #require(0 == chdir("/")) + await #expect(processExitsWith: .success) {} + } +#endif } // MARK: - Fixtures From 56a6b790cdaac476d8fee35f373e787f338797dd Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Wed, 5 Nov 2025 23:44:21 -0500 Subject: [PATCH 196/216] Mark `earlyCWD` (OpenBSD-only) `constinit` and fix some comments/style. (#1396) See title. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- Sources/_TestingInternals/ExecutablePath.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Sources/_TestingInternals/ExecutablePath.cpp b/Sources/_TestingInternals/ExecutablePath.cpp index 0d23653f6..7213cfa75 100644 --- a/Sources/_TestingInternals/ExecutablePath.cpp +++ b/Sources/_TestingInternals/ExecutablePath.cpp @@ -1,7 +1,7 @@ // // This source file is part of the Swift.org open source project // -// Copyright (c) 2024 Apple Inc. and the Swift project authors +// 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 @@ -13,7 +13,8 @@ #include #if defined(__OpenBSD__) -static std::atomic earlyCWD { nullptr }; +/// Storage for ``swt_getEarlyCWD()``. +static constinit std::atomic earlyCWD { nullptr }; /// At process start (before `main()` is called), capture the current working /// directory. @@ -22,7 +23,7 @@ static std::atomic earlyCWD { nullptr }; /// possible) resolve the executable path when the first argument is a relative /// path (which can occur when manually invoking the test executable.) __attribute__((__constructor__(101), __used__)) -static void swt_captureEarlyCWD(void) { +static void captureEarlyCWD(void) { static char buffer[PATH_MAX * 2]; if (getcwd(buffer, sizeof(buffer))) { earlyCWD.store(buffer); From d341a659b29adc9c77f23e68c31d3e4386b0289e Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Thu, 6 Nov 2025 11:13:16 -0500 Subject: [PATCH 197/216] Avoid a potential race condition and use-after-free when calling `Test.cancel()`. (#1395) If a test creates an unstructured, non-detached task that continues running after the test has finished and eventually calls `Test.cancel()`, then it may be able to see a reference to the test's (or test case's) task after it has been destroyed by the Swift runtime. This PR ensures that the infrastructure under `Test.cancel()` clears its reference to the test's task before returning. This then minimizes the risk of observing the task after it has been destroyed. Further work at the Swift runtime level may be required to completely eliminate this race condition, but this change makes it sufficiently narrow that any example I can come up with is contrived. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- Sources/Testing/Test+Cancellation.swift | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/Sources/Testing/Test+Cancellation.swift b/Sources/Testing/Test+Cancellation.swift index 5a4b425c7..1ded9359f 100644 --- a/Sources/Testing/Test+Cancellation.swift +++ b/Sources/Testing/Test+Cancellation.swift @@ -87,10 +87,19 @@ extension TestCancellable { /// the current task, test, or test case is cancelled, it records a /// corresponding cancellation event. func withCancellationHandling(_ body: () async throws -> R) async rethrows -> R { + let taskReference = _TaskReference() var currentTaskReferences = _currentTaskReferences - currentTaskReferences[ObjectIdentifier(Self.self)] = _TaskReference() + currentTaskReferences[ObjectIdentifier(Self.self)] = taskReference return try await $_currentTaskReferences.withValue(currentTaskReferences) { - try await withTaskCancellationHandler { + // Before returning, explicitly clear the stored task. This minimizes + // the potential race condition that can occur if test code creates an + // unstructured task and calls `Test.cancel()` in it after the test body + // has finished. + defer { + _ = taskReference.takeUnsafeCurrentTask() + } + + return try await withTaskCancellationHandler { try await body() } onCancel: { // The current task was cancelled, so cancel the test case or test From db33906f93415f7c181628602c36f640ec96b5dc Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Thu, 6 Nov 2025 11:14:01 -0500 Subject: [PATCH 198/216] Remove VERSION.txt workaround for OpenBSD 7.7. (#1397) This PR removes a workaround for the lack of `#embed` support in OpenBSD 7.7's version of clang. OpenBSD 7.8 has a newer clang that has full `#embed` support, so we no longer need to special-case OpenBSD here. Swift Testing will continue to compile for OpenBSD 7.7, but will emit a compile-time warning of the form: > SWT_TESTING_LIBRARY_VERSION not defined and could not read from VERSION.txt at > compile time: testing library version is unavailable ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- Sources/_TestingInternals/Versions.cpp | 3 --- 1 file changed, 3 deletions(-) diff --git a/Sources/_TestingInternals/Versions.cpp b/Sources/_TestingInternals/Versions.cpp index 8ca708b26..66fc3c985 100644 --- a/Sources/_TestingInternals/Versions.cpp +++ b/Sources/_TestingInternals/Versions.cpp @@ -44,9 +44,6 @@ const char *swt_getTestingLibraryVersion(void) { #warning SWT_TESTING_LIBRARY_VERSION not defined and VERSION.txt not found: testing library version is unavailable return nullptr; #endif -#elif defined(__OpenBSD__) - // OpenBSD's version of clang doesn't support __has_embed or #embed. - return nullptr; #else #warning SWT_TESTING_LIBRARY_VERSION not defined and could not read from VERSION.txt at compile time: testing library version is unavailable return nullptr; From d3b0af597a0e2ed1fbcd44d2dbce79658b8a88e6 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Thu, 6 Nov 2025 11:42:45 -0500 Subject: [PATCH 199/216] Remove non-functional `swt_getWASIVersion()`. (#1398) A while back I implemented `swt_getWASIVersion()` to speculatively return the value of `WASI_SDK_VERSION` as defined by WASI, but that never landed and the [GitHub issue](https://github.com/WebAssembly/wasi-libc/issues/490) for it was closed. Remove the inoperative helper function here. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- Sources/Testing/Support/Versions.swift | 5 ++--- Sources/_TestingInternals/include/Versions.h | 18 ------------------ 2 files changed, 2 insertions(+), 21 deletions(-) diff --git a/Sources/Testing/Support/Versions.swift b/Sources/Testing/Support/Versions.swift index b671b302b..85d55429d 100644 --- a/Sources/Testing/Support/Versions.swift +++ b/Sources/Testing/Support/Versions.swift @@ -89,9 +89,8 @@ let operatingSystemVersion: String = { } } #elseif os(WASI) - if let version = swt_getWASIVersion().flatMap(String.init(validatingCString:)) { - return version - } + // WASI does not have an API to get the current WASI or Wasm version. + // wasi-libc does have uname(3), but it's stubbed out. #else #warning("Platform-specific implementation missing: OS version unavailable") #endif diff --git a/Sources/_TestingInternals/include/Versions.h b/Sources/_TestingInternals/include/Versions.h index b188a4d66..65fc4170e 100644 --- a/Sources/_TestingInternals/include/Versions.h +++ b/Sources/_TestingInternals/include/Versions.h @@ -53,24 +53,6 @@ SWT_EXTERN void swt_getTestingLibraryCommit(const char *_Nullable *_Nonnull outH /// testing library, or `nullptr` if that information is not available. SWT_EXTERN const char *_Nullable swt_getTargetTriple(void); -#if defined(__wasi__) -/// Get the version of the C standard library and runtime used by WASI, if -/// available. -/// -/// This function is provided because `WASI_SDK_VERSION` may or may not be -/// defined and may or may not be a complex macro. -/// -/// For more information about the `WASI_SDK_VERSION` macro, see -/// [wasi-libc-#490](https://github.com/WebAssembly/wasi-libc/issues/490). -static const char *_Nullable swt_getWASIVersion(void) { -#if defined(WASI_SDK_VERSION) - return WASI_SDK_VERSION; -#else - return 0; -#endif -} -#endif - SWT_ASSUME_NONNULL_END #endif From ef3e4af30defe4d6441b3a90b96df2aa51a7c86e Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Thu, 6 Nov 2025 16:41:15 -0500 Subject: [PATCH 200/216] Fix a regression that prevents saving attachments. (#1400) This PR ensures that the attachment-saving logic in `Runner` is applied consistently. I made the mistake of moving that logic to `configureEventHandlerRuntimeState()` because I'd forgotten that that function only has an effect when we're running our own tests. And, as a result, all our tests continued to pass because the code was in place for our tests (and for nobody else.) I've moved the code to a different location that we will always call when running a `Runner` instance, so the issue is resolved now. I don't have a great way to set up a unit test for this; tested manually at desk. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- Sources/Testing/Attachments/Attachment.swift | 22 ++++++++++++++++++- .../Testing/Running/Runner.RuntimeState.swift | 10 --------- Sources/Testing/Running/Runner.swift | 1 + 3 files changed, 22 insertions(+), 11 deletions(-) diff --git a/Sources/Testing/Attachments/Attachment.swift b/Sources/Testing/Attachments/Attachment.swift index b22911919..40d498433 100644 --- a/Sources/Testing/Attachments/Attachment.swift +++ b/Sources/Testing/Attachments/Attachment.swift @@ -501,6 +501,26 @@ extension Attachment where AttachableValue: ~Copyable { } } +extension Runner { + /// Modify this runner's configured event handler so that it handles "value + /// attached" events and saves attachments where necessary. + mutating func configureAttachmentHandling() { + configuration.eventHandler = { [oldEventHandler = configuration.eventHandler] event, context in +#if !SWT_NO_FILE_IO + var event = copy event + if case .valueAttached = event.kind { + guard let configuration = context.configuration, + configuration.handleValueAttachedEvent(&event, in: context) else { + // The attachment could not be handled, so suppress this event. + return + } + } + oldEventHandler(event, context) +#endif + } + } +} + extension Configuration { /// Handle the given "value attached" event. /// @@ -517,7 +537,7 @@ extension Configuration { /// not need to call it elsewhere. It automatically saves the attachment /// associated with `event` and modifies `event` to include the path where the /// attachment was saved. - func handleValueAttachedEvent(_ event: inout Event, in eventContext: borrowing Event.Context) -> Bool { + fileprivate func handleValueAttachedEvent(_ event: inout Event, in eventContext: borrowing Event.Context) -> Bool { guard let attachmentsPath else { // If there is no path to which attachments should be written, there's // nothing to do here. The event handler may still want to handle it. diff --git a/Sources/Testing/Running/Runner.RuntimeState.swift b/Sources/Testing/Running/Runner.RuntimeState.swift index 2d22002c5..eb892da62 100644 --- a/Sources/Testing/Running/Runner.RuntimeState.swift +++ b/Sources/Testing/Running/Runner.RuntimeState.swift @@ -48,16 +48,6 @@ extension Runner { } configuration.eventHandler = { [oldEventHandler = configuration.eventHandler] event, context in -#if !SWT_NO_FILE_IO - var event = copy event - if case .valueAttached = event.kind { - guard let configuration = context.configuration, - configuration.handleValueAttachedEvent(&event, in: context) else { - // The attachment could not be handled, so suppress this event. - return - } - } -#endif RuntimeState.$current.withValue(existingRuntimeState) { oldEventHandler(event, context) } diff --git a/Sources/Testing/Running/Runner.swift b/Sources/Testing/Running/Runner.swift index 1cedf6182..e67b7cd8b 100644 --- a/Sources/Testing/Running/Runner.swift +++ b/Sources/Testing/Running/Runner.swift @@ -416,6 +416,7 @@ extension Runner { private static func _run(_ runner: Self) async { var runner = runner runner.configureEventHandlerRuntimeState() + runner.configureAttachmentHandling() // Track whether or not any issues were recorded across the entire run. let issueRecorded = Locked(rawValue: false) From 021ff0824efdc5eb591086ebde39cff649ff0cea Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Thu, 6 Nov 2025 16:56:33 -0500 Subject: [PATCH 201/216] Use `AtomicLazyReference` to store the fallback event handler. (#1401) I forgot we had this type! `AtomicLazyReference` is designed for exactly our use case. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- .../_TestingInterop/FallbackEventHandler.swift | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/Sources/_TestingInterop/FallbackEventHandler.swift b/Sources/_TestingInterop/FallbackEventHandler.swift index bd4286315..9408bbdfb 100644 --- a/Sources/_TestingInterop/FallbackEventHandler.swift +++ b/Sources/_TestingInterop/FallbackEventHandler.swift @@ -36,7 +36,7 @@ private final class _FallbackEventHandlerStorage: Sendable, RawRepresentable { } /// The installed event handler. -private let _fallbackEventHandler = Atomic?>(nil) +private let _fallbackEventHandler = AtomicLazyReference<_FallbackEventHandlerStorage>() #endif /// A type describing a fallback event handler that testing API can invoke as an @@ -82,9 +82,7 @@ package func _swift_testing_getFallbackEventHandler() -> FallbackEventHandler? { // would need a full lock in order to avoid that problem. However, because we // instead have a one-time installation function, we can be sure that the // loaded value (if non-nil) will never be replaced with another value. - return _fallbackEventHandler.load(ordering: .sequentiallyConsistent).map { fallbackEventHandler in - fallbackEventHandler.takeUnretainedValue().rawValue - } + return _fallbackEventHandler.load()?.rawValue #endif } @@ -116,14 +114,9 @@ package func _swift_testing_installFallbackEventHandler(_ handler: FallbackEvent return true } #else - let handler = Unmanaged.passRetained(_FallbackEventHandlerStorage(rawValue: handler)) - defer { - if !result { - handler.release() - } - } - - result = _fallbackEventHandler.compareExchange(expected: nil, desired: handler, ordering: .sequentiallyConsistent).exchanged + let handler = _FallbackEventHandlerStorage(rawValue: handler) + let stored = _fallbackEventHandler.storeIfNil(handler) + result = (handler === stored) #endif return result From 6a52f30c7f735040bb0a3cb2ed81a08800f2d4a1 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Fri, 7 Nov 2025 09:55:14 -0500 Subject: [PATCH 202/216] Handle image types derived from the base types. (#1399) On Apple platforms, you can declare file types (uniform type identifiers, or "UTTypes" for short) that conform to existing types. That means a developer could conceivably say "save this image as .florb" where `.florb` represents a type that conforms to, but is not identical to, `.jpeg`. This PR ensures that if a developer passes such a type, we handle it by passing the supported base type to Image I/O. And, if the developer passes some image format that is unsupported by Image I/O, we can throw an error with a more specific diagnostic than just "it failed." ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- .../Attachments/AttachableAsCGImage.swift | 32 ++++++++++++++++++- .../Images/ImageAttachmentError.swift | 5 +++ Tests/TestingTests/AttachmentTests.swift | 27 ++++++++++++++++ 3 files changed, 63 insertions(+), 1 deletion(-) diff --git a/Sources/Overlays/_Testing_CoreGraphics/Attachments/AttachableAsCGImage.swift b/Sources/Overlays/_Testing_CoreGraphics/Attachments/AttachableAsCGImage.swift index 4bb060377..0fbcac010 100644 --- a/Sources/Overlays/_Testing_CoreGraphics/Attachments/AttachableAsCGImage.swift +++ b/Sources/Overlays/_Testing_CoreGraphics/Attachments/AttachableAsCGImage.swift @@ -12,6 +12,9 @@ package import CoreGraphics package import ImageIO private import UniformTypeIdentifiers +#if canImport(UniformTypeIdentifiers_Private) +@_spi(Private) private import UniformTypeIdentifiers +#endif /// A protocol describing images that can be converted to instances of /// [`Attachment`](https://developer.apple.com/documentation/testing/attachment) @@ -47,6 +50,20 @@ package protocol AttachableAsCGImage: AttachableAsImage { var attachmentScaleFactor: CGFloat { get } } +/// All type identifiers supported by Image I/O. +@available(_uttypesAPI, *) +private let _supportedTypeIdentifiers = Set(CGImageDestinationCopyTypeIdentifiers() as? [String] ?? []) + +/// All content types supported by Image I/O. +@available(_uttypesAPI, *) +private let _supportedContentTypes = { +#if canImport(UniformTypeIdentifiers_Private) + UTType._types(identifiers: _supportedTypeIdentifiers).values +#else + _supportedTypeIdentifiers.compactMap(UTType.init(_:)) +#endif +}() + @available(_uttypesAPI, *) extension AttachableAsCGImage { package var attachmentOrientation: CGImagePropertyOrientation { @@ -63,8 +80,21 @@ extension AttachableAsCGImage { // Convert the image to a CGImage. let attachableCGImage = try attachableCGImage + // Determine the base content type to use. We do a naïve case-sensitive + // string comparison on the identifier first as it's faster than querying + // the corresponding UTType instances (because it doesn't need to touch the + // Launch Services database). The common cases where the developer passes + // no image format or passes .png/.jpeg are covered by the fast path. + var contentType = imageFormat.contentType + if !_supportedTypeIdentifiers.contains(contentType.identifier) { + guard let baseType = _supportedContentTypes.first(where: contentType.conforms(to:)) else { + throw ImageAttachmentError.unsupportedImageFormat(contentType.identifier) + } + contentType = baseType + } + // Create the image destination. - guard let dest = CGImageDestinationCreateWithData(data as CFMutableData, imageFormat.contentType.identifier as CFString, 1, nil) else { + guard let dest = CGImageDestinationCreateWithData(data as CFMutableData, contentType.identifier as CFString, 1, nil) else { throw ImageAttachmentError.couldNotCreateImageDestination } diff --git a/Sources/Testing/Attachments/Images/ImageAttachmentError.swift b/Sources/Testing/Attachments/Images/ImageAttachmentError.swift index e172abd8c..b1ad5a347 100644 --- a/Sources/Testing/Attachments/Images/ImageAttachmentError.swift +++ b/Sources/Testing/Attachments/Images/ImageAttachmentError.swift @@ -25,6 +25,9 @@ package enum ImageAttachmentError: Error { /// The image could not be converted. case couldNotConvertImage + + /// The specified content type is not supported by Image I/O. + case unsupportedImageFormat(_ typeIdentifier: String) #elseif os(Windows) /// A call to `QueryInterface()` failed. case queryInterfaceFailed(Any.Type, CLong) @@ -57,6 +60,8 @@ extension ImageAttachmentError: CustomStringConvertible { "Could not create the Core Graphics image destination to encode this image." case .couldNotConvertImage: "Could not convert the image to the specified format." + case let .unsupportedImageFormat(typeIdentifier): + "Could not convert the image to the format '\(typeIdentifier)' because the system does not support it." } #elseif os(Windows) switch self { diff --git a/Tests/TestingTests/AttachmentTests.swift b/Tests/TestingTests/AttachmentTests.swift index b457fc271..623ead084 100644 --- a/Tests/TestingTests/AttachmentTests.swift +++ b/Tests/TestingTests/AttachmentTests.swift @@ -597,6 +597,33 @@ extension AttachmentTests { } } + @available(_uttypesAPI, *) + @Test func attachCGImageWithCustomUTType() throws { + let contentType = try #require(UTType(tag: "derived-from-jpeg", tagClass: .filenameExtension, conformingTo: .jpeg)) + let format = AttachableImageFormat(contentType: contentType) + let image = try Self.cgImage.get() + let attachment = Attachment(image, named: "diamond", as: format) + #expect(attachment.attachableValue === image) + try attachment.attachableValue.withUnsafeBytes(for: attachment) { buffer in + #expect(buffer.count > 32) + } + if let ext = format.contentType.preferredFilenameExtension { + #expect(attachment.preferredName == ("diamond" as NSString).appendingPathExtension(ext)) + } + } + + @available(_uttypesAPI, *) + @Test func attachCGImageWithUnsupportedImageType() throws { + let contentType = try #require(UTType(tag: "unsupported-image-format", tagClass: .filenameExtension, conformingTo: .image)) + let format = AttachableImageFormat(contentType: contentType) + let image = try Self.cgImage.get() + let attachment = Attachment(image, named: "diamond", as: format) + #expect(attachment.attachableValue === image) + #expect(throws: ImageAttachmentError.self) { + try attachment.attachableValue.withUnsafeBytes(for: attachment) { _ in } + } + } + #if !SWT_NO_EXIT_TESTS @available(_uttypesAPI, *) @Test func cannotAttachCGImageWithNonImageType() async { From e727d1a2fdba391ca52411a34dd31fb162cb158b Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Fri, 7 Nov 2025 09:55:26 -0500 Subject: [PATCH 203/216] Keep libtool from complaining about ExecutablePath.cpp being empty. (#1402) On non-OpenBSD (so, everywhere), libtool complains that ExecutablePath.cpp is empty. Silence it by providing a dummy symbol. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- Sources/_TestingInternals/ExecutablePath.cpp | 6 +++++- Sources/_TestingInternals/include/ExecutablePath.h | 4 ++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/Sources/_TestingInternals/ExecutablePath.cpp b/Sources/_TestingInternals/ExecutablePath.cpp index 7213cfa75..82297b452 100644 --- a/Sources/_TestingInternals/ExecutablePath.cpp +++ b/Sources/_TestingInternals/ExecutablePath.cpp @@ -29,8 +29,12 @@ static void captureEarlyCWD(void) { earlyCWD.store(buffer); } } +#endif const char *swt_getEarlyCWD(void) { +#if defined(__OpenBSD__) return earlyCWD.load(); -} +#else + return nullptr; #endif +} diff --git a/Sources/_TestingInternals/include/ExecutablePath.h b/Sources/_TestingInternals/include/ExecutablePath.h index 22f3acc62..dfa9b1e7e 100644 --- a/Sources/_TestingInternals/include/ExecutablePath.h +++ b/Sources/_TestingInternals/include/ExecutablePath.h @@ -16,15 +16,15 @@ SWT_ASSUME_NONNULL_BEGIN -#if defined(__OpenBSD__) /// Get the current working directory as it was set shortly after the process /// started and before `main()` has been called. /// /// This function is necessary on OpenBSD so that we can (as correctly as /// possible) resolve the executable path when the first argument is a relative /// path (which can occur when manually invoking the test executable.) +/// +/// On all other platforms, this function always returns `nullptr`. SWT_EXTERN const char *_Nullable swt_getEarlyCWD(void); -#endif SWT_ASSUME_NONNULL_END From 10970ce1b7e985d5a70ff6451c8ed5a44bd3d339 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Fri, 7 Nov 2025 12:30:43 -0500 Subject: [PATCH 204/216] Avoid `PATH_MAX` when getting the executable path on Linux and OpenBSD. (#1403) This PR adjusts the implementations of `CommandLine.executablePath` on Linux and OpenBSD to avoid an artificial upper bound based on `PATH_MAX`. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- .../Additions/CommandLineAdditions.swift | 25 ++++++++++++++----- Sources/_TestingInternals/ExecutablePath.cpp | 5 ++-- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/Sources/Testing/Support/Additions/CommandLineAdditions.swift b/Sources/Testing/Support/Additions/CommandLineAdditions.swift index 63decd1ad..895082e05 100644 --- a/Sources/Testing/Support/Additions/CommandLineAdditions.swift +++ b/Sources/Testing/Support/Additions/CommandLineAdditions.swift @@ -33,14 +33,27 @@ extension CommandLine { } return result! #elseif os(Linux) || os(Android) - return try withUnsafeTemporaryAllocation(of: CChar.self, capacity: Int(PATH_MAX) * 2) { buffer in - let readCount = readlink("/proc/self/exe", buffer.baseAddress!, buffer.count - 1) - guard readCount >= 0 else { - throw CError(rawValue: swt_errno()) + var result: String? +#if DEBUG + var bufferCount = Int(1) // force looping +#else + var bufferCount = Int(PATH_MAX) +#endif + while result == nil { + try withUnsafeTemporaryAllocation(of: CChar.self, capacity: bufferCount) { buffer in + let readCount = readlink("/proc/self/exe", buffer.baseAddress!, buffer.count) + guard readCount >= 0 else { + throw CError(rawValue: swt_errno()) + } + if readCount < buffer.count { + buffer[readCount] = 0 // NUL-terminate the string. + result = String(cString: buffer.baseAddress!) + } else { + bufferCount += Int(PATH_MAX) // add more space and try again + } } - buffer[readCount] = 0 // NUL-terminate the string. - return String(cString: buffer.baseAddress!) } + return result! #elseif os(FreeBSD) var mib = [CTL_KERN, KERN_PROC, KERN_PROC_PATHNAME, -1] return try mib.withUnsafeMutableBufferPointer { mib in diff --git a/Sources/_TestingInternals/ExecutablePath.cpp b/Sources/_TestingInternals/ExecutablePath.cpp index 82297b452..f1a9b6656 100644 --- a/Sources/_TestingInternals/ExecutablePath.cpp +++ b/Sources/_TestingInternals/ExecutablePath.cpp @@ -24,9 +24,8 @@ static constinit std::atomic earlyCWD { nullptr }; /// path (which can occur when manually invoking the test executable.) __attribute__((__constructor__(101), __used__)) static void captureEarlyCWD(void) { - static char buffer[PATH_MAX * 2]; - if (getcwd(buffer, sizeof(buffer))) { - earlyCWD.store(buffer); + if (auto cwd = getcwd(nil, 0)) { + earlyCWD.store(cwd); } } #endif From 44b5378ef5463470247aa59dcc79356d423de568 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Fri, 7 Nov 2025 13:29:00 -0500 Subject: [PATCH 205/216] Fix OpenBSD-only typo (nil instead of nullptr) --- Sources/_TestingInternals/ExecutablePath.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/_TestingInternals/ExecutablePath.cpp b/Sources/_TestingInternals/ExecutablePath.cpp index f1a9b6656..ba0c2b0dd 100644 --- a/Sources/_TestingInternals/ExecutablePath.cpp +++ b/Sources/_TestingInternals/ExecutablePath.cpp @@ -24,7 +24,7 @@ static constinit std::atomic earlyCWD { nullptr }; /// path (which can occur when manually invoking the test executable.) __attribute__((__constructor__(101), __used__)) static void captureEarlyCWD(void) { - if (auto cwd = getcwd(nil, 0)) { + if (auto cwd = getcwd(nullptr, 0)) { earlyCWD.store(cwd); } } From b86ca03a143d9deeaf8c47af7ba9da0f83494124 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Fri, 7 Nov 2025 15:55:45 -0500 Subject: [PATCH 206/216] `SWT_NO_FILE_IO` guard is in the wrong place around `configureAttachmentHandling()`. --- Sources/Testing/Attachments/Attachment.swift | 2 -- Sources/Testing/Running/Runner.swift | 2 ++ 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/Testing/Attachments/Attachment.swift b/Sources/Testing/Attachments/Attachment.swift index 40d498433..0cbd5d703 100644 --- a/Sources/Testing/Attachments/Attachment.swift +++ b/Sources/Testing/Attachments/Attachment.swift @@ -506,7 +506,6 @@ extension Runner { /// attached" events and saves attachments where necessary. mutating func configureAttachmentHandling() { configuration.eventHandler = { [oldEventHandler = configuration.eventHandler] event, context in -#if !SWT_NO_FILE_IO var event = copy event if case .valueAttached = event.kind { guard let configuration = context.configuration, @@ -516,7 +515,6 @@ extension Runner { } } oldEventHandler(event, context) -#endif } } } diff --git a/Sources/Testing/Running/Runner.swift b/Sources/Testing/Running/Runner.swift index e67b7cd8b..8adf7e088 100644 --- a/Sources/Testing/Running/Runner.swift +++ b/Sources/Testing/Running/Runner.swift @@ -416,7 +416,9 @@ extension Runner { private static func _run(_ runner: Self) async { var runner = runner runner.configureEventHandlerRuntimeState() +#if !SWT_NO_FILE_IO runner.configureAttachmentHandling() +#endif // Track whether or not any issues were recorded across the entire run. let issueRecorded = Locked(rawValue: false) From 564e4f2f33b3cc143bca55231410a83db6e42925 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Fri, 7 Nov 2025 16:51:57 -0500 Subject: [PATCH 207/216] When saving a `String` as an attachment, assume `".txt"` as a path extension. (#1404) When a string is attached to a test, we encode it as UTF-8 (as is tradition). This PR adjusts `String`'s conformance to `Attachable` so that it uses the `".txt"` extension (unless the user specifies something else). This improves usability in Xcode test reports by making these attachments double-clickable and QuickLookable (is that a word?) ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- Sources/Testing/Attachments/Attachable.swift | 79 +++++++++++++++++++ Tests/TestingTests/AttachmentTests.swift | 8 ++ .../Traits/AttachmentSavingTraitTests.swift | 10 +-- 3 files changed, 92 insertions(+), 5 deletions(-) diff --git a/Sources/Testing/Attachments/Attachable.swift b/Sources/Testing/Attachments/Attachable.swift index 8e2c06420..8c3476657 100644 --- a/Sources/Testing/Attachments/Attachable.swift +++ b/Sources/Testing/Attachments/Attachable.swift @@ -103,17 +103,37 @@ public protocol Attachable: ~Copyable { // MARK: - Default implementations +/// @Metadata { +/// @Available(Swift, introduced: 6.2) +/// @Available(Xcode, introduced: 26.0) +/// } extension Attachable where Self: ~Copyable { + /// @Metadata { + /// @Available(Swift, introduced: 6.2) + /// @Available(Xcode, introduced: 26.0) + /// } public var estimatedAttachmentByteCount: Int? { nil } + /// @Metadata { + /// @Available(Swift, introduced: 6.2) + /// @Available(Xcode, introduced: 26.0) + /// } public borrowing func preferredName(for attachment: borrowing Attachment, basedOn suggestedName: String) -> String { suggestedName } } +/// @Metadata { +/// @Available(Swift, introduced: 6.2) +/// @Available(Xcode, introduced: 26.0) +/// } extension Attachable where Self: Collection, Element == UInt8 { + /// @Metadata { + /// @Available(Swift, introduced: 6.2) + /// @Available(Xcode, introduced: 26.0) + /// } public var estimatedAttachmentByteCount: Int? { count } @@ -125,37 +145,88 @@ extension Attachable where Self: Collection, Element == UInt8 { // (potentially expensive!) copy of the collection. } +/// @Metadata { +/// @Available(Swift, introduced: 6.2) +/// @Available(Xcode, introduced: 26.0) +/// } extension Attachable where Self: StringProtocol { + /// @Metadata { + /// @Available(Swift, introduced: 6.2) + /// @Available(Xcode, introduced: 26.0) + /// } public var estimatedAttachmentByteCount: Int? { // NOTE: utf8.count may be O(n) for foreign strings. // SEE: https://github.com/swiftlang/swift/blob/main/stdlib/public/core/StringUTF8View.swift utf8.count } + + /// @Metadata { + /// @Available(Swift, introduced: 6.2) + /// @Available(Xcode, introduced: 26.0) + /// } + public borrowing func preferredName(for attachment: borrowing Attachment, basedOn suggestedName: String) -> String { + if suggestedName.contains(".") { + return suggestedName + } + return "\(suggestedName).txt" + } } // MARK: - Default conformances // Implement the protocol requirements for byte arrays and buffers so that // developers can attach raw data when needed. +/// @Metadata { +/// @Available(Swift, introduced: 6.2) +/// @Available(Xcode, introduced: 26.0) +/// } extension Array: Attachable { + /// @Metadata { + /// @Available(Swift, introduced: 6.2) + /// @Available(Xcode, introduced: 26.0) + /// } public func withUnsafeBytes(for attachment: borrowing Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { try withUnsafeBytes(body) } } +/// @Metadata { +/// @Available(Swift, introduced: 6.2) +/// @Available(Xcode, introduced: 26.0) +/// } extension ContiguousArray: Attachable { + /// @Metadata { + /// @Available(Swift, introduced: 6.2) + /// @Available(Xcode, introduced: 26.0) + /// } public func withUnsafeBytes(for attachment: borrowing Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { try withUnsafeBytes(body) } } +/// @Metadata { +/// @Available(Swift, introduced: 6.2) +/// @Available(Xcode, introduced: 26.0) +/// } extension ArraySlice: Attachable { + /// @Metadata { + /// @Available(Swift, introduced: 6.2) + /// @Available(Xcode, introduced: 26.0) + /// } public func withUnsafeBytes(for attachment: borrowing Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { try withUnsafeBytes(body) } } +/// @Metadata { +/// @Available(Swift, introduced: 6.2) +/// @Available(Xcode, introduced: 26.0) +/// } extension String: Attachable { + /// @Metadata { + /// @Available(Swift, introduced: 6.2) + /// @Available(Xcode, introduced: 26.0) + /// } public func withUnsafeBytes(for attachment: borrowing Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { var selfCopy = self return try selfCopy.withUTF8 { utf8 in @@ -164,7 +235,15 @@ extension String: Attachable { } } +/// @Metadata { +/// @Available(Swift, introduced: 6.2) +/// @Available(Xcode, introduced: 26.0) +/// } extension Substring: Attachable { + /// @Metadata { + /// @Available(Swift, introduced: 6.2) + /// @Available(Xcode, introduced: 26.0) + /// } public func withUnsafeBytes(for attachment: borrowing Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { var selfCopy = self return try selfCopy.withUTF8 { utf8 in diff --git a/Tests/TestingTests/AttachmentTests.swift b/Tests/TestingTests/AttachmentTests.swift index 623ead084..854ecd133 100644 --- a/Tests/TestingTests/AttachmentTests.swift +++ b/Tests/TestingTests/AttachmentTests.swift @@ -62,6 +62,14 @@ struct AttachmentTests { } #endif + @Test func preferredNameOfStringAttachment() { + let attachment1 = Attachment("", named: "abc123") + #expect(attachment1.preferredName == "abc123.txt") + + let attachment2 = Attachment("", named: "abc123.html") + #expect(attachment2.preferredName == "abc123.html") + } + #if !SWT_NO_FILE_IO func compare(_ attachableValue: borrowing MySendableAttachable, toContentsOfFileAtPath filePath: String) throws { let file = try FileHandle(forReadingAtPath: filePath) diff --git a/Tests/TestingTests/Traits/AttachmentSavingTraitTests.swift b/Tests/TestingTests/Traits/AttachmentSavingTraitTests.swift index 1ea63eeda..47edd8b25 100644 --- a/Tests/TestingTests/Traits/AttachmentSavingTraitTests.swift +++ b/Tests/TestingTests/Traits/AttachmentSavingTraitTests.swift @@ -130,27 +130,27 @@ extension `AttachmentSavingTrait tests` { @Suite(.hidden, currentAttachmentSavingTrait) struct FixtureSuite { @Test(.hidden) func `Records an attachment (passing)`() { - Attachment.record("", named: "PASSING TEST") + Attachment.record([], named: "PASSING TEST") } @Test(.hidden) func `Records an attachment (warning)`() { - Attachment.record("", named: "PASSING TEST") + Attachment.record([], named: "PASSING TEST") Issue.record("", severity: .warning) } @Test(.hidden) func `Records an attachment (failing)`() { - Attachment.record("", named: "FAILING TEST") + Attachment.record([], named: "FAILING TEST") Issue.record("") } @Test(.hidden, arguments: 0 ..< 5) func `Records an attachment (passing, parameterized)`(i: Int) async { - Attachment.record("\(i)", named: "PASSING TEST") + Attachment.record([UInt8(i)], named: "PASSING TEST") } @Test(.hidden, arguments: 0 ..< 7) // intentionally different count func `Records an attachment (failing, parameterized)`(i: Int) async { - Attachment.record("\(i)", named: "FAILING TEST") + Attachment.record([UInt8(i)], named: "FAILING TEST") Issue.record("\(i)") } } From c9e74ad26933dda3dcdbd4a63502e8a11a235d58 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Mon, 10 Nov 2025 13:17:24 -0500 Subject: [PATCH 208/216] Add more availability annotations for image attachment symbols. (#1409) Adds more of these: ```swift /// @Metadata { /// @Available(Swift, introduced: 6.3) /// } ``` To public symbols that DocC can see. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- .../AttachableImageFormat+UTType.swift | 3 +++ ...AttachableImageWrapper+AttachableWrapper.swift | 9 +++++++++ .../Attachments/AttachableImageFormat+CLSID.swift | 3 +++ ...AttachableImageWrapper+AttachableWrapper.swift | 9 +++++++++ .../Attachments/Images/AttachableAsImage.swift | 4 ++++ .../Images/AttachableImageFormat.swift | 15 +++++++++++++++ .../Images/Attachment+AttachableAsImage.swift | 3 +++ 7 files changed, 46 insertions(+) diff --git a/Sources/Overlays/_Testing_CoreGraphics/Attachments/AttachableImageFormat+UTType.swift b/Sources/Overlays/_Testing_CoreGraphics/Attachments/AttachableImageFormat+UTType.swift index 43c1346e4..1adcdaea4 100644 --- a/Sources/Overlays/_Testing_CoreGraphics/Attachments/AttachableImageFormat+UTType.swift +++ b/Sources/Overlays/_Testing_CoreGraphics/Attachments/AttachableImageFormat+UTType.swift @@ -11,6 +11,9 @@ #if SWT_TARGET_OS_APPLE && canImport(CoreGraphics) public import UniformTypeIdentifiers +/// @Metadata { +/// @Available(Swift, introduced: 6.3) +/// } @available(_uttypesAPI, *) extension AttachableImageFormat { /// The content type corresponding to this image format. diff --git a/Sources/Overlays/_Testing_CoreGraphics/Attachments/_AttachableImageWrapper+AttachableWrapper.swift b/Sources/Overlays/_Testing_CoreGraphics/Attachments/_AttachableImageWrapper+AttachableWrapper.swift index 16c165ae1..3db932096 100644 --- a/Sources/Overlays/_Testing_CoreGraphics/Attachments/_AttachableImageWrapper+AttachableWrapper.swift +++ b/Sources/Overlays/_Testing_CoreGraphics/Attachments/_AttachableImageWrapper+AttachableWrapper.swift @@ -13,6 +13,9 @@ private import CoreGraphics private import UniformTypeIdentifiers +/// @Metadata { +/// @Available(Swift, introduced: 6.3) +/// } @available(_uttypesAPI, *) extension _AttachableImageWrapper: Attachable, AttachableWrapper where Image: AttachableAsImage { /// Get the image format to use when encoding an image, substituting a @@ -47,11 +50,17 @@ extension _AttachableImageWrapper: Attachable, AttachableWrapper where Image: At return encodingQuality < 1.0 ? .jpeg : .png } + /// @Metadata { + /// @Available(Swift, introduced: 6.3) + /// } public func withUnsafeBytes(for attachment: borrowing Attachment<_AttachableImageWrapper>, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { let imageFormat = _imageFormat(forPreferredName: attachment.preferredName) return try wrappedValue.withUnsafeBytes(as: imageFormat, body) } + /// @Metadata { + /// @Available(Swift, introduced: 6.3) + /// } public borrowing func preferredName(for attachment: borrowing Attachment<_AttachableImageWrapper>, basedOn suggestedName: String) -> String { let imageFormat = _imageFormat(forPreferredName: suggestedName) return (suggestedName as NSString).appendingPathExtension(for: imageFormat.contentType) diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableImageFormat+CLSID.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableImageFormat+CLSID.swift index deaf032c4..6ea0dc0ad 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableImageFormat+CLSID.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableImageFormat+CLSID.swift @@ -11,6 +11,9 @@ #if os(Windows) public import WinSDK +/// @Metadata { +/// @Available(Swift, introduced: 6.3) +/// } extension AttachableImageFormat { private static let _encoderPathExtensionsByCLSID = Result { var result = [CLSID.Wrapper: [String]]() diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/_AttachableImageWrapper+AttachableWrapper.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/_AttachableImageWrapper+AttachableWrapper.swift index 3437f2deb..fe148a9f0 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/_AttachableImageWrapper+AttachableWrapper.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/_AttachableImageWrapper+AttachableWrapper.swift @@ -11,6 +11,9 @@ #if os(Windows) private import WinSDK +/// @Metadata { +/// @Available(Swift, introduced: 6.3) +/// } extension _AttachableImageWrapper: Attachable, AttachableWrapper where Image: AttachableAsImage { /// Get the image format to use when encoding an image. /// @@ -38,11 +41,17 @@ extension _AttachableImageWrapper: Attachable, AttachableWrapper where Image: At return encodingQuality < 1.0 ? .jpeg : .png } + /// @Metadata { + /// @Available(Swift, introduced: 6.3) + /// } public func withUnsafeBytes(for attachment: borrowing Attachment<_AttachableImageWrapper>, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { let imageFormat = _imageFormat(forPreferredName: attachment.preferredName) return try wrappedValue.withUnsafeBytes(as: imageFormat, body) } + /// @Metadata { + /// @Available(Swift, introduced: 6.3) + /// } public borrowing func preferredName(for attachment: borrowing Attachment<_AttachableImageWrapper>, basedOn suggestedName: String) -> String { let imageFormat = _imageFormat(forPreferredName: suggestedName) return AttachableImageFormat.appendPathExtension(for: imageFormat.encoderCLSID, to: suggestedName) diff --git a/Sources/Testing/Attachments/Images/AttachableAsImage.swift b/Sources/Testing/Attachments/Images/AttachableAsImage.swift index 5091ec9ae..a0683561c 100644 --- a/Sources/Testing/Attachments/Images/AttachableAsImage.swift +++ b/Sources/Testing/Attachments/Images/AttachableAsImage.swift @@ -81,6 +81,10 @@ public protocol AttachableAsImage { /// The testing library uses this function when saving an image as an /// attachment. The implementation should use `imageFormat` to determine what /// encoder to use. + /// + /// @Metadata { + /// @Available(Swift, introduced: 6.3) + /// } borrowing func withUnsafeBytes(as imageFormat: AttachableImageFormat, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R /// Make a copy of this instance to pass to an attachment. diff --git a/Sources/Testing/Attachments/Images/AttachableImageFormat.swift b/Sources/Testing/Attachments/Images/AttachableImageFormat.swift index d0fb16507..9bbc99c75 100644 --- a/Sources/Testing/Attachments/Images/AttachableImageFormat.swift +++ b/Sources/Testing/Attachments/Images/AttachableImageFormat.swift @@ -79,6 +79,9 @@ public struct AttachableImageFormat: Sendable { // MARK: - Equatable, Hashable +/// @Metadata { +/// @Available(Swift, introduced: 6.3) +/// } #if SWT_NO_IMAGE_ATTACHMENTS @_unavailableInEmbedded @available(*, unavailable, message: "Image attachments are not available on this platform.") @@ -120,12 +123,18 @@ extension AttachableImageFormat.Kind: Equatable, Hashable { // MARK: - CustomStringConvertible, CustomDebugStringConvertible +/// @Metadata { +/// @Available(Swift, introduced: 6.3) +/// } #if SWT_NO_IMAGE_ATTACHMENTS @_unavailableInEmbedded @available(*, unavailable, message: "Image attachments are not available on this platform.") #endif @available(_uttypesAPI, *) extension AttachableImageFormat: CustomStringConvertible, CustomDebugStringConvertible { + /// @Metadata { + /// @Available(Swift, introduced: 6.3) + /// } public var description: String { let kindDescription = String(describing: kind) if encodingQuality < 1.0 { @@ -134,6 +143,9 @@ extension AttachableImageFormat: CustomStringConvertible, CustomDebugStringConve return kindDescription } + /// @Metadata { + /// @Available(Swift, introduced: 6.3) + /// } public var debugDescription: String { let kindDescription = String(reflecting: kind) return "\(kindDescription) at quality \(encodingQuality)" @@ -142,6 +154,9 @@ extension AttachableImageFormat: CustomStringConvertible, CustomDebugStringConve // MARK: - +/// @Metadata { +/// @Available(Swift, introduced: 6.3) +/// } #if SWT_NO_IMAGE_ATTACHMENTS @_unavailableInEmbedded @available(*, unavailable, message: "Image attachments are not available on this platform.") diff --git a/Sources/Testing/Attachments/Images/Attachment+AttachableAsImage.swift b/Sources/Testing/Attachments/Images/Attachment+AttachableAsImage.swift index 045b5938e..3612596c5 100644 --- a/Sources/Testing/Attachments/Images/Attachment+AttachableAsImage.swift +++ b/Sources/Testing/Attachments/Images/Attachment+AttachableAsImage.swift @@ -8,6 +8,9 @@ // See https://swift.org/CONTRIBUTORS.txt for Swift project authors // +/// @Metadata { +/// @Available(Swift, introduced: 6.3) +/// } #if SWT_NO_IMAGE_ATTACHMENTS @_unavailableInEmbedded @available(*, unavailable, message: "Image attachments are not available on this platform.") From fbc3bbffdab18c79b46dc865f518abdbb21961ae Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Mon, 10 Nov 2025 14:10:13 -0500 Subject: [PATCH 209/216] Mark `Issue.Severity` cases with availability. (#1410) It is necessary to explicitly mark the cases of this enum with their Swift 6.3 availability as it is not inherited when rendered in DocC. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- Sources/Testing/Issues/Issue.swift | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Sources/Testing/Issues/Issue.swift b/Sources/Testing/Issues/Issue.swift index beeca101e..70e4a01a9 100644 --- a/Sources/Testing/Issues/Issue.swift +++ b/Sources/Testing/Issues/Issue.swift @@ -94,12 +94,20 @@ public struct Issue: Sendable { /// /// An issue with warning severity does not cause the test it's associated /// with to be marked as a failure, but is noted in the results. + /// + /// @Metadata { + /// @Available(Swift, introduced: 6.3) + /// } case warning /// The severity level for an issue which represents an error in a test. /// /// An issue with error severity causes the test it's associated with to be /// marked as a failure. + /// + /// @Metadata { + /// @Available(Swift, introduced: 6.3) + /// } case error } From 01413a500aa448c48a7c105beae31649124540f1 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Tue, 11 Nov 2025 11:59:04 -0500 Subject: [PATCH 210/216] Fix a typo in exit tests documentation. (#1414) Add a missing word in the exit tests documentation. Resolves #1372. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- Sources/Testing/Testing.docc/exit-testing.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Testing/Testing.docc/exit-testing.md b/Sources/Testing/Testing.docc/exit-testing.md index fc00bf31d..a94bfbd78 100644 --- a/Sources/Testing/Testing.docc/exit-testing.md +++ b/Sources/Testing/Testing.docc/exit-testing.md @@ -128,7 +128,7 @@ value using the `as` operator: Every value you capture in an exit test must conform to [`Sendable`](https://developer.apple.com/documentation/swift/sendable) and [`Codable`](https://developer.apple.com/documentation/swift/codable). Each value is encoded by the parent process using [`encode(to:)`](https://developer.apple.com/documentation/swift/encodable/encode(to:)) -and is decoded by the child process [`init(from:)`](https://developer.apple.com/documentation/swift/decodable/init(from:)) +and is decoded by the child process using [`init(from:)`](https://developer.apple.com/documentation/swift/decodable/init(from:)) before being passed to the exit test body. If a captured value's type does not conform to both `Sendable` and `Codable`, or From 31395386a962fc7e68badb49bb1d625ad353b632 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Tue, 11 Nov 2025 11:59:17 -0500 Subject: [PATCH 211/216] Replace `wchar_t` with `CWideChar`. (#1412) Previously, the definition of `CWideChar` was incorrect on Windows and referred to a 32-bit type instead of a 16-bit type. This has been corrected, so we can use Swift's canonical name for the type instead of C's `wchar_t`. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- .../_Testing_Foundation/Attachments/Attachment+URL.swift | 4 ++-- .../Testing/Support/Additions/CommandLineAdditions.swift | 2 +- Sources/Testing/Support/CError.swift | 6 +++--- Sources/Testing/Support/Environment.swift | 2 +- Sources/Testing/Support/FileHandle.swift | 4 ++-- Sources/Testing/Support/Versions.swift | 2 +- Tests/TestingTests/ABIEntryPointTests.swift | 2 +- Tests/TestingTests/Support/FileHandleTests.swift | 2 +- 8 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Sources/Overlays/_Testing_Foundation/Attachments/Attachment+URL.swift b/Sources/Overlays/_Testing_Foundation/Attachments/Attachment+URL.swift index 3ca05b8d1..e072bf76d 100644 --- a/Sources/Overlays/_Testing_Foundation/Attachments/Attachment+URL.swift +++ b/Sources/Overlays/_Testing_Foundation/Attachments/Attachment+URL.swift @@ -121,14 +121,14 @@ private let _archiverPath: String? = { return nil } - return withUnsafeTemporaryAllocation(of: wchar_t.self, capacity: Int(bufferCount)) { buffer -> String? in + return withUnsafeTemporaryAllocation(of: CWideChar.self, capacity: Int(bufferCount)) { buffer -> String? in let bufferCount = GetSystemDirectoryW(buffer.baseAddress!, UINT(buffer.count)) guard bufferCount > 0 && bufferCount < buffer.count else { return nil } return _archiverName.withCString(encodedAs: UTF16.self) { archiverName -> String? in - var result: UnsafeMutablePointer? + var result: UnsafeMutablePointer? let flags = ULONG(PATHCCH_ALLOW_LONG_PATHS.rawValue) guard S_OK == PathAllocCombine(buffer.baseAddress!, archiverName, flags, &result) else { diff --git a/Sources/Testing/Support/Additions/CommandLineAdditions.swift b/Sources/Testing/Support/Additions/CommandLineAdditions.swift index 895082e05..6f307acfb 100644 --- a/Sources/Testing/Support/Additions/CommandLineAdditions.swift +++ b/Sources/Testing/Support/Additions/CommandLineAdditions.swift @@ -90,7 +90,7 @@ extension CommandLine { var bufferCount = Int(MAX_PATH) #endif while result == nil { - try withUnsafeTemporaryAllocation(of: wchar_t.self, capacity: bufferCount) { buffer in + try withUnsafeTemporaryAllocation(of: CWideChar.self, capacity: bufferCount) { buffer in SetLastError(DWORD(ERROR_SUCCESS)) _ = GetModuleFileNameW(nil, buffer.baseAddress!, DWORD(buffer.count)) switch GetLastError() { diff --git a/Sources/Testing/Support/CError.swift b/Sources/Testing/Support/CError.swift index b392191d1..648cd917e 100644 --- a/Sources/Testing/Support/CError.swift +++ b/Sources/Testing/Support/CError.swift @@ -76,9 +76,9 @@ extension Win32Error: CustomStringConvertible { // error message... _unless_ you pass `FORMAT_MESSAGE_ALLOCATE_BUFFER` in // which case it takes a pointer-to-pointer that it populates with a // heap-allocated string. However, the signature for FormatMessageW() - // still takes an LPWSTR? (Optional>), so we - // need to temporarily mis-cast the pointer before we can pass it in. - let count = buffer.withMemoryRebound(to: wchar_t.self) { buffer in + // still takes an LPWSTR? (Optional>), so + // we need to temporarily mis-cast the pointer before we can pass it in. + let count = buffer.withMemoryRebound(to: CWideChar.self) { buffer in FormatMessageW( DWORD(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS | FORMAT_MESSAGE_MAX_WIDTH_MASK), nil, diff --git a/Sources/Testing/Support/Environment.swift b/Sources/Testing/Support/Environment.swift index 4cddde9e0..514e70a2c 100644 --- a/Sources/Testing/Support/Environment.swift +++ b/Sources/Testing/Support/Environment.swift @@ -175,7 +175,7 @@ package enum Environment { #elseif os(Windows) name.withCString(encodedAs: UTF16.self) { name in func getVariable(maxCount: Int) -> String? { - withUnsafeTemporaryAllocation(of: wchar_t.self, capacity: maxCount) { buffer in + withUnsafeTemporaryAllocation(of: CWideChar.self, capacity: maxCount) { buffer in SetLastError(DWORD(ERROR_SUCCESS)) let count = GetEnvironmentVariableW(name, buffer.baseAddress!, DWORD(buffer.count)) if count == 0 { diff --git a/Sources/Testing/Support/FileHandle.swift b/Sources/Testing/Support/FileHandle.swift index d038db101..408ba2cd6 100644 --- a/Sources/Testing/Support/FileHandle.swift +++ b/Sources/Testing/Support/FileHandle.swift @@ -625,7 +625,7 @@ func appendPathComponent(_ pathComponent: String, to path: String) -> String { #if os(Windows) path.withCString(encodedAs: UTF16.self) { path in pathComponent.withCString(encodedAs: UTF16.self) { pathComponent in - withUnsafeTemporaryAllocation(of: wchar_t.self, capacity: (wcslen(path) + wcslen(pathComponent)) * 2 + 1) { buffer in + withUnsafeTemporaryAllocation(of: CWideChar.self, capacity: (wcslen(path) + wcslen(pathComponent)) * 2 + 1) { buffer in _ = wcscpy_s(buffer.baseAddress, buffer.count, path) _ = PathCchAppendEx(buffer.baseAddress, buffer.count, pathComponent, ULONG(PATHCCH_ALLOW_LONG_PATHS.rawValue)) return (String.decodeCString(buffer.baseAddress, as: UTF16.self)?.result)! @@ -734,7 +734,7 @@ let rootDirectoryPath: String = { // https://devblogs.microsoft.com/oldnewthing/20140723-00/?p=423 . let count = GetSystemWindowsDirectoryW(nil, 0) if count > 0 { - withUnsafeTemporaryAllocation(of: wchar_t.self, capacity: Int(count) + 1) { buffer in + withUnsafeTemporaryAllocation(of: CWideChar.self, capacity: Int(count) + 1) { buffer in _ = GetSystemWindowsDirectoryW(buffer.baseAddress!, UINT(buffer.count)) let rStrip = PathCchStripToRoot(buffer.baseAddress!, buffer.count) if rStrip == S_OK || rStrip == S_FALSE { diff --git a/Sources/Testing/Support/Versions.swift b/Sources/Testing/Support/Versions.swift index 85d55429d..44955cb8f 100644 --- a/Sources/Testing/Support/Versions.swift +++ b/Sources/Testing/Support/Versions.swift @@ -77,7 +77,7 @@ let operatingSystemVersion: String = { // Include Service Pack details if available. if versionInfo.szCSDVersion.0 != 0 { withUnsafeBytes(of: versionInfo.szCSDVersion) { szCSDVersion in - szCSDVersion.withMemoryRebound(to: wchar_t.self) { szCSDVersion in + szCSDVersion.withMemoryRebound(to: CWideChar.self) { szCSDVersion in if let szCSDVersion = String.decodeCString(szCSDVersion.baseAddress!, as: UTF16.self)?.result { result += " (\(szCSDVersion))" } diff --git a/Tests/TestingTests/ABIEntryPointTests.swift b/Tests/TestingTests/ABIEntryPointTests.swift index 15b9cc879..a50f92afa 100644 --- a/Tests/TestingTests/ABIEntryPointTests.swift +++ b/Tests/TestingTests/ABIEntryPointTests.swift @@ -192,7 +192,7 @@ private func withTestingLibraryImageAddress(_ body: (ImageAddress?) throws -> // ELF-based platforms. #elseif os(Windows) let flags = DWORD(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS) - try addressInTestingLibrary.withMemoryRebound(to: wchar_t.self, capacity: MemoryLayout.stride / MemoryLayout.stride) { addressInTestingLibrary in + try addressInTestingLibrary.withMemoryRebound(to: CWideChar.self, capacity: MemoryLayout.stride / MemoryLayout.stride) { addressInTestingLibrary in try #require(GetModuleHandleExW(flags, addressInTestingLibrary, &testingLibraryAddress)) } defer { diff --git a/Tests/TestingTests/Support/FileHandleTests.swift b/Tests/TestingTests/Support/FileHandleTests.swift index acca1dbea..fd52678d7 100644 --- a/Tests/TestingTests/Support/FileHandleTests.swift +++ b/Tests/TestingTests/Support/FileHandleTests.swift @@ -271,7 +271,7 @@ func temporaryDirectory() throws -> String { #elseif os(Android) Environment.variable(named: "TMPDIR") ?? "/data/local/tmp" #elseif os(Windows) - try withUnsafeTemporaryAllocation(of: wchar_t.self, capacity: Int(MAX_PATH + 1)) { buffer in + try withUnsafeTemporaryAllocation(of: CWideChar.self, capacity: Int(MAX_PATH + 1)) { buffer in // NOTE: GetTempPath2W() was introduced in Windows 10 Build 20348. if 0 == GetTempPathW(DWORD(buffer.count), buffer.baseAddress) { throw Win32Error(rawValue: GetLastError()) From b0aef4867851b20247cd7da9cce2fafd97153fb2 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Tue, 11 Nov 2025 12:43:47 -0500 Subject: [PATCH 212/216] Add an upper bound on the number of test cases we run in parallel. (#1390) This PR adds an upper bound, `NCORES * 2`, on the number of test cases we run in parallel. Depending on the exact nature of your tests, this can significantly reduce the maximum amount of dirty memory needed, but does not generally impact execution time. As an example, Swift Testing's own tests go from > 300MB max memory usage to around 60MB. You can configure a different upper bound by setting the `"SWT_EXPERIMENTAL_MAXIMUM_PARALLELIZATION_WIDTH"` environment variable (look, naming is hard okay?) ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- .../Testing/ABI/EntryPoints/EntryPoint.swift | 25 ++++- Sources/Testing/CMakeLists.txt | 1 + Sources/Testing/Running/Configuration.swift | 30 +++++- Sources/Testing/Running/Runner.swift | 63 ++++++++++--- Sources/Testing/Support/Serializer.swift | 93 +++++++++++++++++++ Tests/TestingTests/SwiftPMTests.swift | 19 ++++ 6 files changed, 218 insertions(+), 13 deletions(-) create mode 100644 Sources/Testing/Support/Serializer.swift diff --git a/Sources/Testing/ABI/EntryPoints/EntryPoint.swift b/Sources/Testing/ABI/EntryPoints/EntryPoint.swift index f4f1a751c..6163e7bd1 100644 --- a/Sources/Testing/ABI/EntryPoints/EntryPoint.swift +++ b/Sources/Testing/ABI/EntryPoints/EntryPoint.swift @@ -210,6 +210,9 @@ public struct __CommandLineArguments_v0: Sendable { /// The value of the `--parallel` or `--no-parallel` argument. public var parallel: Bool? + /// The maximum number of test tasks to run in parallel. + public var experimentalMaximumParallelizationWidth: Int? + /// The value of the `--symbolicate-backtraces` argument. public var symbolicateBacktraces: String? @@ -336,6 +339,7 @@ extension __CommandLineArguments_v0: Codable { enum CodingKeys: String, CodingKey { case listTests case parallel + case experimentalMaximumParallelizationWidth case symbolicateBacktraces case verbose case veryVerbose @@ -485,6 +489,10 @@ func parseCommandLineArguments(from args: [String]) throws -> __CommandLineArgum if args.contains("--no-parallel") { result.parallel = false } + if let maximumParallelizationWidth = args.argumentValue(forLabel: "--experimental-maximum-parallelization-width").flatMap(Int.init) { + // TODO: decide if we want to repurpose --num-workers for this use case? + result.experimentalMaximumParallelizationWidth = maximumParallelizationWidth + } // Whether or not to symbolicate backtraces in the event stream. if let symbolicateBacktraces = args.argumentValue(forLabel: "--symbolicate-backtraces") { @@ -545,7 +553,22 @@ public func configurationForEntryPoint(from args: __CommandLineArguments_v0) thr var configuration = Configuration() // Parallelization (on by default) - configuration.isParallelizationEnabled = args.parallel ?? true + if let parallel = args.parallel, !parallel { + configuration.isParallelizationEnabled = parallel + } else { + var maximumParallelizationWidth = args.experimentalMaximumParallelizationWidth + if maximumParallelizationWidth == nil && Test.current == nil { + // Don't check the environment variable when a current test is set (which + // presumably means we're running our own unit tests). + maximumParallelizationWidth = Environment.variable(named: "SWT_EXPERIMENTAL_MAXIMUM_PARALLELIZATION_WIDTH").flatMap(Int.init) + } + if let maximumParallelizationWidth { + if maximumParallelizationWidth < 1 { + throw _EntryPointError.invalidArgument("--experimental-maximum-parallelization-width", value: String(describing: maximumParallelizationWidth)) + } + configuration.maximumParallelizationWidth = maximumParallelizationWidth + } + } // Whether or not to symbolicate backtraces in the event stream. if let symbolicateBacktraces = args.symbolicateBacktraces { diff --git a/Sources/Testing/CMakeLists.txt b/Sources/Testing/CMakeLists.txt index b7836e3e1..b60688731 100644 --- a/Sources/Testing/CMakeLists.txt +++ b/Sources/Testing/CMakeLists.txt @@ -90,6 +90,7 @@ add_library(Testing Support/Graph.swift Support/JSON.swift Support/Locked.swift + Support/Serializer.swift Support/VersionNumber.swift Support/Versions.swift Discovery+Macro.swift diff --git a/Sources/Testing/Running/Configuration.swift b/Sources/Testing/Running/Configuration.swift index 12b8827de..e0fe009ba 100644 --- a/Sources/Testing/Running/Configuration.swift +++ b/Sources/Testing/Running/Configuration.swift @@ -8,6 +8,8 @@ // See https://swift.org/CONTRIBUTORS.txt for Swift project authors // +private import _TestingInternals + /// A type containing settings for preparing and running tests. @_spi(ForToolsIntegrationOnly) public struct Configuration: Sendable { @@ -18,7 +20,33 @@ public struct Configuration: Sendable { // MARK: - Parallelization /// Whether or not to parallelize the execution of tests and test cases. - public var isParallelizationEnabled: Bool = true + /// + /// - Note: Setting the value of this property implicitly sets the value of + /// the experimental ``maximumParallelizationWidth`` property. + public var isParallelizationEnabled: Bool { + get { + maximumParallelizationWidth > 1 + } + set { + maximumParallelizationWidth = newValue ? defaultParallelizationWidth : 1 + } + } + + /// The maximum width of parallelization. + /// + /// The value of this property determines how many tests (or rather, test + /// cases) will run in parallel. + /// + /// @Comment { + /// The default value of this property is equal to twice the number of CPU + /// cores reported by the operating system, or `Int.max` if that value is + /// not available. + /// } + /// + /// - Note: Setting the value of this property implicitly sets the value of + /// the ``isParallelizationEnabled`` property. + @_spi(Experimental) + public var maximumParallelizationWidth: Int = defaultParallelizationWidth /// How to symbolicate backtraces captured during a test run. /// diff --git a/Sources/Testing/Running/Runner.swift b/Sources/Testing/Running/Runner.swift index 8adf7e088..a6a76189e 100644 --- a/Sources/Testing/Running/Runner.swift +++ b/Sources/Testing/Running/Runner.swift @@ -66,6 +66,20 @@ extension Runner { .current ?? .init() } + /// Context to apply to a test run. + /// + /// Instances of this type are passed directly to the various functions in + /// this file and represent context for the run itself. As such, they are not + /// task-local nor are they meant to change as the test run progresses. + /// + /// This type is distinct from ``Configuration`` which _can_ change on a + /// per-test basis. If you find yourself wanting to modify a property of this + /// type at runtime, it may be better-suited for ``Configuration`` instead. + private struct _Context: Sendable { + /// A serializer used to reduce parallelism among test cases. + var testCaseSerializer: Serializer? + } + /// Apply the custom scope for any test scope providers of the traits /// associated with a specified test by calling their /// ``TestScoping/provideScope(for:testCase:performing:)`` function. @@ -179,6 +193,7 @@ extension Runner { /// /// - Parameters: /// - stepGraph: The subgraph whose root value, a step, is to be run. + /// - context: Context for the test run. /// /// - Throws: Whatever is thrown from the test body. Thrown errors are /// normally reported as test failures. @@ -193,7 +208,7 @@ extension Runner { /// ## See Also /// /// - ``Runner/run()`` - private static func _runStep(atRootOf stepGraph: Graph) async throws { + private static func _runStep(atRootOf stepGraph: Graph, context: _Context) async throws { // Whether to send a `.testEnded` event at the end of running this step. // Some steps' actions may not require a final event to be sent — for // example, a skip event only sends `.testSkipped`. @@ -250,18 +265,18 @@ extension Runner { try await _applyScopingTraits(for: step.test, testCase: nil) { // Run the test function at this step (if one is present.) if let testCases = step.test.testCases { - await _runTestCases(testCases, within: step) + await _runTestCases(testCases, within: step, context: context) } // Run the children of this test (i.e. the tests in this suite.) - try await _runChildren(of: stepGraph) + try await _runChildren(of: stepGraph, context: context) } } } } else { // There is no test at this node in the graph, so just skip down to the // child nodes. - try await _runChildren(of: stepGraph) + try await _runChildren(of: stepGraph, context: context) } } @@ -286,10 +301,11 @@ extension Runner { /// - Parameters: /// - stepGraph: The subgraph whose root value, a step, will be used to /// find children to run. + /// - context: Context for the test run. /// /// - Throws: Whatever is thrown from the test body. Thrown errors are /// normally reported as test failures. - private static func _runChildren(of stepGraph: Graph) async throws { + private static func _runChildren(of stepGraph: Graph, context: _Context) async throws { let childGraphs = if _configuration.isParallelizationEnabled { // Explicitly shuffle the steps to help detect accidental dependencies // between tests due to their ordering. @@ -331,7 +347,7 @@ extension Runner { // Run the child nodes. try await _forEach(in: childGraphs.lazy.map(\.value), namingTasksWith: taskNamer) { childGraph in - try await _runStep(atRootOf: childGraph) + try await _runStep(atRootOf: childGraph, context: context) } } @@ -340,12 +356,15 @@ extension Runner { /// - Parameters: /// - testCases: The test cases to be run. /// - step: The runner plan step associated with this test case. + /// - context: Context for the test run. /// /// If parallelization is supported and enabled, the generated test cases will /// be run in parallel using a task group. - private static func _runTestCases(_ testCases: some Sequence, within step: Plan.Step) async { + private static func _runTestCases(_ testCases: some Sequence, within step: Plan.Step, context: _Context) async { + let configuration = _configuration + // Apply the configuration's test case filter. - let testCaseFilter = _configuration.testCaseFilter + let testCaseFilter = configuration.testCaseFilter let testCases = testCases.lazy.filter { testCase in testCaseFilter(testCase, step.test) } @@ -359,7 +378,13 @@ extension Runner { } await _forEach(in: testCases.enumerated(), namingTasksWith: taskNamer) { _, testCase in - await _runTestCase(testCase, within: step) + if let testCaseSerializer = context.testCaseSerializer { + // Note that if .serialized is applied to an inner scope, we still use + // this serializer (if set) so that we don't overcommit. + await testCaseSerializer.run { await _runTestCase(testCase, within: step, context: context) } + } else { + await _runTestCase(testCase, within: step, context: context) + } } } @@ -368,10 +393,11 @@ extension Runner { /// - Parameters: /// - testCase: The test case to run. /// - step: The runner plan step associated with this test case. + /// - context: Context for the test run. /// /// This function sets ``Test/Case/current``, then invokes the test case's /// body closure. - private static func _runTestCase(_ testCase: Test.Case, within step: Plan.Step) async { + private static func _runTestCase(_ testCase: Test.Case, within step: Plan.Step, context: _Context) async { let configuration = _configuration Event.post(.testCaseStarted, for: (step.test, testCase), configuration: configuration) @@ -431,6 +457,21 @@ extension Runner { eventHandler(event, context) } + // Context to pass into the test run. We intentionally don't pass the Runner + // itself (implicitly as `self` nor as an argument) because we don't want to + // accidentally depend on e.g. the `configuration` property rather than the + // current configuration. + let context: _Context = { + var context = _Context() + + let maximumParallelizationWidth = runner.configuration.maximumParallelizationWidth + if maximumParallelizationWidth > 1 && maximumParallelizationWidth < .max { + context.testCaseSerializer = Serializer(maximumWidth: runner.configuration.maximumParallelizationWidth) + } + + return context + }() + 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. @@ -457,7 +498,7 @@ extension Runner { taskAction = "running iteration #\(iterationIndex + 1)" } _ = taskGroup.addTaskUnlessCancelled(name: decorateTaskName("test run", withAction: taskAction)) { - try? await _runStep(atRootOf: runner.plan.stepGraph) + try? await _runStep(atRootOf: runner.plan.stepGraph, context: context) } await taskGroup.waitForAll() } diff --git a/Sources/Testing/Support/Serializer.swift b/Sources/Testing/Support/Serializer.swift new file mode 100644 index 000000000..96adfca7c --- /dev/null +++ b/Sources/Testing/Support/Serializer.swift @@ -0,0 +1,93 @@ +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024–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 +// + +private import _TestingInternals + +/// The number of CPU cores on the current system, or `nil` if that +/// information is not available. +var cpuCoreCount: Int? { +#if SWT_TARGET_OS_APPLE || os(Linux) || os(FreeBSD) || os(OpenBSD) || os(Android) + return Int(sysconf(Int32(_SC_NPROCESSORS_CONF))) +#elseif os(Windows) + var siInfo = SYSTEM_INFO() + GetSystemInfo(&siInfo) + return Int(siInfo.dwNumberOfProcessors) +#elseif os(WASI) + return 1 +#else +#warning("Platform-specific implementation missing: CPU core count unavailable") + return nil +#endif +} + +/// The default parallelization width when parallelized testing is enabled. +var defaultParallelizationWidth: Int { + // cpuCoreCount.map { max(1, $0) * 2 } ?? .max + .max +} + +/// A type whose instances can run a series of work items in strict order. +/// +/// When a work item is scheduled on an instance of this type, it runs after any +/// previously-scheduled work items. If it suspends, subsequently-scheduled work +/// items do not start running; they must wait until the suspended work item +/// either returns or throws an error. +/// +/// This type is not part of the public interface of the testing library. +final actor Serializer { + /// The maximum number of work items that may run concurrently. + nonisolated let maximumWidth: Int + + /// The number of scheduled work items, including any currently running. + private var _currentWidth = 0 + + /// Continuations for any scheduled work items that haven't started yet. + private var _continuations = [CheckedContinuation]() + + init(maximumWidth: Int = 1) { + precondition(maximumWidth >= 1, "Invalid serializer width \(maximumWidth).") + self.maximumWidth = maximumWidth + } + + /// Run a work item serially after any previously-scheduled work items. + /// + /// - Parameters: + /// - workItem: A closure to run. + /// + /// - Returns: Whatever is returned from `workItem`. + /// + /// - Throws: Whatever is thrown by `workItem`. + func run(_ workItem: @isolated(any) @Sendable () async throws -> R) async rethrows -> R where R: Sendable { + _currentWidth += 1 + defer { + // Resume the next scheduled closure. + if !_continuations.isEmpty { + let continuation = _continuations.removeFirst() + continuation.resume() + } + + _currentWidth -= 1 + } + + await withCheckedContinuation { continuation in + if _currentWidth <= maximumWidth { + // Nothing else was scheduled, so we can resume immediately. + continuation.resume() + } else { + // Something was scheduled, so add the continuation to the + // list. When it resumes, we can run. + _continuations.append(continuation) + } + } + + return try await workItem() + } +} + diff --git a/Tests/TestingTests/SwiftPMTests.swift b/Tests/TestingTests/SwiftPMTests.swift index 4668fbb25..2d7001e8f 100644 --- a/Tests/TestingTests/SwiftPMTests.swift +++ b/Tests/TestingTests/SwiftPMTests.swift @@ -59,6 +59,25 @@ struct SwiftPMTests { #expect(!configuration.isParallelizationEnabled) } + @Test("--experimental-maximum-parallelization-width argument") + func maximumParallelizationWidth() throws { + var configuration = try configurationForEntryPoint(withArguments: ["PATH", "--experimental-maximum-parallelization-width", "12345"]) + #expect(configuration.isParallelizationEnabled) + #expect(configuration.maximumParallelizationWidth == 12345) + + configuration = try configurationForEntryPoint(withArguments: ["PATH", "--experimental-maximum-parallelization-width", "1"]) + #expect(!configuration.isParallelizationEnabled) + #expect(configuration.maximumParallelizationWidth == 1) + + configuration = try configurationForEntryPoint(withArguments: ["PATH", "--experimental-maximum-parallelization-width", "\(Int.max)"]) + #expect(configuration.isParallelizationEnabled) + #expect(configuration.maximumParallelizationWidth == .max) + + #expect(throws: (any Error).self) { + _ = try configurationForEntryPoint(withArguments: ["PATH", "--experimental-maximum-parallelization-width", "0"]) + } + } + @Test("--symbolicate-backtraces argument", arguments: [ (String?.none, Backtrace.SymbolicationMode?.none), From fd350e44f8714efb5cb233ea07e40cec8d2bc9d0 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Wed, 12 Nov 2025 10:08:26 -0500 Subject: [PATCH 213/216] Use `trylock` to eliminate the remaining race condition in `Test.cancel()`. (#1415) This PR fixes the race condition in `Test.cancel()` that could occur if an unstructured task, created from within a test's task, called `Test.cancel()` at just the right moment. The order of events for the race is: - Unstructured task is created and inherits task-locals including the reference to the test's unsafe current task; - Test's task starts tearing down; - Unstructured task calls `takeUnsafeCurrentTask()` and gets a reference to the unsafe current task; - Test's task finishes tearing down; - Unstructured task calls `UnsafeCurrentTask.cancel()`. The fix is to use `trylock` semantics when cancelling the unsafe current task. If the test's task is still alive, the task is cancelled while the lock is held, which will block the test's task from being torn down as it has a lock-guarded call to clear the unsafe current task reference. If the test's task is no longer alive, the reference is already `nil` by the time the unstructured task acquires the lock and it bails early. If we recursively call `cancel()` (which can happen via the concurrency-level cancellation handler), the `trylock` means we won't acquire the lock a second time, so we won't end up deadlocking or aborting (which is what prevents calling `cancel()` while holding the lock in the current implementation). It is possible for `cancel()` to trigger user code, especially if the user has set up a cancellation handler, but there is no code path that can then lead to a deadlock because the only user-accessible calls that might touch this lock use `trylock`. I hope some part of that made sense. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- Sources/Testing/Support/Locked.swift | 71 +++++++++++++++++++- Sources/Testing/Test+Cancellation.swift | 87 +++++++++++++++---------- 2 files changed, 120 insertions(+), 38 deletions(-) diff --git a/Sources/Testing/Support/Locked.swift b/Sources/Testing/Support/Locked.swift index fac062adb..fbebaa063 100644 --- a/Sources/Testing/Support/Locked.swift +++ b/Sources/Testing/Support/Locked.swift @@ -26,6 +26,14 @@ struct Locked { /// A type providing storage for the underlying lock and wrapped value. #if SWT_TARGET_OS_APPLE && canImport(os) private typealias _Storage = ManagedBuffer +#elseif !SWT_FIXED_85448 && (os(Linux) || os(Android)) + private final class _Storage: ManagedBuffer { + deinit { + withUnsafeMutablePointerToElements { lock in + _ = pthread_mutex_destroy(lock) + } + } + } #else private final class _Storage { let mutex: Mutex @@ -49,6 +57,11 @@ extension Locked: RawRepresentable { _storage.withUnsafeMutablePointerToElements { lock in lock.initialize(to: .init()) } +#elseif !SWT_FIXED_85448 && (os(Linux) || os(Android)) + _storage = _Storage.create(minimumCapacity: 1, makingHeaderWith: { _ in rawValue }) as! _Storage + _storage.withUnsafeMutablePointerToElements { lock in + _ = pthread_mutex_init(lock, nil) + } #else nonisolated(unsafe) let rawValue = rawValue _storage = _Storage(rawValue) @@ -77,20 +90,72 @@ extension Locked { /// synchronous caller. Wherever possible, use actor isolation or other Swift /// concurrency tools. func withLock(_ body: (inout T) throws -> sending R) rethrows -> sending R where R: ~Copyable { + nonisolated(unsafe) let result: R #if SWT_TARGET_OS_APPLE && canImport(os) - nonisolated(unsafe) let result = try _storage.withUnsafeMutablePointers { rawValue, lock in + result = try _storage.withUnsafeMutablePointers { rawValue, lock in os_unfair_lock_lock(lock) defer { os_unfair_lock_unlock(lock) } return try body(&rawValue.pointee) } - return result +#elseif !SWT_FIXED_85448 && (os(Linux) || os(Android)) + result = try _storage.withUnsafeMutablePointers { rawValue, lock in + pthread_mutex_lock(lock) + defer { + pthread_mutex_unlock(lock) + } + return try body(&rawValue.pointee) + } #else - try _storage.mutex.withLock { rawValue in + result = try _storage.mutex.withLock { rawValue in try body(&rawValue) } #endif + return result + } + + /// Try to acquire the lock and invoke a function while it is held. + /// + /// - Parameters: + /// - body: A closure to invoke while the lock is held. + /// + /// - Returns: Whatever is returned by `body`, or `nil` if the lock could not + /// be acquired. + /// + /// - Throws: Whatever is thrown by `body`. + /// + /// This function can be used to synchronize access to shared data from a + /// synchronous caller. Wherever possible, use actor isolation or other Swift + /// concurrency tools. + func withLockIfAvailable(_ body: (inout T) throws -> sending R) rethrows -> sending R? where R: ~Copyable { + nonisolated(unsafe) let result: R? +#if SWT_TARGET_OS_APPLE && canImport(os) + result = try _storage.withUnsafeMutablePointers { rawValue, lock in + guard os_unfair_lock_trylock(lock) else { + return nil + } + defer { + os_unfair_lock_unlock(lock) + } + return try body(&rawValue.pointee) + } +#elseif !SWT_FIXED_85448 && (os(Linux) || os(Android)) + result = try _storage.withUnsafeMutablePointers { rawValue, lock in + guard 0 == pthread_mutex_trylock(lock) else { + return nil + } + defer { + pthread_mutex_unlock(lock) + } + return try body(&rawValue.pointee) + } +#else + result = try _storage.mutex.withLockIfAvailable { rawValue in + return try body(&rawValue) + } +#endif + return result } } diff --git a/Sources/Testing/Test+Cancellation.swift b/Sources/Testing/Test+Cancellation.swift index 1ded9359f..43fd54391 100644 --- a/Sources/Testing/Test+Cancellation.swift +++ b/Sources/Testing/Test+Cancellation.swift @@ -25,9 +25,8 @@ protocol TestCancellable: Sendable { // MARK: - Tracking the current task -/// A structure describing a reference to a task that is associated with some -/// ``TestCancellable`` value. -private struct _TaskReference: Sendable { +/// A structure that is able to cancel a task. +private struct _TaskCanceller: Sendable { /// The unsafe underlying reference to the associated task. private nonisolated(unsafe) var _unsafeCurrentTask = Locked() @@ -45,25 +44,46 @@ private struct _TaskReference: Sendable { _unsafeCurrentTask = withUnsafeCurrentTask { Locked(rawValue: $0) } } - /// Take this instance's reference to its associated task. - /// - /// - Returns: An `UnsafeCurrentTask` instance, or `nil` if it was already - /// taken or if it was never available. - /// - /// This function consumes the reference to the task. After the first call, - /// subsequent calls on the same instance return `nil`. - func takeUnsafeCurrentTask() -> UnsafeCurrentTask? { + /// Clear this instance's reference to its associated task without first + /// cancelling it. + func clear() { _unsafeCurrentTask.withLock { unsafeCurrentTask in - let result = unsafeCurrentTask unsafeCurrentTask = nil - return result } } + + /// Cancel this instance's associated task and clear the reference to it. + /// + /// - Returns: Whether or not this instance's task was cancelled. + /// + /// After the first call to this function _starts_, subsequent calls on the + /// same instance return `false`. In other words, if another thread calls this + /// function before it has returned (or the same thread calls it recursively), + /// it returns `false` without cancelling the task a second time. + func cancel(with skipInfo: SkipInfo) -> Bool { + // trylock means a recursive call to this function won't ruin our day, nor + // should interleaving locks. + _unsafeCurrentTask.withLockIfAvailable { unsafeCurrentTask in + defer { + unsafeCurrentTask = nil + } + if let unsafeCurrentTask { + // The task is still valid, so we'll cancel it. + $_currentSkipInfo.withValue(skipInfo) { + unsafeCurrentTask.cancel() + } + return true + } + + // The task has already been cancelled and/or cleared. + return false + } ?? false + } } -/// A dictionary of tracked tasks, keyed by types that conform to +/// A dictionary of cancellable tasks keyed by types that conform to /// ``TestCancellable``. -@TaskLocal private var _currentTaskReferences = [ObjectIdentifier: _TaskReference]() +@TaskLocal private var _currentTaskCancellers = [ObjectIdentifier: _TaskCanceller]() /// The instance of ``SkipInfo`` to propagate to children of the current task. /// @@ -87,16 +107,15 @@ extension TestCancellable { /// the current task, test, or test case is cancelled, it records a /// corresponding cancellation event. func withCancellationHandling(_ body: () async throws -> R) async rethrows -> R { - let taskReference = _TaskReference() - var currentTaskReferences = _currentTaskReferences - currentTaskReferences[ObjectIdentifier(Self.self)] = taskReference - return try await $_currentTaskReferences.withValue(currentTaskReferences) { - // Before returning, explicitly clear the stored task. This minimizes - // the potential race condition that can occur if test code creates an - // unstructured task and calls `Test.cancel()` in it after the test body - // has finished. + let taskCanceller = _TaskCanceller() + var currentTaskCancellers = _currentTaskCancellers + currentTaskCancellers[ObjectIdentifier(Self.self)] = taskCanceller + return try await $_currentTaskCancellers.withValue(currentTaskCancellers) { + // Before returning, explicitly clear the stored task so that an + // unstructured task that inherits the task local isn't able to + // accidentally cancel the task after it has been deallocated. defer { - _ = taskReference.takeUnsafeCurrentTask() + taskCanceller.clear() } return try await withTaskCancellationHandler { @@ -121,18 +140,16 @@ extension TestCancellable { /// - testAndTestCase: The test and test case to use when posting an event. /// - skipInfo: Information about the cancellation event. private func _cancel(_ cancellableValue: T?, for testAndTestCase: (Test?, Test.Case?), skipInfo: SkipInfo) where T: TestCancellable { - if cancellableValue != nil { - // If the current test case is still running, take its task property (which - // signals to subsequent callers that it has been cancelled.) - let task = _currentTaskReferences[ObjectIdentifier(T.self)]?.takeUnsafeCurrentTask() - - // If we just cancelled the current test case's task, post a corresponding - // event with the relevant skip info. - if let task { - $_currentSkipInfo.withValue(skipInfo) { - task.cancel() - } + if cancellableValue != nil, let taskCanceller = _currentTaskCancellers[ObjectIdentifier(T.self)] { + // Try to cancel the task associated with `T`, if any. If we succeed, post a + // corresponding event with the relevant skip info. If we fail, we still + // attempt to cancel the current *task* in order to honor our API contract. + if taskCanceller.cancel(with: skipInfo) { Event.post(T.makeCancelledEventKind(with: skipInfo), for: testAndTestCase) + } else { + withUnsafeCurrentTask { task in + task?.cancel() + } } } else { // The current task isn't associated with a test/case, so just cancel the From 9865ea1f16f5d7e31840418e330be6a37d17c624 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Wed, 12 Nov 2025 15:04:58 -0500 Subject: [PATCH 214/216] Fix up a few build failures in environments with constrained concurrency. (#1417) Fixes a few build failures that have snuck in recently when building with minimal or older concurrency support. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated. --- Sources/Testing/Support/Locked.swift | 26 ++++++++++++++++-------- Sources/Testing/Support/Serializer.swift | 10 +++++++-- 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/Sources/Testing/Support/Locked.swift b/Sources/Testing/Support/Locked.swift index fbebaa063..601fb14ce 100644 --- a/Sources/Testing/Support/Locked.swift +++ b/Sources/Testing/Support/Locked.swift @@ -9,7 +9,9 @@ // internal import _TestingInternals +#if canImport(Synchronization) private import Synchronization +#endif /// A type that wraps a value requiring access from a synchronous caller during /// concurrent execution. @@ -24,7 +26,7 @@ private import Synchronization /// This type is not part of the public interface of the testing library. struct Locked { /// A type providing storage for the underlying lock and wrapped value. -#if SWT_TARGET_OS_APPLE && canImport(os) +#if SWT_TARGET_OS_APPLE && !SWT_NO_OS_UNFAIR_LOCK private typealias _Storage = ManagedBuffer #elseif !SWT_FIXED_85448 && (os(Linux) || os(Android)) private final class _Storage: ManagedBuffer { @@ -34,7 +36,7 @@ struct Locked { } } } -#else +#elseif canImport(Synchronization) private final class _Storage { let mutex: Mutex @@ -42,6 +44,8 @@ struct Locked { mutex = Mutex(rawValue) } } +#else +#error("Platform-specific misconfiguration: no mutex or lock type available") #endif /// Storage for the underlying lock and wrapped value. @@ -52,7 +56,7 @@ extension Locked: Sendable where T: Sendable {} extension Locked: RawRepresentable { init(rawValue: T) { -#if SWT_TARGET_OS_APPLE && canImport(os) +#if SWT_TARGET_OS_APPLE && !SWT_NO_OS_UNFAIR_LOCK _storage = .create(minimumCapacity: 1, makingHeaderWith: { _ in rawValue }) _storage.withUnsafeMutablePointerToElements { lock in lock.initialize(to: .init()) @@ -62,9 +66,11 @@ extension Locked: RawRepresentable { _storage.withUnsafeMutablePointerToElements { lock in _ = pthread_mutex_init(lock, nil) } -#else +#elseif canImport(Synchronization) nonisolated(unsafe) let rawValue = rawValue _storage = _Storage(rawValue) +#else +#error("Platform-specific misconfiguration: no mutex or lock type available") #endif } @@ -91,7 +97,7 @@ extension Locked { /// concurrency tools. func withLock(_ body: (inout T) throws -> sending R) rethrows -> sending R where R: ~Copyable { nonisolated(unsafe) let result: R -#if SWT_TARGET_OS_APPLE && canImport(os) +#if SWT_TARGET_OS_APPLE && !SWT_NO_OS_UNFAIR_LOCK result = try _storage.withUnsafeMutablePointers { rawValue, lock in os_unfair_lock_lock(lock) defer { @@ -107,10 +113,12 @@ extension Locked { } return try body(&rawValue.pointee) } -#else +#elseif canImport(Synchronization) result = try _storage.mutex.withLock { rawValue in try body(&rawValue) } +#else +#error("Platform-specific misconfiguration: no mutex or lock type available") #endif return result } @@ -130,7 +138,7 @@ extension Locked { /// concurrency tools. func withLockIfAvailable(_ body: (inout T) throws -> sending R) rethrows -> sending R? where R: ~Copyable { nonisolated(unsafe) let result: R? -#if SWT_TARGET_OS_APPLE && canImport(os) +#if SWT_TARGET_OS_APPLE && !SWT_NO_OS_UNFAIR_LOCK result = try _storage.withUnsafeMutablePointers { rawValue, lock in guard os_unfair_lock_trylock(lock) else { return nil @@ -150,10 +158,12 @@ extension Locked { } return try body(&rawValue.pointee) } -#else +#elseif canImport(Synchronization) result = try _storage.mutex.withLockIfAvailable { rawValue in return try body(&rawValue) } +#else +#error("Platform-specific misconfiguration: no mutex or lock type available") #endif return result } diff --git a/Sources/Testing/Support/Serializer.swift b/Sources/Testing/Support/Serializer.swift index 96adfca7c..94f7d4f5b 100644 --- a/Sources/Testing/Support/Serializer.swift +++ b/Sources/Testing/Support/Serializer.swift @@ -10,9 +10,10 @@ private import _TestingInternals +#if !SWT_NO_UNSTRUCTURED_TASKS /// The number of CPU cores on the current system, or `nil` if that /// information is not available. -var cpuCoreCount: Int? { +private var _cpuCoreCount: Int? { #if SWT_TARGET_OS_APPLE || os(Linux) || os(FreeBSD) || os(OpenBSD) || os(Android) return Int(sysconf(Int32(_SC_NPROCESSORS_CONF))) #elseif os(Windows) @@ -26,10 +27,11 @@ var cpuCoreCount: Int? { return nil #endif } +#endif /// The default parallelization width when parallelized testing is enabled. var defaultParallelizationWidth: Int { - // cpuCoreCount.map { max(1, $0) * 2 } ?? .max + // _cpuCoreCount.map { max(1, $0) * 2 } ?? .max .max } @@ -45,11 +47,13 @@ final actor Serializer { /// The maximum number of work items that may run concurrently. nonisolated let maximumWidth: Int +#if !SWT_NO_UNSTRUCTURED_TASKS /// The number of scheduled work items, including any currently running. private var _currentWidth = 0 /// Continuations for any scheduled work items that haven't started yet. private var _continuations = [CheckedContinuation]() +#endif init(maximumWidth: Int = 1) { precondition(maximumWidth >= 1, "Invalid serializer width \(maximumWidth).") @@ -65,6 +69,7 @@ final actor Serializer { /// /// - Throws: Whatever is thrown by `workItem`. func run(_ workItem: @isolated(any) @Sendable () async throws -> R) async rethrows -> R where R: Sendable { +#if !SWT_NO_UNSTRUCTURED_TASKS _currentWidth += 1 defer { // Resume the next scheduled closure. @@ -86,6 +91,7 @@ final actor Serializer { _continuations.append(continuation) } } +#endif return try await workItem() } From aecca482cba9ec250855cdde8ea2a49db93fc2bb Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Fri, 14 Nov 2025 12:38:45 -0500 Subject: [PATCH 215/216] [6.3] Set the version for release/6.3. (#1422) --- VERSION.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION.txt b/VERSION.txt index a2b88412f..0faee7d96 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -1 +1 @@ -6.3-dev +6.3 From b5599675dd00aa639a3dc1983974bb2c0af91ce4 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Fri, 14 Nov 2025 12:40:13 -0500 Subject: [PATCH 216/216] Revert "[6.3] Set the version for release/6.3. (#1422)" This reverts commit aecca482cba9ec250855cdde8ea2a49db93fc2bb. --- VERSION.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION.txt b/VERSION.txt index 0faee7d96..a2b88412f 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -1 +1 @@ -6.3 +6.3-dev