Skip to content

Commit c5ef565

Browse files
committed
Output console message for test case ended events in verbose mode, with status and issue counts
Fixes #1021 Fixes rdar://146863942
1 parent 981aa1c commit c5ef565

File tree

2 files changed

+87
-29
lines changed

2 files changed

+87
-29
lines changed

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

Lines changed: 74 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ extension Event {
3636

3737
/// A type that contains mutable context for
3838
/// ``Event/ConsoleOutputRecorder``.
39-
private struct _Context {
39+
fileprivate struct Context {
4040
/// The instant at which the run started.
4141
var runStartInstant: Test.Clock.Instant?
4242

@@ -51,6 +51,17 @@ extension Event {
5151
/// The number of test suites started or skipped during the run.
5252
var suiteCount = 0
5353

54+
/// An enumeration describing the various keys which can be used in a test
55+
/// data graph for an output recorder.
56+
enum TestDataKey: Hashable {
57+
/// A string key, typically containing one key from the key path
58+
/// representation of a ``Test/ID`` instance.
59+
case string(String)
60+
61+
/// A test case ID.
62+
case testCaseID(Test.Case.ID)
63+
}
64+
5465
/// A type describing data tracked on a per-test basis.
5566
struct TestData {
5667
/// The instant at which the test started.
@@ -62,18 +73,15 @@ extension Event {
6273

6374
/// The number of known issues recorded for the test.
6475
var knownIssueCount = 0
65-
66-
/// The number of test cases for the test.
67-
var testCasesCount = 0
6876
}
6977

7078
/// Data tracked on a per-test basis.
71-
var testData = Graph<String, TestData?>()
79+
var testData = Graph<TestDataKey, TestData?>()
7280
}
7381

7482
/// This event recorder's mutable context about events it has received,
7583
/// which may be used to inform how subsequent events are written.
76-
private var _context = Locked(rawValue: _Context())
84+
private var _context = Locked(rawValue: Context())
7785

7886
/// Initialize a new human-readable event recorder.
7987
///
@@ -128,7 +136,9 @@ extension Event.HumanReadableOutputRecorder {
128136
/// - graph: The graph to walk while counting issues.
129137
///
130138
/// - Returns: A tuple containing the number of issues recorded in `graph`.
131-
private func _issueCounts(in graph: Graph<String, Event.HumanReadableOutputRecorder._Context.TestData?>?) -> (errorIssueCount: Int, warningIssueCount: Int, knownIssueCount: Int, totalIssueCount: Int, description: String) {
139+
private func _issueCounts(
140+
in graph: Graph<Event.HumanReadableOutputRecorder.Context.TestDataKey, Event.HumanReadableOutputRecorder.Context.TestData?>?
141+
) -> (errorIssueCount: Int, warningIssueCount: Int, knownIssueCount: Int, totalIssueCount: Int, description: String) {
132142
guard let graph else {
133143
return (0, 0, 0, 0, "")
134144
}
@@ -241,6 +251,7 @@ extension Event.HumanReadableOutputRecorder {
241251
0
242252
}
243253
let test = eventContext.test
254+
let keyPath = eventContext.keyPath
244255
let testName = if let test {
245256
if let displayName = test.displayName {
246257
if verbosity > 0 {
@@ -271,7 +282,7 @@ extension Event.HumanReadableOutputRecorder {
271282

272283
case .testStarted:
273284
let test = test!
274-
context.testData[test.id.keyPathRepresentation] = .init(startInstant: instant)
285+
context.testData[keyPath] = .init(startInstant: instant)
275286
if test.isSuite {
276287
context.suiteCount += 1
277288
} else {
@@ -287,23 +298,17 @@ extension Event.HumanReadableOutputRecorder {
287298
}
288299

289300
case let .issueRecorded(issue):
290-
let id: [String] = if let test {
291-
test.id.keyPathRepresentation
292-
} else {
293-
[]
294-
}
295-
var testData = context.testData[id] ?? .init(startInstant: instant)
301+
var testData = context.testData[keyPath] ?? .init(startInstant: instant)
296302
if issue.isKnown {
297303
testData.knownIssueCount += 1
298304
} else {
299305
let issueCount = testData.issueCount[issue.severity] ?? 0
300306
testData.issueCount[issue.severity] = issueCount + 1
301307
}
302-
context.testData[id] = testData
308+
context.testData[keyPath] = testData
303309

304310
case .testCaseStarted:
305-
let test = test!
306-
context.testData[test.id.keyPathRepresentation]?.testCasesCount += 1
311+
context.testData[keyPath] = .init(startInstant: instant)
307312

308313
default:
309314
// These events do not manipulate the context structure.
@@ -384,13 +389,12 @@ extension Event.HumanReadableOutputRecorder {
384389

385390
case .testEnded:
386391
let test = test!
387-
let id = test.id
388-
let testDataGraph = context.testData.subgraph(at: id.keyPathRepresentation)
392+
let testDataGraph = context.testData.subgraph(at: keyPath)
389393
let testData = testDataGraph?.value ?? .init(startInstant: instant)
390394
let issues = _issueCounts(in: testDataGraph)
391395
let duration = testData.startInstant.descriptionOfDuration(to: instant)
392-
let testCasesCount = if test.isParameterized {
393-
" with \(testData.testCasesCount.counting("test case"))"
396+
let testCasesCount = if test.isParameterized, let testDataGraph {
397+
" with \(testDataGraph.children.count.counting("test case"))"
394398
} else {
395399
""
396400
}
@@ -517,15 +521,37 @@ extension Event.HumanReadableOutputRecorder {
517521
break
518522
}
519523

524+
let status = verbosity > 0 ? " started" : ""
525+
520526
return [
521527
Message(
522528
symbol: .default,
523-
stringValue: "Passing \(arguments.count.counting("argument")) \(testCase.labeledArguments(includingQualifiedTypeNames: verbosity > 0)) to \(testName)"
529+
stringValue: "Passing \(arguments.count.counting("argument")) \(testCase.labeledArguments(includingQualifiedTypeNames: verbosity > 0)) to \(testName)\(status)."
524530
)
525531
]
526532

527533
case .testCaseEnded:
528-
break
534+
guard verbosity > 0, let testCase = eventContext.testCase, testCase.isParameterized, let arguments = testCase.arguments else {
535+
break
536+
}
537+
538+
let testDataGraph = context.testData.subgraph(at: keyPath)
539+
let testData = testDataGraph?.value ?? .init(startInstant: instant)
540+
let issues = _issueCounts(in: testDataGraph)
541+
let duration = testData.startInstant.descriptionOfDuration(to: instant)
542+
543+
let message = if issues.errorIssueCount > 0 {
544+
Message(
545+
symbol: .fail,
546+
stringValue: "Passing \(arguments.count.counting("argument")) \(testCase.labeledArguments(includingQualifiedTypeNames: verbosity > 0)) to \(testName) failed after \(duration)\(issues.description)."
547+
)
548+
} else {
549+
Message(
550+
symbol: .pass(knownIssueCount: issues.knownIssueCount),
551+
stringValue: "Passing \(arguments.count.counting("argument")) \(testCase.labeledArguments(includingQualifiedTypeNames: verbosity > 0)) to \(testName) passed after \(duration)\(issues.description)."
552+
)
553+
}
554+
return [message]
529555

530556
case let .iterationEnded(index):
531557
guard let iterationStartInstant = context.iterationStartInstant else {
@@ -568,6 +594,31 @@ extension Event.HumanReadableOutputRecorder {
568594
}
569595
}
570596

597+
extension Test.ID {
598+
/// The key path in a test data graph representing this test ID.
599+
fileprivate var keyPath: some Collection<Event.HumanReadableOutputRecorder.Context.TestDataKey> {
600+
keyPathRepresentation.map { .string($0) }
601+
}
602+
}
603+
604+
extension Event.Context {
605+
/// The key path in a test data graph representing this event this context is
606+
/// associated with, including its test and/or test case IDs.
607+
fileprivate var keyPath: some Collection<Event.HumanReadableOutputRecorder.Context.TestDataKey> {
608+
var keyPath = [Event.HumanReadableOutputRecorder.Context.TestDataKey]()
609+
610+
if let test {
611+
keyPath.append(contentsOf: test.id.keyPath)
612+
613+
if let testCase {
614+
keyPath.append(.testCaseID(testCase.id))
615+
}
616+
}
617+
618+
return keyPath
619+
}
620+
}
621+
571622
// MARK: - Codable
572623

573624
extension Event.HumanReadableOutputRecorder.Message: Codable {}

Tests/TestingTests/EventRecorderTests.swift

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ struct EventRecorderTests {
9494
}
9595

9696
@Test("Verbose output")
97+
@available(_regexAPI, *)
9798
func verboseOutput() async throws {
9899
let stream = Stream()
99100

@@ -112,6 +113,14 @@ struct EventRecorderTests {
112113
#expect(buffer.contains(#"\#(Event.Symbol.details.unicodeCharacter) lhs: Swift.String → "987""#))
113114
#expect(buffer.contains(#""Animal Crackers" (aka 'WrittenTests')"#))
114115
#expect(buffer.contains(#""Not A Lobster" (aka 'actuallyCrab()')"#))
116+
do {
117+
let regex = try Regex(".* Passing 1 argument i → 0 \\(Swift.Int\\) to multitudeOcelot\\(i:\\) passed after .*.")
118+
#expect(try buffer.split(whereSeparator: \.isNewline).compactMap(regex.wholeMatch(in:)).first != nil)
119+
}
120+
do {
121+
let regex = try Regex(".* Passing 1 argument i → 3 \\(Swift.Int\\) to multitudeOcelot\\(i:\\) failed after .* with 1 issue.")
122+
#expect(try buffer.split(whereSeparator: \.isNewline).compactMap(regex.wholeMatch(in:)).first != nil)
123+
}
115124

116125
if testsWithSignificantIOAreEnabled {
117126
print(buffer, terminator: "")
@@ -203,17 +212,15 @@ struct EventRecorderTests {
203212
await runTest(for: PredictablyFailingTests.self, configuration: configuration)
204213

205214
let buffer = stream.buffer.rawValue
206-
if testsWithSignificantIOAreEnabled {
207-
print(buffer, terminator: "")
208-
}
209215

210-
let aurgmentRegex = try Regex(expectedPattern)
216+
let argumentRegex = try Regex(expectedPattern)
211217

212218
#expect(
213219
(try buffer
214220
.split(whereSeparator: \.isNewline)
215-
.compactMap(aurgmentRegex.wholeMatch(in:))
216-
.first) != nil
221+
.compactMap(argumentRegex.wholeMatch(in:))
222+
.first) != nil,
223+
"buffer: \(buffer)"
217224
)
218225
}
219226

0 commit comments

Comments
 (0)