1
0
Fork 0
mirror of https://github.com/composer/composer synced 2025-05-09 00:22:53 +00:00

Implemented PoolOptimizer

This commit is contained in:
Yanick Witschi 2020-10-01 16:42:02 +02:00 committed by Jordi Boggiano
parent 7eca450d9b
commit 34183f49f9
No known key found for this signature in database
GPG key ID: 7BBD42C429EC80BC
30 changed files with 1492 additions and 18 deletions

View file

@ -105,3 +105,13 @@ Check that replacers from additional repositories are loaded when doing a partia
"shared/dep-1.0.0.0",
"shared/dep-1.2.0.0"
]
--EXPECT-OPTIMIZED--
[
"indirect/replacer-1.2.0.0",
"indirect/replacer-1.0.0.0",
"replacer/package-1.2.0.0",
"replacer/package-1.0.0.0",
"base/package-1.0.0.0",
"shared/dep-1.2.0.0"
]

View file

@ -96,3 +96,13 @@ Check that replacers from additional repositories are loaded
"replacer/package-1.0.0.0",
"shared/dep-1.0.0.0"
]
--EXPECT-OPTIMIZED--
[
"base/package-1.0.0.0",
"indirect/replacer-1.2.0.0",
"indirect/replacer-1.0.0.0",
"shared/dep-1.2.0.0",
"replacer/package-1.2.0.0",
"replacer/package-1.0.0.0"
]

View file

@ -48,3 +48,13 @@ locked packages still need to be taking into account for loading all necessary v
"dep/pkg1-1.0.1.0",
"dep/pkg1-2.0.0.0"
]
--EXPECT-OPTIMIZED--
[
"root/req1-1.0.0.0 (locked)",
"root/req2-1.0.0.0 (locked)",
"dep/pkg2-1.0.0.0",
"dep/pkg2-1.2.0.0",
"dep/pkg1-1.0.1.0",
"dep/pkg1-2.0.0.0"
]

View file

@ -50,3 +50,12 @@ Fixed packages and replacers get unfixed correctly (refs https://github.com/comp
"replaced/pkg-1.2.3.0",
"replaced/pkg-1.2.4.0"
]
--EXPECT-OPTIMIZED--
[
"root/req3-1.0.0.0 (locked)",
"dep/dep-2.3.5.0 (locked)",
"root/req1-1.1.0.0",
"replacer/pkg-1.1.0.0",
"replaced/pkg-1.2.4.0"
]

View file

@ -46,3 +46,10 @@ Stability flags apply
6,
"default/pkg-1.2.0.0 (alias of 6)"
]
--EXPECT-OPTIMIZED--
[
1,
6,
"default/pkg-1.2.0.0 (alias of 6)"
]

View file

@ -0,0 +1,99 @@
--TEST--
Test aliased and aliasees remain untouched if either is required, but are still optimized away otherwise.
--REQUEST--
{
"require": {
"package/a": "^1.0",
"package/required-aliasof-and-alias": "dev-main-both",
"package/required-aliasof": "dev-main-direct",
"package/required-alias": "1.*"
}
}
--POOL-BEFORE--
[
{
"name": "package/a",
"version": "1.0.0",
"require": {
"package/required-aliasof-and-alias": "^1.0"
}
},
{
"name": "package/required-aliasof-and-alias",
"version": "dev-main-both",
"extra": {
"branch-alias": {
"dev-main-both": "1.x-dev"
}
}
},
{
"name": "package/required-aliasof",
"version": "dev-main-direct",
"extra": {
"branch-alias": {
"dev-main-direct": "1.x-dev"
}
}
},
{
"name": "package/required-alias",
"version": "dev-main-alias",
"extra": {
"branch-alias": {
"dev-main-alias": "1.x-dev"
}
}
},
{
"name": "package/not-referenced",
"version": "dev-lonesome-pkg",
"extra": {
"branch-alias": {
"dev-lonesome-pkg": "1.x-dev"
}
}
}
]
--POOL-AFTER--
[
{
"name": "package/a",
"version": "1.0.0",
"require": {
"package/b": "^1.0"
}
},
{
"name": "package/required-aliasof-and-alias",
"version": "dev-main-both",
"extra": {
"branch-alias": {
"dev-main-both": "1.x-dev"
}
}
},
{
"name": "package/required-aliasof",
"version": "dev-main-direct",
"extra": {
"branch-alias": {
"dev-main-direct": "1.x-dev"
}
}
},
{
"name": "package/required-alias",
"version": "dev-main-alias",
"extra": {
"branch-alias": {
"dev-main-alias": "1.x-dev"
}
}
}
]

View file

@ -0,0 +1,46 @@
--TEST--
Test filters irrelevant package "package/b" in version 1.0.0
--REQUEST--
{
"require": {
"package/a": "^1.0"
}
}
--POOL-BEFORE--
[
{
"name": "package/a",
"version": "1.0.0",
"require": {
"package/b": "^1.0"
}
},
{
"name": "package/b",
"version": "1.0.0"
},
{
"name": "package/b",
"version": "1.0.1"
}
]
--POOL-AFTER--
[
{
"name": "package/a",
"version": "1.0.0",
"require": {
"package/b": "^1.0"
}
},
{
"name": "package/b",
"version": "1.0.1"
}
]

View file

@ -0,0 +1,47 @@
--TEST--
Test filters irrelevant package "package/b" in version 1.0.1 because prefer-lowest
--REQUEST--
{
"require": {
"package/a": "^1.0"
},
"preferLowest": true
}
--POOL-BEFORE--
[
{
"name": "package/a",
"version": "1.0.0",
"require": {
"package/b": "^1.0"
}
},
{
"name": "package/b",
"version": "1.0.0"
},
{
"name": "package/b",
"version": "1.0.1"
}
]
--POOL-AFTER--
[
{
"name": "package/a",
"version": "1.0.0",
"require": {
"package/b": "^1.0"
}
},
{
"name": "package/b",
"version": "1.0.0"
}
]

View file

@ -0,0 +1,107 @@
--TEST--
We have to make sure, conflicts are considered in the grouping so we do not remove packages
from the pool which might end up being part of the solution.
--REQUEST--
{
"require": {
"nesty/nest": "^1.0"
}
}
--POOL-BEFORE--
[
{
"name": "nesty/nest",
"version": "1.0.0",
"require": {
"conflicter/pkg": "^1.0",
"victim/pkg": "^1 <1.2"
}
},
{
"name": "conflicter/pkg",
"version": "1.0.1",
"conflict": {
"victim/pkg": "1.1.0 || 1.1.1"
}
},
{
"name": "conflicter/pkg",
"version": "1.0.2",
"conflict": {
"victim/pkg": "1.1.1 || 1.1.2"
}
},
{
"name": "victim/pkg",
"version": "1.0.0"
},
{
"name": "victim/pkg",
"version": "1.0.1"
},
{
"name": "victim/pkg",
"version": "1.0.2"
},
{
"name": "victim/pkg",
"version": "1.1.0"
},
{
"name": "victim/pkg",
"version": "1.1.1"
},
{
"name": "victim/pkg",
"version": "1.1.2"
},
{
"name": "victim/pkg",
"version": "1.2.0"
}
]
--POOL-AFTER--
[
{
"name": "nesty/nest",
"version": "1.0.0",
"require": {
"conflicter/pkg": "^1.0",
"victim/pkg": "^1 <1.2"
}
},
{
"name": "conflicter/pkg",
"version": "1.0.1",
"conflict": {
"victim/pkg": "1.1.0 || 1.1.1"
}
},
{
"name": "conflicter/pkg",
"version": "1.0.2",
"conflict": {
"victim/pkg": "1.1.2"
}
},
{
"name": "victim/pkg",
"version": "1.0.2"
},
{
"name": "victim/pkg",
"version": "1.1.0"
},
{
"name": "victim/pkg",
"version": "1.1.1"
},
{
"name": "victim/pkg",
"version": "1.1.2"
}
]

View file

@ -0,0 +1,103 @@
--TEST--
We have to make sure, conflicts are considered in the grouping so we do not remove packages
from the pool which might end up being part of the solution.
--REQUEST--
{
"require": {
"nesty/nest": "^1.0"
}
}
--POOL-BEFORE--
[
{
"name": "nesty/nest",
"version": "1.0.0",
"require": {
"conflicter/pkg": "^1.0",
"victim/pkg": "^1 <1.2"
}
},
{
"name": "conflicter/pkg",
"version": "1.0.1",
"conflict": {
"victim/pkg": "1.1.0"
}
},
{
"name": "conflicter/pkg",
"version": "1.0.2",
"conflict": {
"victim/pkg": "1.1.2"
}
},
{
"name": "victim/pkg",
"version": "1.0.0"
},
{
"name": "victim/pkg",
"version": "1.0.1"
},
{
"name": "victim/pkg",
"version": "1.0.2"
},
{
"name": "victim/pkg",
"version": "1.1.0"
},
{
"name": "victim/pkg",
"version": "1.1.1"
},
{
"name": "victim/pkg",
"version": "1.1.2"
},
{
"name": "victim/pkg",
"version": "1.2.0"
}
]
--POOL-AFTER--
[
{
"name": "nesty/nest",
"version": "1.0.0",
"require": {
"conflicter/pkg": "^1.0",
"victim/pkg": "^1 <1.2"
}
},
{
"name": "conflicter/pkg",
"version": "1.0.1",
"conflict": {
"victim/pkg": "1.1.0 || 1.1.1"
}
},
{
"name": "conflicter/pkg",
"version": "1.0.2",
"conflict": {
"victim/pkg": "1.1.2"
}
},
{
"name": "victim/pkg",
"version": "1.1.0"
},
{
"name": "victim/pkg",
"version": "1.1.1"
},
{
"name": "victim/pkg",
"version": "1.1.2"
}
]

View file

@ -0,0 +1,99 @@
--TEST--
We are not allowed to group packages only by their dependency definition. It's also relevant what other
packages require (package/b@1.0.1 must not be dropped although it has the very same definition as 2.0.0 and both are
allowed by the request). However, package/b@1.0.0 can be removed.
--REQUEST--
{
"require": {
"package/a": "^1.0"
}
}
--POOL-BEFORE--
[
{
"name": "package/a",
"version": "1.0.0",
"require": {
"package/b": "^1.0 || ^2.0"
}
},
{
"name": "package/b",
"version": "1.0.0",
"require": {
"package/c": "^1.0"
}
},
{
"name": "package/b",
"version": "1.0.1",
"require": {
"package/c": "^1.0"
}
},
{
"name": "package/b",
"version": "2.0.0",
"require": {
"package/c": "^1.0"
}
},
{
"name": "package/c",
"version": "1.0.0",
"require": {
"package/d": "^1.0"
}
},
{
"name": "package/d",
"version": "1.0.0",
"require": {
"package/b": ">=1.0 <1.1"
}
}
]
--POOL-AFTER--
[
{
"name": "package/a",
"version": "1.0.0",
"require": {
"package/b": "^1.0 || ^2.0"
}
},
{
"name": "package/b",
"version": "1.0.1",
"require": {
"package/c": "^1.0"
}
},
{
"name": "package/b",
"version": "2.0.0",
"require": {
"package/c": "^1.0"
}
},
{
"name": "package/c",
"version": "1.0.0",
"require": {
"package/d": "^1.0"
}
},
{
"name": "package/d",
"version": "1.0.0",
"require": {
"package/b": ">=1.0 <1.1"
}
}
]

View file

@ -0,0 +1,46 @@
--TEST--
Test locked and fixed packages remain untouched.
--REQUEST--
{
"require": {
},
"locked": [
{
"name": "package/a",
"version": "1.0.0"
}
],
"fixed": [
{
"name": "package/c",
"version": "2.0.0"
}
]
}
--POOL-BEFORE--
[
{
"name": "package/a",
"version": "1.0.0"
},
{
"name": "package/c",
"version": "2.0.0"
}
]
--POOL-AFTER--
[
{
"name": "package/a",
"version": "1.0.0"
},
{
"name": "package/c",
"version": "2.0.0"
}
]

View file

@ -0,0 +1,59 @@
--TEST--
Test replaced packages are correctly removed.
--REQUEST--
{
"require": {
"package/a": "^1.0"
}
}
--POOL-BEFORE--
[
{
"name": "package/a",
"version": "1.0.0",
"require": {
"package/b": "^1.0"
}
},
{
"name": "package/b",
"version": "1.0.0",
"replace": {
"package/c": "^1.0"
}
},
{
"name": "package/b",
"version": "1.0.1",
"replace": {
"package/c": "^1.0"
}
},
{
"name": "package/c",
"version": "1.0.0"
}
]
--POOL-AFTER--
[
{
"name": "package/a",
"version": "1.0.0",
"require": {
"package/b": "^1.0"
}
},
{
"name": "package/b",
"version": "1.0.1",
"replace": {
"package/c": "^1.0"
}
}
]

View file

@ -12,6 +12,9 @@
namespace Composer\Test\DependencyResolver;
use Composer\DependencyResolver\DefaultPolicy;
use Composer\DependencyResolver\Pool;
use Composer\DependencyResolver\PoolOptimizer;
use Composer\IO\NullIO;
use Composer\Repository\ArrayRepository;
use Composer\Repository\FilterRepository;
@ -31,13 +34,14 @@ class PoolBuilderTest extends TestCase
* @dataProvider getIntegrationTests
* @param string $file
* @param string $message
* @param mixed[] $expect
* @param string[] $expect
* @param string[] $expectOptimized
* @param mixed[] $root
* @param mixed[] $requestData
* @param mixed[] $packageRepos
* @param mixed[] $fixed
*/
public function testPoolBuilder($file, $message, $expect, $root, $requestData, $packageRepos, $fixed)
public function testPoolBuilder($file, $message, $expect, $expectOptimized, $root, $requestData, $packageRepos, $fixed)
{
$rootAliases = !empty($root['aliases']) ? $root['aliases'] : array();
$minimumStability = !empty($root['minimum-stability']) ? $root['minimum-stability'] : 'stable';
@ -56,6 +60,8 @@ class PoolBuilderTest extends TestCase
$loader = new ArrayLoader();
$packageIds = array();
$loadPackage = function ($data) use ($loader, &$packageIds) {
/** @var ?int $id */
$id = null;
if (!empty($data['id'])) {
$id = $data['id'];
unset($data['id']);
@ -115,12 +121,28 @@ class PoolBuilderTest extends TestCase
}
$pool = $repositorySet->createPool($request, new NullIO());
$result = $this->getPackageResultSet($pool, $packageIds);
$this->assertSame($expect, $result, 'Unoptimized pool does not match expected package set');
$optimizer = new PoolOptimizer(new DefaultPolicy());
$result = $this->getPackageResultSet($optimizer->optimize($request, $pool), $packageIds);
$this->assertSame($expectOptimized, $result, 'Optimized pool does not match expected package set');
}
/**
* @param array<int, BasePackage> $packageIds
* @return string[]
*/
private function getPackageResultSet(Pool $pool, $packageIds)
{
$result = array();
for ($i = 1, $count = count($pool); $i <= $count; $i++) {
$result[] = $pool->packageById($i);
}
$result = array_map(function ($package) use ($packageIds) {
return array_map(function (BasePackage $package) use ($packageIds) {
if ($id = array_search($package, $packageIds, true)) {
return $id;
}
@ -143,8 +165,6 @@ class PoolBuilderTest extends TestCase
return (string) $package->getName().'-'.$package->getVersion() . $suffix;
}, $result);
$this->assertSame($expect, $result);
}
/**
@ -173,11 +193,12 @@ class PoolBuilderTest extends TestCase
$fixed = JsonFile::parseJson($testData['FIXED']);
}
$expect = JsonFile::parseJson($testData['EXPECT']);
$expectOptimized = !empty($testData['EXPECT-OPTIMIZED']) ? JsonFile::parseJson($testData['EXPECT-OPTIMIZED']) : $expect;
} catch (\Exception $e) {
die(sprintf('Test "%s" is not valid: '.$e->getMessage(), str_replace($fixturesDir.'/', '', $file)));
}
$tests[basename($file)] = array(str_replace($fixturesDir.'/', '', $file), $message, $expect, $root, $request, $packageRepos, $fixed);
$tests[basename($file)] = array(str_replace($fixturesDir.'/', '', $file), $message, $expect, $expectOptimized, $root, $request, $packageRepos, $fixed);
}
return $tests;
@ -199,6 +220,7 @@ class PoolBuilderTest extends TestCase
'FIXED' => false,
'PACKAGE-REPOS' => true,
'EXPECT' => true,
'EXPECT-OPTIMIZED' => false,
);
$section = null;

View file

@ -0,0 +1,197 @@
<?php
/*
* 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\DependencyResolver;
use Composer\DependencyResolver\DefaultPolicy;
use Composer\DependencyResolver\Pool;
use Composer\DependencyResolver\PoolOptimizer;
use Composer\DependencyResolver\Request;
use Composer\Json\JsonFile;
use Composer\Package\AliasPackage;
use Composer\Package\BasePackage;
use Composer\Package\Loader\ArrayLoader;
use Composer\Package\Version\VersionParser;
use Composer\Repository\LockArrayRepository;
use Composer\Test\TestCase;
class PoolOptimizerTest extends TestCase
{
/**
* @dataProvider provideIntegrationTests
* @param mixed[] $requestData
* @param BasePackage[] $packagesBefore
* @param BasePackage[] $expectedPackages
* @param string $message
*/
public function testPoolOptimizer(array $requestData, array $packagesBefore, array $expectedPackages, $message)
{
$lockedRepo = new LockArrayRepository();
$request = new Request($lockedRepo);
$parser = new VersionParser();
if (isset($requestData['locked'])) {
foreach ($requestData['locked'] as $package) {
$request->lockPackage($this->loadPackage($package));
}
}
if (isset($requestData['fixed'])) {
foreach ($requestData['fixed'] as $package) {
$request->fixPackage($this->loadPackage($package));
}
}
foreach ($requestData['require'] as $package => $constraint) {
$request->requireName($package, $parser->parseConstraints($constraint));
}
$preferStable = isset($requestData['preferStable']) ? $requestData['preferStable'] : false;
$preferLowest = isset($requestData['preferLowest']) ? $requestData['preferLowest'] : false;
$pool = new Pool($packagesBefore);
$poolOptimizer = new PoolOptimizer(new DefaultPolicy($preferStable, $preferLowest));
$pool = $poolOptimizer->optimize($request, $pool);
$this->assertSame(
$this->reducePackagesInfoForComparison($expectedPackages),
$this->reducePackagesInfoForComparison($pool->getPackages()),
$message
);
}
public function provideIntegrationTests()
{
$fixturesDir = realpath(__DIR__.'/Fixtures/pooloptimizer/');
$tests = array();
foreach (new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($fixturesDir), \RecursiveIteratorIterator::LEAVES_ONLY) as $file) {
if (!preg_match('/\.test$/', $file)) {
continue;
}
try {
$testData = $this->readTestFile($file, $fixturesDir);
$message = $testData['TEST'];
$requestData = JsonFile::parseJson($testData['REQUEST']);
$packagesBefore = $this->loadPackages(JsonFile::parseJson($testData['POOL-BEFORE']));
$expectedPackages = $this->loadPackages(JsonFile::parseJson($testData['POOL-AFTER']));
} catch (\Exception $e) {
die(sprintf('Test "%s" is not valid: '.$e->getMessage(), str_replace($fixturesDir.'/', '', $file)));
}
$tests[basename($file)] = array($requestData, $packagesBefore, $expectedPackages, $message);
}
return $tests;
}
/**
* @param string $fixturesDir
* @return mixed[]
*/
protected function readTestFile(\SplFileInfo $file, $fixturesDir)
{
$tokens = preg_split('#(?:^|\n*)--([A-Z-]+)--\n#', file_get_contents($file->getRealPath()), -1, PREG_SPLIT_DELIM_CAPTURE);
/** @var array<string, bool> $sectionInfo */
$sectionInfo = array(
'TEST' => true,
'REQUEST' => true,
'POOL-BEFORE' => true,
'POOL-AFTER' => true,
);
$section = null;
$data = array();
foreach ($tokens as $i => $token) {
if (null === $section && empty($token)) {
continue; // skip leading blank
}
if (null === $section) {
if (!isset($sectionInfo[$token])) {
throw new \RuntimeException(sprintf(
'The test file "%s" must not contain a section named "%s".',
str_replace($fixturesDir.'/', '', $file),
$token
));
}
$section = $token;
continue;
}
$sectionData = $token;
$data[$section] = $sectionData;
$section = $sectionData = null;
}
foreach ($sectionInfo as $section => $required) {
if ($required && !isset($data[$section])) {
throw new \RuntimeException(sprintf(
'The test file "%s" must have a section named "%s".',
str_replace($fixturesDir.'/', '', $file),
$section
));
}
}
return $data;
}
/**
* @param BasePackage[] $packages
* @return string[]
*/
private function reducePackagesInfoForComparison(array $packages)
{
$packagesInfo = array();
foreach ($packages as $package) {
$packagesInfo[] = $package->getName() . '@' . $package->getVersion() . ($package instanceof AliasPackage ? ' (alias of '.$package->getAliasOf()->getVersion().')' : '');
}
sort($packagesInfo);
return $packagesInfo;
}
/**
* @param mixed[][] $packagesData
* @return BasePackage[]
*/
private function loadPackages(array $packagesData)
{
$packages = array();
foreach ($packagesData as $packageData) {
$packages[] = $package = $this->loadPackage($packageData);
if ($package instanceof AliasPackage) {
$packages[] = $package->getAliasOf();
}
}
return $packages;
}
/**
* @param mixed[] $packageData
* @return BasePackage
*/
private function loadPackage(array $packageData)
{
$loader = new ArrayLoader();
return $loader->load($packageData);
}
}