Skip to content

Commit

Permalink
Implement dataset parallelization with --functional flag (#770)
Browse files Browse the repository at this point in the history
  • Loading branch information
Slamdunk authored Jun 14, 2023
1 parent 893eab6 commit 406eed2
Show file tree
Hide file tree
Showing 13 changed files with 175 additions and 48 deletions.
10 changes: 1 addition & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ start using it with no additional bootstrap or configurations!

Benefits:

* Zero configuration. After the installation, run with `vendor/bin/paratest`. That's it!
* Zero configuration. After the installation, run with `vendor/bin/paratest` to parallelize by TestCase or `vendor/bin/paratest --functional` to parallelize by Test. That's it!
* Code Coverage report combining. Run your tests in N parallel processes and all the code coverage output will be combined into one report.

# Installation
Expand Down Expand Up @@ -72,14 +72,6 @@ If you have `xDebug` installed, activating it by the environment variable is eno
XDEBUG_MODE=coverage vendor/bin/paratest
```

### PHPDBG

`PHPDBG` is automatically detected and used in the subprocesses if it's the running binary of the main process:

```
phpdbg vendor/bin/paratest
```

## Initial setup for all tests

Because ParaTest runs multiple processes in parallel, each with their own instance of the PHP interpreter,
Expand Down
2 changes: 1 addition & 1 deletion bin/phpunit-wrapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@
}

// It must be a 1 byte string to ensure filesize() is equal to the number of tests executed
$exitCode = $application->runTest(trim($testPath));
$exitCode = $application->runTest(trim($testPath, "\n"));

fwrite($statusFile, (string) $exitCode);
fflush($statusFile);
Expand Down
5 changes: 0 additions & 5 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,6 @@ parameters:
count: 1
path: src/WrapperRunner/ResultPrinter.php

-
message: "#^Property ParaTest\\\\WrapperRunner\\\\SuiteLoader\\:\\:\\$files \\(array\\<int, non\\-empty\\-string\\>\\) does not accept array\\<int, int\\|string\\>\\.$#"
count: 1
path: src/WrapperRunner/SuiteLoader.php

-
message: "#^Unreachable statement \\- code above always terminates\\.$#"
count: 1
Expand Down
12 changes: 12 additions & 0 deletions src/Options.php
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ private function __construct(
public readonly string $runner,
public readonly string $tmpDir,
public readonly bool $verbose,
public readonly bool $functional,
) {
$this->needsTeamcity = $configuration->outputIsTeamCity() || $configuration->hasLogfileTeamcity();
}
Expand Down Expand Up @@ -137,6 +138,10 @@ public static function fromConsoleInput(InputInterface $input, string $cwd): sel
$verbose = $options['verbose'];
unset($options['verbose']);

assert(is_bool($options['functional']));
$functional = $options['functional'];
unset($options['functional']);

assert(array_key_exists('colors', $options));
if ($options['colors'] === Configuration::COLOR_DEFAULT) {
unset($options['colors']);
Expand Down Expand Up @@ -192,6 +197,7 @@ public static function fromConsoleInput(InputInterface $input, string $cwd): sel
$runner,
$tmpDir,
$verbose,
$functional,
);
}

Expand All @@ -206,6 +212,12 @@ public static function setInputDefinition(InputDefinition $inputDefinition): voi
),

// ParaTest options
new InputOption(
'functional',
null,
InputOption::VALUE_NONE,
'Whether to enable functional testing, for unit and dataset parallelization',
),
new InputOption(
'max-batch-size',
'm',
Expand Down
30 changes: 28 additions & 2 deletions src/WrapperRunner/ApplicationForWrapperWorker.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
use PHPUnit\Runner\Extension\ExtensionBootstrapper;
use PHPUnit\Runner\Extension\Facade as ExtensionFacade;
use PHPUnit\Runner\Extension\PharLoader;
use PHPUnit\Runner\Filter\Factory;
use PHPUnit\Runner\TestSuiteLoader;
use PHPUnit\Runner\TestSuiteSorter;
use PHPUnit\TestRunner\TestResult\Facade as TestResultFacade;
Expand All @@ -32,8 +33,12 @@

use function assert;
use function file_put_contents;
use function is_file;
use function mt_srand;
use function serialize;
use function str_ends_with;
use function strpos;
use function substr;

/**
* @internal
Expand All @@ -60,10 +65,23 @@ public function __construct(

public function runTest(string $testPath): int
{
$null = strpos($testPath, "\0");
$filter = null;
if ($null !== false) {
$filter = new Factory();
$filter->addNameFilter(substr($testPath, $null + 1));
$testPath = substr($testPath, 0, $null);
}

$this->bootstrap();

$testSuiteRefl = (new TestSuiteLoader())->load($testPath);
$testSuite = TestSuite::fromClassReflector($testSuiteRefl);
if (is_file($testPath) && str_ends_with($testPath, '.phpt')) {
$testSuite = TestSuite::empty($testPath);
$testSuite->addTestFile($testPath);
} else {
$testSuiteRefl = (new TestSuiteLoader())->load($testPath);
$testSuite = TestSuite::fromClassReflector($testSuiteRefl);
}

(new TestSuiteFilterProcessor())->process($this->configuration, $testSuite);

Expand All @@ -73,6 +91,14 @@ public function runTest(string $testPath): int
);
}

if ($filter !== null) {
$testSuite->injectFilter($filter);

EventFacade::emitter()->testSuiteFiltered(
TestSuiteBuilder::from($testSuite),
);
}

EventFacade::emitter()->testRunnerExecutionStarted(
TestSuiteBuilder::from($testSuite),
);
Expand Down
47 changes: 37 additions & 10 deletions src/WrapperRunner/SuiteLoader.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace ParaTest\WrapperRunner;

use Generator;
use ParaTest\Options;
use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\TestSuite;
Expand All @@ -23,6 +24,7 @@
use function array_keys;
use function assert;
use function count;
use function is_int;
use function is_string;
use function mt_srand;
use function ob_get_clean;
Expand All @@ -36,7 +38,7 @@ final class SuiteLoader
{
public readonly int $testCount;
/** @var list<non-empty-string> */
public readonly array $files;
public readonly array $tests;

public function __construct(
private readonly Options $options,
Expand Down Expand Up @@ -73,8 +75,30 @@ public function __construct(
$this->testCount = count($testSuite);

$files = [];
$this->loadFiles($testSuite, $files);
$this->files = array_keys($files);
$tests = [];
foreach ($this->loadFiles($testSuite) as $file => $test) {
$files[$file] = null;

if ($test instanceof PhptTestCase) {
$tests[] = $file;
} else {
$name = $test->name();
if ($test->providedData() !== []) {
$dataName = $test->dataName();
if (is_int($dataName)) {
$name .= '#' . $dataName;
} else {
$name .= '@' . $dataName;
}
}

$tests[] = "$file\0$name";
}
}

$this->tests = $this->options->functional
? $tests
: array_keys($files);

if (! $this->options->configuration->hasCoverageReport()) {
return;
Expand All @@ -94,21 +118,23 @@ public function __construct(
}
}

/** @param array<non-empty-string, bool> $files */
private function loadFiles(TestSuite $testSuite, array &$files): void
/** @return Generator<non-empty-string, (PhptTestCase|TestCase)> */
private function loadFiles(TestSuite $testSuite): Generator
{
foreach ($testSuite as $test) {
if ($test instanceof TestSuite) {
$this->loadFiles($test, $files);
yield from $this->loadFiles($test);

continue;
}

if ($test instanceof PhptTestCase) {
$refProperty = new ReflectionProperty(PhptTestCase::class, 'filename');
$filename = $refProperty->getValue($test);
assert(is_string($filename) && $filename !== '');
$filename = $this->stripCwd($filename);
$files[$filename] = true;
$filename = $this->stripCwd($filename);

yield $filename => $test;

continue;
}
Expand All @@ -117,8 +143,9 @@ private function loadFiles(TestSuite $testSuite, array &$files): void
$refClass = new ReflectionClass($test);
$filename = $refClass->getFileName();
assert(is_string($filename) && $filename !== '');
$filename = $this->stripCwd($filename);
$files[$filename] = true;
$filename = $this->stripCwd($filename);

yield $filename => $test;

continue;
}
Expand Down
2 changes: 1 addition & 1 deletion src/WrapperRunner/WrapperRunner.php
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ public function run(): int
);
$result = TestResultFacade::result();

$this->pending = $suiteLoader->files;
$this->pending = $suiteLoader->tests;
$this->printer->setTestCount($suiteLoader->testCount);
$this->printer->start();
$this->startWorkers();
Expand Down
4 changes: 4 additions & 0 deletions src/WrapperRunner/WrapperWorker.php
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,10 @@ public function __construct(

$phpunitArguments = [$options->phpunit];
foreach ($options->phpunitOptions as $key => $value) {
if ($options->functional && $key === 'filter') {
continue;
}

$phpunitArguments[] = "--{$key}";
if ($value === true) {
continue;
Expand Down
6 changes: 3 additions & 3 deletions test/Unit/WrapperRunner/SuiteLoaderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,14 @@ public function testLoadTestsuiteFileFromConfig(): void
$loader = $this->loadSuite();

self::assertSame(7, $loader->testCount);
self::assertCount(7, $loader->files);
self::assertCount(7, $loader->tests);
}

public function testLoadFileGetsPathOfFile(): void
{
$path = $this->fixture('common_results' . DIRECTORY_SEPARATOR . 'SuccessTest.php');
$this->bareOptions['path'] = $path;
$files = $this->loadSuite()->files;
$files = $this->loadSuite()->tests;

$file = array_shift($files);
self::assertNotNull($file);
Expand All @@ -61,7 +61,7 @@ public function testCacheIsWarmedWhenSpecified(): void
public function testLoadsPhptFiles(): void
{
$this->bareOptions['path'] = $this->fixture('phpt');
$files = $this->loadSuite()->files;
$files = $this->loadSuite()->tests;

$file = array_shift($files);
self::assertNotNull($file);
Expand Down
Loading

0 comments on commit 406eed2

Please sign in to comment.