1
0
Fork 0

Implemented PoolOptimizer

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

View File

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

View File

@ -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 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 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`. 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.

View File

@ -221,6 +221,14 @@ class Pool implements \Countable
return \in_array($package, $this->unacceptableFixedOrLockedPackages, true); return \in_array($package, $this->unacceptableFixedOrLockedPackages, true);
} }
/**
* @return BasePackage[]
*/
public function getUnacceptableFixedOrLockedPackages()
{
return $this->unacceptableFixedOrLockedPackages;
}
public function __toString() public function __toString()
{ {
$str = "Pool:\n"; $str = "Pool:\n";

View File

@ -61,6 +61,10 @@ class PoolBuilder
* @var ?EventDispatcher * @var ?EventDispatcher
*/ */
private $eventDispatcher; private $eventDispatcher;
/**
* @var PoolOptimizer|null
*/
private $poolOptimizer;
/** /**
* @var IOInterface * @var IOInterface
*/ */
@ -128,13 +132,14 @@ class PoolBuilder
* @param string[] $rootReferences an array of package name => source reference * @param string[] $rootReferences an array of package name => source reference
* @phpstan-param array<string, string> $rootReferences * @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->acceptableStabilities = $acceptableStabilities;
$this->stabilityFlags = $stabilityFlags; $this->stabilityFlags = $stabilityFlags;
$this->rootAliases = $rootAliases; $this->rootAliases = $rootAliases;
$this->rootReferences = $rootReferences; $this->rootReferences = $rootReferences;
$this->eventDispatcher = $eventDispatcher; $this->eventDispatcher = $eventDispatcher;
$this->poolOptimizer = $poolOptimizer;
$this->io = $io; $this->io = $io;
} }
@ -259,6 +264,8 @@ class PoolBuilder
$this->skippedLoad = array(); $this->skippedLoad = array();
$this->indexCounter = 0; $this->indexCounter = 0;
$pool = $this->runOptimizer($request, $pool);
Intervals::clear(); Intervals::clear();
return $pool; return $pool;
@ -572,4 +579,33 @@ class PoolBuilder
unset($this->aliasMap[spl_object_hash($package)]); 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;
}
} }

View File

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

View File

@ -20,6 +20,7 @@ use Composer\DependencyResolver\LockTransaction;
use Composer\DependencyResolver\Operation\UpdateOperation; use Composer\DependencyResolver\Operation\UpdateOperation;
use Composer\DependencyResolver\Operation\InstallOperation; use Composer\DependencyResolver\Operation\InstallOperation;
use Composer\DependencyResolver\Operation\UninstallOperation; use Composer\DependencyResolver\Operation\UninstallOperation;
use Composer\DependencyResolver\PoolOptimizer;
use Composer\DependencyResolver\Pool; use Composer\DependencyResolver\Pool;
use Composer\DependencyResolver\Request; use Composer\DependencyResolver\Request;
use Composer\DependencyResolver\Solver; use Composer\DependencyResolver\Solver;
@ -430,7 +431,7 @@ class Installer
$request->setUpdateAllowList($this->updateAllowList, $this->updateAllowTransitiveDependencies); $request->setUpdateAllowList($this->updateAllowList, $this->updateAllowTransitiveDependencies);
} }
$pool = $repositorySet->createPool($request, $this->io, $this->eventDispatcher); $pool = $repositorySet->createPool($request, $this->io, $this->eventDispatcher, $this->createPoolOptimizer($policy));
$this->io->writeError('<info>Updating dependencies</info>'); $this->io->writeError('<info>Updating dependencies</info>');
@ -999,6 +1000,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 * Create Installer
* *

View File

@ -12,6 +12,8 @@
namespace Composer\Repository; namespace Composer\Repository;
use Composer\DependencyResolver\PoolOptimizer;
use Composer\DependencyResolver\PolicyInterface;
use Composer\DependencyResolver\Pool; use Composer\DependencyResolver\Pool;
use Composer\DependencyResolver\PoolBuilder; use Composer\DependencyResolver\PoolBuilder;
use Composer\DependencyResolver\Request; use Composer\DependencyResolver\Request;
@ -244,9 +246,9 @@ class RepositorySet
* *
* @return Pool * @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) { foreach ($this->repositories as $repo) {
if (($repo instanceof InstalledRepositoryInterface || $repo instanceof InstalledRepository) && !$this->allowInstalledRepositories) { if (($repo instanceof InstalledRepositoryInterface || $repo instanceof InstalledRepository) && !$this->allowInstalledRepositories) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -44,5 +44,14 @@ Your requirements could not be resolved to an installable set of packages.
- 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.0.0, ..., 2.6.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]. - Root composer.json requires package/a * -> satisfiable by package/a[2.0.0, ..., 2.6.0].
--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 package/a * -> satisfiable by package/a[2.6.0].
- package/a 2.6.0 requires missing/dep ^1.0 -> found missing/dep[2.0.0] but it does not match the constraint.
--EXPECT-- --EXPECT--

View File

@ -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]. - 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]. - 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) - 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.10].
- illuminate/console v5.2.26 requires symfony/console 2.8.* -> satisfiable by symfony/console[v2.8.8].
- You can only install one version of a package, so only one of these can be installed: symfony/console[v2.8.8, v3.1.10, v3.4.29].
- friendsofphp/php-cs-fixer v2.10.5 requires symfony/console ^3.2 || ^4.0 -> satisfiable by symfony/console[v3.4.29].
- Root composer.json requires friendsofphp/php-cs-fixer * -> satisfiable by friendsofphp/php-cs-fixer[v2.10.5].
--EXPECT-- --EXPECT--
--EXPECT-EXIT-CODE-- --EXPECT-EXIT-CODE--

View File

@ -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 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]. - 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.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.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-- --EXPECT--

View File

@ -36,6 +36,7 @@ use Composer\Package\Locker;
use Composer\Test\Mock\FactoryMock; use Composer\Test\Mock\FactoryMock;
use Composer\Test\Mock\InstalledFilesystemRepositoryMock; use Composer\Test\Mock\InstalledFilesystemRepositoryMock;
use Composer\Test\Mock\InstallationManagerMock; use Composer\Test\Mock\InstallationManagerMock;
use Composer\Util\Platform;
use Symfony\Component\Console\Input\StringInput; use Symfony\Component\Console\Input\StringInput;
use Symfony\Component\Console\Output\StreamOutput; use Symfony\Component\Console\Output\StreamOutput;
use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\OutputInterface;
@ -56,6 +57,8 @@ class InstallerTest extends TestCase
public function tearDown() public function tearDown()
{ {
Platform::clearEnv('COMPOSER_POOL_OPTIMIZER');
chdir($this->prevCwd); chdir($this->prevCwd);
if (isset($this->tempComposerHome) && is_dir($this->tempComposerHome)) { if (isset($this->tempComposerHome) && is_dir($this->tempComposerHome)) {
$fs = new Filesystem; $fs = new Filesystem;
@ -228,12 +231,15 @@ class InstallerTest extends TestCase
* @param mixed[]|false $expectLock * @param mixed[]|false $expectLock
* @param ?mixed[] $expectInstalled * @param ?mixed[] $expectInstalled
* @param ?string $expectOutput * @param ?string $expectOutput
* @param ?string $expectOutputOptimized
* @param string $expect * @param string $expect
* @param int|string $expectResult * @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);
} }
/** /**
@ -248,10 +254,56 @@ class InstallerTest extends TestCase
* @param mixed[]|false $expectLock * @param mixed[]|false $expectLock
* @param ?mixed[] $expectInstalled * @param ?mixed[] $expectInstalled
* @param ?string $expectOutput * @param ?string $expectOutput
* @param ?string $expectOutputOptimized
* @param string $expect * @param string $expect
* @param int|string $expectResult * @param int|string $expectResult
*/ */
public function testIntegration($file, $message, $condition, $composerConfig, $lock, $installed, $run, $expectLock, $expectInstalled, $expectOutput, $expect, $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
* @param Config $composerConfig
* @param ?mixed[] $lock
* @param ?mixed[] $installed
* @param string $run
* @param mixed[]|false $expectLock
* @param ?mixed[] $expectInstalled
* @param ?string $expectOutput
* @param string $expect
* @param int|string $expectResult
* @return void
*/
private function doTestIntegration($file, $message, $condition, $composerConfig, $lock, $installed, $run, $expectLock, $expectInstalled, $expectOutput, $expect, $expectResult)
{ {
if ($condition) { if ($condition) {
eval('$res = '.$condition.';'); eval('$res = '.$condition.';');
@ -518,6 +570,7 @@ class InstallerTest extends TestCase
$expectInstalled = JsonFile::parseJson($testData['EXPECT-INSTALLED']); $expectInstalled = JsonFile::parseJson($testData['EXPECT-INSTALLED']);
} }
$expectOutput = isset($testData['EXPECT-OUTPUT']) ? $testData['EXPECT-OUTPUT'] : null; $expectOutput = isset($testData['EXPECT-OUTPUT']) ? $testData['EXPECT-OUTPUT'] : null;
$expectOutputOptimized = isset($testData['EXPECT-OUTPUT-OPTIMIZED']) ? $testData['EXPECT-OUTPUT-OPTIMIZED'] : null;
$expect = $testData['EXPECT']; $expect = $testData['EXPECT'];
if (!empty($testData['EXPECT-EXCEPTION'])) { if (!empty($testData['EXPECT-EXCEPTION'])) {
$expectResult = $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))); 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; return $tests;
@ -557,6 +610,7 @@ class InstallerTest extends TestCase
'EXPECT-LOCK' => false, 'EXPECT-LOCK' => false,
'EXPECT-INSTALLED' => false, 'EXPECT-INSTALLED' => false,
'EXPECT-OUTPUT' => false, 'EXPECT-OUTPUT' => false,
'EXPECT-OUTPUT-OPTIMIZED' => false,
'EXPECT-EXIT-CODE' => false, 'EXPECT-EXIT-CODE' => false,
'EXPECT-EXCEPTION' => false, 'EXPECT-EXCEPTION' => false,
'EXPECT' => true, 'EXPECT' => true,

View File

@ -10,6 +10,8 @@
* file that was distributed with this source code. * file that was distributed with this source code.
*/ */
use Composer\Util\Platform;
error_reporting(E_ALL); error_reporting(E_ALL);
if (function_exists('date_default_timezone_set') && function_exists('date_default_timezone_get')) { 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/bootstrap.php';
require __DIR__.'/../src/Composer/InstalledVersions.php'; require __DIR__.'/../src/Composer/InstalledVersions.php';
require __DIR__.'/Composer/Test/TestCase.php'; require __DIR__.'/Composer/Test/TestCase.php';
Platform::putEnv('COMPOSER_TESTS_ARE_RUNNING', '1');

View File

@ -75,9 +75,14 @@
"count": 1 "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", "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", "location": "Composer\\Test\\Package\\Archiver\\ArchivableFilesFinderTest::testManualExcludes",