From d2038553c23b26de4e3cd8fdd2e52219c6dbfaf1 Mon Sep 17 00:00:00 2001 From: Ako Tulu Date: Sun, 1 Dec 2024 00:56:18 +0200 Subject: [PATCH 1/4] Changed separated process runner templates to return object instead of array allowing class template to return array of objects when refactored. --- .../TestRunner/SeparateProcessTestRunner.php | 29 ++++++++++++------- src/Framework/TestRunner/templates/class.tpl | 2 +- src/Framework/TestRunner/templates/method.tpl | 2 +- .../regression/5884/tests/FooTest.php | 6 ++-- 4 files changed, 24 insertions(+), 15 deletions(-) diff --git a/src/Framework/TestRunner/SeparateProcessTestRunner.php b/src/Framework/TestRunner/SeparateProcessTestRunner.php index d29907550af..cbb1515a85a 100644 --- a/src/Framework/TestRunner/SeparateProcessTestRunner.php +++ b/src/Framework/TestRunner/SeparateProcessTestRunner.php @@ -14,6 +14,7 @@ use function file_get_contents; use function get_include_path; use function hrtime; +use function is_array; use function is_file; use function restore_error_handler; use function serialize; @@ -252,22 +253,28 @@ static function (int $errno, string $errstr, string $errfile, int $errline): nev } if ($childResult !== false) { - if (!empty($childResult['output'])) { - $output = $childResult['output']; + if (!is_array($childResult)) { + $childResult = [$childResult]; } - Facade::instance()->forward($childResult['events']); - PassedTests::instance()->import($childResult['passedTests']); + foreach ($childResult as $result) { + if (!empty($result->output)) { + $output = $result->output; + } - assert($test instanceof TestCase); + Facade::instance()->forward($result->events); + PassedTests::instance()->import($result->passedTests); + + assert($test instanceof TestCase); - $test->setResult($childResult['testResult']); - $test->addToAssertionCount($childResult['numAssertions']); + $test->setResult($result->testResult); + $test->addToAssertionCount($result->numAssertions); - if (CodeCoverage::instance()->isActive() && $childResult['codeCoverage'] instanceof \SebastianBergmann\CodeCoverage\CodeCoverage) { - CodeCoverage::instance()->codeCoverage()->merge( - $childResult['codeCoverage'], - ); + if (CodeCoverage::instance()->isActive() && $result->codeCoverage instanceof \SebastianBergmann\CodeCoverage\CodeCoverage) { + CodeCoverage::instance()->codeCoverage()->merge( + $result->codeCoverage, + ); + } } } diff --git a/src/Framework/TestRunner/templates/class.tpl b/src/Framework/TestRunner/templates/class.tpl index be3693a1006..6efe0af90bb 100644 --- a/src/Framework/TestRunner/templates/class.tpl +++ b/src/Framework/TestRunner/templates/class.tpl @@ -105,7 +105,7 @@ function __phpunit_run_isolated_test() file_put_contents( '{processResultFile}', serialize( - [ + (object)[ 'testResult' => $test->result(), 'codeCoverage' => {collectCodeCoverageInformation} ? CodeCoverage::instance()->codeCoverage() : null, 'numAssertions' => $test->numberOfAssertionsPerformed(), diff --git a/src/Framework/TestRunner/templates/method.tpl b/src/Framework/TestRunner/templates/method.tpl index 6b5aba47bac..5d245e8c28d 100644 --- a/src/Framework/TestRunner/templates/method.tpl +++ b/src/Framework/TestRunner/templates/method.tpl @@ -105,7 +105,7 @@ function __phpunit_run_isolated_test() file_put_contents( '{processResultFile}', serialize( - [ + (object)[ 'testResult' => $test->result(), 'codeCoverage' => {collectCodeCoverageInformation} ? CodeCoverage::instance()->codeCoverage() : null, 'numAssertions' => $test->numberOfAssertionsPerformed(), diff --git a/tests/end-to-end/regression/5884/tests/FooTest.php b/tests/end-to-end/regression/5884/tests/FooTest.php index 0f33a691e22..a1a5ce0213a 100644 --- a/tests/end-to-end/regression/5884/tests/FooTest.php +++ b/tests/end-to-end/regression/5884/tests/FooTest.php @@ -61,10 +61,11 @@ public function testStreamToNonWritableFileWithPHPUnitErrorHandler(): void // Now verify the original file is unchanged. $contents = file_get_contents($filename); - $this->assertSame('foo', $contents); chmod($filename, 0o755); unlink($filename); + + $this->assertSame('foo', $contents); } #[WithoutErrorHandler] @@ -85,10 +86,11 @@ public function testStreamToNonWritableFileWithoutPHPUnitErrorHandler(): void // Now verify the original file is unchanged. $contents = file_get_contents($filename); - $this->assertSame('foo', $contents); chmod($filename, 0o755); unlink($filename); + + $this->assertSame('foo', $contents); } public function testStreamToInvalidFile(): void From b082853b3749bf25bc8ff5eedf5814d64ec4a641 Mon Sep 17 00:00:00 2001 From: Ako Tulu Date: Sun, 1 Dec 2024 01:20:31 +0200 Subject: [PATCH 2/4] 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 6efe0af90bb..f487fdb7ed3 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( @@ -69,52 +72,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) @@ -137,4 +155,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 b7e77354575..6a7214982de 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; } @@ -677,9 +598,9 @@ private function invokeMethodsBeforeFirstTest(Event\Emitter $emitter, Event\Test return true; } - private function invokeMethodsAfterLastTest(Event\Emitter $emitter): void + public function invokeMethodsAfterLastTest(Event\Emitter $emitter): void { - if (!$this->isForTestClass()) { + if (!$this->isForTestClass() || $this->isInSeparatedProcess) { return; } @@ -718,4 +639,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 f4728e7338ff0007b993a0d7e3b9c00f654a98e2 Mon Sep 17 00:00:00 2001 From: Ako Tulu Date: Thu, 5 Dec 2024 08:10:50 +0200 Subject: [PATCH 3/4] 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 6a7214982de..f30a909de76 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 @@ -639,114 +749,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), - ); - } } From bf7170b8bac86b28f3ce846eb7acd7363877fb34 Mon Sep 17 00:00:00 2001 From: Ako Tulu Date: Thu, 6 Feb 2025 19:17:10 +0200 Subject: [PATCH 4/4] Implemented filter support to separated process TestSuits. --- src/Framework/TestRunner/templates/class.tpl | 10 ++++- .../FilterSeparatedProcessTestSuiteTest.php | 45 +++++++++++++++++++ .../end-to-end/sandbox/_files/temp/.gitignore | 1 + ...ore-and-after-class-method-call-count.phpt | 11 +++-- .../filter-separated-process-testsuite.phpt | 34 ++++++++++++++ ...ore-and-after-class-method-call-count.phpt | 13 ++++-- ...ore-and-after-class-method-call-count.phpt | 13 ++++-- 7 files changed, 115 insertions(+), 12 deletions(-) create mode 100644 tests/end-to-end/sandbox/_files/FilterSeparatedProcessTestSuiteTest.php create mode 100644 tests/end-to-end/sandbox/_files/temp/.gitignore create mode 100644 tests/end-to-end/sandbox/filter-separated-process-testsuite.phpt diff --git a/src/Framework/TestRunner/templates/class.tpl b/src/Framework/TestRunner/templates/class.tpl index 6705c6ab579..286f5532adb 100644 --- a/src/Framework/TestRunner/templates/class.tpl +++ b/src/Framework/TestRunner/templates/class.tpl @@ -8,6 +8,7 @@ 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 @@ -86,13 +87,20 @@ function __phpunit_run_isolated_class() $suite = TestSuite::fromClassReflector($testClass); $suite->setIsInSeparatedProcess(false); + (new TestSuiteFilterProcessor)->process($configuration, $suite); + $testSuiteValueObjectForEvents = Event\TestSuite\TestSuiteBuilder::from($suite); if (!$suite->invokeMethodsBeforeFirstTest(Facade::emitter(), $testSuiteValueObjectForEvents)) { return; } - foreach($suite->tests() as $test) { + $tests = []; + foreach ($suite as $test) { + $tests[] = $test; + } + + foreach($tests as $test) { $test->setRunClassInSeparateProcess(false); $test->run(); diff --git a/tests/end-to-end/sandbox/_files/FilterSeparatedProcessTestSuiteTest.php b/tests/end-to-end/sandbox/_files/FilterSeparatedProcessTestSuiteTest.php new file mode 100644 index 00000000000..793707d416a --- /dev/null +++ b/tests/end-to-end/sandbox/_files/FilterSeparatedProcessTestSuiteTest.php @@ -0,0 +1,45 @@ + + * + * 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); + } +} diff --git a/tests/end-to-end/sandbox/_files/temp/.gitignore b/tests/end-to-end/sandbox/_files/temp/.gitignore new file mode 100644 index 00000000000..72e8ffc0db8 --- /dev/null +++ b/tests/end-to-end/sandbox/_files/temp/.gitignore @@ -0,0 +1 @@ +* 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 index c2b8b9d3904..489fb342cd2 100644 --- 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 @@ -8,12 +8,17 @@ $_SERVER['argv'][] = __DIR__ . '/_files/ClassIsolationBeforeAndAfterClassMethodC require_once __DIR__ . '/../../bootstrap.php'; -\file_put_contents(__DIR__ . '/_files/temp/class_before_method_call_count.txt', 0); -\file_put_contents(__DIR__ . '/_files/temp/class_after_method_call_count.txt', 0); +$tmpDir = __DIR__ . '/_files/temp'; + +if (!\file_exists($tmpDir)) { + \mkdir($tmpDir, recursive: true); +} +\file_put_contents($tmpDir . '/class_before_method_call_count.txt', 0); +\file_put_contents($tmpDir . '/class_after_method_call_count.txt', 0); (new PHPUnit\TextUI\Application)->run($_SERVER['argv']); -if (\intval(\file_get_contents(__DIR__ . '/_files/temp/class_after_method_call_count.txt')) !== 1){ +if (\intval(\file_get_contents($tmpDir . '/class_after_method_call_count.txt')) !== 1){ throw new \Exception('Invalid after class method call count!'); } --EXPECTF-- diff --git a/tests/end-to-end/sandbox/filter-separated-process-testsuite.phpt b/tests/end-to-end/sandbox/filter-separated-process-testsuite.phpt new file mode 100644 index 00000000000..630122988af --- /dev/null +++ b/tests/end-to-end/sandbox/filter-separated-process-testsuite.phpt @@ -0,0 +1,34 @@ +--TEST-- +Filter test must be handled by separated process tests. +--FILE-- +run($_SERVER['argv']); + +if (\intval(\file_get_contents($tmpDir . '/filter_separated_process_testsuite_count.txt')) !== 1){ + throw new \Exception('Invalid method call count!'); +} +--EXPECTF-- +PHPUnit %s by Sebastian Bergmann and contributors. + +Runtime: %s + +. 1 / 1 (100%) + +Time: %s, Memory: %s + +OK (1 test, 1 assertion) 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 index 5ffa788fdaa..9e0e3b38249 100644 --- 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 @@ -8,16 +8,21 @@ $_SERVER['argv'][] = __DIR__ . '/_files/MethodIsolationBeforeAndAfterClassMethod require_once __DIR__ . '/../../bootstrap.php'; -\file_put_contents(__DIR__ . '/_files/temp/method_before_method_call_count.txt', 0); -\file_put_contents(__DIR__ . '/_files/temp/method_after_method_call_count.txt', 0); +$tmpDir = __DIR__ . '/_files/temp'; + +if (!\file_exists($tmpDir)) { + \mkdir($tmpDir, recursive: true); +} +\file_put_contents($tmpDir . '/method_before_method_call_count.txt', 0); +\file_put_contents($tmpDir . '/method_after_method_call_count.txt', 0); (new PHPUnit\TextUI\Application)->run($_SERVER['argv']); -if (\intval(\file_get_contents(__DIR__ . '/_files/temp/method_before_method_call_count.txt')) !== 3){ +if (\intval(\file_get_contents($tmpDir . '/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){ +if (\intval(\file_get_contents($tmpDir . '/method_after_method_call_count.txt')) !== 3){ throw new \Exception('Invalid after class method call count!'); } --EXPECTF-- 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 index c4eb7ffccb2..a6440dd8083 100644 --- 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 @@ -8,12 +8,17 @@ $_SERVER['argv'][] = __DIR__ . '/_files/TestsIsolationBeforeAndAfterClassMethodC require_once __DIR__ . '/../../bootstrap.php'; -\file_put_contents(__DIR__ . '/_files/temp/tests_before_method_call_count.txt', 0); -\file_put_contents(__DIR__ . '/_files/temp/tests_after_method_call_count.txt', 0); +$tmpDir = __DIR__ . '/_files/temp'; + +if (!\file_exists($tmpDir)) { + \mkdir($tmpDir, recursive: true); +} +\file_put_contents($tmpDir . '/tests_before_method_call_count.txt', 0); +\file_put_contents($tmpDir . '/tests_after_method_call_count.txt', 0); (new PHPUnit\TextUI\Application)->run($_SERVER['argv']); -if (\intval(\file_get_contents(__DIR__ . '/_files/temp/tests_after_method_call_count.txt')) !== 3){ +if (\intval(\file_get_contents($tmpDir . '/tests_after_method_call_count.txt')) !== 3){ throw new \Exception('Invalid after class method call count!'); } --EXPECTF-- @@ -25,4 +30,4 @@ Runtime: %s Time: %s, Memory: %s -OK (3 tests, 6 assertions) \ No newline at end of file +OK (3 tests, 6 assertions)