feat: rewrites --dirty support

This commit is contained in:
Nuno Maduro
2023-01-10 22:23:52 +00:00
parent f6676118ac
commit 15931e2418
6 changed files with 102 additions and 84 deletions

View File

@ -3,10 +3,9 @@
use Pest\Actions\ValidatesEnvironment; use Pest\Actions\ValidatesEnvironment;
use Pest\ConfigLoader; use Pest\ConfigLoader;
use Pest\Repositories\TestRepository;
use Pest\Support\Container;
use Pest\Kernel; use Pest\Kernel;
use Pest\Support\DirtyTestCaseFilter; use Pest\Support\Container;
use Pest\TestCaseFilters\GitDirtyTestCaseFilter;
use Pest\TestSuite; use Pest\TestSuite;
use Symfony\Component\Console\Input\ArgvInput; use Symfony\Component\Console\Input\ArgvInput;
use Symfony\Component\Console\Output\ConsoleOutput; use Symfony\Component\Console\Output\ConsoleOutput;
@ -46,20 +45,17 @@ use Symfony\Component\Console\Output\OutputInterface;
$rootPath = dirname($autoloadPath, 2); $rootPath = dirname($autoloadPath, 2);
$argv = new ArgvInput(); $argv = new ArgvInput();
$filters = [];
foreach ($args as $key => $value) {
if (str_contains($value, '--dirty')) {
$filters[] = new DirtyTestCaseFilter($rootPath);
}
}
$testSuite = TestSuite::getInstance( $testSuite = TestSuite::getInstance(
$rootPath, $rootPath,
$argv->getParameterOption('--test-directory', (new ConfigLoader($rootPath))->getTestsDirectory()), $argv->getParameterOption('--test-directory', (new ConfigLoader($rootPath))->getTestsDirectory()),
new TestRepository($filters),
); );
foreach ($args as $key => $value) {
if (str_contains($value, '--dirty')) {
$testSuite->tests->filter(new GitDirtyTestCaseFilter($rootPath));
}
}
$isDecorated = $argv->getParameterOption('--colors', 'always') !== 'never'; $isDecorated = $argv->getParameterOption('--colors', 'always') !== 'never';
$output = new ConsoleOutput(ConsoleOutput::VERBOSITY_NORMAL, $isDecorated); $output = new ConsoleOutput(ConsoleOutput::VERBOSITY_NORMAL, $isDecorated);

View File

@ -6,5 +6,8 @@ namespace Pest\Contracts;
interface TestCaseFilter interface TestCaseFilter
{ {
public function canLoad(string $suiteClassFile): bool; /**
* Whether the test case is accepted.
*/
public function accept(string $testCaseFilename): bool;
} }

View File

@ -29,11 +29,9 @@ final class TestRepository
private array $uses = []; private array $uses = [];
/** /**
* @param array<int, TestCaseFilter> $testCaseFilters * @var array<int, TestCaseFilter>
*/ */
public function __construct(private readonly array $testCaseFilters = []) private array $filters = [];
{
}
/** /**
* Counts the number of test cases. * Counts the number of test cases.
@ -56,7 +54,7 @@ final class TestRepository
$testCases = $this->testCases; $testCases = $this->testCases;
} }
return array_values(array_map(static fn(TestCaseFactory $factory): string => $factory->filename, $testCases)); return array_values(array_map(static fn (TestCaseFactory $factory): string => $factory->filename, $testCases));
} }
/** /**
@ -92,6 +90,14 @@ final class TestRepository
} }
} }
/**
* Filters the test cases using the given filter.
*/
public function filter(TestCaseFilter $filter): void
{
$this->filters[] = $filter;
}
/** /**
* Gets the test case factory from the given filename. * Gets the test case factory from the given filename.
*/ */
@ -105,7 +111,7 @@ final class TestRepository
*/ */
public function set(TestCaseMethodFactory $method): void public function set(TestCaseMethodFactory $method): void
{ {
if (!array_key_exists($method->filename, $this->testCases)) { if (! array_key_exists($method->filename, $this->testCases)) {
$this->testCases[$method->filename] = new TestCaseFactory($method->filename); $this->testCases[$method->filename] = new TestCaseFactory($method->filename);
} }
@ -117,17 +123,17 @@ final class TestRepository
*/ */
public function makeIfNeeded(string $filename): void public function makeIfNeeded(string $filename): void
{ {
if (!array_key_exists($filename, $this->testCases)) { if (! array_key_exists($filename, $this->testCases)) {
return; return;
} }
$canLoad = array_reduce( $accepted = array_reduce(
$this->testCaseFilters, $this->filters,
fn(bool $carry, TestCaseFilter $filter): bool => $carry && $filter->canLoad($filename), fn (bool $carry, TestCaseFilter $filter): bool => $carry && $filter->accept($filename),
true, true,
); );
if ($canLoad) { if ($accepted) {
$this->make($this->testCases[$filename]); $this->make($this->testCases[$filename]);
} }
} }
@ -137,12 +143,12 @@ final class TestRepository
*/ */
private function make(TestCaseFactory $testCase): void private function make(TestCaseFactory $testCase): void
{ {
$startsWith = static fn(string $target, string $directory): bool => Str::startsWith($target, $directory . DIRECTORY_SEPARATOR); $startsWith = static fn (string $target, string $directory): bool => Str::startsWith($target, $directory.DIRECTORY_SEPARATOR);
foreach ($this->uses as $path => $uses) { foreach ($this->uses as $path => $uses) {
[$classOrTraits, $groups, $hooks] = $uses; [$classOrTraits, $groups, $hooks] = $uses;
if ((!is_dir($path) && $testCase->filename === $path) || (is_dir($path) && $startsWith($testCase->filename, $path))) { if ((! is_dir($path) && $testCase->filename === $path) || (is_dir($path) && $startsWith($testCase->filename, $path))) {
foreach ($classOrTraits as $class) { foreach ($classOrTraits as $class) {
/** @var string $class */ /** @var string $class */
if (class_exists($class)) { if (class_exists($class)) {

View File

@ -1,47 +0,0 @@
<?php
declare(strict_types=1);
namespace Pest\Support;
use Pest\Contracts\TestCaseFilter;
use Symfony\Component\Process\Process;
final class DirtyTestCaseFilter implements TestCaseFilter
{
/**
* @var array<string>
*/
private array $changedFiles = [];
public function __construct(private string $projectRoot)
{
$this->loadDiff();
}
public function canLoad(string $suiteClassFile): bool
{
$relativePath = str_replace($this->projectRoot, '', $suiteClassFile);
if (str_starts_with($relativePath, '/')) {
$relativePath = substr($relativePath, 1);
}
return in_array($relativePath, $this->changedFiles, true);
}
private function loadDiff(): void
{
$process = new Process([
'git',
'diff',
'--name-only',
'HEAD',
'--',
'*.php',
]);
$process->run();
$this->changedFiles = explode(PHP_EOL, trim($process->getOutput()));
}
}

View File

@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
namespace Pest\TestCaseFilters;
use Pest\Contracts\TestCaseFilter;
use Pest\Exceptions\MissingDependency;
use Symfony\Component\Process\Process;
final class GitDirtyTestCaseFilter implements TestCaseFilter
{
/**
* @var array<string>
*/
private array $changedFiles = [];
public function __construct(private readonly string $projectRoot)
{
$this->loadDiff();
}
public function accept(string $testCaseFilename): bool
{
$relativePath = str_replace($this->projectRoot, '', $testCaseFilename);
if (str_starts_with($relativePath, '/')) {
$relativePath = substr($relativePath, 1);
}
return in_array($relativePath, $this->changedFiles, true);
}
private function loadDiff(): void
{
$process = new Process(['git', 'status', '--short', '--', '*.php']);
$process->run();
if (! $process->isSuccessful()) {
throw new MissingDependency('Filter by dirty files', 'git');
}
$output = preg_split('/\R+/', $process->getOutput(), flags: PREG_SPLIT_NO_EMPTY);
assert(is_array($output));
$dirtyFiles = [];
foreach ($output as $dirtyFile) {
$dirtyFiles[substr($dirtyFile, 3)] = trim(substr($dirtyFile, 0, 3));
}
$dirtyFiles = array_filter($dirtyFiles, fn ($status): bool => $status !== 'D');
$dirtyFiles = array_map(fn ($file, $status): string => in_array($status, ['R', 'RM'], true) ? explode(' -> ', $file)[1] : $file, array_keys($dirtyFiles), $dirtyFiles);
$this->changedFiles = $dirtyFiles = array_values($dirtyFiles);
}
}

View File

@ -23,6 +23,11 @@ final class TestSuite
*/ */
public ?TestCase $test = null; public ?TestCase $test = null;
/**
* Holds the tests repository.
*/
public TestRepository $tests;
/** /**
* Holds the before each repository. * Holds the before each repository.
*/ */
@ -64,11 +69,10 @@ final class TestSuite
public function __construct( public function __construct(
string $rootPath, string $rootPath,
public string $testPath, public string $testPath,
public TestRepository $tests = new TestRepository(), ) {
)
{
$this->beforeAll = new BeforeAllRepository(); $this->beforeAll = new BeforeAllRepository();
$this->beforeEach = new BeforeEachRepository(); $this->beforeEach = new BeforeEachRepository();
$this->tests = new TestRepository();
$this->afterEach = new AfterEachRepository(); $this->afterEach = new AfterEachRepository();
$this->afterAll = new AfterAllRepository(); $this->afterAll = new AfterAllRepository();
$this->retryRepository = new RetryRepository('retry'); $this->retryRepository = new RetryRepository('retry');
@ -82,11 +86,9 @@ final class TestSuite
public static function getInstance( public static function getInstance(
string $rootPath = null, string $rootPath = null,
string $testPath = null, string $testPath = null,
TestRepository $tests = null, ): TestSuite {
): TestSuite
{
if (is_string($rootPath) && is_string($testPath)) { if (is_string($rootPath) && is_string($testPath)) {
self::$instance = new TestSuite($rootPath, $testPath, $tests); self::$instance = new TestSuite($rootPath, $testPath);
foreach (Plugin::$callables as $callable) { foreach (Plugin::$callables as $callable) {
$callable(); $callable();