mirror of
https://github.com/pestphp/pest.git
synced 2026-03-07 00:07:22 +01:00
Merge pull request #318 from owenvoke/feature/coverage
feat: move Coverage out of external plugin
This commit is contained in:
@ -20,7 +20,6 @@
|
|||||||
"php": "^7.3 || ^8.0",
|
"php": "^7.3 || ^8.0",
|
||||||
"nunomaduro/collision": "^5.0",
|
"nunomaduro/collision": "^5.0",
|
||||||
"pestphp/pest-plugin": "^1.0",
|
"pestphp/pest-plugin": "^1.0",
|
||||||
"pestphp/pest-plugin-coverage": "^1.0",
|
|
||||||
"pestphp/pest-plugin-expectations": "^1.6",
|
"pestphp/pest-plugin-expectations": "^1.6",
|
||||||
"phpunit/phpunit": ">= 9.3.7 <= 9.5.5"
|
"phpunit/phpunit": ">= 9.3.7 <= 9.5.5"
|
||||||
},
|
},
|
||||||
@ -76,6 +75,7 @@
|
|||||||
},
|
},
|
||||||
"pest": {
|
"pest": {
|
||||||
"plugins": [
|
"plugins": [
|
||||||
|
"Pest\\Plugins\\Coverage",
|
||||||
"Pest\\Plugins\\Init",
|
"Pest\\Plugins\\Init",
|
||||||
"Pest\\Plugins\\Version"
|
"Pest\\Plugins\\Version"
|
||||||
]
|
]
|
||||||
|
|||||||
119
src/Plugins/Coverage.php
Normal file
119
src/Plugins/Coverage.php
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pest\Plugins;
|
||||||
|
|
||||||
|
use Pest\Contracts\Plugins\AddsOutput;
|
||||||
|
use Pest\Contracts\Plugins\HandlesArguments;
|
||||||
|
use Pest\Support\Str;
|
||||||
|
use Symfony\Component\Console\Input\ArgvInput;
|
||||||
|
use Symfony\Component\Console\Input\InputDefinition;
|
||||||
|
use Symfony\Component\Console\Input\InputOption;
|
||||||
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
final class Coverage implements AddsOutput, HandlesArguments
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
private const COVERAGE_OPTION = 'coverage';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
private const MIN_OPTION = 'min';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether should show the coverage or not.
|
||||||
|
*
|
||||||
|
* @var bool
|
||||||
|
*/
|
||||||
|
public $coverage = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The minimum coverage.
|
||||||
|
*
|
||||||
|
* @var float
|
||||||
|
*/
|
||||||
|
public $coverageMin = 0.0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var OutputInterface
|
||||||
|
*/
|
||||||
|
private $output;
|
||||||
|
|
||||||
|
public function __construct(OutputInterface $output)
|
||||||
|
{
|
||||||
|
$this->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 <fg=white;bg=red;options=bold> 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 <fg=white;bg=red;options=bold> FAIL </> Code coverage below expected:<fg=red;options=bold> %s %%</>. Minimum:<fg=white;options=bold> %s %%</>.",
|
||||||
|
number_format($coverage, 1),
|
||||||
|
number_format($this->coverageMin, 1)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
}
|
||||||
169
src/Support/Coverage.php
Normal file
169
src/Support/Coverage.php
Normal file
@ -0,0 +1,169 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pest\Support;
|
||||||
|
|
||||||
|
use Pest\Exceptions\ShouldNotHappen;
|
||||||
|
use SebastianBergmann\CodeCoverage\CodeCoverage;
|
||||||
|
use SebastianBergmann\CodeCoverage\Node\Directory;
|
||||||
|
use SebastianBergmann\CodeCoverage\Node\File;
|
||||||
|
use SebastianBergmann\Environment\Runtime;
|
||||||
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
|
use Symfony\Component\Console\Terminal;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
final class Coverage
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Returns the coverage path.
|
||||||
|
*/
|
||||||
|
public static function getPath(): string
|
||||||
|
{
|
||||||
|
return implode(DIRECTORY_SEPARATOR, [
|
||||||
|
dirname(__DIR__, 2),
|
||||||
|
'.temp',
|
||||||
|
'coverage.php',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs true there is any code
|
||||||
|
* coverage driver available.
|
||||||
|
*/
|
||||||
|
public static function isAvailable(): bool
|
||||||
|
{
|
||||||
|
return (new Runtime())->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(
|
||||||
|
' <fg=white;options=bold>Cov: </><fg=default>%s</>',
|
||||||
|
$totalCoverage->asString()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
$output->writeln('');
|
||||||
|
|
||||||
|
/** @var Directory<File|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(' <fg=red>%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(
|
||||||
|
'<fg=%s>%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<int, string>
|
||||||
|
*/
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -17,6 +17,12 @@
|
|||||||
✓ it gets executed before each test
|
✓ it gets executed before each test
|
||||||
✓ it gets executed before each test once again
|
✓ 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
|
PASS Tests\Features\Datasets
|
||||||
✓ it throws exception if dataset does not exist
|
✓ it throws exception if dataset does not exist
|
||||||
✓ it throws exception if dataset already exist
|
✓ it throws exception if dataset already exist
|
||||||
@ -276,5 +282,5 @@
|
|||||||
✓ it is a test
|
✓ it is a test
|
||||||
✓ it uses correct parent class
|
✓ it uses correct parent class
|
||||||
|
|
||||||
Tests: 4 incompleted, 7 skipped, 168 passed
|
Tests: 4 incompleted, 7 skipped, 172 passed
|
||||||
|
|
||||||
58
tests/Features/Coverage.php
Normal file
58
tests/Features/Coverage.php
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Pest\Plugins\Coverage as CoveragePlugin;
|
||||||
|
use Pest\Support\Coverage;
|
||||||
|
use Pest\TestSuite;
|
||||||
|
use Symfony\Component\Console\Output\ConsoleOutput;
|
||||||
|
|
||||||
|
it('has plugin')->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',
|
||||||
|
]);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user