Skip to content

Commit

Permalink
Merge pull request #168 from nunomaduro/feat/parallel-testing
Browse files Browse the repository at this point in the history
feat: adds parallel testing
  • Loading branch information
nunomaduro authored Jan 13, 2021
2 parents 163d5c2 + 2193d13 commit c143c38
Show file tree
Hide file tree
Showing 15 changed files with 221 additions and 80 deletions.
3 changes: 3 additions & 0 deletions .php_cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ $rules = [
'array_syntax' => ['syntax' => 'short'],
'yoda_style' => false,
'declare_strict_types' => true,
'braces' => false,
'phpdoc_indent' => false,
'protected_to_private' => false,
'binary_operator_spaces' => [
'operators' => [
'=>' => 'align',
Expand Down
18 changes: 9 additions & 9 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,16 @@
"symfony/console": "^5.0"
},
"require-dev": {
"nunomaduro/mock-final-classes": "^1.0",
"friendsofphp/php-cs-fixer": "^2.16.4",
"fideloper/proxy": "^4.4.0",
"fruitcake/laravel-cors": "^2.0.1",
"laravel/framework": "^8.0",
"laravel/tinker": "^2.4.1",
"brianium/paratest": "^6.1",
"fideloper/proxy": "^4.4.1",
"friendsofphp/php-cs-fixer": "^2.17.3",
"fruitcake/laravel-cors": "^2.0.3",
"laravel/framework": "^9.0",
"nunomaduro/larastan": "^0.6.2",
"orchestra/testbench": "^6.0",
"phpstan/phpstan": "^0.12.36",
"phpunit/phpunit": "^9.3.3"
"nunomaduro/mock-final-classes": "^1.0",
"orchestra/testbench": "^7.0",
"phpstan/phpstan": "^0.12.64",
"phpunit/phpunit": "^9.5.0"
},
"autoload-dev": {
"psr-4": {
Expand Down
157 changes: 134 additions & 23 deletions src/Adapters/Laravel/Commands/TestCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,14 @@
use Illuminate\Console\Command;
use Illuminate\Support\Env;
use Illuminate\Support\Str;
use NunoMaduro\Collision\Adapters\Laravel\Exceptions\RequirementsException;
use RuntimeException;
use Symfony\Component\Process\Exception\ProcessSignaledException;
use Symfony\Component\Process\Process;

/**
* @internal
*
* @final
*/
class TestCommand extends Command
Expand All @@ -24,7 +27,11 @@ class TestCommand extends Command
*
* @var string
*/
protected $signature = 'test {--without-tty : Disable output to TTY}';
protected $signature = 'test
{--without-tty : Disable output to TTY}
{--parallel : Indicates if the tests should run in parallel}
{--recreate-databases : Indicates if the test databases should be re-created}
';

/**
* The console command description.
Expand All @@ -33,16 +40,6 @@ class TestCommand extends Command
*/
protected $description = 'Run the application tests';

/**
* The arguments to be used while calling phpunit.
*
* @var array
*/
protected $arguments = [
'--printer',
'NunoMaduro\Collision\Adapters\Phpunit\Printer',
];

/**
* Create a new command instance.
*
Expand All @@ -63,25 +60,48 @@ public function __construct()
public function handle()
{
if ((int) \PHPUnit\Runner\Version::id()[0] < 9) {
throw new RuntimeException('Running Collision ^5.0 artisan test command requires PHPUnit ^9.0.');
throw new RequirementsException('Running Collision ^5.0 artisan test command requires at least PHPUnit ^9.0.');
}

// @phpstan-ignore-next-line
if ((int) \Illuminate\Foundation\Application::VERSION[0] < 8) {
throw new RuntimeException('Running Collision ^5.0 artisan test command requires Laravel ^8.0.');
throw new RequirementsException('Running Collision ^5.0 artisan test command requires at least Laravel ^8.0.');
}

if ($this->option('parallel')) {
// @phpstan-ignore-next-line
if ((int) \Illuminate\Foundation\Application::VERSION[0] < 9) {
throw new RequirementsException('Running tests in parallel requires at least Laravel ^9.0.');
}

if (!$this->isParallelDependenciesInstalled()) {
if (!$this->confirm('Running tests in parallel requires "brianium/paratest". Do you wish to install it as a dev dependency?')) {
return 1;
}

$this->installParallelDependencies();
}
}

$options = array_slice($_SERVER['argv'], $this->option('without-tty') ? 3 : 2);

$this->clearEnv();

$parallel = $this->option('parallel');

$process = (new Process(array_merge(
$this->binary(),
array_merge(
$this->arguments,
$this->phpunitArguments($options)
)
)))->setTimeout(null);
// Binary ...
$this->binary(),
// Arguments ...
$parallel ? $this->paratestArguments($options) : $this->phpunitArguments($options)
),
null,
// Envs ...
$parallel ? [
'LARAVEL_PARALLEL_TESTING' => 1,
'LARAVEL_PARALLEL_TESTING_RECREATE_DATABASES' => $this->option('recreate-databases'),
] : [],
))->setTimeout(null);

try {
$process->setTty(!$this->option('without-tty'));
Expand All @@ -107,9 +127,17 @@ public function handle()
*/
protected function binary()
{
$command = class_exists(\Pest\Laravel\PestServiceProvider::class)
? 'vendor/pestphp/pest/bin/pest'
: 'vendor/phpunit/phpunit/phpunit';
switch (true) {
case $this->option('parallel'):
$command = 'vendor/brianium/paratest/bin/paratest';
break;
case class_exists(\Pest\Laravel\PestServiceProvider::class):
$command = 'vendor/pestphp/pest/bin/pest';
break;
default:
$command = 'vendor/phpunit/phpunit/phpunit';
break;
}

if ('phpdbg' === PHP_SAPI) {
return [PHP_BINARY, '-qrr', $command];
Expand All @@ -127,6 +155,8 @@ protected function binary()
*/
protected function phpunitArguments($options)
{
$options = array_merge(['--printer=NunoMaduro\\Collision\\Adapters\\Phpunit\\Printer'], $options);

$options = array_values(array_filter($options, function ($option) {
return !Str::startsWith($option, '--env=');
}));
Expand All @@ -135,7 +165,32 @@ protected function phpunitArguments($options)
$file = base_path('phpunit.xml.dist');
}

return array_merge(['-c', $file], $options);
return array_merge(["--configuration=$file"], $options);
}

/**
* Get the array of arguments for running Paratest.
*
* @param array $options
*
* @return array
*/
protected function paratestArguments($options)
{
$options = array_values(array_filter($options, function ($option) {
return !Str::startsWith($option, '--env=')
&& !Str::startsWith($option, '--parallel')
&& !Str::startsWith($option, '--recreate-databases');
}));

if (!file_exists($file = base_path('phpunit.xml'))) {
$file = base_path('phpunit.xml.dist');
}

return array_merge([
"--configuration=$file",
"--runner=\Illuminate\Testing\ParallelRunner",
], $options);
}

/**
Expand Down Expand Up @@ -187,4 +242,60 @@ protected static function getEnvironmentVariables($path, $file)

return $vars;
}

/**
* Check if the parallel dependencies are installed.
*
* @return bool
*/
protected function isParallelDependenciesInstalled()
{
return class_exists(\ParaTest\Console\Commands\ParaTestCommand::class);
}

/**
* Install parallel testing needed dependencies.
*
* @return void
*/
protected function installParallelDependencies()
{
$command = $this->findComposer() . ' require brianium/paratest --dev';

$process = Process::fromShellCommandline($command, null, null, null, null);

if ('\\' !== DIRECTORY_SEPARATOR && file_exists('/dev/tty') && is_readable('/dev/tty')) {
try {
$process->setTty(true);
} catch (RuntimeException $e) {
$this->output->writeln('Warning: ' . $e->getMessage());
}
}

try {
$process->run(function ($type, $line) {
$this->output->write($line);
});
} catch (ProcessSignaledException $e) {
if (extension_loaded('pcntl') && $e->getSignal() !== SIGINT) {
throw $e;
}
}
}

/**
* Get the composer command for the environment.
*
* @return string
*/
protected function findComposer()
{
$composerPath = getcwd() . '/composer.phar';

if (file_exists($composerPath)) {
return '"' . PHP_BINARY . '" ' . $composerPath;
}

return 'composer';
}
}
16 changes: 16 additions & 0 deletions src/Adapters/Laravel/Exceptions/RequirementsException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

declare(strict_types=1);

namespace NunoMaduro\Collision\Adapters\Laravel\Exceptions;

use NunoMaduro\Collision\Contracts\RenderlessEditor;
use NunoMaduro\Collision\Contracts\RenderlessTrace;
use RuntimeException;

/**
* @internal
*/
final class RequirementsException extends RuntimeException implements RenderlessEditor, RenderlessTrace
{
}
1 change: 0 additions & 1 deletion src/Highlighter.php
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,6 @@ private function tokenize($source): array
case T_CLOSE_TAG:
case T_STRING:
case T_VARIABLE:

// Constants
case T_DIR:
case T_FILE:
Expand Down
12 changes: 12 additions & 0 deletions tests/LaravelApp/app/Console/Commands/TestCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,18 @@ class TestCommand extends BaseTestCommand
*/
protected function binary()
{
switch (true) {
case $this->option('parallel'):
$command = 'vendor/brianium/paratest/bin/paratest';
break;
case class_exists(\Pest\Laravel\PestServiceProvider::class):
$command = 'vendor/pestphp/pest/bin/pest';
break;
default:
$command = 'vendor/phpunit/phpunit/phpunit';
break;
}

if ('phpdbg' === PHP_SAPI) {
return [PHP_BINARY, '-qrr', __DIR__ . '/../../../../../vendor/bin/phpunit'];
}
Expand Down
6 changes: 3 additions & 3 deletions tests/LaravelApp/tests/Feature/EnvironmentTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,20 @@
class EnvironmentTest extends TestCase
{
/** @test */
public function variable_only_in_dot_env()
public function variableOnlyInDotEnv()
{
$this->assertEquals('VAL_IN_DOT_ENV', env('VAR_IN_DOT_ENV'));
$this->assertEquals(null, env('VAR_IN_DOT_ENV_TESTING'));
}

/** @test */
public function variable_only_in_phpunit()
public function variableOnlyInPhpunit()
{
$this->assertEquals('VAL_IN_PHPUNIT', env('VAR_IN_PHPUNIT'));
}

/** @test */
public function variable_in_dot_env_but_overridden_in_phpunit()
public function variableInDotEnvButOverriddenInPhpunit()
{
$this->assertEquals('VAL_OVERRIDDEN_IN_PHPUNIT', env('VAR_OVERRIDDEN_IN_PHPUNIT'));
}
Expand Down
6 changes: 3 additions & 3 deletions tests/LaravelApp/tests/Feature/EnvironmentTestingTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,20 @@
class EnvironmentTestingTest extends TestCase
{
/** @test */
public function variable_only_in_dot_env()
public function variableOnlyInDotEnv()
{
$this->assertEquals(null, env('VAR_IN_DOT_ENV'));
$this->assertEquals('VAL_IN_DOT_ENV_TESTING', env('VAR_IN_DOT_ENV_TESTING'));
}

/** @test */
public function variable_only_in_phpunit()
public function variableOnlyInPhpunit()
{
$this->assertEquals('VAL_IN_PHPUNIT', env('VAR_IN_PHPUNIT'));
}

/** @test */
public function variable_in_dot_env_but_overridden_in_phpunit()
public function variableInDotEnvButOverriddenInPhpunit()
{
$this->assertEquals('VAL_OVERRIDDEN_IN_PHPUNIT', env('VAR_OVERRIDDEN_IN_PHPUNIT'));
}
Expand Down
4 changes: 2 additions & 2 deletions tests/Unit/Adapters/ArtisanTestCommandTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
class ArtisanTestCommandTest extends TestCase
{
/** @test */
public function test_env(): void
public function testEnv(): void
{
$this->runTests([
'./vendor/bin/phpunit',
Expand All @@ -25,7 +25,7 @@ public function test_env(): void
}

/** @test */
public function test_env_testing(): void
public function testEnvTesting(): void
{
file_put_contents(__DIR__ . '/../../../tests/LaravelApp/.env.testing', <<<EOF
VAR_IN_DOT_ENV_TESTING=VAL_IN_DOT_ENV_TESTING
Expand Down
Loading

0 comments on commit c143c38

Please sign in to comment.