Chris@17: Chris@17: * @copyright 2006-2015 Squiz Pty Ltd (ABN 77 084 670 600) Chris@17: * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence Chris@17: */ Chris@17: Chris@17: namespace PHP_CodeSniffer\Tests\Standards; Chris@17: Chris@17: use PHP_CodeSniffer\Config; Chris@17: use PHP_CodeSniffer\Exceptions\RuntimeException; Chris@17: use PHP_CodeSniffer\Ruleset; Chris@17: use PHP_CodeSniffer\Files\LocalFile; Chris@17: use PHP_CodeSniffer\Util\Common; Chris@17: use PHPUnit\Framework\TestCase; Chris@17: Chris@17: abstract class AbstractSniffUnitTest extends TestCase Chris@17: { Chris@17: Chris@17: /** Chris@17: * Enable or disable the backup and restoration of the $GLOBALS array. Chris@17: * Overwrite this attribute in a child class of TestCase. Chris@17: * Setting this attribute in setUp() has no effect! Chris@17: * Chris@17: * @var boolean Chris@17: */ Chris@17: protected $backupGlobals = false; Chris@17: Chris@17: /** Chris@17: * The path to the standard's main directory. Chris@17: * Chris@17: * @var string Chris@17: */ Chris@17: public $standardsDir = null; Chris@17: Chris@17: /** Chris@17: * The path to the standard's test directory. Chris@17: * Chris@17: * @var string Chris@17: */ Chris@17: public $testsDir = null; Chris@17: Chris@17: Chris@17: /** Chris@17: * Sets up this unit test. Chris@17: * Chris@17: * @return void Chris@17: */ Chris@17: protected function setUp() Chris@17: { Chris@17: $class = get_class($this); Chris@17: $this->standardsDir = $GLOBALS['PHP_CODESNIFFER_STANDARD_DIRS'][$class]; Chris@17: $this->testsDir = $GLOBALS['PHP_CODESNIFFER_TEST_DIRS'][$class]; Chris@17: Chris@17: }//end setUp() Chris@17: Chris@17: Chris@17: /** Chris@17: * Get a list of all test files to check. Chris@17: * Chris@17: * These will have the same base as the sniff name but different extensions. Chris@17: * We ignore the .php file as it is the class. Chris@17: * Chris@17: * @param string $testFileBase The base path that the unit tests files will have. Chris@17: * Chris@17: * @return string[] Chris@17: */ Chris@17: protected function getTestFiles($testFileBase) Chris@17: { Chris@17: $testFiles = []; Chris@17: Chris@17: $dir = substr($testFileBase, 0, strrpos($testFileBase, DIRECTORY_SEPARATOR)); Chris@17: $di = new \DirectoryIterator($dir); Chris@17: Chris@17: foreach ($di as $file) { Chris@17: $path = $file->getPathname(); Chris@17: if (substr($path, 0, strlen($testFileBase)) === $testFileBase) { Chris@17: if ($path !== $testFileBase.'php' && substr($path, -5) !== 'fixed' && substr($path, -4) !== '.bak') { Chris@17: $testFiles[] = $path; Chris@17: } Chris@17: } Chris@17: } Chris@17: Chris@17: // Put them in order. Chris@17: sort($testFiles); Chris@17: Chris@17: return $testFiles; Chris@17: Chris@17: }//end getTestFiles() Chris@17: Chris@17: Chris@17: /** Chris@17: * Should this test be skipped for some reason. Chris@17: * Chris@17: * @return boolean Chris@17: */ Chris@17: protected function shouldSkipTest() Chris@17: { Chris@17: return false; Chris@17: Chris@17: }//end shouldSkipTest() Chris@17: Chris@17: Chris@17: /** Chris@17: * Tests the extending classes Sniff class. Chris@17: * Chris@17: * @return void Chris@17: * @throws \PHPUnit\Framework\Exception Chris@17: */ Chris@17: final public function testSniff() Chris@17: { Chris@17: // Skip this test if we can't run in this environment. Chris@17: if ($this->shouldSkipTest() === true) { Chris@17: $this->markTestSkipped(); Chris@17: } Chris@17: Chris@17: $sniffCode = Common::getSniffCode(get_class($this)); Chris@17: list($standardName, $categoryName, $sniffName) = explode('.', $sniffCode); Chris@17: Chris@17: $testFileBase = $this->testsDir.$categoryName.DIRECTORY_SEPARATOR.$sniffName.'UnitTest.'; Chris@17: Chris@17: // Get a list of all test files to check. Chris@17: $testFiles = $this->getTestFiles($testFileBase); Chris@17: $GLOBALS['PHP_CODESNIFFER_SNIFF_CASE_FILES'][] = $testFiles; Chris@17: Chris@17: if (isset($GLOBALS['PHP_CODESNIFFER_CONFIG']) === true) { Chris@17: $config = $GLOBALS['PHP_CODESNIFFER_CONFIG']; Chris@17: } else { Chris@17: $config = new Config(); Chris@17: $config->cache = false; Chris@17: $GLOBALS['PHP_CODESNIFFER_CONFIG'] = $config; Chris@17: } Chris@17: Chris@17: $config->standards = [$standardName]; Chris@17: $config->sniffs = [$sniffCode]; Chris@17: $config->ignored = []; Chris@17: Chris@17: if (isset($GLOBALS['PHP_CODESNIFFER_RULESETS']) === false) { Chris@17: $GLOBALS['PHP_CODESNIFFER_RULESETS'] = []; Chris@17: } Chris@17: Chris@17: if (isset($GLOBALS['PHP_CODESNIFFER_RULESETS'][$standardName]) === false) { Chris@17: $ruleset = new Ruleset($config); Chris@17: $GLOBALS['PHP_CODESNIFFER_RULESETS'][$standardName] = $ruleset; Chris@17: } Chris@17: Chris@17: $ruleset = $GLOBALS['PHP_CODESNIFFER_RULESETS'][$standardName]; Chris@17: Chris@17: $sniffFile = $this->standardsDir.DIRECTORY_SEPARATOR.'Sniffs'.DIRECTORY_SEPARATOR.$categoryName.DIRECTORY_SEPARATOR.$sniffName.'Sniff.php'; Chris@17: Chris@17: $sniffClassName = substr(get_class($this), 0, -8).'Sniff'; Chris@17: $sniffClassName = str_replace('\Tests\\', '\Sniffs\\', $sniffClassName); Chris@17: $sniffClassName = Common::cleanSniffClass($sniffClassName); Chris@17: Chris@17: $restrictions = [strtolower($sniffClassName) => true]; Chris@17: $ruleset->registerSniffs([$sniffFile], $restrictions, []); Chris@17: $ruleset->populateTokenListeners(); Chris@17: Chris@17: $failureMessages = []; Chris@17: foreach ($testFiles as $testFile) { Chris@17: $filename = basename($testFile); Chris@17: $oldConfig = $config->getSettings(); Chris@17: Chris@17: try { Chris@17: $this->setCliValues($filename, $config); Chris@17: $phpcsFile = new LocalFile($testFile, $ruleset, $config); Chris@17: $phpcsFile->process(); Chris@17: } catch (RuntimeException $e) { Chris@17: $this->fail('An unexpected exception has been caught: '.$e->getMessage()); Chris@17: } Chris@17: Chris@17: $failures = $this->generateFailureMessages($phpcsFile); Chris@17: $failureMessages = array_merge($failureMessages, $failures); Chris@17: Chris@17: if ($phpcsFile->getFixableCount() > 0) { Chris@17: // Attempt to fix the errors. Chris@17: $phpcsFile->fixer->fixFile(); Chris@17: $fixable = $phpcsFile->getFixableCount(); Chris@17: if ($fixable > 0) { Chris@17: $failureMessages[] = "Failed to fix $fixable fixable violations in $filename"; Chris@17: } Chris@17: Chris@17: // Check for a .fixed file to check for accuracy of fixes. Chris@17: $fixedFile = $testFile.'.fixed'; Chris@17: if (file_exists($fixedFile) === true) { Chris@17: $diff = $phpcsFile->fixer->generateDiff($fixedFile); Chris@17: if (trim($diff) !== '') { Chris@17: $filename = basename($testFile); Chris@17: $fixedFilename = basename($fixedFile); Chris@17: $failureMessages[] = "Fixed version of $filename does not match expected version in $fixedFilename; the diff is\n$diff"; Chris@17: } Chris@17: } Chris@17: } Chris@17: Chris@17: // Restore the config. Chris@17: $config->setSettings($oldConfig); Chris@17: }//end foreach Chris@17: Chris@17: if (empty($failureMessages) === false) { Chris@17: $this->fail(implode(PHP_EOL, $failureMessages)); Chris@17: } Chris@17: Chris@17: }//end testSniff() Chris@17: Chris@17: Chris@17: /** Chris@17: * Generate a list of test failures for a given sniffed file. Chris@17: * Chris@17: * @param \PHP_CodeSniffer\Files\LocalFile $file The file being tested. Chris@17: * Chris@17: * @return array Chris@17: * @throws \PHP_CodeSniffer\Exceptions\RuntimeException Chris@17: */ Chris@17: public function generateFailureMessages(LocalFile $file) Chris@17: { Chris@17: $testFile = $file->getFilename(); Chris@17: Chris@17: $foundErrors = $file->getErrors(); Chris@17: $foundWarnings = $file->getWarnings(); Chris@17: $expectedErrors = $this->getErrorList(basename($testFile)); Chris@17: $expectedWarnings = $this->getWarningList(basename($testFile)); Chris@17: Chris@17: if (is_array($expectedErrors) === false) { Chris@17: throw new RuntimeException('getErrorList() must return an array'); Chris@17: } Chris@17: Chris@17: if (is_array($expectedWarnings) === false) { Chris@17: throw new RuntimeException('getWarningList() must return an array'); Chris@17: } Chris@17: Chris@17: /* Chris@17: We merge errors and warnings together to make it easier Chris@17: to iterate over them and produce the errors string. In this way, Chris@17: we can report on errors and warnings in the same line even though Chris@17: it's not really structured to allow that. Chris@17: */ Chris@17: Chris@17: $allProblems = []; Chris@17: $failureMessages = []; Chris@17: Chris@17: foreach ($foundErrors as $line => $lineErrors) { Chris@17: foreach ($lineErrors as $column => $errors) { Chris@17: if (isset($allProblems[$line]) === false) { Chris@17: $allProblems[$line] = [ Chris@17: 'expected_errors' => 0, Chris@17: 'expected_warnings' => 0, Chris@17: 'found_errors' => [], Chris@17: 'found_warnings' => [], Chris@17: ]; Chris@17: } Chris@17: Chris@17: $foundErrorsTemp = []; Chris@17: foreach ($allProblems[$line]['found_errors'] as $foundError) { Chris@17: $foundErrorsTemp[] = $foundError; Chris@17: } Chris@17: Chris@17: $errorsTemp = []; Chris@17: foreach ($errors as $foundError) { Chris@17: $errorsTemp[] = $foundError['message'].' ('.$foundError['source'].')'; Chris@17: Chris@17: $source = $foundError['source']; Chris@18: if (in_array($source, $GLOBALS['PHP_CODESNIFFER_SNIFF_CODES'], true) === false) { Chris@17: $GLOBALS['PHP_CODESNIFFER_SNIFF_CODES'][] = $source; Chris@17: } Chris@17: Chris@17: if ($foundError['fixable'] === true Chris@18: && in_array($source, $GLOBALS['PHP_CODESNIFFER_FIXABLE_CODES'], true) === false Chris@17: ) { Chris@17: $GLOBALS['PHP_CODESNIFFER_FIXABLE_CODES'][] = $source; Chris@17: } Chris@17: } Chris@17: Chris@17: $allProblems[$line]['found_errors'] = array_merge($foundErrorsTemp, $errorsTemp); Chris@17: }//end foreach Chris@17: Chris@17: if (isset($expectedErrors[$line]) === true) { Chris@17: $allProblems[$line]['expected_errors'] = $expectedErrors[$line]; Chris@17: } else { Chris@17: $allProblems[$line]['expected_errors'] = 0; Chris@17: } Chris@17: Chris@17: unset($expectedErrors[$line]); Chris@17: }//end foreach Chris@17: Chris@17: foreach ($expectedErrors as $line => $numErrors) { Chris@17: if (isset($allProblems[$line]) === false) { Chris@17: $allProblems[$line] = [ Chris@17: 'expected_errors' => 0, Chris@17: 'expected_warnings' => 0, Chris@17: 'found_errors' => [], Chris@17: 'found_warnings' => [], Chris@17: ]; Chris@17: } Chris@17: Chris@17: $allProblems[$line]['expected_errors'] = $numErrors; Chris@17: } Chris@17: Chris@17: foreach ($foundWarnings as $line => $lineWarnings) { Chris@17: foreach ($lineWarnings as $column => $warnings) { Chris@17: if (isset($allProblems[$line]) === false) { Chris@17: $allProblems[$line] = [ Chris@17: 'expected_errors' => 0, Chris@17: 'expected_warnings' => 0, Chris@17: 'found_errors' => [], Chris@17: 'found_warnings' => [], Chris@17: ]; Chris@17: } Chris@17: Chris@17: $foundWarningsTemp = []; Chris@17: foreach ($allProblems[$line]['found_warnings'] as $foundWarning) { Chris@17: $foundWarningsTemp[] = $foundWarning; Chris@17: } Chris@17: Chris@17: $warningsTemp = []; Chris@17: foreach ($warnings as $warning) { Chris@17: $warningsTemp[] = $warning['message'].' ('.$warning['source'].')'; Chris@17: } Chris@17: Chris@17: $allProblems[$line]['found_warnings'] = array_merge($foundWarningsTemp, $warningsTemp); Chris@17: }//end foreach Chris@17: Chris@17: if (isset($expectedWarnings[$line]) === true) { Chris@17: $allProblems[$line]['expected_warnings'] = $expectedWarnings[$line]; Chris@17: } else { Chris@17: $allProblems[$line]['expected_warnings'] = 0; Chris@17: } Chris@17: Chris@17: unset($expectedWarnings[$line]); Chris@17: }//end foreach Chris@17: Chris@17: foreach ($expectedWarnings as $line => $numWarnings) { Chris@17: if (isset($allProblems[$line]) === false) { Chris@17: $allProblems[$line] = [ Chris@17: 'expected_errors' => 0, Chris@17: 'expected_warnings' => 0, Chris@17: 'found_errors' => [], Chris@17: 'found_warnings' => [], Chris@17: ]; Chris@17: } Chris@17: Chris@17: $allProblems[$line]['expected_warnings'] = $numWarnings; Chris@17: } Chris@17: Chris@17: // Order the messages by line number. Chris@17: ksort($allProblems); Chris@17: Chris@17: foreach ($allProblems as $line => $problems) { Chris@17: $numErrors = count($problems['found_errors']); Chris@17: $numWarnings = count($problems['found_warnings']); Chris@17: $expectedErrors = $problems['expected_errors']; Chris@17: $expectedWarnings = $problems['expected_warnings']; Chris@17: Chris@17: $errors = ''; Chris@17: $foundString = ''; Chris@17: Chris@17: if ($expectedErrors !== $numErrors || $expectedWarnings !== $numWarnings) { Chris@17: $lineMessage = "[LINE $line]"; Chris@17: $expectedMessage = 'Expected '; Chris@17: $foundMessage = 'in '.basename($testFile).' but found '; Chris@17: Chris@17: if ($expectedErrors !== $numErrors) { Chris@17: $expectedMessage .= "$expectedErrors error(s)"; Chris@17: $foundMessage .= "$numErrors error(s)"; Chris@17: if ($numErrors !== 0) { Chris@17: $foundString .= 'error(s)'; Chris@17: $errors .= implode(PHP_EOL.' -> ', $problems['found_errors']); Chris@17: } Chris@17: Chris@17: if ($expectedWarnings !== $numWarnings) { Chris@17: $expectedMessage .= ' and '; Chris@17: $foundMessage .= ' and '; Chris@17: if ($numWarnings !== 0) { Chris@17: if ($foundString !== '') { Chris@17: $foundString .= ' and '; Chris@17: } Chris@17: } Chris@17: } Chris@17: } Chris@17: Chris@17: if ($expectedWarnings !== $numWarnings) { Chris@17: $expectedMessage .= "$expectedWarnings warning(s)"; Chris@17: $foundMessage .= "$numWarnings warning(s)"; Chris@17: if ($numWarnings !== 0) { Chris@17: $foundString .= 'warning(s)'; Chris@17: if (empty($errors) === false) { Chris@17: $errors .= PHP_EOL.' -> '; Chris@17: } Chris@17: Chris@17: $errors .= implode(PHP_EOL.' -> ', $problems['found_warnings']); Chris@17: } Chris@17: } Chris@17: Chris@17: $fullMessage = "$lineMessage $expectedMessage $foundMessage."; Chris@17: if ($errors !== '') { Chris@17: $fullMessage .= " The $foundString found were:".PHP_EOL." -> $errors"; Chris@17: } Chris@17: Chris@17: $failureMessages[] = $fullMessage; Chris@17: }//end if Chris@17: }//end foreach Chris@17: Chris@17: return $failureMessages; Chris@17: Chris@17: }//end generateFailureMessages() Chris@17: Chris@17: Chris@17: /** Chris@17: * Get a list of CLI values to set before the file is tested. Chris@17: * Chris@17: * @param string $filename The name of the file being tested. Chris@17: * @param \PHP_CodeSniffer\Config $config The config data for the run. Chris@17: * Chris@17: * @return void Chris@17: */ Chris@17: public function setCliValues($filename, $config) Chris@17: { Chris@17: return; Chris@17: Chris@17: }//end setCliValues() Chris@17: Chris@17: Chris@17: /** Chris@17: * Returns the lines where errors should occur. Chris@17: * Chris@17: * The key of the array should represent the line number and the value Chris@17: * should represent the number of errors that should occur on that line. Chris@17: * Chris@17: * @return array Chris@17: */ Chris@17: abstract protected function getErrorList(); Chris@17: Chris@17: Chris@17: /** Chris@17: * Returns the lines where warnings should occur. Chris@17: * Chris@17: * The key of the array should represent the line number and the value Chris@17: * should represent the number of warnings that should occur on that line. Chris@17: * Chris@17: * @return array Chris@17: */ Chris@17: abstract protected function getWarningList(); Chris@17: Chris@17: Chris@17: }//end class