mirror of
https://github.com/pestphp/pest.git
synced 2026-03-06 07:47:22 +01:00
feat: adds --retry option
This commit is contained in:
1
.temp/retry.json
Normal file
1
.temp/retry.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
[]
|
||||||
@ -82,7 +82,8 @@
|
|||||||
"Pest\\Plugins\\Coverage",
|
"Pest\\Plugins\\Coverage",
|
||||||
"Pest\\Plugins\\Init",
|
"Pest\\Plugins\\Init",
|
||||||
"Pest\\Plugins\\Version",
|
"Pest\\Plugins\\Version",
|
||||||
"Pest\\Plugins\\Environment"
|
"Pest\\Plugins\\Environment",
|
||||||
|
"Pest\\Plugins\\Retry"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"laravel": {
|
"laravel": {
|
||||||
|
|||||||
@ -85,7 +85,7 @@ final class TestSuiteLoader
|
|||||||
(static function () use ($suiteClassFile) {
|
(static function () use ($suiteClassFile) {
|
||||||
include_once $suiteClassFile;
|
include_once $suiteClassFile;
|
||||||
|
|
||||||
TestSuite::getInstance()->tests->makeIfExists($suiteClassFile);
|
TestSuite::getInstance()->tests->makeIfNeeded($suiteClassFile);
|
||||||
})();
|
})();
|
||||||
|
|
||||||
$loadedClasses = array_values(
|
$loadedClasses = array_values(
|
||||||
|
|||||||
@ -20,6 +20,8 @@ final class BootSubscribers
|
|||||||
private static array $subscribers = [
|
private static array $subscribers = [
|
||||||
Subscribers\EnsureConfigurationIsValid::class,
|
Subscribers\EnsureConfigurationIsValid::class,
|
||||||
Subscribers\EnsureConfigurationDefaults::class,
|
Subscribers\EnsureConfigurationDefaults::class,
|
||||||
|
Subscribers\EnsureRetryRepositoryExists::class,
|
||||||
|
Subscribers\EnsureFailedTestsAreStoredForRetry::class,
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -101,7 +101,7 @@ final class TestCaseFactory
|
|||||||
*
|
*
|
||||||
* @param array<int, TestCaseMethodFactory> $methods
|
* @param array<int, TestCaseMethodFactory> $methods
|
||||||
*/
|
*/
|
||||||
public function evaluate(string $filename, array $methods): string
|
public function evaluate(string $filename, array $methods): void
|
||||||
{
|
{
|
||||||
if ('\\' === DIRECTORY_SEPARATOR) {
|
if ('\\' === DIRECTORY_SEPARATOR) {
|
||||||
// In case Windows, strtolower drive name, like in UsesCall.
|
// In case Windows, strtolower drive name, like in UsesCall.
|
||||||
@ -123,7 +123,7 @@ final class TestCaseFactory
|
|||||||
|
|
||||||
$classFQN = 'P\\' . $relativePath;
|
$classFQN = 'P\\' . $relativePath;
|
||||||
if (class_exists($classFQN)) {
|
if (class_exists($classFQN)) {
|
||||||
return $classFQN;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$hasPrintableTestCaseClassFQN = sprintf('\%s', HasPrintableTestCaseName::class);
|
$hasPrintableTestCaseClassFQN = sprintf('\%s', HasPrintableTestCaseName::class);
|
||||||
@ -142,7 +142,7 @@ final class TestCaseFactory
|
|||||||
}
|
}
|
||||||
|
|
||||||
$methodsCode = implode('', array_map(
|
$methodsCode = implode('', array_map(
|
||||||
fn (TestCaseMethodFactory $methodFactory) => $methodFactory->buildForEvaluation(self::$annotations),
|
fn (TestCaseMethodFactory $methodFactory) => $methodFactory->buildForEvaluation($classFQN, self::$annotations),
|
||||||
$methods
|
$methods
|
||||||
));
|
));
|
||||||
|
|
||||||
@ -164,8 +164,6 @@ final class TestCaseFactory
|
|||||||
} catch (ParseError $caught) {
|
} catch (ParseError $caught) {
|
||||||
throw new RuntimeException(sprintf('Unable to create test case for test file at %s', $filename), 1, $caught);
|
throw new RuntimeException(sprintf('Unable to create test case for test file at %s', $filename), 1, $caught);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $classFQN;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -8,6 +8,7 @@ use Closure;
|
|||||||
use Pest\Datasets;
|
use Pest\Datasets;
|
||||||
use Pest\Exceptions\ShouldNotHappen;
|
use Pest\Exceptions\ShouldNotHappen;
|
||||||
use Pest\Factories\Concerns\HigherOrderable;
|
use Pest\Factories\Concerns\HigherOrderable;
|
||||||
|
use Pest\Plugins\Retry;
|
||||||
use Pest\Support\Str;
|
use Pest\Support\Str;
|
||||||
use Pest\TestSuite;
|
use Pest\TestSuite;
|
||||||
use PHPUnit\Framework\Assert;
|
use PHPUnit\Framework\Assert;
|
||||||
@ -107,7 +108,7 @@ final class TestCaseMethodFactory
|
|||||||
*
|
*
|
||||||
* @param array<int, class-string> $annotationsToUse
|
* @param array<int, class-string> $annotationsToUse
|
||||||
*/
|
*/
|
||||||
public function buildForEvaluation(array $annotationsToUse): string
|
public function buildForEvaluation(string $classFQN, array $annotationsToUse): string
|
||||||
{
|
{
|
||||||
if ($this->description === null) {
|
if ($this->description === null) {
|
||||||
throw ShouldNotHappen::fromMessage('The test description may not be empty.');
|
throw ShouldNotHappen::fromMessage('The test description may not be empty.');
|
||||||
@ -115,6 +116,10 @@ final class TestCaseMethodFactory
|
|||||||
|
|
||||||
$methodName = Str::evaluable($this->description);
|
$methodName = Str::evaluable($this->description);
|
||||||
|
|
||||||
|
if (Retry::$retrying && !TestSuite::getInstance()->retryTempRepository->exists(sprintf('%s::%s', $classFQN, $methodName))) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
$datasetsCode = '';
|
$datasetsCode = '';
|
||||||
$annotations = ['@test'];
|
$annotations = ['@test'];
|
||||||
|
|
||||||
|
|||||||
37
src/Plugins/Concerns/HandleArguments.php
Normal file
37
src/Plugins/Concerns/HandleArguments.php
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pest\Plugins\Concerns;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
trait HandleArguments
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Checks if the given argument exists on the arguments.
|
||||||
|
*
|
||||||
|
* @param array<int, string> $arguments
|
||||||
|
*/
|
||||||
|
public function hasArgument(string $argument, array $arguments): bool
|
||||||
|
{
|
||||||
|
return in_array($argument, $arguments, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pops the given argument from the arguments.
|
||||||
|
*
|
||||||
|
* @param array<int, string> $arguments
|
||||||
|
*
|
||||||
|
* @return array<int, string>
|
||||||
|
*/
|
||||||
|
public function popArgument(string $argument, array $arguments): array
|
||||||
|
{
|
||||||
|
$arguments = array_flip($arguments);
|
||||||
|
|
||||||
|
unset($arguments[$argument]);
|
||||||
|
|
||||||
|
return array_flip($arguments);
|
||||||
|
}
|
||||||
|
}
|
||||||
30
src/Plugins/Retry.php
Normal file
30
src/Plugins/Retry.php
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pest\Plugins;
|
||||||
|
|
||||||
|
use Pest\Contracts\Plugins\HandlesArguments;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
final class Retry implements HandlesArguments
|
||||||
|
{
|
||||||
|
use Concerns\HandleArguments;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether it should show retry or not.
|
||||||
|
*/
|
||||||
|
public static bool $retrying = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
public function handleArguments(array $arguments): array
|
||||||
|
{
|
||||||
|
self::$retrying = $this->hasArgument('--retry', $arguments);
|
||||||
|
|
||||||
|
return $this->popArgument('--retry', $arguments);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -13,6 +13,8 @@ use Symfony\Component\Console\Output\OutputInterface;
|
|||||||
*/
|
*/
|
||||||
final class Version implements HandlesArguments
|
final class Version implements HandlesArguments
|
||||||
{
|
{
|
||||||
|
use Concerns\HandleArguments;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new instance of the plugin.
|
* Creates a new instance of the plugin.
|
||||||
*/
|
*/
|
||||||
@ -24,7 +26,7 @@ final class Version implements HandlesArguments
|
|||||||
|
|
||||||
public function handleArguments(array $arguments): array
|
public function handleArguments(array $arguments): array
|
||||||
{
|
{
|
||||||
if (in_array('--version', $arguments, true)) {
|
if ($this->hasArgument('--version', $arguments)) {
|
||||||
$this->output->writeln(
|
$this->output->writeln(
|
||||||
sprintf('Pest %s', version()),
|
sprintf('Pest %s', version()),
|
||||||
);
|
);
|
||||||
|
|||||||
78
src/Repositories/TempRepository.php
Normal file
78
src/Repositories/TempRepository.php
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pest\Repositories;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
final class TempRepository
|
||||||
|
{
|
||||||
|
private const FOLDER = __DIR__ . '/../../.temp';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new Temp Repository instance.
|
||||||
|
*/
|
||||||
|
public function __construct(private string $filename)
|
||||||
|
{
|
||||||
|
// ..
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a new element.
|
||||||
|
*/
|
||||||
|
public function add(string $element): void
|
||||||
|
{
|
||||||
|
$this->save(array_merge(
|
||||||
|
$this->all(),
|
||||||
|
[$element]
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears the existing file, if any, and re-creates it.
|
||||||
|
*/
|
||||||
|
public function boot(): void
|
||||||
|
{
|
||||||
|
@unlink(self::FOLDER . '/' . $this->filename . '.json'); // @phpstan-ignore-line
|
||||||
|
|
||||||
|
$this->save([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the given element exists.
|
||||||
|
*/
|
||||||
|
public function exists(string $element): bool
|
||||||
|
{
|
||||||
|
return in_array($element, $this->all(), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets all elements.
|
||||||
|
*
|
||||||
|
* @return array<int, string>
|
||||||
|
*/
|
||||||
|
private function all(): array
|
||||||
|
{
|
||||||
|
$contents = file_get_contents(self::FOLDER . '/' . $this->filename . '.json');
|
||||||
|
|
||||||
|
assert(is_string($contents));
|
||||||
|
|
||||||
|
$all = json_decode($contents, true);
|
||||||
|
|
||||||
|
return is_array($all) ? $all : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save the given elements.
|
||||||
|
*
|
||||||
|
* @param array<int, string> $elements
|
||||||
|
*/
|
||||||
|
private function save(array $elements): void
|
||||||
|
{
|
||||||
|
$contents = json_encode($elements);
|
||||||
|
|
||||||
|
file_put_contents(self::FOLDER . '/' . $this->filename . '.json', $contents);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -100,7 +100,7 @@ final class TestRepository
|
|||||||
/**
|
/**
|
||||||
* Makes a Test Case from the given filename, if exists.
|
* Makes a Test Case from the given filename, if exists.
|
||||||
*/
|
*/
|
||||||
public function makeIfExists(string $filename): void
|
public function makeIfNeeded(string $filename): void
|
||||||
{
|
{
|
||||||
if (array_key_exists($filename, $this->testCases)) {
|
if (array_key_exists($filename, $this->testCases)) {
|
||||||
$this->make($this->testCases[$filename]);
|
$this->make($this->testCases[$filename]);
|
||||||
|
|||||||
23
src/Subscribers/EnsureFailedTestsAreStoredForRetry.php
Normal file
23
src/Subscribers/EnsureFailedTestsAreStoredForRetry.php
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pest\Subscribers;
|
||||||
|
|
||||||
|
use Pest\TestSuite;
|
||||||
|
use PHPUnit\Event\Test\Failed;
|
||||||
|
use PHPUnit\Event\Test\FailedSubscriber;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
final class EnsureFailedTestsAreStoredForRetry implements FailedSubscriber
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Runs the subscriber.
|
||||||
|
*/
|
||||||
|
public function notify(Failed $event): void
|
||||||
|
{
|
||||||
|
TestSuite::getInstance()->retryTempRepository->add($event->test()->id());
|
||||||
|
}
|
||||||
|
}
|
||||||
23
src/Subscribers/EnsureRetryRepositoryExists.php
Normal file
23
src/Subscribers/EnsureRetryRepositoryExists.php
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pest\Subscribers;
|
||||||
|
|
||||||
|
use Pest\TestSuite;
|
||||||
|
use PHPUnit\Event\TestRunner\Started;
|
||||||
|
use PHPUnit\Event\TestRunner\StartedSubscriber;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
final class EnsureRetryRepositoryExists implements StartedSubscriber
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Runs the subscriber.
|
||||||
|
*/
|
||||||
|
public function notify(Started $event): void
|
||||||
|
{
|
||||||
|
TestSuite::getInstance()->retryTempRepository->boot();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -9,6 +9,7 @@ use Pest\Repositories\AfterAllRepository;
|
|||||||
use Pest\Repositories\AfterEachRepository;
|
use Pest\Repositories\AfterEachRepository;
|
||||||
use Pest\Repositories\BeforeAllRepository;
|
use Pest\Repositories\BeforeAllRepository;
|
||||||
use Pest\Repositories\BeforeEachRepository;
|
use Pest\Repositories\BeforeEachRepository;
|
||||||
|
use Pest\Repositories\TempRepository;
|
||||||
use Pest\Repositories\TestRepository;
|
use Pest\Repositories\TestRepository;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
@ -47,6 +48,11 @@ final class TestSuite
|
|||||||
*/
|
*/
|
||||||
public AfterAllRepository $afterAll;
|
public AfterAllRepository $afterAll;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Holds the retry temp repository.
|
||||||
|
*/
|
||||||
|
public TempRepository $retryTempRepository;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Holds the root path.
|
* Holds the root path.
|
||||||
*/
|
*/
|
||||||
@ -64,11 +70,12 @@ final class TestSuite
|
|||||||
string $rootPath,
|
string $rootPath,
|
||||||
public string $testPath)
|
public string $testPath)
|
||||||
{
|
{
|
||||||
$this->beforeAll = new BeforeAllRepository();
|
$this->beforeAll = new BeforeAllRepository();
|
||||||
$this->beforeEach = new BeforeEachRepository();
|
$this->beforeEach = new BeforeEachRepository();
|
||||||
$this->tests = new TestRepository();
|
$this->tests = new TestRepository();
|
||||||
$this->afterEach = new AfterEachRepository();
|
$this->afterEach = new AfterEachRepository();
|
||||||
$this->afterAll = new AfterAllRepository();
|
$this->afterAll = new AfterAllRepository();
|
||||||
|
$this->retryTempRepository = new TempRepository('retry');
|
||||||
|
|
||||||
$this->rootPath = (string) realpath($rootPath);
|
$this->rootPath = (string) realpath($rootPath);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,45 +1 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
uses()->afterAll(function () {
|
|
||||||
expect($_SERVER['globalHook'])
|
|
||||||
->toHaveProperty('afterAll')
|
|
||||||
->and($_SERVER['globalHook']->afterAll)
|
|
||||||
->toBe(0)
|
|
||||||
->and($_SERVER['globalHook']->calls)
|
|
||||||
->afterAll
|
|
||||||
->toBe(1);
|
|
||||||
|
|
||||||
$_SERVER['globalHook']->afterAll = 1;
|
|
||||||
$_SERVER['globalHook']->calls->afterAll++;
|
|
||||||
});
|
|
||||||
|
|
||||||
afterAll(function () {
|
|
||||||
expect($_SERVER['globalHook'])
|
|
||||||
->toHaveProperty('afterAll')
|
|
||||||
->and($_SERVER['globalHook']->afterAll)
|
|
||||||
->toBe(1)
|
|
||||||
->and($_SERVER['globalHook']->calls)
|
|
||||||
->afterAll
|
|
||||||
->toBe(2);
|
|
||||||
|
|
||||||
$_SERVER['globalHook']->afterAll = 2;
|
|
||||||
$_SERVER['globalHook']->calls->afterAll++;
|
|
||||||
});
|
|
||||||
|
|
||||||
test('global afterAll execution order', function () {
|
|
||||||
expect($_SERVER['globalHook'])
|
|
||||||
->not()
|
|
||||||
->toHaveProperty('afterAll')
|
|
||||||
->and($_SERVER['globalHook']->calls)
|
|
||||||
->afterAll
|
|
||||||
->toBe(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('only gets called once per file', function () {
|
|
||||||
expect($_SERVER['globalHook'])
|
|
||||||
->not()
|
|
||||||
->toHaveProperty('afterAll')
|
|
||||||
->and($_SERVER['globalHook']->calls)
|
|
||||||
->afterAll
|
|
||||||
->toBe(0);
|
|
||||||
});
|
|
||||||
|
|||||||
@ -1,54 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
use Pest\Support\Str;
|
|
||||||
|
|
||||||
// HACK: we have to determine our $_SERVER['globalHook-]>calls baseline. This is because
|
|
||||||
// two other tests are executed before this one due to filename ordering.
|
|
||||||
$args = $_SERVER['argv'] ?? [];
|
|
||||||
$single = (isset($args[1]) && Str::endsWith(__FILE__, $args[1])) || ($_SERVER['PEST_PARALLEL'] ?? false);
|
|
||||||
$offset = $single ? 0 : 2;
|
|
||||||
|
|
||||||
uses()->beforeAll(function () use ($offset) {
|
|
||||||
expect($_SERVER['globalHook'])
|
|
||||||
->toHaveProperty('beforeAll')
|
|
||||||
->and($_SERVER['globalHook']->beforeAll)
|
|
||||||
->toBe(0)
|
|
||||||
->and($_SERVER['globalHook']->calls)
|
|
||||||
->beforeAll
|
|
||||||
->toBe(1 + $offset);
|
|
||||||
|
|
||||||
$_SERVER['globalHook']->beforeAll = 1;
|
|
||||||
$_SERVER['globalHook']->calls->beforeAll++;
|
|
||||||
});
|
|
||||||
|
|
||||||
beforeAll(function () use ($offset) {
|
|
||||||
expect($_SERVER['globalHook'])
|
|
||||||
->toHaveProperty('beforeAll')
|
|
||||||
->and($_SERVER['globalHook']->beforeAll)
|
|
||||||
->toBe(1)
|
|
||||||
->and($_SERVER['globalHook']->calls)
|
|
||||||
->beforeAll
|
|
||||||
->toBe(2 + $offset);
|
|
||||||
|
|
||||||
$_SERVER['globalHook']->beforeAll = 2;
|
|
||||||
$_SERVER['globalHook']->calls->beforeAll++;
|
|
||||||
});
|
|
||||||
|
|
||||||
test('global beforeAll execution order', function () use ($offset) {
|
|
||||||
expect($_SERVER['globalHook'])
|
|
||||||
->toHaveProperty('beforeAll')
|
|
||||||
->and($_SERVER['globalHook']->beforeAll)
|
|
||||||
->toBe(2)
|
|
||||||
->and($_SERVER['globalHook']->calls)
|
|
||||||
->beforeAll
|
|
||||||
->toBe(3 + $offset);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('only gets called once per file', function () use ($offset) {
|
|
||||||
expect($_SERVER['globalHook'])
|
|
||||||
->beforeAll
|
|
||||||
->toBe(2)
|
|
||||||
->and($_SERVER['globalHook']->calls)
|
|
||||||
->beforeAll
|
|
||||||
->toBe(3 + $offset);
|
|
||||||
});
|
|
||||||
|
|||||||
15
tests/Unit/Plugins/Retry.php
Normal file
15
tests/Unit/Plugins/Retry.php
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Pest\Plugins\Retry;
|
||||||
|
|
||||||
|
beforeEach(fn () => Retry::$retrying = false);
|
||||||
|
|
||||||
|
afterEach(fn () => Retry::$retrying = false);
|
||||||
|
|
||||||
|
it('retries if --retry argument is used', function () {
|
||||||
|
$retry = new Retry();
|
||||||
|
|
||||||
|
$retry->handleArguments(['--retry']);
|
||||||
|
|
||||||
|
expect(Retry::$retrying)->toBeTrue();
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user