Skip to content

Resolved test suite run in separated process executed one by one in separated process. #6063

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions phpunit.xml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
<directory suffix=".phpt">tests/end-to-end/mock-objects</directory>
<directory suffix=".phpt">tests/end-to-end/phpt</directory>
<directory suffix=".phpt">tests/end-to-end/regression</directory>
<directory suffix=".phpt">tests/end-to-end/sandbox</directory>
<directory suffix=".phpt">tests/end-to-end/self-direct-indirect</directory>
<directory suffix=".phpt">tests/end-to-end/testdox</directory>

Expand Down
92 changes: 59 additions & 33 deletions src/Framework/TestRunner/templates/class.tpl
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
<?php declare(strict_types=1);
use PHPUnit\Event;
use PHPUnit\Event\Facade;
use PHPUnit\Framework\TestSuite;
use PHPUnit\Runner\CodeCoverage;
use PHPUnit\Runner\ErrorHandler;
use PHPUnit\Runner\TestSuiteLoader;
use PHPUnit\TextUI\Configuration\Registry as ConfigurationRegistry;
use PHPUnit\TextUI\Configuration\CodeCoverageFilterRegistry;
use PHPUnit\TextUI\Configuration\PhpHandler;
use PHPUnit\TextUI\TestSuiteFilterProcessor;
use PHPUnit\TestRunner\TestResult\PassedTests;

// php://stdout does not obey output buffering. Any output would break
Expand All @@ -31,7 +35,7 @@ if ($composerAutoload) {
require $phar;
}

function __phpunit_run_isolated_test()
function __phpunit_run_isolated_class()
{
$dispatcher = Facade::instance()->initForIsolation(
PHPUnit\Event\Telemetry\HRTime::fromSecondsAndNanoseconds(
Expand Down Expand Up @@ -68,52 +72,74 @@ function __phpunit_run_isolated_test()

ErrorHandler::instance()->useDeprecationTriggers($deprecationTriggers);

$test = new {className}('{name}');
ini_set('xdebug.scream', '0');

$test->setData('{dataName}', unserialize('{data}'));
$test->setDependencyInput(unserialize('{dependencyInput}'));
$test->setInIsolation(true);
try {
$testClass = (new TestSuiteLoader)->load('{filename}');
} catch (Exception $e) {
print $e->getMessage() . PHP_EOL;
exit(1);
}

ob_end_clean();
$output = '';
$results = [];

$test->run();
$suite = TestSuite::fromClassReflector($testClass);
$suite->setIsInSeparatedProcess(false);

$output = '';
(new TestSuiteFilterProcessor)->process($configuration, $suite);

if (!$test->expectsOutput()) {
$output = $test->output();
$testSuiteValueObjectForEvents = Event\TestSuite\TestSuiteBuilder::from($suite);

if (!$suite->invokeMethodsBeforeFirstTest(Facade::emitter(), $testSuiteValueObjectForEvents)) {
return;
}

ini_set('xdebug.scream', '0');
$tests = [];
foreach ($suite as $test) {
$tests[] = $test;
}

foreach($tests as $test) {
$test->setRunClassInSeparateProcess(false);
$test->run();

// Not every STDOUT target stream is rewindable
@rewind(STDOUT);
$testOutput = '';

if ($stdout = @stream_get_contents(STDOUT)) {
$output = $stdout . $output;
$streamMetaData = stream_get_meta_data(STDOUT);
if (!$test->expectsOutput()) {
$testOutput = $test->output();
}

// Not every STDOUT target stream is rewindable
@rewind(STDOUT);

if (!empty($streamMetaData['stream_type']) && 'STDIO' === $streamMetaData['stream_type']) {
@ftruncate(STDOUT, 0);
@rewind(STDOUT);
if ($stdout = @stream_get_contents(STDOUT)) {
$testOutput = $stdout . $testOutput;
$streamMetaData = stream_get_meta_data(STDOUT);

if (!empty($streamMetaData['stream_type']) && 'STDIO' === $streamMetaData['stream_type']) {
@ftruncate(STDOUT, 0);
@rewind(STDOUT);
}
}

$results[] = (object)[
'testResult' => $test->result(),
'codeCoverage' => {collectCodeCoverageInformation} ? CodeCoverage::instance()->codeCoverage() : null,
'numAssertions' => $test->numberOfAssertionsPerformed(),
'output' => $testOutput,
'events' => $dispatcher->flush(),
'passedTests' => PassedTests::instance()
];

$output .= $testOutput;
}

$suite->invokeMethodsAfterLastTest(Facade::emitter());

Facade::emitter()->testRunnerFinishedChildProcess($output, '');

file_put_contents(
'{processResultFile}',
serialize(
(object)[
'testResult' => $test->result(),
'codeCoverage' => {collectCodeCoverageInformation} ? CodeCoverage::instance()->codeCoverage() : null,
'numAssertions' => $test->numberOfAssertionsPerformed(),
'output' => $output,
'events' => $dispatcher->flush(),
'passedTests' => PassedTests::instance()
]
)
);
file_put_contents('{processResultFile}', serialize($results));
}

function __phpunit_error_handler($errno, $errstr, $errfile, $errline)
Expand All @@ -136,4 +162,4 @@ if ('{bootstrap}' !== '') {
require_once '{bootstrap}';
}

__phpunit_run_isolated_test();
__phpunit_run_isolated_class();
47 changes: 39 additions & 8 deletions src/Framework/TestSuite.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
use PHPUnit\Metadata\Api\HookMethods;
use PHPUnit\Metadata\Api\Requirements;
use PHPUnit\Metadata\MetadataCollection;
use PHPUnit\Metadata\Parser\Registry as MetadataRegistry;
use PHPUnit\Runner\Exception as RunnerException;
use PHPUnit\Runner\Filter\Factory;
use PHPUnit\Runner\PhptTestCase;
Expand Down Expand Up @@ -81,9 +82,11 @@
/**
* @var ?list<ExecutionOrderDependency>
*/
private ?array $providedTests = null;
private ?Factory $iteratorFilter = null;
private bool $wasRun = false;
private ?array $providedTests = null;
private ?Factory $iteratorFilter = null;
private bool $wasRun = false;
private bool $isInSeparatedProcess = false;
private bool $isTestsInSeparatedProcess = false;

/**
* @param non-empty-string $name
Expand Down Expand Up @@ -118,6 +121,10 @@
);
}

$registry = MetadataRegistry::parser()->forClass($class->name);
$testSuite->isTestsInSeparatedProcess = $registry->isRunTestsInSeparateProcesses()->isNotEmpty();
$testSuite->isInSeparatedProcess = $registry->isRunClassInSeparateProcess()->isNotEmpty() || $testSuite->isTestsInSeparatedProcess;

return $testSuite;
}

Expand Down Expand Up @@ -316,6 +323,16 @@
return $tests;
}

public function isInSeparatedProcess(): bool

Check warning on line 326 in src/Framework/TestSuite.php

View check run for this annotation

Codecov / codecov/patch

src/Framework/TestSuite.php#L326

Added line #L326 was not covered by tests
{
return $this->isInSeparatedProcess;

Check warning on line 328 in src/Framework/TestSuite.php

View check run for this annotation

Codecov / codecov/patch

src/Framework/TestSuite.php#L328

Added line #L328 was not covered by tests
}

public function setIsInSeparatedProcess(bool $isInSeparatedProcess): void

Check warning on line 331 in src/Framework/TestSuite.php

View check run for this annotation

Codecov / codecov/patch

src/Framework/TestSuite.php#L331

Added line #L331 was not covered by tests
{
$this->isInSeparatedProcess = $isInSeparatedProcess;

Check warning on line 333 in src/Framework/TestSuite.php

View check run for this annotation

Codecov / codecov/patch

src/Framework/TestSuite.php#L333

Added line #L333 was not covered by tests
}

/**
* @throws CodeCoverageException
* @throws Event\RuntimeException
Expand Down Expand Up @@ -367,6 +384,20 @@
}

$test->run();

// When all tests are run in a separated process, the primary process loads
// all the test methods. After executing the first test, TestRunner spawns
// a separated process which loads all the tests again.
// Skip primary process tests expect the first which initiates
// the separated process TestSuite.
if ($this->isInSeparatedProcess && !$this->isTestsInSeparatedProcess) {
// TestSuite statuses are returned from the separated process.
// Skipped and incomplete tests should continue processing, otherwise
// only a single test result is outputted to the console.
if ($test->status()->isUnknown()) {
break;
}
}
}

$this->invokeMethodsAfterLastTest($emitter);
Expand Down Expand Up @@ -571,7 +602,7 @@
$reflector = new ReflectionClass($this->name);

return !$reflector->hasMethod($methodName) ||
$reflector->getMethod($methodName)->getDeclaringClass()->getName() === TestCase::class;
$reflector->getMethod($methodName)->getDeclaringClass()->getName() === TestCase::class;
}

/**
Expand Down Expand Up @@ -605,9 +636,9 @@
* @throws Exception
* @throws NoPreviousThrowableException
*/
private function invokeMethodsBeforeFirstTest(Event\Emitter $emitter, Event\TestSuite\TestSuite $testSuiteValueObjectForEvents): bool
public function invokeMethodsBeforeFirstTest(Event\Emitter $emitter, Event\TestSuite\TestSuite $testSuiteValueObjectForEvents): bool
{
if (!$this->isForTestClass()) {
if (!$this->isForTestClass() || $this->isInSeparatedProcess) {
return true;
}

Expand Down Expand Up @@ -678,9 +709,9 @@
return $result;
}

private function invokeMethodsAfterLastTest(Event\Emitter $emitter): void
public function invokeMethodsAfterLastTest(Event\Emitter $emitter): void
{
if (!$this->isForTestClass()) {
if (!$this->isForTestClass() || $this->isInSeparatedProcess) {
return;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php declare(strict_types=1);
/*
* This file is part of PHPUnit.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace PHPUnit\TestFixture;

use function file_get_contents;
use function file_put_contents;
use PHPUnit\Framework\Attributes\RunClassInSeparateProcess;
use PHPUnit\Framework\TestCase;

#[RunClassInSeparateProcess]
final class ClassIsolationBeforeAndAfterClassMethodCallCountTest extends TestCase
{
public const string BEFORE_CALL_COUNT_FILE_PATH = __DIR__ . '/temp/class_before_method_call_count.txt';
public const string AFTER_CALL_COUNT_FILE_PATH = __DIR__ . '/temp/class_after_method_call_count.txt';

public static function setUpBeforeClass(): void
{
$count = (int) (file_get_contents(self::BEFORE_CALL_COUNT_FILE_PATH));
file_put_contents(self::BEFORE_CALL_COUNT_FILE_PATH, ++$count);
}

public static function tearDownAfterClass(): void
{
$count = (int) (file_get_contents(self::AFTER_CALL_COUNT_FILE_PATH));
file_put_contents(self::AFTER_CALL_COUNT_FILE_PATH, ++$count);
}

public function testBeforeAndAfterClassMethodCallCount1(): void
{
$this->assertEquals('1', file_get_contents(self::BEFORE_CALL_COUNT_FILE_PATH), 'before_method_call_count');
$this->assertEquals('0', file_get_contents(self::AFTER_CALL_COUNT_FILE_PATH), 'after_method_call_count');
}

public function testBeforeAndAfterClassMethodCallCount2(): void
{
$this->assertEquals('1', file_get_contents(self::BEFORE_CALL_COUNT_FILE_PATH), 'before_method_call_count');
$this->assertEquals('0', file_get_contents(self::AFTER_CALL_COUNT_FILE_PATH), 'after_method_call_count');
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php declare(strict_types=1);
/*
* This file is part of PHPUnit.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace PHPUnit\TestFixture;

use PHPUnit\Framework\Attributes\RunClassInSeparateProcess;
use PHPUnit\Framework\TestCase;
use function file_get_contents;
use function file_put_contents;

#[RunClassInSeparateProcess]
final class FilterSeparatedProcessTestSuiteTest extends TestCase
{
public const string FILTER_SEPARATED_PROC_TS_COUNT_FILE_PATH = __DIR__ . '/temp/filter_separated_process_testsuite_count.txt';

public function testFilterSeparatedProcessTestSuiteNoSkip(): void
{
$count = (int)(file_get_contents(self::FILTER_SEPARATED_PROC_TS_COUNT_FILE_PATH));
file_put_contents(self::FILTER_SEPARATED_PROC_TS_COUNT_FILE_PATH, ++$count);

$this->assertTrue(true);
}

public function testFilterSeparatedProcessTestSuiteSkip(): void
{
$count = (int)(file_get_contents(self::FILTER_SEPARATED_PROC_TS_COUNT_FILE_PATH));
file_put_contents(self::FILTER_SEPARATED_PROC_TS_COUNT_FILE_PATH, ++$count);

$this->assertTrue(true);
}

public function testFilterSeparatedProcessTestSuiteSkip2(): void
{
$count = (int)(file_get_contents(self::FILTER_SEPARATED_PROC_TS_COUNT_FILE_PATH));
file_put_contents(self::FILTER_SEPARATED_PROC_TS_COUNT_FILE_PATH, ++$count);

$this->assertTrue(true);
}
}
Loading
Loading