Skip to content

Commit 6ab23c9

Browse files
Experiment with branch/path summary table per method
1 parent 72011d2 commit 6ab23c9

6 files changed

Lines changed: 165 additions & 223 deletions

File tree

src/Report/Html/Renderer/File.php

Lines changed: 89 additions & 140 deletions
Original file line numberDiff line numberDiff line change
@@ -93,10 +93,12 @@
9393
use function explode;
9494
use function file_get_contents;
9595
use function htmlspecialchars;
96+
use function implode;
9697
use function is_string;
9798
use function ksort;
99+
use function max;
100+
use function min;
98101
use function range;
99-
use function sort;
100102
use function sprintf;
101103
use function str_ends_with;
102104
use 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>&ndash;<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 = '&mdash;';
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">&nbsp;</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(' &rarr; ', $branchLabels);
974927

975928
$numTests = count($path->hit);
976929

977930
if ($numTests === 0) {
978-
$trClass = 'danger';
931+
$statusClass = 'danger';
932+
$statusLabel = 'Not covered';
933+
$testsLabel = '&mdash;';
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

src/Report/Html/Renderer/Template/css/style.css

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,19 @@ table + .structure-heading {
224224
padding-top: 0.5em;
225225
}
226226

227+
.structure-table {
228+
width: auto;
229+
}
230+
231+
.structure-table td, .structure-table th {
232+
white-space: nowrap;
233+
}
234+
235+
.structure-table .popin {
236+
cursor: pointer;
237+
text-decoration: underline dotted;
238+
}
239+
227240
table#code td:first-of-type {
228241
padding-left: .75em;
229242
padding-right: .75em;

0 commit comments

Comments
 (0)