diff --git a/src/Node/AbstractNode.php b/src/Node/AbstractNode.php index 2cee547f9..7e82a3daf 100644 --- a/src/Node/AbstractNode.php +++ b/src/Node/AbstractNode.php @@ -178,6 +178,24 @@ public function numberOfTestedFunctionsAndMethods(): int return $this->numberOfTestedFunctions() + $this->numberOfTestedMethods(); } + /** + * @return non-negative-int + */ + public function cyclomaticComplexity(): int + { + $ccn = 0; + + foreach ($this->classesAndTraits() as $classLike) { + $ccn += $classLike['ccn']; + } + + foreach ($this->functions() as $function) { + $ccn += $function['ccn']; + } + + return $ccn; + } + /** * @return array */ diff --git a/src/Report/OpenClover.php b/src/Report/OpenClover.php new file mode 100644 index 000000000..042132b33 --- /dev/null +++ b/src/Report/OpenClover.php @@ -0,0 +1,257 @@ + + * + * 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 $packages */ + $packages = []; + $report = $coverage->getReport(); + + foreach ($report as $item) { + if (!$item instanceof File) { + continue; + } + + $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']; + } + + 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)); + } + + if (@file_put_contents($target, $buffer) === false) { + throw new WriteOperationFailedException($target); + } + } + + return $buffer; + } +} diff --git a/tests/_files/BankAccount-clover-line.xml b/tests/_files/Report/Clover/BankAccount-line.xml similarity index 100% rename from tests/_files/BankAccount-clover-line.xml rename to tests/_files/Report/Clover/BankAccount-line.xml diff --git a/tests/_files/BankAccount-clover-path.xml b/tests/_files/Report/Clover/BankAccount-path.xml similarity index 100% rename from tests/_files/BankAccount-clover-path.xml rename to tests/_files/Report/Clover/BankAccount-path.xml diff --git a/tests/_files/class-with-anonymous-function-clover.xml b/tests/_files/Report/Clover/class-with-anonymous-function.xml similarity index 100% rename from tests/_files/class-with-anonymous-function-clover.xml rename to tests/_files/Report/Clover/class-with-anonymous-function.xml diff --git a/tests/_files/ignored-lines-clover.xml b/tests/_files/Report/Clover/ignored-lines.xml similarity index 100% rename from tests/_files/ignored-lines-clover.xml rename to tests/_files/Report/Clover/ignored-lines.xml diff --git a/tests/_files/BankAccount-cobertura-line.xml b/tests/_files/Report/Cobertura/BankAccount-line.xml similarity index 100% rename from tests/_files/BankAccount-cobertura-line.xml rename to tests/_files/Report/Cobertura/BankAccount-line.xml diff --git a/tests/_files/BankAccount-cobertura-path.xml b/tests/_files/Report/Cobertura/BankAccount-path.xml similarity index 100% rename from tests/_files/BankAccount-cobertura-path.xml rename to tests/_files/Report/Cobertura/BankAccount-path.xml diff --git a/tests/_files/class-with-anonymous-function-cobertura.xml b/tests/_files/Report/Cobertura/class-with-anonymous-function.xml similarity index 100% rename from tests/_files/class-with-anonymous-function-cobertura.xml rename to tests/_files/Report/Cobertura/class-with-anonymous-function.xml diff --git a/tests/_files/class-with-outside-function-cobertura.xml b/tests/_files/Report/Cobertura/class-with-outside-function.xml similarity index 100% rename from tests/_files/class-with-outside-function-cobertura.xml rename to tests/_files/Report/Cobertura/class-with-outside-function.xml diff --git a/tests/_files/ignored-lines-cobertura.xml b/tests/_files/Report/Cobertura/ignored-lines.xml similarity index 100% rename from tests/_files/ignored-lines-cobertura.xml rename to tests/_files/Report/Cobertura/ignored-lines.xml diff --git a/tests/_files/BankAccount-crap4j.xml b/tests/_files/Report/Crap4j/BankAccount.xml similarity index 100% rename from tests/_files/BankAccount-crap4j.xml rename to tests/_files/Report/Crap4j/BankAccount.xml diff --git a/tests/_files/class-with-anonymous-function-crap4j.xml b/tests/_files/Report/Crap4j/class-with-anonymous-function.xml similarity index 100% rename from tests/_files/class-with-anonymous-function-crap4j.xml rename to tests/_files/Report/Crap4j/class-with-anonymous-function.xml diff --git a/tests/_files/ignored-lines-crap4j.xml b/tests/_files/Report/Crap4j/ignored-lines.xml similarity index 100% rename from tests/_files/ignored-lines-crap4j.xml rename to tests/_files/Report/Crap4j/ignored-lines.xml diff --git a/tests/_files/Report/OpenClover/BankAccount-line.xml b/tests/_files/Report/OpenClover/BankAccount-line.xml new file mode 100644 index 000000000..eef39f2aa --- /dev/null +++ b/tests/_files/Report/OpenClover/BankAccount-line.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/_files/Report/OpenClover/BankAccount-path.xml b/tests/_files/Report/OpenClover/BankAccount-path.xml new file mode 100644 index 000000000..2612cb850 --- /dev/null +++ b/tests/_files/Report/OpenClover/BankAccount-path.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/_files/Report/OpenClover/class-with-anonymous-function.xml b/tests/_files/Report/OpenClover/class-with-anonymous-function.xml new file mode 100644 index 000000000..2ec261e6c --- /dev/null +++ b/tests/_files/Report/OpenClover/class-with-anonymous-function.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/_files/Report/OpenClover/clover.xsd b/tests/_files/Report/OpenClover/clover.xsd new file mode 100644 index 000000000..3b41fac49 --- /dev/null +++ b/tests/_files/Report/OpenClover/clover.xsd @@ -0,0 +1,236 @@ + + + + + + + Top-most element describing the coverage report. Contains a + project and a test project. + + + + + + + + + + + + + + Project metrics relating to non-test source. + @name - project name (optional) + @timestamp - seconds since UTC + + + + + + + + + + + + + + + Project metrics relating to test source. + @name - project name (optional) + @timestamp - seconds since UTC + + + + + + + + + + + + + + + Package metrics. + @name - the.package.name + + + + + + + + + + + + + + File metrics. + @name - the file name e.g. Foo.java or Bar.groovy + @path - the filesystem-specific original path to the file e.g. c:\path\to\Bar.groovy + + + + + + + + + + + + + + + + Class metrics. + @name - the unqualified class name + + + + + + + + + + + + + + + + + + + + + Line-specific information. + @line - the line number + @type - the type of syntactic construct - one of method|stmt|cond + @complexity - only applicable if @type == 'method'; the cyclomatic complexity of the construct + @count - only applicable if @type == 'stmt' or 'method'; the number of times the construct was executed + @truecount - only applicable if @type == 'cond'; the number of times the true branch was executed + @falsecount - only applicable if @type == 'cond'; the number of times the false branch was executed + @signature - only applicable if @type == 'method'; the signature of the method + @testduration - only applicable if @type == 'method' and the method was identified as a test method; the duration of the test + @testsuccess - only applicable if @type == 'method' and the method was identified as a test method; true if the test passed, false otherwise + @visibility - only applicable if @type == 'method' + + + + + + + + + + + + + + + + + + + + Metrics information for projects/packages/files/classes. + @complexity - the cyclomatic complexity + @conditionals - the number of contained conditionals (2 * number of branches) + @coveredconditionals - the number of contained conditionals (2 * number of branches) with coverage + @elements - the number of contained statements, conditionals and methods + @coveredelements - the number of contained statements, conditionals and methods with coverage + @statements - the number of contained statements + @coveredstatements - the number of contained statements with coverage + @methods - the number of contained methods + @coveredmethods - the number of contained methods with coverage + @testduration - the total duration of all contained test methods + @testfailures - the total number of test method failures + @testpasses - the total number of test method passes + @testruns - the total number of test methods run + + + + + + + + + + + + + + + + + + + + + Metrics information for projects/packages/files. + @classes - the total number of contained classes + @loc - the total number of lines of code + @ncloc - the total number of non-comment lines of code + + + + + + + + + + + + + + Metrics information for projects/packages. + @files - the total number of contained files + + + + + + + + + + + + Metrics information for projects. + @files - the total number of packages + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/_files/Report/OpenClover/ignored-lines.xml b/tests/_files/Report/OpenClover/ignored-lines.xml new file mode 100644 index 000000000..063db4b79 --- /dev/null +++ b/tests/_files/Report/OpenClover/ignored-lines.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/tests/_files/BankAccount-text-line.txt b/tests/_files/Report/Text/BankAccount-line.txt similarity index 100% rename from tests/_files/BankAccount-text-line.txt rename to tests/_files/Report/Text/BankAccount-line.txt diff --git a/tests/_files/BankAccount-text-path.txt b/tests/_files/Report/Text/BankAccount-path.txt similarity index 100% rename from tests/_files/BankAccount-text-path.txt rename to tests/_files/Report/Text/BankAccount-path.txt diff --git a/tests/_files/BankAccount-text-summary.txt b/tests/_files/Report/Text/BankAccount-summary.txt similarity index 100% rename from tests/_files/BankAccount-text-summary.txt rename to tests/_files/Report/Text/BankAccount-summary.txt diff --git a/tests/_files/BankAccountWithUncovered-text-line.txt b/tests/_files/Report/Text/BankAccountWithUncovered-line.txt similarity index 100% rename from tests/_files/BankAccountWithUncovered-text-line.txt rename to tests/_files/Report/Text/BankAccountWithUncovered-line.txt diff --git a/tests/_files/BankAccountWithoutUncovered-text-line.txt b/tests/_files/Report/Text/BankAccountWithoutUncovered-line.txt similarity index 100% rename from tests/_files/BankAccountWithoutUncovered-text-line.txt rename to tests/_files/Report/Text/BankAccountWithoutUncovered-line.txt diff --git a/tests/_files/NamespacedBankAccount-text-with-colors.txt b/tests/_files/Report/Text/NamespacedBankAccount-colors.txt similarity index 100% rename from tests/_files/NamespacedBankAccount-text-with-colors.txt rename to tests/_files/Report/Text/NamespacedBankAccount-colors.txt diff --git a/tests/_files/NamespacedBankAccount-text.txt b/tests/_files/Report/Text/NamespacedBankAccount.txt similarity index 100% rename from tests/_files/NamespacedBankAccount-text.txt rename to tests/_files/Report/Text/NamespacedBankAccount.txt diff --git a/tests/_files/class-with-anonymous-function-text.txt b/tests/_files/Report/Text/class-with-anonymous-function.txt similarity index 100% rename from tests/_files/class-with-anonymous-function-text.txt rename to tests/_files/Report/Text/class-with-anonymous-function.txt diff --git a/tests/_files/ignored-lines-text.txt b/tests/_files/Report/Text/ignored-lines.txt similarity index 100% rename from tests/_files/ignored-lines-text.txt rename to tests/_files/Report/Text/ignored-lines.txt diff --git a/tests/tests/Report/CloverTest.php b/tests/tests/Report/CloverTest.php index 434415772..114642061 100644 --- a/tests/tests/Report/CloverTest.php +++ b/tests/tests/Report/CloverTest.php @@ -20,7 +20,7 @@ public function testLineCoverageForBankAccountTest(): void $clover = new Clover; $this->assertStringMatchesFormatFile( - TEST_FILES_PATH . 'BankAccount-clover-line.xml', + TEST_FILES_PATH . 'Report/Clover/BankAccount-line.xml', $clover->process($this->getLineCoverageForBankAccount(), null, 'BankAccount'), ); } @@ -30,7 +30,7 @@ public function testPathCoverageForBankAccountTest(): void $clover = new Clover; $this->assertStringMatchesFormatFile( - TEST_FILES_PATH . 'BankAccount-clover-path.xml', + TEST_FILES_PATH . 'Report/Clover/BankAccount-path.xml', $clover->process($this->getPathCoverageForBankAccount(), null, 'BankAccount'), ); } @@ -40,7 +40,7 @@ public function testCloverForFileWithIgnoredLines(): void $clover = new Clover; $this->assertStringMatchesFormatFile( - TEST_FILES_PATH . 'ignored-lines-clover.xml', + TEST_FILES_PATH . 'Report/Clover/ignored-lines.xml', $clover->process($this->getCoverageForFileWithIgnoredLines()), ); } @@ -50,7 +50,7 @@ public function testCloverForClassWithAnonymousFunction(): void $clover = new Clover; $this->assertStringMatchesFormatFile( - TEST_FILES_PATH . 'class-with-anonymous-function-clover.xml', + TEST_FILES_PATH . 'Report/Clover/class-with-anonymous-function.xml', $clover->process($this->getCoverageForClassWithAnonymousFunction()), ); } diff --git a/tests/tests/Report/CoberturaTest.php b/tests/tests/Report/CoberturaTest.php index 2694bd1e4..48f0667d2 100644 --- a/tests/tests/Report/CoberturaTest.php +++ b/tests/tests/Report/CoberturaTest.php @@ -20,7 +20,7 @@ public function testLineCoverageForBankAccountTest(): void $cobertura = new Cobertura; $this->assertStringMatchesFormatFile( - TEST_FILES_PATH . 'BankAccount-cobertura-line.xml', + TEST_FILES_PATH . 'Report/Cobertura/BankAccount-line.xml', $cobertura->process($this->getLineCoverageForBankAccount(), null), ); } @@ -30,7 +30,7 @@ public function testPathCoverageForBankAccountTest(): void $cobertura = new Cobertura; $this->assertStringMatchesFormatFile( - TEST_FILES_PATH . 'BankAccount-cobertura-path.xml', + TEST_FILES_PATH . 'Report/Cobertura/BankAccount-path.xml', $cobertura->process($this->getPathCoverageForBankAccount(), null), ); } @@ -40,7 +40,7 @@ public function testCoberturaForFileWithIgnoredLines(): void $cobertura = new Cobertura; $this->assertStringMatchesFormatFile( - TEST_FILES_PATH . 'ignored-lines-cobertura.xml', + TEST_FILES_PATH . 'Report/Cobertura/ignored-lines.xml', $cobertura->process($this->getCoverageForFileWithIgnoredLines()), ); } @@ -50,7 +50,7 @@ public function testCoberturaForClassWithAnonymousFunction(): void $cobertura = new Cobertura; $this->assertStringMatchesFormatFile( - TEST_FILES_PATH . 'class-with-anonymous-function-cobertura.xml', + TEST_FILES_PATH . 'Report/Cobertura/class-with-anonymous-function.xml', $cobertura->process($this->getCoverageForClassWithAnonymousFunction()), ); } @@ -60,7 +60,7 @@ public function testCoberturaForClassAndOutsideFunction(): void $cobertura = new Cobertura; $this->assertStringMatchesFormatFile( - TEST_FILES_PATH . 'class-with-outside-function-cobertura.xml', + TEST_FILES_PATH . 'Report/Cobertura/class-with-outside-function.xml', $cobertura->process($this->getCoverageForClassWithOutsideFunction()), ); } diff --git a/tests/tests/Report/Crap4jTest.php b/tests/tests/Report/Crap4jTest.php index 72f029027..8e0fb1b06 100644 --- a/tests/tests/Report/Crap4jTest.php +++ b/tests/tests/Report/Crap4jTest.php @@ -20,7 +20,7 @@ public function testForBankAccountTest(): void $crap4j = new Crap4j; $this->assertStringMatchesFormatFile( - TEST_FILES_PATH . 'BankAccount-crap4j.xml', + TEST_FILES_PATH . 'Report/Crap4j/BankAccount.xml', $crap4j->process($this->getLineCoverageForBankAccount(), null, 'BankAccount'), ); } @@ -30,7 +30,7 @@ public function testForFileWithIgnoredLines(): void $crap4j = new Crap4j; $this->assertStringMatchesFormatFile( - TEST_FILES_PATH . 'ignored-lines-crap4j.xml', + TEST_FILES_PATH . 'Report/Crap4j/ignored-lines.xml', $crap4j->process($this->getCoverageForFileWithIgnoredLines(), null, 'CoverageForFileWithIgnoredLines'), ); } @@ -40,7 +40,7 @@ public function testForClassWithAnonymousFunction(): void $crap4j = new Crap4j; $this->assertStringMatchesFormatFile( - TEST_FILES_PATH . 'class-with-anonymous-function-crap4j.xml', + TEST_FILES_PATH . 'Report/Crap4j/class-with-anonymous-function.xml', $crap4j->process($this->getCoverageForClassWithAnonymousFunction(), null, 'CoverageForClassWithAnonymousFunction'), ); } diff --git a/tests/tests/Report/OpenCloverTest.php b/tests/tests/Report/OpenCloverTest.php new file mode 100644 index 000000000..0b5a6c659 --- /dev/null +++ b/tests/tests/Report/OpenCloverTest.php @@ -0,0 +1,98 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace SebastianBergmann\CodeCoverage\Report; + +use const PHP_EOL; +use function libxml_clear_errors; +use function libxml_get_errors; +use function libxml_use_internal_errors; +use function sprintf; +use function trim; +use DOMDocument; +use PHPUnit\Framework\Attributes\CoversClass; +use SebastianBergmann\CodeCoverage\TestCase; + +#[CoversClass(OpenClover::class)] +final class OpenCloverTest extends TestCase +{ + public function testLineCoverageForBankAccountTest(): void + { + $clover = new OpenClover; + + $this->assertAndValidate( + TEST_FILES_PATH . 'Report/OpenClover/BankAccount-line.xml', + $clover->process($this->getLineCoverageForBankAccount(), null, 'BankAccount'), + ); + } + + public function testPathCoverageForBankAccountTest(): void + { + $clover = new OpenClover; + + $this->assertAndValidate( + TEST_FILES_PATH . 'Report/OpenClover/BankAccount-path.xml', + $clover->process($this->getPathCoverageForBankAccount(), null, 'BankAccount'), + ); + } + + public function testCloverForFileWithIgnoredLines(): void + { + $clover = new OpenClover; + + $this->assertAndValidate( + TEST_FILES_PATH . 'Report/OpenClover/ignored-lines.xml', + $clover->process($this->getCoverageForFileWithIgnoredLines()), + ); + } + + public function testCloverForClassWithAnonymousFunction(): void + { + $clover = new OpenClover; + + $this->assertAndValidate( + TEST_FILES_PATH . 'Report/OpenClover/class-with-anonymous-function.xml', + $clover->process($this->getCoverageForClassWithAnonymousFunction()), + ); + } + + /** + * @param non-empty-string $expectationFile + * @param non-empty-string $cloverXml + */ + private function assertAndValidate(string $expectationFile, string $cloverXml): void + { + $this->assertStringMatchesFormatFile($expectationFile, $cloverXml); + + libxml_use_internal_errors(true); + + $document = new DOMDocument; + $document->loadXML($cloverXml); + + if (!$document->schemaValidate(__DIR__ . '/../../_files/Report/OpenClover/clover.xsd')) { + $buffer = 'Generated XML document does not validate against Clover schema:' . PHP_EOL . PHP_EOL; + + foreach (libxml_get_errors() as $error) { + $buffer .= sprintf( + '- Line %d: %s' . PHP_EOL, + $error->line, + trim($error->message), + ); + } + + $buffer .= PHP_EOL; + } + + libxml_clear_errors(); + + if (isset($buffer)) { + $this->fail($buffer); + } + } +} diff --git a/tests/tests/Report/TextTest.php b/tests/tests/Report/TextTest.php index e6e58ffdc..452ee9cab 100644 --- a/tests/tests/Report/TextTest.php +++ b/tests/tests/Report/TextTest.php @@ -24,7 +24,7 @@ public function testLineCoverageForBankAccountTest(): void $text = new Text(Thresholds::default(), false, false); $this->assertStringMatchesFormatFile( - TEST_FILES_PATH . 'BankAccount-text-line.txt', + TEST_FILES_PATH . 'Report/Text/BankAccount-line.txt', str_replace(PHP_EOL, "\n", $text->process($this->getLineCoverageForBankAccount())), ); } @@ -34,7 +34,7 @@ public function testPathCoverageForBankAccountTest(): void $text = new Text(Thresholds::default(), false, false); $this->assertStringMatchesFormatFile( - TEST_FILES_PATH . 'BankAccount-text-path.txt', + TEST_FILES_PATH . 'Report/Text/BankAccount-path.txt', str_replace(PHP_EOL, "\n", $text->process($this->getPathCoverageForBankAccount())), ); } @@ -44,7 +44,7 @@ public function testTextOnlySummaryForBankAccountTest(): void $text = new Text(Thresholds::default(), false, true); $this->assertStringMatchesFormatFile( - TEST_FILES_PATH . 'BankAccount-text-summary.txt', + TEST_FILES_PATH . 'Report/Text/BankAccount-summary.txt', str_replace(PHP_EOL, "\n", $text->process($this->getLineCoverageForBankAccount())), ); } @@ -54,7 +54,7 @@ public function testTextForNamespacedBankAccountTest(): void $text = new Text(Thresholds::default(), true, false); $this->assertStringMatchesFormatFile( - TEST_FILES_PATH . 'NamespacedBankAccount-text.txt', + TEST_FILES_PATH . 'Report/Text/NamespacedBankAccount.txt', str_replace(PHP_EOL, "\n", $text->process($this->getLineCoverageForNamespacedBankAccount())), ); } @@ -64,7 +64,7 @@ public function testTextForNamespacedBankAccountTestWhenColorsAreEnabled(): void $text = new Text(Thresholds::default(), true, false); $this->assertStringMatchesFormatFile( - TEST_FILES_PATH . 'NamespacedBankAccount-text-with-colors.txt', + TEST_FILES_PATH . 'Report/Text/NamespacedBankAccount-colors.txt', str_replace(PHP_EOL, "\n", $text->process($this->getLineCoverageForNamespacedBankAccount(), true)), ); } @@ -74,7 +74,7 @@ public function testTextForFileWithIgnoredLines(): void $text = new Text(Thresholds::default(), false, false); $this->assertStringMatchesFormatFile( - TEST_FILES_PATH . 'ignored-lines-text.txt', + TEST_FILES_PATH . 'Report/Text/ignored-lines.txt', str_replace(PHP_EOL, "\n", $text->process($this->getCoverageForFileWithIgnoredLines())), ); } @@ -84,7 +84,7 @@ public function testTextForClassWithAnonymousFunction(): void $text = new Text(Thresholds::default(), false, false); $this->assertStringMatchesFormatFile( - TEST_FILES_PATH . 'class-with-anonymous-function-text.txt', + TEST_FILES_PATH . 'Report/Text/class-with-anonymous-function.txt', str_replace(PHP_EOL, "\n", $text->process($this->getCoverageForClassWithAnonymousFunction())), ); } @@ -94,7 +94,7 @@ public function testUncoveredFilesAreIncludedWhenConfiguredTest(): void $text = new Text(Thresholds::default(), false, false); $this->assertStringMatchesFormatFile( - TEST_FILES_PATH . 'BankAccountWithUncovered-text-line.txt', + TEST_FILES_PATH . 'Report/Text/BankAccountWithUncovered-line.txt', str_replace(PHP_EOL, "\n", $text->process($this->getCoverageForFilesWithUncoveredIncluded())), ); } @@ -104,7 +104,7 @@ public function testUncoveredFilesAreExcludedWhenConfiguredTest(): void $text = new Text(Thresholds::default(), false, false); $this->assertStringMatchesFormatFile( - TEST_FILES_PATH . 'BankAccountWithoutUncovered-text-line.txt', + TEST_FILES_PATH . 'Report/Text/BankAccountWithoutUncovered-line.txt', str_replace(PHP_EOL, "\n", $text->process($this->getCoverageForFilesWithUncoveredExcluded())), ); }