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
+
+[](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.
+