DrupalStandardsListenerTrait.php 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  1. <?php
  2. namespace Drupal\Tests\Listeners;
  3. use PHPUnit\Framework\AssertionFailedError;
  4. use PHPUnit\Framework\TestCase;
  5. use PHPUnit\Framework\TestSuite;
  6. /**
  7. * Listens for PHPUnit tests and fails those with invalid coverage annotations.
  8. *
  9. * Enforces various coding standards within test runs.
  10. *
  11. * @internal
  12. */
  13. trait DrupalStandardsListenerTrait {
  14. /**
  15. * Signals a coding standards failure to the user.
  16. *
  17. * @param \PHPUnit\Framework\TestCase $test
  18. * The test where we should insert our test failure.
  19. * @param string $message
  20. * The message to add to the failure notice. The test class name and test
  21. * name will be appended to this message automatically.
  22. */
  23. private function fail(TestCase $test, $message) {
  24. // Add the report to the test's results.
  25. $message .= ': ' . get_class($test) . '::' . $test->getName();
  26. $fail = new AssertionFailedError($message);
  27. $result = $test->getTestResultObject();
  28. $result->addFailure($test, $fail, 0);
  29. }
  30. /**
  31. * Helper method to check if a string names a valid class or trait.
  32. *
  33. * @param string $class
  34. * Name of the class to check.
  35. *
  36. * @return bool
  37. * TRUE if the class exists, FALSE otherwise.
  38. */
  39. private function classExists($class) {
  40. return class_exists($class, TRUE) || trait_exists($class, TRUE);
  41. }
  42. /**
  43. * Check an individual test run for valid @covers annotation.
  44. *
  45. * This method is called from $this::endTest().
  46. *
  47. * @param \PHPUnit\Framework\TestCase $test
  48. * The test to examine.
  49. */
  50. private function checkValidCoversForTest(TestCase $test) {
  51. // If we're generating a coverage report already, don't do anything here.
  52. if ($test->getTestResultObject() && $test->getTestResultObject()->getCollectCodeCoverageInformation()) {
  53. return;
  54. }
  55. // Gather our annotations.
  56. $annotations = $test->getAnnotations();
  57. // Glean the @coversDefaultClass annotation.
  58. $default_class = '';
  59. $valid_default_class = FALSE;
  60. if (isset($annotations['class']['coversDefaultClass'])) {
  61. if (count($annotations['class']['coversDefaultClass']) > 1) {
  62. $this->fail($test, '@coversDefaultClass has too many values');
  63. }
  64. // Grab the first one.
  65. $default_class = reset($annotations['class']['coversDefaultClass']);
  66. // Check whether the default class exists.
  67. $valid_default_class = $this->classExists($default_class);
  68. if (!$valid_default_class && interface_exists($default_class)) {
  69. $this->fail($test, "@coversDefaultClass refers to an interface '$default_class' and those can not be tested.");
  70. }
  71. elseif (!$valid_default_class) {
  72. $this->fail($test, "@coversDefaultClass does not exist '$default_class'");
  73. }
  74. }
  75. // Glean @covers annotation.
  76. if (isset($annotations['method']['covers'])) {
  77. // Drupal allows multiple @covers per test method, so we have to check
  78. // them all.
  79. foreach ($annotations['method']['covers'] as $covers) {
  80. // Ensure the annotation isn't empty.
  81. if (trim($covers) === '') {
  82. $this->fail($test, '@covers should not be empty');
  83. // If @covers is empty, we can't proceed.
  84. return;
  85. }
  86. // Ensure we don't have ().
  87. if (strpos($covers, '()') !== FALSE) {
  88. $this->fail($test, "@covers invalid syntax: Do not use '()'");
  89. }
  90. // Glean the class and method from @covers.
  91. $class = $covers;
  92. $method = '';
  93. if (strpos($covers, '::') !== FALSE) {
  94. list($class, $method) = explode('::', $covers);
  95. }
  96. // Check for the existence of the class if it's specified by @covers.
  97. if (!empty($class)) {
  98. // If the class doesn't exist we have either a bad classname or
  99. // are missing the :: for a method. Either way we can't proceed.
  100. if (!$this->classExists($class)) {
  101. if (empty($method)) {
  102. $this->fail($test, "@covers invalid syntax: Needs '::' or class does not exist in $covers");
  103. return;
  104. }
  105. elseif (interface_exists($class)) {
  106. $this->fail($test, "@covers refers to an interface '$class' and those can not be tested.");
  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. * Handles errors to ensure deprecation messages are not triggered.
  147. *
  148. * @param int $type
  149. * The severity level of the error.
  150. * @param string $msg
  151. * The error message.
  152. * @param $file
  153. * The file that caused the error.
  154. * @param $line
  155. * The line number that caused the error.
  156. * @param array $context
  157. * The error context.
  158. */
  159. public static function errorHandler($type, $msg, $file, $line, $context = []) {
  160. if ($type === E_USER_DEPRECATED) {
  161. return;
  162. }
  163. $error_handler = class_exists('PHPUnit_Util_ErrorHandler') ? 'PHPUnit_Util_ErrorHandler' : 'PHPUnit\Util\ErrorHandler';
  164. return $error_handler::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|\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|\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 \PHPUnit_Framework_TestSuite) {
  214. return TRUE;
  215. }
  216. if (class_exists('PHPUnit\Framework\TestSuite') && $test instanceof TestSuite) {
  217. return TRUE;
  218. }
  219. return FALSE;
  220. }
  221. /**
  222. * Reacts to the end of a test.
  223. *
  224. * @param \PHPUnit\Framework\Test|\PHPUnit_Framework_Test $test
  225. * The test object that has ended its test run.
  226. * @param float $time
  227. * The time the test took.
  228. */
  229. protected function standardsEndTest($test, $time) {
  230. $this->doEndTest($test, $time);
  231. }
  232. }