From 919a2d067fdf5315d542794f407d39caca2c8fda Mon Sep 17 00:00:00 2001 From: Ako Tulu Date: Sun, 1 Dec 2024 01:20:31 +0200 Subject: [PATCH 1/2] Resolved test suite run in separated process executed one by one in separated process. --- phpunit.xml | 1 + src/Framework/TestRunner/templates/class.tpl | 84 +++--- src/Framework/TestSuite.php | 265 ++++++++++-------- ...BeforeAndAfterClassMethodCallCountTest.php | 46 +++ ...BeforeAndAfterClassMethodCallCountTest.php | 64 +++++ ...BeforeAndAfterClassMethodCallCountTest.php | 52 ++++ ...ore-and-after-class-method-call-count.phpt | 28 ++ ...ore-and-after-class-method-call-count.phpt | 32 +++ ...ore-and-after-class-method-call-count.phpt | 28 ++ 9 files changed, 450 insertions(+), 150 deletions(-) create mode 100644 tests/end-to-end/sandbox/_files/ClassIsolationBeforeAndAfterClassMethodCallCountTest.php create mode 100644 tests/end-to-end/sandbox/_files/MethodIsolationBeforeAndAfterClassMethodCallCountTest.php create mode 100644 tests/end-to-end/sandbox/_files/TestsIsolationBeforeAndAfterClassMethodCallCountTest.php create mode 100644 tests/end-to-end/sandbox/class-isolation-before-and-after-class-method-call-count.phpt create mode 100644 tests/end-to-end/sandbox/method-isolation-before-and-after-class-method-call-count.phpt create mode 100644 tests/end-to-end/sandbox/tests-isolation-before-and-after-class-method-call-count.phpt diff --git a/phpunit.xml b/phpunit.xml index 456f32ff341..19cf3a5ab67 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -29,6 +29,7 @@ tests/end-to-end/mock-objects tests/end-to-end/phpt tests/end-to-end/regression + tests/end-to-end/sandbox tests/end-to-end/self-direct-indirect tests/end-to-end/testdox diff --git a/src/Framework/TestRunner/templates/class.tpl b/src/Framework/TestRunner/templates/class.tpl index d73716fe6e2..6705c6ab579 100644 --- a/src/Framework/TestRunner/templates/class.tpl +++ b/src/Framework/TestRunner/templates/class.tpl @@ -1,7 +1,10 @@ initForIsolation( PHPUnit\Event\Telemetry\HRTime::fromSecondsAndNanoseconds( @@ -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) @@ -136,4 +154,4 @@ if ('{bootstrap}' !== '') { require_once '{bootstrap}'; } -__phpunit_run_isolated_test(); +__phpunit_run_isolated_class(); diff --git a/src/Framework/TestSuite.php b/src/Framework/TestSuite.php index beaa0b64d1d..74280c50bcc 100644 --- a/src/Framework/TestSuite.php +++ b/src/Framework/TestSuite.php @@ -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; @@ -81,9 +82,11 @@ class TestSuite implements IteratorAggregate, Reorderable, Test /** * @var ?list */ - 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 @@ -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; } @@ -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 @@ -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); @@ -491,123 +522,13 @@ public function isForTestClass(): bool return class_exists($this->name, false) && is_subclass_of($this->name, TestCase::class); } - /** - * @param ReflectionClass $class - * @param list $groups - * - * @throws Exception - */ - protected function addTestMethod(ReflectionClass $class, ReflectionMethod $method, array $groups): void - { - $className = $class->getName(); - $methodName = $method->getName(); - - assert(!empty($methodName)); - - try { - $test = (new TestBuilder)->build($class, $methodName, $groups); - } catch (InvalidDataProviderException $e) { - Event\Facade::emitter()->testTriggeredPhpunitError( - new TestMethod( - $className, - $methodName, - $class->getFileName(), - $method->getStartLine(), - Event\Code\TestDoxBuilder::fromClassNameAndMethodName( - $className, - $methodName, - ), - MetadataCollection::fromArray([]), - Event\TestData\TestDataCollection::fromArray([]), - ), - sprintf( - "The data provider specified for %s::%s is invalid\n%s", - $className, - $methodName, - $this->throwableToString($e), - ), - ); - - return; - } - - if ($test instanceof TestCase || $test instanceof DataProviderTestSuite) { - $test->setDependencies( - Dependencies::dependencies($class->getName(), $methodName), - ); - } - - $this->addTest( - $test, - array_merge( - $groups, - (new Groups)->groups($class->getName(), $methodName), - ), - ); - } - - private function clearCaches(): void - { - $this->providedTests = null; - $this->requiredTests = null; - } - - /** - * @param list $groups - */ - private function containsOnlyVirtualGroups(array $groups): bool - { - foreach ($groups as $group) { - if (!str_starts_with($group, '__phpunit_')) { - return false; - } - } - - return true; - } - - private function methodDoesNotExistOrIsDeclaredInTestCase(string $methodName): bool - { - $reflector = new ReflectionClass($this->name); - - return !$reflector->hasMethod($methodName) || - $reflector->getMethod($methodName)->getDeclaringClass()->getName() === TestCase::class; - } - - /** - * @throws Exception - */ - private function throwableToString(Throwable $t): string - { - $message = $t->getMessage(); - - if (empty(trim($message))) { - $message = ''; - } - - if ($t instanceof InvalidDataProviderException) { - return sprintf( - "%s\n%s", - $message, - Filter::stackTraceFromThrowableAsString($t), - ); - } - - return sprintf( - "%s: %s\n%s", - $t::class, - $message, - Filter::stackTraceFromThrowableAsString($t), - ); - } - /** * @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; } @@ -678,9 +599,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; } @@ -725,4 +646,114 @@ private function invokeMethodsAfterLastTest(Event\Emitter $emitter): void ); } } + + /** + * @param ReflectionClass $class + * @param list $groups + * + * @throws Exception + */ + protected function addTestMethod(ReflectionClass $class, ReflectionMethod $method, array $groups): void + { + $className = $class->getName(); + $methodName = $method->getName(); + + assert(!empty($methodName)); + + try { + $test = (new TestBuilder)->build($class, $methodName, $groups); + } catch (InvalidDataProviderException $e) { + Event\Facade::emitter()->testTriggeredPhpunitError( + new TestMethod( + $className, + $methodName, + $class->getFileName(), + $method->getStartLine(), + Event\Code\TestDoxBuilder::fromClassNameAndMethodName( + $className, + $methodName, + ), + MetadataCollection::fromArray([]), + Event\TestData\TestDataCollection::fromArray([]), + ), + sprintf( + "The data provider specified for %s::%s is invalid\n%s", + $className, + $methodName, + $this->throwableToString($e), + ), + ); + + return; + } + + if ($test instanceof TestCase || $test instanceof DataProviderTestSuite) { + $test->setDependencies( + Dependencies::dependencies($class->getName(), $methodName), + ); + } + + $this->addTest( + $test, + array_merge( + $groups, + (new Groups)->groups($class->getName(), $methodName), + ), + ); + } + + private function clearCaches(): void + { + $this->providedTests = null; + $this->requiredTests = null; + } + + /** + * @param list $groups + */ + private function containsOnlyVirtualGroups(array $groups): bool + { + foreach ($groups as $group) { + if (!str_starts_with($group, '__phpunit_')) { + return false; + } + } + + return true; + } + + private function methodDoesNotExistOrIsDeclaredInTestCase(string $methodName): bool + { + $reflector = new ReflectionClass($this->name); + + return !$reflector->hasMethod($methodName) || + $reflector->getMethod($methodName)->getDeclaringClass()->getName() === TestCase::class; + } + + /** + * @throws Exception + */ + private function throwableToString(Throwable $t): string + { + $message = $t->getMessage(); + + if (empty(trim($message))) { + $message = ''; + } + + if ($t instanceof InvalidDataProviderException) { + return sprintf( + "%s\n%s", + $message, + Filter::stackTraceFromThrowableAsString($t), + ); + } + + return sprintf( + "%s: %s\n%s", + $t::class, + $message, + Filter::stackTraceFromThrowableAsString($t), + ); + } } diff --git a/tests/end-to-end/sandbox/_files/ClassIsolationBeforeAndAfterClassMethodCallCountTest.php b/tests/end-to-end/sandbox/_files/ClassIsolationBeforeAndAfterClassMethodCallCountTest.php new file mode 100644 index 00000000000..40c4b80de94 --- /dev/null +++ b/tests/end-to-end/sandbox/_files/ClassIsolationBeforeAndAfterClassMethodCallCountTest.php @@ -0,0 +1,46 @@ + + * + * 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'); + } +} diff --git a/tests/end-to-end/sandbox/_files/MethodIsolationBeforeAndAfterClassMethodCallCountTest.php b/tests/end-to-end/sandbox/_files/MethodIsolationBeforeAndAfterClassMethodCallCountTest.php new file mode 100644 index 00000000000..90f150770e1 --- /dev/null +++ b/tests/end-to-end/sandbox/_files/MethodIsolationBeforeAndAfterClassMethodCallCountTest.php @@ -0,0 +1,64 @@ + + * + * 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'); + } +} diff --git a/tests/end-to-end/sandbox/_files/TestsIsolationBeforeAndAfterClassMethodCallCountTest.php b/tests/end-to-end/sandbox/_files/TestsIsolationBeforeAndAfterClassMethodCallCountTest.php new file mode 100644 index 00000000000..be6f6699d96 --- /dev/null +++ b/tests/end-to-end/sandbox/_files/TestsIsolationBeforeAndAfterClassMethodCallCountTest.php @@ -0,0 +1,52 @@ + + * + * 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\RunTestsInSeparateProcesses; +use PHPUnit\Framework\TestCase; + +#[RunTestsInSeparateProcesses] +final class TestsIsolationBeforeAndAfterClassMethodCallCountTest extends TestCase +{ + public const string BEFORE_CALL_COUNT_FILE_PATH = __DIR__ . '/temp/tests_before_method_call_count.txt'; + public const string AFTER_CALL_COUNT_FILE_PATH = __DIR__ . '/temp/tests_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('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'); + } + + public function testBeforeAndAfterClassMethodCallCount3(): 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'); + } +} diff --git a/tests/end-to-end/sandbox/class-isolation-before-and-after-class-method-call-count.phpt b/tests/end-to-end/sandbox/class-isolation-before-and-after-class-method-call-count.phpt new file mode 100644 index 00000000000..c2b8b9d3904 --- /dev/null +++ b/tests/end-to-end/sandbox/class-isolation-before-and-after-class-method-call-count.phpt @@ -0,0 +1,28 @@ +--TEST-- +Before and after class methods must not be called from primary process when test class or method is run in separated process. +--FILE-- +run($_SERVER['argv']); + +if (\intval(\file_get_contents(__DIR__ . '/_files/temp/class_after_method_call_count.txt')) !== 1){ + throw new \Exception('Invalid after class method call count!'); +} +--EXPECTF-- +PHPUnit %s by Sebastian Bergmann and contributors. + +Runtime: %s + +.. 2 / 2 (100%) + +Time: %s, Memory: %s + +OK (2 tests, 4 assertions) diff --git a/tests/end-to-end/sandbox/method-isolation-before-and-after-class-method-call-count.phpt b/tests/end-to-end/sandbox/method-isolation-before-and-after-class-method-call-count.phpt new file mode 100644 index 00000000000..5ffa788fdaa --- /dev/null +++ b/tests/end-to-end/sandbox/method-isolation-before-and-after-class-method-call-count.phpt @@ -0,0 +1,32 @@ +--TEST-- +Before and after class methods must not be called from primary process when test class or method is run in separated process. +--FILE-- +run($_SERVER['argv']); + +if (\intval(\file_get_contents(__DIR__ . '/_files/temp/method_before_method_call_count.txt')) !== 3){ + throw new \Exception('Invalid before class method call count!'); +} + +if (\intval(\file_get_contents(__DIR__ . '/_files/temp/method_after_method_call_count.txt')) !== 3){ + throw new \Exception('Invalid after class method call count!'); +} +--EXPECTF-- +PHPUnit %s by Sebastian Bergmann and contributors. + +Runtime: %s + +.... 4 / 4 (100%) + +Time: %s, Memory: %s + +OK (4 tests, 8 assertions) diff --git a/tests/end-to-end/sandbox/tests-isolation-before-and-after-class-method-call-count.phpt b/tests/end-to-end/sandbox/tests-isolation-before-and-after-class-method-call-count.phpt new file mode 100644 index 00000000000..c4eb7ffccb2 --- /dev/null +++ b/tests/end-to-end/sandbox/tests-isolation-before-and-after-class-method-call-count.phpt @@ -0,0 +1,28 @@ +--TEST-- +Before and after class methods must not be called from primary process when test class or method is run in separated process. +--FILE-- +run($_SERVER['argv']); + +if (\intval(\file_get_contents(__DIR__ . '/_files/temp/tests_after_method_call_count.txt')) !== 3){ + throw new \Exception('Invalid after class method call count!'); +} +--EXPECTF-- +PHPUnit %s by Sebastian Bergmann and contributors. + +Runtime: %s + +... 3 / 3 (100%) + +Time: %s, Memory: %s + +OK (3 tests, 6 assertions) \ No newline at end of file From 387c16347a555476f8c52bc98c27295266f30e61 Mon Sep 17 00:00:00 2001 From: Ako Tulu Date: Thu, 5 Dec 2024 08:10:50 +0200 Subject: [PATCH 2/2] Reverted CS-fixer changes. --- src/Framework/TestSuite.php | 220 ++++++++++++++++++------------------ 1 file changed, 110 insertions(+), 110 deletions(-) diff --git a/src/Framework/TestSuite.php b/src/Framework/TestSuite.php index 74280c50bcc..725a5ff317c 100644 --- a/src/Framework/TestSuite.php +++ b/src/Framework/TestSuite.php @@ -522,6 +522,116 @@ public function isForTestClass(): bool return class_exists($this->name, false) && is_subclass_of($this->name, TestCase::class); } + /** + * @param ReflectionClass $class + * @param list $groups + * + * @throws Exception + */ + protected function addTestMethod(ReflectionClass $class, ReflectionMethod $method, array $groups): void + { + $className = $class->getName(); + $methodName = $method->getName(); + + assert(!empty($methodName)); + + try { + $test = (new TestBuilder)->build($class, $methodName, $groups); + } catch (InvalidDataProviderException $e) { + Event\Facade::emitter()->testTriggeredPhpunitError( + new TestMethod( + $className, + $methodName, + $class->getFileName(), + $method->getStartLine(), + Event\Code\TestDoxBuilder::fromClassNameAndMethodName( + $className, + $methodName, + ), + MetadataCollection::fromArray([]), + Event\TestData\TestDataCollection::fromArray([]), + ), + sprintf( + "The data provider specified for %s::%s is invalid\n%s", + $className, + $methodName, + $this->throwableToString($e), + ), + ); + + return; + } + + if ($test instanceof TestCase || $test instanceof DataProviderTestSuite) { + $test->setDependencies( + Dependencies::dependencies($class->getName(), $methodName), + ); + } + + $this->addTest( + $test, + array_merge( + $groups, + (new Groups)->groups($class->getName(), $methodName), + ), + ); + } + + private function clearCaches(): void + { + $this->providedTests = null; + $this->requiredTests = null; + } + + /** + * @param list $groups + */ + private function containsOnlyVirtualGroups(array $groups): bool + { + foreach ($groups as $group) { + if (!str_starts_with($group, '__phpunit_')) { + return false; + } + } + + return true; + } + + private function methodDoesNotExistOrIsDeclaredInTestCase(string $methodName): bool + { + $reflector = new ReflectionClass($this->name); + + return !$reflector->hasMethod($methodName) || + $reflector->getMethod($methodName)->getDeclaringClass()->getName() === TestCase::class; + } + + /** + * @throws Exception + */ + private function throwableToString(Throwable $t): string + { + $message = $t->getMessage(); + + if (empty(trim($message))) { + $message = ''; + } + + if ($t instanceof InvalidDataProviderException) { + return sprintf( + "%s\n%s", + $message, + Filter::stackTraceFromThrowableAsString($t), + ); + } + + return sprintf( + "%s: %s\n%s", + $t::class, + $message, + Filter::stackTraceFromThrowableAsString($t), + ); + } + /** * @throws Exception * @throws NoPreviousThrowableException @@ -646,114 +756,4 @@ public function invokeMethodsAfterLastTest(Event\Emitter $emitter): void ); } } - - /** - * @param ReflectionClass $class - * @param list $groups - * - * @throws Exception - */ - protected function addTestMethod(ReflectionClass $class, ReflectionMethod $method, array $groups): void - { - $className = $class->getName(); - $methodName = $method->getName(); - - assert(!empty($methodName)); - - try { - $test = (new TestBuilder)->build($class, $methodName, $groups); - } catch (InvalidDataProviderException $e) { - Event\Facade::emitter()->testTriggeredPhpunitError( - new TestMethod( - $className, - $methodName, - $class->getFileName(), - $method->getStartLine(), - Event\Code\TestDoxBuilder::fromClassNameAndMethodName( - $className, - $methodName, - ), - MetadataCollection::fromArray([]), - Event\TestData\TestDataCollection::fromArray([]), - ), - sprintf( - "The data provider specified for %s::%s is invalid\n%s", - $className, - $methodName, - $this->throwableToString($e), - ), - ); - - return; - } - - if ($test instanceof TestCase || $test instanceof DataProviderTestSuite) { - $test->setDependencies( - Dependencies::dependencies($class->getName(), $methodName), - ); - } - - $this->addTest( - $test, - array_merge( - $groups, - (new Groups)->groups($class->getName(), $methodName), - ), - ); - } - - private function clearCaches(): void - { - $this->providedTests = null; - $this->requiredTests = null; - } - - /** - * @param list $groups - */ - private function containsOnlyVirtualGroups(array $groups): bool - { - foreach ($groups as $group) { - if (!str_starts_with($group, '__phpunit_')) { - return false; - } - } - - return true; - } - - private function methodDoesNotExistOrIsDeclaredInTestCase(string $methodName): bool - { - $reflector = new ReflectionClass($this->name); - - return !$reflector->hasMethod($methodName) || - $reflector->getMethod($methodName)->getDeclaringClass()->getName() === TestCase::class; - } - - /** - * @throws Exception - */ - private function throwableToString(Throwable $t): string - { - $message = $t->getMessage(); - - if (empty(trim($message))) { - $message = ''; - } - - if ($t instanceof InvalidDataProviderException) { - return sprintf( - "%s\n%s", - $message, - Filter::stackTraceFromThrowableAsString($t), - ); - } - - return sprintf( - "%s: %s\n%s", - $t::class, - $message, - Filter::stackTraceFromThrowableAsString($t), - ); - } }