diff --git a/src/Framework/TestBuilder.php b/src/Framework/TestBuilder.php index cd387dfa680..96a18f9241f 100644 --- a/src/Framework/TestBuilder.php +++ b/src/Framework/TestBuilder.php @@ -19,6 +19,9 @@ use PHPUnit\Metadata\ExcludeStaticPropertyFromBackup; use PHPUnit\Metadata\Parser\Registry as MetadataRegistry; use PHPUnit\Metadata\PreserveGlobalState; +use PHPUnit\Metadata\RunClassInSeparateProcess; +use PHPUnit\Metadata\RunInSeparateProcess; +use PHPUnit\Metadata\RunTestsInSeparateProcesses; use PHPUnit\TextUI\Configuration\Registry as ConfigurationRegistry; use ReflectionClass; @@ -50,6 +53,7 @@ public function build(ReflectionClass $theClass, string $methodName, array $grou $this->shouldTestMethodBeRunInSeparateProcess($className, $methodName), $this->shouldGlobalStateBePreserved($className, $methodName), $this->shouldAllTestMethodsOfTestClassBeRunInSingleSeparateProcess($className), + $this->shouldForkIfPossible($className, $methodName), $this->backupSettings($className, $methodName), $groups, ); @@ -64,6 +68,7 @@ public function build(ReflectionClass $theClass, string $methodName, array $grou $this->shouldTestMethodBeRunInSeparateProcess($className, $methodName), $this->shouldGlobalStateBePreserved($className, $methodName), $this->shouldAllTestMethodsOfTestClassBeRunInSingleSeparateProcess($className), + $this->shouldForkIfPossible($className, $methodName), $this->backupSettings($className, $methodName), ); @@ -76,7 +81,7 @@ public function build(ReflectionClass $theClass, string $methodName, array $grou * @psalm-param array{backupGlobals: ?bool, backupGlobalsExcludeList: list, backupStaticProperties: ?bool, backupStaticPropertiesExcludeList: array>} $backupSettings * @psalm-param list $groups */ - private function buildDataProviderTestSuite(string $methodName, string $className, array $data, bool $runTestInSeparateProcess, ?bool $preserveGlobalState, bool $runClassInSeparateProcess, array $backupSettings, array $groups): DataProviderTestSuite + private function buildDataProviderTestSuite(string $methodName, string $className, array $data, bool $runTestInSeparateProcess, ?bool $preserveGlobalState, bool $runClassInSeparateProcess, bool $forkIfPossible, array $backupSettings, array $groups): DataProviderTestSuite { $dataProviderTestSuite = DataProviderTestSuite::empty( $className . '::' . $methodName, @@ -99,6 +104,7 @@ private function buildDataProviderTestSuite(string $methodName, string $classNam $runTestInSeparateProcess, $preserveGlobalState, $runClassInSeparateProcess, + $forkIfPossible, $backupSettings, ); @@ -111,7 +117,7 @@ private function buildDataProviderTestSuite(string $methodName, string $classNam /** * @psalm-param array{backupGlobals: ?bool, backupGlobalsExcludeList: list, backupStaticProperties: ?bool, backupStaticPropertiesExcludeList: array>} $backupSettings */ - private function configureTestCase(TestCase $test, bool $runTestInSeparateProcess, ?bool $preserveGlobalState, bool $runClassInSeparateProcess, array $backupSettings): void + private function configureTestCase(TestCase $test, bool $runTestInSeparateProcess, ?bool $preserveGlobalState, bool $runClassInSeparateProcess, bool $forkIfPossible, array $backupSettings): void { if ($runTestInSeparateProcess) { $test->setRunTestInSeparateProcess(true); @@ -121,6 +127,10 @@ private function configureTestCase(TestCase $test, bool $runTestInSeparateProces $test->setRunClassInSeparateProcess(true); } + if ($forkIfPossible) { + $test->setForkIfPossible(true); + } + if ($preserveGlobalState !== null) { $test->setPreserveGlobalState($preserveGlobalState); } @@ -273,4 +283,50 @@ private function shouldAllTestMethodsOfTestClassBeRunInSingleSeparateProcess(str { return MetadataRegistry::parser()->forClass($className)->isRunClassInSeparateProcess()->isNotEmpty(); } + + /** + * @psalm-param class-string $className + * @psalm-param non-empty-string $methodName + */ + private function shouldForkIfPossible(string $className, string $methodName): bool + { + $metadataForMethod = MetadataRegistry::parser()->forMethod($className, $methodName); + + if ($metadataForMethod->isRunInSeparateProcess()->isNotEmpty()) { + $metadata = $metadataForMethod->isRunInSeparateProcess()->asArray()[0]; + + assert($metadata instanceof RunInSeparateProcess); + + $forkIfPossible = $metadata->forkIfPossible(); + if ($forkIfPossible !== null) { + return $forkIfPossible; + } + } + + $metadataForClass = MetadataRegistry::parser()->forClass($className); + + if ($metadataForClass->isRunTestsInSeparateProcesses()->isNotEmpty()) { + $metadata = $metadataForClass->isRunTestsInSeparateProcesses()->asArray()[0]; + + assert($metadata instanceof RunTestsInSeparateProcesses); + + $forkIfPossible = $metadata->forkIfPossible(); + if ($forkIfPossible !== null) { + return $forkIfPossible; + } + } + + if ($metadataForClass->isRunClassInSeparateProcess()->isNotEmpty()) { + $metadata = $metadataForClass->isRunClassInSeparateProcess()->asArray()[0]; + + assert($metadata instanceof RunClassInSeparateProcess); + + $forkIfPossible = $metadata->forkIfPossible(); + if ($forkIfPossible !== null) { + return $forkIfPossible; + } + } + + return false; + } } diff --git a/src/Framework/TestCase.php b/src/Framework/TestCase.php index dd5fc99a96a..a0d786b798e 100644 --- a/src/Framework/TestCase.php +++ b/src/Framework/TestCase.php @@ -142,6 +142,7 @@ abstract class TestCase extends Assert implements Reorderable, SelfDescribing, T */ private ?array $backupGlobalExceptionHandlers = null; private ?bool $runClassInSeparateProcess = null; + private ?bool $forkIfPossible = null; private ?bool $runTestInSeparateProcess = null; private bool $preserveGlobalState = false; private bool $inIsolation = false; @@ -335,6 +336,7 @@ final public function run(): void $this, $this->runClassInSeparateProcess && !$this->runTestInSeparateProcess, $this->preserveGlobalState, + $this->forkIfPossible === true, ); } } @@ -718,6 +720,14 @@ final public function setRunClassInSeparateProcess(bool $runClassInSeparateProce $this->runClassInSeparateProcess = $runClassInSeparateProcess; } + /** + * @internal This method is not covered by the backward compatibility promise for PHPUnit + */ + final public function setForkIfPossible(bool $forkIfPossible): void + { + $this->forkIfPossible = $forkIfPossible; + } + /** * @internal This method is not covered by the backward compatibility promise for PHPUnit */ diff --git a/src/Framework/TestRunner.php b/src/Framework/TestRunner.php index c2e6bc5a185..30965b60e2b 100644 --- a/src/Framework/TestRunner.php +++ b/src/Framework/TestRunner.php @@ -249,9 +249,9 @@ public function run(TestCase $test): void * @throws ProcessIsolationException * @throws StaticAnalysisCacheNotConfiguredException */ - public function runInSeparateProcess(TestCase $test, bool $runEntireClass, bool $preserveGlobalState): void + public function runInSeparateProcess(TestCase $test, bool $runEntireClass, bool $preserveGlobalState, bool $forkIfPossible): void { - if (PcntlFork::isPcntlForkAvailable()) { + if ($forkIfPossible && PcntlFork::isPcntlForkAvailable()) { // forking the parent process is a more lightweight way to run a test in isolation. // it requires the pcntl extension though. $fork = new PcntlFork; diff --git a/tests/_files/Metadata/Attribute/tests/ProcessIsolationForkedTest.php b/tests/_files/Metadata/Attribute/tests/ProcessIsolationForkedTest.php new file mode 100644 index 00000000000..3278fa91c7e --- /dev/null +++ b/tests/_files/Metadata/Attribute/tests/ProcessIsolationForkedTest.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace PHPUnit\TestFixture\Metadata\Attribute; + +use PHPUnit\Framework\Attributes\RunClassInSeparateProcess; +use PHPUnit\Framework\Attributes\RunInSeparateProcess; +use PHPUnit\Framework\Attributes\RunTestsInSeparateProcesses; +use PHPUnit\Framework\TestCase; + +#[RunClassInSeparateProcess(true)] +#[RunTestsInSeparateProcesses] +final class ProcessIsolationForkedTest extends TestCase +{ + #[RunInSeparateProcess] + public function testOne(): void + { + } +} diff --git a/tests/_files/TestWithClassLevelIsolationAttributesForked.php b/tests/_files/TestWithClassLevelIsolationAttributesForked.php new file mode 100644 index 00000000000..4b363c3e690 --- /dev/null +++ b/tests/_files/TestWithClassLevelIsolationAttributesForked.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace PHPUnit\TestFixture\TestBuilder; + +use PHPUnit\Framework\Attributes\BackupGlobals; +use PHPUnit\Framework\Attributes\BackupStaticProperties; +use PHPUnit\Framework\Attributes\RunClassInSeparateProcess; +use PHPUnit\Framework\Attributes\RunTestsInSeparateProcesses; +use PHPUnit\Framework\TestCase; + +#[BackupGlobals(true)] +#[BackupStaticProperties(true)] +#[RunClassInSeparateProcess] +#[RunTestsInSeparateProcesses(true)] +final class TestWithClassLevelIsolationAttributesForked extends TestCase +{ + public function testOne(): void + { + } +} diff --git a/tests/_files/TestWithMethodLevelIsolationAttributesForked.php b/tests/_files/TestWithMethodLevelIsolationAttributesForked.php new file mode 100644 index 00000000000..8a5221cfd87 --- /dev/null +++ b/tests/_files/TestWithMethodLevelIsolationAttributesForked.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace PHPUnit\TestFixture\TestBuilder; + +use PHPUnit\Framework\Attributes\BackupGlobals; +use PHPUnit\Framework\Attributes\BackupStaticProperties; +use PHPUnit\Framework\Attributes\RunInSeparateProcess; +use PHPUnit\Framework\TestCase; + +final class TestWithMethodLevelIsolationAttributes extends TestCase +{ + #[BackupGlobals(true)] + #[BackupStaticProperties(true)] + #[RunInSeparateProcess(true)] + public function testOne(): void + { + } +}