chore: different refactors

This commit is contained in:
Nuno Maduro
2023-02-11 16:07:30 +00:00
parent e1406554fc
commit 8eaf4859ff
19 changed files with 207 additions and 165 deletions

View File

@ -11,7 +11,7 @@ use Pest\Plugins\Actions\CallsBoot;
use Pest\Plugins\Actions\CallsShutdown;
use Pest\Support\Container;
use PHPUnit\TextUI\Application;
use PHPUnit\TextUI\Exception;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
/**
@ -36,7 +36,8 @@ final class Kernel
* Creates a new Kernel instance.
*/
public function __construct(
private readonly Application $application
private readonly Application $application,
private readonly OutputInterface $output,
) {
register_shutdown_function(function (): void {
if (error_get_last() !== null) {
@ -50,8 +51,16 @@ final class Kernel
/**
* Boots the Kernel.
*/
public static function boot(): self
public static function boot(TestSuite $testSuite, InputInterface $input, OutputInterface $output): self
{
$container = Container::getInstance();
$container
->add(TestSuite::class, $testSuite)
->add(InputInterface::class, $input)
->add(OutputInterface::class, $output)
->add(Container::class, $container);
foreach (self::BOOTSTRAPPERS as $bootstrapper) {
$bootstrapper = Container::getInstance()->get($bootstrapper);
assert($bootstrapper instanceof Bootstrapper);
@ -61,24 +70,25 @@ final class Kernel
(new CallsBoot())->__invoke();
return new self(new Application());
return new self(
new Application(),
$output,
);
}
/**
* Handles the given argv.
* Runs the application, and returns the exit code.
*
* @param array<int, string> $argv
*
* @throws Exception
* @param array<int, string> $args
*/
public function handle(OutputInterface $output, array $argv): int
public function handle(array $args): int
{
$argv = (new Plugins\Actions\CallsHandleArguments())->__invoke($argv);
$args = (new Plugins\Actions\CallsHandleArguments())->__invoke($args);
try {
$this->application->run($argv);
$this->application->run($args);
} catch (NoTestsFound) {
$output->writeln([
$this->output->writeln([
'',
' <fg=white;options=bold;bg=blue> INFO </> No tests found.',
'',

View File

@ -4,11 +4,12 @@ declare(strict_types=1);
namespace Pest\Plugins;
use JsonException;
use ParaTest\ParaTestCommand;
use Pest\Contracts\Plugins\HandlesArguments;
use Pest\Plugins\Actions\CallsAddsOutput;
use Pest\Plugins\Concerns\HandleArguments;
use Pest\Plugins\Parallel\Contracts\HandlesSubprocessArguments;
use Pest\Plugins\Parallel\Contracts\HandlersWorkerArguments;
use Pest\Plugins\Parallel\Paratest\CleanConsoleOutput;
use Pest\Support\Arr;
use Pest\Support\Container;
@ -31,7 +32,11 @@ final class Parallel implements HandlesArguments
public static function isInParallelProcess(): bool
{
return (int) Arr::get($_SERVER, 'PARATEST') === 1;
$argvValue = Arr::get($_SERVER, 'PARATEST');
assert(is_string($argvValue) || is_int($argvValue) || is_null($argvValue));
return ((int) $argvValue) === 1;
}
public function handleArguments(array $arguments): array
@ -41,12 +46,15 @@ final class Parallel implements HandlesArguments
}
if (self::isInParallelProcess()) {
return $this->runSubprocessHandlers($arguments);
return $this->runWorkersHandlers($arguments);
}
return $arguments;
}
/**
* @param array<int, string> $arguments
*/
private function argumentsContainParallelFlags(array $arguments): bool
{
if ($this->hasArgument('--parallel', $arguments)) {
@ -56,6 +64,11 @@ final class Parallel implements HandlesArguments
return $this->hasArgument('-p', $arguments);
}
/**
* @param array<int, string> $arguments
*
* @throws JsonException
*/
private function runTestSuiteInParallel(array $arguments): int
{
if (! class_exists(ParaTestCommand::class)) {
@ -64,16 +77,16 @@ final class Parallel implements HandlesArguments
return Command::FAILURE;
}
$_ENV['PEST_PARALLEL_ARGV'] = json_encode($_SERVER['argv']);
$_ENV['PEST_PARALLEL_ARGV'] = json_encode($_SERVER['argv'], JSON_THROW_ON_ERROR);
$handlers = array_filter(
array_map(fn ($handler) => Container::getInstance()->get($handler), self::HANDLERS),
fn ($handler) => $handler instanceof HandlesArguments,
array_map(fn ($handler): object|string => Container::getInstance()->get($handler), self::HANDLERS),
fn ($handler): bool => $handler instanceof HandlesArguments,
);
$filteredArguments = array_reduce(
$handlers,
fn ($arguments, HandlesArguments $handler) => $handler->handleArguments($arguments),
fn ($arguments, HandlesArguments $handler): array => $handler->handleArguments($arguments),
$arguments
);
@ -82,23 +95,30 @@ final class Parallel implements HandlesArguments
return (new CallsAddsOutput())($exitCode);
}
private function runSubprocessHandlers(array $arguments): array
/**
* @param array<int, string> $arguments
* @return array<int, string>
*/
private function runWorkersHandlers(array $arguments): array
{
$handlers = array_filter(
array_map(fn ($handler) => Container::getInstance()->get($handler), self::HANDLERS),
fn ($handler) => $handler instanceof HandlesSubprocessArguments,
array_map(fn ($handler): object|string => Container::getInstance()->get($handler), self::HANDLERS),
fn ($handler): bool => $handler instanceof HandlersWorkerArguments,
);
return array_reduce(
$handlers,
fn ($arguments, HandlesSubprocessArguments $handler) => $handler->handleSubprocessArguments($arguments),
fn ($arguments, HandlersWorkerArguments $handler): array => $handler->handleWorkerArguments($arguments),
$arguments
);
}
private function askUserToInstallParatest(): void
{
Container::getInstance()->get(OutputInterface::class)->writeln([
/** @var OutputInterface $output */
$output = Container::getInstance()->get(OutputInterface::class);
$output->writeln([
'<fg=red>Pest Parallel requires ParaTest to run.</>',
'Please run <fg=yellow>composer require --dev brianium/paratest</>.',
]);
@ -106,7 +126,10 @@ final class Parallel implements HandlesArguments
private function paratestCommand(): Application
{
$command = ParaTestCommand::applicationFactory(TestSuite::getInstance()->rootPath);
/** @var non-empty-string $rootPath */
$rootPath = TestSuite::getInstance()->rootPath;
$command = ParaTestCommand::applicationFactory($rootPath);
$command->setAutoExit(false);
$command->setName('Pest');
$command->setVersion(version());

View File

@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace Pest\Plugins\Parallel\Contracts;
interface HandlersWorkerArguments
{
/**
* @param array<int, string> $arguments
* @return array<int, string>
*/
public function handleWorkerArguments(array $arguments): array;
}

View File

@ -1,10 +0,0 @@
<?php
declare(strict_types=1);
namespace Pest\Plugins\Parallel\Contracts;
interface HandlesSubprocessArguments
{
public function handleSubprocessArguments(array $arguments): array;
}

View File

@ -11,8 +11,6 @@ use ParaTest\RunnerInterface;
use Pest\Contracts\Plugins\HandlesArguments;
use Pest\Plugins\Concerns\HandleArguments;
use Pest\Plugins\Parallel\Paratest\WrapperRunner;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
/**
@ -22,13 +20,6 @@ final class Laravel implements HandlesArguments
{
use HandleArguments;
public function __construct(
private readonly OutputInterface $output,
private readonly InputInterface $input,
)
{
}
public function handleArguments(array $arguments): array
{
if (! self::isALaravelApplication()) {
@ -44,22 +35,22 @@ final class Laravel implements HandlesArguments
private function setLaravelParallelRunner(): void
{
if (! method_exists(ParallelRunner::class, 'resolveRunnerUsing')) {
$this->output->writeln(' <fg=red>Using parallel with Pest requires Laravel v8.55.0 or higher.</>');
exit(Command::FAILURE);
}
ParallelRunner::resolveRunnerUsing(fn (Options $options, OutputInterface $output): RunnerInterface => new WrapperRunner($options, $output));
ParallelRunner::resolveRunnerUsing( // @phpstan-ignore-line
fn (Options $options, OutputInterface $output): RunnerInterface => new WrapperRunner($options, $output)
);
}
private static function isALaravelApplication(): bool
{
return InstalledVersions::isInstalled('laravel/framework', false)
&& ! class_exists(\Orchestra\Testbench\TestCase::class);
if (! InstalledVersions::isInstalled('laravel/framework', false)) {
return false;
}
return ! class_exists(\Orchestra\Testbench\TestCase::class);
}
/**
* @param array<int, string> $arguments
* @param array<int, string> $arguments
* @return array<int, string>
*/
private function setEnvironmentVariables(array $arguments): array
@ -75,17 +66,18 @@ final class Laravel implements HandlesArguments
}
$arguments = $this->popArgument('--recreate-databases', $arguments);
return $this->popArgument('--drop-databases', $arguments);
}
/**
* @param array<int, string> $arguments
* @param array<int, string> $arguments
* @return array<int, string>
*/
private function useLaravelRunner(array $arguments): array
{
foreach ($arguments as $value) {
if (str_starts_with((string)$value, '--runner')) {
if (str_starts_with($value, '--runner')) {
$arguments = $this->popArgument($value, $arguments);
}
}

View File

@ -1,13 +1,15 @@
<?php
declare(strict_types=1);
namespace Pest\Plugins\Parallel\Handlers;
use Pest\Contracts\Plugins\HandlesArguments;
use Pest\Plugins\Concerns\HandleArguments;
use Pest\Plugins\Parallel\Contracts\HandlesSubprocessArguments;
use Pest\Plugins\Parallel\Contracts\HandlersWorkerArguments;
use Pest\Plugins\Retry;
final class Pest implements HandlesArguments, HandlesSubprocessArguments
final class Pest implements HandlesArguments, HandlersWorkerArguments
{
use HandleArguments;
@ -20,7 +22,7 @@ final class Pest implements HandlesArguments, HandlesSubprocessArguments
return $arguments;
}
public function handleSubprocessArguments(array $arguments): array
public function handleWorkerArguments(array $arguments): array
{
$_SERVER['PEST_PARALLEL'] = '1';

View File

@ -1,13 +1,15 @@
<?php
declare(strict_types=1);
namespace Pest\Plugins\Parallel\Paratest;
use Symfony\Component\Console\Output\ConsoleOutput;
class CleanConsoleOutput extends ConsoleOutput
final class CleanConsoleOutput extends ConsoleOutput
{
/**
* @inheritdoc
* {@inheritdoc}
*/
protected function doWrite(string $message, bool $newline): void
{

View File

@ -37,7 +37,9 @@ use function unlink;
use function unserialize;
use function usleep;
/** @internal */
/**
* @internal
*/
final class WrapperRunner implements RunnerInterface
{
private const CYCLE_SLEEP = 10000;
@ -46,36 +48,36 @@ final class WrapperRunner implements RunnerInterface
private readonly Timer $timer;
/** @var non-empty-string[] */
/** @var array<int, string> */
private array $pending = [];
private int $exitcode = -1;
private int $exitCode = -1;
/** @var array<positive-int,WrapperWorker> */
/** @var array<int,WrapperWorker> */
private array $workers = [];
/** @var array<int,int> */
private array $batches = [];
/** @var list<SplFileInfo> */
/** @var array<int, SplFileInfo> */
private array $testresultFiles = [];
/** @var list<SplFileInfo> */
/** @var array<int, SplFileInfo> */
private array $coverageFiles = [];
/** @var list<SplFileInfo> */
/** @var array<int, SplFileInfo> */
private array $junitFiles = [];
/** @var list<SplFileInfo> */
/** @var array<int, SplFileInfo> */
private array $teamcityFiles = [];
/** @var list<SplFileInfo> */
/** @var array<int, SplFileInfo> */
private array $testdoxFiles = [];
/** @var non-empty-string[] */
/** @var array<int, string> */
private readonly array $parameters;
private CodeCoverageFilterRegistry $codeCoverageFilterRegistry;
private readonly CodeCoverageFilterRegistry $codeCoverageFilterRegistry;
public function __construct(
private readonly Options $options,
@ -84,11 +86,11 @@ final class WrapperRunner implements RunnerInterface
$this->printer = new ResultPrinter($output, $options);
$this->timer = new Timer();
$wrapper = realpath(
dirname(__DIR__, 4).DIRECTORY_SEPARATOR.'bin'.DIRECTORY_SEPARATOR.'pest-wrapper.php',
$worker = realpath(
dirname(__DIR__, 4).DIRECTORY_SEPARATOR.'bin'.DIRECTORY_SEPARATOR.'worker.php',
);
assert($wrapper !== false);
assert($worker !== false);
$phpFinder = new PhpExecutableFinder();
$phpBin = $phpFinder->find(false);
assert($phpBin !== false);
@ -99,7 +101,7 @@ final class WrapperRunner implements RunnerInterface
$parameters = array_merge($parameters, $options->passthruPhp);
}
$parameters[] = $wrapper;
$parameters[] = $worker;
$this->parameters = $parameters;
$this->codeCoverageFilterRegistry = new CodeCoverageFilterRegistry();
@ -113,6 +115,7 @@ final class WrapperRunner implements RunnerInterface
TestResultFacade::init();
EventFacade::seal();
$suiteLoader = new SuiteLoader($this->options, $this->output, $this->codeCoverageFilterRegistry);
$this->pending = $this->getTestFiles($suiteLoader);
@ -159,7 +162,7 @@ final class WrapperRunner implements RunnerInterface
}
if (
$this->exitcode > 0
$this->exitCode > 0
&& $this->options->configuration->stopOnFailure()
) {
$this->pending = [];
@ -177,7 +180,7 @@ final class WrapperRunner implements RunnerInterface
private function flushWorker(WrapperWorker $worker): void
{
$this->exitcode = max($this->exitcode, $worker->getExitCode());
$this->exitCode = max($this->exitCode, $worker->getExitCode());
$this->printer->printFeedback(
$worker->progressFile,
$this->teamcityFiles,
@ -191,7 +194,7 @@ final class WrapperRunner implements RunnerInterface
while ($this->workers !== []) {
foreach ($this->workers as $index => $worker) {
if ($worker->isRunning()) {
if (! isset($stopped[$index]) && $worker->isFree()) {
if (! array_key_exists($index, $stopped) && $worker->isFree()) {
$worker->stop();
$stopped[$index] = true;
}
@ -213,16 +216,22 @@ final class WrapperRunner implements RunnerInterface
private function startWorker(int $token): WrapperWorker
{
/** @var array<non-empty-string> $parameters */
$parameters = $this->parameters;
$worker = new WrapperWorker(
$this->output,
$this->options,
$this->parameters,
$parameters,
$token,
);
$worker->start();
$this->batches[$token] = 0;
$this->testresultFiles[] = $worker->testresultFile;
if (isset($worker->junitFile)) {
$this->junitFiles[] = $worker->junitFile;
}
@ -349,7 +358,7 @@ final class WrapperRunner implements RunnerInterface
);
}
/** @param list<SplFileInfo> $files */
/** @param array<int, SplFileInfo> $files */
private function clearFiles(array $files): void
{
foreach ($files as $file) {
@ -362,23 +371,29 @@ final class WrapperRunner implements RunnerInterface
}
/**
* We are doing this because the SuiteLoader returns filenames incorrectly
* for Pest tests. Ideally we should find a cleaner solution.
* Returns the test files to be executed.
*
* @return array<int, string>
*/
private function getTestFiles(SuiteLoader $suiteLoader): array
{
$this->debug(sprintf('Found %d test file%s', count($suiteLoader->files), count($suiteLoader->files) === 1 ? '' : 's'));
$phpunitTests = array_filter(
$suiteLoader->files,
fn (string $filename): bool => ! str_ends_with($filename, "eval()'d code")
);
/** @var array<string, string> $files */
$files = $suiteLoader->files;
$pestTests = TestSuite::getInstance()->tests->getFilenames();
return [...$phpunitTests, ...$pestTests];
return [
...array_values(array_filter(
$files,
fn (string $filename): bool => ! str_ends_with($filename, "eval()'d code")
)),
...TestSuite::getInstance()->tests->getFilenames(),
];
}
/**
* Prints a debug message.
*/
private function debug(string $message): void
{
if ($this->options->verbose) {

View File

@ -54,22 +54,22 @@ final class TestRepository
*/
public function getFilenames(): array
{
$testCases = array_filter($this->testCases, static fn(TestCaseFactory $testCase): bool => $testCase->methodsUsingOnly() !== []);
$testCases = array_filter($this->testCases, static fn (TestCaseFactory $testCase): bool => $testCase->methodsUsingOnly() !== []);
if ($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));
}
/**
* 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
{
@ -126,18 +126,18 @@ final class TestRepository
public function set(TestCaseMethodFactory $method): void
{
foreach ($this->testCaseFilters as $filter) {
if (!$filter->accept($method->filename)) {
if (! $filter->accept($method->filename)) {
return;
}
}
foreach ($this->testCaseMethodFilters as $filter) {
if (!$filter->accept($method)) {
if (! $filter->accept($method)) {
return;
}
}
if (!array_key_exists($method->filename, $this->testCases)) {
if (! array_key_exists($method->filename, $this->testCases)) {
$this->testCases[$method->filename] = new TestCaseFactory($method->filename);
}
@ -149,12 +149,12 @@ final class TestRepository
*/
public function makeIfNeeded(string $filename): void
{
if (!array_key_exists($filename, $this->testCases)) {
if (! array_key_exists($filename, $this->testCases)) {
return;
}
foreach ($this->testCaseFilters as $filter) {
if (!$filter->accept($filename)) {
if (! $filter->accept($filename)) {
return;
}
}
@ -167,12 +167,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)) {

View File

@ -21,8 +21,8 @@ final class EnsureTeamCityEnabled implements ConfiguredSubscriber
* Creates a new Configured Subscriber instance.
*/
public function __construct(
private readonly OutputInterface $output,
private readonly InputInterface $input,
private readonly OutputInterface $output,
private readonly TestSuite $testSuite,
) {
}

View File

@ -47,10 +47,14 @@ final class Container
/**
* Adds the given instance to the container.
*
* @return $this
*/
public function add(string $id, object|string $instance): void
public function add(string $id, object|string $instance): self
{
$this->instances[$id] = $instance;
return $this;
}
/**

View File

@ -23,12 +23,15 @@ final class StateGenerator
$state = new State();
foreach ($testResult->testErroredEvents() as $testResultEvent) {
assert($testResultEvent instanceof Errored);
$state->add(\NunoMaduro\Collision\Adapters\Phpunit\TestResult::fromTestCase(
$testResultEvent->test(),
TestResult::FAIL,
$testResultEvent->throwable()
));
if ($testResultEvent instanceof Errored) {
$state->add(TestResult::fromTestCase(
$testResultEvent->test(),
TestResult::FAIL,
$testResultEvent->throwable()
));
} else {
$state->add(TestResult::fromBeforeFirstTestMethodErrored($testResultEvent));
}
}
foreach ($testResult->testFailedEvents() as $testResultEvent) {