1
0
Fork 0

Add bump command to bump requirements to the currently installed version, fixes #7273 (#10829)

pull/10885/head
Jordi Boggiano 2022-06-09 11:43:59 +02:00 committed by GitHub
parent 73fd0f22e8
commit 70f2dd6edd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 634 additions and 13 deletions

View File

@ -322,6 +322,24 @@ uninstalled.
* **--apcu-autoloader-prefix:** Use a custom prefix for the APCu autoloader cache. * **--apcu-autoloader-prefix:** Use a custom prefix for the APCu autoloader cache.
Implicitly enables `--apcu-autoloader`. Implicitly enables `--apcu-autoloader`.
## bump
The `bump` command increases the lower limit of your composer.json requirements
to the currently installed versions. This helps to ensure your dependencies do not
accidentally get downgraded due to some other conflict, and can slightly improve
dependency resolution performance as it limits the amount of package versions
Composer has to look at.
Running this blindly on libraries is **NOT** recommended as it will narrow down
your allowed dependencies, which may cause dependency hell for your users.
Running it with `--dev-only` on libraries may be fine however as dev requirements
are local to the library and do not affect consumers of the package.
### Options
* **--dev-only:** Only bump requirements in "require-dev".
* **--no-dev-only:** Only bump requirements in "require".
## reinstall ## reinstall
The `reinstall` command looks up installed packages by name, The `reinstall` command looks up installed packages by name,

View File

@ -0,0 +1,227 @@
<?php declare(strict_types=1);
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Composer\Command;
use Composer\DependencyResolver\Request;
use Composer\Package\AliasPackage;
use Composer\Package\Locker;
use Composer\Package\Version\VersionBumper;
use Composer\Package\Version\VersionSelector;
use Composer\Util\Filesystem;
use Symfony\Component\Console\Input\InputInterface;
use Composer\Console\Input\InputArgument;
use Composer\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Composer\Factory;
use Composer\Installer;
use Composer\Installer\InstallerEvents;
use Composer\Json\JsonFile;
use Composer\Json\JsonManipulator;
use Composer\Package\Version\VersionParser;
use Composer\Package\Loader\ArrayLoader;
use Composer\Package\BasePackage;
use Composer\Plugin\CommandEvent;
use Composer\Plugin\PluginEvents;
use Composer\Repository\CompositeRepository;
use Composer\Repository\PlatformRepository;
use Composer\IO\IOInterface;
use Composer\Util\Silencer;
/**
* @author Jordi Boggiano <j.boggiano@seld.be>
*/
final class BumpCommand extends BaseCommand
{
private const ERROR_GENERIC = 1;
private const ERROR_LOCK_OUTDATED = 2;
use CompletionTrait;
protected function configure(): void
{
$this
->setName('bump')
->setDescription('Increases the lower limit of your composer.json requirements to the currently installed versions.')
->setDefinition(array(
new InputArgument('packages', InputArgument::IS_ARRAY | InputArgument::OPTIONAL, 'Optional package name(s) to restrict which packages are bumped.', null, $this->suggestRootRequirement()),
new InputOption('dev-only', 'D', InputOption::VALUE_NONE, 'Only bump requirements in "require-dev".'),
new InputOption('no-dev-only', 'R', InputOption::VALUE_NONE, 'Only bump requirements in "require".'),
))
->setHelp(
<<<EOT
The <info>bump</info> command increases the lower limit of your composer.json requirements
to the currently installed versions. This helps to ensure your dependencies do not
accidentally get downgraded due to some other conflict, and can slightly improve
dependency resolution performance as it limits the amount of package versions
Composer has to look at.
Running this blindly on libraries is **NOT** recommended as it will narrow down
your allowed dependencies, which may cause dependency hell for your users.
Running it with <info>--dev-only</info> on libraries may be fine however as dev requirements
are local to the library and do not affect consumers of the package.
EOT
)
;
}
/**
* @throws \Seld\JsonLint\ParsingException
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
/** @readonly */
$composerJsonPath = Factory::getComposerFile();
$io = $this->getIO();
if (!Filesystem::isReadable($composerJsonPath)) {
$io->writeError('<error>'.$composerJsonPath.' is not readable.</error>');
return self::ERROR_GENERIC;
}
$composerJson = new JsonFile($composerJsonPath);
$contents = file_get_contents($composerJson->getPath());
if (false === $contents) {
$io->writeError('<error>'.$composerJsonPath.' is not readable.</error>');
return self::ERROR_GENERIC;
}
// check for writability by writing to the file as is_writable can not be trusted on network-mounts
// see https://github.com/composer/composer/issues/8231 and https://bugs.php.net/bug.php?id=68926
if (!is_writable($composerJsonPath) && false === Silencer::call('file_put_contents', $composerJsonPath, $contents)) {
$io->writeError('<error>'.$composerJsonPath.' is not writable.</error>');
return self::ERROR_GENERIC;
}
unset($contents);
$composer = $this->requireComposer();
if ($composer->getLocker()->isLocked()) {
if (!$composer->getLocker()->isFresh()) {
$io->writeError('<error>The lock file is not up to date with the latest changes in composer.json. Run the appropriate `update` to fix that before you use the `bump` command.</error>');
return self::ERROR_LOCK_OUTDATED;
}
$repo = $composer->getLocker()->getLockedRepository(true);
} else {
$repo = $composer->getRepositoryManager()->getLocalRepository();
}
if ($composer->getPackage()->getType() !== 'project' && !$input->getOption('dev-only')) {
$io->writeError('<warning>Warning: Bumping dependency constraints is not recommended for libraries as it will narrow down your dependencies and may cause problems for your users.</warning>');
$contents = $composerJson->read();
if (!isset($contents['type'])) {
$io->writeError('<warning>If your package is not a library, you can explicitly specify the "type" by using "composer config type project".</warning>');
$io->writeError('<warning>Alternatively you can use --dev to only bump dependencies within "require-dev".</warning>');
}
unset($contents);
}
$bumper = new VersionBumper();
$tasks = [];
if (!$input->getOption('no-dev-only')) {
$tasks['require-dev'] = $composer->getPackage()->getDevRequires();
};
if (!$input->getOption('dev-only')) {
$tasks['require'] = $composer->getPackage()->getRequires();
}
$updates = [];
foreach ($tasks as $key => $reqs) {
foreach ($reqs as $pkgName => $link) {
if (PlatformRepository::isPlatformPackage($pkgName)) {
continue;
}
$currentConstraint = $link->getPrettyConstraint();
$package = $repo->findPackage($pkgName, '*');
// name must be provided or replaced
if (null === $package) {
continue;
}
while ($package instanceof AliasPackage) {
$package = $package->getAliasOf();
}
$bumped = $bumper->bumpRequirement($link->getConstraint(), $package);
if ($bumped === $currentConstraint) {
continue;
}
$updates[$key][$pkgName] = $bumped;
}
}
if (!$this->updateFileCleanly($composerJson, $updates)) {
$composerDefinition = $composerJson->read();
foreach ($updates as $key => $packages) {
foreach ($packages as $package => $version) {
$composerDefinition[$key][$package] = $version;
}
}
$composerJson->write($composerDefinition);
}
$changeCount = array_sum(array_map('count', $updates));
if ($changeCount > 0) {
$io->write('<info>'.$composerJsonPath.' has been updated ('.$changeCount.' changes).</info>');
} else {
$io->write('<info>No requirements to update in '.$composerJsonPath.'.</info>');
}
if ($composer->getLocker()->isLocked() && $changeCount > 0) {
$contents = file_get_contents($composerJson->getPath());
if (false === $contents) {
throw new \RuntimeException('Unable to read '.$composerJson->getPath().' contents to update the lock file hash.');
}
$lock = new JsonFile(Factory::getLockFile($composerJsonPath));
$lockData = $lock->read();
$lockData['content-hash'] = Locker::getContentHash($contents);
$lock->write($lockData);
}
return 0;
}
/**
* @param array<'require'|'require-dev', array<string, string>> $updates
*/
private function updateFileCleanly(JsonFile $json, array $updates): bool
{
$contents = file_get_contents($json->getPath());
if (false === $contents) {
throw new \RuntimeException('Unable to read '.$json->getPath().' contents.');
}
$manipulator = new JsonManipulator($contents);
foreach ($updates as $key => $packages) {
foreach ($packages as $package => $version) {
if (!$manipulator->addLink($key, $package, $version)) {
return false;
}
}
}
if (false === file_put_contents($json->getPath(), $manipulator->getContents())) {
throw new \RuntimeException('Unable to write new '.$json->getPath().' contents.');
}
return true;
}
}

View File

@ -45,6 +45,18 @@ trait CompletionTrait
return ['dist', 'source', 'auto']; return ['dist', 'source', 'auto'];
} }
/**
* Suggest package names from root requirements.
*/
private function suggestRootRequirement(): \Closure
{
return function (CompletionInput $input): array {
$composer = $this->requireComposer();
return array_merge(array_keys($composer->getPackage()->getRequires()), array_keys($composer->getPackage()->getDevRequires()));
};
}
/** /**
* Suggest package names from installed. * Suggest package names from installed.
*/ */

View File

@ -116,13 +116,6 @@ EOT
*/ */
protected function execute(InputInterface $input, OutputInterface $output) protected function execute(InputInterface $input, OutputInterface $output)
{ {
if (function_exists('pcntl_async_signals') && function_exists('pcntl_signal')) {
pcntl_async_signals(true);
pcntl_signal(SIGINT, function () { $this->revertComposerFile(); });
pcntl_signal(SIGTERM, function () { $this->revertComposerFile(); });
pcntl_signal(SIGHUP, function () { $this->revertComposerFile(); });
}
$this->file = Factory::getComposerFile(); $this->file = Factory::getComposerFile();
$io = $this->getIO(); $io = $this->getIO();
@ -151,9 +144,16 @@ EOT
$this->composerBackup = file_get_contents($this->json->getPath()); $this->composerBackup = file_get_contents($this->json->getPath());
$this->lockBackup = file_exists($this->lock) ? file_get_contents($this->lock) : null; $this->lockBackup = file_exists($this->lock) ? file_get_contents($this->lock) : null;
if (function_exists('pcntl_async_signals') && function_exists('pcntl_signal')) {
pcntl_async_signals(true);
pcntl_signal(SIGINT, function () { $this->revertComposerFile(); });
pcntl_signal(SIGTERM, function () { $this->revertComposerFile(); });
pcntl_signal(SIGHUP, function () { $this->revertComposerFile(); });
}
// check for writability by writing to the file as is_writable can not be trusted on network-mounts // check for writability by writing to the file as is_writable can not be trusted on network-mounts
// see https://github.com/composer/composer/issues/8231 and https://bugs.php.net/bug.php?id=68926 // see https://github.com/composer/composer/issues/8231 and https://bugs.php.net/bug.php?id=68926
if (!is_writable($this->file) && !Silencer::call('file_put_contents', $this->file, $this->composerBackup)) { if (!is_writable($this->file) && false === Silencer::call('file_put_contents', $this->file, $this->composerBackup)) {
$io->writeError('<error>'.$this->file.' is not writable.</error>'); $io->writeError('<error>'.$this->file.' is not writable.</error>');
return 1; return 1;
@ -168,10 +168,10 @@ EOT
* @see https://github.com/composer/composer/pull/8313#issuecomment-532637955 * @see https://github.com/composer/composer/pull/8313#issuecomment-532637955
*/ */
if ($packageType !== 'project') { if ($packageType !== 'project') {
$io->writeError('<error>"--fixed" option is allowed for "project" package types only to prevent possible misuses.</error>'); $io->writeError('<error>The "--fixed" option is only allowed for packages with a "project" type to prevent possible misuses.</error>');
if (empty($config['type'])) { if (!isset($config['type'])) {
$io->writeError('<error>If your package is not library, you should explicitly specify "type" parameter in composer.json.</error>'); $io->writeError('<error>If your package is not a library, you can explicitly specify the "type" by using "composer config type project".</error>');
} }
return 1; return 1;

View File

@ -547,6 +547,7 @@ class Application extends BaseApplication
new Command\CheckPlatformReqsCommand(), new Command\CheckPlatformReqsCommand(),
new Command\FundCommand(), new Command\FundCommand(),
new Command\ReinstallCommand(), new Command\ReinstallCommand(),
new Command\BumpCommand(),
)); ));
if (strpos(__FILE__, 'phar:') === 0 || '1' === Platform::getEnv('COMPOSER_TESTS_ARE_RUNNING')) { if (strpos(__FILE__, 'phar:') === 0 || '1' === Platform::getEnv('COMPOSER_TESTS_ARE_RUNNING')) {

View File

@ -0,0 +1,114 @@
<?php declare(strict_types=1);
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Composer\Package\Version;
use Composer\Filter\PlatformRequirementFilter\IgnoreAllPlatformRequirementFilter;
use Composer\Filter\PlatformRequirementFilter\PlatformRequirementFilterFactory;
use Composer\Filter\PlatformRequirementFilter\PlatformRequirementFilterInterface;
use Composer\Package\BasePackage;
use Composer\Package\AliasPackage;
use Composer\Package\PackageInterface;
use Composer\Composer;
use Composer\Package\Loader\ArrayLoader;
use Composer\Package\Dumper\ArrayDumper;
use Composer\Pcre\Preg;
use Composer\Repository\RepositorySet;
use Composer\Repository\PlatformRepository;
use Composer\Semver\Constraint\Constraint;
use Composer\Semver\Constraint\ConstraintInterface;
use Composer\Semver\Intervals;
use Composer\Util\Platform;
/**
* @author Jordi Boggiano <j.boggiano@seld.be>
* @internal
*/
class VersionBumper
{
/**
* Given a constraint, this returns a new constraint with
* the lower bound bumped to match the given package's version.
*
* For example:
* * ^1.0 + 1.2.1 -> ^1.2.1
* * ^1.2 + 1.2.0 -> ^1.2
* * ^1.2 || ^2.3 + 1.3.0 -> ^1.3 || ^2.3
* * ^1.2 || ^2.3 + 2.4.0 -> ^1.2 || ^2.4
* * ^3@dev + 3.2.99999-dev -> ^3.2@dev
* * ~2 + 2.0-beta.1 -> ~2
* * dev-master + dev-master -> dev-master
*/
public function bumpRequirement(ConstraintInterface $constraint, PackageInterface $package): string
{
$parser = new VersionParser();
$prettyConstraint = $constraint->getPrettyString();
if (str_starts_with($constraint->getPrettyString(), 'dev-')) {
return $prettyConstraint;
}
$version = $package->getVersion();
if (str_starts_with($package->getVersion(), 'dev-')) {
$loader = new ArrayLoader($parser);
$dumper = new ArrayDumper();
$extra = $loader->getBranchAlias($dumper->dump($package));
// dev packages without branch alias cannot be processed
if (null === $extra || $extra === VersionParser::DEFAULT_BRANCH_ALIAS) {
return $prettyConstraint;
}
$version = $extra;
}
$intervals = Intervals::get($constraint);
// complex constraints with branch names are not bumped
if (\count($intervals['branches']['names']) > 0) {
return $prettyConstraint;
}
$major = Preg::replace('{^(\d+).*}', '$1', $version);
$newPrettyConstraint = '^'.Preg::replace('{(?:\.(?:0|9999999))+(-dev)?$}', '', $version);
// not a simple stable version, abort
if (!Preg::isMatch('{^\^\d+(\.\d+)*$}', $newPrettyConstraint)) {
return $prettyConstraint;
}
$pattern = '{
(?<=,|\ |\||^) # leading separator
(?P<constraint>
\^'.$major.'(?:\.\d+)* # e.g. ^2.anything
| ~'.$major.'(?:\.\d+)? # e.g. ~2 or ~2.2 but no more
| '.$major.'(?:\.[*x])+ # e.g. 2.* or 2.*.* or 2.x.x.x etc
)
(?=,|$|\ |\||@) # trailing separator
}x';
if (Preg::isMatchAllWithOffsets($pattern, $prettyConstraint, $matches)) {
$modified = $prettyConstraint;
foreach (array_reverse($matches['constraint']) as $match) {
$modified = substr_replace($modified, $newPrettyConstraint, $match[1], Platform::strlen($match[0]));
}
// if it is strictly equal to the previous one then no need to change anything
$newConstraint = $parser->parseConstraints($modified);
if (Intervals::isSubsetOf($newConstraint, $constraint) && Intervals::isSubsetOf($constraint, $newConstraint)) {
return $prettyConstraint;
}
return $modified;
}
return $prettyConstraint;
}
}

View File

@ -0,0 +1,136 @@
<?php declare(strict_types=1);
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Composer\Test\Command;
use Composer\Json\JsonFile;
use Composer\Test\TestCase;
class BumpCommandTest extends TestCase
{
/**
* @dataProvider provideTests
* @param array<mixed> $composerJson
* @param array<mixed> $command
* @param array<mixed> $expected
*/
public function testBump(array $composerJson, array $command, array $expected, bool $lock = true): void
{
$this->initTempComposer($composerJson);
$packages = [
$this->getPackage('first/pkg', '2.3.4'),
$this->getPackage('second/pkg', '3.4.0'),
];
$devPackages = [
$this->getPackage('dev/pkg', '2.3.4.5'),
];
$this->createInstalledJson($packages, $devPackages);
if ($lock) {
$this->createComposerLock($packages, $devPackages);
}
$appTester = $this->getApplicationTester();
$appTester->run(array_merge(['command' => 'bump'], $command));
$json = new JsonFile('./composer.json');
$this->assertSame($expected, $json->read());
}
public function provideTests(): \Generator
{
yield 'bump all by default' => [
[
'require' => [
'first/pkg' => '^2.0',
'second/pkg' => '3.*',
],
'require-dev' => [
'dev/pkg' => '~2.0',
],
],
[],
[
'require' => [
'first/pkg' => '^2.3.4',
'second/pkg' => '^3.4',
],
'require-dev' => [
'dev/pkg' => '^2.3.4.5',
],
]
];
yield 'bump only dev with --dev-only' => [
[
'require' => [
'first/pkg' => '^2.0',
'second/pkg' => '3.*',
],
'require-dev' => [
'dev/pkg' => '~2.0',
],
],
['--dev-only' => true],
[
'require' => [
'first/pkg' => '^2.0',
'second/pkg' => '3.*',
],
'require-dev' => [
'dev/pkg' => '^2.3.4.5',
],
]
];
yield 'bump only non-dev with --no-dev-only' => [
[
'require' => [
'first/pkg' => '^2.0',
'second/pkg' => '3.*',
],
'require-dev' => [
'dev/pkg' => '~2.0',
],
],
['--no-dev-only' => true],
[
'require' => [
'first/pkg' => '^2.3.4',
'second/pkg' => '^3.4',
],
'require-dev' => [
'dev/pkg' => '~2.0',
],
]
];
yield 'bump works from installed repo without lock file' => [
[
'require' => [
'first/pkg' => '^2.0',
'second/pkg' => '3.*',
],
],
[],
[
'require' => [
'first/pkg' => '^2.3.4',
'second/pkg' => '^3.4',
],
],
false
];
}
}

View File

@ -53,7 +53,7 @@ class FactoryMock extends Factory
$rm->setLocalRepository(new InstalledArrayRepository); $rm->setLocalRepository(new InstalledArrayRepository);
} }
public function createInstallationManager(Loop $loop, IOInterface $io, EventDispatcher $dispatcher = null): InstallationManager public function createInstallationManager(Loop $loop = null, IOInterface $io = null, EventDispatcher $dispatcher = null): InstallationManager
{ {
return new InstallationManagerMock(); return new InstallationManagerMock();
} }

View File

@ -57,7 +57,7 @@ class InstallationManagerMock extends InstallationManager
public function getInstallPath(PackageInterface $package): string public function getInstallPath(PackageInterface $package): string
{ {
return ''; return 'vendor/'.$package->getName();
} }
public function isPackageInstalled(InstalledRepositoryInterface $repo, PackageInterface $package): bool public function isPackageInstalled(InstalledRepositoryInterface $repo, PackageInterface $package): bool

View File

@ -0,0 +1,69 @@
<?php declare(strict_types=1);
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Composer\Test\Package\Version;
use Composer\Filter\PlatformRequirementFilter\PlatformRequirementFilterFactory;
use Composer\Package\Version\VersionBumper;
use Composer\Package\Version\VersionSelector;
use Composer\Package\Package;
use Composer\Package\Link;
use Composer\Package\AliasPackage;
use Composer\Repository\PlatformRepository;
use Composer\Package\Version\VersionParser;
use Composer\Test\TestCase;
use Generator;
class VersionBumperTest extends TestCase
{
/**
* @dataProvider provideBumpRequirementTests
*/
public function testBumpRequirement(string $requirement, string $prettyVersion, string $expectedRequirement, ?string $branchAlias = null): void
{
$versionBumper = new VersionBumper();
$versionParser = new VersionParser();
$package = new Package('foo/bar', $versionParser->normalize($prettyVersion), $prettyVersion);
if ($branchAlias !== null) {
$package->setExtra(array('branch-alias' => array($prettyVersion => $branchAlias)));
}
$newConstraint = $versionBumper->bumpRequirement($versionParser->parseConstraints($requirement), $package);
// assert that the recommended version is what we expect
$this->assertSame($expectedRequirement, $newConstraint);
}
public function provideBumpRequirementTests(): Generator
{
// constraint, version, expected recommendation, [branch-alias]
yield 'upgrade caret' => ['^1.0', '1.2.1', '^1.2.1'];
yield 'skip trailing .0s' => ['^1.0', '1.0.0', '^1.0'];
yield 'skip trailing .0s/2' => ['^1.2', '1.2.0', '^1.2'];
yield 'preserve multi constraints' => ['^1.2 || ^2.3', '1.3.2', '^1.3.2 || ^2.3'];
yield 'preserve multi constraints/2' => ['^1.2 || ^2.3', '2.4.0', '^1.2 || ^2.4'];
yield 'preserve multi constraints/3' => ['^1.2 || ^2.3 || ^2', '2.4.0', '^1.2 || ^2.4 || ^2.4'];
yield '@dev is preserved' => ['^3@dev', '3.2.x-dev', '^3.2@dev'];
yield 'non-stable versions abort upgrades' => ['~2', '2.1-beta.1', '~2'];
yield 'dev reqs are skipped' => ['dev-main', 'dev-foo', 'dev-main'];
yield 'dev version does not upgrade' => ['^3.2', 'dev-main', '^3.2'];
yield 'upgrade dev version if aliased' => ['^3.2', 'dev-main', '^3.3', '3.3.x-dev'];
yield 'upgrade major wildcard to caret' => ['2.*', '2.4.0', '^2.4'];
yield 'upgrade major wildcard as x to caret' => ['2.x.x', '2.4.0', '^2.4'];
yield 'leave minor wildcard alone' => ['2.4.*', '2.4.3', '2.4.*'];
yield 'leave patch wildcard alone' => ['2.4.3.*', '2.4.3.2', '2.4.3.*'];
yield 'upgrade tilde to caret when compatible' => ['~2.2', '2.4.3', '^2.4.3'];
yield 'leave patch-only-tilde alone' => ['~2.2.3', '2.2.6', '~2.2.3'];
}
}

View File

@ -16,10 +16,13 @@ use Composer\Config;
use Composer\Console\Application; use Composer\Console\Application;
use Composer\IO\IOInterface; use Composer\IO\IOInterface;
use Composer\Json\JsonFile; use Composer\Json\JsonFile;
use Composer\Package\Locker;
use Composer\Pcre\Preg; use Composer\Pcre\Preg;
use Composer\Repository\InstalledFilesystemRepository;
use Composer\Semver\VersionParser; use Composer\Semver\VersionParser;
use Composer\Package\PackageInterface; use Composer\Package\PackageInterface;
use Composer\Semver\Constraint\Constraint; use Composer\Semver\Constraint\Constraint;
use Composer\Test\Mock\FactoryMock;
use Composer\Test\Mock\HttpDownloaderMock; use Composer\Test\Mock\HttpDownloaderMock;
use Composer\Test\Mock\ProcessExecutorMock; use Composer\Test\Mock\ProcessExecutorMock;
use Composer\Util\Filesystem; use Composer\Util\Filesystem;
@ -109,6 +112,9 @@ abstract class TestCase extends \PHPUnit\Framework\TestCase
* *
* The directory will be cleaned up on tearDown automatically. * The directory will be cleaned up on tearDown automatically.
* *
* @see createInstalledJson
* @see createComposerLock
* @see getApplicationTester
* @param mixed[] $composerJson * @param mixed[] $composerJson
* @param mixed[] $authJson * @param mixed[] $authJson
* @return string the newly created temp dir * @return string the newly created temp dir
@ -138,6 +144,44 @@ abstract class TestCase extends \PHPUnit\Framework\TestCase
return $dir; return $dir;
} }
/**
* Creates a vendor/composer/installed.json in CWD with the given packages
*
* @param PackageInterface[] $packages
* @param PackageInterface[] $devPackages
*/
protected function createInstalledJson(array $packages = [], array $devPackages = [], bool $devMode = true): void
{
mkdir('vendor/composer', 0777, true);
$repo = new InstalledFilesystemRepository(new JsonFile('vendor/composer/installed.json'));
$repo->setDevPackageNames(array_map(function (PackageInterface $pkg) { return $pkg->getPrettyName(); }, $devPackages));
foreach ($packages as $pkg) {
$repo->addPackage($pkg);
mkdir('vendor/'.$pkg->getName(), 0777, true);
}
foreach ($devPackages as $pkg) {
$repo->addPackage($pkg);
mkdir('vendor/'.$pkg->getName(), 0777, true);
}
$factory = new FactoryMock();
$repo->write($devMode, $factory->createInstallationManager());
}
/**
* Creates a composer.lock in CWD with the given packages
*
* @param PackageInterface[] $packages
* @param PackageInterface[] $devPackages
*/
protected function createComposerLock(array $packages = [], array $devPackages = []): void
{
$factory = new FactoryMock();
$locker = new Locker($this->getMockBuilder(IOInterface::class)->getMock(), new JsonFile('./composer.lock'), $factory->createInstallationManager(), (string) file_get_contents('./composer.json'));
$locker->setLockData($packages, $devPackages, [], [], [], 'dev', [], false, false, []);
}
public function getApplicationTester(): ApplicationTester public function getApplicationTester(): ApplicationTester
{ {
$application = new Application(); $application = new Application();