diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml
index dfc5b0e63728f..0e8c7cc123143 100644
--- a/.github/workflows/unit-tests.yml
+++ b/.github/workflows/unit-tests.yml
@@ -239,3 +239,11 @@ jobs:
mkdir -p /opt/php/lib
echo memory_limit=-1 > /opt/php/lib/php.ini
./build/php/bin/php ./phpunit --colors=always src/Symfony/Component/Process
+
+ - name: Run PhpUnitBridge tests with PHPUnit 11
+ if: '! matrix.mode'
+ run: |
+ ./phpunit src/Symfony/Bridge/PhpUnit
+ env:
+ SYMFONY_PHPUNIT_VERSION: '11.3'
+ SYMFONY_DEPRECATIONS_HELPER: 'disabled'
diff --git a/src/Symfony/Bridge/PhpUnit/CHANGELOG.md b/src/Symfony/Bridge/PhpUnit/CHANGELOG.md
index a2dc7bd6706e0..3c747025792f5 100644
--- a/src/Symfony/Bridge/PhpUnit/CHANGELOG.md
+++ b/src/Symfony/Bridge/PhpUnit/CHANGELOG.md
@@ -4,6 +4,7 @@ CHANGELOG
7.2
---
+ * Add a PHPUnit extension that registers the clock mock and DNS mock and the `DebugClassLoader` from the ErrorHandler component if present
* Add `ExpectUserDeprecationMessageTrait` with a polyfill of PHPUnit's `expectUserDeprecationMessage()`
* Use `total` for asserting deprecation count when a group is not defined
diff --git a/src/Symfony/Bridge/PhpUnit/Extension/DisableClockMockSubscriber.php b/src/Symfony/Bridge/PhpUnit/Extension/DisableClockMockSubscriber.php
new file mode 100644
index 0000000000000..885e6ea585e54
--- /dev/null
+++ b/src/Symfony/Bridge/PhpUnit/Extension/DisableClockMockSubscriber.php
@@ -0,0 +1,39 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Bridge\PhpUnit\Extension;
+
+use PHPUnit\Event\Code\TestMethod;
+use PHPUnit\Event\Test\Finished;
+use PHPUnit\Event\Test\FinishedSubscriber;
+use PHPUnit\Metadata\Group;
+use Symfony\Bridge\PhpUnit\ClockMock;
+
+/**
+ * @internal
+ */
+class DisableClockMockSubscriber implements FinishedSubscriber
+{
+ public function notify(Finished $event): void
+ {
+ $test = $event->test();
+
+ if (!$test instanceof TestMethod) {
+ return;
+ }
+
+ foreach ($test->metadata() as $metadata) {
+ if ($metadata instanceof Group && 'time-sensitive' === $metadata->groupName()) {
+ ClockMock::withClockMock(false);
+ }
+ }
+ }
+}
diff --git a/src/Symfony/Bridge/PhpUnit/Extension/DisableDnsMockSubscriber.php b/src/Symfony/Bridge/PhpUnit/Extension/DisableDnsMockSubscriber.php
new file mode 100644
index 0000000000000..fc3e754d140d5
--- /dev/null
+++ b/src/Symfony/Bridge/PhpUnit/Extension/DisableDnsMockSubscriber.php
@@ -0,0 +1,39 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Bridge\PhpUnit\Extension;
+
+use PHPUnit\Event\Code\TestMethod;
+use PHPUnit\Event\Test\Finished;
+use PHPUnit\Event\Test\FinishedSubscriber;
+use PHPUnit\Metadata\Group;
+use Symfony\Bridge\PhpUnit\DnsMock;
+
+/**
+ * @internal
+ */
+class DisableDnsMockSubscriber implements FinishedSubscriber
+{
+ public function notify(Finished $event): void
+ {
+ $test = $event->test();
+
+ if (!$test instanceof TestMethod) {
+ return;
+ }
+
+ foreach ($test->metadata() as $metadata) {
+ if ($metadata instanceof Group && 'dns-sensitive' === $metadata->groupName()) {
+ DnsMock::withMockedHosts([]);
+ }
+ }
+ }
+}
diff --git a/src/Symfony/Bridge/PhpUnit/Extension/EnableClockMockSubscriber.php b/src/Symfony/Bridge/PhpUnit/Extension/EnableClockMockSubscriber.php
new file mode 100644
index 0000000000000..c10c5dcd18cd5
--- /dev/null
+++ b/src/Symfony/Bridge/PhpUnit/Extension/EnableClockMockSubscriber.php
@@ -0,0 +1,39 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Bridge\PhpUnit\Extension;
+
+use PHPUnit\Event\Code\TestMethod;
+use PHPUnit\Event\Test\PreparationStarted;
+use PHPUnit\Event\Test\PreparationStartedSubscriber;
+use PHPUnit\Metadata\Group;
+use Symfony\Bridge\PhpUnit\ClockMock;
+
+/**
+ * @internal
+ */
+class EnableClockMockSubscriber implements PreparationStartedSubscriber
+{
+ public function notify(PreparationStarted $event): void
+ {
+ $test = $event->test();
+
+ if (!$test instanceof TestMethod) {
+ return;
+ }
+
+ foreach ($test->metadata() as $metadata) {
+ if ($metadata instanceof Group && 'time-sensitive' === $metadata->groupName()) {
+ ClockMock::withClockMock(true);
+ }
+ }
+ }
+}
diff --git a/src/Symfony/Bridge/PhpUnit/Extension/RegisterClockMockSubscriber.php b/src/Symfony/Bridge/PhpUnit/Extension/RegisterClockMockSubscriber.php
new file mode 100644
index 0000000000000..e2955fe6003e8
--- /dev/null
+++ b/src/Symfony/Bridge/PhpUnit/Extension/RegisterClockMockSubscriber.php
@@ -0,0 +1,39 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Bridge\PhpUnit\Extension;
+
+use PHPUnit\Event\Code\TestMethod;
+use PHPUnit\Event\TestSuite\Loaded;
+use PHPUnit\Event\TestSuite\LoadedSubscriber;
+use PHPUnit\Metadata\Group;
+use Symfony\Bridge\PhpUnit\ClockMock;
+
+/**
+ * @internal
+ */
+class RegisterClockMockSubscriber implements LoadedSubscriber
+{
+ public function notify(Loaded $event): void
+ {
+ foreach ($event->testSuite()->tests() as $test) {
+ if (!$test instanceof TestMethod) {
+ continue;
+ }
+
+ foreach ($test->metadata() as $metadata) {
+ if ($metadata instanceof Group && 'time-sensitive' === $metadata->groupName()) {
+ ClockMock::register($test->className());
+ }
+ }
+ }
+ }
+}
diff --git a/src/Symfony/Bridge/PhpUnit/Extension/RegisterDnsMockSubscriber.php b/src/Symfony/Bridge/PhpUnit/Extension/RegisterDnsMockSubscriber.php
new file mode 100644
index 0000000000000..81382d5e13b43
--- /dev/null
+++ b/src/Symfony/Bridge/PhpUnit/Extension/RegisterDnsMockSubscriber.php
@@ -0,0 +1,39 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Bridge\PhpUnit\Extension;
+
+use PHPUnit\Event\Code\TestMethod;
+use PHPUnit\Event\TestSuite\Loaded;
+use PHPUnit\Event\TestSuite\LoadedSubscriber;
+use PHPUnit\Metadata\Group;
+use Symfony\Bridge\PhpUnit\DnsMock;
+
+/**
+ * @internal
+ */
+class RegisterDnsMockSubscriber implements LoadedSubscriber
+{
+ public function notify(Loaded $event): void
+ {
+ foreach ($event->testSuite()->tests() as $test) {
+ if (!$test instanceof TestMethod) {
+ continue;
+ }
+
+ foreach ($test->metadata() as $metadata) {
+ if ($metadata instanceof Group && 'dns-sensitive' === $metadata->groupName()) {
+ DnsMock::register($test->className());
+ }
+ }
+ }
+ }
+}
diff --git a/src/Symfony/Bridge/PhpUnit/SymfonyExtension.php b/src/Symfony/Bridge/PhpUnit/SymfonyExtension.php
new file mode 100644
index 0000000000000..1df4f20658905
--- /dev/null
+++ b/src/Symfony/Bridge/PhpUnit/SymfonyExtension.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 Symfony\Bridge\PhpUnit;
+
+use PHPUnit\Runner\Extension\Extension;
+use PHPUnit\Runner\Extension\Facade;
+use PHPUnit\Runner\Extension\ParameterCollection;
+use PHPUnit\TextUI\Configuration\Configuration;
+use Symfony\Bridge\PhpUnit\Extension\DisableClockMockSubscriber;
+use Symfony\Bridge\PhpUnit\Extension\DisableDnsMockSubscriber;
+use Symfony\Bridge\PhpUnit\Extension\EnableClockMockSubscriber;
+use Symfony\Bridge\PhpUnit\Extension\RegisterClockMockSubscriber;
+use Symfony\Bridge\PhpUnit\Extension\RegisterDnsMockSubscriber;
+use Symfony\Component\ErrorHandler\DebugClassLoader;
+
+class SymfonyExtension implements Extension
+{
+ public function bootstrap(Configuration $configuration, Facade $facade, ParameterCollection $parameters): void
+ {
+ if (class_exists(DebugClassLoader::class)) {
+ DebugClassLoader::enable();
+ }
+
+ if ($parameters->has('clock-mock-namespaces')) {
+ foreach (explode(',', $parameters->get('clock-mock-namespaces')) as $namespace) {
+ ClockMock::register($namespace.'\DummyClass');
+ }
+ }
+
+ $facade->registerSubscriber(new RegisterClockMockSubscriber());
+ $facade->registerSubscriber(new EnableClockMockSubscriber());
+ $facade->registerSubscriber(new DisableClockMockSubscriber());
+
+ if ($parameters->has('dns-mock-namespaces')) {
+ foreach (explode(',', $parameters->get('dns-mock-namespaces')) as $namespace) {
+ DnsMock::register($namespace.'\DummyClass');
+ }
+ }
+
+ $facade->registerSubscriber(new RegisterDnsMockSubscriber());
+ $facade->registerSubscriber(new DisableDnsMockSubscriber());
+ }
+}
diff --git a/src/Symfony/Bridge/PhpUnit/Tests/CoverageListenerTest.php b/src/Symfony/Bridge/PhpUnit/Tests/CoverageListenerTest.php
index 22f3565fab44c..99d4a4bcfcee8 100644
--- a/src/Symfony/Bridge/PhpUnit/Tests/CoverageListenerTest.php
+++ b/src/Symfony/Bridge/PhpUnit/Tests/CoverageListenerTest.php
@@ -13,6 +13,9 @@
use PHPUnit\Framework\TestCase;
+/**
+ * @requires PHPUnit < 10
+ */
class CoverageListenerTest extends TestCase
{
public function test()
diff --git a/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/ConfigurationTest.php b/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/ConfigurationTest.php
index 5bbf714dd2cc3..7eec02954c1ca 100644
--- a/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/ConfigurationTest.php
+++ b/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/ConfigurationTest.php
@@ -463,6 +463,9 @@ public function testExistingBaselineAndGeneration()
$this->assertEquals(json_encode($expected, \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES), file_get_contents($filename));
}
+ /**
+ * @requires PHPUnit < 10
+ */
public function testBaselineGenerationWithDeprecationTriggeredByDebugClassLoader()
{
$filename = $this->createFile();
diff --git a/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/log_file.phpt b/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/log_file.phpt
index 0a64337d08089..fe14db7c53da5 100644
--- a/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/log_file.phpt
+++ b/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/log_file.phpt
@@ -1,5 +1,7 @@
--TEST--
Test DeprecationErrorHandler with log file
+--SKIPIF--
+=')) die('Skipping on PHPUnit 10+');
--FILE--
+
+
+
+ tests
+
+
+
+
+
+ src
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Symfony/Bridge/PhpUnit/Tests/Fixtures/symfonyextension/phpunit-without-extension.xml.dist b/src/Symfony/Bridge/PhpUnit/Tests/Fixtures/symfonyextension/phpunit-without-extension.xml.dist
new file mode 100644
index 0000000000000..843be2fafdfb3
--- /dev/null
+++ b/src/Symfony/Bridge/PhpUnit/Tests/Fixtures/symfonyextension/phpunit-without-extension.xml.dist
@@ -0,0 +1,22 @@
+
+
+
+
+ tests
+
+
+
+
+
+ src
+
+
+
diff --git a/src/Symfony/Bridge/PhpUnit/Tests/Fixtures/symfonyextension/src/ClassExtendingFinalClass.php b/src/Symfony/Bridge/PhpUnit/Tests/Fixtures/symfonyextension/src/ClassExtendingFinalClass.php
new file mode 100644
index 0000000000000..e3377aaf15f5b
--- /dev/null
+++ b/src/Symfony/Bridge/PhpUnit/Tests/Fixtures/symfonyextension/src/ClassExtendingFinalClass.php
@@ -0,0 +1,16 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Bridge\PhpUnit\Tests\Fixtures\symfonyextension\src;
+
+class ClassExtendingFinalClass extends FinalClass
+{
+}
diff --git a/src/Symfony/Bridge/PhpUnit/Tests/Fixtures/symfonyextension/src/FinalClass.php b/src/Symfony/Bridge/PhpUnit/Tests/Fixtures/symfonyextension/src/FinalClass.php
new file mode 100644
index 0000000000000..8a320dd347cac
--- /dev/null
+++ b/src/Symfony/Bridge/PhpUnit/Tests/Fixtures/symfonyextension/src/FinalClass.php
@@ -0,0 +1,19 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Bridge\PhpUnit\Tests\Fixtures\symfonyextension\src;
+
+/**
+ * @final
+ */
+class FinalClass
+{
+}
diff --git a/src/Symfony/Bridge/PhpUnit/Tests/Fixtures/symfonyextension/tests/bootstrap.php b/src/Symfony/Bridge/PhpUnit/Tests/Fixtures/symfonyextension/tests/bootstrap.php
new file mode 100644
index 0000000000000..95dcc78ef026c
--- /dev/null
+++ b/src/Symfony/Bridge/PhpUnit/Tests/Fixtures/symfonyextension/tests/bootstrap.php
@@ -0,0 +1,34 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+use Symfony\Bridge\PhpUnit\Tests\Fixtures\symfonyextension\src\ClassExtendingFinalClass;
+use Symfony\Bridge\PhpUnit\Tests\Fixtures\symfonyextension\src\FinalClass;
+
+spl_autoload_register(function ($class) {
+ if (FinalClass::class === $class) {
+ require __DIR__.'/../src/FinalClass.php';
+ } elseif (ClassExtendingFinalClass::class === $class) {
+ require __DIR__.'/../src/ClassExtendingFinalClass.php';
+ }
+});
+
+require __DIR__.'/../../../../SymfonyExtension.php';
+require __DIR__.'/../../../../Extension/DisableClockMockSubscriber.php';
+require __DIR__.'/../../../../Extension/DisableDnsMockSubscriber.php';
+require __DIR__.'/../../../../Extension/EnableClockMockSubscriber.php';
+require __DIR__.'/../../../../Extension/RegisterClockMockSubscriber.php';
+require __DIR__.'/../../../../Extension/RegisterDnsMockSubscriber.php';
+
+if (file_exists(__DIR__.'/../../../../vendor/autoload.php')) {
+ require __DIR__.'/../../../../vendor/autoload.php';
+} elseif (file_exists(__DIR__.'/../../../..//../../../../vendor/autoload.php')) {
+ require __DIR__.'/../../../../../../../../vendor/autoload.php';
+}
diff --git a/src/Symfony/Bridge/PhpUnit/Tests/ProcessIsolationTest.php b/src/Symfony/Bridge/PhpUnit/Tests/ProcessIsolationTest.php
index 04bf6ec80776a..07fb9a2287f06 100644
--- a/src/Symfony/Bridge/PhpUnit/Tests/ProcessIsolationTest.php
+++ b/src/Symfony/Bridge/PhpUnit/Tests/ProcessIsolationTest.php
@@ -19,6 +19,8 @@
* @group legacy
*
* @runTestsInSeparateProcesses
+ *
+ * @requires PHPUnit < 10
*/
class ProcessIsolationTest extends TestCase
{
diff --git a/src/Symfony/Bridge/PhpUnit/Tests/SymfonyExtension.php b/src/Symfony/Bridge/PhpUnit/Tests/SymfonyExtension.php
new file mode 100644
index 0000000000000..ac2d90757bbaf
--- /dev/null
+++ b/src/Symfony/Bridge/PhpUnit/Tests/SymfonyExtension.php
@@ -0,0 +1,140 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Bridge\PhpUnit\Tests;
+
+use PHPUnit\Framework\Attributes\DataProvider;
+use PHPUnit\Framework\Attributes\Group;
+use PHPUnit\Framework\TestCase;
+use Symfony\Bridge\PhpUnit\Tests\Fixtures\symfonyextension\src\ClassExtendingFinalClass;
+use Symfony\Bridge\PhpUnit\Tests\Fixtures\symfonyextension\src\FinalClass;
+
+class SymfonyExtension extends TestCase
+{
+ public function testExtensionOfFinalClass()
+ {
+ $this->expectUserDeprecationMessage(\sprintf('The "%s" class is considered final. It may change without further notice as of its next major version. You should not extend it from "%s".', FinalClass::class, ClassExtendingFinalClass::class));
+
+ new ClassExtendingFinalClass();
+ }
+
+ #[DataProvider('mockedNamespaces')]
+ #[Group('time-sensitive')]
+ public function testTimeMockIsRegistered(string $namespace)
+ {
+ $this->assertTrue(\function_exists(\sprintf('%s\time', $namespace)));
+ }
+
+ #[DataProvider('mockedNamespaces')]
+ #[Group('time-sensitive')]
+ public function testMicrotimeMockIsRegistered(string $namespace)
+ {
+ $this->assertTrue(\function_exists(\sprintf('%s\microtime', $namespace)));
+ }
+
+ #[DataProvider('mockedNamespaces')]
+ #[Group('time-sensitive')]
+ public function testSleepMockIsRegistered(string $namespace)
+ {
+ $this->assertTrue(\function_exists(\sprintf('%s\sleep', $namespace)));
+ }
+
+ #[DataProvider('mockedNamespaces')]
+ #[Group('time-sensitive')]
+ public function testUsleepMockIsRegistered(string $namespace)
+ {
+ $this->assertTrue(\function_exists(\sprintf('%s\usleep', $namespace)));
+ }
+
+ #[DataProvider('mockedNamespaces')]
+ #[Group('time-sensitive')]
+ public function testDateMockIsRegistered(string $namespace)
+ {
+ $this->assertTrue(\function_exists(\sprintf('%s\date', $namespace)));
+ }
+
+ #[DataProvider('mockedNamespaces')]
+ #[Group('time-sensitive')]
+ public function testGmdateMockIsRegistered(string $namespace)
+ {
+ $this->assertTrue(\function_exists(\sprintf('%s\gmdate', $namespace)));
+ }
+
+ #[DataProvider('mockedNamespaces')]
+ #[Group('time-sensitive')]
+ public function testHrtimeMockIsRegistered(string $namespace)
+ {
+ $this->assertTrue(\function_exists(\sprintf('%s\hrtime', $namespace)));
+ }
+
+ #[DataProvider('mockedNamespaces')]
+ #[Group('dns-sensitive')]
+ public function testCheckdnsrrMockIsRegistered(string $namespace)
+ {
+ $this->assertTrue(\function_exists(\sprintf('%s\checkdnsrr', $namespace)));
+ }
+
+ #[DataProvider('mockedNamespaces')]
+ #[Group('dns-sensitive')]
+ public function testDnsCheckRecordMockIsRegistered(string $namespace)
+ {
+ $this->assertTrue(\function_exists(\sprintf('%s\dns_check_record', $namespace)));
+ }
+
+ #[DataProvider('mockedNamespaces')]
+ #[Group('dns-sensitive')]
+ public function testGetmxrrMockIsRegistered(string $namespace)
+ {
+ $this->assertTrue(\function_exists(\sprintf('%s\getmxrr', $namespace)));
+ }
+
+ #[DataProvider('mockedNamespaces')]
+ #[Group('dns-sensitive')]
+ public function testDnsGetMxMockIsRegistered(string $namespace)
+ {
+ $this->assertTrue(\function_exists(\sprintf('%s\dns_get_mx', $namespace)));
+ }
+
+ #[DataProvider('mockedNamespaces')]
+ #[Group('dns-sensitive')]
+ public function testGethostbyaddrMockIsRegistered(string $namespace)
+ {
+ $this->assertTrue(\function_exists(\sprintf('%s\gethostbyaddr', $namespace)));
+ }
+
+ #[DataProvider('mockedNamespaces')]
+ #[Group('dns-sensitive')]
+ public function testGethostbynameMockIsRegistered(string $namespace)
+ {
+ $this->assertTrue(\function_exists(\sprintf('%s\gethostbyname', $namespace)));
+ }
+
+ #[DataProvider('mockedNamespaces')]
+ #[Group('dns-sensitive')]
+ public function testGethostbynamelMockIsRegistered(string $namespace)
+ {
+ $this->assertTrue(\function_exists(\sprintf('%s\gethostbynamel', $namespace)));
+ }
+
+ #[DataProvider('mockedNamespaces')]
+ #[Group('dns-sensitive')]
+ public function testDnsGetRecordMockIsRegistered(string $namespace)
+ {
+ $this->assertTrue(\function_exists(\sprintf('%s\dns_get_record', $namespace)));
+ }
+
+ public static function mockedNamespaces(): iterable
+ {
+ yield 'test class namespace' => [__NAMESPACE__];
+ yield 'namespace derived from test namespace' => ['Symfony\Bridge\PhpUnit'];
+ yield 'explicitly configured namespace' => ['App'];
+ }
+}
diff --git a/src/Symfony/Bridge/PhpUnit/Tests/expectdeprecationfail.phpt b/src/Symfony/Bridge/PhpUnit/Tests/expectdeprecationfail.phpt
index f968cd188a0a7..61811467f77a2 100644
--- a/src/Symfony/Bridge/PhpUnit/Tests/expectdeprecationfail.phpt
+++ b/src/Symfony/Bridge/PhpUnit/Tests/expectdeprecationfail.phpt
@@ -1,5 +1,7 @@
--TEST--
Test ExpectDeprecationTrait failing tests
+--SKIPIF--
+=')) die('Skipping on PHPUnit 10+');
--FILE--
=')) die('Skipping on PHPUnit 10+');
--FILE--
=')) die('Skipping on PHPUnit 10+');
--FILE--