DrupalStandardsListenerTrait.php 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240
  1. <?php
  2. namespace Drupal\Tests\Listeners;
  3. use PHPUnit\Framework\AssertionFailedError;
  4. use PHPUnit\Framework\TestCase;
  5. use PHPUnit\Framework\TestSuite;
  6. use PHPUnit\Util\ErrorHandler;
  7. /**
  8. * Listens for PHPUnit tests and fails those with invalid coverage annotations.
  9. *
  10. * Enforces various coding standards within test runs.
  11. *
  12. * @internal
  13. */
  14. trait DrupalStandardsListenerTrait {
  15. /**
  16. * Signals a coding standards failure to the user.
  17. *
  18. * @param \PHPUnit\Framework\TestCase $test
  19. * The test where we should insert our test failure.
  20. * @param string $message
  21. * The message to add to the failure notice. The test class name and test
  22. * name will be appended to this message automatically.
  23. */
  24. private function fail(TestCase $test, $message) {
  25. // Add the report to the test's results.
  26. $message .= ': ' . get_class($test) . '::' . $test->getName();
  27. $fail = new AssertionFailedError($message);
  28. $result = $test->getTestResultObject();
  29. $result->addFailure($test, $fail, 0);
  30. }
  31. /**
  32. * Helper method to check if a string names a valid class or trait.
  33. *
  34. * @param string $class
  35. * Name of the class to check.
  36. *
  37. * @return bool
  38. * TRUE if the class exists, FALSE otherwise.
  39. */
  40. private function classExists($class) {
  41. return class_exists($class, TRUE) || trait_exists($class, TRUE);
  42. }
  43. /**
  44. * Check an individual test run for valid @covers annotation.
  45. *
  46. * This method is called from $this::endTest().
  47. *
  48. * @param \PHPUnit\Framework\TestCase $test
  49. * The test to examine.
  50. */
  51. private function checkValidCoversForTest(TestCase $test) {
  52. // If we're generating a coverage report already, don't do anything here.
  53. if ($test->getTestResultObject() && $test->getTestResultObject()->getCollectCodeCoverageInformation()) {
  54. return;
  55. }
  56. // Gather our annotations.
  57. $annotations = $test->getAnnotations();
  58. // Glean the @coversDefaultClass annotation.
  59. $default_class = '';
  60. $valid_default_class = FALSE;
  61. if (isset($annotations['class']['coversDefaultClass'])) {
  62. if (count($annotations['class']['coversDefaultClass']) > 1) {
  63. $this->fail($test, '@coversDefaultClass has too many values');
  64. }
  65. // Grab the first one.
  66. $default_class = reset($annotations['class']['coversDefaultClass']);
  67. // Check whether the default class exists.
  68. $valid_default_class = $this->classExists($default_class);
  69. if (!$valid_default_class && interface_exists($default_class)) {
  70. $this->fail($test, "@coversDefaultClass refers to an interface '$default_class' and those can not be tested.");
  71. }
  72. elseif (!$valid_default_class) {
  73. $this->fail($test, "@coversDefaultClass does not exist '$default_class'");
  74. }
  75. }
  76. // Glean @covers annotation.
  77. if (isset($annotations['method']['covers'])) {
  78. // Drupal allows multiple @covers per test method, so we have to check
  79. // them all.
  80. foreach ($annotations['method']['covers'] as $covers) {
  81. // Ensure the annotation isn't empty.
  82. if (trim($covers) === '') {
  83. $this->fail($test, '@covers should not be empty');
  84. // If @covers is empty, we can't proceed.
  85. return;
  86. }
  87. // Ensure we don't have ().
  88. if (strpos($covers, '()') !== FALSE) {
  89. $this->fail($test, "@covers invalid syntax: Do not use '()'");
  90. }
  91. // Glean the class and method from @covers.
  92. $class = $covers;
  93. $method = '';
  94. if (strpos($covers, '::') !== FALSE) {
  95. list($class, $method) = explode('::', $covers);
  96. }
  97. // Check for the existence of the class if it's specified by @covers.
  98. if (!empty($class)) {
  99. // If the class doesn't exist we have either a bad classname or
  100. // are missing the :: for a method. Either way we can't proceed.
  101. if (!$this->classExists($class)) {
  102. if (empty($method)) {
  103. $this->fail($test, "@covers invalid syntax: Needs '::' or class does not exist in $covers");
  104. return;
  105. }
  106. elseif (interface_exists($class)) {
  107. $this->fail($test, "@covers refers to an interface '$class' and those can not be tested.");
  108. }
  109. else {
  110. $this->fail($test, '@covers class does not exist ' . $class);
  111. return;
  112. }
  113. }
  114. }
  115. else {
  116. // The class isn't specified and we have the ::, so therefore this
  117. // test either covers a function, or relies on a default class.
  118. if (empty($default_class)) {
  119. // If there's no default class, then we need to check if the global
  120. // function exists. Since this listener should always be listening
  121. // for endTest(), the function should have already been loaded from
  122. // its .module or .inc file.
  123. if (!function_exists($method)) {
  124. $this->fail($test, '@covers global method does not exist ' . $method);
  125. }
  126. }
  127. else {
  128. // We have a default class and this annotation doesn't act like a
  129. // global function, so we should use the default class if it's
  130. // valid.
  131. if ($valid_default_class) {
  132. $class = $default_class;
  133. }
  134. }
  135. }
  136. // Finally, after all that, let's see if the method exists.
  137. if (!empty($class) && !empty($method)) {
  138. $ref_class = new \ReflectionClass($class);
  139. if (!$ref_class->hasMethod($method)) {
  140. $this->fail($test, '@covers method does not exist ' . $class . '::' . $method);
  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. return ErrorHandler::handleError($type, $msg, $file, $line, $context);
  165. }
  166. /**
  167. * Reacts to the end of a test.
  168. *
  169. * We must mark this method as belonging to the special legacy group because
  170. * it might trigger an E_USER_DEPRECATED error during coverage annotation
  171. * validation. The legacy group allows symfony/phpunit-bridge to keep the
  172. * deprecation notice as a warning instead of an error, which would fail the
  173. * test.
  174. *
  175. * @group legacy
  176. *
  177. * @param \PHPUnit\Framework\Test $test
  178. * The test object that has ended its test run.
  179. * @param float $time
  180. * The time the test took.
  181. *
  182. * @see http://symfony.com/doc/current/components/phpunit_bridge.html#mark-tests-as-legacy
  183. */
  184. private function doEndTest($test, $time) {
  185. // \PHPUnit\Framework\Test does not have any useful methods of its own for
  186. // our purpose, so we have to distinguish between the different known
  187. // subclasses.
  188. if ($test instanceof TestCase) {
  189. // Change the error handler to ensure deprecation messages are not
  190. // triggered.
  191. set_error_handler([$this, 'errorHandler']);
  192. $this->checkValidCoversForTest($test);
  193. restore_error_handler();
  194. }
  195. elseif ($this->isTestSuite($test)) {
  196. foreach ($test->getGroupDetails() as $tests) {
  197. foreach ($tests as $test) {
  198. $this->doEndTest($test, $time);
  199. }
  200. }
  201. }
  202. }
  203. /**
  204. * Determine if a test object is a test suite regardless of PHPUnit version.
  205. *
  206. * @param \PHPUnit\Framework\Test $test
  207. * The test object to test if it is a test suite.
  208. *
  209. * @return bool
  210. * TRUE if it is a test suite, FALSE if not.
  211. */
  212. private function isTestSuite($test) {
  213. if (class_exists('PHPUnit\Framework\TestSuite') && $test instanceof TestSuite) {
  214. return TRUE;
  215. }
  216. return FALSE;
  217. }
  218. /**
  219. * Reacts to the end of a test.
  220. *
  221. * @param \PHPUnit\Framework\Test $test
  222. * The test object that has ended its test run.
  223. * @param float $time
  224. * The time the test took.
  225. */
  226. protected function standardsEndTest($test, $time) {
  227. $this->doEndTest($test, $time);
  228. }
  229. }