From 1aba9cc1aa1c1a85e501d4b6826c83b907cdff08 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Thu, 27 Jan 2022 09:38:22 +0100 Subject: [PATCH 01/14] Tests: create initial setup * Require the PHPUnit Polyfills as a basis for the tests. The PHPUnit Polyfills will ensure a compatible PHPUnit version will be available and will allow for writing tests for the latest version of PHPUnit (9.x), while still being able to run the tests on PHPUnit 4-8. * Include a basic test bootstrap file which will allow for running the tests both via a Composer installed version of PHPUnit as well as via a PHPUnit PHAR file. Note: the bootstrap file will try to determine where the `composer.phar` file is located. For running the tests locally and/or overruling the default version of Composer, a `COMPOSER_PHAR` environment variable can be set in a local `phpunit.xml` overload file. * Include information about the test setup in the `CONTRIBUTING` documentation. * Includes adding new jobs to the "Quick test" and "Integration test" GH Actions workflows. Once the existing tests in the _old_ workflows have been converted to the new test setup, the old workflows can be removed. * Minor tweaks to various repository config files. Note: this test setup presumes that the proposal to drop support for PHP 5.3 - see issue 145 - will be accepted as the PHPUnit Polyfills require PHP >= 5.4. Refs: * https://github.com/Yoast/PHPUnit-Polyfills/ --- .gitattributes | 6 +- .github/workflows/integrationtest.yml | 61 ++++++++++++++++++ .github/workflows/quicktest.yml | 44 +++++++++++++ .gitignore | 3 + CONTRIBUTING.md | 40 +++++++++++- composer.json | 14 ++++- phpcs.xml.dist | 12 +++- phpunit.xml.dist | 30 +++++++++ tests/TestCase.php | 17 +++++ tests/bootstrap.php | 91 +++++++++++++++++++++++++++ 10 files changed, 314 insertions(+), 4 deletions(-) create mode 100644 phpunit.xml.dist create mode 100644 tests/TestCase.php create mode 100644 tests/bootstrap.php diff --git a/.gitattributes b/.gitattributes index 5e43d025..79c814f5 100644 --- a/.gitattributes +++ b/.gitattributes @@ -6,4 +6,8 @@ .github export-ignore CONTRIBUTING.md export-ignore -phpcs.xml.dist export-ignore \ No newline at end of file +phpcs.xml.dist export-ignore +phpunit.xml.dist export-ignore + +# Ignore directories for distribution archives. +/tests/ export-ignore diff --git a/.github/workflows/integrationtest.yml b/.github/workflows/integrationtest.yml index 0c772663..e312cf67 100644 --- a/.github/workflows/integrationtest.yml +++ b/.github/workflows/integrationtest.yml @@ -185,3 +185,64 @@ jobs: - name: 'Test the PHPCompatibility standard was installed succesfully (PHPCS >= 2.3)' if: ${{ matrix.phpcompat == 'composer' }} run: ./vendor/bin/phpcs -ps ./src/ --standard=PHPCompatibility --sniffs=PHPCompatibility.FunctionUse.RemovedFunctions --runtime-set testVersion ${{ matrix.php }} + + new-test: + runs-on: "${{ matrix.os }}" + + strategy: + matrix: + php: + - '5.4' + - '5.5' + - '5.6' + - '7.0' + - '7.1' + - '7.2' + - '7.3' + - '7.4' + - '8.0' + - '8.1' + - '8.2' + composer: + - 'v1' + - 'v2' + os: + - 'ubuntu-latest' + - 'windows-latest' + + name: "Integration test" + + continue-on-error: ${{ matrix.php == '8.2' }} + + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Install PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + ini-values: zend.assertions=1, error_reporting=-1, display_errors=On + tools: "composer:${{ matrix.composer }}" + coverage: none + + - name: "Conditionally disable tls (Composer 1.x/Windows/PHP 5.4)" + if: ${{ matrix.os == 'windows-latest' && matrix.composer == 'v1' && matrix.php == '5.4' }} + run: composer config -- disable-tls true + + # Install dependencies and handle caching in one go. + # @link https://github.com/marketplace/actions/install-composer-dependencies + - name: Install Composer dependencies + if: ${{ matrix.php != '8.2' }} + uses: "ramsey/composer-install@v2" + with: + composer-options: '--optimize-autoloader' + + - name: Install Composer dependencies + if: ${{ matrix.php == '8.2' }} + uses: "ramsey/composer-install@v2" + with: + composer-options: '--ignore-platform-reqs --optimize-autoloader' + + - name: Run integration tests + run: vendor/bin/phpunit --no-coverage diff --git a/.github/workflows/quicktest.yml b/.github/workflows/quicktest.yml index 1b88771a..7c31ce8b 100644 --- a/.github/workflows/quicktest.yml +++ b/.github/workflows/quicktest.yml @@ -89,3 +89,47 @@ jobs: - name: 'Test the PHPCompatibility standard was installed succesfully (PHPCS >= 2.3)' if: ${{ matrix.phpcompat == 'composer' }} run: ./vendor/bin/phpcs -ps ./src/ --standard=PHPCompatibility --sniffs=PHPCompatibility.FunctionUse.RemovedFunctions --runtime-set testVersion ${{ matrix.php }} + + new-quicktest: + runs-on: "${{ matrix.os }}" + + strategy: + matrix: + php: + - '5.4' + - '7.2' + - 'latest' + composer: + - 'v1' + - 'v2' + os: + - 'ubuntu-latest' + - 'windows-latest' + + name: "Quick test" + + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Install PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + ini-values: zend.assertions=1, error_reporting=-1, display_errors=On + tools: "composer:${{ matrix.composer }}" + coverage: none + + - name: "Conditionally disable tls (Composer 1.x/Windows/PHP 5.4)" + if: ${{ matrix.os == 'windows-latest' && matrix.composer == 'v1' && matrix.php == '5.4' }} + run: composer config -- disable-tls true + + # Install dependencies and handle caching in one go. + # @link https://github.com/marketplace/actions/install-composer-dependencies + - name: Install Composer dependencies + uses: "ramsey/composer-install@v2" + with: + composer-options: '--optimize-autoloader' + + - name: Run integration tests + run: vendor/bin/phpunit --no-coverage diff --git a/.gitignore b/.gitignore index c5f3d254..a02668c0 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,6 @@ vendor/ composer.lock .phpcs.xml phpcs.xml +.phpunit.result.cache +phpunit.xml +/build/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6056ec53..5c4b8d99 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -23,7 +23,9 @@ Even better: You could submit a pull request with a fix / new feature! 1. Search our repository for open or closed [pull requests][prs] that relate to your submission. You don't want to duplicate effort. -2. You may merge the pull request in once you have the sign-off of two other +2. All pull requests are expected to be accompanied by tests which cover the change. + +3. You may merge the pull request in once you have the sign-off of two other developers, or if you do not have permission to do that, you may request the second reviewer to merge it for you. @@ -39,6 +41,7 @@ These tools fall into two categories: PHP and non-PHP. The PHP specific tools used by this build are: +- [PHPUnit][] and the [PHPUnit Polyfills][] for the integration tests. - [PHP_CodeSniffer][] to verify PHP code complies with the [PSR12][] standard. - [PHPCompatibility][] to verify that code is written in a PHP cross-version compatible manner. - [PHP-Parallel-Lint][] to check against parse errors in PHP files. @@ -57,6 +60,8 @@ can be downloaded suitable for your operating system from their [releases page][ Alternatively, these tools can be run using `docker run`, through the Docker images provided by [Pipeline-Component][]. +[PHPUnit]: https://phpunit.de/ +[PHPUnit Polyfills]: https://github.com/Yoast/PHPUnit-Polyfills/ [PHP_CodeSniffer]: https://github.com/squizlabs/PHP_CodeSniffer [PHPCompatibility]: https://github.com/PHPCompatibility/PHPCompatibility [PHP-Parallel-Lint]: https://github.com/php-parallel-lint/PHP-Parallel-Lint @@ -64,6 +69,39 @@ images provided by [Pipeline-Component][]. [PSR12]: https://www.php-fig.org/psr/psr-12/ [releases page]: https://github.com/fabpot/local-php-security-checker/releases/ +#### Automated testing + +This package includes a test setup for automated testing on all supported PHP versions +using [PHPUnit][] with the [PHPUnit Polyfills][]. +This means that tests can be written for the latest version of PHPUnit +(9.x at the time of writing) and still be run on all PHPUnit versions needed to test +all supported PHP versions (PHPUnit 4.x - 9.x). + +The tests can be run both via a Composer installed version of PHPUnit, as well as using +a PHPUnit PHAR file, however, whichever way you run the tests, you will always need to +make sure that `composer install` has been run on the repository to make sure the +PHPUnit Polyfills are available. + +**Note**: _as these tests run Composer and other CLI commands they will be slow to run._ + +To run the tests locally: +1. Run `composer install` +2. Run the tests either using a PHPUnit PHAR file or by calling `composer test`. + +In case the test setup has trouble locating your `composer.phar` file: + +1. Copy the `phpunit.xml.dist` file to `phpunit.xml`. + +2. Edit the `phpunit.xml` file and add the following, replacing the value with the applicable path to Composer for your local machine: + ```xml + + + + ``` + **Note**: this setting also allows for locally testing with different versions of Composer. + You could, for instance, have multiple Composer Phar files locally, `composer1.phar`, `composer2.1.phar`, `composer2.2.phar`. + By changing the path in the value of this `env` setting, you can switch which version will be used in the tests. + ### Non-PHP The non-PHP specific tools used by this build are: diff --git a/composer.json b/composer.json index 545a9cfc..ec44bb96 100644 --- a/composer.json +++ b/composer.json @@ -34,7 +34,8 @@ "require-dev": { "composer/composer": "*", "phpcompatibility/php-compatibility": "^9.0", - "php-parallel-lint/php-parallel-lint": "^1.3.1" + "php-parallel-lint/php-parallel-lint": "^1.3.1", + "yoast/phpunit-polyfills": "^1.0.1" }, "minimum-stability": "dev", "prefer-stable": true, @@ -43,6 +44,11 @@ "Dealerdirect\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\": "src/" } }, + "autoload-dev": { + "psr-4": { + "Dealerdirect\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\Tests\\": "tests/" + } + }, "extra": { "class": "Dealerdirect\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\Plugin" }, @@ -52,6 +58,12 @@ ], "lint": [ "@php ./vendor/php-parallel-lint/php-parallel-lint/parallel-lint . -e php --exclude vendor --exclude .git" + ], + "test": [ + "@php ./vendor/phpunit/phpunit/phpunit --no-coverage" + ], + "coverage": [ + "@php ./vendor/phpunit/phpunit/phpunit" ] } } diff --git a/phpcs.xml.dist b/phpcs.xml.dist index 2a73914f..0bbe32ed 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -9,7 +9,8 @@ . - */.github/* + */.git* + */build/* */vendor/* @@ -23,4 +24,13 @@ + + */tests/bootstrap\.php$ + + + + + */tests/ + + diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 00000000..6a49f70b --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,30 @@ + + + + + + tests/IntegrationTest/ + + + + + + ./src/ + + + + + + + + diff --git a/tests/TestCase.php b/tests/TestCase.php new file mode 100644 index 00000000..4c094fb8 --- /dev/null +++ b/tests/TestCase.php @@ -0,0 +1,17 @@ + configuration to your local phpunit.xml' + . ' overload file before running the tests.' . \PHP_EOL + . 'The value should point to the local Composer phar file you want to use for the tests.'; + die(1); +} + +// Get the version of Composer being used. +$command = '"' . \PHP_BINARY . '" "' . \COMPOSER_PHAR . '" --version --no-ansi --no-interaction'; +$lastLine = exec($command, $output, $exitcode); +if ($exitcode === 0 && preg_match('`Composer version ([^\s]+)`', $lastLine, $matches) === 1) { + define('COMPOSER_VERSION', $matches[1]); +} else { + echo 'Could not determine the version of Composer being used.'; + die(1); +} From 8a644c0c3158154790c3c4e92bae574c8ceb5ecc Mon Sep 17 00:00:00 2001 From: jrfnl Date: Fri, 28 Jan 2022 10:59:57 +0100 Subject: [PATCH 02/14] TestCase: add helper methods for setup/teardown As most tests will need to run a `composer install`, tests should be run in separate, temporary directories. With that in mind, I'm adding two helper methods to the base `TestCase` class: * `createTestEnvironment()` - this method will create a `PHPCSPluginTest/TestClassName_uniqueID/local` and a `PHPCSPluginTest/TestClassName_uniqueID/global` directory in the system temp directory and set the Composer global `HOME` directory to the created `global` directory. * `removeTestEnvironment()` - this method will remove the created directories and reset the Composer global `HOME` directory to its original location. Notes: * The paths to the created directories will be added to the `$tempDir`, `$tempGlobalPath` and `$tempLocalPath` properties of the class and can be used in the tests. * The methods (and associated properties) have been made `static` on purpose to allow for these methods to be used both in `setUp()`/`tearDown()` fixtures as well as in `setUpBeforeClass()`/`tearDownAfterClass()` fixtures. The latter would be useful for a test class needing only one environment with various test methods depending on each other and building on each other. This is also the reason that these methods are set up as helpers instead of as fixtures as this way, the decision what fixtures are most appropriate can be made for each individual concrete test class. * A hard cleanup of the `PHPCSPluginTest` directory in the system temp directory has been added to the test bootstrap to make sure that any stray "old" test directories will be removed before a new test run. Includes a `static` `onWindows()` helper method to determine whether or not the tests are being run on Windows. --- tests/TestCase.php | 68 +++++++++++++++++++++++++++++++++++++++++++++ tests/bootstrap.php | 13 +++++++++ 2 files changed, 81 insertions(+) diff --git a/tests/TestCase.php b/tests/TestCase.php index 4c094fb8..c0bed4db 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -10,8 +10,76 @@ namespace Dealerdirect\Composer\Plugin\Installers\PHPCodeSniffer\Tests; +use RuntimeException; use Yoast\PHPUnitPolyfills\TestCases\TestCase as PolyfillTestCase; abstract class TestCase extends PolyfillTestCase { + protected static $tempDir; + + protected static $tempGlobalPath; + + protected static $tempLocalPath; + + + /* ***** SETUP AND TEARDOWN HELPERS ***** */ + + public static function createTestEnvironment() + { + // Make temp directory + $class = substr(strrchr(get_called_class(), '\\'), 1); + static::$tempDir = sys_get_temp_dir() . '/PHPCSPluginTest/' . uniqid("{$class}_", true); + + $subDirs = array( + 'tempLocalPath' => 'local', + 'tempGlobalPath' => 'global', + ); + + foreach ($subDirs as $property => $subDir) { + $path = static::$tempDir . '/' . $subDir; + if (mkdir($path, 0766, true) === false || is_dir($path) === false) { + throw new RuntimeException("Failed to create the $path directory for the test"); + } + + static::${$property} = $path; + } + + putenv('COMPOSER_HOME=' . static::$tempGlobalPath); + } + + public static function removeTestEnvironment() + { + if (file_exists(static::$tempDir) === true) { + // Remove temp directory, including all files. + if (static::onWindows() === true) { + // Windows. + exec(sprintf('rd /s /q %s', escapeshellarg(static::$tempDir)), $output, $exitCode); + } else { + exec(sprintf('rm -rf %s', escapeshellarg(static::$tempDir)), $output, $exitCode); + } + + if ($exitCode !== 0) { + throw new RuntimeException( + 'Failed to remove the temp directory created for the test: ' . \PHP_EOL . 'Error: ' . $output + ); + } + + clearstatcache(); + } + + putenv('COMPOSER_HOME'); + } + + + /* ***** HELPER METHODS ***** */ + + /** + * Determine whether or not the tests are being run on Windows. + * + * @return bool + */ + protected static function onWindows() + { + return strpos(strtoupper(\PHP_OS), 'WIN') === 0; + } } diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 011d3890..c241ebfc 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -10,6 +10,19 @@ namespace Dealerdirect\Composer\Plugin\Installers\PHPCodeSniffer\Tests; +/* + * Make sure the tests always start with a clean slate. + */ +$tempDir = sys_get_temp_dir() . '/PHPCSPluginTest'; +if (file_exists($tempDir) === true) { + if (strpos(strtoupper(\PHP_OS), 'WIN') === 0) { + // Windows. + shell_exec(sprintf('rd /s /q %s', escapeshellarg($tempDir))); + } else { + shell_exec(sprintf('rm -rf %s', escapeshellarg($tempDir))); + } +} + if (is_dir(dirname(__DIR__) . '/vendor') && file_exists(dirname(__DIR__) . '/vendor/autoload.php')) { $vendorDir = dirname(__DIR__) . '/vendor'; } else { From 7891fcda0d29cae1aef7d76df856846c2068bde7 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Fri, 28 Jan 2022 11:15:40 +0100 Subject: [PATCH 03/14] TestCase: add helper method for on-the-fly composer.json creation This adds a test helper method, which, given an array with a Composer configuration and a target location, will on-the-fly create a `composer.json` file for use in a test. The method will automatically add two additional settings to the `composer.json` file: * It will add a `install-codestandards` script to run the plugin on demand. * It will add the `allow-plugins` permission, which is needed for Composer 2.2. By adding these keys automatically, this kind of "noise" is not needed in the actual test classes. The method is `static` so as to allow for it to be used in `setUpBeforeClass()`/`tearDownAfterClass()` fixtures. --- tests/TestCase.php | 54 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/tests/TestCase.php b/tests/TestCase.php index c0bed4db..bf582b92 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -82,4 +82,58 @@ protected static function onWindows() { return strpos(strtoupper(\PHP_OS), 'WIN') === 0; } + + /** + * Create a composer.json file based on a given configuration. + * + * @param array $config Composer configuration as an array. + * @param string $directory Location to write the resulting `composer.json` file to (without trailing slash). + * + * @return void + * + * @throws RuntimeException When either of the passed parameters are of the wrong data type. + * @throws RuntimeException When the provided configuration is invalid. + * @throws RuntimeException When the configuration could not be written to a file. + */ + protected static function writeComposerJsonFile($config, $directory) + { + if (is_array($config) === false || $config === array()) { + throw new RuntimeException('Configuration must be a non-empty array.'); + } + + if (is_string($directory) === false || $directory === '') { + throw new RuntimeException('Directory must be a non-empty string.'); + } + + // Inject ability to run the plugin via a script. + if (isset($config['scripts']['install-codestandards']) === false) { + $config['scripts']['install-codestandards'] = array( + 'Dealerdirect\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\Plugin::run', + ); + } + + // Inject permission for this plugin to run (Composer 2.2 compat). + if (isset($config['config']['allow-plugins']['dealerdirect/phpcodesniffer-composer-installer']) === false) { + $config['config']['allow-plugins']['dealerdirect/phpcodesniffer-composer-installer'] = true; + } + + /* + * Disable TLS when on Windows with Composer 1.x and PHP 5.4. + * @link https://github.com/composer/composer/issues/10495 + */ + if (static::onWindows() === true && \CLI_PHP_MINOR === '5.4' && substr(\COMPOSER_VERSION, 0, 1) === '1') { + $config['config']['disable-tls'] = true; + } + + $encoded = json_encode($config, \JSON_UNESCAPED_SLASHES | \JSON_PRETTY_PRINT); + if (json_last_error() !== \JSON_ERROR_NONE || $encoded === false) { + throw new RuntimeException('Provided configuration can not be encoded to valid JSON'); + } + + $written = file_put_contents($directory . '/composer.json', $encoded); + + if ($written === false) { + throw new RuntimeException('Failed to create the composer.json file in the temp directory for the test'); + } + } } From 80f6caf6ebf5d5bec3add80d72505f0d53f24d0c Mon Sep 17 00:00:00 2001 From: jrfnl Date: Thu, 27 Jan 2022 11:49:31 +0100 Subject: [PATCH 04/14] Tests: create zip package artifact for the current plugin code In normal circumstances, a `composer install` will download a version of the plugin from Packagist. This will not work for the tests however, as the development version of the plugin being tested will generally not be available on Packagist. With this in mind, we need a work-around to ensure that the tests are run with the **current** version of the plugin as per the last commit in the current branch. I've implemented this by using the `repositories - artifact` feature of Composer. This works as follow: * In the test bootstrap, ahead of the test run, the plugin is zipped up as a package and placed in the `tests/artifact` directory. * Whenever a `composer install/update` command is run within the test suite, the `repositories-artifact` key is injected into the `composer.json` when the `composer.json` file is written to the temp directory used for the test. * With this in place, Composer will install the plugin from the zipped file in the `tests/artifact` directory instead of downloading from Packagist. Notes: * The version number to be used for the zip artifact of the plugin is defined in the test bootstrap file. This number may need updating once in a while to ensure it is higher than the latest released version of the plugin. * In the `CreateComposerZipArtifacts` class, three properties are used to determine which files to exclude from the package - `$excludedFiles`, `$excludedExtensions` and `$excludedDirs`. Depending on new files/directories being introduced in the package, these lists may need to be updated once in a while. Ref: https://getcomposer.org/doc/05-repositories.md#artifact --- .gitignore | 1 + composer.json | 1 + tests/CreateComposerZipArtifacts.php | 210 +++++++++++++++++++++++++++ tests/TestCase.php | 8 + tests/artifact/.gitkeep | 1 + tests/bootstrap.php | 17 +++ 6 files changed, 238 insertions(+) create mode 100644 tests/CreateComposerZipArtifacts.php create mode 100644 tests/artifact/.gitkeep diff --git a/.gitignore b/.gitignore index a02668c0..568b9ede 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ phpcs.xml .phpunit.result.cache phpunit.xml /build/ +/tests/artifact/*.zip diff --git a/composer.json b/composer.json index ec44bb96..df05073f 100644 --- a/composer.json +++ b/composer.json @@ -32,6 +32,7 @@ "squizlabs/php_codesniffer": "^2.0 || ^3.1.0 || ^4.0" }, "require-dev": { + "ext-zip": "*", "composer/composer": "*", "phpcompatibility/php-compatibility": "^9.0", "php-parallel-lint/php-parallel-lint": "^1.3.1", diff --git a/tests/CreateComposerZipArtifacts.php b/tests/CreateComposerZipArtifacts.php new file mode 100644 index 00000000..613a5cf9 --- /dev/null +++ b/tests/CreateComposerZipArtifacts.php @@ -0,0 +1,210 @@ + 'composer.lock', + 'phpcs.xml.dist' => 'phpcs.xml.dist', + 'phpunit.xml.dist' => 'phpunit.xml.dist', + 'phpunit.xml' => 'phpunit.xml', + ); + + /** + * List of file extensions for files which should be excluded from the zip archives. + * + * @var array + */ + private $excludedExtensions = array( + 'md' => 'md', + 'bak' => 'bak', + 'orig' => 'orig', + ); + + /** + * List of top-level directories which should be excluded from the zip archives. + * + * Note: no need to list directories starting with a `.` as those will always be filtered out. + * + * @var array + */ + private $excludedDirs = array( + 'bin' => 'bin', + 'build' => 'build', // PHPUnit code coverage directory. + 'tests' => 'tests', + 'vendor' => 'vendor', + ); + + /** + * Constructor. + * + * @param string $artifactDir Full path to the directory to place the sipped artifacts in. + */ + public function __construct($artifactDir) + { + // Make sure the directory has a trailing slash. + if (substr($artifactDir, -1) !== '/') { + $artifactDir .= '/'; + } + + $this->artifactDir = $artifactDir; + } + + /** + * Delete all zip artifacts from the artifacts directory. + * + * @return void + */ + public function clearOldArtifacts() + { + $di = new DirectoryIterator($this->artifactDir); + foreach ($di as $fileinfo) { + if ( + $fileinfo->isDot() + || $fileinfo->isFile() === false + || $fileinfo->getExtension() !== 'zip' + ) { + continue; + } + + @unlink($fileinfo->getPathname()); + } + } + + /** + * Create a zip file of the *current* state of the plugin to be passed to Composer as an artifact. + * + * @param string $source Path to the directory to package up. + * @param string $version Version number to use for the package. + * + * @return void + */ + public function createPluginArtifact($source, $version) + { + $fileName = "dealerdirect-phpcodesniffer-composer-installer-{$version}.zip"; + $this->createZipArtifact($source, \ZIP_ARTIFACT_DIR . $fileName, $version); + } + + /** + * Create a zip file of an arbitrary directory and package it for use by Composer. + * + * Inspired by https://github.com/composer/package-versions-deprecated/blob/c6522afe5540d5fc46675043d3ed5a45a740b27c/test/PackageVersionsTest/E2EInstallerTest.php#L262-L301 + * + * @param string $source Path to the directory to package up. + * @param string $target Path to the file where to save the zip. + * @param string $version Version number to use for the package. + * + * @return void + */ + private function createZipArtifact($source, $target, $version) + { + if (file_exists($target) === true) { + @unlink($target); + } + + $zip = new ZipArchive(); + $zip->open($target, ZipArchive::CREATE); + + $directoryIterator = new RecursiveDirectoryIterator( + realpath($source), + RecursiveDirectoryIterator::SKIP_DOTS + ); + + $filteredFileIterator = new RecursiveIteratorIterator( + new RecursiveCallbackFilterIterator( + $directoryIterator, + function (SplFileInfo $file, $key, RecursiveDirectoryIterator $iterator) { + $subPathName = $iterator->getSubPathname(); + $extension = $file->getExtension(); + + return (isset($this->excludedFiles[$subPathName]) === false) + && isset($this->excludedExtensions[$extension]) === false + && isset($this->excludedDirs[$subPathName]) === false + && $subPathName[0] !== '.'; // Not a .dot-file. + } + ), + RecursiveIteratorIterator::LEAVES_ONLY + ); + + foreach ($filteredFileIterator as $file) { + if ($file->isFile() === false) { + continue; + } + + /* + * DO NOT REMOVE! + * While this block may seem unnecessary, adding an arbitrary version number in the composer.json + * file **IS** necessary for Composer installs via a repo artifact to actual work. + * This does not seem to be documented in the Composer documentation, but if the + * version is not declared in the composer.json of the artifact, the install will fail + * with a "Package ... has no version defined." exception. + */ + if ($file->getFilename() === 'composer.json') { + $contents = json_decode(file_get_contents($file->getRealPath()), true); + $contents['version'] = $version; + + $zip->addFromString( + 'composer.json', + json_encode($contents, \JSON_UNESCAPED_SLASHES | \JSON_PRETTY_PRINT) + ); + + continue; + } + + $zip->addFile( + $file->getRealPath(), + str_replace('\\', '/', substr($file->getRealPath(), strlen(realpath($source)) + 1)) + ); + } + + $zip->close(); + } +} diff --git a/tests/TestCase.php b/tests/TestCase.php index bf582b92..8759682b 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -105,6 +105,14 @@ protected static function writeComposerJsonFile($config, $directory) throw new RuntimeException('Directory must be a non-empty string.'); } + // Inject artifact for this plugin. + if (isset($config['repositories']) === false) { + $config['repositories'][] = array( + 'type' => 'artifact', + 'url' => \ZIP_ARTIFACT_DIR, + ); + } + // Inject ability to run the plugin via a script. if (isset($config['scripts']['install-codestandards']) === false) { $config['scripts']['install-codestandards'] = array( diff --git a/tests/artifact/.gitkeep b/tests/artifact/.gitkeep new file mode 100644 index 00000000..d06031b9 --- /dev/null +++ b/tests/artifact/.gitkeep @@ -0,0 +1 @@ +# This directory has to exist to allow artifact files to be written to it. \ No newline at end of file diff --git a/tests/bootstrap.php b/tests/bootstrap.php index c241ebfc..73d1d822 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -65,6 +65,23 @@ require_once $vendorDir . '/autoload.php'; } +/* + * Create Zip artifact of the plugin. + */ +define('ZIP_ARTIFACT_DIR', __DIR__ . '/artifact/'); + +if (extension_loaded('zip') === true) { + define('PLUGIN_ARTIFACT_VERSION', '1.0.0'); + + $zipCreator = new CreateComposerZipArtifacts(\ZIP_ARTIFACT_DIR); + $zipCreator->clearOldArtifacts(); + $zipCreator->createPluginArtifact(dirname(__DIR__), \PLUGIN_ARTIFACT_VERSION); + unset($zipCreator); +} else { + echo 'Please enable the zip extension before running the tests.'; + die(1); +} + /* * Set a few constants for use throughout the tests. */ From 83ca52a14f14d34950029babc82f27ab14a738fe Mon Sep 17 00:00:00 2001 From: jrfnl Date: Fri, 28 Jan 2022 11:43:02 +0100 Subject: [PATCH 05/14] TestCase: add helper methods to run CLI commands and custom assertions This commit adds six additional methods to the base `TestCase` class: * `executeCliCommand()`: a helper method to execute a CLI command and return the exit code, the contents of `stdout` and the content of `stderr` for use in assertions. * `stabilizeCommand()`: a helper method which is automatically called from the `executeCliCommand()` method. This method will: - Ensure that the PHP version which was used to initiate the test run will also be used in the CLI command. Without this, the system default PHP version would be used for CLI commands, which may be a different version from the PHP version used to run the tests and would skew the test results. - Ensure that the Composer version as set up using the `env` variable is used in the tests. Again, this will prevent the system default version of Composer being used, which may skew the results when the tests are intended to be run against a specific version of Composer. - Adds the `--no-interaction` flag to all Composer CLI commands to prevent tests hanging when interaction would be expected. - Adds quotes around a command when the tests are run on Windows in combination with PHP < 8 to ensure cross-platform compatibility of the CLI commands. * `getPhpcsCommand()`: a helper method to get the path to the vendor installed PHPCS entry point file. * `maybeStripColors()`: a helper method to compare CLI output. **This method is still WIP and will probably still need some tweaking.** This method will take an output expectation and an actual output and will attempt to strip CLI colour codes from the actual output when the output expectation does not contain colour codes. This way, expected colour codes can still be tested by including them in the output expectation. Note: until the bugs in this method have been smoothed out, it is recommended to use `--no-ansi` CLI arguments in CLI commands not testing for colour codes. * `assertExecute()`: a custom assertion which will run a given CLI command, optionally in a specific working directory and will subsequently assert that the exit code is the same as expected and the `stdout` and `stderr` outputs contain the expected output. For more complex assertions against the output of CLI commands, the `executeCliCommand()` can be used directly in tests. The `StringContainsString` assertions used for `stdout` and `stderr` will attempt to strip color codes from the output before doing a compare using the `maybeStripColors()` method. * `assertComposerValidates()`: a custom assertion to verify that a given `composer.json` file validates for use in the tests. A number of these methods have been made `static` to allow for them to be used in conjunction with `setUpBeforeClass()` fixtures. --- tests/TestCase.php | 313 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 313 insertions(+) diff --git a/tests/TestCase.php b/tests/TestCase.php index 8759682b..ee3cd3c5 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -71,6 +71,131 @@ public static function removeTestEnvironment() } + /* ***** CUSTOM ASSERTIONS ***** */ + + /** + * Assert that a composer.json file is valid for use in the tests. + * + * @param string $workingDir The working directory in which to execute the command. + * @param string $file The file to execute the command on. + * By default the command will execute on the `composer.json` file + * in the current or working directory. + * + * @return void + * + * @throws RuntimeException When either passed argument is not a string. + * @throws RuntimeException When both arguments are passed as Composer can only handle one. + */ + public function assertComposerValidates($workingDir = '', $file = '') + { + if (is_string($workingDir) === false) { + throw new RuntimeException('Working directory must be a string.'); + } + + if (is_string($file) === false) { + throw new RuntimeException('File must be a string.'); + } + + if ($workingDir !== '' && $file !== '') { + throw new RuntimeException( + 'Pass either the working directory OR a file name. Composer does not handle both in the same command.' + ); + } + + $command = 'composer validate --no-check-all --no-check-publish --no-check-lock --no-ansi'; + $stderr = '%s is valid'; + $message = 'Provided Composer configuration is not valid.'; + + if ($workingDir !== '') { + $command .= sprintf(' --working-dir=%s', escapeshellarg($workingDir)); + $stderr = sprintf($stderr, 'composer.json'); + $message .= ' Working directory: ' . $workingDir; + } + + if ($file !== '') { + $command .= ' ' . escapeshellarg($file); + $stderr = sprintf($stderr, $file); + $message .= ' File: ' . $file; + } + + $this->assertExecute( + $command, + 0, // Expected exit code. + null, // Expected stdout. + $stderr, // Expected sterr. + $message + ); + } + + /** + * Assert that a command when executed meets certain expectations for exit code and output. + * + * Note: the stdout and stderr assertions will verify that the passed expectation is a **substring** + * of the actual output using `assertStringContainsString()`. + * + * The stdout and stderr assertions will disregard potential color codes in the actual output + * when no color codes are included in the expectation. + * + * If more specific assertions are needed, use the `TestCase::executeCliCommand()` directly and + * apply assertions to the results from that function call. + * + * @param string $command The CLI command to execute. + * @param int|null $expectedExitCode Optional. The expected exit code for the command. + * @param string|null $expectedStdOut Optional. The expected command output to stdout. + * @param string|null $expectedStdErr Optional. The expected command output to stderr. + * @param string $message Optional. Message to display when an assertion fails. + * @param string|null $workingDir Optional. The directory in which to execute the command. + * Defaults to `null` = the working directory of the current PHP process. + * Note: if the command itself already contains a "working directory" argument, + * this parameter will normally not need to be passed. + * + * @return void + * + * @throws RuntimeException When neither $expectedExitCode, $expectedStdOut or $expectedStdErr are passed. + */ + public function assertExecute( + $command, + $expectedExitCode = null, + $expectedStdOut = null, + $expectedStdErr = null, + $message = '', + $workingDir = null + ) { + if ($expectedExitCode === null && $expectedStdOut === null && $expectedStdErr === null) { + throw new RuntimeException('At least one expectation has to be set for the executed command.'); + } + + $result = $this->executeCliCommand($command, $workingDir); + + if (is_string($expectedStdOut)) { + $msg = 'stdOut did not contain the expected output. ' . $message; + + if ($expectedStdOut === '') { + $this->assertSame($expectedStdOut, $result['stdout'], $msg); + } else { + $stdout = $this->maybeStripColors($expectedStdOut, $result['stdout']); + $this->assertStringContainsString($expectedStdOut, $stdout, $msg); + } + } + + if (is_string($expectedStdErr)) { + $msg = 'stdErr did not contain the expected output. ' . $message; + + if ($expectedStdErr === '') { + $this->assertSame($expectedStdErr, $result['stderr'], $msg); + } else { + $stderr = $this->maybeStripColors($expectedStdErr, $result['stderr']); + $this->assertStringContainsString($expectedStdErr, $stderr, $msg); + } + } + + if (is_int($expectedExitCode)) { + $msg = 'Exit code did not match expected code. ' . $message; + $this->assertSame($expectedExitCode, $result['exitcode'], $msg); + } + } + + /* ***** HELPER METHODS ***** */ /** @@ -144,4 +269,192 @@ protected static function writeComposerJsonFile($config, $directory) throw new RuntimeException('Failed to create the composer.json file in the temp directory for the test'); } } + + /** + * Helper function for CLI commands. + * + * This function stabilizes the CLI command for the purpose of these tests when the + * tests are run in a non-isolated environment with multiple installed PHP versions + * and multiple installed Composer versions. + * + * This prevents the system default PHP version being used instead of the PHP version + * which was used to initiate the test run. + * Similarly, this prevents the system default Composer version being used instead of the + * target Composer version for this test run. + * + * @param string $command The command to stabilize. + * @param string|null $workingDir Optional. The directory in which the command will be executed. + * Defaults to `null` = the working directory of the current PHP process. + * + * @return string + * + * @throws RuntimeException When the passed command is not a string. + */ + protected static function stabilizeCommand($command, $workingDir = null) + { + if (is_string($command) === false) { + throw new RuntimeException('Command must be a string.'); + } + + if (strpos($command, 'vendor/bin/phpcs') !== false) { + $phpcsCommand = static::getPhpcsCommand($workingDir); + if (strpos($command, 'vendor/bin/phpcs') === 0) { + $command = '"' . \PHP_BINARY . '" ' . $phpcsCommand . substr($command, 16); + } + + if (strpos($command, '"vendor/bin/phpcs"') === 0) { + $command = '"' . \PHP_BINARY . '" ' . $phpcsCommand . substr($command, 18); + } + + if (strpos($command, ' vendor/bin/phpcs ') !== false) { + $command = str_replace(' vendor/bin/phpcs ', ' ' . $phpcsCommand . ' ', $command); + } + + if (strpos($command, ' "vendor/bin/phpcs" ') !== false) { + $command = str_replace(' "vendor/bin/phpcs" ', ' ' . $phpcsCommand . ' ', $command); + } + } + + if (strpos($command, 'php composer.phar ') !== false) { + $command = str_replace('php composer.phar ', '"' . \PHP_BINARY . '" "' . \COMPOSER_PHAR . '" ', $command); + } + + if (strpos($command, 'php ') === 0) { + $command = '"' . \PHP_BINARY . '" ' . substr($command, 3); + } + + if (strpos($command, ' php ') !== false) { + $command = str_replace(' php ', ' "' . \PHP_BINARY . '" ', $command); + } + + if (strpos($command, 'composer ') !== false) { + $command = str_replace('composer ', '"' . \PHP_BINARY . '" "' . \COMPOSER_PHAR . '" ', $command); + } + + // Make sure the `--no-interaction` flag is set for all Composer commands to prevent tests hanging. + if (strpos($command, '"' . \COMPOSER_PHAR . '"') !== false && strpos($command, ' --no-interaction') === false) { + $command = str_replace('"' . \COMPOSER_PHAR . '"', '"' . \COMPOSER_PHAR . '" --no-interaction', $command); + } + + /* + * If the command will be run on Windows in combination with PHP < 8.0, wrap it in an extra set of quotes. + * Note: it is unclear what changes in PHP 8.0, but the quotes will now suddenly break things. + * Ref: https://www.php.net/manual/en/function.proc-open.php#example-3331 + */ + if (static::onWindows() === true && substr(\CLI_PHP_MINOR, 0, 1) < 8) { + $command = '"' . $command . '"'; + } + + return $command; + } + + /** + * Retrieve the command to use to run PHPCS. + * + * @param string|null $workingDir Optional. The directory in which the command will be executed. + * Defaults to `null` = the working directory of the current PHP process. + * + * @return string + */ + protected static function getPhpcsCommand($workingDir = null) + { + $command = '"vendor/squizlabs/php_codesniffer/bin/phpcs"'; // PHPCS 3.x. + + if (is_string($workingDir) && file_exists($workingDir . '/vendor/squizlabs/php_codesniffer/scripts/phpcs')) { + // PHPCS 2.x. + $command = '"vendor/squizlabs/php_codesniffer/scripts/phpcs"'; + } + + return $command; + } + + /** + * Helper function to execute a CLI command. + * + * @param string $command The CLI command to execute. + * @param string|null $workingDir Optional. The directory in which to execute the command. + * Defaults to `null` = the working directory of the current PHP process. + * Note: if the command itself already contains a "working directory" argument, + * this parameter will normally not need to be passed. + * + * @return array Format: + * 'exitcode' int The exit code from the command. + * 'stdout' string The output send to stdout. + * 'stderr' string The output send to stderr. + * + * @throws RuntimeException When the passed arguments do not comply. + * @throws RuntimeException When no resource could be obtained to execute the command. + */ + public static function executeCliCommand($command, $workingDir = null) + { + if (is_string($command) === false || $command === '') { + throw new RuntimeException('Command must be a non-empty string.'); + } + + if (is_null($workingDir) === false && (is_string($workingDir) === false || $workingDir === '')) { + throw new RuntimeException('Working directory must be a non-empty string or null.'); + } + + $command = static::stabilizeCommand($command, $workingDir); + $descriptorspec = array( + 0 => array("pipe", "r"), // stdin + 1 => array("pipe", "w"), // stdout + 2 => array("pipe", "w"), // stderr + ); + + $process = proc_open($command, $descriptorspec, $pipes, $workingDir); + + if (is_resource($process) === false) { + throw new RuntimeException('Could not obtain a resource with proc_open() to execute the command.'); + } + + $result = array(); + fclose($pipes[0]); + + $result['stdout'] = stream_get_contents($pipes[1]); + fclose($pipes[1]); + + $result['stderr'] = stream_get_contents($pipes[2]); + fclose($pipes[2]); + + $result['exitcode'] = proc_close($process); + + return $result; + } + + /** + * Helper function which strips potential CLI colour codes from the actual output + * when the expected output does not contain any colour codes. + * + * @param string $expected Expected output. + * @param string $actual Actual output. + * + * @return string Actual output, potentially stripped of colour codes. + * + * @throws RuntimeException When either passed argument is not a string. + */ + protected function maybeStripColors($expected, $actual) + { + if (is_string($expected) === false) { + throw new RuntimeException('Expected output must be a string.'); + } + + if (is_string($actual) === false) { + throw new RuntimeException('Actual output must be a string.'); + } + + if ($expected === '') { + // Nothing to do. + return $actual; + } + + if ( + strpos($expected, "\033") === false && strpos($actual, "\033") !== false + || strpos($expected, "\x1b") === false && strpos($actual, "\x1b") !== false + ) { + $actual = preg_replace('`(?:\\\\033|\\\\x1b)\\\\[[0-9]+(;[0-9]*)[A-Za-z]`', '', $actual); + } + + return $actual; + } } From 74058e424445d2d700ba455cafec2c028d10d788 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Sun, 30 Jan 2022 18:25:03 +0100 Subject: [PATCH 06/14] Tests: add TestListener to log and display CLI output for failing tests While not necessarily the most pretty solution (output is displayed in the middle of the progress report), this allows for access to the `composer.json` content, the CLI commands and their output for any test which failed, which will be helpful when debugging test failures, especially in CI. --- phpunit.xml.dist | 4 +++ tests/DebugTestListener.php | 66 +++++++++++++++++++++++++++++++++++++ tests/TestCase.php | 16 +++++++++ 3 files changed, 86 insertions(+) create mode 100644 tests/DebugTestListener.php diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 6a49f70b..7ac1a77c 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -17,6 +17,10 @@ + + + + ./src/ diff --git a/tests/DebugTestListener.php b/tests/DebugTestListener.php new file mode 100644 index 00000000..226644b6 --- /dev/null +++ b/tests/DebugTestListener.php @@ -0,0 +1,66 @@ +getName(), \PHP_EOL, self::$debugLog; + } + } + + /** + * Add information to the debug log. + * + * @param string The information to add to the log. + * + * @return void + */ + public static function debugLog($str) + { + self::$debugLog .= $str . \PHP_EOL; + } +} diff --git a/tests/TestCase.php b/tests/TestCase.php index ee3cd3c5..e8be1ca7 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -268,6 +268,14 @@ protected static function writeComposerJsonFile($config, $directory) if ($written === false) { throw new RuntimeException('Failed to create the composer.json file in the temp directory for the test'); } + + // Add debug information to the test listener which will be displayed in case the test fails. + DebugTestListener::debugLog( + '---------------------------------------' . \PHP_EOL + . 'composer.json: ' . \PHP_EOL + . $encoded . \PHP_EOL + . '---------------------------------------' . \PHP_EOL + ); } /** @@ -419,6 +427,14 @@ public static function executeCliCommand($command, $workingDir = null) $result['exitcode'] = proc_close($process); + // Add debug information to the test listener which will be displayed in case the test fails. + DebugTestListener::debugLog( + '---------------------------------------' . \PHP_EOL + . 'Command: ' . $command . \PHP_EOL + . 'Output: ' . var_export($result, true) . \PHP_EOL + . '---------------------------------------' . \PHP_EOL + ); + return $result; } From eaa99e4dce9b33e63632ace958ebfbfc5cc8fe11 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Fri, 28 Jan 2022 13:21:46 +0100 Subject: [PATCH 07/14] Tests: add initial baseline test These tests verify: - That the plugin can be installed and functions correctly with the full range of supported PHPCS versions. While the test is not run against the full range of supported PHPCS version each time, due to the random PHPCS version selection, all supported PHPCS versions will be tested over time in CI. - That the plugin runs when installed. - That the PHPCS native standards are the only recognized standards when no external standards are available. - That no `CodeSniffer.conf``file gets created when no external standards are found. These tests will be run in both a Composer global as well as a Composer project local environment. The tests use a dataprovider which will randomly select two PHPCS versions compatible with the PHP version on which the test is being run + PHPCS `dev-master` + PHPCS `4.x` (dev version for the next major). As which PHPCS versions are compatible with which PHP versions needs quite some logic, a separate helper class `PHPCSVersions` is introduced, which encapsulates that knowledge. The `PHPCSVersions` class contains a number of (`static`) methods as helpers. These can be used in any test class: * `get()`: retrieves an array with a specific number of PHPCS versions valid for the current PHP version. * `getHighLow()`: retrieves an array of the highest and lowest PHPCS versions valid for the current PHP version. * `getHighLowEachMajor()`: retrieves an array of the highest and lowest supported PHPCS versions for each PHPCS major (and valid for the current PHP version). * `getRandom()`: gets a random PHPCS version which is valid for the current PHP version. * `toDataprovider()`: converts a version array to an array suitable for use as a PHPUnit dataprovider. * `getSupportedVersions()`: retrieves an array with all PHPCS versions valid for the current PHP version. * `isNextMajorSupported()`: determines whether the current PHP version is supported on the "next major" branch of PHPCS. * `getStandards()`: retrieves an array of the names of the PHPCS native standards for a particular PHPCS version. Generally speaking, the `dev-master` and `4.x` branch will not be included in the retrieved ranges, but can be specifically requested by passing optional parameters. This `PHPCSVersions` class will need semi-regular updates when new versions of PHPCS are released and new PHP versions are released. --- tests/IntegrationTest/BaseLineTest.php | 165 ++++++++++ tests/PHPCSVersions.php | 425 +++++++++++++++++++++++++ tests/TestCase.php | 47 +++ 3 files changed, 637 insertions(+) create mode 100644 tests/IntegrationTest/BaseLineTest.php create mode 100644 tests/PHPCSVersions.php diff --git a/tests/IntegrationTest/BaseLineTest.php b/tests/IntegrationTest/BaseLineTest.php new file mode 100644 index 00000000..413b8f26 --- /dev/null +++ b/tests/IntegrationTest/BaseLineTest.php @@ -0,0 +1,165 @@ + 'phpcs-composer-installer/baseline-test', + 'require-dev' => array( + 'squizlabs/php_codesniffer' => null, + 'dealerdirect/phpcodesniffer-composer-installer' => '*', + ), + ); + + /** + * Set up test environment before each test. + */ + protected function set_up() + { + $this->createTestEnvironment(); + } + + /** + * Clean up after each test. + */ + protected function tear_down() + { + $this->removeTestEnvironment(); + } + + /** + * Baseline test for a Composer GLOBAL install. + * + * @dataProvider dataBaseLine + * + * @param string $phpcsVersion PHPCS version to use in this test. + * This version is randomly selected from the PHPCS versions compatible + * with the PHP version used in the test. + * @param array $expectedStnds List of the standards which are expected to be registered. + * + * @return void + */ + public function testBaseLineGlobal($phpcsVersion, $expectedStnds) + { + $config = $this->composerConfig; + $config['require-dev']['squizlabs/php_codesniffer'] = $phpcsVersion; + + $this->writeComposerJsonFile($config, static::$tempGlobalPath); + $this->assertComposerValidates(static::$tempGlobalPath); + + // Make sure the plugin runs. + $this->assertExecute( + 'composer global install -v --no-ansi', + 0, // Expected exit code. + 'Running PHPCodeSniffer Composer Installer', // Expected stdout. + null, // No stderr expectation. + 'Failed to install dependencies.' + ); + + $result = $this->executeCliCommand('"vendor/bin/phpcs" -i', static::$tempGlobalPath); + $this->assertSame(0, $result['exitcode'], 'Exitcode for phpcs -i did not match 0'); + $this->assertSame( + $expectedStnds, + $this->standardsPhraseToArray($result['stdout']), + 'Installed standards do not match the expected standards.' + ); + + // Make sure the CodeSniffer.conf file does not get created when no external standards are found. + $this->assertFileDoesNotExist( + static::$tempGlobalPath . '/vendor/squizlabs/php_codesniffer/CodeSniffer.conf' + ); + } + + /** + * Baseline test for a Composer LOCAL install. + * + * @dataProvider dataBaseLine + * + * @param string $phpcsVersion PHPCS version to use in this test. + * This version is randomly selected from the PHPCS versions compatible + * with the PHP version used in the test. + * @param array $expectedStnds List of the standards which are expected to be registered. + * + * @return void + */ + public function testBaseLineLocal($phpcsVersion, $expectedStnds) + { + $config = $this->composerConfig; + $config['require-dev']['squizlabs/php_codesniffer'] = $phpcsVersion; + + $this->writeComposerJsonFile($config, static::$tempLocalPath); + $this->assertComposerValidates(static::$tempLocalPath); + + // Make sure the plugin runs. + $this->assertExecute( + sprintf('composer install -v --no-ansi --working-dir=%s', escapeshellarg(static::$tempLocalPath)), + 0, // Expected exit code. + 'Running PHPCodeSniffer Composer Installer', // Expected stdout. + null, // No stderr expectation. + 'Failed to install dependencies.' + ); + + $result = $this->executeCliCommand('"vendor/bin/phpcs" -i', static::$tempLocalPath); + $this->assertSame(0, $result['exitcode'], 'Exitcode for phpcs -i did not match 0'); + $this->assertSame( + $expectedStnds, + $this->standardsPhraseToArray($result['stdout']), + 'Installed standards do not match the expected standards.' + ); + + // Make sure the CodeSniffer.conf file does not get created when no external standards are found. + $this->assertFileDoesNotExist( + static::$tempLocalPath . '/vendor/squizlabs/php_codesniffer/CodeSniffer.conf' + ); + } + + /** + * Data provider. + * + * Note: PHPCS does not display the names of the standards in a fixed order, so the order in which standards + * get displayed may differ depending on the machine/OS on which the tests get run. + * With that in mind, the verification that the PHPCS native standards are the only recognized standards + * is done using a regex instead of an exact match. + * Also see: https://github.com/squizlabs/PHP_CodeSniffer/pull/3539 + * + * @return array + */ + public function dataBaseLine() + { + // Get two PHPCS versions suitable for this PHP version + `master` + PHPCS 4.x dev. + $versions = PHPCSVersions::get(2, true, true); + + $data = array(); + foreach ($versions as $version) { + $data["phpcs $version"] = array( + 'phpcsVersion' => $version, + 'expectedStnds' => PHPCSVersions::getStandards($version), + ); + } + + return $data; + } +} diff --git a/tests/PHPCSVersions.php b/tests/PHPCSVersions.php new file mode 100644 index 00000000..18c884c9 --- /dev/null +++ b/tests/PHPCSVersions.php @@ -0,0 +1,425 @@ + '2.0.0', + '2.1.0' => '2.1.0', + '2.2.0' => '2.2.0', + '2.3.0' => '2.3.0', + '2.3.1' => '2.3.1', + '2.3.2' => '2.3.2', + '2.3.3' => '2.3.3', + '2.3.4' => '2.3.4', + '2.4.0' => '2.4.0', + '2.5.0' => '2.5.0', + '2.5.1' => '2.5.1', + '2.6.0' => '2.6.0', + '2.6.1' => '2.6.1', + '2.6.2' => '2.6.2', + '2.7.0' => '2.7.0', + '2.7.1' => '2.7.1', + '2.8.0' => '2.8.0', + '2.8.1' => '2.8.1', + '2.9.0' => '2.9.0', + '2.9.1' => '2.9.1', + '2.9.2' => '2.9.2', + '3.1.0' => '3.1.0', + '3.1.1' => '3.1.1', + '3.2.0' => '3.2.0', + '3.2.1' => '3.2.1', + '3.2.2' => '3.2.2', + '3.2.3' => '3.2.3', + '3.3.0' => '3.3.0', + '3.3.1' => '3.3.1', + '3.3.2' => '3.3.2', + '3.4.0' => '3.4.0', + '3.4.1' => '3.4.1', + '3.4.2' => '3.4.2', + '3.5.0' => '3.5.0', + '3.5.1' => '3.5.1', + '3.5.2' => '3.5.2', + '3.5.3' => '3.5.3', + '3.5.4' => '3.5.4', + '3.5.5' => '3.5.5', + '3.5.6' => '3.5.6', + '3.5.7' => '3.5.7', + '3.5.8' => '3.5.8', + '3.6.0' => '3.6.0', + '3.6.1' => '3.6.1', + '3.6.2' => '3.6.2', + ); + + /** + * Retrieve an array with a specific number of PHPCS versions valid for the current PHP version. + * + * @param int $number Number of PHPCS versions to retrieve (excluding master/next major). + * Defaults to `0` = all supported versions for the current PHP version. + * When a non-0 value is passed, a random selection of versions supported + * by the current PHP version will be returned. + * @param bool $addMaster Whether or not `dev-master` should be added to the version array (providing + * it supports the current PHP version). + * Defaults to `false`. + * @param bool $addNextMajor Whether or not the development branch for the next PHPCS major should be + * added to the version array (providing it supports the current PHP version). + * Defaults to `false`. + * Note: if `true`, the version will be returned in a Composer usable format. + * + * @return array Numerically indexed array with PHPCS version identifiers as values. + */ + public static function get($number = 0, $addMaster = false, $addNextMajor = false) + { + if (is_int($number) === false || $number < 0) { + throw new RuntimeException('The number parameter must be a positive integer.'); + } + + $versions = self::getSupportedVersions(); + + $selection = array_values($versions); + if ($number !== 0 && empty($versions) === false) { + $number = min($number, count($versions)); + $selection = (array) array_rand($versions, $number); + } + + if ($addMaster === true) { + $selection[] = self::MASTER; + } + + if ($addNextMajor === true && self::isNextMajorSupported()) { + $selection[] = self::NEXT_MAJOR; + } + + return $selection; + } + + /** + * Retrieve an array of the highest and lowest PHPCS versions valid for the current PHP version. + * + * @param bool $addMaster Whether or not `dev-master` should be added to the version array (providing + * it supports the current PHP version). + * Defaults to `false`. + * @param bool $addNextMajor Whether or not the development branch for the next PHPCS major should be + * added to the version array (providing it supports the current PHP version). + * Defaults to `false`. + * Note: if `true`, the version will be returned in a Composer usable format. + * + * @return array Numerically indexed array with PHPCS version identifiers as values. + */ + public static function getHighLow($addMaster = false, $addNextMajor = false) + { + $versions = self::getSupportedVersions(); + $selection = array(); + + if (empty($versions) === false) { + $selection[] = min($versions); + $selection[] = max($versions); + } + + if ($addMaster === true) { + $selection[] = self::MASTER; + } + + if ($addNextMajor === true && self::isNextMajorSupported()) { + $selection[] = self::NEXT_MAJOR; + } + + return $selection; + } + + /** + * Retrieve an array of the highest and lowest supported PHPCS versions for each PHPCS major + * (valid for the current PHP version). + * + * @param bool $addMaster Whether or not `dev-master` should be added to the version array (providing + * it supports the current PHP version). + * Defaults to `false`. + * @param bool $addNextMajor Whether or not the development branch for the next PHPCS major should be + * added to the version array (providing it supports the current PHP version). + * Defaults to `false`. + * Note: if `true`, the version will be returned in a Composer usable format. + * + * @return array Numerically indexed array with PHPCS version identifiers as values. + */ + public static function getHighLowEachMajor($addMaster = false, $addNextMajor = false) + { + $versions = self::getSupportedVersions(); + $versions2 = array(); + $versions3 = array(); + + if (empty($versions) === false) { + $versions2 = array_filter( + $versions, + function ($v) { + return $v[0] === '2'; + } + ); + $versions3 = array_filter( + $versions, + function ($v) { + return $v[0] === '3'; + } + ); + } + + $selection = array(); + if (empty($versions2) === false) { + $selection[] = min($versions2); + $selection[] = max($versions2); + } + + if (empty($versions3) === false) { + $selection[] = min($versions3); + $selection[] = max($versions3); + } + + if ($addMaster === true) { + $selection[] = self::MASTER; + } + + if ($addNextMajor === true && self::isNextMajorSupported()) { + $selection[] = self::NEXT_MAJOR; + } + + return $selection; + } + + /** + * Get a random PHPCS version which is valid for the current PHP version. + * + * @param bool $inclMaster Whether or not `dev-master` should be included in the array to pick + * the version from (providing it supports the current PHP version). + * Defaults to `false`. + * @param bool $inclNextMajor Whether or not the development branch for the next PHPCS major should be included + * in the array to pick the version (providing it supports the current PHP version). + * Defaults to `false`. + * Note: if `true`, the version will be returned in a Composer usable format. + * + * @return string + */ + public static function getRandom($inclMaster = false, $inclNextMajor = false) + { + $versions = self::getSupportedVersions(); + + if ($inclMaster === true) { + $versions[self::MASTER] = self::MASTER; + } + + if ($inclNextMajor === true && self::isNextMajorSupported()) { + $versions[self::NEXT_MAJOR] = self::NEXT_MAJOR; + } + + return array_rand($versions); + } + + /** + * Convert a versions array to an array suitable for use as a PHPUnit dataprovider. + * + * @param array $version Array with PHPCS version numbers as values. + * + * @return array Array of PHPCS version identifiers in a format usable for a test data provider. + */ + public static function toDataprovider($versions) + { + if (is_array($versions) === false || $versions === array()) { + throw new RuntimeException('The versions parameter must be a non-empty array.'); + } + + $data = array(); + foreach ($versions as $version) { + $data['phpcs ' . $version] = array( + 'phpcsVersion' => $version, + ); + } + + return $data; + } + + /** + * Retrieve an array with PHPCS versions valid for the current PHP version. + * + * @return array Array with PHPCS version identifiers as both keys and values. + */ + public static function getSupportedVersions() + { + $versions = self::$allPhpcsVersions; + + /* + * Adjust the list of available versions based on the PHP version on which the tests are run. + */ + switch (\CLI_PHP_MINOR) { + case '5.3': + $versions = array_filter( + self::$allPhpcsVersions, + function ($version) { + // PHPCS 2.9.2 is the highest version still supporting PHP 5.3. + return version_compare($version, '2.9.2', '<='); + } + ); + break; + + case '7.2': + $versions = array_filter( + self::$allPhpcsVersions, + function ($version) { + // PHPCS 2.9.2 is the first PHPCS version with runtime support for PHP 7.2. + return version_compare($version, '2.9.2', '>='); + } + ); + break; + + case '7.3': + $versions = array_filter( + self::$allPhpcsVersions, + function ($version) { + // PHPCS 3.3.1 is the first PHPCS version with runtime support for PHP 7.3. + return version_compare($version, '3.3.1', '>='); + } + ); + break; + + case '7.4': + $versions = array_filter( + self::$allPhpcsVersions, + function ($version) { + // PHPCS 3.5.0 is the first PHPCS version with runtime support for PHP 7.4. + return version_compare($version, '3.5.0', '>='); + } + ); + break; + + case '8.0': + $versions = array_filter( + self::$allPhpcsVersions, + function ($version) { + // PHPCS 3.5.7 is the first PHPCS version with runtime support for PHP 8.0. + return version_compare($version, '3.5.7', '>='); + } + ); + break; + + case '8.1': + $versions = array_filter( + self::$allPhpcsVersions, + function ($version) { + // PHPCS 3.6.1 is the first PHPCS version with runtime support for PHP 8.1. + return version_compare($version, '3.6.1', '>='); + } + ); + break; + + case '8.2': + /* + * At this point in time, it is unclear as of which PHPCS version PHP 8.2 will be supported. + * In other words: tests should only use dev-master/4.x when on PHP 8.2 for the time being. + */ + $versions = array(); + break; + + default: + $versions = self::$allPhpcsVersions; + break; + } + + return $versions; + } + + /** + * Determine if the current PHP version is supported on the "next major" branch of PHPCS. + * + * @return bool + */ + public static function isNextMajorSupported() + { + return version_compare(\CLI_PHP_MINOR, '7.2', '>='); + } + + /** + * Retrieve an array of the PHPCS native standards which are included in a particular PHPCS version. + * + * @param string $version PHPCS version number. + * + * @return array Numerically indexed array of standards, natural sort applied. + */ + public static function getStandards($version) + { + if ( + is_string($version) === false + || (isset(self::$allPhpcsVersions[$version]) === false + && $version !== PHPCSVersions::MASTER + && $version !== PHPCSVersions::NEXT_MAJOR) + ) { + throw new RuntimeException('The version parameter must be a valid PHPCS version number as a string.'); + } + + $standards = array( + 'PEAR', + 'PSR1', + 'PSR2', + 'Squiz', + 'Zend', + ); + + if ($version !== PHPCSVersions::NEXT_MAJOR) { + // The MySource standard is available in PHPCS 2.x and 3.x, but will be removed in 4.0. + $standards[] = 'MySource'; + } + + if ( + $version !== PHPCSVersions::MASTER + && $version !== PHPCSVersions::NEXT_MAJOR + && version_compare($version, '3.0.0', '<') + ) { + // The PHPCS standard was available in PHPCS 2.x, but has been removed in 3.0. + $standards[] = 'PHPCS'; + } + + if ( + $version === PHPCSVersions::MASTER + || $version === PHPCSVersions::NEXT_MAJOR + || version_compare($version, '3.3.0', '>=') + ) { + // The PSR12 standard is available since PHPCS 3.3.0. + $standards[] = 'PSR12'; + } + + sort($standards, \SORT_NATURAL); + + return $standards; + } +} diff --git a/tests/TestCase.php b/tests/TestCase.php index e8be1ca7..b890db27 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -473,4 +473,51 @@ protected function maybeStripColors($expected, $actual) return $actual; } + + /** + * Retrieve a list of the standards recognized by PHPCS based on the output of `phpcs -i`. + * + * @param string $phrase The output of `phpcs -i`. + * + * @return array Numerically indexed array of standards, natural sort applied. + * + * @throws RuntimeException When the passed argument is not a string. + * @throws RuntimeException When the output could not be parsed. + */ + protected function standardsPhraseToArray($phrase) + { + if (is_string($phrase) === false) { + throw new RuntimeException('The input phrase must be a string.'); + } + + $phrase = trim($phrase); + + if ($phrase === 'No coding standards are installed.') { + return array(); + } + + if (strpos($phrase, 'The only coding standard installed is ') === 0) { + $standard = str_replace('The only coding standard installed is ', '', $phrase); + return (array) trim($standard); + } + + $phrase = str_replace('The installed coding standards are ', '', $phrase); + $parts = explode(' and ', $phrase); + + if (count($parts) !== 2) { + throw new RuntimeException( + 'Looks like the output from PHPCS for phpcs -i has changed.' + . ' Please update the `TestCase::standardsPhraseToArray()` method.' + . ' Input phrase: ' . $phrase + ); + } + + $standards = explode(', ', $parts[0]); + $standards[] = $parts[1]; + $standards = array_map('trim', $standards); // Trim off whitespace just to be on the safe side. + + sort($standards, \SORT_NATURAL); + + return $standards; + } } From 8cf335aa1823a90b2cf5507e44e3818106af21c5 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Wed, 2 Feb 2022 08:25:35 +0100 Subject: [PATCH 08/14] BaseLineTest: selectively skip the tests ... in a very specific combination of circumstances - Windows + Composer 1.x + PHP 5.5 + PHPCS `dev-master` + no external standards -, as the Composer logs in that case, don't always show the "Running PHPCodeSniffer Composer Installer" message. As it is such a specific combination, which will probably be rare to encounter in real life, I'm proposing to skip the tests for that combination instead of spending lots of time trying to debug this (for now). --- tests/IntegrationTest/BaseLineTest.php | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/IntegrationTest/BaseLineTest.php b/tests/IntegrationTest/BaseLineTest.php index 413b8f26..a87ee410 100644 --- a/tests/IntegrationTest/BaseLineTest.php +++ b/tests/IntegrationTest/BaseLineTest.php @@ -64,6 +64,18 @@ protected function tear_down() */ public function testBaseLineGlobal($phpcsVersion, $expectedStnds) { + if ( + $phpcsVersion === PHPCSVersions::MASTER + && \CLI_PHP_MINOR === '5.5' + && $this->onWindows() === true + && substr(\COMPOSER_VERSION, 0, 1) === '1' + ) { + $this->markTestSkipped( + 'Composer 1.x on Windows with PHP 5.5 does run the plugin when there are no external standards,' + . ' but doesn\'t consistently show this in the logs' + ); + } + $config = $this->composerConfig; $config['require-dev']['squizlabs/php_codesniffer'] = $phpcsVersion; @@ -107,6 +119,18 @@ public function testBaseLineGlobal($phpcsVersion, $expectedStnds) */ public function testBaseLineLocal($phpcsVersion, $expectedStnds) { + if ( + $phpcsVersion === PHPCSVersions::MASTER + && \CLI_PHP_MINOR === '5.5' + && $this->onWindows() === true + && substr(\COMPOSER_VERSION, 0, 1) === '1' + ) { + $this->markTestSkipped( + 'Composer 1.x on Windows with PHP 5.5 does run the plugin when there are no external standards,' + . ' but doesn\'t consistently show this in the logs' + ); + } + $config = $this->composerConfig; $config['require-dev']['squizlabs/php_codesniffer'] = $phpcsVersion; From cdb03cd51b3004a9bbe179ac415d5722f6f85e0e Mon Sep 17 00:00:00 2001 From: jrfnl Date: Sun, 30 Jan 2022 06:49:52 +0100 Subject: [PATCH 09/14] Tests: move test with external standard from CI to test class This moves the test which was being run via GH Actions to a test class in the integration test suite. Differences between the previous and new setup: * The previous test via GH Actions would only fail on the exit code not being `0` for any of the steps. The new PHPUnit based tests executes more detailed assertions on the results of the commands being run in the tests. * The previous test would execute the test run of PHPCS against the `src/Plugin.php` file in this package. The new PHPUnit based tests will execute against a simple PHP file created on the fly. * The previous test via GH Actions would only run for a Composer `local` situation. The new PHPUnit based tests cover both a Composer `global` as well as a `local` setup. * The matrix against which the tests are run is different. In the old situation, the test would only be run on a fixed set of PHP/PHPCS combinations with Composer 2.x on Ubuntu. In the new situation, the tests will run against both Composer 1.x, as well as 2.x on both Ubuntu and Windows. Same as before, it will be run against all supported PHP versions (in the extended `integrationtest` workflow), but the PHPCS versions to test against are more varied and - in part - randomly selected out of the PHPCS version compatible with the current PHP version, which results in more comprehensive testing of these scenarios. Includes: * Adding a new `createFile()` helper method to the base `TestCase` to generate a (PHP) file to do a test run against. * Removing the "old" integration test from the GH Actions script. --- .github/workflows/integrationtest.yml | 169 ---------- .github/workflows/quicktest.yml | 71 ---- .../RegisterExternalStandardsTest.php | 306 ++++++++++++++++++ tests/TestCase.php | 40 +++ 4 files changed, 346 insertions(+), 240 deletions(-) create mode 100644 tests/IntegrationTest/RegisterExternalStandardsTest.php diff --git a/.github/workflows/integrationtest.yml b/.github/workflows/integrationtest.yml index e312cf67..a2ddc968 100644 --- a/.github/workflows/integrationtest.yml +++ b/.github/workflows/integrationtest.yml @@ -18,175 +18,6 @@ concurrency: jobs: test: - runs-on: ubuntu-latest - - strategy: - matrix: - php: ['5.4', '5.5', '5.6', '7.0', '7.1', '7.2', '7.3', '7.4', '8.0', '8.1'] - phpcs_version: ['dev-master'] - phpcompat: ['composer'] - experimental: [false] - - include: - # Ensure a "highest" PHP/PHPCS build combination for PHPCS 2.x is included. - - php: '5.4' - phpcs_version: '2.9.2' - phpcompat: 'composer' - experimental: false - - # Complement the matrix with build against the lowest supported PHPCS version - # for each PHP version. - - php: '8.1' - # Lowest PHPCS version on which PHP 8.1 is supported. - phpcs_version: '3.6.1' - phpcompat: 'composer' - experimental: false - - php: '8.0' - # Lowest PHPCS version on which PHP 8.0 is supported. - phpcs_version: '3.5.7' - phpcompat: 'composer' - experimental: false - - php: '7.4' - # Lowest PHPCS version on which PHP 7.4 is supported. - phpcs_version: '3.5.0' - phpcompat: 'composer' - experimental: false - - php: '7.3' - # Lowest PHPCS version on which PHP 7.3 is supported. - phpcs_version: '3.3.1' - phpcompat: 'composer' - experimental: false - - php: '7.2' - # Lowest PHPCS version on which PHP 7.2 is supported. - phpcs_version: '2.9.2' - phpcompat: 'composer' - experimental: false - - php: '7.1' - phpcs_version: '2.0.0' - phpcompat: '^7.0' - experimental: false - - php: '7.0' - phpcs_version: '2.0.0' - phpcompat: '^7.0' - experimental: false - - php: '5.6' - phpcs_version: '2.0.0' - phpcompat: '^7.0' - experimental: false - - php: '5.5' - phpcs_version: '2.0.0' - phpcompat: '^7.0' - experimental: false - - php: '5.4' - phpcs_version: '2.0.0' - phpcompat: '^7.0' - experimental: false - - # Additional builds against arbitrary interim PHPCS versions. - - php: '7.3' - phpcs_version: '3.5.3' - phpcompat: 'composer' - experimental: false - - php: '7.2' - phpcs_version: '3.2.3' - phpcompat: 'composer' - experimental: false - - php: '7.1' - phpcs_version: '3.1.1' - phpcompat: 'composer' - experimental: false - - php: '7.0' - phpcs_version: '3.4.2' - phpcompat: 'composer' - experimental: false - - php: '7.0' - phpcs_version: '2.2.0' - phpcompat: '^8.0' - experimental: false - - php: '5.6' - phpcs_version: '3.0.2' - phpcompat: 'composer' - experimental: false - - php: '5.6' - phpcs_version: '2.4.0' - phpcompat: 'composer' - experimental: false - - php: '5.5' - phpcs_version: '2.6.1' - phpcompat: 'composer' - experimental: false - - php: '5.4' - phpcs_version: '3.5.3' - phpcompat: 'composer' - experimental: false - - php: '5.4' - phpcs_version: '2.8.1' - phpcompat: 'composer' - experimental: false - - # Experimental builds. These are allowed to fail. - - php: '8.2' - phpcs_version: 'dev-master' - phpcompat: 'composer' - experimental: true - - - php: '8.1' - phpcs_version: '4.0.x-dev as 3.9.99' - phpcompat: 'composer' - experimental: true - - name: "Integration test: PHP ${{ matrix.php }} - PHPCS ${{ matrix.phpcs_version }}" - - continue-on-error: ${{ matrix.experimental }} - - steps: - - name: Checkout code - uses: actions/checkout@v2 - - - name: Install PHP - uses: shivammathur/setup-php@v2 - with: - php-version: ${{ matrix.php }} - ini-values: error_reporting=-1, display_errors=On - coverage: none - - - name: 'Composer: set PHPCS version for tests' - run: composer require --no-update --no-scripts squizlabs/php_codesniffer:"${{ matrix.phpcs_version }}" --no-interaction - - # Install PHPCompatibility 7.x/8.x for PHPCS < 2.3. - - name: 'Composer: set PHPCompatibility version for tests (PHPCS < 2.3)' - if: ${{ matrix.phpcompat != 'composer' }} - run: composer require --dev --no-update --no-scripts phpcompatibility/php-compatibility:"${{ matrix.phpcompat }}" --no-interaction - - # Install dependencies and handle caching in one go. - # @link https://github.com/marketplace/actions/install-composer-dependencies - - name: 'Install Composer dependencies' - uses: "ramsey/composer-install@v2" - with: - composer-options: --no-scripts --optimize-autoloader - - # Rename the PHPCompatibility directory as PHPCompatibility 7.x wasn't fully compatible with Composer yet. - - name: 'Rename the PHPCompatibility directory (PHPCS < 2.2)' - if: ${{ matrix.phpcompat == '^7.0' }} - run: mv ./vendor/phpcompatibility/php-compatibility ./vendor/phpcompatibility/PHPCompatibility - - - name: 'Install standards' - run: composer install-codestandards - - - name: 'Show installed standards' - run: vendor/bin/phpcs -i - - # Test that an external standard has been registered correctly by running it against the codebase on PHPCS < 2.3. - - name: 'Test the PHPCompatibility standard was installed succesfully (PHPCS < 2.3)' - if: ${{ matrix.phpcompat != 'composer' }} - run: ./vendor/bin/phpcs -ps ./src/ --standard=PHPCompatibility --sniffs=PHPCompatibility.PHP.DeprecatedFunctions --runtime-set testVersion ${{ matrix.php }} - - # Test that an external standard has been registered correctly by running it against the codebase. - - name: 'Test the PHPCompatibility standard was installed succesfully (PHPCS >= 2.3)' - if: ${{ matrix.phpcompat == 'composer' }} - run: ./vendor/bin/phpcs -ps ./src/ --standard=PHPCompatibility --sniffs=PHPCompatibility.FunctionUse.RemovedFunctions --runtime-set testVersion ${{ matrix.php }} - - new-test: runs-on: "${{ matrix.os }}" strategy: diff --git a/.github/workflows/quicktest.yml b/.github/workflows/quicktest.yml index 7c31ce8b..5bf8f2fa 100644 --- a/.github/workflows/quicktest.yml +++ b/.github/workflows/quicktest.yml @@ -20,77 +20,6 @@ jobs: # This is a much quicker test which only runs the integration tests against a limited set of # supported PHP/PHPCS combinations. quicktest: - runs-on: ubuntu-latest - - strategy: - matrix: - include: - - php: 'latest' - phpcs_version: 'dev-master' - phpcompat: 'composer' - - php: '7.3' - phpcs_version: '2.9.2' - phpcompat: 'composer' - - php: '7.1' - phpcs_version: '3.3.1' - phpcompat: 'composer' - - php: '5.6' - phpcs_version: '2.6.0' - phpcompat: 'composer' - - php: '5.4' - phpcs_version: '2.0.0' - phpcompat: '^7.0' - - name: "Quick test: PHP ${{ matrix.php }} - PHPCS ${{ matrix.phpcs_version }}" - - steps: - - name: Checkout code - uses: actions/checkout@v2 - - - name: Install PHP - uses: shivammathur/setup-php@v2 - with: - php-version: ${{ matrix.php }} - ini-values: error_reporting=-1, display_errors=On - coverage: none - - - name: 'Composer: set PHPCS version for tests' - run: composer require --no-update --no-scripts squizlabs/php_codesniffer:"${{ matrix.phpcs_version }}" --no-interaction - - # Install PHPCompatibility 7.x/8.x for PHPCS < 2.3. - - name: 'Composer: set PHPCompatibility version for tests (PHPCS < 2.3)' - if: ${{ matrix.phpcompat != 'composer' }} - run: composer require --dev --no-update --no-scripts phpcompatibility/php-compatibility:"${{ matrix.phpcompat }}" --no-interaction - - # Install dependencies and handle caching in one go. - # @link https://github.com/marketplace/actions/install-composer-dependencies - - name: 'Install Composer dependencies' - uses: "ramsey/composer-install@v2" - with: - composer-options: --no-scripts --optimize-autoloader - - # Rename the PHPCompatibility directory as PHPCompatibility 7.x wasn't fully compatible with Composer yet. - - name: 'Rename the PHPCompatibility directory (PHPCS < 2.2)' - if: ${{ matrix.phpcompat == '^7.0' }} - run: mv ./vendor/phpcompatibility/php-compatibility ./vendor/phpcompatibility/PHPCompatibility - - - name: 'Install standards' - run: composer install-codestandards - - - name: 'Show installed standards' - run: vendor/bin/phpcs -i - - # Test that an external standard has been registered correctly by running it against the codebase on PHPCS < 2.3. - - name: 'Test the PHPCompatibility standard was installed succesfully (PHPCS < 2.3)' - if: ${{ matrix.phpcompat != 'composer' }} - run: ./vendor/bin/phpcs -ps ./src/ --standard=PHPCompatibility --sniffs=PHPCompatibility.PHP.DeprecatedFunctions --runtime-set testVersion ${{ matrix.php }} - - # Test that an external standard has been registered correctly by running it against the codebase. - - name: 'Test the PHPCompatibility standard was installed succesfully (PHPCS >= 2.3)' - if: ${{ matrix.phpcompat == 'composer' }} - run: ./vendor/bin/phpcs -ps ./src/ --standard=PHPCompatibility --sniffs=PHPCompatibility.FunctionUse.RemovedFunctions --runtime-set testVersion ${{ matrix.php }} - - new-quicktest: runs-on: "${{ matrix.os }}" strategy: diff --git a/tests/IntegrationTest/RegisterExternalStandardsTest.php b/tests/IntegrationTest/RegisterExternalStandardsTest.php new file mode 100644 index 00000000..dab3fa2f --- /dev/null +++ b/tests/IntegrationTest/RegisterExternalStandardsTest.php @@ -0,0 +1,306 @@ + 'phpcs-composer-installer/register-external-stnds-one-stnd', + 'require-dev' => array( + 'squizlabs/php_codesniffer' => null, + 'phpcompatibility/php-compatibility' => null, + 'dealerdirect/phpcodesniffer-composer-installer' => '*', + ), + ); + + /** + * Set up test environment before each test. + */ + protected function set_up() + { + $this->createTestEnvironment(); + } + + /** + * Clean up after each test. + */ + protected function tear_down() + { + $this->removeTestEnvironment(); + } + + /** + * Test registering one external standard for a Composer GLOBAL install. + * + * @dataProvider dataRegisterOneStandard + * + * @param string $phpcsVersion PHPCS version to use in this test. + * This version is randomly selected from the PHPCS versions compatible + * with the PHP version used in the test. + * @param string $phpcompatVersion PHPCompatibility version to use in this test. + * @param string $sniff Name of the sniff to use for a PHPCS test run. + * + * @return void + */ + public function testRegisterOneStandardGlobal($phpcsVersion, $phpcompatVersion, $sniff) + { + $config = $this->configOneStandard; + $config['require-dev']['squizlabs/php_codesniffer'] = $phpcsVersion; + $config['require-dev']['phpcompatibility/php-compatibility'] = $phpcompatVersion; + + $this->writeComposerJsonFile($config, static::$tempGlobalPath); + $this->assertComposerValidates(static::$tempGlobalPath); + + // Install the dependencies. + $this->assertExecute( + 'composer global install --no-plugins', + 0, // Expected exit code. + null, // No stdout expectation. + null, // No stderr expectation. + 'Failed to install dependencies.' + ); + + /* + * In PHPCompatibility 7.x, the directory layout was not yet compatible with a Composer install, + * so we need to "move" the directory to make it compatible. + * Note: PHPCompatibility >= 8 has a conflict setting with PHPCS 2.6.2, which means that + * PHPCompatibility 7.x would be used in that case. + */ + if ($phpcompatVersion === '^7.0' || $phpcsVersion === '2.6.2') { + $moveCommand = sprintf( + '%1$s %2$s/vendor/phpcompatibility/php-compatibility %2$s/vendor/phpcompatibility/PHPCompatibility', + (\DIRECTORY_SEPARATOR === '\\') ? 'move' : 'mv', + escapeshellarg(static::$tempGlobalPath) + ); + + $this->assertExecute( + $moveCommand, + 0, // Expected exit code. + null, // No stdout expectation. + '', // Empty stderr expectation. + 'Moving the PHPCompatibility directory failed.' + ); + } + + // Verify that the standard registers correctly. + $installResult = $this->executeCliCommand('composer global install-codestandards --no-ansi'); + $this->assertSame(0, $installResult['exitcode'], 'Exitcode for install-codestandards did not match 0'); + + $regex = sprintf( + '`^PHP CodeSniffer Config installed_paths set to ' + . '[^\s]+/phpcompatibility%s$`', + ($phpcompatVersion === '^7.0' || $phpcsVersion === '2.6.2') ? '' : '/(?:php-|PHP)compatibility' + ); + $this->assertMatchesRegularExpression( + $regex, + trim($installResult['stdout']), + 'Installing the standards failed.' + ); + + // Make sure the CodeSniffer.conf file has been created. + $this->assertFileExists( + static::$tempGlobalPath . '/vendor/squizlabs/php_codesniffer/CodeSniffer.conf' + ); + + // Verify that PHPCS sees the external standard. + $this->assertExecute( + '"vendor/bin/phpcs" -i', + 0, // Expected exit code. + 'and PHPCompatibility', // Expected stdout. + '', // Empty stderr expectation. + 'Running phpcs -i failed.', + static::$tempGlobalPath + ); + + // Make sure there is a PHP file to scan. + $this->createFile(static::$tempGlobalPath . '/test.php'); + + // Verify that PHPCS can run with the external standard. + $phpcsCommand = sprintf( + '"vendor/bin/phpcs" -psl . --standard=PHPCompatibility --sniffs=%s --runtime-set testVersion %s', + $sniff, + \CLI_PHP_MINOR + ); + $phpcsResult = $this->executeCliCommand($phpcsCommand, static::$tempGlobalPath); + + $this->assertSame(0, $phpcsResult['exitcode'], 'Exitcode for PHPCS scan did not match 0'); + $this->assertMatchesRegularExpression( + // PHPCS 3.x added the "1/1 (100%)" annotation, the new line (+timing) was added early in the 2.x cycle. + '`^\.(?: 1 / 1 \(100%\))?(?:' . \PHP_EOL . '|$)`', + // Progress reporting moved from stdout to stderr in PHPCS 4.x. + ($phpcsVersion[0] !== '4') ? trim($phpcsResult['stdout']) : trim($phpcsResult['stderr']), + 'Scanning the directory with PHPCS failed.' + ); + } + + /** + * Test registering one external standard for a Composer LOCAL install. + * + * @dataProvider dataRegisterOneStandard + * + * @param string $phpcsVersion PHPCS version to use in this test. + * This version is randomly selected from the PHPCS versions compatible + * with the PHP version used in the test. + * @param string $phpcompatVersion PHPCompatibility version to use in this test. + * @param string $sniff Name of the sniff to use for a PHPCS test run. + * + * @return void + */ + public function testRegisterOneStandardLocal($phpcsVersion, $phpcompatVersion, $sniff) + { + $config = $this->configOneStandard; + $config['require-dev']['squizlabs/php_codesniffer'] = $phpcsVersion; + $config['require-dev']['phpcompatibility/php-compatibility'] = $phpcompatVersion; + + $this->writeComposerJsonFile($config, static::$tempLocalPath); + $this->assertComposerValidates(static::$tempLocalPath); + + // Install the dependencies. + $this->assertExecute( + sprintf('composer install --no-plugins --working-dir=%s', escapeshellarg(static::$tempLocalPath)), + 0, // Expected exit code. + null, // No stdout expectation. + null, // No stderr expectation. + 'Failed to install dependencies.' + ); + + /* + * In PHPCompatibility 7.x, the directory layout was not yet compatible with a Composer install, + * so we need to "move" the directory to make it compatible. + * Note: PHPCompatibility >= 8 has a conflict setting with PHPCS 2.6.2, which means that + * PHPCompatibility 7.x would be used in that case. + */ + if ($phpcompatVersion === '^7.0' || $phpcsVersion === '2.6.2') { + $moveCommand = sprintf( + '%1$s %2$s/vendor/phpcompatibility/php-compatibility %2$s/vendor/phpcompatibility/PHPCompatibility', + (\DIRECTORY_SEPARATOR === '\\') ? 'move' : 'mv', + escapeshellarg(static::$tempLocalPath) + ); + + $this->assertExecute( + $moveCommand, + 0, // Expected exit code. + null, // No stdout expectation. + '', // Empty stderr expectation. + 'Moving the PHPCompatibility directory failed.' + ); + } + + // Verify that the standard registers correctly. + $installCommand = sprintf( + 'composer install-codestandards --no-ansi --working-dir=%s', + escapeshellarg(static::$tempLocalPath) + ); + $installResult = $this->executeCliCommand($installCommand); + $this->assertSame(0, $installResult['exitcode'], 'Exitcode for install-codestandards did not match 0'); + + $regex = sprintf( + '`^PHP CodeSniffer Config installed_paths set to ' + . '[^\s]+/phpcompatibility%s$`', + ($phpcompatVersion === '^7.0' || $phpcsVersion === '2.6.2') ? '' : '/(?:php-|PHP)compatibility' + ); + $this->assertMatchesRegularExpression( + $regex, + trim($installResult['stdout']), + 'Installing the standards failed.' + ); + + // Make sure the CodeSniffer.conf file has been created. + $this->assertFileExists( + static::$tempLocalPath . '/vendor/squizlabs/php_codesniffer/CodeSniffer.conf' + ); + + // Verify that PHPCS sees the external standard. + $this->assertExecute( + '"vendor/bin/phpcs" -i', + 0, // Expected exit code. + 'and PHPCompatibility', // Expected stdout. + '', // Empty stderr expectation. + 'Running phpcs -i failed.', + static::$tempLocalPath + ); + + // Make sure there is a PHP file to scan. + $this->createFile(static::$tempLocalPath . '/test.php'); + + // Verify that PHPCS can run with the external standard. + $phpcsCommand = sprintf( + '"vendor/bin/phpcs" -psl . --standard=PHPCompatibility --sniffs=%s --runtime-set testVersion %s', + $sniff, + \CLI_PHP_MINOR + ); + $phpcsResult = $this->executeCliCommand($phpcsCommand, static::$tempLocalPath); + + $this->assertSame(0, $phpcsResult['exitcode'], 'Exitcode for PHPCS scan did not match 0'); + $this->assertMatchesRegularExpression( + // PHPCS 3.x added the "1/1 (100%)" annotation, the new line (+timing) was added early in the 2.x cycle. + '`^\.(?: 1 / 1 \(100%\))?(?:' . \PHP_EOL . '|$)`', + // Progress reporting moved from stdout to stderr in PHPCS 4.x. + ($phpcsVersion[0] !== '4') ? ltrim($phpcsResult['stdout']) : ltrim($phpcsResult['stderr']), + 'Scanning the directory with PHPCS failed.' + ); + } + + /** + * Data provider. + * + * @return array + */ + public function dataRegisterOneStandard() + { + // Get two PHPCS versions suitable for this PHP version + `master` + PHPCS 4.x dev. + $versions = PHPCSVersions::get(2, true, true); + + $data = array(); + foreach ($versions as $phpcs) { + switch (true) { + case $phpcs === PHPCSVersions::MASTER: + case $phpcs === PHPCSVersions::NEXT_MAJOR: + default: + $phpcompat = '*'; + $sniff = 'PHPCompatibility.FunctionUse.RemovedFunctions'; + break; + + case version_compare($phpcs, '2.2.0', '<'): + // PHPCompatibility 7.x is the last version supporting PHPCS < 2.2.0. + $phpcompat = '^7.0'; + $sniff = 'PHPCompatibility.PHP.DeprecatedFunctions'; + break; + + case version_compare($phpcs, '2.3.0', '<'): + // PHPCompatibility 8.x is the last version supporting PHPCS < 2.3.0. + $phpcompat = '^8.0'; + $sniff = 'PHPCompatibility.PHP.DeprecatedFunctions'; + break; + + case (version_compare($phpcs, '2.6.0', '<')): + // PHPCompatibility 9.x is the last version supporting PHPCS < 2.6.0. + $phpcompat = '^9.0'; + $sniff = 'PHPCompatibility.FunctionUse.RemovedFunctions'; + break; + } + + $data["phpcs $phpcs - phpcompat $phpcompat"] = array( + 'phpcsVersion' => $phpcs, + 'phpcompatVersion' => $phpcompat, + 'sniff' => $sniff, + ); + } + + return $data; + } +} diff --git a/tests/TestCase.php b/tests/TestCase.php index b890db27..babe59ed 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -474,6 +474,46 @@ protected function maybeStripColors($expected, $actual) return $actual; } + /** + * Helper function to create a file which can be used to run PHPCS against. + * + * @param string $path The full path, including filename, to write the file to. + * @param string $contents Optional. The ccntents for the file. + * Defaults to a simple `echo 'Hello world';` PHP file. + * + * @return void + * + * @throws RuntimeException When either passed argument is not a string. + * @throws RuntimeException When the file could not be created. + */ + protected function createFile($path, $contents = '') + { + if (is_string($path) === false || $path === '') { + throw new RuntimeException('Path must be a non-empty string.'); + } + + if (is_string($contents) === false) { + throw new RuntimeException('Contents must be a string.'); + } + + if ($contents === '') { + $contents = <<<'PHP' + Date: Sun, 30 Jan 2022 21:26:04 +0100 Subject: [PATCH 10/14] Tests: set up a fake PHPCS standard as a fixture and create an artifact of it The tests in the `RegisterExternalStandardsTest` class contain quite some work-arounds to handle changes made over time to the external PHPCompatibility standard. The `PHPCompatibility` standard was chosen for use in the tests as it is one of the rare standards which still supports both PHPCS 2.x as well as 3.x. All the same, to improve the stability of the tests and to simplify the actual test code, using "fake" PHPCS standards set up as fixtures as part of this test suite will make life easier. This commit creates the initial setup for that, by: * Adding code to the `CreateComposerZipArtifacts` class to handle creating Composer package zip artifacts for each subdirectory of the `tests/fixtures` directory. * Calling that code from the test bootstrap file to ensure all test artifacts are zipped up and ready for use ahead of running the tests. * Adding an initial "fake" `DummySubDir` standard as a test fixture. * Adding a `README` file to the `tests/fixtures` directory with the rationale and a list of available fixtures. --- tests/CreateComposerZipArtifacts.php | 49 +++++++++++++++++++ tests/TestCase.php | 2 +- tests/bootstrap.php | 3 +- tests/fixtures/README.md | 42 ++++++++++++++++ .../DummySubDir/Sniffs/Demo/DemoSniff.php | 46 +++++++++++++++++ .../dummy-subdir/DummySubDir/ruleset.xml | 5 ++ tests/fixtures/dummy-subdir/composer.json | 10 ++++ 7 files changed, 155 insertions(+), 2 deletions(-) create mode 100644 tests/fixtures/README.md create mode 100644 tests/fixtures/dummy-subdir/DummySubDir/Sniffs/Demo/DemoSniff.php create mode 100644 tests/fixtures/dummy-subdir/DummySubDir/ruleset.xml create mode 100644 tests/fixtures/dummy-subdir/composer.json diff --git a/tests/CreateComposerZipArtifacts.php b/tests/CreateComposerZipArtifacts.php index 613a5cf9..bc796a28 100644 --- a/tests/CreateComposerZipArtifacts.php +++ b/tests/CreateComposerZipArtifacts.php @@ -14,6 +14,7 @@ use RecursiveCallbackFilterIterator; use RecursiveDirectoryIterator; use RecursiveIteratorIterator; +use RuntimeException; use SplFileInfo; use ZipArchive; @@ -133,6 +134,54 @@ public function createPluginArtifact($source, $version) $this->createZipArtifact($source, \ZIP_ARTIFACT_DIR . $fileName, $version); } + /** + * Create a zip package artifact for each test fixture. + * + * @param string $source The source directory where the fixtures can be found. + * Each subdirectory of this directory will be treated as a + * package to be zipped up. + * + * @return void + */ + public function createFixtureArtifacts($source) + { + $di = new DirectoryIterator($source); + foreach ($di as $fileinfo) { + if ($fileinfo->isDot() || $fileinfo->isDir() === false) { + continue; + } + + $sourcePath = $fileinfo->getRealPath(); + $composerFile = $sourcePath . '/composer.json'; + if (file_exists($composerFile) === false) { + throw new RuntimeException( + sprintf( + 'Each fixture MUST contain a composer.json file. File not found in %s', + $composerFile + ) + ); + } + + $config = json_decode(file_get_contents($composerFile), true); + if (isset($config['name']) === false) { + throw new RuntimeException( + sprintf('The fixture composer.json file is missing the "name" for the package in %s', $composerFile) + ); + } + + $targetVersion = self::FIXTURE_VERSION; + if (isset($config['version'])) { + $targetVersion = $config['version']; + } + + $package = $config['name']; + $targetFile = str_replace('/', '-', $package) . "-{$targetVersion}.zip"; + $targetPath = $this->artifactDir . $targetFile; + + $this->createZipArtifact($sourcePath, $targetPath, $targetVersion); + } + } + /** * Create a zip file of an arbitrary directory and package it for use by Composer. * diff --git a/tests/TestCase.php b/tests/TestCase.php index babe59ed..c0f31d57 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -230,7 +230,7 @@ protected static function writeComposerJsonFile($config, $directory) throw new RuntimeException('Directory must be a non-empty string.'); } - // Inject artifact for this plugin. + // Inject artifact for this plugin and some dummy standards. if (isset($config['repositories']) === false) { $config['repositories'][] = array( 'type' => 'artifact', diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 73d1d822..b2c676f4 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -66,7 +66,7 @@ } /* - * Create Zip artifact of the plugin. + * Create Zip artifacts of the plugin itself as well as the test fixtures. */ define('ZIP_ARTIFACT_DIR', __DIR__ . '/artifact/'); @@ -76,6 +76,7 @@ $zipCreator = new CreateComposerZipArtifacts(\ZIP_ARTIFACT_DIR); $zipCreator->clearOldArtifacts(); $zipCreator->createPluginArtifact(dirname(__DIR__), \PLUGIN_ARTIFACT_VERSION); + $zipCreator->createFixtureArtifacts(__DIR__ . '/fixtures/'); unset($zipCreator); } else { echo 'Please enable the zip extension before running the tests.'; diff --git a/tests/fixtures/README.md b/tests/fixtures/README.md new file mode 100644 index 00000000..6a470872 --- /dev/null +++ b/tests/fixtures/README.md @@ -0,0 +1,42 @@ +# Test fixtures + +The subdirectories in this folder contain "fake" PHPCS standards. + +As more and more external PHPCS standards include a `require` for this plugin, these test fixtures should be used in the integration tests instead of _real_ PHPCS standards. + +Using these fixtures will make creating tests more straight-forward as: +* It should prevent issues with the plugin version as used in the tests (version of the created zip artifact) not matching the version constraint for the plugin in a _real_ standard, which would result in Composer downloading from Packagist instead of using the artifact package created for use in the tests. +* It means we don't need to keep track of the version history of external standards in regards to: + - whether or not the external standard uses the correct project `type` in their `composer.json`. + - whether or not they `require` the plugin. + - whether or not they comply with the PHPCS naming conventions. + - whether or not the standard is compatible with PHPCS 2.x/3.x/4.x. + - etc... + +Each subdirectory in this `fixtures` directory will be zipped up and placed in the `artifact` subdirectory ahead of running the tests, making them available to all tests. + +The artifact version of each fake standard will always be `1.0.0`, unless otherwise indicated. +Setting a different version for a fake standard can be achieved by explicitly setting the `version` in the `composer.json` file of the fake standard. + +Any particular test can use one or more of these fake standards. + +Notes: +* The "fake" standards DO need to comply with the naming conventions from PHPCS, which means that the name of a standard as set in the `ruleset.xml` file MUST be the same as the name of the directory containing the `ruleset.xml` file. + So the `ruleset.xml` file for a standard called `Dummy` MUST be in a (sub)directory named `Dummy`. +* A "fake" standard will normally consist of a `composer.json` file in the fake project root and one or more `ruleset.xml` files. +* If the "fake" standard `require`s the plugin, it should do so with `'*'` as the version constraint. +* The "fake" standards generally do NOT need to contain any sniffs or actual rules in the ruleset (unless the standard will be used in a test for running PHPCS). + +It is recommended to add a short description of the situation which can be tested with each fixture to the below list. + +### Package name: `phpcs-composer-installer/dummy-subdir` + +**Description:** +An external PHPCS standard with the `ruleset.xml` file in a subdirectory ("normal" standard setup). + +| Characteristics | Notes | +|--------------------------|--------------------------------------------------------------------------------------------------| +| **Standard(s):** | `DummySubDir` | +| **Includes sniff(s):** | :heavy_checkmark: One sniff - `DummySubDir.Demo.Demo` - which is PHPCS cross-version compatible. | +| **Requires the plugin:** | :x: | + diff --git a/tests/fixtures/dummy-subdir/DummySubDir/Sniffs/Demo/DemoSniff.php b/tests/fixtures/dummy-subdir/DummySubDir/Sniffs/Demo/DemoSniff.php new file mode 100644 index 00000000..f18b7583 --- /dev/null +++ b/tests/fixtures/dummy-subdir/DummySubDir/Sniffs/Demo/DemoSniff.php @@ -0,0 +1,46 @@ + + + + Dummy PHPCS standard for testing. + diff --git a/tests/fixtures/dummy-subdir/composer.json b/tests/fixtures/dummy-subdir/composer.json new file mode 100644 index 00000000..8d082365 --- /dev/null +++ b/tests/fixtures/dummy-subdir/composer.json @@ -0,0 +1,10 @@ +{ + "name" : "phpcs-composer-installer/dummy-subdir", + "description" : "Dummy PHPCS standard with subdirectory ruleset for use in the tests.", + "type" : "phpcodesniffer-standard", + "license" : "MIT", + "require" : { + "php" : ">=5.4", + "squizlabs/php_codesniffer" : "*" + } +} From 12b890d4bb565dd3bb27cdffc468259c467941fa Mon Sep 17 00:00:00 2001 From: jrfnl Date: Sun, 30 Jan 2022 21:43:47 +0100 Subject: [PATCH 11/14] RegisterExternalStandardsTest: switch out PHPCompatibility for the test fixture This replaces the use of the PHPCompatibility standard in the `RegisterExternalStandardsTest` tests with the use of the `dummy-subdir` fake standard fixture. --- .../RegisterExternalStandardsTest.php | 153 +++--------------- 1 file changed, 24 insertions(+), 129 deletions(-) diff --git a/tests/IntegrationTest/RegisterExternalStandardsTest.php b/tests/IntegrationTest/RegisterExternalStandardsTest.php index dab3fa2f..4a9199d3 100644 --- a/tests/IntegrationTest/RegisterExternalStandardsTest.php +++ b/tests/IntegrationTest/RegisterExternalStandardsTest.php @@ -22,7 +22,7 @@ final class RegisterExternalStandardsTest extends TestCase 'name' => 'phpcs-composer-installer/register-external-stnds-one-stnd', 'require-dev' => array( 'squizlabs/php_codesniffer' => null, - 'phpcompatibility/php-compatibility' => null, + 'phpcs-composer-installer/dummy-subdir' => '*', 'dealerdirect/phpcodesniffer-composer-installer' => '*', ), ); @@ -48,19 +48,16 @@ protected function tear_down() * * @dataProvider dataRegisterOneStandard * - * @param string $phpcsVersion PHPCS version to use in this test. - * This version is randomly selected from the PHPCS versions compatible - * with the PHP version used in the test. - * @param string $phpcompatVersion PHPCompatibility version to use in this test. - * @param string $sniff Name of the sniff to use for a PHPCS test run. + * @param string $phpcsVersion PHPCS version to use in this test. + * This version is randomly selected from the PHPCS versions compatible + * with the PHP version used in the test. * * @return void */ - public function testRegisterOneStandardGlobal($phpcsVersion, $phpcompatVersion, $sniff) + public function testRegisterOneStandardGlobal($phpcsVersion) { - $config = $this->configOneStandard; - $config['require-dev']['squizlabs/php_codesniffer'] = $phpcsVersion; - $config['require-dev']['phpcompatibility/php-compatibility'] = $phpcompatVersion; + $config = $this->configOneStandard; + $config['require-dev']['squizlabs/php_codesniffer'] = $phpcsVersion; $this->writeComposerJsonFile($config, static::$tempGlobalPath); $this->assertComposerValidates(static::$tempGlobalPath); @@ -74,39 +71,12 @@ public function testRegisterOneStandardGlobal($phpcsVersion, $phpcompatVersion, 'Failed to install dependencies.' ); - /* - * In PHPCompatibility 7.x, the directory layout was not yet compatible with a Composer install, - * so we need to "move" the directory to make it compatible. - * Note: PHPCompatibility >= 8 has a conflict setting with PHPCS 2.6.2, which means that - * PHPCompatibility 7.x would be used in that case. - */ - if ($phpcompatVersion === '^7.0' || $phpcsVersion === '2.6.2') { - $moveCommand = sprintf( - '%1$s %2$s/vendor/phpcompatibility/php-compatibility %2$s/vendor/phpcompatibility/PHPCompatibility', - (\DIRECTORY_SEPARATOR === '\\') ? 'move' : 'mv', - escapeshellarg(static::$tempGlobalPath) - ); - - $this->assertExecute( - $moveCommand, - 0, // Expected exit code. - null, // No stdout expectation. - '', // Empty stderr expectation. - 'Moving the PHPCompatibility directory failed.' - ); - } - // Verify that the standard registers correctly. $installResult = $this->executeCliCommand('composer global install-codestandards --no-ansi'); $this->assertSame(0, $installResult['exitcode'], 'Exitcode for install-codestandards did not match 0'); - $regex = sprintf( - '`^PHP CodeSniffer Config installed_paths set to ' - . '[^\s]+/phpcompatibility%s$`', - ($phpcompatVersion === '^7.0' || $phpcsVersion === '2.6.2') ? '' : '/(?:php-|PHP)compatibility' - ); $this->assertMatchesRegularExpression( - $regex, + '`^PHP CodeSniffer Config installed_paths set to [^\s]+/dummy-subdir$`', trim($installResult['stdout']), 'Installing the standards failed.' ); @@ -119,9 +89,9 @@ public function testRegisterOneStandardGlobal($phpcsVersion, $phpcompatVersion, // Verify that PHPCS sees the external standard. $this->assertExecute( '"vendor/bin/phpcs" -i', - 0, // Expected exit code. - 'and PHPCompatibility', // Expected stdout. - '', // Empty stderr expectation. + 0, // Expected exit code. + 'and DummySubDir', // Expected stdout. + '', // Empty stderr expectation. 'Running phpcs -i failed.', static::$tempGlobalPath ); @@ -130,11 +100,7 @@ public function testRegisterOneStandardGlobal($phpcsVersion, $phpcompatVersion, $this->createFile(static::$tempGlobalPath . '/test.php'); // Verify that PHPCS can run with the external standard. - $phpcsCommand = sprintf( - '"vendor/bin/phpcs" -psl . --standard=PHPCompatibility --sniffs=%s --runtime-set testVersion %s', - $sniff, - \CLI_PHP_MINOR - ); + $phpcsCommand = '"vendor/bin/phpcs" -psl . --standard=DummySubDir'; $phpcsResult = $this->executeCliCommand($phpcsCommand, static::$tempGlobalPath); $this->assertSame(0, $phpcsResult['exitcode'], 'Exitcode for PHPCS scan did not match 0'); @@ -152,19 +118,16 @@ public function testRegisterOneStandardGlobal($phpcsVersion, $phpcompatVersion, * * @dataProvider dataRegisterOneStandard * - * @param string $phpcsVersion PHPCS version to use in this test. - * This version is randomly selected from the PHPCS versions compatible - * with the PHP version used in the test. - * @param string $phpcompatVersion PHPCompatibility version to use in this test. - * @param string $sniff Name of the sniff to use for a PHPCS test run. + * @param string $phpcsVersion PHPCS version to use in this test. + * This version is randomly selected from the PHPCS versions compatible + * with the PHP version used in the test. * * @return void */ - public function testRegisterOneStandardLocal($phpcsVersion, $phpcompatVersion, $sniff) + public function testRegisterOneStandardLocal($phpcsVersion) { - $config = $this->configOneStandard; - $config['require-dev']['squizlabs/php_codesniffer'] = $phpcsVersion; - $config['require-dev']['phpcompatibility/php-compatibility'] = $phpcompatVersion; + $config = $this->configOneStandard; + $config['require-dev']['squizlabs/php_codesniffer'] = $phpcsVersion; $this->writeComposerJsonFile($config, static::$tempLocalPath); $this->assertComposerValidates(static::$tempLocalPath); @@ -178,28 +141,6 @@ public function testRegisterOneStandardLocal($phpcsVersion, $phpcompatVersion, $ 'Failed to install dependencies.' ); - /* - * In PHPCompatibility 7.x, the directory layout was not yet compatible with a Composer install, - * so we need to "move" the directory to make it compatible. - * Note: PHPCompatibility >= 8 has a conflict setting with PHPCS 2.6.2, which means that - * PHPCompatibility 7.x would be used in that case. - */ - if ($phpcompatVersion === '^7.0' || $phpcsVersion === '2.6.2') { - $moveCommand = sprintf( - '%1$s %2$s/vendor/phpcompatibility/php-compatibility %2$s/vendor/phpcompatibility/PHPCompatibility', - (\DIRECTORY_SEPARATOR === '\\') ? 'move' : 'mv', - escapeshellarg(static::$tempLocalPath) - ); - - $this->assertExecute( - $moveCommand, - 0, // Expected exit code. - null, // No stdout expectation. - '', // Empty stderr expectation. - 'Moving the PHPCompatibility directory failed.' - ); - } - // Verify that the standard registers correctly. $installCommand = sprintf( 'composer install-codestandards --no-ansi --working-dir=%s', @@ -208,13 +149,8 @@ public function testRegisterOneStandardLocal($phpcsVersion, $phpcompatVersion, $ $installResult = $this->executeCliCommand($installCommand); $this->assertSame(0, $installResult['exitcode'], 'Exitcode for install-codestandards did not match 0'); - $regex = sprintf( - '`^PHP CodeSniffer Config installed_paths set to ' - . '[^\s]+/phpcompatibility%s$`', - ($phpcompatVersion === '^7.0' || $phpcsVersion === '2.6.2') ? '' : '/(?:php-|PHP)compatibility' - ); $this->assertMatchesRegularExpression( - $regex, + '`^PHP CodeSniffer Config installed_paths set to [^\s]+/dummy-subdir$`', trim($installResult['stdout']), 'Installing the standards failed.' ); @@ -227,9 +163,9 @@ public function testRegisterOneStandardLocal($phpcsVersion, $phpcompatVersion, $ // Verify that PHPCS sees the external standard. $this->assertExecute( '"vendor/bin/phpcs" -i', - 0, // Expected exit code. - 'and PHPCompatibility', // Expected stdout. - '', // Empty stderr expectation. + 0, // Expected exit code. + 'and DummySubDir', // Expected stdout. + '', // Empty stderr expectation. 'Running phpcs -i failed.', static::$tempLocalPath ); @@ -238,11 +174,7 @@ public function testRegisterOneStandardLocal($phpcsVersion, $phpcompatVersion, $ $this->createFile(static::$tempLocalPath . '/test.php'); // Verify that PHPCS can run with the external standard. - $phpcsCommand = sprintf( - '"vendor/bin/phpcs" -psl . --standard=PHPCompatibility --sniffs=%s --runtime-set testVersion %s', - $sniff, - \CLI_PHP_MINOR - ); + $phpcsCommand = '"vendor/bin/phpcs" -psl . --standard=DummySubDir'; $phpcsResult = $this->executeCliCommand($phpcsCommand, static::$tempLocalPath); $this->assertSame(0, $phpcsResult['exitcode'], 'Exitcode for PHPCS scan did not match 0'); @@ -264,43 +196,6 @@ public function dataRegisterOneStandard() { // Get two PHPCS versions suitable for this PHP version + `master` + PHPCS 4.x dev. $versions = PHPCSVersions::get(2, true, true); - - $data = array(); - foreach ($versions as $phpcs) { - switch (true) { - case $phpcs === PHPCSVersions::MASTER: - case $phpcs === PHPCSVersions::NEXT_MAJOR: - default: - $phpcompat = '*'; - $sniff = 'PHPCompatibility.FunctionUse.RemovedFunctions'; - break; - - case version_compare($phpcs, '2.2.0', '<'): - // PHPCompatibility 7.x is the last version supporting PHPCS < 2.2.0. - $phpcompat = '^7.0'; - $sniff = 'PHPCompatibility.PHP.DeprecatedFunctions'; - break; - - case version_compare($phpcs, '2.3.0', '<'): - // PHPCompatibility 8.x is the last version supporting PHPCS < 2.3.0. - $phpcompat = '^8.0'; - $sniff = 'PHPCompatibility.PHP.DeprecatedFunctions'; - break; - - case (version_compare($phpcs, '2.6.0', '<')): - // PHPCompatibility 9.x is the last version supporting PHPCS < 2.6.0. - $phpcompat = '^9.0'; - $sniff = 'PHPCompatibility.FunctionUse.RemovedFunctions'; - break; - } - - $data["phpcs $phpcs - phpcompat $phpcompat"] = array( - 'phpcsVersion' => $phpcs, - 'phpcompatVersion' => $phpcompat, - 'sniff' => $sniff, - ); - } - - return $data; + return PHPCSVersions::toDataprovider($versions); } } From f8d256fd8040a47a2e862a9f976c6d0851a7ff29 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Tue, 1 Feb 2022 05:06:04 +0100 Subject: [PATCH 12/14] RegisterExternalStandardsTest: simplify check whether PHPCS can run with the standard To check whether PHPCS can run with the standard, previously, a scan on a simple file was done, with this file being created on the fly for each test. This commit replaces that check with running the "explain" command. Advantages: * The "explain" command does not need a file to scan and can still confirm that the standard can be read correctly by PHPCS. * The output for the "explain" command has fewer differences across PHPCS versions, so is simpler to verify. --- .../RegisterExternalStandardsTest.php | 34 +++++++------------ 1 file changed, 12 insertions(+), 22 deletions(-) diff --git a/tests/IntegrationTest/RegisterExternalStandardsTest.php b/tests/IntegrationTest/RegisterExternalStandardsTest.php index 4a9199d3..57125ac9 100644 --- a/tests/IntegrationTest/RegisterExternalStandardsTest.php +++ b/tests/IntegrationTest/RegisterExternalStandardsTest.php @@ -96,20 +96,15 @@ public function testRegisterOneStandardGlobal($phpcsVersion) static::$tempGlobalPath ); - // Make sure there is a PHP file to scan. - $this->createFile(static::$tempGlobalPath . '/test.php'); - - // Verify that PHPCS can run with the external standard. - $phpcsCommand = '"vendor/bin/phpcs" -psl . --standard=DummySubDir'; + // Verify that PHPCS can with the external standard set as the standard. + $phpcsCommand = '"vendor/bin/phpcs" --standard=DummySubDir -e'; $phpcsResult = $this->executeCliCommand($phpcsCommand, static::$tempGlobalPath); - $this->assertSame(0, $phpcsResult['exitcode'], 'Exitcode for PHPCS scan did not match 0'); + $this->assertSame(0, $phpcsResult['exitcode'], 'Exitcode for PHPCS explain did not match 0'); $this->assertMatchesRegularExpression( - // PHPCS 3.x added the "1/1 (100%)" annotation, the new line (+timing) was added early in the 2.x cycle. - '`^\.(?: 1 / 1 \(100%\))?(?:' . \PHP_EOL . '|$)`', - // Progress reporting moved from stdout to stderr in PHPCS 4.x. - ($phpcsVersion[0] !== '4') ? trim($phpcsResult['stdout']) : trim($phpcsResult['stderr']), - 'Scanning the directory with PHPCS failed.' + '`DummySubDir \(1 sniffs?\)\s+[-]+\s+DummySubDir\.Demo\.Demo(?:[\r\n]+|$)`', + $phpcsResult['stdout'], + 'Output of the PHPCS explain command did not match the expectation.' ); } @@ -170,20 +165,15 @@ public function testRegisterOneStandardLocal($phpcsVersion) static::$tempLocalPath ); - // Make sure there is a PHP file to scan. - $this->createFile(static::$tempLocalPath . '/test.php'); - - // Verify that PHPCS can run with the external standard. - $phpcsCommand = '"vendor/bin/phpcs" -psl . --standard=DummySubDir'; + // Verify that PHPCS can with the external standard set as the standard. + $phpcsCommand = '"vendor/bin/phpcs" --standard=DummySubDir -e'; $phpcsResult = $this->executeCliCommand($phpcsCommand, static::$tempLocalPath); - $this->assertSame(0, $phpcsResult['exitcode'], 'Exitcode for PHPCS scan did not match 0'); + $this->assertSame(0, $phpcsResult['exitcode'], 'Exitcode for PHPCS explain did not match 0'); $this->assertMatchesRegularExpression( - // PHPCS 3.x added the "1/1 (100%)" annotation, the new line (+timing) was added early in the 2.x cycle. - '`^\.(?: 1 / 1 \(100%\))?(?:' . \PHP_EOL . '|$)`', - // Progress reporting moved from stdout to stderr in PHPCS 4.x. - ($phpcsVersion[0] !== '4') ? ltrim($phpcsResult['stdout']) : ltrim($phpcsResult['stderr']), - 'Scanning the directory with PHPCS failed.' + '`DummySubDir \(1 sniffs?\)\s+[-]+\s+DummySubDir\.Demo\.Demo(?:[\r\n]+|$)`', + $phpcsResult['stdout'], + 'Output of the PHPCS explain command did not match the expectation.' ); } From d4f64b822a76e1aac325f18ac160b9daac01ff49 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Sat, 5 Feb 2022 16:32:19 +0100 Subject: [PATCH 13/14] TestCase: add `willPluginOutputShow()` method In very select circumstances (PHP 5.5 with Composer 1.x on Windows) and sometimes even only with select PHPCS versions, the plugin output will not be shown in the transcript from Composer, even though the plugin _does_ actually run. Instead of skipping those tests completely, I'm suggestion to selectively skip the output expectation for the `composer install/update` run instead. That way we can still verify that the plugin _has_ actually done its job by checking that paths have been registered with PHPCS. I would recommend to only implement the use of this method for those select tests where we have actually seen failures due to this Composer bug and to just leave the other tests alone. --- tests/IntegrationTest/BaseLineTest.php | 30 ++++---------------------- tests/TestCase.php | 19 ++++++++++++++++ 2 files changed, 23 insertions(+), 26 deletions(-) diff --git a/tests/IntegrationTest/BaseLineTest.php b/tests/IntegrationTest/BaseLineTest.php index a87ee410..83356543 100644 --- a/tests/IntegrationTest/BaseLineTest.php +++ b/tests/IntegrationTest/BaseLineTest.php @@ -64,18 +64,6 @@ protected function tear_down() */ public function testBaseLineGlobal($phpcsVersion, $expectedStnds) { - if ( - $phpcsVersion === PHPCSVersions::MASTER - && \CLI_PHP_MINOR === '5.5' - && $this->onWindows() === true - && substr(\COMPOSER_VERSION, 0, 1) === '1' - ) { - $this->markTestSkipped( - 'Composer 1.x on Windows with PHP 5.5 does run the plugin when there are no external standards,' - . ' but doesn\'t consistently show this in the logs' - ); - } - $config = $this->composerConfig; $config['require-dev']['squizlabs/php_codesniffer'] = $phpcsVersion; @@ -83,10 +71,11 @@ public function testBaseLineGlobal($phpcsVersion, $expectedStnds) $this->assertComposerValidates(static::$tempGlobalPath); // Make sure the plugin runs. + $expectedStdOut = $this->willPluginOutputShow() ? 'Running PHPCodeSniffer Composer Installer' : null; $this->assertExecute( 'composer global install -v --no-ansi', 0, // Expected exit code. - 'Running PHPCodeSniffer Composer Installer', // Expected stdout. + $expectedStdOut, // Expected stdout. null, // No stderr expectation. 'Failed to install dependencies.' ); @@ -119,18 +108,6 @@ public function testBaseLineGlobal($phpcsVersion, $expectedStnds) */ public function testBaseLineLocal($phpcsVersion, $expectedStnds) { - if ( - $phpcsVersion === PHPCSVersions::MASTER - && \CLI_PHP_MINOR === '5.5' - && $this->onWindows() === true - && substr(\COMPOSER_VERSION, 0, 1) === '1' - ) { - $this->markTestSkipped( - 'Composer 1.x on Windows with PHP 5.5 does run the plugin when there are no external standards,' - . ' but doesn\'t consistently show this in the logs' - ); - } - $config = $this->composerConfig; $config['require-dev']['squizlabs/php_codesniffer'] = $phpcsVersion; @@ -138,10 +115,11 @@ public function testBaseLineLocal($phpcsVersion, $expectedStnds) $this->assertComposerValidates(static::$tempLocalPath); // Make sure the plugin runs. + $expectedStdOut = $this->willPluginOutputShow() ? 'Running PHPCodeSniffer Composer Installer' : null; $this->assertExecute( sprintf('composer install -v --no-ansi --working-dir=%s', escapeshellarg(static::$tempLocalPath)), 0, // Expected exit code. - 'Running PHPCodeSniffer Composer Installer', // Expected stdout. + $expectedStdOut, // Expected stdout. null, // No stderr expectation. 'Failed to install dependencies.' ); diff --git a/tests/TestCase.php b/tests/TestCase.php index c0f31d57..2375b1c0 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -208,6 +208,25 @@ protected static function onWindows() return strpos(strtoupper(\PHP_OS), 'WIN') === 0; } + /** + * Determine whether output expectations can be set for a typical Composer `install`/`update` run. + * + * Composer 1.x on Windows with PHP 5.5 DOES run the plugin, but doesn't consistently show this in the logs. + * This method can be used to still test output expectations in _most_ cases, without failing the tests + * in the rare case they won't show. + * + * It is recommended to only add a call to this method to a test when it has been proven + * to fail without it. + * + * @return bool + */ + protected function willPluginOutputShow() + { + return ((\CLI_PHP_MINOR === '5.5' + && $this->onWindows() === true + && substr(\COMPOSER_VERSION, 0, 1) === '1') === false); + } + /** * Create a composer.json file based on a given configuration. * From 97ba038345c7666a01781874b69358a8101ab060 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Sun, 6 Feb 2022 15:51:13 +0100 Subject: [PATCH 14/14] Tests: processed review feedback --- composer.json | 3 ++- phpcs.xml.dist | 6 +++++- tests/CreateComposerZipArtifacts.php | 16 +++------------- tests/IntegrationTest/BaseLineTest.php | 4 ++-- .../RegisterExternalStandardsTest.php | 4 ++-- tests/PHPCSVersions.php | 18 ++++++++---------- tests/TestCase.php | 12 ++++++------ tests/bootstrap.php | 2 +- 8 files changed, 29 insertions(+), 36 deletions(-) diff --git a/composer.json b/composer.json index df05073f..ba8cf7c0 100644 --- a/composer.json +++ b/composer.json @@ -32,11 +32,12 @@ "squizlabs/php_codesniffer": "^2.0 || ^3.1.0 || ^4.0" }, "require-dev": { + "ext-json": "*", "ext-zip": "*", "composer/composer": "*", "phpcompatibility/php-compatibility": "^9.0", "php-parallel-lint/php-parallel-lint": "^1.3.1", - "yoast/phpunit-polyfills": "^1.0.1" + "yoast/phpunit-polyfills": "^1.0" }, "minimum-stability": "dev", "prefer-stable": true, diff --git a/phpcs.xml.dist b/phpcs.xml.dist index 0bbe32ed..493a03ea 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -22,7 +22,11 @@ - + + + + + */tests/bootstrap\.php$ diff --git a/tests/CreateComposerZipArtifacts.php b/tests/CreateComposerZipArtifacts.php index bc796a28..202cdefe 100644 --- a/tests/CreateComposerZipArtifacts.php +++ b/tests/CreateComposerZipArtifacts.php @@ -92,11 +92,7 @@ class CreateComposerZipArtifacts public function __construct($artifactDir) { // Make sure the directory has a trailing slash. - if (substr($artifactDir, -1) !== '/') { - $artifactDir .= '/'; - } - - $this->artifactDir = $artifactDir; + $this->artifactDir = rtrim($artifactDir, '/') . '/'; } /** @@ -108,15 +104,9 @@ public function clearOldArtifacts() { $di = new DirectoryIterator($this->artifactDir); foreach ($di as $fileinfo) { - if ( - $fileinfo->isDot() - || $fileinfo->isFile() === false - || $fileinfo->getExtension() !== 'zip' - ) { - continue; + if ($fileinfo->isFile() && $fileinfo->getExtension() === 'zip') { + @unlink($fileinfo->getPathname()); } - - @unlink($fileinfo->getPathname()); } } diff --git a/tests/IntegrationTest/BaseLineTest.php b/tests/IntegrationTest/BaseLineTest.php index 83356543..8c635346 100644 --- a/tests/IntegrationTest/BaseLineTest.php +++ b/tests/IntegrationTest/BaseLineTest.php @@ -64,7 +64,7 @@ protected function tear_down() */ public function testBaseLineGlobal($phpcsVersion, $expectedStnds) { - $config = $this->composerConfig; + $config = $this->composerConfig; $config['require-dev']['squizlabs/php_codesniffer'] = $phpcsVersion; $this->writeComposerJsonFile($config, static::$tempGlobalPath); @@ -108,7 +108,7 @@ public function testBaseLineGlobal($phpcsVersion, $expectedStnds) */ public function testBaseLineLocal($phpcsVersion, $expectedStnds) { - $config = $this->composerConfig; + $config = $this->composerConfig; $config['require-dev']['squizlabs/php_codesniffer'] = $phpcsVersion; $this->writeComposerJsonFile($config, static::$tempLocalPath); diff --git a/tests/IntegrationTest/RegisterExternalStandardsTest.php b/tests/IntegrationTest/RegisterExternalStandardsTest.php index 57125ac9..e2841dd6 100644 --- a/tests/IntegrationTest/RegisterExternalStandardsTest.php +++ b/tests/IntegrationTest/RegisterExternalStandardsTest.php @@ -56,7 +56,7 @@ protected function tear_down() */ public function testRegisterOneStandardGlobal($phpcsVersion) { - $config = $this->configOneStandard; + $config = $this->configOneStandard; $config['require-dev']['squizlabs/php_codesniffer'] = $phpcsVersion; $this->writeComposerJsonFile($config, static::$tempGlobalPath); @@ -121,7 +121,7 @@ public function testRegisterOneStandardGlobal($phpcsVersion) */ public function testRegisterOneStandardLocal($phpcsVersion) { - $config = $this->configOneStandard; + $config = $this->configOneStandard; $config['require-dev']['squizlabs/php_codesniffer'] = $phpcsVersion; $this->writeComposerJsonFile($config, static::$tempLocalPath); diff --git a/tests/PHPCSVersions.php b/tests/PHPCSVersions.php index 18c884c9..51b65842 100644 --- a/tests/PHPCSVersions.php +++ b/tests/PHPCSVersions.php @@ -250,7 +250,7 @@ public static function getRandom($inclMaster = false, $inclNextMajor = false) /** * Convert a versions array to an array suitable for use as a PHPUnit dataprovider. * - * @param array $version Array with PHPCS version numbers as values. + * @param array $versions Array with PHPCS version numbers as values. * * @return array Array of PHPCS version identifiers in a format usable for a test data provider. */ @@ -277,8 +277,6 @@ public static function toDataprovider($versions) */ public static function getSupportedVersions() { - $versions = self::$allPhpcsVersions; - /* * Adjust the list of available versions based on the PHP version on which the tests are run. */ @@ -381,8 +379,8 @@ public static function getStandards($version) if ( is_string($version) === false || (isset(self::$allPhpcsVersions[$version]) === false - && $version !== PHPCSVersions::MASTER - && $version !== PHPCSVersions::NEXT_MAJOR) + && $version !== self::MASTER + && $version !== self::NEXT_MAJOR) ) { throw new RuntimeException('The version parameter must be a valid PHPCS version number as a string.'); } @@ -395,14 +393,14 @@ public static function getStandards($version) 'Zend', ); - if ($version !== PHPCSVersions::NEXT_MAJOR) { + if ($version !== self::NEXT_MAJOR) { // The MySource standard is available in PHPCS 2.x and 3.x, but will be removed in 4.0. $standards[] = 'MySource'; } if ( - $version !== PHPCSVersions::MASTER - && $version !== PHPCSVersions::NEXT_MAJOR + $version !== self::MASTER + && $version !== self::NEXT_MAJOR && version_compare($version, '3.0.0', '<') ) { // The PHPCS standard was available in PHPCS 2.x, but has been removed in 3.0. @@ -410,8 +408,8 @@ public static function getStandards($version) } if ( - $version === PHPCSVersions::MASTER - || $version === PHPCSVersions::NEXT_MAJOR + $version === self::MASTER + || $version === self::NEXT_MAJOR || version_compare($version, '3.3.0', '>=') ) { // The PSR12 standard is available since PHPCS 3.3.0. diff --git a/tests/TestCase.php b/tests/TestCase.php index 2375b1c0..57b00c04 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -122,7 +122,7 @@ public function assertComposerValidates($workingDir = '', $file = '') $command, 0, // Expected exit code. null, // Expected stdout. - $stderr, // Expected sterr. + $stderr, // Expected stderr. $message ); } @@ -205,7 +205,7 @@ public function assertExecute( */ protected static function onWindows() { - return strpos(strtoupper(\PHP_OS), 'WIN') === 0; + return stripos(\PHP_OS, 'WIN') === 0; } /** @@ -224,7 +224,7 @@ protected function willPluginOutputShow() { return ((\CLI_PHP_MINOR === '5.5' && $this->onWindows() === true - && substr(\COMPOSER_VERSION, 0, 1) === '1') === false); + && strpos(\COMPOSER_VERSION, '1') === 0) === false); } /** @@ -273,7 +273,7 @@ protected static function writeComposerJsonFile($config, $directory) * Disable TLS when on Windows with Composer 1.x and PHP 5.4. * @link https://github.com/composer/composer/issues/10495 */ - if (static::onWindows() === true && \CLI_PHP_MINOR === '5.4' && substr(\COMPOSER_VERSION, 0, 1) === '1') { + if (static::onWindows() === true && \CLI_PHP_MINOR === '5.4' && strpos(\COMPOSER_VERSION, '1') === 0) { $config['config']['disable-tls'] = true; } @@ -484,8 +484,8 @@ protected function maybeStripColors($expected, $actual) } if ( - strpos($expected, "\033") === false && strpos($actual, "\033") !== false - || strpos($expected, "\x1b") === false && strpos($actual, "\x1b") !== false + (strpos($expected, "\033") === false && strpos($actual, "\033") !== false) + || (strpos($expected, "\x1b") === false && strpos($actual, "\x1b") !== false) ) { $actual = preg_replace('`(?:\\\\033|\\\\x1b)\\\\[[0-9]+(;[0-9]*)[A-Za-z]`', '', $actual); } diff --git a/tests/bootstrap.php b/tests/bootstrap.php index b2c676f4..52c18a2a 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -15,7 +15,7 @@ */ $tempDir = sys_get_temp_dir() . '/PHPCSPluginTest'; if (file_exists($tempDir) === true) { - if (strpos(strtoupper(\PHP_OS), 'WIN') === 0) { + if (stripos(\PHP_OS, 'WIN') === 0) { // Windows. shell_exec(sprintf('rd /s /q %s', escapeshellarg($tempDir))); } else {