Skip to content

Commit edb9a6a

Browse files
authored
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.
1 parent 603a568 commit edb9a6a

File tree

3 files changed

+84
-30
lines changed

3 files changed

+84
-30
lines changed

Sources/Testing/Testing.docc/Traits.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ types that customize the behavior of your tests.
5151
<!--
5252
### Handling issues
5353
54-
- ``Trait/transformIssues(_:)``
54+
- ``Trait/compactMapIssues(_:)``
5555
- ``Trait/filterIssues(_:)``
5656
-->
5757

Sources/Testing/Traits/IssueHandlingTrait.swift

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,14 @@
1515
/// modifying one or more of its properties, and returning the copy. You can
1616
/// observe recorded issues by returning them unmodified. Or you can suppress an
1717
/// issue by either filtering it using ``Trait/filterIssues(_:)`` or returning
18-
/// `nil` from the closure passed to ``Trait/transformIssues(_:)``.
18+
/// `nil` from the closure passed to ``Trait/compactMapIssues(_:)``.
1919
///
2020
/// When an instance of this trait is applied to a suite, it is recursively
2121
/// inherited by all child suites and tests.
2222
///
2323
/// To add this trait to a test, use one of the following functions:
2424
///
25-
/// - ``Trait/transformIssues(_:)``
25+
/// - ``Trait/compactMapIssues(_:)``
2626
/// - ``Trait/filterIssues(_:)``
2727
@_spi(Experimental)
2828
public struct IssueHandlingTrait: TestTrait, SuiteTrait {
@@ -96,15 +96,26 @@ extension IssueHandlingTrait: TestScoping {
9696
return
9797
}
9898

99+
// Ignore system issues, as they are not expected to be caused by users.
100+
if case .system = issue.kind {
101+
oldConfiguration.eventHandler(event, context)
102+
return
103+
}
104+
99105
// Use the original configuration's event handler when invoking the
100-
// transformer to avoid infinite recursion if the transformer itself
106+
// handler closure to avoid infinite recursion if the handler itself
101107
// records new issues. This means only issue handling traits whose scope
102108
// is outside this one will be allowed to handle such issues.
103109
let newIssue = Configuration.withCurrent(oldConfiguration) {
104110
handleIssue(issue)
105111
}
106112

107113
if let newIssue {
114+
// Prohibit assigning the issue's kind to system.
115+
if case .system = newIssue.kind {
116+
preconditionFailure("Issue returned by issue handling closure cannot have kind 'system': \(newIssue)")
117+
}
118+
108119
var event = event
109120
event.kind = .issueRecorded(newIssue)
110121
oldConfiguration.eventHandler(event, context)
@@ -120,31 +131,35 @@ extension Trait where Self == IssueHandlingTrait {
120131
/// Constructs an trait that transforms issues recorded by a test.
121132
///
122133
/// - Parameters:
123-
/// - transformer: The closure called for each issue recorded by the test
134+
/// - transform: A closure called for each issue recorded by the test
124135
/// this trait is applied to. It is passed a recorded issue, and returns
125136
/// an optional issue to replace the passed-in one.
126137
///
127138
/// - Returns: An instance of ``IssueHandlingTrait`` that transforms issues.
128139
///
129-
/// The `transformer` closure is called synchronously each time an issue is
140+
/// The `transform` closure is called synchronously each time an issue is
130141
/// recorded by the test this trait is applied to. The closure is passed the
131142
/// recorded issue, and if it returns a non-`nil` value, that will be recorded
132143
/// instead of the original. Otherwise, if the closure returns `nil`, the
133144
/// issue is suppressed and will not be included in the results.
134145
///
135-
/// The `transformer` closure may be called more than once if the test records
146+
/// The `transform` closure may be called more than once if the test records
136147
/// multiple issues. If more than one instance of this trait is applied to a
137-
/// test (including via inheritance from a containing suite), the `transformer`
148+
/// test (including via inheritance from a containing suite), the `transform`
138149
/// closure for each instance will be called in right-to-left, innermost-to-
139150
/// outermost order, unless `nil` is returned, which will skip invoking the
140151
/// remaining traits' closures.
141152
///
142-
/// Within `transformer`, you may access the current test or test case (if any)
153+
/// Within `transform`, you may access the current test or test case (if any)
143154
/// using ``Test/current`` ``Test/Case/current``, respectively. You may also
144155
/// record new issues, although they will only be handled by issue handling
145156
/// traits which precede this trait or were inherited from a containing suite.
146-
public static func transformIssues(_ transformer: @escaping @Sendable (Issue) -> Issue?) -> Self {
147-
Self(handler: transformer)
157+
///
158+
/// - Note: `transform` will never be passed an issue for which the value of
159+
/// ``Issue/kind`` is ``Issue/Kind/system``, and may not return such an
160+
/// issue.
161+
public static func compactMapIssues(_ transform: @escaping @Sendable (Issue) -> Issue?) -> Self {
162+
Self(handler: transform)
148163
}
149164

150165
/// Constructs a trait that filters issues recorded by a test.
@@ -174,6 +189,9 @@ extension Trait where Self == IssueHandlingTrait {
174189
/// using ``Test/current`` ``Test/Case/current``, respectively. You may also
175190
/// record new issues, although they will only be handled by issue handling
176191
/// traits which precede this trait or were inherited from a containing suite.
192+
///
193+
/// - Note: `isIncluded` will never be passed an issue for which the value of
194+
/// ``Issue/kind`` is ``Issue/Kind/system``.
177195
public static func filterIssues(_ isIncluded: @escaping @Sendable (Issue) -> Bool) -> Self {
178196
Self { issue in
179197
isIncluded(issue) ? issue : nil

Tests/TestingTests/Traits/IssueHandlingTraitTests.swift

Lines changed: 55 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
// See https://swift.org/CONTRIBUTORS.txt for Swift project authors
99
//
1010

11-
@_spi(Experimental) @_spi(ForToolsIntegrationOnly) import Testing
11+
@testable @_spi(Experimental) @_spi(ForToolsIntegrationOnly) import Testing
1212

1313
@Suite("IssueHandlingTrait Tests")
1414
struct IssueHandlingTraitTests {
@@ -23,7 +23,7 @@ struct IssueHandlingTraitTests {
2323
#expect(issue.comments == ["Foo", "Bar"])
2424
}
2525

26-
let handler = IssueHandlingTrait.transformIssues { issue in
26+
let handler = IssueHandlingTrait.compactMapIssues { issue in
2727
var issue = issue
2828
issue.comments.append("Bar")
2929
return issue
@@ -34,16 +34,16 @@ struct IssueHandlingTraitTests {
3434
}.run(configuration: configuration)
3535
}
3636

37-
@Test("Suppressing an issue by returning `nil` from the transform closure")
38-
func suppressIssueUsingTransformer() async throws {
37+
@Test("Suppressing an issue by returning `nil` from the closure passed to compactMapIssues()")
38+
func suppressIssueUsingCompactMapIssues() async throws {
3939
var configuration = Configuration()
4040
configuration.eventHandler = { event, context in
4141
if case .issueRecorded = event.kind {
4242
Issue.record("Unexpected issue recorded event: \(event)")
4343
}
4444
}
4545

46-
let handler = IssueHandlingTrait.transformIssues { _ in
46+
let handler = IssueHandlingTrait.compactMapIssues { _ in
4747
// Return nil to suppress the issue.
4848
nil
4949
}
@@ -81,10 +81,10 @@ struct IssueHandlingTraitTests {
8181

8282
struct MyError: Error {}
8383

84-
try await confirmation("Transformer closure is called") { transformerCalled in
85-
let transformer: @Sendable (Issue) -> Issue? = { issue in
84+
try await confirmation("Issue handler closure is called") { issueHandlerCalled in
85+
let transform: @Sendable (Issue) -> Issue? = { issue in
8686
defer {
87-
transformerCalled()
87+
issueHandlerCalled()
8888
}
8989

9090
#expect(Test.Case.current == nil)
@@ -96,7 +96,7 @@ struct IssueHandlingTraitTests {
9696

9797
let test = Test(
9898
.enabled(if: try { throw MyError() }()),
99-
.transformIssues(transformer)
99+
.compactMapIssues(transform)
100100
) {}
101101

102102
// Use a detached task to intentionally clear task local values for the
@@ -108,12 +108,12 @@ struct IssueHandlingTraitTests {
108108
}
109109
#endif
110110

111-
@Test("Accessing the current Test and Test.Case from a transformer closure")
111+
@Test("Accessing the current Test and Test.Case from an issue handler closure")
112112
func currentTestAndCase() async throws {
113-
await confirmation("Transformer closure is called") { transformerCalled in
114-
let handler = IssueHandlingTrait.transformIssues { issue in
113+
await confirmation("Issue handler closure is called") { issueHandlerCalled in
114+
let handler = IssueHandlingTrait.compactMapIssues { issue in
115115
defer {
116-
transformerCalled()
116+
issueHandlerCalled()
117117
}
118118
#expect(Test.current?.name == "fixture()")
119119
#expect(Test.Case.current != nil)
@@ -140,12 +140,12 @@ struct IssueHandlingTraitTests {
140140
#expect(issue.comments == ["Foo", "Bar", "Baz"])
141141
}
142142

143-
let outerHandler = IssueHandlingTrait.transformIssues { issue in
143+
let outerHandler = IssueHandlingTrait.compactMapIssues { issue in
144144
var issue = issue
145145
issue.comments.append("Baz")
146146
return issue
147147
}
148-
let innerHandler = IssueHandlingTrait.transformIssues { issue in
148+
let innerHandler = IssueHandlingTrait.compactMapIssues { issue in
149149
var issue = issue
150150
issue.comments.append("Bar")
151151
return issue
@@ -156,7 +156,7 @@ struct IssueHandlingTraitTests {
156156
}.run(configuration: configuration)
157157
}
158158

159-
@Test("Secondary issue recorded from a transformer closure")
159+
@Test("Secondary issue recorded from an issue handler closure")
160160
func issueRecordedFromClosure() async throws {
161161
await confirmation("Original issue recorded") { originalIssueRecorded in
162162
await confirmation("Secondary issue recorded") { secondaryIssueRecorded in
@@ -175,14 +175,14 @@ struct IssueHandlingTraitTests {
175175
}
176176
}
177177

178-
let handler1 = IssueHandlingTrait.transformIssues { issue in
178+
let handler1 = IssueHandlingTrait.compactMapIssues { issue in
179179
return issue
180180
}
181-
let handler2 = IssueHandlingTrait.transformIssues { issue in
181+
let handler2 = IssueHandlingTrait.compactMapIssues { issue in
182182
Issue.record("Something else")
183183
return issue
184184
}
185-
let handler3 = IssueHandlingTrait.transformIssues { issue in
185+
let handler3 = IssueHandlingTrait.compactMapIssues { issue in
186186
// The "Something else" issue should not be passed to this closure.
187187
#expect(issue.comments.contains("Foo"))
188188
return issue
@@ -194,4 +194,40 @@ struct IssueHandlingTraitTests {
194194
}
195195
}
196196
}
197+
198+
@Test("System issues are not passed to issue handler closures")
199+
func ignoresSystemIssues() async throws {
200+
var configuration = Configuration()
201+
configuration.eventHandler = { event, context in
202+
if case let .issueRecorded(issue) = event.kind, case .unconditional = issue.kind {
203+
issue.record()
204+
}
205+
}
206+
207+
let handler = IssueHandlingTrait.compactMapIssues { issue in
208+
if case .system = issue.kind {
209+
Issue.record("Unexpectedly received a system issue")
210+
}
211+
return nil
212+
}
213+
214+
await Test(handler) {
215+
Issue(kind: .system).record()
216+
}.run(configuration: configuration)
217+
}
218+
219+
#if !SWT_NO_EXIT_TESTS
220+
@Test("Disallow assigning kind to .system")
221+
func disallowAssigningSystemKind() async throws {
222+
await #expect(processExitsWith: .failure) {
223+
await Test(.compactMapIssues { issue in
224+
var issue = issue
225+
issue.kind = .system
226+
return issue
227+
}) {
228+
Issue.record("A non-system issue")
229+
}.run()
230+
}
231+
}
232+
#endif
197233
}

0 commit comments

Comments
 (0)