From fa260871512d2d6171e38227efefaf6f8768c07f Mon Sep 17 00:00:00 2001 From: Stuart Montgomery Date: Mon, 8 Dec 2025 11:27:45 -0600 Subject: [PATCH 1/2] Ignore ExpectationFailedError when thrown by the body closure passed to withKnownIssue Fixes rdar://153550847 --- Sources/Testing/Issues/KnownIssue.swift | 25 ++++++++++++++++++++---- Tests/TestingTests/KnownIssueTests.swift | 21 ++++++++++++++++++++ 2 files changed, 42 insertions(+), 4 deletions(-) diff --git a/Sources/Testing/Issues/KnownIssue.swift b/Sources/Testing/Issues/KnownIssue.swift index f59185388..1f060c5bf 100644 --- a/Sources/Testing/Issues/KnownIssue.swift +++ b/Sources/Testing/Issues/KnownIssue.swift @@ -83,6 +83,11 @@ private func _matchError(_ error: any Error, in scope: KnownIssueScope, comment: // It's a known issue, so mark it as such before recording it. issue.knownIssueContext = context issue.record() + } else if error 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 rethrow this error and let another issue be recorded + // since that would be redundant. } else { // Rethrow the error, allowing the caller to catch it or for it to propagate // to the runner to record it as an issue. @@ -120,6 +125,18 @@ private func _handleMiscount(by matchCounter: Locked, comment: Comment?, so /// - Returns: Whether or not `issue` is known to occur. public typealias KnownIssueMatcher = @Sendable (_ issue: Issue) -> Bool +// TODO: Document +/// The default known issue matcher, which matches all issues except those +@usableFromInline +var defaultKnownIssueMatcher: KnownIssueMatcher { + { issue in + if case .errorCaught(_ as ExpectationFailedError) = issue.kind { + return false + } + return true + } +} + /// Invoke a function that has a known issue that is expected to occur during /// its execution. /// @@ -158,7 +175,7 @@ public func withKnownIssue( sourceLocation: SourceLocation = #_sourceLocation, _ body: () throws -> Void ) { - try? withKnownIssue(comment, isIntermittent: isIntermittent, sourceLocation: sourceLocation, body, matching: { _ in true }) + try? withKnownIssue(comment, isIntermittent: isIntermittent, sourceLocation: sourceLocation, body, matching: defaultKnownIssueMatcher) } /// Invoke a function that has a known issue that is expected to occur during @@ -215,7 +232,7 @@ public func withKnownIssue( sourceLocation: SourceLocation = #_sourceLocation, _ body: () throws -> Void, when precondition: () -> Bool = { true }, - matching issueMatcher: @escaping KnownIssueMatcher = { _ in true } + matching issueMatcher: @escaping KnownIssueMatcher = defaultKnownIssueMatcher ) rethrows { guard precondition() else { return try body() @@ -275,7 +292,7 @@ public func withKnownIssue( sourceLocation: SourceLocation = #_sourceLocation, _ body: () async throws -> Void ) async { - try? await withKnownIssue(comment, isIntermittent: isIntermittent, isolation: isolation, sourceLocation: sourceLocation, body, matching: { _ in true }) + try? await withKnownIssue(comment, isIntermittent: isIntermittent, isolation: isolation, sourceLocation: sourceLocation, body, matching: defaultKnownIssueMatcher) } /// Invoke a function that has a known issue that is expected to occur during @@ -334,7 +351,7 @@ public func withKnownIssue( sourceLocation: SourceLocation = #_sourceLocation, _ body: () async throws -> Void, when precondition: () async -> Bool = { true }, - matching issueMatcher: @escaping KnownIssueMatcher = { _ in true } + matching issueMatcher: @escaping KnownIssueMatcher = defaultKnownIssueMatcher ) async rethrows { guard await precondition() else { return try await body() diff --git a/Tests/TestingTests/KnownIssueTests.swift b/Tests/TestingTests/KnownIssueTests.swift index 9174bfdd8..d316f83e1 100644 --- a/Tests/TestingTests/KnownIssueTests.swift +++ b/Tests/TestingTests/KnownIssueTests.swift @@ -615,3 +615,24 @@ func mainActorIsolatedKnownIssue() async { }.run(configuration: .init()) } #endif + +@Test func `withKnownIssue containing a #require failure`() { + withKnownIssue { + try #require(Bool(false)) + } +} + +@Test func `withKnownIssue w/custom matcher containing a #require failure`() throws { + try withKnownIssue { + try #require(Bool(false)) + } matching: { issue in + return switch issue.kind { + case .expectationFailed: + true + case .errorCaught(_ as ExpectationFailedError): + true + default: + false + } + } +} From 41d11a0795c628bc95d09ee55afc3ee6ad681e2c Mon Sep 17 00:00:00 2001 From: Stuart Montgomery Date: Mon, 8 Dec 2025 12:07:04 -0600 Subject: [PATCH 2/2] Simplify the apprpach: Unconditionally skip creating an issue for errors of type ExpectationFailedError. And add a proper test --- Sources/Testing/Issues/KnownIssue.swift | 32 +++++++----------- Tests/TestingTests/KnownIssueTests.swift | 41 ++++++++++++------------ 2 files changed, 31 insertions(+), 42 deletions(-) diff --git a/Sources/Testing/Issues/KnownIssue.swift b/Sources/Testing/Issues/KnownIssue.swift index 1f060c5bf..17214c12c 100644 --- a/Sources/Testing/Issues/KnownIssue.swift +++ b/Sources/Testing/Issues/KnownIssue.swift @@ -77,17 +77,19 @@ struct KnownIssueScope: Sendable { /// - sourceLocation: The source location to which the issue should be /// attributed. private func _matchError(_ error: any Error, in scope: KnownIssueScope, comment: Comment?, sourceLocation: SourceLocation) throws { + // ExpectationFailedError 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 create a new issue and attempt to match it. + if error is ExpectationFailedError { + return + } + let sourceContext = SourceContext(backtrace: Backtrace(forFirstThrowOf: error), sourceLocation: sourceLocation) var issue = Issue(kind: .errorCaught(error), comments: [], sourceContext: sourceContext) if let context = scope.matcher(issue) { // It's a known issue, so mark it as such before recording it. issue.knownIssueContext = context issue.record() - } else if error 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 rethrow this error and let another issue be recorded - // since that would be redundant. } else { // Rethrow the error, allowing the caller to catch it or for it to propagate // to the runner to record it as an issue. @@ -125,18 +127,6 @@ private func _handleMiscount(by matchCounter: Locked, comment: Comment?, so /// - Returns: Whether or not `issue` is known to occur. public typealias KnownIssueMatcher = @Sendable (_ issue: Issue) -> Bool -// TODO: Document -/// The default known issue matcher, which matches all issues except those -@usableFromInline -var defaultKnownIssueMatcher: KnownIssueMatcher { - { issue in - if case .errorCaught(_ as ExpectationFailedError) = issue.kind { - return false - } - return true - } -} - /// Invoke a function that has a known issue that is expected to occur during /// its execution. /// @@ -175,7 +165,7 @@ public func withKnownIssue( sourceLocation: SourceLocation = #_sourceLocation, _ body: () throws -> Void ) { - try? withKnownIssue(comment, isIntermittent: isIntermittent, sourceLocation: sourceLocation, body, matching: defaultKnownIssueMatcher) + try? withKnownIssue(comment, isIntermittent: isIntermittent, sourceLocation: sourceLocation, body, matching: { _ in true }) } /// Invoke a function that has a known issue that is expected to occur during @@ -232,7 +222,7 @@ public func withKnownIssue( sourceLocation: SourceLocation = #_sourceLocation, _ body: () throws -> Void, when precondition: () -> Bool = { true }, - matching issueMatcher: @escaping KnownIssueMatcher = defaultKnownIssueMatcher + matching issueMatcher: @escaping KnownIssueMatcher = { _ in true } ) rethrows { guard precondition() else { return try body() @@ -292,7 +282,7 @@ public func withKnownIssue( sourceLocation: SourceLocation = #_sourceLocation, _ body: () async throws -> Void ) async { - try? await withKnownIssue(comment, isIntermittent: isIntermittent, isolation: isolation, sourceLocation: sourceLocation, body, matching: defaultKnownIssueMatcher) + try? await withKnownIssue(comment, isIntermittent: isIntermittent, isolation: isolation, sourceLocation: sourceLocation, body, matching: { _ in true }) } /// Invoke a function that has a known issue that is expected to occur during @@ -351,7 +341,7 @@ public func withKnownIssue( sourceLocation: SourceLocation = #_sourceLocation, _ body: () async throws -> Void, when precondition: () async -> Bool = { true }, - matching issueMatcher: @escaping KnownIssueMatcher = defaultKnownIssueMatcher + matching issueMatcher: @escaping KnownIssueMatcher = { _ in true } ) async rethrows { guard await precondition() else { return try await body() diff --git a/Tests/TestingTests/KnownIssueTests.swift b/Tests/TestingTests/KnownIssueTests.swift index d316f83e1..88deef5ec 100644 --- a/Tests/TestingTests/KnownIssueTests.swift +++ b/Tests/TestingTests/KnownIssueTests.swift @@ -574,6 +574,26 @@ final class KnownIssueTests: XCTestCase { await fulfillment(of: [issueRecorded, knownIssueNotRecorded], timeout: 0.0) } + func testKnownIssueWithRequiredExpectationFailure() async { + let issueRecorded = expectation(description: "Issue recorded") + + var configuration = Configuration() + configuration.eventHandler = { event, _ in + guard case .issueRecorded = event.kind else { + return + } + issueRecorded.fulfill() + } + + await Test { + withKnownIssue { + try #require(Bool(false)) + } + }.run(configuration: configuration) + + await fulfillment(of: [issueRecorded], timeout: 0.0) + } + func testAsyncKnownIssueThatDoesNotAlwaysOccur() async { struct MyError: Error {} @@ -615,24 +635,3 @@ func mainActorIsolatedKnownIssue() async { }.run(configuration: .init()) } #endif - -@Test func `withKnownIssue containing a #require failure`() { - withKnownIssue { - try #require(Bool(false)) - } -} - -@Test func `withKnownIssue w/custom matcher containing a #require failure`() throws { - try withKnownIssue { - try #require(Bool(false)) - } matching: { issue in - return switch issue.kind { - case .expectationFailed: - true - case .errorCaught(_ as ExpectationFailedError): - true - default: - false - } - } -}