From 82f26e10e9343798311cc295f0677be8362b6d06 Mon Sep 17 00:00:00 2001 From: dannc Date: Thu, 21 Sep 2023 14:57:44 +0700 Subject: [PATCH] create merge clover coverage command --- .gitignore | 5 + composer.json | 41 +++ merger | 51 ++++ phpcs.xml | 8 + phpunit.xml | 18 ++ src/Clover/Dto/Accumulator.php | 41 +++ src/Clover/Dto/ClassDto.php | 21 ++ src/Clover/Dto/FileDto.php | 81 ++++++ src/Clover/Dto/LineDto.php | 44 ++++ src/Clover/Dto/Metrics.php | 78 ++++++ src/Clover/ElementsDictionary.php | 15 ++ src/Clover/Exceptions/HandleException.php | 9 + src/Clover/Exceptions/ParseException.php | 9 + src/Clover/Handler.php | 152 +++++++++++ src/Clover/Parser.php | 89 +++++++ src/Clover/Renderer.php | 239 ++++++++++++++++++ src/Command/CloverMergeCommand.php | 131 ++++++++++ src/Command/Exceptions/CommandException.php | 22 ++ src/Command/Exceptions/ExecuteException.php | 15 ++ .../Exceptions/InvalidArgumentException.php | 15 ++ 20 files changed, 1084 insertions(+) create mode 100644 .gitignore create mode 100644 composer.json create mode 100755 merger create mode 100644 phpcs.xml create mode 100644 phpunit.xml create mode 100644 src/Clover/Dto/Accumulator.php create mode 100644 src/Clover/Dto/ClassDto.php create mode 100644 src/Clover/Dto/FileDto.php create mode 100644 src/Clover/Dto/LineDto.php create mode 100644 src/Clover/Dto/Metrics.php create mode 100644 src/Clover/ElementsDictionary.php create mode 100644 src/Clover/Exceptions/HandleException.php create mode 100644 src/Clover/Exceptions/ParseException.php create mode 100644 src/Clover/Handler.php create mode 100644 src/Clover/Parser.php create mode 100644 src/Clover/Renderer.php create mode 100644 src/Command/CloverMergeCommand.php create mode 100644 src/Command/Exceptions/CommandException.php create mode 100644 src/Command/Exceptions/ExecuteException.php create mode 100644 src/Command/Exceptions/InvalidArgumentException.php diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b015552 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +/.idea/ +/vendor/ + +/.phpunit.result.cache +/composer.lock diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..bb8f16c --- /dev/null +++ b/composer.json @@ -0,0 +1,41 @@ +{ + "name": "dannecron/coverage-merger", + "description": "Merge coverage files into one", + "type": "library", + "license": "MIT", + "authors": [ + { + "name": "Dannecron" + } + ], + "require": { + "php": "^8.1.0", + "ext-dom": "*", + "ext-libxml": "*", + "ext-simplexml": "*", + "adhocore/cli": "^1.6.1" + }, + "require-dev": { + "mockery/mockery": "1.6.6", + "pestphp/pest": "2.18.2", + "squizlabs/php_codesniffer": "3.7.2" + }, + "autoload": { + "psr-4": { + "Dannecron\\CoverageMerger\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "Tests\\": "tests/" + } + }, + "bin": "merger", + "minimum-stability": "stable", + "config": { + "sort-packages": true, + "allow-plugins": { + "pestphp/pest-plugin": false + } + } +} diff --git a/merger b/merger new file mode 100755 index 0000000..b642ae1 --- /dev/null +++ b/merger @@ -0,0 +1,51 @@ +#!/usr/bin/env php +onException(static function (\Throwable $exception, int $exitCode) use ($app): void { + $io = $app->io(); + + $io->error($exception->getMessage(), true); + + if (($exception instanceof CommandException) === false) { + exit($exitCode); + } + + if ($exception->isVerbose() === false) { + exit($exception->getCode()); + } + + $trace = $exception->getTrace(); + $io->table(\array_map(static function (array $traceStep): array { + $methodOrFunc = \array_key_exists('class', $traceStep) + ? "{$traceStep['class']}::{$traceStep['function']}" + : $traceStep['function']; + + return [ + 'method/func' => $methodOrFunc, + 'file' => "{$traceStep['file']}:{$traceStep['line']}", + + ]; + }, $trace)); + + exit($exception->getCode()); +}); + +$app->add( + new CloverMergeCommand( + new Clover\Handler(new Clover\Parser()), + new Clover\Renderer(), + $app, + ), +); + +$app->handle($_SERVER['argv']); diff --git a/phpcs.xml b/phpcs.xml new file mode 100644 index 0000000..9bbd771 --- /dev/null +++ b/phpcs.xml @@ -0,0 +1,8 @@ + + + + + ./vendor/* + ./tests/data/* + ./*\.(xml|js)$ + diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..d18bef3 --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,18 @@ + + + + + ./tests + + + + + ./src + + + diff --git a/src/Clover/Dto/Accumulator.php b/src/Clover/Dto/Accumulator.php new file mode 100644 index 0000000..ae1172b --- /dev/null +++ b/src/Clover/Dto/Accumulator.php @@ -0,0 +1,41 @@ + $files */ + private array $files; + + public function __construct() + { + $this->files = []; + } + + /** + * @return array + */ + public function getFiles(): array + { + return $this->files; + } + + public function addFile(string $fileName, FileDto $file): self + { + /** @var FileDto|null $existedFile */ + $existedFile = $this->getFiles()[$fileName] ?? null; + + if ($existedFile !== null) { + $existedFile->mergeFile($file); + + return $this; + } + + $merged = \array_merge($this->files, [$fileName => $file]); + $this->files = $merged; + + return $this; + } +} diff --git a/src/Clover/Dto/ClassDto.php b/src/Clover/Dto/ClassDto.php new file mode 100644 index 0000000..3df1fa5 --- /dev/null +++ b/src/Clover/Dto/ClassDto.php @@ -0,0 +1,21 @@ + $properties + */ + public function __construct( + private readonly array $properties, + ) { + } + + public function getProperties(): array + { + return $this->properties; + } +} diff --git a/src/Clover/Dto/FileDto.php b/src/Clover/Dto/FileDto.php new file mode 100644 index 0000000..15e1283 --- /dev/null +++ b/src/Clover/Dto/FileDto.php @@ -0,0 +1,81 @@ + */ + private array $classes; + /** @var array */ + private array $lines; + + public function __construct( + public readonly ?string $packageName = null, + ) { + $this->classes = []; + $this->lines = []; + } + + /** + * @return array + */ + public function getClasses(): array + { + return $this->classes; + } + + /** + * @return array + */ + public function getLines(): array + { + return $this->lines; + } + + public function hasClass(string $name): bool + { + return (bool) ($this->classes[$name] ?? false); + } + + public function hasLine(int $number): bool + { + return (bool) ($this->lines[$number] ?? false); + } + + public function mergeFile(FileDto $otherFile): self + { + $mergedClasses = \array_merge($otherFile->getClasses(), $this->classes); + $this->classes = $mergedClasses; + + foreach ($otherFile->getLines() as $number => $line) { + $this->mergeLine($number, $line); + } + + return $this; + } + + public function mergeClass(string $name, ClassDto $class): self + { + if ($this->hasClass($name) === false) { + $this->classes[$name] = $class; + } + + return $this; + } + + public function mergeLine(int $number, LineDto $line): self + { + if ($this->hasLine($number) === false) { + $this->lines[$number] = $line; + + return $this; + } + + $existedLine = $this->lines[$number]; + $existedLine->merge($line); + + return $this; + } +} diff --git a/src/Clover/Dto/LineDto.php b/src/Clover/Dto/LineDto.php new file mode 100644 index 0000000..8f8dea6 --- /dev/null +++ b/src/Clover/Dto/LineDto.php @@ -0,0 +1,44 @@ + $properties Other properties on the line. + * E.g. name, visibility, complexity, crap + * @param int $count Number of hits on this line + */ + public function __construct( + private array $properties, + private int $count, + ) { + } + + public function getCount(): int + { + return $this->count; + } + + public function getProperties(): array + { + return $this->properties; + } + + public function getNum(): ?int + { + return isset($this->properties['num']) + ? (int) $this->properties['num'] + : null; + } + + public function merge(LineDto $otherLine): self + { + $this->properties = \array_merge($otherLine->getProperties(), $this->properties); + $this->count += $otherLine->getCount(); + + return $this; + } +} diff --git a/src/Clover/Dto/Metrics.php b/src/Clover/Dto/Metrics.php new file mode 100644 index 0000000..4587568 --- /dev/null +++ b/src/Clover/Dto/Metrics.php @@ -0,0 +1,78 @@ +statementCount + + $this->conditionalCount + + $this->methodCount; + } + + /** + * Return the number of covered elements. + * @return int + */ + public function getCoveredElementCount(): int + { + return $this->coveredStatementCount + + $this->coveredConditionalCount + + $this->coveredMethodCount; + } + + /** + * Merge another set of metrics into new one. + * @param Metrics $metrics + * @return Metrics + */ + public function merge(Metrics $metrics): Metrics + { + $statementCount = $this->statementCount + $metrics->statementCount; + $coveredStatementCount = $this->coveredStatementCount + $metrics->coveredStatementCount; + $conditionalCount = $this->conditionalCount + $metrics->conditionalCount; + $coveredConditionalCount = $this->coveredConditionalCount + $metrics->coveredConditionalCount; + $methodCount = $this->methodCount + $metrics->methodCount; + $coveredMethodCount = $this->coveredMethodCount + $metrics->coveredMethodCount; + $classCount = $this->classCount + $metrics->classCount; + $fileCount = $this->fileCount + $metrics->fileCount; + $packageCount = $this->packageCount + $metrics->packageCount; + + return new Metrics( + $statementCount, + $coveredStatementCount, + $conditionalCount, + $coveredConditionalCount, + $methodCount, + $coveredMethodCount, + $classCount, + $fileCount, + $packageCount, + ); + } + + public static function makeEmpty(): self + { + return new self(0, 0, 0, 0, 0, 0, 0); + } +} diff --git a/src/Clover/ElementsDictionary.php b/src/Clover/ElementsDictionary.php new file mode 100644 index 0000000..fa6a441 --- /dev/null +++ b/src/Clover/ElementsDictionary.php @@ -0,0 +1,15 @@ +handleSingleDocument($document, $accumulator); + } + + return $accumulator; + } + + /** + * @param \SimpleXMLElement $document + * @param Accumulator|null $accumulator + * @return Accumulator + * @throws HandleException + */ + public function handleSingleDocument( + \SimpleXMLElement $document, + ?Accumulator $accumulator = null + ): Accumulator { + if ($accumulator === null) { + $accumulator = new Accumulator(); + } + + $name = $document->getName(); + + if ($name !== self::ELEMENT_NAME_COVERAGE) { + throw new HandleException('Unexpected element: not coverage'); + } + + foreach ($document->children() as $project) { + $accumulator = $this->handleProject($project, $accumulator); + } + + return $accumulator; + } + + /** + * @param \SimpleXMLElement $project + * @param Accumulator $accumulator + * @return Accumulator + * @throws HandleException + */ + private function handleProject(\SimpleXMLElement $project, Accumulator $accumulator): Accumulator + { + $name = $project->getName(); + if ($name !== self::ELEMENT_NAME_PROJECT) { + throw new HandleException('Unexpected element: not project'); + } + + return $this->handleItems($project->children(), $accumulator); + } + + /** + * @param \SimpleXMLElement $items + * @param Accumulator $accumulator + * @param string|null $packageName + * @return Accumulator + * @throws ParseException + */ + private function handleItems( + \SimpleXMLElement $items, + Accumulator $accumulator, + ?string $packageName = null, + ): Accumulator { + foreach ($items as $item) { + $accumulator = $this->handleItem($item, $accumulator, $packageName); + } + + return $accumulator; + } + + /** + * @param \SimpleXMLElement $item + * @param Accumulator $accumulator + * @param string|null $packageName + * @return Accumulator + * @throws ParseException + */ + private function handleItem( + \SimpleXMLElement $item, + Accumulator $accumulator, + ?string $packageName = null, + ): Accumulator { + $name = $item->getName(); + + if ($name === self::ELEMENT_NAME_PACKAGE) { + $attributes = $this->parser->getAttributes($item); + $attributePackageName = $attributes['name'] ?? null; + + // Don't return here so that the package's files are still parsed regardless + if ($attributePackageName === null) { + return $accumulator; + } + + return $this->handleItems($item->children(), $accumulator, (string) $attributePackageName); + } + + if ($name === self::ELEMENT_NAME_FILE) { + return $this->handleFile($item, $accumulator, $packageName); + } + + return $accumulator; + } + + /** + * @param \SimpleXMLElement $xmlFile + * @param Accumulator $accumulator + * @param string|null $packageName + * @return Accumulator + * @throws ParseException + */ + private function handleFile( + \SimpleXMLElement $xmlFile, + Accumulator $accumulator, + ?string $packageName = null, + ): Accumulator { + $attributes = $this->parser->getAttributes($xmlFile); + $fileName = $attributes['name'] ?? null; + + if ($fileName === null) { + return $accumulator; + } + + $file = $this->parser->parseFile($xmlFile, $packageName); + + return $accumulator->addFile((string) $fileName, $file); + } +} diff --git a/src/Clover/Parser.php b/src/Clover/Parser.php new file mode 100644 index 0000000..3aefdb2 --- /dev/null +++ b/src/Clover/Parser.php @@ -0,0 +1,89 @@ +children() as $child) { + $file = $this->parseChildXml($child, $file); + } + + return $file; + } + + public function parseClass(\SimpleXMLElement $xml): ClassDto + { + $properties = $this->getAttributes($xml); + $properties = \array_map(static fn (mixed $val): string => (string) $val, $properties); + + return new ClassDto($properties); + } + + /** + * @param \SimpleXMLElement $xml + * @return LineDto + * @throws ParseException + */ + public function parseLine(\SimpleXMLElement $xml): LineDto + { + $properties = $this->getAttributes($xml); + $properties = \array_map(static fn (mixed $val): string => (string) $val, $properties); + + $count = $properties['count'] ?? null; + + if ($count === null) { + throw new ParseException('Unable to parse line, missing count attribute'); + } + + unset($properties['count']); + + return new LineDto($properties, (int) $count); + } + + public function getAttributes(\SimpleXMLElement $xml): array + { + return ((array) $xml->attributes())['@attributes'] ?? []; + } + + /** + * @param \SimpleXMLElement $childXml + * @param FileDto $file + * @return FileDto + * @throws ParseException + */ + private function parseChildXml(\SimpleXMLElement $childXml, FileDto $file): FileDto + { + $name = $childXml->getName(); + $attributes = $this->getAttributes($childXml); + + if ($name === self::ELEMENT_NAME_CLASS) { + $name = $attributes['name'] ?? ''; + $file->mergeClass((string) $name, $this->parseClass($childXml)); + + return $file; + } + + if ($name === self::ELEMENT_NAME_LINE) { + $file->mergeLine((int) $attributes['num'], $this->parseLine($childXml)); + } + + return $file; + } +} diff --git a/src/Clover/Renderer.php b/src/Clover/Renderer.php new file mode 100644 index 0000000..5bc8cec --- /dev/null +++ b/src/Clover/Renderer.php @@ -0,0 +1,239 @@ +getFiles(); + \ksort($files); + + $xmlDocument = new \DOMDocument('1.0', 'UTF-8'); + + $xmlCoverage = $xmlDocument->createElement('coverage'); + $xmlCoverage->setAttribute('generated', (string)\time()); + $xmlDocument->appendChild($xmlCoverage); + + $xmlProject = $xmlDocument->createElement('project'); + $xmlProject->setAttribute('timestamp', (string)\time()); + $xmlCoverage->appendChild($xmlProject); + + $projectMetrics = Metrics::makeEmpty(); + $packages = []; + + foreach ($files as $name => $file) { + [$xmlFile, $fileMetrics] = $this->renderFile($xmlDocument, $file, $name); + $projectMetrics = $projectMetrics->merge($fileMetrics); + $packageName = $file->packageName; + if ($packageName === null) { + $xmlProject->appendChild($xmlFile); + continue; + } + + $existedPackage = $packages[$packageName] ?? null; + if ($existedPackage !== null) { + $existedPackage[0]->appendChild($xmlFile); + $existedPackage[1] = $existedPackage[1]->merge($fileMetrics); + $packages[$packageName] = $existedPackage; + continue; + } + + $xmlPackage = $xmlDocument->createElement('package'); + $xmlPackage->setAttribute('name', $packageName); + $xmlProject->appendChild($xmlPackage); + $xmlPackage->appendChild($xmlFile); + + $packageMetrics = Metrics::makeEmpty(); + $packageMetrics->packageCount = 1; + $packageMetrics = $packageMetrics->merge($fileMetrics); + $packages[$packageName] = [$xmlPackage, $packageMetrics]; + } + + foreach ($packages as $package) { + /** @var Metrics $packageMetrics */ + $packageMetrics = $package[1]; + $package[0]->appendChild($this->renderMetricsPackage($xmlDocument, $packageMetrics)); + } + + $xmlProject->appendChild($this->renderMetricsProject($xmlDocument, $projectMetrics)); + + return [$xmlDocument->saveXML(), $projectMetrics]; + } + + /** + * Create an XML element to represent these metrics under a file. + * @param \DOMDocument $xmlDocument The parent document + * @param Metrics $metrics + * @return \DOMElement + * @throws \DOMException + */ + private function renderMetrics(\DOMDocument $xmlDocument, Metrics $metrics): \DOMElement + { + $xmlMetrics = $xmlDocument->createElement('metrics'); + // We can't know the complexity, just set 0 + // (attribute required by the clover xml schema) + $xmlMetrics->setAttribute('complexity', '0'); + + $xmlMetrics->setAttribute('elements', (string) $metrics->getElementCount()); + $xmlMetrics->setAttribute('coveredelements', (string) $metrics->getCoveredElementCount()); + $xmlMetrics->setAttribute('conditionals', (string) $metrics->conditionalCount); + $xmlMetrics->setAttribute('coveredconditionals', (string) $metrics->coveredConditionalCount); + $xmlMetrics->setAttribute('statements', (string) $metrics->statementCount); + $xmlMetrics->setAttribute('coveredstatements', (string) $metrics->coveredStatementCount); + $xmlMetrics->setAttribute('methods', (string) $metrics->methodCount); + $xmlMetrics->setAttribute('coveredmethods', (string) $metrics->coveredMethodCount); + $xmlMetrics->setAttribute('classes', (string) $metrics->classCount); + + return $xmlMetrics; + } + + /** + * Create an XML element to represent these metrics under a package. + * Contains all the attributes of the file context plus the number of files. + * @param \DOMDocument $xmlDocument The parent document. + * @param Metrics $metrics + * @return \DOMElement + * @throws \DOMException + */ + private function renderMetricsPackage(\DOMDocument $xmlDocument, Metrics $metrics): \DOMElement + { + $xmlMetrics = $this->renderMetrics($xmlDocument, $metrics); + $xmlMetrics->setAttribute('files', (string) $metrics->fileCount); + + return $xmlMetrics; + } + + /** + * Create an XML element to represent these metrics under a project. + * Contains all the attributes of the package context plus the number of packages. + * @param \DOMDocument $xmlDocument The parent document. + * @param Metrics $metrics + * @return \DOMElement + * @throws \DOMException + */ + private function renderMetricsProject(\DOMDocument $xmlDocument, Metrics $metrics): \DOMElement + { + $xmlMetrics = $this->renderMetricsPackage($xmlDocument, $metrics); + $xmlMetrics->setAttribute('packages', (string) $metrics->packageCount); + + return $xmlMetrics; + } + + /** + * @param \DOMDocument $document + * @param FileDto $fileDto + * @param string $name + * @return array{0:\DOMElement,1:Metrics} + * @throws \DOMException + */ + private function renderFile(\DOMDocument $document, FileDto $fileDto, string $name): array + { + $xmlFile = $document->createElement('file'); + $xmlFile->setAttribute('name', $name); + + $classes = $fileDto->getClasses(); + $lines = $fileDto->getLines(); + + // Metric counts + $statementCount = 0; + $coveredStatementCount = 0; + $conditionalCount = 0; + $coveredConditionalCount = 0; + $methodCount = 0; + $coveredMethodCount = 0; + $classCount = \count($classes); + + foreach ($classes as $class) { + $xmlFile->appendChild($this->renderClass($document, $class)); + } + + foreach ($lines as $line) { + $xmlFile->appendChild($this->renderLine($document, $line)); + $properties = $line->getProperties(); + + $covered = $line->getCount() > 0; + $type = $properties['type'] ?? 'stmt'; + + if ($type === 'method') { + $methodCount++; + if ($covered) { + $coveredMethodCount++; + } + } elseif ($type === 'stmt') { + $statementCount++; + if ($covered) { + $coveredStatementCount++; + } + } elseif ($type === 'cond') { + $conditionalCount++; + if ($covered) { + $coveredConditionalCount++; + } + } + } + + $metrics = new Metrics( + $statementCount, + $coveredStatementCount, + $conditionalCount, + $coveredConditionalCount, + $methodCount, + $coveredMethodCount, + $classCount, + 1 + ); + + $xmlFile->appendChild($this->renderMetrics($document, $metrics)); + + return [$xmlFile, $metrics]; + } + + /** + * @param \DOMDocument $document + * @param ClassDto $class + * @return \DOMElement + * @throws \DOMException + */ + private function renderClass(\DOMDocument $document, ClassDto $class): \DOMElement + { + $xmlClass = $document->createElement('class'); + + foreach ($class->getProperties() as $key => $value) { + $xmlClass->setAttribute($key, $value); + } + + return $xmlClass; + } + + /** + * @param \DOMDocument $document + * @param LineDto $line + * @return \DOMElement + * @throws \DOMException + */ + private function renderLine(\DOMDocument $document, LineDto $line): \DOMElement + { + $xmlLine = $document->createElement('line'); + foreach ($line->getProperties() as $key => $value) { + $xmlLine->setAttribute($key, $value); + } + + $xmlLine->setAttribute('count', (string) $line->getCount()); + + return $xmlLine; + } +} diff --git a/src/Command/CloverMergeCommand.php b/src/Command/CloverMergeCommand.php new file mode 100644 index 0000000..943835c --- /dev/null +++ b/src/Command/CloverMergeCommand.php @@ -0,0 +1,131 @@ +arguments('[files...]') + ->option('-w|--workdir', 'Path to workdir, to work with relative paths in files') + ->option('-o|--output', 'Path to result file', null, './merged.xml') + ->option('-s|--stats', 'Print calculated statistic', null, false); + + $this->handler = $handler; + $this->renderer = $renderer; + } + + /** + * @param Interactor $io + * @return void + * @throws InvalidArgumentException + */ + public function interact(Interactor $io): void + { + $isVerbose = $this->verbosity >= 1; + + $values = $this->values(false); + $files = $values['files'] ?? null; + $workdir = $values['workdir'] ?? null; + + if ($files === null || \count($files) === 0) { + throw new InvalidArgumentException('files', $isVerbose); + } + + if ($workdir !== null && \is_dir($workdir) === false) { + throw new InvalidArgumentException('workdir', $isVerbose); + } + } + + /** + * @throws ExecuteException + */ + public function execute(array $files, string $output, ?string $workdir, bool $stats): int + { + $isVerbose = $this->verbosity >= 1; + + $fullOutputPath = $output; + if ($workdir !== null) { + $fullOutputPath = "{$workdir}/{$output}"; + } + + $documentsCollection = \array_map(static function (string $path) use ($workdir): \SimpleXMLElement { + $fullPath = $path; + if ($workdir !== null) { + $fullPath = "{$workdir}/{$path}"; + } + + $file = new \SplFileInfo($fullPath); + + if ($file->isFile() === false || $file->isReadable() === false) { + throw new ExecuteException("File {$fullPath} does not exists or not readable"); + } + + $document = \simplexml_load_file( + $file->getPathname(), + \SimpleXMLElement::class, + LIBXML_NOWARNING | LIBXML_NOERROR, + ); + + if ($document === false) { + throw new ExecuteException("Unable to parse file {$file->getPathname()}"); + } + + return $document; + }, $files); + + try { + $accumulator = $this->handler->handle(...$documentsCollection); + [$xml, $metrics] = $this->renderer->renderAccumulator($accumulator); + } catch (\Throwable $exception) { + throw new ExecuteException($exception->getMessage(), $isVerbose, $exception); + } + + $writeResult = \file_put_contents($fullOutputPath, $xml); + + if ($writeResult === false) { + throw new ExecuteException('Unable to write to given output file', $isVerbose); + } + + if ($stats === false) { + return 0; + } + + $filesDiscovered = $metrics->fileCount; + $elementCount = $metrics->getElementCount(); + $coveredElementCount = $metrics->getCoveredElementCount(); + + if ($elementCount === 0) { + $coveragePercentage = 0; + } else { + $coveragePercentage = 100 * $coveredElementCount / $elementCount; + } + + $io = $this->io(); + $io->info(\sprintf("Files Discovered: %d", $filesDiscovered), true); + $io->info( + \sprintf("Final Coverage: %d/%d (%.2f%%)", $coveredElementCount, $elementCount, $coveragePercentage), + true, + ); + + return 0; + } +} diff --git a/src/Command/Exceptions/CommandException.php b/src/Command/Exceptions/CommandException.php new file mode 100644 index 0000000..139781c --- /dev/null +++ b/src/Command/Exceptions/CommandException.php @@ -0,0 +1,22 @@ +isVerbose = $isVerbose; + } + + public function isVerbose(): bool + { + return $this->isVerbose; + } +} diff --git a/src/Command/Exceptions/ExecuteException.php b/src/Command/Exceptions/ExecuteException.php new file mode 100644 index 0000000..6581f0f --- /dev/null +++ b/src/Command/Exceptions/ExecuteException.php @@ -0,0 +1,15 @@ +