-
Notifications
You must be signed in to change notification settings - Fork 103
Add withKnownIssue comments to known Issues #1014
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
c6cbd3d
acdb283
08b1c3f
45f8d9f
e2da439
e0251d8
89b0a67
deffa29
e6e4fd4
a9b3163
1282cab
0dfcff8
7cd86f6
c0e8ce7
af08da2
b66a484
b9275e8
81fe877
9192e4a
feb96cf
874f830
6caad07
316d10c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -93,10 +93,20 @@ extension Event.HumanReadableOutputRecorder { | |
/// - Parameters: | ||
/// - comments: The comments that should be formatted. | ||
/// | ||
/// - Returns: A formatted string representing `comments`, or `nil` if there | ||
/// are none. | ||
/// - Returns: An array of formatted messages representing `comments`, or an | ||
/// empty array if there are none. | ||
private func _formattedComments(_ comments: [Comment]) -> [Message] { | ||
comments.map { Message(symbol: .details, stringValue: $0.rawValue) } | ||
comments.map(_formattedComment) | ||
} | ||
|
||
/// Get a string representing a single comment, formatted for output. | ||
/// | ||
/// - Parameters: | ||
/// - comment: The comment that should be formatted. | ||
/// | ||
/// - Returns: A formatted message representing `comment`. | ||
private func _formattedComment(_ comment: Comment) -> Message { | ||
Message(symbol: .details, stringValue: comment.rawValue) | ||
} | ||
|
||
/// Get a string representing the comments attached to a test, formatted for | ||
|
@@ -443,6 +453,15 @@ extension Event.HumanReadableOutputRecorder { | |
additionalMessages.append(Message(symbol: .difference, stringValue: differenceDescription)) | ||
} | ||
additionalMessages += _formattedComments(issue.comments) | ||
if let knownIssueComment = issue.knownIssueContext?.comment { | ||
if case .errorCaught = issue.kind { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you explain why this specific case is being singled out? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Pushed an updated comment, how does it look to you? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Seems like the correct fix might be to remove the comment from There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Note for compatibility, we would want to clear the withKnownIssue {
...
} matching: { issue in
issue.comments.first?.contains("abc") ?? false
} (Something like that.) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
We certainly could, but that has a downside that I mentioned in #1014 (comment):
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, but I'm trying not to introduce any regressions along the way. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I appreciate that :) And that's a good goal—but we also need to be mindful of creating technical debt for ourselves. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. OK, so do you think I should take approach 2 despite the regression in Xcode and the weirdness of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We discussed this and decided to remove the comment entirely from @Test func knownIssueCommentPassedToIssueMatcher() throws {
struct E: Error {}
try withKnownIssue("Known Issue Comment") {
throw E()
} matching: { issue in
print("Comments passed to matcher:", issue.comments)
// prints: Comments passed to matcher: ["Known Issue Comment"]
// which seems unhelpful since we can see the comment a few lines up in the withKnownIssue call
return true
}
}``` There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree we should stop including the comment passed to |
||
// `_matchError()` put the known issue comment in `issue.comments` | ||
// when it first created the issue, so it's already included in | ||
// `additionalMessages`. | ||
} else { | ||
additionalMessages.append(_formattedComment(knownIssueComment)) | ||
} | ||
} | ||
|
||
if verbosity > 0, case let .expectationFailed(expectation) = issue.kind { | ||
let expression = expectation.evaluatedExpression | ||
|
Original file line number | Diff line number | Diff line change | ||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -8,44 +8,83 @@ | |||||||||||||||
// See https://swift.org/CONTRIBUTORS.txt for Swift project authors | ||||||||||||||||
// | ||||||||||||||||
|
||||||||||||||||
/// Combine an instance of ``KnownIssueMatcher`` with any previously-set one. | ||||||||||||||||
/// | ||||||||||||||||
/// - Parameters: | ||||||||||||||||
/// - issueMatcher: A function to invoke when an issue occurs that is used to | ||||||||||||||||
/// determine if the issue is known to occur. | ||||||||||||||||
/// - matchCounter: The counter responsible for tracking the number of matches | ||||||||||||||||
/// found with `issueMatcher`. | ||||||||||||||||
/// | ||||||||||||||||
/// - Returns: A new instance of ``Configuration`` or `nil` if there was no | ||||||||||||||||
/// current configuration set. | ||||||||||||||||
private func _combineIssueMatcher(_ issueMatcher: @escaping KnownIssueMatcher, matchesCountedBy matchCounter: Locked<Int>) -> KnownIssueMatcher { | ||||||||||||||||
let oldIssueMatcher = Issue.currentKnownIssueMatcher | ||||||||||||||||
return { issue in | ||||||||||||||||
if issueMatcher(issue) || true == oldIssueMatcher?(issue) { | ||||||||||||||||
matchCounter.increment() | ||||||||||||||||
return true | ||||||||||||||||
/// A type that represents an active | ||||||||||||||||
/// ``withKnownIssue(_:isIntermittent:sourceLocation:_:when:matching:)`` | ||||||||||||||||
/// call and any parent calls. | ||||||||||||||||
/// | ||||||||||||||||
/// A stack of these is stored in `KnownIssueScope.current`. | ||||||||||||||||
struct KnownIssueScope: Sendable { | ||||||||||||||||
/// A function which determines if an issue matches a known issue scope or | ||||||||||||||||
/// any of its ancestor scopes. | ||||||||||||||||
/// | ||||||||||||||||
/// - Parameters: | ||||||||||||||||
/// - issue: The issue being matched. | ||||||||||||||||
/// - Returns: A known issue context containing information about the known | ||||||||||||||||
/// issue, if the issue is considered "known" by this known issue scope or any | ||||||||||||||||
/// ancestor scope, or `nil` otherwise. | ||||||||||||||||
Comment on lines
+22
to
+24
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||
typealias Matcher = @Sendable (Issue) -> Issue.KnownIssueContext? | ||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Explicitly declare
Suggested change
|
||||||||||||||||
|
||||||||||||||||
/// Determine if an issue is known to this scope or any of its ancestor | ||||||||||||||||
/// scopes. | ||||||||||||||||
/// | ||||||||||||||||
/// Returns `nil` if the issue is not known. | ||||||||||||||||
var matcher: @Sendable (Issue) -> Issue.KnownIssueContext? | ||||||||||||||||
|
||||||||||||||||
/// The number of issues this scope and its ancestors have matched. | ||||||||||||||||
let matchCounter: Locked<Int> | ||||||||||||||||
|
||||||||||||||||
/// Create a new ``KnownIssueScope`` by combining a new issue matcher with | ||||||||||||||||
/// any already-active scope. | ||||||||||||||||
/// | ||||||||||||||||
/// - Parameters: | ||||||||||||||||
/// - parent: The context that should be checked next if `issueMatcher` | ||||||||||||||||
/// fails to match an issue. | ||||||||||||||||
/// - issueMatcher: A function to invoke when an issue occurs that is used | ||||||||||||||||
/// to determine if the issue is known to occur. | ||||||||||||||||
/// - context: The context to be associated with issues matched by | ||||||||||||||||
/// `issueMatcher`. | ||||||||||||||||
/// - Returns: A new instance of ``KnownIssueScope``. | ||||||||||||||||
init(parent: KnownIssueScope?, issueMatcher: @escaping KnownIssueMatcher, context: Issue.KnownIssueContext) { | ||||||||||||||||
let matchCounter = Locked(rawValue: 0) | ||||||||||||||||
self.matchCounter = matchCounter | ||||||||||||||||
matcher = { issue in | ||||||||||||||||
let matchedContext = if issueMatcher(issue) { | ||||||||||||||||
context | ||||||||||||||||
} else { | ||||||||||||||||
parent?.matcher(issue) | ||||||||||||||||
} | ||||||||||||||||
if matchedContext != nil { | ||||||||||||||||
matchCounter.increment() | ||||||||||||||||
} | ||||||||||||||||
return matchedContext | ||||||||||||||||
} | ||||||||||||||||
return false | ||||||||||||||||
} | ||||||||||||||||
|
||||||||||||||||
/// The active known issue scope for the current task. | ||||||||||||||||
/// | ||||||||||||||||
/// If there is no call to | ||||||||||||||||
/// ``withKnownIssue(_:isIntermittent:sourceLocation:_:when:matching:)`` | ||||||||||||||||
/// executing on the current task, the value of this property is `nil`. | ||||||||||||||||
@TaskLocal | ||||||||||||||||
static var current: KnownIssueScope? | ||||||||||||||||
} | ||||||||||||||||
|
||||||||||||||||
/// Check if an error matches using an issue-matching function, and throw it if | ||||||||||||||||
/// it does not. | ||||||||||||||||
/// | ||||||||||||||||
/// - Parameters: | ||||||||||||||||
/// - error: The error to test. | ||||||||||||||||
/// - issueMatcher: A function to which `error` is passed (after boxing it in | ||||||||||||||||
/// an instance of ``Issue``) to determine if it is known to occur. | ||||||||||||||||
/// - scope: The known issue scope that is processing the error. | ||||||||||||||||
/// - comment: An optional comment to apply to any issues generated by this | ||||||||||||||||
/// function. | ||||||||||||||||
/// - sourceLocation: The source location to which the issue should be | ||||||||||||||||
/// attributed. | ||||||||||||||||
private func _matchError(_ error: any Error, using issueMatcher: KnownIssueMatcher, comment: Comment?, sourceLocation: SourceLocation) throws { | ||||||||||||||||
private func _matchError(_ error: any Error, using scope: KnownIssueScope, comment: Comment?, sourceLocation: SourceLocation) throws { | ||||||||||||||||
aroben marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||
let sourceContext = SourceContext(backtrace: Backtrace(forFirstThrowOf: error), sourceLocation: sourceLocation) | ||||||||||||||||
var issue = Issue(kind: .errorCaught(error), comments: Array(comment), sourceContext: sourceContext) | ||||||||||||||||
if issueMatcher(issue) { | ||||||||||||||||
if let context = scope.matcher(issue) { | ||||||||||||||||
// It's a known issue, so mark it as such before recording it. | ||||||||||||||||
issue.isKnown = true | ||||||||||||||||
issue.knownIssueContext = context | ||||||||||||||||
issue.record() | ||||||||||||||||
} else { | ||||||||||||||||
// Rethrow the error, allowing the caller to catch it or for it to propagate | ||||||||||||||||
|
@@ -184,18 +223,17 @@ public func withKnownIssue( | |||||||||||||||
guard precondition() else { | ||||||||||||||||
return try body() | ||||||||||||||||
} | ||||||||||||||||
let matchCounter = Locked(rawValue: 0) | ||||||||||||||||
let issueMatcher = _combineIssueMatcher(issueMatcher, matchesCountedBy: matchCounter) | ||||||||||||||||
let scope = KnownIssueScope(parent: .current, issueMatcher: issueMatcher, context: Issue.KnownIssueContext(comment: comment)) | ||||||||||||||||
defer { | ||||||||||||||||
if !isIntermittent { | ||||||||||||||||
_handleMiscount(by: matchCounter, comment: comment, sourceLocation: sourceLocation) | ||||||||||||||||
_handleMiscount(by: scope.matchCounter, comment: comment, sourceLocation: sourceLocation) | ||||||||||||||||
} | ||||||||||||||||
} | ||||||||||||||||
try Issue.$currentKnownIssueMatcher.withValue(issueMatcher) { | ||||||||||||||||
try KnownIssueScope.$current.withValue(scope) { | ||||||||||||||||
do { | ||||||||||||||||
try body() | ||||||||||||||||
} catch { | ||||||||||||||||
try _matchError(error, using: issueMatcher, comment: comment, sourceLocation: sourceLocation) | ||||||||||||||||
try _matchError(error, using: scope, comment: comment, sourceLocation: sourceLocation) | ||||||||||||||||
} | ||||||||||||||||
} | ||||||||||||||||
} | ||||||||||||||||
|
@@ -304,18 +342,17 @@ public func withKnownIssue( | |||||||||||||||
guard await precondition() else { | ||||||||||||||||
return try await body() | ||||||||||||||||
} | ||||||||||||||||
let matchCounter = Locked(rawValue: 0) | ||||||||||||||||
let issueMatcher = _combineIssueMatcher(issueMatcher, matchesCountedBy: matchCounter) | ||||||||||||||||
let scope = KnownIssueScope(parent: .current, issueMatcher: issueMatcher, context: Issue.KnownIssueContext(comment: comment)) | ||||||||||||||||
defer { | ||||||||||||||||
if !isIntermittent { | ||||||||||||||||
_handleMiscount(by: matchCounter, comment: comment, sourceLocation: sourceLocation) | ||||||||||||||||
_handleMiscount(by: scope.matchCounter, comment: comment, sourceLocation: sourceLocation) | ||||||||||||||||
} | ||||||||||||||||
} | ||||||||||||||||
try await Issue.$currentKnownIssueMatcher.withValue(issueMatcher) { | ||||||||||||||||
try await KnownIssueScope.$current.withValue(scope) { | ||||||||||||||||
do { | ||||||||||||||||
try await body() | ||||||||||||||||
} catch { | ||||||||||||||||
try _matchError(error, using: issueMatcher, comment: comment, sourceLocation: sourceLocation) | ||||||||||||||||
try _matchError(error, using: scope, comment: comment, sourceLocation: sourceLocation) | ||||||||||||||||
} | ||||||||||||||||
} | ||||||||||||||||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.