* @author Nicolas Grekas
*/
@@ -90,6 +91,9 @@ class CurlDownloader
$this->authHelper = new AuthHelper($io, $config);
}
+ /**
+ * @return int internal job id
+ */
public function download($resolve, $reject, $origin, $url, $options, $copyTo = null)
{
$attributes = array();
@@ -101,6 +105,9 @@ class CurlDownloader
return $this->initDownload($resolve, $reject, $origin, $url, $options, $copyTo, $attributes);
}
+ /**
+ * @return int internal job id
+ */
private function initDownload($resolve, $reject, $origin, $url, $options, $copyTo = null, array $attributes = array())
{
$attributes = array_merge(array(
@@ -199,8 +206,29 @@ class CurlDownloader
}
$this->checkCurlResult(curl_multi_add_handle($this->multiHandle, $curlHandle));
-// TODO progress
+ // TODO progress
//$params['notification'](STREAM_NOTIFY_RESOLVE, STREAM_NOTIFY_SEVERITY_INFO, '', 0, 0, 0, false);
+
+ return (int) $curlHandle;
+ }
+
+ public function abortRequest($id)
+ {
+ if (isset($this->jobs[$id]) && isset($this->jobs[$id]['handle'])) {
+ $job = $this->jobs[$id];
+ curl_multi_remove_handle($this->multiHandle, $job['handle']);
+ curl_close($job['handle']);
+ if (is_resource($job['headerHandle'])) {
+ fclose($job['headerHandle']);
+ }
+ if (is_resource($job['bodyHandle'])) {
+ fclose($job['bodyHandle']);
+ }
+ if ($job['filename']) {
+ @unlink($job['filename'].'~');
+ }
+ unset($this->jobs[$id]);
+ }
}
public function tick()
@@ -235,7 +263,7 @@ class CurlDownloader
$statusCode = null;
$response = null;
try {
-// TODO progress
+ // TODO progress
//$this->onProgress($curlHandle, $job['callback'], $progress, $job['progress']);
if (CURLE_OK !== $errno || $error) {
throw new TransportException($error);
@@ -285,8 +313,6 @@ class CurlDownloader
// fail 4xx and 5xx responses and capture the response
if ($statusCode >= 400 && $statusCode <= 599) {
throw $this->failResponse($job, $response, $response->getStatusMessage());
-// TODO progress
-// $this->io->overwriteError("Downloading (failed)", false);
}
if ($job['attributes']['storeAuth']) {
diff --git a/src/Composer/Util/HttpDownloader.php b/src/Composer/Util/HttpDownloader.php
index 2fa8fa716..889fae07e 100644
--- a/src/Composer/Util/HttpDownloader.php
+++ b/src/Composer/Util/HttpDownloader.php
@@ -31,6 +31,7 @@ class HttpDownloader
const STATUS_STARTED = 2;
const STATUS_COMPLETED = 3;
const STATUS_FAILED = 4;
+ const STATUS_ABORTED = 5;
private $io;
private $config;
@@ -44,6 +45,7 @@ class HttpDownloader
private $rfs;
private $idGen = 0;
private $disabled;
+ private $allowAsync = false;
/**
* @param IOInterface $io The IO instance
@@ -139,6 +141,10 @@ class HttpDownloader
'origin' => Url::getOrigin($this->config, $request['url']),
);
+ if (!$sync && !$this->allowAsync) {
+ throw new \LogicException('You must use the HttpDownloader instance which is part of a Composer\Loop instance to be able to run async http requests');
+ }
+
// capture username/password from URL if there is one
if (preg_match('{^https?://([^:/]+):([^@/]+)@([^/]+)}i', $request['url'], $match)) {
$this->io->setAuthentication($job['origin'], rawurldecode($match[1]), rawurldecode($match[2]));
@@ -179,8 +185,20 @@ class HttpDownloader
$downloader = $this;
$io = $this->io;
+ $curl = $this->curl;
- $canceler = function () {};
+ $canceler = function () use (&$job, $curl) {
+ if ($job['status'] === self::STATUS_QUEUED) {
+ $job['status'] = self::STATUS_ABORTED;
+ }
+ if ($job['status'] !== self::STATUS_STARTED) {
+ return;
+ }
+ $job['status'] = self::STATUS_ABORTED;
+ if (isset($job['curl_id'])) {
+ $curl->abortRequest($job['curl_id']);
+ }
+ };
$promise = new Promise($resolver, $canceler);
$promise->then(function ($response) use (&$job, $downloader) {
@@ -189,7 +207,6 @@ class HttpDownloader
// TODO 3.0 this should be done directly on $this when PHP 5.3 is dropped
$downloader->markJobDone();
- $downloader->scheduleNextJob();
return $response;
}, function ($e) use (&$job, $downloader) {
@@ -197,7 +214,6 @@ class HttpDownloader
$job['exception'] = $e;
$downloader->markJobDone();
- $downloader->scheduleNextJob();
throw $e;
});
@@ -239,9 +255,9 @@ class HttpDownloader
}
if ($job['request']['copyTo']) {
- $this->curl->download($resolve, $reject, $origin, $url, $options, $job['request']['copyTo']);
+ $job['curl_id'] = $this->curl->download($resolve, $reject, $origin, $url, $options, $job['request']['copyTo']);
} else {
- $this->curl->download($resolve, $reject, $origin, $url, $options);
+ $job['curl_id'] = $this->curl->download($resolve, $reject, $origin, $url, $options);
}
}
@@ -253,51 +269,60 @@ class HttpDownloader
$this->runningJobs--;
}
- /**
- * @private
- */
- public function scheduleNextJob()
- {
- foreach ($this->jobs as $job) {
- if ($job['status'] === self::STATUS_QUEUED) {
- $this->startJob($job['id']);
- if ($this->runningJobs >= $this->maxJobs) {
- return;
- }
- }
- }
- }
-
- public function wait($index = null, $progress = false)
+ public function wait($index = null)
{
while (true) {
- if ($this->curl) {
- $this->curl->tick();
- }
-
- if (null !== $index) {
- if ($this->jobs[$index]['status'] === self::STATUS_COMPLETED || $this->jobs[$index]['status'] === self::STATUS_FAILED) {
- return;
- }
- } else {
- $done = true;
- foreach ($this->jobs as $job) {
- if (!in_array($job['status'], array(self::STATUS_COMPLETED, self::STATUS_FAILED), true)) {
- $done = false;
- break;
- } elseif (!$job['sync']) {
- unset($this->jobs[$job['id']]);
- }
- }
- if ($done) {
- return;
- }
+ if (!$this->countActiveJobs($index)) {
+ return;
}
usleep(1000);
}
}
+ /**
+ * @internal
+ */
+ public function enableAsync()
+ {
+ $this->allowAsync = true;
+ }
+
+ /**
+ * @internal
+ *
+ * @return int number of active (queued or started) jobs
+ */
+ public function countActiveJobs($index = null)
+ {
+ if ($this->runningJobs < $this->maxJobs) {
+ foreach ($this->jobs as $job) {
+ if ($job['status'] === self::STATUS_QUEUED && $this->runningJobs < $this->maxJobs) {
+ $this->startJob($job['id']);
+ }
+ }
+ }
+
+ if ($this->curl) {
+ $this->curl->tick();
+ }
+
+ if (null !== $index) {
+ return $this->jobs[$index]['status'] < self::STATUS_COMPLETED ? 1 : 0;
+ }
+
+ $active = 0;
+ foreach ($this->jobs as $job) {
+ if ($job['status'] < self::STATUS_COMPLETED) {
+ $active++;
+ } elseif (!$job['sync']) {
+ unset($this->jobs[$job['id']]);
+ }
+ }
+
+ return $active;
+ }
+
private function getResponse($index)
{
if (!isset($this->jobs[$index])) {
diff --git a/src/Composer/Util/Loop.php b/src/Composer/Util/Loop.php
index dfaa2ac53..00159d562 100644
--- a/src/Composer/Util/Loop.php
+++ b/src/Composer/Util/Loop.php
@@ -14,6 +14,7 @@ namespace Composer\Util;
use Composer\Util\HttpDownloader;
use React\Promise\Promise;
+use Symfony\Component\Console\Helper\ProgressBar;
/**
* @author Jordi Boggiano
@@ -21,13 +22,22 @@ use React\Promise\Promise;
class Loop
{
private $httpDownloader;
+ private $processExecutor;
+ private $currentPromises;
- public function __construct(HttpDownloader $httpDownloader)
+ public function __construct(HttpDownloader $httpDownloader = null, ProcessExecutor $processExecutor = null)
{
$this->httpDownloader = $httpDownloader;
+ if ($this->httpDownloader) {
+ $this->httpDownloader->enableAsync();
+ }
+ $this->processExecutor = $processExecutor;
+ if ($this->processExecutor) {
+ $this->processExecutor->enableAsync();
+ }
}
- public function wait(array $promises)
+ public function wait(array $promises, ProgressBar $progress = null)
{
/** @var \Exception|null */
$uncaught = null;
@@ -39,10 +49,52 @@ class Loop
}
);
- $this->httpDownloader->wait();
+ $this->currentPromises = $promises;
+ if ($progress) {
+ $totalJobs = 0;
+ if ($this->httpDownloader) {
+ $totalJobs += $this->httpDownloader->countActiveJobs();
+ }
+ if ($this->processExecutor) {
+ $totalJobs += $this->processExecutor->countActiveJobs();
+ }
+ $progress->start($totalJobs);
+ }
+
+ while (true) {
+ $activeJobs = 0;
+
+ if ($this->httpDownloader) {
+ $activeJobs += $this->httpDownloader->countActiveJobs();
+ }
+ if ($this->processExecutor) {
+ $activeJobs += $this->processExecutor->countActiveJobs();
+ }
+
+ if ($progress) {
+ $progress->setProgress($progress->getMaxSteps() - $activeJobs);
+ }
+
+ if (!$activeJobs) {
+ break;
+ }
+
+ usleep(5000);
+ }
+
+ $this->currentPromises = null;
if ($uncaught) {
throw $uncaught;
}
}
+
+ public function abortJobs()
+ {
+ if ($this->currentPromises) {
+ foreach ($this->currentPromises as $promise) {
+ $promise->cancel();
+ }
+ }
+ }
}
diff --git a/src/Composer/Util/ProcessExecutor.php b/src/Composer/Util/ProcessExecutor.php
index a30a04d15..96b9235c8 100644
--- a/src/Composer/Util/ProcessExecutor.php
+++ b/src/Composer/Util/ProcessExecutor.php
@@ -16,18 +16,32 @@ use Composer\IO\IOInterface;
use Symfony\Component\Process\Process;
use Symfony\Component\Process\ProcessUtils;
use Symfony\Component\Process\Exception\RuntimeException;
+use React\Promise\Promise;
/**
* @author Robert Schönthal
+ * @author Jordi Boggiano
*/
class ProcessExecutor
{
+ const STATUS_QUEUED = 1;
+ const STATUS_STARTED = 2;
+ const STATUS_COMPLETED = 3;
+ const STATUS_FAILED = 4;
+ const STATUS_ABORTED = 5;
+
protected static $timeout = 300;
protected $captureOutput;
protected $errorOutput;
protected $io;
+ private $jobs = array();
+ private $runningJobs = 0;
+ private $maxJobs = 10;
+ private $idGen = 0;
+ private $allowAsync = false;
+
public function __construct(IOInterface $io = null)
{
$this->io = $io;
@@ -112,6 +126,192 @@ class ProcessExecutor
return $process->getExitCode();
}
+ /**
+ * starts a process on the commandline in async mode
+ *
+ * @param string $command the command to execute
+ * @param mixed $output the output will be written into this var if passed by ref
+ * if a callable is passed it will be used as output handler
+ * @param string $cwd the working directory
+ * @return int statuscode
+ */
+ public function executeAsync($command, $cwd = null)
+ {
+ if (!$this->allowAsync) {
+ throw new \LogicException('You must use the ProcessExecutor instance which is part of a Composer\Loop instance to be able to run async processes');
+ }
+
+ $job = array(
+ 'id' => $this->idGen++,
+ 'status' => self::STATUS_QUEUED,
+ 'command' => $command,
+ 'cwd' => $cwd,
+ );
+
+ $resolver = function ($resolve, $reject) use (&$job) {
+ $job['status'] = ProcessExecutor::STATUS_QUEUED;
+ $job['resolve'] = $resolve;
+ $job['reject'] = $reject;
+ };
+
+ $self = $this;
+ $io = $this->io;
+
+ $canceler = function () use (&$job) {
+ if ($job['status'] === self::STATUS_QUEUED) {
+ $job['status'] = self::STATUS_ABORTED;
+ }
+ if ($job['status'] !== self::STATUS_STARTED) {
+ return;
+ }
+ $job['status'] = self::STATUS_ABORTED;
+ try {
+ if (defined('SIGINT')) {
+ $job['process']->signal(SIGINT);
+ }
+ } catch (\Exception $e) {
+ // signal can throw in various conditions, but we don't care if it fails
+ }
+ $job['process']->stop(1);
+ };
+
+ $promise = new Promise($resolver, $canceler);
+ $promise = $promise->then(function () use (&$job, $self) {
+ if ($job['process']->isSuccessful()) {
+ $job['status'] = ProcessExecutor::STATUS_COMPLETED;
+ } else {
+ $job['status'] = ProcessExecutor::STATUS_FAILED;
+ }
+
+ // TODO 3.0 this should be done directly on $this when PHP 5.3 is dropped
+ $self->markJobDone();
+
+ return $job['process'];
+ }, function ($e) use (&$job, $self) {
+ $job['status'] = ProcessExecutor::STATUS_FAILED;
+
+ $self->markJobDone();
+
+ throw $e;
+ });
+ $this->jobs[$job['id']] =& $job;
+
+ if ($this->runningJobs < $this->maxJobs) {
+ $this->startJob($job['id']);
+ }
+
+ return $promise;
+ }
+
+ private function startJob($id)
+ {
+ $job =& $this->jobs[$id];
+ if ($job['status'] !== self::STATUS_QUEUED) {
+ return;
+ }
+
+ // start job
+ $job['status'] = self::STATUS_STARTED;
+ $this->runningJobs++;
+
+ $command = $job['command'];
+ $cwd = $job['cwd'];
+
+ if ($this->io && $this->io->isDebug()) {
+ $safeCommand = preg_replace_callback('{://(?P[^:/\s]+):(?P[^@\s/]+)@}i', function ($m) {
+ if (preg_match('{^[a-f0-9]{12,}$}', $m['user'])) {
+ return '://***:***@';
+ }
+
+ return '://'.$m['user'].':***@';
+ }, $command);
+ $safeCommand = preg_replace("{--password (.*[^\\\\]\') }", '--password \'***\' ', $safeCommand);
+ $this->io->writeError('Executing async command ('.($cwd ?: 'CWD').'): '.$safeCommand);
+ }
+
+ // make sure that null translate to the proper directory in case the dir is a symlink
+ // and we call a git command, because msysgit does not handle symlinks properly
+ if (null === $cwd && Platform::isWindows() && false !== strpos($command, 'git') && getcwd()) {
+ $cwd = realpath(getcwd());
+ }
+
+ // TODO in v3, commands should be passed in as arrays of cmd + args
+ if (method_exists('Symfony\Component\Process\Process', 'fromShellCommandline')) {
+ $process = Process::fromShellCommandline($command, $cwd, null, null, static::getTimeout());
+ } else {
+ $process = new Process($command, $cwd, null, null, static::getTimeout());
+ }
+
+ $job['process'] = $process;
+
+ $process->start();
+ }
+
+ public function wait($index = null)
+ {
+ while (true) {
+ if (!$this->countActiveJobs($index)) {
+ return;
+ }
+
+ usleep(1000);
+ }
+ }
+
+ /**
+ * @internal
+ */
+ public function enableAsync()
+ {
+ $this->allowAsync = true;
+ }
+
+ /**
+ * @internal
+ *
+ * @return int number of active (queued or started) jobs
+ */
+ public function countActiveJobs($index = null)
+ {
+ // tick
+ foreach ($this->jobs as $job) {
+ if ($job['status'] === self::STATUS_STARTED) {
+ if (!$job['process']->isRunning()) {
+ call_user_func($job['resolve'], $job['process']);
+ }
+ }
+
+ if ($this->runningJobs < $this->maxJobs) {
+ if ($job['status'] === self::STATUS_QUEUED) {
+ $this->startJob($job['id']);
+ }
+ }
+ }
+
+ if (null !== $index) {
+ return $this->jobs[$index]['status'] < self::STATUS_COMPLETED ? 1 : 0;
+ }
+
+ $active = 0;
+ foreach ($this->jobs as $job) {
+ if ($job['status'] < self::STATUS_COMPLETED) {
+ $active++;
+ } else {
+ unset($this->jobs[$job['id']]);
+ }
+ }
+
+ return $active;
+ }
+
+ /**
+ * @private
+ */
+ public function markJobDone()
+ {
+ $this->runningJobs--;
+ }
+
public function splitLines($output)
{
$output = trim($output);
diff --git a/src/Composer/Util/RemoteFilesystem.php b/src/Composer/Util/RemoteFilesystem.php
index b0dfbea94..4bac6a88f 100644
--- a/src/Composer/Util/RemoteFilesystem.php
+++ b/src/Composer/Util/RemoteFilesystem.php
@@ -20,6 +20,7 @@ use Composer\Util\HttpDownloader;
use Composer\Util\Http\Response;
/**
+ * @internal
* @author François Pluchino
* @author Jordi Boggiano
* @author Nils Adermann
diff --git a/tests/Composer/Test/Downloader/FileDownloaderTest.php b/tests/Composer/Test/Downloader/FileDownloaderTest.php
index c86ffa2f7..ba8f95db9 100644
--- a/tests/Composer/Test/Downloader/FileDownloaderTest.php
+++ b/tests/Composer/Test/Downloader/FileDownloaderTest.php
@@ -139,8 +139,8 @@ class FileDownloaderTest extends TestCase
->will($this->returnValue($path.'/vendor'));
try {
- $promise = $downloader->download($packageMock, $path);
$loop = new Loop($this->httpDownloader);
+ $promise = $downloader->download($packageMock, $path);
$loop->wait(array($promise));
$this->fail('Download was expected to throw');
@@ -225,8 +225,8 @@ class FileDownloaderTest extends TestCase
touch($dlFile);
try {
- $promise = $downloader->download($packageMock, $path);
$loop = new Loop($this->httpDownloader);
+ $promise = $downloader->download($packageMock, $path);
$loop->wait(array($promise));
$this->fail('Download was expected to throw');
@@ -296,8 +296,8 @@ class FileDownloaderTest extends TestCase
mkdir(dirname($dlFile), 0777, true);
touch($dlFile);
- $promise = $downloader->download($newPackage, $path, $oldPackage);
$loop = new Loop($this->httpDownloader);
+ $promise = $downloader->download($newPackage, $path, $oldPackage);
$loop->wait(array($promise));
$downloader->update($oldPackage, $newPackage, $path);
diff --git a/tests/Composer/Test/Downloader/XzDownloaderTest.php b/tests/Composer/Test/Downloader/XzDownloaderTest.php
index f770b0d35..6996d67f6 100644
--- a/tests/Composer/Test/Downloader/XzDownloaderTest.php
+++ b/tests/Composer/Test/Downloader/XzDownloaderTest.php
@@ -70,8 +70,8 @@ class XzDownloaderTest extends TestCase
$downloader = new XzDownloader($io, $config, $httpDownloader = new HttpDownloader($io, $this->getMockBuilder('Composer\Config')->getMock()), null, null, null);
try {
- $promise = $downloader->download($packageMock, $this->testDir.'/install-path');
$loop = new Loop($httpDownloader);
+ $promise = $downloader->download($packageMock, $this->testDir.'/install-path');
$loop->wait(array($promise));
$downloader->install($packageMock, $this->testDir.'/install-path');
diff --git a/tests/Composer/Test/Downloader/ZipDownloaderTest.php b/tests/Composer/Test/Downloader/ZipDownloaderTest.php
index 4436c6ad7..dc0f55aec 100644
--- a/tests/Composer/Test/Downloader/ZipDownloaderTest.php
+++ b/tests/Composer/Test/Downloader/ZipDownloaderTest.php
@@ -60,9 +60,6 @@ class ZipDownloaderTest extends TestCase
}
}
- /**
- * @group only
- */
public function testErrorMessages()
{
if (!class_exists('ZipArchive')) {
@@ -92,8 +89,8 @@ class ZipDownloaderTest extends TestCase
$this->setPrivateProperty('hasSystemUnzip', false);
try {
- $promise = $downloader->download($this->package, $path = sys_get_temp_dir().'/composer-zip-test');
$loop = new Loop($this->httpDownloader);
+ $promise = $downloader->download($this->package, $path = sys_get_temp_dir().'/composer-zip-test');
$loop->wait(array($promise));
$downloader->install($this->package, $path);
@@ -125,7 +122,8 @@ class ZipDownloaderTest extends TestCase
->will($this->returnValue(false));
$this->setPrivateProperty('zipArchiveObject', $zipArchive, $downloader);
- $downloader->extract($this->package, 'testfile.zip', 'vendor/dir');
+ $promise = $downloader->extract($this->package, 'testfile.zip', 'vendor/dir');
+ $this->wait($promise);
}
/**
@@ -150,12 +148,10 @@ class ZipDownloaderTest extends TestCase
->will($this->throwException(new \ErrorException('Not a directory')));
$this->setPrivateProperty('zipArchiveObject', $zipArchive, $downloader);
- $downloader->extract($this->package, 'testfile.zip', 'vendor/dir');
+ $promise = $downloader->extract($this->package, 'testfile.zip', 'vendor/dir');
+ $this->wait($promise);
}
- /**
- * @group only
- */
public function testZipArchiveOnlyGood()
{
if (!class_exists('ZipArchive')) {
@@ -174,45 +170,66 @@ class ZipDownloaderTest extends TestCase
->will($this->returnValue(true));
$this->setPrivateProperty('zipArchiveObject', $zipArchive, $downloader);
- $downloader->extract($this->package, 'testfile.zip', 'vendor/dir');
+ $promise = $downloader->extract($this->package, 'testfile.zip', 'vendor/dir');
+ $this->wait($promise);
}
/**
* @expectedException \Exception
- * @expectedExceptionMessage Failed to execute (1) unzip
+ * @expectedExceptionMessage Failed to extract : (1) unzip
*/
public function testSystemUnzipOnlyFailed()
{
- if (!class_exists('ZipArchive')) {
- $this->markTestSkipped('zip extension missing');
- }
-
+ $this->setPrivateProperty('isWindows', false);
$this->setPrivateProperty('hasSystemUnzip', true);
$this->setPrivateProperty('hasZipArchive', false);
+
+ $procMock = $this->getMockBuilder('Symfony\Component\Process\Process')->disableOriginalConstructor()->getMock();
+ $procMock->expects($this->any())
+ ->method('getExitCode')
+ ->will($this->returnValue(1));
+ $procMock->expects($this->any())
+ ->method('isSuccessful')
+ ->will($this->returnValue(false));
+ $procMock->expects($this->any())
+ ->method('getErrorOutput')
+ ->will($this->returnValue('output'));
+
$processExecutor = $this->getMockBuilder('Composer\Util\ProcessExecutor')->getMock();
$processExecutor->expects($this->at(0))
- ->method('execute')
- ->will($this->returnValue(1));
+ ->method('executeAsync')
+ ->will($this->returnValue(\React\Promise\resolve($procMock)));
$downloader = new MockedZipDownloader($this->io, $this->config, $this->httpDownloader, null, null, null, $processExecutor);
- $downloader->extract($this->package, 'testfile.zip', 'vendor/dir');
+ $promise = $downloader->extract($this->package, 'testfile.zip', 'vendor/dir');
+ $this->wait($promise);
}
public function testSystemUnzipOnlyGood()
{
- if (!class_exists('ZipArchive')) {
- $this->markTestSkipped('zip extension missing');
- }
-
+ $this->setPrivateProperty('isWindows', false);
$this->setPrivateProperty('hasSystemUnzip', true);
$this->setPrivateProperty('hasZipArchive', false);
+
+ $procMock = $this->getMockBuilder('Symfony\Component\Process\Process')->disableOriginalConstructor()->getMock();
+ $procMock->expects($this->any())
+ ->method('getExitCode')
+ ->will($this->returnValue(0));
+ $procMock->expects($this->any())
+ ->method('isSuccessful')
+ ->will($this->returnValue(true));
+ $procMock->expects($this->any())
+ ->method('getErrorOutput')
+ ->will($this->returnValue('output'));
+
$processExecutor = $this->getMockBuilder('Composer\Util\ProcessExecutor')->getMock();
$processExecutor->expects($this->at(0))
- ->method('execute')
- ->will($this->returnValue(0));
+ ->method('executeAsync')
+ ->will($this->returnValue(\React\Promise\resolve($procMock)));
$downloader = new MockedZipDownloader($this->io, $this->config, $this->httpDownloader, null, null, null, $processExecutor);
- $downloader->extract($this->package, 'testfile.zip', 'vendor/dir');
+ $promise = $downloader->extract($this->package, 'testfile.zip', 'vendor/dir');
+ $this->wait($promise);
}
public function testNonWindowsFallbackGood()
@@ -225,10 +242,21 @@ class ZipDownloaderTest extends TestCase
$this->setPrivateProperty('hasSystemUnzip', true);
$this->setPrivateProperty('hasZipArchive', true);
+ $procMock = $this->getMockBuilder('Symfony\Component\Process\Process')->disableOriginalConstructor()->getMock();
+ $procMock->expects($this->any())
+ ->method('getExitCode')
+ ->will($this->returnValue(1));
+ $procMock->expects($this->any())
+ ->method('isSuccessful')
+ ->will($this->returnValue(false));
+ $procMock->expects($this->any())
+ ->method('getErrorOutput')
+ ->will($this->returnValue('output'));
+
$processExecutor = $this->getMockBuilder('Composer\Util\ProcessExecutor')->getMock();
$processExecutor->expects($this->at(0))
- ->method('execute')
- ->will($this->returnValue(1));
+ ->method('executeAsync')
+ ->will($this->returnValue(\React\Promise\resolve($procMock)));
$zipArchive = $this->getMockBuilder('ZipArchive')->getMock();
$zipArchive->expects($this->at(0))
@@ -240,7 +268,8 @@ class ZipDownloaderTest extends TestCase
$downloader = new MockedZipDownloader($this->io, $this->config, $this->httpDownloader, null, null, null, $processExecutor);
$this->setPrivateProperty('zipArchiveObject', $zipArchive, $downloader);
- $downloader->extract($this->package, 'testfile.zip', 'vendor/dir');
+ $promise = $downloader->extract($this->package, 'testfile.zip', 'vendor/dir');
+ $this->wait($promise);
}
/**
@@ -257,10 +286,21 @@ class ZipDownloaderTest extends TestCase
$this->setPrivateProperty('hasSystemUnzip', true);
$this->setPrivateProperty('hasZipArchive', true);
+ $procMock = $this->getMockBuilder('Symfony\Component\Process\Process')->disableOriginalConstructor()->getMock();
+ $procMock->expects($this->any())
+ ->method('getExitCode')
+ ->will($this->returnValue(1));
+ $procMock->expects($this->any())
+ ->method('isSuccessful')
+ ->will($this->returnValue(false));
+ $procMock->expects($this->any())
+ ->method('getErrorOutput')
+ ->will($this->returnValue('output'));
+
$processExecutor = $this->getMockBuilder('Composer\Util\ProcessExecutor')->getMock();
$processExecutor->expects($this->at(0))
- ->method('execute')
- ->will($this->returnValue(1));
+ ->method('executeAsync')
+ ->will($this->returnValue(\React\Promise\resolve($procMock)));
$zipArchive = $this->getMockBuilder('ZipArchive')->getMock();
$zipArchive->expects($this->at(0))
@@ -272,7 +312,8 @@ class ZipDownloaderTest extends TestCase
$downloader = new MockedZipDownloader($this->io, $this->config, $this->httpDownloader, null, null, null, $processExecutor);
$this->setPrivateProperty('zipArchiveObject', $zipArchive, $downloader);
- $downloader->extract($this->package, 'testfile.zip', 'vendor/dir');
+ $promise = $downloader->extract($this->package, 'testfile.zip', 'vendor/dir');
+ $this->wait($promise);
}
public function testWindowsFallbackGood()
@@ -300,7 +341,8 @@ class ZipDownloaderTest extends TestCase
$downloader = new MockedZipDownloader($this->io, $this->config, $this->httpDownloader, null, null, null, $processExecutor);
$this->setPrivateProperty('zipArchiveObject', $zipArchive, $downloader);
- $downloader->extract($this->package, 'testfile.zip', 'vendor/dir');
+ $promise = $downloader->extract($this->package, 'testfile.zip', 'vendor/dir');
+ $this->wait($promise);
}
/**
@@ -332,7 +374,26 @@ class ZipDownloaderTest extends TestCase
$downloader = new MockedZipDownloader($this->io, $this->config, $this->httpDownloader, null, null, null, $processExecutor);
$this->setPrivateProperty('zipArchiveObject', $zipArchive, $downloader);
- $downloader->extract($this->package, 'testfile.zip', 'vendor/dir');
+ $promise = $downloader->extract($this->package, 'testfile.zip', 'vendor/dir');
+ $this->wait($promise);
+ }
+
+ private function wait($promise)
+ {
+ if (null === $promise) {
+ return;
+ }
+
+ $e = null;
+ $promise->then(function () {
+ // noop
+ }, function ($ex) use (&$e) {
+ $e = $ex;
+ });
+
+ if ($e) {
+ throw $e;
+ }
}
}
@@ -350,6 +411,6 @@ class MockedZipDownloader extends ZipDownloader
public function extract(PackageInterface $package, $file, $path)
{
- parent::extract($package, $file, $path);
+ return parent::extract($package, $file, $path);
}
}
diff --git a/tests/Composer/Test/Repository/ComposerRepositoryTest.php b/tests/Composer/Test/Repository/ComposerRepositoryTest.php
index 4fcbbb431..01e3be4ce 100644
--- a/tests/Composer/Test/Repository/ComposerRepositoryTest.php
+++ b/tests/Composer/Test/Repository/ComposerRepositoryTest.php
@@ -189,16 +189,19 @@ class ComposerRepositoryTest extends TestCase
->getMock();
$httpDownloader->expects($this->at(0))
+ ->method('enableAsync');
+
+ $httpDownloader->expects($this->at(1))
->method('get')
->with($url = 'http://example.org/packages.json')
->willReturn(new \Composer\Util\Http\Response(array('url' => $url), 200, array(), json_encode(array('search' => '/search.json?q=%query%&type=%type%'))));
- $httpDownloader->expects($this->at(1))
+ $httpDownloader->expects($this->at(2))
->method('get')
->with($url = 'http://example.org/search.json?q=foo&type=composer-plugin')
->willReturn(new \Composer\Util\Http\Response(array('url' => $url), 200, array(), json_encode($result)));
- $httpDownloader->expects($this->at(2))
+ $httpDownloader->expects($this->at(3))
->method('get')
->with($url = 'http://example.org/search.json?q=foo&type=library')
->willReturn(new \Composer\Util\Http\Response(array('url' => $url), 200, array(), json_encode(array())));
@@ -291,6 +294,9 @@ class ComposerRepositoryTest extends TestCase
->getMock();
$httpDownloader->expects($this->at(0))
+ ->method('enableAsync');
+
+ $httpDownloader->expects($this->at(1))
->method('get')
->with($url = 'http://example.org/packages.json')
->willReturn(new \Composer\Util\Http\Response(array('url' => $url), 200, array(), json_encode(array(