PageRenderTime 51ms CodeModel.GetById 22ms RepoModel.GetById 0ms app.codeStats 0ms

/htdocs/lithium/0.9.9/libraries/lithium/test/Unit.php

http://github.com/pmjones/php-framework-benchmarks
PHP | 901 lines | 692 code | 49 blank | 160 comment | 67 complexity | 31b4eb04dfdfae973f5e02713765d8cf MD5 | raw file
Possible License(s): LGPL-3.0, Apache-2.0, BSD-3-Clause, ISC, AGPL-3.0, LGPL-2.1
  1. <?php
  2. /**
  3. * Lithium: the most rad php framework
  4. *
  5. * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
  6. * @license http://opensource.org/licenses/bsd-license.php The BSD License
  7. */
  8. namespace lithium\test;
  9. use \Exception;
  10. use \lithium\util\String;
  11. use \lithium\core\Libraries;
  12. use \lithium\util\Validator;
  13. use \lithium\analysis\Debugger;
  14. use \lithium\analysis\Inspector;
  15. use \RecursiveDirectoryIterator;
  16. use \RecursiveIteratorIterator;
  17. /**
  18. * This is the base class for all test cases. Test are performed using an assertion method. If the
  19. * assertion is correct, the test passes, otherwise it fails. Most assertions take an expected
  20. * result, a received result, and a message (to describe the failure) as parameters.
  21. *
  22. * Available assertions are (see `assert<assertion-name>` methods for details): Equal, False,
  23. * Identical, NoPattern, NotEqual, Null, Pattern, Tags, True.
  24. *
  25. * If an assertion is expected to produce an exception, the `expectException` method should be
  26. * called before it.
  27. *
  28. * Both _case_ (unit) and _integration_ tests extend this class. These two test types can loosely
  29. * be defined as follows:
  30. * - Case: These tests are used to check a small unit of functionality, such as if a method
  31. * returns an expected result for a known input, or whether an adapter can successfully open a
  32. * connection.
  33. * - Integration: These are tests for determining that different parts of the framework will work
  34. * together (integrate) as expected. For example, a model has CRUD functionality with its
  35. * underlying data source.
  36. *
  37. */
  38. class Unit extends \lithium\core\Object {
  39. /**
  40. * The Reference to a test reporter class.
  41. *
  42. * @var string
  43. */
  44. protected $_reporter = null;
  45. /**
  46. * The list of test results.
  47. *
  48. * @var string
  49. */
  50. protected $_results = array();
  51. /**
  52. * The list of expected exceptions.
  53. *
  54. * @var string
  55. */
  56. protected $_expected = array();
  57. /**
  58. * Finds the test case for the corresponding class name.
  59. *
  60. * @param string $class A fully-namespaced class reference for which to find a test case.
  61. * @return string Returns the class name of a test case for `$class`, or `null` if none exists.
  62. */
  63. public static function get($class) {
  64. $parts = explode('\\', $class);
  65. $library = array_shift($parts);
  66. $name = array_pop($parts);
  67. $type = "tests.cases." . implode('.', $parts);
  68. return Libraries::locate($type, $name, compact('library'));
  69. }
  70. /**
  71. * Setup method run before every test method. override in subclasses
  72. *
  73. * @return void
  74. */
  75. public function setUp() {}
  76. /**
  77. * Teardown method run after every test method. override in subclasses
  78. *
  79. * @return void
  80. */
  81. public function tearDown() {}
  82. /**
  83. * Subclasses should use this method to set conditions that, if failed, terminate further
  84. * testing.
  85. *
  86. * For example:
  87. * {{{
  88. * public function skip() {
  89. * $this->_dbConfig = Connections::get('default', array('config' => true));
  90. * $hasDb = (isset($this->_dbConfig['adapter']) && $this->_dbConfig['adapter'] == 'MySql');
  91. * $message = 'Test database is either unavailable, or not using a MySQL adapter';
  92. * $this->skipIf(!$hasDb, $message);
  93. * }
  94. * }}}
  95. *
  96. * @return void
  97. */
  98. public function skip() {
  99. }
  100. /**
  101. * Skips test(s) if the condition is met.
  102. *
  103. * When used within a subclass' `skip` method, all tests are ignored if the condition is met,
  104. * otherwise processing continues as normal.
  105. * For other methods, only the remainder of the method is skipped, when the condition is met.
  106. *
  107. * @param boolean $condition
  108. * @param string $message Message to pass if the condition is met.
  109. * @return mixed
  110. */
  111. public function skipIf($condition, $message = 'Skipped test {:class}::{:function}()') {
  112. if (!$condition) {
  113. return;
  114. }
  115. $trace = Debugger::trace(array('start' => 2, 'depth' => 3, 'format' => 'array'));
  116. throw new Exception(String::insert($message, $trace));
  117. }
  118. /**
  119. * Returns the class name that is the subject under test for this test case.
  120. *
  121. * @return string
  122. */
  123. public function subject() {
  124. return preg_replace('/Test$/', '', str_replace('tests\\cases\\', '', get_class($this)));
  125. }
  126. /**
  127. * Return test methods to run
  128. *
  129. * @return array
  130. */
  131. public function methods() {
  132. static $methods;
  133. return $methods ?: $methods = array_values(preg_grep('/^test/', get_class_methods($this)));
  134. }
  135. /**
  136. * Runs the test methods in this test case, with the given options.
  137. *
  138. * @param array $options The options to use when running the test. Available options are:
  139. * - 'methods': An arbitrary array of method names to execute. If
  140. * unspecified, all methods starting with 'test' are run.
  141. * - 'reporter': A closure which gets called after each test result,
  142. * which may modify the results presented.
  143. * @return array
  144. */
  145. public function run(array $options = array()) {
  146. $defaults = array('methods' => array(), 'reporter' => null, 'handler' => null);
  147. $options += $defaults;
  148. $this->_results = array();
  149. $self = $this;
  150. try {
  151. $this->skip();
  152. } catch (Exception $e) {
  153. $this->_handleException($e);
  154. return $this->_results;
  155. }
  156. $h = function($code, $message, $file, $line = 0, $context = array()) use ($self) {
  157. $trace = debug_backtrace();
  158. $trace = array_slice($trace, 1, count($trace));
  159. $self->invokeMethod('_reportException', array(
  160. compact('code', 'message', 'file', 'line', 'trace', 'context')
  161. ));
  162. };
  163. $options['handler'] = $options['handler'] ?: $h;
  164. set_error_handler($options['handler']);
  165. $methods = $options['methods'] ?: $this->methods();
  166. $this->_reporter = $options['reporter'] ?: $this->_reporter;
  167. foreach ($methods as $method) {
  168. if ($this->_runTestMethod($method, $options) === false) {
  169. break;
  170. }
  171. }
  172. restore_error_handler();
  173. return $this->_results;
  174. }
  175. /**
  176. * General assert method used by others for common output.
  177. *
  178. * @param boolean $expression
  179. * @param string $message The message to output. If the message is not a string, then it will be
  180. * converted to '{:message}'. Use '{:message}' in the string and it will use the `$data`
  181. * to format the message with `String::insert()`.
  182. * @param array $data
  183. * @return void
  184. */
  185. public function assert($expression, $message = false, $data = array()) {
  186. if (!is_string($message)) {
  187. $message = '{:message}';
  188. }
  189. if (strpos($message, "{:message}") !== false) {
  190. $params = $data;
  191. $params['message'] = $this->_message($params);
  192. $message = String::insert($message, $params);
  193. }
  194. $trace = Debugger::trace(array('start' => 1, 'format' => 'array'));
  195. $methods = $this->methods();
  196. $i = 1;
  197. while ($i < count($trace)) {
  198. if (in_array($trace[$i]['function'], $methods) && $trace[$i - 1]['object'] == $this) {
  199. break;
  200. }
  201. $i++;
  202. }
  203. $class = isset($trace[$i - 1]['object']) ? get_class($trace[$i - 1]['object']) : null;
  204. $result = compact('class', 'message', 'data') + array(
  205. 'file' => $trace[$i - 1]['file'],
  206. 'line' => $trace[$i - 1]['line'],
  207. 'method' => $trace[$i]['function'],
  208. 'assertion' => $trace[$i - 1]['function'],
  209. );
  210. $this->_result(($expression ? 'pass' : 'fail'), $result);
  211. return $expression;
  212. }
  213. /**
  214. * Checks that the actual result is equal, but not neccessarily identical, to the expected
  215. * result.
  216. *
  217. * @param mixed $expected
  218. * @param mixed $result
  219. * @param string $message
  220. */
  221. public function assertEqual($expected, $result, $message = false) {
  222. $data = ($expected != $result) ? $this->_compare('equal', $expected, $result) : null;
  223. $this->assert($expected == $result, $message, $data);
  224. }
  225. /**
  226. * Checks that the actual result and the expected result are not equal to each other.
  227. *
  228. * @param mixed $expected
  229. * @param mixed $result
  230. * @param string $message
  231. */
  232. public function assertNotEqual($expected, $result, $message = false) {
  233. $this->assert($result != $expected, $message, compact('expected', 'result'));
  234. }
  235. /**
  236. * Checks that the actual result and the expected result are identical.
  237. *
  238. * @param mixed $expected
  239. * @param mixed $result
  240. * @param string $message
  241. */
  242. public function assertIdentical($expected, $result, $message = false) {
  243. $data = ($expected !== $result) ? $this->_compare('identical', $expected, $result) : null;
  244. $this->assert($expected === $result, $message, $data);
  245. }
  246. /**
  247. * Checks that the result evalutes to true.
  248. *
  249. * For example:
  250. * {{{
  251. * $this->assertTrue('false', 'String has content');
  252. * }}}
  253. * {{{
  254. * $this->assertTrue(10, 'Non-Zero value');
  255. * }}}
  256. * {{{
  257. * $this->assertTrue(true, 'Boolean true');
  258. * }}}
  259. * all evaluate to true.
  260. *
  261. * @param mixed $result
  262. * @param string $message
  263. */
  264. public function assertTrue($result, $message = '{:message}') {
  265. $expected = true;
  266. $this->assert(!empty($result), $message, compact('expected', 'result'));
  267. }
  268. /**
  269. * Checks that the result evalutes to false.
  270. *
  271. * For example:
  272. * {{{
  273. * $this->assertFalse('', 'String is empty');
  274. * }}}
  275. *
  276. * {{{
  277. * $this->assertFalse(0, 'Zero value');
  278. * }}}
  279. *
  280. * {{{
  281. * $this->assertFalse(false, 'Boolean false');
  282. * }}}
  283. * all evaluate to false.
  284. *
  285. * @param mixed $result
  286. * @param string $message
  287. */
  288. public function assertFalse($result, $message = '{:message}') {
  289. $expected = false;
  290. $this->assert(empty($result), $message, compact('expected', 'result'));
  291. }
  292. /**
  293. * Checks if the result is null.
  294. *
  295. * @param mixed $result
  296. * @param string $message
  297. */
  298. public function assertNull($result, $message = '{:message}') {
  299. $expected = null;
  300. $this->assert($result === null, $message, compact('expected', 'result'));
  301. }
  302. /**
  303. * Checks that the regular expression `$expected` is not matched in the result.
  304. *
  305. * @param mixed $expected
  306. * @param mixed $result
  307. * @param string $message
  308. */
  309. public function assertNoPattern($expected, $result, $message = '{:message}') {
  310. $this->assert(!preg_match($expected, $result), $message, compact('expected', 'result'));
  311. }
  312. /**
  313. * Checks that the regular expression `$expected` is matched in the result.
  314. *
  315. * @param mixed $expected
  316. * @param mixed $result
  317. * @param string $message
  318. */
  319. public function assertPattern($expected, $result, $message = '{:message}') {
  320. $this->assert(!!preg_match($expected, $result), $message, compact('expected', 'result'));
  321. }
  322. /**
  323. * Takes an array $expected and generates a regex from it to match the provided $string.
  324. * Samples for $expected:
  325. *
  326. * Checks for an input tag with a name attribute (contains any non-empty value) and an id
  327. * attribute that contains 'my-input':
  328. * {{{
  329. * array('input' => array('name', 'id' => 'my-input'))
  330. * }}}
  331. *
  332. * Checks for two p elements with some text in them:
  333. * {{{
  334. * array(
  335. * array('p' => true),
  336. * 'textA',
  337. * '/p',
  338. * array('p' => true),
  339. * 'textB',
  340. * '/p'
  341. * )
  342. * }}}
  343. *
  344. * You can also specify a pattern expression as part of the attribute values, or the tag
  345. * being defined, if you prepend the value with preg: and enclose it with slashes, like so:
  346. * {{{
  347. * array(
  348. * array('input' => array('name', 'id' => 'preg:/FieldName\d+/')),
  349. * 'preg:/My\s+field/'
  350. * )
  351. * }}}
  352. *
  353. * Important: This function is very forgiving about whitespace and also accepts any
  354. * permutation of attribute order. It will also allow whitespaces between specified tags.
  355. *
  356. * @param string $string An HTML/XHTML/XML string
  357. * @param array $expected An array, see above
  358. * @param boolean $fullDebug
  359. * @access public
  360. */
  361. function assertTags($string, $expected, $fullDebug = false) {
  362. $regex = array();
  363. $normalized = array();
  364. foreach ((array) $expected as $key => $val) {
  365. if (!is_numeric($key)) {
  366. $normalized[] = array($key => $val);
  367. } else {
  368. $normalized[] = $val;
  369. }
  370. }
  371. $i = 0;
  372. foreach ($normalized as $tags) {
  373. $i++;
  374. if (is_string($tags) && $tags{0} == '<') {
  375. $tags = array(substr($tags, 1) => array());
  376. } elseif (is_string($tags)) {
  377. $tagsTrimmed = preg_replace('/\s+/m', '', $tags);
  378. if (preg_match('/^\*?\//', $tags, $match) && $tagsTrimmed !== '//') {
  379. $prefix = array(null, null);
  380. if ($match[0] == '*/') {
  381. $prefix = array('Anything, ', '.*?');
  382. }
  383. $regex[] = array(
  384. sprintf('%sClose %s tag', $prefix[0], substr($tags, strlen($match[0]))),
  385. sprintf('%s<[\s]*\/[\s]*%s[\s]*>[\n\r]*', $prefix[1], substr(
  386. $tags, strlen($match[0])
  387. )),
  388. $i
  389. );
  390. continue;
  391. }
  392. if (!empty($tags) && preg_match('/^regex\:\/(.+)\/$/i', $tags, $matches)) {
  393. $tags = $matches[1];
  394. $type = 'Regex matches';
  395. } else {
  396. $tags = preg_quote($tags, '/');
  397. $type = 'Text equals';
  398. }
  399. $regex[] = array(sprintf('%s "%s"', $type, $tags), $tags, $i);
  400. continue;
  401. }
  402. foreach ($tags as $tag => $attributes) {
  403. $regex[] = array(
  404. sprintf('Open %s tag', $tag),
  405. sprintf('[\s]*<%s', preg_quote($tag, '/')),
  406. $i
  407. );
  408. if ($attributes === true) {
  409. $attributes = array();
  410. }
  411. $attrs = array();
  412. $explanations = array();
  413. foreach ($attributes as $attr => $val) {
  414. if (is_numeric($attr) && preg_match('/^regex\:\/(.+)\/$/i', $val, $matches)) {
  415. $attrs[] = $matches[1];
  416. $explanations[] = sprintf('Regex "%s" matches', $matches[1]);
  417. continue;
  418. } else {
  419. $quotes = '"';
  420. if (is_numeric($attr)) {
  421. $attr = $val;
  422. $val = '.+?';
  423. $explanations[] = sprintf('Attribute "%s" present', $attr);
  424. } elseif (
  425. !empty($val) && preg_match('/^regex\:\/(.+)\/$/i', $val, $matches)
  426. ) {
  427. $quotes = '"?';
  428. $val = $matches[1];
  429. $explanations[] = sprintf('Attribute "%s" matches "%s"', $attr, $val);
  430. } else {
  431. $explanations[] = sprintf('Attribute "%s" == "%s"', $attr, $val);
  432. $val = preg_quote($val, '/');
  433. }
  434. $attrs[] = '[\s]+' . preg_quote($attr, '/') . "={$quotes}{$val}{$quotes}";
  435. }
  436. }
  437. if ($attrs) {
  438. $permutations = $this->_arrayPermute($attrs);
  439. $permutationTokens = array();
  440. foreach ($permutations as $permutation) {
  441. $permutationTokens[] = join('', $permutation);
  442. }
  443. $regex[] = array(
  444. sprintf('%s', join(', ', $explanations)),
  445. $permutationTokens,
  446. $i
  447. );
  448. }
  449. $regex[] = array(sprintf('End %s tag', $tag), '[\s]*\/?[\s]*>[\n\r]*', $i);
  450. }
  451. }
  452. foreach ($regex as $i => $assertation) {
  453. list($description, $expressions, $itemNum) = $assertation;
  454. $matches = false;
  455. foreach ((array) $expressions as $expression) {
  456. if (preg_match(sprintf('/^%s/s', $expression), $string, $match)) {
  457. $matches = true;
  458. $string = substr($string, strlen($match[0]));
  459. break;
  460. }
  461. }
  462. if (!$matches) {
  463. $this->assert(false, sprintf(
  464. '- Item #%d / regex #%d failed: %s', $itemNum, $i, $description
  465. ));
  466. return false;
  467. }
  468. }
  469. return $this->assert(true);
  470. }
  471. /**
  472. * Assert Cookie data is properly set in headers.
  473. *
  474. * The value passed to `exepected` is an array of the cookie data, with at least the key and
  475. * value expected, but can support any of the following keys:
  476. * - `key`: the expected key
  477. * - `value`: the expected value
  478. * - `path`: optionally specifiy a path
  479. * - `name`: optionally specify the cookie name
  480. * - `expires`: optionally assert a specific expire time
  481. *
  482. * @param array $expected
  483. * @param array $headers When empty, value of `headers_list()` is used.
  484. */
  485. public function assertCookie($expected, $headers = null) {
  486. $defaults = array('path' => '/', 'name' => '[\w.-]+');
  487. $expected += $defaults;
  488. $headers = ($headers) ?: headers_list();
  489. $value = preg_quote(urlencode($expected['value']), '/');
  490. $key = explode('.', $expected['key']);
  491. $key = (count($key) == 1) ? '[' . current($key) . ']' : ('[' . join('][', $key) . ']');
  492. $key = preg_quote($key, '/');
  493. if (isset($expected['expires'])) {
  494. $date = gmdate('D, d-M-Y H:i:s \G\M\T', strtotime($expected['expires']));
  495. $expires = preg_quote($date, '/');
  496. } else {
  497. $expires = '(?:.+?)';
  498. }
  499. $path = preg_quote($expected['path'], '/');
  500. $pattern = "/^Set\-Cookie:\s{$expected['name']}$key=$value;";
  501. $pattern .= "\sexpires=$expires;\spath=$path/";
  502. $match = false;
  503. foreach ($headers as $header) {
  504. if (preg_match($pattern, $header)) {
  505. $match = true;
  506. continue;
  507. }
  508. }
  509. if (!$match) {
  510. $this->assert(false,
  511. sprintf('{:message} - Cookie %s not found in headers.', $pattern),
  512. compact('expected', 'result')
  513. );
  514. return false;
  515. }
  516. return $this->assert(true, '%s');
  517. }
  518. /**
  519. * Used before a call to `assert*()` if you expect the test assertion to generate an exception
  520. * or PHP error. If no error or exception is thrown, a test failure will be reported. Can
  521. * be called multiple times per assertion, if more than one error is expected.
  522. *
  523. * @param mixed $message A string indicating what the error text is expected to be. This can
  524. * be an exact string, a /-delimited regular expression, or true, indicating that
  525. * any error text is acceptable.
  526. * @return void
  527. */
  528. public function expectException($message = true) {
  529. $this->_expected[] = $message;
  530. }
  531. /**
  532. * Reports test result messages.
  533. *
  534. * @param string $type The type of result being reported. Can be `'pass'`, `'fail'`, `'skip'`
  535. * or `'exception'`.
  536. * @param array $info An array of information about the test result. At a minimum, this should
  537. * contain a `'message'` key. Other possible keys are `'file'`, `'line'`,
  538. * `'class'`, `'method'`, `'assertion'` and `'data'`.
  539. * @param array $options Currently unimplemented.
  540. * @return void
  541. */
  542. protected function _result($type, $info, array $options = array()) {
  543. $info = (array('result' => $type) + $info);
  544. $defaults = array();
  545. $options += $defaults;
  546. if ($this->_reporter) {
  547. $filtered = $this->_reporter->__invoke($info);
  548. $info = is_array($filtered) ? $filtered : $info;
  549. }
  550. $this->_results[] = $info;
  551. }
  552. /**
  553. * Runs an individual test method, collecting results and catching exceptions along the way.
  554. *
  555. * @param string $method The name of the test method to run.
  556. * @param array $options
  557. * @return void | false
  558. */
  559. protected function _runTestMethod($method, $options) {
  560. try {
  561. $this->setUp();
  562. } catch (Exception $e) {
  563. $this->_handleException($e, __LINE__ - 2);
  564. return $this->_results;
  565. }
  566. $params = compact('options', 'method');
  567. $passed = $this->_filter(__CLASS__ . '::run', $params, function($self, $params, $chain) {
  568. try {
  569. $method = $params['method'];
  570. $lineFlag = __LINE__ + 1;
  571. $self->$method();
  572. } catch (Exception $e) {
  573. $self->invokeMethod('_handleException', array($e));
  574. }
  575. });
  576. $this->tearDown();
  577. return $passed;
  578. }
  579. /**
  580. * Normalizes `Exception` objects and PHP error data into a single array format, and checks
  581. * each error against the list of expected errors (set using `expectException()`). If a match
  582. * is found, the expectation is removed from the stack and the error is ignored. If no match
  583. * is found, then the error data is logged to the test results.
  584. *
  585. * @see lithium\test\Unit::expectException()
  586. * @see lithium\test\Unit::_reportException()
  587. * @param mixed $exception An `Exception` object instance, or an array containing the following
  588. * keys: `'message'`, `'file'`, `'line'`, `'trace'` (in `debug_backtrace()`
  589. * format) and optionally `'code'` (error code number) and `'context'` (an array
  590. * of variables relevant to the scope of where the error occurred).
  591. * @param integer $lineFlag A flag used for determining the relevant scope of the call stack.
  592. * Set to the line number where test methods are called.
  593. * @return void
  594. */
  595. protected function _handleException($exception, $lineFlag = null) {
  596. $data = $exception;
  597. if (is_object($exception)) {
  598. $data = array();
  599. foreach (array('message', 'file', 'line', 'trace') as $key) {
  600. $method = 'get' . ucfirst($key);
  601. $data[$key] = $exception->{$method}();
  602. }
  603. $ref = $exception->getTrace();
  604. $ref = $ref[0] + array('class' => null);
  605. if ($ref['class'] == __CLASS__ && $ref['function'] == 'skipIf') {
  606. return $this->_result('skip', $data);
  607. }
  608. }
  609. return $this->_reportException($data, $lineFlag);
  610. }
  611. /**
  612. * Convert an exception object to an exception result array for test reporting.
  613. *
  614. * @param array $exception The exception data to report on. Statistics are gathered and
  615. * added to the reporting stack contained in `Unit::$_results`.
  616. * @param string $lineFlag
  617. * @return void
  618. * @todo Refactor so that reporters handle trace formatting.
  619. */
  620. protected function _reportException($exception, $lineFlag = null) {
  621. $message = $exception['message'];
  622. $isExpected = (($exp = end($this->_expected)) && ($exp === true || $exp == $message || (
  623. Validator::isRegex($exp) && preg_match($exp, $message)
  624. )));
  625. if ($isExpected) {
  626. return array_pop($this->_expected);
  627. }
  628. $initFrame = current($exception['trace']) + array('class' => '-', 'function' => '-');
  629. foreach ($exception['trace'] as $frame) {
  630. if (isset($scopedFrame)) {
  631. break;
  632. }
  633. if (!class_exists('lithium\analysis\Inspector')) {
  634. continue;
  635. }
  636. if (isset($frame['class']) && in_array($frame['class'], Inspector::parents($this))) {
  637. $scopedFrame = $frame;
  638. }
  639. }
  640. if (class_exists('lithium\analysis\Debugger')) {
  641. $exception['trace'] = Debugger::trace(array(
  642. 'trace' => $exception['trace'],
  643. 'format' => '{:functionRef}, line {:line}',
  644. 'includeScope' => false,
  645. 'scope' => array_filter(array(
  646. 'functionRef' => __NAMESPACE__ . '\{closure}',
  647. 'line' => $lineFlag
  648. )),
  649. ));
  650. }
  651. $this->_result('exception', $exception + array(
  652. 'class' => $initFrame['class'],
  653. 'method' => $initFrame['function']
  654. ));
  655. }
  656. /**
  657. * Compare the expected with the result. If `$result` is null `$expected` equals `$type`
  658. * and `$result` equals `$expected`.
  659. *
  660. * @param string $type The type of comparison either `'identical'` or `'equal'` (default).
  661. * @param mixed $expected The expected value.
  662. * @param mixed $result An optional result value, defaults to `null`
  663. * @param string $trace An optional trace used internally to track arrays and objects,
  664. * defaults to `null`.
  665. * @return array Data with the keys `trace'`, `'expected'` and `'result'`.
  666. */
  667. protected function _compare($type, $expected, $result = null, $trace = null) {
  668. $types = array('expected' => gettype($expected), 'result' => gettype($result));
  669. if ($types['expected'] !== $types['result']) {
  670. return array('trace' => $trace,
  671. 'expected' => trim("({$types['expected']}) " . print_r($expected, true)),
  672. 'result' => trim("({$types['result']}) " . print_r($result, true))
  673. );
  674. }
  675. $data = array();
  676. if (!is_scalar($expected)) {
  677. foreach ($expected as $key => $value) {
  678. $isObject = false;
  679. if (is_object($expected)) {
  680. $isObject = true;
  681. $expected = (array) $expected;
  682. $result = (array) $result;
  683. }
  684. $check = array_key_exists($key, $result) ? $result[$key] : array();
  685. $newTrace = "{$trace}[{$key}]";
  686. if ($isObject) {
  687. $newTrace = ($trace) ? "{$trace}->{$key}" : $key;
  688. $expected = (object) $expected;
  689. $result = (object) $result;
  690. }
  691. if ($type === 'identical') {
  692. if ($value === $check) {
  693. continue;
  694. }
  695. if ($check === array()) {
  696. $trace = $newTrace;
  697. return compact('trace', 'expected', 'result');
  698. }
  699. if (is_string($check)) {
  700. $trace = $newTrace;
  701. $expected = $value;
  702. $result = $check;
  703. return compact('trace', 'expected', 'result');
  704. }
  705. } else {
  706. if ($value == $check) {
  707. continue;
  708. }
  709. if (!is_array($value)) {
  710. $trace = $newTrace;
  711. return compact('trace', 'expected', 'result');
  712. }
  713. }
  714. $compare = $this->_compare($type, $value, $check, $newTrace);
  715. if ($compare !== true) {
  716. $data[] = $compare;
  717. }
  718. }
  719. return $data;
  720. }
  721. if ($type === 'identical') {
  722. if ($expected === $result) {
  723. return true;
  724. }
  725. return compact('trace', 'expected', 'result');
  726. }
  727. if ($expected == $result) {
  728. return true;
  729. }
  730. return compact('trace', 'expected', 'result');
  731. }
  732. /**
  733. * Returns a basic message for the data returned from `_result()`.
  734. *
  735. * @see lithium\test\Unit::assert()
  736. * @see lithium\test\Unit::_result()
  737. * @param array $data The data to use for creating the message.
  738. * @param string $message The string prepended to the generate message in the current scope.
  739. * @return string
  740. */
  741. protected function _message(&$data = array(), $message = null) {
  742. if (!empty($data[0])) {
  743. foreach ($data as $key => $value) {
  744. $message = (!empty($data[$key][0])) ? $message : null;
  745. $message .= $this->_message($value, $message);
  746. unset($data[$key]);
  747. }
  748. return $message;
  749. }
  750. $defaults = array('trace' => null, 'expected' => null, 'result' => null);
  751. $result = (array) $data + $defaults;
  752. $message = null;
  753. if (!empty($result['trace'])) {
  754. $message = sprintf("trace: %s\n", $result['trace']);
  755. }
  756. if (is_object($result['expected'])) {
  757. $result['expected'] = get_object_vars($result['expected']);
  758. }
  759. if (is_object($result['result'])) {
  760. $result['result'] = get_object_vars($result['result']);
  761. }
  762. return $message . sprintf("expected: %s\nresult: %s\n",
  763. var_export($result['expected'], true),
  764. var_export($result['result'], true)
  765. );
  766. }
  767. /**
  768. * Generates all permutation of an array $items and returns them in a new array.
  769. *
  770. * @param array $items An array of items
  771. * @param array $perms
  772. * @return array
  773. */
  774. protected function _arrayPermute($items, $perms = array()) {
  775. static $permuted;
  776. if (empty($perms)) {
  777. $permuted = array();
  778. }
  779. if (empty($items)) {
  780. $permuted[] = $perms;
  781. return;
  782. }
  783. $numItems = count($items) - 1;
  784. for ($i = $numItems; $i >= 0; --$i) {
  785. $newItems = $items;
  786. $newPerms = $perms;
  787. list($tmp) = array_splice($newItems, $i, 1);
  788. array_unshift($newPerms, $tmp);
  789. $this->_arrayPermute($newItems, $newPerms);
  790. }
  791. return $permuted;
  792. }
  793. /**
  794. * Removes everything from `resources/tmp/tests` directory.
  795. * Call from inside of your test method or `tearDown()`.
  796. *
  797. * @param string $path path to directory of contents to remove
  798. * if first character is NOT `/` prepend `LITHIUM_APP_PATH/resources/tmp/`
  799. * @return void
  800. */
  801. protected function _cleanUp($path = null) {
  802. $path = $path ?: LITHIUM_APP_PATH . '/resources/tmp/tests';
  803. $path = $path[0] !== '/' ? LITHIUM_APP_PATH . '/resources/tmp/' . $path : $path;
  804. if (!is_dir($path)) {
  805. return;
  806. }
  807. $dirs = new RecursiveDirectoryIterator($path);
  808. $iterator = new RecursiveIteratorIterator($dirs, RecursiveIteratorIterator::CHILD_FIRST);
  809. foreach ($iterator as $item) {
  810. if ($item->getPathname() === "{$path}/empty" || $iterator->isDot()) {
  811. continue;
  812. }
  813. ($item->isDir()) ? rmdir($item->getPathname()) : unlink($item->getPathname());
  814. }
  815. }
  816. /**
  817. * Returns the current results
  818. *
  819. * @return array The Results, currently
  820. */
  821. public function results() {
  822. return $this->_results;
  823. }
  824. }
  825. ?>