mirror of
https://github.com/pestphp/pest.git
synced 2026-03-06 07:47:22 +01:00
feat: rewrites --dirty support
This commit is contained in:
20
bin/pest
20
bin/pest
@ -3,10 +3,9 @@
|
||||
|
||||
use Pest\Actions\ValidatesEnvironment;
|
||||
use Pest\ConfigLoader;
|
||||
use Pest\Repositories\TestRepository;
|
||||
use Pest\Support\Container;
|
||||
use Pest\Kernel;
|
||||
use Pest\Support\DirtyTestCaseFilter;
|
||||
use Pest\Support\Container;
|
||||
use Pest\TestCaseFilters\GitDirtyTestCaseFilter;
|
||||
use Pest\TestSuite;
|
||||
use Symfony\Component\Console\Input\ArgvInput;
|
||||
use Symfony\Component\Console\Output\ConsoleOutput;
|
||||
@ -46,20 +45,17 @@ use Symfony\Component\Console\Output\OutputInterface;
|
||||
$rootPath = dirname($autoloadPath, 2);
|
||||
$argv = new ArgvInput();
|
||||
|
||||
$filters = [];
|
||||
|
||||
foreach ($args as $key => $value) {
|
||||
if (str_contains($value, '--dirty')) {
|
||||
$filters[] = new DirtyTestCaseFilter($rootPath);
|
||||
}
|
||||
}
|
||||
|
||||
$testSuite = TestSuite::getInstance(
|
||||
$rootPath,
|
||||
$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';
|
||||
|
||||
$output = new ConsoleOutput(ConsoleOutput::VERBOSITY_NORMAL, $isDecorated);
|
||||
|
||||
@ -6,5 +6,8 @@ namespace Pest\Contracts;
|
||||
|
||||
interface TestCaseFilter
|
||||
{
|
||||
public function canLoad(string $suiteClassFile): bool;
|
||||
/**
|
||||
* Whether the test case is accepted.
|
||||
*/
|
||||
public function accept(string $testCaseFilename): bool;
|
||||
}
|
||||
|
||||
@ -29,11 +29,9 @@ final class TestRepository
|
||||
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.
|
||||
@ -56,16 +54,16 @@ final class TestRepository
|
||||
$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));
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses the given `$testCaseClass` on the given `$paths`.
|
||||
*
|
||||
* @param array<int, string> $classOrTraits
|
||||
* @param array<int, string> $groups
|
||||
* @param array<int, string> $paths
|
||||
* @param array<int, Closure> $hooks
|
||||
* @param array<int, string> $classOrTraits
|
||||
* @param array<int, string> $groups
|
||||
* @param array<int, string> $paths
|
||||
* @param array<int, Closure> $hooks
|
||||
*/
|
||||
public function use(array $classOrTraits, array $groups, array $paths, array $hooks): void
|
||||
{
|
||||
@ -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.
|
||||
*/
|
||||
@ -105,7 +111,7 @@ final class TestRepository
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
||||
@ -117,17 +123,17 @@ final class TestRepository
|
||||
*/
|
||||
public function makeIfNeeded(string $filename): void
|
||||
{
|
||||
if (!array_key_exists($filename, $this->testCases)) {
|
||||
if (! array_key_exists($filename, $this->testCases)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$canLoad = array_reduce(
|
||||
$this->testCaseFilters,
|
||||
fn(bool $carry, TestCaseFilter $filter): bool => $carry && $filter->canLoad($filename),
|
||||
$accepted = array_reduce(
|
||||
$this->filters,
|
||||
fn (bool $carry, TestCaseFilter $filter): bool => $carry && $filter->accept($filename),
|
||||
true,
|
||||
);
|
||||
|
||||
if ($canLoad) {
|
||||
if ($accepted) {
|
||||
$this->make($this->testCases[$filename]);
|
||||
}
|
||||
}
|
||||
@ -137,12 +143,12 @@ final class TestRepository
|
||||
*/
|
||||
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) {
|
||||
[$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) {
|
||||
/** @var string $class */
|
||||
if (class_exists($class)) {
|
||||
|
||||
@ -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()));
|
||||
}
|
||||
}
|
||||
58
src/TestCaseFilters/GitDirtyTestCaseFilter.php
Normal file
58
src/TestCaseFilters/GitDirtyTestCaseFilter.php
Normal 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);
|
||||
}
|
||||
}
|
||||
@ -23,6 +23,11 @@ final class TestSuite
|
||||
*/
|
||||
public ?TestCase $test = null;
|
||||
|
||||
/**
|
||||
* Holds the tests repository.
|
||||
*/
|
||||
public TestRepository $tests;
|
||||
|
||||
/**
|
||||
* Holds the before each repository.
|
||||
*/
|
||||
@ -64,11 +69,10 @@ final class TestSuite
|
||||
public function __construct(
|
||||
string $rootPath,
|
||||
public string $testPath,
|
||||
public TestRepository $tests = new TestRepository(),
|
||||
)
|
||||
{
|
||||
) {
|
||||
$this->beforeAll = new BeforeAllRepository();
|
||||
$this->beforeEach = new BeforeEachRepository();
|
||||
$this->tests = new TestRepository();
|
||||
$this->afterEach = new AfterEachRepository();
|
||||
$this->afterAll = new AfterAllRepository();
|
||||
$this->retryRepository = new RetryRepository('retry');
|
||||
@ -82,11 +86,9 @@ final class TestSuite
|
||||
public static function getInstance(
|
||||
string $rootPath = null,
|
||||
string $testPath = null,
|
||||
TestRepository $tests = null,
|
||||
): TestSuite
|
||||
{
|
||||
): TestSuite {
|
||||
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) {
|
||||
$callable();
|
||||
|
||||
Reference in New Issue
Block a user