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 @@
"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",
......
......@@ -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"
}
}
......@@ -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"
}
......
......@@ -57,9 +57,6 @@
<listeners>
<listener class="\Drupal\Tests\Listeners\DrupalListener">
</listener>
<!-- The Symfony deprecation listener has to come after the Drupal listener -->
<listener class="Symfony\Bridge\PhpUnit\SymfonyTestsListener">
</listener>
</listeners>
<!-- Filter for coverage reports. -->
<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() {
$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 {
protected function deprecationStartTest($test) {
if ($test instanceof TestCase) {
if ('disabled' !== getenv('SYMFONY_DEPRECATIONS_HELPER')) {
$this->registerErrorHandler($test);
}
if ($this->willBeIsolated($test)) {
putenv('DRUPAL_EXPECTED_DEPRECATIONS_SERIALIZE=' . tempnam(sys_get_temp_dir(), 'exdep'));
}
......@@ -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".',
'"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.",
......@@ -201,7 +196,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)) {
......@@ -210,28 +208,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();
}
}
......
......@@ -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();
}
}
......@@ -2,20 +2,15 @@
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\Legacy\SymfonyTestsListenerTrait;
/**
* Adds the ability to dynamically set expected deprecation messages in tests.
*
* @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 {
......@@ -34,40 +29,11 @@ protected function addExpectedDeprecationMessage($message) {
*
* @param string[] $messages
* The expected deprecation messages.
*
* @see \Symfony\Bridge\PhpUnit\Legacy\ExpectDeprecationTraitForV8_4::expectDeprecation()
*/
public function expectedDeprecations(array $messages) {
if ($this instanceof TestCase) {
// 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;
}
}
$this->getTestResultObject()->beStrictAboutTestsThatDoNotTestAnything(FALSE);
// Expected deprecations set by isolated tests need to be written to a file
// so that the test running process can take account of them.
......@@ -80,31 +46,15 @@ public function expectedDeprecations(array $messages) {
$expected_deprecations = $messages;
}
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.');
}
/**
* 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) {
SymfonyTestsListenerTrait::$expectedDeprecations[] = $message;
}
}
}
......
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