Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Give Mocker the ability to Mock non li3 classes. #802

Merged
merged 1 commit into from
Feb 4, 2013
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion analysis/Debugger.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
* The `Debugger` class provides basic facilities for generating and rendering meta-data about the
* state of an application in its current context.
*/
class Debugger extends \lithium\core\StaticObject {
class Debugger {

/**
* Used for temporary closure caching.
Expand Down
211 changes: 171 additions & 40 deletions test/Mocker.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
namespace lithium\test;

use lithium\util\String;
use lithium\util\collection\Filters;
use ReflectionClass;
use ReflectionMethod;
use Reflection;
Expand Down Expand Up @@ -61,6 +62,13 @@
*/
class Mocker {

/**
* Stores the closures that represent the method filters. They are indexed by called class.
*
* @var array Method filters, indexed by class.
*/
protected static $_methodFilters = array();

/**
* A list of code to be generated for the delegator.
*
Expand All @@ -78,18 +86,19 @@ class Mocker {
),
'constructor' => array(
'{:modifiers} function __construct({:args}) {',
' $args = func_get_args();',
' $this->parent = array_pop($args);',
' $args = compact({:stringArgs});',
' $this->parent = func_get_arg(count(func_get_args()) - 1);',
' $this->parent->mocker = $this;',
' call_user_func_array("parent::__construct", $args);',
' if (method_exists("{:mocker}", "__construct")) {',
' call_user_func_array("parent::__construct", $args);',
' }',
'}',
),
'method' => array(
'{:modifiers} function {:method}({:args}) {',
' $args = func_get_args();',
' $args = compact({:stringArgs});',
' $token = spl_object_hash($this);',
' $id = count($args) - 1;',
' if (!isset($args[$id]) || $args[$id] !== $token) {',
' if (func_get_arg(count(func_get_args()) - 1) !== $token) {',
' $method = array($this->parent, "{:method}");',
' return call_user_func_array($method, $args);',
' }',
Expand All @@ -98,10 +107,10 @@ class Mocker {
),
'staticMethod' => array(
'{:modifiers} function {:method}({:args}) {',
' $args = func_get_args();',
' $args = compact({:stringArgs});',
' $token = "1f3870be274f6c49b3e31a0c6728957f";',
' $id = count($args) - 1;',
' if (!isset($args[$id]) || $args[$id] !== $token) {',
' $id = func_get_arg(count(func_get_args()) - 1);',
' if (func_get_arg(count(func_get_args()) - 1) !== $token) {',
' $method = \'{:namespace}\Mock::{:method}\';',
' return call_user_func_array($method, $args);',
' }',
Expand Down Expand Up @@ -131,13 +140,15 @@ class Mocker {
'namespace {:namespace};',
'class Mock extends \{:mocker} {',
' public $mocker;',
' public {:static} $results = array();',
' public $results = array();',
' public static $staticResults = array();',
' protected $_safeVars = array(',
' "_classes",',
' "_methodFilters",',
' "mocker",',
' "_safeVars",',
' "results",',
' "staticResults",',
' );',
),
'get' => array(
Expand All @@ -153,7 +164,7 @@ class Mocker {
),
'constructor' => array(
'{:modifiers} function __construct({:args}) {',
' $args = array_values(get_defined_vars());',
' $args = compact({:stringArgs});',
' array_push($args, $this);',
' foreach ($this as $key => $value) {',
' if (!in_array($key, $this->_safeVars)) {',
Expand All @@ -172,13 +183,18 @@ class Mocker {
' $args = compact({:stringArgs});',
' $args["hash"] = "1f3870be274f6c49b3e31a0c6728957f";',
' $method = \'{:namespace}\MockDelegate::{:method}\';',
' $result = self::_filter("{:method}", $args, function($self, $args) use(&$method) {',
' return call_user_func_array($method, $args);',
' });',
' if (!isset(self::$results["{:method}"])) {',
' self::$results["{:method}"] = array();',
' $result = {:master}::invokeMethod("_filter", array(',
' __CLASS__, ',
' "{:method}",',
' $args,',
' function($self, $args) use(&$method) {',
' return call_user_func_array($method, $args);',
' }',
' ));',
' if (!isset(self::$staticResults["{:method}"])) {',
' self::$staticResults["{:method}"] = array();',
' }',
' self::$results["{:method}"][] = array(',
' self::$staticResults["{:method}"][] = array(',
' "args" => func_get_args(),',
' "result" => $result,',
' "time" => microtime(true),',
Expand All @@ -191,9 +207,14 @@ class Mocker {
' $args = compact({:stringArgs});',
' $args["hash"] = spl_object_hash($this->mocker);',
' $method = array($this->mocker, "{:method}");',
' $result = $this->_filter(__METHOD__, $args, function($self, $args) use(&$method) {',
' return call_user_func_array($method, $args);',
' });',
' $result = {:master}::invokeMethod("_filter", array(',
' __CLASS__,',
' "{:method}",',
' $args,',
' function($self, $args) use(&$method) {',
' return call_user_func_array($method, $args);',
' }',
' ));',
' if (!isset($this->results["{:method}"])) {',
' $this->results["{:method}"] = array();',
' }',
Expand All @@ -205,6 +226,11 @@ class Mocker {
' return $result;',
'}',
),
'applyFilter' => array(
'public {:static} function applyFilter($method, $filter = null) {',
' return {:master}::applyFilter(__CLASS__, $method, $filter);',
'}',
),
'endClass' => array(
'}',
),
Expand Down Expand Up @@ -259,10 +285,15 @@ public static function create($mockee) {
$reflectedClass = new ReflectionClass($mocker);
$reflecedMethods = $reflectedClass->getMethods();
$getByReference = false;
$staticApplyFilter = true;
$constructor = false;
foreach ($reflecedMethods as $methodId => $method) {
if (!in_array($method->name, self::$_blackList)) {
$key = $method->isStatic() ? 'staticMethod' : 'method';
$key = $method->name === '__construct' ? 'constructor' : $key;
if ($method->name === '__construct') {
$key = 'constructor';
$constructor = true;
}
$docs = ReflectionMethod::export($mocker, $method->name, true);
if (preg_match('/&' . $method->name . '/', $docs) === 1) {
continue;
Expand All @@ -280,14 +311,31 @@ public static function create($mockee) {
} elseif ($method->name === '__get') {
$docs = ReflectionMethod::export($mocker, '__get', true);
$getByReference = preg_match('/&__get/', $docs) === 1;
} elseif ($method->name === 'applyFilter') {
$staticApplyFilter = $method->isStatic();
}
}

if (!$constructor) {
$tokens = array(
'namespace' => self::_namespace($mockee),
'modifiers' => 'public',
'args' => null,
'stringArgs' => 'array()',
'mocker' => $mocker,
);
$mock .= self::_dynamicCode('mock', 'constructor', $tokens);
$mockDelegate .= self::_dynamicCode('mockDelegate', 'constructor', $tokens);
}

$mockDelegate .= self::_dynamicCode('mockDelegate', 'endClass');
$mock .= self::_dynamicCode('mock', 'get', array(
'reference' => $getByReference ? '&' : '',
));
$mock .= self::_dynamicCode('mock', 'set');
$mock .= self::_dynamicCode('mock', 'applyFilter', array(
'static' => $staticApplyFilter ? 'static' : '',
));
$mock .= self::_dynamicCode('mock', 'destructor');
$mock .= self::_dynamicCode('mock', 'endClass');

Expand Down Expand Up @@ -352,6 +400,10 @@ protected static function _stringMethodParams(ReflectionMethod $method) {
* @return string
*/
protected static function _dynamicCode($type, $key, $tokens = array()) {
$defaults = array(
'master' => '\lithium\test\Mocker',
);
$tokens += $defaults;
$name = '_' . $type . 'Ingredients';
$code = implode("\n", self::${$name}[$key]);
return String::insert($code, $tokens) . "\n";
Expand All @@ -364,12 +416,10 @@ protected static function _dynamicCode($type, $key, $tokens = array()) {
* @return array
*/
protected static function _mocker($mockee) {
$matches = array();
preg_match_all('/^(.*)\\\\([^\\\\]+)\\\\Mock$/', $mockee, $matches);
if (!isset($matches[1][0])) {
return;
}
return $matches[1][0] . '\\' . ucfirst($matches[2][0]);
$sections = explode('\\', $mockee);
array_pop($sections);
$sections[] = ucfirst(array_pop($sections));
return implode('\\', $sections);
}

/**
Expand All @@ -387,20 +437,14 @@ protected static function _namespace($mockee) {
/**
* Will validate if mockee is a valid class we should mock.
*
* Will fail if the mock already exists, or it doesn't contain `\Mock` in
* the namespace.
*
* @param string $mockee The fully namespaced `\Mock` class
* @return bool
*/
protected static function _validateMockee($mockee) {
if (class_exists($mockee) || preg_match('/\\\\Mock$/', $mockee) !== 1) {
return false;
}
$mocker = self::_mocker($mockee);
$isObject = is_subclass_of($mocker, 'lithium\core\Object');
$isStatic = is_subclass_of($mocker, 'lithium\core\StaticObject');
if (!$isObject && !$isStatic) {
return false;
}
return true;
return preg_match('/\\\\Mock$/', $mockee) === 1;
}

/**
Expand All @@ -412,13 +456,100 @@ protected static function _validateMockee($mockee) {
public static function chain($mock) {
$results = array();
if (is_object($mock) && isset($mock->results)) {
$results = $mock->results;
} elseif (is_string($mock) && class_exists($mock) && isset($mock::$results)) {
$results = $mock::$results;
$results = static::mergeResults($mock->results, $mock::$staticResults);
} elseif (is_string($mock) && class_exists($mock) && isset($mock::$staticResults)) {
$results = $mock::$staticResults;
}
return new MockerChain($results);
}

/**
* Will merge two sets of results into each other.
*
* @param array $results
* @param array $secondary
* @return array
*/
public static function mergeResults($results, $secondary) {
foreach ($results as $method => $calls) {
if (isset($secondary[$method])) {
$results['method1'] = array_merge($results['method1'], $secondary['method1']);
usort($results['method1'], function($el1, $el2) {
return strcmp($el1['time'], $el2['time']);
});
unset($secondary['method1']);
}
}
return $results + $secondary;
}

/**
* Apply a closure to a method of the current static object.
*
* @see lithium\core\StaticObject::_filter()
* @see lithium\util\collection\Filters
* @param string $class Fully namespaced class to apply filters.
* @param mixed $method The name of the method to apply the closure to. Can either be a single
* method name as a string, or an array of method names. Can also be false to remove
* all filters on the current object.
* @param closure $filter The closure that is used to filter the method(s), can also be false
* to remove all the current filters for the given method.
* @return void
*/
public static function applyFilter($class, $method, $filter = null) {
if ($method === false) {
static::$_methodFilters[$class] = array();
return;
}
foreach ((array) $method as $m) {
if (!isset(static::$_methodFilters[$class][$m]) || $filter === false) {
static::$_methodFilters[$class][$m] = array();
}
if ($filter !== false) {
static::$_methodFilters[$class][$m][] = $filter;
}
}
}

/**
* Executes a set of filters against a method by taking a method's main implementation as a
* callback, and iteratively wrapping the filters around it.
*
* @see lithium\util\collection\Filters
* @param string $class Fully namespaced class to apply filters.
* @param string|array $method The name of the method being executed, or an array containing
* the name of the class that defined the method, and the method name.
* @param array $params An associative array containing all the parameters passed into
* the method.
* @param Closure $callback The method's implementation, wrapped in a closure.
* @param array $filters Additional filters to apply to the method for this call only.
* @return mixed
*/
protected static function _filter($class, $method, $params, $callback, $filters = array()) {
$hasNoFilters = empty(static::$_methodFilters[$class][$method]);
if ($hasNoFilters && !$filters && !Filters::hasApplied($class, $method)) {
return $callback($class, $params, null);
}
if (!isset(static::$_methodFilters[$class][$method])) {
static::$_methodFilters += array($class => array());
static::$_methodFilters[$class][$method] = array();
}
$data = array_merge(static::$_methodFilters[$class][$method], $filters, array($callback));
return Filters::run($class, $params, compact('data', 'class', 'method'));
}

/**
* Calls a method on this object with the given parameters. Provides an OO wrapper for
* `forward_static_call_array()`.
*
* @param string $method Name of the method to call.
* @param array $params Parameter list to use when calling `$method`.
* @return mixed Returns the result of the method call.
*/
public static function invokeMethod($method, $params = array()) {
return forward_static_call_array(array(get_called_class(), $method), $params);
}

}

?>
17 changes: 17 additions & 0 deletions tests/cases/test/MockerChainTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,23 @@ public function testStartSuccessful() {
$this->assertTrue($chain->success());
}

public function testStaticSuccessful() {
$class = '\lithium\tests\mocks\test\mockStdStaticClass\Mock';
$class::applyFilter(false);
$chain = Mocker::chain($class);

$this->assertTrue($chain->success());
}

public function testBasicStaticCalled() {
$class = '\lithium\tests\mocks\test\mockStdStaticClass\Mock';
$class::applyFilter(false);
$class::method1();
$chain = Mocker::chain($class);

$this->assertTrue($chain->called('method1')->success());
}

public function testBasicNotCalled() {
$mock = new \lithium\tests\mocks\test\mockStdClass\Mock();
$chain = Mocker::chain($mock);
Expand Down
Loading