-
Notifications
You must be signed in to change notification settings - Fork 112
Introduce issue handling trait (as SPI) #1080
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
stmontgomery
merged 2 commits into
swiftlang:main
from
stmontgomery:issue-handling-trait
Apr 25, 2025
Merged
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,167 @@ | ||
// | ||
// 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 allows transforming or filtering the issues recorded by a test. | ||
/// | ||
/// Use this type to observe or customize the issue(s) recorded by the test this | ||
/// trait is applied to. You can transform a recorded issue by copying it, | ||
/// 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(_:)``. | ||
/// | ||
/// 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/filterIssues(_:)`` | ||
@_spi(Experimental) | ||
public struct IssueHandlingTrait: TestTrait, SuiteTrait { | ||
/// A function which transforms an issue and returns an optional replacement. | ||
/// | ||
/// - Parameters: | ||
/// - issue: The issue to transform. | ||
/// | ||
/// - Returns: An issue to replace `issue`, or else `nil` if the issue should | ||
/// not be recorded. | ||
fileprivate typealias Transformer = @Sendable (_ issue: Issue) -> Issue? | ||
|
||
/// This trait's transformer function. | ||
private var _transformer: Transformer | ||
|
||
fileprivate init(transformer: @escaping Transformer) { | ||
_transformer = transformer | ||
} | ||
|
||
public var isRecursive: Bool { | ||
true | ||
} | ||
} | ||
|
||
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 | ||
// for the test function level. This avoids redundantly invoking the closure | ||
// twice, and potentially double-processing, issues recorded by test | ||
// functions. | ||
test.isSuite || testCase != nil ? self : nil | ||
} | ||
|
||
public func provideScope(for test: Test, testCase: Test.Case?, performing function: @Sendable () async throws -> Void) async throws { | ||
try await provideScope(performing: function) | ||
} | ||
|
||
/// Provide scope for a specified function. | ||
/// | ||
/// - Parameters: | ||
/// - function: The function to perform. | ||
/// | ||
/// This is a simplified version of ``provideScope(for:testCase:performing:)`` | ||
/// which doesn't accept test or test case parameters. It's included so that | ||
/// a runner can invoke this trait's closure even when there is no test case, | ||
/// such as if a trait on a test function threw an error during `prepare(for:)` | ||
/// and caused an issue to be recorded for the test function. In that scenario, | ||
/// this trait still needs to be invoked, but its `scopeProvider(for:testCase:)` | ||
/// intentionally returns `nil` (see the comment in that method), so this | ||
/// function can be called instead to ensure this trait can still handle that | ||
/// issue. | ||
func provideScope(performing function: @Sendable () async throws -> Void) async throws { | ||
guard var configuration = Configuration.current else { | ||
preconditionFailure("Configuration.current is nil when calling \(#function). Please file a bug report at https://github.com/swiftlang/swift-testing/issues/new") | ||
} | ||
|
||
configuration.eventHandler = { [oldConfiguration = configuration] event, context in | ||
guard case let .issueRecorded(issue) = event.kind else { | ||
oldConfiguration.eventHandler(event, context) | ||
return | ||
} | ||
|
||
// Use the original configuration's event handler when invoking the | ||
// transformer to avoid infinite recursion if the transformer 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) { | ||
_transformer(issue) | ||
} | ||
|
||
if let newIssue { | ||
var event = event | ||
event.kind = .issueRecorded(newIssue) | ||
oldConfiguration.eventHandler(event, context) | ||
} | ||
} | ||
|
||
try await Configuration.withCurrent(configuration, perform: function) | ||
} | ||
} | ||
|
||
@_spi(Experimental) | ||
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 | ||
/// this trait is applied to. It is passed a recorded issue, and returns | ||
/// an optional issue to replace the passed-in one. | ||
/// | ||
/// 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 | ||
/// 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 | ||
/// multiple issues. If more than one instance of this trait is applied to a | ||
/// test (including via inheritance from a containing suite), the `transformer` | ||
/// 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) | ||
/// 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(transformer: transformer) | ||
} | ||
|
||
/// Constructs a trait that filters issues recorded by a test. | ||
/// | ||
/// - Parameters: | ||
/// - isIncluded: The predicate with which to filter issues recorded by the | ||
/// test this trait is applied to. It is passed a recorded issue, and | ||
/// should return `true` if the issue should be included, or `false` if it | ||
/// should be suppressed. | ||
/// | ||
/// 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 | ||
/// the test results. Otherwise, if the closure returns `false`, the issue | ||
/// will not be included in the test results. | ||
/// | ||
/// The `isIncluded` 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 `isIncluded` | ||
/// closure for each instance will be called in right-to-left, innermost-to- | ||
/// outermost order, unless `false` is returned, which will skip invoking the | ||
/// remaining traits' closures. | ||
/// | ||
/// Within `isIncluded`, 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 filterIssues(_ isIncluded: @escaping @Sendable (Issue) -> Bool) -> Self { | ||
Self { issue in | ||
isIncluded(issue) ? issue : nil | ||
} | ||
} | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.