Skip to content
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

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

Open
wants to merge 2 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 @@ -29,6 +29,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
84 changes: 51 additions & 33 deletions src/Framework/TestRunner/templates/class.tpl
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
<?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;
Expand Down Expand Up @@ -31,7 +34,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 +71,67 @@ 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 = '';
$testSuiteValueObjectForEvents = Event\TestSuite\TestSuiteBuilder::from($suite);

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

ini_set('xdebug.scream', '0');
foreach($suite->tests() as $test) {
$test->setRunClassInSeparateProcess(false);
$test->run();

$testOutput = '';

// Not every STDOUT target stream is rewindable
@rewind(STDOUT);
if (!$test->expectsOutput()) {
$testOutput = $test->output();
}

if ($stdout = @stream_get_contents(STDOUT)) {
$output = $stdout . $output;
$streamMetaData = stream_get_meta_data(STDOUT);
// 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 +154,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 @@ class TestSuite implements IteratorAggregate, Reorderable, Test
/**
* @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 @@ public static function fromClassReflector(ReflectionClass $class, array $groups
);
}

$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 @@ public function collect(): array
return $tests;
}

public function isInSeparatedProcess(): bool
{
return $this->isInSeparatedProcess;
}

public function setIsInSeparatedProcess(bool $isInSeparatedProcess): void
{
$this->isInSeparatedProcess = $isInSeparatedProcess;
}

/**
* @throws CodeCoverageException
* @throws Event\RuntimeException
Expand Down Expand Up @@ -367,6 +384,20 @@ public function run(): void
}

$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 @@ private function methodDoesNotExistOrIsDeclaredInTestCase(string $methodName): b
$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 @@ private function throwableToString(Throwable $t): string
* @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 @@ private function invokeMethodsBeforeFirstTest(Event\Emitter $emitter, Event\Test
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 <[email protected]>
*
* 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,64 @@
<?php declare(strict_types=1);
/*
* This file is part of PHPUnit.
*
* (c) Sebastian Bergmann <[email protected]>
*
* 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\Depends;
use PHPUnit\Framework\Attributes\RunInSeparateProcess;
use PHPUnit\Framework\TestCase;

final class MethodIsolationBeforeAndAfterClassMethodCallCountTest extends TestCase
{
public const string BEFORE_CALL_COUNT_FILE_PATH = __DIR__ . '/temp/method_before_method_call_count.txt';
public const string AFTER_CALL_COUNT_FILE_PATH = __DIR__ . '/temp/method_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);
}

#[RunInSeparateProcess]
public function testBeforeAndAfterClassMethodCallCount1(): void
{
// TODO: Due source code design, before methods for primary process are always called first. Should be 1
$this->assertEquals('2', 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');
}

#[Depends('testBeforeAndAfterClassMethodCallCount1')]
public function testBeforeAndAfterClassMethodCallCount2(): void
{
$this->assertEquals('2', file_get_contents(self::BEFORE_CALL_COUNT_FILE_PATH), 'before_method_call_count');
$this->assertEquals('1', file_get_contents(self::AFTER_CALL_COUNT_FILE_PATH), 'after_method_call_count');
}

#[RunInSeparateProcess]
#[Depends('testBeforeAndAfterClassMethodCallCount2')]
public function testBeforeAndAfterClassMethodCallCount3(): void
{
$this->assertEquals('3', file_get_contents(self::BEFORE_CALL_COUNT_FILE_PATH), 'before_method_call_count');
$this->assertEquals('1', file_get_contents(self::AFTER_CALL_COUNT_FILE_PATH), 'after_method_call_count');
}

#[Depends('testBeforeAndAfterClassMethodCallCount3')]
public function testBeforeAndAfterClassMethodCallCount4(): void
{
$this->assertEquals('3', file_get_contents(self::BEFORE_CALL_COUNT_FILE_PATH), 'before_method_call_count');
$this->assertEquals('2', file_get_contents(self::AFTER_CALL_COUNT_FILE_PATH), 'after_method_call_count');
}
}
Loading
Loading