Skip to content

Add OpenClover XML reporter #1080

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 21 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
bbb0245
Download https://raw.githubusercontent.com/openclover/clover/refs/tag…
sebastianbergmann May 21, 2025
ddbe978
Validate generated Clover XML against Clover XSD
sebastianbergmann May 21, 2025
103f221
Set clover attribute of <coverage> element to version identifier for …
sebastianbergmann May 21, 2025
16f80f9
Perform validation after comparison with expectation
sebastianbergmann May 21, 2025
feaa804
Remove superfluous type annotation
sebastianbergmann May 21, 2025
f40356a
Make <metrics> element the first child of <project> and <file>
sebastianbergmann May 22, 2025
e9b3260
Set both name and path attributes on the <file> element
sebastianbergmann May 22, 2025
4284f0f
Rename method to reflect that we assert before we validate
sebastianbergmann May 22, 2025
7a6b2cb
Generate <package> element for global namespace
sebastianbergmann May 22, 2025
9442bc7
Ignore <testproject> for now, see https://github.com/sebastianbergman…
sebastianbergmann May 22, 2025
30ef16c
Set complexity attribute on the <metrics> element under <project>
sebastianbergmann May 22, 2025
364b232
Narrow type
sebastianbergmann May 22, 2025
668aafa
- Use consistent order for attributes on <metrics> elements
sebastianbergmann May 22, 2025
1a7d4b6
Remove namespace attribute from <class> element
sebastianbergmann May 22, 2025
5b619a7
Remove name and crap attributes from <line> element
sebastianbergmann May 22, 2025
32d33df
Fix CS/WS issue
sebastianbergmann May 22, 2025
9e10699
Set signature attribute on the <line> element (and reorder attributes…
sebastianbergmann May 22, 2025
1c61bb3
Set attributes of <metrics> element under <package> to actual values
sebastianbergmann May 22, 2025
d75e902
Set attribute name of <class> element to unqualified class name
sebastianbergmann May 22, 2025
b78c110
Keep Report\Clover as-is and introduce new Report\OpenClover for repo…
sebastianbergmann May 23, 2025
7ea8837
Reorganize
sebastianbergmann May 23, 2025
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
18 changes: 18 additions & 0 deletions src/Node/AbstractNode.php
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,24 @@
return $this->numberOfTestedFunctions() + $this->numberOfTestedMethods();
}

/**
* @return non-negative-int
*/
public function cyclomaticComplexity(): int

Check warning on line 184 in src/Node/AbstractNode.php

View check run for this annotation

Codecov / codecov/patch

src/Node/AbstractNode.php#L184

Added line #L184 was not covered by tests
{
$ccn = 0;

Check warning on line 186 in src/Node/AbstractNode.php

View check run for this annotation

Codecov / codecov/patch

src/Node/AbstractNode.php#L186

Added line #L186 was not covered by tests

foreach ($this->classesAndTraits() as $classLike) {
$ccn += $classLike['ccn'];

Check warning on line 189 in src/Node/AbstractNode.php

View check run for this annotation

Codecov / codecov/patch

src/Node/AbstractNode.php#L188-L189

Added lines #L188 - L189 were not covered by tests
}

foreach ($this->functions() as $function) {
$ccn += $function['ccn'];

Check warning on line 193 in src/Node/AbstractNode.php

View check run for this annotation

Codecov / codecov/patch

src/Node/AbstractNode.php#L192-L193

Added lines #L192 - L193 were not covered by tests
}

return $ccn;

Check warning on line 196 in src/Node/AbstractNode.php

View check run for this annotation

Codecov / codecov/patch

src/Node/AbstractNode.php#L196

Added line #L196 was not covered by tests
}

/**
* @return array<string, ProcessedClassType>
*/
Expand Down
257 changes: 257 additions & 0 deletions src/Report/OpenClover.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,257 @@
<?php declare(strict_types=1);
/*
* This file is part of phpunit/php-code-coverage.
*
* (c) Sebastian Bergmann <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\CodeCoverage\Report;

use function assert;
use function basename;
use function count;
use function dirname;
use function file_put_contents;
use function is_string;
use function ksort;
use function max;
use function range;
use function str_contains;
use function str_replace;
use function time;
use DOMDocument;
use DOMElement;
use SebastianBergmann\CodeCoverage\CodeCoverage;
use SebastianBergmann\CodeCoverage\Node\File;
use SebastianBergmann\CodeCoverage\Util\Filesystem;
use SebastianBergmann\CodeCoverage\Version;
use SebastianBergmann\CodeCoverage\WriteOperationFailedException;

final class OpenClover
{
/**
* @throws WriteOperationFailedException
*/
public function process(CodeCoverage $coverage, ?string $target = null, ?string $name = null): string
{
$time = (string) time();

$xmlDocument = new DOMDocument('1.0', 'UTF-8');
$xmlDocument->formatOutput = true;

$xmlCoverage = $xmlDocument->createElement('coverage');
$xmlCoverage->setAttribute('clover', Version::id());
$xmlCoverage->setAttribute('generated', $time);
$xmlDocument->appendChild($xmlCoverage);

$xmlProject = $xmlDocument->createElement('project');
$xmlProject->setAttribute('timestamp', $time);

if (is_string($name)) {
$xmlProject->setAttribute('name', $name);
}

$xmlCoverage->appendChild($xmlProject);

/** @var array<non-empty-string, DOMElement> $packages */
$packages = [];
$report = $coverage->getReport();

foreach ($report as $item) {
if (!$item instanceof File) {
continue;

Check warning on line 64 in src/Report/OpenClover.php

View check run for this annotation

Codecov / codecov/patch

src/Report/OpenClover.php#L64

Added line #L64 was not covered by tests
}

$xmlFile = $xmlDocument->createElement('file');
$xmlFile->setAttribute('name', basename($item->pathAsString()));
$xmlFile->setAttribute('path', $item->pathAsString());

$classes = $item->classesAndTraits();
$coverageData = $item->lineCoverageData();
$lines = [];
$namespace = 'global';

foreach ($classes as $className => $class) {
$classStatements = 0;
$coveredClassStatements = 0;
$coveredMethods = 0;
$classMethods = 0;

// Assumption: one namespace per file
if ($class['namespace'] !== '') {
$namespace = $class['namespace'];

Check warning on line 84 in src/Report/OpenClover.php

View check run for this annotation

Codecov / codecov/patch

src/Report/OpenClover.php#L84

Added line #L84 was not covered by tests
}

foreach ($class['methods'] as $methodName => $method) {
/** @phpstan-ignore equal.notAllowed */
if ($method['executableLines'] == 0) {
continue;
}

$classMethods++;
$classStatements += $method['executableLines'];
$coveredClassStatements += $method['executedLines'];

/** @phpstan-ignore equal.notAllowed */
if ($method['coverage'] == 100) {
$coveredMethods++;
}

$methodCount = 0;

foreach (range($method['startLine'], $method['endLine']) as $line) {
if (isset($coverageData[$line])) {
$methodCount = max($methodCount, count($coverageData[$line]));
}
}

$lines[$method['startLine']] = [
'ccn' => $method['ccn'],
'count' => $methodCount,
'type' => 'method',
'signature' => $method['signature'],
'visibility' => $method['visibility'],
];
}

$xmlClass = $xmlDocument->createElement('class');
$xmlClass->setAttribute('name', str_replace($class['namespace'] . '\\', '', $className));

$xmlFile->appendChild($xmlClass);

$xmlMetrics = $xmlDocument->createElement('metrics');
$xmlMetrics->setAttribute('complexity', (string) $class['ccn']);
$xmlMetrics->setAttribute('elements', (string) ($classMethods + $classStatements + $class['executableBranches']));
$xmlMetrics->setAttribute('coveredelements', (string) ($coveredMethods + $coveredClassStatements + $class['executedBranches']));
$xmlMetrics->setAttribute('conditionals', (string) $class['executableBranches']);
$xmlMetrics->setAttribute('coveredconditionals', (string) $class['executedBranches']);
$xmlMetrics->setAttribute('statements', (string) $classStatements);
$xmlMetrics->setAttribute('coveredstatements', (string) $coveredClassStatements);
$xmlMetrics->setAttribute('methods', (string) $classMethods);
$xmlMetrics->setAttribute('coveredmethods', (string) $coveredMethods);
$xmlClass->insertBefore($xmlMetrics, $xmlClass->firstChild);
}

foreach ($coverageData as $line => $data) {
if ($data === null || isset($lines[$line])) {
continue;
}

$lines[$line] = [
'count' => count($data),
'type' => 'stmt',
];
}

ksort($lines);

foreach ($lines as $line => $data) {
$xmlLine = $xmlDocument->createElement('line');
$xmlLine->setAttribute('num', (string) $line);
$xmlLine->setAttribute('type', $data['type']);

if (isset($data['ccn'])) {
$xmlLine->setAttribute('complexity', (string) $data['ccn']);
}

$xmlLine->setAttribute('count', (string) $data['count']);

if (isset($data['signature'])) {
$xmlLine->setAttribute('signature', $data['signature']);
}

if (isset($data['visibility'])) {
$xmlLine->setAttribute('visibility', $data['visibility']);
}

$xmlFile->appendChild($xmlLine);
}

$linesOfCode = $item->linesOfCode();

$xmlMetrics = $xmlDocument->createElement('metrics');
$xmlMetrics->setAttribute('loc', (string) $linesOfCode->linesOfCode());
$xmlMetrics->setAttribute('ncloc', (string) $linesOfCode->nonCommentLinesOfCode());
$xmlMetrics->setAttribute('classes', (string) $item->numberOfClassesAndTraits());
$xmlMetrics->setAttribute('complexity', (string) $item->cyclomaticComplexity());
$xmlMetrics->setAttribute('elements', (string) ($item->numberOfMethods() + $item->numberOfExecutableLines() + $item->numberOfExecutableBranches()));
$xmlMetrics->setAttribute('coveredelements', (string) ($item->numberOfTestedMethods() + $item->numberOfExecutedLines() + $item->numberOfExecutedBranches()));
$xmlMetrics->setAttribute('conditionals', (string) $item->numberOfExecutableBranches());
$xmlMetrics->setAttribute('coveredconditionals', (string) $item->numberOfExecutedBranches());
$xmlMetrics->setAttribute('statements', (string) $item->numberOfExecutableLines());
$xmlMetrics->setAttribute('coveredstatements', (string) $item->numberOfExecutedLines());
$xmlMetrics->setAttribute('methods', (string) $item->numberOfMethods());
$xmlMetrics->setAttribute('coveredmethods', (string) $item->numberOfTestedMethods());
$xmlFile->insertBefore($xmlMetrics, $xmlFile->firstChild);

if (!isset($packages[$namespace])) {
$packages[$namespace] = $xmlDocument->createElement('package');
$packages[$namespace]->setAttribute('name', $namespace);

$xmlPackageMetrics = $xmlDocument->createElement('metrics');
$xmlPackageMetrics->setAttribute('complexity', '0');
$xmlPackageMetrics->setAttribute('elements', '0');
$xmlPackageMetrics->setAttribute('coveredelements', '0');
$xmlPackageMetrics->setAttribute('conditionals', '0');
$xmlPackageMetrics->setAttribute('coveredconditionals', '0');
$xmlPackageMetrics->setAttribute('statements', '0');
$xmlPackageMetrics->setAttribute('coveredstatements', '0');
$xmlPackageMetrics->setAttribute('methods', '0');
$xmlPackageMetrics->setAttribute('coveredmethods', '0');
$packages[$namespace]->appendChild($xmlPackageMetrics);

$xmlProject->appendChild($packages[$namespace]);
}

$xmlPackageMetrics = $packages[$namespace]->firstChild;

assert($xmlPackageMetrics instanceof DOMElement);

$xmlPackageMetrics->setAttribute('complexity', (string) ((int) $xmlPackageMetrics->getAttribute('complexity') + $item->cyclomaticComplexity()));
$xmlPackageMetrics->setAttribute('elements', (string) ((int) $xmlPackageMetrics->getAttribute('elements') + $item->numberOfMethods() + $item->numberOfExecutableLines() + $item->numberOfExecutableBranches()));
$xmlPackageMetrics->setAttribute('coveredelements', (string) ((int) $xmlPackageMetrics->getAttribute('coveredelements') + $item->numberOfTestedMethods() + $item->numberOfExecutedLines() + $item->numberOfExecutedBranches()));
$xmlPackageMetrics->setAttribute('conditionals', (string) ((int) $xmlPackageMetrics->getAttribute('conditionals') + $item->numberOfExecutableBranches()));
$xmlPackageMetrics->setAttribute('coveredconditionals', (string) ((int) $xmlPackageMetrics->getAttribute('coveredconditionals') + $item->numberOfExecutedBranches()));
$xmlPackageMetrics->setAttribute('statements', (string) ((int) $xmlPackageMetrics->getAttribute('statements') + $item->numberOfExecutableLines()));
$xmlPackageMetrics->setAttribute('coveredstatements', (string) ((int) $xmlPackageMetrics->getAttribute('coveredstatements') + $item->numberOfExecutedLines()));
$xmlPackageMetrics->setAttribute('methods', (string) ((int) $xmlPackageMetrics->getAttribute('methods') + $item->numberOfMethods()));
$xmlPackageMetrics->setAttribute('coveredmethods', (string) ((int) $xmlPackageMetrics->getAttribute('coveredmethods') + $item->numberOfTestedMethods()));

$packages[$namespace]->appendChild($xmlFile);
}

$linesOfCode = $report->linesOfCode();

$xmlMetrics = $xmlDocument->createElement('metrics');
$xmlMetrics->setAttribute('files', (string) count($report));
$xmlMetrics->setAttribute('loc', (string) $linesOfCode->linesOfCode());
$xmlMetrics->setAttribute('ncloc', (string) $linesOfCode->nonCommentLinesOfCode());
$xmlMetrics->setAttribute('classes', (string) $report->numberOfClassesAndTraits());
$xmlMetrics->setAttribute('complexity', (string) $report->cyclomaticComplexity());
$xmlMetrics->setAttribute('elements', (string) ($report->numberOfMethods() + $report->numberOfExecutableLines() + $report->numberOfExecutableBranches()));
$xmlMetrics->setAttribute('coveredelements', (string) ($report->numberOfTestedMethods() + $report->numberOfExecutedLines() + $report->numberOfExecutedBranches()));
$xmlMetrics->setAttribute('conditionals', (string) $report->numberOfExecutableBranches());
$xmlMetrics->setAttribute('coveredconditionals', (string) $report->numberOfExecutedBranches());
$xmlMetrics->setAttribute('statements', (string) $report->numberOfExecutableLines());
$xmlMetrics->setAttribute('coveredstatements', (string) $report->numberOfExecutedLines());
$xmlMetrics->setAttribute('methods', (string) $report->numberOfMethods());
$xmlMetrics->setAttribute('coveredmethods', (string) $report->numberOfTestedMethods());
$xmlProject->insertBefore($xmlMetrics, $xmlProject->firstChild);

$buffer = $xmlDocument->saveXML();

if ($target !== null) {
if (!str_contains($target, '://')) {
Filesystem::createDirectory(dirname($target));

Check warning on line 247 in src/Report/OpenClover.php

View check run for this annotation

Codecov / codecov/patch

src/Report/OpenClover.php#L246-L247

Added lines #L246 - L247 were not covered by tests
}

if (@file_put_contents($target, $buffer) === false) {
throw new WriteOperationFailedException($target);

Check warning on line 251 in src/Report/OpenClover.php

View check run for this annotation

Codecov / codecov/patch

src/Report/OpenClover.php#L250-L251

Added lines #L250 - L251 were not covered by tests
}
}

return $buffer;
}
}
27 changes: 27 additions & 0 deletions tests/_files/Report/OpenClover/BankAccount-line.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<coverage clover="%s" generated="%i">
<project timestamp="%i" name="BankAccount">
<metrics files="1" loc="35" ncloc="35" classes="1" complexity="5" elements="12" coveredelements="8" conditionals="0" coveredconditionals="0" statements="8" coveredstatements="5" methods="4" coveredmethods="3"/>
<package name="global">
<metrics complexity="5" elements="12" coveredelements="8" conditionals="0" coveredconditionals="0" statements="8" coveredstatements="5" methods="4" coveredmethods="3"/>
<file name="BankAccount.php" path="%s%eBankAccount.php">
<metrics loc="35" ncloc="35" classes="1" complexity="5" elements="12" coveredelements="8" conditionals="0" coveredconditionals="0" statements="8" coveredstatements="5" methods="4" coveredmethods="3"/>
<class name="BankAccount">
<metrics complexity="5" elements="12" coveredelements="8" conditionals="0" coveredconditionals="0" statements="8" coveredstatements="5" methods="4" coveredmethods="3"/>
</class>
<line num="6" type="method" complexity="1" count="2" signature="getBalance()" visibility="public"/>
<line num="8" type="stmt" count="2"/>
<line num="11" type="method" complexity="2" count="0" signature="setBalance($balance)" visibility="protected"/>
<line num="13" type="stmt" count="0"/>
<line num="14" type="stmt" count="0"/>
<line num="16" type="stmt" count="0"/>
<line num="20" type="method" complexity="1" count="2" signature="depositMoney($balance)" visibility="public"/>
<line num="22" type="stmt" count="2"/>
<line num="24" type="stmt" count="1"/>
<line num="27" type="method" complexity="1" count="2" signature="withdrawMoney($balance)" visibility="public"/>
<line num="29" type="stmt" count="2"/>
<line num="31" type="stmt" count="1"/>
</file>
</package>
</project>
</coverage>
27 changes: 27 additions & 0 deletions tests/_files/Report/OpenClover/BankAccount-path.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<coverage clover="%s" generated="%i">
<project timestamp="%i" name="BankAccount">
<metrics files="1" loc="35" ncloc="35" classes="1" complexity="5" elements="19" coveredelements="11" conditionals="7" coveredconditionals="3" statements="8" coveredstatements="5" methods="4" coveredmethods="3"/>
<package name="global">
<metrics complexity="5" elements="19" coveredelements="11" conditionals="7" coveredconditionals="3" statements="8" coveredstatements="5" methods="4" coveredmethods="3"/>
<file name="BankAccount.php" path="%s%eBankAccount.php">
<metrics loc="35" ncloc="35" classes="1" complexity="5" elements="19" coveredelements="11" conditionals="7" coveredconditionals="3" statements="8" coveredstatements="5" methods="4" coveredmethods="3"/>
<class name="BankAccount">
<metrics complexity="5" elements="19" coveredelements="11" conditionals="7" coveredconditionals="3" statements="8" coveredstatements="5" methods="4" coveredmethods="3"/>
</class>
<line num="6" type="method" complexity="1" count="2" signature="getBalance()" visibility="public"/>
<line num="8" type="stmt" count="2"/>
<line num="11" type="method" complexity="2" count="0" signature="setBalance($balance)" visibility="protected"/>
<line num="13" type="stmt" count="0"/>
<line num="14" type="stmt" count="0"/>
<line num="16" type="stmt" count="0"/>
<line num="20" type="method" complexity="1" count="2" signature="depositMoney($balance)" visibility="public"/>
<line num="22" type="stmt" count="2"/>
<line num="24" type="stmt" count="1"/>
<line num="27" type="method" complexity="1" count="2" signature="withdrawMoney($balance)" visibility="public"/>
<line num="29" type="stmt" count="2"/>
<line num="31" type="stmt" count="1"/>
</file>
</package>
</project>
</coverage>
24 changes: 24 additions & 0 deletions tests/_files/Report/OpenClover/class-with-anonymous-function.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<coverage clover="%s" generated="%i">
<project timestamp="%i">
<metrics files="1" loc="20" ncloc="19" classes="1" complexity="1" elements="9" coveredelements="9" conditionals="0" coveredconditionals="0" statements="8" coveredstatements="8" methods="1" coveredmethods="1"/>
<package name="global">
<metrics complexity="1" elements="9" coveredelements="9" conditionals="0" coveredconditionals="0" statements="8" coveredstatements="8" methods="1" coveredmethods="1"/>
<file name="source_with_class_and_anonymous_function.php" path="%s%esource_with_class_and_anonymous_function.php">
<metrics loc="20" ncloc="19" classes="1" complexity="1" elements="9" coveredelements="9" conditionals="0" coveredconditionals="0" statements="8" coveredstatements="8" methods="1" coveredmethods="1"/>
<class name="CoveredClassWithAnonymousFunctionInStaticMethod">
<metrics complexity="1" elements="9" coveredelements="9" conditionals="0" coveredconditionals="0" statements="8" coveredstatements="8" methods="1" coveredmethods="1"/>
</class>
<line num="5" type="method" complexity="1" count="1" signature="runAnonymous()" visibility="public"/>
<line num="7" type="stmt" count="1"/>
<line num="9" type="stmt" count="1"/>
<line num="10" type="stmt" count="1"/>
<line num="11" type="stmt" count="1"/>
<line num="12" type="stmt" count="1"/>
<line num="13" type="stmt" count="1"/>
<line num="14" type="stmt" count="1"/>
<line num="17" type="stmt" count="1"/>
</file>
</package>
</project>
</coverage>
Loading