3352d900b380da0e382e25cde7ec2c9df245ee00
[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) || interface_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) {
75         $this->fail($test, "@coversDefaultClass does not exist '$default_class'");
76       }
77     }
78     // Glean @covers annotation.
79     if (isset($annotations['method']['covers'])) {
80       // Drupal allows multiple @covers per test method, so we have to check
81       // them all.
82       foreach ($annotations['method']['covers'] as $covers) {
83         // Ensure the annotation isn't empty.
84         if (trim($covers) === '') {
85           $this->fail($test, '@covers should not be empty');
86           // If @covers is empty, we can't proceed.
87           return;
88         }
89         // Ensure we don't have ().
90         if (strpos($covers, '()') !== FALSE) {
91           $this->fail($test, "@covers invalid syntax: Do not use '()'");
92         }
93         // Glean the class and method from @covers.
94         $class = $covers;
95         $method = '';
96         if (strpos($covers, '::') !== FALSE) {
97           list($class, $method) = explode('::', $covers);
98         }
99         // Check for the existence of the class if it's specified by @covers.
100         if (!empty($class)) {
101           // If the class doesn't exist we have either a bad classname or
102           // are missing the :: for a method. Either way we can't proceed.
103           if (!$this->classExists($class)) {
104             if (empty($method)) {
105               $this->fail($test, "@covers invalid syntax: Needs '::' or class does not exist in $covers");
106               return;
107             }
108             else {
109               $this->fail($test, '@covers class does not exist ' . $class);
110               return;
111             }
112           }
113         }
114         else {
115           // The class isn't specified and we have the ::, so therefore this
116           // test either covers a function, or relies on a default class.
117           if (empty($default_class)) {
118             // If there's no default class, then we need to check if the global
119             // function exists. Since this listener should always be listening
120             // for endTest(), the function should have already been loaded from
121             // its .module or .inc file.
122             if (!function_exists($method)) {
123               $this->fail($test, '@covers global method does not exist ' . $method);
124             }
125           }
126           else {
127             // We have a default class and this annotation doesn't act like a
128             // global function, so we should use the default class if it's
129             // valid.
130             if ($valid_default_class) {
131               $class = $default_class;
132             }
133           }
134         }
135         // Finally, after all that, let's see if the method exists.
136         if (!empty($class) && !empty($method)) {
137           $ref_class = new \ReflectionClass($class);
138           if (!$ref_class->hasMethod($method)) {
139             $this->fail($test, '@covers method does not exist ' . $class . '::' . $method);
140           }
141         }
142       }
143     }
144   }
145
146   /**
147    * Handles errors to ensure deprecation messages are not triggered.
148    *
149    * @param int $type
150    *   The severity level of the error.
151    * @param string $msg
152    *   The error message.
153    * @param $file
154    *   The file that caused the error.
155    * @param $line
156    *   The line number that caused the error.
157    * @param array $context
158    *   The error context.
159    */
160   public static function errorHandler($type, $msg, $file, $line, $context = []) {
161     if ($type === E_USER_DEPRECATED) {
162       return;
163     }
164     $error_handler = class_exists('PHPUnit_Util_ErrorHandler') ? 'PHPUnit_Util_ErrorHandler' : 'PHPUnit\Util\ErrorHandler';
165     return $error_handler::handleError($type, $msg, $file, $line, $context);
166   }
167
168   /**
169    * Reacts to the end of a test.
170    *
171    * We must mark this method as belonging to the special legacy group because
172    * it might trigger an E_USER_DEPRECATED error during coverage annotation
173    * validation. The legacy group allows symfony/phpunit-bridge to keep the
174    * deprecation notice as a warning instead of an error, which would fail the
175    * test.
176    *
177    * @group legacy
178    *
179    * @param \PHPUnit\Framework\Test|\PHPUnit_Framework_Test $test
180    *   The test object that has ended its test run.
181    * @param float $time
182    *   The time the test took.
183    *
184    * @see http://symfony.com/doc/current/components/phpunit_bridge.html#mark-tests-as-legacy
185    */
186   private function doEndTest($test, $time) {
187     // \PHPUnit_Framework_Test does not have any useful methods of its own for
188     // our purpose, so we have to distinguish between the different known
189     // subclasses.
190     if ($test instanceof TestCase) {
191       // Change the error handler to ensure deprecation messages are not
192       // triggered.
193       set_error_handler([$this, 'errorHandler']);
194       $this->checkValidCoversForTest($test);
195       restore_error_handler();
196     }
197     elseif ($this->isTestSuite($test)) {
198       foreach ($test->getGroupDetails() as $tests) {
199         foreach ($tests as $test) {
200           $this->doEndTest($test, $time);
201         }
202       }
203     }
204   }
205
206   /**
207    * Determine if a test object is a test suite regardless of PHPUnit version.
208    *
209    * @param \PHPUnit\Framework\Test|\PHPUnit_Framework_Test $test
210    *   The test object to test if it is a test suite.
211    *
212    * @return bool
213    *   TRUE if it is a test suite, FALSE if not.
214    */
215   private function isTestSuite($test) {
216     if (class_exists('\PHPUnit_Framework_TestSuite') && $test instanceof \PHPUnit_Framework_TestSuite) {
217       return TRUE;
218     }
219     if (class_exists('PHPUnit\Framework\TestSuite') && $test instanceof TestSuite) {
220       return TRUE;
221     }
222     return FALSE;
223   }
224
225   /**
226    * Reacts to the end of a test.
227    *
228    * @param \PHPUnit\Framework\Test|\PHPUnit_Framework_Test $test
229    *   The test object that has ended its test run.
230    * @param float $time
231    *   The time the test took.
232    */
233   protected function standardsEndTest($test, $time) {
234     $this->doEndTest($test, $time);
235   }
236
237 }