Sfoglia il codice sorgente

Merge branch 'master' of https://github.com/composer/composer

digitalkaoz 13 anni fa
parent
commit
0b6bfe6f63
32 ha cambiato i file con 1925 aggiunte e 297 eliminazioni
  1. 3 3
      composer.lock
  2. 37 0
      src/Composer/Command/Helper/DialogHelper.php
  3. 386 0
      src/Composer/Command/InitCommand.php
  4. 25 17
      src/Composer/Command/SearchCommand.php
  5. 1 1
      src/Composer/Command/SelfUpdateCommand.php
  6. 14 0
      src/Composer/Console/Application.php
  7. 1 11
      src/Composer/DependencyResolver/DefaultPolicy.php
  8. 1 2
      src/Composer/DependencyResolver/PolicyInterface.php
  9. 3 1
      src/Composer/DependencyResolver/Rule.php
  10. 16 46
      src/Composer/DependencyResolver/Solver.php
  11. 12 63
      src/Composer/Downloader/FileDownloader.php
  12. 5 4
      src/Composer/Downloader/VcsDownloader.php
  13. 25 12
      src/Composer/Factory.php
  14. 1 5
      src/Composer/IO/ConsoleIO.php
  15. 122 0
      src/Composer/IO/NullIO.php
  16. 8 4
      src/Composer/Installer/LibraryInstaller.php
  17. 56 20
      src/Composer/Json/JsonFile.php
  18. 10 1
      src/Composer/Repository/Vcs/GitDriver.php
  19. 1 1
      src/Composer/Repository/Vcs/HgDriver.php
  20. 3 84
      src/Composer/Repository/Vcs/VcsDriver.php
  21. 0 4
      src/Composer/Repository/VcsRepository.php
  22. 206 0
      src/Composer/Util/RemoteFilesystem.php
  23. 61 0
      tests/Composer/Test/DependencyResolver/PoolTest.php
  24. 19 2
      tests/Composer/Test/DependencyResolver/RuleSetIteratorTest.php
  25. 127 1
      tests/Composer/Test/DependencyResolver/RuleSetTest.php
  26. 170 0
      tests/Composer/Test/DependencyResolver/RuleTest.php
  27. 70 0
      tests/Composer/Test/DependencyResolver/SolverTest.php
  28. 124 0
      tests/Composer/Test/Downloader/GitDownloaderTest.php
  29. 124 0
      tests/Composer/Test/Downloader/HgDownloaderTest.php
  30. 219 0
      tests/Composer/Test/IO/ConsoleIOTest.php
  31. 27 11
      tests/Composer/Test/Installer/LibraryInstallerTest.php
  32. 48 4
      tests/Composer/Test/Json/JsonFileTest.php

+ 3 - 3
composer.lock

@@ -2,15 +2,15 @@
     "hash": "9c243b2c15fdc7c3e35c5200d704ba53",
     "packages": [
         {
-            "package": "symfony\/finder",
+            "package": "symfony\/process",
             "version": "2.1.0-dev"
         },
         {
-            "package": "symfony\/console",
+            "package": "symfony\/finder",
             "version": "2.1.0-dev"
         },
         {
-            "package": "symfony\/process",
+            "package": "symfony\/console",
             "version": "2.1.0-dev"
         }
     ]

+ 37 - 0
src/Composer/Command/Helper/DialogHelper.php

@@ -0,0 +1,37 @@
+<?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\Command\Helper;
+
+use Symfony\Component\Console\Helper\DialogHelper as BaseDialogHelper;
+use Symfony\Component\Console\Output\OutputInterface;
+
+class DialogHelper extends BaseDialogHelper
+{
+    /**
+     * Build text for asking a question. For example:
+     *
+     *  "Do you want to continue [yes]:"
+     *
+     * @param string $question The question you want to ask
+     * @param mixed $default Default value to add to message, if false no default will be shown
+     * @param string $sep Separation char for between message and user input
+     *
+     * @return string
+     */
+    public function getQuestion($question, $default = null, $sep = ':')
+    {
+        return $default !== null ?
+            sprintf('<info>%s</info> [<comment>%s</comment>]%s ', $question, $default, $sep) :
+            sprintf('<info>%s</info>%s ', $question, $sep);
+    }
+}

+ 386 - 0
src/Composer/Command/InitCommand.php

@@ -0,0 +1,386 @@
+<?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\Command;
+
+use Composer\Json\JsonFile;
+use Composer\Repository\CompositeRepository;
+use Composer\Repository\PlatformRepository;
+use Composer\Repository\ComposerRepository;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Component\Process\Process;
+use Symfony\Component\Process\ExecutableFinder;
+
+/**
+ * @author Justin Rainbow <justin.rainbow@gmail.com>
+ * @author Jordi Boggiano <j.boggiano@seld.be>
+ */
+class InitCommand extends Command
+{
+    private $gitConfig;
+    private $repos;
+
+    public function parseAuthorString($author)
+    {
+        if (preg_match('/^(?P<name>[- \.,a-z0-9]+) <(?P<email>.+?)>$/i', $author, $match)) {
+            if ($match['email'] === filter_var($match['email'], FILTER_VALIDATE_EMAIL)) {
+                return array(
+                    'name'  => trim($match['name']),
+                    'email' => $match['email']
+                );
+            }
+        }
+
+        throw new \InvalidArgumentException(
+            'Invalid author string.  Must be in the format: '.
+            'John Smith <john@example.com>'
+        );
+    }
+
+    protected function configure()
+    {
+        $this
+            ->setName('init')
+            ->setDescription('Creates a basic composer.json file in current directory.')
+            ->setDefinition(array(
+                new InputOption('name', null, InputOption::VALUE_NONE, 'Name of the package'),
+                new InputOption('description', null, InputOption::VALUE_NONE, 'Description of package'),
+                new InputOption('author', null, InputOption::VALUE_NONE, 'Author name of package'),
+                // new InputOption('version', null, InputOption::VALUE_NONE, 'Version of package'),
+                new InputOption('homepage', null, InputOption::VALUE_NONE, 'Homepage of package'),
+                new InputOption('require', null, InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'An array required packages'),
+            ))
+            ->setHelp(<<<EOT
+The <info>init</info> command creates a basic composer.json file
+in the current directory.
+
+<info>php composer.phar init</info>
+
+EOT
+            )
+        ;
+    }
+
+    protected function execute(InputInterface $input, OutputInterface $output)
+    {
+        $dialog = $this->getHelperSet()->get('dialog');
+
+        $whitelist = array('name', 'description', 'author', 'require');
+
+        $options = array_filter(array_intersect_key($input->getOptions(), array_flip($whitelist)));
+
+        if (isset($options['author'])) {
+            $options['authors'] = $this->formatAuthors($options['author']);
+            unset($options['author']);
+        }
+
+        $options['require'] = isset($options['require']) ?
+            $this->formatRequirements($options['require']) :
+            new \stdClass;
+
+        $file = new JsonFile('composer.json');
+
+        $json = $file->encode($options);
+
+        if ($input->isInteractive()) {
+            $output->writeln(array(
+                '',
+                $json,
+                ''
+            ));
+            if (!$dialog->askConfirmation($output, $dialog->getQuestion('Do you confirm generation', 'yes', '?'), true)) {
+                $output->writeln('<error>Command aborted</error>');
+
+                return 1;
+            }
+        }
+
+        $file->write($options);
+
+        if ($input->isInteractive()) {
+            $ignoreFile = realpath('.gitignore');
+
+            if (false === $ignoreFile) {
+                $ignoreFile = realpath('.') . '/.gitignore';
+            }
+
+            if (!$this->hasVendorIgnore($ignoreFile)) {
+                $question = 'Would you like the <info>vendor</info> directory added to your <info>.gitignore</info> [<comment>yes</comment>]?';
+
+                if ($dialog->askConfirmation($output, $question, true)) {
+                    $this->addVendorIgnore($ignoreFile);
+                }
+            }
+        }
+    }
+
+    protected function interact(InputInterface $input, OutputInterface $output)
+    {
+        $git = $this->getGitConfig();
+
+        $dialog = $this->getHelperSet()->get('dialog');
+        $formatter = $this->getHelperSet()->get('formatter');
+        $output->writeln(array(
+            '',
+            $formatter->formatBlock('Welcome to the Composer config generator', 'bg=blue;fg=white', true),
+            ''
+        ));
+
+        // namespace
+        $output->writeln(array(
+            '',
+            'This command will guide you through creating your composer.json config.',
+            '',
+        ));
+
+        $cwd = realpath(".");
+
+        if (false === $name = $input->getOption('name')) {
+            $name = basename($cwd);
+            if (isset($git['github.user'])) {
+                $name = $git['github.user'] . '/' . $name;
+            } elseif (!empty($_SERVER['USERNAME'])) {
+                $name = $_SERVER['USERNAME'] . '/' . $name;
+            } elseif (get_current_user()) {
+                $name = get_current_user() . '/' . $name;
+            } else {
+                // package names must be in the format foo/bar
+                $name = $name . '/' . $name;
+            }
+        }
+
+        $name = $dialog->askAndValidate(
+            $output,
+            $dialog->getQuestion('Package name (<vendor>/<name>)', $name),
+            function ($value) use ($name) {
+                if (null === $value) {
+                    return $name;
+                }
+
+                if (!preg_match('{^[a-z0-9_.-]+/[a-z0-9_.-]+$}i', $value)) {
+                    throw new \InvalidArgumentException(
+                        'The package name '.$value.' is invalid, it should have a vendor name, a forward slash, and a package name, matching: [a-z0-9_.-]+/[a-z0-9_.-]+'
+                    );
+                }
+
+                return $value;
+            }
+        );
+        $input->setOption('name', $name);
+
+        $description = $input->getOption('description') ?: false;
+        $description = $dialog->ask(
+            $output,
+            $dialog->getQuestion('Description', $description)
+        );
+        $input->setOption('description', $description);
+
+        if (false === $author = $input->getOption('author')) {
+            if (isset($git['user.name']) && isset($git['user.email'])) {
+                $author = sprintf('%s <%s>', $git['user.name'], $git['user.email']);
+            }
+        }
+
+        $self = $this;
+        $author = $dialog->askAndValidate(
+            $output,
+            $dialog->getQuestion('Author', $author),
+            function ($value) use ($self, $author) {
+                if (null === $value) {
+                    return $author;
+                }
+
+                $author = $self->parseAuthorString($value);
+
+                return sprintf('%s <%s>', $author['name'], $author['email']);
+            }
+        );
+        $input->setOption('author', $author);
+
+        $output->writeln(array(
+            '',
+            'Define your dependencies.',
+            ''
+        ));
+
+        $requirements = array();
+        if ($dialog->askConfirmation($output, $dialog->getQuestion('Would you like to define your dependencies interactively', 'yes', '?'), true)) {
+            $requirements = $this->determineRequirements($input, $output);
+        }
+        $input->setOption('require', $requirements);
+    }
+
+    protected function findPackages($name)
+    {
+        $packages = array();
+
+        // init repos
+        if (!$this->repos) {
+            $this->repos = new CompositeRepository(array(
+                new PlatformRepository,
+                new ComposerRepository(array('url' => 'http://packagist.org'))
+            ));
+        }
+
+        $token = strtolower($name);
+        foreach ($this->repos->getPackages() as $package) {
+            if (false === ($pos = strpos($package->getName(), $token))) {
+                continue;
+            }
+
+            $packages[] = $package;
+        }
+
+        return $packages;
+    }
+
+    protected function determineRequirements(InputInterface $input, OutputInterface $output)
+    {
+        $dialog = $this->getHelperSet()->get('dialog');
+        $prompt = $dialog->getQuestion('Search for a package', false, ':');
+
+        $requires = $input->getOption('require') ?: array();
+
+        while (null !== $package = $dialog->ask($output, $prompt)) {
+            $matches = $this->findPackages($package);
+
+            if (count($matches)) {
+                $output->writeln(array(
+                    '',
+                    sprintf('Found <info>%s</info> packages matching <info>%s</info>', count($matches), $package),
+                    ''
+                ));
+
+                foreach ($matches as $position => $package) {
+                    $output->writeln(sprintf(' <info>%5s</info> %s <comment>%s</comment>', "[$position]", $package->getPrettyName(), $package->getPrettyVersion()));
+                }
+
+                $output->writeln('');
+
+                $validator = function ($selection) use ($matches) {
+                    if ('' === $selection) {
+                        return false;
+                    }
+
+                    if (!is_numeric($selection) && preg_match('{^\s*(\S+) +(\S.*)\s*}', $selection, $matches)) {
+                        return $matches[1].' '.$matches[2];
+                    }
+
+                    if (!isset($matches[(int) $selection])) {
+                        throw new \Exception('Not a valid selection');
+                    }
+
+                    $package = $matches[(int) $selection];
+
+                    return sprintf('%s %s', $package->getName(), $package->getPrettyVersion());
+                };
+
+                $package = $dialog->askAndValidate($output, $dialog->getQuestion('Enter package # to add, or a <package> <version> couple if it is not listed', false, ':'), $validator, 3);
+
+                if (false !== $package) {
+                    $requires[] = $package;
+                }
+            }
+        }
+
+        return $requires;
+    }
+
+    protected function formatAuthors($author)
+    {
+        return array($this->parseAuthorString($author));
+    }
+
+    protected function formatRequirements(array $requirements)
+    {
+        $requires = array();
+        foreach ($requirements as $requirement) {
+            list($packageName, $packageVersion) = explode(" ", $requirement, 2);
+
+            $requires[$packageName] = $packageVersion;
+        }
+
+        return empty($requires) ? new \stdClass : $requires;
+    }
+
+    protected function getGitConfig()
+    {
+        if (null !== $this->gitConfig) {
+            return $this->gitConfig;
+        }
+
+        $finder = new ExecutableFinder();
+        $gitBin = $finder->find('git');
+
+        $cmd = new Process(sprintf('%s config -l', $gitBin));
+        $cmd->run();
+
+        if ($cmd->isSuccessful()) {
+            return $this->gitConfig = parse_ini_string($cmd->getOutput(), false, INI_SCANNER_RAW);
+        }
+
+        return $this->gitConfig = array();
+    }
+
+    /**
+     * Checks the local .gitignore file for the Composer vendor directory.
+     *
+     * Tested patterns include:
+     *  "/$vendor"
+     *  "$vendor"
+     *  "$vendor/"
+     *  "/$vendor/"
+     *  "/$vendor/*"
+     *  "$vendor/*"
+     *
+     * @param string $ignoreFile
+     * @param string $vendor
+     *
+     * @return Boolean
+     */
+    protected function hasVendorIgnore($ignoreFile, $vendor = 'vendor')
+    {
+        if (!file_exists($ignoreFile)) {
+            return false;
+        }
+
+        $pattern = sprintf(
+            '~^/?%s(/|/\*)?$~',
+            preg_quote($vendor, '~')
+        );
+
+        $lines = file($ignoreFile, FILE_IGNORE_NEW_LINES);
+        foreach ($lines as $line) {
+            if (preg_match($pattern, $line)) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    protected function addVendorIgnore($ignoreFile, $vendor = 'vendor')
+    {
+        $contents = "";
+        if (file_exists($ignoreFile)) {
+            $contents = file_get_contents($ignoreFile);
+
+            if ("\n" !== substr($contents, 0, -1)) {
+                $contents .= "\n";
+            }
+        }
+
+        file_put_contents($ignoreFile, $contents . $vendor. "\n");
+    }
+}

+ 25 - 17
src/Composer/Command/SearchCommand.php

@@ -15,6 +15,9 @@ namespace Composer\Command;
 use Symfony\Component\Console\Input\InputInterface;
 use Symfony\Component\Console\Input\InputArgument;
 use Symfony\Component\Console\Output\OutputInterface;
+use Composer\Repository\CompositeRepository;
+use Composer\Repository\PlatformRepository;
+use Composer\Repository\ComposerRepository;
 
 /**
  * @author Robert Schönthal <seroscho@googlemail.com>
@@ -40,27 +43,32 @@ EOT
 
     protected function execute(InputInterface $input, OutputInterface $output)
     {
-        $composer = $this->getComposer();
-
-        // create local repo, this contains all packages that are installed in the local project
-        $localRepo = $composer->getRepositoryManager()->getLocalRepository();
+        // init repos
+        $platformRepo = new PlatformRepository;
+        if ($composer = $this->getComposer(false)) {
+            $localRepo = $composer->getRepositoryManager()->getLocalRepository();
+            $installedRepo = new CompositeRepository(array($localRepo, $platformRepo));
+            $repos = new CompositeRepository(array_merge(array($installedRepo), $composer->getRepositoryManager()->getRepositories()));
+        } else {
+            $output->writeln('No composer.json found in the current directory, showing packages from packagist.org');
+            $installedRepo = $platformRepo;
+            $repos = new CompositeRepository(array($installedRepo, new ComposerRepository(array('url' => 'http://packagist.org'))));
+        }
 
         $tokens = array_map('strtolower', $input->getArgument('tokens'));
-        foreach ($composer->getRepositoryManager()->getRepositories() as $repository) {
-            foreach ($repository->getPackages() as $package) {
-                foreach ($tokens as $token) {
-                    if (false === ($pos = strpos($package->getName(), $token))) {
-                        continue;
-                    }
+        foreach ($repos->getPackages() as $package) {
+            foreach ($tokens as $token) {
+                if (false === ($pos = strpos($package->getName(), $token))) {
+                    continue;
+                }
 
-                    $state = $localRepo->hasPackage($package) ? '<info>installed</info>' : $state = '<comment>available</comment>';
+                $state = $localRepo->hasPackage($package) ? '<info>installed</info>' : $state = '<comment>available</comment>';
 
-                    $name = substr($package->getPrettyName(), 0, $pos)
-                        . '<highlight>' . substr($package->getPrettyName(), $pos, strlen($token)) . '</highlight>'
-                        . substr($package->getPrettyName(), $pos + strlen($token));
-                    $output->writeln($state . ': ' . $name . ' <comment>' . $package->getPrettyVersion() . '</comment>');
-                    continue 2;
-                }
+                $name = substr($package->getPrettyName(), 0, $pos)
+                    . '<highlight>' . substr($package->getPrettyName(), $pos, strlen($token)) . '</highlight>'
+                    . substr($package->getPrettyName(), $pos + strlen($token));
+                $output->writeln($state . ': ' . $name . ' <comment>' . $package->getPrettyVersion() . '</comment>');
+                continue 2;
             }
         }
     }

+ 1 - 1
src/Composer/Command/SelfUpdateCommand.php

@@ -42,7 +42,7 @@ EOT
     {
         $ctx = StreamContextFactory::getContext();
 
-        $latest = trim(file_get_contents('http://getcomposer.org/version'), false, $ctx);
+        $latest = trim(file_get_contents('http://getcomposer.org/version', false, $ctx));
 
         if (Composer::VERSION !== $latest) {
             $output->writeln(sprintf("Updating to version <info>%s</info>.", $latest));

+ 14 - 0
src/Composer/Console/Application.php

@@ -20,6 +20,7 @@ use Symfony\Component\Console\Formatter\OutputFormatter;
 use Symfony\Component\Console\Formatter\OutputFormatterStyle;
 use Symfony\Component\Finder\Finder;
 use Composer\Command;
+use Composer\Command\Helper\DialogHelper;
 use Composer\Composer;
 use Composer\Factory;
 use Composer\IO\IOInterface;
@@ -104,6 +105,7 @@ class Application extends BaseApplication
     {
         $this->add(new Command\AboutCommand());
         $this->add(new Command\DependsCommand());
+        $this->add(new Command\InitCommand());
         $this->add(new Command\InstallCommand());
         $this->add(new Command\UpdateCommand());
         $this->add(new Command\SearchCommand());
@@ -114,4 +116,16 @@ class Application extends BaseApplication
             $this->add(new Command\SelfUpdateCommand());
         }
     }
+
+    /**
+     * {@inheritDoc}
+     */
+    protected function getDefaultHelperSet()
+    {
+        $helperSet = parent::getDefaultHelperSet();
+
+        $helperSet->set(new DialogHelper());
+
+        return $helperSet;
+    }
 }

+ 1 - 11
src/Composer/DependencyResolver/DefaultPolicy.php

@@ -26,11 +26,6 @@ class DefaultPolicy implements PolicyInterface
         return true;
     }
 
-    public function allowDowngrade()
-    {
-        return true;
-    }
-
     public function versionCompare(PackageInterface $a, PackageInterface $b, $operator)
     {
         $constraint = new VersionConstraint($operator, $b->getVersion());
@@ -39,16 +34,11 @@ class DefaultPolicy implements PolicyInterface
         return $constraint->matchSpecific($version);
     }
 
-    public function findUpdatePackages(Solver $solver, Pool $pool, array $installedMap, PackageInterface $package, $allowAll = false)
+    public function findUpdatePackages(Solver $solver, Pool $pool, array $installedMap, PackageInterface $package)
     {
         $packages = array();
 
         foreach ($pool->whatProvides($package->getName()) as $candidate) {
-            // skip old packages unless downgrades are an option
-            if (!$allowAll && !$this->allowDowngrade() && $this->versionCompare($package, $candidate, '>')) {
-                continue;
-            }
-
             if ($candidate !== $package) {
                 $packages[] = $candidate;
             }

+ 1 - 2
src/Composer/DependencyResolver/PolicyInterface.php

@@ -21,9 +21,8 @@ use Composer\Package\PackageInterface;
 interface PolicyInterface
 {
     function allowUninstall();
-    function allowDowngrade();
     function versionCompare(PackageInterface $a, PackageInterface $b, $operator);
-    function findUpdatePackages(Solver $solver, Pool $pool, array $installedMap, PackageInterface $package, $allowAll);
+    function findUpdatePackages(Solver $solver, Pool $pool, array $installedMap, PackageInterface $package);
     function installable(Solver $solver, Pool $pool, array $installedMap, PackageInterface $package);
     function selectPreferedPackages(Pool $pool, array $installedMap, array $literals);
 }

+ 3 - 1
src/Composer/DependencyResolver/Rule.php

@@ -29,6 +29,8 @@ class Rule
     public $next1;
     public $next2;
 
+    public $ruleHash;
+
     public function __construct(array $literals, $reason, $reasonData)
     {
         // sort all packages ascending by id
@@ -85,7 +87,7 @@ class Rule
         }
 
         for ($i = 0, $n = count($this->literals); $i < $n; $i++) {
-            if (!$this->literals[$i]->getId() === $rule->literals[$i]->getId()) {
+            if ($this->literals[$i]->getId() !== $rule->literals[$i]->getId()) {
                 return false;
             }
         }

+ 16 - 46
src/Composer/DependencyResolver/Solver.php

@@ -77,7 +77,7 @@ class Solver
      *                                     that goes with the reason
      * @return Rule                        The generated rule or null if tautological
      */
-    public function createRequireRule(PackageInterface $package, array $providers, $reason, $reasonData = null)
+    protected function createRequireRule(PackageInterface $package, array $providers, $reason, $reasonData = null)
     {
         $literals = array(new Literal($package, false));
 
@@ -128,7 +128,7 @@ class Solver
      *                                     goes with the reason
      * @return Rule                        The generated rule
      */
-    public function createInstallRule(PackageInterface $package, $reason, $reasonData = null)
+    protected function createInstallRule(PackageInterface $package, $reason, $reasonData = null)
     {
         return new Rule(new Literal($package, true));
     }
@@ -146,7 +146,7 @@ class Solver
      *                            the reason
      * @return Rule               The generated rule
      */
-    public function createInstallOneOfRule(array $packages, $reason, $reasonData = null)
+    protected function createInstallOneOfRule(array $packages, $reason, $reasonData = null)
     {
         if (empty($packages)) {
             return $this->createImpossibleRule($reason, $reasonData);
@@ -172,7 +172,7 @@ class Solver
      *                                     goes with the reason
      * @return Rule                        The generated rule
      */
-    public function createRemoveRule(PackageInterface $package, $reason, $reasonData = null)
+    protected function createRemoveRule(PackageInterface $package, $reason, $reasonData = null)
     {
         return new Rule(array(new Literal($package, false)), $reason, $reasonData);
     }
@@ -191,7 +191,7 @@ class Solver
      *                                     goes with the reason
      * @return Rule                        The generated rule
      */
-    public function createConflictRule(PackageInterface $issuer, PackageInterface $provider, $reason, $reasonData = null)
+    protected function createConflictRule(PackageInterface $issuer, PackageInterface $provider, $reason, $reasonData = null)
     {
         // ignore self conflict
         if ($issuer === $provider) {
@@ -212,7 +212,7 @@ class Solver
      *                            the reason
      * @return Rule               An empty rule
      */
-    public function createImpossibleRule($reason, $reasonData = null)
+    protected function createImpossibleRule($reason, $reasonData = null)
     {
         return new Rule(array(), $reason, $reasonData);
     }
@@ -237,7 +237,7 @@ class Solver
         }
     }
 
-    public function addRulesForPackage(PackageInterface $package)
+    protected function addRulesForPackage(PackageInterface $package)
     {
         $workQueue = new \SplQueue;
         $workQueue->enqueue($package);
@@ -375,9 +375,9 @@ class Solver
      *                                   be added
      * @param bool             $allowAll Whether downgrades are allowed
      */
-    private function addRulesForUpdatePackages(PackageInterface $package, $allowAll)
+    private function addRulesForUpdatePackages(PackageInterface $package)
     {
-        $updates = $this->policy->findUpdatePackages($this, $this->pool, $this->installedMap, $package, $allowAll);
+        $updates = $this->policy->findUpdatePackages($this, $this->pool, $this->installedMap, $package);
 
         $this->addRulesForPackage($package);
 
@@ -571,7 +571,7 @@ class Solver
         }
     }
 
-    public function addChoiceRules()
+    protected function addChoiceRules()
     {
 
 // void
@@ -944,20 +944,6 @@ class Solver
         }
 
         foreach ($this->jobs as $job) {
-            switch ($job['cmd']) {
-                case 'update-all':
-                    foreach ($installedPackages as $package) {
-                        $this->updateMap[$package->getId()] = true;
-                    }
-                break;
-
-                case 'fix-all':
-                    foreach ($installedPackages as $package) {
-                        $this->fixMap[$package->getId()] = true;
-                    }
-                break;
-            }
-
             foreach ($job['packages'] as $package) {
                 switch ($job['cmd']) {
                     case 'fix':
@@ -979,7 +965,7 @@ class Solver
         }
 
         foreach ($installedPackages as $package) {
-            $this->addRulesForUpdatePackages($package, true);
+            $this->addRulesForUpdatePackages($package);
         }
 
 
@@ -997,31 +983,15 @@ class Solver
         // solver_addrpmrulesforweak(solv, &addedmap);
 
         foreach ($installedPackages as $package) {
-            // create a feature rule which allows downgrades
-            $updates = $this->policy->findUpdatePackages($this, $this->pool, $this->installedMap, $package, true);
-            $featureRule = $this->createUpdateRule($package, $updates, self::RULE_INTERNAL_ALLOW_UPDATE, (string) $package);
-
-            // create an update rule which does not allow downgrades
-            $updates = $this->policy->findUpdatePackages($this, $this->pool, $this->installedMap, $package, false);
+            $updates = $this->policy->findUpdatePackages($this, $this->pool, $this->installedMap, $package);
             $rule = $this->createUpdateRule($package, $updates, self::RULE_INTERNAL_ALLOW_UPDATE, (string) $package);
 
-            if ($rule->equals($featureRule)) {
-                if ($this->policy->allowUninstall()) {
-                    $featureRule->setWeak(true);
-                    $this->addRule(RuleSet::TYPE_FEATURE, $featureRule);
-                    $this->packageToFeatureRule[$package->getId()] = $rule;
-                } else {
-                    $this->addRule(RuleSet::TYPE_UPDATE, $rule);
-                    $this->packageToUpdateRule[$package->getId()] = $rule;
-                }
-            } else if ($this->policy->allowUninstall()) {
-                $featureRule->setWeak(true);
+            if ($this->policy->allowUninstall()) {
                 $rule->setWeak(true);
-
                 $this->addRule(RuleSet::TYPE_FEATURE, $featureRule);
-                $this->addRule(RuleSet::TYPE_UPDATE, $rule);
-
                 $this->packageToFeatureRule[$package->getId()] = $rule;
+            } else {
+                $this->addRule(RuleSet::TYPE_UPDATE, $rule);
                 $this->packageToUpdateRule[$package->getId()] = $rule;
             }
         }
@@ -1477,7 +1447,7 @@ class Solver
 
                 $l1retry = false;
 
-                if (!$num && !$l1num) {
+                if (!$num && !--$l1num) {
                     // all level 1 literals done
                     break 2;
                 }

+ 12 - 63
src/Composer/Downloader/FileDownloader.php

@@ -1,4 +1,5 @@
 <?php
+
 /*
  * This file is part of Composer.
  *
@@ -14,7 +15,7 @@ namespace Composer\Downloader;
 use Composer\IO\IOInterface;
 use Composer\Package\PackageInterface;
 use Composer\Util\Filesystem;
-use Composer\Util\StreamContextFactory;
+use Composer\Util\RemoteFilesystem;
 
 /**
  * Base downloader for file packages
@@ -26,7 +27,6 @@ use Composer\Util\StreamContextFactory;
 abstract class FileDownloader implements DownloaderInterface
 {
     protected $io;
-    private $bytesMax;
 
     /**
      * Constructor.
@@ -51,9 +51,6 @@ abstract class FileDownloader implements DownloaderInterface
      */
     public function download(PackageInterface $package, $path)
     {
-        // init the progress bar
-        $this->bytesMax = 0;
-
         $url = $package->getDistUrl();
         $checksum = $package->getDistSha1Checksum();
 
@@ -79,18 +76,9 @@ abstract class FileDownloader implements DownloaderInterface
             }
         }
 
-        $options = array();
-        if ($this->io->hasAuthorization($package->getSourceUrl())) {
-            $auth = $this->io->getAuthorization($package->getSourceUrl());
-            $authStr = base64_encode($auth['username'] . ':' . $auth['password']);
-            $options['http']['header'] = "Authorization: Basic $authStr\r\n";
-        }
-
-        $ctx = StreamContextFactory::getContext($options, array('notification' => array($this, 'callbackGet')));
-
-        $this->io->overwrite("    Downloading: <comment>connection...</comment>", false);
-        @copy($url, $fileName, $ctx);
-        $this->io->overwrite("    Downloading");
+        $rfs = new RemoteFilesystem($this->io);
+        $rfs->copy($package->getSourceUrl(), $url, $fileName);
+        $this->io->write('');
 
         if (!file_exists($fileName)) {
             throw new \UnexpectedValueException($url.' could not be saved to '.$fileName.', make sure the'
@@ -111,6 +99,13 @@ abstract class FileDownloader implements DownloaderInterface
         $contentDir = glob($path . '/*');
         if (1 === count($contentDir)) {
             $contentDir = $contentDir[0];
+
+            // Rename the content directory to avoid error when moving up
+            // a child folder with the same name
+            $temporaryName = md5(time().rand());
+            rename($contentDir, $temporaryName);
+            $contentDir = $temporaryName;
+
             foreach (array_merge(glob($contentDir . '/.*'), glob($contentDir . '/*')) as $file) {
                 if (trim(basename($file), '.')) {
                     rename($file, $path . '/' . basename($file));
@@ -141,52 +136,6 @@ abstract class FileDownloader implements DownloaderInterface
         $fs->removeDirectory($path);
     }
 
-    /**
-     * Get notification action.
-     *
-     * @param integer $notificationCode The notification code
-     * @param integer $severity         The severity level
-     * @param string  $message          The message
-     * @param integer $messageCode      The message code
-     * @param integer $bytesTransferred The loaded size
-     * @param integer $bytesMax         The total size
-     */
-    protected function callbackGet($notificationCode, $severity, $message, $messageCode, $bytesTransferred, $bytesMax)
-    {
-        switch ($notificationCode) {
-            case STREAM_NOTIFY_AUTH_REQUIRED:
-                throw new \LogicException("Authorization is required");
-                break;
-
-            case STREAM_NOTIFY_FAILURE:
-                throw new \LogicException("File not found");
-                break;
-
-            case STREAM_NOTIFY_FILE_SIZE_IS:
-                if ($this->bytesMax < $bytesMax) {
-                    $this->bytesMax = $bytesMax;
-                }
-                break;
-
-            case STREAM_NOTIFY_PROGRESS:
-                if ($this->bytesMax > 0) {
-                    $progression = 0;
-
-                    if ($this->bytesMax > 0) {
-                        $progression = round($bytesTransferred / $this->bytesMax * 100);
-                    }
-
-                    if (0 === $progression % 5) {
-                        $this->io->overwrite("    Downloading: <comment>$progression%</comment>", false);
-                    }
-                }
-                break;
-
-            default:
-                break;
-        }
-    }
-
     /**
      * Extract file to directory
      *

+ 5 - 4
src/Composer/Downloader/VcsDownloader.php

@@ -24,11 +24,13 @@ abstract class VcsDownloader implements DownloaderInterface
 {
     protected $io;
     protected $process;
+    protected $filesystem;
 
-    public function __construct(IOInterface $io, ProcessExecutor $process = null)
+    public function __construct(IOInterface $io, ProcessExecutor $process = null, Filesystem $fs = null)
     {
         $this->io = $io;
         $this->process = $process ?: new ProcessExecutor;
+        $this->filesystem = $fs ?: new Filesystem;
     }
 
     /**
@@ -74,8 +76,7 @@ abstract class VcsDownloader implements DownloaderInterface
     public function remove(PackageInterface $package, $path)
     {
         $this->enforceCleanDirectory($path);
-        $fs = new Filesystem();
-        $fs->removeDirectory($path);
+        $this->filesystem->removeDirectory($path);
     }
 
     /**
@@ -101,4 +102,4 @@ abstract class VcsDownloader implements DownloaderInterface
      * @throws \RuntimeException if the directory is not clean
      */
     abstract protected function enforceCleanDirectory($path);
-}
+}

+ 25 - 12
src/Composer/Factory.php

@@ -14,6 +14,7 @@ namespace Composer;
 
 use Composer\Json\JsonFile;
 use Composer\IO\IOInterface;
+use Composer\Repository\RepositoryManager;
 
 /**
  * Creates an configured instance of composer.
@@ -67,22 +68,25 @@ class Factory
         $binDir = getenv('COMPOSER_BIN_DIR') ?: $packageConfig['config']['bin-dir'];
 
         // initialize repository manager
-        $rm = $this->createRepositoryManager($io, $vendorDir);
+        $rm = $this->createRepositoryManager($io);
 
-        // initialize download manager
-        $dm = $this->createDownloadManager($io);
+        // load default repository unless it's explicitly disabled
+        if (!isset($packageConfig['repositories']['packagist']) || $packageConfig['repositories']['packagist'] !== false) {
+            $this->addPackagistRepository($rm);
+        }
 
-        // initialize installation manager
-        $im = $this->createInstallationManager($rm, $dm, $vendorDir, $binDir, $io);
+        // load local repository
+        $this->addLocalRepository($rm, $vendorDir);
 
         // load package
         $loader  = new Package\Loader\RootPackageLoader($rm);
         $package = $loader->load($packageConfig);
 
-        // load default repository unless it's explicitly disabled
-        if (!isset($packageConfig['repositories']['packagist']) || $packageConfig['repositories']['packagist'] !== false) {
-            $rm->addRepository(new Repository\ComposerRepository(array('url' => 'http://packagist.org')));
-        }
+        // initialize download manager
+        $dm = $this->createDownloadManager($io);
+
+        // initialize installation manager
+        $im = $this->createInstallationManager($rm, $dm, $vendorDir, $binDir, $io);
 
         // init locker
         $lockFile = substr($composerFile, -5) === '.json' ? substr($composerFile, 0, -4).'lock' : $composerFile . '.lock';
@@ -99,10 +103,9 @@ class Factory
         return $composer;
     }
 
-    protected function createRepositoryManager(IOInterface $io, $vendorDir)
+    protected function createRepositoryManager(IOInterface $io)
     {
-        $rm = new Repository\RepositoryManager($io);
-        $rm->setLocalRepository(new Repository\FilesystemRepository(new JsonFile($vendorDir.'/.composer/installed.json')));
+        $rm = new RepositoryManager($io);
         $rm->setRepositoryClass('composer', 'Composer\Repository\ComposerRepository');
         $rm->setRepositoryClass('vcs', 'Composer\Repository\VcsRepository');
         $rm->setRepositoryClass('pear', 'Composer\Repository\PearRepository');
@@ -111,6 +114,16 @@ class Factory
         return $rm;
     }
 
+    protected function addLocalRepository(RepositoryManager $rm, $vendorDir)
+    {
+        $rm->setLocalRepository(new Repository\FilesystemRepository(new JsonFile($vendorDir.'/.composer/installed.json')));
+    }
+
+    protected function addPackagistRepository(RepositoryManager $rm)
+    {
+        $rm->addRepository(new Repository\ComposerRepository(array('url' => 'http://packagist.org')));
+    }
+
     protected function createDownloadManager(IOInterface $io)
     {
         $dm = new Downloader\DownloadManager();

+ 1 - 5
src/Composer/IO/ConsoleIO.php

@@ -129,11 +129,7 @@ class ConsoleIO implements IOInterface
             $value = rtrim(shell_exec($command));
             unlink($vbscript);
 
-            for ($i = 0; $i < strlen($value); ++$i) {
-                $this->write('*', false);
-            }
-
-            $this->write('');
+            $this->write('***');
 
             return $value;
         }

+ 122 - 0
src/Composer/IO/NullIO.php

@@ -0,0 +1,122 @@
+<?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\IO;
+
+/**
+ * IOInterface that is not interactive and never writes the output
+ *
+ * @author Christophe Coevoet <stof@notk.org>
+ */
+class NullIO implements IOInterface
+{
+    /**
+     * {@inheritDoc}
+     */
+    public function isInteractive()
+    {
+        return false;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function write($messages, $newline = true)
+    {
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function overwrite($messages, $newline = true, $size = 80)
+    {
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function ask($question, $default = null)
+    {
+        return $default;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function askConfirmation($question, $default = true)
+    {
+        return $default;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function askAndValidate($question, $validator, $attempts = false, $default = null)
+    {
+        return $default;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function askAndHideAnswer($question)
+    {
+        return null;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function getLastUsername()
+    {
+        return null;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function getLastPassword()
+    {
+        return null;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function getAuthorizations()
+    {
+        return array();
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function hasAuthorization($repositoryName)
+    {
+        return false;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function getAuthorization($repositoryName)
+    {
+        return array('username' => null, 'password' => null);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function setAuthorization($repositoryName, $username, $password = null)
+    {
+    }
+}

+ 8 - 4
src/Composer/Installer/LibraryInstaller.php

@@ -53,10 +53,8 @@ class LibraryInstaller implements InstallerInterface
         $this->type = $type;
 
         $this->filesystem = new Filesystem();
-        $this->filesystem->ensureDirectoryExists($vendorDir);
-        $this->filesystem->ensureDirectoryExists($binDir);
-        $this->vendorDir = realpath($vendorDir);
-        $this->binDir = realpath($binDir);
+        $this->vendorDir = rtrim($vendorDir, '/');
+        $this->binDir = rtrim($binDir, '/');
     }
 
     /**
@@ -82,6 +80,9 @@ class LibraryInstaller implements InstallerInterface
     {
         $downloadPath = $this->getInstallPath($package);
 
+        $this->filesystem->ensureDirectoryExists($this->vendorDir);
+        $this->filesystem->ensureDirectoryExists($this->binDir);
+
         // remove the binaries if it appears the package files are missing
         if (!is_readable($downloadPath) && $this->repository->hasPackage($package)) {
             $this->removeBinaries($package);
@@ -105,6 +106,9 @@ class LibraryInstaller implements InstallerInterface
 
         $downloadPath = $this->getInstallPath($initial);
 
+        $this->filesystem->ensureDirectoryExists($this->vendorDir);
+        $this->filesystem->ensureDirectoryExists($this->binDir);
+
         $this->removeBinaries($initial);
         $this->downloadManager->update($initial, $target, $downloadPath);
         $this->installBinaries($target);

+ 56 - 20
src/Composer/Json/JsonFile.php

@@ -16,6 +16,16 @@ use Composer\Repository\RepositoryManager;
 use Composer\Composer;
 use Composer\Util\StreamContextFactory;
 
+if (!defined('JSON_UNESCAPED_SLASHES')) {
+    define('JSON_UNESCAPED_SLASHES', 64);
+}
+if (!defined('JSON_PRETTY_PRINT')) {
+    define('JSON_PRETTY_PRINT', 128);
+}
+if (!defined('JSON_UNESCAPED_UNICODE')) {
+    define('JSON_UNESCAPED_UNICODE', 256);
+}
+
 /**
  * Reads/writes json files.
  *
@@ -77,9 +87,9 @@ class JsonFile
      * Writes json file.
      *
      * @param   array   $hash   writes hash into json file
-     * @param Boolean $prettyPrint If true, output is pretty-printed
+     * @param int $options json_encode options
      */
-    public function write(array $hash, $prettyPrint = true)
+    public function write(array $hash, $options = 448)
     {
         $dir = dirname($this->path);
         if (!is_dir($dir)) {
@@ -94,7 +104,7 @@ class JsonFile
                 );
             }
         }
-        file_put_contents($this->path, static::encode($hash, $prettyPrint));
+        file_put_contents($this->path, static::encode($hash, $options). ($options & JSON_PRETTY_PRINT ? "\n" : ''));
     }
 
     /**
@@ -103,19 +113,23 @@ class JsonFile
      * Original code for this function can be found at:
      *  http://recursive-design.com/blog/2008/03/11/format-json-with-php/
      *
-     * @param array $hash Data to encode into a formatted JSON string
-     * @param Boolean $prettyPrint If true, output is pretty-printed
-     * @return string Indented version of the original JSON string
+     * @param mixed $data Data to encode into a formatted JSON string
+     * @param int $options json_encode options
+     * @return string Encoded json
      */
-    static public function encode(array $hash, $prettyPrint = true)
+    static public function encode($data, $options = 448)
     {
-        if ($prettyPrint && defined('JSON_PRETTY_PRINT')) {
-            return json_encode($hash, JSON_PRETTY_PRINT);
+        if (version_compare(PHP_VERSION, '5.4', '>=')) {
+            return json_encode($data, $options);
         }
 
-        $json = json_encode($hash);
+        $json = json_encode($data);
 
-        if (!$prettyPrint) {
+        $prettyPrint = (Boolean) ($options & JSON_PRETTY_PRINT);
+        $unescapeUnicode = (Boolean) ($options & JSON_UNESCAPED_UNICODE);
+        $unescapeSlashes = (Boolean) ($options & JSON_UNESCAPED_SLASHES);
+
+        if (!$prettyPrint && !$unescapeUnicode && !$unescapeSlashes) {
             return $json;
         }
 
@@ -124,21 +138,46 @@ class JsonFile
         $strLen = strlen($json);
         $indentStr = '    ';
         $newLine = "\n";
-        $prevChar = '';
         $outOfQuotes = true;
+        $buffer = '';
+        $noescape = true;
 
         for ($i = 0; $i <= $strLen; $i++) {
             // Grab the next character in the string
             $char = substr($json, $i, 1);
 
             // Are we inside a quoted string?
-            if ('"' === $char && ('\\' !== $prevChar || '\\\\' === substr($json, $i-2, 2))) {
+            if ('"' === $char && $noescape) {
                 $outOfQuotes = !$outOfQuotes;
-            } elseif (':' === $char && $outOfQuotes) {
+            }
+
+            if (!$outOfQuotes) {
+                $buffer .= $char;
+                $noescape = '\\' === $char ? !$noescape : true;
+                continue;
+            } elseif ('' !== $buffer) {
+                if ($unescapeSlashes) {
+                    $buffer = str_replace('\\/', '/', $buffer);
+                }
+
+                if ($unescapeUnicode && function_exists('mb_convert_encoding')) {
+                    // http://stackoverflow.com/questions/2934563/how-to-decode-unicode-escape-sequences-like-u00ed-to-proper-utf-8-encoded-cha
+                    $buffer = preg_replace_callback('/\\\\u([0-9a-f]{4})/i', function($match) {
+                        return mb_convert_encoding(pack('H*', $match[1]), 'UTF-8', 'UCS-2BE');
+                    }, $buffer);
+                }
+
+                $result .= $buffer.$char;
+                $buffer = '';
+                continue;
+            }
+
+            if (':' === $char) {
                 // Add a space after the : character
                 $char .= ' ';
-            } elseif (('}' === $char || ']' === $char) && $outOfQuotes) {
+            } elseif (('}' === $char || ']' === $char)) {
                 $pos--;
+                $prevChar = substr($json, $i - 1, 1);
 
                 if ('{' !== $prevChar && '[' !== $prevChar) {
                     // If this character is the end of an element,
@@ -153,12 +192,11 @@ class JsonFile
                 }
             }
 
-            // Add the character to the result string
             $result .= $char;
 
             // If the last character was the beginning of an element,
             // output a new line and indent the next line
-            if ((',' === $char || '{' === $char || '[' === $char) && $outOfQuotes) {
+            if (',' === $char || '{' === $char || '[' === $char) {
                 $result .= $newLine;
 
                 if ('{' === $char || '[' === $char) {
@@ -169,8 +207,6 @@ class JsonFile
                     $result .= $indentStr;
                 }
             }
-
-            $prevChar = $char;
         }
 
         return $result;
@@ -181,7 +217,7 @@ class JsonFile
      *
      * @param   string  $json   json string
      *
-     * @return  array
+     * @return  mixed
      */
     static public function parseJson($json)
     {

+ 10 - 1
src/Composer/Repository/Vcs/GitDriver.php

@@ -101,7 +101,7 @@ class GitDriver extends VcsDriver implements VcsDriverInterface
 
             if (!isset($composer['time'])) {
                 $this->process->execute(sprintf('cd %s && git log -1 --format=%%at %s', escapeshellarg($this->tmpDir), escapeshellarg($identifier)), $output);
-                $date = new \DateTime('@'.$output[0]);
+                $date = new \DateTime('@'.trim($output));
                 $composer['time'] = $date->format('Y-m-d H:i:s');
             }
             $this->infoCache[$identifier] = $composer;
@@ -169,6 +169,15 @@ class GitDriver extends VcsDriver implements VcsDriverInterface
             return true;
         }
 
+        // local filesystem
+        if (preg_match('{^(file://|/|[a-z]:[\\\\/])}i', $url)) {
+            $process = new ProcessExecutor();
+            // check whether there is a git repo in that path
+            if ($process->execute(sprintf('cd %s && git show', escapeshellarg($url)), $output) === 0) {
+                return true;
+            }
+        }
+
         if (!$deep) {
             return false;
         }

+ 1 - 1
src/Composer/Repository/Vcs/HgDriver.php

@@ -107,7 +107,7 @@ class HgDriver extends VcsDriver implements VcsDriverInterface
 
             if (!isset($composer['time'])) {
                 $this->process->execute(sprintf('cd %s && hg log --template "{date|rfc822date}" -r %s', escapeshellarg($this->tmpDir), escapeshellarg($identifier)), $output);
-                $date = new \DateTime($output[0]);
+                $date = new \DateTime(trim($output));
                 $composer['time'] = $date->format('Y-m-d H:i:s');
             }
             $this->infoCache[$identifier] = $composer;

+ 3 - 84
src/Composer/Repository/Vcs/VcsDriver.php

@@ -14,6 +14,7 @@ namespace Composer\Repository\Vcs;
 
 use Composer\IO\IOInterface;
 use Composer\Util\ProcessExecutor;
+use Composer\Util\RemoteFilesystem;
 
 /**
  * A driver implementation for driver with authorization interaction.
@@ -25,9 +26,6 @@ abstract class VcsDriver
     protected $url;
     protected $io;
     protected $process;
-    private $firstCall;
-    private $contentUrl;
-    private $content;
 
     /**
      * Constructor.
@@ -41,7 +39,6 @@ abstract class VcsDriver
         $this->url = $url;
         $this->io = $io;
         $this->process = $process ?: new ProcessExecutor;
-        $this->firstCall = true;
     }
 
     /**
@@ -68,85 +65,7 @@ abstract class VcsDriver
      */
     protected function getContents($url)
     {
-        $this->contentUrl = $url;
-        $auth = $this->io->getAuthorization($this->url);
-        $params = array();
-
-        // add authorization to curl options
-        if ($this->io->hasAuthorization($this->url)) {
-            $authStr = base64_encode($auth['username'] . ':' . $auth['password']);
-            $params['http'] = array('header' => "Authorization: Basic $authStr\r\n");
-        } else if (null !== $this->io->getLastUsername()) {
-            $authStr = base64_encode($this->io->getLastUsername() . ':' . $this->io->getLastPassword());
-            $params['http'] = array('header' => "Authorization: Basic $authStr\r\n");
-            $this->io->setAuthorization($this->url, $this->io->getLastUsername(), $this->io->getLastPassword());
-        }
-
-        $ctx = stream_context_create($params);
-        stream_context_set_params($ctx, array("notification" => array($this, 'callbackGet')));
-
-        $content = @file_get_contents($url, false, $ctx);
-
-        // content get after authorization
-        if (false === $content) {
-            $content = $this->content;
-            $this->content = null;
-            $this->contentUrl = null;
-        }
-
-        return $content;
-    }
-
-    /**
-     * Get notification action.
-     *
-     * @param integer $notificationCode The notification code
-     * @param integer $severity         The severity level
-     * @param string  $message          The message
-     * @param integer $messageCode      The message code
-     * @param integer $bytesTransferred The loaded size
-     * @param integer $bytesMax         The total size
-     */
-    protected function callbackGet($notificationCode, $severity, $message, $messageCode, $bytesTransferred, $bytesMax)
-    {
-        switch ($notificationCode) {
-            case STREAM_NOTIFY_AUTH_REQUIRED:
-            case STREAM_NOTIFY_FAILURE:
-                // for private repository returning 404 error when the authorization is incorrect
-                $auth = $this->io->getAuthorization($this->url);
-                $ps = $this->firstCall && 404 === $messageCode
-                        && null === $this->io->getLastUsername()
-                        && null === $auth['username'];
-
-                if (404 === $messageCode && !$this->firstCall) {
-                    throw new \RuntimeException("The '" . $this->contentUrl . "' URL not found");
-                }
-
-                $this->firstCall = false;
-
-                // get authorization informations
-                if (401 === $messageCode || $ps) {
-                    if (!$this->io->isInteractive()) {
-                        $mess = "The '" . $this->contentUrl . "' URL not found";
-
-                        if (401 === $code || $ps) {
-                            $mess = "The '" . $this->contentUrl . "' URL required the authorization.\nYou must be used the interactive console";
-                        }
-
-                        throw new \RuntimeException($mess);
-                    }
-
-                    $this->io->write("Authorization for <info>" . $this->contentUrl . "</info>:");
-                    $username = $this->io->ask('    Username: ');
-                    $password = $this->io->askAndHideAnswer('    Password: ');
-                    $this->io->setAuthorization($this->url, $username, $password);
-
-                    $this->content = $this->getContents($this->contentUrl);
-                }
-                break;
-
-            default:
-                break;
-        }
+        $rfs = new RemoteFilesystem($this->io);
+        return $rfs->getContents($this->url, $url, false);
     }
 }

+ 0 - 4
src/Composer/Repository/VcsRepository.php

@@ -19,10 +19,6 @@ class VcsRepository extends ArrayRepository
 
     public function __construct(array $config, IOInterface $io, array $drivers = null)
     {
-        if (!filter_var($config['url'], FILTER_VALIDATE_URL)) {
-            throw new \UnexpectedValueException('Invalid url given for PEAR repository: '.$config['url']);
-        }
-
         $this->drivers = $drivers ?: array(
             'Composer\Repository\Vcs\GitHubDriver',
             'Composer\Repository\Vcs\GitBitbucketDriver',

+ 206 - 0
src/Composer/Util/RemoteFilesystem.php

@@ -0,0 +1,206 @@
+<?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;
+
+use Composer\IO\IOInterface;
+
+/**
+ * @author François Pluchino <francois.pluchino@opendisplay.com>
+ */
+class RemoteFilesystem
+{
+    private $io;
+    private $firstCall;
+    private $bytesMax;
+    private $originUrl;
+    private $fileUrl;
+    private $fileName;
+    private $result;
+    private $progess;
+    private $lastProgress;
+
+    /**
+     * Constructor.
+     *
+     * @param IOInterface  $io  The IO instance
+     */
+    public function __construct(IOInterface $io)
+    {
+        $this->io = $io;
+    }
+
+    /**
+     * Copy the remote file in local.
+     *
+     * @param string  $originUrl The orgin URL
+     * @param string  $fileUrl   The file URL
+     * @param string  $fileName  the local filename
+     * @param boolean $progess   Display the progression
+     *
+     * @return Boolean true
+     */
+    public function copy($originUrl, $fileUrl, $fileName, $progess = true)
+    {
+        $this->get($originUrl, $fileUrl, $fileName, $progess);
+
+        return $this->result;
+    }
+
+    /**
+     * Get the content.
+     *
+     * @param string  $originUrl The orgin URL
+     * @param string  $fileUrl   The file URL
+     * @param boolean $progess   Display the progression
+     *
+     * @return string The content
+     */
+    public function getContents($originUrl, $fileUrl, $progess = true)
+    {
+        $this->get($originUrl, $fileUrl, null, $progess);
+
+        return $this->result;
+    }
+
+    /**
+     * Get file content or copy action.
+     *
+     * @param string  $originUrl The orgin URL
+     * @param string  $fileUrl   The file URL
+     * @param string  $fileName  the local filename
+     * @param boolean $progess   Display the progression
+     * @param boolean $firstCall Whether this is the first attempt at fetching this resource
+     *
+     * @throws \RuntimeException When the file could not be downloaded
+     */
+    protected function get($originUrl, $fileUrl, $fileName = null, $progess = true, $firstCall = true)
+    {
+        $this->firstCall = $firstCall;
+        $this->bytesMax = 0;
+        $this->result = null;
+        $this->originUrl = $originUrl;
+        $this->fileUrl = $fileUrl;
+        $this->fileName = $fileName;
+        $this->progress = $progess;
+        $this->lastProgress = null;
+
+        // add authorization in context
+        $options = array();
+        if ($this->io->hasAuthorization($originUrl)) {
+            $auth = $this->io->getAuthorization($originUrl);
+            $authStr = base64_encode($auth['username'] . ':' . $auth['password']);
+            $options['http']['header'] = "Authorization: Basic $authStr\r\n";
+        } elseif (null !== $this->io->getLastUsername()) {
+            $authStr = base64_encode($this->io->getLastUsername() . ':' . $this->io->getLastPassword());
+            $options['http'] = array('header' => "Authorization: Basic $authStr\r\n");
+            $this->io->setAuthorization($originUrl, $this->io->getLastUsername(), $this->io->getLastPassword());
+        }
+
+        $ctx = StreamContextFactory::getContext($options, array('notification' => array($this, 'callbackGet')));
+
+        if ($this->progress) {
+            $this->io->overwrite("    Downloading: <comment>connection...</comment>", false);
+        }
+
+        if (null !== $fileName) {
+            $result = @copy($fileUrl, $fileName, $ctx);
+        } else {
+            $result = @file_get_contents($fileUrl, false, $ctx);
+        }
+
+        // avoid overriding if content was loaded by a sub-call to get()
+        if (null === $this->result) {
+            $this->result = $result;
+        }
+
+        if ($this->progress) {
+            $this->io->overwrite("    Downloading", false);
+        }
+
+        if (false === $this->result) {
+            throw new \RuntimeException("The '$fileUrl' file could not be downloaded");
+        }
+    }
+
+    /**
+     * Get notification action.
+     *
+     * @param integer $notificationCode The notification code
+     * @param integer $severity         The severity level
+     * @param string  $message          The message
+     * @param integer $messageCode      The message code
+     * @param integer $bytesTransferred The loaded size
+     * @param integer $bytesMax         The total size
+     */
+    protected function callbackGet($notificationCode, $severity, $message, $messageCode, $bytesTransferred, $bytesMax)
+    {
+        switch ($notificationCode) {
+            case STREAM_NOTIFY_AUTH_REQUIRED:
+            case STREAM_NOTIFY_FAILURE:
+                // for private repository returning 404 error when the authorization is incorrect
+                $auth = $this->io->getAuthorization($this->originUrl);
+                $attemptAuthentication = $this->firstCall && 404 === $messageCode && null === $auth['username'];
+
+                if (404 === $messageCode && !$this->firstCall) {
+                    throw new \RuntimeException("The '" . $this->fileUrl . "' URL not found");
+                }
+
+                $this->firstCall = false;
+
+                // get authorization informations
+                if (401 === $messageCode || $attemptAuthentication) {
+                    if (!$this->io->isInteractive()) {
+                        $mess = "The '" . $this->fileUrl . "' URL was not found";
+
+                        if (401 === $code || $attemptAuthentication) {
+                            $mess = "The '" . $this->fileUrl . "' URL required authentication.\nYou must be using the interactive console";
+                        }
+
+                        throw new \RuntimeException($mess);
+                    }
+
+                    $this->io->overwrite('    Authentication required (<info>'.parse_url($this->fileUrl, PHP_URL_HOST).'</info>):');
+                    $username = $this->io->ask('      Username: ');
+                    $password = $this->io->askAndHideAnswer('      Password: ');
+                    $this->io->setAuthorization($this->originUrl, $username, $password);
+
+                    $this->get($this->originUrl, $this->fileUrl, $this->fileName, $this->progress, false);
+                }
+                break;
+
+            case STREAM_NOTIFY_FILE_SIZE_IS:
+                if ($this->bytesMax < $bytesMax) {
+                    $this->bytesMax = $bytesMax;
+                }
+                break;
+
+            case STREAM_NOTIFY_PROGRESS:
+                if ($this->bytesMax > 0 && $this->progress) {
+                    $progression = 0;
+
+                    if ($this->bytesMax > 0) {
+                        $progression = round($bytesTransferred / $this->bytesMax * 100);
+                    }
+
+                    if ((0 === $progression % 5) && $progression !== $this->lastProgress) {
+                        $this->lastProgress = $progression;
+                        $this->io->overwrite("    Downloading: <comment>$progression%</comment>", false);
+                    }
+                }
+                break;
+
+            default:
+                break;
+        }
+    }
+}

+ 61 - 0
tests/Composer/Test/DependencyResolver/PoolTest.php

@@ -30,4 +30,65 @@ class PoolTest extends TestCase
         $this->assertEquals(array($package), $pool->whatProvides('foo'));
         $this->assertEquals(array($package), $pool->whatProvides('foo'));
     }
+
+    /**
+     * @expectedException \RuntimeException
+     */
+    public function testGetPriorityForNotRegisteredRepository()
+    {
+        $pool = new Pool;
+        $repository = new ArrayRepository;
+
+        $pool->getPriority($repository);
+    }
+
+    public function testGetPriorityWhenRepositoryIsRegistered()
+    {
+        $pool = new Pool;
+        $firstRepository = new ArrayRepository;
+        $pool->addRepository($firstRepository);
+        $secondRepository = new ArrayRepository;
+        $pool->addRepository($secondRepository);
+
+        $firstPriority = $pool->getPriority($firstRepository);
+        $secondPriority = $pool->getPriority($secondRepository);
+
+        $this->assertEquals(0, $firstPriority);
+        $this->assertEquals(1, $secondPriority);
+    }
+
+    public function testPackageById()
+    {
+        $pool = new Pool;
+        $repository = new ArrayRepository;
+        $package = $this->getPackage('foo', '1');
+
+        $repository->addPackage($package);
+        $pool->addRepository($repository);
+
+        $this->assertSame($package, $pool->packageById(1));
+    }
+
+    public function testWhatProvidesWhenPackageCannotBeFound()
+    {
+        $pool = new Pool;
+
+        $this->assertEquals(array(), $pool->whatProvides('foo'));
+    }
+
+    public function testGetMaxId()
+    {
+        $pool = new Pool;
+        $repository = new ArrayRepository;
+        $firstPackage = $this->getPackage('foo', '1');
+        $secondPackage = $this->getPackage('foo1', '1');
+
+        $this->assertEquals(0, $pool->getMaxId());
+
+        $repository->addPackage($firstPackage);
+        $repository->addPackage($secondPackage);
+        $pool->addRepository($repository);
+
+        $this->assertEquals(2, $pool->getMaxId());
+    }
 }

+ 19 - 2
tests/Composer/Test/DependencyResolver/RuleSetIteratorTest.php

@@ -39,8 +39,7 @@ class ResultSetIteratorTest extends \PHPUnit_Framework_TestCase
         $ruleSetIterator = new RuleSetIterator($this->rules);
 
         $result = array();
-        foreach ($ruleSetIterator as $rule)
-        {
+        foreach ($ruleSetIterator as $rule) {
             $result[] = $rule;
         }
 
@@ -52,4 +51,22 @@ class ResultSetIteratorTest extends \PHPUnit_Framework_TestCase
 
         $this->assertEquals($expected, $result);
     }
+
+    public function testKeys()
+    {
+        $ruleSetIterator = new RuleSetIterator($this->rules);
+
+        $result = array();
+        foreach ($ruleSetIterator as $key => $rule) {
+            $result[] = $key;
+        }
+
+        $expected = array(
+            RuleSet::TYPE_JOB,
+            RuleSet::TYPE_JOB,
+            RuleSet::TYPE_UPDATE,
+        );
+
+        $this->assertEquals($expected, $result);
+    }
 }

+ 127 - 1
tests/Composer/Test/DependencyResolver/RuleSetTest.php

@@ -14,8 +14,10 @@ namespace Composer\Test\DependencyResolver;
 
 use Composer\DependencyResolver\Rule;
 use Composer\DependencyResolver\RuleSet;
+use Composer\DependencyResolver\Literal;
+use Composer\Test\TestCase;
 
-class RuleSetTest extends \PHPUnit_Framework_TestCase
+class RuleSetTest extends TestCase
 {
     public function testAdd()
     {
@@ -41,4 +43,128 @@ class RuleSetTest extends \PHPUnit_Framework_TestCase
 
         $this->assertEquals($rules, $ruleSet->getRules());
     }
+
+    /**
+     * @expectedException \OutOfBoundsException
+     */
+    public function testAddWhenTypeIsNotRecognized()
+    {
+        $ruleSet = new RuleSet;
+
+        $ruleSet->add(new Rule(array(), 'job1', null), 7);
+    }
+
+    public function testCount()
+    {
+        $ruleSet = new RuleSet;
+
+        $ruleSet->add(new Rule(array(), 'job1', null), RuleSet::TYPE_JOB);
+        $ruleSet->add(new Rule(array(), 'job2', null), RuleSet::TYPE_JOB);
+
+        $this->assertEquals(2, $ruleSet->count());
+    }
+
+    public function testRuleById()
+    {
+        $ruleSet = new RuleSet;
+
+        $rule = new Rule(array(), 'job1', null);
+        $ruleSet->add($rule, RuleSet::TYPE_JOB);
+
+        $this->assertSame($rule, $ruleSet->ruleById(0));
+    }
+
+    public function testGetIterator()
+    {
+        $ruleSet = new RuleSet;
+
+        $rule1 = new Rule(array(), 'job1', null);
+        $rule2 = new Rule(array(), 'job1', null);
+        $ruleSet->add($rule1, RuleSet::TYPE_JOB);
+        $ruleSet->add($rule2, RuleSet::TYPE_UPDATE);
+
+        $iterator = $ruleSet->getIterator();
+
+        $this->assertSame($rule1, $iterator->current());
+        $iterator->next();
+        $this->assertSame($rule2, $iterator->current());
+    }
+
+    public function testGetIteratorFor()
+    {
+        $ruleSet = new RuleSet;
+        $rule1 = new Rule(array(), 'job1', null);
+        $rule2 = new Rule(array(), 'job1', null);
+
+        $ruleSet->add($rule1, RuleSet::TYPE_JOB);
+        $ruleSet->add($rule2, RuleSet::TYPE_UPDATE);
+
+        $iterator = $ruleSet->getIteratorFor(RuleSet::TYPE_UPDATE);
+
+        $this->assertSame($rule2, $iterator->current());
+    }
+
+    public function testGetIteratorWithout()
+    {
+        $ruleSet = new RuleSet;
+        $rule1 = new Rule(array(), 'job1', null);
+        $rule2 = new Rule(array(), 'job1', null);
+
+        $ruleSet->add($rule1, RuleSet::TYPE_JOB);
+        $ruleSet->add($rule2, RuleSet::TYPE_UPDATE);
+
+        $iterator = $ruleSet->getIteratorWithout(RuleSet::TYPE_JOB);
+
+        $this->assertSame($rule2, $iterator->current());
+    }
+
+    public function testContainsEqual()
+    {
+        $ruleSet = new RuleSet;
+
+        $rule = $this->getRuleMock();
+        $rule->expects($this->any())
+            ->method('getHash')
+            ->will($this->returnValue('rule_1_hash'));
+        $rule->expects($this->any())
+            ->method('equals')
+            ->will($this->returnValue(true));
+
+        $rule2 = $this->getRuleMock();
+        $rule2->expects($this->any())
+            ->method('getHash')
+            ->will($this->returnValue('rule_2_hash'));
+
+        $rule3 = $this->getRuleMock();
+        $rule3->expects($this->any())
+            ->method('getHash')
+            ->will($this->returnValue('rule_1_hash'));
+        $rule3->expects($this->any())
+            ->method('equal')
+            ->will($this->returnValue(false));
+
+        $ruleSet->add($rule, RuleSet::TYPE_UPDATE);
+
+        $this->assertTrue($ruleSet->containsEqual($rule));
+        $this->assertFalse($ruleSet->containsEqual($rule2));
+        $this->assertFalse($ruleSet->containsEqual($rule3));
+    }
+
+    public function testToString()
+    {
+        $ruleSet = new RuleSet;
+        $literal = new Literal($this->getPackage('foo', '2.1'), true);
+        $rule = new Rule(array($literal), 'job1', null);
+
+        $ruleSet->add($rule, RuleSet::TYPE_UPDATE);
+
+        $this->assertContains('UPDATE  : (+foo-2.1.0.0)', $ruleSet->__toString());
+    }
+
+    private function getRuleMock()
+    {
+        return $this->getMockBuilder('Composer\DependencyResolver\Rule')
+            ->disableOriginalConstructor()
+            ->getMock();
+    }
 }

+ 170 - 0
tests/Composer/Test/DependencyResolver/RuleTest.php

@@ -0,0 +1,170 @@
+<?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\DependencyResolver;
+
+use Composer\DependencyResolver\Rule;
+use Composer\DependencyResolver\Literal;
+use Composer\Test\TestCase;
+
+class RuleTest extends TestCase
+{
+    public function testGetHash()
+    {
+        $rule = new Rule(array(), 'job1', null);
+        $rule->ruleHash = '123';
+
+        $this->assertEquals('123', $rule->getHash());
+    }
+
+    public function testSetAndGetId()
+    {
+        $rule = new Rule(array(), 'job1', null);
+        $rule->setId(666);
+
+        $this->assertEquals(666, $rule->getId());
+    }
+
+    public function testEqualsForRulesWithDifferentHashes()
+    {
+        $rule = new Rule(array(), 'job1', null);
+        $rule->ruleHash = '123';
+
+        $rule2 = new Rule(array(), 'job1', null);
+        $rule2->ruleHash = '321';
+
+        $this->assertFalse($rule->equals($rule2));
+    }
+
+    public function testEqualsForRulesWithDifferentLiterals()
+    {
+        $literal = $this->getLiteralMock();
+        $literal->expects($this->any())
+            ->method('getId')
+            ->will($this->returnValue(1));
+        $rule = new Rule(array($literal), 'job1', null);
+        $rule->ruleHash = '123';
+
+        $literal = $this->getLiteralMock();
+        $literal->expects($this->any())
+            ->method('getId')
+            ->will($this->returnValue(12));
+        $rule2 = new Rule(array($literal), 'job1', null);
+        $rule2->ruleHash = '123';
+
+        $this->assertFalse($rule->equals($rule2));
+    }
+
+    public function testEqualsForRulesWithDifferLiteralsQuantity()
+    {
+        $literal = $this->getLiteralMock();
+        $literal->expects($this->any())
+            ->method('getId')
+            ->will($this->returnValue(1));
+        $literal2 = $this->getLiteralMock();
+        $literal2->expects($this->any())
+            ->method('getId')
+            ->will($this->returnValue(12));
+
+        $rule = new Rule(array($literal, $literal2), 'job1', null);
+        $rule->ruleHash = '123';
+        $rule2 = new Rule(array($literal), 'job1', null);
+        $rule2->ruleHash = '123';
+
+        $this->assertFalse($rule->equals($rule2));
+    }
+
+    public function testEqualsForRulesWithThisSameLiterals()
+    {
+        $literal = $this->getLiteralMock();
+        $literal->expects($this->any())
+            ->method('getId')
+            ->will($this->returnValue(1));
+        $literal2 = $this->getLiteralMock();
+        $literal2->expects($this->any())
+            ->method('getId')
+            ->will($this->returnValue(12));
+
+        $rule = new Rule(array($literal, $literal2), 'job1', null);
+        $rule2 = new Rule(array($literal, $literal2), 'job1', null);
+
+        $this->assertTrue($rule->equals($rule2));
+    }
+
+    public function testSetAndGetType()
+    {
+        $rule = new Rule(array(), 'job1', null);
+        $rule->setType('someType');
+
+        $this->assertEquals('someType', $rule->getType());
+    }
+
+    public function testEnable()
+    {
+        $rule = new Rule(array(), 'job1', null);
+        $rule->disable();
+        $rule->enable();
+
+        $this->assertTrue($rule->isEnabled());
+        $this->assertFalse($rule->isDisabled());
+    }
+
+    public function testDisable()
+    {
+        $rule = new Rule(array(), 'job1', null);
+        $rule->enable();
+        $rule->disable();
+
+        $this->assertTrue($rule->isDisabled());
+        $this->assertFalse($rule->isEnabled());
+    }
+
+    public function testSetWeak()
+    {
+        $rule = new Rule(array(), 'job1', null);
+        $rule->setWeak(true);
+
+        $rule2 = new Rule(array(), 'job1', null);
+        $rule2->setWeak(false);
+
+        $this->assertTrue($rule->isWeak());
+        $this->assertFalse($rule2->isWeak());
+    }
+
+    public function testIsAssertions()
+    {
+        $literal = $this->getLiteralMock();
+        $literal2 = $this->getLiteralMock();
+        $rule = new Rule(array($literal, $literal2), 'job1', null);
+        $rule2 = new Rule(array($literal), 'job1', null);
+
+        $this->assertFalse($rule->isAssertion());
+        $this->assertTrue($rule2->isAssertion());
+    }
+
+    public function testToString()
+    {
+        $literal = new Literal($this->getPackage('foo', '2.1'), true);
+        $literal2 = new Literal($this->getPackage('baz', '1.1'), false);
+
+        $rule = new Rule(array($literal, $literal2), 'job1', null);
+
+        $this->assertEquals('(-baz-1.1.0.0|+foo-2.1.0.0)', $rule->__toString());
+    }
+
+    private function getLiteralMock()
+    {
+        return $this->getMockBuilder('Composer\DependencyResolver\Literal')
+            ->disableOriginalConstructor()
+            ->getMock();
+    }
+}

+ 70 - 0
tests/Composer/Test/DependencyResolver/SolverTest.php

@@ -158,6 +158,22 @@ class SolverTest extends TestCase
         $this->checkSolverResult(array());
     }
 
+    public function testSolverUpdateOnlyUpdatesSelectedPackage()
+    {
+        $this->repoInstalled->addPackage($packageA = $this->getPackage('A', '1.0'));
+        $this->repoInstalled->addPackage($packageB = $this->getPackage('B', '1.0'));
+        $this->repo->addPackage($packageAnewer = $this->getPackage('A', '1.1'));
+        $this->repo->addPackage($packageBnewer = $this->getPackage('B', '1.1'));
+
+        $this->reposComplete();
+
+        $this->request->update('A');
+
+        $this->checkSolverResult(array(
+            array('job' => 'update', 'from' => $packageA, 'to' => $packageAnewer),
+        ));
+    }
+
     public function testSolverUpdateConstrained()
     {
         $this->repoInstalled->addPackage($packageA = $this->getPackage('A', '1.0'));
@@ -192,6 +208,24 @@ class SolverTest extends TestCase
         )));
     }
 
+    public function testSolverUpdateFullyConstrainedPrunesInstalledPackages()
+    {
+        $this->repoInstalled->addPackage($packageA = $this->getPackage('A', '1.0'));
+        $this->repoInstalled->addPackage($this->getPackage('B', '1.0'));
+        $this->repo->addPackage($newPackageA = $this->getPackage('A', '1.2'));
+        $this->repo->addPackage($this->getPackage('A', '2.0'));
+        $this->reposComplete();
+
+        $this->request->install('A', new VersionConstraint('<', '2.0.0.0'));
+        $this->request->update('A', new VersionConstraint('=', '1.0.0.0'));
+
+        $this->checkSolverResult(array(array(
+            'job' => 'update',
+            'from' => $packageA,
+            'to' => $newPackageA,
+        )));
+    }
+
     public function testSolverAllJobs()
     {
         $this->repoInstalled->addPackage($packageD = $this->getPackage('D', '1.0'));
@@ -414,6 +448,42 @@ class SolverTest extends TestCase
         ));
     }
 
+    /**
+     * If a replacer D replaces B and C with C not otherwise available,
+     * D must be installed instead of the original B.
+     */
+    public function testUseReplacerIfNecessary()
+    {
+        $this->repo->addPackage($packageA = $this->getPackage('A', '1.0'));
+        $this->repo->addPackage($packageB = $this->getPackage('B', '1.0'));
+        $this->repo->addPackage($packageD = $this->getPackage('D', '1.0'));
+        $this->repo->addPackage($packageD2 = $this->getPackage('D', '1.1'));
+
+        $packageA->setRequires(array(
+            new Link('A', 'B', new VersionConstraint('>=', '1.0'), 'requires'),
+            new Link('A', 'C', new VersionConstraint('>=', '1.0'), 'requires'),
+        ));
+
+        $packageD->setReplaces(array(
+            new Link('D', 'B', new VersionConstraint('>=', '1.0'), 'replaces'),
+            new Link('D', 'C', new VersionConstraint('>=', '1.0'), 'replaces'),
+        ));
+
+        $packageD2->setReplaces(array(
+            new Link('D', 'B', new VersionConstraint('>=', '1.0'), 'replaces'),
+            new Link('D', 'C', new VersionConstraint('>=', '1.0'), 'replaces'),
+        ));
+
+        $this->reposComplete();
+
+        $this->request->install('A');
+
+        $this->checkSolverResult(array(
+            array('job' => 'install', 'package' => $packageD2),
+            array('job' => 'install', 'package' => $packageA),
+        ));
+    }
+
     protected function reposComplete()
     {
         $this->pool->addRepository($this->repoInstalled);

+ 124 - 0
tests/Composer/Test/Downloader/GitDownloaderTest.php

@@ -0,0 +1,124 @@
+<?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\Downloader;
+
+use Composer\Downloader\GitDownloader;
+
+class GitDownloaderTest extends \PHPUnit_Framework_TestCase
+{
+    /**
+     * @expectedException \InvalidArgumentException
+     */
+    public function testDownloadForPackageWithoutSourceReference()
+    {
+        $packageMock = $this->getMock('Composer\Package\PackageInterface');
+        $packageMock->expects($this->once())
+            ->method('getSourceReference')
+            ->will($this->returnValue(null));
+
+        $downloader = new GitDownloader($this->getMock('Composer\IO\IOInterface'));
+        $downloader->download($packageMock, '/path');
+    }
+
+    public function testDownload()
+    {
+        $expectedGitCommand = $this->getCmd('git clone \'https://github.com/l3l0/composer\' \'composerPath\' && cd \'composerPath\' && git checkout \'ref\' && git reset --hard \'ref\'');
+        $packageMock = $this->getMock('Composer\Package\PackageInterface');
+        $packageMock->expects($this->any())
+            ->method('getSourceReference')
+            ->will($this->returnValue('ref'));
+        $packageMock->expects($this->once())
+            ->method('getSourceUrl')
+            ->will($this->returnValue('https://github.com/l3l0/composer'));
+        $processExecutor = $this->getMock('Composer\Util\ProcessExecutor');
+        $processExecutor->expects($this->once())
+            ->method('execute')
+            ->with($this->equalTo($expectedGitCommand));
+
+        $downloader = new GitDownloader($this->getMock('Composer\IO\IOInterface'), $processExecutor);
+        $downloader->download($packageMock, 'composerPath');
+    }
+
+    /**
+     * @expectedException \InvalidArgumentException
+     */
+    public function testUpdateforPackageWithoutSourceReference()
+    {
+        $initialPackageMock = $this->getMock('Composer\Package\PackageInterface');
+        $sourcePackageMock = $this->getMock('Composer\Package\PackageInterface');
+        $sourcePackageMock->expects($this->once())
+            ->method('getSourceReference')
+            ->will($this->returnValue(null));
+
+        $downloader = new GitDownloader($this->getMock('Composer\IO\IOInterface'));
+        $downloader->update($initialPackageMock, $sourcePackageMock, '/path');
+    }
+
+    public function testUpdate()
+    {
+        $expectedGitUpdateCommand = $this->getCmd('cd \'composerPath\' && git fetch && git checkout \'ref\' && git reset --hard \'ref\'');
+        $expectedGitResetCommand = $this->getCmd('cd \'composerPath\' && git status --porcelain');
+
+        $packageMock = $this->getMock('Composer\Package\PackageInterface');
+        $packageMock->expects($this->any())
+            ->method('getSourceReference')
+            ->will($this->returnValue('ref'));
+        $packageMock->expects($this->any())
+            ->method('getSourceUrl')
+            ->will($this->returnValue('https://github.com/l3l0/composer'));
+        $processExecutor = $this->getMock('Composer\Util\ProcessExecutor');
+        $processExecutor->expects($this->at(0))
+            ->method('execute')
+            ->with($this->equalTo($expectedGitResetCommand));
+        $processExecutor->expects($this->at(1))
+            ->method('execute')
+            ->with($this->equalTo($expectedGitUpdateCommand));
+
+        $downloader = new GitDownloader($this->getMock('Composer\IO\IOInterface'), $processExecutor);
+        $downloader->update($packageMock, $packageMock, 'composerPath');
+    }
+
+    public function testRemove()
+    {
+        $expectedGitResetCommand = $this->getCmd('cd \'composerPath\' && git status --porcelain');
+
+        $packageMock = $this->getMock('Composer\Package\PackageInterface');
+        $processExecutor = $this->getMock('Composer\Util\ProcessExecutor');
+        $processExecutor->expects($this->any())
+            ->method('execute')
+            ->with($this->equalTo($expectedGitResetCommand));
+        $filesystem = $this->getMock('Composer\Util\Filesystem');
+        $filesystem->expects($this->any())
+            ->method('removeDirectory')
+            ->with($this->equalTo('composerPath'));
+
+        $downloader = new GitDownloader($this->getMock('Composer\IO\IOInterface'), $processExecutor, $filesystem);
+        $downloader->remove($packageMock, 'composerPath');
+    }
+
+    public function testGetInstallationSource()
+    {
+        $downloader = new GitDownloader($this->getMock('Composer\IO\IOInterface'));
+
+        $this->assertEquals('source', $downloader->getInstallationSource());
+    }
+
+    private function getCmd($cmd)
+    {
+        if (defined('PHP_WINDOWS_VERSION_BUILD')) {
+            return strtr($cmd, "'", '"');
+        }
+
+        return $cmd;
+    }
+}

+ 124 - 0
tests/Composer/Test/Downloader/HgDownloaderTest.php

@@ -0,0 +1,124 @@
+<?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\Downloader;
+
+use Composer\Downloader\HgDownloader;
+
+class HgDownloaderTest extends \PHPUnit_Framework_TestCase
+{
+    /**
+     * @expectedException \InvalidArgumentException
+     */
+    public function testDownloadForPackageWithoutSourceReference()
+    {
+        $packageMock = $this->getMock('Composer\Package\PackageInterface');
+        $packageMock->expects($this->once())
+            ->method('getSourceReference')
+            ->will($this->returnValue(null));
+
+        $downloader = new HgDownloader($this->getMock('Composer\IO\IOInterface'));
+        $downloader->download($packageMock, '/path');
+    }
+
+    public function testDownload()
+    {
+        $expectedGitCommand = $this->getCmd('hg clone \'https://mercurial.dev/l3l0/composer\' \'composerPath\' && cd \'composerPath\' && hg up \'ref\'');
+        $packageMock = $this->getMock('Composer\Package\PackageInterface');
+        $packageMock->expects($this->any())
+            ->method('getSourceReference')
+            ->will($this->returnValue('ref'));
+        $packageMock->expects($this->once())
+            ->method('getSourceUrl')
+            ->will($this->returnValue('https://mercurial.dev/l3l0/composer'));
+        $processExecutor = $this->getMock('Composer\Util\ProcessExecutor');
+        $processExecutor->expects($this->once())
+            ->method('execute')
+            ->with($this->equalTo($expectedGitCommand));
+
+        $downloader = new HgDownloader($this->getMock('Composer\IO\IOInterface'), $processExecutor);
+        $downloader->download($packageMock, 'composerPath');
+    }
+
+    /**
+     * @expectedException \InvalidArgumentException
+     */
+    public function testUpdateforPackageWithoutSourceReference()
+    {
+        $initialPackageMock = $this->getMock('Composer\Package\PackageInterface');
+        $sourcePackageMock = $this->getMock('Composer\Package\PackageInterface');
+        $sourcePackageMock->expects($this->once())
+            ->method('getSourceReference')
+            ->will($this->returnValue(null));
+
+        $downloader = new HgDownloader($this->getMock('Composer\IO\IOInterface'));
+        $downloader->update($initialPackageMock, $sourcePackageMock, '/path');
+    }
+
+    public function testUpdate()
+    {
+        $expectedUpdateCommand = $this->getCmd('cd \'composerPath\' && hg pull && hg up \'ref\'');
+        $expectedResetCommand = $this->getCmd('cd \'composerPath\' && hg st');
+
+        $packageMock = $this->getMock('Composer\Package\PackageInterface');
+        $packageMock->expects($this->any())
+            ->method('getSourceReference')
+            ->will($this->returnValue('ref'));
+        $packageMock->expects($this->any())
+            ->method('getSourceUrl')
+            ->will($this->returnValue('https://github.com/l3l0/composer'));
+        $processExecutor = $this->getMock('Composer\Util\ProcessExecutor');
+        $processExecutor->expects($this->at(0))
+            ->method('execute')
+            ->with($this->equalTo($expectedResetCommand));
+        $processExecutor->expects($this->at(1))
+            ->method('execute')
+            ->with($this->equalTo($expectedUpdateCommand));
+
+        $downloader = new HgDownloader($this->getMock('Composer\IO\IOInterface'), $processExecutor);
+        $downloader->update($packageMock, $packageMock, 'composerPath');
+    }
+
+    public function testRemove()
+    {
+        $expectedResetCommand = $this->getCmd('cd \'composerPath\' && hg st');
+
+        $packageMock = $this->getMock('Composer\Package\PackageInterface');
+        $processExecutor = $this->getMock('Composer\Util\ProcessExecutor');
+        $processExecutor->expects($this->any())
+            ->method('execute')
+            ->with($this->equalTo($expectedResetCommand));
+        $filesystem = $this->getMock('Composer\Util\Filesystem');
+        $filesystem->expects($this->any())
+            ->method('removeDirectory')
+            ->with($this->equalTo('composerPath'));
+
+        $downloader = new HgDownloader($this->getMock('Composer\IO\IOInterface'), $processExecutor, $filesystem);
+        $downloader->remove($packageMock, 'composerPath');
+    }
+
+    public function testGetInstallationSource()
+    {
+        $downloader = new HgDownloader($this->getMock('Composer\IO\IOInterface'));
+
+        $this->assertEquals('source', $downloader->getInstallationSource());
+    }
+
+    private function getCmd($cmd)
+    {
+        if (defined('PHP_WINDOWS_VERSION_BUILD')) {
+            return strtr($cmd, "'", '"');
+        }
+
+        return $cmd;
+    }
+}

+ 219 - 0
tests/Composer/Test/IO/ConsoleIOTest.php

@@ -0,0 +1,219 @@
+<?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\IO;
+
+use Composer\IO\ConsoleIO;
+use Composer\Test\TestCase;
+
+class ConsoleIOTest extends TestCase
+{
+    public function testIsInteractive()
+    {
+        $inputMock = $this->getMock('Symfony\Component\Console\Input\InputInterface');
+        $inputMock->expects($this->at(0))
+            ->method('isInteractive')
+            ->will($this->returnValue(true));
+        $inputMock->expects($this->at(1))
+            ->method('isInteractive')
+            ->will($this->returnValue(false));
+
+        $outputMock = $this->getMock('Symfony\Component\Console\Output\OutputInterface');
+        $helperMock = $this->getMock('Symfony\Component\Console\Helper\HelperSet');
+
+        $consoleIO = new ConsoleIO($inputMock, $outputMock, $helperMock);
+
+        $this->assertTrue($consoleIO->isInteractive());
+        $this->assertFalse($consoleIO->isInteractive());
+    }
+
+    public function testWrite()
+    {
+        $inputMock = $this->getMock('Symfony\Component\Console\Input\InputInterface');
+        $outputMock = $this->getMock('Symfony\Component\Console\Output\OutputInterface');
+        $outputMock->expects($this->once())
+            ->method('write')
+            ->with($this->equalTo('some information about something'), $this->equalTo(false));
+        $helperMock = $this->getMock('Symfony\Component\Console\Helper\HelperSet');
+
+        $consoleIO = new ConsoleIO($inputMock, $outputMock, $helperMock);
+        $consoleIO->write('some information about something', false);
+    }
+
+    public function testOverwrite()
+    {
+        $inputMock = $this->getMock('Symfony\Component\Console\Input\InputInterface');
+        $outputMock = $this->getMock('Symfony\Component\Console\Output\OutputInterface');
+        $outputMock->expects($this->at(0))
+            ->method('write')
+            ->with($this->equalTo("\x08"), $this->equalTo(false));
+        $outputMock->expects($this->at(19))
+            ->method('write')
+            ->with($this->equalTo("\x08"), $this->equalTo(false));
+        $outputMock->expects($this->at(20))
+            ->method('write')
+            ->with($this->equalTo('some information'), $this->equalTo(false));
+        $outputMock->expects($this->at(21))
+            ->method('write')
+            ->with($this->equalTo(' '), $this->equalTo(false));
+        $outputMock->expects($this->at(24))
+            ->method('write')
+            ->with($this->equalTo(' '), $this->equalTo(false));
+        $outputMock->expects($this->at(25))
+            ->method('write')
+            ->with($this->equalTo("\x08"), $this->equalTo(false));
+        $outputMock->expects($this->at(28))
+            ->method('write')
+            ->with($this->equalTo("\x08"), $this->equalTo(false));
+        $outputMock->expects($this->at(29))
+            ->method('write')
+            ->with($this->equalTo(''));
+
+        $helperMock = $this->getMock('Symfony\Component\Console\Helper\HelperSet');
+
+        $consoleIO = new ConsoleIO($inputMock, $outputMock, $helperMock);
+        $consoleIO->overwrite('some information', true, 20);
+    }
+
+    public function testAsk()
+    {
+        $inputMock = $this->getMock('Symfony\Component\Console\Input\InputInterface');
+        $outputMock = $this->getMock('Symfony\Component\Console\Output\OutputInterface');
+        $dialogMock = $this->getMock('Symfony\Component\Console\Helper\DialogHelper');
+        $helperMock = $this->getMock('Symfony\Component\Console\Helper\HelperSet');
+
+        $dialogMock->expects($this->once())
+            ->method('ask')
+            ->with($this->isInstanceOf('Symfony\Component\Console\Output\OutputInterface'),
+                $this->equalTo('Why?'),
+                $this->equalTo('default'));
+        $helperMock->expects($this->once())
+            ->method('get')
+            ->with($this->equalTo('dialog'))
+            ->will($this->returnValue($dialogMock));
+
+        $consoleIO = new ConsoleIO($inputMock, $outputMock, $helperMock);
+        $consoleIO->ask('Why?', 'default');
+    }
+
+    public function testAskConfirmation()
+    {
+        $inputMock = $this->getMock('Symfony\Component\Console\Input\InputInterface');
+        $outputMock = $this->getMock('Symfony\Component\Console\Output\OutputInterface');
+        $dialogMock = $this->getMock('Symfony\Component\Console\Helper\DialogHelper');
+        $helperMock = $this->getMock('Symfony\Component\Console\Helper\HelperSet');
+
+        $dialogMock->expects($this->once())
+            ->method('askConfirmation')
+            ->with($this->isInstanceOf('Symfony\Component\Console\Output\OutputInterface'),
+                $this->equalTo('Why?'),
+                $this->equalTo('default'));
+        $helperMock->expects($this->once())
+            ->method('get')
+            ->with($this->equalTo('dialog'))
+            ->will($this->returnValue($dialogMock));
+
+        $consoleIO = new ConsoleIO($inputMock, $outputMock, $helperMock);
+        $consoleIO->askConfirmation('Why?', 'default');
+    }
+
+    public function testAskAndValidate()
+    {
+        $inputMock = $this->getMock('Symfony\Component\Console\Input\InputInterface');
+        $outputMock = $this->getMock('Symfony\Component\Console\Output\OutputInterface');
+        $dialogMock = $this->getMock('Symfony\Component\Console\Helper\DialogHelper');
+        $helperMock = $this->getMock('Symfony\Component\Console\Helper\HelperSet');
+
+        $dialogMock->expects($this->once())
+            ->method('askAndValidate')
+            ->with($this->isInstanceOf('Symfony\Component\Console\Output\OutputInterface'),
+                $this->equalTo('Why?'),
+                $this->equalTo('validator'),
+                $this->equalTo(10),
+                $this->equalTo('default'));
+        $helperMock->expects($this->once())
+            ->method('get')
+            ->with($this->equalTo('dialog'))
+            ->will($this->returnValue($dialogMock));
+
+        $consoleIO = new ConsoleIO($inputMock, $outputMock, $helperMock);
+        $consoleIO->askAndValidate('Why?', 'validator', 10, 'default');
+    }
+
+    public function testSetAndGetAuthorization()
+    {
+        $inputMock = $this->getMock('Symfony\Component\Console\Input\InputInterface');
+        $outputMock = $this->getMock('Symfony\Component\Console\Output\OutputInterface');
+        $helperMock = $this->getMock('Symfony\Component\Console\Helper\HelperSet');
+
+        $consoleIO = new ConsoleIO($inputMock, $outputMock, $helperMock);
+        $consoleIO->setAuthorization('repoName', 'l3l0', 'passwd');
+
+        $this->assertEquals(
+            array('username' => 'l3l0', 'password' => 'passwd'),
+            $consoleIO->getAuthorization('repoName')
+        );
+    }
+
+    public function testGetAuthorizationWhenDidNotSet()
+    {
+        $inputMock = $this->getMock('Symfony\Component\Console\Input\InputInterface');
+        $outputMock = $this->getMock('Symfony\Component\Console\Output\OutputInterface');
+        $helperMock = $this->getMock('Symfony\Component\Console\Helper\HelperSet');
+
+        $consoleIO = new ConsoleIO($inputMock, $outputMock, $helperMock);
+
+        $this->assertEquals(
+            array('username' => null, 'password' => null),
+            $consoleIO->getAuthorization('repoName')
+        );
+    }
+
+    public function testHasAuthorization()
+    {
+        $inputMock = $this->getMock('Symfony\Component\Console\Input\InputInterface');
+        $outputMock = $this->getMock('Symfony\Component\Console\Output\OutputInterface');
+        $helperMock = $this->getMock('Symfony\Component\Console\Helper\HelperSet');
+
+        $consoleIO = new ConsoleIO($inputMock, $outputMock, $helperMock);
+        $consoleIO->setAuthorization('repoName', 'l3l0', 'passwd');
+
+        $this->assertTrue($consoleIO->hasAuthorization('repoName'));
+        $this->assertFalse($consoleIO->hasAuthorization('repoName2'));
+    }
+
+    public function testGetLastUsername()
+    {
+        $inputMock = $this->getMock('Symfony\Component\Console\Input\InputInterface');
+        $outputMock = $this->getMock('Symfony\Component\Console\Output\OutputInterface');
+        $helperMock = $this->getMock('Symfony\Component\Console\Helper\HelperSet');
+
+        $consoleIO = new ConsoleIO($inputMock, $outputMock, $helperMock);
+        $consoleIO->setAuthorization('repoName', 'l3l0', 'passwd');
+        $consoleIO->setAuthorization('repoName2', 'l3l02', 'passwd2');
+
+        $this->assertEquals('l3l02', $consoleIO->getLastUsername());
+    }
+
+    public function testGetLastPassword()
+    {
+        $inputMock = $this->getMock('Symfony\Component\Console\Input\InputInterface');
+        $outputMock = $this->getMock('Symfony\Component\Console\Output\OutputInterface');
+        $helperMock = $this->getMock('Symfony\Component\Console\Helper\HelperSet');
+
+        $consoleIO = new ConsoleIO($inputMock, $outputMock, $helperMock);
+        $consoleIO->setAuthorization('repoName', 'l3l0', 'passwd');
+        $consoleIO->setAuthorization('repoName2', 'l3l02', 'passwd2');
+
+        $this->assertEquals('passwd2', $consoleIO->getLastPassword());
+    }
+}

+ 27 - 11
tests/Composer/Test/Installer/LibraryInstallerTest.php

@@ -22,22 +22,22 @@ class LibraryInstallerTest extends \PHPUnit_Framework_TestCase
     private $binDir;
     private $dm;
     private $repository;
-    private $library;
     private $io;
+    private $fs;
 
     protected function setUp()
     {
-        $fs = new Filesystem;
+        $this->fs = new Filesystem;
 
         $this->vendorDir = realpath(sys_get_temp_dir()).DIRECTORY_SEPARATOR.'composer-test-vendor';
         if (is_dir($this->vendorDir)) {
-            $fs->removeDirectory($this->vendorDir);
+            $this->fs->removeDirectory($this->vendorDir);
         }
         mkdir($this->vendorDir);
 
         $this->binDir = realpath(sys_get_temp_dir()).DIRECTORY_SEPARATOR.'composer-test-bin';
         if (is_dir($this->binDir)) {
-            $fs->removeDirectory($this->binDir);
+            $this->fs->removeDirectory($this->binDir);
         }
         mkdir($this->binDir);
 
@@ -52,16 +52,20 @@ class LibraryInstallerTest extends \PHPUnit_Framework_TestCase
             ->getMock();
     }
 
-    public function testInstallerCreation()
+    public function testInstallerCreationShouldNotCreateVendorDirectory()
     {
-        $library = new LibraryInstaller($this->vendorDir, $this->binDir, $this->dm, $this->repository, $this->io);
-        $this->assertTrue(is_dir($this->vendorDir));
+        $this->fs->removeDirectory($this->vendorDir);
+
+        new LibraryInstaller($this->vendorDir, $this->binDir, $this->dm, $this->repository, $this->io);
+        $this->assertFileNotExists($this->vendorDir);
+    }
 
-        $file = sys_get_temp_dir().'/file';
-        touch($file);
+    public function testInstallerCreationShouldNotCreateBinDirectory()
+    {
+        $this->fs->removeDirectory($this->binDir);
 
-        $this->setExpectedException('RuntimeException');
-        $library = new LibraryInstaller($file, $this->binDir, $this->dm, $this->repository, $this->io);
+        new LibraryInstaller($this->vendorDir, $this->binDir, $this->dm, $this->repository, $this->io);
+        $this->assertFileNotExists($this->binDir);
     }
 
     public function testIsInstalled()
@@ -79,6 +83,10 @@ class LibraryInstallerTest extends \PHPUnit_Framework_TestCase
         $this->assertFalse($library->isInstalled($package));
     }
 
+    /**
+     * @depends testInstallerCreationShouldNotCreateVendorDirectory
+     * @depends testInstallerCreationShouldNotCreateBinDirectory
+     */
     public function testInstall()
     {
         $library = new LibraryInstaller($this->vendorDir, $this->binDir, $this->dm, $this->repository, $this->io);
@@ -100,8 +108,14 @@ class LibraryInstallerTest extends \PHPUnit_Framework_TestCase
             ->with($package);
 
         $library->install($package);
+        $this->assertFileExists($this->vendorDir, 'Vendor dir should be created');
+        $this->assertFileExists($this->binDir, 'Bin dir should be created');
     }
 
+    /**
+     * @depends testInstallerCreationShouldNotCreateVendorDirectory
+     * @depends testInstallerCreationShouldNotCreateBinDirectory
+     */
     public function testUpdate()
     {
         $library = new LibraryInstaller($this->vendorDir, $this->binDir, $this->dm, $this->repository, $this->io);
@@ -135,6 +149,8 @@ class LibraryInstallerTest extends \PHPUnit_Framework_TestCase
             ->with($target);
 
         $library->update($initial, $target);
+        $this->assertFileExists($this->vendorDir, 'Vendor dir should be created');
+        $this->assertFileExists($this->binDir, 'Bin dir should be created');
 
         $this->setExpectedException('InvalidArgumentException');
 

+ 48 - 4
tests/Composer/Test/Json/JsonFileTest.php

@@ -107,7 +107,7 @@ class JsonFileTest extends \PHPUnit_Framework_TestCase
     {
         $data = array('name' => 'composer/composer');
         $json = '{
-    "name": "composer\/composer"
+    "name": "composer/composer"
 }';
         $this->assertJsonFormat($json, $data);
     }
@@ -116,11 +116,51 @@ class JsonFileTest extends \PHPUnit_Framework_TestCase
     {
         $data = array('Metadata\\' => 'src/');
         $json = '{
-    "Metadata\\\\": "src\/"
+    "Metadata\\\\": "src/"
 }';
         $this->assertJsonFormat($json, $data);
     }
 
+    public function testEscape()
+    {
+        $data = array("Metadata\\\"" => 'src/');
+        $json = '{
+    "Metadata\\\\\\"": "src/"
+}';
+
+        $this->assertJsonFormat($json, $data);
+    }
+
+    public function testUnicode()
+    {
+        if (!function_exists('mb_convert_encoding')) {
+            $this->markTestSkipped('Test requires the mbstring extension');
+        }
+        $data = array("Žluťoučký \" kůň" => "úpěl ďábelské ódy za €");
+        $json = '{
+    "Žluťoučký \" kůň": "úpěl ďábelské ódy za €"
+}';
+
+        $this->assertJsonFormat($json, $data);
+    }
+
+    public function testEscapedSlashes()
+    {
+        if (!function_exists('mb_convert_encoding')) {
+            $this->markTestSkipped('Test requires the mbstring extension');
+        }
+        $data = "\\/fooƌ";
+
+        $this->assertJsonFormat('"\\\\\\/fooƌ"', $data, JSON_UNESCAPED_UNICODE);
+    }
+
+    public function testEscapedUnicode()
+    {
+        $data = "ƌ";
+
+        $this->assertJsonFormat('"\\u018c"', $data, 0);
+    }
+
     private function expectParseException($text, $json)
     {
         try {
@@ -131,11 +171,15 @@ class JsonFileTest extends \PHPUnit_Framework_TestCase
         }
     }
 
-    private function assertJsonFormat($json, $data)
+    private function assertJsonFormat($json, $data, $options = null)
     {
         $file = new JsonFile('composer.json');
 
-        $this->assertEquals($json, $file->encode($data));
+        if (null === $options) {
+            $this->assertEquals($json, $file->encode($data));
+        } else {
+            $this->assertEquals($json, $file->encode($data, $options));
+        }
     }
 
 }