Merge pull request #369 from pestphp/parallel

Parallel testing support
This commit is contained in:
Nuno Maduro
2021-08-13 16:00:26 +01:00
committed by GitHub
14 changed files with 138 additions and 44 deletions

View File

@ -10,8 +10,9 @@ jobs:
os: [ubuntu-latest, macos-latest, windows-latest]
php: ['7.3', '7.4', '8.0']
dependency-version: [prefer-lowest, prefer-stable]
parallel: ['', '--parallel']
name: PHP ${{ matrix.php }} - ${{ matrix.os }} - ${{ matrix.dependency-version }}
name: PHP ${{ matrix.php }} - ${{ matrix.os }} - ${{ matrix.dependency-version }} - ${{ matrix.parallel }}
steps:
- name: Checkout
@ -38,7 +39,7 @@ jobs:
if: "matrix.php >= 8"
- name: Unit Tests
run: php bin/pest --colors=always --exclude-group=integration
run: php bin/pest --colors=always --exclude-group=integration ${{ matrix.parallel }}
- name: Integration Tests
run: php bin/pest --colors=always --group=integration

View File

@ -3,9 +3,9 @@
use NunoMaduro\Collision\Provider;
use Pest\Actions\ValidatesEnvironment;
use Pest\Console\Command;
use Pest\Support\Container;
use Pest\TestSuite;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\ArgvInput;
use Symfony\Component\Console\Output\ConsoleOutput;
use Symfony\Component\Console\Output\OutputInterface;
@ -42,14 +42,22 @@ use Symfony\Component\Console\Output\OutputInterface;
ValidatesEnvironment::in($testSuite);
$args = $_SERVER['argv'];
// Let's remove any arguments that PHPUnit does not understand
if ($argv->hasParameterOption('--test-directory')) {
foreach ($_SERVER['argv'] as $key => $value) {
foreach ($args as $key => $value) {
if (strpos($value, '--test-directory') !== false) {
unset($_SERVER['argv'][$key]);
unset($args[$key]);
}
}
}
exit($container->get(Command::class)->run($_SERVER['argv']));
if (($runInParallel = $argv->hasParameterOption(['--parallel', '-P'])) && !class_exists(\Pest\Parallel\Command::class)) {
$output->writeln("Parallel support requires the Pest Parallel plugin. Run <fg=yellow;options=bold>`composer require --dev pestphp/pest-plugin-parallel`</> first.");
exit(Command::FAILURE);
}
$command = $runInParallel ? \Pest\Parallel\Command::class : \Pest\Console\Command::class;
exit($container->get($command)->run($args));
})();

View File

@ -43,7 +43,8 @@
"illuminate/console": "^8.47.0",
"illuminate/support": "^8.47.0",
"laravel/dusk": "^6.15.0",
"pestphp/pest-dev-tools": "dev-master"
"pestphp/pest-dev-tools": "dev-master",
"pestphp/pest-plugin-parallel": "^1.0"
},
"minimum-stability": "dev",
"prefer-stable": true,
@ -59,12 +60,14 @@
"test:lint": "php-cs-fixer fix -v --dry-run",
"test:types": "phpstan analyse --ansi --memory-limit=-1",
"test:unit": "php bin/pest --colors=always --exclude-group=integration",
"test:parallel": "php bin/pest -P --colors=always --exclude-group=integration",
"test:integration": "php bin/pest --colors=always --group=integration",
"update:snapshots": "REBUILD_SNAPSHOTS=true php bin/pest --colors=always",
"test": [
"@test:lint",
"@test:types",
"@test:unit",
"@test:parallel",
"@test:integration"
]
},

View File

@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
namespace Pest\Actions;
use Pest\Contracts\Plugins\AddsOutput;
use Pest\Contracts\Plugins\HandlesArguments;
use Pest\Plugin\Loader;
/**
* @internal
*/
final class InteractsWithPlugins
{
/**
* Transform the input arguments by passing it to the relevant plugins.
*
* @param array<int, string> $argv
*
* @return array<int, string>
*/
public static function handleArguments(array $argv): array
{
$plugins = Loader::getPlugins(HandlesArguments::class);
/** @var HandlesArguments $plugin */
foreach ($plugins as $plugin) {
$argv = $plugin->handleArguments($argv);
}
return $argv;
}
/**
* Provides an opportunity for any plugins that want
* to provide additional output after test execution.
*/
public static function addOutput(int $result): int
{
$plugins = Loader::getPlugins(AddsOutput::class);
/** @var AddsOutput $plugin */
foreach ($plugins as $plugin) {
$result = $plugin->addOutput($result);
}
return $result;
}
}

View File

@ -6,11 +6,9 @@ namespace Pest\Console;
use Pest\Actions\AddsDefaults;
use Pest\Actions\AddsTests;
use Pest\Actions\InteractsWithPlugins;
use Pest\Actions\LoadStructure;
use Pest\Actions\ValidatesConfiguration;
use Pest\Contracts\Plugins\AddsOutput;
use Pest\Contracts\Plugins\HandlesArguments;
use Pest\Plugin\Loader;
use Pest\Plugins\Version;
use Pest\Support\Container;
use Pest\TestSuite;
@ -57,23 +55,12 @@ final class Command extends BaseCommand
*/
protected function handleArguments(array $argv): void
{
/*
* First, let's call all plugins that want to handle arguments
*/
$plugins = Loader::getPlugins(HandlesArguments::class);
$argv = InteractsWithPlugins::handleArguments($argv);
/** @var HandlesArguments $plugin */
foreach ($plugins as $plugin) {
$argv = $plugin->handleArguments($argv);
}
/*
* Next, as usual, let's send the console arguments to PHPUnit.
*/
parent::handleArguments($argv);
/*
* Finally, let's validate the configuration. Making
* Let's validate the configuration. Making
* sure all options are yet supported by Pest.
*/
ValidatesConfiguration::in($this->arguments);
@ -128,16 +115,7 @@ final class Command extends BaseCommand
LoadStructure::in($this->testSuite->rootPath);
$result = parent::run($argv, false);
/*
* Let's call all plugins that want to add output after test execution
*/
$plugins = Loader::getPlugins(AddsOutput::class);
/** @var AddsOutput $plugin */
foreach ($plugins as $plugin) {
$result = $plugin->addOutput($result);
}
$result = InteractsWithPlugins::addOutput($result);
exit($result);
}

View File

@ -286,7 +286,7 @@ final class TeamCity extends DefaultResultPrinter
{
$this->markAsFailure($t);
$this->writeWarning($test->getName());
$this->phpunitTeamCity->addSkippedTest($test, $t, $time);
$this->phpunitTeamCity->printIgnoredTest($test->getName(), $t, $time);
}
private function markAsFailure(Throwable $t): void

View File

@ -44,6 +44,20 @@ final class TestRepository
return count($this->state);
}
/**
* Returns the filename of each test that should be executed in the suite.
*
* @return array<int, string>
*/
public function getFilenames(): array
{
$testsWithOnly = $this->testsUsingOnly();
return array_values(array_map(function (TestCaseFactory $factory): string {
return $factory->filename;
}, count($testsWithOnly) > 0 ? $testsWithOnly : $this->state));
}
/**
* Calls the given callable foreach test case.
*/
@ -85,9 +99,7 @@ final class TestRepository
}
}
$onlyState = array_filter($this->state, function ($testFactory): bool {
return $testFactory->only;
});
$onlyState = $this->testsUsingOnly();
$state = count($onlyState) > 0 ? $onlyState : $this->state;
@ -100,6 +112,18 @@ final class TestRepository
}
}
/**
* Return all tests that have called the only method.
*
* @return array<TestCaseFactory>
*/
private function testsUsingOnly(): array
{
return array_filter($this->state, function ($testFactory): bool {
return $testFactory->only;
});
}
/**
* Uses the given `$testCaseClass` on the given `$paths`.
*

View File

@ -614,6 +614,8 @@
PASS Tests\Unit\TestSuite
✓ it does not allow to add the same test description twice
✓ it alerts users about tests with arguments but no input
✓ it can return an array of all test suite filenames
✓ it can filter the test suite filenames to those with the only method
PASS Tests\Visual\Help
✓ visual snapshot of help command output
@ -647,5 +649,5 @@
✓ it is a test
✓ it uses correct parent class
Tests: 4 incompleted, 9 skipped, 419 passed
Tests: 4 incompleted, 9 skipped, 421 passed

View File

@ -7,7 +7,7 @@ global $globalHook;
// HACK: we have to determine our $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]);
$single = (isset($args[1]) && Str::endsWith(__FILE__, $args[1])) || ($_SERVER['PEST_PARALLEL'] ?? false);
$offset = $single ? 0 : 2;
uses()->beforeAll(function () use ($globalHook, $offset) {

View File

@ -2,7 +2,7 @@
declare(strict_types=1);
namespace Tests\SubFolder\SubFolder\SubFolder;
namespace Tests\CustomTestCaseInSubFolders\SubFolder\SubFolder;
use PHPUnit\Framework\TestCase;

View File

@ -1,3 +0,0 @@
<?php
uses(Tests\SubFolder\SubFolder\SubFolder\CustomTestCaseInSubFolder::class)->in('CustomTestCaseInSubFolders/SubFolder');

View File

@ -1,5 +1,9 @@
<?php
use Tests\CustomTestCaseInSubFolders\SubFolder\SubFolder\CustomTestCaseInSubFolder;
uses(CustomTestCaseInSubFolder::class)->in('PHPUnit/CustomTestCaseInSubFolders/SubFolder/SubFolder');
uses()->group('integration')->in('Visual');
// NOTE: global test value container to be mutated and checked across files, as needed

View File

@ -22,3 +22,30 @@ it('alerts users about tests with arguments but no input', function () {
DatasetMissing::class,
sprintf("A test with the description '%s' has %d argument(s) ([%s]) and no dataset(s) provided in %s", 'foo', 1, 'int $arg', __FILE__),
);
it('can return an array of all test suite filenames', function () {
$testSuite = new TestSuite(getcwd(), 'tests');
$test = function () {};
$testSuite->tests->set(new \Pest\Factories\TestCaseFactory(__FILE__, 'foo', $test));
$testSuite->tests->set(new \Pest\Factories\TestCaseFactory(__FILE__, 'bar', $test));
expect($testSuite->tests->getFilenames())->toEqual([
__FILE__,
__FILE__,
]);
});
it('can filter the test suite filenames to those with the only method', function () {
$testSuite = new TestSuite(getcwd(), 'tests');
$test = function () {};
$testWithOnly = new \Pest\Factories\TestCaseFactory(__FILE__, 'foo', $test);
$testWithOnly->only = true;
$testSuite->tests->set($testWithOnly);
$testSuite->tests->set(new \Pest\Factories\TestCaseFactory('Baz/Bar/Boo.php', 'bar', $test));
expect($testSuite->tests->getFilenames())->toEqual([
__FILE__,
]);
});

View File

@ -9,7 +9,7 @@ test('visual snapshot of test suite on success', function () {
]);
$output = function () use ($testsPath) {
$process = (new Symfony\Component\Process\Process(['php', 'bin/pest'], dirname($testsPath), ['EXCLUDE' => 'integration', 'REBUILD_SNAPSHOTS' => false]));
$process = (new Symfony\Component\Process\Process(['php', 'bin/pest'], dirname($testsPath), ['EXCLUDE' => 'integration', 'REBUILD_SNAPSHOTS' => false, 'PARATEST' => 0]));
$process->run();