feat: adds --retry option

This commit is contained in:
Nuno Maduro
2021-12-04 21:18:55 +00:00
parent 8047ae570d
commit 106b279ed0
17 changed files with 237 additions and 113 deletions

1
.temp/retry.json Normal file
View File

@ -0,0 +1 @@
[]

View File

@ -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": {

View File

@ -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(

View File

@ -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,
]; ];
/** /**

View File

@ -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;
} }
/** /**

View File

@ -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'];

View 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
View 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);
}
}

View File

@ -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()),
); );

View 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);
}
}

View File

@ -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]);

View 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());
}
}

View 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();
}
}

View File

@ -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);
} }

View File

@ -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);
});

View File

@ -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);
});

View 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();
});