This commit is contained in:
Nuno Maduro
2020-05-11 18:38:30 +02:00
commit de2929077b
112 changed files with 6211 additions and 0 deletions

143
src/Console/Command.php Normal file
View File

@ -0,0 +1,143 @@
<?php
declare(strict_types=1);
namespace Pest\Console;
use Pest\Actions\AddsCoverage;
use Pest\Actions\AddsDefaults;
use Pest\Actions\AddsTests;
use Pest\Actions\LoadStructure;
use Pest\Actions\ValidatesConfiguration;
use Pest\Exceptions\CodeCoverageDriverNotAvailable;
use Pest\TestSuite;
use PHPUnit\Framework\TestSuite as BaseTestSuite;
use PHPUnit\TextUI\Command as BaseCommand;
use PHPUnit\TextUI\TestRunner;
use SebastianBergmann\FileIterator\Facade as FileIteratorFacade;
use Symfony\Component\Console\Output\OutputInterface;
/**
* @internal
*/
final class Command extends BaseCommand
{
/**
* Holds the current testing suite.
*
* @var TestSuite
*/
private $testSuite;
/**
* Holds the current console output.
*
* @var OutputInterface
*/
private $output;
/**
* Creates a new instance of the command class.
*/
public function __construct(TestSuite $testSuite, OutputInterface $output)
{
$this->testSuite = $testSuite;
$this->output = $output;
}
/**
* {@inheritdoc}
*
* @phpstan-ignore-next-line
*
* @param array<int, string> $argv
*/
protected function handleArguments(array $argv): void
{
/*
* First, let's handle pest is own `--coverage` param.
*/
$argv = AddsCoverage::from($this->testSuite, $argv);
/*
* Next, as usual, let's send the console arguments to PHPUnit.
*/
parent::handleArguments($argv);
/*
* Finally, let's validate the configuration. Making
* sure all options are yet supported by Pest.
*/
ValidatesConfiguration::in($this->arguments);
}
/**
* Creates a new PHPUnit test runner.
*/
protected function createRunner(): TestRunner
{
/*
* First, let's add the defaults we use on `pest`. Those
* are the printer class, and others that may be appear.
*/
$this->arguments = AddsDefaults::to($this->arguments);
$testRunner = new TestRunner($this->arguments['loader']);
$testSuite = $this->arguments['test'];
if (is_string($testSuite)) {
if (\is_dir($testSuite)) {
/** @var string[] $files */
$files = (new FileIteratorFacade())->getFilesAsArray(
$testSuite,
$this->arguments['testSuffixes']
);
} else {
$files = [$testSuite];
}
$testSuite = new BaseTestSuite($testSuite);
$testSuite->addTestFiles($files);
$this->arguments['test'] = $testSuite;
}
LoadStructure::in($this->testSuite->rootPath);
AddsTests::to($testSuite, $this->testSuite);
return $testRunner;
}
/**
* {@inheritdoc}
*
* @phpstan-ignore-next-line
*
* @param array<int, string> $argv
*/
public function run(array $argv, bool $exit = true): int
{
$result = parent::run($argv, false);
if ($result === 0 && $this->testSuite->coverage) {
if (!Coverage::isAvailable()) {
throw new CodeCoverageDriverNotAvailable();
}
$coverage = Coverage::report($this->output);
$result = (int) ($coverage < $this->testSuite->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->testSuite->coverageMin, 1)
));
}
}
exit($result);
}
}

167
src/Console/Coverage.php Normal file
View File

@ -0,0 +1,167 @@
<?php
declare(strict_types=1);
namespace Pest\Console;
use Pest\Exceptions\ShouldNotHappen;
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 \SebastianBergmann\CodeCoverage\CodeCoverage $codeCoverage */
$codeCoverage = require $reportPath;
unlink($reportPath);
$totalWidth = (new Terminal())->getWidth();
$dottedLineLength = $totalWidth <= 70 ? $totalWidth : 70;
$totalCoverage = $codeCoverage->getReport()->getLineExecutedPercent();
$output->writeln(
sprintf(
' <fg=white;options=bold>Cov: </><fg=default>%s</>',
$totalCoverage
)
);
$output->writeln('');
/** @var Directory<File|Directory> $report */
$report = $codeCoverage->getReport();
foreach ($report->getIterator() as $file) {
if (!$file instanceof File) {
continue;
}
$dirname = dirname($file->getId());
$basename = basename($file->getId(), '.php');
$name = $dirname === '.' ? $basename : implode(DIRECTORY_SEPARATOR, [
$dirname,
$basename,
]);
$rawName = $dirname === '.' ? $basename : implode(DIRECTORY_SEPARATOR, [
$dirname,
$basename,
]);
$linesExecutedTakenSize = 0;
if ($file->getLineExecutedPercent() != '0.00%') {
$linesExecutedTakenSize = strlen($uncoveredLines = trim(implode(', ', self::getMissingCoverage($file)))) + 1;
$name .= sprintf(' <fg=red>%s</>', $uncoveredLines);
}
$percentage = $file->getNumExecutableLines() === 0
? '100.0'
: number_format((float) $file->getLineExecutedPercent(), 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 (float) $totalCoverage;
}
/**
* 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] = sprintf('%s..%s', $from, $line);
return $array;
}
$array[$lastKey] = sprintf('%s..%s', $array[$lastKey], $line);
return $array;
};
$array = [];
foreach (array_filter($file->getCoverageData(), 'is_array') as $line => $tests) {
$array = $eachLine($array, $tests, $line);
}
return $array;
}
}