AbstractSniffTestCase.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476
  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/PHPCSStandards/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
  12. */
  13. namespace PHP_CodeSniffer\Tests\Standards;
  14. use DirectoryIterator;
  15. use PHP_CodeSniffer\Exceptions\RuntimeException;
  16. use PHP_CodeSniffer\Files\LocalFile;
  17. use PHP_CodeSniffer\Ruleset;
  18. use PHP_CodeSniffer\Tests\ConfigDouble;
  19. use PHP_CodeSniffer\Util\Common;
  20. use PHPUnit\Framework\TestCase;
  21. use ReflectionClass;
  22. abstract class AbstractSniffTestCase extends TestCase
  23. {
  24. /**
  25. * Ruleset template with placeholders.
  26. *
  27. * @var string
  28. */
  29. private const RULESET_TEMPLATE = <<<'TEMPLATE'
  30. <?xml version="1.0"?>
  31. <ruleset xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" name="[STANDARDNAME]" xsi:noNamespaceSchemaLocation="../../phpcs.xsd">
  32. <description>Temporary ruleset used by the AbstractSniffUnitTest class.</description>
  33. <rule ref="[SNIFFFILEREF]"/>
  34. </ruleset>
  35. TEMPLATE;
  36. /**
  37. * Placeholders used in the ruleset template which need to be replaced.
  38. *
  39. * @var array<string>
  40. */
  41. private const SEARCH_FOR = [
  42. '[STANDARDNAME]',
  43. '[SNIFFFILEREF]',
  44. ];
  45. /**
  46. * Location where the temporary ruleset file will be saved.
  47. *
  48. * @var string
  49. */
  50. private const RULESET_FILENAME = __DIR__.'/sniffStnd.xml';
  51. /**
  52. * Cache for the Config object.
  53. *
  54. * @var \PHP_CodeSniffer\Tests\ConfigDouble
  55. */
  56. private static $config;
  57. /**
  58. * Extensions to disregard when gathering the test files.
  59. *
  60. * @var array<string, string>
  61. */
  62. private $ignoreExtensions = [
  63. 'php' => 'php',
  64. 'fixed' => 'fixed',
  65. 'bak' => 'bak',
  66. 'orig' => 'orig',
  67. ];
  68. /**
  69. * Clean up temporary ruleset file.
  70. *
  71. * @return void
  72. */
  73. public static function tearDownAfterClass(): void
  74. {
  75. @unlink(self::RULESET_FILENAME);
  76. }//end tearDownAfterClass()
  77. /**
  78. * Get a list of all test files to check.
  79. *
  80. * These will have the same base as the sniff name but different extensions.
  81. * We ignore the .php file as it is the test class.
  82. *
  83. * @param string $testFileBase The base path that the unit tests files will have.
  84. *
  85. * @return string[]
  86. */
  87. protected function getTestFiles($testFileBase)
  88. {
  89. $testFiles = [];
  90. $dir = dirname($testFileBase);
  91. $di = new DirectoryIterator($dir);
  92. foreach ($di as $file) {
  93. $path = $file->getPathname();
  94. if (substr($path, 0, strlen($testFileBase)) === $testFileBase) {
  95. $extension = $file->getExtension();
  96. if (isset($this->ignoreExtensions[$extension]) === false) {
  97. $testFiles[] = $path;
  98. }
  99. }
  100. }
  101. // Put them in order.
  102. sort($testFiles, SORT_NATURAL);
  103. return $testFiles;
  104. }//end getTestFiles()
  105. /**
  106. * Should this test be skipped for some reason.
  107. *
  108. * @return boolean
  109. */
  110. protected function shouldSkipTest()
  111. {
  112. return false;
  113. }//end shouldSkipTest()
  114. /**
  115. * Tests the extending classes Sniff class.
  116. *
  117. * @return void
  118. *
  119. * @throws \PHP_CodeSniffer\Exceptions\RuntimeException
  120. */
  121. final public function testSniff()
  122. {
  123. // Skip this test if we can't run in this environment.
  124. if ($this->shouldSkipTest() === true) {
  125. $this->markTestSkipped();
  126. }
  127. $sniffCode = Common::getSniffCode(get_class($this));
  128. $sniffCodeParts = explode('.', $sniffCode);
  129. $standardName = $sniffCodeParts[0];
  130. $testFileBase = (new ReflectionClass(static::class))->getFileName();
  131. $testFileBase = substr($testFileBase, 0, -3);
  132. // Get a list of all test files to check.
  133. $testFiles = $this->getTestFiles($testFileBase);
  134. if (empty($testFiles) === true) {
  135. $this->markTestIncomplete('No test case files found for '.static::class);
  136. }
  137. $sniffFile = preg_replace('`[/\\\\]Tests[/\\\\]`', DIRECTORY_SEPARATOR.'Sniffs'.DIRECTORY_SEPARATOR, $testFileBase);
  138. $sniffFile = str_replace('UnitTest.', 'Sniff.php', $sniffFile);
  139. if (file_exists($sniffFile) === false) {
  140. $this->fail(sprintf('ERROR: Sniff file %s for test %s does not appear to exist', $sniffFile, static::class));
  141. }
  142. $replacements = [
  143. $standardName,
  144. $sniffFile,
  145. ];
  146. $rulesetContents = str_replace(self::SEARCH_FOR, $replacements, self::RULESET_TEMPLATE);
  147. if (file_put_contents(self::RULESET_FILENAME, $rulesetContents) === false) {
  148. throw new RuntimeException('Failed to write custom ruleset file');
  149. }
  150. if (isset(self::$config) === true) {
  151. $config = self::$config;
  152. } else {
  153. $config = new ConfigDouble();
  154. $config->cache = false;
  155. self::$config = $config;
  156. }
  157. $config->standards = [self::RULESET_FILENAME];
  158. $config->sniffs = [$sniffCode];
  159. $config->ignored = [];
  160. $ruleset = new Ruleset($config);
  161. $failureMessages = [];
  162. foreach ($testFiles as $testFile) {
  163. $filename = basename($testFile);
  164. $oldConfig = $config->getSettings();
  165. try {
  166. $this->setCliValues($filename, $config);
  167. $phpcsFile = new LocalFile($testFile, $ruleset, $config);
  168. $phpcsFile->process();
  169. } catch (RuntimeException $e) {
  170. $this->fail('An unexpected exception has been caught: '.$e->getMessage());
  171. }
  172. $failures = $this->generateFailureMessages($phpcsFile);
  173. $failureMessages = array_merge($failureMessages, $failures);
  174. if ($phpcsFile->getFixableCount() > 0) {
  175. // Attempt to fix the errors.
  176. $phpcsFile->fixer->fixFile();
  177. $fixable = $phpcsFile->getFixableCount();
  178. if ($fixable > 0) {
  179. $failureMessages[] = "Failed to fix $fixable fixable violations in $filename";
  180. }
  181. // Check for a .fixed file to check for accuracy of fixes.
  182. $fixedFile = $testFile.'.fixed';
  183. $filename = basename($testFile);
  184. if (file_exists($fixedFile) === true) {
  185. if ($phpcsFile->fixer->getContents() !== file_get_contents($fixedFile)) {
  186. // Only generate the (expensive) diff if a difference is expected.
  187. $diff = $phpcsFile->fixer->generateDiff($fixedFile);
  188. if (trim($diff) !== '') {
  189. $fixedFilename = basename($fixedFile);
  190. $failureMessages[] = "Fixed version of $filename does not match expected version in $fixedFilename; the diff is\n$diff";
  191. }
  192. }
  193. } else {
  194. $diff = trim($phpcsFile->fixer->generateDiff($testFile));
  195. $failureMessages[] = "Missing fixed version of $filename to verify the accuracy of fixes, while the sniff is making fixes against the test case file; the diff is\n$diff";
  196. }
  197. }//end if
  198. // Restore the config.
  199. $config->setSettings($oldConfig);
  200. }//end foreach
  201. if (empty($failureMessages) === false) {
  202. $this->fail(implode(PHP_EOL, $failureMessages));
  203. }
  204. }//end testSniff()
  205. /**
  206. * Generate a list of test failures for a given sniffed file.
  207. *
  208. * @param \PHP_CodeSniffer\Files\LocalFile $file The file being tested.
  209. *
  210. * @return array
  211. * @throws \PHP_CodeSniffer\Exceptions\RuntimeException
  212. */
  213. public function generateFailureMessages(LocalFile $file)
  214. {
  215. $testFile = $file->getFilename();
  216. $foundErrors = $file->getErrors();
  217. $foundWarnings = $file->getWarnings();
  218. $expectedErrors = $this->getErrorList(basename($testFile));
  219. $expectedWarnings = $this->getWarningList(basename($testFile));
  220. if (is_array($expectedErrors) === false) {
  221. throw new RuntimeException('getErrorList() must return an array');
  222. }
  223. if (is_array($expectedWarnings) === false) {
  224. throw new RuntimeException('getWarningList() must return an array');
  225. }
  226. /*
  227. We merge errors and warnings together to make it easier
  228. to iterate over them and produce the errors string. In this way,
  229. we can report on errors and warnings in the same line even though
  230. it's not really structured to allow that.
  231. */
  232. $allProblems = [];
  233. $failureMessages = [];
  234. foreach ($foundErrors as $line => $lineErrors) {
  235. foreach ($lineErrors as $column => $errors) {
  236. if (isset($allProblems[$line]) === false) {
  237. $allProblems[$line] = [
  238. 'expected_errors' => 0,
  239. 'expected_warnings' => 0,
  240. 'found_errors' => [],
  241. 'found_warnings' => [],
  242. ];
  243. }
  244. $foundErrorsTemp = [];
  245. foreach ($allProblems[$line]['found_errors'] as $foundError) {
  246. $foundErrorsTemp[] = $foundError;
  247. }
  248. $errorsTemp = [];
  249. foreach ($errors as $foundError) {
  250. $errorsTemp[] = $foundError['message'].' ('.$foundError['source'].')';
  251. }
  252. $allProblems[$line]['found_errors'] = array_merge($foundErrorsTemp, $errorsTemp);
  253. }//end foreach
  254. if (isset($expectedErrors[$line]) === true) {
  255. $allProblems[$line]['expected_errors'] = $expectedErrors[$line];
  256. } else {
  257. $allProblems[$line]['expected_errors'] = 0;
  258. }
  259. unset($expectedErrors[$line]);
  260. }//end foreach
  261. foreach ($expectedErrors as $line => $numErrors) {
  262. if (isset($allProblems[$line]) === false) {
  263. $allProblems[$line] = [
  264. 'expected_errors' => 0,
  265. 'expected_warnings' => 0,
  266. 'found_errors' => [],
  267. 'found_warnings' => [],
  268. ];
  269. }
  270. $allProblems[$line]['expected_errors'] = $numErrors;
  271. }
  272. foreach ($foundWarnings as $line => $lineWarnings) {
  273. foreach ($lineWarnings as $column => $warnings) {
  274. if (isset($allProblems[$line]) === false) {
  275. $allProblems[$line] = [
  276. 'expected_errors' => 0,
  277. 'expected_warnings' => 0,
  278. 'found_errors' => [],
  279. 'found_warnings' => [],
  280. ];
  281. }
  282. $foundWarningsTemp = [];
  283. foreach ($allProblems[$line]['found_warnings'] as $foundWarning) {
  284. $foundWarningsTemp[] = $foundWarning;
  285. }
  286. $warningsTemp = [];
  287. foreach ($warnings as $warning) {
  288. $warningsTemp[] = $warning['message'].' ('.$warning['source'].')';
  289. }
  290. $allProblems[$line]['found_warnings'] = array_merge($foundWarningsTemp, $warningsTemp);
  291. }//end foreach
  292. if (isset($expectedWarnings[$line]) === true) {
  293. $allProblems[$line]['expected_warnings'] = $expectedWarnings[$line];
  294. } else {
  295. $allProblems[$line]['expected_warnings'] = 0;
  296. }
  297. unset($expectedWarnings[$line]);
  298. }//end foreach
  299. foreach ($expectedWarnings as $line => $numWarnings) {
  300. if (isset($allProblems[$line]) === false) {
  301. $allProblems[$line] = [
  302. 'expected_errors' => 0,
  303. 'expected_warnings' => 0,
  304. 'found_errors' => [],
  305. 'found_warnings' => [],
  306. ];
  307. }
  308. $allProblems[$line]['expected_warnings'] = $numWarnings;
  309. }
  310. // Order the messages by line number.
  311. ksort($allProblems);
  312. foreach ($allProblems as $line => $problems) {
  313. $numErrors = count($problems['found_errors']);
  314. $numWarnings = count($problems['found_warnings']);
  315. $expectedErrors = $problems['expected_errors'];
  316. $expectedWarnings = $problems['expected_warnings'];
  317. $errors = '';
  318. $foundString = '';
  319. if ($expectedErrors !== $numErrors || $expectedWarnings !== $numWarnings) {
  320. $lineMessage = "[LINE $line]";
  321. $expectedMessage = 'Expected ';
  322. $foundMessage = 'in '.basename($testFile).' but found ';
  323. if ($expectedErrors !== $numErrors) {
  324. $expectedMessage .= "$expectedErrors error(s)";
  325. $foundMessage .= "$numErrors error(s)";
  326. if ($numErrors !== 0) {
  327. $foundString .= 'error(s)';
  328. $errors .= implode(PHP_EOL.' -> ', $problems['found_errors']);
  329. }
  330. if ($expectedWarnings !== $numWarnings) {
  331. $expectedMessage .= ' and ';
  332. $foundMessage .= ' and ';
  333. if ($numWarnings !== 0) {
  334. if ($foundString !== '') {
  335. $foundString .= ' and ';
  336. }
  337. }
  338. }
  339. }
  340. if ($expectedWarnings !== $numWarnings) {
  341. $expectedMessage .= "$expectedWarnings warning(s)";
  342. $foundMessage .= "$numWarnings warning(s)";
  343. if ($numWarnings !== 0) {
  344. $foundString .= 'warning(s)';
  345. if (empty($errors) === false) {
  346. $errors .= PHP_EOL.' -> ';
  347. }
  348. $errors .= implode(PHP_EOL.' -> ', $problems['found_warnings']);
  349. }
  350. }
  351. $fullMessage = "$lineMessage $expectedMessage $foundMessage.";
  352. if ($errors !== '') {
  353. $fullMessage .= " The $foundString found were:".PHP_EOL." -> $errors";
  354. }
  355. $failureMessages[] = $fullMessage;
  356. }//end if
  357. }//end foreach
  358. return $failureMessages;
  359. }//end generateFailureMessages()
  360. /**
  361. * Get a list of CLI values to set before the file is tested.
  362. *
  363. * @param string $filename The name of the file being tested.
  364. * @param \PHP_CodeSniffer\Config $config The config data for the run.
  365. *
  366. * @return void
  367. */
  368. public function setCliValues($filename, $config)
  369. {
  370. }//end setCliValues()
  371. /**
  372. * Returns the lines where errors should occur.
  373. *
  374. * The key of the array should represent the line number and the value
  375. * should represent the number of errors that should occur on that line.
  376. *
  377. * @return array<int, int>
  378. */
  379. abstract protected function getErrorList();
  380. /**
  381. * Returns the lines where warnings should occur.
  382. *
  383. * The key of the array should represent the line number and the value
  384. * should represent the number of warnings that should occur on that line.
  385. *
  386. * @return array<int, int>
  387. */
  388. abstract protected function getWarningList();
  389. }//end class