Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
* when a fiber switch happens under an open coverage window. Testo closes the window around every
* switch, which is enough from 3.4.5 on; older builds fault regardless, so the run is stopped with
* this exception instead.
*
* @api
*/
final class BranchCoverageUnsafeInFiber extends \RuntimeException
{
Expand Down
2 changes: 2 additions & 0 deletions plugin/codecov/src/Exception/CoverageDriverNotAvailable.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

/**
* Thrown when no supported coverage driver extension is available.
*
* @api
*/
final class CoverageDriverNotAvailable extends \RuntimeException
{
Expand Down
69 changes: 69 additions & 0 deletions plugin/codecov/src/Internal/BranchCoverageAggregator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<?php

declare(strict_types=1);

namespace Testo\Codecov\Internal;

use Testo\Codecov\Result\FileCoverage;

/**
* Reduces {@see FileCoverage::$functions} branch data into the shapes report writers need:
* a file-level edge count and a per-line map of branch decision points.
*
* @internal
*/
final class BranchCoverageAggregator
{
private function __construct() {}

/**
* Counts outgoing edges of every branch in the file, including single-edge linear jumps.
*
* @return array{int<0, max>, int<0, max>} [total edges, covered edges]
*/
public static function countBranches(FileCoverage $fileCoverage): array
{
$total = 0;
$covered = 0;

foreach ($fileCoverage->functions as $function) {
foreach ($function->branches as $branch) {
$total += \count($branch->outHit);
$covered += \count(\array_filter($branch->outHit));
}
}

return [$total, $covered];
}

/**
* Maps line number to [total edges, covered edges] for branch decision points only:
* a branch with a single outgoing edge is a linear jump, not a decision.
* Branches starting on the same line are summed.
*
* @return array<int, array{int<0, max>, int<0, max>}>
*/
public static function buildLineBranchMap(FileCoverage $fileCoverage): array
{
$map = [];

foreach ($fileCoverage->functions as $function) {
foreach ($function->branches as $branch) {
if (\count($branch->out) < 2) {
continue;
}

$line = $branch->lineStart;
$total = \count($branch->outHit);
$covered = \count(\array_filter($branch->outHit));

$map[$line] ??= [0, 0];

$map[$line][0] += $total;
$map[$line][1] += $covered;
}
}

return $map;
}
}
2 changes: 1 addition & 1 deletion plugin/codecov/src/Internal/CoverageDriver.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
/**
* Interface for code coverage collection engines.
*
* @api
* @internal
*/
interface CoverageDriver
{
Expand Down
55 changes: 41 additions & 14 deletions plugin/codecov/src/Report/CloverReport.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace Testo\Codecov\Report;

use Internal\Path;
use Testo\Codecov\Internal\BranchCoverageAggregator;
use Testo\Codecov\Result\CoverageResult;
use Testo\Codecov\Result\FileCoverage;
use Testo\Codecov\Result\LineStatus;
Expand Down Expand Up @@ -46,12 +47,16 @@ public function generate(CoverageResult $result): void

$totalStatements = 0;
$totalCovered = 0;
$totalConditionals = 0;
$totalCoveredConditionals = 0;
$fileCount = 0;

foreach ($result->files as $fileCoverage) {
[$statements, $covered] = $this->writeFile($xml, $fileCoverage);
[$statements, $covered, $conditionals, $coveredConditionals] = $this->writeFile($xml, $fileCoverage);
$totalStatements += $statements;
$totalCovered += $covered;
$totalConditionals += $conditionals;
$totalCoveredConditionals += $coveredConditionals;
$fileCount++;
}

Expand All @@ -60,10 +65,10 @@ public function generate(CoverageResult $result): void
'files' => $fileCount,
'statements' => $totalStatements,
'coveredstatements' => $totalCovered,
'elements' => $totalStatements,
'coveredelements' => $totalCovered,
'conditionals' => 0,
'coveredconditionals' => 0,
'conditionals' => $totalConditionals,
'coveredconditionals' => $totalCoveredConditionals,
'elements' => $totalStatements + $totalConditionals,
'coveredelements' => $totalCovered + $totalCoveredConditionals,
]);

$xml->endElement(); // project
Expand All @@ -82,7 +87,8 @@ public function info(): ReportInfo
}

/**
* @return array{int<0, max>, int<0, max>} [statements, covered]
* @return array{int<0, max>, int<0, max>, int<0, max>, int<0, max>}
* [statements, covered, conditionals, covered conditionals]
*/
private function writeFile(\XMLWriter $xml, FileCoverage $fileCoverage): array
{
Expand All @@ -91,8 +97,17 @@ private function writeFile(\XMLWriter $xml, FileCoverage $fileCoverage): array

$statements = 0;
$covered = 0;
$conditionals = 0;
$coveredConditionals = 0;

// Only decision points count as conditionals, so the file metrics equal
// the sum of truecount/falsecount over the cond lines written below.
$lineBranches = BranchCoverageAggregator::buildLineBranchMap($fileCoverage);
foreach ($lineBranches as [$branchTotal, $branchCovered]) {
$conditionals += $branchTotal;
$coveredConditionals += $branchCovered;
}

// Sort lines by number
$lines = $fileCoverage->lines;
\ksort($lines);

Expand All @@ -107,23 +122,35 @@ private function writeFile(\XMLWriter $xml, FileCoverage $fileCoverage): array

$xml->startElement('line');
$xml->writeAttribute('num', (string) $lineNumber);
$xml->writeAttribute('type', 'stmt');
$xml->writeAttribute('count', (string) $count);

if (isset($lineBranches[$lineNumber])) {
[$branchTotal, $branchCovered] = $lineBranches[$lineNumber];

// A branch may have more than two outgoing edges (`match` arms), so
// truecount/falsecount carry covered/uncovered edge counts, not a literal pair.
$xml->writeAttribute('type', 'cond');
$xml->writeAttribute('truecount', (string) $branchCovered);
$xml->writeAttribute('falsecount', (string) ($branchTotal - $branchCovered));
} else {
$xml->writeAttribute('type', 'stmt');
$xml->writeAttribute('count', (string) $count);
}

$xml->endElement();
}

$this->writeMetrics($xml, [
'statements' => $statements,
'coveredstatements' => $covered,
'elements' => $statements,
'coveredelements' => $covered,
'conditionals' => 0,
'coveredconditionals' => 0,
'conditionals' => $conditionals,
'coveredconditionals' => $coveredConditionals,
'elements' => $statements + $conditionals,
'coveredelements' => $covered + $coveredConditionals,
]);

$xml->endElement(); // file

return [$statements, $covered];
return [$statements, $covered, $conditionals, $coveredConditionals];
}

/**
Expand Down
58 changes: 5 additions & 53 deletions plugin/codecov/src/Report/CoberturaReport.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace Testo\Codecov\Report;

use Internal\Path;
use Testo\Codecov\Internal\BranchCoverageAggregator;
use Testo\Codecov\Result\CoverageResult;
use Testo\Codecov\Result\FileCoverage;
use Testo\Codecov\Result\LineStatus;
Expand Down Expand Up @@ -50,7 +51,7 @@ public function generate(CoverageResult $result): void
[$s, $c] = self::countLines($fileCoverage);
$totalLines += $s;
$totalLinesCovered += $c;
[$b, $bc] = self::countBranches($fileCoverage);
[$b, $bc] = BranchCoverageAggregator::countBranches($fileCoverage);
$totalBranches += $b;
$totalBranchesCovered += $bc;
}
Expand Down Expand Up @@ -112,55 +113,6 @@ private static function countLines(FileCoverage $fileCoverage): array
return [$statements, $covered];
}

/**
* @return array{int<0, max>, int<0, max>} [total branches, covered branches]
*/
private static function countBranches(FileCoverage $fileCoverage): array
{
$total = 0;
$covered = 0;

foreach ($fileCoverage->functions as $function) {
foreach ($function->branches as $branch) {
$total += \count($branch->outHit);
$covered += \count(\array_filter($branch->outHit));
}
}

return [$total, $covered];
}

/**
* Builds a map of line number => [total_branches, covered_branches]
* for lines that are branch decision points.
*
* @return array<int, array{int<0, max>, int<0, max>}>
*/
private static function buildLineBranchMap(FileCoverage $fileCoverage): array
{
$map = [];

foreach ($fileCoverage->functions as $function) {
foreach ($function->branches as $branch) {
// Only mark lines with multiple outgoing edges as branch points
if (\count($branch->out) < 2) {
continue;
}

$line = $branch->lineStart;
$total = \count($branch->outHit);
$covered = \count(\array_filter($branch->outHit));

$map[$line] ??= [0, 0];

$map[$line][0] += $total;
$map[$line][1] += $covered;
}
}

return $map;
}

private static function rate(int $covered, int $total): string
{
return $total === 0 ? '0' : \sprintf('%.4f', $covered / $total);
Expand Down Expand Up @@ -205,7 +157,7 @@ private function writePackage(\XMLWriter $xml, string $packageName, array $files
[$s, $c] = self::countLines($file['coverage']);
$pkgLines += $s;
$pkgLinesCovered += $c;
[$b, $bc] = self::countBranches($file['coverage']);
[$b, $bc] = BranchCoverageAggregator::countBranches($file['coverage']);
$pkgBranches += $b;
$pkgBranchesCovered += $bc;
}
Expand All @@ -228,7 +180,7 @@ private function writePackage(\XMLWriter $xml, string $packageName, array $files
private function writeClass(\XMLWriter $xml, string $relativePath, FileCoverage $fileCoverage): void
{
[$statements, $covered] = self::countLines($fileCoverage);
[$branches, $branchesCovered] = self::countBranches($fileCoverage);
[$branches, $branchesCovered] = BranchCoverageAggregator::countBranches($fileCoverage);

$className = \basename($relativePath, '.php');

Expand All @@ -240,7 +192,7 @@ private function writeClass(\XMLWriter $xml, string $relativePath, FileCoverage
$xml->writeAttribute('complexity', '0');

// Build per-line branch map
$lineBranches = self::buildLineBranchMap($fileCoverage);
$lineBranches = BranchCoverageAggregator::buildLineBranchMap($fileCoverage);

$xml->startElement('lines');

Expand Down
2 changes: 2 additions & 0 deletions plugin/codecov/src/Result/BranchCoverage.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

/**
* A single branch within a function's control flow graph.
*
* @api
*/
final readonly class BranchCoverage
{
Expand Down
2 changes: 2 additions & 0 deletions plugin/codecov/src/Result/CoverageResult.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

/**
* Aggregated code coverage data across multiple files.
*
* @api
*/
final readonly class CoverageResult
{
Expand Down
2 changes: 2 additions & 0 deletions plugin/codecov/src/Result/FileCoverage.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
* Line coverage is always present. Branch and path coverage (via {@see $functions})
* is optional and only populated when collected with {@see CoverageLevel::Branch}
* or {@see CoverageLevel::Path}.
*
* @api
*/
final readonly class FileCoverage
{
Expand Down
2 changes: 2 additions & 0 deletions plugin/codecov/src/Result/FunctionCoverage.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

/**
* Branch and path coverage data for a single function or method.
*
* @api
*/
final readonly class FunctionCoverage
{
Expand Down
2 changes: 2 additions & 0 deletions plugin/codecov/src/Result/LineCoverage.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
* Carries the line's coverage status and the list of tests that executed it.
* Per-test attribution is populated by {@see \Testo\Codecov\Internal\Middleware\CoverageTestInterceptor}
* via {@see CoverageResult::withTestMethod()} and merged across runs by {@see self::merge()}.
*
* @api
*/
final readonly class LineCoverage
{
Expand Down
2 changes: 2 additions & 0 deletions plugin/codecov/src/Result/LineStatus.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

/**
* Represents the coverage status of a single source code line.
*
* @api
*/
enum LineStatus: int
{
Expand Down
2 changes: 2 additions & 0 deletions plugin/codecov/src/Result/PathCoverage.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
*
* A path is an ordered sequence of branches (identified by their opcode start indices)
* that were followed during execution.
*
* @api
*/
final readonly class PathCoverage
{
Expand Down
Loading
Loading