diff --git a/composer.json b/composer.json index c2af7b2..fa1e6ab 100644 --- a/composer.json +++ b/composer.json @@ -20,12 +20,12 @@ ], "require": { "php": "^7.2 || ^8.0", + "ext-dom": "*", "ext-json": "*", + "ext-libxml": "*", "ext-tokenizer": "*" }, "require-dev": { - "ext-dom": "*", - "ext-libxml": "*", "editorconfig-checker/editorconfig-checker": "^10.3.0", "ergebnis/composer-normalize": "^2.19", "phpcompatibility/php-compatibility": "^9.3", diff --git a/src/Initializer.php b/src/Initializer.php index dc4f067..00acaed 100644 --- a/src/Initializer.php +++ b/src/Initializer.php @@ -245,7 +245,7 @@ public function initFormatter(CliOptions $options): ResultFormatter throw new InvalidConfigException("Cannot use 'junit' format with '--dump-usages' option."); } - return new JunitFormatter($this->cwd, $this->stdOutPrinter); + return new JunitFormatter($this->cwd, $this->stdOutPrinter, $options->verbose); } if ($format === 'console') { diff --git a/src/Result/AbstractXmlFormatter.php b/src/Result/AbstractXmlFormatter.php new file mode 100644 index 0000000..b464f75 --- /dev/null +++ b/src/Result/AbstractXmlFormatter.php @@ -0,0 +1,35 @@ +printer = $printer; + + $this->document = new DOMDocument('1.0', 'UTF-8'); + $this->document->formatOutput = $verbose; + } + +} diff --git a/src/Result/JunitFormatter.php b/src/Result/JunitFormatter.php index f206518..4b70f55 100644 --- a/src/Result/JunitFormatter.php +++ b/src/Result/JunitFormatter.php @@ -2,6 +2,7 @@ namespace ShipMonk\ComposerDependencyAnalyser\Result; +use DOMException; use ShipMonk\ComposerDependencyAnalyser\CliOptions; use ShipMonk\ComposerDependencyAnalyser\Config\Configuration; use ShipMonk\ComposerDependencyAnalyser\Config\Ignore\UnusedErrorIgnore; @@ -19,9 +20,10 @@ use function substr; use const ENT_COMPAT; use const ENT_XML1; +use const LIBXML_NOEMPTYTAG; use const PHP_INT_MAX; -class JunitFormatter implements ResultFormatter +final class JunitFormatter extends AbstractXmlFormatter implements ResultFormatter { /** @@ -30,25 +32,29 @@ class JunitFormatter implements ResultFormatter private $cwd; /** - * @var Printer + * @throws DOMException */ - private $printer; - - public function __construct(string $cwd, Printer $printer) + public function __construct(string $cwd, Printer $printer, ?bool $verbose = null) { + if ($verbose === null) { + $verbose = false; + } + + parent::__construct($printer, $verbose); $this->cwd = $cwd; - $this->printer = $printer; + $this->rootElement = $this->document->createElement('testsuites'); + $this->document->appendChild($this->rootElement); } + /** + * @throws DOMException + */ public function format( AnalysisResult $result, CliOptions $options, Configuration $configuration ): int { - $xml = ''; - $xml .= ''; - $hasError = false; $unusedIgnores = $result->getUnusedIgnores(); @@ -63,7 +69,7 @@ public function format( if (count($unknownClassErrors) > 0) { $hasError = true; - $xml .= $this->createSymbolBasedTestSuite( + $this->createSymbolBasedTestSuite( 'unknown classes', $unknownClassErrors, $maxShownUsages @@ -72,7 +78,7 @@ public function format( if (count($unknownFunctionErrors) > 0) { $hasError = true; - $xml .= $this->createSymbolBasedTestSuite( + $this->createSymbolBasedTestSuite( 'unknown functions', $unknownFunctionErrors, $maxShownUsages @@ -81,7 +87,7 @@ public function format( if (count($shadowDependencyErrors) > 0) { $hasError = true; - $xml .= $this->createPackageBasedTestSuite( + $this->createPackageBasedTestSuite( 'shadow dependencies', $shadowDependencyErrors, $maxShownUsages @@ -90,7 +96,7 @@ public function format( if (count($devDependencyInProductionErrors) > 0) { $hasError = true; - $xml .= $this->createPackageBasedTestSuite( + $this->createPackageBasedTestSuite( 'dev dependencies in production code', $devDependencyInProductionErrors, $maxShownUsages @@ -99,7 +105,7 @@ public function format( if (count($prodDependencyOnlyInDevErrors) > 0) { $hasError = true; - $xml .= $this->createPackageBasedTestSuite( + $this->createPackageBasedTestSuite( 'prod dependencies used only in dev paths', array_fill_keys($prodDependencyOnlyInDevErrors, []), $maxShownUsages @@ -108,7 +114,7 @@ public function format( if (count($unusedDependencyErrors) > 0) { $hasError = true; - $xml .= $this->createPackageBasedTestSuite( + $this->createPackageBasedTestSuite( 'unused dependencies', array_fill_keys($unusedDependencyErrors, []), $maxShownUsages @@ -117,12 +123,16 @@ public function format( if ($unusedIgnores !== [] && $configuration->shouldReportUnmatchedIgnoredErrors()) { $hasError = true; - $xml .= $this->createUnusedIgnoresTestSuite($unusedIgnores); + $this->createUnusedIgnoresTestSuite($unusedIgnores); } - $xml .= ''; + $xmlString = $this->document->saveXML(null, LIBXML_NOEMPTYTAG); - $this->printer->print($xml); + if ($xmlString === false) { + $xmlString = ''; + } + + $this->printer->print($xmlString); if ($hasError) { return 255; @@ -146,13 +156,20 @@ private function getMaxUsagesShownForErrors(CliOptions $options): int /** * @param array> $errors + * @throws DOMException */ - private function createSymbolBasedTestSuite(string $title, array $errors, int $maxShownUsages): string + private function createSymbolBasedTestSuite(string $title, array $errors, int $maxShownUsages): void { - $xml = sprintf('', $this->escape($title), count($errors)); + $testsuite = $this->document->createElement('testsuite'); + $testsuite->setAttribute('name', $this->escape($title)); + $testsuite->setAttribute('failures', sprintf('%u', count($errors))); + + $this->rootElement->appendChild($testsuite); foreach ($errors as $symbol => $usages) { - $xml .= sprintf('', $this->escape($symbol)); + $testcase = $this->document->createElement('testcase'); + $testcase->setAttribute('name', $this->escape($symbol)); + $testsuite->appendChild($testcase); if ($maxShownUsages > 1) { $failureUsage = []; @@ -170,38 +187,43 @@ private function createSymbolBasedTestSuite(string $title, array $errors, int $m } } - $xml .= sprintf('%s', $this->escape(implode('\n', $failureUsage))); + $failureMessage = $this->escape(implode('\n', $failureUsage)); } else { $firstUsage = $usages[0]; $restUsagesCount = count($usages) - 1; $rest = $restUsagesCount > 0 ? " (+ {$restUsagesCount} more)" : ''; - $xml .= sprintf('in %s%s', $this->escape($this->relativizeUsage($firstUsage)), $rest); + + $failureMessage = sprintf('in %s%s', $this->escape($this->relativizeUsage($firstUsage)), $rest); } - $xml .= ''; + $failure = $this->document->createElement('failure', $failureMessage); + $testcase->appendChild($failure); } - - $xml .= ''; - - return $xml; } /** * @param array>> $errors + * @throws DOMException */ - private function createPackageBasedTestSuite(string $title, array $errors, int $maxShownUsages): string + private function createPackageBasedTestSuite(string $title, array $errors, int $maxShownUsages): void { - $xml = sprintf('', $this->escape($title), count($errors)); + $testsuite = $this->document->createElement('testsuite'); + $testsuite->setAttribute('name', $this->escape($title)); + $testsuite->setAttribute('failures', sprintf('%u', count($errors))); - foreach ($errors as $packageName => $usagesPerClassname) { - $xml .= sprintf('', $this->escape($packageName)); - $xml .= sprintf('%s', $this->escape(implode('\n', $this->createUsages($usagesPerClassname, $maxShownUsages)))); - $xml .= ''; - } + $this->rootElement->appendChild($testsuite); - $xml .= ''; + foreach ($errors as $packageName => $usagesPerClassname) { + $testcase = $this->document->createElement('testcase'); + $testcase->setAttribute('name', $this->escape($packageName)); + $testsuite->appendChild($testcase); - return $xml; + $failure = $this->document->createElement( + 'failure', + $this->escape(implode('\n', $this->createUsages($usagesPerClassname, $maxShownUsages))) + ); + $testcase->appendChild($failure); + } } /** @@ -265,17 +287,23 @@ static function (int $carry, array $usages): int { /** * @param list $unusedIgnores + * @throws DOMException */ - private function createUnusedIgnoresTestSuite(array $unusedIgnores): string + private function createUnusedIgnoresTestSuite(array $unusedIgnores): void { - $xml = sprintf('', count($unusedIgnores)); + $testsuite = $this->document->createElement('testsuite'); + $testsuite->setAttribute('name', 'unused-ignore'); + $testsuite->setAttribute('failures', sprintf('%u', count($unusedIgnores))); + + $this->rootElement->appendChild($testsuite); foreach ($unusedIgnores as $unusedIgnore) { if ($unusedIgnore instanceof UnusedSymbolIgnore) { $kind = $unusedIgnore->getSymbolKind() === SymbolKind::CLASSLIKE ? 'class' : 'function'; $regex = $unusedIgnore->isRegex() ? ' regex' : ''; $message = "Unknown {$kind}{$regex} '{$unusedIgnore->getUnknownSymbol()}' was ignored, but it was never applied."; - $xml .= sprintf('%s', $this->escape($unusedIgnore->getUnknownSymbol()), $this->escape($message)); + + $testcaseName = $this->escape($unusedIgnore->getUnknownSymbol()); } else { $package = $unusedIgnore->getPackage(); $path = $unusedIgnore->getPath(); @@ -297,11 +325,16 @@ private function createUnusedIgnoresTestSuite(array $unusedIgnores): string $message = "'{$unusedIgnore->getErrorType()}' was ignored for package '{$package}' and path '{$this->relativizePath($path)}', but it was never applied."; } - $xml .= sprintf('%s', $this->escape($unusedIgnore->getErrorType()), $this->escape($message)); + $testcaseName = $this->escape($unusedIgnore->getErrorType()); } - } - return $xml . ''; + $testcase = $this->document->createElement('testcase'); + $testcase->setAttribute('name', $testcaseName); + $testsuite->appendChild($testcase); + + $failure = $this->document->createElement('failure', $this->escape($message)); + $testcase->appendChild($failure); + } } private function relativizeUsage(SymbolUsage $usage): string diff --git a/tests/BinTest.php b/tests/BinTest.php index c4dd187..c32904a 100644 --- a/tests/BinTest.php +++ b/tests/BinTest.php @@ -27,7 +27,7 @@ public function test(): void $usingConfig = 'Using config'; - $junitOutput = ''; + $junitOutput = '' . "\n" . ''; $this->runCommand('php bin/composer-dependency-analyser', $rootDir, 0, $okOutput, $usingConfig); $this->runCommand('php bin/composer-dependency-analyser --verbose', $rootDir, 0, $okOutput, $usingConfig); diff --git a/tests/JunitFormatterTest.php b/tests/JunitFormatterTest.php index 995e2f0..6a788fd 100644 --- a/tests/JunitFormatterTest.php +++ b/tests/JunitFormatterTest.php @@ -3,6 +3,7 @@ namespace ShipMonk\ComposerDependencyAnalyser; use DOMDocument; +use DOMException; use ShipMonk\ComposerDependencyAnalyser\Config\Configuration; use ShipMonk\ComposerDependencyAnalyser\Config\ErrorType; use ShipMonk\ComposerDependencyAnalyser\Config\Ignore\UnusedErrorIgnore; @@ -168,7 +169,7 @@ private function prettyPrintXml(string $inputXml): string { $dom = new DOMDocument(); $dom->preserveWhiteSpace = false; - $dom->formatOutput = true; + $dom->formatOutput = true; // always in human-readable format $dom->loadXML($inputXml); $outputXml = $dom->saveXML(null, LIBXML_NOEMPTYTAG); @@ -177,9 +178,13 @@ private function prettyPrintXml(string $inputXml): string return trim($outputXml); } + /** + * @throws DOMException + */ protected function createFormatter(Printer $printer): ResultFormatter { - return new JunitFormatter('/app', $printer); + // human-readable format with 'verbose' option set to true + return new JunitFormatter('/app', $printer, true); } }