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 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276
|
//
// 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
//
extension Issue {
/// The known issue matcher, as set by `withKnownIssue()`, associated with the
/// current task.
///
/// If there is no call to `withKnownIssue()` executing on the current task,
/// the value of this property is `nil`.
@TaskLocal
static var currentKnownIssueMatcher: KnownIssueMatcher?
/// Record a new issue with the specified properties.
///
/// - Parameters:
/// - kind: The kind of issue.
/// - comments: An array of comments describing the issue. This array may be
/// empty.
/// - backtrace: The backtrace of the issue, if available. This value is
/// used to construct an instance of ``SourceContext``.
/// - sourceLocation: The source location of the issue. This value is used
/// to construct an instance of ``SourceContext``.
/// - configuration: The test configuration to use when recording the issue.
/// The default value is ``Configuration/current``.
///
/// - Returns: The issue that was recorded.
@discardableResult
static func record(_ kind: Kind, comments: [Comment], backtrace: Backtrace?, sourceLocation: SourceLocation, configuration: Configuration? = nil) -> Self {
let sourceContext = SourceContext(backtrace: backtrace, sourceLocation: sourceLocation)
return record(kind, comments: comments, sourceContext: sourceContext, configuration: configuration)
}
/// Record a new issue with the specified properties.
///
/// - Parameters:
/// - kind: The kind of issue.
/// - comments: An array of comments describing the issue. This array may be
/// empty.
/// - sourceContext: The source context of the issue.
/// - configuration: The test configuration to use when recording the issue.
/// The default value is ``Configuration/current``.
///
/// - Returns: The issue that was recorded.
@discardableResult
static func record(_ kind: Kind, comments: [Comment], sourceContext: SourceContext, configuration: Configuration? = nil) -> Self {
let issue = Issue(kind: kind, comments: comments, sourceContext: sourceContext)
return issue.record(configuration: configuration)
}
/// Record this issue by wrapping it in an ``Event`` and passing it to the
/// current event handler.
///
/// - Parameters:
/// - configuration: The test configuration to use when recording the issue.
/// The default value is ``Configuration/current``.
///
/// - Returns: The issue that was recorded (`self` or a modified copy of it.)
@discardableResult
func record(configuration: Configuration? = nil) -> Self {
// If this issue is a caught error of kind SystemError, reinterpret it as a
// testing system issue instead (per the documentation for SystemError.)
if case let .errorCaught(error) = kind, let error = error as? SystemError {
var selfCopy = self
selfCopy.kind = .system
selfCopy.comments.append(Comment(rawValue: String(describing: error)))
return selfCopy.record(configuration: configuration)
}
// If this issue matches via the known issue matcher, set a copy of it to be
// known and record the copy instead.
if !isKnown, let issueMatcher = Self.currentKnownIssueMatcher, issueMatcher(self) {
var selfCopy = self
selfCopy.isKnown = true
return selfCopy.record(configuration: configuration)
}
Event.post(.issueRecorded(self), configuration: configuration)
if !isKnown {
// Since this is not a known issue, invoke the failure breakpoint.
//
// Do this after posting the event above, to allow the issue to be printed
// to the console first (assuming the event handler does this), since that
// can help explain the failure.
failureBreakpoint()
}
return self
}
/// Record an issue when a running test fails unexpectedly.
///
/// - Parameters:
/// - comment: A comment describing the expectation.
/// - sourceLocation: The source location to which the issue should be
/// attributed.
///
/// - Returns: The issue that was recorded.
///
/// 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.)
@discardableResult public static func record(
_ comment: Comment? = nil,
sourceLocation: SourceLocation = #_sourceLocation
) -> Self {
let sourceContext = SourceContext(backtrace: .current(), sourceLocation: sourceLocation)
let issue = Issue(kind: .unconditional, comments: Array(comment), sourceContext: sourceContext)
return issue.record()
}
}
// MARK: - Recording issues for errors
extension Issue {
/// Record a new issue when a running test unexpectedly catches an error.
///
/// - Parameters:
/// - error: The error that caused the issue.
/// - comment: A comment describing the expectation.
/// - sourceLocation: The source location to which the issue should be
/// attributed.
///
/// - Returns: The issue that was recorded.
///
/// This function can be used if an unexpected error is caught while running a
/// test and it should be treated as a test failure. If an error is thrown
/// from a test function, it is automatically recorded as an issue and this
/// function does not need to be used.
@discardableResult public static func record(
_ error: any Error,
_ comment: Comment? = nil,
sourceLocation: SourceLocation = #_sourceLocation
) -> Self {
let backtrace = Backtrace(forFirstThrowOf: error) ?? Backtrace.current()
let sourceContext = SourceContext(backtrace: backtrace, sourceLocation: sourceLocation)
let issue = Issue(kind: .errorCaught(error), comments: Array(comment), sourceContext: sourceContext)
return issue.record()
}
/// Catch any error thrown from a closure and record it as an issue instead of
/// allowing it to propagate to the caller.
///
/// - Parameters:
/// - sourceLocation: The source location to attribute any caught error to.
/// - configuration: The test configuration to use when recording an issue.
/// The default value is ``Configuration/current``.
/// - body: A closure that might throw an error.
///
/// - Returns: The issue representing the caught error, if any error was
/// caught, otherwise `nil`.
@discardableResult
static func withErrorRecording(
at sourceLocation: SourceLocation,
configuration: Configuration? = nil,
_ body: () throws -> Void
) -> (any Error)? {
// Ensure that we are capturing backtraces for errors before we start
// expecting to see them.
Backtrace.startCachingForThrownErrors()
defer {
Backtrace.flushThrownErrorCache()
}
do {
try body()
} catch is ExpectationFailedError {
// 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 {
Issue.record(
.errorCaught(error),
comments: [],
backtrace: Backtrace(forFirstThrowOf: error),
sourceLocation: sourceLocation,
configuration: configuration
)
return error
}
return nil
}
/// Catch any error thrown from an asynchronous closure and record it as an
/// issue instead of allowing it to propagate to the caller.
///
/// - Parameters:
/// - sourceLocation: The source location to attribute any caught error to.
/// - 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.
/// - body: An asynchronous closure that might throw an error.
///
/// - Returns: The issue representing the caught error, if any error was
/// caught, otherwise `nil`.
@discardableResult
static func withErrorRecording(
at sourceLocation: SourceLocation,
configuration: Configuration? = nil,
isolation: isolated (any Actor)? = #isolation,
_ body: () async throws -> Void
) async -> (any Error)? {
// Ensure that we are capturing backtraces for errors before we start
// expecting to see them.
Backtrace.startCachingForThrownErrors()
defer {
Backtrace.flushThrownErrorCache()
}
do {
try await body()
} catch is ExpectationFailedError {
// 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 {
Issue.record(
.errorCaught(error),
comments: [],
backtrace: Backtrace(forFirstThrowOf: error),
sourceLocation: sourceLocation,
configuration: configuration
)
return error
}
return nil
}
}
// MARK: - Debugging failures
/// A unique value used by ``failureBreakpoint()``.
@usableFromInline nonisolated(unsafe) var failureBreakpointValue = 0
/// A function called by the testing library when a failure occurs.
///
/// Whenever a test failure (specifically, a non-known ``Issue``) is recorded,
/// the testing library calls this function synchronously. This facilitates
/// interactive debugging of test failures: If you add a symbolic breakpoint
/// specifying the name of this function, the debugger will pause execution and
/// allow you to inspect the process state.
///
/// When creating a symbolic breakpoint for this function, it is recommended
/// that you constrain it to the `Testing` module to avoid collisions with
/// similarly-named functions in other modules. If you are using LLDB, you can
/// use the following command to create the breakpoint:
///
/// ```lldb
/// (lldb) breakpoint set -s Testing -n "failureBreakpoint()"
/// ```
///
/// This function performs no action of its own. It is not part of the public
/// interface of the testing library, but it is exported and its symbol name
/// must remain stable.
@inline(never) @_optimize(none)
@usableFromInline
func failureBreakpoint() {
// This function's body cannot be completely empty or else linker symbol
// de-duplication will cause its symbol to be consolidated with that of
// another, arbitrarily chosen empty function in this module. This linker
// behavior can be disabled by passing the `-no_deduplicate` flag described in
// ld(1), but that would disable it module-wide and sacrifice optimization
// opportunities elsewhere. Instead, this function performs a trivial
// operation on a usable-from-inline value, which the compiler must assume
// cannot be optimized away.
failureBreakpointValue = 1
}
|