Преглед изворни кода

Merge branch 'master' of https://github.com/composer/composer into sort-suggestions

Niels Keurentjes пре 9 година
родитељ
комит
990c07e4db
70 измењених фајлова са 692 додато и 350 уклоњено
  1. 1 1
      CHANGELOG.md
  2. 2 1
      composer.json
  3. 1 1
      composer.lock
  4. 1 1
      doc/00-intro.md
  5. 1 1
      doc/03-cli.md
  6. 18 11
      src/Composer/Autoload/ClassMapGenerator.php
  7. 4 12
      src/Composer/Cache.php
  8. 3 2
      src/Composer/Command/ConfigCommand.php
  9. 13 0
      src/Composer/Command/DiagnoseCommand.php
  10. 2 1
      src/Composer/Command/HomeCommand.php
  11. 0 7
      src/Composer/Command/SelfUpdateCommand.php
  12. 11 10
      src/Composer/Command/ShowCommand.php
  13. 8 9
      src/Composer/Console/Application.php
  14. 23 6
      src/Composer/DependencyResolver/Rule.php
  15. 1 1
      src/Composer/DependencyResolver/SolverProblemsException.php
  16. 2 3
      src/Composer/Downloader/ArchiveDownloader.php
  17. 1 3
      src/Composer/Downloader/FileDownloader.php
  18. 3 2
      src/Composer/Downloader/GitDownloader.php
  19. 21 8
      src/Composer/Downloader/GzipDownloader.php
  20. 3 2
      src/Composer/Downloader/RarDownloader.php
  21. 3 2
      src/Composer/Downloader/ZipDownloader.php
  22. 1 3
      src/Composer/EventDispatcher/EventDispatcher.php
  23. 22 12
      src/Composer/Factory.php
  24. 11 36
      src/Composer/IO/BaseIO.php
  25. 1 1
      src/Composer/IO/BufferIO.php
  26. 32 17
      src/Composer/IO/ConsoleIO.php
  27. 24 14
      src/Composer/IO/IOInterface.php
  28. 4 4
      src/Composer/IO/NullIO.php
  29. 5 8
      src/Composer/Installer.php
  30. 2 1
      src/Composer/Installer/LibraryInstaller.php
  31. 3 4
      src/Composer/Installer/PearInstaller.php
  32. 65 0
      src/Composer/Package/Archiver/ZipArchiver.php
  33. 5 0
      src/Composer/Package/Loader/RootPackageLoader.php
  34. 1 3
      src/Composer/Plugin/PluginManager.php
  35. 3 7
      src/Composer/Repository/ArtifactRepository.php
  36. 2 3
      src/Composer/Repository/PathRepository.php
  37. 1 3
      src/Composer/Repository/PearRepository.php
  38. 23 3
      src/Composer/Repository/PlatformRepository.php
  39. 1 3
      src/Composer/Repository/Vcs/GitBitbucketDriver.php
  40. 1 3
      src/Composer/Repository/Vcs/GitHubDriver.php
  41. 1 3
      src/Composer/Repository/Vcs/GitLabDriver.php
  42. 1 3
      src/Composer/Repository/Vcs/HgBitbucketDriver.php
  43. 8 8
      src/Composer/Util/Filesystem.php
  44. 1 4
      src/Composer/Util/Perforce.php
  45. 28 0
      src/Composer/Util/Platform.php
  46. 1 1
      src/Composer/Util/ProcessExecutor.php
  47. 8 16
      src/Composer/Util/RemoteFilesystem.php
  48. 1 1
      src/Composer/Util/TlsHelper.php
  49. 9 0
      tests/Composer/Test/ApplicationTest.php
  50. 1 1
      tests/Composer/Test/Autoload/ClassMapGeneratorTest.php
  51. 1 1
      tests/Composer/Test/DependencyResolver/SolverTest.php
  52. 2 1
      tests/Composer/Test/Downloader/GitDownloaderTest.php
  53. 2 5
      tests/Composer/Test/Downloader/HgDownloaderTest.php
  54. 2 1
      tests/Composer/Test/Downloader/XzDownloaderTest.php
  55. 15 42
      tests/Composer/Test/EventDispatcher/EventDispatcherTest.php
  56. 4 4
      tests/Composer/Test/Fixtures/installer/abandoned-listed.test
  57. 4 4
      tests/Composer/Test/Fixtures/installer/broken-deps-do-not-replace.test
  58. 45 0
      tests/Composer/Test/Fixtures/installer/github-issues-4319.test
  59. 47 0
      tests/Composer/Test/Fixtures/installer/github-issues-4795.test
  60. 16 0
      tests/Composer/Test/Fixtures/installer/install-self-from-root.test
  61. 4 4
      tests/Composer/Test/Fixtures/installer/suggest-installed.test
  62. 4 4
      tests/Composer/Test/Fixtures/installer/suggest-prod.test
  63. 4 4
      tests/Composer/Test/Fixtures/installer/suggest-replaced.test
  64. 4 4
      tests/Composer/Test/Fixtures/installer/suggest-uninstalled.test
  65. 20 7
      tests/Composer/Test/IO/ConsoleIOTest.php
  66. 33 17
      tests/Composer/Test/InstallerTest.php
  67. 64 0
      tests/Composer/Test/Package/Archiver/ZipArchiverTest.php
  68. 2 1
      tests/Composer/Test/Repository/Vcs/SvnDriverTest.php
  69. 29 0
      tests/Composer/Test/Util/PlatformTest.php
  70. 2 5
      tests/Composer/Test/Util/SvnTest.php

+ 1 - 1
CHANGELOG.md

@@ -12,7 +12,7 @@
   * Added --strict to the `validate` command to treat any warning as an error that then returns a non-zero exit code
   * Added a dependency on composer/semver, which is the externalized lib for all the version constraints parsing and handling
   * Added support for classmap autoloading to load plugin classes and script handlers
-  * Added `bin-compat` config option that if set to `full` will create .bat proxy for binaries even if Compoesr runs in a linux VM
+  * Added `bin-compat` config option that if set to `full` will create .bat proxy for binaries even if Composer runs in a linux VM
   * Added SPDX 2.0 support, and externalized that in a composer/spdx-licenses lib
   * Added warnings when the classmap autoloader finds duplicate classes
   * Added --file to the `archive` command to choose the filename

+ 2 - 1
composer.json

@@ -45,7 +45,8 @@
         }
     },
     "suggest": {
-        "ext-zip": "Enabling the zip extension allows you to unzip archives, and allows gzip compression of all internet traffic",
+        "ext-zip": "Enabling the zip extension allows you to unzip archives",
+        "ext-zlib": "Allow gzip compression of HTTP requests",
         "ext-openssl": "Enabling the openssl extension allows you to access https URLs for repositories and packages"
     },
     "autoload": {

+ 1 - 1
composer.lock

@@ -4,7 +4,7 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
         "This file is @generated automatically"
     ],
-    "hash": "fdf4b487fa59607376721ebec4ff4783",
+    "hash": "31b3c13c89f8d6c810637ca1fe8fc6ae",
     "content-hash": "454148e20b837d9755dee7862f9c7a5d",
     "packages": [
         {

+ 1 - 1
doc/00-intro.md

@@ -109,7 +109,7 @@ mv composer.phar /usr/local/bin/composer
 A quick copy-paste version including sudo:
 
 ```sh
-curl -sS https://getcomposer.org/installer | sudo php -- --install-dir=/usr/local/bin --filename=composer
+curl -sS https://getcomposer.org/installer | sudo -H php -- --install-dir=/usr/local/bin --filename=composer
 ```
 
 > **Note:** On some versions of OSX the `/usr` directory does not exist by

+ 1 - 1
doc/03-cli.md

@@ -413,7 +413,7 @@ If you have installed Composer for your entire system (see [global installation]
 you may have to run the command with `root` privileges
 
 ```sh
-sudo composer self-update
+sudo -H composer self-update
 ```
 
 ### Options

+ 18 - 11
src/Composer/Autoload/ClassMapGenerator.php

@@ -122,18 +122,25 @@ class ClassMapGenerator
             $extraTypes .= '|enum';
         }
 
-        try {
-            $contents = Silencer::call('php_strip_whitespace', $path);
-            if (!$contents) {
-                if (!file_exists($path)) {
-                    throw new \Exception('File does not exist');
-                }
-                if (!is_readable($path)) {
-                    throw new \Exception('File is not readable');
-                }
+        // Use @ here instead of Silencer to actively suppress 'unhelpful' output
+        // @link https://github.com/composer/composer/pull/4886
+        $contents = @php_strip_whitespace($path);
+        if (!$contents) {
+            if (!file_exists($path)) {
+                $message = 'File at "%s" does not exist, check your classmap definitions';
+            } elseif (!is_readable($path)) {
+                $message = 'File at "%s" is not readable, check its permissions';
+            } elseif ('' === trim(file_get_contents($path))) {
+                // The input file was really empty and thus contains no classes
+                return array();
+            } else {
+                $message = 'File at "%s" could not be parsed as PHP, it may be binary or corrupted';
+            }
+            $error = error_get_last();
+            if (isset($error['message'])) {
+                $message .= PHP_EOL . 'The following message may be helpful:' . PHP_EOL . $error['message'];
             }
-        } catch (\Exception $e) {
-            throw new \RuntimeException('Could not scan for classes inside '.$path.": \n".$e->getMessage(), 0, $e);
+            throw new \RuntimeException(sprintf($message, $path));
         }
 
         // return early if there is no chance of matching anything in this file

+ 4 - 12
src/Composer/Cache.php

@@ -67,9 +67,7 @@ class Cache
     {
         $file = preg_replace('{[^'.$this->whitelist.']}i', '-', $file);
         if ($this->enabled && file_exists($this->root . $file)) {
-            if ($this->io->isDebug()) {
-                $this->io->writeError('Reading '.$this->root . $file.' from cache');
-            }
+            $this->io->writeError('Reading '.$this->root . $file.' from cache', true, IOInterface::DEBUG);
 
             return file_get_contents($this->root . $file);
         }
@@ -82,16 +80,12 @@ class Cache
         if ($this->enabled) {
             $file = preg_replace('{[^'.$this->whitelist.']}i', '-', $file);
 
-            if ($this->io->isDebug()) {
-                $this->io->writeError('Writing '.$this->root . $file.' into cache');
-            }
+            $this->io->writeError('Writing '.$this->root . $file.' into cache', true, IOInterface::DEBUG);
 
             try {
                 return file_put_contents($this->root . $file, $contents);
             } catch (\ErrorException $e) {
-                if ($this->io->isDebug()) {
-                    $this->io->writeError('<warning>Failed to write into cache: '.$e->getMessage().'</warning>');
-                }
+                $this->io->writeError('<warning>Failed to write into cache: '.$e->getMessage().'</warning>', true, IOInterface::DEBUG);
                 if (preg_match('{^file_put_contents\(\): Only ([0-9]+) of ([0-9]+) bytes written}', $e->getMessage(), $m)) {
                     // Remove partial file.
                     unlink($this->root . $file);
@@ -152,9 +146,7 @@ class Cache
                 touch($this->root . $file);
             }
 
-            if ($this->io->isDebug()) {
-                $this->io->writeError('Reading '.$this->root . $file.' from cache');
-            }
+            $this->io->writeError('Reading '.$this->root . $file.' from cache', true, IOInterface::DEBUG);
 
             return copy($this->root . $file, $target);
         }

+ 3 - 2
src/Composer/Command/ConfigCommand.php

@@ -12,6 +12,7 @@
 
 namespace Composer\Command;
 
+use Composer\Util\Platform;
 use Composer\Util\Silencer;
 use Symfony\Component\Console\Input\InputInterface;
 use Symfony\Component\Console\Input\InputArgument;
@@ -184,7 +185,7 @@ EOT
         if ($input->getOption('editor')) {
             $editor = escapeshellcmd(getenv('EDITOR'));
             if (!$editor) {
-                if (defined('PHP_WINDOWS_VERSION_BUILD')) {
+                if (Platform::isWindows()) {
                     $editor = 'notepad';
                 } else {
                     foreach (array('vim', 'vi', 'nano', 'pico', 'ed') as $candidate) {
@@ -197,7 +198,7 @@ EOT
             }
 
             $file = $input->getOption('auth') ? $this->authConfigFile->getPath() : $this->configFile->getPath();
-            system($editor . ' ' . $file . (defined('PHP_WINDOWS_VERSION_BUILD') ? '' : ' > `tty`'));
+            system($editor . ' ' . $file . (Platform::isWindows() ? '' : ' > `tty`'));
 
             return 0;
         }

+ 13 - 0
src/Composer/Command/DiagnoseCommand.php

@@ -453,6 +453,10 @@ EOT
             $errors['openssl'] = true;
         }
 
+        if (extension_loaded('openssl') && OPENSSL_VERSION_NUMBER < 0x1000100f) {
+            $warnings['openssl_version'] = true;
+        }
+
         if (!defined('HHVM_VERSION') && !extension_loaded('apcu') && ini_get('apc.enable_cli')) {
             $warnings['apc_cli'] = true;
         }
@@ -570,6 +574,15 @@ EOT
                         $text .= " Composer works with 5.3.2+ for most people, but there might be edge case issues.";
                         break;
 
+                    case 'openssl_version':
+                        // Attempt to parse version number out, fallback to whole string value.
+                        $opensslVersion = strstr(trim(strstr(OPENSSL_VERSION_TEXT, ' ')), ' ', true);
+                        $opensslVersion = $opensslVersion ?: OPENSSL_VERSION_TEXT;
+
+                        $text = "The OpenSSL library ({$opensslVersion}) used by PHP does not support TLSv1.2 or TLSv1.1.".PHP_EOL;
+                        $text .= "If possible you should upgrade OpenSSL to version 1.0.1 or above.";
+                        break;
+
                     case 'xdebug_loaded':
                         $text  = "The xdebug extension is loaded, this can slow down Composer a little.".PHP_EOL;
                         $text .= " Disabling it when using Composer is recommended.";

+ 2 - 1
src/Composer/Command/HomeCommand.php

@@ -16,6 +16,7 @@ use Composer\Factory;
 use Composer\Package\CompletePackageInterface;
 use Composer\Repository\RepositoryInterface;
 use Composer\Repository\ArrayRepository;
+use Composer\Util\Platform;
 use Composer\Util\ProcessExecutor;
 use Symfony\Component\Console\Input\InputArgument;
 use Symfony\Component\Console\Input\InputOption;
@@ -117,7 +118,7 @@ EOT
     {
         $url = ProcessExecutor::escape($url);
 
-        if (defined('PHP_WINDOWS_VERSION_MAJOR')) {
+        if (Platform::isWindows()) {
             return passthru('start "web" explorer "' . $url . '"');
         }
 

+ 0 - 7
src/Composer/Command/SelfUpdateCommand.php

@@ -88,9 +88,6 @@ EOT
         if (!is_writable($tmpDir)) {
             throw new FilesystemException('Composer update failed: the "'.$tmpDir.'" directory used to download the temp file could not be written');
         }
-        if (!is_writable($localFilename)) {
-            throw new FilesystemException('Composer update failed: the "'.$localFilename.'" file could not be written');
-        }
 
         if ($input->getOption('rollback')) {
             return $this->rollback($output, $rollbackDir, $localFilename);
@@ -271,10 +268,6 @@ TAGSPUBKEY
             throw new \UnexpectedValueException('Composer rollback failed: no installation to roll back to in "'.$rollbackDir.'"');
         }
 
-        if (!is_writable($rollbackDir)) {
-            throw new FilesystemException('Composer rollback failed: the "'.$rollbackDir.'" dir could not be written to');
-        }
-
         $old = $rollbackDir . '/' . $rollbackVersion . self::OLD_INSTALL_EXT;
 
         if (!is_file($old)) {

+ 11 - 10
src/Composer/Command/ShowCommand.php

@@ -20,6 +20,7 @@ use Composer\Semver\VersionParser;
 use Composer\Plugin\CommandEvent;
 use Composer\Plugin\PluginEvents;
 use Composer\Package\PackageInterface;
+use Composer\Util\Platform;
 use Symfony\Component\Console\Formatter\OutputFormatterStyle;
 use Symfony\Component\Console\Input\InputInterface;
 use Symfony\Component\Console\Input\InputArgument;
@@ -232,7 +233,7 @@ EOT
                     // outside of a real terminal, use space without a limit
                     $width = PHP_INT_MAX;
                 }
-                if (defined('PHP_WINDOWS_VERSION_BUILD')) {
+                if (Platform::isWindows()) {
                     $width--;
                 }
 
@@ -246,10 +247,10 @@ EOT
                 $writeDescription = !$input->getOption('name-only') && !$input->getOption('path') && ($nameLength + ($showVersion ? $versionLength : 0) + 24 <= $width);
                 foreach ($packages[$type] as $package) {
                     if (is_object($package)) {
-                        $output->write($indent . str_pad($package->getPrettyName(), $nameLength, ' '), false);
+                        $io->write($indent . str_pad($package->getPrettyName(), $nameLength, ' '), false);
 
                         if ($writeVersion) {
-                            $output->write(' ' . str_pad($package->getFullPrettyVersion(), $versionLength, ' '), false);
+                            $io->write(' ' . str_pad($package->getFullPrettyVersion(), $versionLength, ' '), false);
                         }
 
                         if ($writeDescription) {
@@ -258,15 +259,15 @@ EOT
                             if (strlen($description) > $remaining) {
                                 $description = substr($description, 0, $remaining - 3) . '...';
                             }
-                            $output->write(' ' . $description);
+                            $io->write(' ' . $description, false);
                         }
 
                         if ($writePath) {
                             $path = strtok(realpath($composer->getInstallationManager()->getInstallPath($package)), "\r\n");
-                            $output->write(' ' . $path);
+                            $io->write(' ' . $path, false);
                         }
                     } else {
-                        $output->write($indent . $package);
+                        $io->write($indent . $package, false);
                     }
                     $io->write('');
                 }
@@ -489,10 +490,10 @@ EOT
         $packagesInTree = array();
         $packagesInTree[] = $package;
 
-        $output->write(sprintf('<info>%s</info>', $package->getPrettyName()));
-        $output->write(' ' . $package->getPrettyVersion());
-        $output->write(' ' . strtok($package->getDescription(), "\r\n"));
-        $output->writeln('');
+        $io = $this->getIO();
+        $io->write(sprintf('<info>%s</info>', $package->getPrettyName()), false);
+        $io->write(' ' . $package->getPrettyVersion(), false);
+        $io->write(' ' . strtok($package->getDescription(), "\r\n"));
 
         if (is_object($package)) {
             $requires = $package->getRequires();

+ 8 - 9
src/Composer/Console/Application.php

@@ -12,6 +12,7 @@
 
 namespace Composer\Console;
 
+use Composer\Util\Platform;
 use Composer\Util\Silencer;
 use Symfony\Component\Console\Application as BaseApplication;
 use Symfony\Component\Console\Input\InputInterface;
@@ -136,9 +137,7 @@ class Application extends BaseApplication
             if ($newWorkDir = $this->getNewWorkingDir($input)) {
                 $oldWorkingDir = getcwd();
                 chdir($newWorkDir);
-                if ($io->isDebug() >= 4) {
-                    $io->writeError('Changed CWD to ' . getcwd());
-                }
+                $io->writeError('Changed CWD to ' . getcwd(), true, IOInterface::DEBUG);
             }
 
             // add non-standard scripts as own commands
@@ -214,21 +213,21 @@ class Application extends BaseApplication
                     || (($df = disk_free_space($dir = $config->get('vendor-dir'))) !== false && $df < $minSpaceFree)
                     || (($df = disk_free_space($dir = sys_get_temp_dir())) !== false && $df < $minSpaceFree)
                 ) {
-                    $io->writeError('<error>The disk hosting '.$dir.' is full, this may be the cause of the following exception</error>');
+                    $io->writeError('<error>The disk hosting '.$dir.' is full, this may be the cause of the following exception</error>', true, IOInterface::QUIET);
                 }
             }
         } catch (\Exception $e) {
         }
         Silencer::restore();
 
-        if (defined('PHP_WINDOWS_VERSION_BUILD') && false !== strpos($exception->getMessage(), 'The system cannot find the path specified')) {
-            $io->writeError('<error>The following exception may be caused by a stale entry in your cmd.exe AutoRun</error>');
-            $io->writeError('<error>Check https://getcomposer.org/doc/articles/troubleshooting.md#-the-system-cannot-find-the-path-specified-windows- for details</error>');
+        if (Platform::isWindows() && false !== strpos($exception->getMessage(), 'The system cannot find the path specified')) {
+            $io->writeError('<error>The following exception may be caused by a stale entry in your cmd.exe AutoRun</error>', true, IOInterface::QUIET);
+            $io->writeError('<error>Check https://getcomposer.org/doc/articles/troubleshooting.md#-the-system-cannot-find-the-path-specified-windows- for details</error>', true, IOInterface::QUIET);
         }
 
         if (false !== strpos($exception->getMessage(), 'fork failed - Cannot allocate memory')) {
-            $io->writeError('<error>The following exception is caused by a lack of memory and not having swap configured</error>');
-            $io->writeError('<error>Check https://getcomposer.org/doc/articles/troubleshooting.md#proc-open-fork-failed-errors for details</error>');
+            $io->writeError('<error>The following exception is caused by a lack of memory and not having swap configured</error>', true, IOInterface::QUIET);
+            $io->writeError('<error>Check https://getcomposer.org/doc/articles/troubleshooting.md#proc-open-fork-failed-errors for details</error>', true, IOInterface::QUIET);
         }
     }
 

+ 23 - 6
src/Composer/DependencyResolver/Rule.php

@@ -12,6 +12,8 @@
 
 namespace Composer\DependencyResolver;
 
+use Composer\Package\CompletePackage;
+
 /**
  * @author Nils Adermann <naderman@naderman.de>
  */
@@ -203,25 +205,40 @@ class Rule
                     if ($targetName === 'php' || $targetName === 'php-64bit' || $targetName === 'hhvm') {
                         // handle php/hhvm
                         if (defined('HHVM_VERSION')) {
-                            $text .= ' -> your HHVM version does not satisfy that requirement.';
+                            return $text . ' -> your HHVM version does not satisfy that requirement.';
                         } elseif ($targetName === 'hhvm') {
-                            $text .= ' -> you are running this with PHP and not HHVM.';
+                            return $text . ' -> you are running this with PHP and not HHVM.';
                         } else {
-                            $text .= ' -> your PHP version ('. phpversion() .') or value of "config.platform.php" in composer.json does not satisfy that requirement.';
+                            $packages = $pool->whatProvides($targetName);
+                            $package = count($packages) ? current($packages) : phpversion();
+
+                            if (!($package instanceof CompletePackage)) {
+                                return $text . ' -> your PHP version ('.phpversion().') does not satisfy that requirement.';
+                            }
+
+                            $extra = $package->getExtra();
+
+                            if (!empty($extra['config.platform'])) {
+                                $text .= ' -> your PHP version ('.phpversion().') overriden by "config.platform.php" version ('.$package->getPrettyVersion().') does not satisfy that requirement.';
+                            } else {
+                                $text .= ' -> your PHP version ('.$package->getPrettyVersion().') does not satisfy that requirement.';
+                            }
+
+                            return $text;
                         }
                     } elseif (0 === strpos($targetName, 'ext-')) {
                         // handle php extensions
                         $ext = substr($targetName, 4);
                         $error = extension_loaded($ext) ? 'has the wrong version ('.(phpversion($ext) ?: '0').') installed' : 'is missing from your system';
 
-                        $text .= ' -> the requested PHP extension '.$ext.' '.$error.'.';
+                        return $text . ' -> the requested PHP extension '.$ext.' '.$error.'.';
                     } elseif (0 === strpos($targetName, 'lib-')) {
                         // handle linked libs
                         $lib = substr($targetName, 4);
 
-                        $text .= ' -> the requested linked library '.$lib.' has the wrong version installed or is missing from your system, make sure to have the extension providing it.';
+                        return $text . ' -> the requested linked library '.$lib.' has the wrong version installed or is missing from your system, make sure to have the extension providing it.';
                     } else {
-                        $text .= ' -> no matching package found.';
+                        return $text . ' -> no matching package found.';
                     }
                 }
 

+ 1 - 1
src/Composer/DependencyResolver/SolverProblemsException.php

@@ -41,7 +41,7 @@ class SolverProblemsException extends \RuntimeException
         }
 
         if (strpos($text, 'could not be found') || strpos($text, 'no matching package found')) {
-            $text .= "\nPotential causes:\n - A typo in the package name\n - The package is not available in a stable-enough version according to your minimum-stability setting\n   see <https://groups.google.com/d/topic/composer-dev/_g3ASeIFlrc/discussion> for more details.\n\nRead <https://getcomposer.org/doc/articles/troubleshooting.md> for further common problems.";
+            $text .= "\nPotential causes:\n - A typo in the package name\n - The package is not available in a stable-enough version according to your minimum-stability setting\n   see <https://getcomposer.org/doc/04-schema.md#minimum-stability> for more details.\n\nRead <https://getcomposer.org/doc/articles/troubleshooting.md> for further common problems.";
         }
 
         if ($hasExtensionProblems) {

+ 2 - 3
src/Composer/Downloader/ArchiveDownloader.php

@@ -14,6 +14,7 @@ namespace Composer\Downloader;
 
 use Composer\Package\PackageInterface;
 use Symfony\Component\Finder\Finder;
+use Composer\IO\IOInterface;
 
 /**
  * Base downloader for archives
@@ -34,9 +35,7 @@ abstract class ArchiveDownloader extends FileDownloader
         while ($retries--) {
             $fileName = parent::download($package, $path);
 
-            if ($this->io->isVerbose()) {
-                $this->io->writeError('    Extracting archive');
-            }
+            $this->io->writeError('    Extracting archive', true, IOInterface::VERBOSE);
 
             try {
                 $this->filesystem->ensureDirectoryExists($temporaryDir);

+ 1 - 3
src/Composer/Downloader/FileDownloader.php

@@ -141,9 +141,7 @@ class FileDownloader implements DownloaderInterface
                         if ((0 !== $e->getCode() && !in_array($e->getCode(), array(500, 502, 503, 504))) || !$retries) {
                             throw $e;
                         }
-                        if ($this->io->isVerbose()) {
-                            $this->io->writeError('    Download failed, retrying...');
-                        }
+                        $this->io->writeError('    Download failed, retrying...', true, IOInterface::VERBOSE);
                         usleep(500000);
                     }
                 }

+ 3 - 2
src/Composer/Downloader/GitDownloader.php

@@ -14,6 +14,7 @@ namespace Composer\Downloader;
 
 use Composer\Package\PackageInterface;
 use Composer\Util\Git as GitUtil;
+use Composer\Util\Platform;
 use Composer\Util\ProcessExecutor;
 use Composer\IO\IOInterface;
 use Composer\Util\Filesystem;
@@ -43,7 +44,7 @@ class GitDownloader extends VcsDownloader
         $path = $this->normalizePath($path);
 
         $ref = $package->getSourceReference();
-        $flag = defined('PHP_WINDOWS_VERSION_MAJOR') ? '/D ' : '';
+        $flag = Platform::isWindows() ? '/D ' : '';
         $command = 'git clone --no-checkout %s %s && cd '.$flag.'%2$s && git remote add composer %1$s && git fetch composer';
         $this->io->writeError("    Cloning ".$ref);
 
@@ -353,7 +354,7 @@ class GitDownloader extends VcsDownloader
 
     protected function normalizePath($path)
     {
-        if (defined('PHP_WINDOWS_VERSION_MAJOR') && strlen($path) > 0) {
+        if (Platform::isWindows() && strlen($path) > 0) {
             $basePath = $path;
             $removed = array();
 

+ 21 - 8
src/Composer/Downloader/GzipDownloader.php

@@ -16,6 +16,7 @@ use Composer\Config;
 use Composer\Cache;
 use Composer\EventDispatcher\EventDispatcher;
 use Composer\Package\PackageInterface;
+use Composer\Util\Platform;
 use Composer\Util\ProcessExecutor;
 use Composer\Util\RemoteFilesystem;
 use Composer\IO\IOInterface;
@@ -40,25 +41,26 @@ class GzipDownloader extends ArchiveDownloader
         $targetFilepath = $path . DIRECTORY_SEPARATOR . basename(substr($file, 0, -3));
 
         // Try to use gunzip on *nix
-        if (!defined('PHP_WINDOWS_VERSION_BUILD')) {
+        if (!Platform::isWindows()) {
             $command = 'gzip -cd ' . ProcessExecutor::escape($file) . ' > ' . ProcessExecutor::escape($targetFilepath);
 
             if (0 === $this->process->execute($command, $ignoredOutput)) {
                 return;
             }
 
+            if (extension_loaded('zlib')) {
+                // Fallback to using the PHP extension.
+                $this->extractUsingExt($file, $targetFilepath);
+
+                return;
+            }
+
             $processError = 'Failed to execute ' . $command . "\n\n" . $this->process->getErrorOutput();
             throw new \RuntimeException($processError);
         }
 
         // Windows version of PHP has built-in support of gzip functions
-        $archiveFile = gzopen($file, 'rb');
-        $targetFile = fopen($targetFilepath, 'wb');
-        while ($string = gzread($archiveFile, 4096)) {
-            fwrite($targetFile, $string, strlen($string));
-        }
-        gzclose($archiveFile);
-        fclose($targetFile);
+        $this->extractUsingExt($file, $targetFilepath);
     }
 
     /**
@@ -68,4 +70,15 @@ class GzipDownloader extends ArchiveDownloader
     {
         return $path.'/'.pathinfo(parse_url($package->getDistUrl(), PHP_URL_PATH), PATHINFO_BASENAME);
     }
+
+    private function extractUsingExt($file, $targetFilepath)
+    {
+        $archiveFile = gzopen($file, 'rb');
+        $targetFile = fopen($targetFilepath, 'wb');
+        while ($string = gzread($archiveFile, 4096)) {
+            fwrite($targetFile, $string, strlen($string));
+        }
+        gzclose($archiveFile);
+        fclose($targetFile);
+    }
 }

+ 3 - 2
src/Composer/Downloader/RarDownloader.php

@@ -15,6 +15,7 @@ namespace Composer\Downloader;
 use Composer\Config;
 use Composer\Cache;
 use Composer\EventDispatcher\EventDispatcher;
+use Composer\Util\Platform;
 use Composer\Util\ProcessExecutor;
 use Composer\Util\RemoteFilesystem;
 use Composer\IO\IOInterface;
@@ -42,7 +43,7 @@ class RarDownloader extends ArchiveDownloader
         $processError = null;
 
         // Try to use unrar on *nix
-        if (!defined('PHP_WINDOWS_VERSION_BUILD')) {
+        if (!Platform::isWindows()) {
             $command = 'unrar x ' . ProcessExecutor::escape($file) . ' ' . ProcessExecutor::escape($path) . ' && chmod -R u+w ' . ProcessExecutor::escape($path);
 
             if (0 === $this->process->execute($command, $ignoredOutput)) {
@@ -65,7 +66,7 @@ class RarDownloader extends ArchiveDownloader
             $error = "Could not decompress the archive, enable the PHP rar extension or install unrar.\n"
                 . $iniMessage . "\n" . $processError;
 
-            if (!defined('PHP_WINDOWS_VERSION_BUILD')) {
+            if (!Platform::isWindows()) {
                 $error = "Could not decompress the archive, enable the PHP rar extension.\n" . $iniMessage;
             }
 

+ 3 - 2
src/Composer/Downloader/ZipDownloader.php

@@ -15,6 +15,7 @@ namespace Composer\Downloader;
 use Composer\Config;
 use Composer\Cache;
 use Composer\EventDispatcher\EventDispatcher;
+use Composer\Util\Platform;
 use Composer\Util\ProcessExecutor;
 use Composer\Util\RemoteFilesystem;
 use Composer\IO\IOInterface;
@@ -38,7 +39,7 @@ class ZipDownloader extends ArchiveDownloader
         $processError = null;
 
         // try to use unzip on *nix
-        if (!defined('PHP_WINDOWS_VERSION_BUILD')) {
+        if (!Platform::isWindows()) {
             $command = 'unzip '.ProcessExecutor::escape($file).' -d '.ProcessExecutor::escape($path) . ' && chmod -R u+w ' . ProcessExecutor::escape($path);
             try {
                 if (0 === $this->process->execute($command, $ignoredOutput)) {
@@ -64,7 +65,7 @@ class ZipDownloader extends ArchiveDownloader
             $error = "Could not decompress the archive, enable the PHP zip extension or install unzip.\n"
                 . $iniMessage . "\n" . $processError;
 
-            if (!defined('PHP_WINDOWS_VERSION_BUILD')) {
+            if (!Platform::isWindows()) {
                 $error = "Could not decompress the archive, enable the PHP zip extension.\n" . $iniMessage;
             }
 

+ 1 - 3
src/Composer/EventDispatcher/EventDispatcher.php

@@ -155,9 +155,7 @@ class EventDispatcher
                 $event = $this->checkListenerExpectedEvent($callable, $event);
                 $return = false === call_user_func($callable, $event) ? 1 : 0;
             } elseif ($this->isComposerScript($callable)) {
-                if ($this->io->isVerbose()) {
-                    $this->io->writeError(sprintf('> %s: %s', $event->getName(), $callable));
-                }
+                $this->io->writeError(sprintf('> %s: %s', $event->getName(), $callable), true, IOInterface::VERBOSE);
                 $scriptName = substr($callable, 1);
                 $args = $event->getArguments();
                 $flags = $event->getFlags();

+ 22 - 12
src/Composer/Factory.php

@@ -20,6 +20,7 @@ use Composer\Package\Version\VersionGuesser;
 use Composer\Repository\RepositoryManager;
 use Composer\Repository\WritableRepositoryInterface;
 use Composer\Util\Filesystem;
+use Composer\Util\Platform;
 use Composer\Util\ProcessExecutor;
 use Composer\Util\RemoteFilesystem;
 use Composer\Util\Silencer;
@@ -51,7 +52,7 @@ class Factory
             return $home;
         }
 
-        if (defined('PHP_WINDOWS_VERSION_MAJOR')) {
+        if (Platform::isWindows()) {
             if (!getenv('APPDATA')) {
                 throw new \RuntimeException('The APPDATA or COMPOSER_HOME environment variable must be set for composer to run correctly');
             }
@@ -90,7 +91,7 @@ class Factory
             return $homeEnv . '/cache';
         }
 
-        if (defined('PHP_WINDOWS_VERSION_MAJOR')) {
+        if (Platform::isWindows()) {
             if ($cacheDir = getenv('LOCALAPPDATA')) {
                 $cacheDir .= '/Composer';
             } else {
@@ -125,7 +126,7 @@ class Factory
             return $homeEnv;
         }
 
-        if (defined('PHP_WINDOWS_VERSION_MAJOR')) {
+        if (Platform::isWindows()) {
             return strtr($home, '\\', '/');
         }
 
@@ -190,6 +191,20 @@ class Factory
         }
         $config->setAuthConfigSource(new JsonConfigSource($file, true));
 
+        // load COMPOSER_AUTH environment variable if set
+        if ($composerAuthEnv = getenv('COMPOSER_AUTH')) {
+            $authData = json_decode($composerAuthEnv, true);
+
+            if (is_null($authData)) {
+                throw new \UnexpectedValueException('COMPOSER_AUTH environment variable is malformed, should be a valid JSON object');
+            }
+
+            if ($io && $io->isDebug()) {
+                $io->writeError('Loading auth config from COMPOSER_AUTH');
+            }
+            $config->merge(array('config' => $authData));
+        }
+
         return $config;
     }
 
@@ -293,14 +308,10 @@ class Factory
         $config = static::createConfig($io, $cwd);
         $config->merge($localConfig);
         if (isset($composerFile)) {
-            if ($io && $io->isDebug()) {
-                $io->writeError('Loading config file ' . $composerFile);
-            }
+            $io->writeError('Loading config file ' . $composerFile, true, IOInterface::DEBUG);
             $localAuthFile = new JsonFile(dirname(realpath($composerFile)) . '/auth.json');
             if ($localAuthFile->exists()) {
-                if ($io && $io->isDebug()) {
-                    $io->writeError('Loading config file ' . $localAuthFile->getPath());
-                }
+                $io->writeError('Loading config file ' . $localAuthFile->getPath(), true, IOInterface::DEBUG);
                 $config->merge(array('config' => $localAuthFile->read()));
                 $config->setAuthConfigSource(new JsonConfigSource($localAuthFile, true));
             }
@@ -435,9 +446,7 @@ class Factory
         try {
             $composer = self::createComposer($io, $config->get('home') . '/composer.json', $disablePlugins, $config->get('home'), false);
         } catch (\Exception $e) {
-            if ($io->isDebug()) {
-                $io->writeError('Failed to initialize global composer: '.$e->getMessage());
-            }
+            $io->writeError('Failed to initialize global composer: '.$e->getMessage(), true, IOInterface::DEBUG);
         }
 
         return $composer;
@@ -503,6 +512,7 @@ class Factory
         }
 
         $am = new Archiver\ArchiveManager($dm);
+        $am->addArchiver(new Archiver\ZipArchiver);
         $am->addArchiver(new Archiver\PharArchiver);
 
         return $am;

+ 11 - 36
src/Composer/IO/BaseIO.php

@@ -60,50 +60,25 @@ abstract class BaseIO implements IOInterface
      */
     public function loadConfiguration(Config $config)
     {
-        $githubOauth = $config->get('github-oauth');
-        $gitlabOauth = $config->get('gitlab-oauth');
-        $httpBasic = $config->get('http-basic');
-
-        // Use COMPOSER_AUTH environment variable if set
-        if ($composerAuthEnv = getenv('COMPOSER_AUTH')) {
-            $authData = json_decode($composerAuthEnv, true);
-
-            if (is_null($authData)) {
-                throw new \UnexpectedValueException('COMPOSER_AUTH environment variable is malformed');
-            }
-
-            if (isset($authData['github-oauth'])) {
-                $githubOauth = array_merge($githubOauth, $authData['github-oauth']);
-            }
-            if (isset($authData['gitlab-oauth'])) {
-                $gitlabOauth = array_merge($gitlabOauth, $authData['gitlab-oauth']);
-            }
-            if (isset($authData['http-basic'])) {
-                $httpBasic = array_merge($httpBasic, $authData['http-basic']);
-            }
-        }
+        $githubOauth = $config->get('github-oauth') ?: array();
+        $gitlabOauth = $config->get('gitlab-oauth') ?: array();
+        $httpBasic = $config->get('http-basic') ?: array();
 
         // reload oauth token from config if available
-        if ($githubOauth) {
-            foreach ($githubOauth as $domain => $token) {
-                if (!preg_match('{^[a-z0-9]+$}', $token)) {
-                    throw new \UnexpectedValueException('Your github oauth token for '.$domain.' contains invalid characters: "'.$token.'"');
-                }
-                $this->setAuthentication($domain, $token, 'x-oauth-basic');
+        foreach ($githubOauth as $domain => $token) {
+            if (!preg_match('{^[a-z0-9]+$}', $token)) {
+                throw new \UnexpectedValueException('Your github oauth token for '.$domain.' contains invalid characters: "'.$token.'"');
             }
+            $this->setAuthentication($domain, $token, 'x-oauth-basic');
         }
 
-        if ($gitlabOauth) {
-            foreach ($gitlabOauth as $domain => $token) {
-                $this->setAuthentication($domain, $token, 'oauth2');
-            }
+        foreach ($gitlabOauth as $domain => $token) {
+            $this->setAuthentication($domain, $token, 'oauth2');
         }
 
         // reload http basic credentials from config if available
-        if ($httpBasic) {
-            foreach ($httpBasic as $domain => $cred) {
-                $this->setAuthentication($domain, $cred['username'], $cred['password']);
-            }
+        foreach ($httpBasic as $domain => $cred) {
+            $this->setAuthentication($domain, $cred['username'], $cred['password']);
         }
 
         // setup process timeout

+ 1 - 1
src/Composer/IO/BufferIO.php

@@ -35,7 +35,7 @@ class BufferIO extends ConsoleIO
         $input = new StringInput($input);
         $input->setInteractive(false);
 
-        $output = new StreamOutput(fopen('php://memory', 'rw'), $verbosity, !empty($formatter), $formatter);
+        $output = new StreamOutput(fopen('php://memory', 'rw'), $verbosity, $formatter ? $formatter->isDecorated() : false, $formatter);
 
         parent::__construct($input, $output, new HelperSet(array()));
     }

+ 32 - 17
src/Composer/IO/ConsoleIO.php

@@ -33,6 +33,7 @@ class ConsoleIO extends BaseIO
     protected $lastMessage;
     protected $lastMessageErr;
     private $startTime;
+    private $verbosityMap;
 
     /**
      * Constructor.
@@ -46,6 +47,13 @@ class ConsoleIO extends BaseIO
         $this->input = $input;
         $this->output = $output;
         $this->helperSet = $helperSet;
+        $this->verbosityMap = array(
+            self::QUIET => OutputInterface::VERBOSITY_QUIET,
+            self::NORMAL => OutputInterface::VERBOSITY_NORMAL,
+            self::VERBOSE => OutputInterface::VERBOSITY_VERBOSE,
+            self::VERY_VERBOSE => OutputInterface::VERBOSITY_VERY_VERBOSE,
+            self::DEBUG => OutputInterface::VERBOSITY_DEBUG,
+        );
     }
 
     public function enableDebugging($startTime)
@@ -96,26 +104,32 @@ class ConsoleIO extends BaseIO
     /**
      * {@inheritDoc}
      */
-    public function write($messages, $newline = true)
+    public function write($messages, $newline = true, $verbosity = self::NORMAL)
     {
-        $this->doWrite($messages, $newline, false);
+        $this->doWrite($messages, $newline, false, $verbosity);
     }
 
     /**
      * {@inheritDoc}
      */
-    public function writeError($messages, $newline = true)
+    public function writeError($messages, $newline = true, $verbosity = self::NORMAL)
     {
-        $this->doWrite($messages, $newline, true);
+        $this->doWrite($messages, $newline, true, $verbosity);
     }
 
     /**
      * @param array|string $messages
      * @param bool         $newline
      * @param bool         $stderr
+     * @param int          $verbosity
      */
-    private function doWrite($messages, $newline, $stderr)
+    private function doWrite($messages, $newline, $stderr, $verbosity)
     {
+        $sfVerbosity = $this->verbosityMap[$verbosity];
+        if ($sfVerbosity > $this->output->getVerbosity()) {
+            return;
+        }
+
         if (null !== $this->startTime) {
             $memoryUsage = memory_get_usage() / 1024 / 1024;
             $timeSpent = microtime(true) - $this->startTime;
@@ -125,30 +139,30 @@ class ConsoleIO extends BaseIO
         }
 
         if (true === $stderr && $this->output instanceof ConsoleOutputInterface) {
-            $this->output->getErrorOutput()->write($messages, $newline);
+            $this->output->getErrorOutput()->write($messages, $newline, $sfVerbosity);
             $this->lastMessageErr = join($newline ? "\n" : '', (array) $messages);
 
             return;
         }
 
-        $this->output->write($messages, $newline);
+        $this->output->write($messages, $newline, $sfVerbosity);
         $this->lastMessage = join($newline ? "\n" : '', (array) $messages);
     }
 
     /**
      * {@inheritDoc}
      */
-    public function overwrite($messages, $newline = true, $size = null)
+    public function overwrite($messages, $newline = true, $size = null, $verbosity = self::NORMAL)
     {
-        $this->doOverwrite($messages, $newline, $size, false);
+        $this->doOverwrite($messages, $newline, $size, false, $verbosity);
     }
 
     /**
      * {@inheritDoc}
      */
-    public function overwriteError($messages, $newline = true, $size = null)
+    public function overwriteError($messages, $newline = true, $size = null, $verbosity = self::NORMAL)
     {
-        $this->doOverwrite($messages, $newline, $size, true);
+        $this->doOverwrite($messages, $newline, $size, true, $verbosity);
     }
 
     /**
@@ -156,8 +170,9 @@ class ConsoleIO extends BaseIO
      * @param bool         $newline
      * @param int|null     $size
      * @param bool         $stderr
+     * @param int          $verbosity
      */
-    private function doOverwrite($messages, $newline, $size, $stderr)
+    private function doOverwrite($messages, $newline, $size, $stderr, $verbosity)
     {
         // messages can be an array, let's convert it to string anyway
         $messages = join($newline ? "\n" : '', (array) $messages);
@@ -168,21 +183,21 @@ class ConsoleIO extends BaseIO
             $size = strlen(strip_tags($stderr ? $this->lastMessageErr : $this->lastMessage));
         }
         // ...let's fill its length with backspaces
-        $this->doWrite(str_repeat("\x08", $size), false, $stderr);
+        $this->doWrite(str_repeat("\x08", $size), false, $stderr, $verbosity);
 
         // write the new message
-        $this->doWrite($messages, false, $stderr);
+        $this->doWrite($messages, false, $stderr, $verbosity);
 
         $fill = $size - strlen(strip_tags($messages));
         if ($fill > 0) {
             // whitespace whatever has left
-            $this->doWrite(str_repeat(' ', $fill), false, $stderr);
+            $this->doWrite(str_repeat(' ', $fill), false, $stderr, $verbosity);
             // move the cursor back
-            $this->doWrite(str_repeat("\x08", $fill), false, $stderr);
+            $this->doWrite(str_repeat("\x08", $fill), false, $stderr, $verbosity);
         }
 
         if ($newline) {
-            $this->doWrite('', true, $stderr);
+            $this->doWrite('', true, $stderr, $verbosity);
         }
 
         if ($stderr) {

+ 24 - 14
src/Composer/IO/IOInterface.php

@@ -21,6 +21,12 @@ use Composer\Config;
  */
 interface IOInterface
 {
+    const QUIET = 1;
+    const NORMAL = 2;
+    const VERBOSE = 4;
+    const VERY_VERBOSE = 8;
+    const DEBUG = 16;
+
     /**
      * Is this input means interactive?
      *
@@ -59,36 +65,40 @@ interface IOInterface
     /**
      * Writes a message to the output.
      *
-     * @param string|array $messages The message as an array of lines or a single string
-     * @param bool         $newline  Whether to add a newline or not
+     * @param string|array $messages  The message as an array of lines or a single string
+     * @param bool         $newline   Whether to add a newline or not
+     * @param int          $verbosity Verbosity level from the VERBOSITY_* constants
      */
-    public function write($messages, $newline = true);
+    public function write($messages, $newline = true, $verbosity = self::NORMAL);
 
     /**
      * Writes a message to the error output.
      *
-     * @param string|array $messages The message as an array of lines or a single string
-     * @param bool         $newline  Whether to add a newline or not
+     * @param string|array $messages  The message as an array of lines or a single string
+     * @param bool         $newline   Whether to add a newline or not
+     * @param int          $verbosity Verbosity level from the VERBOSITY_* constants
      */
-    public function writeError($messages, $newline = true);
+    public function writeError($messages, $newline = true, $verbosity = self::NORMAL);
 
     /**
      * Overwrites a previous message to the output.
      *
-     * @param string|array $messages The message as an array of lines or a single string
-     * @param bool         $newline  Whether to add a newline or not
-     * @param int          $size     The size of line
+     * @param string|array $messages  The message as an array of lines or a single string
+     * @param bool         $newline   Whether to add a newline or not
+     * @param int          $size      The size of line
+     * @param int          $verbosity Verbosity level from the VERBOSITY_* constants
      */
-    public function overwrite($messages, $newline = true, $size = null);
+    public function overwrite($messages, $newline = true, $size = null, $verbosity = self::NORMAL);
 
     /**
      * Overwrites a previous message to the error output.
      *
-     * @param string|array $messages The message as an array of lines or a single string
-     * @param bool         $newline  Whether to add a newline or not
-     * @param int          $size     The size of line
+     * @param string|array $messages  The message as an array of lines or a single string
+     * @param bool         $newline   Whether to add a newline or not
+     * @param int          $size      The size of line
+     * @param int          $verbosity Verbosity level from the VERBOSITY_* constants
      */
-    public function overwriteError($messages, $newline = true, $size = null);
+    public function overwriteError($messages, $newline = true, $size = null, $verbosity = self::NORMAL);
 
     /**
      * Asks a question to the user.

+ 4 - 4
src/Composer/IO/NullIO.php

@@ -62,28 +62,28 @@ class NullIO extends BaseIO
     /**
      * {@inheritDoc}
      */
-    public function write($messages, $newline = true)
+    public function write($messages, $newline = true, $verbosity = self::NORMAL)
     {
     }
 
     /**
      * {@inheritDoc}
      */
-    public function writeError($messages, $newline = true)
+    public function writeError($messages, $newline = true, $verbosity = self::NORMAL)
     {
     }
 
     /**
      * {@inheritDoc}
      */
-    public function overwrite($messages, $newline = true, $size = 80)
+    public function overwrite($messages, $newline = true, $size = 80, $verbosity = self::NORMAL)
     {
     }
 
     /**
      * {@inheritDoc}
      */
-    public function overwriteError($messages, $newline = true, $size = 80)
+    public function overwriteError($messages, $newline = true, $size = 80, $verbosity = self::NORMAL)
     {
     }
 

+ 5 - 8
src/Composer/Installer.php

@@ -529,10 +529,8 @@ class Installer
             return max(1, $e->getCode());
         }
 
-        if ($this->io->isVerbose()) {
-            $this->io->writeError("Analyzed ".count($pool)." packages to resolve dependencies");
-            $this->io->writeError("Analyzed ".$solver->getRuleSetSize()." rules to resolve dependencies");
-        }
+        $this->io->writeError("Analyzed ".count($pool)." packages to resolve dependencies", true, IOInterface::VERBOSE);
+        $this->io->writeError("Analyzed ".$solver->getRuleSetSize()." rules to resolve dependencies", true, IOInterface::VERBOSE);
 
         // force dev packages to be updated if we update or install from a (potentially new) lock
         $operations = $this->processDevPackages($localRepo, $pool, $policy, $repositories, $installedRepo, $lockedRepository, $installFromLock, $withDevReqs, 'force-updates', $operations);
@@ -578,10 +576,8 @@ class Installer
                     && (!$operation->getTargetPackage()->getSourceReference() || $operation->getTargetPackage()->getSourceReference() === $operation->getInitialPackage()->getSourceReference())
                     && (!$operation->getTargetPackage()->getDistReference() || $operation->getTargetPackage()->getDistReference() === $operation->getInitialPackage()->getDistReference())
                 ) {
-                    if ($this->io->isDebug()) {
-                        $this->io->writeError('  - Skipping update of '. $operation->getTargetPackage()->getPrettyName().' to the same reference-locked version');
-                        $this->io->writeError('');
-                    }
+                    $this->io->writeError('  - Skipping update of '. $operation->getTargetPackage()->getPrettyName().' to the same reference-locked version', true, IOInterface::DEBUG);
+                    $this->io->writeError('', true, IOInterface::DEBUG);
 
                     continue;
                 }
@@ -1199,6 +1195,7 @@ class Installer
 
                     foreach ($requirePackages as $requirePackage) {
                         if (isset($skipPackages[$requirePackage->getName()])) {
+                            $this->io->writeError('<warning>Dependency "' . $requirePackage->getName() . '" is also a root requirement, but is not explicitly whitelisted. Ignoring.</warning>');
                             continue;
                         }
                         $packageQueue->enqueue($requirePackage);

+ 2 - 1
src/Composer/Installer/LibraryInstaller.php

@@ -17,6 +17,7 @@ use Composer\IO\IOInterface;
 use Composer\Repository\InstalledRepositoryInterface;
 use Composer\Package\PackageInterface;
 use Composer\Util\Filesystem;
+use Composer\Util\Platform;
 use Composer\Util\ProcessExecutor;
 use Composer\Util\Silencer;
 
@@ -241,7 +242,7 @@ class LibraryInstaller implements InstallerInterface
             }
 
             if ($this->binCompat === "auto") {
-                if (defined('PHP_WINDOWS_VERSION_BUILD')) {
+                if (Platform::isWindows()) {
                     $this->installFullBinaries($binPath, $link, $bin, $package);
                 } else {
                     $this->installSymlinkBinaries($binPath, $link);

+ 3 - 4
src/Composer/Installer/PearInstaller.php

@@ -17,6 +17,7 @@ use Composer\Composer;
 use Composer\Downloader\PearPackageExtractor;
 use Composer\Repository\InstalledRepositoryInterface;
 use Composer\Package\PackageInterface;
+use Composer\Util\Platform;
 use Composer\Util\ProcessExecutor;
 
 /**
@@ -53,7 +54,7 @@ class PearInstaller extends LibraryInstaller
         parent::installCode($package);
         parent::initializeBinDir();
 
-        $isWindows = defined('PHP_WINDOWS_VERSION_BUILD');
+        $isWindows = Platform::isWindows();
         $php_bin = $this->binDir . ($isWindows ? '/composer-php.bat' : '/composer-php');
 
         if (!$isWindows) {
@@ -75,9 +76,7 @@ class PearInstaller extends LibraryInstaller
         $pearExtractor = new PearPackageExtractor($packageArchive);
         $pearExtractor->extractTo($this->getInstallPath($package), array('php' => '/', 'script' => '/bin', 'data' => '/data'), $vars);
 
-        if ($this->io->isVerbose()) {
-            $this->io->writeError('    Cleaning up');
-        }
+        $this->io->writeError('    Cleaning up', true, IOInterface::VERBOSE);
         $this->filesystem->unlink($packageArchive);
     }
 

+ 65 - 0
src/Composer/Package/Archiver/ZipArchiver.php

@@ -0,0 +1,65 @@
+<?php
+
+/*
+ * This file is part of Composer.
+ *
+ * (c) Nils Adermann <naderman@naderman.de>
+ *     Jordi Boggiano <j.boggiano@seld.be>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Composer\Package\Archiver;
+
+use ZipArchive;
+
+/**
+ * @author Jan Prieser <jan@prieser.net>
+ */
+class ZipArchiver implements ArchiverInterface
+{
+    protected static $formats = array(
+        'zip' => 1
+    );
+
+    /**
+     * {@inheritdoc}
+     */
+    public function archive($sources, $target, $format, array $excludes = array())
+    {
+        $sources = realpath($sources);
+        $zip = new ZipArchive();
+        $res = $zip->open($target, ZipArchive::CREATE);
+        if ($res === true) {
+            $files = new ArchivableFilesFinder($sources, $excludes);
+            foreach($files as $file) {
+                /** @var $file \SplFileInfo */
+                $filepath = $file->getPath()."/".$file->getFilename();
+                $localname = str_replace($sources."/", '', $filepath);
+                $zip->addFile($filepath, $localname);
+            }
+            if ($zip->close()) {
+                return $target;
+            }
+        }
+        $message = sprintf("Could not create archive '%s' from '%s': %s",
+            $target,
+            $sources,
+            $zip->getStatusString()
+        );
+        throw new \RuntimeException($message);
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function supports($format, $sourceType)
+    {
+        return isset(static::$formats[$format]) && $this->compressionAvailable();
+    }
+
+    private function compressionAvailable() {
+        return class_exists('ZipArchive');
+    }
+}

+ 5 - 0
src/Composer/Package/Loader/RootPackageLoader.php

@@ -113,6 +113,11 @@ class RootPackageLoader extends ArrayLoader
             }
         }
 
+        if (isset($links[$config['name']])) {
+            throw new \InvalidArgumentException(sprintf('Root package \'%s\' cannot require itself in its composer.json' . PHP_EOL .
+                        'Did you accidentally name your root package after an external package?', $config['name']));
+        }
+
         $realPackage->setAliases($aliases);
         $realPackage->setStabilityFlags($stabilityFlags);
         $realPackage->setReferences($references);

+ 1 - 3
src/Composer/Plugin/PluginManager.php

@@ -206,9 +206,7 @@ class PluginManager
      */
     private function addPlugin(PluginInterface $plugin)
     {
-        if ($this->io->isDebug()) {
-            $this->io->writeError('Loading plugin '.get_class($plugin));
-        }
+        $this->io->writeError('Loading plugin '.get_class($plugin), true, IOInterface::DEBUG);
         $this->plugins[] =  $plugin;
         $plugin->activate($this->composer, $this->io);
 

+ 3 - 7
src/Composer/Repository/ArtifactRepository.php

@@ -67,16 +67,12 @@ class ArtifactRepository extends ArrayRepository implements ConfigurableReposito
 
             $package = $this->getComposerInformation($file);
             if (!$package) {
-                if ($io->isVerbose()) {
-                    $io->writeError("File <comment>{$file->getBasename()}</comment> doesn't seem to hold a package");
-                }
+                $io->writeError("File <comment>{$file->getBasename()}</comment> doesn't seem to hold a package", true, IOInterface::VERBOSE);
                 continue;
             }
 
-            if ($io->isVerbose()) {
-                $template = 'Found package <info>%s</info> (<comment>%s</comment>) in file <info>%s</info>';
-                $io->writeError(sprintf($template, $package->getName(), $package->getPrettyVersion(), $file->getBasename()));
-            }
+            $template = 'Found package <info>%s</info> (<comment>%s</comment>) in file <info>%s</info>';
+            $io->writeError(sprintf($template, $package->getName(), $package->getPrettyVersion(), $file->getBasename()), true, IOInterface::VERBOSE);
 
             $this->addPackage($package);
         }

+ 2 - 3
src/Composer/Repository/PathRepository.php

@@ -125,17 +125,16 @@ class PathRepository extends ArrayRepository implements ConfigurableRepositoryIn
             $package['dist'] = array(
                 'type' => 'path',
                 'url' => $url,
-                'reference' => '',
+                'reference' => sha1($json),
             );
 
             if (!isset($package['version'])) {
                 $package['version'] = $this->versionGuesser->guessVersion($package, $path) ?: 'dev-master';
             }
+
             $output = '';
             if (is_dir($path . DIRECTORY_SEPARATOR . '.git') && 0 === $this->process->execute('git log -n1 --pretty=%H', $output, $path)) {
                 $package['dist']['reference'] = trim($output);
-            } else {
-                $package['dist']['reference'] = Locker::getContentHash($json);
             }
 
             $package = $this->loader->load($package);

+ 1 - 3
src/Composer/Repository/PearRepository.php

@@ -105,9 +105,7 @@ class PearRepository extends ArrayRepository implements ConfigurableRepositoryIn
                 try {
                     $normalizedVersion = $versionParser->normalize($version);
                 } catch (\UnexpectedValueException $e) {
-                    if ($this->io->isVerbose()) {
-                        $this->io->writeError('Could not load '.$packageDefinition->getPackageName().' '.$version.': '.$e->getMessage());
-                    }
+                    $this->io->writeError('Could not load '.$packageDefinition->getPackageName().' '.$version.': '.$e->getMessage(), true, IOInterface::VERBOSE);
                     continue;
                 }
 

+ 23 - 3
src/Composer/Repository/PlatformRepository.php

@@ -59,6 +59,7 @@ class PlatformRepository extends ArrayRepository
             $version = $versionParser->normalize($override['version']);
             $package = new CompletePackage($override['name'], $version, $override['version']);
             $package->setDescription('Package overridden via config.platform');
+            $package->setExtra(array('config.platform' => true));
             parent::addPackage($package);
         }
 
@@ -114,6 +115,7 @@ class PlatformRepository extends ArrayRepository
         // relying on them.
         foreach ($loadedExtensions as $name) {
             $prettyVersion = null;
+            $description = 'The '.$name.' PHP library';
             switch ($name) {
                 case 'curl':
                     $curlVersion = curl_version();
@@ -146,9 +148,27 @@ class PlatformRepository extends ArrayRepository
                     break;
 
                 case 'openssl':
-                    $prettyVersion = preg_replace_callback('{^(?:OpenSSL\s*)?([0-9.]+)([a-z]?).*}', function ($match) {
-                        return $match[1] . (empty($match[2]) ? '' : '.'.(ord($match[2]) - 96));
+                    $prettyVersion = preg_replace_callback('{^(?:OpenSSL\s*)?([0-9.]+)([a-z]*).*}', function ($match) {
+                        if (empty($match[2])) {
+                            return $match[1];
+                        }
+
+                        // OpenSSL versions add another letter when they reach Z.
+                        // e.g. OpenSSL 0.9.8zh 3 Dec 2015
+
+                        if (!preg_match('{^z*[a-z]$}', $match[2])) {
+                            // 0.9.8abc is garbage
+                            return 0;
+                        }
+
+                        $len = strlen($match[2]);
+                        $patchVersion = ($len - 1) * 26; // All Z
+                        $patchVersion += ord($match[2][$len - 1]) - 96;
+
+                        return $match[1].'.'.$patchVersion;
                     }, OPENSSL_VERSION_TEXT);
+
+                    $description = OPENSSL_VERSION_TEXT;
                     break;
 
                 case 'pcre':
@@ -175,7 +195,7 @@ class PlatformRepository extends ArrayRepository
             }
 
             $lib = new CompletePackage('lib-'.$name, $version, $prettyVersion);
-            $lib->setDescription('The '.$name.' PHP library');
+            $lib->setDescription($description);
             $this->addPackage($lib);
         }
 

+ 1 - 3
src/Composer/Repository/Vcs/GitBitbucketDriver.php

@@ -160,9 +160,7 @@ class GitBitbucketDriver extends VcsDriver implements VcsDriverInterface
         }
 
         if (!extension_loaded('openssl')) {
-            if ($io->isVerbose()) {
-                $io->writeError('Skipping Bitbucket git driver for '.$url.' because the OpenSSL PHP extension is missing.');
-            }
+            $io->writeError('Skipping Bitbucket git driver for '.$url.' because the OpenSSL PHP extension is missing.', true, IOInterface::VERBOSE);
 
             return false;
         }

+ 1 - 3
src/Composer/Repository/Vcs/GitHubDriver.php

@@ -268,9 +268,7 @@ class GitHubDriver extends VcsDriver
         }
 
         if (!extension_loaded('openssl')) {
-            if ($io->isVerbose()) {
-                $io->writeError('Skipping GitHub driver for '.$url.' because the OpenSSL PHP extension is missing.');
-            }
+            $io->writeError('Skipping GitHub driver for '.$url.' because the OpenSSL PHP extension is missing.', true, IOInterface::VERBOSE);
 
             return false;
         }

+ 1 - 3
src/Composer/Repository/Vcs/GitLabDriver.php

@@ -367,9 +367,7 @@ class GitLabDriver extends VcsDriver
         }
 
         if ('https' === $scheme && !extension_loaded('openssl')) {
-            if ($io->isVerbose()) {
-                $io->write('Skipping GitLab driver for '.$url.' because the OpenSSL PHP extension is missing.');
-            }
+            $io->writeError('Skipping GitLab driver for '.$url.' because the OpenSSL PHP extension is missing.', true, IOInterface::VERBOSE);
 
             return false;
         }

+ 1 - 3
src/Composer/Repository/Vcs/HgBitbucketDriver.php

@@ -170,9 +170,7 @@ class HgBitbucketDriver extends VcsDriver
         }
 
         if (!extension_loaded('openssl')) {
-            if ($io->isVerbose()) {
-                $io->writeError('Skipping Bitbucket hg driver for '.$url.' because the OpenSSL PHP extension is missing.');
-            }
+            $io->writeError('Skipping Bitbucket hg driver for '.$url.' because the OpenSSL PHP extension is missing.', true, IOInterface::VERBOSE);
 
             return false;
         }

+ 8 - 8
src/Composer/Util/Filesystem.php

@@ -110,7 +110,7 @@ class Filesystem
             return $this->removeDirectoryPhp($directory);
         }
 
-        if (defined('PHP_WINDOWS_VERSION_BUILD')) {
+        if (Platform::isWindows()) {
             $cmd = sprintf('rmdir /S /Q %s', ProcessExecutor::escape(realpath($directory)));
         } else {
             $cmd = sprintf('rm -rf %s', ProcessExecutor::escape($directory));
@@ -181,10 +181,10 @@ class Filesystem
     {
         if (!@$this->unlinkImplementation($path)) {
             // retry after a bit on windows since it tends to be touchy with mass removals
-            if (!defined('PHP_WINDOWS_VERSION_BUILD') || (usleep(350000) && !@$this->unlinkImplementation($path))) {
+            if (!Platform::isWindows() || (usleep(350000) && !@$this->unlinkImplementation($path))) {
                 $error = error_get_last();
                 $message = 'Could not delete '.$path.': ' . @$error['message'];
-                if (defined('PHP_WINDOWS_VERSION_BUILD')) {
+                if (Platform::isWindows()) {
                     $message .= "\nThis can be due to an antivirus or the Windows Search Indexer locking the file while they are analyzed";
                 }
 
@@ -206,10 +206,10 @@ class Filesystem
     {
         if (!@rmdir($path)) {
             // retry after a bit on windows since it tends to be touchy with mass removals
-            if (!defined('PHP_WINDOWS_VERSION_BUILD') || (usleep(350000) && !@rmdir($path))) {
+            if (!Platform::isWindows() || (usleep(350000) && !@rmdir($path))) {
                 $error = error_get_last();
                 $message = 'Could not delete '.$path.': ' . @$error['message'];
-                if (defined('PHP_WINDOWS_VERSION_BUILD')) {
+                if (Platform::isWindows()) {
                     $message .= "\nThis can be due to an antivirus or the Windows Search Indexer locking the file while they are analyzed";
                 }
 
@@ -264,7 +264,7 @@ class Filesystem
             return $this->copyThenRemove($source, $target);
         }
 
-        if (defined('PHP_WINDOWS_VERSION_BUILD')) {
+        if (Platform::isWindows()) {
             // Try to copy & delete - this is a workaround for random "Access denied" errors.
             $command = sprintf('xcopy %s %s /E /I /Q /Y', ProcessExecutor::escape($source), ProcessExecutor::escape($target));
             $result = $this->processExecutor->execute($command, $output);
@@ -460,7 +460,7 @@ class Filesystem
 
     public static function getPlatformPath($path)
     {
-        if (defined('PHP_WINDOWS_VERSION_BUILD')) {
+        if (Platform::isWindows()) {
             $path = preg_replace('{^(?:file:///([a-z])/)}i', 'file://$1:/', $path);
         }
 
@@ -498,7 +498,7 @@ class Filesystem
      */
     private function unlinkImplementation($path)
     {
-        if (defined('PHP_WINDOWS_VERSION_BUILD') && is_dir($path) && is_link($path)) {
+        if (Platform::isWindows() && is_dir($path) && is_link($path)) {
             return rmdir($path);
         }
 

+ 1 - 4
src/Composer/Util/Perforce.php

@@ -51,10 +51,7 @@ class Perforce
 
     public static function create($repoConfig, $port, $path, ProcessExecutor $process, IOInterface $io)
     {
-        $isWindows = defined('PHP_WINDOWS_VERSION_BUILD');
-        $perforce = new Perforce($repoConfig, $port, $path, $process, $isWindows, $io);
-
-        return $perforce;
+        return new Perforce($repoConfig, $port, $path, $process, Platform::isWindows(), $io);
     }
 
     public static function checkServerExists($url, ProcessExecutor $processExecutor)

+ 28 - 0
src/Composer/Util/Platform.php

@@ -0,0 +1,28 @@
+<?php
+/**
+ * This file is part of Composer.
+ *
+ * (c) Nils Adermann <naderman@naderman.de>
+ *     Jordi Boggiano <j.boggiano@seld.be>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Composer\Util;
+
+/**
+ * Platform helper for uniform platform-specific tests.
+ *
+ * @author Niels Keurentjes <niels.keurentjes@omines.com>
+ */
+class Platform
+{
+    /**
+     * @return bool Whether the host machine is running a Windows OS
+     */
+    public static function isWindows()
+    {
+        return defined('PHP_WINDOWS_VERSION_BUILD');
+    }
+}

+ 1 - 1
src/Composer/Util/ProcessExecutor.php

@@ -50,7 +50,7 @@ class ProcessExecutor
 
         // make sure that null translate to the proper directory in case the dir is a symlink
         // and we call a git command, because msysgit does not handle symlinks properly
-        if (null === $cwd && defined('PHP_WINDOWS_VERSION_BUILD') && false !== strpos($command, 'git') && getcwd()) {
+        if (null === $cwd && Platform::isWindows() && false !== strpos($command, 'git') && getcwd()) {
             $cwd = realpath(getcwd());
         }
 

+ 8 - 16
src/Composer/Util/RemoteFilesystem.php

@@ -227,9 +227,7 @@ class RemoteFilesystem
         unset($tempAdditionalOptions);
         $userlandFollow = isset($options['http']['follow_location']) && !$options['http']['follow_location'];
 
-        if ($this->io->isDebug()) {
-            $this->io->writeError((substr($fileUrl, 0, 4) === 'http' ? 'Downloading ' : 'Reading ') . $fileUrl);
-        }
+        $this->io->writeError((substr($fileUrl, 0, 4) === 'http' ? 'Downloading ' : 'Reading ') . $fileUrl, true, IOInterface::DEBUG);
 
         if (isset($options['github-token'])) {
             $fileUrl .= (false === strpos($fileUrl, '?') ? '?' : '&') . 'access_token='.$options['github-token'];
@@ -609,13 +607,11 @@ class RemoteFilesystem
                 // Handle subjectAltName on lesser PHP's.
                 $certMap = $this->peerCertificateMap[$urlAuthority];
 
-                if ($this->io->isDebug()) {
-                    $this->io->writeError(sprintf(
-                        'Using <info>%s</info> as CN for subjectAltName enabled host <info>%s</info>',
-                        $certMap['cn'],
-                        $urlAuthority
-                    ));
-                }
+                $this->io->writeError(sprintf(
+                    'Using <info>%s</info> as CN for subjectAltName enabled host <info>%s</info>',
+                    $certMap['cn'],
+                    $urlAuthority
+                ), true, IOInterface::DEBUG);
 
                 $tlsOptions['ssl']['CN_match'] = $certMap['cn'];
                 $tlsOptions['ssl']['peer_fingerprint'] = $certMap['fp'];
@@ -689,9 +685,7 @@ class RemoteFilesystem
         if (!empty($targetUrl)) {
             $this->redirects++;
 
-            if ($this->io->isDebug()) {
-                $this->io->writeError(sprintf('Following redirect (%u) %s', $this->redirects, $targetUrl));
-            }
+            $this->io->writeError(sprintf('Following redirect (%u) %s', $this->redirects, $targetUrl), true, IOInterface::DEBUG);
 
             $additionalOptions['redirects'] = $this->redirects;
 
@@ -914,9 +908,7 @@ class RemoteFilesystem
             return $files[$filename];
         }
 
-        if ($this->io->isDebug()) {
-            $this->io->writeError('Checking CA file '.realpath($filename));
-        }
+        $this->io->writeError('Checking CA file '.realpath($filename), true, IOInterface::DEBUG);
         $contents = file_get_contents($filename);
 
         // assume the CA is valid if php is vulnerable to

+ 1 - 1
src/Composer/Util/TlsHelper.php

@@ -175,7 +175,7 @@ final class TlsHelper
             return self::$useOpensslParse = true;
         }
 
-        if ('\\' === DIRECTORY_SEPARATOR) {
+        if (Platform::isWindows()) {
             // Windows is probably insecure in this case.
             return self::$useOpensslParse = false;
         }

+ 9 - 0
tests/Composer/Test/ApplicationTest.php

@@ -14,6 +14,7 @@ namespace Composer\Test;
 
 use Composer\Console\Application;
 use Composer\TestCase;
+use Symfony\Component\Console\Output\OutputInterface;
 
 class ApplicationTest extends TestCase
 {
@@ -30,11 +31,19 @@ class ApplicationTest extends TestCase
 
         $index = 0;
         if (extension_loaded('xdebug')) {
+            $outputMock->expects($this->at($index++))
+                ->method("getVerbosity")
+                ->willReturn(OutputInterface::VERBOSITY_NORMAL);
+
             $outputMock->expects($this->at($index++))
                 ->method("write")
                 ->with($this->equalTo('<warning>You are running composer with xdebug enabled. This has a major impact on runtime performance. See https://getcomposer.org/xdebug</warning>'));
         }
 
+        $outputMock->expects($this->at($index++))
+            ->method("getVerbosity")
+            ->willReturn(OutputInterface::VERBOSITY_NORMAL);
+
         $outputMock->expects($this->at($index++))
             ->method("write")
             ->with($this->equalTo(sprintf('<warning>Warning: This development build of composer is over 60 days old. It is recommended to update it by running "%s self-update" to get the latest version.</warning>', $_SERVER['PHP_SELF'])));

+ 1 - 1
tests/Composer/Test/Autoload/ClassMapGeneratorTest.php

@@ -113,7 +113,7 @@ class ClassMapGeneratorTest extends TestCase
 
     /**
      * @expectedException \RuntimeException
-     * @expectedExceptionMessage Could not scan for classes inside
+     * @expectedExceptionMessage does not exist
      */
     public function testFindClassesThrowsWhenFileDoesNotExist()
     {

+ 1 - 1
tests/Composer/Test/DependencyResolver/SolverTest.php

@@ -709,7 +709,7 @@ class SolverTest extends TestCase
             $msg .= "Potential causes:\n";
             $msg .= " - A typo in the package name\n";
             $msg .= " - The package is not available in a stable-enough version according to your minimum-stability setting\n";
-            $msg .= "   see <https://groups.google.com/d/topic/composer-dev/_g3ASeIFlrc/discussion> for more details.\n\n";
+            $msg .= "   see <https://getcomposer.org/doc/04-schema.md#minimum-stability> for more details.\n\n";
             $msg .= "Read <https://getcomposer.org/doc/articles/troubleshooting.md> for further common problems.";
             $this->assertEquals($msg, $e->getMessage());
         }

+ 2 - 1
tests/Composer/Test/Downloader/GitDownloaderTest.php

@@ -16,6 +16,7 @@ use Composer\Downloader\GitDownloader;
 use Composer\Config;
 use Composer\TestCase;
 use Composer\Util\Filesystem;
+use Composer\Util\Platform;
 
 class GitDownloaderTest extends TestCase
 {
@@ -353,7 +354,7 @@ class GitDownloaderTest extends TestCase
 
     private function winCompat($cmd)
     {
-        if (defined('PHP_WINDOWS_VERSION_BUILD')) {
+        if (Platform::isWindows()) {
             $cmd = str_replace('cd ', 'cd /D ', $cmd);
             $cmd = str_replace('composerPath', getcwd().'/composerPath', $cmd);
 

+ 2 - 5
tests/Composer/Test/Downloader/HgDownloaderTest.php

@@ -15,6 +15,7 @@ namespace Composer\Test\Downloader;
 use Composer\Downloader\HgDownloader;
 use Composer\TestCase;
 use Composer\Util\Filesystem;
+use Composer\Util\Platform;
 
 class HgDownloaderTest extends TestCase
 {
@@ -156,10 +157,6 @@ class HgDownloaderTest extends TestCase
 
     private function getCmd($cmd)
     {
-        if (defined('PHP_WINDOWS_VERSION_BUILD')) {
-            return strtr($cmd, "'", '"');
-        }
-
-        return $cmd;
+        return Platform::isWindows() ? strtr($cmd, "'", '"') : $cmd;
     }
 }

+ 2 - 1
tests/Composer/Test/Downloader/XzDownloaderTest.php

@@ -15,6 +15,7 @@ namespace Composer\Test\Downloader;
 use Composer\Downloader\XzDownloader;
 use Composer\TestCase;
 use Composer\Util\Filesystem;
+use Composer\Util\Platform;
 use Composer\Util\RemoteFilesystem;
 
 class XzDownloaderTest extends TestCase
@@ -31,7 +32,7 @@ class XzDownloaderTest extends TestCase
 
     public function setUp()
     {
-        if (defined('PHP_WINDOWS_VERSION_BUILD')) {
+        if (Platform::isWindows()) {
             $this->markTestSkipped('Skip test on Windows');
         }
         $this->testDir = $this->getUniqueTmpDirectory();

+ 15 - 42
tests/Composer/Test/EventDispatcher/EventDispatcherTest.php

@@ -15,9 +15,11 @@ namespace Composer\Test\EventDispatcher;
 use Composer\EventDispatcher\Event;
 use Composer\Installer\InstallerEvents;
 use Composer\TestCase;
+use Composer\IO\BufferIO;
 use Composer\Script\ScriptEvents;
 use Composer\Script\CommandEvent;
 use Composer\Util\ProcessExecutor;
+use Symfony\Component\Console\Output\OutputInterface;
 
 class EventDispatcherTest extends TestCase
 {
@@ -101,7 +103,7 @@ class EventDispatcherTest extends TestCase
         $dispatcher = $this->getMockBuilder('Composer\EventDispatcher\EventDispatcher')
             ->setConstructorArgs(array(
                 $this->getMock('Composer\Composer'),
-                $io = $this->getMock('Composer\IO\IOInterface'),
+                $io = new BufferIO('', OutputInterface::VERBOSITY_VERBOSE),
                 $process,
             ))
             ->setMethods(array(
@@ -123,23 +125,12 @@ class EventDispatcherTest extends TestCase
             ->method('getListeners')
             ->will($this->returnValue($listeners));
 
-        $io->expects($this->any())
-            ->method('isVerbose')
-            ->willReturn(1);
-
-        $io->expects($this->at(1))
-            ->method('writeError')
-            ->with($this->equalTo('> post-install-cmd: echo -n foo'));
-
-        $io->expects($this->at(3))
-            ->method('writeError')
-            ->with($this->equalTo('> post-install-cmd: Composer\Test\EventDispatcher\EventDispatcherTest::someMethod'));
-
-        $io->expects($this->at(5))
-            ->method('writeError')
-            ->with($this->equalTo('> post-install-cmd: echo -n bar'));
-
         $dispatcher->dispatchScript(ScriptEvents::POST_INSTALL_CMD, false);
+
+        $expected = '> post-install-cmd: echo -n foo'.PHP_EOL.
+            '> post-install-cmd: Composer\Test\EventDispatcher\EventDispatcherTest::someMethod'.PHP_EOL.
+            '> post-install-cmd: echo -n bar'.PHP_EOL;
+        $this->assertEquals($expected, $io->getOutput());
     }
 
     public function testDispatcherCanExecuteComposerScriptGroups()
@@ -148,7 +139,7 @@ class EventDispatcherTest extends TestCase
         $dispatcher = $this->getMockBuilder('Composer\EventDispatcher\EventDispatcher')
             ->setConstructorArgs(array(
                 $composer = $this->getMock('Composer\Composer'),
-                $io = $this->getMock('Composer\IO\IOInterface'),
+                $io = new BufferIO('', OutputInterface::VERBOSITY_VERBOSE),
                 $process,
             ))
             ->setMethods(array(
@@ -174,31 +165,13 @@ class EventDispatcherTest extends TestCase
                 return array();
             }));
 
-        $io->expects($this->any())
-            ->method('isVerbose')
-            ->willReturn(1);
-
-        $io->expects($this->at(1))
-            ->method('writeError')
-            ->with($this->equalTo('> root: @group'));
-
-        $io->expects($this->at(3))
-            ->method('writeError')
-            ->with($this->equalTo('> group: echo -n foo'));
-
-        $io->expects($this->at(5))
-            ->method('writeError')
-            ->with($this->equalTo('> group: @subgroup'));
-
-        $io->expects($this->at(7))
-            ->method('writeError')
-            ->with($this->equalTo('> subgroup: echo -n baz'));
-
-        $io->expects($this->at(9))
-            ->method('writeError')
-            ->with($this->equalTo('> group: echo -n bar'));
-
         $dispatcher->dispatch('root', new CommandEvent('root', $composer, $io));
+        $expected = '> root: @group'.PHP_EOL.
+            '> group: echo -n foo'.PHP_EOL.
+            '> group: @subgroup'.PHP_EOL.
+            '> subgroup: echo -n baz'.PHP_EOL.
+            '> group: echo -n bar'.PHP_EOL;
+        $this->assertEquals($expected, $io->getOutput());
     }
 
     /**

+ 4 - 4
tests/Composer/Test/Fixtures/installer/abandoned-listed.test

@@ -24,12 +24,12 @@ Abandoned packages are flagged
 --RUN--
 install
 --EXPECT-OUTPUT--
-<info>Loading composer repositories with package information</info>
-<info>Installing dependencies (including require-dev)</info>
+Loading composer repositories with package information
+Installing dependencies (including require-dev)
 <warning>Package a/a is abandoned, you should avoid using it. No replacement was suggested.</warning>
 <warning>Package c/c is abandoned, you should avoid using it. Use b/b instead.</warning>
-<info>Writing lock file</info>
-<info>Generating autoload files</info>
+Writing lock file
+Generating autoload files
 
 --EXPECT--
 Installing a/a (1.0.0)

+ 4 - 4
tests/Composer/Test/Fixtures/installer/broken-deps-do-not-replace.test

@@ -21,9 +21,9 @@ Broken dependencies should not lead to a replacer being installed which is not m
 --RUN--
 install
 --EXPECT-OUTPUT--
-<info>Loading composer repositories with package information</info>
-<info>Installing dependencies (including require-dev)</info>
-<error>Your requirements could not be resolved to an installable set of packages.</error>
+Loading composer repositories with package information
+Installing dependencies (including require-dev)
+Your requirements could not be resolved to an installable set of packages.
 
   Problem 1
     - c/c 1.0.0 requires x/x 1.0 -> no matching package found.
@@ -33,7 +33,7 @@ install
 Potential causes:
  - A typo in the package name
  - The package is not available in a stable-enough version according to your minimum-stability setting
-   see <https://groups.google.com/d/topic/composer-dev/_g3ASeIFlrc/discussion> for more details.
+   see <https://getcomposer.org/doc/04-schema.md#minimum-stability> for more details.
 
 Read <https://getcomposer.org/doc/articles/troubleshooting.md> for further common problems.
 

+ 45 - 0
tests/Composer/Test/Fixtures/installer/github-issues-4319.test

@@ -0,0 +1,45 @@
+--TEST--
+
+See Github issue #4319 ( github.com/composer/composer/issues/4319 ).
+
+Present a clear error message when config.platform.php version results in a conflict rule.
+
+--CONDITION--
+!defined('HHVM_VERSION')
+
+--COMPOSER--
+{
+    "repositories": [
+        {
+            "type": "package",
+            "package": [
+                { "name": "a", "version": "1.0.0", "require": { "php": "5.5" } }
+            ]
+        }
+    ],
+    "require": {
+        "a": "~1.0"
+    },
+    "config": {
+        "platform": {
+            "php": "5.3"
+        }
+    }
+}
+
+--RUN--
+install
+
+--EXPECT-OUTPUT--
+Loading composer repositories with package information
+Installing dependencies (including require-dev)
+Your requirements could not be resolved to an installable set of packages.
+
+  Problem 1
+    - Installation request for a ~1.0 -> satisfiable by a[1.0.0].
+    - a 1.0.0 requires php 5.5 -> your PHP version (%s) overriden by "config.platform.php" version (5.3) does not satisfy that requirement.
+
+--EXPECT--
+
+--EXPECT-EXIT-CODE--
+2

+ 47 - 0
tests/Composer/Test/Fixtures/installer/github-issues-4795.test

@@ -0,0 +1,47 @@
+--TEST--
+
+See Github issue #4795 ( github.com/composer/composer/issues/4795 ).
+
+Composer\Installer::whitelistUpdateDependencies intentionally ignores root requirements even if said package is also a
+dependency of one the requirements that is whitelisted for update.
+
+--COMPOSER--
+{
+    "repositories": [
+        {
+            "type": "package",
+            "package": [
+                { "name": "a", "version": "1.0.0" },
+                { "name": "a", "version": "1.1.0" },
+                { "name": "b", "version": "1.0.0", "require": { "a": "~1.0" } },
+                { "name": "b", "version": "1.1.0", "require": { "a": "~1.1" } },
+                { "name": "c", "version": "1.0.0", "require": { "a": "~1.0" } }
+            ]
+        }
+    ],
+    "require": {
+        "a": "~1.0",
+        "b": "~1.0",
+        "c": "~1.0"
+    }
+}
+
+--INSTALLED--
+[
+    { "name": "a", "version": "1.0.0" },
+    { "name": "b", "version": "1.0.0", "require": { "a": "~1.0" } },
+    { "name": "c", "version": "1.0.0", "require": { "a": "~1.0" } }
+]
+
+--RUN--
+update B --with-dependencies
+
+--EXPECT-OUTPUT--
+<warning>Dependency "a" is also a root requirement, but is not explicitly whitelisted. Ignoring.</warning>
+Loading composer repositories with package information
+Updating dependencies (including require-dev)
+Nothing to install or update
+Writing lock file
+Generating autoload files
+
+--EXPECT--

+ 16 - 0
tests/Composer/Test/Fixtures/installer/install-self-from-root.test

@@ -0,0 +1,16 @@
+--TEST--
+Tries to require a package with the same name as the root package
+--COMPOSER--
+{
+    "name": "foo/bar",
+    "require": {
+        "foo/bar": "@dev"
+    }
+}
+--RUN--
+install
+--EXPECT-EXCEPTION--
+InvalidArgumentException
+--EXPECT--
+Root package 'foo/bar' cannot require itself in its composer.json
+Did you accidentally name your root package after an external package?

+ 4 - 4
tests/Composer/Test/Fixtures/installer/suggest-installed.test

@@ -19,10 +19,10 @@ Suggestions are not displayed for installed packages
 --RUN--
 install
 --EXPECT-OUTPUT--
-<info>Loading composer repositories with package information</info>
-<info>Installing dependencies (including require-dev)</info>
-<info>Writing lock file</info>
-<info>Generating autoload files</info>
+Loading composer repositories with package information
+Installing dependencies (including require-dev)
+Writing lock file
+Generating autoload files
 
 --EXPECT--
 Installing a/a (1.0.0)

+ 4 - 4
tests/Composer/Test/Fixtures/installer/suggest-prod.test

@@ -17,10 +17,10 @@ Suggestions are not displayed in non-dev mode
 --RUN--
 install --no-dev
 --EXPECT-OUTPUT--
-<info>Loading composer repositories with package information</info>
-<info>Installing dependencies</info>
-<info>Writing lock file</info>
-<info>Generating autoload files</info>
+Loading composer repositories with package information
+Installing dependencies
+Writing lock file
+Generating autoload files
 
 --EXPECT--
 Installing a/a (1.0.0)

+ 4 - 4
tests/Composer/Test/Fixtures/installer/suggest-replaced.test

@@ -19,10 +19,10 @@ Suggestions are not displayed for packages if they are replaced
 --RUN--
 install
 --EXPECT-OUTPUT--
-<info>Loading composer repositories with package information</info>
-<info>Installing dependencies (including require-dev)</info>
-<info>Writing lock file</info>
-<info>Generating autoload files</info>
+Loading composer repositories with package information
+Installing dependencies (including require-dev)
+Writing lock file
+Generating autoload files
 
 --EXPECT--
 Installing c/c (1.0.0)

+ 4 - 4
tests/Composer/Test/Fixtures/installer/suggest-uninstalled.test

@@ -17,11 +17,11 @@ Suggestions are displayed
 --RUN--
 install
 --EXPECT-OUTPUT--
-<info>Loading composer repositories with package information</info>
-<info>Installing dependencies (including require-dev)</info>
+Loading composer repositories with package information
+Installing dependencies (including require-dev)
 a/a suggests installing b/b (an obscure reason)
-<info>Writing lock file</info>
-<info>Generating autoload files</info>
+Writing lock file
+Generating autoload files
 
 --EXPECT--
 Installing a/a (1.0.0)

+ 20 - 7
tests/Composer/Test/IO/ConsoleIOTest.php

@@ -14,6 +14,7 @@ namespace Composer\Test\IO;
 
 use Composer\IO\ConsoleIO;
 use Composer\TestCase;
+use Symfony\Component\Console\Output\OutputInterface;
 
 class ConsoleIOTest extends TestCase
 {
@@ -40,6 +41,9 @@ class ConsoleIOTest extends TestCase
     {
         $inputMock = $this->getMock('Symfony\Component\Console\Input\InputInterface');
         $outputMock = $this->getMock('Symfony\Component\Console\Output\OutputInterface');
+        $outputMock->expects($this->once())
+            ->method('getVerbosity')
+            ->willReturn(OutputInterface::VERBOSITY_NORMAL);
         $outputMock->expects($this->once())
             ->method('write')
             ->with($this->equalTo('some information about something'), $this->equalTo(false));
@@ -53,6 +57,9 @@ class ConsoleIOTest extends TestCase
     {
         $inputMock = $this->getMock('Symfony\Component\Console\Input\InputInterface');
         $outputMock = $this->getMock('Symfony\Component\Console\Output\ConsoleOutputInterface');
+        $outputMock->expects($this->once())
+            ->method('getVerbosity')
+            ->willReturn(OutputInterface::VERBOSITY_NORMAL);
         $outputMock->expects($this->once())
             ->method('getErrorOutput')
             ->willReturn($outputMock);
@@ -69,6 +76,9 @@ class ConsoleIOTest extends TestCase
     {
         $inputMock = $this->getMock('Symfony\Component\Console\Input\InputInterface');
         $outputMock = $this->getMock('Symfony\Component\Console\Output\OutputInterface');
+        $outputMock->expects($this->once())
+            ->method('getVerbosity')
+            ->willReturn(OutputInterface::VERBOSITY_NORMAL);
         $outputMock->expects($this->once())
             ->method('write')
             ->with(
@@ -95,25 +105,28 @@ class ConsoleIOTest extends TestCase
         $inputMock = $this->getMock('Symfony\Component\Console\Input\InputInterface');
         $outputMock = $this->getMock('Symfony\Component\Console\Output\OutputInterface');
 
-        $outputMock->expects($this->at(0))
+        $outputMock->expects($this->any())
+            ->method('getVerbosity')
+            ->willReturn(OutputInterface::VERBOSITY_NORMAL);
+        $outputMock->expects($this->at(1))
             ->method('write')
             ->with($this->equalTo('something (<question>strlen = 23</question>)'));
-        $outputMock->expects($this->at(1))
+        $outputMock->expects($this->at(3))
             ->method('write')
             ->with($this->equalTo(str_repeat("\x08", 23)), $this->equalTo(false));
-        $outputMock->expects($this->at(2))
+        $outputMock->expects($this->at(5))
             ->method('write')
             ->with($this->equalTo('shorter (<comment>12</comment>)'), $this->equalTo(false));
-        $outputMock->expects($this->at(3))
+        $outputMock->expects($this->at(7))
             ->method('write')
             ->with($this->equalTo(str_repeat(' ', 11)), $this->equalTo(false));
-        $outputMock->expects($this->at(4))
+        $outputMock->expects($this->at(9))
             ->method('write')
             ->with($this->equalTo(str_repeat("\x08", 11)), $this->equalTo(false));
-        $outputMock->expects($this->at(5))
+        $outputMock->expects($this->at(11))
             ->method('write')
             ->with($this->equalTo(str_repeat("\x08", 12)), $this->equalTo(false));
-        $outputMock->expects($this->at(6))
+        $outputMock->expects($this->at(13))
             ->method('write')
             ->with($this->equalTo('something longer than initial (<info>34</info>)'));
 

+ 33 - 17
tests/Composer/Test/InstallerTest.php

@@ -26,7 +26,10 @@ use Composer\Test\Mock\InstalledFilesystemRepositoryMock;
 use Composer\Test\Mock\InstallationManagerMock;
 use Symfony\Component\Console\Input\StringInput;
 use Symfony\Component\Console\Output\StreamOutput;
+use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Component\Console\Formatter\OutputFormatter;
 use Composer\TestCase;
+use Composer\IO\BufferIO;
 
 class InstallerTest extends TestCase
 {
@@ -137,7 +140,7 @@ class InstallerTest extends TestCase
     /**
      * @dataProvider getIntegrationTests
      */
-    public function testIntegration($file, $message, $condition, $composerConfig, $lock, $installed, $run, $expectLock, $expectOutput, $expect, $expectExitCode)
+    public function testIntegration($file, $message, $condition, $composerConfig, $lock, $installed, $run, $expectLock, $expectOutput, $expect, $expectResult)
     {
         if ($condition) {
             eval('$res = '.$condition.';');
@@ -146,18 +149,15 @@ class InstallerTest extends TestCase
             }
         }
 
-        $output = null;
-        $io = $this->getMock('Composer\IO\IOInterface');
-        $callback = function ($text, $newline) use (&$output) {
-            $output .= $text . ($newline ? "\n" : "");
-        };
-        $io->expects($this->any())
-            ->method('write')
-            ->will($this->returnCallback($callback));
-        $io->expects($this->any())
-            ->method('writeError')
-            ->will($this->returnCallback($callback));
+        $io = new BufferIO('', OutputInterface::VERBOSITY_NORMAL, new OutputFormatter(false));
+
+        // Prepare for exceptions
+        if (!is_int($expectResult)) {
+            $normalizedOutput = rtrim(str_replace("\n", PHP_EOL, $expect));
+            $this->setExpectedException($expectResult, $normalizedOutput);
+        }
 
+        // Create Composer mock object according to configuration
         $composer = FactoryMock::create($io, $composerConfig);
 
         $jsonMock = $this->getMockBuilder('Composer\Json\JsonFile')->disableOriginalConstructor()->getMock();
@@ -233,8 +233,14 @@ class InstallerTest extends TestCase
         $appOutput = fopen('php://memory', 'w+');
         $result = $application->run(new StringInput($run), new StreamOutput($appOutput));
         fseek($appOutput, 0);
-        $this->assertEquals($expectExitCode, $result, $output . stream_get_contents($appOutput));
 
+        // Shouldn't check output and results if an exception was expected by this point
+        if (!is_int($expectResult)) {
+            return;
+        }
+
+        $output = str_replace("\r", '', $io->getOutput());
+        $this->assertEquals($expectResult, $result, $output . stream_get_contents($appOutput));
         if ($expectLock) {
             unset($actualLock['hash']);
             unset($actualLock['content-hash']);
@@ -246,7 +252,7 @@ class InstallerTest extends TestCase
         $this->assertSame(rtrim($expect), implode("\n", $installationManager->getTrace()));
 
         if ($expectOutput) {
-            $this->assertEquals(rtrim($expectOutput), rtrim($output));
+            $this->assertStringMatchesFormat(rtrim($expectOutput), rtrim($output));
         }
     }
 
@@ -266,7 +272,7 @@ class InstallerTest extends TestCase
             $installedDev = array();
             $lock = array();
             $expectLock = array();
-            $expectExitCode = 0;
+            $expectResult = 0;
 
             try {
                 $message = $testData['TEST'];
@@ -303,12 +309,21 @@ class InstallerTest extends TestCase
                 }
                 $expectOutput = isset($testData['EXPECT-OUTPUT']) ? $testData['EXPECT-OUTPUT'] : null;
                 $expect = $testData['EXPECT'];
-                $expectExitCode = isset($testData['EXPECT-EXIT-CODE']) ? (int) $testData['EXPECT-EXIT-CODE'] : 0;
+                if (!empty($testData['EXPECT-EXCEPTION'])) {
+                    $expectResult = $testData['EXPECT-EXCEPTION'];
+                    if (!empty($testData['EXPECT-EXIT-CODE'])) {
+                        throw new \LogicException('EXPECT-EXCEPTION and EXPECT-EXIT-CODE are mutually exclusive');
+                    }
+                } elseif (!empty($testData['EXPECT-EXIT-CODE'])) {
+                    $expectResult = (int) $testData['EXPECT-EXIT-CODE'];
+                } else {
+                    $expectResult = 0;
+                }
             } catch (\Exception $e) {
                 die(sprintf('Test "%s" is not valid: '.$e->getMessage(), str_replace($fixturesDir.'/', '', $file)));
             }
 
-            $tests[basename($file)] = array(str_replace($fixturesDir.'/', '', $file), $message, $condition, $composer, $lock, $installed, $run, $expectLock, $expectOutput, $expect, $expectExitCode);
+            $tests[basename($file)] = array(str_replace($fixturesDir.'/', '', $file), $message, $condition, $composer, $lock, $installed, $run, $expectLock, $expectOutput, $expect, $expectResult);
         }
 
         return $tests;
@@ -328,6 +343,7 @@ class InstallerTest extends TestCase
             'EXPECT-LOCK' => false,
             'EXPECT-OUTPUT' => false,
             'EXPECT-EXIT-CODE' => false,
+            'EXPECT-EXCEPTION' => false,
             'EXPECT' => true,
         );
 

+ 64 - 0
tests/Composer/Test/Package/Archiver/ZipArchiverTest.php

@@ -0,0 +1,64 @@
+<?php
+
+/*
+ * This file is part of Composer.
+ *
+ * (c) Nils Adermann <naderman@naderman.de>
+ *     Jordi Boggiano <j.boggiano@seld.be>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Composer\Test\Package\Archiver;
+
+use Composer\Package\Archiver\ZipArchiver;
+
+class ZipArchiverTest extends ArchiverTest
+{
+
+    public function testZipArchive()
+    {
+        // Set up repository
+        $this->setupDummyRepo();
+        $package = $this->setupPackage();
+        $target  = sys_get_temp_dir().'/composer_archiver_test.zip';
+
+        // Test archive
+        $archiver = new ZipArchiver();
+        $archiver->archive($package->getSourceUrl(), $target, 'zip');
+        $this->assertFileExists($target);
+
+        unlink($target);
+    }
+
+    /**
+     * Create a local dummy repository to run tests against!
+     */
+    protected function setupDummyRepo()
+    {
+        $currentWorkDir = getcwd();
+        chdir($this->testDir);
+
+        $this->writeFile('file.txt', 'content', $currentWorkDir);
+        $this->writeFile('foo/bar/baz', 'content', $currentWorkDir);
+        $this->writeFile('foo/bar/ignoreme', 'content', $currentWorkDir);
+        $this->writeFile('x/baz', 'content', $currentWorkDir);
+        $this->writeFile('x/includeme', 'content', $currentWorkDir);
+
+        chdir($currentWorkDir);
+    }
+
+    protected function writeFile($path, $content, $currentWorkDir)
+    {
+        if (!file_exists(dirname($path))) {
+            mkdir(dirname($path), 0777, true);
+        }
+
+        $result = file_put_contents($path, 'a');
+        if (false === $result) {
+            chdir($currentWorkDir);
+            throw new \RuntimeException('Could not save file.');
+        }
+    }
+}

+ 2 - 1
tests/Composer/Test/Repository/Vcs/SvnDriverTest.php

@@ -16,6 +16,7 @@ use Composer\Repository\Vcs\SvnDriver;
 use Composer\Config;
 use Composer\TestCase;
 use Composer\Util\Filesystem;
+use Composer\Util\Platform;
 
 class SvnDriverTest extends TestCase
 {
@@ -71,7 +72,7 @@ class SvnDriverTest extends TestCase
 
     private function getCmd($cmd)
     {
-        if (defined('PHP_WINDOWS_VERSION_BUILD')) {
+        if (Platform::isWindows()) {
             return strtr($cmd, "'", '"');
         }
 

+ 29 - 0
tests/Composer/Test/Util/PlatformTest.php

@@ -0,0 +1,29 @@
+<?php
+/**
+ * This file is part of Composer.
+ *
+ * (c) Nils Adermann <naderman@naderman.de>
+ *     Jordi Boggiano <j.boggiano@seld.be>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Composer\Test\Util;
+
+use Composer\Util\Platform;
+
+/**
+ * PlatformTest
+ *
+ * @author Niels Keurentjes <niels.keurentjes@omines.com>
+ */
+class PlatformTest extends \PHPUnit_Framework_TestCase
+{
+    public function testWindows()
+    {
+        // Compare 2 common tests for Windows to the built-in Windows test
+        $this->assertEquals(('\\' === DIRECTORY_SEPARATOR), Platform::isWindows());
+        $this->assertEquals(defined('PHP_WINDOWS_VERSION_MAJOR'), Platform::isWindows());
+    }
+}

+ 2 - 5
tests/Composer/Test/Util/SvnTest.php

@@ -14,6 +14,7 @@ namespace Composer\Test\Util;
 
 use Composer\Config;
 use Composer\IO\NullIO;
+use Composer\Util\Platform;
 use Composer\Util\Svn;
 
 class SvnTest extends \PHPUnit_Framework_TestCase
@@ -131,10 +132,6 @@ class SvnTest extends \PHPUnit_Framework_TestCase
 
     private function getCmd($cmd)
     {
-        if (defined('PHP_WINDOWS_VERSION_BUILD')) {
-            return strtr($cmd, "'", '"');
-        }
-
-        return $cmd;
+        return Platform::isWindows() ? strtr($cmd, "'", '"') : $cmd;
     }
 }