644 lines
21 KiB
PHP
644 lines
21 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace ParaTest;
|
|
|
|
use Fidry\CpuCoreCounter\CpuCoreCounter;
|
|
use Fidry\CpuCoreCounter\NumberOfCpuCoreNotFound;
|
|
use PHPUnit\TextUI\Configuration\Builder;
|
|
use PHPUnit\TextUI\Configuration\Configuration;
|
|
use RuntimeException;
|
|
use Symfony\Component\Console\Input\InputArgument;
|
|
use Symfony\Component\Console\Input\InputDefinition;
|
|
use Symfony\Component\Console\Input\InputInterface;
|
|
use Symfony\Component\Console\Input\InputOption;
|
|
use Symfony\Component\Process\Process;
|
|
|
|
use function array_filter;
|
|
use function array_intersect_key;
|
|
use function array_key_exists;
|
|
use function array_shift;
|
|
use function assert;
|
|
use function count;
|
|
use function dirname;
|
|
use function escapeshellarg;
|
|
use function file_exists;
|
|
use function is_array;
|
|
use function is_bool;
|
|
use function is_numeric;
|
|
use function is_string;
|
|
use function realpath;
|
|
use function sprintf;
|
|
use function str_starts_with;
|
|
use function strlen;
|
|
use function substr;
|
|
use function sys_get_temp_dir;
|
|
use function uniqid;
|
|
use function unserialize;
|
|
|
|
use const PHP_BINARY;
|
|
|
|
/**
|
|
* @internal
|
|
*
|
|
* @immutable
|
|
*/
|
|
final readonly class Options
|
|
{
|
|
public const ENV_KEY_TOKEN = 'TEST_TOKEN';
|
|
public const ENV_KEY_UNIQUE_TOKEN = 'UNIQUE_TEST_TOKEN';
|
|
|
|
private const OPTIONS_TO_KEEP_FOR_PHPUNIT_IN_WORKER = [
|
|
'bootstrap' => true,
|
|
'cache-directory' => true,
|
|
'configuration' => true,
|
|
'coverage-filter' => true,
|
|
'dont-report-useless-tests' => true,
|
|
'exclude-group' => true,
|
|
'fail-on-incomplete' => true,
|
|
'fail-on-risky' => true,
|
|
'fail-on-skipped' => true,
|
|
'fail-on-warning' => true,
|
|
'fail-on-deprecation' => true,
|
|
'filter' => true,
|
|
'group' => true,
|
|
'no-configuration' => true,
|
|
'order-by' => true,
|
|
'process-isolation' => true,
|
|
'random-order-seed' => true,
|
|
'stop-on-defect' => true,
|
|
'stop-on-error' => true,
|
|
'stop-on-warning' => true,
|
|
'stop-on-risky' => true,
|
|
'stop-on-skipped' => true,
|
|
'stop-on-incomplete' => true,
|
|
'strict-coverage' => true,
|
|
'strict-global-state' => true,
|
|
'disallow-test-output' => true,
|
|
];
|
|
|
|
public readonly bool $needsTeamcity;
|
|
|
|
/**
|
|
* @param non-empty-string $phpunit
|
|
* @param non-empty-string $cwd
|
|
* @param list<non-empty-string>|null $passthruPhp
|
|
* @param array<non-empty-string, non-empty-string|true|list<non-empty-string>> $phpunitOptions
|
|
* @param non-empty-string $runner
|
|
* @param non-empty-string $tmpDir
|
|
*/
|
|
public function __construct(
|
|
public Configuration $configuration,
|
|
public string $phpunit,
|
|
public string $cwd,
|
|
public int $maxBatchSize,
|
|
public bool $noTestTokens,
|
|
public ?array $passthruPhp,
|
|
public array $phpunitOptions,
|
|
public int $processes,
|
|
public string $runner,
|
|
public string $tmpDir,
|
|
public bool $verbose,
|
|
public bool $functional,
|
|
) {
|
|
$this->needsTeamcity = $configuration->outputIsTeamCity() || $configuration->hasLogfileTeamcity();
|
|
}
|
|
|
|
/** @param non-empty-string $cwd */
|
|
public static function fromConsoleInput(InputInterface $input, string $cwd): self
|
|
{
|
|
$options = $input->getOptions();
|
|
|
|
$maxBatchSize = (int) $options['max-batch-size'];
|
|
unset($options['max-batch-size']);
|
|
|
|
assert(is_bool($options['no-test-tokens']));
|
|
$noTestTokens = $options['no-test-tokens'];
|
|
unset($options['no-test-tokens']);
|
|
|
|
assert($options['passthru-php'] === null || is_string($options['passthru-php']));
|
|
$passthruPhp = self::parsePassthru($options['passthru-php']);
|
|
unset($options['passthru-php']);
|
|
|
|
assert(is_string($options['processes']));
|
|
$processes = is_numeric($options['processes'])
|
|
? (int) $options['processes']
|
|
: self::getNumberOfCPUCores();
|
|
unset($options['processes']);
|
|
|
|
assert(is_string($options['runner']) && $options['runner'] !== '');
|
|
$runner = $options['runner'];
|
|
unset($options['runner']);
|
|
|
|
assert(is_string($options['tmp-dir']) && $options['tmp-dir'] !== '');
|
|
$tmpDir = $options['tmp-dir'];
|
|
unset($options['tmp-dir']);
|
|
|
|
assert(is_bool($options['verbose']));
|
|
$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']);
|
|
} elseif ($options['colors'] === null) {
|
|
$options['colors'] = Configuration::COLOR_AUTO;
|
|
}
|
|
|
|
assert(array_key_exists('coverage-text', $options));
|
|
if ($options['coverage-text'] === null) {
|
|
$options['coverage-text'] = 'php://stdout';
|
|
}
|
|
|
|
// Must be a static non-customizable reference because ParaTest code
|
|
// is strictly coupled with PHPUnit pinned version
|
|
$phpunit = self::getPhpunitBinary();
|
|
if (str_starts_with($phpunit, $cwd)) {
|
|
$phpunit = substr($phpunit, 1 + strlen($cwd));
|
|
}
|
|
|
|
$phpunitArgv = [$phpunit];
|
|
foreach ($options as $key => $value) {
|
|
if ($value === null || $value === false) {
|
|
continue;
|
|
}
|
|
|
|
if ($value === true) {
|
|
$phpunitArgv[] = "--{$key}";
|
|
continue;
|
|
}
|
|
|
|
if (! is_array($value)) {
|
|
$value = [$value];
|
|
}
|
|
|
|
foreach ($value as $innerValue) {
|
|
$phpunitArgv[] = "--{$key}={$innerValue}";
|
|
}
|
|
}
|
|
|
|
if (($path = $input->getArgument('path')) !== null) {
|
|
assert(is_string($path));
|
|
$phpunitArgv[] = '--';
|
|
$phpunitArgv[] = $path;
|
|
}
|
|
|
|
$phpunitOptions = array_intersect_key($options, self::OPTIONS_TO_KEEP_FOR_PHPUNIT_IN_WORKER);
|
|
$phpunitOptions = array_filter($phpunitOptions);
|
|
|
|
$configuration = (new Builder())->build($phpunitArgv);
|
|
|
|
return new self(
|
|
$configuration,
|
|
$phpunit,
|
|
$cwd,
|
|
$maxBatchSize,
|
|
$noTestTokens,
|
|
$passthruPhp,
|
|
$phpunitOptions,
|
|
$processes,
|
|
$runner,
|
|
$tmpDir,
|
|
$verbose,
|
|
$functional,
|
|
);
|
|
}
|
|
|
|
public static function setInputDefinition(InputDefinition $inputDefinition): void
|
|
{
|
|
$inputDefinition->setDefinition([
|
|
// Arguments
|
|
new InputArgument(
|
|
'path',
|
|
InputArgument::OPTIONAL,
|
|
'The path to a directory or file containing tests.',
|
|
),
|
|
|
|
// 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',
|
|
InputOption::VALUE_REQUIRED,
|
|
'Max batch size.',
|
|
'0',
|
|
),
|
|
new InputOption(
|
|
'no-test-tokens',
|
|
null,
|
|
InputOption::VALUE_NONE,
|
|
'Disable TEST_TOKEN environment variables.',
|
|
),
|
|
new InputOption(
|
|
'passthru-php',
|
|
null,
|
|
InputOption::VALUE_REQUIRED,
|
|
'Pass the given arguments verbatim to the underlying php process. Example: --passthru-php="\'-d\' ' .
|
|
'\'pcov.enabled=1\'"',
|
|
),
|
|
new InputOption(
|
|
'processes',
|
|
'p',
|
|
InputOption::VALUE_REQUIRED,
|
|
'The number of test processes to run.',
|
|
'auto',
|
|
),
|
|
new InputOption(
|
|
'runner',
|
|
null,
|
|
InputOption::VALUE_REQUIRED,
|
|
sprintf('A %s.', RunnerInterface::class),
|
|
'WrapperRunner',
|
|
),
|
|
new InputOption(
|
|
'tmp-dir',
|
|
null,
|
|
InputOption::VALUE_REQUIRED,
|
|
'Temporary directory for internal ParaTest files',
|
|
sys_get_temp_dir(),
|
|
),
|
|
new InputOption(
|
|
'verbose',
|
|
'v',
|
|
InputOption::VALUE_NONE,
|
|
'Output more verbose information',
|
|
),
|
|
|
|
// PHPUnit options
|
|
new InputOption(
|
|
'bootstrap',
|
|
null,
|
|
InputOption::VALUE_REQUIRED,
|
|
'@see PHPUnit guide, chapter: ' . $chapter = 'Configuration',
|
|
),
|
|
new InputOption(
|
|
'configuration',
|
|
'c',
|
|
InputOption::VALUE_REQUIRED,
|
|
'@see PHPUnit guide, chapter: ' . $chapter,
|
|
),
|
|
new InputOption(
|
|
'no-configuration',
|
|
null,
|
|
InputOption::VALUE_NONE,
|
|
'@see PHPUnit guide, chapter: ' . $chapter,
|
|
),
|
|
new InputOption(
|
|
'cache-directory',
|
|
null,
|
|
InputOption::VALUE_REQUIRED,
|
|
'@see PHPUnit guide, chapter: ' . $chapter,
|
|
),
|
|
new InputOption(
|
|
'testsuite',
|
|
null,
|
|
InputOption::VALUE_REQUIRED,
|
|
'@see PHPUnit guide, chapter: ' . $chapter = 'Selection',
|
|
),
|
|
new InputOption(
|
|
'exclude-testsuite',
|
|
null,
|
|
InputOption::VALUE_REQUIRED,
|
|
'@see PHPUnit guide, chapter: ' . $chapter,
|
|
),
|
|
new InputOption(
|
|
'group',
|
|
null,
|
|
InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY,
|
|
'@see PHPUnit guide, chapter: ' . $chapter,
|
|
),
|
|
new InputOption(
|
|
'exclude-group',
|
|
null,
|
|
InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY,
|
|
'@see PHPUnit guide, chapter: ' . $chapter,
|
|
),
|
|
new InputOption(
|
|
'filter',
|
|
null,
|
|
InputOption::VALUE_REQUIRED,
|
|
'@see PHPUnit guide, chapter: ' . $chapter,
|
|
),
|
|
new InputOption(
|
|
'process-isolation',
|
|
null,
|
|
InputOption::VALUE_NONE,
|
|
'@see PHPUnit guide, chapter: ' . $chapter = 'Execution',
|
|
),
|
|
new InputOption(
|
|
'strict-coverage',
|
|
null,
|
|
InputOption::VALUE_NONE,
|
|
'@see PHPUnit guide, chapter: ' . $chapter,
|
|
),
|
|
new InputOption(
|
|
'strict-global-state',
|
|
null,
|
|
InputOption::VALUE_NONE,
|
|
'@see PHPUnit guide, chapter: ' . $chapter,
|
|
),
|
|
new InputOption(
|
|
'disallow-test-output',
|
|
null,
|
|
InputOption::VALUE_NONE,
|
|
'@see PHPUnit guide, chapter: ' . $chapter,
|
|
),
|
|
new InputOption(
|
|
'dont-report-useless-tests',
|
|
null,
|
|
InputOption::VALUE_NONE,
|
|
'@see PHPUnit guide, chapter: ' . $chapter,
|
|
),
|
|
new InputOption(
|
|
'stop-on-defect',
|
|
null,
|
|
InputOption::VALUE_NONE,
|
|
'@see PHPUnit guide, chapter: ' . $chapter,
|
|
),
|
|
new InputOption(
|
|
'stop-on-error',
|
|
null,
|
|
InputOption::VALUE_NONE,
|
|
'@see PHPUnit guide, chapter: ' . $chapter,
|
|
),
|
|
new InputOption(
|
|
'stop-on-failure',
|
|
null,
|
|
InputOption::VALUE_NONE,
|
|
'@see PHPUnit guide, chapter: ' . $chapter,
|
|
),
|
|
new InputOption(
|
|
'stop-on-warning',
|
|
null,
|
|
InputOption::VALUE_NONE,
|
|
'@see PHPUnit guide, chapter: ' . $chapter,
|
|
),
|
|
new InputOption(
|
|
'stop-on-risky',
|
|
null,
|
|
InputOption::VALUE_NONE,
|
|
'@see PHPUnit guide, chapter: ' . $chapter,
|
|
),
|
|
new InputOption(
|
|
'stop-on-skipped',
|
|
null,
|
|
InputOption::VALUE_NONE,
|
|
'@see PHPUnit guide, chapter: ' . $chapter,
|
|
),
|
|
new InputOption(
|
|
'stop-on-incomplete',
|
|
null,
|
|
InputOption::VALUE_NONE,
|
|
'@see PHPUnit guide, chapter: ' . $chapter,
|
|
),
|
|
new InputOption(
|
|
'fail-on-incomplete',
|
|
null,
|
|
InputOption::VALUE_NONE,
|
|
'@see PHPUnit guide, chapter: ' . $chapter,
|
|
),
|
|
new InputOption(
|
|
'fail-on-risky',
|
|
null,
|
|
InputOption::VALUE_NONE,
|
|
'@see PHPUnit guide, chapter: ' . $chapter,
|
|
),
|
|
new InputOption(
|
|
'fail-on-skipped',
|
|
null,
|
|
InputOption::VALUE_NONE,
|
|
'@see PHPUnit guide, chapter: ' . $chapter,
|
|
),
|
|
new InputOption(
|
|
'fail-on-warning',
|
|
null,
|
|
InputOption::VALUE_NONE,
|
|
'@see PHPUnit guide, chapter: ' . $chapter,
|
|
),
|
|
new InputOption(
|
|
'fail-on-deprecation',
|
|
null,
|
|
InputOption::VALUE_NONE,
|
|
'@see PHPUnit guide, chapter: ' . $chapter,
|
|
),
|
|
new InputOption(
|
|
'order-by',
|
|
null,
|
|
InputOption::VALUE_REQUIRED,
|
|
'@see PHPUnit guide, chapter: ' . $chapter,
|
|
),
|
|
new InputOption(
|
|
'random-order-seed',
|
|
null,
|
|
InputOption::VALUE_REQUIRED,
|
|
'@see PHPUnit guide, chapter: ' . $chapter,
|
|
),
|
|
new InputOption(
|
|
'colors',
|
|
null,
|
|
InputOption::VALUE_OPTIONAL,
|
|
'@see PHPUnit guide, chapter: ' . $chapter = 'Reporting',
|
|
Configuration::COLOR_DEFAULT,
|
|
),
|
|
new InputOption(
|
|
'no-progress',
|
|
null,
|
|
InputOption::VALUE_NONE,
|
|
'@see PHPUnit guide, chapter: ' . $chapter,
|
|
),
|
|
new InputOption(
|
|
'display-incomplete',
|
|
null,
|
|
InputOption::VALUE_NONE,
|
|
'@see PHPUnit guide, chapter: ' . $chapter,
|
|
),
|
|
new InputOption(
|
|
'display-skipped',
|
|
null,
|
|
InputOption::VALUE_NONE,
|
|
'@see PHPUnit guide, chapter: ' . $chapter,
|
|
),
|
|
new InputOption(
|
|
'display-deprecations',
|
|
null,
|
|
InputOption::VALUE_NONE,
|
|
'@see PHPUnit guide, chapter: ' . $chapter,
|
|
),
|
|
new InputOption(
|
|
'display-errors',
|
|
null,
|
|
InputOption::VALUE_NONE,
|
|
'@see PHPUnit guide, chapter: ' . $chapter,
|
|
),
|
|
new InputOption(
|
|
'display-notices',
|
|
null,
|
|
InputOption::VALUE_NONE,
|
|
'@see PHPUnit guide, chapter: ' . $chapter,
|
|
),
|
|
new InputOption(
|
|
'display-warnings',
|
|
null,
|
|
InputOption::VALUE_NONE,
|
|
'@see PHPUnit guide, chapter: ' . $chapter,
|
|
),
|
|
new InputOption(
|
|
'teamcity',
|
|
null,
|
|
InputOption::VALUE_NONE,
|
|
'@see PHPUnit guide, chapter: ' . $chapter,
|
|
),
|
|
new InputOption(
|
|
'testdox',
|
|
null,
|
|
InputOption::VALUE_NONE,
|
|
'@see PHPUnit guide, chapter: ' . $chapter,
|
|
),
|
|
new InputOption(
|
|
'log-junit',
|
|
null,
|
|
InputOption::VALUE_REQUIRED,
|
|
'@see PHPUnit guide, chapter: ' . $chapter = 'Logging',
|
|
),
|
|
new InputOption(
|
|
'log-teamcity',
|
|
null,
|
|
InputOption::VALUE_REQUIRED,
|
|
'@see PHPUnit guide, chapter: ' . $chapter,
|
|
),
|
|
new InputOption(
|
|
'coverage-clover',
|
|
null,
|
|
InputOption::VALUE_REQUIRED,
|
|
'@see PHPUnit guide, chapter: ' . $chapter = 'Code Coverage',
|
|
),
|
|
new InputOption(
|
|
'coverage-cobertura',
|
|
null,
|
|
InputOption::VALUE_REQUIRED,
|
|
'@see PHPUnit guide, chapter: ' . $chapter,
|
|
),
|
|
new InputOption(
|
|
'coverage-crap4j',
|
|
null,
|
|
InputOption::VALUE_REQUIRED,
|
|
'@see PHPUnit guide, chapter: ' . $chapter,
|
|
),
|
|
new InputOption(
|
|
'coverage-html',
|
|
null,
|
|
InputOption::VALUE_REQUIRED,
|
|
'@see PHPUnit guide, chapter: ' . $chapter,
|
|
),
|
|
new InputOption(
|
|
'coverage-php',
|
|
null,
|
|
InputOption::VALUE_REQUIRED,
|
|
'@see PHPUnit guide, chapter: ' . $chapter,
|
|
),
|
|
new InputOption(
|
|
'coverage-text',
|
|
null,
|
|
InputOption::VALUE_OPTIONAL,
|
|
'@see PHPUnit guide, chapter: ' . $chapter,
|
|
false,
|
|
),
|
|
new InputOption(
|
|
'coverage-xml',
|
|
null,
|
|
InputOption::VALUE_REQUIRED,
|
|
'@see PHPUnit guide, chapter: ' . $chapter,
|
|
),
|
|
new InputOption(
|
|
'coverage-filter',
|
|
null,
|
|
InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY,
|
|
'@see PHPUnit guide, chapter: ' . $chapter,
|
|
),
|
|
new InputOption(
|
|
'no-coverage',
|
|
null,
|
|
InputOption::VALUE_NONE,
|
|
'@see PHPUnit guide, chapter: ' . $chapter,
|
|
),
|
|
]);
|
|
}
|
|
|
|
/** @return non-empty-string $phpunit the path to phpunit */
|
|
private static function getPhpunitBinary(): string
|
|
{
|
|
$tryPaths = [
|
|
dirname(__DIR__, 3) . '/bin/phpunit',
|
|
dirname(__DIR__, 3) . '/phpunit/phpunit/phpunit',
|
|
dirname(__DIR__) . '/vendor/phpunit/phpunit/phpunit',
|
|
];
|
|
|
|
foreach ($tryPaths as $path) {
|
|
if (($realPath = realpath($path)) !== false && file_exists($realPath)) {
|
|
return $realPath;
|
|
}
|
|
}
|
|
|
|
throw new RuntimeException('PHPUnit not found'); // @codeCoverageIgnore
|
|
}
|
|
|
|
public static function getNumberOfCPUCores(): int
|
|
{
|
|
try {
|
|
return (new CpuCoreCounter())->getCount();
|
|
} catch (NumberOfCpuCoreNotFound) {
|
|
return 2;
|
|
}
|
|
}
|
|
|
|
/** @return list<non-empty-string>|null */
|
|
private static function parsePassthru(?string $param): ?array
|
|
{
|
|
if ($param === null) {
|
|
return null;
|
|
}
|
|
|
|
$stringToArgumentProcess = Process::fromShellCommandline(
|
|
sprintf(
|
|
'%s -r %s -- %s',
|
|
escapeshellarg(PHP_BINARY),
|
|
escapeshellarg('echo serialize($argv);'),
|
|
$param,
|
|
),
|
|
);
|
|
$stringToArgumentProcess->mustRun();
|
|
|
|
$passthruAsArguments = unserialize($stringToArgumentProcess->getOutput());
|
|
assert(is_array($passthruAsArguments));
|
|
array_shift($passthruAsArguments);
|
|
|
|
if (count($passthruAsArguments) === 0) {
|
|
return null;
|
|
}
|
|
|
|
return $passthruAsArguments;
|
|
}
|
|
|
|
/** @return array{PARATEST: int, TEST_TOKEN?: int, UNIQUE_TEST_TOKEN?: non-empty-string} */
|
|
public function fillEnvWithTokens(int $inc): array
|
|
{
|
|
$env = ['PARATEST' => 1];
|
|
if (! $this->noTestTokens) {
|
|
$env[self::ENV_KEY_TOKEN] = $inc;
|
|
$env[self::ENV_KEY_UNIQUE_TOKEN] = uniqid($inc . '_');
|
|
}
|
|
|
|
return $env;
|
|
}
|
|
}
|