You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

2020 lines
63 KiB

<?php
/**
* li₃: the most RAD framework for PHP (http://li3.me)
*
* Copyright 2009, Union of RAD. All rights reserved. This source
* code is distributed under the terms of the BSD 3-Clause License.
* The full license text can be found in the LICENSE.txt file.
*/
namespace lithium\test;
use Error;
use Exception;
use ErrorException;
use ReflectionClass;
use InvalidArgumentException;
use lithium\aop\Filters;
use lithium\util\Text;
use lithium\core\Libraries;
use lithium\util\Validator;
use lithium\analysis\Debugger;
use lithium\analysis\Inspector;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
/**
* This is the base class for all test cases. Test are performed using an assertion method.
* If the assertion is correct, the test passes, otherwise it fails. Most assertions take an
* expected result, a received result, and a message (to describe the failure) as parameters.
*
* Unit tests are used to check a small unit of functionality, such as if a method returns an
* expected result for a known input, or whether an adapter can successfully open a connection.
*
* Available assertions are (see `assert<assertion-name>` methods for details): Equal, False,
* Identical, NoPattern, NotEqual, Null, Pattern, Tags, True.
*
* If an assertion is expected to produce an exception, `assertException()` should be used.
*
* @see lithium\test\Unit::assertException()
*/
class Unit extends \lithium\core\ObjectDeprecated {
/**
* The Reference to a test reporter class.
*
* @var string
*/
protected $_reporter = null;
/**
* The list of test results.
*
* @var string
*/
protected $_results = [];
/**
* Internal types and how to test for them
*
* @var array
*/
protected static $_internalTypes = [
'array' => 'is_array',
'bool' => 'is_bool',
'boolean' => 'is_bool',
'callable' => 'is_callable',
'double' => 'is_double',
'float' => 'is_float',
'int' => 'is_int',
'integer' => 'is_integer',
'long' => 'is_long',
'null' => 'is_null',
'numeric' => 'is_numeric',
'object' => 'is_object',
'real' => 'is_real',
'resource' => 'is_resource',
'scalar' => 'is_scalar',
'string' => 'is_string'
];
/**
* Finds the test case for the corresponding class name.
*
* @param string $class A fully-namespaced class reference for which to find a test case.
* @return string Returns the class name of a test case for `$class`, or `null` if none exists.
*/
public static function get($class) {
$parts = explode('\\', $class);
$library = array_shift($parts);
$name = array_pop($parts);
$type = 'tests.cases.' . implode('.', $parts);
return Libraries::locate($type, $name, compact('library'));
}
/**
* Setup method run before every test method. Override in subclasses.
*
* @return void
*/
public function setUp() {}
/**
* Teardown method run after every test method. Override in subclasses.
*
* @return void
*/
public function tearDown() {}
/**
* Subclasses should use this method to set conditions that, if failed, terminate further
* testing.
*
* For example:
* ```
* public function skip() {
* $connection = Connections::get('test', ['config' => true]);
* $this->skipIf(!$connection, 'Test database is unavailable.');
* }
* ```
*/
public function skip() {}
/**
* Skips test(s) if the condition is met.
*
* When used within a subclass' `skip` method, all tests are ignored if the condition is
* met, otherwise processing continues as normal. For other methods, only the remainder of
* the method is skipped, when the condition is met.
*
* @throws Exception
* @param boolean $condition
* @param string|boolean $message Message to pass if the condition is met.
* @return mixed
*/
public function skipIf($condition, $message = false) {
if ($condition) {
throw new Exception(is_string($message) ? $message : null);
}
}
/**
* Returns the class name that is the subject under test for this test case.
*
* @return string
*/
public function subject() {
return preg_replace('/Test$/', '', str_replace('tests\\cases\\', '', get_class($this)));
}
/**
* Return test methods to run.
*
* @return array
*/
public function methods() {
static $methods;
return $methods ?: $methods = array_values(preg_grep('/^test/', get_class_methods($this)));
}
/**
* Returns the current results.
*
* @return array The Results... currently.
*/
public function results() {
return $this->_results;
}
/**
* Runs the test methods in this test case, with the given options.
*
* Installs a temporary error handler that will convert regular errors to
* exceptions in order to make both errors and exceptions be handled
* in a unified way. ErrorExceptions created like this, will get the
* error's code as their severity. As this comes closest to their meaning.
*
* The error handler honors the PHP `error_level` and will not convert errors
* to exceptions if they are masked by the `error_level`. This allows test
* methods to run assertions against i.e. deprecated functions. Usually
* the error_level is set by the test runner so that all errors are converted.
*
* @link http://php.net/manual/function.error-reporting.php
* @param array $options The options to use when running the test. Available options are:
* - `'methods'`: An arbitrary array of method names to execute. If
* unspecified, all methods starting with 'test' are run.
* - `'reporter'`: A closure which gets called after each test result,
* which may modify the results presented.
* - `'handler'`: A closure which gets registered as the temporary error handler.
* @return array
*/
public function run(array $options = []) {
$defaults = [
'methods' => $this->methods(),
'reporter' => $this->_reporter,
'handler' => function($code, $message, $file = null, $line = null) {
if (error_reporting() & $code) {
throw new ErrorException($message, 0, $code, $file, $line);
}
}
];
$options += $defaults;
$this->_results = [];
$this->_reporter = $options['reporter'];
try {
$this->skip();
} catch (Exception $e) {
$this->_handleException($e);
return $this->_results;
}
set_error_handler($options['handler']);
foreach ($options['methods'] as $method) {
if ($this->_runTestMethod($method, $options) === false) {
break;
}
}
restore_error_handler();
return $this->_results;
}
/**
* General assert method used by others for common output.
*
* @param boolean $expression
* @param string|boolean $message The message to output. If the message is not a string,
* then it will be converted to '{:message}'. Use '{:message}' in the string and it
* will use the `$data` to format the message with `Text::insert()`.
* @param array $data
* @return boolean `true` if the assertion succeeded, `false` otherwise.
*/
public function assert($expression, $message = false, $data = []) {
if (!is_string($message)) {
$message = '{:message}';
}
if (strpos($message, "{:message}") !== false) {
$params = $data;
$params['message'] = $this->_message($params);
$message = Text::insert($message, $params);
}
$trace = Debugger::trace([
'start' => 1, 'depth' => 4, 'format' => 'array', 'closures' => !$expression
]);
$methods = $this->methods();
$i = 1;
while ($i < count($trace)) {
if (in_array($trace[$i]['function'], $methods) && $trace[$i - 1]['object'] == $this) {
break;
}
$i++;
}
$class = isset($trace[$i - 1]['object']) ? get_class($trace[$i - 1]['object']) : null;
$method = isset($trace[$i]) ? $trace[$i]['function'] : $trace[$i - 1]['function'];
$result = compact('class', 'method', 'message', 'data') + [
'file' => $trace[$i - 1]['file'],
'line' => $trace[$i - 1]['line'],
'assertion' => $trace[$i - 1]['function']
];
$this->_result($expression ? 'pass' : 'fail', $result);
return $expression;
}
/**
* Generates a failed test with the given message.
*
* @param string $message
*/
public function fail($message = false) {
$this->assert(false, $message);
}
/**
* Assert that the actual result is equal, but not neccessarily identical, to the expected
* result.
*
* @see lithium\test\Unit::assert()
* @param mixed $expected
* @param mixed $result
* @param string|boolean $message
* @return boolean `true` if the assertion succeeded, `false` otherwise.
*/
public function assertEqual($expected, $result, $message = '{:message}') {
list($expected, $result) = $this->_normalizeLineEndings($expected, $result);
$data = ($expected != $result) ? $this->_compare('equal', $expected, $result) : null;
return $this->assert($expected == $result, $message, $data);
}
/**
* Assert that the actual result and the expected result are *not* equal to each other.
*
* @see lithium\test\Unit::assert()
* @param mixed $expected
* @param mixed $result
* @param string|boolean $message
* @return boolean `true` if the assertion succeeded, `false` otherwise.
*/
public function assertNotEqual($expected, $result, $message = '{:message}') {
list($expected, $result) = $this->_normalizeLineEndings($expected, $result);
return $this->assert($result != $expected, $message, compact('expected', 'result'));
}
/**
* Assert that the actual result and the expected result are identical using a strict
* comparison.
*
* @see lithium\test\Unit::assert()
* @param mixed $expected
* @param mixed $result
* @param string|boolean $message
* @return boolean `true` if the assertion succeeded, `false` otherwise.
*/
public function assertIdentical($expected, $result, $message = '{:message}') {
$data = ($expected !== $result) ? $this->_compare('identical', $expected, $result) : null;
return $this->assert($expected === $result, $message, $data);
}
/**
* Assert that the actual result and the expected result are *not* identical using a strict
* comparison.
*
* @see lithium\test\Unit::assert()
* @param mixed $expected
* @param mixed $result
* @param string|boolean $message
* @return boolean `true` if the assertion succeeded, `false` otherwise.
*/
public function assertNotIdentical($expected, $result, $message = '{:message}') {
return $this->assert($expected !== $result, $message, compact('expected', 'result'));
}
/**
* Assert that the result is strictly `true`.
*
* ```
* $this->assertTrue(true, 'Boolean true'); // succeeds
* $this->assertTrue('false', 'String has content'); // fails
* $this->assertTrue(10, 'Non-Zero value'); // fails
* ```
*
* @see lithium\test\Unit::assert()
* @param mixed $result
* @param string $message
* @return boolean `true` if the assertion succeeded, `false` otherwise.
*/
public function assertTrue($result, $message = '{:message}') {
$expected = true;
return $this->assert($result === $expected, $message, compact('expected', 'result'));
}
/**
* Assert that the result strictly is `false`.
*
* ```
* $this->assertFalse(false, 'Boolean false'); // succeeds
* $this->assertFalse('', 'String is empty'); // fails
* $this->assertFalse(0, 'Zero value'); // fails
* ```
*
* @see lithium\test\Unit::assert()
* @param mixed $result
* @param string $message
* @return boolean `true` if the assertion succeeded, `false` otherwise.
*/
public function assertFalse($result, $message = '{:message}') {
$expected = false;
return $this->assert($result === $expected, $message, compact('expected', 'result'));
}
/**
* Assert that the result is strictly `null`.
*
* @see lithium\test\Unit::assert()
* @param mixed $result
* @param string $message
* @return boolean `true` if the assertion succeeded, `false` otherwise.
*/
public function assertNull($result, $message = '{:message}') {
$expected = null;
return $this->assert($result === null, $message, compact('expected', 'result'));
}
/**
* Assert that the result is *not* strictly `null`.
*
* ```
* $this->assertNotNull(1); // succeeds
* $this->assertNotNull(null); // fails
* ```
*
* @see lithium\test\Unit::assert()
* @param mixed $result
* @param string $message
* @return boolean `true` if the assertion succeeded, `false` otherwise.
*/
public function assertNotNull($actual, $message = '{:message}') {
return $this->assert($actual !== null, $message, [
'expected' => null,
'actual' => gettype($actual)
]);
}
/**
* Assert that given result is empty.
*
* ```
* $this->assertEmpty(''); // succeeds
* $this->assertEmpty(0); // succeeds
* $this->assertEmpty(0.0); // succeeds
* $this->assertEmpty('0'); // succeeds
* $this->assertEmpty(null); // succeeds
* $this->assertEmpty(false); // succeeds
* $this->assertEmpty([]); // succeeds
* $this->assertEmpty(1); // fails
* ```
*
* @link http://php.net/empty
* @see lithium\test\Unit::assert()
* @param string $actual
* @param string|boolean $message
* @return boolean `true` if the assertion succeeded, `false` otherwise.
*/
public function assertEmpty($actual, $message = '{:message}') {
return $this->assert(empty($actual), $message, [
'expected' => $actual,
'result' => empty($actual)
]);
}
/**
* Assert that given result is *not* empty.
*
* ```
* $this->assertNotEmpty(1); // succeeds
* $this->assertNotEmpty([]); // fails
* ```
*
* @link http://php.net/empty
* @see lithium\test\Unit::assert()
* @param string $actual
* @param string|boolean $message
* @return boolean `true` if the assertion succeeded, `false` otherwise.
*/
public function assertNotEmpty($actual, $message = '{:message}') {
return $this->assert(!empty($actual), $message, [
'expected' => $actual,
'result' => !empty($actual)
]);
}
/**
* Assert that the code passed in a closure throws an exception or raises a PHP error. The
* first argument to this method specifies which class name or message the exception must
* have in order to make the assertion successful.
*
* @see lithium\test\Unit::assert()
* @param mixed $expected A string indicating what the error text is expected to be. This can
* be an exact string, a /-delimited regular expression, or true, indicating that
* any error text is acceptable.
* @param \Closure $closure A closure containing the code that should throw the exception.
* @param string $message
* @return boolean `true` if the assertion succeeded, `false` otherwise.
*/
public function assertException($expected, $closure, $message = '{:message}') {
$result = null;
try {
$closure();
$message = sprintf('An exception "%s" was expected but not thrown.', $expected);
return $this->assert(false, $message, compact('expected', 'result'));
} catch (Exception $e) {
// fallthrough
} catch (Error $e) {
// fallthrough
}
$class = get_class($e);
$eMessage = $e->getMessage();
if (get_class($e) === $expected) {
$result = $class;
return $this->assert(true, $message, compact('expected', 'result'));
}
if ($eMessage === $expected) {
$result = $eMessage;
return $this->assert(true, $message, compact('expected', 'result'));
}
if (Validator::isRegex($expected) && preg_match($expected, $eMessage)) {
$result = $eMessage;
return $this->assert(true, $message, compact('expected', 'result'));
}
$message = sprintf(
'Exception "%s" was expected. Exception "%s" with message "%s" was thrown instead.',
$expected, get_class($e), $eMessage
);
return $this->assert(false, $message);
}
/**
* Assert that the code passed in a closure does not throw an exception matching the passed
* expected exception.
*
* The value passed to `exepected` is either an exception class name or the expected message.
*
* @see lithium\test\Unit::assert()
* @param mixed $expected A string indicating what the error text is not expected to be. This
* can be an exact string, a /-delimited regular expression, or true, indicating
* that any error text is acceptable.
* @param \Closure $closure A closure containing the code that should throw the exception.
* @param string $message
* @return boolean `true` if the assertion succeeded, `false` otherwise.
*/
public function assertNotException($expected, $closure, $message = '{:message}') {
$result = null;
try {
$closure();
} catch (Exception $e) {
$class = get_class($e);
$eMessage = $e->getMessage();
if (is_a($e, $expected)) {
$result = $class;
return $this->assert(false, $message, compact('expected', 'result'));
}
if ($eMessage === $expected) {
$result = $eMessage;
return $this->assert(false, $message, compact('expected', 'result'));
}
if (Validator::isRegex($expected) && preg_match($expected, $eMessage)) {
$result = $eMessage;
return $this->assert(false, $message, compact('expected', 'result'));
}
}
$message = sprintf('Exception "%s" was not expected.', $expected);
return $this->assert(true, $message, compact('expected', 'result'));
}
/**
* Assert that the regular expression `$expected` is matched in the result.
*
* @see lithium\test\Unit::assert()
* @param mixed $expected
* @param mixed $result
* @param string $message
* @return boolean `true` if the assertion succeeded, `false` otherwise.
*/
public function assertPattern($expected, $result, $message = '{:message}') {
list($expected, $result) = $this->_normalizeLineEndings($expected, $result);
$params = compact('expected', 'result');
return $this->assert(!!preg_match($expected, $result), $message, $params);
}
/**
* Assert that the regular expression `$expected` is *not* matched in the result.
*
* @see lithium\test\Unit::assert()
* @param mixed $expected
* @param mixed $result
* @param string $message
* @return boolean `true` if the assertion succeeded, `false` otherwise.
*/
public function assertNotPattern($expected, $result, $message = '{:message}') {
list($expected, $result) = $this->_normalizeLineEndings($expected, $result);
$params = compact('expected', 'result');
return $this->assert(!preg_match($expected, $result), $message, $params);
}
/**
* Assert that given value matches the `sprintf` format.
*
* ```
* $this->assertStringMatchesFormat('%d', '10'); // succeeds
* $this->assertStringMatchesFormat('%d', '10.555'); // fails
* ```
*
* @link http://php.net/sprintf
* @link http://php.net/sscanf
* @see lithium\test\Unit::assert()
* @param string $expected Expected format using sscanf's format.
* @param string $actual
* @param string|boolean $message
* @return boolean `true` if the assertion succeeded, `false` otherwise.
*/
public function assertStringMatchesFormat($expected, $actual, $message = '{:message}') {
$result = sscanf($actual, $expected);
return $this->assert($result[0] == $actual, $message, compact('expected', 'result'));
}
/**
* Assert that given value does *not* matche the `sprintf` format.
*
* ```
* $this->assertStringNotMatchesFormat('%d', '10.555'); // succeeds
* $this->assertStringNotMatchesFormat('%d', '10'); // fails
* ```
*
* @link http://php.net/sprintf
* @link http://php.net/sscanf
* @see lithium\test\Unit::assert()
* @param string $expected Expected format using sscanf's format.
* @param string $actual
* @param string|boolean $message
* @return boolean `true` if the assertion succeeded, `false` otherwise.
*/
public function assertStringNotMatchesFormat($expected, $actual, $message = '{:message}') {
$result = sscanf($actual, $expected);
return $this->assert($result[0] != $actual, $message, compact('expected', 'result'));
}
/**
* Assert given result string has given suffix.
*
* ```
* $this->assertStringEndsWith('bar', 'foobar'); // succeeds
* $this->assertStringEndsWith('foo', 'foobar'); // fails
* ```
*
* @param string $expected The suffix to check for.
* @param string $actual
* @param string|boolean $message
* @return boolean `true` if the assertion succeeded, `false` otherwise.
*/
public function assertStringEndsWith($expected, $actual, $message = '{:message}') {
return $this->assert(preg_match("/$expected$/", $actual, $matches) === 1, $message, [
'expected' => $expected,
'result' => $actual
]);
}
/**
* Assert given result string has given prefix.
*
* ```
* $this->assertStringStartsWith('foo', 'foobar'); // succeeds
* $this->assertStringStartsWith('bar', 'foobar'); // fails
* ```
*
* @param string $expected The prefix to check for.
* @param string $actual
* @param string|boolean $message
* @return boolean `true` if the assertion succeeded, `false` otherwise.
*/
public function assertStringStartsWith($expected, $actual, $message = '{:message}') {
return $this->assert(preg_match("/^$expected/", $actual, $matches) === 1, $message, [
'expected' => $expected,
'result' => $actual
]);
}
/**
* Takes an array $expected and generates a regex from it to match the provided $string.
* Samples for $expected:
*
* Checks for an input tag with a name attribute (contains any non-empty value) and an id
* attribute that contains 'my-input':
* ```
* ['input' => ['name', 'id' => 'my-input']]
* ```
*
* Checks for two p elements with some text in them:
* ```
* [
* ['p' => true],
* 'textA',
* '/p',
* ['p' => true],
* 'textB',
* '/p'
* ]
* ```
*
* You can also specify a pattern expression as part of the attribute values, or the tag
* being defined, if you prepend the value with preg: and enclose it with slashes, like so:
* ```
* [
* ['input' => ['name', 'id' => 'preg:/FieldName\d+/']],
* 'preg:/My\s+field/'
* ]
* ```
*
* Important: This function is very forgiving about whitespace and also accepts any
* permutation of attribute order. It will also allow whitespaces between specified tags.
*
* @see lithium\test\Unit::assert()
* @param string $string An HTML/XHTML/XML string
* @param array $expected An array, see above
* @return boolean `true` if the assertion succeeded, `false` otherwise.
*/
public function assertTags($string, $expected) {
$regex = [];
$normalized = [];
foreach ((array) $expected as $key => $val) {
if (!is_numeric($key)) {
$normalized[] = [$key => $val];
} else {
$normalized[] = $val;
}
}
$i = 0;
foreach ($normalized as $tags) {
$i++;
if (is_string($tags) && $tags[0] === '<') {
$tags = [substr($tags, 1) => []];
} elseif (is_string($tags)) {
$tagsTrimmed = preg_replace('/\s+/m', '', $tags);
if (preg_match('/^\*?\//', $tags, $match) && $tagsTrimmed !== '//') {
$prefix = [null, null];
if ($match[0] === '*/') {
$prefix = ['Anything, ', '.*?'];
}
$regex[] = [
sprintf('%sClose %s tag', $prefix[0], substr($tags, strlen($match[0]))),
sprintf('%s<[\s]*\/[\s]*%s[\s]*>[\n\r]*', $prefix[1], substr(
$tags, strlen($match[0])
)),
$i
];
continue;
}
if (!empty($tags) && preg_match('/^regex\:\/(.+)\/$/i', $tags, $matches)) {
$tags = $matches[1];
$type = 'Regex matches';
} else {
$tags = preg_quote($tags, '/');
$type = 'Text equals';
}
$regex[] = [sprintf('%s "%s"', $type, $tags), $tags, $i];
continue;
}
foreach ($tags as $tag => $attributes) {
$regex[] = [
sprintf('Open %s tag', $tag),
sprintf('[\s]*<%s', preg_quote($tag, '/')),
$i
];
if ($attributes === true) {
$attributes = [];
}
$attrs = [];
$explanations = [];
foreach ($attributes as $attr => $val) {
if (is_numeric($attr) && preg_match('/^regex\:\/(.+)\/$/i', $val, $matches)) {
$attrs[] = $matches[1];
$explanations[] = sprintf('Regex "%s" matches', $matches[1]);
continue;
} else {
$quotes = '"';
if (is_numeric($attr)) {
$attr = $val;
$val = '.+?';
$explanations[] = sprintf('Attribute "%s" present', $attr);
} elseif (
!empty($val) && preg_match('/^regex\:\/(.+)\/$/i', $val, $matches)
) {
$quotes = '"?';
$val = $matches[1];
$explanations[] = sprintf('Attribute "%s" matches "%s"', $attr, $val);
} else {
$explanations[] = sprintf('Attribute "%s" == "%s"', $attr, $val);
$val = preg_quote($val, '/');
}
$attrs[] = '[\s]+' . preg_quote($attr, '/') . "={$quotes}{$val}{$quotes}";
}
}
if ($attrs) {
$permutations = $this->_arrayPermute($attrs);
$permutationTokens = [];
foreach ($permutations as $permutation) {
$permutationTokens[] = join('', $permutation);
}
$regex[] = [
sprintf('%s', join(', ', $explanations)),
$permutationTokens,
$i
];
}
$regex[] = [sprintf('End %s tag', $tag), '[\s]*\/?[\s]*>[\n\r]*', $i];
}
}
foreach ($regex as $i => $assertation) {
list($description, $expressions, $itemNum) = $assertation;
$matches = false;
foreach ((array) $expressions as $expression) {
if (preg_match(sprintf('/^%s/s', $expression), $string, $match)) {
$matches = true;
$string = substr($string, strlen($match[0]));
break;
}
}
if (!$matches) {
$this->assert(false, sprintf(
'- Item #%d / regex #%d failed: %s', $itemNum, $i, $description
));
return false;
}
}
return $this->assert(true);
}
/**
* Assert Cookie data is properly set in headers.
*
* The value passed to `exepected` is an array of the cookie data, with at least the key and
* value expected, but can support any of the following keys:
* - `key`: the expected key
* - `value`: the expected value
* - `path`: optionally specifiy a path
* - `name`: optionally specify the cookie name
* - `expires`: optionally assert a specific expire time
*
* @see lithium\test\Unit::assert()
* @param array $expected
* @param array $headers When empty, value of `headers_list()` is used.
* @return boolean `true` if the assertion succeeded, `false` otherwise.
*/
public function assertCookie($expected, $headers = null) {
$result = null;
$matched = $this->_cookieMatch($expected, $headers);
if (!$matched['match']) {
$message = sprintf('%s - Cookie not found in headers.', $matched['pattern']);
return $this->assert(false, $message, compact('expected', 'result'));
}
return $this->assert(true, '%s');
}
/**
* Assert Cookie data is *not* set in headers.
*
* The value passed to `expected` is an array of the cookie data, with at least the key and
* value expected, but can support any of the following keys:
* - `key`: the expected key
* - `value`: the expected value
* - `path`: optionally specify a path
* - `name`: optionally specify the cookie name
* - `expires`: optionally assert a specific expire time
*
* @see lithium\test\Unit::assert()
* @param array $expected
* @param array $headers When empty, value of `headers_list()` is used.
* @return boolean `true` if the assertion succeeded, `false` otherwise.
*/
public function assertNoCookie($expected, $headers = null) {
$matched = $this->_cookieMatch($expected, $headers);
if ($matched['match']) {
$message = sprintf('%s - Cookie found in headers.', $matched['pattern']);
return $this->assert(false, $message, compact('expected', 'result'));
}
return $this->assert(true, '%s');
}
/**
* Match an `$expected` cookie with the given headers. If no headers are provided, then
* the value of `headers_list()` will be used.
*
* @param array $expected
* @param array $headers When empty, value of `headers_list()` will be used.
* @return boolean `true` if the assertion succeeded, `false` otherwise.
*/
protected function _cookieMatch($expected, $headers) {
$defaults = ['path' => '/', 'name' => '[\w.-]+'];
$expected += $defaults;
$headers = ($headers) ?: headers_list();
$value = preg_quote(urlencode($expected['value']), '/');
$key = explode('.', $expected['key']);
$key = (count($key) === 1) ? '[' . current($key) . ']' : ('[' . join('][', $key) . ']');
$key = preg_quote($key, '/');
if (isset($expected['expires'])) {
$expectedExpires = strtotime($expected['expires']);
$expires = gmdate('D, d-M-Y H:i:s \G\M\T', $expectedExpires);
$expires = preg_quote($expires, '/');
$maxAge = $expectedExpires - time();
} else {
$expires = '(?:.+?)';
$maxAge = '([0-9]+)';
}
$path = preg_quote($expected['path'], '/');
$pattern = "/^Set\-Cookie:\s{$expected['name']}$key=$value;";
$pattern .= "\sexpires=$expires;";
$pattern .= "\sMax-Age=$maxAge;";
$pattern .= "\spath=$path/";
$match = false;
foreach ($headers as $header) {
if (preg_match($pattern, $header)) {
$match = true;
continue;
}
}
return compact('match', 'pattern');
}
/**
* Assert that the passed result array has expected number of elements.
*
* ```
* $this->assertCount(1, ['foo']); // succeeds
* $this->assertCount(2, ['foo', 'bar', 'bar']); // fails
* ```
*
* @see lithium\test\Unit::assert()
* @param integer $expected
* @param array $array
* @param string|boolean $message
* @return boolean `true` if the assertion succeeded, `false` otherwise.
*/
public function assertCount($expected, $array, $message = '{:message}') {
return $this->assert($expected === ($result = count($array)), $message, [
'expected' => $expected,
'result' => $result
]);
}
/**
* Assert that the passed result array has *not* the expected number of elements.
*
* ```
* $this->assertNotCount(2, ['foo', 'bar', 'bar']); // succeeds
* $this->assertNotCount(1, ['foo']); // fails
* ```
*
* @see lithium\test\Unit::assert()
* @param integer $expected
* @param array $array
* @param string|boolean $message
* @return boolean `true` if the assertion succeeded, `false` otherwise.
*/
public function assertNotCount($expected, $array, $message = '{:message}') {
return $this->assert($expected !== ($result = count($array)), $message, [
'expected' => $expected,
'result' => $result
]);
}
/**
* Assert that the result array has given key.
*
* ```
* $this->assertArrayHasKey('bar', ['bar' => 'baz']); // succeeds
* $this->assertArrayHasKey('foo', ['bar' => 'baz']); // fails
* ```
*
* @see lithium\test\Unit::assert()
* @param mixed $expected
* @param array $array
* @param string|boolean $message
* @return boolean `true` if the assertion succeeded, `false` otherwise.
*/
public function assertArrayHasKey($key, $array, $message = '{:message}') {
if (is_object($array) && $array instanceof \ArrayAccess) {
$result = isset($array[$key]);
} else {
$result = array_key_exists($key, $array);
}
return $this->assert($result, $message, [
'expected' => $key,
'result' => $array
]);
}
/**
* Assert that the result array does *not* have given key.
*
* ```
* $this->assertArrayNotHasKey('foo', ['bar' => 'baz']); // succeeds
* $this->assertArrayNotHasKey('bar', ['bar' => 'baz']); // fails
* ```
*
* @see lithium\test\Unit::assert()
* @param mixed $expected
* @param array $array
* @param string|boolean $message
* @return boolean `true` if the assertion succeeded, `false` otherwise.
*/
public function assertArrayNotHasKey($key, $array, $message = '{:message}') {
if (is_object($array) && $array instanceof \ArrayAccess) {
$result = isset($array[$key]);
} else {
$result = array_key_exists($key, $array);
}
return $this->assert(!$result, $message, [
'expected' => $key,
'result' => $array
]);
}
/**
* Assert that `$haystack` contains `$needle` as a value.
*
* ```
* $this->assertContains('foo', ['foo', 'bar', 'baz']); // succeeds
* $this->assertContains(4, [1,2,3]); // fails
* ```
*
* @see lithium\test\Unit::assert()
* @param string $needle The needle you are looking for.
* @param mixed $haystack An array, iterable object, or string.
* @param string|boolean $message
* @return boolean `true` if the assertion succeeded, `false` otherwise.
*/
public function assertContains($needle, $haystack, $message = '{:message}') {
if (is_string($haystack)) {
return $this->assert(strpos($haystack, $needle) !== false, $message, [
'expected' => $needle,
'result' => $haystack
]);
}
foreach ($haystack as $key => $value) {
if ($value === $needle) {
return $this->assert(true, $message, [
'expected' => $needle,
'result' => $haystack
]);
}
}
return $this->assert(false, $message, [
'expected' => $needle,
'result' => $haystack
]);
}
/**
* Assert that `$haystack` does *not* contain `$needle` as a value.
*
* ```
* $this->assertNotContains(4, [1,2,3]); // succeeds
* $this->assertNotContains('foo', ['foo', 'bar', 'baz']); // fails
* ```
*
* @see lithium\test\Unit::assert()
* @param string $needle The needle you are looking for.
* @param miexed $haystack Array or iterable object or a string.
* @param string|boolean $message
* @return boolean `true` if the assertion succeeded, `false` otherwise.
*/
public function assertNotContains($needle, $haystack, $message = '{:message}') {
if (is_string($haystack)) {
return $this->assert(strpos($haystack, $needle) === false, $message, [
'expected' => $needle,
'result' => $haystack
]);
}
foreach ($haystack as $key => $value) {
if ($value === $needle) {
return $this->assert(false, $message, [
'expected' => $needle,
'result' => $haystack
]);
}
}
return $this->assert(true, $message, [
'expected' => $needle,
'result' => $haystack
]);
}
/**
* Assert that `$haystack` does only contain item of given type.
*
* ```
* $this->assertContainsOnly('integer', [1,2,3]); // succeeds
* $this->assertContainsOnly('integer', ['foo', 'bar', 'baz']); // fails
* ```
*
* @see lithium\test\Unit::$_internalTypes
* @see lithium\test\Unit::assert()
* @param string $type
* @param array|object $haystack Array or iterable object.
* @param string|boolean $message
* @return boolean `true` if the assertion succeeded, `false` otherwise.
*/
public function assertContainsOnly($type, $haystack, $message = '{:message}') {
$method = static::$_internalTypes[$type];
foreach ($haystack as $key => $value) {
if (!$method($value)) {
return $this->assert(false, $message, [
'expected' => $type,
'result' => $haystack
]);
}
}
return $this->assert(true, $message, [
'expected' => $type,
'result' => $haystack
]);
}
/**
* Assert that `$haystack` hasn't any items of given type.
*
* ```
* $this->assertNotContainsOnly('integer', ['foo', 'bar', 'baz']); // succeeds
* $this->assertNotContainsOnly('integer', [1,2,3]); // fails
* ```
*
* @see lithium\test\Unit::$_internalTypes
* @see lithium\test\Unit::assert()
* @param string $type
* @param array|object $haystack Array or iterable object.
* @param string|boolean $message
* @return boolean `true` if the assertion succeeded, `false` otherwise.
*/
public function assertNotContainsOnly($type, $haystack, $message = '{:message}') {
$method = static::$_internalTypes[$type];
foreach ($haystack as $key => $value) {
if (!$method($value)) {
return $this->assert(true, $message, [
'expected' => $type,
'result' => $haystack
]);
}
}
return $this->assert(false, $message, [
'expected' => $type,
'result' => $haystack
]);
}
/**
* Assert that `$haystack` contains only instances of given class.
*
* ```
* $this->assertContainsOnlyInstancesOf('stdClass', [new \stdClass]); // succeeds
* $this->assertContainsOnlyInstancesOf('stdClass', [new \lithium\test\Unit]); // fails
* ```
*
* @see lithium\test\Unit::assert()
* @param string $class
* @param array|object $haystack Array or iterable object.
* @param string|boolean $message
* @return boolean `true` if the assertion succeeded, `false` otherwise.
*/
public function assertContainsOnlyInstancesOf($class, $haystack, $message = '{:message}') {
$result = [];
foreach ($haystack as $key => &$value) {
if (!is_a($value, $class)) {
$result[$key] =& $value;
break;
}
}
return $this->assert(empty($result), $message, [
'expected' => $class,
'result' => $result
]);
}
/**
* Assert that `$expected` is greater than `$actual`.
*
* ```
* $this->assertGreaterThan(5, 3); // succeeds
* $this->assertGreaterThan(3, 5); // fails
* ```
*
* @see lithium\test\Unit::assert()
* @param float|integer $expected
* @param float|integer $actual
* @param string|boolean $message
* @return boolean `true` if the assertion succeeded, `false` otherwise.
*/
public function assertGreaterThan($expected, $actual, $message = '{:message}') {
return $this->assert($expected > $actual, $message, [
'expected' => $expected,
'result' => $actual
]);
}
/**
* Assert that `$expected` is greater than or equal to `$actual`.
*
* ```
* $this->assertGreaterThanOrEqual(5, 5); // succeeds
* $this->assertGreaterThanOrEqual(3, 5); // fails
* ```
*
* @see lithium\test\Unit::assert()
* @param float|integer $expected
* @param float|integer $actual
* @param string|boolean $message
* @return boolean `true` if the assertion succeeded, `false` otherwise.
*/
public function assertGreaterThanOrEqual($expected, $actual, $message = '{:message}') {
return $this->assert($expected >= $actual, $message, [
'expected' => $expected,
'result' => $actual
]);
}
/**
* Assert that `$expected` is less than `$actual`.
*
* ```
* $this->assertLessThan(3, 5); // succeeds
* $this->assertLessThan(5, 3); // fails
* ```
*
* @see lithium\test\Unit::assert()
* @param float|integer $expected
* @param float|integer $actual
* @param string|boolean $message
* @return boolean `true` if the assertion succeeded, `false` otherwise.
*/
public function assertLessThan($expected, $actual, $message = '{:message}') {
return $this->assert($expected < $actual, $message, [
'expected' => $expected,
'result' => $actual
]);
}
/**
* Assert that `$expected` is less than or equal to `$actual`.
*
* ```
* $this->assertLessThanOrEqual(5, 5); // succeeds
* $this->assertLessThanOrEqual(5, 3); // fails
* ```
*
* @see lithium\test\Unit::assert()
* @param float|integer $expected
* @param float|integer $actual
* @param string|boolean $message
* @return boolean `true` if the assertion succeeded, `false` otherwise.
*/
public function assertLessThanOrEqual($expected, $actual, $message = '{:message}') {
return $this->assert($expected <= $actual, $message, [
'expected' => $expected,
'result' => $actual
]);
}
/**
* Assert that `$actual` is an instance of `$expected`.
*
* ```
* $this->assertInstanceOf('stdClass', new stdClass); // succeeds
* $this->assertInstanceOf('ReflectionClass', new stdClass); // fails
* ```
*
* @see lithium\test\Unit::assert()
* @param string $expected Fully namespaced expected class.
* @param object $actual Object you are testing.
* @param string|boolean $message
* @return boolean `true` if the assertion succeeded, `false` otherwise.
*/
public function assertInstanceOf($expected, $actual, $message = '{:message}') {
return $this->assert(is_a($actual, $expected), $message, [
'expected' => $expected,
'result' => get_class($actual)
]);
}
/**
* Assert that `$actual` is *not* an instance of `$expected`.
*
* ```
* $this->assertNotInstanceOf('ReflectionClass', new stdClass); // succeeds
* $this->assertNotInstanceOf('stdClass', new stdClass); // fails
* ```
*
* @see lithium\test\Unit::assert()
* @param string $expected Fully namespaced expected class.
* @param object $actual Object you are testing.
* @param string|boolean $message
* @return boolean `true` if the assertion succeeded, `false` otherwise.
*/
public function assertNotInstanceOf($expected, $actual, $message = '{:message}') {
return $this->assert(!is_a($actual, $expected), $message, [
'expected' => $expected,
'result' => is_object($actual) ? get_class($actual) : gettype($actual),
]);
}
/**
* Assert that `$actual` is of given type.
*
* ```
* $this->assertInternalType('string', 'foobar'); // succeeds
* $this->assertInternalType('integer', 'foobar'); // fails
* ```
*
* @see lithium\test\Unit::$_internalTypes
* @see lithium\test\Unit::assert()
* @param string $expected Internal type.
* @param object $actual Object you are testing.
* @param string|boolean $message
* @return boolean `true` if the assertion succeeded, `false` otherwise.
*/
public function assertInternalType($expected, $actual, $message = '{:message}') {
$method = static::$_internalTypes[$expected];
return $this->assert($method($actual), $message, [
'expected' => $expected,
'result' => gettype($actual)
]);
}
/**
* Assert that `$actual` is *not* of given type.
*
* ```
* $this->assertNotInternalType('integer', 'foobar'); // succeeds
* $this->assertNotInternalType('string', 'foobar'); // fails
* ```
*
* @see lithium\test\Unit::$_internalTypes
* @see lithium\test\Unit::assert()
* @param string $expected Internal type.
* @param object $actual Object you are testing.
* @param string|boolean $message
* @return boolean `true` if the assertion succeeded, `false` otherwise.
*/
public function assertNotInternalType($expected, $actual, $message = '{:message}') {
$method = static::$_internalTypes[$expected];
return $this->assert(!$method($actual), $message, [
'expected' => $expected,
'result' => gettype($actual)
]);
}
/**
* Assert that the file contents of `$expected` are equal to the contents of `$actual`.
*
* ```
* $this->assertFileEquals('/tmp/foo.txt', '/tmp/foo.txt'); // succeeds
* $this->assertFileEquals('/tmp/foo.txt', '/tmp/bar.txt'); // fails
* ```
*
* @see lithium\test\Unit::assert()
* @param string $expected Absolute path to the expected file.
* @param string $actual Absolute path to the actual file.
* @param string|boolean $message
* @return boolean `true` if the assertion succeeded, `false` otherwise.
*/
public function assertFileEquals($expected, $actual, $message = '{:message}') {
$expected = md5_file($expected);
$result = md5_file($actual);
return $this->assert($expected === $result, $message, compact('expected', 'result'));
}
/**
* Assert that the file contents of `$expected` are *not* equal to the contents of `$actual`.
*
* ```
* $this->assertFileNotEquals('/tmp/foo.txt', '/tmp/bar.txt'); // succeeds
* $this->assertFileNotEquals('/tmp/foo.txt', '/tmp/foo.txt'); // fails
* ```
*
* @see lithium\test\Unit::assert()
* @param string $expected Absolute path to the expected file.
* @param string $actual Absolute path to the actual file.
* @param string|boolean $message
* @return boolean `true` if the assertion succeeded, `false` otherwise.
*/
public function assertFileNotEquals($expected, $actual, $message = '{:message}') {
$expected = md5_file($expected);
$result = md5_file($actual);
return $this->assert($expected !== $result, $message, compact('expected', 'result'));
}
/**
* Assert that a file exists.
*
* ```
* $this->assertFileExists(__FILE__); // succeeds
* $this->assertFileExists('/tmp/bar.txt'); // fails
* ```
*
* @see lithium\test\Unit::assert()
* @param string $actual Absolute path to the actual file.
* @param string|boolean $message
* @return boolean `true` if the assertion succeeded, `false` otherwise.
*/
public function assertFileExists($actual, $message = '{:message}') {
return $this->assert(file_exists($actual), $message, [
'expected' => $actual,
'result' => file_exists($actual)
]);
}
/**
* Assert that a file does *not* exist.
*
* ```
* $this->assertFileNotExists('/tmp/bar.txt'); // succeeds
* $this->assertFileNotExists(__FILE__); // fails
* ```
*
* @see lithium\test\Unit::assert()
* @param string $actual Absolute path to the actual file.
* @param string|boolean $message
* @return boolean `true` if the assertion succeeded, `false` otherwise.
*/
public function assertFileNotExists($actual, $message = '{:message}') {
return $this->assert(!file_exists($actual), $message, [
'expected' => $actual,
'result' => !file_exists($actual)
]);
}
/**
* Assert that a class has a given attribute.
*
* ```
* $this->assertClassHasAttribute('__construct', 'ReflectionClass'); // succeeds
* $this->assertClassHasAttribute('name', 'ReflectionClass'); // fails
* ```
*
* @see lithium\test\Unit::assert()
* @see lithium\test\Unit::assertObjectHasAttribute()
* @throws InvalidArgumentException When $class does not exist.
* @param mixed $attributeName
* @param string $class
* @param string|boolean $message
* @return boolean `true` if the assertion succeeded, `false` otherwise.
*/
public function assertClassHasAttribute($attributeName, $class, $message = '{:message}') {
if (!is_string($class)) {
throw new InvalidArgumentException('Argument $class must be a string');
}
$object = new ReflectionClass($class);
return $this->assert($object->hasProperty($attributeName), $message, [
'expected' => $attributeName,
'result' => $object->getProperties()
]);
}
/**
* Assert that a class does *not* have a given attribute.
*
* ```
* $this->assertClassNotHasAttribute('name', 'ReflectionClass'); // succeeds
* $this->assertClassNotHasAttribute('__construct', 'ReflectionClass'); // fails
* ```
*
* @see lithium\test\Unit::assert()
* @see lithium\test\Unit::assertObjectHasAttribute()
* @throws InvalidArgumentException When $class does not exist.
* @param mixed $attributeName
* @param string $class
* @param string|boolean $message
* @return boolean `true` if the assertion succeeded, `false` otherwise.
*/
public function assertClassNotHasAttribute($attributeName, $class, $message = '{:message}') {
if (!is_string($class)) {
throw new InvalidArgumentException('Argument $class must be a string.');
}
$object = new ReflectionClass($class);
return $this->assert(!$object->hasProperty($attributeName), $message, [
'expected' => $attributeName,
'result' => $object->getProperties()
]);
}
/**
* Assert that a class does have a given _static_ attribute.
*
* ```
* $this->assertClassHasStaticAttribute('_methodFilters', '\lithium\core\StaticObjectDeprecated'); // succeeds
* $this->assertClassHasStaticAttribute('foobar', '\lithium\core\StaticObjectDeprecated'); // fails
* ```
*
* @see lithium\test\Unit::assert()
* @param mixed $attributeName
* @param string $class
* @param string|boolean $message
* @return boolean `true` if the assertion succeeded, `false` otherwise.
*/
public function assertClassHasStaticAttribute($attributeName, $class, $message = '{:message}') {
$object = new ReflectionClass($class);
if ($object->hasProperty($attributeName)) {
$attribute = $object->getProperty($attributeName);
return $this->assert($attribute->isStatic(), $message, [
'expected' => $attributeName,
'result' => $object->getProperties()
]);
}
return $this->assert(false, $message, [
'expected' => $attributeName,
'result' => $object->getProperties()
]);
}
/**
* Assert that a class does *not* have a given _static_ attribute.
*
* ```
* $this->assertClassNotHasStaticAttribute('foobar', '\lithium\core\StaticObjectDeprecated'); // succeeds
* $this->assertClassNotHasStaticAttribute('_methodFilters', '\lithium\core\StaticObjectDeprecated'); // fails
* ```
*
* @see lithium\test\Unit::assert()
* @param mixed $attributeName
* @param string $class
* @param string|boolean $message
* @return boolean `true` if the assertion succeeded, `false` otherwise.
*/
public function assertClassNotHasStaticAttribute($attrName, $class, $message = '{:message}') {
$object = new ReflectionClass($class);
if ($object->hasProperty($attrName)) {
$attribute = $object->getProperty($attrName);
return $this->assert(!$attribute->isStatic(), $message, [
'expected' => $attrName,
'result' => $object->getProperties()
]);
}
return $this->assert(true, $message, [
'expected' => $attrName,
'result' => $object->getProperties()
]);
}
/**
* Assert that `$object` has given attribute.
*
* ```
* $this->assertObjectHasAttribute('__construct', 'ReflectionClass'); // succeeds
* $this->assertObjectHasAttribute('name', 'ReflectionClass'); // fails
* ```
*
* @see lithium\test\Unit::assert()
* @throws InvalidArgumentException When $object is not an object.
* @param string $attributeName
* @param string $object
* @param string|boolean $message
* @return boolean `true` if the assertion succeeded, `false` otherwise.
*/
public function assertObjectHasAttribute($attributeName, $object, $message = '{:message}') {
if (!is_object($object)) {
throw new InvalidArgumentException('Second argument $object must be an object.');
}
$object = new ReflectionClass($object);
return $this->assert($object->hasProperty($attributeName), $message, [
'expected' => $attributeName,
'result' => $object->getProperties()
]);
}
/**
* Assert that `$object` does *not* have given attribute.
*
* ```
* $this->assertObjectNotHasAttribute('name', 'ReflectionClass'); // succeeds
* $this->assertObjectNotHasAttribute('__construct', 'ReflectionClass'); // fails
* ```
*
* @see lithium\test\Unit::assert()
* @throws InvalidArgumentException When $object is not an object.
* @param string $attributeName
* @param string $object
* @param string|boolean $message
* @return boolean `true` if the assertion succeeded, `false` otherwise.
*/
public function assertObjectNotHasAttribute($attributeName, $object, $message = '{:message}') {
if (!is_object($object)) {
throw new InvalidArgumentException('Second argument $object must be an object');
}
$object = new ReflectionClass($object);
return $this->assert(!$object->hasProperty($attributeName), $message, [
'expected' => $attributeName,
'result' => $object->getProperties()
]);
}
/**
* Reports test result messages.
*
* @param string $type The type of result being reported. Can be `'pass'`, `'fail'`, `'skip'`
* or `'exception'`.
* @param array $info An array of information about the test result. At a minimum, this should
* contain a `'message'` key. Other possible keys are `'file'`, `'line'`,
* `'class'`, `'method'`, `'assertion'` and `'data'`.
* @param array $options Currently unimplemented.
* @return void
*/
protected function _result($type, $info, array $options = []) {
$info = (['result' => $type] + $info);
$defaults = [];
$options += $defaults;
if ($this->_reporter) {
$filtered = $this->_reporter->__invoke($info);
$info = is_array($filtered) ? $filtered : $info;
}
$this->_results[] = $info;
}
/**
* Runs an individual test method, collecting results and catching exceptions along the way.
*
* @param string $method The name of the test method to run.
* @param array $options
* @return mixed
* @filter Executes filters applied to this class' run method.
*/
protected function _runTestMethod($method, $options) {
try {
$this->setUp();
} catch (Exception $e) {
$this->_handleException($e, __LINE__ - 2);
return $this->_results;
}
$params = compact('options', 'method');
$passed = Filters::run($this, 'run', $params, function($params) {
try {
$method = $params['method'];
$lineFlag = __LINE__ + 1;
$this->{$method}();
} catch (Exception $e) {
$this->_handleException($e);
}
});
foreach ($this->_expected as $expected) {
$this->_result('fail', compact('method') + [
'class' => get_class($this),
'message' => "Expected exception matching `{$expected}` uncaught.",
'data' => [],
'file' => null,
'line' => null,
'assertion' => 'expectException'
]);
}
$this->_expected = [];
try {
$this->tearDown();
} catch (Exception $e) {
$this->_handleException($e, __LINE__ - 2);
}
return $passed;
}
/**
* Normalizes `Exception` objects and PHP error data into a single array format
* then the error data is logged to the test results.
*
* @see lithium\test\Unit::_reportException()
* @param mixed $exception An `Exception` object instance, or an array containing the following
* keys: `'name'`,` 'message'`, `'file'`, `'line'`, `'trace'` (in
* `debug_backtrace()` format) and optionally `'code'` (error code number)
* and `'context'` (an array of variables relevant to the scope of where the
* error occurred).
* @param integer $lineFlag A flag used for determining the relevant scope of the call stack.
* Set to the line number where test methods are called.
* @return void
*/
protected function _handleException($exception, $lineFlag = null) {
$data = $exception;
if (is_object($exception)) {
$data = ['name' => get_class($exception)];
foreach (['message', 'file', 'line', 'trace', 'code'] as $key) {
$method = 'get' . ucfirst($key);
$data[$key] = $exception->{$method}();
}
if ($exception instanceof ErrorException) {
$mapSeverity = function($severity) {
foreach (get_defined_constants(true)['Core'] as $constant => $value) {
if (substr($constant, 0, 2) === 'E_' && $value === $severity) {
return $constant;
}
}
return 'E_UNKNOWN';
};
$data['code'] = $mapSeverity($exception->getSeverity());
}
$ref = $exception->getTrace();
$ref = $ref[0] + ['class' => null];
if ($ref['class'] === __CLASS__ && $ref['function'] === 'skipIf') {
return $this->_result('skip', $data);
}
}
return $this->_reportException($data, $lineFlag);
}
/**
* Convert an exception object to an exception result array for test reporting.
*
* @param array $exception The exception data to report on. Statistics are gathered and
* added to the reporting stack contained in `Unit::$_results`.
* @param string $lineFlag
* @return void
* @todo Refactor so that reporters handle trace formatting.
*/
protected function _reportException($exception, $lineFlag = null) {
$message = $exception['message'];
$isExpected = (($exp = end($this->_expected)) && ($exp === true || $exp === $message || (
Validator::isRegex($exp) && preg_match($exp, $message)
)));
if ($isExpected) {
return array_pop($this->_expected);
}
$initFrame = current($exception['trace']) + ['class' => '-', 'function' => '-'];
foreach ($exception['trace'] as $frame) {
if (isset($scopedFrame)) {
break;
}
if (!class_exists('lithium\analysis\Inspector')) {
continue;
}
if (isset($frame['class']) && in_array($frame['class'], Inspector::parents($this))) {
$scopedFrame = $frame;
}
}
if (class_exists('lithium\analysis\Debugger')) {
$exception['trace'] = Debugger::trace([
'trace' => $exception['trace'],
'format' => '{:functionRef}, line {:line}',
'includeScope' => false,
'scope' => array_filter([
'functionRef' => __NAMESPACE__ . '\{closure}',
'line' => $lineFlag
])
]);
}
$this->_result('exception', $exception + [
'class' => $initFrame['class'],
'method' => $initFrame['function']
]);
}
/**
* Compare the expected with the result. If `$result` is null `$expected` equals `$type`
* and `$result` equals `$expected`.
*
* @param string $type The type of comparison either `'identical'` or `'equal'` (default).
* @param mixed $expected The expected value.
* @param mixed $result An optional result value, defaults to `null`
* @param string $trace An optional trace used internally to track arrays and objects,
* defaults to `null`.
* @return array Data with the keys `trace'`, `'expected'` and `'result'`.
*/
protected function _compare($type, $expected, $result = null, $trace = null) {
$compareTypes = function($expected, $result, $trace) {
$types = ['expected' => gettype($expected), 'result' => gettype($result)];
if ($types['expected'] !== $types['result']) {
$expected = trim("({$types['expected']}) " . print_r($expected, true));
$result = trim("({$types['result']}) " . print_r($result, true));
return compact('trace', 'expected', 'result');
}
};
if ($types = $compareTypes($expected, $result, $trace)) {
return $types;
}
$data = [];
if (!is_scalar($expected)) {
foreach ($expected as $key => $value) {
$newTrace = "{$trace}[{$key}]";
$isObject = false;
if (is_object($expected)) {
$isObject = true;
$expected = (array) $expected;
$result = (array) $result;
}
if (!array_key_exists($key, $result)) {
$trace = (!$key) ? null : $newTrace;
$expected = (!$key) ? $expected : $value;
$result = ($key) ? null : $result;
return compact('trace', 'expected', 'result');
}
$check = $result[$key];
if ($isObject) {
$newTrace = ($trace) ? "{$trace}->{$key}" : $key;
$expected = (object) $expected;
$result = (object) $result;
}
if ($type === 'identical') {
if ($value === $check) {
if ($types = $compareTypes($value, $check, $trace)) {
return $types;
}
continue;
}
if ($check === []) {
$trace = $newTrace;
return compact('trace', 'expected', 'result');
}
if (is_string($check)) {
$trace = $newTrace;
$expected = $value;
$result = $check;
return compact('trace', 'expected', 'result');
}
} else {
if ($value == $check) {
if ($types = $compareTypes($value, $check, $trace)) {
return $types;
}
continue;
}
if (!is_array($value)) {
$trace = $newTrace;
return compact('trace', 'expected', 'result');
}
}
$compare = $this->_compare($type, $value, $check, $newTrace);
if ($compare !== true) {
$data[] = $compare;
}
}
if (!empty($data)) {
return $data;
}
} elseif (!is_scalar($result)) {
$data = $this->_compare($type, $result, $expected);
if (!empty($data)) {
return [
'trace' => $data['trace'],
'expected' => $data['result'],
'result' => $data['expected']
];
}
}
if ((($type === 'identical') ? $expected === $result : $expected == $result)) {
if ($types = $compareTypes($expected, $result, $trace)) {
return $types;
}
return true;
}
return compact('trace', 'expected', 'result');
}
/**
* Returns a basic message for the data returned from `_result()`.
*
* @see lithium\test\Unit::assert()
* @see lithium\test\Unit::_result()
* @param array $data The data to use for creating the message.
* @param string $message The string prepended to the generate message in the current scope.
* @return string
*/
protected function _message(&$data = [], $message = null) {
if (!empty($data[0])) {
foreach ($data as $key => $value) {
$message = (!empty($data[$key][0])) ? $message : null;
$message .= $this->_message($value, $message);
unset($data[$key]);
}
return $message;
}
$defaults = ['trace' => null, 'expected' => null, 'result' => null];
$data = (array) $data + $defaults;
$message = null;
if (!empty($data['trace'])) {
$message = sprintf("trace: %s\n", $data['trace']);
}
if (is_object($data['expected'])) {
$data['expected'] = get_object_vars($data['expected']);
}
if (is_object($data['result'])) {
$data['result'] = get_object_vars($data['result']);
}
return $message . sprintf("expected: %s\nresult: %s\n",
print_r($data['expected'], true),
print_r($data['result'], true)
);
}
/**
* Generates all permutation of an array $items and returns them in a new array.
*
* @param array $items An array of items
* @param array $perms
* @return array
*/
protected function _arrayPermute($items, $perms = []) {
static $permuted;
if (empty($perms)) {
$permuted = [];
}
if (empty($items)) {
$permuted[] = $perms;
return;
}
$numItems = count($items) - 1;
for ($i = $numItems; $i >= 0; --$i) {
$newItems = $items;
$newPerms = $perms;
list($tmp) = array_splice($newItems, $i, 1);
array_unshift($newPerms, $tmp);
$this->_arrayPermute($newItems, $newPerms);
}
return $permuted;
}
/**
* Removes everything from `resources/tmp/tests` directory. Call from inside of your test
* method or `tearDown()`.
*
* Uses `DIRECTORY_SEPARATOR` as `getPathname()` is used in a a direct string comparison.
* The method may contain slashes and backslashes.
*
* If the file to unlink is readonly, it throws a exception (Permission denied) on Windows.
* So, the file is checked before an unlink is tried. (this will make the tests run slower
* but is prefered over a if (!unlink { chmod; unlink }.
* http://stringoftheseus.com/blog/2010/12/22/php-unlink-permisssion-denied-error-on-windows/
*
* @param string $path Path to directory with contents to remove. If first
* character is NOT a slash (`/`) or a Windows drive letter (`C:`)
* prepends `Libraries::get(true, 'resources')/tmp/`.
* @return void
*/
protected function _cleanUp($path = null) {
$resources = Libraries::get(true, 'resources');
$path = $path ?: $resources . '/tmp/tests';
$path = preg_match('/^\w:|^\//', $path) ? $path : $resources . '/tmp/' . $path;
if (!is_dir($path)) {
return;
}
$dirs = new RecursiveDirectoryIterator($path);
$iterator = new RecursiveIteratorIterator($dirs, RecursiveIteratorIterator::CHILD_FIRST);
foreach ($iterator as $item) {
$empty = $item->getPathname() === $path . DIRECTORY_SEPARATOR . 'empty';
if ($empty || $iterator->isDot()) {
continue;
}
if ($item->isDir()) {
rmdir($item->getPathname());
continue;
}
if (!$item->isWritable()) {
chmod($item->getPathname(), 0777);
}
unlink($item->getPathname());
}
}
/**
* Fixes some issues regarding the used EOL character(s).
*
* On linux EOL is LF, on Windows it is normally CRLF, but the latter may depend also
* on the git config core.autocrlf setting. As some tests use heredoc style (<<<) to
* specify multiline expectations, this EOL issue may cause tests to fail only because
* of a difference in EOL's used.
*
* in `assertEqual`, `assertNotEqual`,`` assertPattern` and `assertNotPattern` this
* function is called to get rid of any EOL differences.
*
* @param mixed $expected
* @param mixed $result
* @return array Array with the normalized elements i.e. `array($expected, $result)`.
*/
protected function _normalizeLineEndings($expected, $result) {
if (is_string($expected) && is_string($result)) {
$expected = preg_replace('/\r\n/', "\n", $expected);
$result = preg_replace('/\r\n/', "\n", $result);
}
return [$expected, $result];
}
/* Deprecated */
/**
* The list of expected exceptions.
*
* @deprecated
* @var string
*/
protected $_expected = [];
/**
* Used before a call to `assert*()` if you expect the test assertion to generate an exception
* or PHP error. If no error or exception is thrown, a test failure will be reported. Can
* be called multiple times per assertion, if more than one error is expected.
*
* @deprecated
* @param mixed $message A string indicating what the error text is expected to be. This can
* be an exact string, a /-delimited regular expression, or true, indicating that
* any error text is acceptable.
* @return void
*/
public function expectException($message = true) {
$message = "Unit::expectException() is deprecated in favor of Unit::assertException().";
trigger_error($message, E_USER_DEPRECATED);
$this->_expected[] = $message;
}
}
?>