diff --git a/phpunit.xml.dist b/phpunit.xml.dist index fe20f2caa..0bf7ddfae 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -36,6 +36,7 @@ ./src/Composer/Autoload/ClassLoader.php + ./src/Composer/PHPStan/ diff --git a/src/Composer/Command/RemoveCommand.php b/src/Composer/Command/RemoveCommand.php index f4717439e..3b8ed88af 100644 --- a/src/Composer/Command/RemoveCommand.php +++ b/src/Composer/Command/RemoveCommand.php @@ -85,7 +85,7 @@ EOT protected function execute(InputInterface $input, OutputInterface $output) { if ($input->getArgument('packages') === [] && !$input->getOption('unused')) { - throw new InvalidArgumentException('Not enough arguments (missing: "packages")'); + throw new InvalidArgumentException('Not enough arguments (missing: "packages").'); } $packages = $input->getArgument('packages'); diff --git a/tests/Composer/Test/Command/AuditCommandTest.php b/tests/Composer/Test/Command/AuditCommandTest.php index 1b820b479..53041f33d 100644 --- a/tests/Composer/Test/Command/AuditCommandTest.php +++ b/tests/Composer/Test/Command/AuditCommandTest.php @@ -13,6 +13,7 @@ namespace Composer\Test\Command; use Composer\Test\TestCase; +use UnexpectedValueException; class AuditCommandTest extends TestCase { @@ -24,5 +25,20 @@ class AuditCommandTest extends TestCase $appTester->run(['command' => 'audit']); $appTester->assertCommandIsSuccessful(); + self::assertEquals('No packages - skipping audit.', trim($appTester->getDisplay(true))); + } + + public function testErrorAuditingLockFileWhenItIsMissing(): void + { + $this->initTempComposer(); + $this->createInstalledJson([self::getPackage()]); + + $this->expectException(UnexpectedValueException::class); + $this->expectExceptionMessage( + "Valid composer.json and composer.lock files are required to run this command with --locked" + ); + + $appTester = $this->getApplicationTester(); + $appTester->run(['command' => 'audit', '--locked' => true]); } } diff --git a/tests/Composer/Test/Command/RemoveCommandTest.php b/tests/Composer/Test/Command/RemoveCommandTest.php new file mode 100644 index 000000000..f34b0c761 --- /dev/null +++ b/tests/Composer/Test/Command/RemoveCommandTest.php @@ -0,0 +1,470 @@ + + * 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\Json\JsonFile; +use Composer\Package\Link; +use Composer\Semver\Constraint\MatchAllConstraint; +use Composer\Test\TestCase; +use InvalidArgumentException; +use Symfony\Component\Console\Command\Command; +use UnexpectedValueException; + +class RemoveCommandTest extends TestCase +{ + public function testExceptionRunningWithNoRemovePackages(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Not enough arguments (missing: "packages").'); + + $appTester = $this->getApplicationTester(); + $this->assertEquals(Command::FAILURE, $appTester->run(['command' => 'remove'])); + } + + public function testExceptionWhenRunningUnusedWithoutLockFile(): void + { + $this->initTempComposer(); + + $this->expectException(UnexpectedValueException::class); + $this->expectExceptionMessage('A valid composer.lock file is required to run this command with --unused'); + + $appTester = $this->getApplicationTester(); + $this->assertEquals(Command::FAILURE, $appTester->run(['command' => 'remove', '--unused' => true])); + } + + public function testWarningWhenRemovingNonExistentPackage(): void + { + $this->initTempComposer(); + $this->createInstalledJson(); + + $appTester = $this->getApplicationTester(); + self::assertEquals(Command::SUCCESS, $appTester->run(['command' => 'remove', 'packages' => ['vendor1/package1']])); + self::assertStringStartsWith('vendor1/package1 is not required in your composer.json and has not been removed', trim($appTester->getDisplay(true))); + } + + public function testWarningWhenRemovingPackageFromWrongType(): void + { + $this->initTempComposer([ + 'require' => [ + 'root/req' => '1.*', + ], + ]); + + $appTester = $this->getApplicationTester(); + self::assertEquals(Command::SUCCESS, $appTester->run(['command' => 'remove', 'packages' => ['root/req'], '--dev' => true, '--no-update' => true, '--no-interaction' => true])); + self::assertSame('root/req could not be found in require-dev but it is present in require +./composer.json has been updated', trim($appTester->getDisplay(true))); + $this->assertEquals(['require' => ['root/req' => '1.*']], (new JsonFile('./composer.json'))->read()); + } + + public function testWarningWhenRemovingPackageWithDeprecatedDependenciesFlag(): void + { + $this->initTempComposer([ + 'require' => [ + 'root/req' => '1.*', + ], + ]); + + $appTester = $this->getApplicationTester(); + self::assertEquals(Command::SUCCESS, $appTester->run(['command' => 'remove', 'packages' => ['root/req'], '--update-with-dependencies' => true, '--no-update' => true, '--no-interaction' => true])); + self::assertSame('You are using the deprecated option "update-with-dependencies". This is now default behaviour. The --no-update-with-dependencies option can be used to remove a package without its dependencies. +./composer.json has been updated', trim($appTester->getDisplay(true))); + $this->assertEmpty((new JsonFile('./composer.json'))->read()); + } + + public function testMessageOutputWhenNoUnusedPackagesToRemove(): void + { + $this->initTempComposer([ + 'repositories' => [ + 'packages' => [ + 'type' => 'package', + 'package' => [ + ['name' => 'root/req', 'version' => '1.0.0', 'require' => ['nested/req' => '^1']], + ['name' => 'nested/req', 'version' => '1.1.0'], + ], + ], + ], + 'require' => [ + 'root/req' => '1.*', + ], + ]); + + $requiredPackage = self::getPackage('root/req'); + $requiredPackage->setRequires([ + 'nested/req' => new Link( + 'root/req', + 'nested/req', + new MatchAllConstraint(), + Link::TYPE_REQUIRE, + '^1' + ) + ]); + $nestedPackage = self::getPackage('nested/req', '1.1.0'); + + $this->createInstalledJson([$requiredPackage, $nestedPackage]); + $this->createComposerLock([$requiredPackage, $nestedPackage]); + + $appTester = $this->getApplicationTester(); + self::assertEquals(Command::SUCCESS, $appTester->run(['command' => 'remove', '--unused' => true, '--no-audit' => true, '--no-interaction' => true])); + self::assertSame('No unused packages to remove', trim($appTester->getDisplay(true))); + } + + public function testRemoveUnusedPackage(): void + { + $this->initTempComposer([ + 'repositories' => [ + 'packages' => [ + 'type' => 'package', + 'package' => [ + ['name' => 'root/req', 'version' => '1.0.0'], + ['name' => 'not/req', 'version' => '1.0.0'], + ], + ] + ], + 'require' => [ + 'root/req' => '1.*', + ], + ]); + + $requiredPackage = self::getPackage('root/req'); + $extraneousPackage = self::getPackage('not/req'); + + $this->createInstalledJson([$requiredPackage]); + $this->createComposerLock([$requiredPackage, $extraneousPackage]); + + $appTester = $this->getApplicationTester(); + self::assertEquals(Command::SUCCESS, $appTester->run(['command' => 'remove', '--unused' => true, '--no-audit' => true, '--no-interaction' => true])); + self::assertStringStartsWith('not/req is not required in your composer.json and has not been removed', $appTester->getDisplay(true)); + self::assertStringContainsString('Running composer update not/req', $appTester->getDisplay(true)); + self::assertStringContainsString('- Removing not/req (1.0.0)', $appTester->getDisplay(true)); + } + + public function testRemovePackageByName(): void + { + $this->initTempComposer([ + 'repositories' => [ + 'packages' => [ + 'type' => 'package', + 'package' => [ + ['name' => 'root/req', 'version' => '1.0.0', 'type' => 'metapackage'], + ['name' => 'root/another', 'version' => '1.0.0', 'type' => 'metapackage'] + ], + ], + ], + 'require' => [ + 'root/req' => '1.*', + 'root/another' => '1.*', + ], + ]); + $rootReqPackage = self::getPackage('root/req'); + $rootAnotherPackage = self::getPackage('root/another'); + // Set as a metapackage so that we can do the whole post-remove update & install process without Composer trying to download them (DownloadManager::getDownloaderForPackage). + $rootReqPackage->setType('metapackage'); + $rootAnotherPackage->setType('metapackage'); + + $this->createInstalledJson([$rootReqPackage, $rootAnotherPackage]); + $this->createComposerLock([$rootReqPackage, $rootAnotherPackage]); + + $appTester = $this->getApplicationTester(); + self::assertEquals(Command::SUCCESS, $appTester->run(['command' => 'remove', 'packages' => ['root/req'], '--no-audit' => true, '--no-interaction' => true])); + self::assertStringStartsWith('./composer.json has been updated', trim($appTester->getDisplay(true))); + self::assertStringContainsString('Running composer update root/req', trim($appTester->getDisplay(true))); + self::assertStringContainsString('Lock file operations: 0 installs, 0 updates, 1 removal', trim($appTester->getDisplay(true))); + self::assertStringContainsString('- Removing root/req (1.0.0)', trim($appTester->getDisplay(true))); + self::assertStringContainsString('Package operations: 0 installs, 0 updates, 1 removal', trim($appTester->getDisplay(true))); + self::assertEquals(['root/another' => '1.*'], (new JsonFile('./composer.json'))->read()['require']); + self::assertEquals([['name' => 'root/another', 'version' => '1.0.0', 'type' => 'metapackage']], (new JsonFile('./composer.lock'))->read()['packages']); + } + + public function testRemovePackageByNameWithDryRun(): void + { + $this->initTempComposer([ + 'repositories' => [ + 'packages' => [ + 'type' => 'package', + 'package' => [ + ['name' => 'root/req', 'version' => '1.0.0', 'type' => 'metapackage'], + ['name' => 'root/another', 'version' => '1.0.0', 'type' => 'metapackage'] + ], + ], + ], + 'require' => [ + 'root/req' => '1.*', + 'root/another' => '1.*', + ], + ]); + $rootReqPackage = self::getPackage('root/req'); + $rootAnotherPackage = self::getPackage('root/another'); + // Set as a metapackage so that we can do the whole post-remove update & install process without Composer trying to download them (DownloadManager::getDownloaderForPackage). + $rootReqPackage->setType('metapackage'); + $rootAnotherPackage->setType('metapackage'); + + $this->createInstalledJson([$rootReqPackage, $rootAnotherPackage]); + $this->createComposerLock([$rootReqPackage, $rootAnotherPackage]); + + $appTester = $this->getApplicationTester(); + self::assertEquals(Command::SUCCESS, $appTester->run(['command' => 'remove', 'packages' => ['root/req'], '--dry-run' => true, '--no-audit' => true, '--no-interaction' => true])); + self::assertStringContainsString('./composer.json has been updated', trim($appTester->getDisplay(true))); + self::assertStringContainsString('Running composer update root/req', trim($appTester->getDisplay(true))); + self::assertStringContainsString('Lock file operations: 0 installs, 0 updates, 1 removal', trim($appTester->getDisplay(true))); + self::assertStringContainsString('- Removing root/req (1.0.0)', trim($appTester->getDisplay(true))); + self::assertStringContainsString('Package operations: 0 installs, 0 updates, 1 removal', trim($appTester->getDisplay(true))); + self::assertEquals(['root/req' => '1.*', 'root/another' => '1.*'], (new JsonFile('./composer.json'))->read()['require']); + self::assertEquals([['name' => 'root/another', 'version' => '1.0.0', 'type' => 'metapackage'], ['name' => 'root/req', 'version' => '1.0.0', 'type' => 'metapackage']], (new JsonFile('./composer.lock'))->read()['packages']); + } + + public function testRemoveAllowedPluginPackageWithNoOtherAllowedPlugins(): void + { + $this->initTempComposer([ + 'repositories' => [ + 'packages' => [ + 'type' => 'package', + 'package' => [ + ['name' => 'root/req', 'version' => '1.0.0', 'type' => 'metapackage'], + ['name' => 'root/another', 'version' => '1.0.0', 'type' => 'metapackage'] + ], + ], + ], + 'require' => [ + 'root/req' => '1.*', + 'root/another' => '1.*', + ], + 'config' => [ + 'allow-plugins' => [ + 'root/req' => true, + ], + ], + ]); + $rootReqPackage = self::getPackage('root/req'); + $rootAnotherPackage = self::getPackage('root/another'); + // Set as a metapackage so that we can do the whole post-remove update & install process without Composer trying to download them (DownloadManager::getDownloaderForPackage). + $rootReqPackage->setType('metapackage'); + $rootAnotherPackage->setType('metapackage'); + + $this->createInstalledJson([$rootReqPackage, $rootAnotherPackage]); + $this->createComposerLock([$rootReqPackage, $rootAnotherPackage]); + + $appTester = $this->getApplicationTester(); + self::assertEquals(Command::SUCCESS, $appTester->run(['command' => 'remove', 'packages' => ['root/req'], '--no-audit' => true, '--no-interaction' => true])); + self::assertEquals(['root/another' => '1.*'], (new JsonFile('./composer.json'))->read()['require']); + self::assertEmpty((new JsonFile('./composer.json'))->read()['config']); + } + + public function testRemoveAllowedPluginPackageWithOtherAllowedPlugins(): void + { + $this->initTempComposer([ + 'repositories' => [ + 'packages' => [ + 'type' => 'package', + 'package' => [ + ['name' => 'root/req', 'version' => '1.0.0', 'type' => 'metapackage'], + ['name' => 'root/another', 'version' => '1.0.0', 'type' => 'metapackage'] + ], + ], + ], + 'require' => [ + 'root/req' => '1.*', + 'root/another' => '1.*', + ], + 'config' => [ + 'allow-plugins' => [ + 'root/another' => true, + 'root/req' => true, + ], + ], + ]); + $rootReqPackage = self::getPackage('root/req'); + $rootAnotherPackage = self::getPackage('root/another'); + // Set as a metapackage so that we can do the whole post-remove update & install process without Composer trying to download them (DownloadManager::getDownloaderForPackage). + $rootReqPackage->setType('metapackage'); + $rootAnotherPackage->setType('metapackage'); + + $appTester = $this->getApplicationTester(); + self::assertEquals(Command::SUCCESS, $appTester->run(['command' => 'remove', 'packages' => ['root/req'], '--no-audit' => true, '--no-interaction' => true])); + self::assertEquals(['root/another' => '1.*'], (new JsonFile('./composer.json'))->read()['require']); + self::assertEquals(['allow-plugins' => ['root/another' => true]], (new JsonFile('./composer.json'))->read()['config']); + } + + public function testRemovePackagesByVendor(): void + { + $this->initTempComposer([ + 'repositories' => [ + 'packages' => [ + 'type' => 'package', + 'package' => [ + ['name' => 'root/req', 'version' => '1.0.0'], + ['name' => 'root/another', 'version' => '1.0.0'], + ['name' => 'another/req', 'version' => '1.0.0'], + ], + ], + ], + 'require' => [ + 'root/req' => '1.*', + 'root/another' => '1.*', + 'another/req' => '1.*', + ], + ]); + $rootReqPackage = self::getPackage('root/req'); + $rootAnotherPackage = self::getPackage('root/another'); + $anotherReqPackage = self::getPackage('another/req'); + + $this->createInstalledJson([$rootReqPackage, $rootAnotherPackage, $anotherReqPackage]); + $this->createComposerLock([$rootReqPackage, $rootAnotherPackage, $anotherReqPackage]); + + $appTester = $this->getApplicationTester(); + self::assertEquals(Command::SUCCESS, $appTester->run(['command' => 'remove', 'packages' => ['root/*'], '--no-install' => true, '--no-audit' => true, '--no-interaction' => true])); + self::assertStringStartsWith('./composer.json has been updated', trim($appTester->getDisplay(true))); + self::assertStringContainsString('Running composer update root/*', $appTester->getDisplay(true)); + self::assertStringContainsString('- Removing root/another (1.0.0)', $appTester->getDisplay(true)); + self::assertStringContainsString('- Removing root/req (1.0.0)', $appTester->getDisplay(true)); + self::assertStringContainsString('Writing lock file', $appTester->getDisplay(true)); + self::assertEquals(['another/req' => '1.*'], (new JsonFile('./composer.json'))->read()['require']); + self::assertEquals([['name' => 'another/req', 'version' => '1.0.0', 'type' => 'library']], (new JsonFile('./composer.lock'))->read()['packages']); + } + + public function testRemovePackagesByVendorWithDryRun(): void + { + $this->initTempComposer([ + 'repositories' => [ + 'packages' => [ + 'type' => 'package', + 'package' => [ + ['name' => 'root/req', 'version' => '1.0.0'], + ['name' => 'root/another', 'version' => '1.0.0'], + ['name' => 'another/req', 'version' => '1.0.0'], + ], + ], + ], + 'require' => [ + 'root/req' => '1.*', + 'root/another' => '1.*', + 'another/req' => '1.*', + ], + ]); + $rootReqPackage = self::getPackage('root/req'); + $rootAnotherPackage = self::getPackage('root/another'); + $anotherReqPackage = self::getPackage('another/req'); + + $this->createInstalledJson([$rootReqPackage, $rootAnotherPackage, $anotherReqPackage]); + $this->createComposerLock([$rootReqPackage, $rootAnotherPackage, $anotherReqPackage]); + + $appTester = $this->getApplicationTester(); + $appTester->run(['command' => 'remove', 'packages' => ['root/*'], '--dry-run' => true, '--no-install' => true, '--no-audit' => true, '--no-interaction' => true]); + self::assertEquals(Command::SUCCESS, $appTester->getStatusCode()); + self::assertSame("./composer.json has been updated +Running composer update root/* +Loading composer repositories with package information +Updating dependencies +Lock file operations: 0 installs, 0 updates, 2 removals + - Removing root/another (1.0.0) + - Removing root/req (1.0.0)", trim($appTester->getDisplay(true))); + self::assertStringNotContainsString('Writing lock file', $appTester->getDisplay(true)); + self::assertEquals(['root/req' => '1.*', 'root/another' => '1.*', 'another/req' => '1.*'], (new JsonFile('./composer.json'))->read()['require']); + self::assertEquals([['name' => 'another/req', 'version' => '1.0.0', 'type' => 'library'], ['name' => 'root/another', 'version' => '1.0.0', 'type' => 'library'], ['name' => 'root/req', 'version' => '1.0.0', 'type' => 'library']], (new JsonFile('./composer.lock'))->read()['packages']); + } + + public function testWarningWhenRemovingPackagesByVendorFromWrongType(): void + { + $this->initTempComposer([ + 'require' => [ + 'root/req' => '1.*', + 'root/another' => '1.*', + 'another/req' => '1.*', + ], + ]); + + $appTester = $this->getApplicationTester(); + self::assertEquals(Command::SUCCESS, $appTester->run(['command' => 'remove', 'packages' => ['root/*'], '--dev' => true, '--no-interaction' => true, '--no-update' => true])); + self::assertSame("root/req could not be found in require-dev but it is present in require +root/another could not be found in require-dev but it is present in require +./composer.json has been updated", trim($appTester->getDisplay(true))); + self::assertEquals(['require' => ['root/req' => '1.*', 'root/another' => '1.*', 'another/req' => '1.*']], (new JsonFile('./composer.json'))->read()); + } + + public function testPackageStillPresentErrorWhenNoInstallFlagUsed(): void + { + $this->initTempComposer([ + 'require' => [ + 'root/req' => '1.*', + ], + ]); + $rootReqPackage = self::getPackage('root/req'); + + $this->createInstalledJson([$rootReqPackage]); + $this->createComposerLock([$rootReqPackage]); + + $appTester = $this->getApplicationTester(); + self::assertEquals(Command::INVALID, $appTester->run(['command' => 'remove', 'packages' => ['root/req'], '--no-install' => true, '--no-audit' => true, '--no-interaction' => true])); + self::assertStringContainsString('./composer.json has been updated', $appTester->getDisplay(true)); + self::assertStringContainsString('Lock file operations: 0 installs, 0 updates, 1 removal', $appTester->getDisplay(true)); + self::assertStringContainsString('- Removing root/req (1.0.0)', $appTester->getDisplay(true)); + self::assertStringContainsString('Writing lock file', $appTester->getDisplay(true)); + self::assertStringContainsString('Removal failed, root/req is still present, it may be required by another package. See `composer why root/req`', $appTester->getDisplay(true)); + self::assertEmpty((new JsonFile('./composer.json'))->read()); + self::assertEmpty((new JsonFile('./composer.lock'))->read()['packages']); + self::assertEquals([['name' => 'root/req', 'version' => '1.0.0', 'version_normalized' => '1.0.0.0', 'type' => 'library', 'install-path' => '../root/req']], (new JsonFile('./vendor/composer/installed.json'))->read()['packages']); + } + + /** + * @dataProvider provideInheritedDependenciesUpdateFlag + */ + public function testUpdateInheritedDependenciesFlagIsPassedToPostRemoveInstaller(string $installFlagName, string $expectedComposerUpdateCommand): void + { + $this->initTempComposer([ + 'repositories' => [ + 'packages' => [ + 'type' => 'package', + 'package' => [ + ['name' => 'root/req', 'version' => '1.0.0', 'type' => 'metapackage'], + ], + ], + ], + 'require' => [ + 'root/req' => '1.*', + ], + ]); + $rootReqPackage = self::getPackage('root/req'); + $rootReqPackage->setType('metapackage'); + + $this->createInstalledJson([$rootReqPackage]); + $this->createComposerLock([$rootReqPackage]); + + $appTester = $this->getApplicationTester(); + self::assertEquals(Command::SUCCESS, $appTester->run(['command' => 'remove', 'packages' => ['root/req'], $installFlagName => true, '--no-audit' => true, '--no-interaction' => true])); + self::assertStringContainsString('./composer.json has been updated', $appTester->getDisplay(true)); + self::assertStringContainsString($expectedComposerUpdateCommand, $appTester->getDisplay(true)); + self::assertStringContainsString('Package operations: 0 installs, 0 updates, 1 removal', $appTester->getDisplay(true)); + self::assertStringContainsString('- Removing root/req (1.0.0)', $appTester->getDisplay(true)); + self::assertStringContainsString('Writing lock file', $appTester->getDisplay(true)); + self::assertStringContainsString('Lock file operations: 0 installs, 0 updates, 1 removal', $appTester->getDisplay(true)); + self::assertEmpty((new JsonFile('./composer.lock'))->read()['packages']); + } + + public static function provideInheritedDependenciesUpdateFlag(): \Generator + { + yield 'update with all dependencies' => [ + '--update-with-all-dependencies', + 'Running composer update root/req --with-all-dependencies', + ]; + + yield 'with all dependencies' => [ + '--with-all-dependencies', + 'Running composer update root/req --with-all-dependencies', + ]; + + yield 'no update with dependencies' => [ + '--no-update-with-dependencies', + 'Running composer update root/req --with-dependencies', + ]; + } +} diff --git a/tests/Composer/Test/Command/SuggestsCommandTest.php b/tests/Composer/Test/Command/SuggestsCommandTest.php new file mode 100644 index 000000000..217f41157 --- /dev/null +++ b/tests/Composer/Test/Command/SuggestsCommandTest.php @@ -0,0 +1,489 @@ + + * 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\CompletePackage; +use Composer\Package\Link; +use Composer\Test\TestCase; +use Symfony\Component\Console\Command\Command; + +class SuggestsCommandTest extends TestCase +{ + public function testInstalledPackagesWithNoSuggestions(): void + { + $this->initTempComposer([ + 'repositories' => [ + 'packages' => [ + 'type' => 'package', + 'package' => [ + ['name' => 'vendor1/package1', 'version' => '1.0.0'], + ['name' => 'vendor2/package2', 'version' => '1.0.0'], + ], + ], + ], + 'require' => [ + 'vendor1/package1' => '1.*', + 'vendor2/package2' => '1.*', + ], + ]); + + $packages = [ + self::getPackage('vendor1/package1'), + self::getPackage('vendor2/package2'), + ]; + + $this->createInstalledJson($packages); + $this->createComposerLock($packages); + + $appTester = $this->getApplicationTester(); + self::assertEquals(Command::SUCCESS, $appTester->run(['command' => 'suggest'])); + self::assertEmpty($appTester->getDisplay(true)); + } + + /** + * @dataProvider provideSuggest + * @param array> $command + */ + public function testSuggest(bool $hasLockFile, array $command, string $expected): void + { + $this->initTempComposer([ + 'repositories' => [ + 'packages' => [ + 'type' => 'package', + 'package' => [ + ['name' => 'vendor1/package1', 'version' => '1.0.0', 'suggests' => ['vendor3/suggested' => 'helpful for vendor1/package1'], 'require' => ['vendor6/package6' => '^1.0'], 'require-dev' => ['vendor3/suggested' => '^1.0', 'vendor4/dev-suggested' => '^1.0']], + ['name' => 'vendor2/package2', 'version' => '1.0.0', 'suggests' => ['vendor4/dev-suggested' => 'helpful for vendor2/package2'], 'require' => ['vendor5/dev-package' => '^1.0']], + ['name' => 'vendor5/dev-package', 'version' => '1.0.0', 'suggests' => ['vendor8/dev-transitive' => 'helpful for vendor5/dev-package'], 'require-dev' => ['vendor8/dev-transitive' => '^1.0']], + ['name' => 'vendor6/package6', 'version' => '1.0.0', 'suggests' => ['vendor7/transitive' => 'helpful for vendor6/package6']], + ], + ], + ], + 'require' => ['vendor1/package1' => '^1'], + 'require-dev' => ['vendor2/package2' => '^1'], + ]); + + $packages = [ + self::getPackageWithSuggestAndRequires( + 'vendor1/package1', + '1.0.0', + [ + 'vendor3/suggested' => 'helpful for vendor1/package1', + ], + [ + 'vendor6/package6' => new Link('vendor1/package1', 'vendor6/package6', self::getVersionConstraint('>=', '1.0'), Link::TYPE_REQUIRE, '^1.0'), + ], + [ + 'vendor4/dev-suggested' => new Link('vendor1/package1', 'vendor4/dev-suggested', self::getVersionConstraint('>=', '1.0'), Link::TYPE_DEV_REQUIRE, '^1.0'), + 'vendor3/suggested' => new Link('vendor1/package1', 'vendor3/suggested', self::getVersionConstraint('>=', '1.0'), Link::TYPE_DEV_REQUIRE, '^1.0'), + ] + ), + self::getPackageWithSuggestAndRequires( + 'vendor6/package6', + '1.0.0', + [ + 'vendor7/transitive' => 'helpful for vendor6/package6', + ] + ), + ]; + $devPackages = [ + self::getPackageWithSuggestAndRequires( + 'vendor2/package2', + '1.0.0', + [ + 'vendor4/dev-suggested' => 'helpful for vendor2/package2', + ], + [ + 'vendor5/dev-package' => new Link('vendor2/package2', 'vendor5/dev-package', self::getVersionConstraint('>=', '1.0'), Link::TYPE_REQUIRE, '^1.0'), + ] + ), + self::getPackageWithSuggestAndRequires( + 'vendor5/dev-package', + '1.0.0', + [ + 'vendor8/dev-transitive' => 'helpful for vendor5/dev-package', + ], + [], + [ + 'vendor8/dev-transitive' => new Link('vendor5/dev-package', 'vendor8/dev-transitive', self::getVersionConstraint('>=', '1.0'), Link::TYPE_DEV_REQUIRE, '^1.0'), + ] + ) + ]; + + $this->createInstalledJson($packages, $devPackages); + if ($hasLockFile) { + $this->createComposerLock($packages, $devPackages); + } + + $appTester = $this->getApplicationTester(); + self::assertEquals(Command::SUCCESS, $appTester->run(array_merge(['command' => 'suggest'], $command))); + self::assertSame(trim($expected), trim($appTester->getDisplay(true))); + } + + public function provideSuggest(): \Generator + { + yield 'with lockfile, show suggested' => [ + true, + [], + 'vendor1/package1 suggests: + - vendor3/suggested: helpful for vendor1/package1 + +vendor2/package2 suggests: + - vendor4/dev-suggested: helpful for vendor2/package2 + +2 additional suggestions by transitive dependencies can be shown with --all' + ]; + + yield 'without lockfile, show suggested' => [ + false, + [], + 'vendor1/package1 suggests: + - vendor3/suggested: helpful for vendor1/package1 + +vendor2/package2 suggests: + - vendor4/dev-suggested: helpful for vendor2/package2 + +2 additional suggestions by transitive dependencies can be shown with --all' + ]; + + yield 'with lockfile, show suggested (excluding dev)' => [ + true, + ['--no-dev' => true], + 'vendor1/package1 suggests: + - vendor3/suggested: helpful for vendor1/package1 + +1 additional suggestions by transitive dependencies can be shown with --all' + ]; + + yield 'without lockfile, show suggested (excluding dev)' => [ + false, + ['--no-dev' => true], + 'vendor1/package1 suggests: + - vendor3/suggested: helpful for vendor1/package1 + +vendor2/package2 suggests: + - vendor4/dev-suggested: helpful for vendor2/package2 + +2 additional suggestions by transitive dependencies can be shown with --all' + ]; + + yield 'with lockfile, show all suggested' => [ + true, + ['--all' => true], + 'vendor1/package1 suggests: + - vendor3/suggested: helpful for vendor1/package1 + +vendor2/package2 suggests: + - vendor4/dev-suggested: helpful for vendor2/package2 + +vendor5/dev-package suggests: + - vendor8/dev-transitive: helpful for vendor5/dev-package + +vendor6/package6 suggests: + - vendor7/transitive: helpful for vendor6/package6' + ]; + + yield 'without lockfile, show all suggested' => [ + false, + ['--all' => true], + 'vendor1/package1 suggests: + - vendor3/suggested: helpful for vendor1/package1 + +vendor2/package2 suggests: + - vendor4/dev-suggested: helpful for vendor2/package2 + +vendor5/dev-package suggests: + - vendor8/dev-transitive: helpful for vendor5/dev-package + +vendor6/package6 suggests: + - vendor7/transitive: helpful for vendor6/package6' + ]; + + yield 'with lockfile, show all suggested (excluding dev)' => [ + true, + ['--all' => true, '--no-dev' => true], + 'vendor1/package1 suggests: + - vendor3/suggested: helpful for vendor1/package1 + +vendor6/package6 suggests: + - vendor7/transitive: helpful for vendor6/package6' + ]; + + yield 'without lockfile, show all suggested (excluding dev)' => [ + false, + ['--all' => true, '--no-dev' => true], + 'vendor1/package1 suggests: + - vendor3/suggested: helpful for vendor1/package1 + +vendor2/package2 suggests: + - vendor4/dev-suggested: helpful for vendor2/package2 + +vendor5/dev-package suggests: + - vendor8/dev-transitive: helpful for vendor5/dev-package + +vendor6/package6 suggests: + - vendor7/transitive: helpful for vendor6/package6' + ]; + + yield 'with lockfile, show suggested grouped by package' => [ + true, + ['--by-package' => true], + 'vendor1/package1 suggests: + - vendor3/suggested: helpful for vendor1/package1 + +vendor2/package2 suggests: + - vendor4/dev-suggested: helpful for vendor2/package2 + +2 additional suggestions by transitive dependencies can be shown with --all' + ]; + + yield 'without lockfile, show suggested grouped by package' => [ + false, + ['--by-package' => true], + 'vendor1/package1 suggests: + - vendor3/suggested: helpful for vendor1/package1 + +vendor2/package2 suggests: + - vendor4/dev-suggested: helpful for vendor2/package2 + +2 additional suggestions by transitive dependencies can be shown with --all' + ]; + + yield 'with lockfile, show suggested grouped by package (excluding dev)' => [ + true, + ['--by-package' => true, '--no-dev' => true], + 'vendor1/package1 suggests: + - vendor3/suggested: helpful for vendor1/package1 + +1 additional suggestions by transitive dependencies can be shown with --all' + ]; + + yield 'without lockfile, show suggested grouped by package (excluding dev)' => [ + false, + ['--by-package' => true, '--no-dev' => true], + 'vendor1/package1 suggests: + - vendor3/suggested: helpful for vendor1/package1 + +vendor2/package2 suggests: + - vendor4/dev-suggested: helpful for vendor2/package2 + +2 additional suggestions by transitive dependencies can be shown with --all' + ]; + + yield 'with lockfile, show suggested grouped by suggestion' => [ + true, + ['--by-suggestion' => true], + 'vendor3/suggested is suggested by: + - vendor1/package1: helpful for vendor1/package1 + +vendor4/dev-suggested is suggested by: + - vendor2/package2: helpful for vendor2/package2 + +2 additional suggestions by transitive dependencies can be shown with --all' + ]; + + yield 'without lockfile, show suggested grouped by suggestion' => [ + false, + ['--by-suggestion' => true], + 'vendor3/suggested is suggested by: + - vendor1/package1: helpful for vendor1/package1 + +vendor4/dev-suggested is suggested by: + - vendor2/package2: helpful for vendor2/package2 + +2 additional suggestions by transitive dependencies can be shown with --all' + ]; + + yield 'with lockfile, show suggested grouped by suggestion (excluding dev)' => [ + true, + ['--by-suggestion' => true, '--no-dev' => true], + 'vendor3/suggested is suggested by: + - vendor1/package1: helpful for vendor1/package1 + +1 additional suggestions by transitive dependencies can be shown with --all' + ]; + + yield 'without lockfile, show suggested grouped by suggestion (excluding dev)' => [ + false, + ['--by-suggestion' => true, '--no-dev' => true], + 'vendor3/suggested is suggested by: + - vendor1/package1: helpful for vendor1/package1 + +vendor4/dev-suggested is suggested by: + - vendor2/package2: helpful for vendor2/package2 + +2 additional suggestions by transitive dependencies can be shown with --all' + ]; + + yield 'with lockfile, show suggested grouped by package and suggestion' => [ + true, + ['--by-package' => true, '--by-suggestion' => true], + 'vendor1/package1 suggests: + - vendor3/suggested: helpful for vendor1/package1 + +vendor2/package2 suggests: + - vendor4/dev-suggested: helpful for vendor2/package2 + +------------------------------------------------------------------------------ +vendor3/suggested is suggested by: + - vendor1/package1: helpful for vendor1/package1 + +vendor4/dev-suggested is suggested by: + - vendor2/package2: helpful for vendor2/package2 + +2 additional suggestions by transitive dependencies can be shown with --all' + ]; + + yield 'without lockfile, show suggested grouped by package and suggestion' => [ + false, + ['--by-package' => true, '--by-suggestion' => true], + 'vendor1/package1 suggests: + - vendor3/suggested: helpful for vendor1/package1 + +vendor2/package2 suggests: + - vendor4/dev-suggested: helpful for vendor2/package2 + +------------------------------------------------------------------------------ +vendor3/suggested is suggested by: + - vendor1/package1: helpful for vendor1/package1 + +vendor4/dev-suggested is suggested by: + - vendor2/package2: helpful for vendor2/package2 + +2 additional suggestions by transitive dependencies can be shown with --all' + ]; + + yield 'with lockfile, show suggested grouped by package and suggestion (excluding dev)' => [ + true, + ['--by-package' => true, '--by-suggestion' => true, '--no-dev' => true], + 'vendor1/package1 suggests: + - vendor3/suggested: helpful for vendor1/package1 + +------------------------------------------------------------------------------ +vendor3/suggested is suggested by: + - vendor1/package1: helpful for vendor1/package1 + +1 additional suggestions by transitive dependencies can be shown with --all' + ]; + + yield 'without lockfile, show suggested grouped by package and suggestion (excluding dev)' => [ + false, + ['--by-package' => true, '--by-suggestion' => true, '--no-dev' => true], + 'vendor1/package1 suggests: + - vendor3/suggested: helpful for vendor1/package1 + +vendor2/package2 suggests: + - vendor4/dev-suggested: helpful for vendor2/package2 + +------------------------------------------------------------------------------ +vendor3/suggested is suggested by: + - vendor1/package1: helpful for vendor1/package1 + +vendor4/dev-suggested is suggested by: + - vendor2/package2: helpful for vendor2/package2 + +2 additional suggestions by transitive dependencies can be shown with --all' + ]; + + yield 'with lockfile, show suggested for package' => [ + true, + ['packages' => ['vendor2/package2']], + 'vendor2/package2 suggests: + - vendor4/dev-suggested: helpful for vendor2/package2' + ]; + + yield 'without lockfile, show suggested for package' => [ + false, + ['packages' => ['vendor2/package2']], + 'vendor2/package2 suggests: + - vendor4/dev-suggested: helpful for vendor2/package2' + ]; + + yield 'with lockfile, list suggested' => [ + true, + ['--list' => true], + 'vendor3/suggested +vendor4/dev-suggested', + ]; + + yield 'without lockfile, list suggested' => [ + false, + ['--list' => true], + 'vendor3/suggested +vendor4/dev-suggested', + ]; + + yield 'with lockfile, list suggested with no transitive or no-dev dependencies' => [ + true, + ['--list' => true, '--no-dev' => true], + 'vendor3/suggested', + ]; + + yield 'without lockfile, list suggested with no transitive or no-dev dependencies' => [ + false, + ['--list' => true, '--no-dev' => true], + 'vendor3/suggested +vendor4/dev-suggested', + ]; + + yield 'with lockfile, list suggested with all dependencies including transitive and dev dependencies' => [ + true, + ['--list' => true, '--all' => true], + 'vendor3/suggested +vendor4/dev-suggested +vendor7/transitive +vendor8/dev-transitive', + ]; + + yield 'without lockfile, list suggested with all dependencies including transitive and dev dependencies' => [ + false, + ['--list' => true, '--all' => true], + 'vendor3/suggested +vendor4/dev-suggested +vendor7/transitive +vendor8/dev-transitive', + ]; + + yield 'with lockfile, list all suggested (excluding dev)' => [ + true, + ['--list' => true, '--all' => true, '--no-dev' => true], + 'vendor3/suggested +vendor7/transitive', + ]; + + yield 'without lockfile, list all suggested (excluding dev)' => [ + false, + ['--list' => true, '--all' => true, '--no-dev' => true], + 'vendor3/suggested +vendor4/dev-suggested +vendor7/transitive +vendor8/dev-transitive', + ]; + } + + /** + * @param array $suggests + * @param array $requires + * @param array $requireDevs + */ + private function getPackageWithSuggestAndRequires(string $name = 'dummy/pkg', string $version = '1.0.0', array $suggests = [], array $requires = [], array $requireDevs = []): CompletePackage + { + $normVersion = self::getVersionParser()->normalize($version); + + $pkg = new CompletePackage($name, $normVersion, $version); + $pkg->setSuggests($suggests); + $pkg->setRequires($requires); + $pkg->setDevRequires($requireDevs); + + return $pkg; + } +} diff --git a/tests/complete.phpunit.xml b/tests/complete.phpunit.xml index b30398e96..e5e4fe0f1 100644 --- a/tests/complete.phpunit.xml +++ b/tests/complete.phpunit.xml @@ -35,6 +35,7 @@ ../src/Composer/Autoload/ClassLoader.php + ../src/Composer/PHPStan/