Parallellize the branch comparisons to speed up bootstrapping/version guessing when on a feature branch (#10632)
* Parallellize the branch comparisons to speed up bootstrapping/version guessing when on a feature branch, fixes #10568 * Allow ProcessExecutorMock to function with async callspull/10637/head
parent
ca3b874414
commit
b0665981c2
|
@ -21,6 +21,8 @@ use Composer\Util\Git as GitUtil;
|
||||||
use Composer\Util\HttpDownloader;
|
use Composer\Util\HttpDownloader;
|
||||||
use Composer\Util\ProcessExecutor;
|
use Composer\Util\ProcessExecutor;
|
||||||
use Composer\Util\Svn as SvnUtil;
|
use Composer\Util\Svn as SvnUtil;
|
||||||
|
use React\Promise\CancellablePromiseInterface;
|
||||||
|
use Symfony\Component\Process\Process;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Try to guess the current version number based on different VCS configuration.
|
* Try to guess the current version number based on different VCS configuration.
|
||||||
|
@ -307,6 +309,8 @@ class VersionGuesser
|
||||||
return strnatcasecmp($b, $a);
|
return strnatcasecmp($b, $a);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$promises = [];
|
||||||
|
$this->process->setMaxJobs(30);
|
||||||
foreach ($branches as $candidate) {
|
foreach ($branches as $candidate) {
|
||||||
$candidateVersion = Preg::replace('{^remotes/\S+/}', '', $candidate);
|
$candidateVersion = Preg::replace('{^remotes/\S+/}', '', $candidate);
|
||||||
|
|
||||||
|
@ -316,19 +320,29 @@ class VersionGuesser
|
||||||
}
|
}
|
||||||
|
|
||||||
$cmdLine = str_replace(array('%candidate%', '%branch%'), array($candidate, $branch), $scmCmdline);
|
$cmdLine = str_replace(array('%candidate%', '%branch%'), array($candidate, $branch), $scmCmdline);
|
||||||
if (0 !== $this->process->execute($cmdLine, $output, $path)) {
|
$promises[] = $this->process->executeAsync($cmdLine, $path)->then(function (Process $process) use (&$length, &$version, &$prettyVersion, $candidateVersion, &$promises): void {
|
||||||
continue;
|
if (!$process->isSuccessful()) {
|
||||||
}
|
return;
|
||||||
|
|
||||||
if (strlen($output) < $length) {
|
|
||||||
$length = strlen($output);
|
|
||||||
$version = $this->versionParser->normalizeBranch($candidateVersion);
|
|
||||||
$prettyVersion = 'dev-' . $candidateVersion;
|
|
||||||
if ($length === 0) {
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
$output = $process->getOutput();
|
||||||
|
if (strlen($output) < $length) {
|
||||||
|
$length = strlen($output);
|
||||||
|
$version = $this->versionParser->normalizeBranch($candidateVersion);
|
||||||
|
$prettyVersion = 'dev-' . $candidateVersion;
|
||||||
|
if ($length === 0) {
|
||||||
|
foreach ($promises as $promise) {
|
||||||
|
if ($promise instanceof CancellablePromiseInterface) {
|
||||||
|
$promise->cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$this->process->wait();
|
||||||
|
$this->process->resetMaxJobs();
|
||||||
}
|
}
|
||||||
|
|
||||||
return array('version' => $version, 'pretty_version' => $prettyVersion);
|
return array('version' => $version, 'pretty_version' => $prettyVersion);
|
||||||
|
|
|
@ -271,6 +271,16 @@ class ProcessExecutor
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function setMaxJobs(int $maxJobs): void
|
||||||
|
{
|
||||||
|
$this->maxJobs = $maxJobs;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function resetMaxJobs(): void
|
||||||
|
{
|
||||||
|
$this->maxJobs = 10;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param ?int $index job id
|
* @param ?int $index job id
|
||||||
* @return void
|
* @return void
|
||||||
|
@ -278,7 +288,7 @@ class ProcessExecutor
|
||||||
public function wait($index = null): void
|
public function wait($index = null): void
|
||||||
{
|
{
|
||||||
while (true) {
|
while (true) {
|
||||||
if (!$this->countActiveJobs($index)) {
|
if (0 === $this->countActiveJobs($index)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -12,6 +12,8 @@
|
||||||
|
|
||||||
namespace Composer\Test\Mock;
|
namespace Composer\Test\Mock;
|
||||||
|
|
||||||
|
use Composer\Test\TestCase;
|
||||||
|
use PHPUnit\Framework\MockObject\MockBuilder;
|
||||||
use React\Promise\PromiseInterface;
|
use React\Promise\PromiseInterface;
|
||||||
use Composer\Util\ProcessExecutor;
|
use Composer\Util\ProcessExecutor;
|
||||||
use Composer\Util\Platform;
|
use Composer\Util\Platform;
|
||||||
|
@ -41,6 +43,19 @@ class ProcessExecutorMock extends ProcessExecutor
|
||||||
* @var string[]
|
* @var string[]
|
||||||
*/
|
*/
|
||||||
private $log = array();
|
private $log = array();
|
||||||
|
/**
|
||||||
|
* @var MockBuilder<Process>
|
||||||
|
*/
|
||||||
|
private $processMockBuilder;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param MockBuilder<Process> $processMockBuilder
|
||||||
|
*/
|
||||||
|
public function __construct(MockBuilder $processMockBuilder)
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
$this->processMockBuilder = $processMockBuilder->disableOriginalConstructor();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array<string|array{cmd: string|list<string>, return?: int, stdout?: string, stderr?: string, callback?: callable}> $expectations
|
* @param array<string|array{cmd: string|list<string>, return?: int, stdout?: string, stderr?: string, callback?: callable}> $expectations
|
||||||
|
@ -180,9 +195,16 @@ class ProcessExecutorMock extends ProcessExecutor
|
||||||
|
|
||||||
public function executeAsync($command, ?string $cwd = null): PromiseInterface
|
public function executeAsync($command, ?string $cwd = null): PromiseInterface
|
||||||
{
|
{
|
||||||
$resolver = function ($resolve, $reject): void {
|
$cwd = $cwd ?? Platform::getCwd();
|
||||||
// TODO strictly speaking this should resolve with a mock Process instance here
|
|
||||||
$resolve();
|
$resolver = function ($resolve, $reject) use ($command, $cwd): void {
|
||||||
|
$result = $this->doExecute($command, $cwd, false, $output);
|
||||||
|
$procMock = $this->processMockBuilder->getMock();
|
||||||
|
$procMock->method('getOutput')->willReturn($output);
|
||||||
|
$procMock->method('isSuccessful')->willReturn($result === 0);
|
||||||
|
$procMock->method('getExitCode')->willReturn($result);
|
||||||
|
|
||||||
|
$resolve($procMock);
|
||||||
};
|
};
|
||||||
|
|
||||||
$canceler = function (): void {
|
$canceler = function (): void {
|
||||||
|
|
|
@ -20,6 +20,7 @@ use Composer\Package\RootPackage;
|
||||||
use Composer\Package\Version\VersionGuesser;
|
use Composer\Package\Version\VersionGuesser;
|
||||||
use Composer\Semver\VersionParser;
|
use Composer\Semver\VersionParser;
|
||||||
use Composer\Test\TestCase;
|
use Composer\Test\TestCase;
|
||||||
|
use Composer\Util\ProcessExecutor;
|
||||||
|
|
||||||
class RootPackageLoaderTest extends TestCase
|
class RootPackageLoaderTest extends TestCase
|
||||||
{
|
{
|
||||||
|
@ -36,8 +37,11 @@ class RootPackageLoaderTest extends TestCase
|
||||||
|
|
||||||
$config = new Config;
|
$config = new Config;
|
||||||
$config->merge(array('repositories' => array('packagist' => false)));
|
$config->merge(array('repositories' => array('packagist' => false)));
|
||||||
|
$processExecutor = new ProcessExecutor();
|
||||||
|
$processExecutor->enableAsync();
|
||||||
|
$guesser = new VersionGuesser($config, $processExecutor, new VersionParser());
|
||||||
|
|
||||||
$loader = new RootPackageLoader($manager, $config);
|
$loader = new RootPackageLoader($manager, $config, null, $guesser);
|
||||||
|
|
||||||
return $loader->load($data);
|
return $loader->load($data);
|
||||||
}
|
}
|
||||||
|
|
|
@ -320,7 +320,7 @@ class GitHubDriverTest extends TestCase
|
||||||
->setConstructorArgs(array($io, $this->config))
|
->setConstructorArgs(array($io, $this->config))
|
||||||
->getMock();
|
->getMock();
|
||||||
|
|
||||||
$gitHubDriver = new GitHubDriver($repoConfig, $io, $this->config, $httpDownloader, new ProcessExecutorMock);
|
$gitHubDriver = new GitHubDriver($repoConfig, $io, $this->config, $httpDownloader, $this->getProcessExecutorMock());
|
||||||
$gitHubDriver->initialize();
|
$gitHubDriver->initialize();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -32,6 +32,7 @@ use Composer\Package\RootAliasPackage;
|
||||||
use Composer\Package\CompletePackage;
|
use Composer\Package\CompletePackage;
|
||||||
use Composer\Package\CompleteAliasPackage;
|
use Composer\Package\CompleteAliasPackage;
|
||||||
use Composer\Package\Package;
|
use Composer\Package\Package;
|
||||||
|
use Symfony\Component\Process\Process;
|
||||||
|
|
||||||
abstract class TestCase extends \PHPUnit\Framework\TestCase
|
abstract class TestCase extends \PHPUnit\Framework\TestCase
|
||||||
{
|
{
|
||||||
|
@ -256,7 +257,7 @@ abstract class TestCase extends \PHPUnit\Framework\TestCase
|
||||||
|
|
||||||
protected function getProcessExecutorMock(): ProcessExecutorMock
|
protected function getProcessExecutorMock(): ProcessExecutorMock
|
||||||
{
|
{
|
||||||
$this->processExecutorMocks[] = $mock = new ProcessExecutorMock();
|
$this->processExecutorMocks[] = $mock = new ProcessExecutorMock($this->getMockBuilder(Process::class));
|
||||||
|
|
||||||
return $mock;
|
return $mock;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue