File: IssueHandlingTrait.swift

package info (click to toggle)
swiftlang 6.2.3-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 2,856,264 kB
  • sloc: cpp: 9,995,718; ansic: 2,234,019; asm: 1,092,167; python: 313,940; objc: 82,726; f90: 80,126; lisp: 38,373; pascal: 25,580; sh: 20,378; ml: 5,058; perl: 4,751; makefile: 4,725; awk: 3,535; javascript: 3,018; xml: 918; fortran: 664; cs: 573; ruby: 396
file content (233 lines) | stat: -rw-r--r-- 9,877 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
//
// 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/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/compactMapIssues(_:)``
/// - ``Trait/filterIssues(_:)``
///
/// @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.
  ///
  /// - Parameters:
  ///   - issue: The issue to handle.
  ///
  /// - Returns: An issue to replace `issue`, or else `nil` if the issue should
  ///   not be recorded.
  fileprivate typealias Handler = @Sendable (_ issue: Issue) -> Issue?

  /// This trait's handler function.
  private var _handler: Handler

  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.
  ///
  /// @Metadata {
  ///   @Available(Swift, introduced: 6.2)
  ///   @Available(Xcode, introduced: 26.0)
  /// }
  public func handleIssue(_ issue: Issue) -> Issue? {
    _handler(issue)
  }

  public var isRecursive: Bool {
    true
  }
}

/// @Metadata {
///   @Available(Swift, introduced: 6.2)
///   @Available(Xcode, introduced: 26.0)
/// }
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
      }

      // 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
      // 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) {
        handleIssue(issue)
      }

      if let newIssue {
        // 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
        event.kind = .issueRecorded(newIssue)
        oldConfiguration.eventHandler(event, context)
      }
    }

    try await Configuration.withCurrent(configuration, perform: function)
  }
}

extension Trait where Self == IssueHandlingTrait {
  /// Constructs an trait that transforms issues recorded by a test.
  ///
  /// - Parameters:
  ///   - 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 `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 `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 `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 `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.
  ///
  /// - 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)
  ///   @Available(Xcode, introduced: 26.0)
  /// }
  public static func compactMapIssues(_ transform: @escaping @Sendable (Issue) -> Issue?) -> Self {
    Self(handler: transform)
  }

  /// 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.
  ///
  /// - 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
  /// 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.
  ///
  /// - 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)
  ///   @Available(Xcode, introduced: 26.0)
  /// }
  public static func filterIssues(_ isIncluded: @escaping @Sendable (Issue) -> Bool) -> Self {
    Self { issue in
      isIncluded(issue) ? issue : nil
    }
  }
}