@@ -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