From ea696f819e017d9ab4d44faa69bb173055106cec Mon Sep 17 00:00:00 2001 From: Owen Voke Date: Tue, 15 Jun 2021 14:26:53 +0100 Subject: [PATCH] feat: move Coverage out of external plugin --- composer.json | 2 +- src/Plugins/Coverage.php | 119 ++++++++++++++++++++++++ src/Support/Coverage.php | 169 +++++++++++++++++++++++++++++++++++ tests/.snapshots/success.txt | 8 +- tests/Features/Coverage.php | 58 ++++++++++++ 5 files changed, 354 insertions(+), 2 deletions(-) create mode 100644 src/Plugins/Coverage.php create mode 100644 src/Support/Coverage.php create mode 100644 tests/Features/Coverage.php diff --git a/composer.json b/composer.json index 75a7e039..e075f4e3 100644 --- a/composer.json +++ b/composer.json @@ -20,7 +20,6 @@ "php": "^7.3 || ^8.0", "nunomaduro/collision": "^5.0", "pestphp/pest-plugin": "^1.0", - "pestphp/pest-plugin-coverage": "^1.0", "pestphp/pest-plugin-expectations": "^1.6", "phpunit/phpunit": ">= 9.3.7 <= 9.5.5" }, @@ -76,6 +75,7 @@ }, "pest": { "plugins": [ + "Pest\\Plugins\\Coverage", "Pest\\Plugins\\Init", "Pest\\Plugins\\Version" ] diff --git a/src/Plugins/Coverage.php b/src/Plugins/Coverage.php new file mode 100644 index 00000000..cc03bd17 --- /dev/null +++ b/src/Plugins/Coverage.php @@ -0,0 +1,119 @@ +output = $output; + } + + public function handleArguments(array $originals): array + { + $arguments = array_merge([''], array_values(array_filter($originals, function ($original): bool { + foreach ([self::COVERAGE_OPTION, self::MIN_OPTION] as $option) { + if ($original === sprintf('--%s', $option) || Str::startsWith($original, sprintf('--%s=', $option))) { + return true; + } + } + + return false; + }))); + + $originals = array_flip($originals); + foreach ($arguments as $argument) { + unset($originals[$argument]); + } + $originals = array_flip($originals); + + $inputs = []; + $inputs[] = new InputOption(self::COVERAGE_OPTION, null, InputOption::VALUE_NONE); + $inputs[] = new InputOption(self::MIN_OPTION, null, InputOption::VALUE_REQUIRED); + + $input = new ArgvInput($arguments, new InputDefinition($inputs)); + if ((bool) $input->getOption(self::COVERAGE_OPTION)) { + $this->coverage = true; + $originals[] = '--coverage-php'; + $originals[] = \Pest\Support\Coverage::getPath(); + } + + if ($input->getOption(self::MIN_OPTION) !== null) { + /* @phpstan-ignore-next-line */ + $this->coverageMin = (float) $input->getOption(self::MIN_OPTION); + } + + return $originals; + } + + /** + * Allows to add custom output after the test suite was executed. + */ + public function addOutput(int $result): int + { + if ($result === 0 && $this->coverage) { + if (!\Pest\Support\Coverage::isAvailable()) { + $this->output->writeln( + "\n ERROR No code coverage driver is available.", + ); + exit(1); + } + + $coverage = \Pest\Support\Coverage::report($this->output); + + $result = (int) ($coverage < $this->coverageMin); + + if ($result === 1) { + $this->output->writeln(sprintf( + "\n FAIL Code coverage below expected: %s %%. Minimum: %s %%.", + number_format($coverage, 1), + number_format($this->coverageMin, 1) + )); + } + } + + return $result; + } +} diff --git a/src/Support/Coverage.php b/src/Support/Coverage.php new file mode 100644 index 00000000..4d2096c9 --- /dev/null +++ b/src/Support/Coverage.php @@ -0,0 +1,169 @@ +canCollectCodeCoverage(); + } + + /** + * Reports the code coverage report to the + * console and returns the result in float. + */ + public static function report(OutputInterface $output): float + { + if (!file_exists($reportPath = self::getPath())) { + throw ShouldNotHappen::fromMessage(sprintf('Coverage not found in path: %s.', $reportPath)); + } + + /** @var CodeCoverage $codeCoverage */ + $codeCoverage = require $reportPath; + unlink($reportPath); + + $totalWidth = (new Terminal())->getWidth(); + + $dottedLineLength = $totalWidth <= 70 ? $totalWidth : 70; + + $totalCoverage = $codeCoverage->getReport()->percentageOfExecutedLines(); + + $output->writeln( + sprintf( + ' Cov: %s', + $totalCoverage->asString() + ) + ); + + $output->writeln(''); + + /** @var Directory $report */ + $report = $codeCoverage->getReport(); + + foreach ($report->getIterator() as $file) { + if (!$file instanceof File) { + continue; + } + $dirname = dirname($file->id()); + $basename = basename($file->id(), '.php'); + + $name = $dirname === '.' ? $basename : implode(DIRECTORY_SEPARATOR, [ + $dirname, + $basename, + ]); + $rawName = $dirname === '.' ? $basename : implode(DIRECTORY_SEPARATOR, [ + $dirname, + $basename, + ]); + + $linesExecutedTakenSize = 0; + + if ($file->percentageOfExecutedLines()->asString() != '0.00%') { + $linesExecutedTakenSize = strlen($uncoveredLines = trim(implode(', ', self::getMissingCoverage($file)))) + 1; + $name .= sprintf(' %s', $uncoveredLines); + } + + $percentage = $file->numberOfExecutableLines() === 0 + ? '100.0' + : number_format($file->percentageOfExecutedLines()->asFloat(), 1, '.', ''); + + $takenSize = strlen($rawName . $percentage) + 4 + $linesExecutedTakenSize; // adding 3 space and percent sign + + $percentage = sprintf( + '%s', + $percentage === '100.0' ? 'green' : ($percentage === '0.0' ? 'red' : 'yellow'), + $percentage + ); + + $output->writeln(sprintf( + ' %s %s %s %%', + $name, + str_repeat('.', max($dottedLineLength - $takenSize, 1)), + $percentage + )); + } + + return $totalCoverage->asFloat(); + } + + /** + * Generates an array of missing coverage on the following format:. + * + * ``` + * ['11', '20..25', '50', '60..80']; + * ``` + * + * @param File $file + * + * @return array + */ + public static function getMissingCoverage($file): array + { + $shouldBeNewLine = true; + + $eachLine = function (array $array, array $tests, int $line) use (&$shouldBeNewLine): array { + if (count($tests) > 0) { + $shouldBeNewLine = true; + + return $array; + } + + if ($shouldBeNewLine) { + $array[] = (string) $line; + $shouldBeNewLine = false; + + return $array; + } + + $lastKey = count($array) - 1; + + if (array_key_exists($lastKey, $array) && strpos($array[$lastKey], '..') !== false) { + [$from] = explode('..', $array[$lastKey]); + $array[$lastKey] = $line > $from ? sprintf('%s..%s', $from, $line) : sprintf('%s..%s', $line, $from); + + return $array; + } + + $array[$lastKey] = sprintf('%s..%s', $array[$lastKey], $line); + + return $array; + }; + + $array = []; + foreach (array_filter($file->lineCoverageData(), 'is_array') as $line => $tests) { + $array = $eachLine($array, $tests, $line); + } + + return $array; + } +} diff --git a/tests/.snapshots/success.txt b/tests/.snapshots/success.txt index 946c9e7b..0bb41268 100644 --- a/tests/.snapshots/success.txt +++ b/tests/.snapshots/success.txt @@ -17,6 +17,12 @@ ✓ it gets executed before each test ✓ it gets executed before each test once again + PASS Tests\Features\Coverage + ✓ it has plugin + ✓ it adds coverage if --coverage exist + ✓ it adds coverage if --min exist + ✓ it generates coverage based on file input + PASS Tests\Features\Datasets ✓ it throws exception if dataset does not exist ✓ it throws exception if dataset already exist @@ -276,5 +282,5 @@ ✓ it is a test ✓ it uses correct parent class - Tests: 4 incompleted, 7 skipped, 168 passed + Tests: 4 incompleted, 7 skipped, 172 passed \ No newline at end of file diff --git a/tests/Features/Coverage.php b/tests/Features/Coverage.php new file mode 100644 index 00000000..e43ae180 --- /dev/null +++ b/tests/Features/Coverage.php @@ -0,0 +1,58 @@ +assertTrue(class_exists(CoveragePlugin::class)); + +it('adds coverage if --coverage exist', function () { + $plugin = new CoveragePlugin(new ConsoleOutput()); + $testSuite = TestSuite::getInstance(); + + expect($plugin->coverage)->toBeFalse(); + $arguments = $plugin->handleArguments([]); + expect($arguments)->toEqual([]); + expect($plugin->coverage)->toBeFalse(); + + $arguments = $plugin->handleArguments(['--coverage']); + expect($arguments)->toEqual(['--coverage-php', Coverage::getPath()]); + expect($plugin->coverage)->toBeTrue(); +}); + +it('adds coverage if --min exist', function () { + $plugin = new CoveragePlugin(new ConsoleOutput()); + expect($plugin->coverageMin)->toEqual(0.0); + + expect($plugin->coverage)->toBeFalse(); + $plugin->handleArguments([]); + expect($plugin->coverageMin)->toEqual(0.0); + + $plugin->handleArguments(['--min=2']); + expect($plugin->coverageMin)->toEqual(2.0); + + $plugin->handleArguments(['--min=2.4']); + expect($plugin->coverageMin)->toEqual(2.4); +}); + +it('generates coverage based on file input', function () { + expect(Coverage::getMissingCoverage(new class() { + public function lineCoverageData(): array + { + return [ + 1 => ['foo'], + 2 => ['bar'], + 4 => [], + 5 => [], + 6 => [], + 7 => null, + 100 => null, + 101 => ['foo'], + 102 => [], + ]; + } + }))->toEqual([ + '4..6', '102', + ]); +});