diff --git a/.gitignore b/.gitignore index 3a693c9..159aba5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,8 @@ +# Ignore composer deps. +/vendor/ +/composer.lock composer.phar -vendor/ -# Commit your application's lock file http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file -# You may choose to ignore a library lock file http://getcomposer.org/doc/02-libraries.md#lock-file -# composer.lock +# Ignore the folder we use when testing the installers. +/build/* +/tmp diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..60ed138 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,23 @@ +language: php +sudo: false +php: + - '5.6' +# - '5.5' +# - '5.4' +#matrix: +# fast_finish: true +env: + global: + # Contains a $GITHUB_TOKEN env var for use with composer to avoid API limits. + - secure: "hsmni1Mi9zCjYaGaCpRnXPYzdBywqPyhKQdrcS4cxJpCIPROd1Txtj8KKa2hdHp6f/xObd3zAqQeJgKDIzkj3ht0D1Za7BCuh+fY21F76k1u/SXIATgq7kk+vFg83EfcuojW4WI94FRhCfJ2bUOzZOpTzpsteo8vadyCJHHztXjcEnvmd9WrTI7OyeMTO/C51dq1yUJyJ1X/XdgwC2VrsUJLAIQzlvEenW7jQzmLp6F2T7b9sh7QrheSmqjX6A8SiN2PBe9YHQgpg0s9Rck3phiG+Th7L+Kpudc+M83a8izI4djyITevZc8l84dHYyonkh68jTNgCjOEz7gRmhuUUAXEaGLeZOJSiwbQKJ8juqPWA9eqTu4x8AR3b56ONb5TVATKNqpNSkzsYu43vtxqsaVsH8GLA3ic3KewRRM9awiLZyuZ1Npk5riD1UhgXv0CdR5geAQmgUh50PGwVcYNFm5/wz9hKaTBevbv1a1fXPzjQqB6aiV/qa7KQppSuYI3h1APOhxpFofQbKy+2plFTPjvWwpowJ3HjULuhSx5Nd7Gg4I8plCfo8yhI8VODOW4KYyh2617hBq2nMvEyS9/DgZ5Bix/mOuOoJMJ/fKnmXS49QLUMadcLSBMlKTUBgEuDtXuK3Q6RgYAY8KUMw6i5Dpu2mPAocoGR1T6ZaDr7Qs=" +branches: + except: + - gh-pages +install: + - composer self-update --no-interaction + - composer config -g github-oauth.github.com $GITHUB_TOKEN + - composer install --no-interaction +script: + - vendor/bin/phpunit +notifications: + email: false diff --git a/README.md b/README.md index 8344bee..376e6a9 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,147 @@ -# composer-plugins +# Loadsys Composer Plugins + +[![Build Status](https://travis-ci.org/loadsys/composer-plugins.svg?branch=master)](https://travis-ci.org/loadsys/composer-plugins) + Including this package in your composer.json makes a number of installers and project types available. + + + + +## PHP CodeSniffer, `phpcs-coding-standard` type (copying folders approach) + +When you develop your own Coding Standard for the [PHP CodeSniffer](https://github.com/squizlabs/PHP_CodeSniffer), you can package it for installation via Composer, but `phpcs` won't know about your standard unless you either manually specify it on the command line every time: + +```shell +$ vendor/bin/phpcs --standard=vendor/yourname/your-codesniffer/YourStandard . +``` + +Or you must use the `--config-set` switch to write your Standard's path into the `phpcs` config file: + +```shell +$ vendor/bin/phpcs --config-set installed_paths vendor/yourname/your-codesniffer +``` + +Neither of these is convenient, and defeats the purpose of using Composer to make your dependencies "automatically" available to your project. + +This plugin provide a custom installer that engages for the Composer `type` of `phpcs-coding-standard`. + +When you create your coding standard package, use this `type` in your composer.json file, and `require` this package of composer plugins in order to gain access to the Installer for that type: + +```json +{ + "name": "yourname/your-codesniffer", + "type": "phpcs-coding-standard", + "require": { + "squizlabs/php_codesniffer": "~2.3", + "loadsys/composer-plugins": "dev-master" + } +} +``` + +You must also make sure your coding standard lives in a subfolder of the package, usually with a proper name. For example: `GIT_ROOT/YourStandard/ruleset.xml`. This allows for multiple standards to be installed from a single package. + +It also usually makes sense for your coding standard to include the required version of `squizlabs/php_codesniffer` itself, so that your projects that use this standard don't have to require it themselves, or accidentally require the wrong version. + +With this setup, when your standard is included in another project, the installer in this package will search its `vendor/your-name/your-codesniffer/` folder for folders that contain `ruleset.xml` files, indicating that those sub-folders contain Coding Standards. This installer will then copy those folders into the `vendor/squizlabs/CodeSniffer/Standards/` folder for you, making your Coding Standard immediately available to `phpcs --standard YourStandard .` without any additional configuration. + + + + +## PHP CodeSniffer, post-install hook (copying folders approach) + +Sometimes you want to use somebody else's coding standard package where you can't set the `type` explicitly. In cases like this, this package provides composer hook scripts that can be used to accomplish the same effect. + +The script works by scanning each installed package for any immediate sub-folders containing a `ruleset.xml` file (which indicates that folder contains a Coding Standard.) It then copies those folders into the `CodeSniffer/Standards/` directory, making them available to `phpcs`. + +To use this hook script, add the following to your root project's `composer.json`: + +```json +{ + "require": { + "squizlabs/php_codesniffer": "~2.3", + "loadsys/composer-plugins": "dev-master" + }, + "scripts": { + "post-install-cmd": [ + "Loadsys\\Composer\\PhpCodesniffer\\CodingStandardHook::postInstall" + ], + "post-update-cmd": [ + "Loadsys\\Composer\\PhpCodesniffer\\CodingStandardHook::postInstall" + ], + "pre-package-uninstall": [ + "Loadsys\\Composer\\PhpCodesniffer\\CodingStandardHook::prePackageUninstall" + ] + } +} +``` + +The `postInstall` command checks every installed package for Coding Standard folders, and copies those folders into the `CodeSniffer/Standards/` folder directly. This should also be run post-update, in order to copy updates to a Coding Standard into the correct place for use. + +The `prePackageUninstall` removes any Coding Standard folders from a package that is being removed from the phpcs `CodeSniffer/Standards/` folder. + + + + +## PHP CodeSniffer, post-install hook (config editing approach) + +:warning: Speculative! :warning: + +@TODO: _This approach is partially coded already, but the plan is to eventually use an `extra` flag in the composer.json file to determine whether to copy standards folders or manage entries in phpcs's config file. This approach could be used both by the `phpcs-coding-standard` type as well as the postInstall hooks._ + + + +Sometimes you want to use somebody else's coding standard package where you can't set the `type` explicitly. In cases like this, this package provides composer hook scripts that can be used to accomplish the same effect. + +The script works by scanning each installed package for any folders containing a `ruleset.xml` file (which indicates that folder contains a Coding Standard.) It then adds the vendor paths for these folders into the `CodeSniffer.conf` file, making them available to `phpcs` in their natural install location. + +To use this hook script, add the following to your root project's `composer.json`: + +```json +{ + "require": { + "squizlabs/php_codesniffer": "~2.3", + "loadsys/composer-plugins": "dev-master" + }, + "scripts": { + "post-install-cmd": [ + "Loadsys\\Composer\\PhpCodesniffer\\CodingStandardHook::postInstall" + ], + "post-update-cmd": [ + "Loadsys\\Composer\\PhpCodesniffer\\CodingStandardHook::postInstall" + ], + "pre-package-uninstall": [ + "Loadsys\\Composer\\PhpCodesniffer\\CodingStandardHook::prePackageUninstall" + ] + } +} +``` + +The `postInstall` command checks every installed package for Coding Standard folders, and adds the path for any packages containing Standards into `phpcs`'s `installed_paths` config setting. This should also be run `post-update` in case the package's installed_path has changed. + +The `postPackageUninstall` removes the path for a package that is being removed from the phpcs config file. + + + + +## Contributing + +Create an issue or submit a pull request. + +### Running Unit Tests + +* `composer install` +* `vendor/bin/phpunit` + + + + +## License + +[MIT](https://github.com/loadsys/puphpet-release/blob/master/LICENSE). + + + + +## Copyright + +© 2015 [Loadsys Web Strategies](http://loadsys.com) diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..c12f401 --- /dev/null +++ b/composer.json @@ -0,0 +1,42 @@ +{ + "name": "loadsys/composer-plugins", + "description": "Provides composer custom installers and hook scripts.", + "keywords": [ + "composer", + "plugins", + "loadsys", + "codesniffer", + "puphpet", + "vagrant" + ], + "type": "composer-plugin", + "license": "MIT", + "authors": [ + { + "name": "Brian Porter", + "email": "beporter@users.sourceforge.net" + } + ], + "autoload": { + "psr-4": { + "Loadsys\\Composer\\": "src" + } + }, + "autoload-dev": { + "psr-4": { + "Loadsys\\Composer\\Test\\": "tests" + } + }, + "extra": { + "class": "Loadsys\\Composer\\LoadsysComposerPlugin" + }, + "require": { + "composer-plugin-api": "~1.0.0", + "symfony/filesystem": "~2.6" + }, + "require-dev": { + "composer/composer": "1.0.*@dev", + "phpunit/phpunit": "~4.8", + "squizlabs/php_codesniffer": "~2.0" + } +} diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..2009bc3 --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,27 @@ + + + + + tests/TestCase + + + + + + src + + + diff --git a/src/LoadsysComposerPlugin.php b/src/LoadsysComposerPlugin.php new file mode 100644 index 0000000..82d4361 --- /dev/null +++ b/src/LoadsysComposerPlugin.php @@ -0,0 +1,29 @@ +getInstallationManager()->addInstaller($phpcsCodingStandardInstaller); + } +} diff --git a/src/PhpCodesniffer/CodingStandardHook.php b/src/PhpCodesniffer/CodingStandardHook.php new file mode 100644 index 0000000..01531b0 --- /dev/null +++ b/src/PhpCodesniffer/CodingStandardHook.php @@ -0,0 +1,312 @@ +getComposer(); + $packages = $composer + ->getRepositoryManager() + ->getLocalRepository() + ->getPackages(); + + foreach ($packages as $package) { + // If the package defines the correct type, + // it will have already been handled by the Installer. + if ($package->getType() === self::PHPCS_PACKAGE_TYPE) { + return; + } + + // Otherwise, check for Coding Standard folders and copy them. + // (This is a relatively quick no-op if there are no + // `ruleset.xml` files in the package.) + $installPath = $composer->getInstallationManager()->getInstallPath($package); + self::mirrorCodingStandardFolders($composer, $installPath); + } + } + + /** + * Intended for use as a pre-package-uninstall script. + * + * Scans each package as it is installed for subfolders containing + * `ruleset.xml` files, and mirrors those folders into the + * CodeSniffer/Standards/ folder of the squizlabs/php_codesniffer + * package, if present. + * + * No-op if there are no `ruleset.xml` files or the PHP CodeSniffer + * package is not installed, making it safe to run on every package. + * + * @param \Composer\Installer\PackageEvent $event The composer Package event being fired. + * @return void + */ + public static function prePackageUninstall(PackageEvent $event) { + $package = $event->getOperation()->getPackage(); + + // If the package defines the correct type, it's coding standard + // folders will have already been removed by the Installer. + if ($package->getType() === self::PHPCS_PACKAGE_TYPE) { + return; + } + + // Otherwise, check for Coding Standard folders in the package + // about to be removed, and remove them from the + // CodeSniffer/Standards/ first. + $composer = $event->getComposer(); + $installPath = $composer->getInstallationManager()->getInstallPath($package); + self::deleteCodingStandardFolders($composer, $installPath); + } + + /** + * Mirror (copy or delete, only as necessary) items from the installed + * package's release/ folder into the target directory. + * + * @param \Composer\Composer $composer Active composer instance. + * @param string $packageBasePath The installation path for the Package being installed. + * @return void + */ + public static function mirrorCodingStandardFolders(Composer $composer, $packageBasePath) { + $rulesets = self::findRulesetFolders($packageBasePath); + $destDir = self::findStandardsFolder($composer); + + // No-op if no ruleset.xml's found or squizlabs/php_codesniffer not installed. + if (empty($rulesets) || !$destDir) { + return; + } + + // Return true if the first part of the subpath for + // the current file exists in the accept array. + $acceptFunc = function ($current, $key, $iterator) use ($rulesets) { + $pathComponents = explode(DS, $iterator->getSubPathname()); + return in_array($pathComponents[0], $rulesets); + }; + + // Build up an iterator that will only select files + // within folders containing `ruleset.xml files. + $dirIterator = new RecursiveDirectoryIterator( + $packageBasePath, + RecursiveDirectoryIterator::SKIP_DOTS + ); + $filterIterator = new RecursiveCallbackFilterIterator( + $dirIterator, + $acceptFunc + ); + $codingStandardsFolders = new RecursiveIteratorIterator( + $filterIterator, + RecursiveIteratorIterator::SELF_FIRST + ); + + // Iterate over all of the select files, + // copying them to the CodeSniffer/Standards/ folder. + $filesystem = new SymfonyFilesytem(); + $filesystem->mirror( + $packageBasePath, + $destDir, + $codingStandardsFolders, + ['override' => true] + ); + } + + /** + * Remove Coding Standards folders from phpcs. + * + * Check the to-be-removed package for Coding Standard folders, + * remove those folders from the CodeSniffer/Standards/ dir. + * + * @param \Composer\Composer $composer Active composer instance. + * @param string $packageBasePath The installation path for the Package about to be removed. + * @return void + */ + public static function deleteCodingStandardFolders(Composer $composer, $packageBasePath) { + $rulesets = self::findRulesetFolders($packageBasePath); + $destDir = self::findStandardsFolder($composer); + + // No-op if no ruleset.xml's found. + if (empty($rulesets) || !$destDir) { + return; + } + + $filesystem = new Filesystem(); + foreach ($rulesets as $ruleset) { + $filesystem->removeDirectory($destDir . DS . $ruleset); + } + } + + /** + * Scan $basePath for folders that contain `ruleset.xml` files. + * + * Return an array of partial paths (from $basePath) for all + * matching folders found. + * + * @param string $basePath A filesystem path without trailing slash to scan for folders with `ruleset.xml` files. + * @return array Array of partial file paths (from $basePath) to folders containing a ruleset.xml, no leading or trailing slashes. Empty array if none found. + */ + protected static function findRulesetFolders($basePath) { + $rulesetFolders = array_map(function ($v) use ($basePath) { + return dirname(str_replace($basePath . DS, '', $v)); + }, glob($basePath . DS . '*' . DS . 'ruleset.xml')); + + return $rulesetFolders; + } + + /** + * Attempt to locate the squizlabs/php_codesniffer/[...]/Standards folder. + * + * Return the full system path if found, no trailing slash. Attempts to + * be compatible with both PHP_CodeSniffer v2.x and v3.x. + * + * @param Composer\Composer $composer Used to get access to the InstallationManager. + * @return string|false Full system path to the PHP CodeSniffer's "Standards/" folder, false if not found. + */ + protected static function findStandardsFolder(Composer $composer) { + $phpcsPackage = new Package('squizlabs/php_codesniffer', '2.0', ''); + $base = $composer->getInstallationManager()->getInstallPath($phpcsPackage); + + $subPaths = [ + 'src' . DS . 'Standards', // PHPCS v3 + 'CodeSniffer' . DS . 'Standards', // v2 + ]; + + foreach ($subPaths as $subPath) { + $path = $base . DS . $subPath; + if (is_dir($path) && is_readable($path)) { + return $path; + } + } + + return false; + } + + +// @TODO: Break this stuff out into a separate Hook class? + + + /** + * Inject the provided filesystem path into phpcs's CodeSniffer.conf's [installed_paths] key. + * + * @param string $path The relative path to the coding standard folder to add. + * @return bool True if the path was successfully added to the config file, false on failure. + */ + public static function configInstalledPathAdd($path) { + //@TODO: test configInstalledPathAdd() + $installedPaths = self::readInstalledPaths(); + $installedPaths[] = $path; + return self::saveInstalledPaths($installedPaths); + } + + /** + * Remove the provided filesystem path into phpcs's CodeSniffer.conf's [installed_paths] key. + * + * @param string $path The relative path to the coding standard folder to remove. + * @return bool True if the path was successfully removed to the config file, false on failure. + */ + public static function configInstalledPathRemove($path) { + //@TODO: test configInstalledPathRemove() + $installedPaths = self::readInstalledPaths(); + $key = array_search($path, $installedPaths); + if ($key) { + unset($installedPaths[$key]); + } + + return self::saveInstalledPaths($installedPaths); + } + + /** + * Fetch the entire array of [installed_paths] from the phpcs config file. + * + * @return array Empty array if the config file load fails at all, otherwise the list of paths in the config. + */ + protected static function readInstalledPaths() { + //@TODO: test readInstalledPaths() + self::codeSnifferInit(); + $pathsString = PHP_CodeSniffer::getInstalledStandardPaths(); + if (is_null($pathsString)) { + return []; + } + + return explode(',', $pathsString); + } + + /** + * Write an array of paths back into phpcs's config file's [installed_paths]. + * + * @param array $paths Array of relative paths. Any duplicates will be stripped before saving. + * @return bool True if the config file was written successfully, false on failure. + */ + protected static function saveInstalledPaths(array $paths) { + //@TODO: test saveInstalledPaths() + return PHP_CodeSniffer::setConfigData('installed_paths', implode(',', array_unique($paths))); + } + + /** + * Ensures that the PHP_CodeSniffer class is available for static access. + * + * @return void + */ + protected static function codeSnifferInit() { + //@TODO: test codeSnifferInit() + if (!class_exists('PHP_CodeSniffer')) { + $composerInstall = dirname(dirname(dirname(__FILE__))) . '/vendor/squizlabs/php_codesniffer/CodeSniffer.php'; + if (file_exists($composerInstall)) { + require_once $composerInstall; + } else { + require_once 'PHP/CodeSniffer.php'; + } + } + } +} diff --git a/src/PhpCodesniffer/CodingStandardInstaller.php b/src/PhpCodesniffer/CodingStandardInstaller.php new file mode 100644 index 0000000..7881c73 --- /dev/null +++ b/src/PhpCodesniffer/CodingStandardInstaller.php @@ -0,0 +1,107 @@ +hook = (!is_null($hook) ? $hook : new CodingStandardHook()); + } + + /** + * Defines the `type`s of composer packages to which this installer applies. + * + * A project's composer.json file must specify + * `"type": "phpcs-coding-standard"` in order to trigger this + * installer. + * + * @param string $packageType The `type` specified in the consuming project's composer.json. + * @return bool True if this installer should be activated for the package in question, false if not. + */ + public function supports($packageType) { + return ($packageType === CodingStandardHook::PHPCS_PACKAGE_TYPE); + } + + /** + * Override LibraryInstaller::installCode() to hook in additional post-download steps. + * + * @param \Composer\Package\PackageInterface $package Package instance. + * @return void + */ + protected function installCode(PackageInterface $package) { + parent::installCode($package); + + if (!$this->supports($package->getType())) { + return; + } + + $installPath = $this->composer->getInstallationManager()->getInstallPath($package); + $this->hook->mirrorCodingStandardFolders($this->composer, $installPath); + } + + /** + * Override LibraryInstaller::updateCode() to hook in additional post-update steps. + * + * @param \Composer\Package\PackageInterface $initial Existing Package instance. + * @param \Composer\Package\PackageInterface $target New Package instance. + * @return void + */ + protected function updateCode(PackageInterface $initial, PackageInterface $target) { + parent::updateCode($initial, $target); + + if (!$this->supports($package->getType())) { + return; + } + + $installPath = $this->composer->getInstallationManager()->getInstallPath($target); + $this->hook->mirrorCodingStandardFolders($this->composer, $installPath); + } + + /** + * Override LibraryInstaller::removeCode() to hook in additional post-update steps. + * + * @param \Composer\Package\PackageInterface $package Package instance. + * @return void + */ + protected function removeCode(PackageInterface $package) { + if ($this->supports($package->getType())) { + $installPath = $this->composer->getInstallationManager()->getInstallPath($package); + $this->hook->deleteCodingStandardFolders($this->composer, $installPath); + } + + parent::removeCode($package); + } +} diff --git a/tests/TestCase/LoadsysComposerPluginTest.php b/tests/TestCase/LoadsysComposerPluginTest.php new file mode 100644 index 0000000..f5bc0e3 --- /dev/null +++ b/tests/TestCase/LoadsysComposerPluginTest.php @@ -0,0 +1,72 @@ +package = new Package('CamelCased', '1.0', '1.0'); + $this->io = $this->getMock('Composer\IO\IOInterface'); + $this->composer = new Composer(); + $this->plugin = new LoadsysComposerPlugin(); + } + + /** + * tearDown + * + * @return void + */ + public function tearDown() { + unset($this->package); + unset($this->io); + unset($this->composer); + unset($this->plugin); + + parent::tearDown(); + } + + /** + * All we can do is confirm that the plugin tried to register the + * correct installer class during ::activate(). + * + * @return void + */ + public function testActivate() { + $this->composer = $this->getMock('Composer\Composer', [ + 'getInstallationManager', + 'addInstaller' + ]); + $this->composer->setConfig(new Config(false)); + + $this->composer->expects($this->any()) + ->method('getInstallationManager') + ->will($this->returnSelf()); + + $this->composer->expects($this->at(1)) + ->method('addInstaller') + ->with($this->isInstanceOf('Loadsys\Composer\PhpCodesniffer\CodingStandardInstaller')); + + $this->plugin->activate($this->composer, $this->io); + } +} diff --git a/tests/TestCase/PhpCodesniffer/CodingStandardHookTest.php b/tests/TestCase/PhpCodesniffer/CodingStandardHookTest.php new file mode 100644 index 0000000..8c0465e --- /dev/null +++ b/tests/TestCase/PhpCodesniffer/CodingStandardHookTest.php @@ -0,0 +1,293 @@ +baseDir = getcwd(); + $this->phpcsInstallDir = sys_get_temp_dir() . md5(__FILE__ . time()); + $this->standardsInstallDir = $this->phpcsInstallDir . '/CodeSniffer/Standards'; + mkdir($this->standardsInstallDir, 0777, true); + + $this->sampleDir = $this->baseDir . '/tests/samples'; + + $this->package = new \Composer\Package\Package('CamelCased', '1.0', '1.0'); + $this->io = $this->getMock('\Composer\IO\IOInterface'); + $this->composer = new \Composer\Composer(); + $this->composer->setConfig(new \Composer\Config(false, $this->baseDir)); + $this->composer->setInstallationManager(new \Composer\Installer\InstallationManager()); + } + + /** + * tearDown + * + * @return void + */ + public function tearDown() { + unset($this->package); + unset($this->io); + unset($this->composer); + $this->removeDir($this->phpcsInstallDir); + + parent::tearDown(); + } + + /** + * Helper method to recursively delete temporary directories created for tests. + * + * @return void + */ + protected function removeDir($d) { + $i = new \RecursiveIteratorIterator( + new \RecursiveDirectoryIterator($d, \FilesystemIterator::SKIP_DOTS), + \RecursiveIteratorIterator::CHILD_FIRST + ); + foreach ($i as $path) { + $path->isDir() && !$path->isLink() ? rmdir($path->getPathname()) : unlink($path->getPathname()); + } + + rmdir($d); + } + + /** + * Helper method to set up the proper paths for the + * CodeSniffer/Standards/ and currently-being-installed-package + * directories. + * + * The returned $composer mock will return each path for the + * matching method call to the mock. Remember that PHPUnit counts + * ALL mocked methods in sequence! + * + * @param array $installedPaths Numeric keys are the `at()` calls to `getInstallPath` where the matching string value is returned as the path. + * @return array [mocked \Composer\Composer, mocked \Composer\Installer\PackageEvent] + */ + protected function mockComposerAndEvent($getInstallPaths) { + $composer = $this->getMock('\Composer\Composer', ['getInstallationManager', 'getInstallPath']); + $composer->method('getInstallationManager')->will($this->returnSelf()); + foreach ($getInstallPaths as $at => $path) { + $composer->expects($this->at($at)) + ->method('getInstallPath') + ->willReturn($path); + } + + $event = $this->getMockBuilder('\Composer\Script\Event') + ->disableOriginalConstructor() + ->setMethods(['getComposer']) + ->getMock(); + $event->method('getComposer')->willReturn($composer); + + return [$composer, $event]; + } + /** + * test postPackageInstall() + * + * @return void + */ + public function testPostInstallMatchingType() { + $this->marktestIncomplete('@TODO: Write a test where the package type is already phpcs-coding-standard'); + } + + /** + * test postPackageInstall() + * + * @return void + */ + public function testPostInstallNoStandards() { + $this->marktestIncomplete('@TODO: Write a test where the package does not have any standards to install.'); + } + + /** + * test postPackageInstall() + * + * @return void + */ + public function testPostInstallSuccessful() { + $this->marktestIncomplete('@TODO: Write a test where the package type is not phpcs-coding-standard and has standards to install.'); + } + + /** + * test mirrorCodingStandardFolders() + * + * @return void + */ + public function testMirrorCodingStandardFoldersSuccessful() { + list($composer, $event) = $this->mockComposerAndEvent([ + 1 => $this->phpcsInstallDir, + ]); + + $expected = [ + 'CodingStandardOne', + 'SecondStandard', + ]; + + $result = TestCodingStandardHook::mirrorCodingStandardFolders($composer, $this->sampleDir); + + foreach ($expected as $standard) { + $this->assertTrue( + is_readable($this->standardsInstallDir . DS . $standard . DS . 'ruleset.xml'), + "Folder `$standard` containing ruleset.xml should be copied to Standards/ folder." + ); + } + } + + /** + * test mirrorCodingStandardFolders() + * + * @return void + */ + public function testMirrorCodingStandardFoldersNoDest() { + list($composer, $event) = $this->mockComposerAndEvent([ + 1 => false, + ]); + + $expected = [ + 'CodingStandardOne', + 'SecondStandard', + ]; + + $result = TestCodingStandardHook::mirrorCodingStandardFolders($composer, $this->sampleDir); + + foreach ($expected as $standard) { + $this->assertFalse( + is_readable($this->standardsInstallDir . DS . $standard . DS . 'ruleset.xml'), + 'No folders should be copied when destination dir is not found.' + ); + } + } + + /** + * test findRulesetFolders() + * + * @return void + */ + public function testFindRulesetFolders() { + $expected = [ + 'CodingStandardOne', + 'SecondStandard', + ]; + $result = TestCodingStandardHook::findRulesetFolders($this->sampleDir); + + $this->assertEquals( + $expected, + $result, + 'Only folders containing a ruleset.xml should be returned.' + ); + } + + /** + * test findStandardsFolder() + * + * @return void + */ + public function testFindStandardsFolderExists() { + list($composer, $event) = $this->mockComposerAndEvent([ + 1 => $this->phpcsInstallDir, + ]); + + $result = TestCodingStandardHook::findStandardsFolder($composer); + + $this->assertEquals( + $this->standardsInstallDir, + $result, + 'Full path to the existing Standards/ folder should be returned.' + ); + } + + /** + * test findStandardsFolder() + * + * @return void + */ + public function testFindStandardsFolderDoesNotExist() { + list($composer, $package) = $this->mockComposerAndEvent([ + 1 => 'does-not-exist', + ]); + + $result = TestCodingStandardHook::findStandardsFolder($composer); + + $this->assertFalse( + $result, + 'False should be returned for a non-existent path.' + ); + } + + /** + * test configInstalledPathAdd() + * + * @return void + */ + public function testConfigInstalledPathAdd() { + $this->marktestIncomplete('@TODO: Write tests for configInstalledPathAdd()'); + } + + /** + * test configInstalledPathRemove() + * + * @return void + */ + public function testConfigInstalledPathRemove() { + $this->marktestIncomplete('@TODO: Write tests for configInstalledPathRemove()'); + } + + /** + * test readInstalledPaths() + * + * @return void + */ + public function testReadInstalledPaths() { + $this->marktestIncomplete('@TODO: Write tests for readInstalledPaths()'); + } + + /** + * test saveInstalledPaths() + * + * @return void + */ + public function testSaveInstalledPaths() { + $this->marktestIncomplete('@TODO: Write tests for saveInstalledPaths()'); + } + + /** + * test codeSnifferInit() + * + * @return void + */ + public function testcodeSnifferInit() { + $this->marktestIncomplete('@TODO: Write tests for codeSnifferInit()'); + } +} diff --git a/tests/TestCase/PhpCodesniffer/CodingStandardInstallerTest.php b/tests/TestCase/PhpCodesniffer/CodingStandardInstallerTest.php new file mode 100644 index 0000000..a336897 --- /dev/null +++ b/tests/TestCase/PhpCodesniffer/CodingStandardInstallerTest.php @@ -0,0 +1,192 @@ +calls[$name] = $arguments; + return $name; + } +} + +/** + * Expose protected methods for direct testing, since this class doesn't + * otherwise expose public interfaces to us. + */ +class TestCodingStandardInstaller extends CodingStandardInstaller { + public function installCode(PackageInterface $package) { + return parent::installCode($package); + } + public function updateCode(PackageInterface $initial, PackageInterface $target) { + return parent::updateCode($initial, $target); + } + public function removeCode(PackageInterface $package) { + return parent::removeCode($package); + } +} + +/** + * PhpCodesniffer\CodingStandardInstaller Test + */ +class CodingStandardInstallerTest extends \PHPUnit_Framework_TestCase { + private $package; + private $composer; + private $io; + + /** + * setUp + * + * @return void + */ + public function setUp() { + parent::setUp(); + + $this->baseDir = getcwd(); + $this->io = $this->getMock('\Composer\IO\IOInterface'); + $this->package = $this->getMock( + '\Composer\Package\Package', + ['getType'], + ['CamelCased', '1.0', '1.0'] + ); + $downloadManager = $this->getMock( + '\Composer\Downloader\DownloadManager', + [], + [$this->io] + ); + $installationManager = $this->getMock( + '\Composer\Installer\InstallationManager', + [], + [] + ); + $this->hook = new StubCodingStandardHook(); + $this->composer = new \Composer\Composer(); + $this->composer->setConfig( + new \Composer\Config(false, $this->baseDir) + ); + $this->composer->setInstallationManager($installationManager); + $this->composer->setDownloadManager($downloadManager); + $this->Installer = new TestCodingStandardInstaller( + $this->io, + $this->composer, + 'library', + new \Composer\Util\Filesystem(), + $this->hook + ); + } + + /** + * tearDown + * + * @return void + */ + public function tearDown() { + unset($this->package); + unset($this->io); + unset($this->composer); + unset($this->hook); + unset($this->Installer); + + parent::tearDown(); + } + + /** + * test supports() + * + * @return void + */ + public function testSupports() { + $this->assertTrue( + $this->Installer->supports('phpcs-coding-standard'), + 'Coding standard installer should activate for `type=phpcs-coding-standard`.' + ); + $this->assertFalse( + $this->Installer->supports('anything-else'), + 'Coding standard installer should not activate for unrecognized package types.' + ); + } + + /** + * test installCode() + * + * @return void + */ + public function testInstallCodeCorrectType() { + $this->package->expects($this->any()) + ->method('getType') + ->willReturn(CodingStandardHook::PHPCS_PACKAGE_TYPE); + + $result = $this->Installer->installCode($this->package); + + $this->assertEquals( + null, + $result, + 'Return value should always be null.' + ); + $this->assertEquals( + ['mirrorCodingStandardFolders' => [$this->composer, null]], + $this->Installer->hook->calls, + 'Our mocked static class should have registered a single call to mirrorCodingStandardFolders().' + ); + } + + /** + * test installCode() + * + * @return void + */ + public function testInstallCodeIncorrectType() { + $this->package->expects($this->any()) + ->method('getType') + ->willReturn('not-the-correct-type'); + + $result = $this->Installer->installCode($this->package); + + $this->assertEquals( + null, + $result, + 'Return value should always be null.' + ); + $this->assertEquals( + [], + $this->Installer->hook->calls, + 'There should be no call to our stubbed static class.' + ); + } + + /** + * test updateCode() + * + * @return void + */ + public function testUpdateCode() { + $this->markTestIncomplete('@TODO: Write updateCode() tests.'); + } + + /** + * test removeCode() + * + * @return void + */ + public function testRemoveCode() { + $this->markTestIncomplete('@TODO: Write removeCode() tests.'); + } +} diff --git a/tests/bootstrap.php b/tests/bootstrap.php new file mode 100644 index 0000000..31d0f65 --- /dev/null +++ b/tests/bootstrap.php @@ -0,0 +1,3 @@ + + This is a dummy ruleset.xml file that identifies the folder it + lives in as being a PHP Codesniffer "coding standard". + diff --git a/tests/samples/NotACodingStandard/README.md b/tests/samples/NotACodingStandard/README.md new file mode 100644 index 0000000..60b4edb --- /dev/null +++ b/tests/samples/NotACodingStandard/README.md @@ -0,0 +1,5 @@ +# Not A Coding Standard + +This folder does **not** contain a `ruleset.xml` file, and should +therefore be excluded from the list of folders to install into +the `CodeSniffer/Standards/` folder. diff --git a/tests/samples/SecondStandard/ruleset.xml b/tests/samples/SecondStandard/ruleset.xml new file mode 100644 index 0000000..70a9e67 --- /dev/null +++ b/tests/samples/SecondStandard/ruleset.xml @@ -0,0 +1,3 @@ + + This folder contains a ruleset.xml file, and should be included. +