Skip to content

Commit 20f531f

Browse files
committed
Use Graph.forEach, Test.ID nameComponents, and humanReadableName for failure summary
1 parent 3c36853 commit 20f531f

1 file changed

Lines changed: 80 additions & 113 deletions

File tree

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

Lines changed: 80 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,12 @@ extension Event {
2525

2626
/// Information about a single failed test.
2727
struct FailedTest: Sendable {
28-
/// The full hierarchical path to the test (e.g., suite names).
29-
var path: [String]
28+
/// The name components from ``Test/ID`` (excludes the module name).
29+
var nameComponents: [String]
3030

31-
/// The test's simple name (last component of the path).
32-
var name: String
33-
34-
/// The test's display name, if any.
35-
var displayName: String?
31+
/// The human-readable name for this test, consistent with
32+
/// ``Test/humanReadableName(withVerbosity:)``.
33+
var humanReadableName: String
3634

3735
/// Whether this is a suite rather than an individual test.
3836
var isSuite: Bool
@@ -47,101 +45,70 @@ extension Event {
4745
/// The list of failed tests collected from the test run.
4846
private let failedTests: [FailedTest]
4947

50-
/// Initialize a test run summary by collecting failures from a test data graph.
48+
/// Initialize a test run summary by collecting failures from a test data
49+
/// graph.
5150
///
5251
/// - Parameters:
5352
/// - testData: The root test data graph to traverse.
5453
fileprivate init(from testData: Graph<HumanReadableOutputRecorder.Context.TestDataKey, HumanReadableOutputRecorder.Context.TestData?>) {
5554
var testMap: [String: FailedTest] = [:]
5655

57-
// Traverse the graph to find all tests with failures
58-
func traverse(graph: Graph<HumanReadableOutputRecorder.Context.TestDataKey, HumanReadableOutputRecorder.Context.TestData?>, path: [String], isTestCase: Bool = false) {
59-
// Check if this node has test data with failures
60-
if let testData = graph.value, !testData.issues.isEmpty {
61-
let testName = path.last ?? "Unknown"
62-
63-
// Use issues directly from testData
64-
let issues = testData.issues
65-
66-
if isTestCase {
67-
// This is a test case node - add it to the parent test's testCases array
68-
// The parent test path is the path without the test case ID component
69-
let parentPath = path.filter { !$0.hasPrefix("arguments:") }
70-
let parentPathKey = parentPath.joined(separator: "/")
71-
72-
if var parentTest = testMap[parentPathKey] {
73-
// Add this test case to the parent
74-
if let arguments = testData.testCaseArguments, !arguments.isEmpty {
75-
parentTest.testCases.append(FailedTestCase(
76-
arguments: arguments,
77-
issues: issues
78-
))
79-
testMap[parentPathKey] = parentTest
80-
}
81-
} else {
82-
// Parent test not found in map, but should exist - create it
83-
let parentTest = FailedTest(
84-
path: parentPath,
85-
name: parentPath.last ?? "Unknown",
86-
displayName: testData.displayName,
87-
isSuite: testData.isSuite,
88-
issues: [],
89-
testCases: (testData.testCaseArguments?.isEmpty ?? true) ? [] : [FailedTestCase(
90-
arguments: testData.testCaseArguments ?? "",
91-
issues: issues
92-
)]
93-
)
94-
testMap[parentPathKey] = parentTest
95-
}
96-
} else {
97-
// This is a test node (not a test case)
98-
let pathKey = path.joined(separator: "/")
99-
let failedTest = FailedTest(
100-
path: path,
101-
name: testName,
102-
displayName: testData.displayName,
103-
isSuite: testData.isSuite,
104-
issues: issues,
105-
testCases: []
106-
)
107-
testMap[pathKey] = failedTest
56+
testData.forEach { keyPath, value in
57+
guard let testData = value, !testData.issues.isEmpty else { return }
58+
guard let testID = testData.testID else { return }
59+
60+
// Determine if this node represents a parameterized test case by
61+
// checking if the key path contains a parameterized test case ID.
62+
// Non-parameterized test cases have nil argument IDs and should be
63+
// treated as regular tests.
64+
let isTestCase = keyPath.contains {
65+
if case let .testCaseID(id) = $0 {
66+
return id.argumentIDs != nil
10867
}
68+
return false
10969
}
11070

111-
// Recursively traverse children
112-
for (key, childGraph) in graph.children {
113-
let pathComponent: String?
114-
let isChildTestCase: Bool
115-
switch key {
116-
case let .string(s):
117-
let parts = s.split(separator: ":")
118-
if s.hasSuffix(".swift:") || (parts.count >= 2 && parts[0].hasSuffix(".swift")) {
119-
pathComponent = nil // Filter out source location strings
120-
isChildTestCase = false
121-
} else {
122-
pathComponent = s
123-
isChildTestCase = false
124-
}
125-
case let .testCaseID(id):
126-
// Only include parameterized test case IDs in path
127-
if let argumentIDs = id.argumentIDs, let discriminator = id.discriminator {
128-
pathComponent = "arguments: \(argumentIDs), discriminator: \(discriminator)"
129-
isChildTestCase = true
130-
} else {
131-
pathComponent = nil // Filter out non-parameterized test case IDs
132-
isChildTestCase = false
71+
let nameComponents = testID.nameComponents
72+
let pathKey = nameComponents.joined(separator: "/")
73+
let humanReadableName = testData.humanReadableName ?? nameComponents.last ?? "Unknown"
74+
75+
if isTestCase {
76+
// This represents a parameterized test case — group it under
77+
// its parent test.
78+
if var parentTest = testMap[pathKey] {
79+
if let arguments = testData.testCaseArguments, !arguments.isEmpty {
80+
parentTest.testCases.append(FailedTestCase(
81+
arguments: arguments,
82+
issues: testData.issues
83+
))
84+
testMap[pathKey] = parentTest
13385
}
86+
} else {
87+
let parentTest = FailedTest(
88+
nameComponents: nameComponents,
89+
humanReadableName: humanReadableName,
90+
isSuite: testData.isSuite,
91+
issues: [],
92+
testCases: (testData.testCaseArguments?.isEmpty ?? true) ? [] : [FailedTestCase(
93+
arguments: testData.testCaseArguments ?? "",
94+
issues: testData.issues
95+
)]
96+
)
97+
testMap[pathKey] = parentTest
13498
}
135-
136-
let newPath = pathComponent.map { path + [$0] } ?? path
137-
traverse(graph: childGraph, path: newPath, isTestCase: isChildTestCase)
99+
} else {
100+
// This represents a test function, not a parameterized test case.
101+
let failedTest = FailedTest(
102+
nameComponents: nameComponents,
103+
humanReadableName: humanReadableName,
104+
isSuite: testData.isSuite,
105+
issues: testData.issues,
106+
testCases: []
107+
)
108+
testMap[pathKey] = failedTest
138109
}
139110
}
140111

141-
// Start traversal from root
142-
traverse(graph: testData, path: [])
143-
144-
// Convert map to array, ensuring we only include tests that have failures
145112
self.failedTests = Array(testMap.values).filter { !$0.issues.isEmpty || !$0.testCases.isEmpty }
146113
}
147114

@@ -233,25 +200,17 @@ extension Event {
233200
/// - Parameters:
234201
/// - failedTest: The failed test.
235202
///
236-
/// - Returns: The fully qualified name, with display name substituted if
237-
/// available. Test case ID components are filtered out since they're
238-
/// shown separately.
203+
/// - Returns: The fully qualified name, using the test's
204+
/// ``Test/humanReadableName(withVerbosity:)`` for the final component.
239205
private func fullyQualifiedName(for failedTest: FailedTest) -> String {
240-
// Omit the leading path component representing the module name from the
241-
// fully-qualified name of the test.
242-
var path = Array(failedTest.path.dropFirst())
243-
244-
// Filter out test case ID components (they're shown separately with arguments)
245-
path = path.filter { !$0.hasPrefix("arguments:") }
246-
247-
// If we have a display name, replace the function name component (which is
248-
// now the last component after filtering) with the display name. This avoids
249-
// showing both the function name and display name.
250-
if let displayName = failedTest.displayName, !path.isEmpty {
251-
path[path.count - 1] = #""\#(displayName)""#
252-
}
253-
254-
return path.joined(separator: "/")
206+
// Use the name components from Test.ID (which already exclude the module
207+
// name) and replace the last component with the human-readable name to
208+
// be consistent with how tests are described elsewhere in the output.
209+
var components = failedTest.nameComponents
210+
if !components.isEmpty {
211+
components[components.count - 1] = failedTest.humanReadableName
212+
}
213+
return components.joined(separator: "/")
255214
}
256215

257216
/// Format a single issue entry.
@@ -359,10 +318,16 @@ extension Event {
359318
/// Each issue is stored individually with its own source location.
360319
var issues: [IssueInfo] = []
361320

362-
/// The test's display name, if any.
363-
var displayName: String?
321+
/// The ID of the test, used to construct its fully qualified name in
322+
/// the failure summary.
323+
var testID: Test.ID?
364324

365-
/// The test case arguments, formatted for display (for parameterized tests).
325+
/// The human-readable name for this test, consistent with
326+
/// ``Test/humanReadableName(withVerbosity:)``.
327+
var humanReadableName: String?
328+
329+
/// The test case arguments, formatted for display (for parameterized
330+
/// tests).
366331
var testCaseArguments: String?
367332

368333
/// Whether this is a suite rather than an individual test.
@@ -635,14 +600,16 @@ extension Event.HumanReadableOutputRecorder {
635600
)
636601
testData.issues.append(issueInfo)
637602

638-
// Capture test metadata once per test (not per issue)
639-
if testData.displayName == nil {
640-
testData.displayName = test?.displayName
603+
// Capture test metadata once per test (not per issue).
604+
if testData.testID == nil {
605+
testData.testID = test?.id
606+
}
607+
if testData.humanReadableName == nil {
608+
testData.humanReadableName = test?.humanReadableName(withVerbosity: verbosity)
641609
}
642610
if testData.testCaseArguments == nil {
643611
testData.testCaseArguments = testCase?.labeledArguments()
644612
}
645-
// Track whether this is a suite (for failure summary labeling)
646613
testData.isSuite = test?.isSuite ?? false
647614
}
648615

0 commit comments

Comments
 (0)