From d37bcd00f363103cf10caa2c658f46a5b731b1a3 Mon Sep 17 00:00:00 2001 From: Jan Nedbal Date: Mon, 22 Apr 2024 14:50:44 +0200 Subject: [PATCH] Ensure stdout is valid XML for junit format (#129) --- bin/composer-dependency-analyser | 7 ++--- src/Initializer.php | 29 ++++++++++++-------- src/Printer.php | 23 ++++++++++++++-- tests/BinTest.php | 45 ++++++++++++++++++++------------ tests/ConsoleFormatterTest.php | 38 ++++++--------------------- tests/FormatterTest.php | 43 ++++++++++++++++++++++++++++++ tests/InitializerTest.php | 12 ++++----- tests/JunitFormatterTest.php | 38 ++++++++------------------- tests/PrinterTest.php | 17 ++++++------ 9 files changed, 148 insertions(+), 104 deletions(-) create mode 100644 tests/FormatterTest.php diff --git a/bin/composer-dependency-analyser b/bin/composer-dependency-analyser index e86fdde..459fba9 100755 --- a/bin/composer-dependency-analyser +++ b/bin/composer-dependency-analyser @@ -27,8 +27,9 @@ spl_autoload_register(static function (string $class) use ($psr4Prefix): void { /** @var non-empty-string $cwd */ $cwd = getcwd(); -$printer = new Printer(); -$initializer = new Initializer($cwd, $printer); +$stdOutPrinter = new Printer(STDOUT); +$stdErrPrinter = new Printer(STDERR); +$initializer = new Initializer($cwd, $stdOutPrinter, $stdErrPrinter); $stopwatch = new Stopwatch(); try { @@ -49,7 +50,7 @@ try { InvalidConfigException | InvalidCliException $e ) { - $printer->printLine("\n{$e->getMessage()}" . PHP_EOL); + $stdErrPrinter->printLine("\n{$e->getMessage()}" . PHP_EOL); exit(255); } diff --git a/src/Initializer.php b/src/Initializer.php index 756d47a..c7c63ca 100644 --- a/src/Initializer.php +++ b/src/Initializer.php @@ -55,15 +55,22 @@ class Initializer /** * @var Printer */ - private $printer; + private $stdOutPrinter; + + /** + * @var Printer + */ + private $stdErrPrinter; public function __construct( string $cwd, - Printer $printer + Printer $stdOutPrinter, + Printer $stdErrPrinter ) { - $this->printer = $printer; $this->cwd = $cwd; + $this->stdOutPrinter = $stdOutPrinter; + $this->stdErrPrinter = $stdErrPrinter; } /** @@ -85,7 +92,7 @@ public function initConfiguration( } if (is_file($configPath)) { - $this->printer->printLine('Using config ' . $configPath); + $this->stdErrPrinter->printLine('Using config ' . $configPath); try { $config = (static function () use ($configPath) { @@ -190,17 +197,17 @@ public function initComposerClassLoaders(): array $loaders = ClassLoader::getRegisteredLoaders(); if (count($loaders) > 1) { - $this->printer->printLine("\nDetected multiple class loaders:"); + $this->stdErrPrinter->printLine("\nDetected multiple class loaders:"); foreach ($loaders as $vendorDir => $_) { - $this->printer->printLine(" • $vendorDir"); + $this->stdErrPrinter->printLine(" • $vendorDir"); } - $this->printer->printLine(''); + $this->stdErrPrinter->printLine(''); } if (count($loaders) === 0) { - $this->printer->printLine("\nNo composer class loader detected!\n"); + $this->stdErrPrinter->printLine("\nNo composer class loader detected!\n"); } return $loaders; @@ -215,7 +222,7 @@ public function initCliOptions(string $cwd, array $argv): CliOptions $cliOptions = (new Cli($cwd, $argv))->getProvidedOptions(); if ($cliOptions->help !== null) { - $this->printer->printLine(self::$help); + $this->stdOutPrinter->printLine(self::$help); throw new InvalidCliException(''); // just exit } @@ -229,11 +236,11 @@ public function initFormatter(CliOptions $options): ResultFormatter { switch ($options->format) { case 'junit': - return new JunitFormatter($this->cwd, $this->printer); + return new JunitFormatter($this->cwd, $this->stdOutPrinter); case 'console': case null: - return new ConsoleFormatter($this->cwd, $this->printer); + return new ConsoleFormatter($this->cwd, $this->stdOutPrinter); default: throw new InvalidConfigException("Invalid format option provided, allowed are 'console' or 'junit'."); diff --git a/src/Printer.php b/src/Printer.php index 69b5e51..dc66842 100644 --- a/src/Printer.php +++ b/src/Printer.php @@ -2,8 +2,10 @@ namespace ShipMonk\ComposerDependencyAnalyser; +use LogicException; use function array_keys; use function array_values; +use function fwrite; use function str_replace; use const PHP_EOL; @@ -21,14 +23,31 @@ class Printer '' => "\033[0m", ]; + /** + * @var resource + */ + private $resource; + + /** + * @param resource $resource + */ + public function __construct($resource) + { + $this->resource = $resource; + } + public function printLine(string $string): void { - echo $this->colorize($string) . PHP_EOL; + $this->print($string . PHP_EOL); } public function print(string $string): void { - echo $this->colorize($string); + $result = fwrite($this->resource, $this->colorize($string)); + + if ($result === false) { + throw new LogicException('Could not write to output stream.'); + } } private function colorize(string $string): string diff --git a/tests/BinTest.php b/tests/BinTest.php index bf3b84c..ac5f6ee 100644 --- a/tests/BinTest.php +++ b/tests/BinTest.php @@ -23,27 +23,30 @@ public function test(): void $okOutput = 'No composer issues found'; $helpOutput = 'Usage:'; + $usingConfig = 'Using config'; + $junitOutput = ''; - $this->runCommand('php bin/composer-dependency-analyser', $rootDir, 0, $okOutput); - $this->runCommand('php bin/composer-dependency-analyser --verbose', $rootDir, 0, $okOutput); - $this->runCommand('php ../bin/composer-dependency-analyser', $testsDir, 255, $noComposerJsonError); + $this->runCommand('php bin/composer-dependency-analyser', $rootDir, 0, $okOutput, $usingConfig); + $this->runCommand('php bin/composer-dependency-analyser --verbose', $rootDir, 0, $okOutput, $usingConfig); + $this->runCommand('php ../bin/composer-dependency-analyser', $testsDir, 255, null, $noComposerJsonError); $this->runCommand('php bin/composer-dependency-analyser --help', $rootDir, 255, $helpOutput); $this->runCommand('php ../bin/composer-dependency-analyser --help', $testsDir, 255, $helpOutput); - $this->runCommand('php bin/composer-dependency-analyser --composer-json=composer.json', $rootDir, 0, $okOutput); - $this->runCommand('php bin/composer-dependency-analyser --composer-json=composer.lock', $rootDir, 255, $noPackagesError); - $this->runCommand('php bin/composer-dependency-analyser --composer-json=README.md', $rootDir, 255, $parseError); - $this->runCommand('php ../bin/composer-dependency-analyser --composer-json=composer.json', $testsDir, 255, $noComposerJsonError); - $this->runCommand('php ../bin/composer-dependency-analyser --composer-json=../composer.json --config=../composer-dependency-analyser.php', $testsDir, 0, $okOutput); - $this->runCommand('php bin/composer-dependency-analyser --composer-json=composer.json --format=console', $rootDir, 0, $okOutput); - $this->runCommand('php bin/composer-dependency-analyser --composer-json=composer.json --format=junit', $rootDir, 0, $junitOutput); + $this->runCommand('php bin/composer-dependency-analyser --composer-json=composer.json', $rootDir, 0, $okOutput, $usingConfig); + $this->runCommand('php bin/composer-dependency-analyser --composer-json=composer.lock', $rootDir, 255, null, $noPackagesError); + $this->runCommand('php bin/composer-dependency-analyser --composer-json=README.md', $rootDir, 255, null, $parseError); + $this->runCommand('php ../bin/composer-dependency-analyser --composer-json=composer.json', $testsDir, 255, null, $noComposerJsonError); + $this->runCommand('php ../bin/composer-dependency-analyser --composer-json=../composer.json --config=../composer-dependency-analyser.php', $testsDir, 0, $okOutput, $usingConfig); + $this->runCommand('php bin/composer-dependency-analyser --composer-json=composer.json --format=console', $rootDir, 0, $okOutput, $usingConfig); + $this->runCommand('php bin/composer-dependency-analyser --composer-json=composer.json --format=junit', $rootDir, 0, $junitOutput, $usingConfig); } private function runCommand( string $command, string $cwd, int $expectedExitCode, - string $expectedOutputContains + ?string $expectedOutputContains = null, + ?string $expectedErrorContains = null ): void { $desc = [ @@ -74,11 +77,21 @@ private function runCommand( $extraInfo ); - self::assertStringContainsString( - $expectedOutputContains, - $output, - $extraInfo - ); + if ($expectedOutputContains !== null) { + self::assertStringContainsString( + $expectedOutputContains, + $output, + $extraInfo + ); + } + + if ($expectedErrorContains !== null) { + self::assertStringContainsString( + $expectedErrorContains, + $errorOutput, + $extraInfo + ); + } } } diff --git a/tests/ConsoleFormatterTest.php b/tests/ConsoleFormatterTest.php index cdac5fc..0f200be 100644 --- a/tests/ConsoleFormatterTest.php +++ b/tests/ConsoleFormatterTest.php @@ -2,31 +2,24 @@ namespace ShipMonk\ComposerDependencyAnalyser; -use Closure; -use PHPUnit\Framework\TestCase; use ShipMonk\ComposerDependencyAnalyser\Config\Configuration; use ShipMonk\ComposerDependencyAnalyser\Config\ErrorType; use ShipMonk\ComposerDependencyAnalyser\Config\Ignore\UnusedErrorIgnore; use ShipMonk\ComposerDependencyAnalyser\Result\AnalysisResult; use ShipMonk\ComposerDependencyAnalyser\Result\ConsoleFormatter; +use ShipMonk\ComposerDependencyAnalyser\Result\ResultFormatter; use ShipMonk\ComposerDependencyAnalyser\Result\SymbolUsage; -use function ob_get_clean; -use function ob_start; -use function preg_replace; -use function str_replace; -class ConsoleFormatterTest extends TestCase +class ConsoleFormatterTest extends FormatterTest { public function testPrintResult(): void { // editorconfig-checker-disable - $formatter = new ConsoleFormatter('/app', new Printer()); - - $noIssuesOutput = $this->captureAndNormalizeOutput(static function () use ($formatter): void { + $noIssuesOutput = $this->getFormatterNormalizedOutput(static function (ResultFormatter $formatter): void { $formatter->format(new AnalysisResult(2, 0.123, [], [], [], [], [], [], [], []), new CliOptions(), new Configuration()); }); - $noIssuesButUnusedIgnores = $this->captureAndNormalizeOutput(static function () use ($formatter): void { + $noIssuesButUnusedIgnores = $this->getFormatterNormalizedOutput(static function (ResultFormatter $formatter): void { $formatter->format(new AnalysisResult(2, 0.123, [], [], [], [], [], [], [], [new UnusedErrorIgnore(ErrorType::SHADOW_DEPENDENCY, null, null)]), new CliOptions(), new Configuration()); }); @@ -79,10 +72,10 @@ public function testPrintResult(): void [] ); - $regularOutput = $this->captureAndNormalizeOutput(static function () use ($formatter, $analysisResult): void { + $regularOutput = $this->getFormatterNormalizedOutput(static function ($formatter) use ($analysisResult): void { $formatter->format($analysisResult, new CliOptions(), new Configuration()); }); - $verboseOutput = $this->captureAndNormalizeOutput(static function () use ($formatter, $analysisResult): void { + $verboseOutput = $this->getFormatterNormalizedOutput(static function ($formatter) use ($analysisResult): void { $options = new CliOptions(); $options->verbose = true; $formatter->format($analysisResult, $options, new Configuration()); @@ -206,24 +199,9 @@ public function testPrintResult(): void // editorconfig-checker-enable } - private function removeColors(string $output): string - { - return (string) preg_replace('#\\x1b[[][^A-Za-z]*[A-Za-z]#', '', $output); - } - - /** - * @param Closure(): void $closure - */ - private function captureAndNormalizeOutput(Closure $closure): string - { - ob_start(); - $closure(); - return $this->normalizeEol((string) ob_get_clean()); - } - - private function normalizeEol(string $string): string + protected function createFormatter(Printer $printer): ResultFormatter { - return str_replace("\r\n", "\n", $string); + return new ConsoleFormatter('/app', $printer); } } diff --git a/tests/FormatterTest.php b/tests/FormatterTest.php new file mode 100644 index 0000000..48e6ea1 --- /dev/null +++ b/tests/FormatterTest.php @@ -0,0 +1,43 @@ +createFormatter($printer); + + $closure($formatter); + return $this->normalizeEol((string) stream_get_contents($stream, -1, 0)); + } + + protected function normalizeEol(string $string): string + { + return str_replace("\r\n", "\n", $string); + } + + protected function removeColors(string $output): string + { + return (string) preg_replace('#\\x1b[[][^A-Za-z]*[A-Za-z]#', '', $output); + } + +} diff --git a/tests/InitializerTest.php b/tests/InitializerTest.php index aaad61e..ac6961f 100644 --- a/tests/InitializerTest.php +++ b/tests/InitializerTest.php @@ -26,7 +26,7 @@ public function testInitConfiguration(): void $options = new CliOptions(); $options->ignoreUnknownClasses = true; - $initializer = new Initializer(__DIR__, $printer); + $initializer = new Initializer(__DIR__, $printer, $printer); $config = $initializer->initConfiguration($options, $composerJson); self::assertEquals([new PathToScan(__DIR__, false)], $config->getPathsToScan()); @@ -44,7 +44,7 @@ public function testInitComposerJson(): void $options = new CliOptions(); $options->composerJson = 'sample.json'; - $initializer = new Initializer($cwd, $printer); + $initializer = new Initializer($cwd, $printer, $printer); $composerJson = $initializer->initComposerJson($options); self::assertSame( @@ -70,7 +70,7 @@ public function testInitComposerJsonWithAbsolutePath(): void $options = new CliOptions(); $options->composerJson = $composerJsonPath; - $initializer = new Initializer($cwd, $printer); + $initializer = new Initializer($cwd, $printer, $printer); $composerJson = $initializer->initComposerJson($options); self::assertSame( @@ -90,7 +90,7 @@ public function testInitCliOptions(): void { $printer = $this->createMock(Printer::class); - $initializer = new Initializer(__DIR__, $printer); + $initializer = new Initializer(__DIR__, $printer, $printer); $options = $initializer->initCliOptions(__DIR__, ['script.php', '--verbose']); self::assertNull($options->showAllUsages); @@ -108,7 +108,7 @@ public function testInitCliOptionsHelp(): void { $printer = $this->createMock(Printer::class); - $initializer = new Initializer(__DIR__, $printer); + $initializer = new Initializer(__DIR__, $printer, $printer); $this->expectException(InvalidCliException::class); $initializer->initCliOptions(__DIR__, ['script.php', '--help']); @@ -118,7 +118,7 @@ public function testInitFormatter(): void { $printer = $this->createMock(Printer::class); - $initializer = new Initializer(__DIR__, $printer); + $initializer = new Initializer(__DIR__, $printer, $printer); $optionsNoFormat = new CliOptions(); self::assertInstanceOf(ConsoleFormatter::class, $initializer->initFormatter($optionsNoFormat)); diff --git a/tests/JunitFormatterTest.php b/tests/JunitFormatterTest.php index fdec353..995e2f0 100644 --- a/tests/JunitFormatterTest.php +++ b/tests/JunitFormatterTest.php @@ -2,33 +2,27 @@ namespace ShipMonk\ComposerDependencyAnalyser; -use Closure; use DOMDocument; -use PHPUnit\Framework\TestCase; use ShipMonk\ComposerDependencyAnalyser\Config\Configuration; use ShipMonk\ComposerDependencyAnalyser\Config\ErrorType; use ShipMonk\ComposerDependencyAnalyser\Config\Ignore\UnusedErrorIgnore; use ShipMonk\ComposerDependencyAnalyser\Result\AnalysisResult; use ShipMonk\ComposerDependencyAnalyser\Result\JunitFormatter; +use ShipMonk\ComposerDependencyAnalyser\Result\ResultFormatter; use ShipMonk\ComposerDependencyAnalyser\Result\SymbolUsage; -use function ob_get_clean; -use function ob_start; -use function str_replace; use function trim; use const LIBXML_NOEMPTYTAG; -class JunitFormatterTest extends TestCase +class JunitFormatterTest extends FormatterTest { public function testPrintResult(): void { // editorconfig-checker-disable - $formatter = new JunitFormatter('/app', new Printer()); - - $noIssuesOutput = $this->captureAndNormalizeOutput(static function () use ($formatter): void { + $noIssuesOutput = $this->getFormatterNormalizedOutput(static function (ResultFormatter $formatter): void { $formatter->format(new AnalysisResult(2, 0.123, [], [], [], [], [], [], [], []), new CliOptions(), new Configuration()); }); - $noIssuesButUnusedIgnores = $this->captureAndNormalizeOutput(static function () use ($formatter): void { + $noIssuesButUnusedIgnores = $this->getFormatterNormalizedOutput(static function (ResultFormatter $formatter): void { $formatter->format(new AnalysisResult(2, 0.123, [], [], [], [], [], [], [], [new UnusedErrorIgnore(ErrorType::SHADOW_DEPENDENCY, null, null)]), new CliOptions(), new Configuration()); }); @@ -79,10 +73,10 @@ public function testPrintResult(): void [] ); - $regularOutput = $this->captureAndNormalizeOutput(static function () use ($formatter, $analysisResult): void { + $regularOutput = $this->getFormatterNormalizedOutput(static function ($formatter) use ($analysisResult): void { $formatter->format($analysisResult, new CliOptions(), new Configuration()); }); - $verboseOutput = $this->captureAndNormalizeOutput(static function () use ($formatter, $analysisResult): void { + $verboseOutput = $this->getFormatterNormalizedOutput(static function ($formatter) use ($analysisResult): void { $options = new CliOptions(); $options->verbose = true; $formatter->format($analysisResult, $options, new Configuration()); @@ -170,21 +164,6 @@ public function testPrintResult(): void // editorconfig-checker-enable } - /** - * @param Closure(): void $closure - */ - private function captureAndNormalizeOutput(Closure $closure): string - { - ob_start(); - $closure(); - return $this->normalizeEol((string) ob_get_clean()); - } - - private function normalizeEol(string $string): string - { - return str_replace("\r\n", "\n", $string); - } - private function prettyPrintXml(string $inputXml): string { $dom = new DOMDocument(); @@ -198,4 +177,9 @@ private function prettyPrintXml(string $inputXml): string return trim($outputXml); } + protected function createFormatter(Printer $printer): ResultFormatter + { + return new JunitFormatter('/app', $printer); + } + } diff --git a/tests/PrinterTest.php b/tests/PrinterTest.php index 4b37fcc..e89e02c 100644 --- a/tests/PrinterTest.php +++ b/tests/PrinterTest.php @@ -3,6 +3,8 @@ namespace ShipMonk\ComposerDependencyAnalyser; use PHPUnit\Framework\TestCase; +use function fopen; +use function stream_get_contents; use const PHP_EOL; class PrinterTest extends TestCase @@ -10,18 +12,15 @@ class PrinterTest extends TestCase public function testPrintLine(): void { - $printer = new Printer(); + $stream = fopen('php://memory', 'w'); + self::assertNotFalse($stream); - $this->expectOutputString("Hello, \033[31mworld\033[0m!" . PHP_EOL); - $printer->printLine('Hello, world!'); - } + $printer = new Printer($stream); - public function testPrint(): void - { - $printer = new Printer(); + $printer->printLine('Hello, world!'); + $printer->print('New line!'); - $this->expectOutputString("Hello, \033[31mworld\033[0m!"); - $printer->print('Hello, world!'); + self::assertSame("Hello, \033[31mworld\033[0m!" . PHP_EOL . 'New line!', stream_get_contents($stream, -1, 0)); } }