Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit c99f329

Browse files
committedDec 5, 2022
Improved Cobertura report
1 parent df97354 commit c99f329

13 files changed

+808
-310
lines changed
 

‎src/Report/Cobertura.php

Lines changed: 2 additions & 270 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,11 @@
99
*/
1010
namespace SebastianBergmann\CodeCoverage\Report;
1111

12-
use function basename;
13-
use function count;
1412
use function dirname;
1513
use function file_put_contents;
16-
use function preg_match;
17-
use function range;
18-
use function str_replace;
19-
use function time;
20-
use DOMImplementation;
2114
use SebastianBergmann\CodeCoverage\CodeCoverage;
2215
use SebastianBergmann\CodeCoverage\Driver\WriteOperationFailedException;
23-
use SebastianBergmann\CodeCoverage\Node\File;
16+
use SebastianBergmann\CodeCoverage\Report\Cobertura\CoberturaCoverage;
2417
use SebastianBergmann\CodeCoverage\Util\Filesystem;
2518

2619
final class Cobertura
@@ -30,268 +23,7 @@ final class Cobertura
3023
*/
3124
public function process(CodeCoverage $coverage, ?string $target = null): string
3225
{
33-
$time = (string) time();
34-
35-
$report = $coverage->getReport();
36-
37-
$implementation = new DOMImplementation;
38-
39-
$documentType = $implementation->createDocumentType(
40-
'coverage',
41-
'',
42-
'http://cobertura.sourceforge.net/xml/coverage-04.dtd'
43-
);
44-
45-
$document = $implementation->createDocument('', '', $documentType);
46-
$document->xmlVersion = '1.0';
47-
$document->encoding = 'UTF-8';
48-
$document->formatOutput = true;
49-
50-
$coverageElement = $document->createElement('coverage');
51-
52-
$linesValid = $report->numberOfExecutableLines();
53-
$linesCovered = $report->numberOfExecutedLines();
54-
$lineRate = $linesValid === 0 ? 0 : ($linesCovered / $linesValid);
55-
$coverageElement->setAttribute('line-rate', (string) $lineRate);
56-
57-
$branchesValid = $report->numberOfExecutableBranches();
58-
$branchesCovered = $report->numberOfExecutedBranches();
59-
$branchRate = $branchesValid === 0 ? 0 : ($branchesCovered / $branchesValid);
60-
$coverageElement->setAttribute('branch-rate', (string) $branchRate);
61-
62-
$coverageElement->setAttribute('lines-covered', (string) $report->numberOfExecutedLines());
63-
$coverageElement->setAttribute('lines-valid', (string) $report->numberOfExecutableLines());
64-
$coverageElement->setAttribute('branches-covered', (string) $report->numberOfExecutedBranches());
65-
$coverageElement->setAttribute('branches-valid', (string) $report->numberOfExecutableBranches());
66-
$coverageElement->setAttribute('complexity', '');
67-
$coverageElement->setAttribute('version', '0.4');
68-
$coverageElement->setAttribute('timestamp', $time);
69-
70-
$document->appendChild($coverageElement);
71-
72-
$sourcesElement = $document->createElement('sources');
73-
$coverageElement->appendChild($sourcesElement);
74-
75-
$sourceElement = $document->createElement('source', $report->pathAsString());
76-
$sourcesElement->appendChild($sourceElement);
77-
78-
$packagesElement = $document->createElement('packages');
79-
$coverageElement->appendChild($packagesElement);
80-
81-
$complexity = 0;
82-
83-
foreach ($report as $item) {
84-
if (!$item instanceof File) {
85-
continue;
86-
}
87-
88-
$packageElement = $document->createElement('package');
89-
$packageComplexity = 0;
90-
91-
$packageElement->setAttribute('name', str_replace($report->pathAsString() . DIRECTORY_SEPARATOR, '', $item->pathAsString()));
92-
93-
$linesValid = $item->numberOfExecutableLines();
94-
$linesCovered = $item->numberOfExecutedLines();
95-
$lineRate = $linesValid === 0 ? 0 : ($linesCovered / $linesValid);
96-
97-
$packageElement->setAttribute('line-rate', (string) $lineRate);
98-
99-
$branchesValid = $item->numberOfExecutableBranches();
100-
$branchesCovered = $item->numberOfExecutedBranches();
101-
$branchRate = $branchesValid === 0 ? 0 : ($branchesCovered / $branchesValid);
102-
103-
$packageElement->setAttribute('branch-rate', (string) $branchRate);
104-
105-
$packageElement->setAttribute('complexity', '');
106-
$packagesElement->appendChild($packageElement);
107-
108-
$classesElement = $document->createElement('classes');
109-
110-
$packageElement->appendChild($classesElement);
111-
112-
$classes = $item->classesAndTraits();
113-
$coverageData = $item->lineCoverageData();
114-
115-
foreach ($classes as $className => $class) {
116-
$complexity += $class['ccn'];
117-
$packageComplexity += $class['ccn'];
118-
119-
if (!empty($class['package']['namespace'])) {
120-
$className = $class['package']['namespace'] . '\\' . $className;
121-
}
122-
123-
$linesValid = $class['executableLines'];
124-
$linesCovered = $class['executedLines'];
125-
$lineRate = $linesValid === 0 ? 0 : ($linesCovered / $linesValid);
126-
127-
$branchesValid = $class['executableBranches'];
128-
$branchesCovered = $class['executedBranches'];
129-
$branchRate = $branchesValid === 0 ? 0 : ($branchesCovered / $branchesValid);
130-
131-
$classElement = $document->createElement('class');
132-
133-
$classElement->setAttribute('name', $className);
134-
$classElement->setAttribute('filename', str_replace($report->pathAsString() . DIRECTORY_SEPARATOR, '', $item->pathAsString()));
135-
$classElement->setAttribute('line-rate', (string) $lineRate);
136-
$classElement->setAttribute('branch-rate', (string) $branchRate);
137-
$classElement->setAttribute('complexity', (string) $class['ccn']);
138-
139-
$classesElement->appendChild($classElement);
140-
141-
$methodsElement = $document->createElement('methods');
142-
143-
$classElement->appendChild($methodsElement);
144-
145-
$classLinesElement = $document->createElement('lines');
146-
147-
$classElement->appendChild($classLinesElement);
148-
149-
foreach ($class['methods'] as $methodName => $method) {
150-
if ($method['executableLines'] === 0) {
151-
continue;
152-
}
153-
154-
preg_match("/\((.*?)\)/", $method['signature'], $signature);
155-
156-
$linesValid = $method['executableLines'];
157-
$linesCovered = $method['executedLines'];
158-
$lineRate = $linesValid === 0 ? 0 : ($linesCovered / $linesValid);
159-
160-
$branchesValid = $method['executableBranches'];
161-
$branchesCovered = $method['executedBranches'];
162-
$branchRate = $branchesValid === 0 ? 0 : ($branchesCovered / $branchesValid);
163-
164-
$methodElement = $document->createElement('method');
165-
166-
$methodElement->setAttribute('name', $methodName);
167-
$methodElement->setAttribute('signature', $signature[1]);
168-
$methodElement->setAttribute('line-rate', (string) $lineRate);
169-
$methodElement->setAttribute('branch-rate', (string) $branchRate);
170-
$methodElement->setAttribute('complexity', (string) $method['ccn']);
171-
172-
$methodLinesElement = $document->createElement('lines');
173-
174-
$methodElement->appendChild($methodLinesElement);
175-
176-
foreach (range($method['startLine'], $method['endLine']) as $line) {
177-
if (!isset($coverageData[$line]) || $coverageData[$line] === null) {
178-
continue;
179-
}
180-
$methodLineElement = $document->createElement('line');
181-
182-
$methodLineElement->setAttribute('number', (string) $line);
183-
$methodLineElement->setAttribute('hits', (string) count($coverageData[$line]));
184-
185-
$methodLinesElement->appendChild($methodLineElement);
186-
187-
$classLineElement = $methodLineElement->cloneNode();
188-
189-
$classLinesElement->appendChild($classLineElement);
190-
}
191-
192-
$methodsElement->appendChild($methodElement);
193-
}
194-
}
195-
196-
if ($report->numberOfFunctions() === 0) {
197-
$packageElement->setAttribute('complexity', (string) $packageComplexity);
198-
199-
continue;
200-
}
201-
202-
$functionsComplexity = 0;
203-
$functionsLinesValid = 0;
204-
$functionsLinesCovered = 0;
205-
$functionsBranchesValid = 0;
206-
$functionsBranchesCovered = 0;
207-
208-
$classElement = $document->createElement('class');
209-
$classElement->setAttribute('name', basename($item->pathAsString()));
210-
$classElement->setAttribute('filename', str_replace($report->pathAsString() . DIRECTORY_SEPARATOR, '', $item->pathAsString()));
211-
212-
$methodsElement = $document->createElement('methods');
213-
214-
$classElement->appendChild($methodsElement);
215-
216-
$classLinesElement = $document->createElement('lines');
217-
218-
$classElement->appendChild($classLinesElement);
219-
220-
$functions = $report->functions();
221-
222-
foreach ($functions as $functionName => $function) {
223-
if ($function['executableLines'] === 0) {
224-
continue;
225-
}
226-
227-
$complexity += $function['ccn'];
228-
$packageComplexity += $function['ccn'];
229-
$functionsComplexity += $function['ccn'];
230-
231-
$linesValid = $function['executableLines'];
232-
$linesCovered = $function['executedLines'];
233-
$lineRate = $linesValid === 0 ? 0 : ($linesCovered / $linesValid);
234-
235-
$functionsLinesValid += $linesValid;
236-
$functionsLinesCovered += $linesCovered;
237-
238-
$branchesValid = $function['executableBranches'];
239-
$branchesCovered = $function['executedBranches'];
240-
$branchRate = $branchesValid === 0 ? 0 : ($branchesCovered / $branchesValid);
241-
242-
$functionsBranchesValid += $branchesValid;
243-
$functionsBranchesCovered += $branchesValid;
244-
245-
$methodElement = $document->createElement('method');
246-
247-
$methodElement->setAttribute('name', $functionName);
248-
$methodElement->setAttribute('signature', $function['signature']);
249-
$methodElement->setAttribute('line-rate', (string) $lineRate);
250-
$methodElement->setAttribute('branch-rate', (string) $branchRate);
251-
$methodElement->setAttribute('complexity', (string) $function['ccn']);
252-
253-
$methodLinesElement = $document->createElement('lines');
254-
255-
$methodElement->appendChild($methodLinesElement);
256-
257-
foreach (range($function['startLine'], $function['endLine']) as $line) {
258-
if (!isset($coverageData[$line]) || $coverageData[$line] === null) {
259-
continue;
260-
}
261-
$methodLineElement = $document->createElement('line');
262-
263-
$methodLineElement->setAttribute('number', (string) $line);
264-
$methodLineElement->setAttribute('hits', (string) count($coverageData[$line]));
265-
266-
$methodLinesElement->appendChild($methodLineElement);
267-
268-
$classLineElement = $methodLineElement->cloneNode();
269-
270-
$classLinesElement->appendChild($classLineElement);
271-
}
272-
273-
$methodsElement->appendChild($methodElement);
274-
}
275-
276-
$packageElement->setAttribute('complexity', (string) $packageComplexity);
277-
278-
if ($functionsLinesValid === 0) {
279-
continue;
280-
}
281-
282-
$lineRate = $functionsLinesCovered / $functionsLinesValid;
283-
$branchRate = $functionsBranchesValid === 0 ? 0 : ($functionsBranchesCovered / $functionsBranchesValid);
284-
285-
$classElement->setAttribute('line-rate', (string) $lineRate);
286-
$classElement->setAttribute('branch-rate', (string) $branchRate);
287-
$classElement->setAttribute('complexity', (string) $functionsComplexity);
288-
289-
$classesElement->appendChild($classElement);
290-
}
291-
292-
$coverageElement->setAttribute('complexity', (string) $complexity);
293-
294-
$buffer = $document->saveXML();
26+
$buffer = CoberturaCoverage::create($coverage->getReport())->generateDocument()->saveXML();
29527

29628
if ($target !== null) {
29729
Filesystem::createDirectory(dirname($target));
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
<?php declare(strict_types=1);
2+
/*
3+
* This file is part of phpunit/php-code-coverage.
4+
*
5+
* (c) Sebastian Bergmann <sebastian@phpunit.de>
6+
*
7+
* For the full copyright and license information, please view the LICENSE
8+
* file that was distributed with this source code.
9+
*/
10+
namespace SebastianBergmann\CodeCoverage\Report\Cobertura;
11+
12+
use function array_merge;
13+
use function range;
14+
use DOMDocument;
15+
use DOMElement;
16+
17+
class CoberturaClass extends CoberturaElement
18+
{
19+
/** @var CoberturaMethod[] */
20+
private $methods = [];
21+
22+
/** @var CoberturaLine[] */
23+
private $lines = [];
24+
25+
/**
26+
* @var string
27+
*/
28+
private $name;
29+
30+
/**
31+
* @var string
32+
*/
33+
private $filename;
34+
35+
/**
36+
* @var float
37+
*/
38+
private $complexity;
39+
40+
public static function create(string $className, string $relativeFilePath, array $classData, array $lineCoverageData): self
41+
{
42+
if (!empty($classData['package']['namespace'])) {
43+
$className = $classData['package']['namespace'] . '\\' . $className;
44+
}
45+
46+
$class = new self(
47+
$className,
48+
$relativeFilePath,
49+
$classData['executableLines'],
50+
$classData['executedLines'],
51+
$classData['executableBranches'],
52+
$classData['executedBranches'],
53+
$classData['ccn']
54+
);
55+
56+
$endLine = $classData['startLine'];
57+
58+
foreach ($classData['methods'] as $methodName => $methodData) {
59+
$method = CoberturaMethod::create($methodName, $methodData, $lineCoverageData);
60+
61+
if (null !== $method) {
62+
$class->methods[] = $method;
63+
}
64+
65+
if ($methodData['endLine'] > $endLine) {
66+
$endLine = $methodData['endLine'];
67+
}
68+
}
69+
70+
/** @var int $lineNumber */
71+
foreach (range($classData['startLine'], $endLine) as $lineNumber) {
72+
$line = CoberturaLine::create($lineNumber, $lineCoverageData);
73+
74+
if (null !== $line) {
75+
$class->lines[] = $line;
76+
}
77+
}
78+
79+
return $class;
80+
}
81+
82+
public static function createForFunctions(
83+
string $className,
84+
string $relativeFilePath,
85+
int $linesValid,
86+
int $linesCovered,
87+
int $branchesValid,
88+
int $branchesCovered,
89+
float $complexity,
90+
array $functions
91+
): self {
92+
$class = new self(
93+
$className,
94+
$relativeFilePath,
95+
$linesValid,
96+
$linesCovered,
97+
$branchesValid,
98+
$branchesCovered,
99+
$complexity
100+
);
101+
102+
$class->methods = $functions;
103+
104+
foreach ($class->methods as $method) {
105+
$class->lines = array_merge($class->lines, $method->getLines());
106+
}
107+
108+
return $class;
109+
}
110+
111+
private function __construct(
112+
string $name,
113+
string $filename,
114+
int $linesValid,
115+
int $linesCovered,
116+
int $branchesValid,
117+
int $branchesCovered,
118+
float $complexity
119+
) {
120+
$this->name = $name;
121+
$this->filename = $filename;
122+
$this->complexity = $complexity;
123+
parent::__construct($linesValid, $linesCovered, $branchesValid, $branchesCovered);
124+
}
125+
126+
public function wrap(DOMDocument $document): DOMElement
127+
{
128+
$classElement = $document->createElement('class');
129+
130+
$classElement->setAttribute('name', $this->name);
131+
$classElement->setAttribute('filename', $this->filename);
132+
$classElement->setAttribute('line-rate', (string) $this->lineRate());
133+
$classElement->setAttribute('branch-rate', (string) $this->branchRate());
134+
$classElement->setAttribute('complexity', (string) $this->complexity);
135+
136+
$methodsElement = $document->createElement('methods');
137+
138+
foreach ($this->methods as $method) {
139+
$methodsElement->appendChild($method->wrap($document));
140+
}
141+
142+
$classElement->appendChild($methodsElement);
143+
144+
$linesElement = $document->createElement('lines');
145+
146+
foreach ($this->lines as $line) {
147+
$linesElement->appendChild($line->wrap($document));
148+
}
149+
150+
$classElement->appendChild($linesElement);
151+
152+
return $classElement;
153+
}
154+
155+
public function getComplexity(): float
156+
{
157+
return $this->complexity;
158+
}
159+
160+
public function getName(): string
161+
{
162+
return $this->name;
163+
}
164+
}
Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
<?php declare(strict_types=1);
2+
/*
3+
* This file is part of phpunit/php-code-coverage.
4+
*
5+
* (c) Sebastian Bergmann <sebastian@phpunit.de>
6+
*
7+
* For the full copyright and license information, please view the LICENSE
8+
* file that was distributed with this source code.
9+
*/
10+
namespace SebastianBergmann\CodeCoverage\Report\Cobertura;
11+
12+
use function array_reduce;
13+
use function basename;
14+
use function count;
15+
use function date;
16+
use function getcwd;
17+
use function in_array;
18+
use function sprintf;
19+
use function str_replace;
20+
use function time;
21+
use Composer\InstalledVersions;
22+
use DOMDocument;
23+
use DOMElement;
24+
use DOMImplementation;
25+
use SebastianBergmann\CodeCoverage\Node\AbstractNode;
26+
use SebastianBergmann\CodeCoverage\Node\Directory;
27+
use SebastianBergmann\CodeCoverage\Node\File;
28+
29+
class CoberturaCoverage extends CoberturaElement
30+
{
31+
private const FUNCTIONS_PACKAGE = '_functions';
32+
33+
/** @var string[] */
34+
private $sources = [];
35+
36+
/** @var array<string, CoberturaPackage> */
37+
private $packages = [];
38+
39+
/**
40+
* @var int
41+
*/
42+
private $timestamp;
43+
44+
public static function create(Directory $report): self
45+
{
46+
$coverage = new self(
47+
time(),
48+
$report->numberOfExecutableLines(),
49+
$report->numberOfExecutedLines(),
50+
$report->numberOfExecutableBranches(),
51+
$report->numberOfExecutedBranches()
52+
);
53+
54+
foreach ($report as $item) {
55+
if (!$item instanceof File) {
56+
continue;
57+
}
58+
59+
$coverage->processFile($item);
60+
}
61+
62+
return $coverage;
63+
}
64+
65+
private function __construct(
66+
int $timestamp,
67+
int $linesValid,
68+
int $linesCovered,
69+
int $branchesValid,
70+
int $branchesCovered
71+
) {
72+
$this->timestamp = $timestamp;
73+
parent::__construct($linesValid, $linesCovered, $branchesValid, $branchesCovered);
74+
}
75+
76+
public function generateDocument(): DOMDocument
77+
{
78+
$implementation = new DOMImplementation;
79+
80+
$documentType = $implementation->createDocumentType(
81+
'coverage',
82+
'',
83+
'http://cobertura.sourceforge.net/xml/coverage-04.dtd'
84+
);
85+
86+
$document = $implementation->createDocument('', '', $documentType);
87+
$document->xmlVersion = '1.0';
88+
$document->encoding = 'UTF-8';
89+
$document->formatOutput = true;
90+
91+
$comment = $document->createComment(sprintf(
92+
'Cobertura coverage report generated by the PHP library "%s" on %s.',
93+
InstalledVersions::getRootPackage()['name'],
94+
date('c', $this->timestamp),
95+
));
96+
$document->appendChild($comment);
97+
98+
$coverageElement = $document->createElement('coverage');
99+
$coverageElement->setAttribute('line-rate', (string) $this->lineRate());
100+
$coverageElement->setAttribute('branch-rate', (string) $this->branchRate());
101+
$coverageElement->setAttribute('lines-covered', (string) $this->linesCovered);
102+
$coverageElement->setAttribute('lines-valid', (string) $this->linesValid);
103+
$coverageElement->setAttribute('branches-covered', (string) $this->branchesCovered);
104+
$coverageElement->setAttribute('branches-valid', (string) $this->branchesValid);
105+
$coverageElement->setAttribute('complexity', (string) $this->complexity());
106+
$coverageElement->setAttribute('version', '0.4');
107+
$coverageElement->setAttribute('timestamp', (string) $this->timestamp);
108+
109+
$coverageElement->appendChild($this->wrapSources($document));
110+
111+
$packagesElement = $document->createElement('packages');
112+
113+
foreach ($this->packages as $package) {
114+
$packagesElement->appendChild($package->wrap($document));
115+
}
116+
117+
$coverageElement->appendChild($packagesElement);
118+
119+
$document->appendChild($coverageElement);
120+
121+
return $document;
122+
}
123+
124+
private function processFile(File $file): void
125+
{
126+
$this->addSource($this->relativePath($this->fileRoot($file)->pathAsString()));
127+
128+
$lineCoverageData = $file->lineCoverageData();
129+
130+
foreach ($file->classesAndTraits() as $className => $classData) {
131+
$class = CoberturaClass::create(
132+
$className,
133+
$this->relativePath($file->pathAsString()),
134+
$classData,
135+
$lineCoverageData
136+
);
137+
138+
$packageName = CoberturaPackage::packageName($class->getName());
139+
140+
if (!isset($this->packages[$packageName])) {
141+
$this->packages[$packageName] = new CoberturaPackage($packageName);
142+
}
143+
144+
$this->packages[$packageName]->addClass($class);
145+
}
146+
147+
$this->processFunctions($file);
148+
}
149+
150+
private function processFunctions(File $file): void
151+
{
152+
$lineCoverageData = $file->lineCoverageData();
153+
154+
$functions = [];
155+
$classComplexity = 0;
156+
157+
foreach ($file->functions() as $functionName => $functionData) {
158+
$method = CoberturaMethod::create($functionName, $functionData, $lineCoverageData);
159+
160+
if (null !== $method) {
161+
$functions[$functionName] = $method;
162+
$classComplexity += $functionData['ccn'];
163+
}
164+
}
165+
166+
if (count($functions) > 0) {
167+
$classCoverageData = array_reduce($functions, static function (array $data, CoberturaMethod $function)
168+
{
169+
$data['linesValid'] += $function->getLinesValid();
170+
$data['linesCovered'] += $function->getLinesCovered();
171+
$data['branchesValid'] += $function->getBranchesValid();
172+
$data['branchesCovered'] += $function->getBranchesCovered();
173+
174+
return $data;
175+
}, ['linesValid' => 0, 'linesCovered' => 0, 'branchesValid' => 0, 'branchesCovered' => 0]);
176+
177+
$relativeFilePath = $this->relativePath($file->pathAsString());
178+
179+
$class = CoberturaClass::createForFunctions(
180+
self::FUNCTIONS_PACKAGE . '\\' . basename($relativeFilePath),
181+
$relativeFilePath,
182+
$classCoverageData['linesValid'],
183+
$classCoverageData['linesCovered'],
184+
$classCoverageData['branchesValid'],
185+
$classCoverageData['branchesCovered'],
186+
$classComplexity,
187+
$functions
188+
);
189+
190+
if (!isset($this->packages[self::FUNCTIONS_PACKAGE])) {
191+
$this->packages[self::FUNCTIONS_PACKAGE] = new CoberturaPackage(self::FUNCTIONS_PACKAGE);
192+
}
193+
194+
$this->packages[self::FUNCTIONS_PACKAGE]->addClass($class);
195+
}
196+
}
197+
198+
private function addSource(string $source): void
199+
{
200+
if (!in_array($source, $this->sources, true)) {
201+
$this->sources[] = $source;
202+
}
203+
}
204+
205+
private function fileRoot(File $file): AbstractNode
206+
{
207+
$root = $file;
208+
209+
while (true) {
210+
if ($root->parent() === null) {
211+
return $root;
212+
}
213+
214+
/** @var AbstractNode $root */
215+
$root = $root->parent();
216+
}
217+
}
218+
219+
private function relativePath(string $path): string
220+
{
221+
return str_replace(getcwd() . DIRECTORY_SEPARATOR, '', $path);
222+
}
223+
224+
private function wrapSources(DOMDocument $document): DOMElement
225+
{
226+
$sourcesElement = $document->createElement('sources');
227+
228+
foreach ($this->sources as $source) {
229+
$sourcesElement->appendChild($document->createElement('source', $source));
230+
}
231+
232+
return $sourcesElement;
233+
}
234+
235+
private function complexity(): float
236+
{
237+
return array_reduce(
238+
$this->packages,
239+
static function (float $complexity, CoberturaPackage $package)
240+
{
241+
return $complexity + $package->complexity();
242+
},
243+
0
244+
);
245+
}
246+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
<?php declare(strict_types=1);
2+
/*
3+
* This file is part of phpunit/php-code-coverage.
4+
*
5+
* (c) Sebastian Bergmann <sebastian@phpunit.de>
6+
*
7+
* For the full copyright and license information, please view the LICENSE
8+
* file that was distributed with this source code.
9+
*/
10+
namespace SebastianBergmann\CodeCoverage\Report\Cobertura;
11+
12+
abstract class CoberturaElement
13+
{
14+
/**
15+
* @var int
16+
*/
17+
protected $linesValid;
18+
19+
/**
20+
* @var int
21+
*/
22+
protected $linesCovered;
23+
24+
/**
25+
* @var int
26+
*/
27+
protected $branchesValid;
28+
29+
/**
30+
* @var int
31+
*/
32+
protected $branchesCovered;
33+
34+
public function __construct(int $linesValid, int $linesCovered, int $branchesValid, int $branchesCovered)
35+
{
36+
$this->linesValid = $linesValid;
37+
$this->linesCovered = $linesCovered;
38+
$this->branchesValid = $branchesValid;
39+
$this->branchesCovered = $branchesCovered;
40+
}
41+
42+
public function getLinesValid(): int
43+
{
44+
return $this->linesValid;
45+
}
46+
47+
public function getLinesCovered(): int
48+
{
49+
return $this->linesCovered;
50+
}
51+
52+
public function getBranchesValid(): int
53+
{
54+
return $this->branchesValid;
55+
}
56+
57+
public function getBranchesCovered(): int
58+
{
59+
return $this->branchesCovered;
60+
}
61+
62+
protected function lineRate(): float
63+
{
64+
return $this->linesValid === 0 ? 0 : $this->linesCovered / $this->linesValid;
65+
}
66+
67+
protected function branchRate(): float
68+
{
69+
return $this->branchesValid === 0 ? 0 : $this->branchesCovered / $this->branchesValid;
70+
}
71+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
<?php declare(strict_types=1);
2+
/*
3+
* This file is part of phpunit/php-code-coverage.
4+
*
5+
* (c) Sebastian Bergmann <sebastian@phpunit.de>
6+
*
7+
* For the full copyright and license information, please view the LICENSE
8+
* file that was distributed with this source code.
9+
*/
10+
namespace SebastianBergmann\CodeCoverage\Report\Cobertura;
11+
12+
use function count;
13+
use DOMDocument;
14+
use DOMElement;
15+
16+
class CoberturaLine
17+
{
18+
/**
19+
* @var int
20+
*/
21+
private $number;
22+
23+
/**
24+
* @var int
25+
*/
26+
private $hits;
27+
28+
/**
29+
* @var null|bool
30+
*/
31+
private $branch;
32+
33+
public static function create(int $lineNumber, array $lineCoverageData): ?self
34+
{
35+
if (!isset($lineCoverageData[$lineNumber])) {
36+
return null;
37+
}
38+
39+
return new self($lineNumber, count($lineCoverageData[$lineNumber]));
40+
}
41+
42+
private function __construct(int $number, int $hits, ?bool $branch = null)
43+
{
44+
$this->number = $number;
45+
$this->hits = $hits;
46+
$this->branch = $branch;
47+
}
48+
49+
public function wrap(DOMDocument $document): DOMElement
50+
{
51+
$element = $document->createElement('line');
52+
53+
$element->setAttribute('number', (string) $this->number);
54+
$element->setAttribute('hits', (string) $this->hits);
55+
56+
if (null !== $this->branch) {
57+
$element->setAttribute('branch', $this->branch ? 'true' : 'false');
58+
}
59+
60+
return $element;
61+
}
62+
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
<?php declare(strict_types=1);
2+
/*
3+
* This file is part of phpunit/php-code-coverage.
4+
*
5+
* (c) Sebastian Bergmann <sebastian@phpunit.de>
6+
*
7+
* For the full copyright and license information, please view the LICENSE
8+
* file that was distributed with this source code.
9+
*/
10+
namespace SebastianBergmann\CodeCoverage\Report\Cobertura;
11+
12+
use function range;
13+
use DOMDocument;
14+
use DOMElement;
15+
16+
class CoberturaMethod extends CoberturaElement
17+
{
18+
/** @var CoberturaLine[] */
19+
private $lines = [];
20+
21+
/**
22+
* @var string
23+
*/
24+
private $name;
25+
26+
/**
27+
* @var string
28+
*/
29+
private $signature;
30+
31+
/**
32+
* @var float
33+
*/
34+
private $complexity;
35+
36+
public static function create(string $name, array $methodData, array $lineCoverageData): ?self
37+
{
38+
if ($methodData['executableLines'] === 0) {
39+
return null;
40+
}
41+
42+
$method = new self(
43+
$name,
44+
$methodData['signature'],
45+
$methodData['executableLines'],
46+
$methodData['executedLines'],
47+
$methodData['executableBranches'],
48+
$methodData['executedBranches'],
49+
$methodData['ccn']
50+
);
51+
52+
/** @var int $lineNumber */
53+
foreach (range($methodData['startLine'], $methodData['endLine']) as $lineNumber) {
54+
$line = CoberturaLine::create($lineNumber, $lineCoverageData);
55+
56+
if (null !== $line) {
57+
$method->lines[] = $line;
58+
}
59+
}
60+
61+
return $method;
62+
}
63+
64+
private function __construct(
65+
string $name,
66+
string $signature,
67+
int $linesValid,
68+
int $linesCovered,
69+
int $branchesValid,
70+
int $branchesCovered,
71+
float $complexity
72+
) {
73+
$this->name = $name;
74+
$this->signature = $signature;
75+
$this->complexity = $complexity;
76+
parent::__construct($linesValid, $linesCovered, $branchesValid, $branchesCovered);
77+
}
78+
79+
public function wrap(DOMDocument $document): DOMElement
80+
{
81+
$methodElement = $document->createElement('method');
82+
83+
$methodElement->setAttribute('name', $this->name);
84+
$methodElement->setAttribute('signature', $this->signature);
85+
$methodElement->setAttribute('line-rate', (string) $this->lineRate());
86+
$methodElement->setAttribute('branch-rate', (string) $this->branchRate());
87+
$methodElement->setAttribute('complexity', (string) $this->complexity);
88+
89+
$linesElement = $document->createElement('lines');
90+
91+
foreach ($this->lines as $line) {
92+
$linesElement->appendChild($line->wrap($document));
93+
}
94+
95+
$methodElement->appendChild($linesElement);
96+
97+
return $methodElement;
98+
}
99+
100+
public function getLines(): array
101+
{
102+
return $this->lines;
103+
}
104+
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
<?php declare(strict_types=1);
2+
/*
3+
* This file is part of phpunit/php-code-coverage.
4+
*
5+
* (c) Sebastian Bergmann <sebastian@phpunit.de>
6+
*
7+
* For the full copyright and license information, please view the LICENSE
8+
* file that was distributed with this source code.
9+
*/
10+
namespace SebastianBergmann\CodeCoverage\Report\Cobertura;
11+
12+
use function array_reduce;
13+
use function explode;
14+
use DOMDocument;
15+
use DOMElement;
16+
17+
class CoberturaPackage
18+
{
19+
/** @var CoberturaClass[] */
20+
private $classes = [];
21+
22+
/**
23+
* @var string
24+
*/
25+
private $name;
26+
27+
public static function packageName(string $className): string
28+
{
29+
return explode('\\', $className)[0];
30+
}
31+
32+
public function __construct(string $name)
33+
{
34+
$this->name = $name;
35+
}
36+
37+
public function addClass(CoberturaClass $class): void
38+
{
39+
$this->classes[] = $class;
40+
}
41+
42+
public function wrap(DOMDocument $document): DOMElement
43+
{
44+
$packageElement = $document->createElement('package');
45+
46+
$packageElement->setAttribute('name', $this->name);
47+
$packageElement->setAttribute('line-rate', (string) $this->lineRate());
48+
$packageElement->setAttribute('branch-rate', (string) $this->branchRate());
49+
$packageElement->setAttribute('complexity', (string) $this->complexity());
50+
51+
$classesElement = $document->createElement('classes');
52+
53+
foreach ($this->classes as $class) {
54+
$classesElement->appendChild($class->wrap($document));
55+
}
56+
57+
$packageElement->appendChild($classesElement);
58+
59+
return $packageElement;
60+
}
61+
62+
public function complexity(): float
63+
{
64+
return array_reduce(
65+
$this->classes,
66+
static function (float $complexity, CoberturaClass $class)
67+
{
68+
return $complexity + $class->getComplexity();
69+
},
70+
0
71+
);
72+
}
73+
74+
private function lineRate(): float
75+
{
76+
$linesData = array_reduce($this->classes, static function (array $data, CoberturaClass $class)
77+
{
78+
$data['valid'] += $class->getLinesValid();
79+
$data['covered'] += $class->getLinesCovered();
80+
81+
return $data;
82+
}, ['valid' => 0, 'covered' => 0]);
83+
84+
return $linesData['valid'] === 0 ? 0 : $linesData['covered'] / $linesData['valid'];
85+
}
86+
87+
private function branchRate(): float
88+
{
89+
$branchesData = array_reduce($this->classes, static function (array $data, CoberturaClass $class)
90+
{
91+
$data['valid'] += $class->getBranchesValid();
92+
$data['covered'] += $class->getBranchesCovered();
93+
94+
return $data;
95+
}, ['valid' => 0, 'covered' => 0]);
96+
97+
return $branchesData['valid'] === 0 ? 0 : $branchesData['covered'] / $branchesData['valid'];
98+
}
99+
}

‎tests/_files/BankAccount-cobertura-line.xml

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,34 @@
11
<?xml version="1.0" encoding="UTF-8"?>
22
<!DOCTYPE coverage SYSTEM "http://cobertura.sourceforge.net/xml/coverage-04.dtd">
3+
<!--Cobertura coverage report generated by the PHP library "phpunit/php-code-coverage" on %s.-->
34
<coverage line-rate="0.625" branch-rate="0" lines-covered="5" lines-valid="8" branches-covered="0" branches-valid="0" complexity="5" version="0.4" timestamp="%i">
45
<sources>
5-
<source>%s</source>
6+
<source>tests%e_files</source>
67
</sources>
78
<packages>
8-
<package name="BankAccount.php" line-rate="0.625" branch-rate="0" complexity="5">
9+
<package name="BankAccount" line-rate="0.625" branch-rate="0" complexity="5">
910
<classes>
10-
<class name="BankAccount" filename="BankAccount.php" line-rate="0.625" branch-rate="0" complexity="5">
11+
<class name="BankAccount" filename="tests%e_files%eBankAccount.php" line-rate="0.625" branch-rate="0" complexity="5">
1112
<methods>
12-
<method name="getBalance" signature="" line-rate="1" branch-rate="0" complexity="1">
13+
<method name="getBalance" signature="getBalance()" line-rate="1" branch-rate="0" complexity="1">
1314
<lines>
1415
<line number="8" hits="2"/>
1516
</lines>
1617
</method>
17-
<method name="setBalance" signature="$balance" line-rate="0" branch-rate="0" complexity="2">
18+
<method name="setBalance" signature="setBalance($balance)" line-rate="0" branch-rate="0" complexity="2">
1819
<lines>
1920
<line number="13" hits="0"/>
2021
<line number="14" hits="0"/>
2122
<line number="16" hits="0"/>
2223
</lines>
2324
</method>
24-
<method name="depositMoney" signature="$balance" line-rate="1" branch-rate="0" complexity="1">
25+
<method name="depositMoney" signature="depositMoney($balance)" line-rate="1" branch-rate="0" complexity="1">
2526
<lines>
2627
<line number="22" hits="2"/>
2728
<line number="24" hits="1"/>
2829
</lines>
2930
</method>
30-
<method name="withdrawMoney" signature="$balance" line-rate="1" branch-rate="0" complexity="1">
31+
<method name="withdrawMoney" signature="withdrawMoney($balance)" line-rate="1" branch-rate="0" complexity="1">
3132
<lines>
3233
<line number="29" hits="2"/>
3334
<line number="31" hits="1"/>

‎tests/_files/BankAccount-cobertura-path.xml

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,34 @@
11
<?xml version="1.0" encoding="UTF-8"?>
22
<!DOCTYPE coverage SYSTEM "http://cobertura.sourceforge.net/xml/coverage-04.dtd">
3+
<!--Cobertura coverage report generated by the PHP library "phpunit/php-code-coverage" on %s.-->
34
<coverage line-rate="0.625" branch-rate="0.42857142857143" lines-covered="5" lines-valid="8" branches-covered="3" branches-valid="7" complexity="5" version="0.4" timestamp="%i">
45
<sources>
5-
<source>%s</source>
6+
<source>tests%e_files</source>
67
</sources>
78
<packages>
8-
<package name="BankAccount.php" line-rate="0.625" branch-rate="0.42857142857143" complexity="5">
9+
<package name="BankAccount" line-rate="0.625" branch-rate="0.42857142857143" complexity="5">
910
<classes>
10-
<class name="BankAccount" filename="BankAccount.php" line-rate="0.625" branch-rate="0.42857142857143" complexity="5">
11+
<class name="BankAccount" filename="tests%e_files%eBankAccount.php" line-rate="0.625" branch-rate="0.42857142857143" complexity="5">
1112
<methods>
12-
<method name="getBalance" signature="" line-rate="1" branch-rate="1" complexity="1">
13+
<method name="getBalance" signature="getBalance()" line-rate="1" branch-rate="1" complexity="1">
1314
<lines>
1415
<line number="8" hits="2"/>
1516
</lines>
1617
</method>
17-
<method name="setBalance" signature="$balance" line-rate="0" branch-rate="0" complexity="2">
18+
<method name="setBalance" signature="setBalance($balance)" line-rate="0" branch-rate="0" complexity="2">
1819
<lines>
1920
<line number="13" hits="0"/>
2021
<line number="14" hits="0"/>
2122
<line number="16" hits="0"/>
2223
</lines>
2324
</method>
24-
<method name="depositMoney" signature="$balance" line-rate="1" branch-rate="1" complexity="1">
25+
<method name="depositMoney" signature="depositMoney($balance)" line-rate="1" branch-rate="1" complexity="1">
2526
<lines>
2627
<line number="22" hits="2"/>
2728
<line number="24" hits="1"/>
2829
</lines>
2930
</method>
30-
<method name="withdrawMoney" signature="$balance" line-rate="1" branch-rate="1" complexity="1">
31+
<method name="withdrawMoney" signature="withdrawMoney($balance)" line-rate="1" branch-rate="1" complexity="1">
3132
<lines>
3233
<line number="29" hits="2"/>
3334
<line number="31" hits="1"/>

‎tests/_files/class-with-anonymous-function-cobertura.xml

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
<?xml version="1.0" encoding="UTF-8"?>
22
<!DOCTYPE coverage SYSTEM "http://cobertura.sourceforge.net/xml/coverage-04.dtd">
3+
<!--Cobertura coverage report generated by the PHP library "phpunit/php-code-coverage" on %s.-->
34
<coverage line-rate="1" branch-rate="0" lines-covered="5" lines-valid="5" branches-covered="0" branches-valid="0" complexity="1" version="0.4" timestamp="%i">
45
<sources>
5-
<source>%s</source>
6+
<source>tests%e_files</source>
67
</sources>
78
<packages>
8-
<package name="source_with_class_and_anonymous_function.php" line-rate="1" branch-rate="0" complexity="1">
9+
<package name="CoveredClassWithAnonymousFunctionInStaticMethod" line-rate="1" branch-rate="0" complexity="1">
910
<classes>
10-
<class name="CoveredClassWithAnonymousFunctionInStaticMethod" filename="source_with_class_and_anonymous_function.php" line-rate="1" branch-rate="0" complexity="1">
11+
<class name="CoveredClassWithAnonymousFunctionInStaticMethod" filename="tests%e_files%esource_with_class_and_anonymous_function.php" line-rate="1" branch-rate="0" complexity="1">
1112
<methods>
12-
<method name="runAnonymous" signature="" line-rate="1" branch-rate="0" complexity="1">
13+
<method name="runAnonymous" signature="runAnonymous()" line-rate="1" branch-rate="0" complexity="1">
1314
<lines>
1415
<line number="7" hits="1"/>
1516
<line number="9" hits="1"/>

‎tests/_files/class-with-outside-function-cobertura.xml

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
<?xml version="1.0" encoding="UTF-8"?>
22
<!DOCTYPE coverage SYSTEM "http://cobertura.sourceforge.net/xml/coverage-04.dtd">
3+
<!--Cobertura coverage report generated by the PHP library "phpunit/php-code-coverage" on %s.-->
34
<coverage line-rate="0.75" branch-rate="0" lines-covered="3" lines-valid="4" branches-covered="0" branches-valid="0" complexity="3" version="0.4" timestamp="%i">
45
<sources>
5-
<source>%s</source>
6+
<source>tests%e_files</source>
67
</sources>
78
<packages>
8-
<package name="source_with_class_and_outside_function.php" line-rate="0.75" branch-rate="0" complexity="3">
9+
<package name="ClassInFileWithOutsideFunction" line-rate="1" branch-rate="0" complexity="1">
910
<classes>
10-
<class name="ClassInFileWithOutsideFunction" filename="source_with_class_and_outside_function.php" line-rate="1" branch-rate="0" complexity="1">
11+
<class name="ClassInFileWithOutsideFunction" filename="tests%e_files%esource_with_class_and_outside_function.php" line-rate="1" branch-rate="0" complexity="1">
1112
<methods>
12-
<method name="classMethod" signature="" line-rate="1" branch-rate="0" complexity="1">
13+
<method name="classMethod" signature="classMethod(): string" line-rate="1" branch-rate="0" complexity="1">
1314
<lines>
1415
<line number="6" hits="1"/>
1516
</lines>
@@ -19,7 +20,11 @@
1920
<line number="6" hits="1"/>
2021
</lines>
2122
</class>
22-
<class name="source_with_class_and_outside_function.php" filename="source_with_class_and_outside_function.php" line-rate="0.66666666666667" branch-rate="0" complexity="2">
23+
</classes>
24+
</package>
25+
<package name="_functions" line-rate="0.66666666666667" branch-rate="0" complexity="2">
26+
<classes>
27+
<class name="_functions\source_with_class_and_outside_function.php" filename="tests%e_files%esource_with_class_and_outside_function.php" line-rate="0.66666666666667" branch-rate="0" complexity="2">
2328
<methods>
2429
<method name="outsideFunction" signature="outsideFunction(bool $test): int" line-rate="0.66666666666667" branch-rate="0" complexity="2">
2530
<lines>

‎tests/_files/ignored-lines-cobertura.xml

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,22 @@
11
<?xml version="1.0" encoding="UTF-8"?>
22
<!DOCTYPE coverage SYSTEM "http://cobertura.sourceforge.net/xml/coverage-04.dtd">
3+
<!--Cobertura coverage report generated by the PHP library "phpunit/php-code-coverage" on %s.-->
34
<coverage line-rate="1" branch-rate="0" lines-covered="1" lines-valid="1" branches-covered="0" branches-valid="0" complexity="2" version="0.4" timestamp="%i">
45
<sources>
5-
<source>%s</source>
6+
<source>tests%e_files</source>
67
</sources>
78
<packages>
8-
<package name="source_with_ignore.php" line-rate="1" branch-rate="0" complexity="2">
9+
<package name="Foo" line-rate="0" branch-rate="0" complexity="1">
910
<classes>
10-
<class name="Foo" filename="source_with_ignore.php" line-rate="0" branch-rate="0" complexity="1">
11+
<class name="Foo" filename="tests%e_files%esource_with_ignore.php" line-rate="0" branch-rate="0" complexity="1">
1112
<methods/>
1213
<lines/>
1314
</class>
14-
<class name="Bar" filename="source_with_ignore.php" line-rate="0" branch-rate="0" complexity="1">
15+
</classes>
16+
</package>
17+
<package name="Bar" line-rate="0" branch-rate="0" complexity="1">
18+
<classes>
19+
<class name="Bar" filename="tests%e_files%esource_with_ignore.php" line-rate="0" branch-rate="0" complexity="1">
1520
<methods/>
1621
<lines/>
1722
</class>

‎tests/tests/Report/CoberturaTest.php

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,60 +9,67 @@
99
*/
1010
namespace SebastianBergmann\CodeCoverage\Report;
1111

12+
use DOMDocument;
1213
use SebastianBergmann\CodeCoverage\TestCase;
1314

14-
/**
15-
* @covers \SebastianBergmann\CodeCoverage\Report\Cobertura
16-
*/
1715
final class CoberturaTest extends TestCase
1816
{
1917
public function testLineCoverageForBankAccountTest(): void
2018
{
21-
$cobertura = new Cobertura;
19+
$report = (new Cobertura)->process($this->getLineCoverageForBankAccount(), null);
2220

2321
$this->assertStringMatchesFormatFile(
2422
TEST_FILES_PATH . 'BankAccount-cobertura-line.xml',
25-
$cobertura->process($this->getLineCoverageForBankAccount(), null)
23+
$report
2624
);
25+
26+
$this->validateReport($report);
2727
}
2828

2929
public function testPathCoverageForBankAccountTest(): void
3030
{
31-
$cobertura = new Cobertura;
31+
$report = (new Cobertura)->process($this->getPathCoverageForBankAccount(), null);
3232

3333
$this->assertStringMatchesFormatFile(
3434
TEST_FILES_PATH . 'BankAccount-cobertura-path.xml',
35-
$cobertura->process($this->getPathCoverageForBankAccount(), null)
35+
$report
3636
);
3737
}
3838

3939
public function testCoberturaForFileWithIgnoredLines(): void
4040
{
41-
$cobertura = new Cobertura;
41+
$report = (new Cobertura)->process($this->getCoverageForFileWithIgnoredLines());
4242

4343
$this->assertStringMatchesFormatFile(
4444
TEST_FILES_PATH . 'ignored-lines-cobertura.xml',
45-
$cobertura->process($this->getCoverageForFileWithIgnoredLines())
45+
$report
4646
);
4747
}
4848

4949
public function testCoberturaForClassWithAnonymousFunction(): void
5050
{
51-
$cobertura = new Cobertura;
51+
$report = (new Cobertura)->process($this->getCoverageForClassWithAnonymousFunction());
5252

5353
$this->assertStringMatchesFormatFile(
5454
TEST_FILES_PATH . 'class-with-anonymous-function-cobertura.xml',
55-
$cobertura->process($this->getCoverageForClassWithAnonymousFunction())
55+
$report
5656
);
5757
}
5858

5959
public function testCoberturaForClassAndOutsideFunction(): void
6060
{
61-
$cobertura = new Cobertura;
61+
$report = (new Cobertura)->process($this->getCoverageForClassWithOutsideFunction());
6262

6363
$this->assertStringMatchesFormatFile(
6464
TEST_FILES_PATH . 'class-with-outside-function-cobertura.xml',
65-
$cobertura->process($this->getCoverageForClassWithOutsideFunction())
65+
$report
6666
);
6767
}
68+
69+
private function validateReport(string $coberturaReport): void
70+
{
71+
$document = (new DOMDocument);
72+
$this->assertTrue($document->loadXML($coberturaReport));
73+
$this->assertTrue(@$document->validate());
74+
}
6875
}

0 commit comments

Comments
 (0)
Please sign in to comment.