Skip to content

Commit bd7e999

Browse files
committed
Introduce a severity level for issues, and a 'warning' severity
1 parent 6df23a4 commit bd7e999

File tree

6 files changed

+126
-34
lines changed

6 files changed

+126
-34
lines changed

Sources/Testing/ABI/EntryPoints/EntryPoint.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ func entryPoint(passing args: __CommandLineArguments_v0?, eventHandler: Event.Ha
4040

4141
// Set up the event handler.
4242
configuration.eventHandler = { [oldEventHandler = configuration.eventHandler] event, context in
43-
if case let .issueRecorded(issue) = event.kind, !issue.isKnown {
43+
if case let .issueRecorded(issue) = event.kind, !issue.isKnown, issue.severity >= .error {
4444
exitCode.withLock { exitCode in
4545
exitCode = EXIT_FAILURE
4646
}

Sources/Testing/Events/Recorder/Event.ConsoleOutputRecorder.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -354,7 +354,7 @@ extension Event.ConsoleOutputRecorder {
354354
///
355355
/// The caller is responsible for presenting this message to the user.
356356
static func warning(_ message: String, options: Event.ConsoleOutputRecorder.Options) -> String {
357-
let symbol = Event.Symbol.warning.stringValue(options: options)
357+
let symbol = Event.Symbol.warning().stringValue(options: options)
358358
return "\(symbol) \(message)\n"
359359
}
360360
}

Sources/Testing/Events/Recorder/Event.HumanReadableOutputRecorder.swift

Lines changed: 59 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,11 @@ extension Event {
5656
/// The instant at which the test started.
5757
var startInstant: Test.Clock.Instant
5858

59-
/// The number of issues recorded for the test.
60-
var issueCount = 0
59+
/// The number of issues with error severity recorded for the test.
60+
var errorIssueCount = 0
61+
62+
/// The number of issues with warning severity recorded for the test.
63+
var warningIssueCount = 0
6164

6265
/// The number of known issues recorded for the test.
6366
var knownIssueCount = 0
@@ -114,27 +117,36 @@ extension Event.HumanReadableOutputRecorder {
114117
/// - graph: The graph to walk while counting issues.
115118
///
116119
/// - Returns: A tuple containing the number of issues recorded in `graph`.
117-
private func _issueCounts(in graph: Graph<String, Event.HumanReadableOutputRecorder._Context.TestData?>?) -> (issueCount: Int, knownIssueCount: Int, totalIssueCount: Int, description: String) {
120+
private func _issueCounts(in graph: Graph<String, Event.HumanReadableOutputRecorder._Context.TestData?>?) -> (errorIssueCount: Int, warningIssueCount: Int, knownIssueCount: Int, totalIssueCount: Int, description: String) {
118121
guard let graph else {
119-
return (0, 0, 0, "")
122+
return (0, 0, 0, 0, "")
120123
}
121-
let issueCount = graph.compactMap(\.value?.issueCount).reduce(into: 0, +=)
124+
let errorIssueCount = graph.compactMap(\.value?.errorIssueCount).reduce(into: 0, +=)
125+
let warningIssueCount = graph.compactMap(\.value?.warningIssueCount).reduce(into: 0, +=)
122126
let knownIssueCount = graph.compactMap(\.value?.knownIssueCount).reduce(into: 0, +=)
123-
let totalIssueCount = issueCount + knownIssueCount
127+
let totalIssueCount = errorIssueCount + warningIssueCount + knownIssueCount
124128

125129
// Construct a string describing the issue counts.
126-
let description = switch (issueCount > 0, knownIssueCount > 0) {
127-
case (true, true):
130+
let description = switch (errorIssueCount > 0, warningIssueCount > 0, knownIssueCount > 0) {
131+
case (true, true, true):
132+
" with \(totalIssueCount.counting("issue")) (including \(warningIssueCount.counting("warning")) and \(knownIssueCount.counting("known issue")))"
133+
case (true, false, true):
128134
" with \(totalIssueCount.counting("issue")) (including \(knownIssueCount.counting("known issue")))"
129-
case (false, true):
135+
case (false, true, true):
136+
" with \(warningIssueCount.counting("warning")) and \(knownIssueCount.counting("known issue"))"
137+
case (false, false, true):
130138
" with \(knownIssueCount.counting("known issue"))"
131-
case (true, false):
139+
case (true, true, false):
140+
" with \(totalIssueCount.counting("issue")) (including \(warningIssueCount.counting("warning")))"
141+
case (true, false, false):
132142
" with \(totalIssueCount.counting("issue"))"
133-
case(false, false):
143+
case(false, true, false):
144+
" with \(warningIssueCount.counting("warning"))"
145+
case(false, false, false):
134146
""
135147
}
136148

137-
return (issueCount, knownIssueCount, totalIssueCount, description)
149+
return (errorIssueCount, warningIssueCount, knownIssueCount, totalIssueCount, description)
138150
}
139151
}
140152

@@ -267,7 +279,12 @@ extension Event.HumanReadableOutputRecorder {
267279
if issue.isKnown {
268280
testData.knownIssueCount += 1
269281
} else {
270-
testData.issueCount += 1
282+
switch issue.severity {
283+
case .warning:
284+
testData.warningIssueCount += 1
285+
case .error:
286+
testData.errorIssueCount += 1
287+
}
271288
}
272289
context.testData[id] = testData
273290

@@ -355,15 +372,22 @@ extension Event.HumanReadableOutputRecorder {
355372
let testData = testDataGraph?.value ?? .init(startInstant: instant)
356373
let issues = _issueCounts(in: testDataGraph)
357374
let duration = testData.startInstant.descriptionOfDuration(to: instant)
358-
return if issues.issueCount > 0 {
375+
return if issues.errorIssueCount > 0 {
359376
CollectionOfOne(
360377
Message(
361378
symbol: .fail,
362379
stringValue: "\(_capitalizedTitle(for: test)) \(testName) failed after \(duration)\(issues.description)."
363380
)
364381
) + _formattedComments(for: test)
382+
} else if issues.warningIssueCount > 0 {
383+
[
384+
Message(
385+
symbol: .warning(warningIssueCount: issues.warningIssueCount),
386+
stringValue: "\(_capitalizedTitle(for: test)) \(testName) passed after \(duration)\(issues.description)."
387+
)
388+
]
365389
} else {
366-
[
390+
[
367391
Message(
368392
symbol: .pass(knownIssueCount: issues.knownIssueCount),
369393
stringValue: "\(_capitalizedTitle(for: test)) \(testName) passed after \(duration)\(issues.description)."
@@ -400,13 +424,19 @@ extension Event.HumanReadableOutputRecorder {
400424
""
401425
}
402426
let symbol: Event.Symbol
403-
let known: String
427+
let introducer: String
404428
if issue.isKnown {
405429
symbol = .pass(knownIssueCount: 1)
406-
known = " known"
430+
introducer = "a known"
407431
} else {
408-
symbol = .fail
409-
known = "n"
432+
switch issue.severity {
433+
case .warning:
434+
symbol = .warning(warningIssueCount: 1)
435+
introducer = "a warning"
436+
case .error:
437+
symbol = .fail
438+
introducer = "an"
439+
}
410440
}
411441

412442
var additionalMessages = [Message]()
@@ -435,13 +465,13 @@ extension Event.HumanReadableOutputRecorder {
435465
let primaryMessage: Message = if parameterCount == 0 {
436466
Message(
437467
symbol: symbol,
438-
stringValue: "\(_capitalizedTitle(for: test)) \(testName) recorded a\(known) issue\(atSourceLocation): \(issue.kind)",
468+
stringValue: "\(_capitalizedTitle(for: test)) \(testName) recorded \(introducer) issue\(atSourceLocation): \(issue.kind)",
439469
conciseStringValue: String(describing: issue.kind)
440470
)
441471
} else {
442472
Message(
443473
symbol: symbol,
444-
stringValue: "\(_capitalizedTitle(for: test)) \(testName) recorded a\(known) issue with \(parameterCount.counting("argument")) \(labeledArguments)\(atSourceLocation): \(issue.kind)",
474+
stringValue: "\(_capitalizedTitle(for: test)) \(testName) recorded \(introducer) issue with \(parameterCount.counting("argument")) \(labeledArguments)\(atSourceLocation): \(issue.kind)",
445475
conciseStringValue: String(describing: issue.kind)
446476
)
447477
}
@@ -498,13 +528,20 @@ extension Event.HumanReadableOutputRecorder {
498528
let runStartInstant = context.runStartInstant ?? instant
499529
let duration = runStartInstant.descriptionOfDuration(to: instant)
500530

501-
return if issues.issueCount > 0 {
531+
return if issues.errorIssueCount > 0 {
502532
[
503533
Message(
504534
symbol: .fail,
505535
stringValue: "Test run with \(testCount.counting("test")) failed after \(duration)\(issues.description)."
506536
)
507537
]
538+
} else if issues.warningIssueCount > 0 {
539+
[
540+
Message(
541+
symbol: .warning(warningIssueCount: issues.warningIssueCount),
542+
stringValue: "Test run with \(testCount.counting("test")) passed after \(duration)\(issues.description)."
543+
)
544+
]
508545
} else {
509546
[
510547
Message(

Sources/Testing/Events/Recorder/Event.Symbol.swift

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ extension Event {
2222
/// The symbol to use when a test passes.
2323
///
2424
/// - Parameters:
25-
/// - knownIssueCount: The number of known issues encountered by the end
26-
/// of the test.
25+
/// - knownIssueCount: The number of known issues recorded for the test.
26+
/// The default value is `0`.
2727
case pass(knownIssueCount: Int = 0)
2828

2929
/// The symbol to use when a test fails.
@@ -34,7 +34,11 @@ extension Event {
3434

3535
/// A warning or caution symbol to use when the developer should be aware of
3636
/// some condition.
37-
case warning
37+
///
38+
/// - Parameters:
39+
/// - warningIssueCount: The number of issues with warning severity
40+
/// recorded for the test. The default value is `0`.
41+
case warning(warningIssueCount: Int = 0)
3842

3943
/// The symbol to use when presenting details about an event to the user.
4044
case details

Sources/Testing/Issues/Issue.swift

Lines changed: 55 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,35 @@ public struct Issue: Sendable {
7979
/// The kind of issue this value represents.
8080
public var kind: Kind
8181

82+
/// An enumeration representing the level of severity of a recorded issue.
83+
///
84+
/// The supported levels, in decreasing order of severity, are:
85+
///
86+
/// - ``error``
87+
/// - ``warning``
88+
@_spi(Experimental)
89+
public enum Severity: Sendable {
90+
/// The severity level representing an issue which may be a concern but is
91+
/// not an error.
92+
///
93+
/// An issue with warning severity does not cause the test it's associated
94+
/// with to be marked as a failure, but is noted in the results.
95+
case warning
96+
97+
/// The severity level representing an issue which represents an error in a
98+
/// test.
99+
///
100+
/// An issue with error severity causes the test it's associated with to be
101+
/// marked as a failure.
102+
case error
103+
}
104+
105+
/// The severity of this issue.
106+
///
107+
/// The default value of this property is ``Severity-swift.enum/error``.
108+
@_spi(Experimental)
109+
public var severity: Severity = .error
110+
82111
/// Any comments provided by the developer and associated with this issue.
83112
///
84113
/// If no comment was supplied when the issue occurred, the value of this
@@ -97,12 +126,15 @@ public struct Issue: Sendable {
97126
///
98127
/// - Parameters:
99128
/// - kind: The kind of issue this value represents.
129+
/// - severity: The severity of this issue. The default value is
130+
/// ``Severity-swift.enum/error``.
100131
/// - comments: An array of comments describing the issue. This array may be
101132
/// empty.
102133
/// - sourceContext: A ``SourceContext`` indicating where and how this issue
103134
/// occurred.
104135
init(
105136
kind: Kind,
137+
severity: Severity = .error,
106138
comments: [Comment],
107139
sourceContext: SourceContext
108140
) {
@@ -154,6 +186,8 @@ public struct Issue: Sendable {
154186
}
155187
}
156188

189+
extension Issue.Severity: Comparable {}
190+
157191
// MARK: - CustomStringConvertible, CustomDebugStringConvertible
158192

159193
extension Issue: CustomStringConvertible, CustomDebugStringConvertible {
@@ -164,7 +198,7 @@ extension Issue: CustomStringConvertible, CustomDebugStringConvertible {
164198
let joinedComments = comments.lazy
165199
.map(\.rawValue)
166200
.joined(separator: "\n")
167-
return "\(kind): \(joinedComments)"
201+
return "\(severity): \(kind): \(joinedComments)"
168202
}
169203

170204
public var debugDescription: String {
@@ -174,7 +208,7 @@ extension Issue: CustomStringConvertible, CustomDebugStringConvertible {
174208
let joinedComments: String = comments.lazy
175209
.map(\.rawValue)
176210
.joined(separator: "\n")
177-
return "\(kind)\(sourceLocation.map { " at \($0)" } ?? ""): \(joinedComments)"
211+
return "\(severity): \(kind)\(sourceLocation.map { " at \($0)" } ?? ""): \(joinedComments)"
178212
}
179213
}
180214

@@ -234,6 +268,17 @@ extension Issue.Kind: CustomStringConvertible {
234268
}
235269
}
236270

271+
extension Issue.Severity: CustomStringConvertible {
272+
public var description: String {
273+
switch self {
274+
case .warning:
275+
"warning"
276+
case .error:
277+
"error"
278+
}
279+
}
280+
}
281+
237282
#if !SWT_NO_SNAPSHOT_TYPES
238283
// MARK: - Snapshotting
239284

@@ -244,6 +289,9 @@ extension Issue {
244289
/// The kind of issue this value represents.
245290
public var kind: Kind.Snapshot
246291

292+
@_spi(Experimental)
293+
public var severity: Severity
294+
247295
/// Any comments provided by the developer and associated with this issue.
248296
///
249297
/// If no comment was supplied when the issue occurred, the value of this
@@ -268,6 +316,7 @@ extension Issue {
268316
self.kind = Issue.Kind.Snapshot(snapshotting: issue.kind)
269317
self.comments = issue.comments
270318
}
319+
self.severity = issue.severity
271320
self.sourceContext = issue.sourceContext
272321
self.isKnown = issue.isKnown
273322
}
@@ -295,6 +344,8 @@ extension Issue {
295344
}
296345
}
297346

347+
extension Issue.Severity: Codable {}
348+
298349
extension Issue.Kind {
299350
/// Serializable kinds of issues which may be recorded.
300351
@_spi(ForToolsIntegrationOnly)
@@ -484,7 +535,7 @@ extension Issue.Snapshot: CustomStringConvertible, CustomDebugStringConvertible
484535
let joinedComments = comments.lazy
485536
.map(\.rawValue)
486537
.joined(separator: "\n")
487-
return "\(kind): \(joinedComments)"
538+
return "\(severity): \(kind): \(joinedComments)"
488539
}
489540

490541
public var debugDescription: String {
@@ -494,7 +545,7 @@ extension Issue.Snapshot: CustomStringConvertible, CustomDebugStringConvertible
494545
let joinedComments: String = comments.lazy
495546
.map(\.rawValue)
496547
.joined(separator: "\n")
497-
return "\(kind)\(sourceLocation.map { " at \($0)" } ?? ""): \(joinedComments)"
548+
return "\(severity): \(kind)\(sourceLocation.map { " at \($0)" } ?? ""): \(joinedComments)"
498549
}
499550
}
500551

Tests/TestingTests/IssueTests.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1124,12 +1124,12 @@ final class IssueTests: XCTestCase {
11241124
do {
11251125
let sourceLocation = SourceLocation.init(fileID: "FakeModule/FakeFile.swift", filePath: "", line: 9999, column: 1)
11261126
let issue = Issue(kind: .system, comments: ["Some issue"], sourceContext: SourceContext(sourceLocation: sourceLocation))
1127-
XCTAssertEqual(issue.description, "A system failure occurred: Some issue")
1128-
XCTAssertEqual(issue.debugDescription, "A system failure occurred at FakeFile.swift:9999:1: Some issue")
1127+
XCTAssertEqual(issue.description, "error: A system failure occurred: Some issue")
1128+
XCTAssertEqual(issue.debugDescription, "error: A system failure occurred at FakeFile.swift:9999:1: Some issue")
11291129
}
11301130
do {
11311131
let issue = Issue(kind: .system, comments: ["Some issue"], sourceContext: SourceContext(sourceLocation: nil))
1132-
XCTAssertEqual(issue.debugDescription, "A system failure occurred: Some issue")
1132+
XCTAssertEqual(issue.debugDescription, "error: A system failure occurred: Some issue")
11331133
}
11341134
}
11351135

0 commit comments

Comments
 (0)