diff --git a/tests/Composer/Test/DependencyResolver/SolverTest.php b/tests/Composer/Test/DependencyResolver/SolverTest.php index cf916fb6b4e0..712b2b108382 100644 --- a/tests/Composer/Test/DependencyResolver/SolverTest.php +++ b/tests/Composer/Test/DependencyResolver/SolverTest.php @@ -175,6 +175,115 @@ public function testSolverInstallWithDepsInOrder() )); } + /** + * This test covers a particular behavior of the solver related to packages with the same name and version, + * but different requirements on other packages. + * Imagine you had multiple instances of packages (same name/version) with e.g. different dists depending on what other related package they were "built" for. + * + * An example people can probably relate to, so it was chosen here for better readability: + * - PHP versions 8.0.10 and 7.4.23 could be a package + * - ext-foobar 1.0.0 could be a package, but it must be built separately for each PHP x.y series + * - thus each of the ext-foobar packages lists the "PHP" package as a dependency + * + * This is not something that can happen with packages on e.g. Packagist, but custom installers with custom repositories might do something like this; + * in fact, some PaaSes do the exact thing above, installing binary builds of PHP and extensions as Composer packages with a custom installer in a separate step before the "userland" `composer install`. + * + * If version selectors are sufficiently permissive (e.g. "ourcustom/php":"*", "ourcustom/ext-foobar":"*"), then it may happen that the Solver won't pick the highest possible PHP version, as it has already settled on an "ext-foobar" (they're all the same version to the Solver, it doesn't know about the different requirements in each of the otherwise identical packages) if that was listed in "require" before "php". + * That's "unfixable", and not even broken, behavior (what if the "ext-foobar" has higher versions for the lower "PHP"? who wins then? any combination of the packages is "correct"), but it shouldn't randomly change. + * This test asserts this behavior to prevent regressions. + * + * CAUTION: IF THIS TEST EVER FAILS, SOLVER BEHAVIOR HAS CHANGED AND MAY BREAK DOWNSTREAM USERS + */ + public function testSolverMultiPackageNameVersionResolutionDependsOnRequireOrder() + { + $this->repo->addPackage($php74 = $this->getPackage('ourcustom/PHP', '7.4.23')); + $this->repo->addPackage($php80 = $this->getPackage('ourcustom/PHP', '8.0.10')); + $this->repo->addPackage($extForPhp74 = $this->getPackage('ourcustom/ext-foobar', '1.0')); + $this->repo->addPackage($extForPhp80 = $this->getPackage('ourcustom/ext-foobar', '1.0')); + + $extForPhp74->setRequires(array( + 'php' => new Link('ourcustom/ext-foobar', 'ourcustom/PHP', new MultiConstraint(array( + $this->getVersionConstraint('>=', '7.4.0'), + $this->getVersionConstraint('<', '7.5.0'), + )), Link::TYPE_REQUIRE), + )); + $extForPhp80->setRequires(array( + 'php' => new Link('ourcustom/ext-foobar', 'ourcustom/PHP', new MultiConstraint(array( + $this->getVersionConstraint('>=', '8.0.0'), + $this->getVersionConstraint('<', '8.1.0'), + )), Link::TYPE_REQUIRE), + )); + + $this->reposComplete(); + + $this->request->requireName('ourcustom/PHP'); + $this->request->requireName('ourcustom/ext-foobar'); + + $this->checkSolverResult(array( + array('job' => 'install', 'package' => $php80), + array('job' => 'install', 'package' => $extForPhp80), + )); + + // now we flip the requirements around: we request "ext-foobar" before "php" + // because the ext-foobar package that requires php74 comes first in the repo, and the one that requires php80 second, the solver will pick the one for php74, and then, as it is a dependency, also php74 + // this is because both packages have the same name and version; just their requirements differ + // and because no other constraint forces a particular version of package "php" + $this->request = new Request($this->repoLocked); + $this->request->requireName('ourcustom/ext-foobar'); + $this->request->requireName('ourcustom/PHP'); + + $this->checkSolverResult(array( + array('job' => 'install', 'package' => $php74), + array('job' => 'install', 'package' => $extForPhp74), + )); + } + + /** + * This test is almost the same as above, except we're inserting the package with the requirement on the other package in a different order, asserting that if that is done, the order of requirements no longer matters + * + * CAUTION: IF THIS TEST EVER FAILS, SOLVER BEHAVIOR HAS CHANGED AND MAY BREAK DOWNSTREAM USERS + */ + public function testSolverMultiPackageNameVersionResolutionIsIndependentOfRequireOrderIfOrderedDescendingByRequirement() + { + $this->repo->addPackage($php74 = $this->getPackage('ourcustom/PHP', '7.4')); + $this->repo->addPackage($php80 = $this->getPackage('ourcustom/PHP', '8.0')); + $this->repo->addPackage($extForPhp80 = $this->getPackage('ourcustom/ext-foobar', '1.0')); // note we are inserting this one into the repo first, unlike in the previous test + $this->repo->addPackage($extForPhp74 = $this->getPackage('ourcustom/ext-foobar', '1.0')); + + $extForPhp80->setRequires(array( + 'php' => new Link('ourcustom/ext-foobar', 'ourcustom/PHP', new MultiConstraint(array( + $this->getVersionConstraint('>=', '8.0.0'), + $this->getVersionConstraint('<', '8.1.0'), + )), Link::TYPE_REQUIRE), + )); + $extForPhp74->setRequires(array( + 'php' => new Link('ourcustom/ext-foobar', 'ourcustom/PHP', new MultiConstraint(array( + $this->getVersionConstraint('>=', '7.4.0'), + $this->getVersionConstraint('<', '7.5.0'), + )), Link::TYPE_REQUIRE), + )); + + $this->reposComplete(); + + $this->request->requireName('ourcustom/PHP'); + $this->request->requireName('ourcustom/ext-foobar'); + + $this->checkSolverResult(array( + array('job' => 'install', 'package' => $php80), + array('job' => 'install', 'package' => $extForPhp80), + )); + + // unlike in the previous test, the order of requirements no longer matters now + $this->request = new Request($this->repoLocked); + $this->request->requireName('ourcustom/ext-foobar'); + $this->request->requireName('ourcustom/PHP'); + + $this->checkSolverResult(array( + array('job' => 'install', 'package' => $php80), + array('job' => 'install', 'package' => $extForPhp80), + )); + } + public function testSolverFixLocked() { $this->repoLocked->addPackage($packageA = $this->getPackage('A', '1.0'));