diff options
Diffstat (limited to '3rdparty/simpletest/mock_objects.php')
-rwxr-xr-x | 3rdparty/simpletest/mock_objects.php | 1641 |
1 files changed, 1641 insertions, 0 deletions
diff --git a/3rdparty/simpletest/mock_objects.php b/3rdparty/simpletest/mock_objects.php new file mode 100755 index 00000000000..93a827b6c02 --- /dev/null +++ b/3rdparty/simpletest/mock_objects.php @@ -0,0 +1,1641 @@ +<?php +/** + * base include file for SimpleTest + * @package SimpleTest + * @subpackage MockObjects + * @version $Id: mock_objects.php 1973 2009-12-22 01:16:59Z lastcraft $ + */ + +/**#@+ + * include SimpleTest files + */ +require_once(dirname(__FILE__) . '/expectation.php'); +require_once(dirname(__FILE__) . '/simpletest.php'); +require_once(dirname(__FILE__) . '/dumper.php'); +require_once(dirname(__FILE__) . '/reflection_php5.php'); +/**#@-*/ + +/** + * Default character simpletest will substitute for any value + */ +if (! defined('MOCK_ANYTHING')) { + define('MOCK_ANYTHING', '*'); +} + +/** + * Parameter comparison assertion. + * @package SimpleTest + * @subpackage MockObjects + */ +class ParametersExpectation extends SimpleExpectation { + private $expected; + + /** + * Sets the expected parameter list. + * @param array $parameters Array of parameters including + * those that are wildcarded. + * If the value is not an array + * then it is considered to match any. + * @param string $message Customised message on failure. + */ + function __construct($expected = false, $message = '%s') { + parent::__construct($message); + $this->expected = $expected; + } + + /** + * Tests the assertion. True if correct. + * @param array $parameters Comparison values. + * @return boolean True if correct. + */ + function test($parameters) { + if (! is_array($this->expected)) { + return true; + } + if (count($this->expected) != count($parameters)) { + return false; + } + for ($i = 0; $i < count($this->expected); $i++) { + if (! $this->testParameter($parameters[$i], $this->expected[$i])) { + return false; + } + } + return true; + } + + /** + * Tests an individual parameter. + * @param mixed $parameter Value to test. + * @param mixed $expected Comparison value. + * @return boolean True if expectation + * fulfilled. + */ + protected function testParameter($parameter, $expected) { + $comparison = $this->coerceToExpectation($expected); + return $comparison->test($parameter); + } + + /** + * Returns a human readable test message. + * @param array $comparison Incoming parameter list. + * @return string Description of success + * or failure. + */ + function testMessage($parameters) { + if ($this->test($parameters)) { + return "Expectation of " . count($this->expected) . + " arguments of [" . $this->renderArguments($this->expected) . + "] is correct"; + } else { + return $this->describeDifference($this->expected, $parameters); + } + } + + /** + * Message to display if expectation differs from + * the parameters actually received. + * @param array $expected Expected parameters as list. + * @param array $parameters Actual parameters received. + * @return string Description of difference. + */ + protected function describeDifference($expected, $parameters) { + if (count($expected) != count($parameters)) { + return "Expected " . count($expected) . + " arguments of [" . $this->renderArguments($expected) . + "] but got " . count($parameters) . + " arguments of [" . $this->renderArguments($parameters) . "]"; + } + $messages = array(); + for ($i = 0; $i < count($expected); $i++) { + $comparison = $this->coerceToExpectation($expected[$i]); + if (! $comparison->test($parameters[$i])) { + $messages[] = "parameter " . ($i + 1) . " with [" . + $comparison->overlayMessage($parameters[$i], $this->getDumper()) . "]"; + } + } + return "Parameter expectation differs at " . implode(" and ", $messages); + } + + /** + * Creates an identical expectation if the + * object/value is not already some type + * of expectation. + * @param mixed $expected Expected value. + * @return SimpleExpectation Expectation object. + */ + protected function coerceToExpectation($expected) { + if (SimpleExpectation::isExpectation($expected)) { + return $expected; + } + return new IdenticalExpectation($expected); + } + + /** + * Renders the argument list as a string for + * messages. + * @param array $args Incoming arguments. + * @return string Simple description of type and value. + */ + protected function renderArguments($args) { + $descriptions = array(); + if (is_array($args)) { + foreach ($args as $arg) { + $dumper = new SimpleDumper(); + $descriptions[] = $dumper->describeValue($arg); + } + } + return implode(', ', $descriptions); + } +} + +/** + * Confirms that the number of calls on a method is as expected. + * @package SimpleTest + * @subpackage MockObjects + */ +class CallCountExpectation extends SimpleExpectation { + private $method; + private $count; + + /** + * Stashes the method and expected count for later + * reporting. + * @param string $method Name of method to confirm against. + * @param integer $count Expected number of calls. + * @param string $message Custom error message. + */ + function __construct($method, $count, $message = '%s') { + $this->method = $method; + $this->count = $count; + parent::__construct($message); + } + + /** + * Tests the assertion. True if correct. + * @param integer $compare Measured call count. + * @return boolean True if expected. + */ + function test($compare) { + return ($this->count == $compare); + } + + /** + * Reports the comparison. + * @param integer $compare Measured call count. + * @return string Message to show. + */ + function testMessage($compare) { + return 'Expected call count for [' . $this->method . + '] was [' . $this->count . + '] got [' . $compare . ']'; + } +} + +/** + * Confirms that the number of calls on a method is as expected. + * @package SimpleTest + * @subpackage MockObjects + */ +class MinimumCallCountExpectation extends SimpleExpectation { + private $method; + private $count; + + /** + * Stashes the method and expected count for later + * reporting. + * @param string $method Name of method to confirm against. + * @param integer $count Minimum number of calls. + * @param string $message Custom error message. + */ + function __construct($method, $count, $message = '%s') { + $this->method = $method; + $this->count = $count; + parent::__construct($message); + } + + /** + * Tests the assertion. True if correct. + * @param integer $compare Measured call count. + * @return boolean True if enough. + */ + function test($compare) { + return ($this->count <= $compare); + } + + /** + * Reports the comparison. + * @param integer $compare Measured call count. + * @return string Message to show. + */ + function testMessage($compare) { + return 'Minimum call count for [' . $this->method . + '] was [' . $this->count . + '] got [' . $compare . ']'; + } +} + +/** + * Confirms that the number of calls on a method is as expected. + * @package SimpleTest + * @subpackage MockObjects + */ +class MaximumCallCountExpectation extends SimpleExpectation { + private $method; + private $count; + + /** + * Stashes the method and expected count for later + * reporting. + * @param string $method Name of method to confirm against. + * @param integer $count Minimum number of calls. + * @param string $message Custom error message. + */ + function __construct($method, $count, $message = '%s') { + $this->method = $method; + $this->count = $count; + parent::__construct($message); + } + + /** + * Tests the assertion. True if correct. + * @param integer $compare Measured call count. + * @return boolean True if not over. + */ + function test($compare) { + return ($this->count >= $compare); + } + + /** + * Reports the comparison. + * @param integer $compare Measured call count. + * @return string Message to show. + */ + function testMessage($compare) { + return 'Maximum call count for [' . $this->method . + '] was [' . $this->count . + '] got [' . $compare . ']'; + } +} + +/** + * Retrieves method actions by searching the + * parameter lists until an expected match is found. + * @package SimpleTest + * @subpackage MockObjects + */ +class SimpleSignatureMap { + private $map; + + /** + * Creates an empty call map. + */ + function __construct() { + $this->map = array(); + } + + /** + * Stashes a reference against a method call. + * @param array $parameters Array of arguments (including wildcards). + * @param mixed $action Reference placed in the map. + */ + function add($parameters, $action) { + $place = count($this->map); + $this->map[$place] = array(); + $this->map[$place]['params'] = new ParametersExpectation($parameters); + $this->map[$place]['content'] = $action; + } + + /** + * Searches the call list for a matching parameter + * set. Returned by reference. + * @param array $parameters Parameters to search by + * without wildcards. + * @return object Object held in the first matching + * slot, otherwise null. + */ + function &findFirstAction($parameters) { + $slot = $this->findFirstSlot($parameters); + if (isset($slot) && isset($slot['content'])) { + return $slot['content']; + } + $null = null; + return $null; + } + + /** + * Searches the call list for a matching parameter + * set. True if successful. + * @param array $parameters Parameters to search by + * without wildcards. + * @return boolean True if a match is present. + */ + function isMatch($parameters) { + return ($this->findFirstSlot($parameters) != null); + } + + /** + * Compares the incoming parameters with the + * internal expectation. Uses the incoming $test + * to dispatch the test message. + * @param SimpleTestCase $test Test to dispatch to. + * @param array $parameters The actual calling arguments. + * @param string $message The message to overlay. + */ + function test($test, $parameters, $message) { + } + + /** + * Searches the map for a matching item. + * @param array $parameters Parameters to search by + * without wildcards. + * @return array Reference to slot or null. + */ + function &findFirstSlot($parameters) { + $count = count($this->map); + for ($i = 0; $i < $count; $i++) { + if ($this->map[$i]["params"]->test($parameters)) { + return $this->map[$i]; + } + } + $null = null; + return $null; + } +} + +/** + * Allows setting of actions against call signatures either + * at a specific time, or always. Specific time settings + * trump lasting ones, otherwise the most recently added + * will mask an earlier match. + * @package SimpleTest + * @subpackage MockObjects + */ +class SimpleCallSchedule { + private $wildcard = MOCK_ANYTHING; + private $always; + private $at; + + /** + * Sets up an empty response schedule. + * Creates an empty call map. + */ + function __construct() { + $this->always = array(); + $this->at = array(); + } + + /** + * Stores an action against a signature that + * will always fire unless masked by a time + * specific one. + * @param string $method Method name. + * @param array $args Calling parameters. + * @param SimpleAction $action Actually simpleByValue, etc. + */ + function register($method, $args, $action) { + $args = $this->replaceWildcards($args); + $method = strtolower($method); + if (! isset($this->always[$method])) { + $this->always[$method] = new SimpleSignatureMap(); + } + $this->always[$method]->add($args, $action); + } + + /** + * Stores an action against a signature that + * will fire at a specific time in the future. + * @param integer $step delay of calls to this method, + * 0 is next. + * @param string $method Method name. + * @param array $args Calling parameters. + * @param SimpleAction $action Actually SimpleByValue, etc. + */ + function registerAt($step, $method, $args, $action) { + $args = $this->replaceWildcards($args); + $method = strtolower($method); + if (! isset($this->at[$method])) { + $this->at[$method] = array(); + } + if (! isset($this->at[$method][$step])) { + $this->at[$method][$step] = new SimpleSignatureMap(); + } + $this->at[$method][$step]->add($args, $action); + } + + /** + * Sets up an expectation on the argument list. + * @param string $method Method to test. + * @param array $args Bare arguments or list of + * expectation objects. + * @param string $message Failure message. + */ + function expectArguments($method, $args, $message) { + $args = $this->replaceWildcards($args); + $message .= Mock::getExpectationLine(); + $this->expected_args[strtolower($method)] = + new ParametersExpectation($args, $message); + + } + + /** + * Actually carry out the action stored previously, + * if the parameters match. + * @param integer $step Time of call. + * @param string $method Method name. + * @param array $args The parameters making up the + * rest of the call. + * @return mixed The result of the action. + */ + function &respond($step, $method, $args) { + $method = strtolower($method); + if (isset($this->at[$method][$step])) { + if ($this->at[$method][$step]->isMatch($args)) { + $action = $this->at[$method][$step]->findFirstAction($args); + if (isset($action)) { + return $action->act(); + } + } + } + if (isset($this->always[$method])) { + $action = $this->always[$method]->findFirstAction($args); + if (isset($action)) { + return $action->act(); + } + } + $null = null; + return $null; + } + + /** + * Replaces wildcard matches with wildcard + * expectations in the argument list. + * @param array $args Raw argument list. + * @return array Argument list with + * expectations. + */ + protected function replaceWildcards($args) { + if ($args === false) { + return false; + } + for ($i = 0; $i < count($args); $i++) { + if ($args[$i] === $this->wildcard) { + $args[$i] = new AnythingExpectation(); + } + } + return $args; + } +} + +/** + * A type of SimpleMethodAction. + * Stashes a value for returning later. Follows usual + * PHP5 semantics of objects being returned by reference. + * @package SimpleTest + * @subpackage MockObjects + */ +class SimpleReturn { + private $value; + + /** + * Stashes it for later. + * @param mixed $value You need to clone objects + * if you want copy semantics + * for these. + */ + function __construct($value) { + $this->value = $value; + } + + /** + * Returns the value stored earlier. + * @return mixed Whatever was stashed. + */ + function act() { + return $this->value; + } +} + +/** + * A type of SimpleMethodAction. + * Stashes a reference for returning later. + * @package SimpleTest + * @subpackage MockObjects + */ +class SimpleByReference { + private $reference; + + /** + * Stashes it for later. + * @param mixed $reference Actual PHP4 style reference. + */ + function __construct(&$reference) { + $this->reference = &$reference; + } + + /** + * Returns the reference stored earlier. + * @return mixed Whatever was stashed. + */ + function &act() { + return $this->reference; + } +} + +/** + * A type of SimpleMethodAction. + * Stashes a value for returning later. + * @package SimpleTest + * @subpackage MockObjects + */ +class SimpleByValue { + private $value; + + /** + * Stashes it for later. + * @param mixed $value You need to clone objects + * if you want copy semantics + * for these. + */ + function __construct($value) { + $this->value = $value; + } + + /** + * Returns the value stored earlier. + * @return mixed Whatever was stashed. + */ + function &act() { + $dummy = $this->value; + return $dummy; + } +} + +/** + * A type of SimpleMethodAction. + * Stashes an exception for throwing later. + * @package SimpleTest + * @subpackage MockObjects + */ +class SimpleThrower { + private $exception; + + /** + * Stashes it for later. + * @param Exception $exception The exception object to throw. + */ + function __construct($exception) { + $this->exception = $exception; + } + + /** + * Throws the exceptins stashed earlier. + */ + function act() { + throw $this->exception; + } +} + +/** + * A type of SimpleMethodAction. + * Stashes an error for emitting later. + * @package SimpleTest + * @subpackage MockObjects + */ +class SimpleErrorThrower { + private $error; + private $severity; + + /** + * Stashes an error to throw later. + * @param string $error Error message. + * @param integer $severity PHP error constant, e.g E_USER_ERROR. + */ + function __construct($error, $severity) { + $this->error = $error; + $this->severity = $severity; + } + + /** + * Triggers the stashed error. + */ + function &act() { + trigger_error($this->error, $this->severity); + $null = null; + return $null; + } +} + +/** + * A base class or delegate that extends an + * empty collection of methods that can have their + * return values set and expectations made of the + * calls upon them. The mock will assert the + * expectations against it's attached test case in + * addition to the server stub behaviour or returning + * preprogrammed responses. + * @package SimpleTest + * @subpackage MockObjects + */ +class SimpleMock { + private $actions; + private $expectations; + private $wildcard = MOCK_ANYTHING; + private $is_strict = true; + private $call_counts; + private $expected_counts; + private $max_counts; + private $expected_args; + private $expected_args_at; + + /** + * Creates an empty action list and expectation list. + * All call counts are set to zero. + */ + function SimpleMock() { + $this->actions = new SimpleCallSchedule(); + $this->expectations = new SimpleCallSchedule(); + $this->call_counts = array(); + $this->expected_counts = array(); + $this->max_counts = array(); + $this->expected_args = array(); + $this->expected_args_at = array(); + $this->getCurrentTestCase()->tell($this); + } + + /** + * Disables a name check when setting expectations. + * This hack is needed for the partial mocks. + */ + function disableExpectationNameChecks() { + $this->is_strict = false; + } + + /** + * Finds currently running test. + * @return SimpeTestCase Current test case. + */ + protected function getCurrentTestCase() { + return SimpleTest::getContext()->getTest(); + } + + /** + * Die if bad arguments array is passed. + * @param mixed $args The arguments value to be checked. + * @param string $task Description of task attempt. + * @return boolean Valid arguments + */ + protected function checkArgumentsIsArray($args, $task) { + if (! is_array($args)) { + trigger_error( + "Cannot $task as \$args parameter is not an array", + E_USER_ERROR); + } + } + + /** + * Triggers a PHP error if the method is not part + * of this object. + * @param string $method Name of method. + * @param string $task Description of task attempt. + */ + protected function dieOnNoMethod($method, $task) { + if ($this->is_strict && ! method_exists($this, $method)) { + trigger_error( + "Cannot $task as no ${method}() in class " . get_class($this), + E_USER_ERROR); + } + } + + /** + * Replaces wildcard matches with wildcard + * expectations in the argument list. + * @param array $args Raw argument list. + * @return array Argument list with + * expectations. + */ + function replaceWildcards($args) { + if ($args === false) { + return false; + } + for ($i = 0; $i < count($args); $i++) { + if ($args[$i] === $this->wildcard) { + $args[$i] = new AnythingExpectation(); + } + } + return $args; + } + + /** + * Adds one to the call count of a method. + * @param string $method Method called. + * @param array $args Arguments as an array. + */ + protected function addCall($method, $args) { + if (! isset($this->call_counts[$method])) { + $this->call_counts[$method] = 0; + } + $this->call_counts[$method]++; + } + + /** + * Fetches the call count of a method so far. + * @param string $method Method name called. + * @return integer Number of calls so far. + */ + function getCallCount($method) { + $this->dieOnNoMethod($method, "get call count"); + $method = strtolower($method); + if (! isset($this->call_counts[$method])) { + return 0; + } + return $this->call_counts[$method]; + } + + /** + * Sets a return for a parameter list that will + * be passed on by all calls to this method that match. + * @param string $method Method name. + * @param mixed $value Result of call by value/handle. + * @param array $args List of parameters to match + * including wildcards. + */ + function returns($method, $value, $args = false) { + $this->dieOnNoMethod($method, "set return"); + $this->actions->register($method, $args, new SimpleReturn($value)); + } + + /** + * Sets a return for a parameter list that will + * be passed only when the required call count + * is reached. + * @param integer $timing Number of calls in the future + * to which the result applies. If + * not set then all calls will return + * the value. + * @param string $method Method name. + * @param mixed $value Result of call passed. + * @param array $args List of parameters to match + * including wildcards. + */ + function returnsAt($timing, $method, $value, $args = false) { + $this->dieOnNoMethod($method, "set return value sequence"); + $this->actions->registerAt($timing, $method, $args, new SimpleReturn($value)); + } + + /** + * Sets a return for a parameter list that will + * be passed by value for all calls to this method. + * @param string $method Method name. + * @param mixed $value Result of call passed by value. + * @param array $args List of parameters to match + * including wildcards. + */ + function returnsByValue($method, $value, $args = false) { + $this->dieOnNoMethod($method, "set return value"); + $this->actions->register($method, $args, new SimpleByValue($value)); + } + + /** @deprecated */ + function setReturnValue($method, $value, $args = false) { + $this->returnsByValue($method, $value, $args); + } + + /** + * Sets a return for a parameter list that will + * be passed by value only when the required call count + * is reached. + * @param integer $timing Number of calls in the future + * to which the result applies. If + * not set then all calls will return + * the value. + * @param string $method Method name. + * @param mixed $value Result of call passed by value. + * @param array $args List of parameters to match + * including wildcards. + */ + function returnsByValueAt($timing, $method, $value, $args = false) { + $this->dieOnNoMethod($method, "set return value sequence"); + $this->actions->registerAt($timing, $method, $args, new SimpleByValue($value)); + } + + /** @deprecated */ + function setReturnValueAt($timing, $method, $value, $args = false) { + $this->returnsByValueAt($timing, $method, $value, $args); + } + + /** + * Sets a return for a parameter list that will + * be passed by reference for all calls. + * @param string $method Method name. + * @param mixed $reference Result of the call will be this object. + * @param array $args List of parameters to match + * including wildcards. + */ + function returnsByReference($method, &$reference, $args = false) { + $this->dieOnNoMethod($method, "set return reference"); + $this->actions->register($method, $args, new SimpleByReference($reference)); + } + + /** @deprecated */ + function setReturnReference($method, &$reference, $args = false) { + $this->returnsByReference($method, $reference, $args); + } + + /** + * Sets a return for a parameter list that will + * be passed by value only when the required call count + * is reached. + * @param integer $timing Number of calls in the future + * to which the result applies. If + * not set then all calls will return + * the value. + * @param string $method Method name. + * @param mixed $reference Result of the call will be this object. + * @param array $args List of parameters to match + * including wildcards. + */ + function returnsByReferenceAt($timing, $method, &$reference, $args = false) { + $this->dieOnNoMethod($method, "set return reference sequence"); + $this->actions->registerAt($timing, $method, $args, new SimpleByReference($reference)); + } + + /** @deprecated */ + function setReturnReferenceAt($timing, $method, &$reference, $args = false) { + $this->returnsByReferenceAt($timing, $method, $reference, $args); + } + + /** + * Sets up an expected call with a set of + * expected parameters in that call. All + * calls will be compared to these expectations + * regardless of when the call is made. + * @param string $method Method call to test. + * @param array $args Expected parameters for the call + * including wildcards. + * @param string $message Overridden message. + */ + function expect($method, $args, $message = '%s') { + $this->dieOnNoMethod($method, 'set expected arguments'); + $this->checkArgumentsIsArray($args, 'set expected arguments'); + $this->expectations->expectArguments($method, $args, $message); + $args = $this->replaceWildcards($args); + $message .= Mock::getExpectationLine(); + $this->expected_args[strtolower($method)] = + new ParametersExpectation($args, $message); + } + + /** + * Sets up an expected call with a set of + * expected parameters in that call. The + * expected call count will be adjusted if it + * is set too low to reach this call. + * @param integer $timing Number of calls in the future at + * which to test. Next call is 0. + * @param string $method Method call to test. + * @param array $args Expected parameters for the call + * including wildcards. + * @param string $message Overridden message. + */ + function expectAt($timing, $method, $args, $message = '%s') { + $this->dieOnNoMethod($method, 'set expected arguments at time'); + $this->checkArgumentsIsArray($args, 'set expected arguments at time'); + $args = $this->replaceWildcards($args); + if (! isset($this->expected_args_at[$timing])) { + $this->expected_args_at[$timing] = array(); + } + $method = strtolower($method); + $message .= Mock::getExpectationLine(); + $this->expected_args_at[$timing][$method] = + new ParametersExpectation($args, $message); + } + + /** + * Sets an expectation for the number of times + * a method will be called. The tally method + * is used to check this. + * @param string $method Method call to test. + * @param integer $count Number of times it should + * have been called at tally. + * @param string $message Overridden message. + */ + function expectCallCount($method, $count, $message = '%s') { + $this->dieOnNoMethod($method, 'set expected call count'); + $message .= Mock::getExpectationLine(); + $this->expected_counts[strtolower($method)] = + new CallCountExpectation($method, $count, $message); + } + + /** + * Sets the number of times a method may be called + * before a test failure is triggered. + * @param string $method Method call to test. + * @param integer $count Most number of times it should + * have been called. + * @param string $message Overridden message. + */ + function expectMaximumCallCount($method, $count, $message = '%s') { + $this->dieOnNoMethod($method, 'set maximum call count'); + $message .= Mock::getExpectationLine(); + $this->max_counts[strtolower($method)] = + new MaximumCallCountExpectation($method, $count, $message); + } + + /** + * Sets the number of times to call a method to prevent + * a failure on the tally. + * @param string $method Method call to test. + * @param integer $count Least number of times it should + * have been called. + * @param string $message Overridden message. + */ + function expectMinimumCallCount($method, $count, $message = '%s') { + $this->dieOnNoMethod($method, 'set minimum call count'); + $message .= Mock::getExpectationLine(); + $this->expected_counts[strtolower($method)] = + new MinimumCallCountExpectation($method, $count, $message); + } + + /** + * Convenience method for barring a method + * call. + * @param string $method Method call to ban. + * @param string $message Overridden message. + */ + function expectNever($method, $message = '%s') { + $this->expectMaximumCallCount($method, 0, $message); + } + + /** + * Convenience method for a single method + * call. + * @param string $method Method call to track. + * @param array $args Expected argument list or + * false for any arguments. + * @param string $message Overridden message. + */ + function expectOnce($method, $args = false, $message = '%s') { + $this->expectCallCount($method, 1, $message); + if ($args !== false) { + $this->expect($method, $args, $message); + } + } + + /** + * Convenience method for requiring a method + * call. + * @param string $method Method call to track. + * @param array $args Expected argument list or + * false for any arguments. + * @param string $message Overridden message. + */ + function expectAtLeastOnce($method, $args = false, $message = '%s') { + $this->expectMinimumCallCount($method, 1, $message); + if ($args !== false) { + $this->expect($method, $args, $message); + } + } + + /** + * Sets up a trigger to throw an exception upon the + * method call. + * @param string $method Method name to throw on. + * @param object $exception Exception object to throw. + * If not given then a simple + * Exception object is thrown. + * @param array $args Optional argument list filter. + * If given then the exception + * will only be thrown if the + * method call matches the arguments. + */ + function throwOn($method, $exception = false, $args = false) { + $this->dieOnNoMethod($method, "throw on"); + $this->actions->register($method, $args, + new SimpleThrower($exception ? $exception : new Exception())); + } + + /** + * Sets up a trigger to throw an exception upon the + * method call. + * @param integer $timing When to throw the exception. A + * value of 0 throws immediately. + * A value of 1 actually allows one call + * to this method before throwing. 2 + * will allow two calls before throwing + * and so on. + * @param string $method Method name to throw on. + * @param object $exception Exception object to throw. + * If not given then a simple + * Exception object is thrown. + * @param array $args Optional argument list filter. + * If given then the exception + * will only be thrown if the + * method call matches the arguments. + */ + function throwAt($timing, $method, $exception = false, $args = false) { + $this->dieOnNoMethod($method, "throw at"); + $this->actions->registerAt($timing, $method, $args, + new SimpleThrower($exception ? $exception : new Exception())); + } + + /** + * Sets up a trigger to throw an error upon the + * method call. + * @param string $method Method name to throw on. + * @param object $error Error message to trigger. + * @param array $args Optional argument list filter. + * If given then the exception + * will only be thrown if the + * method call matches the arguments. + * @param integer $severity The PHP severity level. Defaults + * to E_USER_ERROR. + */ + function errorOn($method, $error = 'A mock error', $args = false, $severity = E_USER_ERROR) { + $this->dieOnNoMethod($method, "error on"); + $this->actions->register($method, $args, new SimpleErrorThrower($error, $severity)); + } + + /** + * Sets up a trigger to throw an error upon a specific + * method call. + * @param integer $timing When to throw the exception. A + * value of 0 throws immediately. + * A value of 1 actually allows one call + * to this method before throwing. 2 + * will allow two calls before throwing + * and so on. + * @param string $method Method name to throw on. + * @param object $error Error message to trigger. + * @param array $args Optional argument list filter. + * If given then the exception + * will only be thrown if the + * method call matches the arguments. + * @param integer $severity The PHP severity level. Defaults + * to E_USER_ERROR. + */ + function errorAt($timing, $method, $error = 'A mock error', $args = false, $severity = E_USER_ERROR) { + $this->dieOnNoMethod($method, "error at"); + $this->actions->registerAt($timing, $method, $args, new SimpleErrorThrower($error, $severity)); + } + + /** + * Receives event from unit test that the current + * test method has finished. Totals up the call + * counts and triggers a test assertion if a test + * is present for expected call counts. + * @param string $test_method Current method name. + * @param SimpleTestCase $test Test to send message to. + */ + function atTestEnd($test_method, &$test) { + foreach ($this->expected_counts as $method => $expectation) { + $test->assert($expectation, $this->getCallCount($method)); + } + foreach ($this->max_counts as $method => $expectation) { + if ($expectation->test($this->getCallCount($method))) { + $test->assert($expectation, $this->getCallCount($method)); + } + } + } + + /** + * Returns the expected value for the method name + * and checks expectations. Will generate any + * test assertions as a result of expectations + * if there is a test present. + * @param string $method Name of method to simulate. + * @param array $args Arguments as an array. + * @return mixed Stored return. + */ + function &invoke($method, $args) { + $method = strtolower($method); + $step = $this->getCallCount($method); + $this->addCall($method, $args); + $this->checkExpectations($method, $args, $step); + $was = $this->disableEStrict(); + try { + $result = &$this->emulateCall($method, $args, $step); + } catch (Exception $e) { + $this->restoreEStrict($was); + throw $e; + } + $this->restoreEStrict($was); + return $result; + } + + /** + * Finds the return value matching the incoming + * arguments. If there is no matching value found + * then an error is triggered. + * @param string $method Method name. + * @param array $args Calling arguments. + * @param integer $step Current position in the + * call history. + * @return mixed Stored return or other action. + */ + protected function &emulateCall($method, $args, $step) { + return $this->actions->respond($step, $method, $args); + } + + /** + * Tests the arguments against expectations. + * @param string $method Method to check. + * @param array $args Argument list to match. + * @param integer $timing The position of this call + * in the call history. + */ + protected function checkExpectations($method, $args, $timing) { + $test = $this->getCurrentTestCase(); + if (isset($this->max_counts[$method])) { + if (! $this->max_counts[$method]->test($timing + 1)) { + $test->assert($this->max_counts[$method], $timing + 1); + } + } + if (isset($this->expected_args_at[$timing][$method])) { + $test->assert( + $this->expected_args_at[$timing][$method], + $args, + "Mock method [$method] at [$timing] -> %s"); + } elseif (isset($this->expected_args[$method])) { + $test->assert( + $this->expected_args[$method], + $args, + "Mock method [$method] -> %s"); + } + } + + /** + * Our mock has to be able to return anything, including + * variable references. To allow for these mixed returns + * we have to disable the E_STRICT warnings while the + * method calls are emulated. + */ + private function disableEStrict() { + $was = error_reporting(); + error_reporting($was & ~E_STRICT); + return $was; + } + + /** + * Restores the E_STRICT level if it was previously set. + * @param integer $was Previous error reporting level. + */ + private function restoreEStrict($was) { + error_reporting($was); + } +} + +/** + * Static methods only service class for code generation of + * mock objects. + * @package SimpleTest + * @subpackage MockObjects + */ +class Mock { + + /** + * Factory for mock object classes. + */ + function __construct() { + trigger_error('Mock factory methods are static.'); + } + + /** + * Clones a class' interface and creates a mock version + * that can have return values and expectations set. + * @param string $class Class to clone. + * @param string $mock_class New class name. Default is + * the old name with "Mock" + * prepended. + * @param array $methods Additional methods to add beyond + * those in the cloned class. Use this + * to emulate the dynamic addition of + * methods in the cloned class or when + * the class hasn't been written yet.sta + */ + static function generate($class, $mock_class = false, $methods = false) { + $generator = new MockGenerator($class, $mock_class); + return @$generator->generateSubclass($methods); + } + + /** + * Generates a version of a class with selected + * methods mocked only. Inherits the old class + * and chains the mock methods of an aggregated + * mock object. + * @param string $class Class to clone. + * @param string $mock_class New class name. + * @param array $methods Methods to be overridden + * with mock versions. + */ + static function generatePartial($class, $mock_class, $methods) { + $generator = new MockGenerator($class, $mock_class); + return @$generator->generatePartial($methods); + } + + /** + * Uses a stack trace to find the line of an assertion. + */ + static function getExpectationLine() { + $trace = new SimpleStackTrace(array('expect')); + return $trace->traceMethod(); + } +} + +/** + * Service class for code generation of mock objects. + * @package SimpleTest + * @subpackage MockObjects + */ +class MockGenerator { + private $class; + private $mock_class; + private $mock_base; + private $reflection; + + /** + * Builds initial reflection object. + * @param string $class Class to be mocked. + * @param string $mock_class New class with identical interface, + * but no behaviour. + */ + function __construct($class, $mock_class) { + $this->class = $class; + $this->mock_class = $mock_class; + if (! $this->mock_class) { + $this->mock_class = 'Mock' . $this->class; + } + $this->mock_base = SimpleTest::getMockBaseClass(); + $this->reflection = new SimpleReflection($this->class); + } + + /** + * Clones a class' interface and creates a mock version + * that can have return values and expectations set. + * @param array $methods Additional methods to add beyond + * those in th cloned class. Use this + * to emulate the dynamic addition of + * methods in the cloned class or when + * the class hasn't been written yet. + */ + function generate($methods) { + if (! $this->reflection->classOrInterfaceExists()) { + return false; + } + $mock_reflection = new SimpleReflection($this->mock_class); + if ($mock_reflection->classExistsSansAutoload()) { + return false; + } + $code = $this->createClassCode($methods ? $methods : array()); + return eval("$code return \$code;"); + } + + /** + * Subclasses a class and overrides every method with a mock one + * that can have return values and expectations set. Chains + * to an aggregated SimpleMock. + * @param array $methods Additional methods to add beyond + * those in the cloned class. Use this + * to emulate the dynamic addition of + * methods in the cloned class or when + * the class hasn't been written yet. + */ + function generateSubclass($methods) { + if (! $this->reflection->classOrInterfaceExists()) { + return false; + } + $mock_reflection = new SimpleReflection($this->mock_class); + if ($mock_reflection->classExistsSansAutoload()) { + return false; + } + if ($this->reflection->isInterface() || $this->reflection->hasFinal()) { + $code = $this->createClassCode($methods ? $methods : array()); + return eval("$code return \$code;"); + } else { + $code = $this->createSubclassCode($methods ? $methods : array()); + return eval("$code return \$code;"); + } + } + + /** + * Generates a version of a class with selected + * methods mocked only. Inherits the old class + * and chains the mock methods of an aggregated + * mock object. + * @param array $methods Methods to be overridden + * with mock versions. + */ + function generatePartial($methods) { + if (! $this->reflection->classExists($this->class)) { + return false; + } + $mock_reflection = new SimpleReflection($this->mock_class); + if ($mock_reflection->classExistsSansAutoload()) { + trigger_error('Partial mock class [' . $this->mock_class . '] already exists'); + return false; + } + $code = $this->extendClassCode($methods); + return eval("$code return \$code;"); + } + + /** + * The new mock class code as a string. + * @param array $methods Additional methods. + * @return string Code for new mock class. + */ + protected function createClassCode($methods) { + $implements = ''; + $interfaces = $this->reflection->getInterfaces(); + if (function_exists('spl_classes')) { + $interfaces = array_diff($interfaces, array('Traversable')); + } + if (count($interfaces) > 0) { + $implements = 'implements ' . implode(', ', $interfaces); + } + $code = "class " . $this->mock_class . " extends " . $this->mock_base . " $implements {\n"; + $code .= " function " . $this->mock_class . "() {\n"; + $code .= " \$this->" . $this->mock_base . "();\n"; + $code .= " }\n"; + if (in_array('__construct', $this->reflection->getMethods())) { + $code .= " function __construct() {\n"; + $code .= " \$this->" . $this->mock_base . "();\n"; + $code .= " }\n"; + } + $code .= $this->createHandlerCode($methods); + $code .= "}\n"; + return $code; + } + + /** + * The new mock class code as a string. The mock will + * be a subclass of the original mocked class. + * @param array $methods Additional methods. + * @return string Code for new mock class. + */ + protected function createSubclassCode($methods) { + $code = "class " . $this->mock_class . " extends " . $this->class . " {\n"; + $code .= " public \$mock;\n"; + $code .= $this->addMethodList(array_merge($methods, $this->reflection->getMethods())); + $code .= "\n"; + $code .= " function " . $this->mock_class . "() {\n"; + $code .= " \$this->mock = new " . $this->mock_base . "();\n"; + $code .= " \$this->mock->disableExpectationNameChecks();\n"; + $code .= " }\n"; + $code .= $this->chainMockReturns(); + $code .= $this->chainMockExpectations(); + $code .= $this->chainThrowMethods(); + $code .= $this->overrideMethods($this->reflection->getMethods()); + $code .= $this->createNewMethodCode($methods); + $code .= "}\n"; + return $code; + } + + /** + * The extension class code as a string. The class + * composites a mock object and chains mocked methods + * to it. + * @param array $methods Mocked methods. + * @return string Code for a new class. + */ + protected function extendClassCode($methods) { + $code = "class " . $this->mock_class . " extends " . $this->class . " {\n"; + $code .= " protected \$mock;\n"; + $code .= $this->addMethodList($methods); + $code .= "\n"; + $code .= " function " . $this->mock_class . "() {\n"; + $code .= " \$this->mock = new " . $this->mock_base . "();\n"; + $code .= " \$this->mock->disableExpectationNameChecks();\n"; + $code .= " }\n"; + $code .= $this->chainMockReturns(); + $code .= $this->chainMockExpectations(); + $code .= $this->chainThrowMethods(); + $code .= $this->overrideMethods($methods); + $code .= "}\n"; + return $code; + } + + /** + * Creates code within a class to generate replaced + * methods. All methods call the invoke() handler + * with the method name and the arguments in an + * array. + * @param array $methods Additional methods. + */ + protected function createHandlerCode($methods) { + $code = ''; + $methods = array_merge($methods, $this->reflection->getMethods()); + foreach ($methods as $method) { + if ($this->isConstructor($method)) { + continue; + } + $mock_reflection = new SimpleReflection($this->mock_base); + if (in_array($method, $mock_reflection->getMethods())) { + continue; + } + $code .= " " . $this->reflection->getSignature($method) . " {\n"; + $code .= " \$args = func_get_args();\n"; + $code .= " \$result = &\$this->invoke(\"$method\", \$args);\n"; + $code .= " return \$result;\n"; + $code .= " }\n"; + } + return $code; + } + + /** + * Creates code within a class to generate a new + * methods. All methods call the invoke() handler + * on the internal mock with the method name and + * the arguments in an array. + * @param array $methods Additional methods. + */ + protected function createNewMethodCode($methods) { + $code = ''; + foreach ($methods as $method) { + if ($this->isConstructor($method)) { + continue; + } + $mock_reflection = new SimpleReflection($this->mock_base); + if (in_array($method, $mock_reflection->getMethods())) { + continue; + } + $code .= " " . $this->reflection->getSignature($method) . " {\n"; + $code .= " \$args = func_get_args();\n"; + $code .= " \$result = &\$this->mock->invoke(\"$method\", \$args);\n"; + $code .= " return \$result;\n"; + $code .= " }\n"; + } + return $code; + } + + /** + * Tests to see if a special PHP method is about to + * be stubbed by mistake. + * @param string $method Method name. + * @return boolean True if special. + */ + protected function isConstructor($method) { + return in_array( + strtolower($method), + array('__construct', '__destruct')); + } + + /** + * Creates a list of mocked methods for error checking. + * @param array $methods Mocked methods. + * @return string Code for a method list. + */ + protected function addMethodList($methods) { + return " protected \$mocked_methods = array('" . + implode("', '", array_map('strtolower', $methods)) . + "');\n"; + } + + /** + * Creates code to abandon the expectation if not mocked. + * @param string $alias Parameter name of method name. + * @return string Code for bail out. + */ + protected function bailOutIfNotMocked($alias) { + $code = " if (! in_array(strtolower($alias), \$this->mocked_methods)) {\n"; + $code .= " trigger_error(\"Method [$alias] is not mocked\");\n"; + $code .= " \$null = null;\n"; + $code .= " return \$null;\n"; + $code .= " }\n"; + return $code; + } + + /** + * Creates source code for chaining to the composited + * mock object. + * @return string Code for mock set up. + */ + protected function chainMockReturns() { + $code = " function returns(\$method, \$value, \$args = false) {\n"; + $code .= $this->bailOutIfNotMocked("\$method"); + $code .= " \$this->mock->returns(\$method, \$value, \$args);\n"; + $code .= " }\n"; + $code .= " function returnsAt(\$timing, \$method, \$value, \$args = false) {\n"; + $code .= $this->bailOutIfNotMocked("\$method"); + $code .= " \$this->mock->returnsAt(\$timing, \$method, \$value, \$args);\n"; + $code .= " }\n"; + $code .= " function returnsByValue(\$method, \$value, \$args = false) {\n"; + $code .= $this->bailOutIfNotMocked("\$method"); + $code .= " \$this->mock->setReturnValue(\$method, \$value, \$args);\n"; + $code .= " }\n"; + $code .= " function returnsByValueAt(\$timing, \$method, \$value, \$args = false) {\n"; + $code .= $this->bailOutIfNotMocked("\$method"); + $code .= " \$this->mock->setReturnValueAt(\$timing, \$method, \$value, \$args);\n"; + $code .= " }\n"; + $code .= " function returnsByReference(\$method, &\$ref, \$args = false) {\n"; + $code .= $this->bailOutIfNotMocked("\$method"); + $code .= " \$this->mock->setReturnReference(\$method, \$ref, \$args);\n"; + $code .= " }\n"; + $code .= " function returnsByReferenceAt(\$timing, \$method, &\$ref, \$args = false) {\n"; + $code .= $this->bailOutIfNotMocked("\$method"); + $code .= " \$this->mock->setReturnReferenceAt(\$timing, \$method, \$ref, \$args);\n"; + $code .= " }\n"; + $code .= " function setReturnValue(\$method, \$value, \$args = false) {\n"; + $code .= $this->bailOutIfNotMocked("\$method"); + $code .= " \$this->mock->setReturnValue(\$method, \$value, \$args);\n"; + $code .= " }\n"; + $code .= " function setReturnValueAt(\$timing, \$method, \$value, \$args = false) {\n"; + $code .= $this->bailOutIfNotMocked("\$method"); + $code .= " \$this->mock->setReturnValueAt(\$timing, \$method, \$value, \$args);\n"; + $code .= " }\n"; + $code .= " function setReturnReference(\$method, &\$ref, \$args = false) {\n"; + $code .= $this->bailOutIfNotMocked("\$method"); + $code .= " \$this->mock->setReturnReference(\$method, \$ref, \$args);\n"; + $code .= " }\n"; + $code .= " function setReturnReferenceAt(\$timing, \$method, &\$ref, \$args = false) {\n"; + $code .= $this->bailOutIfNotMocked("\$method"); + $code .= " \$this->mock->setReturnReferenceAt(\$timing, \$method, \$ref, \$args);\n"; + $code .= " }\n"; + return $code; + } + + /** + * Creates source code for chaining to an aggregated + * mock object. + * @return string Code for expectations. + */ + protected function chainMockExpectations() { + $code = " function expect(\$method, \$args = false, \$msg = '%s') {\n"; + $code .= $this->bailOutIfNotMocked("\$method"); + $code .= " \$this->mock->expect(\$method, \$args, \$msg);\n"; + $code .= " }\n"; + $code .= " function expectAt(\$timing, \$method, \$args = false, \$msg = '%s') {\n"; + $code .= $this->bailOutIfNotMocked("\$method"); + $code .= " \$this->mock->expectAt(\$timing, \$method, \$args, \$msg);\n"; + $code .= " }\n"; + $code .= " function expectCallCount(\$method, \$count) {\n"; + $code .= $this->bailOutIfNotMocked("\$method"); + $code .= " \$this->mock->expectCallCount(\$method, \$count, \$msg = '%s');\n"; + $code .= " }\n"; + $code .= " function expectMaximumCallCount(\$method, \$count, \$msg = '%s') {\n"; + $code .= $this->bailOutIfNotMocked("\$method"); + $code .= " \$this->mock->expectMaximumCallCount(\$method, \$count, \$msg = '%s');\n"; + $code .= " }\n"; + $code .= " function expectMinimumCallCount(\$method, \$count, \$msg = '%s') {\n"; + $code .= $this->bailOutIfNotMocked("\$method"); + $code .= " \$this->mock->expectMinimumCallCount(\$method, \$count, \$msg = '%s');\n"; + $code .= " }\n"; + $code .= " function expectNever(\$method) {\n"; + $code .= $this->bailOutIfNotMocked("\$method"); + $code .= " \$this->mock->expectNever(\$method);\n"; + $code .= " }\n"; + $code .= " function expectOnce(\$method, \$args = false, \$msg = '%s') {\n"; + $code .= $this->bailOutIfNotMocked("\$method"); + $code .= " \$this->mock->expectOnce(\$method, \$args, \$msg);\n"; + $code .= " }\n"; + $code .= " function expectAtLeastOnce(\$method, \$args = false, \$msg = '%s') {\n"; + $code .= $this->bailOutIfNotMocked("\$method"); + $code .= " \$this->mock->expectAtLeastOnce(\$method, \$args, \$msg);\n"; + $code .= " }\n"; + return $code; + } + + /** + * Adds code for chaining the throw methods. + * @return string Code for chains. + */ + protected function chainThrowMethods() { + $code = " function throwOn(\$method, \$exception = false, \$args = false) {\n"; + $code .= $this->bailOutIfNotMocked("\$method"); + $code .= " \$this->mock->throwOn(\$method, \$exception, \$args);\n"; + $code .= " }\n"; + $code .= " function throwAt(\$timing, \$method, \$exception = false, \$args = false) {\n"; + $code .= $this->bailOutIfNotMocked("\$method"); + $code .= " \$this->mock->throwAt(\$timing, \$method, \$exception, \$args);\n"; + $code .= " }\n"; + $code .= " function errorOn(\$method, \$error = 'A mock error', \$args = false, \$severity = E_USER_ERROR) {\n"; + $code .= $this->bailOutIfNotMocked("\$method"); + $code .= " \$this->mock->errorOn(\$method, \$error, \$args, \$severity);\n"; + $code .= " }\n"; + $code .= " function errorAt(\$timing, \$method, \$error = 'A mock error', \$args = false, \$severity = E_USER_ERROR) {\n"; + $code .= $this->bailOutIfNotMocked("\$method"); + $code .= " \$this->mock->errorAt(\$timing, \$method, \$error, \$args, \$severity);\n"; + $code .= " }\n"; + return $code; + } + + /** + * Creates source code to override a list of methods + * with mock versions. + * @param array $methods Methods to be overridden + * with mock versions. + * @return string Code for overridden chains. + */ + protected function overrideMethods($methods) { + $code = ""; + foreach ($methods as $method) { + if ($this->isConstructor($method)) { + continue; + } + $code .= " " . $this->reflection->getSignature($method) . " {\n"; + $code .= " \$args = func_get_args();\n"; + $code .= " \$result = &\$this->mock->invoke(\"$method\", \$args);\n"; + $code .= " return \$result;\n"; + $code .= " }\n"; + } + return $code; + } +} +?>
\ No newline at end of file |