Implemented PoolOptimizer
parent
7eca450d9b
commit
34183f49f9
@ -0,0 +1,345 @@
|
||||
<?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\DependencyResolver;
|
||||
|
||||
use Composer\Package\AliasPackage;
|
||||
use Composer\Package\BasePackage;
|
||||
use Composer\Semver\CompilingMatcher;
|
||||
use Composer\Semver\Constraint\ConstraintInterface;
|
||||
use Composer\Semver\Constraint\Constraint;
|
||||
use Composer\Semver\Constraint\MultiConstraint;
|
||||
use Composer\Semver\Intervals;
|
||||
|
||||
/**
|
||||
* Optimizes a given pool
|
||||
*
|
||||
* @author Yanick Witschi <yanick.witschi@terminal42.ch>
|
||||
*/
|
||||
class PoolOptimizer
|
||||
{
|
||||
/**
|
||||
* @var PolicyInterface
|
||||
*/
|
||||
private $policy;
|
||||
|
||||
/**
|
||||
* @var array<int, true>
|
||||
*/
|
||||
private $irremovablePackages = array();
|
||||
|
||||
/**
|
||||
* @var array<string, array<string, ConstraintInterface>>
|
||||
*/
|
||||
private $requireConstraintsPerPackage = array();
|
||||
|
||||
/**
|
||||
* @var array<string, array<string, ConstraintInterface>>
|
||||
*/
|
||||
private $conflictConstraintsPerPackage = array();
|
||||
|
||||
/**
|
||||
* @var array<int, true>
|
||||
*/
|
||||
private $packagesToRemove = array();
|
||||
|
||||
/**
|
||||
* @var array<int, BasePackage[]>
|
||||
*/
|
||||
private $aliasesPerPackage = array();
|
||||
|
||||
public function __construct(PolicyInterface $policy)
|
||||
{
|
||||
$this->policy = $policy;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Pool
|
||||
*/
|
||||
public function optimize(Request $request, Pool $pool)
|
||||
{
|
||||
$this->prepare($request, $pool);
|
||||
|
||||
$optimizedPool = $this->optimizeByIdenticalDependencies($pool);
|
||||
|
||||
// No need to run this recursively at the moment
|
||||
// because the current optimizations cannot provide
|
||||
// even more gains when ran again. Might change
|
||||
// in the future with additional optimizations.
|
||||
|
||||
$this->irremovablePackages = array();
|
||||
$this->requireConstraintsPerPackage = array();
|
||||
$this->conflictConstraintsPerPackage = array();
|
||||
$this->packagesToRemove = array();
|
||||
$this->aliasesPerPackage = array();
|
||||
|
||||
return $optimizedPool;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
private function prepare(Request $request, Pool $pool)
|
||||
{
|
||||
$irremovablePackageConstraintGroups = array();
|
||||
|
||||
// Mark fixed or locked packages as irremovable
|
||||
foreach ($request->getFixedOrLockedPackages() as $package) {
|
||||
$irremovablePackageConstraintGroups[$package->getName()][] = new Constraint('==', $package->getVersion());
|
||||
}
|
||||
|
||||
// Extract requested package requirements
|
||||
foreach ($request->getRequires() as $require => $constraint) {
|
||||
$constraint = Intervals::compactConstraint($constraint);
|
||||
$this->requireConstraintsPerPackage[$require][(string) $constraint] = $constraint;
|
||||
}
|
||||
|
||||
// First pass over all packages to extract information and mark package constraints irremovable
|
||||
foreach ($pool->getPackages() as $package) {
|
||||
// Extract package requirements
|
||||
foreach ($package->getRequires() as $link) {
|
||||
$constraint = Intervals::compactConstraint($link->getConstraint());
|
||||
$this->requireConstraintsPerPackage[$link->getTarget()][(string) $constraint] = $constraint;
|
||||
}
|
||||
// Extract package conflicts
|
||||
foreach ($package->getConflicts() as $link) {
|
||||
$constraint = Intervals::compactConstraint($link->getConstraint());
|
||||
$this->conflictConstraintsPerPackage[$link->getTarget()][(string) $constraint] = $constraint;
|
||||
}
|
||||
|
||||
// Keep track of alias packages for every package so if either the alias or aliased is kept
|
||||
// we keep the others as they are a unit of packages really
|
||||
if ($package instanceof AliasPackage) {
|
||||
$this->aliasesPerPackage[$package->getAliasOf()->id][] = $package;
|
||||
}
|
||||
}
|
||||
|
||||
$irremovablePackageConstraints = array();
|
||||
foreach ($irremovablePackageConstraintGroups as $packageName => $constraints) {
|
||||
$irremovablePackageConstraints[$packageName] = 1 === \count($constraints) ? $constraints[0] : new MultiConstraint($constraints, false);
|
||||
}
|
||||
unset($irremovablePackageConstraintGroups);
|
||||
|
||||
// Mark the packages as irremovable based on the constraints
|
||||
foreach ($pool->getPackages() as $package) {
|
||||
if (!isset($irremovablePackageConstraints[$package->getName()])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (CompilingMatcher::match($irremovablePackageConstraints[$package->getName()], Constraint::OP_EQ, $package->getVersion())) {
|
||||
$this->markPackageIrremovable($package);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
private function markPackageIrremovable(BasePackage $package)
|
||||
{
|
||||
$this->irremovablePackages[$package->id] = true;
|
||||
if ($package instanceof AliasPackage) {
|
||||
// recursing here so aliasesPerPackage for the aliasOf can be checked
|
||||
// and all its aliases marked as irremovable as well
|
||||
$this->markPackageIrremovable($package->getAliasOf());
|
||||
}
|
||||
if (isset($this->aliasesPerPackage[$package->id])) {
|
||||
foreach ($this->aliasesPerPackage[$package->id] as $aliasPackage) {
|
||||
$this->irremovablePackages[$aliasPackage->id] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Pool Optimized pool
|
||||
*/
|
||||
private function applyRemovalsToPool(Pool $pool)
|
||||
{
|
||||
$packages = array();
|
||||
foreach ($pool->getPackages() as $package) {
|
||||
if (!isset($this->packagesToRemove[$package->id])) {
|
||||
$packages[] = $package;
|
||||
}
|
||||
}
|
||||
|
||||
$optimizedPool = new Pool($packages, $pool->getUnacceptableFixedOrLockedPackages());
|
||||
|
||||
// Reset package removals
|
||||
$this->packagesToRemove = array();
|
||||
|
||||
return $optimizedPool;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Pool
|
||||
*/
|
||||
private function optimizeByIdenticalDependencies(Pool $pool)
|
||||
{
|
||||
$identicalDefinitionPerPackage = array();
|
||||
$packageIdsToRemove = array();
|
||||
|
||||
foreach ($pool->getPackages() as $package) {
|
||||
|
||||
// If that package was already marked irremovable, we can skip
|
||||
// the entire process for it
|
||||
if (isset($this->irremovablePackages[$package->id])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$packageIdsToRemove[$package->id] = true;
|
||||
|
||||
$dependencyHash = $this->calculateDependencyHash($package);
|
||||
|
||||
foreach ($package->getNames(false) as $packageName) {
|
||||
|
||||
if (!isset($this->requireConstraintsPerPackage[$packageName])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($this->requireConstraintsPerPackage[$packageName] as $requireConstraint) {
|
||||
|
||||
$groupHashParts = array();
|
||||
|
||||
if (CompilingMatcher::match($requireConstraint, Constraint::OP_EQ, $package->getVersion())) {
|
||||
$groupHashParts[] = 'require:' . (string) $requireConstraint;
|
||||
}
|
||||
|
||||
if ($package->getReplaces()) {
|
||||
foreach ($package->getReplaces() as $link) {
|
||||
if (CompilingMatcher::match($link->getConstraint(), Constraint::OP_EQ, $package->getVersion())) {
|
||||
// Use the same hash part as the regular require hash because that's what the replacement does
|
||||
$groupHashParts[] = 'require:' . (string) $link->getConstraint();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($this->conflictConstraintsPerPackage[$packageName])) {
|
||||
foreach ($this->conflictConstraintsPerPackage[$packageName] as $conflictConstraint) {
|
||||
if (CompilingMatcher::match($conflictConstraint, Constraint::OP_EQ, $package->getVersion())) {
|
||||
$groupHashParts[] = 'conflict:' . (string) $conflictConstraint;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!$groupHashParts) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$identicalDefinitionPerPackage[$packageName][implode('', $groupHashParts)][$dependencyHash][] = $package;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$keepPackage = function (BasePackage $package, $aliasesPerPackage) use (&$packageIdsToRemove, &$keepPackage) {
|
||||
unset($packageIdsToRemove[$package->id]);
|
||||
if ($package instanceof AliasPackage) {
|
||||
// recursing here so aliasesPerPackage for the aliasOf can be checked
|
||||
// and all its aliases marked to be kept as well
|
||||
$keepPackage($package->getAliasOf(), $aliasesPerPackage);
|
||||
}
|
||||
if (isset($aliasesPerPackage[$package->id])) {
|
||||
foreach ($aliasesPerPackage[$package->id] as $aliasPackage) {
|
||||
unset($packageIdsToRemove[$aliasPackage->id]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
foreach ($identicalDefinitionPerPackage as $package => $constraintGroups) {
|
||||
foreach ($constraintGroups as $constraintGroup) {
|
||||
foreach ($constraintGroup as $hash => $packages) {
|
||||
|
||||
// Only one package in this constraint group has the same requirements, we're not allowed to remove that package
|
||||
if (1 === \count($packages)) {
|
||||
$keepPackage($packages[0], $this->aliasesPerPackage);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Otherwise we find out which one is the preferred package in this constraint group which is
|
||||
// then not allowed to be removed either
|
||||
$literals = array();
|
||||
|
||||
foreach ($packages as $package) {
|
||||
$literals[] = $package->id;
|
||||
}
|
||||
|
||||
foreach ($this->policy->selectPreferredPackages($pool, $literals) as $preferredLiteral) {
|
||||
$keepPackage($pool->literalToPackage($preferredLiteral), $this->aliasesPerPackage);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($packageIdsToRemove as $id => $dummy) {
|
||||
$this->markPackageForRemoval($id);
|
||||
}
|
||||
|
||||
// Apply removals
|
||||
return $this->applyRemovalsToPool($pool);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
private function calculateDependencyHash(BasePackage $package)
|
||||
{
|
||||
$hash = '';
|
||||
|
||||
$hashRelevantLinks = array(
|
||||
'requires' => $package->getRequires(),
|
||||
'conflicts' => $package->getConflicts(),
|
||||
'replaces' => $package->getReplaces(),
|
||||
'provides' => $package->getProvides()
|
||||
);
|
||||
|
||||
foreach ($hashRelevantLinks as $key => $links) {
|
||||
if (0 === \count($links)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// start new hash section
|
||||
$hash .= $key . ':';
|
||||
|
||||
$subhash = array();
|
||||
|
||||
foreach ($links as $link) {
|
||||
// To get the best dependency hash matches we should use Intervals::compactConstraint() here.
|
||||
// However, the majority of projects are going to specify their constraints already pretty
|
||||
// much in the best variant possible. In other words, we'd be wasting time here and it would actually hurt
|
||||
// performance more than the additional few packages that could be filtered out would benefit the process.
|
||||
$subhash[$link->getTarget()] = (string) $link->getConstraint();
|
||||
}
|
||||
|
||||
// Sort for best result
|
||||
ksort($subhash);
|
||||
|
||||
foreach ($subhash as $target => $constraint) {
|
||||
$hash .= $target . '@' . $constraint;
|
||||
}
|
||||
}
|
||||
|
||||
return $hash;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $id
|
||||
* @return void
|
||||
*/
|
||||
private function markPackageForRemoval($id)
|
||||
{
|
||||
// We are not allowed to remove packages if they have been marked as irremovable
|
||||
if (isset($this->irremovablePackages[$id])) {
|
||||
throw new \LogicException('Attempted removing a package which was previously marked irremovable');
|
||||
}
|
||||
|
||||
$this->packagesToRemove[$id] = true;
|
||||
}
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
@ -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"
|
||||
}
|
||||
]
|
||||
|
@ -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"
|
||||
}
|
||||
]
|
||||
|
@ -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"
|
||||
}
|
||||
]
|
@ -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"
|
||||
}
|
||||
]
|
@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
@ -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"
|
||||
}
|
||||
]
|
@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
--TEST--
|
||||
|
||||
Test that a package which has a conflict does not get installed and has to be downgraded
|
||||
|
||||
--COMPOSER--
|
||||
{
|
||||
"repositories": [
|
||||
{
|
||||
"type": "package",
|
||||
"package": [
|
||||
{ "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": "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.2.0" }
|
||||
]
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"nesty/nest": "*"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
--RUN--
|
||||
update
|
||||
|
||||
--EXPECT-EXIT-CODE--
|
||||
0
|
||||
|
||||
--EXPECT--
|
||||
Installing victim/pkg (1.0.2)
|
||||
Installing conflicter/pkg (1.0.1)
|
||||
Installing nesty/nest (1.0.0)
|
@ -0,0 +1,35 @@
|
||||
--TEST--
|
||||
|
||||
Test that a package which has a conflict does not get installed and has to be downgraded
|
||||
|
||||
--COMPOSER--
|
||||
{
|
||||
"repositories": [
|
||||
{
|
||||
"type": "package",
|
||||
"package": [
|
||||
{ "name": "conflicter/pkg", "version": "1.0.1", "conflict": { "victim/pkg": "1.1.0"} },
|
||||
{ "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.2.0" }
|
||||
]
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"conflicter/pkg": "^1.0",
|
||||
"victim/pkg": "^1 <1.2"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
--RUN--
|
||||
update
|
||||
|
||||
--EXPECT-EXIT-CODE--
|
||||
0
|
||||
|
||||
--EXPECT--
|
||||
Installing conflicter/pkg (1.0.1)
|
||||
Installing victim/pkg (1.0.2)
|
Loading…
Reference in New Issue