Browse Source

Add parallel download capability to FileDownloader and derivatives

Jordi Boggiano 6 years ago
parent
commit
3dfcae99a9
50 changed files with 786 additions and 475 deletions
  1. 4 2
      src/Composer/Command/ArchiveCommand.php
  2. 7 15
      src/Composer/Command/CreateProjectCommand.php
  3. 1 1
      src/Composer/Command/StatusCommand.php
  4. 52 46
      src/Composer/Downloader/ArchiveDownloader.php
  5. 139 65
      src/Composer/Downloader/DownloadManager.php
  6. 9 1
      src/Composer/Downloader/DownloaderInterface.php
  7. 132 81
      src/Composer/Downloader/FileDownloader.php
  8. 1 1
      src/Composer/Downloader/FossilDownloader.php
  9. 1 1
      src/Composer/Downloader/GitDownloader.php
  10. 3 10
      src/Composer/Downloader/GzipDownloader.php
  11. 1 1
      src/Composer/Downloader/HgDownloader.php
  12. 9 0
      src/Composer/Downloader/PathDownloader.php
  13. 1 1
      src/Composer/Downloader/PerforceDownloader.php
  14. 3 1
      src/Composer/Downloader/PharDownloader.php
  15. 2 1
      src/Composer/Downloader/RarDownloader.php
  16. 1 1
      src/Composer/Downloader/SvnDownloader.php
  17. 3 1
      src/Composer/Downloader/TarDownloader.php
  18. 10 2
      src/Composer/Downloader/VcsDownloader.php
  19. 1 9
      src/Composer/Downloader/XzDownloader.php
  20. 1 1
      src/Composer/Downloader/ZipDownloader.php
  21. 8 6
      src/Composer/Factory.php
  22. 26 1
      src/Composer/Installer/InstallationManager.php
  23. 10 0
      src/Composer/Installer/InstallerInterface.php
  24. 9 1
      src/Composer/Installer/LibraryInstaller.php
  25. 8 0
      src/Composer/Installer/MetapackageInstaller.php
  26. 7 0
      src/Composer/Installer/NoopInstaller.php
  27. 9 1
      src/Composer/Installer/PluginInstaller.php
  28. 11 2
      src/Composer/Installer/ProjectInstaller.php
  29. 7 2
      src/Composer/Package/Archiver/ArchiveManager.php
  30. 7 6
      src/Composer/Repository/ComposerRepository.php
  31. 1 1
      src/Composer/Util/Http/CurlDownloader.php
  32. 5 1
      src/Composer/Util/HttpDownloader.php
  33. 47 0
      src/Composer/Util/Loop.php
  34. 1 1
      tests/Composer/Test/ComposerTest.php
  35. 1 1
      tests/Composer/Test/Downloader/ArchiveDownloaderTest.php
  36. 123 150
      tests/Composer/Test/Downloader/DownloadManagerTest.php
  37. 30 9
      tests/Composer/Test/Downloader/FileDownloaderTest.php
  38. 2 2
      tests/Composer/Test/Downloader/FossilDownloaderTest.php
  39. 6 6
      tests/Composer/Test/Downloader/GitDownloaderTest.php
  40. 2 2
      tests/Composer/Test/Downloader/HgDownloaderTest.php
  41. 2 2
      tests/Composer/Test/Downloader/PerforceDownloaderTest.php
  42. 7 2
      tests/Composer/Test/Downloader/XzDownloaderTest.php
  43. 27 16
      tests/Composer/Test/Downloader/ZipDownloaderTest.php
  44. 1 1
      tests/Composer/Test/EventDispatcher/EventDispatcherTest.php
  45. 26 15
      tests/Composer/Test/Installer/InstallationManagerTest.php
  46. 1 1
      tests/Composer/Test/Installer/LibraryInstallerTest.php
  47. 3 2
      tests/Composer/Test/Mock/FactoryMock.php
  48. 13 0
      tests/Composer/Test/Mock/InstallationManagerMock.php
  49. 4 2
      tests/Composer/Test/Package/Archiver/ArchiveManagerTest.php
  50. 1 1
      tests/Composer/Test/Plugin/PluginInstallerTest.php

+ 4 - 2
src/Composer/Command/ArchiveCommand.php

@@ -22,6 +22,7 @@ use Composer\Script\ScriptEvents;
 use Composer\Plugin\CommandEvent;
 use Composer\Plugin\PluginEvents;
 use Composer\Util\Filesystem;
+use Composer\Util\Loop;
 use Symfony\Component\Console\Input\InputArgument;
 use Symfony\Component\Console\Input\InputInterface;
 use Symfony\Component\Console\Input\InputOption;
@@ -104,8 +105,9 @@ EOT
             $archiveManager = $composer->getArchiveManager();
         } else {
             $factory = new Factory;
-            $downloadManager = $factory->createDownloadManager($io, $config, $factory->createHttpDownloader($io, $config));
-            $archiveManager = $factory->createArchiveManager($config, $downloadManager);
+            $httpDownloader = $factory->createHttpDownloader($io, $config);
+            $downloadManager = $factory->createDownloadManager($io, $config, $httpDownloader);
+            $archiveManager = $factory->createArchiveManager($config, $downloadManager, new Loop($httpDownloader));
         }
 
         if ($packageName) {

+ 7 - 15
src/Composer/Command/CreateProjectCommand.php

@@ -38,6 +38,7 @@ use Symfony\Component\Finder\Finder;
 use Composer\Json\JsonFile;
 use Composer\Config\JsonConfigSource;
 use Composer\Util\Filesystem;
+use Composer\Util\Loop;
 use Composer\Package\Version\VersionParser;
 
 /**
@@ -345,15 +346,18 @@ EOT
             $package = $package->getAliasOf();
         }
 
-        $dm = $this->createDownloadManager($io, $config);
+        $factory = new Factory();
+
+        $httpDownloader = $factory->createHttpDownloader($io, $config);
+        $dm = $factory->createDownloadManager($io, $config, $httpDownloader);
         $dm->setPreferSource($preferSource)
             ->setPreferDist($preferDist)
             ->setOutputProgress(!$noProgress);
 
         $projectInstaller = new ProjectInstaller($directory, $dm);
-        $im = $this->createInstallationManager();
+        $im = $factory->createInstallationManager(new Loop($httpDownloader));
         $im->addInstaller($projectInstaller);
-        $im->install(new InstalledFilesystemRepository(new JsonFile('php://memory')), new InstallOperation($package));
+        $im->execute(new InstalledFilesystemRepository(new JsonFile('php://memory')), new InstallOperation($package));
         $im->notifyInstalls($io);
 
         // collect suggestions
@@ -369,16 +373,4 @@ EOT
 
         return $installedFromVcs;
     }
-
-    protected function createDownloadManager(IOInterface $io, Config $config)
-    {
-        $factory = new Factory();
-
-        return $factory->createDownloadManager($io, $config, $factory->createHttpDownloader($io, $config));
-    }
-
-    protected function createInstallationManager()
-    {
-        return new InstallationManager();
-    }
 }

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

@@ -89,7 +89,7 @@ EOT
 
         // list packages
         foreach ($installedRepo->getCanonicalPackages() as $package) {
-            $downloader = $dm->getDownloaderForInstalledPackage($package);
+            $downloader = $dm->getDownloaderForPackage($package);
             $targetDir = $im->getInstallPath($package);
 
             if ($downloader instanceof ChangeReportInterface) {

+ 52 - 46
src/Composer/Downloader/ArchiveDownloader.php

@@ -30,33 +30,50 @@ abstract class ArchiveDownloader extends FileDownloader
      * @throws \RuntimeException
      * @throws \UnexpectedValueException
      */
-    public function download(PackageInterface $package, $path, $output = true)
+    public function install(PackageInterface $package, $path, $output = true)
     {
+        if ($output) {
+            $this->io->writeError("  - Installing <info>" . $package->getName() . "</info> (<comment>" . $package->getFullPrettyVersion() . "</comment>)");
+        }
+
         $temporaryDir = $this->config->get('vendor-dir').'/composer/'.substr(md5(uniqid('', true)), 0, 8);
-        $retries = 3;
-        while ($retries--) {
-            $fileName = parent::download($package, $path, $output);
+        $fileName = $this->getFileName($package, $path);
 
-            if ($output) {
-                $this->io->writeError(' Extracting archive', false, IOInterface::VERBOSE);
-            }
+        if ($output) {
+            $this->io->writeError('    Extracting archive', true, IOInterface::VERBOSE);
+        }
 
+        try {
+            $this->filesystem->ensureDirectoryExists($temporaryDir);
             try {
-                $this->filesystem->ensureDirectoryExists($temporaryDir);
-                try {
-                    $this->extract($fileName, $temporaryDir);
-                } catch (\Exception $e) {
-                    // remove cache if the file was corrupted
-                    parent::clearLastCacheWrite($package);
-                    throw $e;
-                }
+                $this->extract($package, $fileName, $temporaryDir);
+            } catch (\Exception $e) {
+                // remove cache if the file was corrupted
+                parent::clearLastCacheWrite($package);
+                throw $e;
+            }
 
-                $this->filesystem->unlink($fileName);
+            $this->filesystem->unlink($fileName);
+
+            $renameAsOne = false;
+            if (!file_exists($path) || ($this->filesystem->isDirEmpty($path) && $this->filesystem->removeDirectory($path))) {
+                $renameAsOne = true;
+            }
 
-                $contentDir = $this->getFolderContent($temporaryDir);
+            $contentDir = $this->getFolderContent($temporaryDir);
+            $singleDirAtTopLevel = 1 === count($contentDir) && is_dir(reset($contentDir));
 
+            if ($renameAsOne) {
+                // if the target $path is clear, we can rename the whole package in one go instead of looping over the contents
+                if ($singleDirAtTopLevel) {
+                    $extractedDir = (string) reset($contentDir);
+                } else {
+                    $extractedDir = $temporaryDir;
+                }
+                $this->filesystem->rename($extractedDir, $path);
+            } else {
                 // only one dir in the archive, extract its contents out of it
-                if (1 === count($contentDir) && is_dir(reset($contentDir))) {
+                if ($singleDirAtTopLevel) {
                     $contentDir = $this->getFolderContent((string) reset($contentDir));
                 }
 
@@ -65,35 +82,24 @@ abstract class ArchiveDownloader extends FileDownloader
                     $file = (string) $file;
                     $this->filesystem->rename($file, $path . '/' . basename($file));
                 }
+            }
 
-                $this->filesystem->removeDirectory($temporaryDir);
-                if ($this->filesystem->isDirEmpty($this->config->get('vendor-dir').'/composer/')) {
-                    $this->filesystem->removeDirectory($this->config->get('vendor-dir').'/composer/');
-                }
-                if ($this->filesystem->isDirEmpty($this->config->get('vendor-dir'))) {
-                    $this->filesystem->removeDirectory($this->config->get('vendor-dir'));
-                }
-            } catch (\Exception $e) {
-                // clean up
-                $this->filesystem->removeDirectory($path);
-                $this->filesystem->removeDirectory($temporaryDir);
-
-                // retry downloading if we have an invalid zip file
-                if ($retries && $e instanceof \UnexpectedValueException && class_exists('ZipArchive') && $e->getCode() === \ZipArchive::ER_NOZIP) {
-                    $this->io->writeError('');
-                    if ($this->io->isDebug()) {
-                        $this->io->writeError('    Invalid zip file ('.$e->getMessage().'), retrying...');
-                    } else {
-                        $this->io->writeError('    Invalid zip file, retrying...');
-                    }
-                    usleep(500000);
-                    continue;
-                }
-
-                throw $e;
+            $this->filesystem->removeDirectory($temporaryDir);
+            if ($this->filesystem->isDirEmpty($this->config->get('vendor-dir').'/composer/')) {
+                $this->filesystem->removeDirectory($this->config->get('vendor-dir').'/composer/');
+            }
+            if ($this->filesystem->isDirEmpty($this->config->get('vendor-dir'))) {
+                $this->filesystem->removeDirectory($this->config->get('vendor-dir'));
+            }
+        } catch (\Exception $e) {
+            // clean up
+            $this->filesystem->removeDirectory($path);
+            $this->filesystem->removeDirectory($temporaryDir);
+            if (file_exists($fileName)) {
+                $this->filesystem->unlink($fileName);
             }
 
-            break;
+            throw $e;
         }
     }
 
@@ -102,7 +108,7 @@ abstract class ArchiveDownloader extends FileDownloader
      */
     protected function getFileName(PackageInterface $package, $path)
     {
-        return rtrim($path.'/'.md5($path.spl_object_hash($package)).'.'.pathinfo(parse_url($package->getDistUrl(), PHP_URL_PATH), PATHINFO_EXTENSION), '.');
+        return rtrim($path.'_'.md5($path.spl_object_hash($package)).'.'.pathinfo(parse_url($package->getDistUrl(), PHP_URL_PATH), PATHINFO_EXTENSION), '.');
     }
 
     /**
@@ -113,7 +119,7 @@ abstract class ArchiveDownloader extends FileDownloader
      *
      * @throws \UnexpectedValueException If can not extract downloaded file to path
      */
-    abstract protected function extract($file, $path);
+    abstract protected function extract(PackageInterface $package, $file, $path);
 
     /**
      * Returns the folder content, excluding dotfiles

+ 139 - 65
src/Composer/Downloader/DownloadManager.php

@@ -15,6 +15,7 @@ namespace Composer\Downloader;
 use Composer\Package\PackageInterface;
 use Composer\IO\IOInterface;
 use Composer\Util\Filesystem;
+use React\Promise\PromiseInterface;
 
 /**
  * Downloaders manager.
@@ -24,6 +25,7 @@ use Composer\Util\Filesystem;
 class DownloadManager
 {
     private $io;
+    private $httpDownloader;
     private $preferDist = false;
     private $preferSource = false;
     private $packagePreferences = array();
@@ -33,9 +35,9 @@ class DownloadManager
     /**
      * Initializes download manager.
      *
-     * @param IOInterface     $io           The Input Output Interface
-     * @param bool            $preferSource prefer downloading from source
-     * @param Filesystem|null $filesystem   custom Filesystem object
+     * @param IOInterface     $io             The Input Output Interface
+     * @param bool            $preferSource   prefer downloading from source
+     * @param Filesystem|null $filesystem     custom Filesystem object
      */
     public function __construct(IOInterface $io, $preferSource = false, Filesystem $filesystem = null)
     {
@@ -140,7 +142,7 @@ class DownloadManager
      *                                           wrong type
      * @return DownloaderInterface|null
      */
-    public function getDownloaderForInstalledPackage(PackageInterface $package)
+    public function getDownloaderForPackage(PackageInterface $package)
     {
         $installationSource = $package->getInstallationSource();
 
@@ -154,7 +156,7 @@ class DownloadManager
             $downloader = $this->getDownloader($package->getSourceType());
         } else {
             throw new \InvalidArgumentException(
-                'Package '.$package.' seems not been installed properly'
+                'Package '.$package.' does not have an installation source set'
             );
         }
 
@@ -171,63 +173,95 @@ class DownloadManager
         return $downloader;
     }
 
+    public function getDownloaderType(DownloaderInterface $downloader)
+    {
+        return array_search($downloader, $this->downloaders);
+    }
+
     /**
      * Downloads package into target dir.
      *
      * @param PackageInterface $package      package instance
      * @param string           $targetDir    target dir
-     * @param bool             $preferSource prefer installation from source
+     * @param PackageInterface $prevPackage  previous package instance in case of updates
      *
+     * @return PromiseInterface
      * @throws \InvalidArgumentException if package have no urls to download from
      * @throws \RuntimeException
      */
-    public function download(PackageInterface $package, $targetDir, $preferSource = null)
+    public function download(PackageInterface $package, $targetDir, PackageInterface $prevPackage = null)
     {
-        $preferSource = null !== $preferSource ? $preferSource : $this->preferSource;
-        $sourceType = $package->getSourceType();
-        $distType = $package->getDistType();
+        $this->filesystem->ensureDirectoryExists(dirname($targetDir));
 
-        $sources = array();
-        if ($sourceType) {
-            $sources[] = 'source';
-        }
-        if ($distType) {
-            $sources[] = 'dist';
-        }
-
-        if (empty($sources)) {
-            throw new \InvalidArgumentException('Package '.$package.' must have a source or dist specified');
-        }
+        $sources = $this->getAvailableSources($package, $prevPackage);
 
-        if (!$preferSource && ($this->preferDist || 'dist' === $this->resolvePackageInstallPreference($package))) {
-            $sources = array_reverse($sources);
-        }
-
-        $this->filesystem->ensureDirectoryExists($targetDir);
+        $io = $this->io;
+        $self = $this;
 
-        foreach ($sources as $i => $source) {
-            if (isset($e)) {
-                $this->io->writeError('    <warning>Now trying to download from ' . $source . '</warning>');
+        $download = function ($retry = false) use (&$sources, $io, $package, $self, $targetDir, &$download) {
+            $source = array_shift($sources);
+            if ($retry) {
+                $io->writeError('    <warning>Now trying to download from ' . $source . '</warning>');
             }
             $package->setInstallationSource($source);
-            try {
-                $downloader = $this->getDownloaderForInstalledPackage($package);
-                if ($downloader) {
-                    $downloader->download($package, $targetDir);
-                }
-                break;
-            } catch (\RuntimeException $e) {
-                if ($i === count($sources) - 1) {
-                    throw $e;
+
+            $downloader = $self->getDownloaderForPackage($package);
+            if (!$downloader) {
+                return \React\Promise\resolve();
+            }
+
+            $handleError = function ($e) use ($sources, $source, $package, $io, $download) {
+                if ($e instanceof \RuntimeException) {
+                    if (!$sources) {
+                        throw $e;
+                    }
+
+                    $io->writeError(
+                        '    <warning>Failed to download '.
+                        $package->getPrettyName().
+                        ' from ' . $source . ': '.
+                        $e->getMessage().'</warning>'
+                    );
+
+                    return $download(true);
                 }
 
-                $this->io->writeError(
-                    '    <warning>Failed to download '.
-                    $package->getPrettyName().
-                    ' from ' . $source . ': '.
-                    $e->getMessage().'</warning>'
-                );
+                throw $e;
+            };
+
+            try {
+                $result = $downloader->download($package, $targetDir);
+            } catch (\Exception $e) {
+                return $handleError($e);
+            }
+            if (!$result instanceof PromiseInterface) {
+                return \React\Promise\resolve($result);
             }
+
+            $res = $result->then(function ($res) {
+                return $res;
+            }, $handleError);
+
+            return $res;
+        };
+
+        return $download();
+    }
+
+    /**
+     * Installs package into target dir.
+     *
+     * @param PackageInterface $package      package instance
+     * @param string           $targetDir    target dir
+     *
+     * @throws \InvalidArgumentException if package have no urls to download from
+     * @throws \RuntimeException
+     */
+    public function install(PackageInterface $package, $targetDir)
+    {
+        $downloader = $this->getDownloaderForPackage($package);
+        if ($downloader) {
+            $downloader->install($package, $targetDir);
         }
     }
 
@@ -242,31 +276,23 @@ class DownloadManager
      */
     public function update(PackageInterface $initial, PackageInterface $target, $targetDir)
     {
-        $downloader = $this->getDownloaderForInstalledPackage($initial);
-        if (!$downloader) {
-            return;
-        }
+        $downloader = $this->getDownloaderForPackage($target);
+        $initialDownloader = $this->getDownloaderForPackage($initial);
 
-        $installationSource = $initial->getInstallationSource();
-
-        if ('dist' === $installationSource) {
-            $initialType = $initial->getDistType();
-            $targetType = $target->getDistType();
-        } else {
-            $initialType = $initial->getSourceType();
-            $targetType = $target->getSourceType();
+        // no downloaders present means update from metapackage to metapackage, nothing to do
+        if (!$initialDownloader && !$downloader) {
+            return;
         }
 
-        // upgrading from a dist stable package to a dev package, force source reinstall
-        if ($target->isDev() && 'dist' === $installationSource) {
-            $downloader->remove($initial, $targetDir);
-            $this->download($target, $targetDir);
-
+        // if we have a downloader present before, but not after, the package became a metapackage and its files should be removed
+        if (!$downloader) {
+            $initialDownloader->remove($initial, $targetDir);
             return;
         }
 
+        $initialType = $this->getDownloaderType($initialDownloader);
+        $targetType = $this->getDownloaderType($downloader);
         if ($initialType === $targetType) {
-            $target->setInstallationSource($installationSource);
             try {
                 $downloader->update($initial, $target, $targetDir);
 
@@ -282,8 +308,12 @@ class DownloadManager
             }
         }
 
-        $downloader->remove($initial, $targetDir);
-        $this->download($target, $targetDir, 'source' === $installationSource);
+        // if downloader type changed, or update failed and user asks for reinstall,
+        // we wipe the dir and do a new install instead of updating it
+        if ($initialDownloader) {
+            $initialDownloader->remove($initial, $targetDir);
+        }
+        $this->install($target, $targetDir);
     }
 
     /**
@@ -294,7 +324,7 @@ class DownloadManager
      */
     public function remove(PackageInterface $package, $targetDir)
     {
-        $downloader = $this->getDownloaderForInstalledPackage($package);
+        $downloader = $this->getDownloaderForPackage($package);
         if ($downloader) {
             $downloader->remove($package, $targetDir);
         }
@@ -322,4 +352,48 @@ class DownloadManager
 
         return $package->isDev() ? 'source' : 'dist';
     }
+
+    /**
+     * @return string[]
+     */
+    private function getAvailableSources(PackageInterface $package, PackageInterface $prevPackage = null)
+    {
+        $sourceType = $package->getSourceType();
+        $distType = $package->getDistType();
+
+        // add source before dist by default
+        $sources = array();
+        if ($sourceType) {
+            $sources[] = 'source';
+        }
+        if ($distType) {
+            $sources[] = 'dist';
+        }
+
+        if (empty($sources)) {
+            throw new \InvalidArgumentException('Package '.$package.' must have a source or dist specified');
+        }
+
+        if (
+            $prevPackage
+            // if we are updating, we want to keep the same source as the previously installed package (if available in the new one)
+            && in_array($prevPackage->getInstallationSource(), $sources, true)
+            // unless the previous package was stable dist (by default) and the new package is dev, then we allow the new default to take over
+            && !(!$prevPackage->isDev() && $prevPackage->getInstallationSource() === 'dist' && $package->isDev())
+        ) {
+            $prevSource = $prevPackage->getInstallationSource();
+            usort($sources, function ($a, $b) use ($prevSource) {
+                return $a === $prevSource ? -1 : 1;
+            });
+
+            return $sources;
+        }
+
+        // reverse sources in case dist is the preferred source for this package
+        if (!$this->preferSource && ($this->preferDist || 'dist' === $this->resolvePackageInstallPreference($package))) {
+            $sources = array_reverse($sources);
+        }
+
+        return $sources;
+    }
 }

+ 9 - 1
src/Composer/Downloader/DownloaderInterface.php

@@ -13,6 +13,7 @@
 namespace Composer\Downloader;
 
 use Composer\Package\PackageInterface;
+use React\Promise\PromiseInterface;
 
 /**
  * Downloader interface.
@@ -29,13 +30,20 @@ interface DownloaderInterface
      */
     public function getInstallationSource();
 
+    /**
+     * This should do any network-related tasks to prepare for install/update
+     *
+     * @return PromiseInterface|null
+     */
+    public function download(PackageInterface $package, $path);
+
     /**
      * Downloads specific package into specific folder.
      *
      * @param PackageInterface $package package instance
      * @param string           $path    download path
      */
-    public function download(PackageInterface $package, $path);
+    public function install(PackageInterface $package, $path);
 
     /**
      * Updates specific package in specific folder from initial to target version.

+ 132 - 81
src/Composer/Downloader/FileDownloader.php

@@ -26,6 +26,7 @@ use Composer\EventDispatcher\EventDispatcher;
 use Composer\Util\Filesystem;
 use Composer\Util\HttpDownloader;
 use Composer\Util\Url as UrlUtil;
+use Composer\Downloader\TransportException;
 
 /**
  * Base downloader for files
@@ -43,7 +44,10 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface
     protected $filesystem;
     protected $cache;
     protected $outputProgress = true;
-    private $lastCacheWrites = array();
+    /**
+     * @private this is only public for php 5.3 support in closures
+     */
+    public $lastCacheWrites = array();
     private $eventDispatcher;
 
     /**
@@ -87,108 +91,149 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface
             throw new \InvalidArgumentException('The given package is missing url information');
         }
 
-        if ($output) {
-            $this->io->writeError("  - Installing <info>" . $package->getName() . "</info> (<comment>" . $package->getFullPrettyVersion() . "</comment>): ", false);
-        }
-
+        $retries = 3;
         $urls = $package->getDistUrls();
-        while ($url = array_shift($urls)) {
-            try {
-                $fileName = $this->doDownload($package, $path, $url);
-                break;
-            } catch (\Exception $e) {
-                if ($this->io->isDebug()) {
-                    $this->io->writeError('');
-                    $this->io->writeError('Failed: ['.get_class($e).'] '.$e->getCode().': '.$e->getMessage());
-                } elseif (count($urls)) {
-                    $this->io->writeError('');
-                    $this->io->writeError(' Failed, trying the next URL ('.$e->getCode().': '.$e->getMessage().')', false);
-                }
-
-                if (!count($urls)) {
-                    throw $e;
-                }
-            }
+        foreach ($urls as $index => $url) {
+            $processedUrl = $this->processUrl($package, $url);
+            $urls[$index] = array(
+                'base' => $url,
+                'processed' => $processedUrl,
+                'cacheKey' => $this->getCacheKey($package, $processedUrl)
+            );
         }
 
-        if ($output) {
-            $this->io->writeError('');
-        }
-
-        return $fileName;
-    }
-
-    protected function doDownload(PackageInterface $package, $path, $url)
-    {
         $this->filesystem->emptyDirectory($path);
-
         $fileName = $this->getFileName($package, $path);
 
-        $processedUrl = $this->processUrl($package, $url);
-
-        $preFileDownloadEvent = new PreFileDownloadEvent(PluginEvents::PRE_FILE_DOWNLOAD, $this->httpDownloader, $processedUrl);
-        if ($this->eventDispatcher) {
-            $this->eventDispatcher->dispatch($preFileDownloadEvent->getName(), $preFileDownloadEvent);
-        }
-        $httpDownloader = $preFileDownloadEvent->getHttpDownloader();
+        $io = $this->io;
+        $cache = $this->cache;
+        $originalHttpDownloader = $this->httpDownloader;
+        $eventDispatcher = $this->eventDispatcher;
+        $filesystem = $this->filesystem;
+        $self = $this;
+
+        $accept = null;
+        $reject = null;
+        $download = function () use ($io, $output, $originalHttpDownloader, $cache, $eventDispatcher, $package, $fileName, $path, &$urls, &$accept, &$reject) {
+            $url = reset($urls);
+
+            $httpDownloader = $originalHttpDownloader;
+            if ($eventDispatcher) {
+                $preFileDownloadEvent = new PreFileDownloadEvent(PluginEvents::PRE_FILE_DOWNLOAD, $httpDownloader, $url['processed']);
+                $eventDispatcher->dispatch($preFileDownloadEvent->getName(), $preFileDownloadEvent);
+                $httpDownloader = $preFileDownloadEvent->getHttpDownloader();
+            }
 
-        try {
             $checksum = $package->getDistSha1Checksum();
-            $cacheKey = $this->getCacheKey($package, $processedUrl);
+            $cacheKey = $url['cacheKey'];
 
             // use from cache if it is present and has a valid checksum or we have no checksum to check against
-            if ($this->cache && (!$checksum || $checksum === $this->cache->sha1($cacheKey)) && $this->cache->copyTo($cacheKey, $fileName)) {
-                $this->io->writeError('Loading from cache', false);
+            if ($cache && (!$checksum || $checksum === $cache->sha1($cacheKey)) && $cache->copyTo($cacheKey, $fileName)) {
+                if ($output) {
+                    $io->writeError("  - Loading <info>" . $package->getName() . "</info> (<comment>" . $package->getFullPrettyVersion() . "</comment>) from cache");
+                }
+                $result = \React\Promise\resolve($fileName);
             } else {
-                // download if cache restore failed
-                if (!$this->outputProgress) {
-                    $this->io->writeError('Downloading', false);
+                if ($output) {
+                    $io->writeError("  - Downloading <info>" . $package->getName() . "</info> (<comment>" . $package->getFullPrettyVersion() . "</comment>)");
                 }
 
-                // try to download 3 times then fail hard
-                $retries = 3;
-                while ($retries--) {
-                    try {
-                        // TODO handle this->outputProgress
-                        $httpDownloader->copy($processedUrl, $fileName, $package->getTransportOptions());
-                        break;
-                    } catch (TransportException $e) {
-                        // if we got an http response with a proper code, then requesting again will probably not help, abort
-                        if ((0 !== $e->getCode() && !in_array($e->getCode(), array(500, 502, 503, 504))) || !$retries) {
-                            throw $e;
-                        }
-                        $this->io->writeError('');
-                        $this->io->writeError('    Download failed, retrying...', true, IOInterface::VERBOSE);
-                        usleep(500000);
-                    }
+                $result = $httpDownloader->addCopy($url['processed'], $fileName, $package->getTransportOptions())
+                    ->then($accept, $reject);
+            }
+
+            return $result->then(function ($result) use ($fileName, $checksum, $url) {
+                // in case of retry, the first call's Promise chain finally calls this twice at the end,
+                // once with $result being the returned $fileName from $accept, and then once for every
+                // failed request with a null result, which can be skipped.
+                if (null === $result) {
+                    return $fileName;
                 }
 
-                if (!$this->outputProgress) {
-                    $this->io->writeError(' (<comment>100%</comment>)', false);
+                if (!file_exists($fileName)) {
+                    throw new \UnexpectedValueException($url['base'].' could not be saved to '.$fileName.', make sure the'
+                        .' directory is writable and you have internet connectivity');
                 }
 
-                if ($this->cache) {
-                    $this->lastCacheWrites[$package->getName()] = $cacheKey;
-                    $this->cache->copyFrom($cacheKey, $fileName);
+                if ($checksum && hash_file('sha1', $fileName) !== $checksum) {
+                    throw new \UnexpectedValueException('The checksum verification of the file failed (downloaded from '.$url['base'].')');
+                }
+
+                return $fileName;
+            });
+        };
+
+        $accept = function ($response) use ($io, $cache, $package, $fileName, $path, $self, &$urls) {
+            $url = reset($urls);
+            $cacheKey = $url['cacheKey'];
+
+            if ($cache) {
+                $self->lastCacheWrites[$package->getName()] = $cacheKey;
+                $cache->copyFrom($cacheKey, $fileName);
+            }
+
+            $response->collect();
+
+            return $fileName;
+        };
+
+        $reject = function ($e) use ($io, &$urls, $download, $fileName, $path, $package, &$retries, $filesystem, $self) {
+            // clean up
+            $filesystem->removeDirectory($path);
+            $self->clearLastCacheWrite($package);
+
+            if ($e instanceof TransportException) {
+                // if we got an http response with a proper code, then requesting again will probably not help, abort
+                if ((0 !== $e->getCode() && !in_array($e->getCode(), array(500, 502, 503, 504))) || !$retries) {
+                    $retries = 0;
                 }
             }
 
-            if (!file_exists($fileName)) {
-                throw new \UnexpectedValueException($url.' could not be saved to '.$fileName.', make sure the'
-                    .' directory is writable and you have internet connectivity');
+            // special error code returned when network is being artificially disabled
+            if ($e instanceof TransportException && $e->getStatusCode() === 499) {
+                $retries = 0;
+                $urls = array();
             }
 
-            if ($checksum && hash_file('sha1', $fileName) !== $checksum) {
-                throw new \UnexpectedValueException('The checksum verification of the file failed (downloaded from '.$url.')');
+            if ($retries) {
+                usleep(500000);
+                $retries--;
+
+                return $download();
             }
-        } catch (\Exception $e) {
-            // clean up
-            $this->filesystem->removeDirectory($path);
-            $this->clearLastCacheWrite($package);
+
+            array_shift($urls);
+            if ($urls) {
+                if ($io->isDebug()) {
+                    $io->writeError('    Failed downloading '.$package->getName().': ['.get_class($e).'] '.$e->getCode().': '.$e->getMessage());
+                    $io->writeError('    Trying the next URL for '.$package->getName());
+                } elseif (count($urls)) {
+                    $io->writeError('    Failed downloading '.$package->getName().', trying the next URL ('.$e->getCode().': '.$e->getMessage().')');
+                }
+
+                $retries = 3;
+                usleep(100000);
+
+                return $download();
+            }
+
             throw $e;
+        };
+
+        return $download();
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function install(PackageInterface $package, $path, $output = true)
+    {
+        if ($output) {
+            $this->io->writeError("  - Installing <info>" . $package->getName() . "</info> (<comment>" . $package->getFullPrettyVersion() . "</comment>)");
         }
 
-        return $fileName;
+        $this->filesystem->ensureDirectoryExists($path);
+        $this->filesystem->rename($this->getFileName($package, $path), $path . pathinfo(parse_url($package->getDistUrl(), PHP_URL_PATH), PATHINFO_BASENAME));
     }
 
     /**
@@ -201,7 +246,11 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface
         return $this;
     }
 
-    protected function clearLastCacheWrite(PackageInterface $package)
+    /**
+     * TODO mark private in v3
+     * @protected This is public due to PHP 5.3
+     */
+    public function clearLastCacheWrite(PackageInterface $package)
     {
         if ($this->cache && isset($this->lastCacheWrites[$package->getName()])) {
             $this->cache->remove($this->lastCacheWrites[$package->getName()]);
@@ -222,7 +271,7 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface
         $this->io->writeError("  - " . $actionName . " <info>" . $name . "</info> (<comment>" . $from . "</comment> => <comment>" . $to . "</comment>): ", false);
 
         $this->remove($initial, $path, false);
-        $this->download($target, $path, false);
+        $this->install($target, $path, false);
 
         $this->io->writeError('');
     }
@@ -249,7 +298,7 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface
      */
     protected function getFileName(PackageInterface $package, $path)
     {
-        return $path.'/'.pathinfo(parse_url($package->getDistUrl(), PHP_URL_PATH), PATHINFO_BASENAME);
+        return $path.'_'.pathinfo(parse_url($package->getDistUrl(), PHP_URL_PATH), PATHINFO_BASENAME);
     }
 
     /**
@@ -299,7 +348,9 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface
         $e = null;
 
         try {
-            $this->download($package, $targetDir.'_compare', false);
+            $res = $this->download($package, $targetDir.'_compare', false);
+            $this->httpDownloader->wait();
+            $res = $this->install($package, $targetDir.'_compare', false);
 
             $comparer = new Comparer();
             $comparer->setSource($targetDir.'_compare');

+ 1 - 1
src/Composer/Downloader/FossilDownloader.php

@@ -23,7 +23,7 @@ class FossilDownloader extends VcsDownloader
     /**
      * {@inheritDoc}
      */
-    public function doDownload(PackageInterface $package, $path, $url)
+    public function doInstall(PackageInterface $package, $path, $url)
     {
         // Ensure we are allowed to use this URL by config
         $this->config->prohibitUrlByConfig($url, $this->io);

+ 1 - 1
src/Composer/Downloader/GitDownloader.php

@@ -38,7 +38,7 @@ class GitDownloader extends VcsDownloader implements DvcsDownloaderInterface
     /**
      * {@inheritDoc}
      */
-    public function doDownload(PackageInterface $package, $path, $url)
+    public function doInstall(PackageInterface $package, $path, $url)
     {
         GitUtil::cleanEnv();
         $path = $this->normalizePath($path);

+ 3 - 10
src/Composer/Downloader/GzipDownloader.php

@@ -36,9 +36,10 @@ class GzipDownloader extends ArchiveDownloader
         parent::__construct($io, $config, $downloader, $eventDispatcher, $cache);
     }
 
-    protected function extract($file, $path)
+    protected function extract(PackageInterface $package, $file, $path)
     {
-        $targetFilepath = $path . DIRECTORY_SEPARATOR . basename(substr($file, 0, -3));
+        $filename = pathinfo(parse_url($package->getDistUrl(), PHP_URL_PATH), PATHINFO_FILENAME);
+        $targetFilepath = $path . DIRECTORY_SEPARATOR . $filename;
 
         // Try to use gunzip on *nix
         if (!Platform::isWindows()) {
@@ -63,14 +64,6 @@ class GzipDownloader extends ArchiveDownloader
         $this->extractUsingExt($file, $targetFilepath);
     }
 
-    /**
-     * {@inheritdoc}
-     */
-    protected function getFileName(PackageInterface $package, $path)
-    {
-        return $path.'/'.pathinfo(parse_url($package->getDistUrl(), PHP_URL_PATH), PATHINFO_BASENAME);
-    }
-
     private function extractUsingExt($file, $targetFilepath)
     {
         $archiveFile = gzopen($file, 'rb');

+ 1 - 1
src/Composer/Downloader/HgDownloader.php

@@ -24,7 +24,7 @@ class HgDownloader extends VcsDownloader
     /**
      * {@inheritDoc}
      */
-    public function doDownload(PackageInterface $package, $path, $url)
+    public function doInstall(PackageInterface $package, $path, $url)
     {
         $hgUtils = new HgUtils($this->io, $this->config, $this->process);
 

+ 9 - 0
src/Composer/Downloader/PathDownloader.php

@@ -61,6 +61,15 @@ class PathDownloader extends FileDownloader implements VcsCapableDownloaderInter
                 $realUrl
             ));
         }
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function install(PackageInterface $package, $path, $output = true)
+    {
+        $url = $package->getDistUrl();
+        $realUrl = realpath($url);
 
         // Get the transport options with default values
         $transportOptions = $package->getTransportOptions() + array('symlink' => null);

+ 1 - 1
src/Composer/Downloader/PerforceDownloader.php

@@ -27,7 +27,7 @@ class PerforceDownloader extends VcsDownloader
     /**
      * {@inheritDoc}
      */
-    public function doDownload(PackageInterface $package, $path, $url)
+    public function doInstall(PackageInterface $package, $path, $url)
     {
         $ref = $package->getSourceReference();
         $label = $this->getLabelFromSourceReference($ref);

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

@@ -12,6 +12,8 @@
 
 namespace Composer\Downloader;
 
+use Composer\Package\PackageInterface;
+
 /**
  * Downloader for phar files
  *
@@ -22,7 +24,7 @@ class PharDownloader extends ArchiveDownloader
     /**
      * {@inheritDoc}
      */
-    protected function extract($file, $path)
+    protected function extract(PackageInterface $package, $file, $path)
     {
         // Can throw an UnexpectedValueException
         $archive = new \Phar($file);

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

@@ -20,6 +20,7 @@ use Composer\Util\Platform;
 use Composer\Util\ProcessExecutor;
 use Composer\Util\HttpDownloader;
 use Composer\IO\IOInterface;
+use Composer\Package\PackageInterface;
 use RarArchive;
 
 /**
@@ -39,7 +40,7 @@ class RarDownloader extends ArchiveDownloader
         parent::__construct($io, $config, $downloader, $eventDispatcher, $cache);
     }
 
-    protected function extract($file, $path)
+    protected function extract(PackageInterface $package, $file, $path)
     {
         $processError = null;
 

+ 1 - 1
src/Composer/Downloader/SvnDownloader.php

@@ -28,7 +28,7 @@ class SvnDownloader extends VcsDownloader
     /**
      * {@inheritDoc}
      */
-    public function doDownload(PackageInterface $package, $path, $url)
+    public function doInstall(PackageInterface $package, $path, $url)
     {
         SvnUtil::cleanEnv();
         $ref = $package->getSourceReference();

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

@@ -12,6 +12,8 @@
 
 namespace Composer\Downloader;
 
+use Composer\Package\PackageInterface;
+
 /**
  * Downloader for tar files: tar, tar.gz or tar.bz2
  *
@@ -22,7 +24,7 @@ class TarDownloader extends ArchiveDownloader
     /**
      * {@inheritDoc}
      */
-    protected function extract($file, $path)
+    protected function extract(PackageInterface $package, $file, $path)
     {
         // Can throw an UnexpectedValueException
         $archive = new \PharData($file);

+ 10 - 2
src/Composer/Downloader/VcsDownloader.php

@@ -55,6 +55,14 @@ abstract class VcsDownloader implements DownloaderInterface, ChangeReportInterfa
      * {@inheritDoc}
      */
     public function download(PackageInterface $package, $path)
+    {
+        // noop for now, ideally we would do a git fetch already here, or make sure the cached git repo is synced, etc.
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function install(PackageInterface $package, $path)
     {
         if (!$package->getSourceReference()) {
             throw new \InvalidArgumentException('Package '.$package->getPrettyName().' is missing reference information');
@@ -87,7 +95,7 @@ abstract class VcsDownloader implements DownloaderInterface, ChangeReportInterfa
                         $url = $needle . $url;
                     }
                 }
-                $this->doDownload($package, $path, $url);
+                $this->doInstall($package, $path, $url);
                 break;
             } catch (\Exception $e) {
                 // rethrow phpunit exceptions to avoid hard to debug bug failures
@@ -260,7 +268,7 @@ abstract class VcsDownloader implements DownloaderInterface, ChangeReportInterfa
      * @param string           $path    download path
      * @param string           $url     package url
      */
-    abstract protected function doDownload(PackageInterface $package, $path, $url);
+    abstract protected function doInstall(PackageInterface $package, $path, $url);
 
     /**
      * Updates specific package in specific folder from initial to target version.

+ 1 - 9
src/Composer/Downloader/XzDownloader.php

@@ -37,7 +37,7 @@ class XzDownloader extends ArchiveDownloader
         parent::__construct($io, $config, $downloader, $eventDispatcher, $cache);
     }
 
-    protected function extract($file, $path)
+    protected function extract(PackageInterface $package, $file, $path)
     {
         $command = 'tar -xJf ' . ProcessExecutor::escape($file) . ' -C ' . ProcessExecutor::escape($path);
 
@@ -49,12 +49,4 @@ class XzDownloader extends ArchiveDownloader
 
         throw new \RuntimeException($processError);
     }
-
-    /**
-     * {@inheritdoc}
-     */
-    protected function getFileName(PackageInterface $package, $path)
-    {
-        return $path.'/'.pathinfo(parse_url($package->getDistUrl(), PHP_URL_PATH), PATHINFO_BASENAME);
-    }
 }

+ 1 - 1
src/Composer/Downloader/ZipDownloader.php

@@ -185,7 +185,7 @@ class ZipDownloader extends ArchiveDownloader
      * @param string $file File to extract
      * @param string $path Path where to extract file
      */
-    public function extract($file, $path)
+    public function extract(PackageInterface $package, $file, $path)
     {
         // Each extract calls its alternative if not available or fails
         if (self::$isWindows) {

+ 8 - 6
src/Composer/Factory.php

@@ -24,6 +24,7 @@ use Composer\Util\Filesystem;
 use Composer\Util\Platform;
 use Composer\Util\ProcessExecutor;
 use Composer\Util\HttpDownloader;
+use Composer\Util\Loop;
 use Composer\Util\Silencer;
 use Composer\Plugin\PluginEvents;
 use Composer\EventDispatcher\Event;
@@ -326,6 +327,7 @@ class Factory
         }
 
         $httpDownloader = self::createHttpDownloader($io, $config);
+        $loop = new Loop($httpDownloader);
 
         // initialize event dispatcher
         $dispatcher = new EventDispatcher($composer, $io);
@@ -352,7 +354,7 @@ class Factory
         $composer->setPackage($package);
 
         // initialize installation manager
-        $im = $this->createInstallationManager();
+        $im = $this->createInstallationManager($loop);
         $composer->setInstallationManager($im);
 
         if ($fullLoad) {
@@ -365,7 +367,7 @@ class Factory
             $composer->setAutoloadGenerator($generator);
 
             // initialize archive manager
-            $am = $this->createArchiveManager($config, $dm);
+            $am = $this->createArchiveManager($config, $dm, $loop);
             $composer->setArchiveManager($am);
         }
 
@@ -501,9 +503,9 @@ class Factory
      * @param  Downloader\DownloadManager $dm     Manager use to download sources
      * @return Archiver\ArchiveManager
      */
-    public function createArchiveManager(Config $config, Downloader\DownloadManager $dm)
+    public function createArchiveManager(Config $config, Downloader\DownloadManager $dm, Loop $loop)
     {
-        $am = new Archiver\ArchiveManager($dm);
+        $am = new Archiver\ArchiveManager($dm, $loop);
         $am->addArchiver(new Archiver\ZipArchiver);
         $am->addArchiver(new Archiver\PharArchiver);
 
@@ -525,9 +527,9 @@ class Factory
     /**
      * @return Installer\InstallationManager
      */
-    protected function createInstallationManager()
+    public function createInstallationManager(Loop $loop)
     {
-        return new Installer\InstallationManager();
+        return new Installer\InstallationManager($loop);
     }
 
     /**

+ 26 - 1
src/Composer/Installer/InstallationManager.php

@@ -24,6 +24,7 @@ use Composer\DependencyResolver\Operation\UninstallOperation;
 use Composer\DependencyResolver\Operation\MarkAliasInstalledOperation;
 use Composer\DependencyResolver\Operation\MarkAliasUninstalledOperation;
 use Composer\Util\StreamContextFactory;
+use Composer\Util\Loop;
 
 /**
  * Package operation manager.
@@ -37,6 +38,12 @@ class InstallationManager
     private $installers = array();
     private $cache = array();
     private $notifiablePackages = array();
+    private $loop;
+
+    public function __construct(Loop $loop)
+    {
+        $this->loop = $loop;
+    }
 
     public function reset()
     {
@@ -156,7 +163,24 @@ class InstallationManager
      */
     public function execute(RepositoryInterface $repo, OperationInterface $operation)
     {
+        // TODO this should take all operations in one go
         $method = $operation->getJobType();
+
+        if ($method === 'install') {
+            $package = $operation->getPackage();
+            $installer = $this->getInstaller($package->getType());
+            $promise = $installer->download($package);
+        } elseif ($method === 'update') {
+            $target = $operation->getTargetPackage();
+            $targetType = $target->getType();
+            $installer = $this->getInstaller($targetType);
+            $promise = $installer->download($target, $operation->getInitialPackage());
+        }
+
+        if (isset($promise)) {
+            $this->loop->wait(array($promise));
+        }
+
         $this->$method($repo, $operation);
     }
 
@@ -194,7 +218,8 @@ class InstallationManager
             $this->markForNotification($target);
         } else {
             $this->getInstaller($initialType)->uninstall($repo, $initial);
-            $this->getInstaller($targetType)->install($repo, $target);
+            $installer = $this->getInstaller($targetType);
+            $installer->install($repo, $target);
         }
     }
 

+ 10 - 0
src/Composer/Installer/InstallerInterface.php

@@ -15,6 +15,7 @@ namespace Composer\Installer;
 use Composer\Package\PackageInterface;
 use Composer\Repository\InstalledRepositoryInterface;
 use InvalidArgumentException;
+use React\Promise\PromiseInterface;
 
 /**
  * Interface for the package installation manager.
@@ -42,6 +43,15 @@ interface InstallerInterface
      */
     public function isInstalled(InstalledRepositoryInterface $repo, PackageInterface $package);
 
+    /**
+     * Downloads the files needed to later install the given package.
+     *
+     * @param  PackageInterface $package     package instance
+     * @param  PackageInterface $prevPackage previous package instance in case of an update
+     * @return PromiseInterface
+     */
+    public function download(PackageInterface $package, PackageInterface $prevPackage = null);
+
     /**
      * Installs specific package.
      *

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

@@ -85,6 +85,14 @@ class LibraryInstaller implements InstallerInterface, BinaryPresenceInterface
         return (Platform::isWindows() && $this->filesystem->isJunction($installPath)) || is_link($installPath);
     }
 
+    public function download(PackageInterface $package, PackageInterface $prevPackage = null)
+    {
+        $this->initializeVendorDir();
+        $downloadPath = $this->getInstallPath($package);
+
+        return $this->downloadManager->download($package, $downloadPath, $prevPackage);
+    }
+
     /**
      * {@inheritDoc}
      */
@@ -194,7 +202,7 @@ class LibraryInstaller implements InstallerInterface, BinaryPresenceInterface
     protected function installCode(PackageInterface $package)
     {
         $downloadPath = $this->getInstallPath($package);
-        $this->downloadManager->download($package, $downloadPath);
+        $this->downloadManager->install($package, $downloadPath);
     }
 
     protected function updateCode(PackageInterface $initial, PackageInterface $target)

+ 8 - 0
src/Composer/Installer/MetapackageInstaller.php

@@ -38,6 +38,14 @@ class MetapackageInstaller implements InstallerInterface
         return $repo->hasPackage($package);
     }
 
+    /**
+     * {@inheritDoc}
+     */
+    public function download(PackageInterface $package, PackageInterface $prevPackage = null)
+    {
+        // noop
+    }
+
     /**
      * {@inheritDoc}
      */

+ 7 - 0
src/Composer/Installer/NoopInstaller.php

@@ -40,6 +40,13 @@ class NoopInstaller implements InstallerInterface
         return $repo->hasPackage($package);
     }
 
+    /**
+     * {@inheritDoc}
+     */
+    public function download(PackageInterface $package, PackageInterface $prevPackage = null)
+    {
+    }
+
     /**
      * {@inheritDoc}
      */

+ 9 - 1
src/Composer/Installer/PluginInstaller.php

@@ -50,13 +50,21 @@ class PluginInstaller extends LibraryInstaller
     /**
      * {@inheritDoc}
      */
-    public function install(InstalledRepositoryInterface $repo, PackageInterface $package)
+    public function download(PackageInterface $package, PackageInterface $prevPackage = null)
     {
         $extra = $package->getExtra();
         if (empty($extra['class'])) {
             throw new \UnexpectedValueException('Error while installing '.$package->getPrettyName().', composer-plugin packages should have a class defined in their extra key to be usable.');
         }
 
+        return parent::download($package, $prevPackage);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function install(InstalledRepositoryInterface $repo, PackageInterface $package)
+    {
         parent::install($repo, $package);
         try {
             $this->composer->getPluginManager()->registerPackage($package, true);

+ 11 - 2
src/Composer/Installer/ProjectInstaller.php

@@ -58,7 +58,7 @@ class ProjectInstaller implements InstallerInterface
     /**
      * {@inheritDoc}
      */
-    public function install(InstalledRepositoryInterface $repo, PackageInterface $package)
+    public function download(PackageInterface $package, PackageInterface $prevPackage = null)
     {
         $installPath = $this->installPath;
         if (file_exists($installPath) && !$this->filesystem->isDirEmpty($installPath)) {
@@ -67,7 +67,16 @@ class ProjectInstaller implements InstallerInterface
         if (!is_dir($installPath)) {
             mkdir($installPath, 0777, true);
         }
-        $this->downloadManager->download($package, $installPath);
+
+        return $this->downloadManager->download($package, $installPath, $prevPackage);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function install(InstalledRepositoryInterface $repo, PackageInterface $package)
+    {
+        $this->downloadManager->install($package, $this->installPath);
     }
 
     /**

+ 7 - 2
src/Composer/Package/Archiver/ArchiveManager.php

@@ -16,6 +16,7 @@ use Composer\Downloader\DownloadManager;
 use Composer\Package\PackageInterface;
 use Composer\Package\RootPackageInterface;
 use Composer\Util\Filesystem;
+use Composer\Util\Loop;
 use Composer\Json\JsonFile;
 
 /**
@@ -25,6 +26,7 @@ use Composer\Json\JsonFile;
 class ArchiveManager
 {
     protected $downloadManager;
+    protected $loop;
 
     protected $archivers = array();
 
@@ -36,9 +38,10 @@ class ArchiveManager
     /**
      * @param DownloadManager $downloadManager A manager used to download package sources
      */
-    public function __construct(DownloadManager $downloadManager)
+    public function __construct(DownloadManager $downloadManager, Loop $loop)
     {
         $this->downloadManager = $downloadManager;
+        $this->loop = $loop;
     }
 
     /**
@@ -148,7 +151,9 @@ class ArchiveManager
             $filesystem->ensureDirectoryExists($sourcePath);
 
             // Download sources
-            $this->downloadManager->download($package, $sourcePath);
+            $promise = $this->downloadManager->download($package, $sourcePath);
+            $this->loop->wait(array($promise));
+            $this->downloadManager->install($package, $sourcePath);
 
             // Check exclude from downloaded composer.json
             if (file_exists($composerJsonPath = $sourcePath.'/composer.json')) {

+ 7 - 6
src/Composer/Repository/ComposerRepository.php

@@ -22,6 +22,7 @@ use Composer\Config;
 use Composer\Factory;
 use Composer\IO\IOInterface;
 use Composer\Util\HttpDownloader;
+use Composer\Util\Loop;
 use Composer\Plugin\PluginEvents;
 use Composer\Plugin\PreFileDownloadEvent;
 use Composer\EventDispatcher\EventDispatcher;
@@ -42,6 +43,7 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito
     private $baseUrl;
     private $io;
     private $httpDownloader;
+    private $loop;
     protected $cache;
     protected $notifyUrl;
     protected $searchUrl;
@@ -107,6 +109,7 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito
         $this->httpDownloader = $httpDownloader;
         $this->eventDispatcher = $eventDispatcher;
         $this->repoConfig = $repoConfig;
+        $this->loop = new Loop($this->httpDownloader);
     }
 
     public function getRepoConfig()
@@ -569,6 +572,7 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito
         $this->loadRootServerFile();
 
         $packages = array();
+        $promises = array();
         $repo = $this;
 
         if (!$this->lazyProvidersUrl) {
@@ -592,7 +596,7 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito
                 $lastModified = isset($contents['last-modified']) ? $contents['last-modified'] : null;
             }
 
-            $this->asyncFetchFile($url, $cacheKey, $lastModified)
+            $promises[] = $this->asyncFetchFile($url, $cacheKey, $lastModified)
                 ->then(function ($response) use (&$packages, $contents, $name, $constraint, $repo, $isPackageAcceptableCallable) {
                     static $uniqKeys = array('version', 'version_normalized', 'source', 'dist', 'time');
 
@@ -637,13 +641,10 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito
                             $packages[spl_object_hash($package->getAliasOf())] = $package->getAliasOf();
                         }
                     }
-                }, function ($e) {
-                    // TODO use ->done() above instead with react/promise 2.0
-                    throw $e;
                 });
         }
 
-        $this->httpDownloader->wait();
+        $this->loop->wait($promises);
 
         return $packages;
         // RepositorySet should call loadMetadata, getMetadata when all promises resolved, then metadataComplete when done so we can GC the loaded json and whatnot then as needed
@@ -1119,7 +1120,7 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito
             }
             $degradedMode = true;
 
-            return true;
+            throw $e;
         };
 
         return $httpDownloader->add($filename, $options)->then($accept, $reject);

+ 1 - 1
src/Composer/Util/Http/CurlDownloader.php

@@ -295,7 +295,7 @@ class CurlDownloader
                 // resolve promise
                 if ($job['filename']) {
                     rename($job['filename'].'~', $job['filename']);
-                    call_user_func($job['resolve'], true);
+                    call_user_func($job['resolve'], $response);
                 } else {
                     call_user_func($job['resolve'], $response);
                 }

+ 5 - 1
src/Composer/Util/HttpDownloader.php

@@ -160,7 +160,10 @@ class HttpDownloader
                 if ($job['request']['copyTo']) {
                     $result = $rfs->copy($job['origin'], $url, $job['request']['copyTo'], false /* TODO progress */, $options);
 
-                    $resolve($result);
+                    $headers = $rfs->getLastHeaders();
+                    $response = new Http\Response($job['request'], $rfs->findStatusCode($headers), $headers, $job['request']['copyTo'].'~');
+
+                    $resolve($response);
                 } else {
                     $body = $rfs->getContents($job['origin'], $url, false /* TODO progress */, $options);
                     $headers = $rfs->getLastHeaders();
@@ -191,6 +194,7 @@ class HttpDownloader
             $job['exception'] = $e;
 
             $downloader->markJobDone();
+            $downloader->scheduleNextJob();
 
             throw $e;
         });

+ 47 - 0
src/Composer/Util/Loop.php

@@ -0,0 +1,47 @@
+<?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\Util\HttpDownloader;
+use React\Promise\Promise;
+
+/**
+ * @author Jordi Boggiano <j.boggiano@seld.be>
+ */
+class Loop
+{
+    private $io;
+
+    public function __construct(HttpDownloader $httpDownloader)
+    {
+        $this->httpDownloader = $httpDownloader;
+    }
+
+    public function wait(array $promises)
+    {
+        $uncaught = null;
+
+        \React\Promise\all($promises)->then(
+            function () { },
+            function ($e) use (&$uncaught) {
+                $uncaught = $e;
+            }
+        );
+
+        $this->httpDownloader->wait();
+
+        if ($uncaught) {
+            throw $uncaught;
+        }
+    }
+}

+ 1 - 1
tests/Composer/Test/ComposerTest.php

@@ -57,7 +57,7 @@ class ComposerTest extends TestCase
     public function testSetGetInstallationManager()
     {
         $composer = new Composer();
-        $manager = $this->getMockBuilder('Composer\Installer\InstallationManager')->getMock();
+        $manager = $this->getMockBuilder('Composer\Installer\InstallationManager')->disableOriginalConstructor()->getMock();
         $composer->setInstallationManager($manager);
 
         $this->assertSame($manager, $composer->getInstallationManager());

+ 1 - 1
tests/Composer/Test/Downloader/ArchiveDownloaderTest.php

@@ -29,7 +29,7 @@ class ArchiveDownloaderTest extends TestCase
         $method->setAccessible(true);
 
         $first = $method->invoke($downloader, $packageMock, '/path');
-        $this->assertRegExp('#/path/[a-z0-9]+\.js#', $first);
+        $this->assertRegExp('#/path_[a-z0-9]+\.js#', $first);
         $this->assertSame($first, $method->invoke($downloader, $packageMock, '/path'));
     }
 

+ 123 - 150
tests/Composer/Test/Downloader/DownloadManagerTest.php

@@ -50,7 +50,7 @@ class DownloadManagerTest extends TestCase
 
         $this->setExpectedException('InvalidArgumentException');
 
-        $manager->getDownloaderForInstalledPackage($package);
+        $manager->getDownloaderForPackage($package);
     }
 
     public function testGetDownloaderForCorrectlyInstalledDistPackage()
@@ -82,7 +82,7 @@ class DownloadManagerTest extends TestCase
             ->with('pear')
             ->will($this->returnValue($downloader));
 
-        $this->assertSame($downloader, $manager->getDownloaderForInstalledPackage($package));
+        $this->assertSame($downloader, $manager->getDownloaderForPackage($package));
     }
 
     public function testGetDownloaderForIncorrectlyInstalledDistPackage()
@@ -116,7 +116,7 @@ class DownloadManagerTest extends TestCase
 
         $this->setExpectedException('LogicException');
 
-        $manager->getDownloaderForInstalledPackage($package);
+        $manager->getDownloaderForPackage($package);
     }
 
     public function testGetDownloaderForCorrectlyInstalledSourcePackage()
@@ -148,7 +148,7 @@ class DownloadManagerTest extends TestCase
             ->with('git')
             ->will($this->returnValue($downloader));
 
-        $this->assertSame($downloader, $manager->getDownloaderForInstalledPackage($package));
+        $this->assertSame($downloader, $manager->getDownloaderForPackage($package));
     }
 
     public function testGetDownloaderForIncorrectlyInstalledSourcePackage()
@@ -182,7 +182,7 @@ class DownloadManagerTest extends TestCase
 
         $this->setExpectedException('LogicException');
 
-        $manager->getDownloaderForInstalledPackage($package);
+        $manager->getDownloaderForPackage($package);
     }
 
     public function testGetDownloaderForMetapackage()
@@ -195,7 +195,7 @@ class DownloadManagerTest extends TestCase
 
         $manager = new DownloadManager($this->io, false, $this->filesystem);
 
-        $this->assertNull($manager->getDownloaderForInstalledPackage($package));
+        $this->assertNull($manager->getDownloaderForPackage($package));
     }
 
     public function testFullPackageDownload()
@@ -223,11 +223,11 @@ class DownloadManagerTest extends TestCase
 
         $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager')
             ->setConstructorArgs(array($this->io, false, $this->filesystem))
-            ->setMethods(array('getDownloaderForInstalledPackage'))
+            ->setMethods(array('getDownloaderForPackage'))
             ->getMock();
         $manager
             ->expects($this->once())
-            ->method('getDownloaderForInstalledPackage')
+            ->method('getDownloaderForPackage')
             ->with($package)
             ->will($this->returnValue($downloader));
 
@@ -274,16 +274,16 @@ class DownloadManagerTest extends TestCase
 
         $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager')
             ->setConstructorArgs(array($this->io, false, $this->filesystem))
-            ->setMethods(array('getDownloaderForInstalledPackage'))
+            ->setMethods(array('getDownloaderForPackage'))
             ->getMock();
         $manager
             ->expects($this->at(0))
-            ->method('getDownloaderForInstalledPackage')
+            ->method('getDownloaderForPackage')
             ->with($package)
             ->will($this->returnValue($downloaderFail));
         $manager
             ->expects($this->at(1))
-            ->method('getDownloaderForInstalledPackage')
+            ->method('getDownloaderForPackage')
             ->with($package)
             ->will($this->returnValue($downloaderSuccess));
 
@@ -333,11 +333,11 @@ class DownloadManagerTest extends TestCase
 
         $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager')
             ->setConstructorArgs(array($this->io, false, $this->filesystem))
-            ->setMethods(array('getDownloaderForInstalledPackage'))
+            ->setMethods(array('getDownloaderForPackage'))
             ->getMock();
         $manager
             ->expects($this->once())
-            ->method('getDownloaderForInstalledPackage')
+            ->method('getDownloaderForPackage')
             ->with($package)
             ->will($this->returnValue($downloader));
 
@@ -369,11 +369,11 @@ class DownloadManagerTest extends TestCase
 
         $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager')
             ->setConstructorArgs(array($this->io, false, $this->filesystem))
-            ->setMethods(array('getDownloaderForInstalledPackage'))
+            ->setMethods(array('getDownloaderForPackage'))
             ->getMock();
         $manager
             ->expects($this->once())
-            ->method('getDownloaderForInstalledPackage')
+            ->method('getDownloaderForPackage')
             ->with($package)
             ->will($this->returnValue($downloader));
 
@@ -399,11 +399,11 @@ class DownloadManagerTest extends TestCase
 
         $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager')
           ->setConstructorArgs(array($this->io, false, $this->filesystem))
-          ->setMethods(array('getDownloaderForInstalledPackage'))
+          ->setMethods(array('getDownloaderForPackage'))
           ->getMock();
         $manager
           ->expects($this->once())
-          ->method('getDownloaderForInstalledPackage')
+          ->method('getDownloaderForPackage')
           ->with($package)
           ->will($this->returnValue(null)); // There is no downloader for Metapackages.
 
@@ -435,11 +435,11 @@ class DownloadManagerTest extends TestCase
 
         $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager')
             ->setConstructorArgs(array($this->io, false, $this->filesystem))
-            ->setMethods(array('getDownloaderForInstalledPackage'))
+            ->setMethods(array('getDownloaderForPackage'))
             ->getMock();
         $manager
             ->expects($this->once())
-            ->method('getDownloaderForInstalledPackage')
+            ->method('getDownloaderForPackage')
             ->with($package)
             ->will($this->returnValue($downloader));
 
@@ -472,11 +472,11 @@ class DownloadManagerTest extends TestCase
 
         $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager')
             ->setConstructorArgs(array($this->io, false, $this->filesystem))
-            ->setMethods(array('getDownloaderForInstalledPackage'))
+            ->setMethods(array('getDownloaderForPackage'))
             ->getMock();
         $manager
             ->expects($this->once())
-            ->method('getDownloaderForInstalledPackage')
+            ->method('getDownloaderForPackage')
             ->with($package)
             ->will($this->returnValue($downloader));
 
@@ -509,11 +509,11 @@ class DownloadManagerTest extends TestCase
 
         $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager')
             ->setConstructorArgs(array($this->io, false, $this->filesystem))
-            ->setMethods(array('getDownloaderForInstalledPackage'))
+            ->setMethods(array('getDownloaderForPackage'))
             ->getMock();
         $manager
             ->expects($this->once())
-            ->method('getDownloaderForInstalledPackage')
+            ->method('getDownloaderForPackage')
             ->with($package)
             ->will($this->returnValue($downloader));
 
@@ -550,33 +550,30 @@ class DownloadManagerTest extends TestCase
         $initial
             ->expects($this->once())
             ->method('getDistType')
-            ->will($this->returnValue('pear'));
+            ->will($this->returnValue('zip'));
 
         $target = $this->createPackageMock();
         $target
             ->expects($this->once())
-            ->method('getDistType')
-            ->will($this->returnValue('pear'));
+            ->method('getInstallationSource')
+            ->will($this->returnValue('dist'));
         $target
             ->expects($this->once())
-            ->method('setInstallationSource')
-            ->with('dist');
+            ->method('getDistType')
+            ->will($this->returnValue('zip'));
 
-        $pearDownloader = $this->createDownloaderMock();
-        $pearDownloader
+        $zipDownloader = $this->createDownloaderMock();
+        $zipDownloader
             ->expects($this->once())
             ->method('update')
             ->with($initial, $target, 'vendor/bundles/FOS/UserBundle');
+        $zipDownloader
+            ->expects($this->any())
+            ->method('getInstallationSource')
+            ->will($this->returnValue('dist'));
 
-        $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager')
-            ->setConstructorArgs(array($this->io, false, $this->filesystem))
-            ->setMethods(array('getDownloaderForInstalledPackage'))
-            ->getMock();
-        $manager
-            ->expects($this->once())
-            ->method('getDownloaderForInstalledPackage')
-            ->with($initial)
-            ->will($this->returnValue($pearDownloader));
+        $manager = new DownloadManager($this->io, false, $this->filesystem);
+        $manager->setDownloader('zip', $zipDownloader);
 
         $manager->update($initial, $target, 'vendor/bundles/FOS/UserBundle');
     }
@@ -591,113 +588,89 @@ class DownloadManagerTest extends TestCase
         $initial
             ->expects($this->once())
             ->method('getDistType')
-            ->will($this->returnValue('pear'));
+            ->will($this->returnValue('xz'));
 
         $target = $this->createPackageMock();
         $target
-            ->expects($this->once())
+            ->expects($this->any())
+            ->method('getInstallationSource')
+            ->will($this->returnValue('dist'));
+        $target
+            ->expects($this->any())
             ->method('getDistType')
-            ->will($this->returnValue('composer'));
+            ->will($this->returnValue('zip'));
 
-        $pearDownloader = $this->createDownloaderMock();
-        $pearDownloader
+        $xzDownloader = $this->createDownloaderMock();
+        $xzDownloader
             ->expects($this->once())
             ->method('remove')
             ->with($initial, 'vendor/bundles/FOS/UserBundle');
+        $xzDownloader
+            ->expects($this->any())
+            ->method('getInstallationSource')
+            ->will($this->returnValue('dist'));
 
-        $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager')
-            ->setConstructorArgs(array($this->io, false, $this->filesystem))
-            ->setMethods(array('getDownloaderForInstalledPackage', 'download'))
-            ->getMock();
-        $manager
+        $zipDownloader = $this->createDownloaderMock();
+        $zipDownloader
             ->expects($this->once())
-            ->method('getDownloaderForInstalledPackage')
-            ->with($initial)
-            ->will($this->returnValue($pearDownloader));
-        $manager
-            ->expects($this->once())
-            ->method('download')
-            ->with($target, 'vendor/bundles/FOS/UserBundle', false);
+            ->method('install')
+            ->with($target, 'vendor/bundles/FOS/UserBundle');
+        $zipDownloader
+            ->expects($this->any())
+            ->method('getInstallationSource')
+            ->will($this->returnValue('dist'));
+
+        $manager = new DownloadManager($this->io, false, $this->filesystem);
+        $manager->setDownloader('xz', $xzDownloader);
+        $manager->setDownloader('zip', $zipDownloader);
 
         $manager->update($initial, $target, 'vendor/bundles/FOS/UserBundle');
     }
 
-    public function testUpdateSourceWithEqualTypes()
+    /**
+     * @dataProvider updatesProvider
+     */
+    public function testGetAvailableSourcesUpdateSticksToSameSource($prevPkgSource, $prevPkgIsDev, $targetAvailable, $targetIsDev, $expected)
     {
-        $initial = $this->createPackageMock();
-        $initial
-            ->expects($this->once())
-            ->method('getInstallationSource')
-            ->will($this->returnValue('source'));
-        $initial
-            ->expects($this->once())
-            ->method('getSourceType')
-            ->will($this->returnValue('svn'));
-
-        $target = $this->createPackageMock();
-        $target
-            ->expects($this->once())
-            ->method('getSourceType')
-            ->will($this->returnValue('svn'));
-
-        $svnDownloader = $this->createDownloaderMock();
-        $svnDownloader
-            ->expects($this->once())
-            ->method('update')
-            ->with($initial, $target, 'vendor/pkg');
+        $initial = null;
+        if ($prevPkgSource) {
+            $initial = $this->prophesize('Composer\Package\PackageInterface');
+            $initial->getInstallationSource()->willReturn($prevPkgSource);
+            $initial->isDev()->willReturn($prevPkgIsDev);
+        }
+
+        $target = $this->prophesize('Composer\Package\PackageInterface');
+        $target->getSourceType()->willReturn(in_array('source', $targetAvailable, true) ? 'git' : null);
+        $target->getDistType()->willReturn(in_array('dist', $targetAvailable, true) ? 'zip' : null);
+        $target->isDev()->willReturn($targetIsDev);
 
-        $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager')
-            ->setConstructorArgs(array($this->io, false, $this->filesystem))
-            ->setMethods(array('getDownloaderForInstalledPackage', 'download'))
-            ->getMock();
-        $manager
-            ->expects($this->once())
-            ->method('getDownloaderForInstalledPackage')
-            ->with($initial)
-            ->will($this->returnValue($svnDownloader));
-
-        $manager->update($initial, $target, 'vendor/pkg');
+        $manager = new DownloadManager($this->io, false, $this->filesystem);
+        $method = new \ReflectionMethod($manager, 'getAvailableSources');
+        $method->setAccessible(true);
+        $this->assertEquals($expected, $method->invoke($manager, $target->reveal(), $initial ? $initial->reveal() : null));
     }
 
-    public function testUpdateSourceWithNotEqualTypes()
+    public static function updatesProvider()
     {
-        $initial = $this->createPackageMock();
-        $initial
-            ->expects($this->once())
-            ->method('getInstallationSource')
-            ->will($this->returnValue('source'));
-        $initial
-            ->expects($this->once())
-            ->method('getSourceType')
-            ->will($this->returnValue('svn'));
-
-        $target = $this->createPackageMock();
-        $target
-            ->expects($this->once())
-            ->method('getSourceType')
-            ->will($this->returnValue('git'));
-
-        $svnDownloader = $this->createDownloaderMock();
-        $svnDownloader
-            ->expects($this->once())
-            ->method('remove')
-            ->with($initial, 'vendor/pkg');
-
-        $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager')
-            ->setConstructorArgs(array($this->io, false, $this->filesystem))
-            ->setMethods(array('getDownloaderForInstalledPackage', 'download'))
-            ->getMock();
-        $manager
-            ->expects($this->once())
-            ->method('getDownloaderForInstalledPackage')
-            ->with($initial)
-            ->will($this->returnValue($svnDownloader));
-        $manager
-            ->expects($this->once())
-            ->method('download')
-            ->with($target, 'vendor/pkg', true);
-
-        $manager->update($initial, $target, 'vendor/pkg');
+        return array(
+            //    prevPkg source,  prevPkg isDev, pkg available,           pkg isDev,  expected
+            // updates keep previous source as preference
+            array('source',        false,         array('source', 'dist'), false,      array('source', 'dist')),
+            array('dist',          false,         array('source', 'dist'), false,      array('dist', 'source')),
+            // updates do not keep previous source if target package does not have it
+            array('source',        false,         array('dist'),           false,      array('dist')),
+            array('dist',          false,         array('source'),         false,      array('source')),
+            // updates do not keep previous source if target is dev and prev wasn't dev and installed from dist
+            array('source',        false,         array('source', 'dist'), true,       array('source', 'dist')),
+            array('dist',          false,         array('source', 'dist'), true,       array('source', 'dist')),
+            // install picks the right default
+            array(null,            null,          array('source', 'dist'), true,       array('source', 'dist')),
+            array(null,            null,          array('dist'),           true,       array('dist')),
+            array(null,            null,          array('source'),         true,       array('source')),
+            array(null,            null,          array('source', 'dist'), false,      array('dist', 'source')),
+            array(null,            null,          array('dist'),           false,      array('dist')),
+            array(null,            null,          array('source'),         false,      array('source')),
+        );
     }
 
     public function testUpdateMetapackage()
@@ -707,11 +680,11 @@ class DownloadManagerTest extends TestCase
 
         $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager')
           ->setConstructorArgs(array($this->io, false, $this->filesystem))
-          ->setMethods(array('getDownloaderForInstalledPackage'))
+          ->setMethods(array('getDownloaderForPackage'))
           ->getMock();
         $manager
-          ->expects($this->once())
-          ->method('getDownloaderForInstalledPackage')
+          ->expects($this->exactly(2))
+          ->method('getDownloaderForPackage')
           ->with($initial)
           ->will($this->returnValue(null)); // There is no downloader for metapackages.
 
@@ -730,11 +703,11 @@ class DownloadManagerTest extends TestCase
 
         $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager')
             ->setConstructorArgs(array($this->io, false, $this->filesystem))
-            ->setMethods(array('getDownloaderForInstalledPackage'))
+            ->setMethods(array('getDownloaderForPackage'))
             ->getMock();
         $manager
             ->expects($this->once())
-            ->method('getDownloaderForInstalledPackage')
+            ->method('getDownloaderForPackage')
             ->with($package)
             ->will($this->returnValue($pearDownloader));
 
@@ -747,11 +720,11 @@ class DownloadManagerTest extends TestCase
 
         $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager')
           ->setConstructorArgs(array($this->io, false, $this->filesystem))
-          ->setMethods(array('getDownloaderForInstalledPackage'))
+          ->setMethods(array('getDownloaderForPackage'))
           ->getMock();
         $manager
           ->expects($this->once())
-          ->method('getDownloaderForInstalledPackage')
+          ->method('getDownloaderForPackage')
           ->with($package)
           ->will($this->returnValue(null)); // There is no downloader for metapackages.
 
@@ -790,11 +763,11 @@ class DownloadManagerTest extends TestCase
 
         $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager')
             ->setConstructorArgs(array($this->io, false, $this->filesystem))
-            ->setMethods(array('getDownloaderForInstalledPackage'))
+            ->setMethods(array('getDownloaderForPackage'))
             ->getMock();
         $manager
             ->expects($this->once())
-            ->method('getDownloaderForInstalledPackage')
+            ->method('getDownloaderForPackage')
             ->with($package)
             ->will($this->returnValue($downloader));
 
@@ -833,11 +806,11 @@ class DownloadManagerTest extends TestCase
 
         $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager')
             ->setConstructorArgs(array($this->io, false, $this->filesystem))
-            ->setMethods(array('getDownloaderForInstalledPackage'))
+            ->setMethods(array('getDownloaderForPackage'))
             ->getMock();
         $manager
             ->expects($this->once())
-            ->method('getDownloaderForInstalledPackage')
+            ->method('getDownloaderForPackage')
             ->with($package)
             ->will($this->returnValue($downloader));
 
@@ -879,11 +852,11 @@ class DownloadManagerTest extends TestCase
 
         $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager')
             ->setConstructorArgs(array($this->io, false, $this->filesystem))
-            ->setMethods(array('getDownloaderForInstalledPackage'))
+            ->setMethods(array('getDownloaderForPackage'))
             ->getMock();
         $manager
             ->expects($this->once())
-            ->method('getDownloaderForInstalledPackage')
+            ->method('getDownloaderForPackage')
             ->with($package)
             ->will($this->returnValue($downloader));
         $manager->setPreferences(array('foo/*' => 'source'));
@@ -926,11 +899,11 @@ class DownloadManagerTest extends TestCase
 
         $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager')
             ->setConstructorArgs(array($this->io, false, $this->filesystem))
-            ->setMethods(array('getDownloaderForInstalledPackage'))
+            ->setMethods(array('getDownloaderForPackage'))
             ->getMock();
         $manager
             ->expects($this->once())
-            ->method('getDownloaderForInstalledPackage')
+            ->method('getDownloaderForPackage')
             ->with($package)
             ->will($this->returnValue($downloader));
         $manager->setPreferences(array('foo/*' => 'source'));
@@ -973,11 +946,11 @@ class DownloadManagerTest extends TestCase
 
         $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager')
             ->setConstructorArgs(array($this->io, false, $this->filesystem))
-            ->setMethods(array('getDownloaderForInstalledPackage'))
+            ->setMethods(array('getDownloaderForPackage'))
             ->getMock();
         $manager
             ->expects($this->once())
-            ->method('getDownloaderForInstalledPackage')
+            ->method('getDownloaderForPackage')
             ->with($package)
             ->will($this->returnValue($downloader));
         $manager->setPreferences(array('foo/*' => 'auto'));
@@ -1020,11 +993,11 @@ class DownloadManagerTest extends TestCase
 
         $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager')
             ->setConstructorArgs(array($this->io, false, $this->filesystem))
-            ->setMethods(array('getDownloaderForInstalledPackage'))
+            ->setMethods(array('getDownloaderForPackage'))
             ->getMock();
         $manager
             ->expects($this->once())
-            ->method('getDownloaderForInstalledPackage')
+            ->method('getDownloaderForPackage')
             ->with($package)
             ->will($this->returnValue($downloader));
         $manager->setPreferences(array('foo/*' => 'auto'));
@@ -1063,11 +1036,11 @@ class DownloadManagerTest extends TestCase
 
         $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager')
             ->setConstructorArgs(array($this->io, false, $this->filesystem))
-            ->setMethods(array('getDownloaderForInstalledPackage'))
+            ->setMethods(array('getDownloaderForPackage'))
             ->getMock();
         $manager
             ->expects($this->once())
-            ->method('getDownloaderForInstalledPackage')
+            ->method('getDownloaderForPackage')
             ->with($package)
             ->will($this->returnValue($downloader));
         $manager->setPreferences(array('foo/*' => 'source'));
@@ -1106,11 +1079,11 @@ class DownloadManagerTest extends TestCase
 
         $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager')
             ->setConstructorArgs(array($this->io, false, $this->filesystem))
-            ->setMethods(array('getDownloaderForInstalledPackage'))
+            ->setMethods(array('getDownloaderForPackage'))
             ->getMock();
         $manager
             ->expects($this->once())
-            ->method('getDownloaderForInstalledPackage')
+            ->method('getDownloaderForPackage')
             ->with($package)
             ->will($this->returnValue($downloader));
         $manager->setPreferences(array('foo/*' => 'dist'));

+ 30 - 9
tests/Composer/Test/Downloader/FileDownloaderTest.php

@@ -15,6 +15,8 @@ namespace Composer\Test\Downloader;
 use Composer\Downloader\FileDownloader;
 use Composer\Test\TestCase;
 use Composer\Util\Filesystem;
+use Composer\Util\Http\Response;
+use Composer\Util\Loop;
 
 class FileDownloaderTest extends TestCase
 {
@@ -23,6 +25,11 @@ class FileDownloaderTest extends TestCase
         $io = $io ?: $this->getMockBuilder('Composer\IO\IOInterface')->getMock();
         $config = $config ?: $this->getMockBuilder('Composer\Config')->getMock();
         $httpDownloader = $httpDownloader ?: $this->getMockBuilder('Composer\Util\HttpDownloader')->disableOriginalConstructor()->getMock();
+        $httpDownloader
+            ->expects($this->any())
+            ->method('addCopy')
+            ->will($this->returnValue(\React\Promise\resolve(new Response(array('url' => 'http://example.org/'), 200, array(), 'file~'))));
+        $this->httpDownloader = $httpDownloader;
 
         return new FileDownloader($io, $config, $httpDownloader, $eventDispatcher, $cache, $filesystem);
     }
@@ -84,7 +91,7 @@ class FileDownloaderTest extends TestCase
         $method = new \ReflectionMethod($downloader, 'getFileName');
         $method->setAccessible(true);
 
-        $this->assertEquals('/path/script.js', $method->invoke($downloader, $packageMock, '/path'));
+        $this->assertEquals('/path_script.js', $method->invoke($downloader, $packageMock, '/path'));
     }
 
     public function testDownloadButFileIsUnsaved()
@@ -118,8 +125,11 @@ class FileDownloaderTest extends TestCase
 
         $downloader = $this->getDownloader($ioMock);
         try {
-            $downloader->download($packageMock, $path);
-            $this->fail();
+            $promise = $downloader->download($packageMock, $path);
+            $loop = new Loop($this->httpDownloader);
+            $loop->wait(array($promise));
+
+            $this->fail('Download was expected to throw');
         } catch (\Exception $e) {
             if (is_dir($path)) {
                 $fs = new Filesystem();
@@ -128,7 +138,7 @@ class FileDownloaderTest extends TestCase
                 unlink($path);
             }
 
-            $this->assertInstanceOf('UnexpectedValueException', $e);
+            $this->assertInstanceOf('UnexpectedValueException', $e, $e->getMessage());
             $this->assertContains('could not be saved to', $e->getMessage());
         }
     }
@@ -188,11 +198,14 @@ class FileDownloaderTest extends TestCase
         $path = $this->getUniqueTmpDirectory();
         $downloader = $this->getDownloader(null, null, null, null, null, $filesystem);
         // make sure the file expected to be downloaded is on disk already
-        touch($path.'/script.js');
+        touch($path.'_script.js');
 
         try {
-            $downloader->download($packageMock, $path);
-            $this->fail();
+            $promise = $downloader->download($packageMock, $path);
+            $loop = new Loop($this->httpDownloader);
+            $loop->wait(array($promise));
+
+            $this->fail('Download was expected to throw');
         } catch (\Exception $e) {
             if (is_dir($path)) {
                 $fs = new Filesystem();
@@ -201,7 +214,7 @@ class FileDownloaderTest extends TestCase
                 unlink($path);
             }
 
-            $this->assertInstanceOf('UnexpectedValueException', $e);
+            $this->assertInstanceOf('UnexpectedValueException', $e, $e->getMessage());
             $this->assertContains('checksum verification', $e->getMessage());
         }
     }
@@ -232,17 +245,25 @@ class FileDownloaderTest extends TestCase
 
         $ioMock = $this->getMock('Composer\IO\IOInterface');
         $ioMock->expects($this->at(0))
+            ->method('writeError')
+            ->with($this->stringContains('Downloading'));
+
+        $ioMock->expects($this->at(1))
             ->method('writeError')
             ->with($this->stringContains('Downgrading'));
 
         $path = $this->getUniqueTmpDirectory();
-        touch($path.'/script.js');
+        touch($path.'_script.js');
         $filesystem = $this->getMock('Composer\Util\Filesystem');
         $filesystem->expects($this->once())
             ->method('removeDirectory')
             ->will($this->returnValue(true));
 
         $downloader = $this->getDownloader($ioMock, null, null, null, null, $filesystem);
+        $promise = $downloader->download($newPackage, $path, $oldPackage);
+        $loop = new Loop($this->httpDownloader);
+        $loop->wait(array($promise));
+
         $downloader->update($oldPackage, $newPackage, $path);
     }
 }

+ 2 - 2
tests/Composer/Test/Downloader/FossilDownloaderTest.php

@@ -56,7 +56,7 @@ class FossilDownloaderTest extends TestCase
             ->will($this->returnValue(null));
 
         $downloader = $this->getDownloaderMock();
-        $downloader->download($packageMock, '/path');
+        $downloader->install($packageMock, '/path');
     }
 
     public function testDownload()
@@ -89,7 +89,7 @@ class FossilDownloaderTest extends TestCase
             ->will($this->returnValue(0));
 
         $downloader = $this->getDownloaderMock(null, null, $processExecutor);
-        $downloader->download($packageMock, 'repo');
+        $downloader->install($packageMock, 'repo');
     }
 
     /**

+ 6 - 6
tests/Composer/Test/Downloader/GitDownloaderTest.php

@@ -79,7 +79,7 @@ class GitDownloaderTest extends TestCase
             ->will($this->returnValue(null));
 
         $downloader = $this->getDownloaderMock();
-        $downloader->download($packageMock, '/path');
+        $downloader->install($packageMock, '/path');
     }
 
     public function testDownload()
@@ -130,7 +130,7 @@ class GitDownloaderTest extends TestCase
             ->will($this->returnValue(0));
 
         $downloader = $this->getDownloaderMock(null, null, $processExecutor);
-        $downloader->download($packageMock, 'composerPath');
+        $downloader->install($packageMock, 'composerPath');
     }
 
     public function testDownloadWithCache()
@@ -195,7 +195,7 @@ class GitDownloaderTest extends TestCase
             ->will($this->returnValue(0));
 
         $downloader = $this->getDownloaderMock(null, $config, $processExecutor);
-        $downloader->download($packageMock, 'composerPath');
+        $downloader->install($packageMock, 'composerPath');
         @rmdir($cachePath);
     }
 
@@ -265,7 +265,7 @@ class GitDownloaderTest extends TestCase
             ->will($this->returnValue(0));
 
         $downloader = $this->getDownloaderMock(null, new Config(), $processExecutor);
-        $downloader->download($packageMock, 'composerPath');
+        $downloader->install($packageMock, 'composerPath');
     }
 
     public function pushUrlProvider()
@@ -329,7 +329,7 @@ class GitDownloaderTest extends TestCase
         $config->merge(array('config' => array('github-protocols' => $protocols)));
 
         $downloader = $this->getDownloaderMock(null, $config, $processExecutor);
-        $downloader->download($packageMock, 'composerPath');
+        $downloader->install($packageMock, 'composerPath');
     }
 
     /**
@@ -360,7 +360,7 @@ class GitDownloaderTest extends TestCase
             ->will($this->returnValue(1));
 
         $downloader = $this->getDownloaderMock(null, null, $processExecutor);
-        $downloader->download($packageMock, 'composerPath');
+        $downloader->install($packageMock, 'composerPath');
     }
 
     /**

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

@@ -56,7 +56,7 @@ class HgDownloaderTest extends TestCase
             ->will($this->returnValue(null));
 
         $downloader = $this->getDownloaderMock();
-        $downloader->download($packageMock, '/path');
+        $downloader->install($packageMock, '/path');
     }
 
     public function testDownload()
@@ -83,7 +83,7 @@ class HgDownloaderTest extends TestCase
             ->will($this->returnValue(0));
 
         $downloader = $this->getDownloaderMock(null, null, $processExecutor);
-        $downloader->download($packageMock, 'composerPath');
+        $downloader->install($packageMock, 'composerPath');
     }
 
     /**

+ 2 - 2
tests/Composer/Test/Downloader/PerforceDownloaderTest.php

@@ -138,7 +138,7 @@ class PerforceDownloaderTest extends TestCase
         $perforce->expects($this->at(5))->method('syncCodeBase')->with($label);
         $perforce->expects($this->at(6))->method('cleanupClientSpec');
         $this->downloader->setPerforce($perforce);
-        $this->downloader->doDownload($this->package, $this->testPath, 'url');
+        $this->downloader->doInstall($this->package, $this->testPath, 'url');
     }
 
     /**
@@ -161,6 +161,6 @@ class PerforceDownloaderTest extends TestCase
         $perforce->expects($this->at(5))->method('syncCodeBase')->with($label);
         $perforce->expects($this->at(6))->method('cleanupClientSpec');
         $this->downloader->setPerforce($perforce);
-        $this->downloader->doDownload($this->package, $this->testPath, 'url');
+        $this->downloader->doInstall($this->package, $this->testPath, 'url');
     }
 }

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

@@ -16,6 +16,7 @@ use Composer\Downloader\XzDownloader;
 use Composer\Test\TestCase;
 use Composer\Util\Filesystem;
 use Composer\Util\Platform;
+use Composer\Util\Loop;
 use Composer\Util\HttpDownloader;
 
 class XzDownloaderTest extends TestCase
@@ -66,10 +67,14 @@ class XzDownloaderTest extends TestCase
             ->method('get')
             ->with('vendor-dir')
             ->will($this->returnValue($this->testDir));
-        $downloader = new XzDownloader($io, $config, new HttpDownloader($io, $this->getMockBuilder('Composer\Config')->getMock()), null, null, null);
+        $downloader = new XzDownloader($io, $config, $httpDownloader = new HttpDownloader($io, $this->getMockBuilder('Composer\Config')->getMock()), null, null, null);
 
         try {
-            $downloader->download($packageMock, $this->getUniqueTmpDirectory());
+            $promise = $downloader->download($packageMock, $this->testDir);
+            $loop = new Loop($httpDownloader);
+            $loop->wait(array($promise));
+            $downloader->install($packageMock, $this->testDir);
+
             $this->fail('Download of invalid tarball should throw an exception');
         } catch (\RuntimeException $e) {
             $this->assertRegexp('/(File format not recognized|Unrecognized archive format)/i', $e->getMessage());

+ 27 - 16
tests/Composer/Test/Downloader/ZipDownloaderTest.php

@@ -17,6 +17,7 @@ use Composer\Package\PackageInterface;
 use Composer\Test\TestCase;
 use Composer\Util\Filesystem;
 use Composer\Util\HttpDownloader;
+use Composer\Util\Loop;
 
 class ZipDownloaderTest extends TestCase
 {
@@ -27,6 +28,7 @@ class ZipDownloaderTest extends TestCase
     private $prophet;
     private $io;
     private $config;
+    private $package;
 
     public function setUp()
     {
@@ -35,6 +37,7 @@ class ZipDownloaderTest extends TestCase
         $this->config = $this->getMockBuilder('Composer\Config')->getMock();
         $dlConfig = $this->getMockBuilder('Composer\Config')->getMock();
         $this->httpDownloader = new HttpDownloader($this->io, $dlConfig);
+        $this->package = $this->getMockBuilder('Composer\Package\PackageInterface')->getMock();
     }
 
     public function tearDown()
@@ -71,16 +74,15 @@ class ZipDownloaderTest extends TestCase
             ->with('vendor-dir')
             ->will($this->returnValue($this->testDir));
 
-        $packageMock = $this->getMockBuilder('Composer\Package\PackageInterface')->getMock();
-        $packageMock->expects($this->any())
+        $this->package->expects($this->any())
             ->method('getDistUrl')
             ->will($this->returnValue($distUrl = 'file://'.__FILE__))
         ;
-        $packageMock->expects($this->any())
+        $this->package->expects($this->any())
             ->method('getDistUrls')
             ->will($this->returnValue(array($distUrl)))
         ;
-        $packageMock->expects($this->atLeastOnce())
+        $this->package->expects($this->atLeastOnce())
             ->method('getTransportOptions')
             ->will($this->returnValue(array()))
         ;
@@ -90,7 +92,11 @@ class ZipDownloaderTest extends TestCase
         $this->setPrivateProperty('hasSystemUnzip', false);
 
         try {
-            $downloader->download($packageMock, sys_get_temp_dir().'/composer-zip-test');
+            $promise = $downloader->download($this->package, $path = sys_get_temp_dir().'/composer-zip-test');
+            $loop = new Loop($this->httpDownloader);
+            $loop->wait(array($promise));
+            $downloader->install($this->package, $path);
+
             $this->fail('Download of invalid zip files should throw an exception');
         } catch (\Exception $e) {
             $this->assertContains('is not a zip archive', $e->getMessage());
@@ -119,7 +125,7 @@ class ZipDownloaderTest extends TestCase
             ->will($this->returnValue(false));
 
         $this->setPrivateProperty('zipArchiveObject', $zipArchive, $downloader);
-        $downloader->extract('testfile.zip', 'vendor/dir');
+        $downloader->extract($this->package, 'testfile.zip', 'vendor/dir');
     }
 
     /**
@@ -144,7 +150,7 @@ class ZipDownloaderTest extends TestCase
             ->will($this->throwException(new \ErrorException('Not a directory')));
 
         $this->setPrivateProperty('zipArchiveObject', $zipArchive, $downloader);
-        $downloader->extract('testfile.zip', 'vendor/dir');
+        $downloader->extract($this->package, 'testfile.zip', 'vendor/dir');
     }
 
     /**
@@ -168,7 +174,7 @@ class ZipDownloaderTest extends TestCase
             ->will($this->returnValue(true));
 
         $this->setPrivateProperty('zipArchiveObject', $zipArchive, $downloader);
-        $downloader->extract('testfile.zip', 'vendor/dir');
+        $downloader->extract($this->package, 'testfile.zip', 'vendor/dir');
     }
 
     /**
@@ -189,7 +195,7 @@ class ZipDownloaderTest extends TestCase
             ->will($this->returnValue(1));
 
         $downloader = new MockedZipDownloader($this->io, $this->config, $this->httpDownloader, null, null, $processExecutor);
-        $downloader->extract('testfile.zip', 'vendor/dir');
+        $downloader->extract($this->package, 'testfile.zip', 'vendor/dir');
     }
 
     public function testSystemUnzipOnlyGood()
@@ -206,7 +212,7 @@ class ZipDownloaderTest extends TestCase
             ->will($this->returnValue(0));
 
         $downloader = new MockedZipDownloader($this->io, $this->config, $this->httpDownloader, null, null, $processExecutor);
-        $downloader->extract('testfile.zip', 'vendor/dir');
+        $downloader->extract($this->package, 'testfile.zip', 'vendor/dir');
     }
 
     public function testNonWindowsFallbackGood()
@@ -234,7 +240,7 @@ class ZipDownloaderTest extends TestCase
 
         $downloader = new MockedZipDownloader($this->io, $this->config, $this->httpDownloader, null, null, $processExecutor);
         $this->setPrivateProperty('zipArchiveObject', $zipArchive, $downloader);
-        $downloader->extract('testfile.zip', 'vendor/dir');
+        $downloader->extract($this->package, 'testfile.zip', 'vendor/dir');
     }
 
     /**
@@ -266,7 +272,7 @@ class ZipDownloaderTest extends TestCase
 
         $downloader = new MockedZipDownloader($this->io, $this->config, $this->httpDownloader, null, null, $processExecutor);
         $this->setPrivateProperty('zipArchiveObject', $zipArchive, $downloader);
-        $downloader->extract('testfile.zip', 'vendor/dir');
+        $downloader->extract($this->package, 'testfile.zip', 'vendor/dir');
     }
 
     public function testWindowsFallbackGood()
@@ -294,7 +300,7 @@ class ZipDownloaderTest extends TestCase
 
         $downloader = new MockedZipDownloader($this->io, $this->config, $this->httpDownloader, null, null, $processExecutor);
         $this->setPrivateProperty('zipArchiveObject', $zipArchive, $downloader);
-        $downloader->extract('testfile.zip', 'vendor/dir');
+        $downloader->extract($this->package, 'testfile.zip', 'vendor/dir');
     }
 
     /**
@@ -326,7 +332,7 @@ class ZipDownloaderTest extends TestCase
 
         $downloader = new MockedZipDownloader($this->io, $this->config, $this->httpDownloader, null, null, $processExecutor);
         $this->setPrivateProperty('zipArchiveObject', $zipArchive, $downloader);
-        $downloader->extract('testfile.zip', 'vendor/dir');
+        $downloader->extract($this->package, 'testfile.zip', 'vendor/dir');
     }
 }
 
@@ -337,8 +343,13 @@ class MockedZipDownloader extends ZipDownloader
         return;
     }
 
-    public function extract($file, $path)
+    public function install(PackageInterface $package, $path, $output = true)
+    {
+        return;
+    }
+
+    public function extract(PackageInterface $package, $file, $path)
     {
-        parent::extract($file, $path);
+        parent::extract($package, $file, $path);
     }
 }

+ 1 - 1
tests/Composer/Test/EventDispatcher/EventDispatcherTest.php

@@ -101,7 +101,7 @@ class EventDispatcherTest extends TestCase
         $composer->setPackage($package);
 
         $composer->setRepositoryManager($this->getRepositoryManagerMockForDevModePassingTest());
-        $composer->setInstallationManager($this->getMockBuilder('Composer\Installer\InstallationManager')->getMock());
+        $composer->setInstallationManager($this->getMockBuilder('Composer\Installer\InstallationManager')->disableOriginalConstructor()->getMock());
 
         $dispatcher = new EventDispatcher(
             $composer,

+ 26 - 15
tests/Composer/Test/Installer/InstallationManagerTest.php

@@ -13,6 +13,7 @@
 namespace Composer\Test\Installer;
 
 use Composer\Installer\InstallationManager;
+use Composer\Installer\NoopInstaller;
 use Composer\DependencyResolver\Operation\InstallOperation;
 use Composer\DependencyResolver\Operation\UpdateOperation;
 use Composer\DependencyResolver\Operation\UninstallOperation;
@@ -21,9 +22,11 @@ use PHPUnit\Framework\TestCase;
 class InstallationManagerTest extends TestCase
 {
     protected $repository;
+    protected $loop;
 
     public function setUp()
     {
+        $this->loop = $this->getMockBuilder('Composer\Util\Loop')->disableOriginalConstructor()->getMock();
         $this->repository = $this->getMockBuilder('Composer\Repository\InstalledRepositoryInterface')->getMock();
     }
 
@@ -38,7 +41,7 @@ class InstallationManagerTest extends TestCase
                 return $arg === 'vendor';
             }));
 
-        $manager = new InstallationManager();
+        $manager = new InstallationManager($this->loop);
 
         $manager->addInstaller($installer);
         $this->assertSame($installer, $manager->getInstaller('vendor'));
@@ -67,7 +70,7 @@ class InstallationManagerTest extends TestCase
                 return $arg === 'vendor';
             }));
 
-        $manager = new InstallationManager();
+        $manager = new InstallationManager($this->loop);
 
         $manager->addInstaller($installer);
         $this->assertSame($installer, $manager->getInstaller('vendor'));
@@ -80,16 +83,21 @@ class InstallationManagerTest extends TestCase
     public function testExecute()
     {
         $manager = $this->getMockBuilder('Composer\Installer\InstallationManager')
+            ->setConstructorArgs(array($this->loop))
             ->setMethods(array('install', 'update', 'uninstall'))
             ->getMock();
 
-        $installOperation = new InstallOperation($this->createPackageMock());
-        $removeOperation = new UninstallOperation($this->createPackageMock());
+        $installOperation = new InstallOperation($package = $this->createPackageMock());
+        $removeOperation = new UninstallOperation($package);
         $updateOperation = new UpdateOperation(
-            $this->createPackageMock(),
-            $this->createPackageMock()
+            $package,
+            $package
         );
 
+        $package->expects($this->any())
+            ->method('getType')
+            ->will($this->returnValue('library'));
+
         $manager
             ->expects($this->once())
             ->method('install')
@@ -103,6 +111,7 @@ class InstallationManagerTest extends TestCase
             ->method('update')
             ->with($this->repository, $updateOperation);
 
+        $manager->addInstaller(new NoopInstaller());
         $manager->execute($this->repository, $installOperation);
         $manager->execute($this->repository, $removeOperation);
         $manager->execute($this->repository, $updateOperation);
@@ -111,7 +120,7 @@ class InstallationManagerTest extends TestCase
     public function testInstall()
     {
         $installer = $this->createInstallerMock();
-        $manager = new InstallationManager();
+        $manager = new InstallationManager($this->loop);
         $manager->addInstaller($installer);
 
         $package = $this->createPackageMock();
@@ -139,7 +148,7 @@ class InstallationManagerTest extends TestCase
     public function testUpdateWithEqualTypes()
     {
         $installer = $this->createInstallerMock();
-        $manager = new InstallationManager();
+        $manager = new InstallationManager($this->loop);
         $manager->addInstaller($installer);
 
         $initial = $this->createPackageMock();
@@ -173,18 +182,17 @@ class InstallationManagerTest extends TestCase
     {
         $libInstaller = $this->createInstallerMock();
         $bundleInstaller = $this->createInstallerMock();
-        $manager = new InstallationManager();
+        $manager = new InstallationManager($this->loop);
         $manager->addInstaller($libInstaller);
         $manager->addInstaller($bundleInstaller);
 
         $initial = $this->createPackageMock();
-        $target = $this->createPackageMock();
-        $operation = new UpdateOperation($initial, $target, 'test');
-
         $initial
             ->expects($this->once())
             ->method('getType')
             ->will($this->returnValue('library'));
+
+        $target = $this->createPackageMock();
         $target
             ->expects($this->once())
             ->method('getType')
@@ -213,13 +221,14 @@ class InstallationManagerTest extends TestCase
             ->method('install')
             ->with($this->repository, $target);
 
+        $operation = new UpdateOperation($initial, $target, 'test');
         $manager->update($this->repository, $operation);
     }
 
     public function testUninstall()
     {
         $installer = $this->createInstallerMock();
-        $manager = new InstallationManager();
+        $manager = new InstallationManager($this->loop);
         $manager->addInstaller($installer);
 
         $package = $this->createPackageMock();
@@ -249,7 +258,7 @@ class InstallationManagerTest extends TestCase
         $installer = $this->getMockBuilder('Composer\Installer\LibraryInstaller')
             ->disableOriginalConstructor()
             ->getMock();
-        $manager = new InstallationManager();
+        $manager = new InstallationManager($this->loop);
         $manager->addInstaller($installer);
 
         $package = $this->createPackageMock();
@@ -281,7 +290,9 @@ class InstallationManagerTest extends TestCase
 
     private function createPackageMock()
     {
-        return $this->getMockBuilder('Composer\Package\PackageInterface')
+        $mock = $this->getMockBuilder('Composer\Package\PackageInterface')
             ->getMock();
+
+        return $mock;
     }
 }

+ 1 - 1
tests/Composer/Test/Installer/LibraryInstallerTest.php

@@ -113,7 +113,7 @@ class LibraryInstallerTest extends TestCase
 
         $this->dm
             ->expects($this->once())
-            ->method('download')
+            ->method('install')
             ->with($package, $this->vendorDir.'/some/package');
 
         $this->repository

+ 3 - 2
tests/Composer/Test/Mock/FactoryMock.php

@@ -20,6 +20,7 @@ use Composer\Repository\WritableRepositoryInterface;
 use Composer\Installer;
 use Composer\IO\IOInterface;
 use Composer\Test\TestCase;
+use Composer\Util\Loop;
 
 class FactoryMock extends Factory
 {
@@ -39,9 +40,9 @@ class FactoryMock extends Factory
     {
     }
 
-    protected function createInstallationManager()
+    public function createInstallationManager(Loop $loop)
     {
-        return new InstallationManagerMock;
+        return new InstallationManagerMock();
     }
 
     protected function createDefaultInstallers(Installer\InstallationManager $im, Composer $composer, IOInterface $io)

+ 13 - 0
tests/Composer/Test/Mock/InstallationManagerMock.php

@@ -17,6 +17,7 @@ use Composer\Repository\RepositoryInterface;
 use Composer\Repository\InstalledRepositoryInterface;
 use Composer\Package\PackageInterface;
 use Composer\DependencyResolver\Operation\InstallOperation;
+use Composer\DependencyResolver\Operation\OperationInterface;
 use Composer\DependencyResolver\Operation\UpdateOperation;
 use Composer\DependencyResolver\Operation\UninstallOperation;
 use Composer\DependencyResolver\Operation\MarkAliasInstalledOperation;
@@ -29,6 +30,18 @@ class InstallationManagerMock extends InstallationManager
     private $uninstalled = array();
     private $trace = array();
 
+    public function __construct()
+    {
+
+    }
+
+    public function execute(RepositoryInterface $repo, OperationInterface $operation)
+    {
+        $method = $operation->getJobType();
+        // skipping download() step here for tests
+        $this->$method($repo, $operation);
+    }
+
     public function getInstallPath(PackageInterface $package)
     {
         return '';

+ 4 - 2
tests/Composer/Test/Package/Archiver/ArchiveManagerTest.php

@@ -16,6 +16,7 @@ use Composer\IO\NullIO;
 use Composer\Factory;
 use Composer\Package\Archiver\ArchiveManager;
 use Composer\Package\PackageInterface;
+use Composer\Util\Loop;
 use Composer\Test\Mock\FactoryMock;
 
 class ArchiveManagerTest extends ArchiverTest
@@ -35,9 +36,10 @@ class ArchiveManagerTest extends ArchiverTest
         $dm = $factory->createDownloadManager(
             $io = new NullIO,
             $config = FactoryMock::createConfig(),
-            $factory->createHttpDownloader($io, $config)
+            $httpDownloader = $factory->createHttpDownloader($io, $config)
         );
-        $this->manager = $factory->createArchiveManager($factory->createConfig(), $dm);
+        $loop = new Loop($httpDownloader);
+        $this->manager = $factory->createArchiveManager($factory->createConfig(), $dm, $loop);
         $this->targetDir = $this->testDir.'/composer_archiver_tests';
     }
 

+ 1 - 1
tests/Composer/Test/Plugin/PluginInstallerTest.php

@@ -89,7 +89,7 @@ class PluginInstallerTest extends TestCase
             ->method('getLocalRepository')
             ->will($this->returnValue($this->repository));
 
-        $im = $this->getMockBuilder('Composer\Installer\InstallationManager')->getMock();
+        $im = $this->getMockBuilder('Composer\Installer\InstallationManager')->disableOriginalConstructor()->getMock();
         $im->expects($this->any())
             ->method('getInstallPath')
             ->will($this->returnCallback(function ($package) {