* Jordi Boggiano * * 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\Package\Link; use Composer\Semver\Constraint\MatchAllConstraint; use Composer\Test\TestCase; use InvalidArgumentException; class UpdateCommandTest extends TestCase { /** * @dataProvider provideUpdates * @param array $composerJson * @param array $command */ public function testUpdate(array $composerJson, array $command, string $expected, bool $createLock = false): void { $this->initTempComposer($composerJson); if ($createLock) { $this->createComposerLock(); } $appTester = $this->getApplicationTester(); $appTester->run(array_merge(['command' => 'update', '--dry-run' => true, '--no-audit' => true], $command)); self::assertStringMatchesFormat(trim($expected), trim($appTester->getDisplay(true))); } public static function provideUpdates(): \Generator { $rootDepAndTransitiveDep = [ 'repositories' => [ 'packages' => [ 'type' => 'package', 'package' => [ ['name' => 'root/req', 'version' => '1.0.0', 'require' => ['dep/pkg' => '^1']], ['name' => 'dep/pkg', 'version' => '1.0.0'], ['name' => 'dep/pkg', 'version' => '1.0.1'], ['name' => 'dep/pkg', 'version' => '1.0.2'], ], ], ], 'require' => [ 'root/req' => '1.*', ], ]; yield 'simple update' => [ $rootDepAndTransitiveDep, [], << [ $rootDepAndTransitiveDep, ['-vv' => true], << [ $rootDepAndTransitiveDep, ['--with' => ['dep/pkg:1.0.0'], '--no-install' => true], << [ $rootDepAndTransitiveDep, ['--with' => ['dep/pkg:^2']], << satisfiable by root/req[1.0.0]. - root/req 1.0.0 requires dep/pkg ^1 -> found dep/pkg[1.0.0, 1.0.1, 1.0.2] but it conflicts with your temporary update constraint (dep/pkg:^2). OUTPUT ]; yield 'update with temporary constraint failing resolution on root package' => [ $rootDepAndTransitiveDep, ['--with' => ['root/req:^2']], << [ $rootDepAndTransitiveDep, ['--bump-after-update' => true], <<Warning: Bumping dependency constraints is not recommended for libraries as it will narrow down your dependencies and may cause problems for your users. If your package is not a library, you can explicitly specify the "type" by using "composer config type project". Alternatively you can use --dev-only to only bump dependencies within "require-dev". No requirements to update in ./composer.json. OUTPUT , true ]; yield 'update & bump dev only' => [ $rootDepAndTransitiveDep, ['--bump-after-update' => 'dev'], << [ $rootDepAndTransitiveDep, ['--with' => ['dep/pkg:^2'], '--bump-after-update' => true], << satisfiable by root/req[1.0.0]. - root/req 1.0.0 requires dep/pkg ^1 -> found dep/pkg[1.0.0, 1.0.1, 1.0.2] but it conflicts with your temporary update constraint (dep/pkg:^2). OUTPUT ]; } public function testUpdateWithPatchOnly(): void { $this->initTempComposer([ 'repositories' => [ 'packages' => [ 'type' => 'package', 'package' => [ ['name' => 'root/req', 'version' => '1.0.0'], ['name' => 'root/req', 'version' => '1.0.1'], ['name' => 'root/req', 'version' => '1.1.0'], ['name' => 'root/req2', 'version' => '1.0.0'], ['name' => 'root/req2', 'version' => '1.0.1'], ['name' => 'root/req2', 'version' => '1.1.0'], ['name' => 'root/req3', 'version' => '1.0.0'], ['name' => 'root/req3', 'version' => '1.0.1'], ['name' => 'root/req3', 'version' => '1.1.0'], ], ], ], 'require' => [ 'root/req' => '1.*', 'root/req2' => '1.*', 'root/req3' => '1.*', ], ]); $package = self::getPackage('root/req', '1.0.0'); $package2 = self::getPackage('root/req2', '1.0.0'); $package3 = self::getPackage('root/req3', '1.0.0'); $this->createComposerLock([$package, $package2, $package3]); $appTester = $this->getApplicationTester(); // root/req fails because of incompatible --with requirement $appTester->run(array_merge(['command' => 'update', '--dry-run' => true, '--no-audit' => true, '--no-install' => true, '--patch-only' => true, '--with' => ['root/req:^1.1']])); $expected = <<= 1.1.0.0-dev < 2.0.0.0-dev] [>= 1.0.0.0-dev < 1.1.0.0-dev]]). OUTPUT; self::assertStringMatchesFormat(trim($expected), trim($appTester->getDisplay(true))); $appTester = $this->getApplicationTester(); // root/req upgrades to 1.0.1 as that is compatible with the --with requirement now // root/req2 upgrades to 1.0.1 only due to --patch-only // root/req3 does not update as it is not in the allowlist $appTester->run(array_merge(['command' => 'update', '--dry-run' => true, '--no-audit' => true, '--no-install' => true, '--patch-only' => true, '--with' => ['root/req:^1.0.1'], 'packages' => ['root/req', 'root/req2']])); $expected = << 1.0.1) - Upgrading root/req2 (1.0.0 => 1.0.1) OUTPUT; self::assertStringMatchesFormat(trim($expected), trim($appTester->getDisplay(true))); } public function testInteractiveModeThrowsIfNoPackageToUpdate(): void { $this->initTempComposer([ 'repositories' => [ 'packages' => [ 'type' => 'package', 'package' => [ ['name' => 'root/req', 'version' => '1.0.0'], ], ], ], 'require' => [ 'root/req' => '1.*', ], ]); $this->createComposerLock([self::getPackage('root/req', '1.0.0')]); self::expectExceptionMessage('Could not find any package with new versions available'); $appTester = $this->getApplicationTester(); $appTester->setInputs(['']); $appTester->run(['command' => 'update', '--interactive' => true]); } public function testInteractiveModeThrowsIfNoPackageEntered(): void { $this->initTempComposer([ 'repositories' => [ 'packages' => [ 'type' => 'package', 'package' => [ ['name' => 'root/req', 'version' => '1.0.0'], ['name' => 'root/req', 'version' => '1.0.1'], ], ], ], 'require' => [ 'root/req' => '1.*', ], ]); $this->createComposerLock([self::getPackage('root/req', '1.0.0')]); self::expectExceptionMessage('No package named "" is installed.'); $appTester = $this->getApplicationTester(); $appTester->setInputs(['']); $appTester->run(['command' => 'update', '--interactive' => true]); } /** * @dataProvider provideInteractiveUpdates * @param array $packageNames */ public function testInteractiveTmp(array $packageNames, string $expected): void { $this->initTempComposer([ 'repositories' => [ 'packages' => [ 'type' => 'package', 'package' => [ ['name' => 'root/req', 'version' => '1.0.0', 'require' => ['dep/pkg' => '^1']], ['name' => 'dep/pkg', 'version' => '1.0.0'], ['name' => 'dep/pkg', 'version' => '1.0.1'], ['name' => 'dep/pkg', 'version' => '1.0.2'], ['name' => 'another-dep/pkg', 'version' => '1.0.2'], ], ], ], 'require' => [ 'root/req' => '1.*', ], ]); $rootPackage = self::getPackage('root/req'); $packages = [$rootPackage]; foreach ($packageNames as $pkg => $ver) { $currentPkg = self::getPackage($pkg, $ver); array_push($packages, $currentPkg); } $rootPackage->setRequires([ 'dep/pkg' => new Link( 'root/req', 'dep/pkg', new MatchAllConstraint(), Link::TYPE_REQUIRE, '^1' ), 'another-dep/pkg' => new Link( 'root/req', 'another-dep/pkg', new MatchAllConstraint(), Link::TYPE_REQUIRE, '^1' ), ]); $this->createComposerLock($packages); $this->createInstalledJson($packages); $appTester = $this->getApplicationTester(); $appTester->setInputs(array_merge(array_keys($packageNames), ['', 'yes'])); $appTester->run([ 'command' => 'update', '--interactive' => true, '--no-audit' => true, '--dry-run' => true, ]); self::assertStringEndsWith( trim($expected), trim($appTester->getDisplay(true)) ); } public function provideInteractiveUpdates(): \Generator { yield [ ['dep/pkg' => '1.0.1'], << 1.0.2) Installing dependencies from lock file (including require-dev) Package operations: 1 install, 1 update, 0 removals - Upgrading dep/pkg (1.0.1 => 1.0.2) - Installing another-dep/pkg (1.0.2) OUTPUT ]; yield [ ['dep/pkg' => '1.0.1', 'another-dep/pkg' => '1.0.2'], << 1.0.2) Installing dependencies from lock file (including require-dev) Package operations: 0 installs, 1 update, 0 removals - Upgrading dep/pkg (1.0.1 => 1.0.2) OUTPUT ]; } }