vendor and env first commit
This commit is contained in:
@@ -0,0 +1,243 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ParaTest\WrapperRunner;
|
||||
|
||||
use ParaTest\RunnerInterface;
|
||||
use PHPUnit\Event\Facade as EventFacade;
|
||||
use PHPUnit\Event\TestSuite\TestSuiteBuilder;
|
||||
use PHPUnit\Framework\TestSuite;
|
||||
use PHPUnit\Logging\JUnit\JunitXmlLogger;
|
||||
use PHPUnit\Logging\TeamCity\TeamCityLogger;
|
||||
use PHPUnit\Logging\TestDox\TestResultCollector;
|
||||
use PHPUnit\Metadata\Api\CodeCoverage as CodeCoverageMetadataApi;
|
||||
use PHPUnit\Runner\CodeCoverage;
|
||||
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\Runner\Version;
|
||||
use PHPUnit\TestRunner\TestResult\Facade as TestResultFacade;
|
||||
use PHPUnit\TextUI\Configuration\Builder;
|
||||
use PHPUnit\TextUI\Configuration\CodeCoverageFilterRegistry;
|
||||
use PHPUnit\TextUI\Configuration\Configuration;
|
||||
use PHPUnit\TextUI\Configuration\PhpHandler;
|
||||
use PHPUnit\TextUI\Output\Default\ProgressPrinter\ProgressPrinter;
|
||||
use PHPUnit\TextUI\Output\Default\UnexpectedOutputPrinter;
|
||||
use PHPUnit\TextUI\Output\DefaultPrinter;
|
||||
use PHPUnit\TextUI\Output\NullPrinter;
|
||||
use PHPUnit\TextUI\Output\TestDox\ResultPrinter as TestDoxResultPrinter;
|
||||
use PHPUnit\TextUI\TestSuiteFilterProcessor;
|
||||
use PHPUnit\Util\ExcludeList;
|
||||
|
||||
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;
|
||||
use function version_compare;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
final class ApplicationForWrapperWorker
|
||||
{
|
||||
private bool $hasBeenBootstrapped = false;
|
||||
private Configuration $configuration;
|
||||
private TestResultCollector $testdoxResultCollector;
|
||||
|
||||
/** @param list<string> $argv */
|
||||
public function __construct(
|
||||
private readonly array $argv,
|
||||
private readonly string $progressFile,
|
||||
private readonly string $unexpectedOutputFile,
|
||||
private readonly string $testresultFile,
|
||||
private readonly ?string $teamcityFile,
|
||||
private readonly ?string $testdoxFile,
|
||||
private readonly bool $testdoxColor,
|
||||
private readonly ?int $testdoxColumns,
|
||||
) {
|
||||
}
|
||||
|
||||
public function runTest(string $testPath): int
|
||||
{
|
||||
$null = strpos($testPath, "\0");
|
||||
$filter = null;
|
||||
if ($null !== false) {
|
||||
$filter = new Factory();
|
||||
$name = substr($testPath, $null + 1);
|
||||
assert($name !== '');
|
||||
if (version_compare(Version::id(), '11.0.0') >= 0) {
|
||||
$filter->addIncludeNameFilter($name);
|
||||
} else {
|
||||
$filter->addNameFilter($name);
|
||||
}
|
||||
|
||||
$testPath = substr($testPath, 0, $null);
|
||||
}
|
||||
|
||||
$this->bootstrap();
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
if (version_compare(Version::id(), '11.0.0') < 0) {
|
||||
if (CodeCoverage::instance()->isActive()) {
|
||||
CodeCoverage::instance()->ignoreLines(
|
||||
(new CodeCoverageMetadataApi())->linesToBeIgnored($testSuite),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
(new TestSuiteFilterProcessor())->process($this->configuration, $testSuite);
|
||||
|
||||
if ($filter !== null) {
|
||||
$testSuite->injectFilter($filter);
|
||||
|
||||
EventFacade::emitter()->testSuiteFiltered(
|
||||
TestSuiteBuilder::from($testSuite),
|
||||
);
|
||||
}
|
||||
|
||||
EventFacade::emitter()->testRunnerExecutionStarted(
|
||||
TestSuiteBuilder::from($testSuite),
|
||||
);
|
||||
|
||||
$testSuite->run();
|
||||
|
||||
return TestResultFacade::result()->wasSuccessfulIgnoringPhpunitWarnings()
|
||||
? RunnerInterface::SUCCESS_EXIT
|
||||
: RunnerInterface::FAILURE_EXIT;
|
||||
}
|
||||
|
||||
private function bootstrap(): void
|
||||
{
|
||||
if ($this->hasBeenBootstrapped) {
|
||||
return;
|
||||
}
|
||||
|
||||
ExcludeList::addDirectory(__DIR__);
|
||||
EventFacade::emitter()->applicationStarted();
|
||||
|
||||
$this->configuration = (new Builder())->build($this->argv);
|
||||
|
||||
(new PhpHandler())->handle($this->configuration->php());
|
||||
|
||||
if ($this->configuration->hasBootstrap()) {
|
||||
$bootstrapFilename = $this->configuration->bootstrap();
|
||||
include_once $bootstrapFilename;
|
||||
EventFacade::emitter()->testRunnerBootstrapFinished($bootstrapFilename);
|
||||
}
|
||||
|
||||
$extensionRequiresCodeCoverageCollection = false;
|
||||
if (! $this->configuration->noExtensions()) {
|
||||
if ($this->configuration->hasPharExtensionDirectory()) {
|
||||
(new PharLoader())->loadPharExtensionsInDirectory(
|
||||
$this->configuration->pharExtensionDirectory(),
|
||||
);
|
||||
}
|
||||
|
||||
$extensionFacade = new ExtensionFacade();
|
||||
$extensionBootstrapper = new ExtensionBootstrapper(
|
||||
$this->configuration,
|
||||
$extensionFacade,
|
||||
);
|
||||
|
||||
foreach ($this->configuration->extensionBootstrappers() as $bootstrapper) {
|
||||
$extensionBootstrapper->bootstrap(
|
||||
$bootstrapper['className'],
|
||||
$bootstrapper['parameters'],
|
||||
);
|
||||
}
|
||||
|
||||
$extensionRequiresCodeCoverageCollection = $extensionFacade->requiresCodeCoverageCollection();
|
||||
}
|
||||
|
||||
CodeCoverage::instance()->init(
|
||||
$this->configuration,
|
||||
CodeCoverageFilterRegistry::instance(),
|
||||
$extensionRequiresCodeCoverageCollection,
|
||||
);
|
||||
|
||||
if ($this->configuration->hasLogfileJunit()) {
|
||||
new JunitXmlLogger(
|
||||
DefaultPrinter::from($this->configuration->logfileJunit()),
|
||||
EventFacade::instance(),
|
||||
);
|
||||
}
|
||||
|
||||
$printer = new ProgressPrinterOutput(
|
||||
DefaultPrinter::from($this->progressFile),
|
||||
DefaultPrinter::from($this->unexpectedOutputFile),
|
||||
);
|
||||
|
||||
new UnexpectedOutputPrinter($printer, EventFacade::instance());
|
||||
new ProgressPrinter(
|
||||
$printer,
|
||||
EventFacade::instance(),
|
||||
false,
|
||||
99999,
|
||||
$this->configuration->source(),
|
||||
);
|
||||
|
||||
if (isset($this->teamcityFile)) {
|
||||
new TeamCityLogger(
|
||||
DefaultPrinter::from($this->teamcityFile),
|
||||
EventFacade::instance(),
|
||||
);
|
||||
}
|
||||
|
||||
if (isset($this->testdoxFile)) {
|
||||
$this->testdoxResultCollector = new TestResultCollector(EventFacade::instance());
|
||||
}
|
||||
|
||||
TestResultFacade::init();
|
||||
EventFacade::instance()->seal();
|
||||
EventFacade::emitter()->testRunnerStarted();
|
||||
|
||||
if ($this->configuration->executionOrder() === TestSuiteSorter::ORDER_RANDOMIZED) {
|
||||
mt_srand($this->configuration->randomOrderSeed());
|
||||
}
|
||||
|
||||
$this->hasBeenBootstrapped = true;
|
||||
}
|
||||
|
||||
public function end(): void
|
||||
{
|
||||
if (! $this->hasBeenBootstrapped) {
|
||||
return;
|
||||
}
|
||||
|
||||
EventFacade::emitter()->testRunnerExecutionFinished();
|
||||
EventFacade::emitter()->testRunnerFinished();
|
||||
|
||||
CodeCoverage::instance()->generateReports(new NullPrinter(), $this->configuration);
|
||||
|
||||
$result = TestResultFacade::result();
|
||||
if (isset($this->testdoxResultCollector)) {
|
||||
assert(isset($this->testdoxFile));
|
||||
assert(isset($this->testdoxColumns));
|
||||
|
||||
(new TestDoxResultPrinter(DefaultPrinter::from($this->testdoxFile), $this->testdoxColor, $this->testdoxColumns))->print(
|
||||
$this->testdoxResultCollector->testMethodsGroupedByClass(),
|
||||
);
|
||||
}
|
||||
|
||||
file_put_contents($this->testresultFile, serialize($result));
|
||||
|
||||
EventFacade::emitter()->applicationFinished(0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ParaTest\WrapperRunner;
|
||||
|
||||
use PHPUnit\TextUI\Output\Printer;
|
||||
|
||||
use function preg_match;
|
||||
|
||||
/** @internal */
|
||||
final class ProgressPrinterOutput implements Printer
|
||||
{
|
||||
public function __construct(
|
||||
private readonly Printer $progressPrinter,
|
||||
private readonly Printer $outputPrinter,
|
||||
) {
|
||||
}
|
||||
|
||||
public function print(string $buffer): void
|
||||
{
|
||||
// Skip anything in \PHPUnit\TextUI\Output\Default\ProgressPrinter\ProgressPrinter::printProgress except $progress
|
||||
if (
|
||||
$buffer === "\n"
|
||||
|| preg_match('/^ +$/', $buffer) === 1
|
||||
|| preg_match('/^ \\d+ \\/ \\d+ \\(...%\\)$/', $buffer) === 1
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
match ($buffer) {
|
||||
'E', 'F', 'I', 'N', 'D', 'R', 'W', 'S', '.' => $this->progressPrinter->print($buffer),
|
||||
default => $this->outputPrinter->print($buffer),
|
||||
};
|
||||
}
|
||||
|
||||
public function flush(): void
|
||||
{
|
||||
$this->progressPrinter->flush();
|
||||
$this->outputPrinter->flush();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,334 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ParaTest\WrapperRunner;
|
||||
|
||||
use ParaTest\Options;
|
||||
use PHPUnit\Runner\TestSuiteSorter;
|
||||
use PHPUnit\TestRunner\TestResult\TestResult;
|
||||
use PHPUnit\TextUI\Output\Default\ResultPrinter as DefaultResultPrinter;
|
||||
use PHPUnit\TextUI\Output\Printer;
|
||||
use PHPUnit\TextUI\Output\SummaryPrinter;
|
||||
use PHPUnit\Util\Color;
|
||||
use SebastianBergmann\CodeCoverage\Driver\Selector;
|
||||
use SebastianBergmann\CodeCoverage\Filter;
|
||||
use SebastianBergmann\Timer\ResourceUsageFormatter;
|
||||
use SplFileInfo;
|
||||
use Symfony\Component\Console\Formatter\OutputFormatter;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
use function assert;
|
||||
use function fclose;
|
||||
use function feof;
|
||||
use function floor;
|
||||
use function fopen;
|
||||
use function fread;
|
||||
use function fseek;
|
||||
use function ftell;
|
||||
use function fwrite;
|
||||
use function sprintf;
|
||||
use function str_repeat;
|
||||
use function strlen;
|
||||
|
||||
use const DIRECTORY_SEPARATOR;
|
||||
use const PHP_EOL;
|
||||
use const PHP_VERSION;
|
||||
|
||||
/** @internal */
|
||||
final class ResultPrinter
|
||||
{
|
||||
public readonly Printer $printer;
|
||||
|
||||
private int $numTestsWidth = 0;
|
||||
private int $maxColumn = 0;
|
||||
private int $totalCases = 0;
|
||||
private int $column = 0;
|
||||
private int $casesProcessed = 0;
|
||||
private int $numberOfColumns;
|
||||
/** @var resource|null */
|
||||
private $teamcityLogFileHandle;
|
||||
/** @var array<non-empty-string, int> */
|
||||
private array $tailPositions;
|
||||
|
||||
public function __construct(
|
||||
private readonly OutputInterface $output,
|
||||
private readonly Options $options
|
||||
) {
|
||||
$this->printer = new class ($this->output) implements Printer {
|
||||
public function __construct(
|
||||
private readonly OutputInterface $output,
|
||||
) {
|
||||
}
|
||||
|
||||
public function print(string $buffer): void
|
||||
{
|
||||
$this->output->write(OutputFormatter::escape($buffer));
|
||||
}
|
||||
|
||||
public function flush(): void
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
$this->numberOfColumns = $this->options->configuration->columns();
|
||||
|
||||
if (! $this->options->configuration->hasLogfileTeamcity()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$teamcityLogFileHandle = fopen($this->options->configuration->logfileTeamcity(), 'ab+');
|
||||
assert($teamcityLogFileHandle !== false);
|
||||
$this->teamcityLogFileHandle = $teamcityLogFileHandle;
|
||||
}
|
||||
|
||||
public function setTestCount(int $testCount): void
|
||||
{
|
||||
$this->totalCases = $testCount;
|
||||
}
|
||||
|
||||
public function start(): void
|
||||
{
|
||||
$this->numTestsWidth = strlen((string) $this->totalCases);
|
||||
$this->maxColumn = $this->numberOfColumns
|
||||
+ (DIRECTORY_SEPARATOR === '\\' ? -1 : 0) // fix windows blank lines
|
||||
- strlen($this->getProgress());
|
||||
|
||||
// @see \PHPUnit\TextUI\TestRunner::writeMessage()
|
||||
$output = $this->output;
|
||||
$write = static function (string $type, string $message) use ($output): void {
|
||||
$output->write(sprintf("%-15s%s\n", $type . ':', $message));
|
||||
};
|
||||
|
||||
// @see \PHPUnit\TextUI\Application::writeRuntimeInformation()
|
||||
$write('Processes', (string) $this->options->processes);
|
||||
|
||||
$runtime = 'PHP ' . PHP_VERSION;
|
||||
|
||||
if ($this->options->configuration->hasCoverageReport()) {
|
||||
$filter = new Filter();
|
||||
if ($this->options->configuration->pathCoverage()) {
|
||||
$codeCoverageDriver = (new Selector())->forLineAndPathCoverage($filter); // @codeCoverageIgnore
|
||||
} else {
|
||||
$codeCoverageDriver = (new Selector())->forLineCoverage($filter);
|
||||
}
|
||||
|
||||
$runtime .= ' with ' . $codeCoverageDriver->nameAndVersion();
|
||||
}
|
||||
|
||||
$write('Runtime', $runtime);
|
||||
|
||||
if ($this->options->configuration->hasConfigurationFile()) {
|
||||
$write('Configuration', $this->options->configuration->configurationFile());
|
||||
}
|
||||
|
||||
if ($this->options->configuration->executionOrder() === TestSuiteSorter::ORDER_RANDOMIZED) {
|
||||
$write('Random Seed', (string) $this->options->configuration->randomOrderSeed());
|
||||
}
|
||||
|
||||
$output->write("\n");
|
||||
}
|
||||
|
||||
public function printFeedback(
|
||||
SplFileInfo $progressFile,
|
||||
SplFileInfo $outputFile,
|
||||
SplFileInfo|null $teamcityFile
|
||||
): void {
|
||||
if ($this->options->needsTeamcity && $teamcityFile !== null) {
|
||||
$teamcityProgress = $this->tailMultiple([$teamcityFile]);
|
||||
|
||||
if ($this->teamcityLogFileHandle !== null) {
|
||||
fwrite($this->teamcityLogFileHandle, $teamcityProgress);
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->options->configuration->outputIsTeamCity()) {
|
||||
assert(isset($teamcityProgress));
|
||||
$this->output->write($teamcityProgress);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->options->configuration->noProgress()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$unexpectedOutput = $this->tail($outputFile);
|
||||
if ($unexpectedOutput !== '') {
|
||||
$this->output->write($unexpectedOutput);
|
||||
}
|
||||
|
||||
$feedbackItems = $this->tail($progressFile);
|
||||
if ($feedbackItems === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
$actualTestCount = strlen($feedbackItems);
|
||||
for ($index = 0; $index < $actualTestCount; ++$index) {
|
||||
$this->printFeedbackItem($feedbackItems[$index]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<SplFileInfo> $teamcityFiles
|
||||
* @param list<SplFileInfo> $testdoxFiles
|
||||
*/
|
||||
public function printResults(TestResult $testResult, array $teamcityFiles, array $testdoxFiles): void
|
||||
{
|
||||
if ($this->options->needsTeamcity) {
|
||||
$teamcityProgress = $this->tailMultiple($teamcityFiles);
|
||||
|
||||
if ($this->teamcityLogFileHandle !== null) {
|
||||
fwrite($this->teamcityLogFileHandle, $teamcityProgress);
|
||||
$resource = $this->teamcityLogFileHandle;
|
||||
$this->teamcityLogFileHandle = null;
|
||||
fclose($resource);
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->options->configuration->outputIsTeamCity()) {
|
||||
assert(isset($teamcityProgress));
|
||||
$this->output->write($teamcityProgress);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->printer->print(PHP_EOL . (new ResourceUsageFormatter())->resourceUsageSinceStartOfRequest() . PHP_EOL . PHP_EOL);
|
||||
|
||||
$defaultResultPrinter = new DefaultResultPrinter(
|
||||
$this->printer,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
$this->options->configuration->displayDetailsOnIncompleteTests(),
|
||||
$this->options->configuration->displayDetailsOnSkippedTests(),
|
||||
$this->options->configuration->displayDetailsOnTestsThatTriggerDeprecations(),
|
||||
$this->options->configuration->displayDetailsOnTestsThatTriggerErrors(),
|
||||
$this->options->configuration->displayDetailsOnTestsThatTriggerNotices(),
|
||||
$this->options->configuration->displayDetailsOnTestsThatTriggerWarnings(),
|
||||
false,
|
||||
);
|
||||
|
||||
if ($this->options->configuration->outputIsTestDox()) {
|
||||
$this->output->write($this->tailMultiple($testdoxFiles));
|
||||
|
||||
$defaultResultPrinter = new DefaultResultPrinter(
|
||||
$this->printer,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
$defaultResultPrinter->print($testResult);
|
||||
|
||||
(new SummaryPrinter(
|
||||
$this->printer,
|
||||
$this->options->configuration->colors(),
|
||||
))->print($testResult);
|
||||
}
|
||||
|
||||
private function printFeedbackItem(string $item): void
|
||||
{
|
||||
$this->printFeedbackItemColor($item);
|
||||
++$this->column;
|
||||
++$this->casesProcessed;
|
||||
if ($this->column !== $this->maxColumn && $this->casesProcessed < $this->totalCases) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
$this->casesProcessed > 0
|
||||
&& $this->casesProcessed === $this->totalCases
|
||||
&& ($pad = $this->maxColumn - $this->column) > 0
|
||||
) {
|
||||
$this->output->write(str_repeat(' ', $pad));
|
||||
}
|
||||
|
||||
$this->output->write($this->getProgress() . "\n");
|
||||
$this->column = 0;
|
||||
}
|
||||
|
||||
private function printFeedbackItemColor(string $item): void
|
||||
{
|
||||
$buffer = match ($item) {
|
||||
'E' => $this->colorizeTextBox('fg-red, bold', $item),
|
||||
'F' => $this->colorizeTextBox('bg-red, fg-white', $item),
|
||||
'I', 'N', 'D', 'R', 'W' => $this->colorizeTextBox('fg-yellow, bold', $item),
|
||||
'S' => $this->colorizeTextBox('fg-cyan, bold', $item),
|
||||
'.' => $item,
|
||||
};
|
||||
$this->output->write($buffer);
|
||||
}
|
||||
|
||||
private function getProgress(): string
|
||||
{
|
||||
return sprintf(
|
||||
' %' . $this->numTestsWidth . 'd / %' . $this->numTestsWidth . 'd (%3s%%)',
|
||||
$this->casesProcessed,
|
||||
$this->totalCases,
|
||||
floor(($this->totalCases > 0 ? $this->casesProcessed / $this->totalCases : 0) * 100),
|
||||
);
|
||||
}
|
||||
|
||||
private function colorizeTextBox(string $color, string $buffer): string
|
||||
{
|
||||
if (! $this->options->configuration->colors()) {
|
||||
return $buffer;
|
||||
}
|
||||
|
||||
return Color::colorizeTextBox($color, $buffer);
|
||||
}
|
||||
|
||||
/** @param list<SplFileInfo> $files */
|
||||
private function tailMultiple(array $files): string
|
||||
{
|
||||
$content = '';
|
||||
foreach ($files as $file) {
|
||||
if (! $file->isFile()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$content .= $this->tail($file);
|
||||
}
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
private function tail(SplFileInfo $file): string
|
||||
{
|
||||
$path = $file->getPathname();
|
||||
assert($path !== '');
|
||||
$handle = fopen($path, 'r');
|
||||
assert($handle !== false);
|
||||
$fseek = fseek($handle, $this->tailPositions[$path] ?? 0);
|
||||
assert($fseek === 0);
|
||||
|
||||
$contents = '';
|
||||
while (! feof($handle)) {
|
||||
$fread = fread($handle, 8192);
|
||||
assert($fread !== false);
|
||||
$contents .= $fread;
|
||||
}
|
||||
|
||||
$ftell = ftell($handle);
|
||||
assert($ftell !== false);
|
||||
$this->tailPositions[$path] = $ftell;
|
||||
fclose($handle);
|
||||
|
||||
return $contents;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ParaTest\WrapperRunner;
|
||||
|
||||
use Generator;
|
||||
use ParaTest\Options;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use PHPUnit\Framework\TestSuite;
|
||||
use PHPUnit\Runner\PhptTestCase;
|
||||
use PHPUnit\Runner\ResultCache\NullResultCache;
|
||||
use PHPUnit\Runner\TestSuiteSorter;
|
||||
use PHPUnit\TextUI\Command\Result;
|
||||
use PHPUnit\TextUI\Command\WarmCodeCoverageCacheCommand;
|
||||
use PHPUnit\TextUI\Configuration\CodeCoverageFilterRegistry;
|
||||
use PHPUnit\TextUI\Configuration\PhpHandler;
|
||||
use PHPUnit\TextUI\Configuration\TestSuiteBuilder;
|
||||
use PHPUnit\TextUI\TestSuiteFilterProcessor;
|
||||
use ReflectionClass;
|
||||
use ReflectionProperty;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
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;
|
||||
use function ob_start;
|
||||
use function sprintf;
|
||||
use function str_starts_with;
|
||||
use function strlen;
|
||||
use function substr;
|
||||
|
||||
/** @internal */
|
||||
final class SuiteLoader
|
||||
{
|
||||
public readonly int $testCount;
|
||||
/** @var list<non-empty-string> */
|
||||
public readonly array $tests;
|
||||
|
||||
public function __construct(
|
||||
private readonly Options $options,
|
||||
OutputInterface $output,
|
||||
CodeCoverageFilterRegistry $codeCoverageFilterRegistry,
|
||||
) {
|
||||
(new PhpHandler())->handle($this->options->configuration->php());
|
||||
|
||||
if ($this->options->configuration->hasBootstrap()) {
|
||||
include_once $this->options->configuration->bootstrap();
|
||||
}
|
||||
|
||||
$testSuite = (new TestSuiteBuilder())->build($this->options->configuration);
|
||||
|
||||
if ($this->options->configuration->executionOrder() === TestSuiteSorter::ORDER_RANDOMIZED) {
|
||||
mt_srand($this->options->configuration->randomOrderSeed());
|
||||
}
|
||||
|
||||
if (
|
||||
$this->options->configuration->executionOrder() !== TestSuiteSorter::ORDER_DEFAULT ||
|
||||
$this->options->configuration->executionOrderDefects() !== TestSuiteSorter::ORDER_DEFAULT ||
|
||||
$this->options->configuration->resolveDependencies()
|
||||
) {
|
||||
(new TestSuiteSorter(new NullResultCache()))->reorderTestsInSuite(
|
||||
$testSuite,
|
||||
$this->options->configuration->executionOrder(),
|
||||
$this->options->configuration->resolveDependencies(),
|
||||
$this->options->configuration->executionOrderDefects(),
|
||||
);
|
||||
}
|
||||
|
||||
(new TestSuiteFilterProcessor())->process($this->options->configuration, $testSuite);
|
||||
|
||||
$this->testCount = count($testSuite);
|
||||
|
||||
$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 ($this->options->functional) {
|
||||
$name = sprintf('/%s\s.*%s.*$/', $name, $dataName);
|
||||
} else {
|
||||
if (is_int($dataName)) {
|
||||
$name .= '#' . $dataName;
|
||||
} else {
|
||||
$name .= '@' . $dataName;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$name = sprintf('/%s$/', $name);
|
||||
}
|
||||
|
||||
$tests[] = "$file\0$name";
|
||||
}
|
||||
}
|
||||
|
||||
$this->tests = $this->options->functional
|
||||
? $tests
|
||||
: array_keys($files);
|
||||
|
||||
if (! $this->options->configuration->hasCoverageReport()) {
|
||||
return;
|
||||
}
|
||||
|
||||
ob_start();
|
||||
$result = (new WarmCodeCoverageCacheCommand(
|
||||
$this->options->configuration,
|
||||
$codeCoverageFilterRegistry,
|
||||
))->execute();
|
||||
$ob_get_clean = ob_get_clean();
|
||||
assert($ob_get_clean !== false);
|
||||
$output->write($ob_get_clean);
|
||||
$output->write($result->output());
|
||||
if ($result->shellExitCode() !== Result::SUCCESS) {
|
||||
exit($result->shellExitCode());
|
||||
}
|
||||
}
|
||||
|
||||
/** @return Generator<non-empty-string, (PhptTestCase|TestCase)> */
|
||||
private function loadFiles(TestSuite $testSuite): Generator
|
||||
{
|
||||
foreach ($testSuite as $test) {
|
||||
if ($test instanceof TestSuite) {
|
||||
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);
|
||||
|
||||
yield $filename => $test;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($test instanceof TestCase) {
|
||||
$refClass = new ReflectionClass($test);
|
||||
$filename = $refClass->getFileName();
|
||||
assert(is_string($filename) && $filename !== '');
|
||||
$filename = $this->stripCwd($filename);
|
||||
|
||||
yield $filename => $test;
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param non-empty-string $filename
|
||||
*
|
||||
* @return non-empty-string
|
||||
*/
|
||||
private function stripCwd(string $filename): string
|
||||
{
|
||||
if (! str_starts_with($filename, $this->options->cwd)) {
|
||||
return $filename;
|
||||
}
|
||||
|
||||
$substr = substr($filename, 1 + strlen($this->options->cwd));
|
||||
assert($substr !== '');
|
||||
|
||||
return $substr;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ParaTest\WrapperRunner;
|
||||
|
||||
use RuntimeException;
|
||||
use Symfony\Component\Process\Process;
|
||||
use Throwable;
|
||||
|
||||
use function escapeshellarg;
|
||||
use function sprintf;
|
||||
|
||||
/** @internal */
|
||||
final class WorkerCrashedException extends RuntimeException
|
||||
{
|
||||
public static function fromProcess(Process $process, string $test, ?Throwable $previousException = null): self
|
||||
{
|
||||
$envs = '';
|
||||
foreach ($process->getEnv() as $key => $value) {
|
||||
$envs .= sprintf('%s=%s ', $key, escapeshellarg((string) $value));
|
||||
}
|
||||
|
||||
$error = sprintf(
|
||||
'The test "%s%s" failed.' . "\n\nExit Code: %s(%s)\n\nWorking directory: %s",
|
||||
$envs,
|
||||
$test,
|
||||
(string) $process->getExitCode(),
|
||||
(string) $process->getExitCodeText(),
|
||||
(string) $process->getWorkingDirectory(),
|
||||
);
|
||||
|
||||
if (! $process->isOutputDisabled()) {
|
||||
$error .= sprintf(
|
||||
"\n\nOutput:\n================\n%s\n\nError Output:\n================\n%s",
|
||||
$process->getOutput(),
|
||||
$process->getErrorOutput(),
|
||||
);
|
||||
}
|
||||
|
||||
return new self($error, 0, $previousException);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,365 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ParaTest\WrapperRunner;
|
||||
|
||||
use ParaTest\Coverage\CoverageMerger;
|
||||
use ParaTest\JUnit\LogMerger;
|
||||
use ParaTest\JUnit\Writer;
|
||||
use ParaTest\Options;
|
||||
use ParaTest\RunnerInterface;
|
||||
use PHPUnit\Event\Facade as EventFacade;
|
||||
use PHPUnit\Runner\CodeCoverage;
|
||||
use PHPUnit\TestRunner\TestResult\Facade as TestResultFacade;
|
||||
use PHPUnit\TestRunner\TestResult\TestResult;
|
||||
use PHPUnit\TextUI\Configuration\CodeCoverageFilterRegistry;
|
||||
use PHPUnit\TextUI\ShellExitCodeCalculator;
|
||||
use PHPUnit\Util\ExcludeList;
|
||||
use SplFileInfo;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Process\PhpExecutableFinder;
|
||||
|
||||
use function array_merge;
|
||||
use function array_merge_recursive;
|
||||
use function array_shift;
|
||||
use function assert;
|
||||
use function count;
|
||||
use function dirname;
|
||||
use function file_get_contents;
|
||||
use function max;
|
||||
use function realpath;
|
||||
use function unlink;
|
||||
use function unserialize;
|
||||
use function usleep;
|
||||
|
||||
use const DIRECTORY_SEPARATOR;
|
||||
|
||||
/** @internal */
|
||||
final class WrapperRunner implements RunnerInterface
|
||||
{
|
||||
private const CYCLE_SLEEP = 10000;
|
||||
private readonly ResultPrinter $printer;
|
||||
|
||||
/** @var list<non-empty-string> */
|
||||
private array $pending = [];
|
||||
private int $exitcode = -1;
|
||||
/** @var array<positive-int,WrapperWorker> */
|
||||
private array $workers = [];
|
||||
/** @var array<int,int> */
|
||||
private array $batches = [];
|
||||
|
||||
/** @var list<SplFileInfo> */
|
||||
private array $statusFiles = [];
|
||||
/** @var list<SplFileInfo> */
|
||||
private array $progressFiles = [];
|
||||
/** @var list<SplFileInfo> */
|
||||
private array $unexpectedOutputFiles = [];
|
||||
/** @var list<SplFileInfo> */
|
||||
private array $testresultFiles = [];
|
||||
/** @var list<SplFileInfo> */
|
||||
private array $coverageFiles = [];
|
||||
/** @var list<SplFileInfo> */
|
||||
private array $junitFiles = [];
|
||||
/** @var list<SplFileInfo> */
|
||||
private array $teamcityFiles = [];
|
||||
/** @var list<SplFileInfo> */
|
||||
private array $testdoxFiles = [];
|
||||
/** @var non-empty-string[] */
|
||||
private readonly array $parameters;
|
||||
private CodeCoverageFilterRegistry $codeCoverageFilterRegistry;
|
||||
|
||||
public function __construct(
|
||||
private readonly Options $options,
|
||||
private readonly OutputInterface $output
|
||||
) {
|
||||
$this->printer = new ResultPrinter($output, $options);
|
||||
|
||||
$wrapper = realpath(
|
||||
dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'bin' . DIRECTORY_SEPARATOR . 'phpunit-wrapper.php',
|
||||
);
|
||||
assert($wrapper !== false);
|
||||
$phpFinder = new PhpExecutableFinder();
|
||||
$phpBin = $phpFinder->find(false);
|
||||
assert($phpBin !== false);
|
||||
$parameters = [$phpBin];
|
||||
$parameters = array_merge($parameters, $phpFinder->findArguments());
|
||||
|
||||
if ($options->passthruPhp !== null) {
|
||||
$parameters = array_merge($parameters, $options->passthruPhp);
|
||||
}
|
||||
|
||||
$parameters[] = $wrapper;
|
||||
|
||||
$this->parameters = $parameters;
|
||||
$this->codeCoverageFilterRegistry = new CodeCoverageFilterRegistry();
|
||||
}
|
||||
|
||||
public function run(): int
|
||||
{
|
||||
$directory = dirname(__DIR__);
|
||||
assert($directory !== '');
|
||||
ExcludeList::addDirectory($directory);
|
||||
TestResultFacade::init();
|
||||
EventFacade::instance()->seal();
|
||||
$suiteLoader = new SuiteLoader(
|
||||
$this->options,
|
||||
$this->output,
|
||||
$this->codeCoverageFilterRegistry,
|
||||
);
|
||||
$result = TestResultFacade::result();
|
||||
|
||||
$this->pending = $suiteLoader->tests;
|
||||
$this->printer->setTestCount($suiteLoader->testCount);
|
||||
$this->printer->start();
|
||||
$this->startWorkers();
|
||||
$this->assignAllPendingTests();
|
||||
$this->waitForAllToFinish();
|
||||
|
||||
return $this->complete($result);
|
||||
}
|
||||
|
||||
private function startWorkers(): void
|
||||
{
|
||||
for ($token = 1; $token <= $this->options->processes; ++$token) {
|
||||
$this->startWorker($token);
|
||||
}
|
||||
}
|
||||
|
||||
private function assignAllPendingTests(): void
|
||||
{
|
||||
$batchSize = $this->options->maxBatchSize;
|
||||
|
||||
while (count($this->pending) > 0 && count($this->workers) > 0) {
|
||||
foreach ($this->workers as $token => $worker) {
|
||||
if (! $worker->isRunning()) {
|
||||
throw $worker->getWorkerCrashedException();
|
||||
}
|
||||
|
||||
if (! $worker->isFree()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->flushWorker($worker);
|
||||
|
||||
if ($batchSize !== 0 && $this->batches[$token] === $batchSize) {
|
||||
$this->destroyWorker($token);
|
||||
$worker = $this->startWorker($token);
|
||||
}
|
||||
|
||||
if (
|
||||
$this->exitcode > 0
|
||||
&& $this->options->configuration->stopOnFailure()
|
||||
) {
|
||||
$this->pending = [];
|
||||
} elseif (($pending = array_shift($this->pending)) !== null) {
|
||||
$worker->assign($pending);
|
||||
$this->batches[$token]++;
|
||||
}
|
||||
}
|
||||
|
||||
usleep(self::CYCLE_SLEEP);
|
||||
}
|
||||
}
|
||||
|
||||
private function flushWorker(WrapperWorker $worker): void
|
||||
{
|
||||
$this->exitcode = max($this->exitcode, $worker->getExitCode());
|
||||
$this->printer->printFeedback(
|
||||
$worker->progressFile,
|
||||
$worker->unexpectedOutputFile,
|
||||
$worker->teamcityFile ?? null,
|
||||
);
|
||||
$worker->reset();
|
||||
}
|
||||
|
||||
private function waitForAllToFinish(): void
|
||||
{
|
||||
$stopped = [];
|
||||
while (count($this->workers) > 0) {
|
||||
foreach ($this->workers as $index => $worker) {
|
||||
if ($worker->isRunning()) {
|
||||
if (! isset($stopped[$index]) && $worker->isFree()) {
|
||||
$worker->stop();
|
||||
$stopped[$index] = true;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (! $worker->isFree()) {
|
||||
throw $worker->getWorkerCrashedException();
|
||||
}
|
||||
|
||||
$this->flushWorker($worker);
|
||||
unset($this->workers[$index]);
|
||||
}
|
||||
|
||||
usleep(self::CYCLE_SLEEP);
|
||||
}
|
||||
}
|
||||
|
||||
/** @param positive-int $token */
|
||||
private function startWorker(int $token): WrapperWorker
|
||||
{
|
||||
$worker = new WrapperWorker(
|
||||
$this->output,
|
||||
$this->options,
|
||||
$this->parameters,
|
||||
$token,
|
||||
);
|
||||
$worker->start();
|
||||
$this->batches[$token] = 0;
|
||||
|
||||
$this->statusFiles[] = $worker->statusFile;
|
||||
$this->progressFiles[] = $worker->progressFile;
|
||||
$this->unexpectedOutputFiles[] = $worker->unexpectedOutputFile;
|
||||
$this->testresultFiles[] = $worker->testresultFile;
|
||||
|
||||
if (isset($worker->junitFile)) {
|
||||
$this->junitFiles[] = $worker->junitFile;
|
||||
}
|
||||
|
||||
if (isset($worker->coverageFile)) {
|
||||
$this->coverageFiles[] = $worker->coverageFile;
|
||||
}
|
||||
|
||||
if (isset($worker->teamcityFile)) {
|
||||
$this->teamcityFiles[] = $worker->teamcityFile;
|
||||
}
|
||||
|
||||
if (isset($worker->testdoxFile)) {
|
||||
$this->testdoxFiles[] = $worker->testdoxFile;
|
||||
}
|
||||
|
||||
return $this->workers[$token] = $worker;
|
||||
}
|
||||
|
||||
private function destroyWorker(int $token): void
|
||||
{
|
||||
$this->workers[$token]->stop();
|
||||
// We need to wait for ApplicationForWrapperWorker::end to end
|
||||
while ($this->workers[$token]->isRunning()) {
|
||||
usleep(self::CYCLE_SLEEP);
|
||||
}
|
||||
|
||||
unset($this->workers[$token]);
|
||||
}
|
||||
|
||||
private function complete(TestResult $testResultSum): int
|
||||
{
|
||||
foreach ($this->testresultFiles as $testresultFile) {
|
||||
if (! $testresultFile->isFile()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$contents = file_get_contents($testresultFile->getPathname());
|
||||
assert($contents !== false);
|
||||
$testResult = unserialize($contents);
|
||||
assert($testResult instanceof TestResult);
|
||||
|
||||
$testResultSum = new TestResult(
|
||||
(int) $testResultSum->hasTests() + (int) $testResult->hasTests(),
|
||||
$testResultSum->numberOfTestsRun() + $testResult->numberOfTestsRun(),
|
||||
$testResultSum->numberOfAssertions() + $testResult->numberOfAssertions(),
|
||||
array_merge_recursive($testResultSum->testErroredEvents(), $testResult->testErroredEvents()),
|
||||
array_merge_recursive($testResultSum->testFailedEvents(), $testResult->testFailedEvents()),
|
||||
array_merge_recursive($testResultSum->testConsideredRiskyEvents(), $testResult->testConsideredRiskyEvents()),
|
||||
array_merge_recursive($testResultSum->testSuiteSkippedEvents(), $testResult->testSuiteSkippedEvents()),
|
||||
array_merge_recursive($testResultSum->testSkippedEvents(), $testResult->testSkippedEvents()),
|
||||
array_merge_recursive($testResultSum->testMarkedIncompleteEvents(), $testResult->testMarkedIncompleteEvents()),
|
||||
array_merge_recursive($testResultSum->testTriggeredPhpunitDeprecationEvents(), $testResult->testTriggeredPhpunitDeprecationEvents()),
|
||||
array_merge_recursive($testResultSum->testTriggeredPhpunitErrorEvents(), $testResult->testTriggeredPhpunitErrorEvents()),
|
||||
array_merge_recursive($testResultSum->testTriggeredPhpunitWarningEvents(), $testResult->testTriggeredPhpunitWarningEvents()),
|
||||
array_merge_recursive($testResultSum->testRunnerTriggeredDeprecationEvents(), $testResult->testRunnerTriggeredDeprecationEvents()),
|
||||
array_merge_recursive($testResultSum->testRunnerTriggeredWarningEvents(), $testResult->testRunnerTriggeredWarningEvents()),
|
||||
array_merge_recursive($testResultSum->errors(), $testResult->errors()),
|
||||
array_merge_recursive($testResultSum->deprecations(), $testResult->deprecations()),
|
||||
array_merge_recursive($testResultSum->notices(), $testResult->notices()),
|
||||
array_merge_recursive($testResultSum->warnings(), $testResult->warnings()),
|
||||
array_merge_recursive($testResultSum->phpDeprecations(), $testResult->phpDeprecations()),
|
||||
array_merge_recursive($testResultSum->phpNotices(), $testResult->phpNotices()),
|
||||
array_merge_recursive($testResultSum->phpWarnings(), $testResult->phpWarnings()),
|
||||
$testResultSum->numberOfIssuesIgnoredByBaseline() + $testResult->numberOfIssuesIgnoredByBaseline(),
|
||||
);
|
||||
}
|
||||
|
||||
$this->printer->printResults(
|
||||
$testResultSum,
|
||||
$this->teamcityFiles,
|
||||
$this->testdoxFiles,
|
||||
);
|
||||
$this->generateCodeCoverageReports();
|
||||
$this->generateLogs();
|
||||
|
||||
$exitcode = (new ShellExitCodeCalculator())->calculate(
|
||||
$this->options->configuration->failOnDeprecation(),
|
||||
$this->options->configuration->failOnEmptyTestSuite(),
|
||||
$this->options->configuration->failOnIncomplete(),
|
||||
$this->options->configuration->failOnNotice(),
|
||||
$this->options->configuration->failOnRisky(),
|
||||
$this->options->configuration->failOnSkipped(),
|
||||
$this->options->configuration->failOnWarning(),
|
||||
$testResultSum,
|
||||
);
|
||||
|
||||
$this->clearFiles($this->statusFiles);
|
||||
$this->clearFiles($this->progressFiles);
|
||||
$this->clearFiles($this->unexpectedOutputFiles);
|
||||
$this->clearFiles($this->testresultFiles);
|
||||
$this->clearFiles($this->coverageFiles);
|
||||
$this->clearFiles($this->junitFiles);
|
||||
$this->clearFiles($this->teamcityFiles);
|
||||
$this->clearFiles($this->testdoxFiles);
|
||||
|
||||
return $exitcode;
|
||||
}
|
||||
|
||||
protected function generateCodeCoverageReports(): void
|
||||
{
|
||||
if ($this->coverageFiles === []) {
|
||||
return;
|
||||
}
|
||||
|
||||
$coverageManager = new CodeCoverage();
|
||||
$coverageManager->init(
|
||||
$this->options->configuration,
|
||||
$this->codeCoverageFilterRegistry,
|
||||
false,
|
||||
);
|
||||
$coverageMerger = new CoverageMerger($coverageManager->codeCoverage());
|
||||
foreach ($this->coverageFiles as $coverageFile) {
|
||||
$coverageMerger->addCoverageFromFile($coverageFile);
|
||||
}
|
||||
|
||||
$coverageManager->generateReports(
|
||||
$this->printer->printer,
|
||||
$this->options->configuration,
|
||||
);
|
||||
}
|
||||
|
||||
private function generateLogs(): void
|
||||
{
|
||||
if ($this->junitFiles === []) {
|
||||
return;
|
||||
}
|
||||
|
||||
$testSuite = (new LogMerger())->merge($this->junitFiles);
|
||||
(new Writer())->write(
|
||||
$testSuite,
|
||||
$this->options->configuration->logfileJunit(),
|
||||
);
|
||||
}
|
||||
|
||||
/** @param list<SplFileInfo> $files */
|
||||
private function clearFiles(array $files): void
|
||||
{
|
||||
foreach ($files as $file) {
|
||||
if (! $file->isFile()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
unlink($file->getPathname());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ParaTest\WrapperRunner;
|
||||
|
||||
use ParaTest\Options;
|
||||
use SplFileInfo;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Process\InputStream;
|
||||
use Symfony\Component\Process\Process;
|
||||
use Throwable;
|
||||
|
||||
use function array_map;
|
||||
use function assert;
|
||||
use function clearstatcache;
|
||||
use function file_get_contents;
|
||||
use function filesize;
|
||||
use function implode;
|
||||
use function is_string;
|
||||
use function serialize;
|
||||
use function sprintf;
|
||||
use function touch;
|
||||
use function uniqid;
|
||||
|
||||
use const DIRECTORY_SEPARATOR;
|
||||
|
||||
/** @internal */
|
||||
final class WrapperWorker
|
||||
{
|
||||
public const COMMAND_EXIT = "EXIT\n";
|
||||
|
||||
public readonly SplFileInfo $statusFile;
|
||||
public readonly SplFileInfo $progressFile;
|
||||
public readonly SplFileInfo $unexpectedOutputFile;
|
||||
public readonly SplFileInfo $testresultFile;
|
||||
public readonly SplFileInfo $junitFile;
|
||||
public readonly SplFileInfo $coverageFile;
|
||||
public readonly SplFileInfo $teamcityFile;
|
||||
|
||||
public readonly SplFileInfo $testdoxFile;
|
||||
private ?string $currentlyExecuting = null;
|
||||
private Process $process;
|
||||
private int $inExecution = 0;
|
||||
private InputStream $input;
|
||||
private int $exitCode = -1;
|
||||
|
||||
/** @param non-empty-string[] $parameters */
|
||||
public function __construct(
|
||||
private readonly OutputInterface $output,
|
||||
private readonly Options $options,
|
||||
array $parameters,
|
||||
private readonly int $token
|
||||
) {
|
||||
$commonTmpFilePath = sprintf(
|
||||
'%s%sworker_%02s_stdout_%s_',
|
||||
$options->tmpDir,
|
||||
DIRECTORY_SEPARATOR,
|
||||
$token,
|
||||
uniqid(),
|
||||
);
|
||||
$this->statusFile = new SplFileInfo($commonTmpFilePath . 'status');
|
||||
touch($this->statusFile->getPathname());
|
||||
$this->progressFile = new SplFileInfo($commonTmpFilePath . 'progress');
|
||||
touch($this->progressFile->getPathname());
|
||||
$this->unexpectedOutputFile = new SplFileInfo($commonTmpFilePath . 'unexpected_output');
|
||||
touch($this->unexpectedOutputFile->getPathname());
|
||||
$this->testresultFile = new SplFileInfo($commonTmpFilePath . 'testresult');
|
||||
if ($options->configuration->hasLogfileJunit()) {
|
||||
$this->junitFile = new SplFileInfo($commonTmpFilePath . 'junit');
|
||||
}
|
||||
|
||||
if ($options->configuration->hasCoverageReport()) {
|
||||
$this->coverageFile = new SplFileInfo($commonTmpFilePath . 'coverage');
|
||||
}
|
||||
|
||||
if ($options->needsTeamcity) {
|
||||
$this->teamcityFile = new SplFileInfo($commonTmpFilePath . 'teamcity');
|
||||
}
|
||||
|
||||
if ($options->configuration->outputIsTestDox()) {
|
||||
$this->testdoxFile = new SplFileInfo($commonTmpFilePath . 'testdox');
|
||||
}
|
||||
|
||||
$parameters[] = '--status-file';
|
||||
$parameters[] = $this->statusFile->getPathname();
|
||||
$parameters[] = '--progress-file';
|
||||
$parameters[] = $this->progressFile->getPathname();
|
||||
$parameters[] = '--unexpected-output-file';
|
||||
$parameters[] = $this->unexpectedOutputFile->getPathname();
|
||||
$parameters[] = '--testresult-file';
|
||||
$parameters[] = $this->testresultFile->getPathname();
|
||||
if (isset($this->teamcityFile)) {
|
||||
$parameters[] = '--teamcity-file';
|
||||
$parameters[] = $this->teamcityFile->getPathname();
|
||||
}
|
||||
|
||||
if (isset($this->testdoxFile)) {
|
||||
$parameters[] = '--testdox-file';
|
||||
$parameters[] = $this->testdoxFile->getPathname();
|
||||
if ($options->configuration->colors()) {
|
||||
$parameters[] = '--testdox-color';
|
||||
}
|
||||
|
||||
$parameters[] = '--testdox-columns';
|
||||
$parameters[] = (string) $options->configuration->columns();
|
||||
}
|
||||
|
||||
$phpunitArguments = [$options->phpunit];
|
||||
foreach ($options->phpunitOptions as $key => $value) {
|
||||
if ($options->functional && $key === 'filter') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$phpunitArguments[] = "--{$key}";
|
||||
if ($value === true) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$phpunitArguments[] = $value;
|
||||
}
|
||||
|
||||
$phpunitArguments[] = '--do-not-cache-result';
|
||||
$phpunitArguments[] = '--no-logging';
|
||||
$phpunitArguments[] = '--no-coverage';
|
||||
$phpunitArguments[] = '--no-output';
|
||||
if (isset($this->junitFile)) {
|
||||
$phpunitArguments[] = '--log-junit';
|
||||
$phpunitArguments[] = $this->junitFile->getPathname();
|
||||
}
|
||||
|
||||
if (isset($this->coverageFile)) {
|
||||
$phpunitArguments[] = '--coverage-php';
|
||||
$phpunitArguments[] = $this->coverageFile->getPathname();
|
||||
}
|
||||
|
||||
$parameters[] = '--phpunit-argv';
|
||||
$parameters[] = serialize($phpunitArguments);
|
||||
|
||||
if ($options->verbose) {
|
||||
$output->write(sprintf(
|
||||
"Starting process {$this->token}: %s\n",
|
||||
implode(' ', array_map('\\escapeshellarg', $parameters)),
|
||||
));
|
||||
}
|
||||
|
||||
$this->input = new InputStream();
|
||||
$this->process = new Process(
|
||||
$parameters,
|
||||
$options->cwd,
|
||||
$options->fillEnvWithTokens($token),
|
||||
$this->input,
|
||||
null,
|
||||
);
|
||||
}
|
||||
|
||||
public function start(): void
|
||||
{
|
||||
$this->process->start();
|
||||
}
|
||||
|
||||
public function getWorkerCrashedException(?Throwable $previousException = null): WorkerCrashedException
|
||||
{
|
||||
return WorkerCrashedException::fromProcess(
|
||||
$this->process,
|
||||
$this->currentlyExecuting ?? 'N.A.',
|
||||
$previousException,
|
||||
);
|
||||
}
|
||||
|
||||
public function assign(string $test): void
|
||||
{
|
||||
assert($this->currentlyExecuting === null);
|
||||
|
||||
if ($this->options->verbose) {
|
||||
$this->output->write("Process {$this->token} executing: {$test}\n");
|
||||
}
|
||||
|
||||
$this->input->write($test . "\n");
|
||||
$this->currentlyExecuting = $test;
|
||||
++$this->inExecution;
|
||||
}
|
||||
|
||||
public function reset(): void
|
||||
{
|
||||
$this->currentlyExecuting = null;
|
||||
}
|
||||
|
||||
public function stop(): void
|
||||
{
|
||||
$this->input->write(self::COMMAND_EXIT);
|
||||
}
|
||||
|
||||
public function isFree(): bool
|
||||
{
|
||||
$statusFilepath = $this->statusFile->getPathname();
|
||||
clearstatcache(true, $statusFilepath);
|
||||
|
||||
$isFree = $this->inExecution === filesize($statusFilepath);
|
||||
|
||||
if ($isFree && $this->inExecution > 0) {
|
||||
$exitCodes = file_get_contents($statusFilepath);
|
||||
assert(is_string($exitCodes) && $exitCodes !== '');
|
||||
$this->exitCode = (int) $exitCodes[-1];
|
||||
}
|
||||
|
||||
return $isFree;
|
||||
}
|
||||
|
||||
public function getExitCode(): int
|
||||
{
|
||||
return $this->exitCode;
|
||||
}
|
||||
|
||||
public function isRunning(): bool
|
||||
{
|
||||
return $this->process->isRunning();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user