Skip to content

Commit

Permalink
Added more descriptive errors when verifications fail. It will now te…
Browse files Browse the repository at this point in the history
…ll you why each recorded call was marked as a failure.

No more will the fine users of Phake have to resort to argument captures for debugging their verification.

#144
  • Loading branch information
mlively committed Apr 7, 2015
1 parent 2ada2e6 commit 70cc682
Show file tree
Hide file tree
Showing 31 changed files with 317 additions and 172 deletions.
3 changes: 2 additions & 1 deletion composer.json
Expand Up @@ -12,7 +12,8 @@
}
],
"require": {
"php": ">=5.3.3"
"php": ">=5.3.3",
"sebastian/comparator": "~1.1"
},
"require-dev": {
"ext-soap": "*",
Expand Down
2 changes: 1 addition & 1 deletion src/Phake.php
Expand Up @@ -423,7 +423,7 @@ public static function createPhake()
*/
public static function equalTo($value)
{
return new Phake_Matchers_EqualsMatcher($value);
return new Phake_Matchers_EqualsMatcher($value, new \SebastianBergmann\Comparator\Factory());
}

/**
Expand Down
19 changes: 15 additions & 4 deletions src/Phake/CallRecorder/Verifier.php
Expand Up @@ -92,11 +92,22 @@ public function verifyCall(Phake_CallRecorder_CallExpectation $expectation)
if ($call->getObject() === $expectation->getObject()) {
$obj_interactions = true;
$args = $call->getArguments();
if ($matcher->matches($call->getMethod(), $args)) {
try
{
$matcher->assertMatches($call->getMethod(), $args);
$matchedCalls[] = $this->recorder->getCallInfo($call);
$this->recorder->markCallVerified($call);
} elseif ($call->getMethod() == $expectation->getMethod()) {
$methodNonMatched[] = $call->__toString();
}
catch (Phake_Exception_MethodMatcherException $e)
{
if ($call->getMethod() == $expectation->getMethod()) {
$message = $e->getMessage();
if (strlen($message))
{
$message = "\n{$message}";
}
$methodNonMatched[] = $call->__toString() . $message;
}
}
}
}
Expand All @@ -109,7 +120,7 @@ public function verifyCall(Phake_CallRecorder_CallExpectation $expectation)
}

if (count($methodNonMatched)) {
$additions .= "\nOther Invocations:\n " . implode("\n ", $methodNonMatched);
$additions .= "\nOther Invocations:\n===\n " . implode("\n===\n ", str_replace("\n", "\n ", $methodNonMatched)) . "\n===";
}

return new Phake_CallRecorder_VerifierResult(
Expand Down
36 changes: 36 additions & 0 deletions src/Phake/Exception/MethodMatcherException.php
@@ -0,0 +1,36 @@
<?php

/**
* Thrown when a method call doesn't match an expection
*/
class Phake_Exception_MethodMatcherException extends Exception
{
private $argument;

/**
* @param string $message
* @param Exception $previous
*/
public function __construct($message = "", Exception $previous = null)
{
parent::__construct($message, 0, $previous);
$this->argument = 0;
}

/**
* Updates the argument position (used in the argument chain)
*/
public function incrementArgumentPosition()
{
$this->argument++;
}

/**
* Returns the argument's position (0 indexed)
* @return int
*/
public function getArgumentPosition()
{
return $this->argument;
}
}
5 changes: 1 addition & 4 deletions src/Phake/Matchers/AnyParameters.php
Expand Up @@ -48,15 +48,12 @@
class Phake_Matchers_AnyParameters extends Phake_Matchers_AbstractChainableArgumentMatcher
{
/**
* Executes the matcher on a given list of argument values. Returns TRUE on a match, FALSE otherwise.
* Do nothing, any parameters always matches
*
* @param array $arguments
*
* @return boolean
*/
public function doArgumentsMatch(array &$arguments)
{
return true;
}

/**
Expand Down
22 changes: 14 additions & 8 deletions src/Phake/Matchers/ArgumentCaptor.php
Expand Up @@ -92,20 +92,26 @@ public function bindAllCapturedValues(&$values)
* Will bind the argument to the variable passed to the constructor.
*
* @param mixed $argument
*
* @return boolean
* @throws Phake_Exception_MethodMatcherException
*/
protected function matches(&$argument)
{
$args = array();
$args[] =& $argument;
if ($this->matcher === null || $this->matcher->doArgumentsMatch($args)) {
$this->boundVariable = $argument;
$this->allCapturedValues[] = $argument;
return true;
} else {
return false;

if ($this->matcher !== null)
{
try
{
$this->matcher->doArgumentsMatch($args);
}
catch (Phake_Exception_MethodMatcherException $e)
{
throw new Phake_Exception_MethodMatcherException(trim("Failed in Phake::capture()->when()\n" . $e->getMessage()), $e);
}
}
$this->boundVariable = $argument;
$this->allCapturedValues[] = $argument;
}

/**
Expand Down
2 changes: 1 addition & 1 deletion src/Phake/Matchers/ChainedArgumentMatcher.php
Expand Up @@ -84,6 +84,6 @@ public function getAdaptedMatcher()
*/
protected function matches(&$argument)
{
return $this->adaptedMatcher->matches($argument);
$this->adaptedMatcher->matches($argument);
}
}
65 changes: 15 additions & 50 deletions src/Phake/Matchers/EqualsMatcher.php
Expand Up @@ -54,72 +54,37 @@ class Phake_Matchers_EqualsMatcher extends Phake_Matchers_SingleArgumentMatcher
*/
private $value;

/**\
* @var \SebastianBergmann\Comparator\Factory
*/
private $comparatorFactory;

/**
* Pass in the value that the upcoming arguments is expected to equal.
*
* @param mixed $value
* @param \SebastianBergmann\Comparator\Factory $comparatorFactory
*/
public function __construct($value)
public function __construct($value, \SebastianBergmann\Comparator\Factory $comparatorFactory)
{
$this->value = $value;
$this->comparatorFactory = $comparatorFactory;
}

/**
* Returns whether or not the passed argument matches the matcher.
*/
protected function matches(&$argument)
{
return $this->compareValues($this->value, $argument);
}

private function compareValues($val1, $val2, &$tested = array())
{
if (is_object($val1) && is_object($val2)) {
return $this->compareObjects($val1, $val2, $tested);
} elseif (is_array($val1) && is_array($val2)) {
return $this->compareArrays($val1, $val2, $tested);
} else {
return $val1 == $val2;
}
}

private function compareObjects($obj1, $obj2, &$tested = array())
{
if (get_class($obj1) != get_class($obj2)) {
return false;
}

if ($obj1 === $obj2) {
return true;
}

if (in_array(array($obj1, $obj2), $tested, true)) {
return true;
}

$tested[] = array($obj1, $obj2);
$tested[] = array($obj2, $obj1);

return $this->compareArrays((array)$obj1, (array)$obj2, $tested);
}

private function compareArrays(array $arr1, array $arr2, array &$tested)
{
if (count($arr1) != count($arr2)) {
return false;
try
{
$compare = $this->comparatorFactory->getComparatorFor($this->value, $argument);
$compare->assertEquals($this->value, $argument);
}

foreach ($arr1 as $key => $value) {
if (!array_key_exists($key, $arr2)) {
return false;
}

if (!$this->compareValues($value, $arr2[$key], $tested)) {
return false;
}
catch (\SebastianBergmann\Comparator\ComparisonFailure $e)
{
throw new Phake_Exception_MethodMatcherException(trim($e->getMessage() . "\n" . $e->getDiff()), $e);
}

return true;
}

/**
Expand Down
2 changes: 1 addition & 1 deletion src/Phake/Matchers/Factory.php
Expand Up @@ -72,7 +72,7 @@ public function createMatcher($argument, Phake_Matchers_IChainableArgumentMatche
} elseif ($argument instanceof Phake_Matchers_IArgumentMatcher) {
$return = new Phake_Matchers_ChainedArgumentMatcher($argument);
} else {
$return = new Phake_Matchers_EqualsMatcher($argument);
$return = new Phake_Matchers_EqualsMatcher($argument, new \SebastianBergmann\Comparator\Factory());
}

if ($nextMatcher !== null)
Expand Down
14 changes: 11 additions & 3 deletions src/Phake/Matchers/HamcrestMatcherAdapter.php
Expand Up @@ -66,12 +66,20 @@ public function __construct(Hamcrest\Matcher $matcher)
* Forwards the call to Hamcrest's matches() method.
*
* @param mixed $argument
*
* @return boolean
* @throws Phake_Exception_MethodMatcherException
*/
protected function matches(&$argument)
{
return $this->matcher->matches($argument);
if (!$this->matcher->matches($argument))
{
$description = new \Hamcrest\StringDescription();
$description->appendText("Expected ")
->appendDescriptionOf($this->matcher)
->appendText(' but ');

$this->matcher->describeMismatch($argument, $description);
throw new Phake_Exception_MethodMatcherException($description);
}
}

public function __toString()
Expand Down
4 changes: 2 additions & 2 deletions src/Phake/Matchers/IChainableArgumentMatcher.php
Expand Up @@ -48,11 +48,11 @@
interface Phake_Matchers_IChainableArgumentMatcher
{
/**
* Executes the matcher on a given list of argument values. Returns TRUE on a match, FALSE otherwise.
* Assert the matcher on a given list of argument values. Throws an exception if the matcher doesn't match
*
* @param array $arguments
*
* @return boolean
* @throw Exception
*/
public function doArgumentsMatch(array &$arguments);

Expand Down
5 changes: 1 addition & 4 deletions src/Phake/Matchers/IgnoreRemainingMatcher.php
Expand Up @@ -48,15 +48,12 @@
class Phake_Matchers_IgnoreRemainingMatcher extends Phake_Matchers_AbstractChainableArgumentMatcher
{
/**
* Executes the matcher on a given list of argument values. Returns TRUE on a match, FALSE otherwise.
* Do nothing, ignore remaining always matches
*
* @param array $arguments
*
* @return boolean
*/
public function doArgumentsMatch(array &$arguments)
{
return true;
}

/**
Expand Down
53 changes: 43 additions & 10 deletions src/Phake/Matchers/MethodMatcher.php
Expand Up @@ -76,29 +76,62 @@ public function __construct($expectedMethod, Phake_Matchers_IChainableArgumentMa
*/
public function matches($method, array &$args)
{
if ($this->expectedMethod == $method
&& $this->doArgumentsMatch($args)
) {
try
{
$this->assertMatches($method, $args);
return true;
} else {
}
catch (Phake_Exception_MethodMatcherException $e)
{
return false;
}
}

/**
* Determines whether or not given arguments match the argument matchers configured in the object.
* Asserts whether or not the given method and arguments match the configured method and argument matchers in this \
* object.
*
* @param string $method
* @param array $args
* @return bool
* @throws Phake_Exception_MethodMatcherException
*/
public function assertMatches($method, array &$args)
{
if ($this->expectedMethod != $method)
{
throw new Phake_Exception_MethodMatcherException("Expected method {$this->expectedMethod} but received {$method}");
}

$this->doArgumentsMatch($args);
}

/**
* Determines whether or not given arguments match the argument matchers configured in the object.
*
* @return boolean
* Throws an exception with a description if the arguments do not match.
*
* @param array $args
* @return bool
* @throws Phake_Exception_MethodMatcherException
*/
private function doArgumentsMatch(array &$args)
{
if ($this->argumentMatcherChain === null)
if ($this->argumentMatcherChain !== null)
{
return count($args) == 0;
try
{
$this->argumentMatcherChain->doArgumentsMatch($args);
}
catch (Phake_Exception_MethodMatcherException $e)
{
$position = $e->getArgumentPosition() + 1;
throw new Phake_Exception_MethodMatcherException(trim("Argument #{$position} failed test\n" . $e->getMessage()), $e);
}
}
elseif (count($args) != 0)
{
throw new Phake_Exception_MethodMatcherException("No matchers were given to Phake::when(), but arguments were received by this method.");
}

return $this->argumentMatcherChain->doArgumentsMatch($args);
}
}

0 comments on commit 70cc682

Please sign in to comment.