9393use function explode ;
9494use function file_get_contents ;
9595use function htmlspecialchars ;
96+ use function implode ;
9697use function is_string ;
9798use function ksort ;
99+ use function max ;
100+ use function min ;
98101use function range ;
99- use function sort ;
100102use function sprintf ;
101103use function str_ends_with ;
102104use function str_replace ;
@@ -810,99 +812,75 @@ private function renderBranchStructure(FileNode $node): string
810812
811813 $ coverageData = $ node ->functionCoverageData ();
812814 $ testData = $ node ->testData ();
813- $ codeLines = $ this ->loadFile ($ node ->pathAsString ());
814815 $ branches = '' ;
815816
816817 ksort ($ coverageData );
817818
818819 /** @var ProcessedFunctionCoverageData $methodData */
819820 foreach ($ coverageData as $ methodName => $ methodData ) {
820- $ branchStructure = '' ;
821-
822- /** @var ProcessedBranchCoverageData $branch */
823- foreach ($ methodData ->branches as $ branch ) {
824- $ branchStructure .= $ this ->renderBranchLines ($ branch , $ codeLines , $ testData );
825- }
826-
827- if ($ branchStructure !== '' ) { // don't show empty branches
828- $ branches .= '<h5 class="structure-heading"><a name=" ' . htmlspecialchars ($ methodName , self ::HTML_SPECIAL_CHARS_FLAGS ) . '"> ' . $ this ->abbreviateMethodName ($ methodName ) . '</a></h5> ' . "\n" ;
829- $ branches .= $ branchStructure ;
830- }
831- }
832-
833- $ branchesTemplate ->setVar (['branches ' => $ branches ]);
834-
835- return $ branchesTemplate ->render ();
836- }
837-
838- /**
839- * @param list<string> $codeLines
840- */
841- private function renderBranchLines (ProcessedBranchCoverageData $ branch , array $ codeLines , array $ testData ): string
842- {
843- $ linesTemplate = new Template ($ this ->templatePath . 'lines.html.dist ' , '{{ ' , '}} ' );
844- $ singleLineTemplate = new Template ($ this ->templatePath . 'line.html.dist ' , '{{ ' , '}} ' );
845-
846- $ lines = '' ;
847-
848- $ branchLines = range ($ branch ->line_start , $ branch ->line_end );
849- sort ($ branchLines ); // sometimes end_line < start_line
850-
851- /** @var int $line */
852- foreach ($ branchLines as $ line ) {
853- if (!isset ($ codeLines [$ line ])) { // blank line at end of file is sometimes included here
821+ if ($ methodData ->branches === []) {
854822 continue ;
855823 }
856824
857- $ popoverContent = '' ;
858- $ popoverTitle = '' ;
825+ $ branches .= '<h5 class="structure-heading"><a name=" ' . htmlspecialchars ($ methodName , self ::HTML_SPECIAL_CHARS_FLAGS ) . '"> ' . $ this ->abbreviateMethodName ($ methodName ) . '</a></h5> ' . "\n" ;
826+ $ branches .= '<table class="table table-bordered table-sm structure-table"> ' . "\n" ;
827+ $ branches .= '<thead><tr><th>#</th><th>Lines</th><th>Status</th><th>Tests</th></tr></thead> ' . "\n" ;
828+ $ branches .= '<tbody> ' . "\n" ;
859829
860- $ numTests = count ( $ branch -> hit ) ;
830+ $ branchIndex = 1 ;
861831
862- if ($ numTests === 0 ) {
863- $ trClass = 'danger ' ;
864- } else {
865- $ lineCss = 'covered-by-large-tests ' ;
866- $ popoverContent = '<ul> ' ;
832+ /** @var ProcessedBranchCoverageData $branch */
833+ foreach ($ methodData ->branches as $ branch ) {
834+ $ lineStart = min ($ branch ->line_start , $ branch ->line_end );
835+ $ lineEnd = max ($ branch ->line_start , $ branch ->line_end );
836+ $ linesLabel = $ lineStart === $ lineEnd
837+ ? sprintf ('<a href="#%d">L%d</a> ' , $ lineStart , $ lineStart )
838+ : sprintf ('<a href="#%d">L%d</a>–<a href="#%d">L%d</a> ' , $ lineStart , $ lineStart , $ lineEnd , $ lineEnd );
867839
868- if ($ numTests > 1 ) {
869- $ popoverTitle = $ numTests . ' tests cover this branch ' ;
840+ $ numTests = count ($ branch ->hit );
841+
842+ if ($ numTests === 0 ) {
843+ $ statusClass = 'danger ' ;
844+ $ statusLabel = 'Not covered ' ;
845+ $ testsLabel = '— ' ;
870846 } else {
871- $ popoverTitle = '1 test covers this branch ' ;
872- }
847+ $ statusClass = 'success ' ;
848+ $ statusLabel = 'Covered ' ;
849+
850+ $ popoverContent = '<ul> ' ;
873851
874- foreach ($ branch ->hit as $ test ) {
875- if ($ lineCss === 'covered-by-large-tests ' && $ testData [$ test ]['size ' ] === 'medium ' ) {
876- $ lineCss = 'covered-by-medium-tests ' ;
877- } elseif ($ testData [$ test ]['size ' ] === 'small ' ) {
878- $ lineCss = 'covered-by-small-tests ' ;
852+ foreach ($ branch ->hit as $ test ) {
853+ $ popoverContent .= $ this ->createPopoverContentForTest ($ test , $ testData [$ test ]);
879854 }
880855
881- $ popoverContent .= $ this ->createPopoverContentForTest ($ test , $ testData [$ test ]);
882- }
883- $ trClass = $ lineCss . ' popin ' ;
884- }
856+ $ popoverContent .= '</ul> ' ;
885857
886- $ popover = '' ;
858+ $ testsLabel = sprintf (
859+ '<span class="popin" data-bs-title="%s" data-bs-content="%s" data-bs-placement="top" data-bs-html="true">%s</span> ' ,
860+ $ numTests === 1 ? '1 test ' : $ numTests . ' tests ' ,
861+ htmlspecialchars ($ popoverContent , self ::HTML_SPECIAL_CHARS_FLAGS ),
862+ $ numTests === 1 ? '1 test ' : $ numTests . ' tests ' ,
863+ );
864+ }
887865
888- if ($ popoverTitle !== '' ) {
889- $ popover = sprintf (
890- ' data-bs-title="%s" data-bs-content="%s" data-bs-placement="top" data-bs-html="true" ' ,
891- $ popoverTitle ,
892- htmlspecialchars ($ popoverContent , self ::HTML_SPECIAL_CHARS_FLAGS ),
866+ $ branches .= sprintf (
867+ '<tr class="%s"><td>%d</td><td>%s</td><td>%s</td><td>%s</td></tr> ' . "\n" ,
868+ $ statusClass ,
869+ $ branchIndex ,
870+ $ linesLabel ,
871+ $ statusLabel ,
872+ $ testsLabel ,
893873 );
894- }
895874
896- $ lines .= $ this -> renderLine ( $ singleLineTemplate , $ line , $ codeLines [ $ line - 1 ], $ trClass , $ popover ) ;
897- }
875+ $ branchIndex ++ ;
876+ }
898877
899- if ($ lines === '' ) {
900- return '' ;
878+ $ branches .= '</tbody></table> ' . "\n" ;
901879 }
902880
903- $ linesTemplate ->setVar (['lines ' => $ lines ]);
881+ $ branchesTemplate ->setVar (['branches ' => $ branches ]);
904882
905- return $ linesTemplate ->render ();
883+ return $ branchesTemplate ->render ();
906884 }
907885
908886 private function renderPathStructure (FileNode $ node ): string
@@ -911,115 +889,86 @@ private function renderPathStructure(FileNode $node): string
911889
912890 $ coverageData = $ node ->functionCoverageData ();
913891 $ testData = $ node ->testData ();
914- $ codeLines = $ this ->loadFile ($ node ->pathAsString ());
915892 $ paths = '' ;
916893
917894 ksort ($ coverageData );
918895
919896 /** @var ProcessedFunctionCoverageData $methodData */
920897 foreach ($ coverageData as $ methodName => $ methodData ) {
921- $ pathStructure = '' ;
922-
923- if (count ($ methodData ->paths ) > 100 ) {
924- $ pathStructure .= '<p> ' . count ($ methodData ->paths ) . ' is too many paths to sensibly render, consider refactoring your code to bring this number down.</p> ' ;
925-
898+ if ($ methodData ->paths === []) {
926899 continue ;
927900 }
928901
929- foreach ($ methodData ->paths as $ path ) {
930- $ pathStructure .= $ this ->renderPathLines ($ path , $ methodData ->branches , $ codeLines , $ testData );
931- }
932-
933- if ($ pathStructure !== '' ) {
902+ if (count ($ methodData ->paths ) > 100 ) {
934903 $ paths .= '<h5 class="structure-heading"><a name=" ' . htmlspecialchars ($ methodName , self ::HTML_SPECIAL_CHARS_FLAGS ) . '"> ' . $ this ->abbreviateMethodName ($ methodName ) . '</a></h5> ' . "\n" ;
935- $ paths .= $ pathStructure ;
936- }
937- }
904+ $ paths .= '<p> ' . count ($ methodData ->paths ) . ' is too many paths to sensibly render, consider refactoring your code to bring this number down.</p> ' ;
938905
939- $ pathsTemplate ->setVar (['paths ' => $ paths ]);
906+ continue ;
907+ }
940908
941- return $ pathsTemplate ->render ();
942- }
909+ $ paths .= '<h5 class="structure-heading"><a name=" ' . htmlspecialchars ($ methodName , self ::HTML_SPECIAL_CHARS_FLAGS ) . '"> ' . $ this ->abbreviateMethodName ($ methodName ) . '</a></h5> ' . "\n" ;
910+ $ paths .= '<table class="table table-bordered table-sm structure-table"> ' . "\n" ;
911+ $ paths .= '<thead><tr><th>#</th><th>Branches</th><th>Status</th><th>Tests</th></tr></thead> ' . "\n" ;
912+ $ paths .= '<tbody> ' . "\n" ;
943913
944- /**
945- * @param array<int, ProcessedBranchCoverageData> $branches
946- * @param list<string> $codeLines
947- */
948- private function renderPathLines (ProcessedPathCoverageData $ path , array $ branches , array $ codeLines , array $ testData ): string
949- {
950- $ linesTemplate = new Template ($ this ->templatePath . 'lines.html.dist ' , '{{ ' , '}} ' );
951- $ singleLineTemplate = new Template ($ this ->templatePath . 'line.html.dist ' , '{{ ' , '}} ' );
952-
953- $ lines = '' ;
954- $ first = true ;
914+ $ pathIndex = 1 ;
955915
956- foreach ($ path ->path as $ branchId ) {
957- if ($ first ) {
958- $ first = false ;
959- } else {
960- $ lines .= ' <tr><td colspan="2"> </td></tr> ' . "\n" ;
961- }
916+ foreach ($ methodData ->paths as $ path ) {
917+ $ branchLabels = [];
962918
963- $ branchLines = range ($ branches [$ branchId ]->line_start , $ branches [$ branchId ]->line_end );
964- sort ($ branchLines ); // sometimes end_line < start_line
919+ foreach ($ path ->path as $ branchId ) {
920+ $ branch = $ methodData ->branches [$ branchId ];
921+ $ branchLine = min ($ branch ->line_start , $ branch ->line_end );
965922
966- /** @var int $line */
967- foreach ($ branchLines as $ line ) {
968- if (!isset ($ codeLines [$ line ])) { // blank line at end of file is sometimes included here
969- continue ;
923+ $ branchLabels [] = sprintf ('<a href="#%d">L%d</a> ' , $ branchLine , $ branchLine );
970924 }
971925
972- $ popoverContent = '' ;
973- $ popoverTitle = '' ;
926+ $ branchesLabel = implode (' → ' , $ branchLabels );
974927
975928 $ numTests = count ($ path ->hit );
976929
977930 if ($ numTests === 0 ) {
978- $ trClass = 'danger ' ;
931+ $ statusClass = 'danger ' ;
932+ $ statusLabel = 'Not covered ' ;
933+ $ testsLabel = '— ' ;
979934 } else {
980- $ lineCss = 'covered-by-large-tests ' ;
981- $ popoverContent = '<ul> ' ;
935+ $ statusClass = 'success ' ;
936+ $ statusLabel = 'Covered ' ;
982937
983- if ($ numTests > 1 ) {
984- $ popoverTitle = $ numTests . ' tests cover this path ' ;
985- } else {
986- $ popoverTitle = '1 test covers this path ' ;
987- }
938+ $ popoverContent = '<ul> ' ;
988939
989940 foreach ($ path ->hit as $ test ) {
990- if ($ lineCss === 'covered-by-large-tests ' && $ testData [$ test ]['size ' ] === 'medium ' ) {
991- $ lineCss = 'covered-by-medium-tests ' ;
992- } elseif ($ testData [$ test ]['size ' ] === 'small ' ) {
993- $ lineCss = 'covered-by-small-tests ' ;
994- }
995-
996941 $ popoverContent .= $ this ->createPopoverContentForTest ($ test , $ testData [$ test ]);
997942 }
998943
999- $ trClass = $ lineCss . ' popin ' ;
1000- }
1001-
1002- $ popover = '' ;
944+ $ popoverContent .= '</ul> ' ;
1003945
1004- if ($ popoverTitle !== '' ) {
1005- $ popover = sprintf (
1006- ' data-bs-title="%s" data-bs-content="%s" data-bs-placement="top" data-bs-html="true" ' ,
1007- $ popoverTitle ,
946+ $ testsLabel = sprintf (
947+ '<span class="popin" data-bs-title="%s" data-bs-content="%s" data-bs-placement="top" data-bs-html="true">%s</span> ' ,
948+ $ numTests === 1 ? '1 test ' : $ numTests . ' tests ' ,
1008949 htmlspecialchars ($ popoverContent , self ::HTML_SPECIAL_CHARS_FLAGS ),
950+ $ numTests === 1 ? '1 test ' : $ numTests . ' tests ' ,
1009951 );
1010952 }
1011953
1012- $ lines .= $ this ->renderLine ($ singleLineTemplate , $ line , $ codeLines [$ line - 1 ], $ trClass , $ popover );
954+ $ paths .= sprintf (
955+ '<tr class="%s"><td>%d</td><td>%s</td><td>%s</td><td>%s</td></tr> ' . "\n" ,
956+ $ statusClass ,
957+ $ pathIndex ,
958+ $ branchesLabel ,
959+ $ statusLabel ,
960+ $ testsLabel ,
961+ );
962+
963+ $ pathIndex ++;
1013964 }
1014- }
1015965
1016- if ($ lines === '' ) {
1017- return '' ;
966+ $ paths .= '</tbody></table> ' . "\n" ;
1018967 }
1019968
1020- $ linesTemplate ->setVar (['lines ' => $ lines ]);
969+ $ pathsTemplate ->setVar (['paths ' => $ paths ]);
1021970
1022- return $ linesTemplate ->render ();
971+ return $ pathsTemplate ->render ();
1023972 }
1024973
1025974 private function renderLine (Template $ template , int $ lineNumber , string $ lineContent , string $ class , string $ popover ): string
0 commit comments