Merge remote-tracking branch 'naderman/error-msgs'

main
Jordi Boggiano 12 years ago
commit 892cfdd57d

@ -163,4 +163,17 @@ class Pool
{ {
return ($literal > 0 ? '+' : '-') . $this->literalToPackage($literal); return ($literal > 0 ? '+' : '-') . $this->literalToPackage($literal);
} }
public function literalToPrettyString($literal, $installedMap)
{
$package = $this->literalToPackage($literal);
if (isset($installedMap[$package->getId()])) {
$prefix = ($literal > 0 ? 'keep' : 'remove');
} else {
$prefix = ($literal > 0 ? 'install' : 'don\'t install');
}
return $prefix.' '.$package->getPrettyString();
}
} }

@ -19,11 +19,19 @@ namespace Composer\DependencyResolver;
*/ */
class Problem class Problem
{ {
/**
* A map containing the id of each rule part of this problem as a key
* @var array
*/
protected $reasonSeen;
/** /**
* A set of reasons for the problem, each is a rule or a job and a rule * A set of reasons for the problem, each is a rule or a job and a rule
* @var array * @var array
*/ */
protected $reasons; protected $reasons = array();
protected $section = 0;
/** /**
* Add a rule as a reason * Add a rule as a reason
@ -50,12 +58,16 @@ class Problem
/** /**
* A human readable textual representation of the problem's reasons * A human readable textual representation of the problem's reasons
*
* @param array $installedMap A map of all installed packages
*/ */
public function __toString() public function getPrettyString(array $installedMap = array())
{ {
if (count($this->reasons) === 1) { $reasons = call_user_func_array('array_merge', array_reverse($this->reasons));
reset($this->reasons);
$reason = current($this->reasons); if (count($reasons) === 1) {
reset($reasons);
$reason = current($reasons);
$rule = $reason['rule']; $rule = $reason['rule'];
$job = $reason['job']; $job = $reason['job'];
@ -73,9 +85,9 @@ class Problem
} }
} }
$messages = array("Problem caused by:"); $messages = array();
foreach ($this->reasons as $reason) { foreach ($reasons as $reason) {
$rule = $reason['rule']; $rule = $reason['rule'];
$job = $reason['job']; $job = $reason['job'];
@ -84,12 +96,12 @@ class Problem
$messages[] = $this->jobToText($job); $messages[] = $this->jobToText($job);
} elseif ($rule) { } elseif ($rule) {
if ($rule instanceof Rule) { if ($rule instanceof Rule) {
$messages[] = $rule->toHumanReadableString(); $messages[] = $rule->getPrettyString($installedMap);
} }
} }
} }
return implode("\n\t\t\t- ", $messages); return "\n - ".implode("\n - ", $messages);
} }
/** /**
@ -100,9 +112,15 @@ class Problem
*/ */
protected function addReason($id, $reason) protected function addReason($id, $reason)
{ {
if (!isset($this->reasons[$id])) { if (!isset($this->reasonSeen[$id])) {
$this->reasons[$id] = $reason; $this->reasonSeen[$id] = true;
$this->reasons[$this->section][] = $reason;
}
} }
public function nextSection()
{
$this->section++;
} }
/** /**
@ -119,7 +137,7 @@ class Problem
return 'No package found to satisfy install request for '.$job['packageName'].$this->constraintToText($job['constraint']); return 'No package found to satisfy install request for '.$job['packageName'].$this->constraintToText($job['constraint']);
} }
return 'Installation request for '.$job['packageName'].$this->constraintToText($job['constraint']).': Satisfiable by ['.$this->getPackageList($job['packages']).'].'; return 'Installation request for '.$job['packageName'].$this->constraintToText($job['constraint']).' -> satisfiable by '.$this->getPackageList($job['packages']).'.';
case 'update': case 'update':
return 'Update request for '.$job['packageName'].$this->constraintToText($job['constraint']).'.'; return 'Update request for '.$job['packageName'].$this->constraintToText($job['constraint']).'.';
case 'remove': case 'remove':
@ -146,6 +164,6 @@ class Problem
*/ */
protected function constraintToText($constraint) protected function constraintToText($constraint)
{ {
return ($constraint) ? ' '.$constraint : ''; return ($constraint) ? ' '.$constraint->getPrettyString() : '';
} }
} }

@ -147,14 +147,14 @@ class Rule
return 1 === count($this->literals); return 1 === count($this->literals);
} }
public function toHumanReadableString() public function getPrettyString(array $installedMap = array())
{ {
$ruleText = ''; $ruleText = '';
foreach ($this->literals as $i => $literal) { foreach ($this->literals as $i => $literal) {
if ($i != 0) { if ($i != 0) {
$ruleText .= '|'; $ruleText .= '|';
} }
$ruleText .= $this->pool->literalToString($literal); $ruleText .= $this->pool->literalToPrettyString($literal, $installedMap);
} }
switch ($this->reason) { switch ($this->reason) {
@ -171,7 +171,7 @@ class Rule
$package1 = $this->pool->literalToPackage($this->literals[0]); $package1 = $this->pool->literalToPackage($this->literals[0]);
$package2 = $this->pool->literalToPackage($this->literals[1]); $package2 = $this->pool->literalToPackage($this->literals[1]);
return 'Package '.$package1->getPrettyString().' conflicts with '.$package2->getPrettyString().'"'; return $package1->getPrettyString().' conflicts with '.$package2->getPrettyString().'.';
case self::RULE_PACKAGE_REQUIRES: case self::RULE_PACKAGE_REQUIRES:
$literals = $this->literals; $literals = $this->literals;
@ -183,11 +183,15 @@ class Rule
$requires[] = $this->pool->literalToPackage($literal); $requires[] = $this->pool->literalToPackage($literal);
} }
$text = 'Package "'.$sourcePackage.'" contains the rule '.$this->reasonData.'. '; $text = $this->reasonData->getPrettyString($sourcePackage);
if ($requires) { if ($requires) {
$text .= 'Any of these packages satisfy the dependency: '.implode(', ', $requires).'.'; $requireText = array();
foreach ($requires as $require) {
$requireText[] = $require->getPrettyString();
}
$text .= ' -> satisfiable by '.implode(', ', $requireText).'.';
} else { } else {
$text .= 'No package satisfies this dependency.'; $text .= ' -> no matching package found.';
} }
return $text; return $text;
@ -197,11 +201,18 @@ class Rule
case self::RULE_INSTALLED_PACKAGE_OBSOLETES: case self::RULE_INSTALLED_PACKAGE_OBSOLETES:
return $ruleText; return $ruleText;
case self::RULE_PACKAGE_SAME_NAME: case self::RULE_PACKAGE_SAME_NAME:
return $ruleText; $text = "Can only install one of: ";
$packages = array();
foreach ($this->literals as $i => $literal) {
$packages[] = $this->pool->literalToPackage($literal)->getPrettyString();
}
return $text.implode(', ', $packages).'.';
case self::RULE_PACKAGE_IMPLICIT_OBSOLETES: case self::RULE_PACKAGE_IMPLICIT_OBSOLETES:
return $ruleText; return $ruleText;
case self::RULE_LEARNED: case self::RULE_LEARNED:
return 'learned: '.$ruleText; return 'Conclusion: '.$ruleText;
case self::RULE_PACKAGE_ALIAS: case self::RULE_PACKAGE_ALIAS:
return $ruleText; return $ruleText;
} }

@ -157,7 +157,7 @@ class RuleSetGenerator
foreach ($package->getRequires() as $link) { foreach ($package->getRequires() as $link) {
$possibleRequires = $this->pool->whatProvides($link->getTarget(), $link->getConstraint()); $possibleRequires = $this->pool->whatProvides($link->getTarget(), $link->getConstraint());
$this->addRule(RuleSet::TYPE_PACKAGE, $rule = $this->createRequireRule($package, $possibleRequires, Rule::RULE_PACKAGE_REQUIRES, (string) $link)); $this->addRule(RuleSet::TYPE_PACKAGE, $rule = $this->createRequireRule($package, $possibleRequires, Rule::RULE_PACKAGE_REQUIRES, $link));
foreach ($possibleRequires as $require) { foreach ($possibleRequires as $require) {
$workQueue->enqueue($require); $workQueue->enqueue($require);
@ -168,7 +168,7 @@ class RuleSetGenerator
$possibleConflicts = $this->pool->whatProvides($link->getTarget(), $link->getConstraint()); $possibleConflicts = $this->pool->whatProvides($link->getTarget(), $link->getConstraint());
foreach ($possibleConflicts as $conflict) { foreach ($possibleConflicts as $conflict) {
$this->addRule(RuleSet::TYPE_PACKAGE, $this->createConflictRule($package, $conflict, Rule::RULE_PACKAGE_CONFLICT, (string) $link)); $this->addRule(RuleSet::TYPE_PACKAGE, $this->createConflictRule($package, $conflict, Rule::RULE_PACKAGE_CONFLICT, $link));
} }
} }
@ -185,7 +185,7 @@ class RuleSetGenerator
if (!$this->obsoleteImpossibleForAlias($package, $provider)) { if (!$this->obsoleteImpossibleForAlias($package, $provider)) {
$reason = ($isInstalled) ? Rule::RULE_INSTALLED_PACKAGE_OBSOLETES : Rule::RULE_PACKAGE_OBSOLETES; $reason = ($isInstalled) ? Rule::RULE_INSTALLED_PACKAGE_OBSOLETES : Rule::RULE_PACKAGE_OBSOLETES;
$this->addRule(RuleSet::TYPE_PACKAGE, $this->createConflictRule($package, $provider, $reason, (string) $link)); $this->addRule(RuleSet::TYPE_PACKAGE, $this->createConflictRule($package, $provider, $reason, $link));
} }
} }
} }
@ -198,10 +198,10 @@ class RuleSetGenerator
} }
if (($package instanceof AliasPackage) && $package->getAliasOf() === $provider) { if (($package instanceof AliasPackage) && $package->getAliasOf() === $provider) {
$this->addRule(RuleSet::TYPE_PACKAGE, $rule = $this->createRequireRule($package, array($provider), Rule::RULE_PACKAGE_ALIAS, (string) $package)); $this->addRule(RuleSet::TYPE_PACKAGE, $rule = $this->createRequireRule($package, array($provider), Rule::RULE_PACKAGE_ALIAS, $package));
} elseif (!$this->obsoleteImpossibleForAlias($package, $provider)) { } elseif (!$this->obsoleteImpossibleForAlias($package, $provider)) {
$reason = ($package->getName() == $provider->getName()) ? Rule::RULE_PACKAGE_SAME_NAME : Rule::RULE_PACKAGE_IMPLICIT_OBSOLETES; $reason = ($package->getName() == $provider->getName()) ? Rule::RULE_PACKAGE_SAME_NAME : Rule::RULE_PACKAGE_IMPLICIT_OBSOLETES;
$this->addRule(RuleSet::TYPE_PACKAGE, $rule = $this->createConflictRule($package, $provider, $reason, (string) $package)); $this->addRule(RuleSet::TYPE_PACKAGE, $rule = $this->createConflictRule($package, $provider, $reason, $package));
} }
} }
} }

@ -182,7 +182,7 @@ class Solver
} }
if ($this->problems) { if ($this->problems) {
throw new SolverProblemsException($this->problems); throw new SolverProblemsException($this->problems, $this->installedMap);
} }
$transaction = new Transaction($this->policy, $this->pool, $this->installedMap, $this->decisions); $transaction = new Transaction($this->policy, $this->pool, $this->installedMap, $this->decisions);
@ -457,6 +457,7 @@ class Solver
return; return;
} }
$problem->nextSection();
$problem->addRule($conflictRule); $problem->addRule($conflictRule);
} }

@ -18,23 +18,24 @@ namespace Composer\DependencyResolver;
class SolverProblemsException extends \RuntimeException class SolverProblemsException extends \RuntimeException
{ {
protected $problems; protected $problems;
protected $installedMap;
public function __construct(array $problems) public function __construct(array $problems, array $installedMap)
{ {
$this->problems = $problems; $this->problems = $problems;
$this->installedMap = $installedMap;
parent::__construct($this->createMessage()); parent::__construct($this->createMessage());
} }
protected function createMessage() protected function createMessage()
{ {
$messages = array(); $text = "\n";
foreach ($this->problems as $i => $problem) {
foreach ($this->problems as $problem) { $text .= " Problem ".($i+1).$problem->getPrettyString($this->installedMap)."\n";
$messages[] = (string) $problem;
} }
return "\n\tProblems:\n\t\t- ".implode("\n\t\t- ", $messages); return $text;
} }
public function getProblems() public function getProblems()

@ -203,7 +203,7 @@ abstract class BasePackage implements PackageInterface
public function getPrettyString() public function getPrettyString()
{ {
return $this->getPrettyName().'-'.$this->getPrettyVersion(); return $this->getPrettyName().' '.$this->getPrettyVersion();
} }
public function __clone() public function __clone()

@ -13,6 +13,7 @@
namespace Composer\Package; namespace Composer\Package;
use Composer\Package\LinkConstraint\LinkConstraintInterface; use Composer\Package\LinkConstraint\LinkConstraintInterface;
use Composer\Package\PackageInterface;
/** /**
* Represents a link between two packages, represented by their names * Represents a link between two packages, represented by their names
@ -71,4 +72,9 @@ class Link
{ {
return $this->source.' '.$this->description.' '.$this->target.' ('.$this->constraint.')'; return $this->source.' '.$this->description.' '.$this->target.' ('.$this->constraint.')';
} }
public function getPrettyString(PackageInterface $sourcePackage)
{
return $sourcePackage->getPrettyString().' '.$this->description.' '.$this->target.' '.$this->constraint->getPrettyString().'';
}
} }

@ -20,5 +20,7 @@ namespace Composer\Package\LinkConstraint;
interface LinkConstraintInterface interface LinkConstraintInterface
{ {
public function matches(LinkConstraintInterface $provider); public function matches(LinkConstraintInterface $provider);
public function setPrettyString($prettyString);
public function getPrettyString();
public function __toString(); public function __toString();
} }

@ -20,6 +20,7 @@ namespace Composer\Package\LinkConstraint;
class MultiConstraint implements LinkConstraintInterface class MultiConstraint implements LinkConstraintInterface
{ {
protected $constraints; protected $constraints;
protected $prettyString;
/** /**
* Sets operator and version to compare a package with * Sets operator and version to compare a package with
@ -42,6 +43,19 @@ class MultiConstraint implements LinkConstraintInterface
return true; return true;
} }
public function setPrettyString($prettyString)
{
$this->prettyString = $prettyString;
}
public function getPrettyString()
{
if ($this->prettyString) {
return $this->prettyString;
}
return $this->__toString();
}
public function __toString() public function __toString()
{ {
$constraints = array(); $constraints = array();

@ -19,6 +19,8 @@ namespace Composer\Package\LinkConstraint;
*/ */
abstract class SpecificConstraint implements LinkConstraintInterface abstract class SpecificConstraint implements LinkConstraintInterface
{ {
protected $prettyString;
public function matches(LinkConstraintInterface $provider) public function matches(LinkConstraintInterface $provider)
{ {
if ($provider instanceof MultiConstraint) { if ($provider instanceof MultiConstraint) {
@ -31,7 +33,21 @@ abstract class SpecificConstraint implements LinkConstraintInterface
return true; return true;
} }
public function setPrettyString($prettyString)
{
$this->prettyString = $prettyString;
}
public function getPrettyString()
{
if ($this->prettyString) {
return $this->prettyString;
}
return $this->__toString();
}
// implementations must implement a method of this format: // implementations must implement a method of this format:
// not declared abstract here because type hinting violates parameter coherence (TODO right word?) // not declared abstract here because type hinting violates parameter coherence (TODO right word?)
// public function matchSpecific(<SpecificConstraintType> $provider); // public function matchSpecific(<SpecificConstraintType> $provider);
} }

@ -164,6 +164,8 @@ class VersionParser
*/ */
public function parseConstraints($constraints) public function parseConstraints($constraints)
{ {
$prettyConstraint = $constraints;
if (preg_match('{^([^,\s]*?)@('.implode('|', array_keys(BasePackage::$stabilities)).')$}i', $constraints, $match)) { if (preg_match('{^([^,\s]*?)@('.implode('|', array_keys(BasePackage::$stabilities)).')$}i', $constraints, $match)) {
$constraints = empty($match[1]) ? '*' : $match[1]; $constraints = empty($match[1]) ? '*' : $match[1];
} }
@ -184,10 +186,13 @@ class VersionParser
} }
if (1 === count($constraintObjects)) { if (1 === count($constraintObjects)) {
return $constraintObjects[0]; $constraint = $constraintObjects[0];
} else {
$constraint = new MultiConstraint($constraintObjects);
} }
return new MultiConstraint($constraintObjects); $constraint->setPrettyString($prettyConstraint);
return $constraint;
} }
private function parseConstraint($constraint) private function parseConstraint($constraint)

@ -66,7 +66,7 @@ class SolverTest extends TestCase
$this->repo->addPackage($this->getPackage('A', '1.0')); $this->repo->addPackage($this->getPackage('A', '1.0'));
$this->reposComplete(); $this->reposComplete();
$this->request->install('B', $this->getVersionConstraint('=', '1')); $this->request->install('B', $this->getVersionConstraint('==', '1'));
try { try {
$transaction = $this->solver->solve($this->request); $transaction = $this->solver->solve($this->request);
@ -74,7 +74,7 @@ class SolverTest extends TestCase
} catch (SolverProblemsException $e) { } catch (SolverProblemsException $e) {
$problems = $e->getProblems(); $problems = $e->getProblems();
$this->assertEquals(1, count($problems)); $this->assertEquals(1, count($problems));
$this->assertEquals('The requested package b == 1.0.0.0 could not be found.', (string) $problems[0]); $this->assertEquals('The requested package b == 1 could not be found.', $problems[0]->getPrettyString());
} }
} }
@ -641,7 +641,13 @@ class SolverTest extends TestCase
} catch (SolverProblemsException $e) { } catch (SolverProblemsException $e) {
$problems = $e->getProblems(); $problems = $e->getProblems();
$this->assertEquals(1, count($problems)); $this->assertEquals(1, count($problems));
// TODO assert problem properties
$msg = "\n";
$msg .= " Problem 1\n";
$msg .= " - Installation request for a -> satisfiable by A 1.0.\n";
$msg .= " - B 1.0 conflicts with A 1.0.\n";
$msg .= " - Installation request for b -> satisfiable by B 1.0.\n";
$this->assertEquals($msg, $e->getMessage());
} }
} }
@ -665,6 +671,56 @@ class SolverTest extends TestCase
$problems = $e->getProblems(); $problems = $e->getProblems();
$this->assertEquals(1, count($problems)); $this->assertEquals(1, count($problems));
// TODO assert problem properties // TODO assert problem properties
$msg = "\n";
$msg .= " Problem 1\n";
$msg .= " - Installation request for a -> satisfiable by A 1.0.\n";
$msg .= " - A 1.0 requires b >= 2.0 -> no matching package found.\n";
$this->assertEquals($msg, $e->getMessage());
}
}
public function testRequireMismatchException()
{
$this->repo->addPackage($packageA = $this->getPackage('A', '1.0'));
$this->repo->addPackage($packageB = $this->getPackage('B', '1.0'));
$this->repo->addPackage($packageB2 = $this->getPackage('B', '0.9'));
$this->repo->addPackage($packageC = $this->getPackage('C', '1.0'));
$this->repo->addPackage($packageD = $this->getPackage('D', '1.0'));
$packageA->setRequires(array(
new Link('A', 'B', $this->getVersionConstraint('>=', '1.0'), 'requires'),
));
$packageB->setRequires(array(
new Link('B', 'C', $this->getVersionConstraint('>=', '1.0'), 'requires'),
));
$packageC->setRequires(array(
new Link('C', 'D', $this->getVersionConstraint('>=', '1.0'), 'requires'),
));
$packageD->setRequires(array(
new Link('D', 'B', $this->getVersionConstraint('<', '1.0'), 'requires'),
));
$this->reposComplete();
$this->request->install('A');
try {
$transaction = $this->solver->solve($this->request);
$this->fail('Unsolvable conflict did not result in exception.');
} catch (SolverProblemsException $e) {
$problems = $e->getProblems();
$this->assertEquals(1, count($problems));
$msg = "\n";
$msg .= " Problem 1\n";
$msg .= " - C 1.0 requires d >= 1.0 -> satisfiable by D 1.0.\n";
$msg .= " - D 1.0 requires b < 1.0 -> satisfiable by B 0.9.\n";
$msg .= " - B 1.0 requires c >= 1.0 -> satisfiable by C 1.0.\n";
$msg .= " - Can only install one of: B 0.9, B 1.0.\n";
$msg .= " - A 1.0 requires b >= 1.0 -> satisfiable by B 1.0.\n";
$msg .= " - Installation request for a -> satisfiable by A 1.0.\n";
$this->assertEquals($msg, $e->getMessage());
} }
} }

@ -33,10 +33,14 @@ abstract class TestCase extends \PHPUnit_Framework_TestCase
protected function getVersionConstraint($operator, $version) protected function getVersionConstraint($operator, $version)
{ {
return new VersionConstraint( $constraint = new VersionConstraint(
$operator, $operator,
self::getVersionParser()->normalize($version) self::getVersionParser()->normalize($version)
); );
$constraint->setPrettyString($operator.' '.$version);
return $constraint;
} }
protected function getPackage($name, $version) protected function getPackage($name, $version)

Loading…
Cancel
Save