This commit is contained in:
Sebastian Molenda
2026-05-12 21:10:38 +02:00
commit ab96d82fcf
2544 changed files with 721700 additions and 0 deletions
@@ -0,0 +1,210 @@
<?php
/**
* Config class for use in the tests.
*
* The Config class contains a number of static properties.
* As the value of these static properties will be retained between instantiations of the class,
* config values set in one test can influence the results for another test, which makes tests unstable.
*
* This class is a "double" of the Config class which prevents this from happening.
* In _most_ cases, tests should be using this class instead of the "normal" Config,
* with the exception of select tests for the Config class itself.
*
* @author Juliette Reinders Folmer <phpcs_nospam@adviesenzo.nl>
* @copyright 2023 PHPCSStandards and contributors
* @license https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/HEAD/licence.txt BSD Licence
*/
namespace PHP_CodeSniffer\Tests;
use PHP_CodeSniffer\Config;
use ReflectionProperty;
final class ConfigDouble extends Config
{
/**
* Whether or not the setting of a standard should be skipped.
*
* @var boolean
*/
private $skipSettingStandard = false;
/**
* Creates a clean Config object and populates it with command line values.
*
* @param array<string> $cliArgs An array of values gathered from CLI args.
* @param bool $skipSettingStandard Whether to skip setting a standard to prevent
* the Config class trying to auto-discover a ruleset file.
* Should only be set to `true` for tests which actually test
* the ruleset auto-discovery.
* Note: there is no need to set this to `true` when a standard
* is being passed via the `$cliArgs`. Those settings will always
* be respected.
* Defaults to `false`. Will result in the standard being set
* to "PSR1" if not provided via `$cliArgs`.
* @param bool $skipSettingReportWidth Whether to skip setting a report-width to prevent
* the Config class trying to auto-discover the screen width.
* Should only be set to `true` for tests which actually test
* the screen width auto-discovery.
* Note: there is no need to set this to `true` when a report-width
* is being passed via the `$cliArgs`. Those settings will always
* respected.
* Defaults to `false`. Will result in the reportWidth being set
* to "80" if not provided via `$cliArgs`.
*
* @return void
*/
public function __construct(array $cliArgs = [], bool $skipSettingStandard = false, bool $skipSettingReportWidth = false)
{
$this->skipSettingStandard = $skipSettingStandard;
$this->resetSelectProperties();
$this->preventReadingCodeSnifferConfFile();
parent::__construct($cliArgs);
if ($skipSettingReportWidth !== true) {
$this->preventAutoDiscoveryScreenWidth();
}
}
/**
* Ensures the static properties in the Config class are reset to their default values
* when the ConfigDouble is no longer used.
*
* @return void
*/
public function __destruct()
{
$this->setStaticConfigProperty('executablePaths', []);
$this->setStaticConfigProperty('configData', null);
$this->setStaticConfigProperty('configDataFile', null);
}
/**
* Sets the command line values and optionally prevents a file system search for a custom ruleset.
*
* @param array<string> $args An array of command line arguments to set.
*
* @return void
*/
public function setCommandLineValues($args)
{
parent::setCommandLineValues($args);
if ($this->skipSettingStandard !== true) {
$this->preventSearchingForRuleset();
}
}
/**
* Reset select properties on the Config class to their default values.
*
* @return void
*/
private function resetSelectProperties()
{
$this->setStaticConfigProperty('executablePaths', []);
}
/**
* Prevent the values in a potentially available user-specific `CodeSniffer.conf` file
* from influencing the tests.
*
* This also prevents some file system calls which can influence the test runtime.
*
* @return void
*/
private function preventReadingCodeSnifferConfFile()
{
$this->setStaticConfigProperty('configData', []);
$this->setStaticConfigProperty('configDataFile', '');
}
/**
* Prevent searching for a custom ruleset by setting a standard, but only if the test
* being run doesn't set a standard itself.
*
* This also prevents some file system calls which can influence the test runtime.
*
* The standard being set is the smallest one available so the ruleset initialization
* will be the fastest possible.
*
* @return void
*/
private function preventSearchingForRuleset()
{
$overriddenDefaults = $this->getStaticConfigProperty('overriddenDefaults');
if (isset($overriddenDefaults['standards']) === false) {
$this->standards = ['PSR1'];
$overriddenDefaults['standards'] = true;
}
self::setStaticConfigProperty('overriddenDefaults', $overriddenDefaults);
}
/**
* Prevent a call to stty to figure out the screen width, but only if the test being run
* doesn't set a report width itself.
*
* @return void
*/
private function preventAutoDiscoveryScreenWidth()
{
$settings = $this->getSettings();
if ($settings['reportWidth'] === 'auto') {
$this->reportWidth = self::DEFAULT_REPORT_WIDTH;
}
}
/**
* Helper function to retrieve the value of a private static property on the Config class.
*
* @param string $name The name of the property to retrieve.
*
* @return mixed
*/
private function getStaticConfigProperty(string $name)
{
$property = new ReflectionProperty(Config::class, $name);
(PHP_VERSION_ID < 80100) && $property->setAccessible(true);
if ($name === 'overriddenDefaults') {
return $property->getValue($this);
}
return $property->getValue();
}
/**
* Helper function to set the value of a private static property on the Config class.
*
* @param string $name The name of the property to set.
* @param mixed $value The value to set the property to.
*
* @return void
*/
private function setStaticConfigProperty(string $name, $value)
{
$property = new ReflectionProperty(Config::class, $name);
(PHP_VERSION_ID < 80100) && $property->setAccessible(true);
if ($name === 'overriddenDefaults') {
$property->setValue($this, $value);
} else {
$property->setValue(null, $value);
}
(PHP_VERSION_ID < 80100) && $property->setAccessible(false);
}
}
@@ -0,0 +1,237 @@
<?php
/**
* Base class to use when testing utility methods.
*
* @author Juliette Reinders Folmer <phpcs_nospam@adviesenzo.nl>
* @copyright 2018-2019 Juliette Reinders Folmer. All rights reserved.
* @copyright 2023 PHPCSStandards and contributors
* @license https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/HEAD/licence.txt BSD Licence
*/
namespace PHP_CodeSniffer\Tests\Core;
use Exception;
use PHP_CodeSniffer\Exceptions\RuntimeException;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Files\LocalFile;
use PHP_CodeSniffer\Ruleset;
use PHP_CodeSniffer\Tests\ConfigDouble;
use PHPUnit\Framework\TestCase;
abstract class AbstractMethodTestCase extends TestCase
{
/**
* The tab width setting to use when tokenizing the file.
*
* This allows for test case files to use a different tab width than the default.
*
* @var integer
*/
protected static $tabWidth = 4;
/**
* The \PHP_CodeSniffer\Files\File object containing the parsed contents of the test case file.
*
* @var \PHP_CodeSniffer\Files\File
*/
protected static $phpcsFile;
/**
* Initialize & tokenize \PHP_CodeSniffer\Files\File with code from the test case file.
*
* The test case file for a unit test class has to be in the same directory
* directory and use the same file name as the test class, using the .inc extension.
*
* @return void
*/
public static function setUpBeforeClass(): void
{
$_SERVER['argv'] = [];
$config = new ConfigDouble();
// Also set a tab-width to enable testing tab-replaced vs `orig_content`.
$config->tabWidth = static::$tabWidth;
$ruleset = new Ruleset($config);
// Default to a file with the same name as the test class. Extension is property based.
$relativeCN = str_replace(__NAMESPACE__, '', static::class);
$relativePath = str_replace('\\', DIRECTORY_SEPARATOR, $relativeCN);
$pathToTestFile = realpath(__DIR__) . $relativePath . '.inc';
self::$phpcsFile = new LocalFile($pathToTestFile, $ruleset, $config);
self::$phpcsFile->parse();
}
/**
* Clean up after finished test by resetting all static properties on the class to their default values.
*
* @return void
*/
public static function tearDownAfterClass(): void
{
// Explicitly trigger __destruct() on the ConfigDouble to reset the Config statics.
// The explicit method call prevents potential stray test-local references to the $config object
// preventing the destructor from running the clean up (which without stray references would be
// automagically triggered when `self::$phpcsFile` is reset, but we can't definitively rely on that).
if (isset(self::$phpcsFile) === true) {
self::$phpcsFile->config->__destruct();
}
self::$tabWidth = 4;
self::$phpcsFile = null;
}
/**
* Test QA: verify that a test case file does not contain any duplicate test markers.
*
* When a test case file contains a lot of test cases, it is easy to overlook that a test marker name
* is already in use.
* A test wouldn't necessarily fail on this, but would not be testing what is intended to be tested as
* it would be verifying token properties for the wrong token.
*
* This test safeguards against this.
*
* @coversNothing
*
* @return void
*/
public function testTestMarkersAreUnique()
{
$this->assertTestMarkersAreUnique(self::$phpcsFile);
}
/**
* Assertion to verify that a test case file does not contain any duplicate test markers.
*
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file to validate.
*
* @return void
*/
public static function assertTestMarkersAreUnique(File $phpcsFile)
{
$tokens = $phpcsFile->getTokens();
// Collect all marker comments in the file.
$seenComments = [];
for ($i = 0; $i < $phpcsFile->numTokens; $i++) {
if ($tokens[$i]['code'] !== T_COMMENT) {
continue;
}
if (stripos($tokens[$i]['content'], '/* test') !== 0) {
continue;
}
$seenComments[] = $tokens[$i]['content'];
}
self::assertSame(array_unique($seenComments), $seenComments, 'Duplicate test markers found.');
}
/**
* Get the token pointer for a target token based on a specific comment found on the line before.
*
* Note: the test delimiter comment MUST start with "/* test" to allow this function to
* distinguish between comments used *in* a test and test delimiters.
*
* @param string $commentString The delimiter comment to look for.
* @param int|string|array<int|string> $tokenType The type of token(s) to look for.
* @param string $tokenContent Optional. The token content for the target token.
*
* @return int
*/
public function getTargetToken($commentString, $tokenType, $tokenContent = null)
{
return self::getTargetTokenFromFile(self::$phpcsFile, $commentString, $tokenType, $tokenContent);
}
/**
* Get the token pointer for a target token based on a specific comment found on the line before.
*
* Note: the test delimiter comment MUST start with "/* test" to allow this function to
* distinguish between comments used *in* a test and test delimiters.
*
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file to find the token in.
* @param string $commentString The delimiter comment to look for.
* @param int|string|array<int|string> $tokenType The type of token(s) to look for.
* @param string $tokenContent Optional. The token content for the target token.
*
* @return int
*
* @throws \Exception When the test delimiter comment is not found.
* @throws \Exception When the test target token is not found.
*/
public static function getTargetTokenFromFile(File $phpcsFile, $commentString, $tokenType, $tokenContent = null)
{
$start = ($phpcsFile->numTokens - 1);
$comment = $phpcsFile->findPrevious(
T_COMMENT,
$start,
null,
false,
$commentString
);
if ($comment === false) {
throw new Exception(
sprintf('Failed to find the test marker: %s in test case file %s', $commentString, $phpcsFile->getFilename())
);
}
$tokens = $phpcsFile->getTokens();
$end = ($start + 1);
// Limit the token finding to between this and the next delimiter comment.
for ($i = ($comment + 1); $i < $end; $i++) {
if ($tokens[$i]['code'] !== T_COMMENT) {
continue;
}
if (stripos($tokens[$i]['content'], '/* test') === 0) {
$end = $i;
break;
}
}
$target = $phpcsFile->findNext(
$tokenType,
($comment + 1),
$end,
false,
$tokenContent
);
if ($target === false) {
$msg = 'Failed to find test target token for comment string: ' . $commentString;
if ($tokenContent !== null) {
$msg .= ' with token content: ' . $tokenContent;
}
throw new Exception($msg);
}
return $target;
}
/**
* Helper method to tell PHPUnit to expect a PHPCS RuntimeException in a PHPUnit cross-version
* compatible manner.
*
* @param string $message The expected exception message.
*
* @return void
*/
public function expectRunTimeException($message)
{
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage($message);
}
}
@@ -0,0 +1,73 @@
#!/usr/bin/env bash
function tear_down() {
rm -f tests/EndToEnd/Fixtures/*.fixed
}
function test_phpcs_exit_code_clean_file() {
bin/phpcs --standard=tests/EndToEnd/Fixtures/endtoend.xml.dist tests/EndToEnd/Fixtures/ClassOneWithoutStyleError.inc
assert_exit_code 0
}
function test_phpcs_exit_code_clean_stdin() {
bin/phpcs --standard=tests/EndToEnd/Fixtures/endtoend.xml.dist < tests/EndToEnd/Fixtures/ClassOneWithoutStyleError.inc
assert_exit_code 0
}
function test_phpcbf_exit_code_clean_file() {
bin/phpcbf --suffix=.fixed --standard=tests/EndToEnd/Fixtures/endtoend.xml.dist tests/EndToEnd/Fixtures/ClassOneWithoutStyleError.inc
assert_exit_code 0
}
function test_phpcbf_exit_code_clean_stdin() {
bin/phpcbf --suffix=.fixed --standard=tests/EndToEnd/Fixtures/endtoend.xml.dist < tests/EndToEnd/Fixtures/ClassOneWithoutStyleError.inc
assert_exit_code 0
}
function test_phpcs_exit_code_fixable_file() {
bin/phpcs --standard=tests/EndToEnd/Fixtures/endtoend.xml.dist tests/EndToEnd/Fixtures/ClassWithStyleError.inc
assert_exit_code 1
}
function test_phpcs_exit_code_fixable_stdin() {
bin/phpcs --standard=tests/EndToEnd/Fixtures/endtoend.xml.dist < tests/EndToEnd/Fixtures/ClassWithStyleError.inc
assert_exit_code 1
}
function test_phpcbf_exit_code_fixable_file() {
bin/phpcbf --suffix=.fixed --standard=tests/EndToEnd/Fixtures/endtoend.xml.dist tests/EndToEnd/Fixtures/ClassWithStyleError.inc
assert_exit_code 0
}
function test_phpcbf_exit_code_fixable_stdin() {
bin/phpcbf --suffix=.fixed --standard=tests/EndToEnd/Fixtures/endtoend.xml.dist < tests/EndToEnd/Fixtures/ClassWithStyleError.inc
assert_exit_code 0
}
function test_phpcs_exit_code_non_fixable_file() {
bin/phpcs --standard=tests/EndToEnd/Fixtures/endtoend.xml.dist tests/EndToEnd/Fixtures/ClassWithUnfixableStyleError.inc
assert_exit_code 2
}
function test_phpcs_exit_code_non_fixable_stdin() {
bin/phpcs --standard=tests/EndToEnd/Fixtures/endtoend.xml.dist < tests/EndToEnd/Fixtures/ClassWithUnfixableStyleError.inc
assert_exit_code 2
}
function test_phpcbf_exit_code_non_fixable_file() {
bin/phpcbf --suffix=.fixed --standard=tests/EndToEnd/Fixtures/endtoend.xml.dist tests/EndToEnd/Fixtures/ClassWithUnfixableStyleError.inc
assert_exit_code 2
}
function test_phpcbf_exit_code_non_fixable_stdin() {
bin/phpcbf --suffix=.fixed --standard=tests/EndToEnd/Fixtures/endtoend.xml.dist < tests/EndToEnd/Fixtures/ClassWithUnfixableStyleError.inc
assert_exit_code 2
}
function test_phpcs_exit_code_fixable_and_non_fixable_file() {
bin/phpcs --standard=tests/EndToEnd/Fixtures/endtoend.xml.dist tests/EndToEnd/Fixtures/ClassWithTwoStyleErrors.inc
assert_exit_code 3
}
function test_phpcs_exit_code_fixable_and_non_fixable_stdin() {
bin/phpcs --standard=tests/EndToEnd/Fixtures/endtoend.xml.dist < tests/EndToEnd/Fixtures/ClassWithTwoStyleErrors.inc
assert_exit_code 3
}
function test_phpcbf_exit_code_fixable_and_non_fixable_file() {
bin/phpcbf --suffix=.fixed --standard=tests/EndToEnd/Fixtures/endtoend.xml.dist tests/EndToEnd/Fixtures/ClassWithTwoStyleErrors.inc
assert_exit_code 2
}
function test_phpcbf_exit_code_fixable_and_non_fixable_stdin() {
bin/phpcbf --suffix=.fixed --standard=tests/EndToEnd/Fixtures/endtoend.xml.dist < tests/EndToEnd/Fixtures/ClassWithTwoStyleErrors.inc
assert_exit_code 2
}
@@ -0,0 +1,25 @@
#!/usr/bin/env bash
function tear_down() {
rm -f tests/EndToEnd/Fixtures/*.fixed
}
function test_phpcs_out_of_memory_error_handling() {
OUTPUT="$( { bin/phpcs -d memory_limit=4M --standard=tests/EndToEnd/Fixtures/endtoend.xml.dist tests/EndToEnd/Fixtures/; } 2>&1)"
# The exact exit code is not our concern, just that it's non-zero.
assert_unsuccessful_code
assert_contains "The PHP_CodeSniffer \"phpcs\" command ran out of memory." "$OUTPUT"
assert_contains "Either raise the \"memory_limit\" of PHP in the php.ini file or raise the memory limit at runtime" "$OUTPUT"
assert_contains "using \"phpcs -d memory_limit=512M\" (replace 512M with the desired memory limit)." "$OUTPUT"
}
function test_phpcbf_out_of_memory_error_handling() {
OUTPUT="$( { bin/phpcbf -d memory_limit=4M --standard=tests/EndToEnd/Fixtures/endtoend.xml.dist tests/EndToEnd/Fixtures/ --suffix=.fixed; } 2>&1)"
# The exact exit code is not our concern, just that it's non-zero.
assert_unsuccessful_code
assert_contains "The PHP_CodeSniffer \"phpcbf\" command ran out of memory." "$OUTPUT"
assert_contains "Either raise the \"memory_limit\" of PHP in the php.ini file or raise the memory limit at runtime" "$OUTPUT"
assert_contains "using \"phpcbf -d memory_limit=512M\" (replace 512M with the desired memory limit)." "$OUTPUT"
}
@@ -0,0 +1,45 @@
#!/usr/bin/env bash
function tear_down() {
rm -rf tests/EndToEnd/Fixtures/*.fixed
}
function test_phpcbf_is_working() {
OUTPUT="$( { bin/phpcbf --no-cache --standard=tests/EndToEnd/Fixtures/endtoend.xml.dist tests/EndToEnd/Fixtures/ClassOneWithoutStyleError.inc tests/EndToEnd/Fixtures/ClassTwoWithoutStyleError.inc; } 2>&1 )"
assert_successful_code
assert_contains "No violations were found" "$OUTPUT"
}
function test_phpcbf_is_working_in_parallel() {
OUTPUT="$( { bin/phpcbf --no-cache --parallel=2 --standard=tests/EndToEnd/Fixtures/endtoend.xml.dist tests/EndToEnd/Fixtures/ClassOneWithoutStyleError.inc tests/EndToEnd/Fixtures/ClassTwoWithoutStyleError.inc; } 2>&1 )"
assert_successful_code
assert_contains "No violations were found" "$OUTPUT"
}
function test_phpcbf_returns_error_on_issues() {
OUTPUT="$( { bin/phpcbf --no-colors --parallel=1 --no-cache --suffix=.fixed --standard=tests/EndToEnd/Fixtures/endtoend.xml.dist tests/EndToEnd/Fixtures/ClassWithStyleError.inc; } 2>&1 )"
assert_successful_code
assert_contains "F 1 / 1 (100%)" "$OUTPUT"
assert_contains "A TOTAL OF 1 ERROR WERE FIXED IN 1 FILE" "$OUTPUT"
}
function test_phpcbf_progressbar_shows_fixes_with_parallel_on() {
OUTPUT="$( { bin/phpcbf --no-colors --parallel=10 --no-cache --suffix=.fixed --standard=tests/EndToEnd/Fixtures/endtoend.xml.dist tests/EndToEnd/Fixtures/ClassWithStyleError.inc; } 2>&1 )"
assert_successful_code
assert_contains "F 1 / 1 (100%)" "$OUTPUT"
}
function test_phpcbf_bug_1112() {
# See https://github.com/PHPCSStandards/PHP_CodeSniffer/issues/1112
if [[ "$(uname)" == "Darwin" ]]; then
# Perform some magic with `& fg` to prevent the processes from turning into a background job.
assert_successful_code "$(bash -ic 'bash --init-file <(echo "echo \"Subprocess\"") -c "bin/phpcbf --no-cache --parallel=2 --standard=tests/EndToEnd/Fixtures/endtoend.xml.dist tests/EndToEnd/Fixtures/ClassOneWithoutStyleError.inc tests/EndToEnd/Fixtures/ClassTwoWithoutStyleError.inc" & fg')"
else
# This is not needed on Linux / GitHub Actions
assert_successful_code "$(bash -ic 'bash --init-file <(echo "echo \"Subprocess\"") -c "bin/phpcbf --no-cache --parallel=2 --standard=tests/EndToEnd/Fixtures/endtoend.xml.dist tests/EndToEnd/Fixtures/ClassOneWithoutStyleError.inc tests/EndToEnd/Fixtures/ClassTwoWithoutStyleError.inc"')"
fi
}
@@ -0,0 +1,28 @@
#!/usr/bin/env bash
function test_phpcs_is_working() {
assert_successful_code "$(bin/phpcs --no-cache --standard=tests/EndToEnd/Fixtures/endtoend.xml.dist tests/EndToEnd/Fixtures/ClassOneWithoutStyleError.inc tests/EndToEnd/Fixtures/ClassTwoWithoutStyleError.inc)"
}
function test_phpcs_is_working_in_parallel() {
assert_successful_code "$(bin/phpcs --no-cache --parallel=2 --standard=tests/EndToEnd/Fixtures/endtoend.xml.dist tests/EndToEnd/Fixtures/ClassOneWithoutStyleError.inc tests/EndToEnd/Fixtures/ClassTwoWithoutStyleError.inc)"
}
function test_phpcs_returns_error_on_issues() {
OUTPUT="$( { bin/phpcs --no-colors --no-cache --standard=tests/EndToEnd/Fixtures/endtoend.xml.dist tests/EndToEnd/Fixtures/ClassWithStyleError.inc; } 2>&1 )"
assert_exit_code 1
assert_contains "E 1 / 1 (100%)" "$OUTPUT"
assert_contains "FOUND 1 ERROR AFFECTING 1 LINE" "$OUTPUT"
}
function test_phpcs_bug_1112() {
# See https://github.com/PHPCSStandards/PHP_CodeSniffer/issues/1112
if [[ "$(uname)" == "Darwin" ]]; then
# Perform some magic with `& fg` to prevent the processes from turning into a background job.
assert_successful_code "$(bash -ic 'bash --init-file <(echo "echo \"Subprocess\"") -c "bin/phpcs --no-cache --parallel=2 --standard=tests/EndToEnd/Fixtures/endtoend.xml.dist tests/EndToEnd/Fixtures/ClassOneWithoutStyleError.inc tests/EndToEnd/Fixtures/ClassTwoWithoutStyleError.inc" & fg')"
else
# This is not needed on Linux / GitHub Actions
assert_successful_code "$(bash -ic 'bash --init-file <(echo "echo \"Subprocess\"") -c "bin/phpcs --no-cache --parallel=2 --standard=tests/EndToEnd/Fixtures/endtoend.xml.dist tests/EndToEnd/Fixtures/ClassOneWithoutStyleError.inc tests/EndToEnd/Fixtures/ClassTwoWithoutStyleError.inc"')"
fi
}
@@ -0,0 +1,470 @@
<?php
/**
* An abstract class that all sniff unit tests must extend.
*
* A sniff unit test checks a .inc file for expected violations of a single
* coding standard. Expected errors and warnings that are not found, or
* warnings and errors that are not expected, are considered test failures.
*
* @author Greg Sherwood <gsherwood@squiz.net>
* @copyright 2006-2023 Squiz Pty Ltd (ABN 77 084 670 600)
* @copyright 2023 PHPCSStandards and contributors
* @license https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/HEAD/licence.txt BSD Licence
*/
namespace PHP_CodeSniffer\Tests\Standards;
use DirectoryIterator;
use PHP_CodeSniffer\Config;
use PHP_CodeSniffer\Exceptions\RuntimeException;
use PHP_CodeSniffer\Files\LocalFile;
use PHP_CodeSniffer\Ruleset;
use PHP_CodeSniffer\Tests\ConfigDouble;
use PHP_CodeSniffer\Util\Common;
use PHPUnit\Framework\TestCase;
use ReflectionClass;
abstract class AbstractSniffTestCase extends TestCase
{
/**
* Ruleset template with placeholders.
*
* @var string
*/
private const RULESET_TEMPLATE = <<<'TEMPLATE'
<?xml version="1.0"?>
<ruleset xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" name="[STANDARDNAME]" xsi:noNamespaceSchemaLocation="../../phpcs.xsd">
<description>Temporary ruleset used by the AbstractSniffUnitTest class.</description>
<rule ref="[SNIFFFILEREF]"/>
</ruleset>
TEMPLATE;
/**
* Placeholders used in the ruleset template which need to be replaced.
*
* @var array<string>
*/
private const SEARCH_FOR = [
'[STANDARDNAME]',
'[SNIFFFILEREF]',
];
/**
* Location where the temporary ruleset file will be saved.
*
* @var string
*/
private const RULESET_FILENAME = __DIR__ . '/sniffStnd.xml';
/**
* Cache for the Config object.
*
* @var \PHP_CodeSniffer\Tests\ConfigDouble
*/
private static $config;
/**
* Extensions to disregard when gathering the test files.
*
* @var array<string, string>
*/
private $ignoreExtensions = [
'php' => 'php',
'fixed' => 'fixed',
'bak' => 'bak',
'orig' => 'orig',
];
/**
* Clean up temporary ruleset file.
*
* @return void
*/
public static function tearDownAfterClass(): void
{
@unlink(self::RULESET_FILENAME);
}
/**
* Get a list of all test files to check.
*
* These will have the same base as the sniff name but different extensions.
* We ignore the .php file as it is the test class.
*
* @param string $testFileBase The base path that the unit tests files will have.
*
* @return string[]
*/
protected function getTestFiles(string $testFileBase)
{
$testFiles = [];
$dir = dirname($testFileBase);
$di = new DirectoryIterator($dir);
foreach ($di as $file) {
$path = $file->getPathname();
if (substr($path, 0, strlen($testFileBase)) === $testFileBase) {
$extension = $file->getExtension();
if (isset($this->ignoreExtensions[$extension]) === false) {
$testFiles[] = $path;
}
}
}
// Put them in order.
sort($testFiles, SORT_NATURAL);
return $testFiles;
}
/**
* Should this test be skipped for some reason.
*
* @return boolean
*/
protected function shouldSkipTest()
{
return false;
}
/**
* Tests the extending classes Sniff class.
*
* @return void
*
* @throws \PHP_CodeSniffer\Exceptions\RuntimeException
*/
final public function testSniff()
{
// Skip this test if we can't run in this environment.
if ($this->shouldSkipTest() === true) {
$this->markTestSkipped();
}
$sniffCode = Common::getSniffCode(static::class);
$sniffCodeParts = explode('.', $sniffCode);
$standardName = $sniffCodeParts[0];
$testFileBase = (new ReflectionClass(static::class))->getFileName();
$testFileBase = substr($testFileBase, 0, -3);
// Get a list of all test files to check.
$testFiles = $this->getTestFiles($testFileBase);
if (empty($testFiles) === true) {
$this->markTestIncomplete('No test case files found for ' . static::class);
}
$sniffFile = preg_replace('`[/\\\\]Tests[/\\\\]`', DIRECTORY_SEPARATOR . 'Sniffs' . DIRECTORY_SEPARATOR, $testFileBase);
$sniffFile = str_replace('UnitTest.', 'Sniff.php', $sniffFile);
if (file_exists($sniffFile) === false) {
$this->fail(sprintf('ERROR: Sniff file %s for test %s does not appear to exist', $sniffFile, static::class));
}
$replacements = [
$standardName,
$sniffFile,
];
$rulesetContents = str_replace(self::SEARCH_FOR, $replacements, self::RULESET_TEMPLATE);
if (file_put_contents(self::RULESET_FILENAME, $rulesetContents) === false) {
throw new RuntimeException('Failed to write custom ruleset file');
}
if (isset(self::$config) === true) {
$config = self::$config;
} else {
$config = new ConfigDouble();
$config->cache = false;
self::$config = $config;
}
$config->standards = [self::RULESET_FILENAME];
$config->sniffs = [$sniffCode];
$config->ignored = [];
$ruleset = new Ruleset($config);
$failureMessages = [];
foreach ($testFiles as $testFile) {
$filename = basename($testFile);
$oldConfig = $config->getSettings();
try {
$this->setCliValues($filename, $config);
$phpcsFile = new LocalFile($testFile, $ruleset, $config);
$phpcsFile->process();
} catch (RuntimeException $e) {
$this->fail('An unexpected exception has been caught: ' . $e->getMessage());
}
$failures = $this->generateFailureMessages($phpcsFile);
$failureMessages = array_merge($failureMessages, $failures);
if ($phpcsFile->getFixableCount() > 0) {
// Attempt to fix the errors.
$phpcsFile->fixer->fixFile();
$fixable = $phpcsFile->getFixableCount();
if ($fixable > 0) {
$failureMessages[] = "Failed to fix $fixable fixable violations in $filename";
}
// Check for a .fixed file to check for accuracy of fixes.
$fixedFile = $testFile . '.fixed';
$filename = basename($testFile);
if (file_exists($fixedFile) === true) {
if ($phpcsFile->fixer->getContents() !== file_get_contents($fixedFile)) {
// Only generate the (expensive) diff if a difference is expected.
$diff = $phpcsFile->fixer->generateDiff($fixedFile);
if (trim($diff) !== '') {
$fixedFilename = basename($fixedFile);
$failureMessages[] = "Fixed version of $filename does not match expected version in $fixedFilename; the diff is\n$diff";
}
}
} else {
$diff = trim($phpcsFile->fixer->generateDiff($testFile));
$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";
}
}
// Restore the config.
$config->setSettings($oldConfig);
}
if (empty($failureMessages) === false) {
$this->fail(implode(PHP_EOL, $failureMessages));
}
}
/**
* Generate a list of test failures for a given sniffed file.
*
* @param \PHP_CodeSniffer\Files\LocalFile $file The file being tested.
*
* @return array
* @throws \PHP_CodeSniffer\Exceptions\RuntimeException
*/
public function generateFailureMessages(LocalFile $file)
{
$testFile = $file->getFilename();
$foundErrors = $file->getErrors();
$foundWarnings = $file->getWarnings();
$expectedErrors = $this->getErrorList(basename($testFile));
$expectedWarnings = $this->getWarningList(basename($testFile));
if (is_array($expectedErrors) === false) {
throw new RuntimeException('getErrorList() must return an array');
}
if (is_array($expectedWarnings) === false) {
throw new RuntimeException('getWarningList() must return an array');
}
/*
We merge errors and warnings together to make it easier
to iterate over them and produce the errors string. In this way,
we can report on errors and warnings in the same line even though
it's not really structured to allow that.
*/
$allProblems = [];
$failureMessages = [];
foreach ($foundErrors as $line => $lineErrors) {
foreach ($lineErrors as $column => $errors) {
if (isset($allProblems[$line]) === false) {
$allProblems[$line] = [
'expected_errors' => 0,
'expected_warnings' => 0,
'found_errors' => [],
'found_warnings' => [],
];
}
$foundErrorsTemp = [];
foreach ($allProblems[$line]['found_errors'] as $foundError) {
$foundErrorsTemp[] = $foundError;
}
$errorsTemp = [];
foreach ($errors as $foundError) {
$errorsTemp[] = $foundError['message'] . ' (' . $foundError['source'] . ')';
}
$allProblems[$line]['found_errors'] = array_merge($foundErrorsTemp, $errorsTemp);
}
if (isset($expectedErrors[$line]) === true) {
$allProblems[$line]['expected_errors'] = $expectedErrors[$line];
} else {
$allProblems[$line]['expected_errors'] = 0;
}
unset($expectedErrors[$line]);
}
foreach ($expectedErrors as $line => $numErrors) {
if (isset($allProblems[$line]) === false) {
$allProblems[$line] = [
'expected_errors' => 0,
'expected_warnings' => 0,
'found_errors' => [],
'found_warnings' => [],
];
}
$allProblems[$line]['expected_errors'] = $numErrors;
}
foreach ($foundWarnings as $line => $lineWarnings) {
foreach ($lineWarnings as $column => $warnings) {
if (isset($allProblems[$line]) === false) {
$allProblems[$line] = [
'expected_errors' => 0,
'expected_warnings' => 0,
'found_errors' => [],
'found_warnings' => [],
];
}
$foundWarningsTemp = [];
foreach ($allProblems[$line]['found_warnings'] as $foundWarning) {
$foundWarningsTemp[] = $foundWarning;
}
$warningsTemp = [];
foreach ($warnings as $warning) {
$warningsTemp[] = $warning['message'] . ' (' . $warning['source'] . ')';
}
$allProblems[$line]['found_warnings'] = array_merge($foundWarningsTemp, $warningsTemp);
}
if (isset($expectedWarnings[$line]) === true) {
$allProblems[$line]['expected_warnings'] = $expectedWarnings[$line];
} else {
$allProblems[$line]['expected_warnings'] = 0;
}
unset($expectedWarnings[$line]);
}
foreach ($expectedWarnings as $line => $numWarnings) {
if (isset($allProblems[$line]) === false) {
$allProblems[$line] = [
'expected_errors' => 0,
'expected_warnings' => 0,
'found_errors' => [],
'found_warnings' => [],
];
}
$allProblems[$line]['expected_warnings'] = $numWarnings;
}
// Order the messages by line number.
ksort($allProblems);
foreach ($allProblems as $line => $problems) {
$numErrors = count($problems['found_errors']);
$numWarnings = count($problems['found_warnings']);
$expectedErrors = $problems['expected_errors'];
$expectedWarnings = $problems['expected_warnings'];
$errors = '';
$foundString = '';
if ($expectedErrors !== $numErrors || $expectedWarnings !== $numWarnings) {
$lineMessage = "[LINE $line]";
$expectedMessage = 'Expected ';
$foundMessage = 'in ' . basename($testFile) . ' but found ';
if ($expectedErrors !== $numErrors) {
$expectedMessage .= "$expectedErrors error(s)";
$foundMessage .= "$numErrors error(s)";
if ($numErrors !== 0) {
$foundString .= 'error(s)';
$errors .= implode(PHP_EOL . ' -> ', $problems['found_errors']);
}
if ($expectedWarnings !== $numWarnings) {
$expectedMessage .= ' and ';
$foundMessage .= ' and ';
if ($numWarnings !== 0) {
if ($foundString !== '') {
$foundString .= ' and ';
}
}
}
}
if ($expectedWarnings !== $numWarnings) {
$expectedMessage .= "$expectedWarnings warning(s)";
$foundMessage .= "$numWarnings warning(s)";
if ($numWarnings !== 0) {
$foundString .= 'warning(s)';
if (empty($errors) === false) {
$errors .= PHP_EOL . ' -> ';
}
$errors .= implode(PHP_EOL . ' -> ', $problems['found_warnings']);
}
}
$fullMessage = "$lineMessage $expectedMessage $foundMessage.";
if ($errors !== '') {
$fullMessage .= " The $foundString found were:" . PHP_EOL . " -> $errors";
}
$failureMessages[] = $fullMessage;
}
}
return $failureMessages;
}
/**
* Get a list of CLI values to set before the file is tested.
*
* @param string $testFile The name of the file being tested.
* @param \PHP_CodeSniffer\Config $config The config data for the run.
*
* @return void
*/
public function setCliValues(string $testFile, Config $config)
{
}
/**
* Returns the lines where errors should occur.
*
* The key of the array should represent the line number and the value
* should represent the number of errors that should occur on that line.
*
* @return array<int, int>
*/
abstract protected function getErrorList();
/**
* Returns the lines where warnings should occur.
*
* The key of the array should represent the line number and the value
* should represent the number of warnings that should occur on that line.
*
* @return array<int, int>
*/
abstract protected function getWarningList();
}
@@ -0,0 +1,54 @@
<?php
/**
* Bootstrap file for PHP_CodeSniffer unit tests.
*
* @author Greg Sherwood <gsherwood@squiz.net>
* @copyright 2006-2023 Squiz Pty Ltd (ABN 77 084 670 600)
* @copyright 2023 PHPCSStandards and contributors
* @license https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/HEAD/licence.txt BSD Licence
*/
use PHP_CodeSniffer\Autoload;
use PHP_CodeSniffer\Util\Standards;
use PHP_CodeSniffer\Util\Tokens;
if (defined('PHP_CODESNIFFER_IN_TESTS') === false) {
define('PHP_CODESNIFFER_IN_TESTS', true);
}
/*
* Determine whether the test suite should be run in CBF mode.
*
* Use `<php><env name="PHP_CODESNIFFER_CBF" value="1"/></php>` in a `phpunit.xml` file
* or set the ENV variable at an OS-level to enable CBF mode.
*
* To run the CBF specific tests, use the following command:
* vendor/bin/phpunit --group CBF --exclude-group nothing
*
* If the ENV variable has not been set, or is set to "false", the tests will run in CS mode.
*/
if (defined('PHP_CODESNIFFER_CBF') === false) {
$cbfMode = getenv('PHP_CODESNIFFER_CBF');
if ($cbfMode === '1') {
define('PHP_CODESNIFFER_CBF', true);
echo 'Note: Tests are running in "CBF" mode' . PHP_EOL . PHP_EOL;
} else {
define('PHP_CODESNIFFER_CBF', false);
echo 'Note: Tests are running in "CS" mode' . PHP_EOL . PHP_EOL;
}
}
if (defined('PHP_CODESNIFFER_VERBOSITY') === false) {
define('PHP_CODESNIFFER_VERBOSITY', 0);
}
require_once __DIR__ . '/../autoload.php';
// Make sure all installed standards are autoloadable.
$installedStandards = Standards::getInstalledStandardDetails();
foreach ($installedStandards as $standardDetails) {
Autoload::addSearchPath($standardDetails['path'], $standardDetails['namespace']);
}
$tokens = new Tokens();