diff --git a/Sources/Testing/Issues/Issue+Recording.swift b/Sources/Testing/Issues/Issue+Recording.swift index aaf721c6a..bd3e9a3bb 100644 --- a/Sources/Testing/Issues/Issue+Recording.swift +++ b/Sources/Testing/Issues/Issue+Recording.swift @@ -73,9 +73,31 @@ extension Issue { @discardableResult public static func record( _ comment: Comment? = nil, sourceLocation: SourceLocation = #_sourceLocation + ) -> Self { + record(comment, severity: .error, sourceLocation: sourceLocation) + } + + /// Record an issue when a running test fails unexpectedly. + /// + /// - Parameters: + /// - comment: A comment describing the expectation. + /// - severity: The severity of the issue. + /// - 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.) + @_spi(Experimental) + @discardableResult public static func record( + _ comment: Comment? = nil, + severity: Severity, + sourceLocation: SourceLocation = #_sourceLocation ) -> Self { let sourceContext = SourceContext(backtrace: .current(), sourceLocation: sourceLocation) - let issue = Issue(kind: .unconditional, comments: Array(comment), sourceContext: sourceContext) + let issue = Issue(kind: .unconditional, severity: severity, comments: Array(comment), sourceContext: sourceContext) return issue.record() } } @@ -101,10 +123,35 @@ extension Issue { _ error: any Error, _ comment: Comment? = nil, sourceLocation: SourceLocation = #_sourceLocation + ) -> Self { + record(error, comment, severity: .error, sourceLocation: sourceLocation) + } + + /// 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. + /// - severity: The severity of the issue. + /// - 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. + @_spi(Experimental) + @discardableResult public static func record( + _ error: any Error, + _ comment: Comment? = nil, + severity: Severity, + 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) + let issue = Issue(kind: .errorCaught(error), severity: severity, comments: Array(comment), sourceContext: sourceContext) return issue.record() } diff --git a/Tests/TestingTests/IssueTests.swift b/Tests/TestingTests/IssueTests.swift index d22bf9fba..cb7ce28f3 100644 --- a/Tests/TestingTests/IssueTests.swift +++ b/Tests/TestingTests/IssueTests.swift @@ -1010,6 +1010,7 @@ final class IssueTests: XCTestCase { return } XCTAssertFalse(issue.isKnown) + XCTAssertEqual(issue.severity, .error) guard case .unconditional = issue.kind else { XCTFail("Unexpected issue kind \(issue.kind)") return @@ -1021,6 +1022,25 @@ final class IssueTests: XCTestCase { Issue.record("Custom message") }.run(configuration: configuration) } + + func testWarning() async throws { + var configuration = Configuration() + configuration.eventHandler = { event, _ in + guard case let .issueRecorded(issue) = event.kind else { + return + } + XCTAssertFalse(issue.isKnown) + XCTAssertEqual(issue.severity, .warning) + guard case .unconditional = issue.kind else { + XCTFail("Unexpected issue kind \(issue.kind)") + return + } + } + + await Test { + Issue.record("Custom message", severity: .warning) + }.run(configuration: configuration) + } #if !SWT_NO_UNSTRUCTURED_TASKS func testFailWithoutCurrentTest() async throws { @@ -1048,6 +1068,7 @@ final class IssueTests: XCTestCase { return } XCTAssertFalse(issue.isKnown) + XCTAssertEqual(issue.severity, .error) guard case let .errorCaught(error) = issue.kind else { XCTFail("Unexpected issue kind \(issue.kind)") return @@ -1060,6 +1081,27 @@ final class IssueTests: XCTestCase { Issue.record(MyError(), "Custom message") }.run(configuration: configuration) } + + func testWarningBecauseOfError() async throws { + var configuration = Configuration() + configuration.eventHandler = { event, _ in + guard case let .issueRecorded(issue) = event.kind else { + return + } + XCTAssertFalse(issue.isKnown) + XCTAssertEqual(issue.severity, .warning) + guard case let .errorCaught(error) = issue.kind else { + XCTFail("Unexpected issue kind \(issue.kind)") + return + } + XCTAssertTrue(error is MyError) + } + + await Test { + Issue.record(MyError(), severity: .warning) + Issue.record(MyError(), "Custom message", severity: .warning) + }.run(configuration: configuration) + } func testErrorPropertyValidForThrownErrors() async throws { var configuration = Configuration()