Commit 8af8df78 authored by catch's avatar catch
Browse files

Issue #3156998 by alexpott, mondrake, jibran: Using @requires extension_name...

Issue #3156998 by alexpott, mondrake, jibran: Using @requires extension_name in PHPUnit unit tests fails if extension is not present
parent aa8d81c7
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "b0f25f872cb3e35d4a7caf8c48aaa3c5", "content-hash": "70ccf971aa2cc71e172543e89d868b90",
"packages": [ "packages": [
{ {
"name": "asm89/stack-cors", "name": "asm89/stack-cors",
...@@ -4540,9 +4540,6 @@ ...@@ -4540,9 +4540,6 @@
"ext-zip": "Enabling the zip extension allows you to unzip archives", "ext-zip": "Enabling the zip extension allows you to unzip archives",
"ext-zlib": "Allow gzip compression of HTTP requests" "ext-zlib": "Allow gzip compression of HTTP requests"
}, },
"bin": [
"bin/composer"
],
"type": "library", "type": "library",
"extra": { "extra": {
"branch-alias": { "branch-alias": {
...@@ -7010,16 +7007,16 @@ ...@@ -7010,16 +7007,16 @@
}, },
{ {
"name": "symfony/phpunit-bridge", "name": "symfony/phpunit-bridge",
"version": "v4.4.10", "version": "v5.1.2",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/phpunit-bridge.git", "url": "https://github.com/symfony/phpunit-bridge.git",
"reference": "84cb4467ca3ea127f636806e2f6f27c5c1de4eb9" "reference": "de5f0fec631a0cbfe98630b053be1fad7b75aece"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/phpunit-bridge/zipball/84cb4467ca3ea127f636806e2f6f27c5c1de4eb9", "url": "https://api.github.com/repos/symfony/phpunit-bridge/zipball/de5f0fec631a0cbfe98630b053be1fad7b75aece",
"reference": "84cb4467ca3ea127f636806e2f6f27c5c1de4eb9", "reference": "de5f0fec631a0cbfe98630b053be1fad7b75aece",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
...@@ -7037,7 +7034,7 @@ ...@@ -7037,7 +7034,7 @@
"type": "symfony-bridge", "type": "symfony-bridge",
"extra": { "extra": {
"branch-alias": { "branch-alias": {
"dev-master": "4.4-dev" "dev-master": "5.1-dev"
}, },
"thanks": { "thanks": {
"name": "phpunit/phpunit", "name": "phpunit/phpunit",
...@@ -7071,6 +7068,9 @@ ...@@ -7071,6 +7068,9 @@
], ],
"description": "Symfony PHPUnit Bridge", "description": "Symfony PHPUnit Bridge",
"homepage": "https://symfony.com", "homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/phpunit-bridge/tree/v5.1.1"
},
"funding": [ "funding": [
{ {
"url": "https://symfony.com/sponsor", "url": "https://symfony.com/sponsor",
...@@ -7085,7 +7085,7 @@ ...@@ -7085,7 +7085,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2020-06-07T15:34:22+00:00" "time": "2020-06-09T09:56:30+00:00"
}, },
{ {
"name": "theseer/tokenizer", "name": "theseer/tokenizer",
......
...@@ -24,6 +24,6 @@ ...@@ -24,6 +24,6 @@
"symfony/filesystem": "^4.4", "symfony/filesystem": "^4.4",
"symfony/finder": "^4.4", "symfony/finder": "^4.4",
"symfony/lock": "^4.4", "symfony/lock": "^4.4",
"symfony/phpunit-bridge": "^4.4" "symfony/phpunit-bridge": "^5.1"
} }
} }
...@@ -57,7 +57,7 @@ ...@@ -57,7 +57,7 @@
"symfony/filesystem": "v4.4.10", "symfony/filesystem": "v4.4.10",
"symfony/finder": "v4.4.10", "symfony/finder": "v4.4.10",
"symfony/lock": "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", "theseer/tokenizer": "1.1.3",
"webmozart/assert": "1.9.0" "webmozart/assert": "1.9.0"
} }
......
...@@ -57,9 +57,6 @@ ...@@ -57,9 +57,6 @@
<listeners> <listeners>
<listener class="\Drupal\Tests\Listeners\DrupalListener"> <listener class="\Drupal\Tests\Listeners\DrupalListener">
</listener> </listener>
<!-- The Symfony deprecation listener has to come after the Drupal listener -->
<listener class="Symfony\Bridge\PhpUnit\SymfonyTestsListener">
</listener>
</listeners> </listeners>
<!-- Filter for coverage reports. --> <!-- Filter for coverage reports. -->
<filter> <filter>
......
<?php
namespace Drupal\Tests\Core\Test;
use Drupal\Tests\UnitTestCase;
use Drupal\deprecation_test\Deprecation\FixtureDeprecatedClass;
/**
* Test how unit tests interact with deprecation errors.
*
* If a test requires an extension that does not exist and has a data provider
* the interaction between Drupal and Symfony's deprecation testing can cause
* errors. This test proves this is not broken.
*
* This test will be skipped and should not cause the test suite to fail.
*
* @group Test
* @requires extension will_hopefully_never_exist
* @see \Drupal\Tests\Listeners\DrupalListener
*/
class PhpUnitBridgeRequiresTest extends UnitTestCase {
/**
* Tests the @requires annotation.
*
* @dataProvider providerTestWillNeverRun
*/
public function testWillNeverRun(): void {
$deprecated = new FixtureDeprecatedClass();
$this->assertEquals('test', $deprecated->testFunction());
}
/**
* Data provider for ::testWillNeverRun().
*/
public function providerTestWillNeverRun(): array {
return [
['this_will_never_run'],
];
}
}
...@@ -26,4 +26,18 @@ public function testDeprecatedFunction() { ...@@ -26,4 +26,18 @@ public function testDeprecatedFunction() {
$this->assertEquals('known_return_value', \deprecation_test_function()); $this->assertEquals('known_return_value', \deprecation_test_function());
} }
/**
* Tests the @requires annotation in conjunction with DrupalListener.
*
* This test method will be skipped and should not cause the test suite to
* fail.
*
* @requires extension will_hopefully_never_exist
* @see \Drupal\Tests\Listeners\DrupalListener
*/
public function testWillNeverRun(): void {
$deprecated = new FixtureDeprecatedClass();
$this->assertEquals('test', $deprecated->testFunction());
}
} }
<?php
namespace Drupal\Tests\Listeners;
use PHPUnit\Framework\Test;
use PHPUnit\Framework\TestListener;
use PHPUnit\Framework\TestListenerDefaultImplementation;
/**
* Listens to PHPUnit test runs.
*
* @internal
*/
class AfterSymfonyListener implements TestListener {
use TestListenerDefaultImplementation;
/**
* {@inheritdoc}
*/
public function endTest(Test $test, float $time): void {
restore_error_handler();
}
}
...@@ -26,9 +26,6 @@ trait DeprecationListenerTrait { ...@@ -26,9 +26,6 @@ trait DeprecationListenerTrait {
protected function deprecationStartTest($test) { protected function deprecationStartTest($test) {
if ($test instanceof TestCase) { if ($test instanceof TestCase) {
if ('disabled' !== getenv('SYMFONY_DEPRECATIONS_HELPER')) {
$this->registerErrorHandler($test);
}
if ($this->willBeIsolated($test)) { if ($this->willBeIsolated($test)) {
putenv('DRUPAL_EXPECTED_DEPRECATIONS_SERIALIZE=' . tempnam(sys_get_temp_dir(), 'exdep')); putenv('DRUPAL_EXPECTED_DEPRECATIONS_SERIALIZE=' . tempnam(sys_get_temp_dir(), 'exdep'));
} }
...@@ -157,8 +154,6 @@ public static function getSkippedDeprecations() { ...@@ -157,8 +154,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".', '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.', '"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. // 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 \"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 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.", "The \"Drupal\Tests\Listeners\DrupalListener\" class uses \"PHPUnit\Framework\TestListenerDefaultImplementation\" that is deprecated The `TestListener` interface is deprecated.",
...@@ -201,7 +196,10 @@ public static function getSkippedDeprecations() { ...@@ -201,7 +196,10 @@ public static function getSkippedDeprecations() {
* @see \Symfony\Bridge\PhpUnit\DeprecationErrorHandler * @see \Symfony\Bridge\PhpUnit\DeprecationErrorHandler
* @see \Symfony\Bridge\PhpUnit\Legacy\SymfonyTestsListenerTrait * @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 = []) { $deprecation_handler = function ($type, $msg, $file, $line, $context = []) {
// Skip listed deprecations. // Skip listed deprecations.
if ($type === E_USER_DEPRECATED && static::isDeprecationSkipped($msg)) { if ($type === E_USER_DEPRECATED && static::isDeprecationSkipped($msg)) {
...@@ -210,28 +208,18 @@ protected function registerErrorHandler($test) { ...@@ -210,28 +208,18 @@ protected function registerErrorHandler($test) {
return call_user_func($this->previousHandler, $type, $msg, $file, $line, $context); 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); $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 * Removes the error handler if registered.
// registered handler. Note this is done like this to ensure the error *
// handler is removed after SymfonyTestsListenerTrait::endTest() is called. * @see \Drupal\Tests\Listeners\DeprecationListenerTrait::registerErrorHandler()
// SymfonyTestsListenerTrait has its own error handler that needs to be */
// removed before this one. protected function removeErrorHandler(): void {
$test_result_object = $test->getTestResultObject(); if ($this->previousHandler) {
// It's possible that a test does not have a result object. This can happen $this->previousHandler = NULL;
// when a test class does not have any test methods. restore_error_handler();
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);
} }
} }
......
...@@ -5,10 +5,20 @@ ...@@ -5,10 +5,20 @@
use PHPUnit\Framework\TestListener; use PHPUnit\Framework\TestListener;
use PHPUnit\Framework\TestListenerDefaultImplementation; use PHPUnit\Framework\TestListenerDefaultImplementation;
use PHPUnit\Framework\Test; 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. * 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 * @internal
*/ */
class DrupalListener implements TestListener { class DrupalListener implements TestListener {
...@@ -33,12 +43,46 @@ class DrupalListener implements TestListener { ...@@ -33,12 +43,46 @@ class DrupalListener implements TestListener {
'onNotSuccessfulTest', '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} * {@inheritdoc}
*/ */
public function startTest(Test $test): void { 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->deprecationStartTest($test);
$this->symfonyListener->startTest($test);
// Check for missing void return typehints in concrete test classes' // Check for missing void return typehints in concrete test classes'
// methods. If the method is inherited from a base test class, do // methods. If the method is inherited from a base test class, do
// nothing. // nothing.
...@@ -59,9 +103,30 @@ public function startTest(Test $test): void { ...@@ -59,9 +103,30 @@ public function startTest(Test $test): void {
* {@inheritdoc} * {@inheritdoc}
*/ */
public function endTest(Test $test, float $time): void { 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->deprecationEndTest($test, $time);
$this->symfonyListener->endTest($test, $time);
$this->componentEndTest($test, $time); $this->componentEndTest($test, $time);
$this->standardsEndTest($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();
} }
} }
...@@ -2,20 +2,15 @@ ...@@ -2,20 +2,15 @@
namespace Drupal\Tests\Traits; namespace Drupal\Tests\Traits;
use Symfony\Bridge\PhpUnit\Legacy\SymfonyTestsListener as LegacySymfonyTestsListener; use Symfony\Bridge\PhpUnit\Legacy\SymfonyTestsListenerTrait;
use Symfony\Bridge\PhpUnit\SymfonyTestsListener;
use PHPUnit\Framework\AssertionFailedError;
use PHPUnit\Framework\TestCase;
use PHPUnit\Util\Test;
/** /**
* Adds the ability to dynamically set expected deprecation messages in tests. * Adds the ability to dynamically set expected deprecation messages in tests.
* *
* @internal * @internal
* 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
* trait and its methods.
*/ */
trait ExpectDeprecationTrait { trait ExpectDeprecationTrait {
...@@ -34,40 +29,11 @@ protected function addExpectedDeprecationMessage($message) { ...@@ -34,40 +29,11 @@ protected function addExpectedDeprecationMessage($message) {
* *
* @param string[] $messages * @param string[] $messages
* The expected deprecation messages. * The expected deprecation messages.
*
* @see \Symfony\Bridge\PhpUnit\Legacy\ExpectDeprecationTraitForV8_4::expectDeprecation()
*/ */
public function expectedDeprecations(array $messages) { public function expectedDeprecations(array $messages) {
if ($this instanceof TestCase) { $this->getTestResultObject()->beStrictAboutTestsThatDoNotTestAnything(FALSE);
// Ensure the class or method is in the legacy group.
$groups = Test::getGroups(static::class, $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 // Expected deprecations set by isolated tests need to be written to a file
// so that the test running process can take account of them. // so that the test running process can take account of them.
...@@ -80,31 +46,15 @@ public function expectedDeprecations(array $messages) { ...@@ -80,31 +46,15 @@ public function expectedDeprecations(array $messages) {
$expected_deprecations = $messages; $expected_deprecations = $messages;
} }
file_put_contents($file, serialize($expected_deprecations)); file_put_contents($file, serialize($expected_deprecations));
return;
} }
else {
// Copy code from ExpectDeprecationTraitForV8_4::expectDeprecation().
if (!SymfonyTestsListenerTrait::$previousErrorHandler) {
SymfonyTestsListenerTrait::$previousErrorHandler = set_error_handler([SymfonyTestsListenerTrait::class, 'handleError']);
}
throw new AssertionFailedError('Can not set an expected deprecation message because the Symfony\Bridge\PhpUnit\SymfonyTestsListener is not registered as a PHPUnit test listener.'); foreach ($messages as $message) {
} SymfonyTestsListenerTrait::$expectedDeprecations[] = $message;
/**
* 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);
} }
} }
} }
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment