3 namespace Drupal\Tests\Listeners;
5 use PHPUnit\Framework\AssertionFailedError;
6 use PHPUnit\Framework\TestCase;
7 use PHPUnit\Framework\TestSuite;
10 * Listens for PHPUnit tests and fails those with invalid coverage annotations.
12 * Enforces various coding standards within test runs.
16 trait DrupalStandardsListenerTrait {
19 * Signals a coding standards failure to the user.
21 * @param \PHPUnit\Framework\TestCase $test
22 * The test where we should insert our test failure.
23 * @param string $message
24 * The message to add to the failure notice. The test class name and test
25 * name will be appended to this message automatically.
27 private function fail(TestCase $test, $message) {
28 // Add the report to the test's results.
29 $message .= ': ' . get_class($test) . '::' . $test->getName();
30 $fail = new AssertionFailedError($message);
31 $result = $test->getTestResultObject();
32 $result->addFailure($test, $fail, 0);
36 * Helper method to check if a string names a valid class or trait.
38 * @param string $class
39 * Name of the class to check.
42 * TRUE if the class exists, FALSE otherwise.
44 private function classExists($class) {
45 return class_exists($class, TRUE) || trait_exists($class, TRUE);
49 * Check an individual test run for valid @covers annotation.
51 * This method is called from $this::endTest().
53 * @param \PHPUnit\Framework\TestCase $test
54 * The test to examine.
56 private function checkValidCoversForTest(TestCase $test) {
57 // If we're generating a coverage report already, don't do anything here.
58 if ($test->getTestResultObject() && $test->getTestResultObject()->getCollectCodeCoverageInformation()) {
61 // Gather our annotations.
62 $annotations = $test->getAnnotations();
63 // Glean the @coversDefaultClass annotation.
65 $valid_default_class = FALSE;
66 if (isset($annotations['class']['coversDefaultClass'])) {
67 if (count($annotations['class']['coversDefaultClass']) > 1) {
68 $this->fail($test, '@coversDefaultClass has too many values');
70 // Grab the first one.
71 $default_class = reset($annotations['class']['coversDefaultClass']);
72 // Check whether the default class exists.
73 $valid_default_class = $this->classExists($default_class);
74 if (!$valid_default_class && interface_exists($default_class)) {
75 $this->fail($test, "@coversDefaultClass refers to an interface '$default_class' and those can not be tested.");
77 elseif (!$valid_default_class) {
78 $this->fail($test, "@coversDefaultClass does not exist '$default_class'");
81 // Glean @covers annotation.
82 if (isset($annotations['method']['covers'])) {
83 // Drupal allows multiple @covers per test method, so we have to check
85 foreach ($annotations['method']['covers'] as $covers) {
86 // Ensure the annotation isn't empty.
87 if (trim($covers) === '') {
88 $this->fail($test, '@covers should not be empty');
89 // If @covers is empty, we can't proceed.
92 // Ensure we don't have ().
93 if (strpos($covers, '()') !== FALSE) {
94 $this->fail($test, "@covers invalid syntax: Do not use '()'");
96 // Glean the class and method from @covers.
99 if (strpos($covers, '::') !== FALSE) {
100 list($class, $method) = explode('::', $covers);
102 // Check for the existence of the class if it's specified by @covers.
103 if (!empty($class)) {
104 // If the class doesn't exist we have either a bad classname or
105 // are missing the :: for a method. Either way we can't proceed.
106 if (!$this->classExists($class)) {
107 if (empty($method)) {
108 $this->fail($test, "@covers invalid syntax: Needs '::' or class does not exist in $covers");
111 elseif (interface_exists($class)) {
112 $this->fail($test, "@covers refers to an interface '$class' and those can not be tested.");
115 $this->fail($test, '@covers class does not exist ' . $class);
121 // The class isn't specified and we have the ::, so therefore this
122 // test either covers a function, or relies on a default class.
123 if (empty($default_class)) {
124 // If there's no default class, then we need to check if the global
125 // function exists. Since this listener should always be listening
126 // for endTest(), the function should have already been loaded from
127 // its .module or .inc file.
128 if (!function_exists($method)) {
129 $this->fail($test, '@covers global method does not exist ' . $method);
133 // We have a default class and this annotation doesn't act like a
134 // global function, so we should use the default class if it's
136 if ($valid_default_class) {
137 $class = $default_class;
141 // Finally, after all that, let's see if the method exists.
142 if (!empty($class) && !empty($method)) {
143 $ref_class = new \ReflectionClass($class);
144 if (!$ref_class->hasMethod($method)) {
145 $this->fail($test, '@covers method does not exist ' . $class . '::' . $method);
153 * Handles errors to ensure deprecation messages are not triggered.
156 * The severity level of the error.
160 * The file that caused the error.
162 * The line number that caused the error.
163 * @param array $context
166 public static function errorHandler($type, $msg, $file, $line, $context = []) {
167 if ($type === E_USER_DEPRECATED) {
170 $error_handler = class_exists('PHPUnit_Util_ErrorHandler') ? 'PHPUnit_Util_ErrorHandler' : 'PHPUnit\Util\ErrorHandler';
171 return $error_handler::handleError($type, $msg, $file, $line, $context);
175 * Reacts to the end of a test.
177 * We must mark this method as belonging to the special legacy group because
178 * it might trigger an E_USER_DEPRECATED error during coverage annotation
179 * validation. The legacy group allows symfony/phpunit-bridge to keep the
180 * deprecation notice as a warning instead of an error, which would fail the
185 * @param \PHPUnit\Framework\Test|\PHPUnit_Framework_Test $test
186 * The test object that has ended its test run.
188 * The time the test took.
190 * @see http://symfony.com/doc/current/components/phpunit_bridge.html#mark-tests-as-legacy
192 private function doEndTest($test, $time) {
193 // \PHPUnit_Framework_Test does not have any useful methods of its own for
194 // our purpose, so we have to distinguish between the different known
196 if ($test instanceof TestCase) {
197 // Change the error handler to ensure deprecation messages are not
199 set_error_handler([$this, 'errorHandler']);
200 $this->checkValidCoversForTest($test);
201 restore_error_handler();
203 elseif ($this->isTestSuite($test)) {
204 foreach ($test->getGroupDetails() as $tests) {
205 foreach ($tests as $test) {
206 $this->doEndTest($test, $time);
213 * Determine if a test object is a test suite regardless of PHPUnit version.
215 * @param \PHPUnit\Framework\Test|\PHPUnit_Framework_Test $test
216 * The test object to test if it is a test suite.
219 * TRUE if it is a test suite, FALSE if not.
221 private function isTestSuite($test) {
222 if (class_exists('\PHPUnit_Framework_TestSuite') && $test instanceof \PHPUnit_Framework_TestSuite) {
225 if (class_exists('PHPUnit\Framework\TestSuite') && $test instanceof TestSuite) {
232 * Reacts to the end of a test.
234 * @param \PHPUnit\Framework\Test|\PHPUnit_Framework_Test $test
235 * The test object that has ended its test run.
237 * The time the test took.
239 protected function standardsEndTest($test, $time) {
240 $this->doEndTest($test, $time);