diff --git a/composer.json b/composer.json index 8a17fafda2..58280b5ed1 100644 --- a/composer.json +++ b/composer.json @@ -24,7 +24,7 @@ "phpunit/phpunit": "^8.4.1", "phpspec/prophecy": "^1.7", "symfony/css-selector": "^4.4", - "symfony/phpunit-bridge": "^4.4", + "symfony/phpunit-bridge": "^5.1", "symfony/error-handler": "^4.4", "justinrainbow/json-schema": "^5.2", "symfony/filesystem": "^4.4", diff --git a/composer.lock b/composer.lock index 6b9e084e01..8c83b73a3d 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "b0f25f872cb3e35d4a7caf8c48aaa3c5", + "content-hash": "70ccf971aa2cc71e172543e89d868b90", "packages": [ { "name": "asm89/stack-cors", @@ -4540,9 +4540,6 @@ "ext-zip": "Enabling the zip extension allows you to unzip archives", "ext-zlib": "Allow gzip compression of HTTP requests" }, - "bin": [ - "bin/composer" - ], "type": "library", "extra": { "branch-alias": { @@ -7010,16 +7007,16 @@ }, { "name": "symfony/phpunit-bridge", - "version": "v4.4.10", + "version": "v5.1.2", "source": { "type": "git", "url": "https://github.com/symfony/phpunit-bridge.git", - "reference": "84cb4467ca3ea127f636806e2f6f27c5c1de4eb9" + "reference": "de5f0fec631a0cbfe98630b053be1fad7b75aece" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/phpunit-bridge/zipball/84cb4467ca3ea127f636806e2f6f27c5c1de4eb9", - "reference": "84cb4467ca3ea127f636806e2f6f27c5c1de4eb9", + "url": "https://api.github.com/repos/symfony/phpunit-bridge/zipball/de5f0fec631a0cbfe98630b053be1fad7b75aece", + "reference": "de5f0fec631a0cbfe98630b053be1fad7b75aece", "shasum": "" }, "require": { @@ -7037,7 +7034,7 @@ "type": "symfony-bridge", "extra": { "branch-alias": { - "dev-master": "4.4-dev" + "dev-master": "5.1-dev" }, "thanks": { "name": "phpunit/phpunit", @@ -7071,6 +7068,9 @@ ], "description": "Symfony PHPUnit Bridge", "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/phpunit-bridge/tree/v5.1.1" + }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -7085,7 +7085,7 @@ "type": "tidelift" } ], - "time": "2020-06-07T15:34:22+00:00" + "time": "2020-06-09T09:56:30+00:00" }, { "name": "theseer/tokenizer", diff --git a/composer/Metapackage/DevDependencies/composer.json b/composer/Metapackage/DevDependencies/composer.json index ceb0cffc4c..caa8e154c5 100644 --- a/composer/Metapackage/DevDependencies/composer.json +++ b/composer/Metapackage/DevDependencies/composer.json @@ -24,6 +24,6 @@ "symfony/filesystem": "^4.4", "symfony/finder": "^4.4", "symfony/lock": "^4.4", - "symfony/phpunit-bridge": "^4.4" + "symfony/phpunit-bridge": "^5.1" } } diff --git a/composer/Metapackage/PinnedDevDependencies/composer.json b/composer/Metapackage/PinnedDevDependencies/composer.json index a9a061b57a..7ac5fd9948 100644 --- a/composer/Metapackage/PinnedDevDependencies/composer.json +++ b/composer/Metapackage/PinnedDevDependencies/composer.json @@ -57,7 +57,7 @@ "symfony/filesystem": "v4.4.10", "symfony/finder": "v4.4.10", "symfony/lock": "v4.4.10", - "symfony/phpunit-bridge": "v4.4.10", + "symfony/phpunit-bridge": "v5.1.2", "theseer/tokenizer": "1.1.3", "webmozart/assert": "1.9.0" } diff --git a/core/phpunit.xml.dist b/core/phpunit.xml.dist index 42f4419115..5bd35d3c55 100644 --- a/core/phpunit.xml.dist +++ b/core/phpunit.xml.dist @@ -57,9 +57,6 @@ - - - diff --git a/core/tests/Drupal/Tests/Core/Test/PhpUnitBridgeRequiresTest.php b/core/tests/Drupal/Tests/Core/Test/PhpUnitBridgeRequiresTest.php new file mode 100644 index 0000000000..c806c5906f --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Test/PhpUnitBridgeRequiresTest.php @@ -0,0 +1,39 @@ +assertEquals('test', $deprecated->testFunction()); + } + + /** + * Data provider for ::testWillNeverRun(). + */ + public function providerTestWillNeverRun(): array { + return [ + ['this_will_never_run'], + ]; + } + +} diff --git a/core/tests/Drupal/Tests/Core/Test/PhpUnitBridgeTest.php b/core/tests/Drupal/Tests/Core/Test/PhpUnitBridgeTest.php index 73ba0de167..c430134cd0 100644 --- a/core/tests/Drupal/Tests/Core/Test/PhpUnitBridgeTest.php +++ b/core/tests/Drupal/Tests/Core/Test/PhpUnitBridgeTest.php @@ -26,4 +26,14 @@ public function testDeprecatedFunction() { $this->assertEquals('known_return_value', \deprecation_test_function()); } + /** + * Tests the @requires annotation. + * + * @requires extension will_hopefully_never_exist + */ + public function testWillNeverRun(): void { + $deprecated = new FixtureDeprecatedClass(); + $this->assertEquals('test', $deprecated->testFunction()); + } + } diff --git a/core/tests/Drupal/Tests/Listeners/AfterSymfonyListener.php b/core/tests/Drupal/Tests/Listeners/AfterSymfonyListener.php deleted file mode 100644 index e128468a38..0000000000 --- a/core/tests/Drupal/Tests/Listeners/AfterSymfonyListener.php +++ /dev/null @@ -1,24 +0,0 @@ -registerErrorHandler($test); - } if ($this->willBeIsolated($test)) { putenv('DRUPAL_EXPECTED_DEPRECATIONS_SERIALIZE=' . tempnam(sys_get_temp_dir(), 'exdep')); } @@ -159,8 +156,6 @@ public static function getSkippedDeprecations() { 'The "Twig\Environment::getTemplateClass()" method is considered internal. It may change without further notice. You should not extend it from "Drupal\Core\Template\TwigEnvironment".', '"Symfony\Component\DomCrawler\Crawler::text()" will normalize whitespaces by default in Symfony 5.0, set the second "$normalizeWhitespace" argument to false to retrieve the non-normalized version of the text.', // PHPUnit 8. - "The \"Drupal\Tests\Listeners\AfterSymfonyListener\" class implements \"PHPUnit\Framework\TestListener\" that is deprecated Use the `TestHook` interfaces instead.", - "The \"Drupal\Tests\Listeners\AfterSymfonyListener\" class uses \"PHPUnit\Framework\TestListenerDefaultImplementation\" that is deprecated The `TestListener` interface is deprecated.", "The \"PHPUnit\TextUI\ResultPrinter\" class is considered internal This class is not covered by the backward compatibility promise for PHPUnit. It may change without further notice. You should not use it from \"Drupal\Tests\Listeners\HtmlOutputPrinter\".", "The \"Drupal\Tests\Listeners\DrupalListener\" class implements \"PHPUnit\Framework\TestListener\" that is deprecated Use the `TestHook` interfaces instead.", "The \"Drupal\Tests\Listeners\DrupalListener\" class uses \"PHPUnit\Framework\TestListenerDefaultImplementation\" that is deprecated The `TestListener` interface is deprecated.", @@ -208,7 +203,10 @@ public static function getSkippedDeprecations() { * @see \Symfony\Bridge\PhpUnit\DeprecationErrorHandler * @see \Symfony\Bridge\PhpUnit\Legacy\SymfonyTestsListenerTrait */ - protected function registerErrorHandler($test) { + protected function registerErrorHandler() { + if ($this->previousHandler || 'disabled' === getenv('SYMFONY_DEPRECATIONS_HELPER')) { + return; + } $deprecation_handler = function ($type, $msg, $file, $line, $context = []) { // Skip listed deprecations. if ($type === E_USER_DEPRECATED && static::isDeprecationSkipped($msg)) { @@ -217,28 +215,18 @@ protected function registerErrorHandler($test) { return call_user_func($this->previousHandler, $type, $msg, $file, $line, $context); }; - if ($this->previousHandler) { - set_error_handler($deprecation_handler); - return; - } $this->previousHandler = set_error_handler($deprecation_handler); + } - // Register another listener so that we can remove the error handler before - // Symfony's DeprecationErrorHandler checks that it is the currently - // registered handler. Note this is done like this to ensure the error - // handler is removed after SymfonyTestsListenerTrait::endTest() is called. - // SymfonyTestsListenerTrait has its own error handler that needs to be - // removed before this one. - $test_result_object = $test->getTestResultObject(); - // It's possible that a test does not have a result object. This can happen - // when a test class does not have any test methods. - if ($test_result_object) { - $reflection_class = new \ReflectionClass($test_result_object); - $reflection_property = $reflection_class->getProperty('listeners'); - $reflection_property->setAccessible(TRUE); - $listeners = $reflection_property->getValue($test_result_object); - $listeners[] = new AfterSymfonyListener(); - $reflection_property->setValue($test_result_object, $listeners); + /** + * Removes the error handler if registered. + * + * @see \Drupal\Tests\Listeners\DeprecationListenerTrait::registerErrorHandler() + */ + protected function removeErrorHandler(): void { + if ($this->previousHandler) { + $this->previousHandler = NULL; + restore_error_handler(); } } diff --git a/core/tests/Drupal/Tests/Listeners/DrupalListener.php b/core/tests/Drupal/Tests/Listeners/DrupalListener.php index 83105089d0..bff353f0b1 100644 --- a/core/tests/Drupal/Tests/Listeners/DrupalListener.php +++ b/core/tests/Drupal/Tests/Listeners/DrupalListener.php @@ -5,10 +5,20 @@ use PHPUnit\Framework\TestListener; use PHPUnit\Framework\TestListenerDefaultImplementation; use PHPUnit\Framework\Test; +use PHPUnit\Framework\TestSuite; +use PHPUnit\Util\Test as UtilTest; +use Symfony\Bridge\PhpUnit\Legacy\SymfonyTestsListenerTrait; +use Symfony\Bridge\PhpUnit\SymfonyTestsListener; /** * Listens to PHPUnit test runs. * + * This listener orchestrates error handlers to ensure deprecations are skipped + * when \Drupal\Tests\Listeners\DeprecationListenerTrait::isDeprecationSkipped() + * returns TRUE. It removes test listeners to ensure that when + * \Symfony\Bridge\PhpUnit\DeprecationErrorHandler::shutdown() is run the error + * handler is in the expected state. + * * @internal */ class DrupalListener implements TestListener { @@ -33,12 +43,46 @@ class DrupalListener implements TestListener { 'onNotSuccessfulTest', ]; + /** + * The wrapped symfony test listener. + * + * @var \Symfony\Bridge\PhpUnit\SymfonyTestsListener + */ + private $symfonyListener; + + /** + * Constructs the DrupalListener object. + */ + public function __construct() { + $this->symfonyListener = new SymfonyTestsListener(); + } + + /** + * {@inheritdoc} + */ + public function startTestSuite(TestSuite $suite): void { + $this->symfonyListener->startTestSuite($suite); + $this->registerErrorHandler(); + } + + /** + * {@inheritdoc} + */ + public function addSkippedTest(Test $test, \Throwable $t, float $time): void { + $this->symfonyListener->addSkippedTest($test, $t, $time); + } + /** * {@inheritdoc} */ public function startTest(Test $test): void { + // The Drupal error handler has to be registered prior to the Symfony error + // handler that is registered in + // \Symfony\Bridge\PhpUnit\Legacy\SymfonyTestsListenerTrait::startTest() + // that handles expected deprecations. + $this->registerErrorHandler(); $this->deprecationStartTest($test); - + $this->symfonyListener->startTest($test); // Check for missing void return typehints in concrete test classes' // methods. If the method is inherited from a base test class, do // nothing. @@ -59,9 +103,30 @@ public function startTest(Test $test): void { * {@inheritdoc} */ public function endTest(Test $test, float $time): void { + if (!SymfonyTestsListenerTrait::$previousErrorHandler) { + $className = \get_class($test); + $groups = UtilTest::getGroups($className, $test->getName(FALSE)); + if (\in_array('legacy', $groups, TRUE)) { + // If the Symfony listener is not registered for legacy tests then + // deprecations triggered by the DebugClassloader in + // \Symfony\Bridge\PhpUnit\Legacy\SymfonyTestsListenerTrait::endTest() + // are not correctly identified as occurring in legacy tests. + $symfony_error_handler = set_error_handler([SymfonyTestsListenerTrait::class, 'handleError']); + } + } $this->deprecationEndTest($test, $time); + $this->symfonyListener->endTest($test, $time); $this->componentEndTest($test, $time); $this->standardsEndTest($test, $time); + if (isset($symfony_error_handler)) { + // If this test listener has added the Symfony error handler then it needs + // to be removed. + restore_error_handler(); + } + // The Drupal error handler has to be removed after the symfony error + // handler is potentially removed in + // \Symfony\Bridge\PhpUnit\Legacy\SymfonyTestsListenerTrait::endTest(). + $this->removeErrorHandler(); } } diff --git a/core/tests/Drupal/Tests/Traits/ExpectDeprecationTrait.php b/core/tests/Drupal/Tests/Traits/ExpectDeprecationTrait.php index d39e32dd54..d9ff33cf91 100644 --- a/core/tests/Drupal/Tests/Traits/ExpectDeprecationTrait.php +++ b/core/tests/Drupal/Tests/Traits/ExpectDeprecationTrait.php @@ -2,11 +2,7 @@ namespace Drupal\Tests\Traits; -use Symfony\Bridge\PhpUnit\Legacy\SymfonyTestsListener as LegacySymfonyTestsListener; -use Symfony\Bridge\PhpUnit\SymfonyTestsListener; -use PHPUnit\Framework\AssertionFailedError; -use PHPUnit\Framework\TestCase; -use PHPUnit\Util\Test; +use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait as SfExpectDeprecationTrait; /** * Adds the ability to dynamically set expected deprecation messages in tests. @@ -15,9 +11,11 @@ * This class should only be used by Drupal core and will be removed once * https://github.com/symfony/symfony/pull/25757 is resolved. * - * @todo Remove once https://github.com/symfony/symfony/pull/25757 is resolved. + * @todo https://www.drupal.org/project/drupal/issues/3157434 Deprecate the + * class and its methods. */ trait ExpectDeprecationTrait { + use SfExpectDeprecationTrait; /** * Sets an expected deprecation message. @@ -26,7 +24,11 @@ trait ExpectDeprecationTrait { * The expected deprecation message. */ protected function addExpectedDeprecationMessage($message) { - $this->expectedDeprecations([$message]); + // @todo Until https://github.com/symfony/symfony/pull/37515 is resolved we + // need to ensure the assertion count is incremented to avoid tests being + // marked as risky. + $this->addToAssertionCount(1); + $this->expectDeprecation($message); } /** @@ -36,76 +38,8 @@ protected function addExpectedDeprecationMessage($message) { * The expected deprecation messages. */ public function expectedDeprecations(array $messages) { - if ($this instanceof TestCase) { - // Ensure the class or method is in the legacy group. - $groups = Test::getGroups(get_class($this), $this->getName(FALSE)); - if (!in_array('legacy', $groups, TRUE)) { - throw new AssertionFailedError('Only tests with the `@group legacy` annotation can call `setExpectedDeprecation()`.'); - } - - // If setting an expected deprecation there is no need to be strict about - // testing nothing as this is an assertion. - $this->getTestResultObject() - ->beStrictAboutTestsThatDoNotTestAnything(FALSE); - - if ($trait = $this->getSymfonyTestListenerTrait()) { - // Add the expected deprecation message to the class property. - $reflection_class = new \ReflectionClass($trait); - $expected_deprecations_property = $reflection_class->getProperty('expectedDeprecations'); - $expected_deprecations_property->setAccessible(TRUE); - $expected_deprecations = $expected_deprecations_property->getValue($trait); - $expected_deprecations = array_merge($expected_deprecations, $messages); - $expected_deprecations_property->setValue($trait, $expected_deprecations); - - // Register the error handler if necessary. - $previous_error_handler_property = $reflection_class->getProperty('previousErrorHandler'); - $previous_error_handler_property->setAccessible(TRUE); - $previous_error_handler = $previous_error_handler_property->getValue($trait); - if (!$previous_error_handler) { - $previous_error_handler = set_error_handler([$trait, 'handleError']); - $previous_error_handler_property->setValue($trait, $previous_error_handler); - } - return; - } - } - - // Expected deprecations set by isolated tests need to be written to a file - // so that the test running process can take account of them. - if ($file = getenv('DRUPAL_EXPECTED_DEPRECATIONS_SERIALIZE')) { - $expected_deprecations = file_get_contents($file); - if ($expected_deprecations) { - $expected_deprecations = array_merge(unserialize($expected_deprecations), $messages); - } - else { - $expected_deprecations = $messages; - } - file_put_contents($file, serialize($expected_deprecations)); - return; - } - - throw new AssertionFailedError('Can not set an expected deprecation message because the Symfony\Bridge\PhpUnit\SymfonyTestsListener is not registered as a PHPUnit test listener.'); - } - - /** - * Gets the SymfonyTestsListenerTrait. - * - * @return \Symfony\Bridge\PhpUnit\Legacy\SymfonyTestsListenerTrait|null - * The SymfonyTestsListenerTrait object or NULL is a Symfony test listener - * is not present. - */ - private function getSymfonyTestListenerTrait() { - $test_result_object = $this->getTestResultObject(); - $reflection_class = new \ReflectionClass($test_result_object); - $reflection_property = $reflection_class->getProperty('listeners'); - $reflection_property->setAccessible(TRUE); - $listeners = $reflection_property->getValue($test_result_object); - foreach ($listeners as $listener) { - if ($listener instanceof SymfonyTestsListener || $listener instanceof LegacySymfonyTestsListener) { - $reflection_class = new \ReflectionClass($listener); - $reflection_property = $reflection_class->getProperty('trait'); - $reflection_property->setAccessible(TRUE); - return $reflection_property->getValue($listener); - } + foreach ($messages as $message) { + $this->addExpectedDeprecationMessage($message); } }