From f0a61269e8359ce7b132b125b910f1f047b3a9f9 Mon Sep 17 00:00:00 2001 From: Joep Hoffland <joephoffland@gmail.com> Date: Thu, 22 Dec 2022 15:48:17 +0100 Subject: [PATCH] Improved Cobertura report --- src/Report/Cobertura.php | 272 +----------------- src/Report/Cobertura/CoberturaClass.php | 164 +++++++++++ src/Report/Cobertura/CoberturaCoverage.php | 245 ++++++++++++++++ src/Report/Cobertura/CoberturaElement.php | 71 +++++ src/Report/Cobertura/CoberturaLine.php | 62 ++++ src/Report/Cobertura/CoberturaMethod.php | 104 +++++++ src/Report/Cobertura/CoberturaPackage.php | 99 +++++++ tests/_files/BankAccount-cobertura-line.xml | 15 +- tests/_files/BankAccount-cobertura-path.xml | 15 +- ...lass-with-anonymous-function-cobertura.xml | 9 +- .../class-with-outside-function-cobertura.xml | 15 +- tests/_files/ignored-lines-cobertura.xml | 13 +- tests/tests/Report/CoberturaTest.php | 33 ++- 13 files changed, 807 insertions(+), 310 deletions(-) create mode 100644 src/Report/Cobertura/CoberturaClass.php create mode 100644 src/Report/Cobertura/CoberturaCoverage.php create mode 100644 src/Report/Cobertura/CoberturaElement.php create mode 100644 src/Report/Cobertura/CoberturaLine.php create mode 100644 src/Report/Cobertura/CoberturaMethod.php create mode 100644 src/Report/Cobertura/CoberturaPackage.php diff --git a/src/Report/Cobertura.php b/src/Report/Cobertura.php index 0d1dde760..67a050b71 100644 --- a/src/Report/Cobertura.php +++ b/src/Report/Cobertura.php @@ -9,18 +9,11 @@ */ namespace SebastianBergmann\CodeCoverage\Report; -use function basename; -use function count; use function dirname; use function file_put_contents; -use function preg_match; -use function range; -use function str_replace; -use function time; -use DOMImplementation; use SebastianBergmann\CodeCoverage\CodeCoverage; use SebastianBergmann\CodeCoverage\Driver\WriteOperationFailedException; -use SebastianBergmann\CodeCoverage\Node\File; +use SebastianBergmann\CodeCoverage\Report\Cobertura\CoberturaCoverage; use SebastianBergmann\CodeCoverage\Util\Filesystem; final class Cobertura @@ -30,268 +23,7 @@ final class Cobertura */ public function process(CodeCoverage $coverage, ?string $target = null): string { - $time = (string) time(); - - $report = $coverage->getReport(); - - $implementation = new DOMImplementation; - - $documentType = $implementation->createDocumentType( - 'coverage', - '', - 'http://cobertura.sourceforge.net/xml/coverage-04.dtd' - ); - - $document = $implementation->createDocument('', '', $documentType); - $document->xmlVersion = '1.0'; - $document->encoding = 'UTF-8'; - $document->formatOutput = true; - - $coverageElement = $document->createElement('coverage'); - - $linesValid = $report->numberOfExecutableLines(); - $linesCovered = $report->numberOfExecutedLines(); - $lineRate = $linesValid === 0 ? 0 : ($linesCovered / $linesValid); - $coverageElement->setAttribute('line-rate', (string) $lineRate); - - $branchesValid = $report->numberOfExecutableBranches(); - $branchesCovered = $report->numberOfExecutedBranches(); - $branchRate = $branchesValid === 0 ? 0 : ($branchesCovered / $branchesValid); - $coverageElement->setAttribute('branch-rate', (string) $branchRate); - - $coverageElement->setAttribute('lines-covered', (string) $report->numberOfExecutedLines()); - $coverageElement->setAttribute('lines-valid', (string) $report->numberOfExecutableLines()); - $coverageElement->setAttribute('branches-covered', (string) $report->numberOfExecutedBranches()); - $coverageElement->setAttribute('branches-valid', (string) $report->numberOfExecutableBranches()); - $coverageElement->setAttribute('complexity', ''); - $coverageElement->setAttribute('version', '0.4'); - $coverageElement->setAttribute('timestamp', $time); - - $document->appendChild($coverageElement); - - $sourcesElement = $document->createElement('sources'); - $coverageElement->appendChild($sourcesElement); - - $sourceElement = $document->createElement('source', $report->pathAsString()); - $sourcesElement->appendChild($sourceElement); - - $packagesElement = $document->createElement('packages'); - $coverageElement->appendChild($packagesElement); - - $complexity = 0; - - foreach ($report as $item) { - if (!$item instanceof File) { - continue; - } - - $packageElement = $document->createElement('package'); - $packageComplexity = 0; - - $packageElement->setAttribute('name', str_replace($report->pathAsString() . DIRECTORY_SEPARATOR, '', $item->pathAsString())); - - $linesValid = $item->numberOfExecutableLines(); - $linesCovered = $item->numberOfExecutedLines(); - $lineRate = $linesValid === 0 ? 0 : ($linesCovered / $linesValid); - - $packageElement->setAttribute('line-rate', (string) $lineRate); - - $branchesValid = $item->numberOfExecutableBranches(); - $branchesCovered = $item->numberOfExecutedBranches(); - $branchRate = $branchesValid === 0 ? 0 : ($branchesCovered / $branchesValid); - - $packageElement->setAttribute('branch-rate', (string) $branchRate); - - $packageElement->setAttribute('complexity', ''); - $packagesElement->appendChild($packageElement); - - $classesElement = $document->createElement('classes'); - - $packageElement->appendChild($classesElement); - - $classes = $item->classesAndTraits(); - $coverageData = $item->lineCoverageData(); - - foreach ($classes as $className => $class) { - $complexity += $class['ccn']; - $packageComplexity += $class['ccn']; - - if (!empty($class['package']['namespace'])) { - $className = $class['package']['namespace'] . '\\' . $className; - } - - $linesValid = $class['executableLines']; - $linesCovered = $class['executedLines']; - $lineRate = $linesValid === 0 ? 0 : ($linesCovered / $linesValid); - - $branchesValid = $class['executableBranches']; - $branchesCovered = $class['executedBranches']; - $branchRate = $branchesValid === 0 ? 0 : ($branchesCovered / $branchesValid); - - $classElement = $document->createElement('class'); - - $classElement->setAttribute('name', $className); - $classElement->setAttribute('filename', str_replace($report->pathAsString() . DIRECTORY_SEPARATOR, '', $item->pathAsString())); - $classElement->setAttribute('line-rate', (string) $lineRate); - $classElement->setAttribute('branch-rate', (string) $branchRate); - $classElement->setAttribute('complexity', (string) $class['ccn']); - - $classesElement->appendChild($classElement); - - $methodsElement = $document->createElement('methods'); - - $classElement->appendChild($methodsElement); - - $classLinesElement = $document->createElement('lines'); - - $classElement->appendChild($classLinesElement); - - foreach ($class['methods'] as $methodName => $method) { - if ($method['executableLines'] === 0) { - continue; - } - - preg_match("/\((.*?)\)/", $method['signature'], $signature); - - $linesValid = $method['executableLines']; - $linesCovered = $method['executedLines']; - $lineRate = $linesValid === 0 ? 0 : ($linesCovered / $linesValid); - - $branchesValid = $method['executableBranches']; - $branchesCovered = $method['executedBranches']; - $branchRate = $branchesValid === 0 ? 0 : ($branchesCovered / $branchesValid); - - $methodElement = $document->createElement('method'); - - $methodElement->setAttribute('name', $methodName); - $methodElement->setAttribute('signature', $signature[1]); - $methodElement->setAttribute('line-rate', (string) $lineRate); - $methodElement->setAttribute('branch-rate', (string) $branchRate); - $methodElement->setAttribute('complexity', (string) $method['ccn']); - - $methodLinesElement = $document->createElement('lines'); - - $methodElement->appendChild($methodLinesElement); - - foreach (range($method['startLine'], $method['endLine']) as $line) { - if (!isset($coverageData[$line]) || $coverageData[$line] === null) { - continue; - } - $methodLineElement = $document->createElement('line'); - - $methodLineElement->setAttribute('number', (string) $line); - $methodLineElement->setAttribute('hits', (string) count($coverageData[$line])); - - $methodLinesElement->appendChild($methodLineElement); - - $classLineElement = $methodLineElement->cloneNode(); - - $classLinesElement->appendChild($classLineElement); - } - - $methodsElement->appendChild($methodElement); - } - } - - if ($report->numberOfFunctions() === 0) { - $packageElement->setAttribute('complexity', (string) $packageComplexity); - - continue; - } - - $functionsComplexity = 0; - $functionsLinesValid = 0; - $functionsLinesCovered = 0; - $functionsBranchesValid = 0; - $functionsBranchesCovered = 0; - - $classElement = $document->createElement('class'); - $classElement->setAttribute('name', basename($item->pathAsString())); - $classElement->setAttribute('filename', str_replace($report->pathAsString() . DIRECTORY_SEPARATOR, '', $item->pathAsString())); - - $methodsElement = $document->createElement('methods'); - - $classElement->appendChild($methodsElement); - - $classLinesElement = $document->createElement('lines'); - - $classElement->appendChild($classLinesElement); - - $functions = $report->functions(); - - foreach ($functions as $functionName => $function) { - if ($function['executableLines'] === 0) { - continue; - } - - $complexity += $function['ccn']; - $packageComplexity += $function['ccn']; - $functionsComplexity += $function['ccn']; - - $linesValid = $function['executableLines']; - $linesCovered = $function['executedLines']; - $lineRate = $linesValid === 0 ? 0 : ($linesCovered / $linesValid); - - $functionsLinesValid += $linesValid; - $functionsLinesCovered += $linesCovered; - - $branchesValid = $function['executableBranches']; - $branchesCovered = $function['executedBranches']; - $branchRate = $branchesValid === 0 ? 0 : ($branchesCovered / $branchesValid); - - $functionsBranchesValid += $branchesValid; - $functionsBranchesCovered += $branchesValid; - - $methodElement = $document->createElement('method'); - - $methodElement->setAttribute('name', $functionName); - $methodElement->setAttribute('signature', $function['signature']); - $methodElement->setAttribute('line-rate', (string) $lineRate); - $methodElement->setAttribute('branch-rate', (string) $branchRate); - $methodElement->setAttribute('complexity', (string) $function['ccn']); - - $methodLinesElement = $document->createElement('lines'); - - $methodElement->appendChild($methodLinesElement); - - foreach (range($function['startLine'], $function['endLine']) as $line) { - if (!isset($coverageData[$line]) || $coverageData[$line] === null) { - continue; - } - $methodLineElement = $document->createElement('line'); - - $methodLineElement->setAttribute('number', (string) $line); - $methodLineElement->setAttribute('hits', (string) count($coverageData[$line])); - - $methodLinesElement->appendChild($methodLineElement); - - $classLineElement = $methodLineElement->cloneNode(); - - $classLinesElement->appendChild($classLineElement); - } - - $methodsElement->appendChild($methodElement); - } - - $packageElement->setAttribute('complexity', (string) $packageComplexity); - - if ($functionsLinesValid === 0) { - continue; - } - - $lineRate = $functionsLinesCovered / $functionsLinesValid; - $branchRate = $functionsBranchesValid === 0 ? 0 : ($functionsBranchesCovered / $functionsBranchesValid); - - $classElement->setAttribute('line-rate', (string) $lineRate); - $classElement->setAttribute('branch-rate', (string) $branchRate); - $classElement->setAttribute('complexity', (string) $functionsComplexity); - - $classesElement->appendChild($classElement); - } - - $coverageElement->setAttribute('complexity', (string) $complexity); - - $buffer = $document->saveXML(); + $buffer = CoberturaCoverage::create($coverage->getReport())->generateDocument()->saveXML(); if ($target !== null) { Filesystem::createDirectory(dirname($target)); diff --git a/src/Report/Cobertura/CoberturaClass.php b/src/Report/Cobertura/CoberturaClass.php new file mode 100644 index 000000000..c08fc8eab --- /dev/null +++ b/src/Report/Cobertura/CoberturaClass.php @@ -0,0 +1,164 @@ +<?php declare(strict_types=1); +/* + * This file is part of phpunit/php-code-coverage. + * + * (c) Sebastian Bergmann <sebastian@phpunit.de> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace SebastianBergmann\CodeCoverage\Report\Cobertura; + +use function array_merge; +use function range; +use DOMDocument; +use DOMElement; + +class CoberturaClass extends CoberturaElement +{ + /** @var CoberturaMethod[] */ + private $methods = []; + + /** @var CoberturaLine[] */ + private $lines = []; + + /** + * @var string + */ + private $name; + + /** + * @var string + */ + private $filename; + + /** + * @var float + */ + private $complexity; + + public static function create(string $className, string $relativeFilePath, array $classData, array $lineCoverageData): self + { + if (!empty($classData['package']['namespace'])) { + $className = $classData['package']['namespace'] . '\\' . $className; + } + + $class = new self( + $className, + $relativeFilePath, + $classData['executableLines'], + $classData['executedLines'], + $classData['executableBranches'], + $classData['executedBranches'], + $classData['ccn'] + ); + + $endLine = $classData['startLine']; + + foreach ($classData['methods'] as $methodName => $methodData) { + $method = CoberturaMethod::create($methodName, $methodData, $lineCoverageData); + + if (null !== $method) { + $class->methods[] = $method; + } + + if ($methodData['endLine'] > $endLine) { + $endLine = $methodData['endLine']; + } + } + + /** @var int $lineNumber */ + foreach (range($classData['startLine'], $endLine) as $lineNumber) { + $line = CoberturaLine::create($lineNumber, $lineCoverageData); + + if (null !== $line) { + $class->lines[] = $line; + } + } + + return $class; + } + + public static function createForFunctions( + string $className, + string $relativeFilePath, + int $linesValid, + int $linesCovered, + int $branchesValid, + int $branchesCovered, + float $complexity, + array $functions + ): self { + $class = new self( + $className, + $relativeFilePath, + $linesValid, + $linesCovered, + $branchesValid, + $branchesCovered, + $complexity + ); + + $class->methods = $functions; + + foreach ($class->methods as $method) { + $class->lines = array_merge($class->lines, $method->getLines()); + } + + return $class; + } + + private function __construct( + string $name, + string $filename, + int $linesValid, + int $linesCovered, + int $branchesValid, + int $branchesCovered, + float $complexity + ) { + $this->name = $name; + $this->filename = $filename; + $this->complexity = $complexity; + parent::__construct($linesValid, $linesCovered, $branchesValid, $branchesCovered); + } + + public function wrap(DOMDocument $document): DOMElement + { + $classElement = $document->createElement('class'); + + $classElement->setAttribute('name', $this->name); + $classElement->setAttribute('filename', $this->filename); + $classElement->setAttribute('line-rate', (string) $this->lineRate()); + $classElement->setAttribute('branch-rate', (string) $this->branchRate()); + $classElement->setAttribute('complexity', (string) $this->complexity); + + $methodsElement = $document->createElement('methods'); + + foreach ($this->methods as $method) { + $methodsElement->appendChild($method->wrap($document)); + } + + $classElement->appendChild($methodsElement); + + $linesElement = $document->createElement('lines'); + + foreach ($this->lines as $line) { + $linesElement->appendChild($line->wrap($document)); + } + + $classElement->appendChild($linesElement); + + return $classElement; + } + + public function getComplexity(): float + { + return $this->complexity; + } + + public function getName(): string + { + return $this->name; + } +} diff --git a/src/Report/Cobertura/CoberturaCoverage.php b/src/Report/Cobertura/CoberturaCoverage.php new file mode 100644 index 000000000..10d839ccb --- /dev/null +++ b/src/Report/Cobertura/CoberturaCoverage.php @@ -0,0 +1,245 @@ +<?php declare(strict_types=1); +/* + * This file is part of phpunit/php-code-coverage. + * + * (c) Sebastian Bergmann <sebastian@phpunit.de> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace SebastianBergmann\CodeCoverage\Report\Cobertura; + +use function array_reduce; +use function basename; +use function count; +use function date; +use function getcwd; +use function in_array; +use function sprintf; +use function str_replace; +use function time; +use Composer\InstalledVersions; +use DOMDocument; +use DOMElement; +use DOMImplementation; +use SebastianBergmann\CodeCoverage\Node\AbstractNode; +use SebastianBergmann\CodeCoverage\Node\Directory; +use SebastianBergmann\CodeCoverage\Node\File; + +class CoberturaCoverage extends CoberturaElement +{ + private const FUNCTIONS_PACKAGE = '_functions'; + + /** @var string[] */ + private $sources = []; + + /** @var array<string, CoberturaPackage> */ + private $packages = []; + + /** + * @var int + */ + private $timestamp; + + public static function create(Directory $report): self + { + $coverage = new self( + time(), + $report->numberOfExecutableLines(), + $report->numberOfExecutedLines(), + $report->numberOfExecutableBranches(), + $report->numberOfExecutedBranches() + ); + + foreach ($report as $item) { + if (!$item instanceof File) { + continue; + } + + $coverage->processFile($item); + } + + return $coverage; + } + + private function __construct( + int $timestamp, + int $linesValid, + int $linesCovered, + int $branchesValid, + int $branchesCovered + ) { + $this->timestamp = $timestamp; + parent::__construct($linesValid, $linesCovered, $branchesValid, $branchesCovered); + } + + public function generateDocument(): DOMDocument + { + $implementation = new DOMImplementation; + + $documentType = $implementation->createDocumentType( + 'coverage', + '', + 'http://cobertura.sourceforge.net/xml/coverage-04.dtd' + ); + + $document = $implementation->createDocument('', '', $documentType); + $document->xmlVersion = '1.0'; + $document->encoding = 'UTF-8'; + $document->formatOutput = true; + + $comment = $document->createComment(sprintf( + 'Cobertura coverage report generated by the PHP library "phpunit/php-code-coverage" on %s.', + date('c', $this->timestamp), + )); + $document->appendChild($comment); + + $coverageElement = $document->createElement('coverage'); + $coverageElement->setAttribute('line-rate', (string) $this->lineRate()); + $coverageElement->setAttribute('branch-rate', (string) $this->branchRate()); + $coverageElement->setAttribute('lines-covered', (string) $this->linesCovered); + $coverageElement->setAttribute('lines-valid', (string) $this->linesValid); + $coverageElement->setAttribute('branches-covered', (string) $this->branchesCovered); + $coverageElement->setAttribute('branches-valid', (string) $this->branchesValid); + $coverageElement->setAttribute('complexity', (string) $this->complexity()); + $coverageElement->setAttribute('version', '0.4'); + $coverageElement->setAttribute('timestamp', (string) $this->timestamp); + + $coverageElement->appendChild($this->wrapSources($document)); + + $packagesElement = $document->createElement('packages'); + + foreach ($this->packages as $package) { + $packagesElement->appendChild($package->wrap($document)); + } + + $coverageElement->appendChild($packagesElement); + + $document->appendChild($coverageElement); + + return $document; + } + + private function processFile(File $file): void + { + $this->addSource($this->relativePath($this->fileRoot($file)->pathAsString())); + + $lineCoverageData = $file->lineCoverageData(); + + foreach ($file->classesAndTraits() as $className => $classData) { + $class = CoberturaClass::create( + $className, + $this->relativePath($file->pathAsString()), + $classData, + $lineCoverageData + ); + + $packageName = CoberturaPackage::packageName($class->getName()); + + if (!isset($this->packages[$packageName])) { + $this->packages[$packageName] = new CoberturaPackage($packageName); + } + + $this->packages[$packageName]->addClass($class); + } + + $this->processFunctions($file); + } + + private function processFunctions(File $file): void + { + $lineCoverageData = $file->lineCoverageData(); + + $functions = []; + $classComplexity = 0; + + foreach ($file->functions() as $functionName => $functionData) { + $method = CoberturaMethod::create($functionName, $functionData, $lineCoverageData); + + if (null !== $method) { + $functions[$functionName] = $method; + $classComplexity += $functionData['ccn']; + } + } + + if (count($functions) > 0) { + $classCoverageData = array_reduce($functions, static function (array $data, CoberturaMethod $function) + { + $data['linesValid'] += $function->getLinesValid(); + $data['linesCovered'] += $function->getLinesCovered(); + $data['branchesValid'] += $function->getBranchesValid(); + $data['branchesCovered'] += $function->getBranchesCovered(); + + return $data; + }, ['linesValid' => 0, 'linesCovered' => 0, 'branchesValid' => 0, 'branchesCovered' => 0]); + + $relativeFilePath = $this->relativePath($file->pathAsString()); + + $class = CoberturaClass::createForFunctions( + self::FUNCTIONS_PACKAGE . '\\' . basename($relativeFilePath), + $relativeFilePath, + $classCoverageData['linesValid'], + $classCoverageData['linesCovered'], + $classCoverageData['branchesValid'], + $classCoverageData['branchesCovered'], + $classComplexity, + $functions + ); + + if (!isset($this->packages[self::FUNCTIONS_PACKAGE])) { + $this->packages[self::FUNCTIONS_PACKAGE] = new CoberturaPackage(self::FUNCTIONS_PACKAGE); + } + + $this->packages[self::FUNCTIONS_PACKAGE]->addClass($class); + } + } + + private function addSource(string $source): void + { + if (!in_array($source, $this->sources, true)) { + $this->sources[] = $source; + } + } + + private function fileRoot(File $file): AbstractNode + { + $root = $file; + + while (true) { + if ($root->parent() === null) { + return $root; + } + + /** @var AbstractNode $root */ + $root = $root->parent(); + } + } + + private function relativePath(string $path): string + { + return str_replace(getcwd() . DIRECTORY_SEPARATOR, '', $path); + } + + private function wrapSources(DOMDocument $document): DOMElement + { + $sourcesElement = $document->createElement('sources'); + + foreach ($this->sources as $source) { + $sourcesElement->appendChild($document->createElement('source', $source)); + } + + return $sourcesElement; + } + + private function complexity(): float + { + return array_reduce( + $this->packages, + static function (float $complexity, CoberturaPackage $package) + { + return $complexity + $package->complexity(); + }, + 0 + ); + } +} diff --git a/src/Report/Cobertura/CoberturaElement.php b/src/Report/Cobertura/CoberturaElement.php new file mode 100644 index 000000000..72c0f473a --- /dev/null +++ b/src/Report/Cobertura/CoberturaElement.php @@ -0,0 +1,71 @@ +<?php declare(strict_types=1); +/* + * This file is part of phpunit/php-code-coverage. + * + * (c) Sebastian Bergmann <sebastian@phpunit.de> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace SebastianBergmann\CodeCoverage\Report\Cobertura; + +abstract class CoberturaElement +{ + /** + * @var int + */ + protected $linesValid; + + /** + * @var int + */ + protected $linesCovered; + + /** + * @var int + */ + protected $branchesValid; + + /** + * @var int + */ + protected $branchesCovered; + + public function __construct(int $linesValid, int $linesCovered, int $branchesValid, int $branchesCovered) + { + $this->linesValid = $linesValid; + $this->linesCovered = $linesCovered; + $this->branchesValid = $branchesValid; + $this->branchesCovered = $branchesCovered; + } + + public function getLinesValid(): int + { + return $this->linesValid; + } + + public function getLinesCovered(): int + { + return $this->linesCovered; + } + + public function getBranchesValid(): int + { + return $this->branchesValid; + } + + public function getBranchesCovered(): int + { + return $this->branchesCovered; + } + + protected function lineRate(): float + { + return $this->linesValid === 0 ? 0 : $this->linesCovered / $this->linesValid; + } + + protected function branchRate(): float + { + return $this->branchesValid === 0 ? 0 : $this->branchesCovered / $this->branchesValid; + } +} diff --git a/src/Report/Cobertura/CoberturaLine.php b/src/Report/Cobertura/CoberturaLine.php new file mode 100644 index 000000000..bc31c02bf --- /dev/null +++ b/src/Report/Cobertura/CoberturaLine.php @@ -0,0 +1,62 @@ +<?php declare(strict_types=1); +/* + * This file is part of phpunit/php-code-coverage. + * + * (c) Sebastian Bergmann <sebastian@phpunit.de> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace SebastianBergmann\CodeCoverage\Report\Cobertura; + +use function count; +use DOMDocument; +use DOMElement; + +class CoberturaLine +{ + /** + * @var int + */ + private $number; + + /** + * @var int + */ + private $hits; + + /** + * @var null|bool + */ + private $branch; + + public static function create(int $lineNumber, array $lineCoverageData): ?self + { + if (!isset($lineCoverageData[$lineNumber])) { + return null; + } + + return new self($lineNumber, count($lineCoverageData[$lineNumber])); + } + + private function __construct(int $number, int $hits, ?bool $branch = null) + { + $this->number = $number; + $this->hits = $hits; + $this->branch = $branch; + } + + public function wrap(DOMDocument $document): DOMElement + { + $element = $document->createElement('line'); + + $element->setAttribute('number', (string) $this->number); + $element->setAttribute('hits', (string) $this->hits); + + if (null !== $this->branch) { + $element->setAttribute('branch', $this->branch ? 'true' : 'false'); + } + + return $element; + } +} diff --git a/src/Report/Cobertura/CoberturaMethod.php b/src/Report/Cobertura/CoberturaMethod.php new file mode 100644 index 000000000..cc0acfaf4 --- /dev/null +++ b/src/Report/Cobertura/CoberturaMethod.php @@ -0,0 +1,104 @@ +<?php declare(strict_types=1); +/* + * This file is part of phpunit/php-code-coverage. + * + * (c) Sebastian Bergmann <sebastian@phpunit.de> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace SebastianBergmann\CodeCoverage\Report\Cobertura; + +use function range; +use DOMDocument; +use DOMElement; + +class CoberturaMethod extends CoberturaElement +{ + /** @var CoberturaLine[] */ + private $lines = []; + + /** + * @var string + */ + private $name; + + /** + * @var string + */ + private $signature; + + /** + * @var float + */ + private $complexity; + + public static function create(string $name, array $methodData, array $lineCoverageData): ?self + { + if ($methodData['executableLines'] === 0) { + return null; + } + + $method = new self( + $name, + $methodData['signature'], + $methodData['executableLines'], + $methodData['executedLines'], + $methodData['executableBranches'], + $methodData['executedBranches'], + $methodData['ccn'] + ); + + /** @var int $lineNumber */ + foreach (range($methodData['startLine'], $methodData['endLine']) as $lineNumber) { + $line = CoberturaLine::create($lineNumber, $lineCoverageData); + + if (null !== $line) { + $method->lines[] = $line; + } + } + + return $method; + } + + private function __construct( + string $name, + string $signature, + int $linesValid, + int $linesCovered, + int $branchesValid, + int $branchesCovered, + float $complexity + ) { + $this->name = $name; + $this->signature = $signature; + $this->complexity = $complexity; + parent::__construct($linesValid, $linesCovered, $branchesValid, $branchesCovered); + } + + public function wrap(DOMDocument $document): DOMElement + { + $methodElement = $document->createElement('method'); + + $methodElement->setAttribute('name', $this->name); + $methodElement->setAttribute('signature', $this->signature); + $methodElement->setAttribute('line-rate', (string) $this->lineRate()); + $methodElement->setAttribute('branch-rate', (string) $this->branchRate()); + $methodElement->setAttribute('complexity', (string) $this->complexity); + + $linesElement = $document->createElement('lines'); + + foreach ($this->lines as $line) { + $linesElement->appendChild($line->wrap($document)); + } + + $methodElement->appendChild($linesElement); + + return $methodElement; + } + + public function getLines(): array + { + return $this->lines; + } +} diff --git a/src/Report/Cobertura/CoberturaPackage.php b/src/Report/Cobertura/CoberturaPackage.php new file mode 100644 index 000000000..c204ded8a --- /dev/null +++ b/src/Report/Cobertura/CoberturaPackage.php @@ -0,0 +1,99 @@ +<?php declare(strict_types=1); +/* + * This file is part of phpunit/php-code-coverage. + * + * (c) Sebastian Bergmann <sebastian@phpunit.de> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace SebastianBergmann\CodeCoverage\Report\Cobertura; + +use function array_reduce; +use function explode; +use DOMDocument; +use DOMElement; + +class CoberturaPackage +{ + /** @var CoberturaClass[] */ + private $classes = []; + + /** + * @var string + */ + private $name; + + public static function packageName(string $className): string + { + return explode('\\', $className)[0]; + } + + public function __construct(string $name) + { + $this->name = $name; + } + + public function addClass(CoberturaClass $class): void + { + $this->classes[] = $class; + } + + public function wrap(DOMDocument $document): DOMElement + { + $packageElement = $document->createElement('package'); + + $packageElement->setAttribute('name', $this->name); + $packageElement->setAttribute('line-rate', (string) $this->lineRate()); + $packageElement->setAttribute('branch-rate', (string) $this->branchRate()); + $packageElement->setAttribute('complexity', (string) $this->complexity()); + + $classesElement = $document->createElement('classes'); + + foreach ($this->classes as $class) { + $classesElement->appendChild($class->wrap($document)); + } + + $packageElement->appendChild($classesElement); + + return $packageElement; + } + + public function complexity(): float + { + return array_reduce( + $this->classes, + static function (float $complexity, CoberturaClass $class) + { + return $complexity + $class->getComplexity(); + }, + 0 + ); + } + + private function lineRate(): float + { + $linesData = array_reduce($this->classes, static function (array $data, CoberturaClass $class) + { + $data['valid'] += $class->getLinesValid(); + $data['covered'] += $class->getLinesCovered(); + + return $data; + }, ['valid' => 0, 'covered' => 0]); + + return $linesData['valid'] === 0 ? 0 : $linesData['covered'] / $linesData['valid']; + } + + private function branchRate(): float + { + $branchesData = array_reduce($this->classes, static function (array $data, CoberturaClass $class) + { + $data['valid'] += $class->getBranchesValid(); + $data['covered'] += $class->getBranchesCovered(); + + return $data; + }, ['valid' => 0, 'covered' => 0]); + + return $branchesData['valid'] === 0 ? 0 : $branchesData['covered'] / $branchesData['valid']; + } +} diff --git a/tests/_files/BankAccount-cobertura-line.xml b/tests/_files/BankAccount-cobertura-line.xml index 75401a6cb..85dcfaa8c 100644 --- a/tests/_files/BankAccount-cobertura-line.xml +++ b/tests/_files/BankAccount-cobertura-line.xml @@ -1,33 +1,34 @@ <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE coverage SYSTEM "http://cobertura.sourceforge.net/xml/coverage-04.dtd"> +<!--Cobertura coverage report generated by the PHP library "phpunit/php-code-coverage" on %s.--> <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"> <sources> - <source>%s</source> + <source>tests%e_files</source> </sources> <packages> - <package name="BankAccount.php" line-rate="0.625" branch-rate="0" complexity="5"> + <package name="BankAccount" line-rate="0.625" branch-rate="0" complexity="5"> <classes> - <class name="BankAccount" filename="BankAccount.php" line-rate="0.625" branch-rate="0" complexity="5"> + <class name="BankAccount" filename="tests%e_files%eBankAccount.php" line-rate="0.625" branch-rate="0" complexity="5"> <methods> - <method name="getBalance" signature="" line-rate="1" branch-rate="0" complexity="1"> + <method name="getBalance" signature="getBalance()" line-rate="1" branch-rate="0" complexity="1"> <lines> <line number="8" hits="2"/> </lines> </method> - <method name="setBalance" signature="$balance" line-rate="0" branch-rate="0" complexity="2"> + <method name="setBalance" signature="setBalance($balance)" line-rate="0" branch-rate="0" complexity="2"> <lines> <line number="13" hits="0"/> <line number="14" hits="0"/> <line number="16" hits="0"/> </lines> </method> - <method name="depositMoney" signature="$balance" line-rate="1" branch-rate="0" complexity="1"> + <method name="depositMoney" signature="depositMoney($balance)" line-rate="1" branch-rate="0" complexity="1"> <lines> <line number="22" hits="2"/> <line number="24" hits="1"/> </lines> </method> - <method name="withdrawMoney" signature="$balance" line-rate="1" branch-rate="0" complexity="1"> + <method name="withdrawMoney" signature="withdrawMoney($balance)" line-rate="1" branch-rate="0" complexity="1"> <lines> <line number="29" hits="2"/> <line number="31" hits="1"/> diff --git a/tests/_files/BankAccount-cobertura-path.xml b/tests/_files/BankAccount-cobertura-path.xml index 9ce9efe6e..fecd9e63b 100644 --- a/tests/_files/BankAccount-cobertura-path.xml +++ b/tests/_files/BankAccount-cobertura-path.xml @@ -1,33 +1,34 @@ <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE coverage SYSTEM "http://cobertura.sourceforge.net/xml/coverage-04.dtd"> +<!--Cobertura coverage report generated by the PHP library "phpunit/php-code-coverage" on %s.--> <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"> <sources> - <source>%s</source> + <source>tests%e_files</source> </sources> <packages> - <package name="BankAccount.php" line-rate="0.625" branch-rate="0.42857142857143" complexity="5"> + <package name="BankAccount" line-rate="0.625" branch-rate="0.42857142857143" complexity="5"> <classes> - <class name="BankAccount" filename="BankAccount.php" line-rate="0.625" branch-rate="0.42857142857143" complexity="5"> + <class name="BankAccount" filename="tests%e_files%eBankAccount.php" line-rate="0.625" branch-rate="0.42857142857143" complexity="5"> <methods> - <method name="getBalance" signature="" line-rate="1" branch-rate="1" complexity="1"> + <method name="getBalance" signature="getBalance()" line-rate="1" branch-rate="1" complexity="1"> <lines> <line number="8" hits="2"/> </lines> </method> - <method name="setBalance" signature="$balance" line-rate="0" branch-rate="0" complexity="2"> + <method name="setBalance" signature="setBalance($balance)" line-rate="0" branch-rate="0" complexity="2"> <lines> <line number="13" hits="0"/> <line number="14" hits="0"/> <line number="16" hits="0"/> </lines> </method> - <method name="depositMoney" signature="$balance" line-rate="1" branch-rate="1" complexity="1"> + <method name="depositMoney" signature="depositMoney($balance)" line-rate="1" branch-rate="1" complexity="1"> <lines> <line number="22" hits="2"/> <line number="24" hits="1"/> </lines> </method> - <method name="withdrawMoney" signature="$balance" line-rate="1" branch-rate="1" complexity="1"> + <method name="withdrawMoney" signature="withdrawMoney($balance)" line-rate="1" branch-rate="1" complexity="1"> <lines> <line number="29" hits="2"/> <line number="31" hits="1"/> diff --git a/tests/_files/class-with-anonymous-function-cobertura.xml b/tests/_files/class-with-anonymous-function-cobertura.xml index 3beb9d9ff..0de293cee 100644 --- a/tests/_files/class-with-anonymous-function-cobertura.xml +++ b/tests/_files/class-with-anonymous-function-cobertura.xml @@ -1,15 +1,16 @@ <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE coverage SYSTEM "http://cobertura.sourceforge.net/xml/coverage-04.dtd"> +<!--Cobertura coverage report generated by the PHP library "phpunit/php-code-coverage" on %s.--> <coverage line-rate="1" branch-rate="0" lines-covered="8" lines-valid="8" branches-covered="0" branches-valid="0" complexity="1" version="0.4" timestamp="%i"> <sources> - <source>%s</source> + <source>tests%e_files</source> </sources> <packages> - <package name="source_with_class_and_anonymous_function.php" line-rate="1" branch-rate="0" complexity="1"> + <package name="CoveredClassWithAnonymousFunctionInStaticMethod" line-rate="1" branch-rate="0" complexity="1"> <classes> - <class name="CoveredClassWithAnonymousFunctionInStaticMethod" filename="source_with_class_and_anonymous_function.php" line-rate="1" branch-rate="0" complexity="1"> + <class name="CoveredClassWithAnonymousFunctionInStaticMethod" filename="tests%e_files%esource_with_class_and_anonymous_function.php" line-rate="1" branch-rate="0" complexity="1"> <methods> - <method name="runAnonymous" signature="" line-rate="1" branch-rate="0" complexity="1"> + <method name="runAnonymous" signature="runAnonymous()" line-rate="1" branch-rate="0" complexity="1"> <lines> <line number="7" hits="1"/> <line number="9" hits="1"/> diff --git a/tests/_files/class-with-outside-function-cobertura.xml b/tests/_files/class-with-outside-function-cobertura.xml index fe6c005fc..fe66f3ae9 100644 --- a/tests/_files/class-with-outside-function-cobertura.xml +++ b/tests/_files/class-with-outside-function-cobertura.xml @@ -1,15 +1,16 @@ <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE coverage SYSTEM "http://cobertura.sourceforge.net/xml/coverage-04.dtd"> +<!--Cobertura coverage report generated by the PHP library "phpunit/php-code-coverage" on %s.--> <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"> <sources> - <source>%s</source> + <source>tests%e_files</source> </sources> <packages> - <package name="source_with_class_and_outside_function.php" line-rate="0.75" branch-rate="0" complexity="3"> + <package name="ClassInFileWithOutsideFunction" line-rate="1" branch-rate="0" complexity="1"> <classes> - <class name="ClassInFileWithOutsideFunction" filename="source_with_class_and_outside_function.php" line-rate="1" branch-rate="0" complexity="1"> + <class name="ClassInFileWithOutsideFunction" filename="tests%e_files%esource_with_class_and_outside_function.php" line-rate="1" branch-rate="0" complexity="1"> <methods> - <method name="classMethod" signature="" line-rate="1" branch-rate="0" complexity="1"> + <method name="classMethod" signature="classMethod(): string" line-rate="1" branch-rate="0" complexity="1"> <lines> <line number="6" hits="1"/> </lines> @@ -19,7 +20,11 @@ <line number="6" hits="1"/> </lines> </class> - <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"> + </classes> + </package> + <package name="_functions" line-rate="0.66666666666667" branch-rate="0" complexity="2"> + <classes> + <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"> <methods> <method name="outsideFunction" signature="outsideFunction(bool $test): int" line-rate="0.66666666666667" branch-rate="0" complexity="2"> <lines> diff --git a/tests/_files/ignored-lines-cobertura.xml b/tests/_files/ignored-lines-cobertura.xml index 2650a3dde..81427d442 100644 --- a/tests/_files/ignored-lines-cobertura.xml +++ b/tests/_files/ignored-lines-cobertura.xml @@ -1,17 +1,22 @@ <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE coverage SYSTEM "http://cobertura.sourceforge.net/xml/coverage-04.dtd"> +<!--Cobertura coverage report generated by the PHP library "phpunit/php-code-coverage" on %s.--> <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"> <sources> - <source>%s</source> + <source>tests%e_files</source> </sources> <packages> - <package name="source_with_ignore.php" line-rate="1" branch-rate="0" complexity="2"> + <package name="Foo" line-rate="0" branch-rate="0" complexity="1"> <classes> - <class name="Foo" filename="source_with_ignore.php" line-rate="0" branch-rate="0" complexity="1"> + <class name="Foo" filename="tests%e_files%esource_with_ignore.php" line-rate="0" branch-rate="0" complexity="1"> <methods/> <lines/> </class> - <class name="Bar" filename="source_with_ignore.php" line-rate="0" branch-rate="0" complexity="1"> + </classes> + </package> + <package name="Bar" line-rate="0" branch-rate="0" complexity="1"> + <classes> + <class name="Bar" filename="tests%e_files%esource_with_ignore.php" line-rate="0" branch-rate="0" complexity="1"> <methods/> <lines/> </class> diff --git a/tests/tests/Report/CoberturaTest.php b/tests/tests/Report/CoberturaTest.php index 05163ed2d..131ef5a76 100644 --- a/tests/tests/Report/CoberturaTest.php +++ b/tests/tests/Report/CoberturaTest.php @@ -9,60 +9,67 @@ */ namespace SebastianBergmann\CodeCoverage\Report; +use DOMDocument; use SebastianBergmann\CodeCoverage\TestCase; -/** - * @covers \SebastianBergmann\CodeCoverage\Report\Cobertura - */ final class CoberturaTest extends TestCase { public function testLineCoverageForBankAccountTest(): void { - $cobertura = new Cobertura; + $report = (new Cobertura)->process($this->getLineCoverageForBankAccount(), null); $this->assertStringMatchesFormatFile( TEST_FILES_PATH . 'BankAccount-cobertura-line.xml', - $cobertura->process($this->getLineCoverageForBankAccount(), null) + $report ); + + $this->validateReport($report); } public function testPathCoverageForBankAccountTest(): void { - $cobertura = new Cobertura; + $report = (new Cobertura)->process($this->getPathCoverageForBankAccount(), null); $this->assertStringMatchesFormatFile( TEST_FILES_PATH . 'BankAccount-cobertura-path.xml', - $cobertura->process($this->getPathCoverageForBankAccount(), null) + $report ); } public function testCoberturaForFileWithIgnoredLines(): void { - $cobertura = new Cobertura; + $report = (new Cobertura)->process($this->getCoverageForFileWithIgnoredLines()); $this->assertStringMatchesFormatFile( TEST_FILES_PATH . 'ignored-lines-cobertura.xml', - $cobertura->process($this->getCoverageForFileWithIgnoredLines()) + $report ); } public function testCoberturaForClassWithAnonymousFunction(): void { - $cobertura = new Cobertura; + $report = (new Cobertura)->process($this->getCoverageForClassWithAnonymousFunction()); $this->assertStringMatchesFormatFile( TEST_FILES_PATH . 'class-with-anonymous-function-cobertura.xml', - $cobertura->process($this->getCoverageForClassWithAnonymousFunction()) + $report ); } public function testCoberturaForClassAndOutsideFunction(): void { - $cobertura = new Cobertura; + $report = (new Cobertura)->process($this->getCoverageForClassWithOutsideFunction()); $this->assertStringMatchesFormatFile( TEST_FILES_PATH . 'class-with-outside-function-cobertura.xml', - $cobertura->process($this->getCoverageForClassWithOutsideFunction()) + $report ); } + + private function validateReport(string $coberturaReport): void + { + $document = (new DOMDocument); + $this->assertTrue($document->loadXML($coberturaReport)); + $this->assertTrue(@$document->validate()); + } }