AbstractSniffUnitTest.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449
  1. <?php
  2. /**
  3. * An abstract class that all sniff unit tests must extend.
  4. *
  5. * A sniff unit test checks a .inc file for expected violations of a single
  6. * coding standard. Expected errors and warnings that are not found, or
  7. * warnings and errors that are not expected, are considered test failures.
  8. *
  9. * @author Greg Sherwood <gsherwood@squiz.net>
  10. * @copyright 2006-2015 Squiz Pty Ltd (ABN 77 084 670 600)
  11. * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
  12. */
  13. namespace PHP_CodeSniffer\Tests\Standards;
  14. use PHP_CodeSniffer\Config;
  15. use PHP_CodeSniffer\Exceptions\RuntimeException;
  16. use PHP_CodeSniffer\Ruleset;
  17. use PHP_CodeSniffer\Files\LocalFile;
  18. use PHP_CodeSniffer\Util\Common;
  19. use PHPUnit\Framework\TestCase;
  20. abstract class AbstractSniffUnitTest extends TestCase
  21. {
  22. /**
  23. * Enable or disable the backup and restoration of the $GLOBALS array.
  24. * Overwrite this attribute in a child class of TestCase.
  25. * Setting this attribute in setUp() has no effect!
  26. *
  27. * @var boolean
  28. */
  29. protected $backupGlobals = false;
  30. /**
  31. * The path to the standard's main directory.
  32. *
  33. * @var string
  34. */
  35. public $standardsDir = null;
  36. /**
  37. * The path to the standard's test directory.
  38. *
  39. * @var string
  40. */
  41. public $testsDir = null;
  42. /**
  43. * Sets up this unit test.
  44. *
  45. * @return void
  46. */
  47. protected function setUp()
  48. {
  49. $class = get_class($this);
  50. $this->standardsDir = $GLOBALS['PHP_CODESNIFFER_STANDARD_DIRS'][$class];
  51. $this->testsDir = $GLOBALS['PHP_CODESNIFFER_TEST_DIRS'][$class];
  52. }//end setUp()
  53. /**
  54. * Get a list of all test files to check.
  55. *
  56. * These will have the same base as the sniff name but different extensions.
  57. * We ignore the .php file as it is the class.
  58. *
  59. * @param string $testFileBase The base path that the unit tests files will have.
  60. *
  61. * @return string[]
  62. */
  63. protected function getTestFiles($testFileBase)
  64. {
  65. $testFiles = [];
  66. $dir = substr($testFileBase, 0, strrpos($testFileBase, DIRECTORY_SEPARATOR));
  67. $di = new \DirectoryIterator($dir);
  68. foreach ($di as $file) {
  69. $path = $file->getPathname();
  70. if (substr($path, 0, strlen($testFileBase)) === $testFileBase) {
  71. if ($path !== $testFileBase.'php' && substr($path, -5) !== 'fixed') {
  72. $testFiles[] = $path;
  73. }
  74. }
  75. }
  76. // Put them in order.
  77. sort($testFiles);
  78. return $testFiles;
  79. }//end getTestFiles()
  80. /**
  81. * Should this test be skipped for some reason.
  82. *
  83. * @return boolean
  84. */
  85. protected function shouldSkipTest()
  86. {
  87. return false;
  88. }//end shouldSkipTest()
  89. /**
  90. * Tests the extending classes Sniff class.
  91. *
  92. * @return void
  93. * @throws \PHPUnit\Framework\Exception
  94. */
  95. final public function testSniff()
  96. {
  97. // Skip this test if we can't run in this environment.
  98. if ($this->shouldSkipTest() === true) {
  99. $this->markTestSkipped();
  100. }
  101. $sniffCode = Common::getSniffCode(get_class($this));
  102. list($standardName, $categoryName, $sniffName) = explode('.', $sniffCode);
  103. $testFileBase = $this->testsDir.$categoryName.DIRECTORY_SEPARATOR.$sniffName.'UnitTest.';
  104. // Get a list of all test files to check.
  105. $testFiles = $this->getTestFiles($testFileBase);
  106. if (isset($GLOBALS['PHP_CODESNIFFER_CONFIG']) === true) {
  107. $config = $GLOBALS['PHP_CODESNIFFER_CONFIG'];
  108. } else {
  109. $config = new Config();
  110. $config->cache = false;
  111. $GLOBALS['PHP_CODESNIFFER_CONFIG'] = $config;
  112. }
  113. $config->standards = [$standardName];
  114. $config->sniffs = [$sniffCode];
  115. $config->ignored = [];
  116. if (isset($GLOBALS['PHP_CODESNIFFER_RULESETS']) === false) {
  117. $GLOBALS['PHP_CODESNIFFER_RULESETS'] = [];
  118. }
  119. if (isset($GLOBALS['PHP_CODESNIFFER_RULESETS'][$standardName]) === false) {
  120. $ruleset = new Ruleset($config);
  121. $GLOBALS['PHP_CODESNIFFER_RULESETS'][$standardName] = $ruleset;
  122. }
  123. $ruleset = $GLOBALS['PHP_CODESNIFFER_RULESETS'][$standardName];
  124. $sniffFile = $this->standardsDir.DIRECTORY_SEPARATOR.'Sniffs'.DIRECTORY_SEPARATOR.$categoryName.DIRECTORY_SEPARATOR.$sniffName.'Sniff.php';
  125. $sniffClassName = substr(get_class($this), 0, -8).'Sniff';
  126. $sniffClassName = str_replace('\Tests\\', '\Sniffs\\', $sniffClassName);
  127. $sniffClassName = Common::cleanSniffClass($sniffClassName);
  128. $restrictions = [strtolower($sniffClassName) => true];
  129. $ruleset->registerSniffs([$sniffFile], $restrictions, []);
  130. $ruleset->populateTokenListeners();
  131. $failureMessages = [];
  132. foreach ($testFiles as $testFile) {
  133. $filename = basename($testFile);
  134. $oldConfig = $config->getSettings();
  135. try {
  136. $this->setCliValues($filename, $config);
  137. $phpcsFile = new LocalFile($testFile, $ruleset, $config);
  138. $phpcsFile->process();
  139. } catch (RuntimeException $e) {
  140. $this->fail('An unexpected exception has been caught: '.$e->getMessage());
  141. }
  142. $failures = $this->generateFailureMessages($phpcsFile);
  143. $failureMessages = array_merge($failureMessages, $failures);
  144. if ($phpcsFile->getFixableCount() > 0) {
  145. // Attempt to fix the errors.
  146. $phpcsFile->fixer->fixFile();
  147. $fixable = $phpcsFile->getFixableCount();
  148. if ($fixable > 0) {
  149. $failureMessages[] = "Failed to fix $fixable fixable violations in $filename";
  150. }
  151. // Check for a .fixed file to check for accuracy of fixes.
  152. $fixedFile = $testFile.'.fixed';
  153. if (file_exists($fixedFile) === true) {
  154. $diff = $phpcsFile->fixer->generateDiff($fixedFile);
  155. if (trim($diff) !== '') {
  156. $filename = basename($testFile);
  157. $fixedFilename = basename($fixedFile);
  158. $failureMessages[] = "Fixed version of $filename does not match expected version in $fixedFilename; the diff is\n$diff";
  159. }
  160. }
  161. }
  162. // Restore the config.
  163. $config->setSettings($oldConfig);
  164. }//end foreach
  165. if (empty($failureMessages) === false) {
  166. $this->fail(implode(PHP_EOL, $failureMessages));
  167. }
  168. }//end testSniff()
  169. /**
  170. * Generate a list of test failures for a given sniffed file.
  171. *
  172. * @param \PHP_CodeSniffer\Files\LocalFile $file The file being tested.
  173. *
  174. * @return array
  175. * @throws \PHP_CodeSniffer\Exceptions\RuntimeException
  176. */
  177. public function generateFailureMessages(LocalFile $file)
  178. {
  179. $testFile = $file->getFilename();
  180. $foundErrors = $file->getErrors();
  181. $foundWarnings = $file->getWarnings();
  182. $expectedErrors = $this->getErrorList(basename($testFile));
  183. $expectedWarnings = $this->getWarningList(basename($testFile));
  184. if (is_array($expectedErrors) === false) {
  185. throw new RuntimeException('getErrorList() must return an array');
  186. }
  187. if (is_array($expectedWarnings) === false) {
  188. throw new RuntimeException('getWarningList() must return an array');
  189. }
  190. /*
  191. We merge errors and warnings together to make it easier
  192. to iterate over them and produce the errors string. In this way,
  193. we can report on errors and warnings in the same line even though
  194. it's not really structured to allow that.
  195. */
  196. $allProblems = [];
  197. $failureMessages = [];
  198. foreach ($foundErrors as $line => $lineErrors) {
  199. foreach ($lineErrors as $column => $errors) {
  200. if (isset($allProblems[$line]) === false) {
  201. $allProblems[$line] = [
  202. 'expected_errors' => 0,
  203. 'expected_warnings' => 0,
  204. 'found_errors' => [],
  205. 'found_warnings' => [],
  206. ];
  207. }
  208. $foundErrorsTemp = [];
  209. foreach ($allProblems[$line]['found_errors'] as $foundError) {
  210. $foundErrorsTemp[] = $foundError;
  211. }
  212. $errorsTemp = [];
  213. foreach ($errors as $foundError) {
  214. $errorsTemp[] = $foundError['message'].' ('.$foundError['source'].')';
  215. $source = $foundError['source'];
  216. if (in_array($source, $GLOBALS['PHP_CODESNIFFER_SNIFF_CODES']) === false) {
  217. $GLOBALS['PHP_CODESNIFFER_SNIFF_CODES'][] = $source;
  218. }
  219. if ($foundError['fixable'] === true
  220. && in_array($source, $GLOBALS['PHP_CODESNIFFER_FIXABLE_CODES']) === false
  221. ) {
  222. $GLOBALS['PHP_CODESNIFFER_FIXABLE_CODES'][] = $source;
  223. }
  224. }
  225. $allProblems[$line]['found_errors'] = array_merge($foundErrorsTemp, $errorsTemp);
  226. }//end foreach
  227. if (isset($expectedErrors[$line]) === true) {
  228. $allProblems[$line]['expected_errors'] = $expectedErrors[$line];
  229. } else {
  230. $allProblems[$line]['expected_errors'] = 0;
  231. }
  232. unset($expectedErrors[$line]);
  233. }//end foreach
  234. foreach ($expectedErrors as $line => $numErrors) {
  235. if (isset($allProblems[$line]) === false) {
  236. $allProblems[$line] = [
  237. 'expected_errors' => 0,
  238. 'expected_warnings' => 0,
  239. 'found_errors' => [],
  240. 'found_warnings' => [],
  241. ];
  242. }
  243. $allProblems[$line]['expected_errors'] = $numErrors;
  244. }
  245. foreach ($foundWarnings as $line => $lineWarnings) {
  246. foreach ($lineWarnings as $column => $warnings) {
  247. if (isset($allProblems[$line]) === false) {
  248. $allProblems[$line] = [
  249. 'expected_errors' => 0,
  250. 'expected_warnings' => 0,
  251. 'found_errors' => [],
  252. 'found_warnings' => [],
  253. ];
  254. }
  255. $foundWarningsTemp = [];
  256. foreach ($allProblems[$line]['found_warnings'] as $foundWarning) {
  257. $foundWarningsTemp[] = $foundWarning;
  258. }
  259. $warningsTemp = [];
  260. foreach ($warnings as $warning) {
  261. $warningsTemp[] = $warning['message'].' ('.$warning['source'].')';
  262. }
  263. $allProblems[$line]['found_warnings'] = array_merge($foundWarningsTemp, $warningsTemp);
  264. }//end foreach
  265. if (isset($expectedWarnings[$line]) === true) {
  266. $allProblems[$line]['expected_warnings'] = $expectedWarnings[$line];
  267. } else {
  268. $allProblems[$line]['expected_warnings'] = 0;
  269. }
  270. unset($expectedWarnings[$line]);
  271. }//end foreach
  272. foreach ($expectedWarnings as $line => $numWarnings) {
  273. if (isset($allProblems[$line]) === false) {
  274. $allProblems[$line] = [
  275. 'expected_errors' => 0,
  276. 'expected_warnings' => 0,
  277. 'found_errors' => [],
  278. 'found_warnings' => [],
  279. ];
  280. }
  281. $allProblems[$line]['expected_warnings'] = $numWarnings;
  282. }
  283. // Order the messages by line number.
  284. ksort($allProblems);
  285. foreach ($allProblems as $line => $problems) {
  286. $numErrors = count($problems['found_errors']);
  287. $numWarnings = count($problems['found_warnings']);
  288. $expectedErrors = $problems['expected_errors'];
  289. $expectedWarnings = $problems['expected_warnings'];
  290. $errors = '';
  291. $foundString = '';
  292. if ($expectedErrors !== $numErrors || $expectedWarnings !== $numWarnings) {
  293. $lineMessage = "[LINE $line]";
  294. $expectedMessage = 'Expected ';
  295. $foundMessage = 'in '.basename($testFile).' but found ';
  296. if ($expectedErrors !== $numErrors) {
  297. $expectedMessage .= "$expectedErrors error(s)";
  298. $foundMessage .= "$numErrors error(s)";
  299. if ($numErrors !== 0) {
  300. $foundString .= 'error(s)';
  301. $errors .= implode(PHP_EOL.' -> ', $problems['found_errors']);
  302. }
  303. if ($expectedWarnings !== $numWarnings) {
  304. $expectedMessage .= ' and ';
  305. $foundMessage .= ' and ';
  306. if ($numWarnings !== 0) {
  307. if ($foundString !== '') {
  308. $foundString .= ' and ';
  309. }
  310. }
  311. }
  312. }
  313. if ($expectedWarnings !== $numWarnings) {
  314. $expectedMessage .= "$expectedWarnings warning(s)";
  315. $foundMessage .= "$numWarnings warning(s)";
  316. if ($numWarnings !== 0) {
  317. $foundString .= 'warning(s)';
  318. if (empty($errors) === false) {
  319. $errors .= PHP_EOL.' -> ';
  320. }
  321. $errors .= implode(PHP_EOL.' -> ', $problems['found_warnings']);
  322. }
  323. }
  324. $fullMessage = "$lineMessage $expectedMessage $foundMessage.";
  325. if ($errors !== '') {
  326. $fullMessage .= " The $foundString found were:".PHP_EOL." -> $errors";
  327. }
  328. $failureMessages[] = $fullMessage;
  329. }//end if
  330. }//end foreach
  331. return $failureMessages;
  332. }//end generateFailureMessages()
  333. /**
  334. * Get a list of CLI values to set before the file is tested.
  335. *
  336. * @param string $filename The name of the file being tested.
  337. * @param \PHP_CodeSniffer\Config $config The config data for the run.
  338. *
  339. * @return void
  340. */
  341. public function setCliValues($filename, $config)
  342. {
  343. return;
  344. }//end setCliValues()
  345. /**
  346. * Returns the lines where errors should occur.
  347. *
  348. * The key of the array should represent the line number and the value
  349. * should represent the number of errors that should occur on that line.
  350. *
  351. * @return array<int, int>
  352. */
  353. abstract protected function getErrorList();
  354. /**
  355. * Returns the lines where warnings should occur.
  356. *
  357. * The key of the array should represent the line number and the value
  358. * should represent the number of warnings that should occur on that line.
  359. *
  360. * @return array<int, int>
  361. */
  362. abstract protected function getWarningList();
  363. }//end class