Updated to Drupal 8.6.4, which is PHP 7.3 friendly. Also updated HTMLaw library....
[yaffs-website] / web / core / tests / Drupal / Tests / Listeners / DrupalStandardsListenerTrait.php
1 <?php
2
3 namespace Drupal\Tests\Listeners;
4
5 use PHPUnit\Framework\AssertionFailedError;
6 use PHPUnit\Framework\TestCase;
7 use PHPUnit\Framework\TestSuite;
8
9 /**
10  * Listens for PHPUnit tests and fails those with invalid coverage annotations.
11  *
12  * Enforces various coding standards within test runs.
13  *
14  * @internal
15  */
16 trait DrupalStandardsListenerTrait {
17
18   /**
19    * Signals a coding standards failure to the user.
20    *
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.
26    */
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);
33   }
34
35   /**
36    * Helper method to check if a string names a valid class or trait.
37    *
38    * @param string $class
39    *   Name of the class to check.
40    *
41    * @return bool
42    *   TRUE if the class exists, FALSE otherwise.
43    */
44   private function classExists($class) {
45     return class_exists($class, TRUE) || trait_exists($class, TRUE);
46   }
47
48   /**
49    * Check an individual test run for valid @covers annotation.
50    *
51    * This method is called from $this::endTest().
52    *
53    * @param \PHPUnit\Framework\TestCase $test
54    *   The test to examine.
55    */
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()) {
59       return;
60     }
61     // Gather our annotations.
62     $annotations = $test->getAnnotations();
63     // Glean the @coversDefaultClass annotation.
64     $default_class = '';
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');
69       }
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.");
76       }
77       elseif (!$valid_default_class) {
78         $this->fail($test, "@coversDefaultClass does not exist '$default_class'");
79       }
80     }
81     // Glean @covers annotation.
82     if (isset($annotations['method']['covers'])) {
83       // Drupal allows multiple @covers per test method, so we have to check
84       // them all.
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.
90           return;
91         }
92         // Ensure we don't have ().
93         if (strpos($covers, '()') !== FALSE) {
94           $this->fail($test, "@covers invalid syntax: Do not use '()'");
95         }
96         // Glean the class and method from @covers.
97         $class = $covers;
98         $method = '';
99         if (strpos($covers, '::') !== FALSE) {
100           list($class, $method) = explode('::', $covers);
101         }
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");
109               return;
110             }
111             elseif (interface_exists($class)) {
112               $this->fail($test, "@covers refers to an interface '$class' and those can not be tested.");
113             }
114             else {
115               $this->fail($test, '@covers class does not exist ' . $class);
116               return;
117             }
118           }
119         }
120         else {
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);
130             }
131           }
132           else {
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
135             // valid.
136             if ($valid_default_class) {
137               $class = $default_class;
138             }
139           }
140         }
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);
146           }
147         }
148       }
149     }
150   }
151
152   /**
153    * Handles errors to ensure deprecation messages are not triggered.
154    *
155    * @param int $type
156    *   The severity level of the error.
157    * @param string $msg
158    *   The error message.
159    * @param $file
160    *   The file that caused the error.
161    * @param $line
162    *   The line number that caused the error.
163    * @param array $context
164    *   The error context.
165    */
166   public static function errorHandler($type, $msg, $file, $line, $context = []) {
167     if ($type === E_USER_DEPRECATED) {
168       return;
169     }
170     $error_handler = class_exists('PHPUnit_Util_ErrorHandler') ? 'PHPUnit_Util_ErrorHandler' : 'PHPUnit\Util\ErrorHandler';
171     return $error_handler::handleError($type, $msg, $file, $line, $context);
172   }
173
174   /**
175    * Reacts to the end of a test.
176    *
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
181    * test.
182    *
183    * @group legacy
184    *
185    * @param \PHPUnit\Framework\Test|\PHPUnit_Framework_Test $test
186    *   The test object that has ended its test run.
187    * @param float $time
188    *   The time the test took.
189    *
190    * @see http://symfony.com/doc/current/components/phpunit_bridge.html#mark-tests-as-legacy
191    */
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
195     // subclasses.
196     if ($test instanceof TestCase) {
197       // Change the error handler to ensure deprecation messages are not
198       // triggered.
199       set_error_handler([$this, 'errorHandler']);
200       $this->checkValidCoversForTest($test);
201       restore_error_handler();
202     }
203     elseif ($this->isTestSuite($test)) {
204       foreach ($test->getGroupDetails() as $tests) {
205         foreach ($tests as $test) {
206           $this->doEndTest($test, $time);
207         }
208       }
209     }
210   }
211
212   /**
213    * Determine if a test object is a test suite regardless of PHPUnit version.
214    *
215    * @param \PHPUnit\Framework\Test|\PHPUnit_Framework_Test $test
216    *   The test object to test if it is a test suite.
217    *
218    * @return bool
219    *   TRUE if it is a test suite, FALSE if not.
220    */
221   private function isTestSuite($test) {
222     if (class_exists('\PHPUnit_Framework_TestSuite') && $test instanceof \PHPUnit_Framework_TestSuite) {
223       return TRUE;
224     }
225     if (class_exists('PHPUnit\Framework\TestSuite') && $test instanceof TestSuite) {
226       return TRUE;
227     }
228     return FALSE;
229   }
230
231   /**
232    * Reacts to the end of a test.
233    *
234    * @param \PHPUnit\Framework\Test|\PHPUnit_Framework_Test $test
235    *   The test object that has ended its test run.
236    * @param float $time
237    *   The time the test took.
238    */
239   protected function standardsEndTest($test, $time) {
240     $this->doEndTest($test, $time);
241   }
242
243 }