@@ -75,6 +75,10 @@ extension Event {
75
75
/// All issues recorded during the test execution.
76
76
/// Includes failures, warnings, and other diagnostic information.
77
77
var issues : [ ABI . EncodedIssue < V > ] = [ ]
78
+
79
+ /// Detailed messages for each issue, preserving the order and association.
80
+ /// Each inner array contains all messages for a single issue.
81
+ var issueMessages : [ [ ABI . EncodedMessage < V > ] ] = [ ]
78
82
}
79
83
80
84
/// Represents a node in the test hierarchy tree.
@@ -207,8 +211,10 @@ extension Event.AdvancedConsoleOutputRecorder {
207
211
}
208
212
}
209
213
214
+ // Generate detailed messages using HumanReadableOutputRecorder
215
+ let messages = _humanReadableRecorder. record ( event, in: eventContext)
216
+
210
217
// Convert Event to ABI.EncodedEvent for processing (if needed)
211
- let messages : [ Event . HumanReadableOutputRecorder . Message ] = [ ]
212
218
if let encodedEvent = ABI . EncodedEvent< V> ( encoding: event, in: eventContext, messages: messages) {
213
219
_processABIEvent ( encodedEvent)
214
220
}
@@ -417,6 +423,7 @@ extension Event.AdvancedConsoleOutputRecorder {
417
423
let issue = encodedEvent. issue {
418
424
var testData = context. testData [ testID] ?? _TestData ( )
419
425
testData. issues. append ( issue)
426
+ testData. issueMessages. append ( encodedEvent. messages)
420
427
context. testData [ testID] = testData
421
428
}
422
429
@@ -538,43 +545,64 @@ extension Event.AdvancedConsoleOutputRecorder {
538
545
// Failed Test Details (only if there are failures)
539
546
let failedTests = context. testData. filter { $0. value. result == . failed }
540
547
if !failedTests. isEmpty {
541
- output += " ══════════════════════════════════════ FAILED TEST DETAILS ══════════════════════════════════════ \n "
548
+ output += " ══════════════════════════════════════ FAILED TEST DETAILS ( \( failedTests . count ) ) ══════════════════════════════════════\n "
542
549
output += " \n "
543
550
544
551
// Iterate through all tests that recorded one or more failures
545
- for (testID, testData) in failedTests {
552
+ for (testIndex, testEntry) in failedTests. enumerated ( ) {
553
+ let ( testID, testData) = testEntry
554
+ let testNumber = testIndex + 1
555
+ let totalFailedTests = failedTests. count
556
+
546
557
// Get the fully qualified test name by traversing up the hierarchy
547
558
let fullyQualifiedName = _getFullyQualifiedTestNameWithFile ( testID: testID, context: context)
548
559
549
560
let failureIcon = _getStatusIcon ( for: . failed)
550
561
output += " \( failureIcon) \( fullyQualifiedName) \n "
551
562
552
- // Show detailed issue information with proper indentation
563
+ // Show detailed issue information with enhanced formatting
553
564
if !testData. issues. isEmpty {
554
- for issue in testData. issues {
555
- // Get detailed error description
556
- if let error = issue. _error {
557
- let errorDescription = " \( error) "
558
-
559
- if !errorDescription. isEmpty && errorDescription != " Test failure " {
560
- output += " Expectation failed: \n "
561
-
562
- // Split multi-line error descriptions and indent each line
563
- let errorLines = errorDescription. split ( separator: " \n " , omittingEmptySubsequences: false )
564
- for line in errorLines {
565
- output += " \( line) \n "
566
- }
565
+ for (issueIndex, issue) in testData. issues. enumerated ( ) {
566
+ // 1. Error Message - Get detailed error description
567
+ let issueDescription = _formatDetailedIssueDescription ( issue, issueIndex: issueIndex, testData: testData)
568
+
569
+ if !issueDescription. isEmpty {
570
+ let errorLines = issueDescription. split ( separator: " \n " , omittingEmptySubsequences: false )
571
+ for line in errorLines {
572
+ output += " \( line) \n "
567
573
}
568
574
}
569
575
570
- // Add source location
576
+ // 2. Location and Source Code Context
571
577
if let sourceLocation = issue. sourceLocation {
572
- output += " at \( sourceLocation. fileName) : \( sourceLocation. line) \n "
578
+ output += " \n "
579
+ output += " Location: \( sourceLocation. fileName) : \( sourceLocation. line) : \( sourceLocation. column) \n "
580
+
581
+ // 3. Source Code Context (2-3 lines before/after)
582
+ let codeContext = _getSourceCodeContext ( for: sourceLocation)
583
+ if !codeContext. isEmpty {
584
+ output += " \n "
585
+ output += codeContext
586
+ }
573
587
}
574
588
589
+ // 4. Statistics - Error counter in lower right
590
+ let errorCounter = " [ \( testNumber) / \( totalFailedTests) ] "
591
+ let paddingLength = max ( 0 , 100 - errorCounter. count)
575
592
output += " \n "
593
+ output += " \( String ( repeating: " " , count: paddingLength) ) \( errorCounter) \n "
594
+
595
+ // Add spacing between issues (except for the last one)
596
+ if issueIndex < testData. issues. count - 1 {
597
+ output += " \n "
598
+ }
576
599
}
577
600
}
601
+
602
+ // Add spacing between tests (except for the last one)
603
+ if testIndex < failedTests. count - 1 {
604
+ output += " \n "
605
+ }
578
606
}
579
607
}
580
608
@@ -680,21 +708,24 @@ extension Event.AdvancedConsoleOutputRecorder {
680
708
let paddedTestLine = _padWithDuration ( testLine, duration: duration)
681
709
output += " \( prefix) \( treePrefix) \( paddedTestLine) \n "
682
710
683
- // Render issues for failed tests
711
+ // Show concise issue summary for quick overview
684
712
if let issues = context. testData [ node. testID] ? . issues, !issues. isEmpty {
685
713
let issuePrefix = prefix + ( isLast ? " " : " \( _treeVertical) " )
686
714
for (issueIndex, issue) in issues. enumerated ( ) {
687
715
let isLastIssue = issueIndex == issues. count - 1
688
716
let issueTreePrefix = isLastIssue ? _treeLastBranch : _treeBranch
689
717
let issueIcon = _getStatusIcon ( for: . failed)
690
- let issueDescription = issue. _error? . description ?? " Test failure "
691
718
692
- output += " \( issuePrefix) \( issueTreePrefix) \( issueIcon) \( issueDescription) \n "
719
+ // Get concise issue description (first line only)
720
+ let fullDescription = _formatDetailedIssueDescription ( issue, issueIndex: issueIndex, testData: context. testData [ node. testID] !)
721
+ let conciseDescription = fullDescription. split ( separator: " \n " ) . first. map ( String . init) ?? " Issue recorded "
722
+
723
+ output += " \( issuePrefix) \( issueTreePrefix) \( issueIcon) \( conciseDescription) \n "
693
724
694
- // Add source location
725
+ // Add concise source location
695
726
if let sourceLocation = issue. sourceLocation {
696
727
let locationPrefix = issuePrefix + ( isLastIssue ? " " : " \( _treeVertical) " )
697
- output += " \( locationPrefix) At \( sourceLocation. fileName) : \( sourceLocation. line) : \( sourceLocation . column ) \n "
728
+ output += " \( locationPrefix) at \( sourceLocation. fileName) : \( sourceLocation. line) \n "
698
729
}
699
730
}
700
731
}
@@ -703,6 +734,126 @@ extension Event.AdvancedConsoleOutputRecorder {
703
734
return output
704
735
}
705
736
737
+ /// Format a detailed description of an issue for the Failed Test Details section.
738
+ ///
739
+ /// - Parameters:
740
+ /// - issue: The encoded issue to format.
741
+ /// - issueIndex: The index of the issue in the testData.issues array.
742
+ /// - testData: The test data containing the stored messages.
743
+ /// - Returns: A detailed description of what failed.
744
+ private func _formatDetailedIssueDescription( _ issue: ABI . EncodedIssue < V > , issueIndex: Int , testData: _TestData ) -> String {
745
+ // Get the corresponding messages for this issue
746
+ guard issueIndex < testData. issueMessages. count else {
747
+ // Fallback to error description if available
748
+ if let error = issue. _error {
749
+ return error. description
750
+ }
751
+ return " Issue recorded "
752
+ }
753
+
754
+ let messages = testData. issueMessages [ issueIndex]
755
+
756
+ // Look for detailed messages (difference, details) that contain the actual failure information
757
+ var detailedMessages : [ String ] = [ ]
758
+
759
+ for message in messages {
760
+ switch message. symbol {
761
+ case . difference, . details:
762
+ // These contain the detailed expectation failure information
763
+ detailedMessages. append ( message. text)
764
+ case . fail:
765
+ // Primary failure message - use if no detailed messages available
766
+ if detailedMessages. isEmpty {
767
+ detailedMessages. append ( message. text)
768
+ }
769
+ default :
770
+ break
771
+ }
772
+ }
773
+
774
+ if !detailedMessages. isEmpty {
775
+ return detailedMessages. joined ( separator: " \n " )
776
+ }
777
+
778
+ // Final fallback
779
+ if let error = issue. _error {
780
+ return error. description
781
+ }
782
+ return " Issue recorded "
783
+ }
784
+
785
+ /// Get source code context around a failing line.
786
+ ///
787
+ /// - Parameters:
788
+ /// - sourceLocation: The source location of the failure.
789
+ /// - Returns: Formatted source code context with line numbers and failure indicator.
790
+ private func _getSourceCodeContext( for sourceLocation: SourceLocation ) -> String {
791
+ // Try to read the source file
792
+ guard let fileContent = _readSourceFile ( at: sourceLocation. fileName) else {
793
+ return " "
794
+ }
795
+
796
+ let lines = fileContent. split ( separator: " \n " , omittingEmptySubsequences: false ) . map ( String . init)
797
+ let failingLineNumber = sourceLocation. line
798
+ let contextLines = 2 // Show 2 lines before and after
799
+
800
+ // Calculate the range of lines to show
801
+ let startLine = max ( 1 , failingLineNumber - contextLines)
802
+ let endLine = min ( lines. count, failingLineNumber + contextLines)
803
+
804
+ var output = " "
805
+
806
+ // Calculate the width needed for line numbers (right-aligned)
807
+ let maxLineNumber = endLine
808
+ let lineNumberWidth = String ( maxLineNumber) . count
809
+
810
+ for lineNumber in startLine... endLine {
811
+ let lineIndex = lineNumber - 1 // Convert to 0-based index
812
+ guard lineIndex < lines. count else { continue }
813
+
814
+ let lineContent = lines [ lineIndex]
815
+ let lineNumberString = String ( lineNumber)
816
+ let paddingNeeded = lineNumberWidth - lineNumberString. count
817
+ let paddedLineNumber = String ( repeating: " " , count: paddingNeeded) + lineNumberString
818
+
819
+ // Add failure indicator for the exact failing line
820
+ let indicator = ( lineNumber == failingLineNumber) ? " > " : " "
821
+
822
+ output += " \( paddedLineNumber) \( indicator) | \( lineContent) \n "
823
+ }
824
+
825
+ return output
826
+ }
827
+
828
+ /// Read the contents of a source file.
829
+ ///
830
+ /// - Parameters:
831
+ /// - fileName: The name of the file to read.
832
+ /// - Returns: The file contents as a string, or nil if the file cannot be read.
833
+ private func _readSourceFile( at fileName: String ) -> String ? {
834
+ // Handle relative paths by checking common locations
835
+ let possiblePaths = [
836
+ fileName, // Try as-is first
837
+ " Tests/TestingTests/ \( fileName) " , // Common test location
838
+ " Sources/Testing/ \( fileName) " , // Main source location
839
+ " \( fileName) " // Current directory
840
+ ]
841
+
842
+ for path in possiblePaths {
843
+ do {
844
+ let fileHandle = try FileHandle ( forReadingAtPath: path)
845
+ let data = try fileHandle. readToEnd ( )
846
+ let content = String ( decoding: data, as: UTF8 . self)
847
+ return content
848
+ } catch {
849
+ // Continue to next path
850
+ continue
851
+ }
852
+ }
853
+
854
+ return nil
855
+ }
856
+
706
857
/// Get the status icon for a test result.
707
858
///
708
859
/// - Parameters:
0 commit comments