Skip to content

Commit 4f71d5d

Browse files
Experiment with branch/path summary table per method
1 parent 12663ff commit 4f71d5d

6 files changed

Lines changed: 165 additions & 225 deletions

File tree

src/Report/Html/Renderer/File.php

Lines changed: 89 additions & 142 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;
@@ -812,100 +814,75 @@ private function renderBranchStructure(FileNode $node): string
812814

813815
$coverageData = $node->functionCoverageData();
814816
$testData = $node->testData();
815-
$codeLines = $this->loadFile($node->pathAsString());
816817
$branches = '';
817818

818819
ksort($coverageData);
819820

820821
/** @var ProcessedFunctionCoverageData $methodData */
821822
foreach ($coverageData as $methodName => $methodData) {
822-
$branchStructure = '';
823-
824-
/** @var ProcessedBranchCoverageData $branch */
825-
foreach ($methodData->branches as $branch) {
826-
$branchStructure .= $this->renderBranchLines($branch, $codeLines, $testData);
827-
}
828-
829-
if ($branchStructure !== '') { // don't show empty branches
830-
$branches .= '<h5 class="structure-heading"><a name="' . htmlspecialchars($methodName, self::HTML_SPECIAL_CHARS_FLAGS) . '">' . $this->abbreviateMethodName($methodName) . '</a></h5>' . "\n";
831-
$branches .= $branchStructure;
832-
}
833-
}
834-
835-
$branchesTemplate->setVar(['branches' => $branches]);
836-
837-
return $branchesTemplate->render();
838-
}
839-
840-
/**
841-
* @param list<string> $codeLines
842-
* @param array<string, TestType> $testData
843-
*/
844-
private function renderBranchLines(ProcessedBranchCoverageData $branch, array $codeLines, array $testData): string
845-
{
846-
$linesTemplate = new Template($this->templatePath . 'lines.html.dist', '{{', '}}');
847-
$singleLineTemplate = new Template($this->templatePath . 'line.html.dist', '{{', '}}');
848-
849-
$lines = '';
850-
851-
$branchLines = range($branch->line_start, $branch->line_end);
852-
sort($branchLines); // sometimes end_line < start_line
853-
854-
/** @var int $line */
855-
foreach ($branchLines as $line) {
856-
if (!isset($codeLines[$line])) { // blank line at end of file is sometimes included here
823+
if ($methodData->branches === []) {
857824
continue;
858825
}
859826

860-
$popoverContent = '';
861-
$popoverTitle = '';
827+
$branches .= '<h5 class="structure-heading"><a name="' . htmlspecialchars($methodName, self::HTML_SPECIAL_CHARS_FLAGS) . '">' . $this->abbreviateMethodName($methodName) . '</a></h5>' . "\n";
828+
$branches .= '<table class="table table-bordered table-sm structure-table">' . "\n";
829+
$branches .= '<thead><tr><th>#</th><th>Lines</th><th>Status</th><th>Tests</th></tr></thead>' . "\n";
830+
$branches .= '<tbody>' . "\n";
862831

863-
$numTests = count($branch->hit);
832+
$branchIndex = 1;
864833

865-
if ($numTests === 0) {
866-
$trClass = 'danger';
867-
} else {
868-
$lineCss = 'covered-by-large-tests';
869-
$popoverContent = '<ul>';
834+
/** @var ProcessedBranchCoverageData $branch */
835+
foreach ($methodData->branches as $branch) {
836+
$lineStart = min($branch->line_start, $branch->line_end);
837+
$lineEnd = max($branch->line_start, $branch->line_end);
838+
$linesLabel = $lineStart === $lineEnd
839+
? sprintf('<a href="#%d">L%d</a>', $lineStart, $lineStart)
840+
: sprintf('<a href="#%d">L%d</a>&ndash;<a href="#%d">L%d</a>', $lineStart, $lineStart, $lineEnd, $lineEnd);
870841

871-
if ($numTests > 1) {
872-
$popoverTitle = $numTests . ' tests cover this branch';
842+
$numTests = count($branch->hit);
843+
844+
if ($numTests === 0) {
845+
$statusClass = 'danger';
846+
$statusLabel = 'Not covered';
847+
$testsLabel = '&mdash;';
873848
} else {
874-
$popoverTitle = '1 test covers this branch';
875-
}
849+
$statusClass = 'success';
850+
$statusLabel = 'Covered';
851+
852+
$popoverContent = '<ul>';
876853

877-
foreach ($branch->hit as $test) {
878-
if ($lineCss === 'covered-by-large-tests' && $testData[$test]['size'] === 'medium') {
879-
$lineCss = 'covered-by-medium-tests';
880-
} elseif ($testData[$test]['size'] === 'small') {
881-
$lineCss = 'covered-by-small-tests';
854+
foreach ($branch->hit as $test) {
855+
$popoverContent .= $this->createPopoverContentForTest($test, $testData[$test]);
882856
}
883857

884-
$popoverContent .= $this->createPopoverContentForTest($test, $testData[$test]);
885-
}
886-
$trClass = $lineCss . ' popin';
887-
}
858+
$popoverContent .= '</ul>';
888859

889-
$popover = '';
860+
$testsLabel = sprintf(
861+
'<span class="popin" data-bs-title="%s" data-bs-content="%s" data-bs-placement="top" data-bs-html="true">%s</span>',
862+
$numTests === 1 ? '1 test' : $numTests . ' tests',
863+
htmlspecialchars($popoverContent, self::HTML_SPECIAL_CHARS_FLAGS),
864+
$numTests === 1 ? '1 test' : $numTests . ' tests',
865+
);
866+
}
890867

891-
if ($popoverTitle !== '') {
892-
$popover = sprintf(
893-
' data-bs-title="%s" data-bs-content="%s" data-bs-placement="top" data-bs-html="true"',
894-
$popoverTitle,
895-
htmlspecialchars($popoverContent, self::HTML_SPECIAL_CHARS_FLAGS),
868+
$branches .= sprintf(
869+
'<tr class="%s"><td>%d</td><td>%s</td><td>%s</td><td>%s</td></tr>' . "\n",
870+
$statusClass,
871+
$branchIndex,
872+
$linesLabel,
873+
$statusLabel,
874+
$testsLabel,
896875
);
897-
}
898876

899-
$lines .= $this->renderLine($singleLineTemplate, $line, $codeLines[$line - 1], $trClass, $popover);
900-
}
877+
$branchIndex++;
878+
}
901879

902-
if ($lines === '') {
903-
return '';
880+
$branches .= '</tbody></table>' . "\n";
904881
}
905882

906-
$linesTemplate->setVar(['lines' => $lines]);
883+
$branchesTemplate->setVar(['branches' => $branches]);
907884

908-
return $linesTemplate->render();
885+
return $branchesTemplate->render();
909886
}
910887

911888
private function renderPathStructure(FileNode $node): string
@@ -914,116 +891,86 @@ private function renderPathStructure(FileNode $node): string
914891

915892
$coverageData = $node->functionCoverageData();
916893
$testData = $node->testData();
917-
$codeLines = $this->loadFile($node->pathAsString());
918894
$paths = '';
919895

920896
ksort($coverageData);
921897

922898
/** @var ProcessedFunctionCoverageData $methodData */
923899
foreach ($coverageData as $methodName => $methodData) {
924-
$pathStructure = '';
925-
926-
if (count($methodData->paths) > 100) {
927-
$pathStructure .= '<p>' . count($methodData->paths) . ' is too many paths to sensibly render, consider refactoring your code to bring this number down.</p>';
928-
900+
if ($methodData->paths === []) {
929901
continue;
930902
}
931903

932-
foreach ($methodData->paths as $path) {
933-
$pathStructure .= $this->renderPathLines($path, $methodData->branches, $codeLines, $testData);
934-
}
935-
936-
if ($pathStructure !== '') {
904+
if (count($methodData->paths) > 100) {
937905
$paths .= '<h5 class="structure-heading"><a name="' . htmlspecialchars($methodName, self::HTML_SPECIAL_CHARS_FLAGS) . '">' . $this->abbreviateMethodName($methodName) . '</a></h5>' . "\n";
938-
$paths .= $pathStructure;
939-
}
940-
}
906+
$paths .= '<p>' . count($methodData->paths) . ' is too many paths to sensibly render, consider refactoring your code to bring this number down.</p>';
941907

942-
$pathsTemplate->setVar(['paths' => $paths]);
908+
continue;
909+
}
943910

944-
return $pathsTemplate->render();
945-
}
911+
$paths .= '<h5 class="structure-heading"><a name="' . htmlspecialchars($methodName, self::HTML_SPECIAL_CHARS_FLAGS) . '">' . $this->abbreviateMethodName($methodName) . '</a></h5>' . "\n";
912+
$paths .= '<table class="table table-bordered table-sm structure-table">' . "\n";
913+
$paths .= '<thead><tr><th>#</th><th>Branches</th><th>Status</th><th>Tests</th></tr></thead>' . "\n";
914+
$paths .= '<tbody>' . "\n";
946915

947-
/**
948-
* @param array<int, ProcessedBranchCoverageData> $branches
949-
* @param list<string> $codeLines
950-
* @param array<string, TestType> $testData
951-
*/
952-
private function renderPathLines(ProcessedPathCoverageData $path, array $branches, array $codeLines, array $testData): string
953-
{
954-
$linesTemplate = new Template($this->templatePath . 'lines.html.dist', '{{', '}}');
955-
$singleLineTemplate = new Template($this->templatePath . 'line.html.dist', '{{', '}}');
956-
957-
$lines = '';
958-
$first = true;
916+
$pathIndex = 1;
959917

960-
foreach ($path->path as $branchId) {
961-
if ($first) {
962-
$first = false;
963-
} else {
964-
$lines .= ' <tr><td colspan="2">&nbsp;</td></tr>' . "\n";
965-
}
918+
foreach ($methodData->paths as $path) {
919+
$branchLabels = [];
966920

967-
$branchLines = range($branches[$branchId]->line_start, $branches[$branchId]->line_end);
968-
sort($branchLines); // sometimes end_line < start_line
921+
foreach ($path->path as $branchId) {
922+
$branch = $methodData->branches[$branchId];
923+
$branchLine = min($branch->line_start, $branch->line_end);
969924

970-
/** @var int $line */
971-
foreach ($branchLines as $line) {
972-
if (!isset($codeLines[$line])) { // blank line at end of file is sometimes included here
973-
continue;
925+
$branchLabels[] = sprintf('<a href="#%d">L%d</a>', $branchLine, $branchLine);
974926
}
975927

976-
$popoverContent = '';
977-
$popoverTitle = '';
928+
$branchesLabel = implode(' &rarr; ', $branchLabels);
978929

979930
$numTests = count($path->hit);
980931

981932
if ($numTests === 0) {
982-
$trClass = 'danger';
933+
$statusClass = 'danger';
934+
$statusLabel = 'Not covered';
935+
$testsLabel = '&mdash;';
983936
} else {
984-
$lineCss = 'covered-by-large-tests';
985-
$popoverContent = '<ul>';
937+
$statusClass = 'success';
938+
$statusLabel = 'Covered';
986939

987-
if ($numTests > 1) {
988-
$popoverTitle = $numTests . ' tests cover this path';
989-
} else {
990-
$popoverTitle = '1 test covers this path';
991-
}
940+
$popoverContent = '<ul>';
992941

993942
foreach ($path->hit as $test) {
994-
if ($lineCss === 'covered-by-large-tests' && $testData[$test]['size'] === 'medium') {
995-
$lineCss = 'covered-by-medium-tests';
996-
} elseif ($testData[$test]['size'] === 'small') {
997-
$lineCss = 'covered-by-small-tests';
998-
}
999-
1000943
$popoverContent .= $this->createPopoverContentForTest($test, $testData[$test]);
1001944
}
1002945

1003-
$trClass = $lineCss . ' popin';
1004-
}
1005-
1006-
$popover = '';
946+
$popoverContent .= '</ul>';
1007947

1008-
if ($popoverTitle !== '') {
1009-
$popover = sprintf(
1010-
' data-bs-title="%s" data-bs-content="%s" data-bs-placement="top" data-bs-html="true"',
1011-
$popoverTitle,
948+
$testsLabel = sprintf(
949+
'<span class="popin" data-bs-title="%s" data-bs-content="%s" data-bs-placement="top" data-bs-html="true">%s</span>',
950+
$numTests === 1 ? '1 test' : $numTests . ' tests',
1012951
htmlspecialchars($popoverContent, self::HTML_SPECIAL_CHARS_FLAGS),
952+
$numTests === 1 ? '1 test' : $numTests . ' tests',
1013953
);
1014954
}
1015955

1016-
$lines .= $this->renderLine($singleLineTemplate, $line, $codeLines[$line - 1], $trClass, $popover);
956+
$paths .= sprintf(
957+
'<tr class="%s"><td>%d</td><td>%s</td><td>%s</td><td>%s</td></tr>' . "\n",
958+
$statusClass,
959+
$pathIndex,
960+
$branchesLabel,
961+
$statusLabel,
962+
$testsLabel,
963+
);
964+
965+
$pathIndex++;
1017966
}
1018-
}
1019967

1020-
if ($lines === '') {
1021-
return '';
968+
$paths .= '</tbody></table>' . "\n";
1022969
}
1023970

1024-
$linesTemplate->setVar(['lines' => $lines]);
971+
$pathsTemplate->setVar(['paths' => $paths]);
1025972

1026-
return $linesTemplate->render();
973+
return $pathsTemplate->render();
1027974
}
1028975

1029976
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)