Merge pull request #9261 from Toflar/pooloptimizer

Implemented PoolOptimizer
main
Jordi Boggiano 3 years ago committed by GitHub
commit 4589b9bb18
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -11,7 +11,6 @@ on:
env:
COMPOSER_FLAGS: "--ansi --no-interaction --no-progress --prefer-dist"
COMPOSER_UPDATE_FLAGS: ""
COMPOSER_TESTS_ARE_RUNNING: "1"
jobs:
tests:

@ -353,3 +353,27 @@ See also https://github.com/composer/composer/issues/4180 for more information.
Composer can unpack zipballs using either a system-provided `unzip` or `7z` (7-Zip) utility, or PHP's
native `ZipArchive` class. On OSes where ZIP files can contain permissions and symlinks, we recommend
installing `unzip` or `7z` as these features are not supported by `ZipArchive`.
## Disabling the pool optimizer
In Composer, the `Pool` class contains all the packages that are relevant for the dependency
resolving process. That is what is used to generate all the rules which are then
passed on to the dependency solver.
In order to improve performance, Composer tries to optimize this `Pool` by removing useless
package information early on.
If all goes well, you should never notice any issues with it but in case you run into
an unexpected result such as an unresolvable set of dependencies or conflicts where you
think Composer is wrong, you might want to disable the optimizer by using the environment
variable `COMPOSER_POOL_OPTIMIZER` and run the update again like so:
```bash
COMPOSER_POOL_OPTIMIZER=0 php composer.phar update
```
Now double check if the result is still the same. It will take significantly longer and use
a lot more memory to run the dependency resolving process.
If the result is different, you likely hit a problem in the pool optimizer.
Please [report this issue](https://github.com/composer/composer/issues) so it can be fixed.

@ -36,16 +36,57 @@ class Pool implements \Countable
protected $providerCache = array();
/** @var BasePackage[] */
protected $unacceptableFixedOrLockedPackages;
/** @var array<string, array<string, string>> Map of package name => normalized version => pretty version */
protected $removedVersions = array();
/** @var array<string, array<string, string>> Map of package object hash => removed normalized versions => removed pretty version */
protected $removedVersionsByPackage = array();
/**
* @param BasePackage[] $packages
* @param BasePackage[] $unacceptableFixedOrLockedPackages
* @param array<string, array<string, string>> $removedVersions
* @param array<string, array<string, string>> $removedVersionsByPackage
*/
public function __construct(array $packages = array(), array $unacceptableFixedOrLockedPackages = array())
public function __construct(array $packages = array(), array $unacceptableFixedOrLockedPackages = array(), array $removedVersions = array(), array $removedVersionsByPackage = array())
{
$this->versionParser = new VersionParser;
$this->setPackages($packages);
$this->unacceptableFixedOrLockedPackages = $unacceptableFixedOrLockedPackages;
$this->removedVersions = $removedVersions;
$this->removedVersionsByPackage = $removedVersionsByPackage;
}
/**
* @param string $name
* @return array<string, string>
*/
public function getRemovedVersions($name, ConstraintInterface $constraint)
{
if (!isset($this->removedVersions[$name])) {
return array();
}
$result = array();
foreach ($this->removedVersions[$name] as $version => $prettyVersion) {
if ($constraint->matches(new Constraint('==', $version))) {
$result[$version] = $prettyVersion;
}
}
return $result;
}
/**
* @param string $objectHash
* @return array<string, string>
*/
public function getRemovedVersionsByPackage($objectHash)
{
if (!isset($this->removedVersionsByPackage[$objectHash])) {
return array();
}
return $this->removedVersionsByPackage[$objectHash];
}
/**
@ -221,6 +262,14 @@ class Pool implements \Countable
return \in_array($package, $this->unacceptableFixedOrLockedPackages, true);
}
/**
* @return BasePackage[]
*/
public function getUnacceptableFixedOrLockedPackages()
{
return $this->unacceptableFixedOrLockedPackages;
}
public function __toString()
{
$str = "Pool:\n";

@ -61,6 +61,10 @@ class PoolBuilder
* @var ?EventDispatcher
*/
private $eventDispatcher;
/**
* @var PoolOptimizer|null
*/
private $poolOptimizer;
/**
* @var IOInterface
*/
@ -128,13 +132,14 @@ class PoolBuilder
* @param string[] $rootReferences an array of package name => source reference
* @phpstan-param array<string, string> $rootReferences
*/
public function __construct(array $acceptableStabilities, array $stabilityFlags, array $rootAliases, array $rootReferences, IOInterface $io, EventDispatcher $eventDispatcher = null)
public function __construct(array $acceptableStabilities, array $stabilityFlags, array $rootAliases, array $rootReferences, IOInterface $io, EventDispatcher $eventDispatcher = null, PoolOptimizer $poolOptimizer = null)
{
$this->acceptableStabilities = $acceptableStabilities;
$this->stabilityFlags = $stabilityFlags;
$this->rootAliases = $rootAliases;
$this->rootReferences = $rootReferences;
$this->eventDispatcher = $eventDispatcher;
$this->poolOptimizer = $poolOptimizer;
$this->io = $io;
}
@ -259,6 +264,8 @@ class PoolBuilder
$this->skippedLoad = array();
$this->indexCounter = 0;
$pool = $this->runOptimizer($request, $pool);
Intervals::clear();
return $pool;
@ -572,4 +579,33 @@ class PoolBuilder
unset($this->aliasMap[spl_object_hash($package)]);
}
}
/**
* @return Pool
*/
private function runOptimizer(Request $request, Pool $pool)
{
if (null === $this->poolOptimizer) {
return $pool;
}
$total = \count($pool->getPackages());
$pool = $this->poolOptimizer->optimize($request, $pool);
$filtered = $total - \count($pool->getPackages());
if (0 === $filtered) {
return $pool;
}
$this->io->write(sprintf(
'<info>Found %s package versions referenced in your dependency graph. %s (%d%%) were optimized away.</info>',
number_format($total),
number_format($filtered),
round(100/$total*$filtered)
), true, IOInterface::VERY_VERBOSE);
return $pool;
}
}

@ -0,0 +1,386 @@
<?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\Package\Version\VersionParser;
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();
/**
* @var array<string, array<string, string>>
*/
private $removedVersionsByPackage = 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();
$this->removedVersionsByPackage = 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();
$removedVersions = array();
foreach ($pool->getPackages() as $package) {
if (!isset($this->packagesToRemove[$package->id])) {
$packages[] = $package;
} else {
$removedVersions[$package->getName()][$package->getVersion()] = $package->getPrettyVersion();
}
}
$optimizedPool = new Pool($packages, $pool->getUnacceptableFixedOrLockedPackages(), $removedVersions, $this->removedVersionsByPackage);
// Reset package removals
$this->packagesToRemove = array();
return $optimizedPool;
}
/**
* @return Pool
*/
private function optimizeByIdenticalDependencies(Pool $pool)
{
$identicalDefinitionsPerPackage = array();
$packageIdenticalDefinitionLookup = 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;
}
$this->markPackageForRemoval($package->id);
$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;
}
$groupHash = implode('', $groupHashParts);
$identicalDefinitionsPerPackage[$packageName][$groupHash][$dependencyHash][] = $package;
$packageIdenticalDefinitionLookup[$package->id][$packageName] = array('groupHash' => $groupHash, 'dependencyHash' => $dependencyHash);
}
}
}
foreach ($identicalDefinitionsPerPackage as $constraintGroups) {
foreach ($constraintGroups as $constraintGroup) {
foreach ($constraintGroup as $packages) {
// Only one package in this constraint group has the same requirements, we're not allowed to remove that package
if (1 === \count($packages)) {
$this->keepPackage($packages[0], $identicalDefinitionsPerPackage, $packageIdenticalDefinitionLookup);
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) {
$this->keepPackage($pool->literalToPackage($preferredLiteral), $identicalDefinitionsPerPackage, $packageIdenticalDefinitionLookup);
}
}
}
}
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;
}
/**
* @param array<string, array<string, array<string, list<BasePackage>>>> $identicalDefinitionsPerPackage
* @param array<int, array<string, array{groupHash: string, dependencyHash: string}>> $packageIdenticalDefinitionLookup
* @return void
*/
private function keepPackage(BasePackage $package, $identicalDefinitionsPerPackage, $packageIdenticalDefinitionLookup)
{
unset($this->packagesToRemove[$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
$this->keepPackage($package->getAliasOf(), $identicalDefinitionsPerPackage, $packageIdenticalDefinitionLookup);
}
// record all the versions of the package group so we can list them later in Problem output
foreach ($package->getNames(false) as $name) {
if (isset($packageIdenticalDefinitionLookup[$package->id][$name])) {
$packageGroupPointers = $packageIdenticalDefinitionLookup[$package->id][$name];
$packageGroup = $identicalDefinitionsPerPackage[$name][$packageGroupPointers['groupHash']][$packageGroupPointers['dependencyHash']];
foreach ($packageGroup as $pkg) {
if ($pkg instanceof AliasPackage && $pkg->getPrettyVersion() === VersionParser::DEFAULT_BRANCH_ALIAS) {
$pkg = $pkg->getAliasOf();
}
$this->removedVersionsByPackage[spl_object_hash($package)][$pkg->getVersion()] = $pkg->getPrettyVersion();
}
}
}
if (isset($this->aliasesPerPackage[$package->id])) {
foreach ($this->aliasesPerPackage[$package->id] as $aliasPackage) {
unset($this->packagesToRemove[$aliasPackage->id]);
// record all the versions of the package group so we can list them later in Problem output
foreach ($aliasPackage->getNames(false) as $name) {
if (isset($packageIdenticalDefinitionLookup[$aliasPackage->id][$name])) {
$packageGroupPointers = $packageIdenticalDefinitionLookup[$aliasPackage->id][$name];
$packageGroup = $identicalDefinitionsPerPackage[$name][$packageGroupPointers['groupHash']][$packageGroupPointers['dependencyHash']];
foreach ($packageGroup as $pkg) {
if ($pkg instanceof AliasPackage && $pkg->getPrettyVersion() === VersionParser::DEFAULT_BRANCH_ALIAS) {
$pkg = $pkg->getAliasOf();
}
$this->removedVersionsByPackage[spl_object_hash($aliasPackage)][$pkg->getVersion()] = $pkg->getPrettyVersion();
}
}
}
}
}
}
}

@ -126,6 +126,10 @@ class Problem
$template = preg_replace('{^\S+ \S+ }', '%s%s ', $message);
$messages[] = $template;
$templates[$template][$m[1]][$parser->normalize($m[2])] = $m[2];
$sourcePackage = $rule->getSourcePackage($pool);
foreach ($pool->getRemovedVersionsByPackage(spl_object_hash($sourcePackage)) as $version => $prettyVersion) {
$templates[$template][$m[1]][$version] = $prettyVersion;
}
} elseif ($message !== '') {
$messages[] = $message;
}
@ -267,7 +271,7 @@ class Problem
return $rootReqs[$packageName]->matches(new Constraint('==', $p->getVersion()));
});
if (0 === count($filtered)) {
return array("- Root composer.json requires $packageName".self::constraintToText($constraint) . ', ', 'found '.self::getPackageList($packages, $isVerbose).' but '.(self::hasMultipleNames($packages) ? 'these conflict' : 'it conflicts').' with your root composer.json require ('.$rootReqs[$packageName]->getPrettyString().').');
return array("- Root composer.json requires $packageName".self::constraintToText($constraint) . ', ', 'found '.self::getPackageList($packages, $isVerbose, $pool, $constraint).' but '.(self::hasMultipleNames($packages) ? 'these conflict' : 'it conflicts').' with your root composer.json require ('.$rootReqs[$packageName]->getPrettyString().').');
}
}
@ -277,7 +281,7 @@ class Problem
return $fixedConstraint->matches(new Constraint('==', $p->getVersion()));
});
if (0 === count($filtered)) {
return array("- Root composer.json requires $packageName".self::constraintToText($constraint) . ', ', 'found '.self::getPackageList($packages, $isVerbose).' but the package is fixed to '.$lockedPackage->getPrettyVersion().' (lock file version) by a partial update and that version does not match. Make sure you list it as an argument for the update command.');
return array("- Root composer.json requires $packageName".self::constraintToText($constraint) . ', ', 'found '.self::getPackageList($packages, $isVerbose, $pool, $constraint).' but the package is fixed to '.$lockedPackage->getPrettyVersion().' (lock file version) by a partial update and that version does not match. Make sure you list it as an argument for the update command.');
}
}
@ -286,27 +290,27 @@ class Problem
});
if (!$nonLockedPackages) {
return array("- Root composer.json requires $packageName".self::constraintToText($constraint) . ', ', 'found '.self::getPackageList($packages, $isVerbose).' in the lock file but not in remote repositories, make sure you avoid updating this package to keep the one from the lock file.');
return array("- Root composer.json requires $packageName".self::constraintToText($constraint) . ', ', 'found '.self::getPackageList($packages, $isVerbose, $pool, $constraint).' in the lock file but not in remote repositories, make sure you avoid updating this package to keep the one from the lock file.');
}
return array("- Root composer.json requires $packageName".self::constraintToText($constraint) . ', ', 'found '.self::getPackageList($packages, $isVerbose).' but these were not loaded, likely because '.(self::hasMultipleNames($packages) ? 'they conflict' : 'it conflicts').' with another require.');
return array("- Root composer.json requires $packageName".self::constraintToText($constraint) . ', ', 'found '.self::getPackageList($packages, $isVerbose, $pool, $constraint).' but these were not loaded, likely because '.(self::hasMultipleNames($packages) ? 'they conflict' : 'it conflicts').' with another require.');
}
// check if the package is found when bypassing stability checks
if ($packages = $repositorySet->findPackages($packageName, $constraint, RepositorySet::ALLOW_UNACCEPTABLE_STABILITIES)) {
// we must first verify if a valid package would be found in a lower priority repository
if ($allReposPackages = $repositorySet->findPackages($packageName, $constraint, RepositorySet::ALLOW_SHADOWED_REPOSITORIES)) {
return self::computeCheckForLowerPrioRepo($isVerbose, $packageName, $packages, $allReposPackages, 'minimum-stability', $constraint);
return self::computeCheckForLowerPrioRepo($pool, $isVerbose, $packageName, $packages, $allReposPackages, 'minimum-stability', $constraint);
}
return array("- Root composer.json requires $packageName".self::constraintToText($constraint) . ', ', 'found '.self::getPackageList($packages, $isVerbose).' but '.(self::hasMultipleNames($packages) ? 'these do' : 'it does').' not match your minimum-stability.');
return array("- Root composer.json requires $packageName".self::constraintToText($constraint) . ', ', 'found '.self::getPackageList($packages, $isVerbose, $pool, $constraint).' but '.(self::hasMultipleNames($packages) ? 'these do' : 'it does').' not match your minimum-stability.');
}
// check if the package is found when bypassing the constraint and stability checks
if ($packages = $repositorySet->findPackages($packageName, null, RepositorySet::ALLOW_UNACCEPTABLE_STABILITIES)) {
// we must first verify if a valid package would be found in a lower priority repository
if ($allReposPackages = $repositorySet->findPackages($packageName, $constraint, RepositorySet::ALLOW_SHADOWED_REPOSITORIES)) {
return self::computeCheckForLowerPrioRepo($isVerbose, $packageName, $packages, $allReposPackages, 'constraint', $constraint);
return self::computeCheckForLowerPrioRepo($pool, $isVerbose, $packageName, $packages, $allReposPackages, 'constraint', $constraint);
}
$suffix = '';
@ -326,7 +330,7 @@ class Problem
$suffix = ' See https://getcomposer.org/dep-on-root for details and assistance.';
}
return array("- Root composer.json requires $packageName".self::constraintToText($constraint) . ', ', 'found '.self::getPackageList($packages, $isVerbose).' but '.(self::hasMultipleNames($packages) ? 'these do' : 'it does').' not match the constraint.' . $suffix);
return array("- Root composer.json requires $packageName".self::constraintToText($constraint) . ', ', 'found '.self::getPackageList($packages, $isVerbose, $pool, $constraint).' but '.(self::hasMultipleNames($packages) ? 'these do' : 'it does').' not match the constraint.' . $suffix);
}
if (!preg_match('{^[A-Za-z0-9_./-]+$}', $packageName)) {
@ -356,15 +360,26 @@ class Problem
* @internal
* @param PackageInterface[] $packages
* @param bool $isVerbose
* @param bool $useRemovedVersionGroup
* @return string
*/
public static function getPackageList(array $packages, $isVerbose)
public static function getPackageList(array $packages, $isVerbose, Pool $pool = null, ConstraintInterface $constraint = null, $useRemovedVersionGroup = false)
{
$prepared = array();
$hasDefaultBranch = array();
foreach ($packages as $package) {
$prepared[$package->getName()]['name'] = $package->getPrettyName();
$prepared[$package->getName()]['versions'][$package->getVersion()] = $package->getPrettyVersion().($package instanceof AliasPackage ? ' (alias of '.$package->getAliasOf()->getPrettyVersion().')' : '');
if ($pool && $constraint) {
foreach ($pool->getRemovedVersions($package->getName(), $constraint) as $version => $prettyVersion) {
$prepared[$package->getName()]['versions'][$version] = $prettyVersion;
}
}
if ($pool && $useRemovedVersionGroup) {
foreach ($pool->getRemovedVersionsByPackage(spl_object_hash($package)) as $version => $prettyVersion) {
$prepared[$package->getName()]['versions'][$version] = $prettyVersion;
}
}
if ($package->isDefaultBranch()) {
$hasDefaultBranch[$package->getName()] = true;
}
@ -469,7 +484,7 @@ class Problem
* @param string $reason
* @return array{0: string, 1: string}
*/
private static function computeCheckForLowerPrioRepo($isVerbose, $packageName, array $higherRepoPackages, array $allReposPackages, $reason, ConstraintInterface $constraint = null)
private static function computeCheckForLowerPrioRepo(Pool $pool, $isVerbose, $packageName, array $higherRepoPackages, array $allReposPackages, $reason, ConstraintInterface $constraint = null)
{
$nextRepoPackages = array();
$nextRepo = null;
@ -488,7 +503,7 @@ class Problem
if ($topPackage instanceof RootPackageInterface) {
return array(
"- Root composer.json requires $packageName".self::constraintToText($constraint).', it is ',
'satisfiable by '.self::getPackageList($nextRepoPackages, $isVerbose).' from '.$nextRepo->getRepoName().' but '.$topPackage->getPrettyName().' is the root package and cannot be modified. See https://getcomposer.org/dep-on-root for details and assistance.',
'satisfiable by '.self::getPackageList($nextRepoPackages, $isVerbose, $pool, $constraint).' from '.$nextRepo->getRepoName().' but '.$topPackage->getPrettyName().' is the root package and cannot be modified. See https://getcomposer.org/dep-on-root for details and assistance.',
);
}
}
@ -497,10 +512,10 @@ class Problem
$singular = count($higherRepoPackages) === 1;
return array("- Root composer.json requires $packageName".self::constraintToText($constraint) . ', it is ',
'found '.self::getPackageList($nextRepoPackages, $isVerbose).' in the lock file and '.self::getPackageList($higherRepoPackages, $isVerbose).' from '.reset($higherRepoPackages)->getRepository()->getRepoName().' but ' . ($singular ? 'it does' : 'these do') . ' not match your '.$reason.' and ' . ($singular ? 'is' : 'are') . ' therefore not installable. Make sure you either fix the '.$reason.' or avoid updating this package to keep the one from the lock file.', );
'found '.self::getPackageList($nextRepoPackages, $isVerbose, $pool, $constraint).' in the lock file and '.self::getPackageList($higherRepoPackages, $isVerbose, $pool, $constraint).' from '.reset($higherRepoPackages)->getRepository()->getRepoName().' but ' . ($singular ? 'it does' : 'these do') . ' not match your '.$reason.' and ' . ($singular ? 'is' : 'are') . ' therefore not installable. Make sure you either fix the '.$reason.' or avoid updating this package to keep the one from the lock file.', );
}
return array("- Root composer.json requires $packageName".self::constraintToText($constraint) . ', it is ', 'satisfiable by '.self::getPackageList($nextRepoPackages, $isVerbose).' from '.$nextRepo->getRepoName().' but '.self::getPackageList($higherRepoPackages, $isVerbose).' from '.reset($higherRepoPackages)->getRepository()->getRepoName().' has higher repository priority. The packages with higher priority do not match your '.$reason.' and are therefore not installable. See https://getcomposer.org/repoprio for details and assistance.');
return array("- Root composer.json requires $packageName".self::constraintToText($constraint) . ', it is ', 'satisfiable by '.self::getPackageList($nextRepoPackages, $isVerbose, $pool, $constraint).' from '.$nextRepo->getRepoName().' but '.self::getPackageList($higherRepoPackages, $isVerbose, $pool, $constraint).' from '.reset($higherRepoPackages)->getRepository()->getRepoName().' has higher repository priority. The packages with higher priority do not match your '.$reason.' and are therefore not installable. See https://getcomposer.org/repoprio for details and assistance.');
}
/**

@ -228,6 +228,41 @@ abstract class Rule
return false;
}
/**
* @internal
* @return BasePackage
*/
public function getSourcePackage(Pool $pool)
{
$literals = $this->getLiterals();
switch ($this->getReason()) {
case self::RULE_PACKAGE_CONFLICT:
$package1 = $this->deduplicateDefaultBranchAlias($pool->literalToPackage($literals[0]));
$package2 = $this->deduplicateDefaultBranchAlias($pool->literalToPackage($literals[1]));
$conflictTarget = $package1->getPrettyString();
if ($reasonData = $this->getReasonData()) {
// swap literals if they are not in the right order with package2 being the conflicter
if ($reasonData->getSource() === $package1->getName()) {
list($package2, $package1) = array($package1, $package2);
}
}
return $package2;
case self::RULE_PACKAGE_REQUIRES:
$sourceLiteral = array_shift($literals);
$sourcePackage = $this->deduplicateDefaultBranchAlias($pool->literalToPackage($sourceLiteral));
return $sourcePackage;
default:
throw new \LogicException('Not implemented');
}
}
/**
* @param bool $isVerbose
* @param BasePackage[] $installedMap
@ -258,7 +293,7 @@ abstract class Rule
}
}
return 'Root composer.json requires '.$packageName.($constraint ? ' '.$constraint->getPrettyString() : '').' -> satisfiable by '.$this->formatPackagesUnique($pool, $packages, $isVerbose).'.';
return 'Root composer.json requires '.$packageName.($constraint ? ' '.$constraint->getPrettyString() : '').' -> satisfiable by '.$this->formatPackagesUnique($pool, $packages, $isVerbose, $constraint).'.';
case self::RULE_FIXED:
$package = $this->deduplicateDefaultBranchAlias($this->reasonData['package']);
@ -320,7 +355,7 @@ abstract class Rule
$text = $reasonData->getPrettyString($sourcePackage);
if ($requires) {
$text .= ' -> satisfiable by ' . $this->formatPackagesUnique($pool, $requires, $isVerbose) . '.';
$text .= ' -> satisfiable by ' . $this->formatPackagesUnique($pool, $requires, $isVerbose, $this->reasonData->getConstraint()) . '.';
} else {
$targetName = $reasonData->getTarget();
@ -368,13 +403,13 @@ abstract class Rule
}
if ($installedPackages && $removablePackages) {
return $this->formatPackagesUnique($pool, $removablePackages, $isVerbose).' cannot be installed as that would require removing '.$this->formatPackagesUnique($pool, $installedPackages, $isVerbose).'. '.$reason;
return $this->formatPackagesUnique($pool, $removablePackages, $isVerbose, null, true).' cannot be installed as that would require removing '.$this->formatPackagesUnique($pool, $installedPackages, $isVerbose, null, true).'. '.$reason;
}
return 'Only one of these can be installed: '.$this->formatPackagesUnique($pool, $literals, $isVerbose).'. '.$reason;
return 'Only one of these can be installed: '.$this->formatPackagesUnique($pool, $literals, $isVerbose, null, true).'. '.$reason;
}
return 'You can only install one version of a package, so only one of these can be installed: ' . $this->formatPackagesUnique($pool, $literals, $isVerbose) . '.';
return 'You can only install one version of a package, so only one of these can be installed: ' . $this->formatPackagesUnique($pool, $literals, $isVerbose, null, true) . '.';
case self::RULE_LEARNED:
/** @TODO currently still generates way too much output to be helpful, and in some cases can even lead to endless recursion */
// if (isset($learnedPool[$this->reasonData])) {
@ -445,9 +480,10 @@ abstract class Rule
/**
* @param array<int|BasePackage> $packages An array containing packages or literals
* @param bool $isVerbose
* @param bool $useRemovedVersionGroup
* @return string
*/
protected function formatPackagesUnique(Pool $pool, array $packages, $isVerbose)
protected function formatPackagesUnique(Pool $pool, array $packages, $isVerbose, ConstraintInterface $constraint = null, $useRemovedVersionGroup = false)
{
foreach ($packages as $index => $package) {
if (!\is_object($package)) {
@ -455,7 +491,7 @@ abstract class Rule
}
}
return Problem::getPackageList($packages, $isVerbose);
return Problem::getPackageList($packages, $isVerbose, $pool, $constraint, $useRemovedVersionGroup);
}
/**

@ -20,6 +20,7 @@ use Composer\DependencyResolver\LockTransaction;
use Composer\DependencyResolver\Operation\UpdateOperation;
use Composer\DependencyResolver\Operation\InstallOperation;
use Composer\DependencyResolver\Operation\UninstallOperation;
use Composer\DependencyResolver\PoolOptimizer;
use Composer\DependencyResolver\Pool;
use Composer\DependencyResolver\Request;
use Composer\DependencyResolver\Solver;
@ -430,34 +431,97 @@ class Installer
$request->setUpdateAllowList($this->updateAllowList, $this->updateAllowTransitiveDependencies);
}
$pool = $repositorySet->createPool($request, $this->io, $this->eventDispatcher);
if (Platform::getEnv('COMPOSER_TESTS_ARE_RUNNING') || '2' === Platform::getEnv('COMPOSER_POOL_OPTIMIZER')) {
$pool = $repositorySet->createPool($request, $this->io, $this->eventDispatcher, $this->createPoolOptimizer($policy));
} else {
$pool = $repositorySet->createPool($request, $this->io, $this->eventDispatcher/*, $this->createPoolOptimizer($policy)*/);
}
$this->io->writeError('<info>Updating dependencies</info>');
// solve dependencies
$solver = new Solver($policy, $pool, $this->io);
try {
$lockTransaction = $solver->solve($request, $this->platformRequirementFilter);
$ruleSetSize = $solver->getRuleSetSize();
$solver = null;
} catch (SolverProblemsException $e) {
$err = 'Your requirements could not be resolved to an installable set of packages.';
$prettyProblem = $e->getPrettyString($repositorySet, $request, $pool, $this->io->isVerbose());
if (!Platform::getEnv('COMPOSER_TESTS_ARE_RUNNING') && '1' === Platform::getEnv('COMPOSER_POOL_OPTIMIZER')) {
try {
$this->io->writeError("<highlight>Updating dependencies with default package pool</highlight>", true, IOInterface::VERBOSE);
$lockTransaction = $solver->solve($request, $this->platformRequirementFilter);
$ruleSetSize = $solver->getRuleSetSize();
$this->io->writeError("Analyzed ".count($pool)." packages to resolve dependencies", true, IOInterface::VERBOSE);
$this->io->writeError("Analyzed ".$ruleSetSize." rules to resolve dependencies", true, IOInterface::VERBOSE);
$this->io->writeError("<highlight>Updating dependencies with optimized package pool</highlight>", true, IOInterface::VERBOSE);
$pool2 = $repositorySet->createPool($request, $this->io, $this->eventDispatcher, $this->createPoolOptimizer($policy));
$solver2 = new Solver($policy, $pool2, $this->io);
$lockTransaction2 = $solver2->solve($request, $this->platformRequirementFilter);
$ruleSetSize2 = $solver2->getRuleSetSize();
$this->io->writeError("Analyzed ".count($pool2)." packages to resolve dependencies", true, IOInterface::VERBOSE);
$this->io->writeError("Analyzed ".$ruleSetSize2." rules to resolve dependencies", true, IOInterface::VERBOSE);
$solver = $solver2 = null;
$pool = $pool2 = null;
} catch (SolverProblemsException $e) {
$err = 'Your requirements could not be resolved to an installable set of packages.';
$prettyProblem = $e->getPrettyString($repositorySet, $request, $pool, $this->io->isVerbose());
$this->io->writeError('<error>'. $err .'</error>', true, IOInterface::QUIET);
$this->io->writeError($prettyProblem);
if (!$this->devMode) {
$this->io->writeError('<warning>Running update with --no-dev does not mean require-dev is ignored, it just means the packages will not be installed. If dev requirements are blocking the update you have to resolve those problems.</warning>', true, IOInterface::QUIET);
if (isset($pool2)) {
throw new \LogicException('Optimized solver failed but non-optimized one did not fail, please report this with your composer.json');
} else {
try {
$pool2 = $repositorySet->createPool($request, $this->io, $this->eventDispatcher, $this->createPoolOptimizer($policy));
$solver2 = new Solver($policy, $pool2, $this->io);
$lockTransaction2 = $solver2->solve($request, $this->platformRequirementFilter);
throw new \LogicException('Optimized solver worked but non-optimized one failed resolving, please report this with your composer.json');
} catch (SolverProblemsException $e2) {
}
}
$this->io->writeError('<error>'. $err .'</error>', true, IOInterface::QUIET);
$this->io->writeError($prettyProblem);
if (!$this->devMode) {
$this->io->writeError('<warning>Running update with --no-dev does not mean require-dev is ignored, it just means the packages will not be installed. If dev requirements are blocking the update you have to resolve those problems.</warning>', true, IOInterface::QUIET);
}
$ghe = new GithubActionError($this->io);
$ghe->emit($err."\n".$prettyProblem);
return max(self::ERROR_GENERIC_FAILURE, $e->getCode());
}
$ghe = new GithubActionError($this->io);
$ghe->emit($err."\n".$prettyProblem);
$txLogOptimized = array_map(function ($op) {
return (string) $op;
}, $lockTransaction2->getOperations());
$txLogRaw = array_map(function ($op) {
return (string) $op;
}, $lockTransaction->getOperations());
if ($txLogOptimized !== $txLogRaw) {
throw new \LogicException('Optimized solver resolved differently from non-optimized one, please report this with your composer.json'.PHP_EOL.implode(PHP_EOL,$txLogOptimized).implode(PHP_EOL,$txLogRaw));
}
$this->io->writeError("<highlight>Done, test successful</highlight>", true, IOInterface::VERBOSE);
} else {
try {
$lockTransaction = $solver->solve($request, $this->platformRequirementFilter);
$ruleSetSize = $solver->getRuleSetSize();
$solver = null;
} catch (SolverProblemsException $e) {
$err = 'Your requirements could not be resolved to an installable set of packages.';
$prettyProblem = $e->getPrettyString($repositorySet, $request, $pool, $this->io->isVerbose());
return max(self::ERROR_GENERIC_FAILURE, $e->getCode());
}
$this->io->writeError('<error>'. $err .'</error>', true, IOInterface::QUIET);
$this->io->writeError($prettyProblem);
if (!$this->devMode) {
$this->io->writeError('<warning>Running update with --no-dev does not mean require-dev is ignored, it just means the packages will not be installed. If dev requirements are blocking the update you have to resolve those problems.</warning>', true, IOInterface::QUIET);
}
$this->io->writeError("Analyzed ".count($pool)." packages to resolve dependencies", true, IOInterface::VERBOSE);
$this->io->writeError("Analyzed ".$ruleSetSize." rules to resolve dependencies", true, IOInterface::VERBOSE);
$ghe = new GithubActionError($this->io);
$ghe->emit($err."\n".$prettyProblem);
return max(self::ERROR_GENERIC_FAILURE, $e->getCode());
}
$this->io->writeError("Analyzed ".count($pool)." packages to resolve dependencies", true, IOInterface::VERBOSE);
$this->io->writeError("Analyzed ".$ruleSetSize." rules to resolve dependencies", true, IOInterface::VERBOSE);
$pool = null;
}
if (!$lockTransaction->getOperations()) {
$this->io->writeError('Nothing to modify in lock file');
@ -999,6 +1063,23 @@ class Installer
);
}
/**
* @return PoolOptimizer|null
*/
private function createPoolOptimizer(PolicyInterface $policy)
{
// Not the best architectural decision here, would need to be able
// to configure from the outside of Installer but this is only
// a debugging tool and should never be required in any other use case
if ('0' === Platform::getEnv('COMPOSER_POOL_OPTIMIZER')) {
$this->io->write('Pool Optimizer was disabled for debugging purposes.', true, IOInterface::DEBUG);
return null;
}
return new PoolOptimizer($policy);
}
/**
* Create Installer
*

@ -12,6 +12,8 @@
namespace Composer\Repository;
use Composer\DependencyResolver\PoolOptimizer;
use Composer\DependencyResolver\PolicyInterface;
use Composer\DependencyResolver\Pool;
use Composer\DependencyResolver\PoolBuilder;
use Composer\DependencyResolver\Request;
@ -244,9 +246,9 @@ class RepositorySet
*
* @return Pool
*/
public function createPool(Request $request, IOInterface $io, EventDispatcher $eventDispatcher = null)
public function createPool(Request $request, IOInterface $io, EventDispatcher $eventDispatcher = null, PoolOptimizer $poolOptimizer = null)
{
$poolBuilder = new PoolBuilder($this->acceptableStabilities, $this->stabilityFlags, $this->rootAliases, $this->rootReferences, $io, $eventDispatcher);
$poolBuilder = new PoolBuilder($this->acceptableStabilities, $this->stabilityFlags, $this->rootAliases, $this->rootReferences, $io, $eventDispatcher, $poolOptimizer);
foreach ($this->repositories as $repo) {
if (($repo instanceof InstalledRepositoryInterface || $repo instanceof InstalledRepository) && !$this->allowInstalledRepositories) {

@ -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"
]

@ -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"
]

@ -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"
]

@ -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"
]

@ -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)"
]

@ -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"
}
}
]

@ -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;

@ -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)

@ -11,15 +11,15 @@ Test the error output of solver problems is deduplicated.
{ "name": "package/a", "version": "2.0.2", "require": { "missing/dep": "^1.0" } },
{ "name": "package/a", "version": "2.0.3", "require": { "missing/dep": "^1.0" } },
{ "name": "package/a", "version": "2.1.0", "require": { "missing/dep": "^1.0" } },
{ "name": "package/a", "version": "2.2.0", "require": { "missing/dep": "^1.0" } },
{ "name": "package/a", "version": "2.3.1", "require": { "missing/dep": "^1.0" } },
{ "name": "package/a", "version": "2.3.2", "require": { "missing/dep": "^1.0" } },
{ "name": "package/a", "version": "2.3.3", "require": { "missing/dep": "^1.0" } },
{ "name": "package/a", "version": "2.3.4", "require": { "missing/dep": "^1.0" } },
{ "name": "package/a", "version": "2.3.5", "require": { "missing/dep": "^1.0" } },
{ "name": "package/a", "version": "2.4.0", "require": { "missing/dep": "^1.0" } },
{ "name": "package/a", "version": "2.5.0", "require": { "missing/dep": "^1.0" } },
{ "name": "package/a", "version": "2.6.0", "require": { "missing/dep": "^1.0" } },
{ "name": "package/a", "version": "2.2.0", "require": { "missing/dep": "^1.1" } },
{ "name": "package/a", "version": "2.3.1", "require": { "missing/dep": "^1.1" } },
{ "name": "package/a", "version": "2.3.2", "require": { "missing/dep": "^1.1" } },
{ "name": "package/a", "version": "2.3.3", "require": { "missing/dep": "^1.1" } },
{ "name": "package/a", "version": "2.3.4", "require": { "missing/dep": "^1.1" } },
{ "name": "package/a", "version": "2.3.5", "require": { "missing/dep": "^1.1" } },
{ "name": "package/a", "version": "2.4.0", "require": { "missing/dep": "^1.1" } },
{ "name": "package/a", "version": "2.5.0", "require": { "missing/dep": "^1.1" } },
{ "name": "package/a", "version": "2.6.0", "require": { "missing/dep": "^1.1" } },
{ "name": "missing/dep", "version": "2.0.0" }
]
}
@ -41,7 +41,8 @@ Updating dependencies
Your requirements could not be resolved to an installable set of packages.
Problem 1
- package/a[2.0.0, ..., 2.6.0] require missing/dep ^1.0 -> found missing/dep[2.0.0] but it does not match the constraint.
- package/a[2.2.0, ..., 2.6.0] require missing/dep ^1.1 -> found missing/dep[2.0.0] but it does not match the constraint.
- package/a[2.0.0, ..., 2.1.0] require missing/dep ^1.0 -> found missing/dep[2.0.0] but it does not match the constraint.
- Root composer.json requires package/a * -> satisfiable by package/a[2.0.0, ..., 2.6.0].
--EXPECT--

@ -128,6 +128,21 @@ Your requirements could not be resolved to an installable set of packages.
- You can only install one version of a package, so only one of these can be installed: symfony/console[v2.8.7, v2.8.8, v3.1.9, ..., v3.4.29].
- illuminate/console v5.2.25 requires symfony/console 3.1.* -> satisfiable by symfony/console[v3.1.9, v3.1.10].
- Conclusion: don't install symfony/console v3.1.10 (conflict analysis result)
--EXPECT-OUTPUT-OPTIMIZED--
Loading composer repositories with package information
Updating dependencies
Your requirements could not be resolved to an installable set of packages.
Problem 1
- Root composer.json requires illuminate/queue * -> satisfiable by illuminate/queue[v5.2.0].
- illuminate/queue v5.2.0 requires illuminate/console 5.2.* -> satisfiable by illuminate/console[v5.2.25, v5.2.26].
- illuminate/console v5.2.25 requires symfony/console 3.1.* -> satisfiable by symfony/console[v3.1.9, v3.1.10].
- illuminate/console v5.2.26 requires symfony/console 2.8.* -> satisfiable by symfony/console[v2.8.7, v2.8.8].
- You can only install one version of a package, so only one of these can be installed: symfony/console[v2.8.7, v2.8.8, v3.1.9, ..., v3.4.29].
- friendsofphp/php-cs-fixer[v2.10.4, ..., v2.10.5] require symfony/console ^3.2 || ^4.0 -> satisfiable by symfony/console[v3.2.13, ..., v3.4.29].
- Root composer.json requires friendsofphp/php-cs-fixer * -> satisfiable by friendsofphp/php-cs-fixer[v2.10.4, v2.10.5].
--EXPECT--
--EXPECT-EXIT-CODE--

@ -44,5 +44,15 @@ Your requirements could not be resolved to an installable set of packages.
- Root composer.json requires regular/pkg 1.* -> satisfiable by regular/pkg[1.0.0, 1.0.1, 1.0.2, 1.0.3].
- Root composer.json requires replacer/pkg 2.* -> satisfiable by replacer/pkg[2.0.0, 2.0.1, 2.0.2, 2.0.3].
--EXPECT-OUTPUT-OPTIMIZED--
Loading composer repositories with package information
Updating dependencies
Your requirements could not be resolved to an installable set of packages.
Problem 1
- Only one of these can be installed: regular/pkg[1.0.0, 1.0.1, 1.0.2, 1.0.3], replacer/pkg[2.0.0, 2.0.1, 2.0.2, 2.0.3]. replacer/pkg replaces regular/pkg and thus cannot coexist with it.
- Root composer.json requires regular/pkg 1.* -> satisfiable by regular/pkg[1.0.0, 1.0.1, 1.0.2, 1.0.3].
- Root composer.json requires replacer/pkg 2.* -> satisfiable by replacer/pkg[2.0.0, 2.0.1, 2.0.2, 2.0.3].
--EXPECT--

@ -36,6 +36,7 @@ use Composer\Package\Locker;
use Composer\Test\Mock\FactoryMock;
use Composer\Test\Mock\InstalledFilesystemRepositoryMock;
use Composer\Test\Mock\InstallationManagerMock;
use Composer\Util\Platform;
use Symfony\Component\Console\Input\StringInput;
use Symfony\Component\Console\Output\StreamOutput;
use Symfony\Component\Console\Output\OutputInterface;
@ -56,6 +57,8 @@ class InstallerTest extends TestCase
public function tearDown()
{
Platform::clearEnv('COMPOSER_POOL_OPTIMIZER');
chdir($this->prevCwd);
if (isset($this->tempComposerHome) && is_dir($this->tempComposerHome)) {
$fs = new Filesystem;
@ -228,16 +231,64 @@ class InstallerTest extends TestCase
* @param mixed[]|false $expectLock
* @param ?mixed[] $expectInstalled
* @param ?string $expectOutput
* @param ?string $expectOutputOptimized
* @param string $expect
* @param int|string $expectResult
*/
public function testSlowIntegration($file, $message, $condition, $composerConfig, $lock, $installed, $run, $expectLock, $expectInstalled, $expectOutput, $expect, $expectResult)
public function testSlowIntegration($file, $message, $condition, $composerConfig, $lock, $installed, $run, $expectLock, $expectInstalled, $expectOutput, $expectOutputOptimized, $expect, $expectResult)
{
return $this->testIntegration($file, $message, $condition, $composerConfig, $lock, $installed, $run, $expectLock, $expectInstalled, $expectOutput, $expect, $expectResult);
Platform::putEnv('COMPOSER_POOL_OPTIMIZER', '0');
$this->doTestIntegration($file, $message, $condition, $composerConfig, $lock, $installed, $run, $expectLock, $expectInstalled, $expectOutput, $expect, $expectResult);
}
/**
* @dataProvider provideIntegrationTests
* @param string $file
* @param string $message
* @param ?string $condition
* @param Config $composerConfig
* @param ?mixed[] $lock
* @param ?mixed[] $installed
* @param string $run
* @param mixed[]|false $expectLock
* @param ?mixed[] $expectInstalled
* @param ?string $expectOutput
* @param ?string $expectOutputOptimized
* @param string $expect
* @param int|string $expectResult
*/
public function testIntegrationWithPoolOptimizer($file, $message, $condition, $composerConfig, $lock, $installed, $run, $expectLock, $expectInstalled, $expectOutput, $expectOutputOptimized, $expect, $expectResult)
{
Platform::putEnv('COMPOSER_POOL_OPTIMIZER', '1');
$this->doTestIntegration($file, $message, $condition, $composerConfig, $lock, $installed, $run, $expectLock, $expectInstalled, $expectOutputOptimized ?: $expectOutput, $expect, $expectResult);
}
/**
* @dataProvider provideIntegrationTests
* @param string $file
* @param string $message
* @param ?string $condition
* @param Config $composerConfig
* @param ?mixed[] $lock
* @param ?mixed[] $installed
* @param string $run
* @param mixed[]|false $expectLock
* @param ?mixed[] $expectInstalled
* @param ?string $expectOutput
* @param ?string $expectOutputOptimized
* @param string $expect
* @param int|string $expectResult
*/
public function testIntegrationWithRawPool($file, $message, $condition, $composerConfig, $lock, $installed, $run, $expectLock, $expectInstalled, $expectOutput, $expectOutputOptimized, $expect, $expectResult)
{
Platform::putEnv('COMPOSER_POOL_OPTIMIZER', '0');
$this->doTestIntegration($file, $message, $condition, $composerConfig, $lock, $installed, $run, $expectLock, $expectInstalled, $expectOutput, $expect, $expectResult);
}
/**
* @param string $file
* @param string $message
* @param ?string $condition
@ -250,8 +301,9 @@ class InstallerTest extends TestCase
* @param ?string $expectOutput
* @param string $expect
* @param int|string $expectResult
* @return void
*/
public function testIntegration($file, $message, $condition, $composerConfig, $lock, $installed, $run, $expectLock, $expectInstalled, $expectOutput, $expect, $expectResult)
private function doTestIntegration($file, $message, $condition, $composerConfig, $lock, $installed, $run, $expectLock, $expectInstalled, $expectOutput, $expect, $expectResult)
{
if ($condition) {
eval('$res = '.$condition.';');
@ -518,6 +570,7 @@ class InstallerTest extends TestCase
$expectInstalled = JsonFile::parseJson($testData['EXPECT-INSTALLED']);
}
$expectOutput = isset($testData['EXPECT-OUTPUT']) ? $testData['EXPECT-OUTPUT'] : null;
$expectOutputOptimized = isset($testData['EXPECT-OUTPUT-OPTIMIZED']) ? $testData['EXPECT-OUTPUT-OPTIMIZED'] : null;
$expect = $testData['EXPECT'];
if (!empty($testData['EXPECT-EXCEPTION'])) {
$expectResult = $testData['EXPECT-EXCEPTION'];
@ -533,7 +586,7 @@ class InstallerTest extends TestCase
die(sprintf('Test "%s" is not valid: '.$e->getMessage(), str_replace($fixturesDir.'/', '', $file)));
}
$tests[basename($file)] = array(str_replace($fixturesDir.'/', '', $file), $message, $condition, $composer, $lock, $installed, $run, $expectLock, $expectInstalled, $expectOutput, $expect, $expectResult);
$tests[basename($file)] = array(str_replace($fixturesDir.'/', '', $file), $message, $condition, $composer, $lock, $installed, $run, $expectLock, $expectInstalled, $expectOutput, $expectOutputOptimized, $expect, $expectResult);
}
return $tests;
@ -557,6 +610,7 @@ class InstallerTest extends TestCase
'EXPECT-LOCK' => false,
'EXPECT-INSTALLED' => false,
'EXPECT-OUTPUT' => false,
'EXPECT-OUTPUT-OPTIMIZED' => false,
'EXPECT-EXIT-CODE' => false,
'EXPECT-EXCEPTION' => false,
'EXPECT' => true,

@ -10,6 +10,8 @@
* file that was distributed with this source code.
*/
use Composer\Util\Platform;
error_reporting(E_ALL);
if (function_exists('date_default_timezone_set') && function_exists('date_default_timezone_get')) {
@ -19,3 +21,5 @@ if (function_exists('date_default_timezone_set') && function_exists('date_defaul
require __DIR__.'/../src/bootstrap.php';
require __DIR__.'/../src/Composer/InstalledVersions.php';
require __DIR__.'/Composer/Test/TestCase.php';
Platform::putEnv('COMPOSER_TESTS_ARE_RUNNING', '1');

@ -75,13 +75,18 @@
"count": 1
},
{
"location": "Composer\\Test\\InstallerTest::testIntegration",
"location": "Composer\\Test\\InstallerTest::testIntegrationWithRawPool",
"message": "preg_match(): Passing null to parameter #4 ($flags) of type int is deprecated",
"count": 1640
"count": 1728
},
{
"location": "Composer\\Test\\InstallerTest::testIntegrationWithPoolOptimizer",
"message": "preg_match(): Passing null to parameter #4 ($flags) of type int is deprecated",
"count": 1728
},
{
"location": "Composer\\Test\\Package\\Archiver\\ArchivableFilesFinderTest::testManualExcludes",
"message": "Return type of Symfony\\Component\\Finder\\Iterator\\CustomFilterIterator::accept() should either be compatible with FilterIterator::accept(): bool, or the #[\\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice",
"count": 1
}
]
]

Loading…
Cancel
Save