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..a2ddc968 100644
--- a/.github/workflows/integrationtest.yml
+++ b/.github/workflows/integrationtest.yml
@@ -18,126 +18,32 @@ concurrency:
jobs:
test:
- runs-on: ubuntu-latest
+ 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']
- 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 }}
+ 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
@@ -147,41 +53,27 @@ jobs:
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
- ini-values: error_reporting=-1, display_errors=On
+ ini-values: zend.assertions=1, error_reporting=-1, display_errors=On
+ tools: "composer:${{ matrix.composer }}"
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
+ - 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'
+ - name: Install Composer dependencies
+ if: ${{ matrix.php != '8.2' }}
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
+ composer-options: '--optimize-autoloader'
- - 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 }}
+ - name: Install Composer dependencies
+ if: ${{ matrix.php == '8.2' }}
+ uses: "ramsey/composer-install@v2"
+ with:
+ composer-options: '--ignore-platform-reqs --optimize-autoloader'
- # 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 }}
+ - name: Run integration tests
+ run: vendor/bin/phpunit --no-coverage
diff --git a/.github/workflows/quicktest.yml b/.github/workflows/quicktest.yml
index 1b88771a..5bf8f2fa 100644
--- a/.github/workflows/quicktest.yml
+++ b/.github/workflows/quicktest.yml
@@ -20,28 +20,22 @@ 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
+ runs-on: "${{ matrix.os }}"
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'
+ php:
+ - '5.4'
+ - '7.2'
+ - 'latest'
+ composer:
+ - 'v1'
+ - 'v2'
+ os:
+ - 'ubuntu-latest'
+ - 'windows-latest'
- name: "Quick test: PHP ${{ matrix.php }} - PHPCS ${{ matrix.phpcs_version }}"
+ name: "Quick test"
steps:
- name: Checkout code
@@ -51,41 +45,20 @@ jobs:
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
- ini-values: error_reporting=-1, display_errors=On
+ ini-values: zend.assertions=1, error_reporting=-1, display_errors=On
+ tools: "composer:${{ matrix.composer }}"
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
+ - 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'
+ - 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 }}
+ composer-options: '--optimize-autoloader'
- # 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 }}
+ - name: Run integration tests
+ run: vendor/bin/phpunit --no-coverage
diff --git a/.gitignore b/.gitignore
index c5f3d254..568b9ede 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,3 +2,7 @@ vendor/
composer.lock
.phpcs.xml
phpcs.xml
+.phpunit.result.cache
+phpunit.xml
+/build/
+/tests/artifact/*.zip
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..ba8cf7c0 100644
--- a/composer.json
+++ b/composer.json
@@ -32,9 +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"
+ "php-parallel-lint/php-parallel-lint": "^1.3.1",
+ "yoast/phpunit-polyfills": "^1.0"
},
"minimum-stability": "dev",
"prefer-stable": true,
@@ -43,6 +46,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 +60,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..493a03ea 100644
--- a/phpcs.xml.dist
+++ b/phpcs.xml.dist
@@ -9,7 +9,8 @@
.
- */.github/*
+ */.git*
+ */build/*
*/vendor/*
@@ -21,6 +22,19 @@
-
+
+
+
+
+
+
+
+ */tests/bootstrap\.php$
+
+
+
+
+ */tests/
+
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
new file mode 100644
index 00000000..7ac1a77c
--- /dev/null
+++ b/phpunit.xml.dist
@@ -0,0 +1,34 @@
+
+
+
+
+
+ tests/IntegrationTest/
+
+
+
+
+
+
+
+
+
+ ./src/
+
+
+
+
+
+
+
+
diff --git a/tests/CreateComposerZipArtifacts.php b/tests/CreateComposerZipArtifacts.php
new file mode 100644
index 00000000..202cdefe
--- /dev/null
+++ b/tests/CreateComposerZipArtifacts.php
@@ -0,0 +1,249 @@
+ '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.
+ $this->artifactDir = rtrim($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->isFile() && $fileinfo->getExtension() === 'zip') {
+ @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 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.
+ *
+ * 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/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/IntegrationTest/BaseLineTest.php b/tests/IntegrationTest/BaseLineTest.php
new file mode 100644
index 00000000..8c635346
--- /dev/null
+++ b/tests/IntegrationTest/BaseLineTest.php
@@ -0,0 +1,167 @@
+ '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.
+ $expectedStdOut = $this->willPluginOutputShow() ? 'Running PHPCodeSniffer Composer Installer' : null;
+ $this->assertExecute(
+ 'composer global install -v --no-ansi',
+ 0, // Expected exit code.
+ $expectedStdOut, // 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.
+ $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.
+ $expectedStdOut, // 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/IntegrationTest/RegisterExternalStandardsTest.php b/tests/IntegrationTest/RegisterExternalStandardsTest.php
new file mode 100644
index 00000000..e2841dd6
--- /dev/null
+++ b/tests/IntegrationTest/RegisterExternalStandardsTest.php
@@ -0,0 +1,191 @@
+ 'phpcs-composer-installer/register-external-stnds-one-stnd',
+ 'require-dev' => array(
+ 'squizlabs/php_codesniffer' => null,
+ 'phpcs-composer-installer/dummy-subdir' => '*',
+ '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.
+ *
+ * @return void
+ */
+ public function testRegisterOneStandardGlobal($phpcsVersion)
+ {
+ $config = $this->configOneStandard;
+ $config['require-dev']['squizlabs/php_codesniffer'] = $phpcsVersion;
+
+ $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.'
+ );
+
+ // 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');
+
+ $this->assertMatchesRegularExpression(
+ '`^PHP CodeSniffer Config installed_paths set to [^\s]+/dummy-subdir$`',
+ 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 DummySubDir', // Expected stdout.
+ '', // Empty stderr expectation.
+ 'Running phpcs -i failed.',
+ static::$tempGlobalPath
+ );
+
+ // 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 explain did not match 0');
+ $this->assertMatchesRegularExpression(
+ '`DummySubDir \(1 sniffs?\)\s+[-]+\s+DummySubDir\.Demo\.Demo(?:[\r\n]+|$)`',
+ $phpcsResult['stdout'],
+ 'Output of the PHPCS explain command did not match the expectation.'
+ );
+ }
+
+ /**
+ * 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.
+ *
+ * @return void
+ */
+ public function testRegisterOneStandardLocal($phpcsVersion)
+ {
+ $config = $this->configOneStandard;
+ $config['require-dev']['squizlabs/php_codesniffer'] = $phpcsVersion;
+
+ $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.'
+ );
+
+ // 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');
+
+ $this->assertMatchesRegularExpression(
+ '`^PHP CodeSniffer Config installed_paths set to [^\s]+/dummy-subdir$`',
+ 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 DummySubDir', // Expected stdout.
+ '', // Empty stderr expectation.
+ 'Running phpcs -i failed.',
+ static::$tempLocalPath
+ );
+
+ // 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 explain did not match 0');
+ $this->assertMatchesRegularExpression(
+ '`DummySubDir \(1 sniffs?\)\s+[-]+\s+DummySubDir\.Demo\.Demo(?:[\r\n]+|$)`',
+ $phpcsResult['stdout'],
+ 'Output of the PHPCS explain command did not match the expectation.'
+ );
+ }
+
+ /**
+ * 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);
+ return PHPCSVersions::toDataprovider($versions);
+ }
+}
diff --git a/tests/PHPCSVersions.php b/tests/PHPCSVersions.php
new file mode 100644
index 00000000..51b65842
--- /dev/null
+++ b/tests/PHPCSVersions.php
@@ -0,0 +1,423 @@
+ '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 $versions 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()
+ {
+ /*
+ * 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 !== self::MASTER
+ && $version !== self::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 !== 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 !== 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.
+ $standards[] = 'PHPCS';
+ }
+
+ if (
+ $version === self::MASTER
+ || $version === self::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
new file mode 100644
index 00000000..57b00c04
--- /dev/null
+++ b/tests/TestCase.php
@@ -0,0 +1,582 @@
+ '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');
+ }
+
+
+ /* ***** 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 stderr.
+ $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 ***** */
+
+ /**
+ * Determine whether or not the tests are being run on Windows.
+ *
+ * @return bool
+ */
+ protected static function onWindows()
+ {
+ return stripos(\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
+ && strpos(\COMPOSER_VERSION, '1') === 0) === false);
+ }
+
+ /**
+ * 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 artifact for this plugin and some dummy standards.
+ 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(
+ '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' && strpos(\COMPOSER_VERSION, '1') === 0) {
+ $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');
+ }
+
+ // 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
+ );
+ }
+
+ /**
+ * 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);
+
+ // 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;
+ }
+
+ /**
+ * 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;
+ }
+
+ /**
+ * 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'
+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.';
+ die(1);
+}
+
+/*
+ * Set a few constants for use throughout the tests.
+ */
+
+define('CLI_PHP_MINOR', substr(\PHP_VERSION, 0, strpos(\PHP_VERSION, '.', 2)));
+
+if (\getenv('COMPOSER_PHAR') !== false) {
+ define('COMPOSER_PHAR', getenv('COMPOSER_PHAR'));
+} elseif (strpos(strtoupper(\PHP_OS), 'WIN') === 0) {
+ // Windows.
+ exec('where composer.phar', $output, $exitcode);
+ if ($exitcode === 0 && empty($output) === false) {
+ define('COMPOSER_PHAR', trim(implode('', $output)));
+ }
+} else {
+ exec('which composer.phar', $output, $exitcode);
+ if ($exitcode === 0 && empty($output) === false) {
+ define('COMPOSER_PHAR', trim(implode('', $output)));
+ }
+}
+
+if (defined('COMPOSER_PHAR') === false) {
+ echo 'Please add a 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);
+}
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" : "*"
+ }
+}