diff --git a/.github/pie-behaviour-tests/Dockerfile b/.github/pie-behaviour-tests/Dockerfile index 274aaffd..17127e7a 100644 --- a/.github/pie-behaviour-tests/Dockerfile +++ b/.github/pie-behaviour-tests/Dockerfile @@ -20,7 +20,7 @@ RUN export DEBIAN_FRONTEND="noninteractive"; \ set -eux; \ apt-get update; \ apt-get install -y --no-install-recommends \ - unzip curl jq wget g++ gcc make autoconf libtool bison re2c pkg-config \ + unzip git curl jq wget g++ gcc make autoconf libtool bison re2c pkg-config \ ca-certificates libxml2-dev libssl-dev; \ update-ca-certificates ; \ rm -rf /var/lib/apt/lists/* @@ -63,5 +63,6 @@ COPY --from=build_pie_phar /app/pie.phar /usr/local/bin/pie COPY --from=build_pie_phar /app/pie.phar /usr/local/bin/pie.original ENV USING_PIE_BEHAT_DOCKERFILE=1 +ENV BEHAT_ALLOW_XDEBUG=1 ENTRYPOINT ["php", "vendor/bin/behat"] CMD ["--no-snippets"] diff --git a/features/uninstall-extensions.feature b/features/uninstall-extensions.feature index 11c13cad..377b7a1b 100644 --- a/features/uninstall-extensions.feature +++ b/features/uninstall-extensions.feature @@ -11,3 +11,9 @@ Feature: Extensions can be uninstalled with PIE Given multiple extensions were previously installed and enabled When I run a command to uninstall multiple extensions Then the extensions should not be installed anymore + + # pie uninstall + Example: A PIE extension installed from a development branch can be uninstalled + Given an extension from a development branch was previously installed and enabled + When I run a command to uninstall an extension + Then the extension should not be installed anymore diff --git a/features/upgrade-extensions.feature b/features/upgrade-extensions.feature index db301584..559b361b 100644 --- a/features/upgrade-extensions.feature +++ b/features/upgrade-extensions.feature @@ -11,3 +11,21 @@ Feature: PIE extensions can be upgraded with PIE Given I have installed PIE extensions with configure options that have upgrades available When I run a command to upgrade my extensions Then the extension has been upgraded with the previous configure options + + # pie upgrade + Example: I can upgrade a PIE extension installed from a development branch that has new commits + Given I have installed a PIE extension from a development branch with configure options that has new commits + When I run a command to upgrade my extensions + Then the extension has been upgraded to the latest commit with the previous configure options + + # pie upgrade + Example: Upgrading a PIE extension installed from a development branch with no new commits does nothing + Given I have installed a PIE extension from a development branch that has no new commits + When I run a command to upgrade my extensions + Then the extension should not have been re-installed + + # pie upgrade + Example: Upgrading when no PIE extensions are installed tells me there is nothing to upgrade + Given I have no PIE extensions installed + When I run a command to upgrade my extensions + Then I should see there is nothing to upgrade diff --git a/src/Command/UpgradeCommand.php b/src/Command/UpgradeCommand.php index f69cf87e..4a63f691 100644 --- a/src/Command/UpgradeCommand.php +++ b/src/Command/UpgradeCommand.php @@ -31,6 +31,8 @@ )] final class UpgradeCommand extends Command { + private const NOTHING_TO_UPGRADE = 'No PIE extensions are currently installed, so there is nothing to upgrade.'; + public function __construct( private readonly ContainerInterface $container, private readonly ComposerIntegrationHandler $composerIntegrationHandler, @@ -70,7 +72,7 @@ public function execute(InputInterface $input, OutputInterface $output): int } if (! file_exists(PieComposerFactory::getLockFile(Platform::getPieJsonFilename($targetPlatform)))) { - $this->io->writeError('No PIE extensions are currently installed, so there is nothing to upgrade.'); + $this->io->writeError(self::NOTHING_TO_UPGRADE); return Command::INVALID; } @@ -80,6 +82,12 @@ public function execute(InputInterface $input, OutputInterface $output): int PieComposerRequest::noOperation($this->io, $targetPlatform), ); + if ($existingComposer->getLocker()->getLockedRepository()->getPackages() === []) { + $this->io->writeError(self::NOTHING_TO_UPGRADE); + + return Command::INVALID; + } + $configureOptions = []; foreach ($this->installedPiePackages->allPiePackages($existingComposer)->packages() as $installedPackage) { $existingConfigureOptions = $installedPackage->installedJsonMetadata()->configureOptions(); diff --git a/src/ComposerIntegration/AddInstalledJsonMetadata.php b/src/ComposerIntegration/AddInstalledJsonMetadata.php index d7907753..8e3ca407 100644 --- a/src/ComposerIntegration/AddInstalledJsonMetadata.php +++ b/src/ComposerIntegration/AddInstalledJsonMetadata.php @@ -12,6 +12,7 @@ use function array_merge; use function implode; +use function in_array; /** @internal This is not public API for PIE, so should not be depended upon unless you accept the risk of BC breaks */ class AddInstalledJsonMetadata @@ -114,15 +115,24 @@ private function addPieMetadata( string $key, string|null $value, ): void { - $localRepositoryPackage = $composer - ->getRepositoryManager() - ->getLocalRepository() - ->findPackages($composerPackage->getName())[0]; + $localRepository = $composer->getRepositoryManager()->getLocalRepository(); + $localRepositoryPackages = $localRepository->getPackages(); + $localRepositoryPackage = null; - if ($localRepositoryPackage instanceof CompleteAliasPackage) { - $localRepositoryPackage = $localRepositoryPackage->getAliasOf(); + foreach ($localRepository->findPackages($composerPackage->getName()) as $candidatePackage) { + if ($candidatePackage instanceof CompleteAliasPackage) { + $candidatePackage = $candidatePackage->getAliasOf(); + } + + if (! in_array($candidatePackage, $localRepositoryPackages, true)) { + continue; + } + + $localRepositoryPackage = $candidatePackage; + break; } + Assert::notNull($localRepositoryPackage); Assert::methodExists($localRepositoryPackage, 'setExtra'); $localRepositoryPackage->setExtra(array_merge($localRepositoryPackage->getExtra(), [$key => $value])); diff --git a/src/ComposerIntegration/Listeners/RemoveUnrelatedInstallOperations.php b/src/ComposerIntegration/Listeners/RemoveUnrelatedInstallOperations.php index 2b840237..eb825af5 100644 --- a/src/ComposerIntegration/Listeners/RemoveUnrelatedInstallOperations.php +++ b/src/ComposerIntegration/Listeners/RemoveUnrelatedInstallOperations.php @@ -7,6 +7,7 @@ use Closure; use Composer\Composer; use Composer\DependencyResolver\Operation\InstallOperation; +use Composer\DependencyResolver\Operation\MarkAliasUninstalledOperation; use Composer\DependencyResolver\Operation\OperationInterface; use Composer\DependencyResolver\Operation\UninstallOperation; use Composer\DependencyResolver\Operation\UpdateOperation; @@ -47,7 +48,12 @@ public function __invoke(InstallerEvent $installerEvent): void $newOperations = array_filter( $installerEvent->getTransaction()?->getOperations() ?? [], function (OperationInterface $operation) use ($pieOutput): bool { - if (! $operation instanceof InstallOperation && ! $operation instanceof UninstallOperation && ! $operation instanceof UpdateOperation) { + if ( + ! $operation instanceof InstallOperation + && ! $operation instanceof UninstallOperation + && ! $operation instanceof UpdateOperation + && ! $operation instanceof MarkAliasUninstalledOperation + ) { $pieOutput->writeError( sprintf( 'Unexpected operation during installer: %s', diff --git a/src/ComposerIntegration/PiePackageInstaller.php b/src/ComposerIntegration/PiePackageInstaller.php index b0e76dee..1fca9010 100644 --- a/src/ComposerIntegration/PiePackageInstaller.php +++ b/src/ComposerIntegration/PiePackageInstaller.php @@ -16,6 +16,7 @@ use function array_map; use function implode; +use function React\Promise\resolve; use function sprintf; /** @internal This is not public API for PIE, so should not be depended upon unless you accept the risk of BC breaks */ @@ -73,6 +74,13 @@ function (CompletePackageInterface $composerPackage): void { )); } + /** @inheritDoc */ + protected function updateCode(PackageInterface $initial, PackageInterface $target) + { + return ($this->removeCode($initial) ?? resolve(null)) + ->then(fn () => $this->installCode($target)); + } + /** @inheritDoc */ public function uninstall(InstalledRepositoryInterface $repo, PackageInterface $package) { diff --git a/test/assets/pie-upgrade-no-extensions/pie.json b/test/assets/pie-upgrade-no-extensions/pie.json new file mode 100644 index 00000000..2c63c085 --- /dev/null +++ b/test/assets/pie-upgrade-no-extensions/pie.json @@ -0,0 +1,2 @@ +{ +} diff --git a/test/assets/pie-upgrade-no-extensions/pie.lock b/test/assets/pie-upgrade-no-extensions/pie.lock new file mode 100644 index 00000000..ba8d4176 --- /dev/null +++ b/test/assets/pie-upgrade-no-extensions/pie.lock @@ -0,0 +1,18 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "d751713988987e9331980363e24189ce", + "packages": [], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": {}, + "prefer-stable": false, + "prefer-lowest": false, + "platform": {}, + "platform-dev": {}, + "plugin-api-version": "2.9.0" +} diff --git a/test/behaviour/CliContext.php b/test/behaviour/CliContext.php index 204172e6..1b7b1880 100644 --- a/test/behaviour/CliContext.php +++ b/test/behaviour/CliContext.php @@ -17,9 +17,9 @@ use Symfony\Component\Process\Process; use Webmozart\Assert\Assert; -use function array_combine; use function array_map; use function array_merge; +use function implode; use function preg_quote; use function Safe\copy; use function Safe\file_get_contents; @@ -30,7 +30,12 @@ use function sprintf; use function str_contains; use function str_replace; +use function substr; +use function sys_get_temp_dir; use function trim; +use function uniqid; + +use const PREG_SET_ORDER; class CliContext implements Context { @@ -49,6 +54,8 @@ class CliContext implements Context private string $pieLockFilename; private string $pieJsonContentBackup; private string $pieLockContentBackup; + /** @var non-empty-string|null */ + private string|null $developmentBranchRepositoryPath = null; /** @throws PcreException */ #[AfterScenario] @@ -68,6 +75,18 @@ public function removeInstalledExtensions(): void } } + #[AfterScenario] + public function removeDevelopmentBranchRepository(): void + { + if ($this->developmentBranchRepositoryPath === null) { + return; + } + + $this->runPieCommand(['repository:remove', $this->developmentBranchRepositoryPath]); + (new Process(['rm', '-rf', $this->developmentBranchRepositoryPath]))->mustRun(); + $this->developmentBranchRepositoryPath = null; + } + #[When('I run a command to download the latest version of an extension')] #[Given('an extension was previously downloaded but not built')] public function iRunACommandToDownloadTheLatestVersionOfAnExtension(): void @@ -290,6 +309,14 @@ public function theExtensionShouldNotBeInstalled(): void sprintf("Failed to remove extension.\n\nOutput:\n%s\n\nError output:\n%s\n", $this->output, $this->errorOutput), ); } + + $this->runPieCommand(['show']); + $this->assertCommandSuccessful(); + $pieShowOutput = $this->output; + + foreach ($this->interactions as $uninstall) { + self::assertPackageNotInstalledInPieShowOutput($pieShowOutput, $uninstall['package']); + } } #[Then('the extension should have been installed')] @@ -597,6 +624,67 @@ public function iHaveInstalledPieExtensionsThatHaveConfigureOptions(): void $this->copyPieJsonAndLock('pie-upgrade-lock'); } + #[Given('I have installed a PIE extension from a development branch with configure options that has new commits')] + public function iHaveInstalledAPieExtensionFromADevelopmentBranchThatHasNewCommits(): void + { + $this->installExampleExtensionFromMainBranchAt( + ExamplePieExtensionFixture::MAIN_BRANCH_PREVIOUS_REFERENCE, + ['--with-hello-name=UpgradeDevBranchTest'], + ); + $this->moveMainBranchTo(ExamplePieExtensionFixture::MAIN_BRANCH_LATEST_REFERENCE); + } + + #[Given('I have installed a PIE extension from a development branch that has no new commits')] + #[Given('an extension from a development branch was previously installed and enabled')] + public function iHaveInstalledAPieExtensionFromADevelopmentBranchThatHasNoNewCommits(): void + { + $this->installExampleExtensionFromMainBranchAt(ExamplePieExtensionFixture::MAIN_BRANCH_LATEST_REFERENCE, []); + } + + /** @param list $installOptions */ + private function installExampleExtensionFromMainBranchAt(string $reference, array $installOptions): void + { + $repositoryPath = sys_get_temp_dir() . '/pie-git-checkout-' . uniqid($reference, true); + $this->developmentBranchRepositoryPath = $repositoryPath; + (new Process(['git', 'clone', '--quiet', '/example-pie-extension', $repositoryPath]))->mustRun(); + $this->moveMainBranchTo($reference); + + $this->runPieCommand(['repository:add', 'vcs', $repositoryPath]); + $this->assertCommandSuccessful(); + + $this->interactions[] = ['extension' => 'example_pie_extension', 'package' => 'asgrim/example-pie-extension']; + $this->runPieCommand(['install', 'asgrim/example-pie-extension:dev-main', ...$installOptions]); + $this->assertCommandSuccessful(); + } + + private function moveMainBranchTo(string $reference): void + { + Assert::notNull($this->developmentBranchRepositoryPath); + + (new Process(['git', 'checkout', '--quiet', '-B', 'main', $reference], $this->developmentBranchRepositoryPath))->mustRun(); + } + + #[Given('I have no PIE extensions installed')] + public function iHaveNoPieExtensionsInstalled(): void + { + $this->copyPieJsonAndLock('pie-upgrade-no-extensions'); + } + + #[Then('I should see there is nothing to upgrade')] + public function iShouldSeeThereIsNothingToUpgrade(): void + { + $upgradeExitCode = $this->exitCode; + $upgradeErrorOutput = (string) $this->errorOutput; + + $this->restorePieJsonAndLock(); + + Assert::contains( + $upgradeErrorOutput, + 'No PIE extensions are currently installed, so there is nothing to upgrade.', + sprintf("Upgrade exited with code %d. Error output:\n%%s", (int) $upgradeExitCode), + ); + } + #[Given('I have a lock file')] public function iHaveALockfile(): void { @@ -616,43 +704,54 @@ public function iRunACommandToUpgradeMyExtensions(): void $this->runPieCommand(['upgrade', '-v']); } - /** @return array */ - private static function installedExtensionPackagesAndVersions(string $pieShowOutput): array + /** @return array> */ + private static function verifiedPiePackageVersions(string $pieShowOutput): array { - if (! preg_match_all('#([a-zA-Z0-9-_]+/[a-zA-Z0-9-_]+):([^ ]+)#', $pieShowOutput, $matches)) { - throw new RuntimeException('no packages found in pie show'); + preg_match_all('#\(from 🥧 ([a-zA-Z0-9-_]+/[a-zA-Z0-9-_]+):(\S+) ✅\)#u', $pieShowOutput, $matches, PREG_SET_ORDER); + + $verifiedPiePackageVersions = []; + foreach ($matches as $match) { + $verifiedPiePackageVersions[$match[1]][] = $match[2]; } - return array_combine($matches[1], $matches[2]); + return $verifiedPiePackageVersions; } private static function assertPackageVersionInstalledInPieShowOutput(string $pieShowOutput, string $expectedPackage, string|null $expectedVersion = null): void { - $installedExtensionPackagesAndVersions = self::installedExtensionPackagesAndVersions($pieShowOutput); - Assert::keyExists($installedExtensionPackagesAndVersions, $expectedPackage); + $verifiedPiePackageVersions = self::verifiedPiePackageVersions($pieShowOutput); + Assert::keyExists( + $verifiedPiePackageVersions, + $expectedPackage, + sprintf("%s is not a loaded and verified PIE extension in pie show output:\n%s", $expectedPackage, $pieShowOutput), + ); if ($expectedVersion === null) { return; } - $versionParser = new VersionParser(); - $installedConstraint = $versionParser->parseConstraints($installedExtensionPackagesAndVersions[$expectedPackage]); - $expectedConstraint = $versionParser->parseConstraints($expectedVersion); - Assert::true( - $expectedConstraint->matches($installedConstraint), - sprintf( - 'Installed version %s does not match expected constraint %s', - $installedConstraint->getPrettyString(), - $expectedConstraint->getPrettyString(), - ), - ); + $versionParser = new VersionParser(); + $expectedConstraint = $versionParser->parseConstraints($expectedVersion); + foreach ($verifiedPiePackageVersions[$expectedPackage] as $installedVersion) { + if ($expectedConstraint->matches($versionParser->parseConstraints($installedVersion))) { + return; + } + } + + throw new RuntimeException(sprintf( + 'Installed version(s) %s of %s do not match expected constraint %s', + implode(', ', $verifiedPiePackageVersions[$expectedPackage]), + $expectedPackage, + $expectedConstraint->getPrettyString(), + )); } private static function assertPackageNotInstalledInPieShowOutput(string $pieShowOutput, string $notExpectedPackage): void { - Assert::keyNotExists( - self::installedExtensionPackagesAndVersions($pieShowOutput), - $notExpectedPackage, + Assert::notContains( + $pieShowOutput, + $notExpectedPackage . ':', + sprintf("%s should not be listed in pie show output:\n%s", $notExpectedPackage, $pieShowOutput), ); } @@ -721,4 +820,27 @@ public function theExtensionsShouldHaveBeenUpgradedWithThePreviousConfigureOptio $this->restorePieJsonAndLock(); } + + #[Then('the extension has been upgraded to the latest commit with the previous configure options')] + public function theExtensionHasBeenUpgradedToTheLatestCommitWithThePreviousConfigureOptions(): void + { + $this->assertCommandSuccessful(); + + Assert::notNull($this->errorOutput); + Assert::contains( + $this->errorOutput, + 'Installing asgrim/example-pie-extension (dev-main ' . substr(ExamplePieExtensionFixture::MAIN_BRANCH_LATEST_REFERENCE, 0, 7) . ')', + ); + Assert::contains($this->output, 'Extension asgrim/example-pie-extension:dev-main is enabled and loaded'); + + $exampleTest = (new Process([self::PHP_BINARY, '-r', 'example_pie_extension_test();'])) + ->mustRun() + ->getOutput(); + + Assert::same(trim($exampleTest), 'Hello, UpgradeDevBranchTest!'); + + $this->runPieCommand(['show']); + $this->assertCommandSuccessful(); + self::assertPackageVersionInstalledInPieShowOutput($this->output, 'asgrim/example-pie-extension', 'dev-main'); + } } diff --git a/test/integration/ComposerIntegration/PiePackageInstallerTest.php b/test/integration/ComposerIntegration/PiePackageInstallerTest.php new file mode 100644 index 00000000..6f2478a0 --- /dev/null +++ b/test/integration/ComposerIntegration/PiePackageInstallerTest.php @@ -0,0 +1,103 @@ +gitRepositoryPath = sys_get_temp_dir() . '/pie-test-git-repository-' . uniqid(); + mkdir($this->gitRepositoryPath, 0755, true); + Process::run(['git', 'init', '--quiet', '--initial-branch=main'], $this->gitRepositoryPath); + } + + protected function tearDown(): void + { + if (file_exists($this->gitRepositoryPath)) { + (new SymfonyProcess(['rm', '-rf', $this->gitRepositoryPath]))->run(); + } + + parent::tearDown(); + } + + public function testUpdateOfSourceInstalledPackageWhenSourceWasRemovedAfterInstall(): void + { + $initialReference = $this->commitConfigM4('initial'); + $targetReference = $this->commitConfigM4('target'); + + $composer = PieComposerFactory::createPieComposer( + Container::testFactory(), + PieComposerRequest::noOperation( + new NullIO(), + TargetPlatform::fromPhpBinaryPath(PhpBinaryPath::fromCurrentProcess(), null, null), + ), + ); + + $installer = $composer->getInstallationManager()->getInstaller('php-ext'); + self::assertInstanceOf(PiePackageInstaller::class, $installer); + + $initialPackage = $this->makeDevMainPackage($initialReference); + $targetPackage = $this->makeDevMainPackage($targetReference); + $localRepository = $composer->getRepositoryManager()->getLocalRepository(); + $localRepository->addPackage($initialPackage); + + $promise = $installer->update($localRepository, $initialPackage, $targetPackage); + self::assertNotNull($promise); + $composer->getLoop()->wait([$promise]); + + self::assertSame('target', file_get_contents($installer->getInstallPath($targetPackage) . '/config.m4')); + } + + private function commitConfigM4(string $content): string + { + file_put_contents($this->gitRepositoryPath . '/config.m4', $content); + Process::run(['git', 'add', 'config.m4'], $this->gitRepositoryPath); + Process::run( + ['git', '-c', 'user.name=PIE Test', '-c', 'user.email=pie-test@example.com', '-c', 'commit.gpgsign=false', 'commit', '--quiet', '-m', $content], + $this->gitRepositoryPath, + ); + + return Process::run(['git', 'rev-parse', 'HEAD'], $this->gitRepositoryPath); + } + + private function makeDevMainPackage(string $reference): CompletePackage + { + $package = new CompletePackage(self::PACKAGE_NAME, 'dev-main', 'dev-main'); + $package->setType('php-ext'); + $package->setInstallationSource('source'); + $package->setSourceType('git'); + $package->setSourceUrl($this->gitRepositoryPath); + $package->setSourceReference($reference); + + return $package; + } +} diff --git a/test/integration/ExamplePieExtensionFixture.php b/test/integration/ExamplePieExtensionFixture.php index 6c4481ad..403f266b 100644 --- a/test/integration/ExamplePieExtensionFixture.php +++ b/test/integration/ExamplePieExtensionFixture.php @@ -6,6 +6,8 @@ final class ExamplePieExtensionFixture { - public const LATEST_VERSION = '2.0.10'; - public const LATEST_VERSION_REFERENCE = 'f4aa08c72cffd395e8ddbcb55622f33165da8660'; + public const LATEST_VERSION = '2.0.10'; + public const LATEST_VERSION_REFERENCE = 'f4aa08c72cffd395e8ddbcb55622f33165da8660'; + public const MAIN_BRANCH_PREVIOUS_REFERENCE = '963c8d70c57c23fa2098e499a0ebffabb64748b3'; + public const MAIN_BRANCH_LATEST_REFERENCE = 'f4aa08c72cffd395e8ddbcb55622f33165da8660'; } diff --git a/test/unit/ComposerIntegration/AddInstalledJsonMetadataTest.php b/test/unit/ComposerIntegration/AddInstalledJsonMetadataTest.php index 26485177..05ee30c0 100644 --- a/test/unit/ComposerIntegration/AddInstalledJsonMetadataTest.php +++ b/test/unit/ComposerIntegration/AddInstalledJsonMetadataTest.php @@ -6,8 +6,10 @@ use Composer\Composer; use Composer\IO\IOInterface; +use Composer\Package\CompleteAliasPackage; use Composer\Package\CompletePackage; use Composer\Package\CompletePackageInterface; +use Composer\Package\Version\VersionParser; use Composer\Repository\InstalledArrayRepository; use Composer\Repository\RepositoryManager; use Php\Pie\ComposerIntegration\AddInstalledJsonMetadata; @@ -31,8 +33,11 @@ final class AddInstalledJsonMetadataTest extends TestCase { private function mockComposerInstalledRepositoryWith(CompletePackageInterface $package): Composer&MockObject { - $installedRepository = new InstalledArrayRepository([$package]); + return $this->mockComposerWithLocalRepository(new InstalledArrayRepository([$package])); + } + private function mockComposerWithLocalRepository(InstalledArrayRepository $installedRepository): Composer&MockObject + { $repositoryManager = $this->createMock(RepositoryManager::class); $repositoryManager->method('getLocalRepository')->willReturn($installedRepository); @@ -136,4 +141,47 @@ public function testMetadataForInstalls(): void $package->getExtra(), ); } + + public function testMetadataIsAddedToAliasedPackageForDefaultBranchAlias(): void + { + $package = new CompletePackage('foo/bar', 'dev-main', 'dev-main'); + + $installedRepository = new InstalledArrayRepository([ + new CompleteAliasPackage($package, VersionParser::DEFAULT_BRANCH_ALIAS, VersionParser::DEFAULT_BRANCH_ALIAS), + ]); + + (new AddInstalledJsonMetadata())->addInstallMetadata( + $this->mockComposerWithLocalRepository($installedRepository), + clone $package, + new BinaryFile('/path/to/installed', 'ignore'), + ); + + self::assertSame( + ['pie-installed-binary' => '/path/to/installed'], + $package->getExtra(), + ); + } + + public function testMetadataIsAddedToCurrentPackageAfterUpdateOfDefaultBranchAlias(): void + { + $replacedPackage = new CompletePackage('foo/bar', 'dev-main', 'dev-main'); + $currentPackage = new CompletePackage('foo/bar', 'dev-main', 'dev-main'); + + $installedRepository = new InstalledArrayRepository([ + new CompleteAliasPackage($replacedPackage, VersionParser::DEFAULT_BRANCH_ALIAS, VersionParser::DEFAULT_BRANCH_ALIAS), + ]); + $installedRepository->removePackage($replacedPackage); + $installedRepository->addPackage($currentPackage); + + (new AddInstalledJsonMetadata())->addInstallMetadata( + $this->mockComposerWithLocalRepository($installedRepository), + clone $currentPackage, + new BinaryFile('/path/to/installed', 'ignore'), + ); + + self::assertSame( + ['pie-installed-binary' => '/path/to/installed'], + $currentPackage->getExtra(), + ); + } } diff --git a/test/unit/ComposerIntegration/Listeners/RemoveUnrelatedInstallOperationsTest.php b/test/unit/ComposerIntegration/Listeners/RemoveUnrelatedInstallOperationsTest.php index 1a82c685..f2a3589f 100644 --- a/test/unit/ComposerIntegration/Listeners/RemoveUnrelatedInstallOperationsTest.php +++ b/test/unit/ComposerIntegration/Listeners/RemoveUnrelatedInstallOperationsTest.php @@ -6,14 +6,18 @@ use Composer\Composer; use Composer\DependencyResolver\Operation\InstallOperation; +use Composer\DependencyResolver\Operation\MarkAliasUninstalledOperation; use Composer\DependencyResolver\Operation\OperationInterface; +use Composer\DependencyResolver\Operation\UninstallOperation; use Composer\DependencyResolver\Operation\UpdateOperation; use Composer\DependencyResolver\Transaction; use Composer\EventDispatcher\EventDispatcher; use Composer\Installer\InstallerEvent; use Composer\Installer\InstallerEvents; use Composer\IO\IOInterface; +use Composer\Package\CompleteAliasPackage; use Composer\Package\CompletePackage; +use Composer\Package\Version\VersionParser; use Php\Pie\ComposerIntegration\Listeners\RemoveUnrelatedInstallOperations; use Php\Pie\ComposerIntegration\PieComposerRequest; use Php\Pie\ComposerIntegration\PieOperation; @@ -31,6 +35,8 @@ use function array_filter; use function array_map; +use function array_values; +use function assert; #[CoversClass(RemoveUnrelatedInstallOperations::class)] final class RemoveUnrelatedInstallOperationsTest extends TestCase @@ -184,4 +190,62 @@ public function testUnrelatedUpdateOperationsAreRemoved(): void ), ); } + + public function testDefaultBranchAliasOfRequestedPackageIsUninstalled(): void + { + $keepPackage = new CompletePackage('bat/baz', 'dev-main', 'dev-main'); + $discardPackage = new CompletePackage('foo/bar', 'dev-main', 'dev-main'); + + $installerEvent = new InstallerEvent( + InstallerEvents::PRE_OPERATIONS_EXEC, + $this->composer, + $this->createMock(IOInterface::class), + false, + true, + new Transaction( + [ + $keepPackage, + new CompleteAliasPackage($keepPackage, VersionParser::DEFAULT_BRANCH_ALIAS, VersionParser::DEFAULT_BRANCH_ALIAS), + $discardPackage, + new CompleteAliasPackage($discardPackage, VersionParser::DEFAULT_BRANCH_ALIAS, VersionParser::DEFAULT_BRANCH_ALIAS), + ], + [], + ), + ); + + (new RemoveUnrelatedInstallOperations( + new PieComposerRequest( + $this->createMock(IOInterface::class), + new TargetPlatform( + OperatingSystem::NonWindows, + OperatingSystemFamily::Linux, + PhpBinaryPath::fromCurrentProcess(), + Architecture::x86_64, + ThreadSafetyMode::NonThreadSafe, + 1, + null, + null, + ), + [new RequestedPackageAndVersion('bat/baz', 'dev-main')], + PieOperation::Uninstall, + [], + false, + ), + ))($installerEvent); + + self::assertSame( + [ + UninstallOperation::class . ' bat/baz dev-main', + MarkAliasUninstalledOperation::class . ' bat/baz ' . VersionParser::DEFAULT_BRANCH_ALIAS, + ], + array_values(array_map( + static function (OperationInterface $operation): string { + assert($operation instanceof UninstallOperation || $operation instanceof MarkAliasUninstalledOperation); + + return $operation::class . ' ' . $operation->getPackage()->getName() . ' ' . $operation->getPackage()->getPrettyVersion(); + }, + $installerEvent->getTransaction()?->getOperations() ?? [], + )), + ); + } }