From dc201b076644047e74193f1d16a4fb57ddd8db04 Mon Sep 17 00:00:00 2001 From: Stuart Montgomery Date: Wed, 2 Jul 2025 17:10:09 -0500 Subject: [PATCH 1/2] Promote Issue Handling Traits to public API --- Sources/Testing/Testing.docc/Traits.md | 4 +--- .../Testing/Traits/IssueHandlingTrait.swift | 21 +++++++++++++++++-- .../Traits/IssueHandlingTraitTests.swift | 2 +- 3 files changed, 21 insertions(+), 6 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..dbe99e540 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 @@ -126,7 +136,6 @@ extension IssueHandlingTrait: TestScoping { } } -@_spi(Experimental) extension Trait where Self == IssueHandlingTrait { /// Constructs an trait that transforms issues recorded by a test. /// @@ -158,6 +167,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 +205,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..3f45c13d0 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 { From ea515ef8197654eb30368ca71a181decc6dbab53 Mon Sep 17 00:00:00 2001 From: Stuart Montgomery Date: Wed, 16 Jul 2025 16:16:19 -0500 Subject: [PATCH 2/2] Add enforcement for returned issues of kind 'apiMisused' as described in proposal --- .../Testing/Traits/IssueHandlingTrait.swift | 15 ++++++-- .../Traits/IssueHandlingTraitTests.swift | 34 +++++++++++++++++++ 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/Sources/Testing/Traits/IssueHandlingTrait.swift b/Sources/Testing/Traits/IssueHandlingTrait.swift index dbe99e540..4d7d408d6 100644 --- a/Sources/Testing/Traits/IssueHandlingTrait.swift +++ b/Sources/Testing/Traits/IssueHandlingTrait.swift @@ -121,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 diff --git a/Tests/TestingTests/Traits/IssueHandlingTraitTests.swift b/Tests/TestingTests/Traits/IssueHandlingTraitTests.swift index 3f45c13d0..2efb08dcc 100644 --- a/Tests/TestingTests/Traits/IssueHandlingTraitTests.swift +++ b/Tests/TestingTests/Traits/IssueHandlingTraitTests.swift @@ -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 }