PageRenderTime 56ms CodeModel.GetById 21ms RepoModel.GetById 0ms app.codeStats 1ms

/test/Unit.php

https://github.com/ifunk/lithium
PHP | 995 lines | 744 code | 57 blank | 194 comment | 74 complexity | 0b278b31cc23ed6fc7b98a102523ad4e MD5 | raw file
Possible License(s): BSD-3-Clause
  1. <?php
  2. /**
  3. * Lithium: the most rad php framework
  4. *
  5. * @copyright Copyright 2011, 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. * Skips test(s) if the condition is met.
  101. *
  102. * When used within a subclass' `skip` method, all tests are ignored if the condition is met,
  103. * otherwise processing continues as normal.
  104. * For other methods, only the remainder of the method is skipped, when the condition is met.
  105. *
  106. * @throws Exception
  107. * @param boolean $condition
  108. * @param string|boolean $message Message to pass if the condition is met.
  109. * @return mixed
  110. */
  111. public function skipIf($condition, $message = false) {
  112. if ($condition) {
  113. throw new Exception(is_string($message) ? $message : null);
  114. }
  115. }
  116. /**
  117. * Returns the class name that is the subject under test for this test case.
  118. *
  119. * @return string
  120. */
  121. public function subject() {
  122. return preg_replace('/Test$/', '', str_replace('tests\\cases\\', '', get_class($this)));
  123. }
  124. /**
  125. * Return test methods to run
  126. *
  127. * @return array
  128. */
  129. public function methods() {
  130. static $methods;
  131. return $methods ?: $methods = array_values(preg_grep('/^test/', get_class_methods($this)));
  132. }
  133. /**
  134. * Runs the test methods in this test case, with the given options.
  135. *
  136. * @param array $options The options to use when running the test. Available options are:
  137. * - 'methods': An arbitrary array of method names to execute. If
  138. * unspecified, all methods starting with 'test' are run.
  139. * - 'reporter': A closure which gets called after each test result,
  140. * which may modify the results presented.
  141. * @return array
  142. */
  143. public function run(array $options = array()) {
  144. $defaults = array('methods' => array(), 'reporter' => null, 'handler' => null);
  145. $options += $defaults;
  146. $this->_results = array();
  147. $self = $this;
  148. try {
  149. $this->skip();
  150. } catch (Exception $e) {
  151. $this->_handleException($e);
  152. return $this->_results;
  153. }
  154. $h = function($code, $message, $file, $line = 0, $context = array()) use ($self) {
  155. $trace = debug_backtrace();
  156. $trace = array_slice($trace, 1, count($trace));
  157. $self->invokeMethod('_reportException', array(
  158. compact('code', 'message', 'file', 'line', 'trace', 'context')
  159. ));
  160. };
  161. $options['handler'] = $options['handler'] ?: $h;
  162. set_error_handler($options['handler']);
  163. $methods = $options['methods'] ?: $this->methods();
  164. $this->_reporter = $options['reporter'] ?: $this->_reporter;
  165. foreach ($methods as $method) {
  166. if ($this->_runTestMethod($method, $options) === false) {
  167. break;
  168. }
  169. }
  170. restore_error_handler();
  171. return $this->_results;
  172. }
  173. /**
  174. * General assert method used by others for common output.
  175. *
  176. * @param boolean $expression
  177. * @param string|boolean $message The message to output. If the message is not a string,
  178. * then it will be converted to '{:message}'. Use '{:message}' in the string and it
  179. * will use the `$data` to format the message with `String::insert()`.
  180. * @param array $data
  181. * @return void
  182. */
  183. public function assert($expression, $message = false, $data = array()) {
  184. if (!is_string($message)) {
  185. $message = '{:message}';
  186. }
  187. if (strpos($message, "{:message}") !== false) {
  188. $params = $data;
  189. $params['message'] = $this->_message($params);
  190. $message = String::insert($message, $params);
  191. }
  192. $trace = Debugger::trace(array(
  193. 'start' => 1, 'depth' => 4, 'format' => 'array', 'closures' => !$expression
  194. ));
  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. $method = isset($trace[$i]) ? $trace[$i]['function'] : $trace[$i - 1]['function'];
  205. $result = compact('class', 'method', 'message', 'data') + array(
  206. 'file' => $trace[$i - 1]['file'],
  207. 'line' => $trace[$i - 1]['line'],
  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|boolean $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|boolean $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|boolean $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. * @return boolean
  359. */
  360. function assertTags($string, $expected) {
  361. $regex = array();
  362. $normalized = array();
  363. foreach ((array) $expected as $key => $val) {
  364. if (!is_numeric($key)) {
  365. $normalized[] = array($key => $val);
  366. } else {
  367. $normalized[] = $val;
  368. }
  369. }
  370. $i = 0;
  371. foreach ($normalized as $tags) {
  372. $i++;
  373. if (is_string($tags) && $tags{0} == '<') {
  374. $tags = array(substr($tags, 1) => array());
  375. } elseif (is_string($tags)) {
  376. $tagsTrimmed = preg_replace('/\s+/m', '', $tags);
  377. if (preg_match('/^\*?\//', $tags, $match) && $tagsTrimmed !== '//') {
  378. $prefix = array(null, null);
  379. if ($match[0] == '*/') {
  380. $prefix = array('Anything, ', '.*?');
  381. }
  382. $regex[] = array(
  383. sprintf('%sClose %s tag', $prefix[0], substr($tags, strlen($match[0]))),
  384. sprintf('%s<[\s]*\/[\s]*%s[\s]*>[\n\r]*', $prefix[1], substr(
  385. $tags, strlen($match[0])
  386. )),
  387. $i
  388. );
  389. continue;
  390. }
  391. if (!empty($tags) && preg_match('/^regex\:\/(.+)\/$/i', $tags, $matches)) {
  392. $tags = $matches[1];
  393. $type = 'Regex matches';
  394. } else {
  395. $tags = preg_quote($tags, '/');
  396. $type = 'Text equals';
  397. }
  398. $regex[] = array(sprintf('%s "%s"', $type, $tags), $tags, $i);
  399. continue;
  400. }
  401. foreach ($tags as $tag => $attributes) {
  402. $regex[] = array(
  403. sprintf('Open %s tag', $tag),
  404. sprintf('[\s]*<%s', preg_quote($tag, '/')),
  405. $i
  406. );
  407. if ($attributes === true) {
  408. $attributes = array();
  409. }
  410. $attrs = array();
  411. $explanations = array();
  412. foreach ($attributes as $attr => $val) {
  413. if (is_numeric($attr) && preg_match('/^regex\:\/(.+)\/$/i', $val, $matches)) {
  414. $attrs[] = $matches[1];
  415. $explanations[] = sprintf('Regex "%s" matches', $matches[1]);
  416. continue;
  417. } else {
  418. $quotes = '"';
  419. if (is_numeric($attr)) {
  420. $attr = $val;
  421. $val = '.+?';
  422. $explanations[] = sprintf('Attribute "%s" present', $attr);
  423. } elseif (
  424. !empty($val) && preg_match('/^regex\:\/(.+)\/$/i', $val, $matches)
  425. ) {
  426. $quotes = '"?';
  427. $val = $matches[1];
  428. $explanations[] = sprintf('Attribute "%s" matches "%s"', $attr, $val);
  429. } else {
  430. $explanations[] = sprintf('Attribute "%s" == "%s"', $attr, $val);
  431. $val = preg_quote($val, '/');
  432. }
  433. $attrs[] = '[\s]+' . preg_quote($attr, '/') . "={$quotes}{$val}{$quotes}";
  434. }
  435. }
  436. if ($attrs) {
  437. $permutations = $this->_arrayPermute($attrs);
  438. $permutationTokens = array();
  439. foreach ($permutations as $permutation) {
  440. $permutationTokens[] = join('', $permutation);
  441. }
  442. $regex[] = array(
  443. sprintf('%s', join(', ', $explanations)),
  444. $permutationTokens,
  445. $i
  446. );
  447. }
  448. $regex[] = array(sprintf('End %s tag', $tag), '[\s]*\/?[\s]*>[\n\r]*', $i);
  449. }
  450. }
  451. foreach ($regex as $i => $assertation) {
  452. list($description, $expressions, $itemNum) = $assertation;
  453. $matches = false;
  454. foreach ((array) $expressions as $expression) {
  455. if (preg_match(sprintf('/^%s/s', $expression), $string, $match)) {
  456. $matches = true;
  457. $string = substr($string, strlen($match[0]));
  458. break;
  459. }
  460. }
  461. if (!$matches) {
  462. $this->assert(false, sprintf(
  463. '- Item #%d / regex #%d failed: %s', $itemNum, $i, $description
  464. ));
  465. return false;
  466. }
  467. }
  468. return $this->assert(true);
  469. }
  470. /**
  471. * Assert Cookie data is properly set in headers.
  472. *
  473. * The value passed to `exepected` is an array of the cookie data, with at least the key and
  474. * value expected, but can support any of the following keys:
  475. * - `key`: the expected key
  476. * - `value`: the expected value
  477. * - `path`: optionally specifiy a path
  478. * - `name`: optionally specify the cookie name
  479. * - `expires`: optionally assert a specific expire time
  480. *
  481. * @param array $expected
  482. * @param array $headers When empty, value of `headers_list()` is used.
  483. * @return boolean
  484. */
  485. public function assertCookie($expected, $headers = null) {
  486. $matched = $this->_cookieMatch($expected, $headers);
  487. if (!$matched['match']) {
  488. $message = sprintf('%s - Cookie not found in headers.', $matched['pattern']);
  489. $this->assert(false, $message, compact('expected', 'result'));
  490. return false;
  491. }
  492. return $this->assert(true, '%s');
  493. }
  494. /**
  495. * Assert Cookie data is *not* set in headers.
  496. *
  497. * The value passed to `expected` is an array of the cookie data, with at least the key and
  498. * value expected, but can support any of the following keys:
  499. * - `key`: the expected key
  500. * - `value`: the expected value
  501. * - `path`: optionally specify a path
  502. * - `name`: optionally specify the cookie name
  503. * - `expires`: optionally assert a specific expire time
  504. *
  505. * @param array $expected
  506. * @param array $headers When empty, value of `headers_list()` is used.
  507. * @return boolean
  508. */
  509. public function assertNoCookie($expected, $headers = null) {
  510. $matched = $this->_cookieMatch($expected, $headers);
  511. if ($matched['match']) {
  512. $message = sprintf('%s - Cookie found in headers.', $matched['pattern']);
  513. $this->assert(false, $message, compact('expected', 'result'));
  514. return false;
  515. }
  516. return $this->assert(true, '%s');
  517. }
  518. /**
  519. * Match an `$expected` cookie with the given headers. If no headers are provided, then
  520. * the value of `headers_list()` will be used.
  521. *
  522. * @param array $expected
  523. * @param array $headers When empty, value of `headers_list()` will be used.
  524. * @return boolean True if cookie is found, false otherwise.
  525. */
  526. protected function _cookieMatch($expected, $headers) {
  527. $defaults = array('path' => '/', 'name' => '[\w.-]+');
  528. $expected += $defaults;
  529. $headers = ($headers) ?: headers_list();
  530. $value = preg_quote(urlencode($expected['value']), '/');
  531. $key = explode('.', $expected['key']);
  532. $key = (count($key) == 1) ? '[' . current($key) . ']' : ('[' . join('][', $key) . ']');
  533. $key = preg_quote($key, '/');
  534. if (isset($expected['expires'])) {
  535. $date = gmdate('D, d-M-Y H:i:s \G\M\T', strtotime($expected['expires']));
  536. $expires = preg_quote($date, '/');
  537. } else {
  538. $expires = '(?:.+?)';
  539. }
  540. $path = preg_quote($expected['path'], '/');
  541. $pattern = "/^Set\-Cookie:\s{$expected['name']}$key=$value;";
  542. $pattern .= "\sexpires=$expires;\spath=$path/";
  543. $match = false;
  544. foreach ($headers as $header) {
  545. if (preg_match($pattern, $header)) {
  546. $match = true;
  547. continue;
  548. }
  549. }
  550. return compact('match', 'pattern');
  551. }
  552. /**
  553. * Used before a call to `assert*()` if you expect the test assertion to generate an exception
  554. * or PHP error. If no error or exception is thrown, a test failure will be reported. Can
  555. * be called multiple times per assertion, if more than one error is expected.
  556. *
  557. * @param mixed $message A string indicating what the error text is expected to be. This can
  558. * be an exact string, a /-delimited regular expression, or true, indicating that
  559. * any error text is acceptable.
  560. * @return void
  561. */
  562. public function expectException($message = true) {
  563. $this->_expected[] = $message;
  564. }
  565. /**
  566. * Reports test result messages.
  567. *
  568. * @param string $type The type of result being reported. Can be `'pass'`, `'fail'`, `'skip'`
  569. * or `'exception'`.
  570. * @param array $info An array of information about the test result. At a minimum, this should
  571. * contain a `'message'` key. Other possible keys are `'file'`, `'line'`,
  572. * `'class'`, `'method'`, `'assertion'` and `'data'`.
  573. * @param array $options Currently unimplemented.
  574. * @return void
  575. */
  576. protected function _result($type, $info, array $options = array()) {
  577. $info = (array('result' => $type) + $info);
  578. $defaults = array();
  579. $options += $defaults;
  580. if ($this->_reporter) {
  581. $filtered = $this->_reporter->__invoke($info);
  582. $info = is_array($filtered) ? $filtered : $info;
  583. }
  584. $this->_results[] = $info;
  585. }
  586. /**
  587. * Runs an individual test method, collecting results and catching exceptions along the way.
  588. *
  589. * @param string $method The name of the test method to run.
  590. * @param array $options
  591. * @return mixed
  592. * @filter
  593. */
  594. protected function _runTestMethod($method, $options) {
  595. try {
  596. $this->setUp();
  597. } catch (Exception $e) {
  598. $this->_handleException($e, __LINE__ - 2);
  599. return $this->_results;
  600. }
  601. $params = compact('options', 'method');
  602. $passed = $this->_filter(__CLASS__ . '::run', $params, function($self, $params, $chain) {
  603. try {
  604. $method = $params['method'];
  605. $lineFlag = __LINE__ + 1;
  606. $self->$method();
  607. } catch (Exception $e) {
  608. $self->invokeMethod('_handleException', array($e));
  609. }
  610. });
  611. $this->tearDown();
  612. return $passed;
  613. }
  614. /**
  615. * Normalizes `Exception` objects and PHP error data into a single array format, and checks
  616. * each error against the list of expected errors (set using `expectException()`). If a match
  617. * is found, the expectation is removed from the stack and the error is ignored. If no match
  618. * is found, then the error data is logged to the test results.
  619. *
  620. * @see lithium\test\Unit::expectException()
  621. * @see lithium\test\Unit::_reportException()
  622. * @param mixed $exception An `Exception` object instance, or an array containing the following
  623. * keys: `'message'`, `'file'`, `'line'`, `'trace'` (in `debug_backtrace()`
  624. * format) and optionally `'code'` (error code number) and `'context'` (an array
  625. * of variables relevant to the scope of where the error occurred).
  626. * @param integer $lineFlag A flag used for determining the relevant scope of the call stack.
  627. * Set to the line number where test methods are called.
  628. * @return void
  629. */
  630. protected function _handleException($exception, $lineFlag = null) {
  631. $data = $exception;
  632. if (is_object($exception)) {
  633. $data = array();
  634. foreach (array('message', 'file', 'line', 'trace') as $key) {
  635. $method = 'get' . ucfirst($key);
  636. $data[$key] = $exception->{$method}();
  637. }
  638. $ref = $exception->getTrace();
  639. $ref = $ref[0] + array('class' => null);
  640. if ($ref['class'] == __CLASS__ && $ref['function'] == 'skipIf') {
  641. return $this->_result('skip', $data);
  642. }
  643. }
  644. return $this->_reportException($data, $lineFlag);
  645. }
  646. /**
  647. * Convert an exception object to an exception result array for test reporting.
  648. *
  649. * @param array $exception The exception data to report on. Statistics are gathered and
  650. * added to the reporting stack contained in `Unit::$_results`.
  651. * @param string $lineFlag
  652. * @return void
  653. * @todo Refactor so that reporters handle trace formatting.
  654. */
  655. protected function _reportException($exception, $lineFlag = null) {
  656. $message = $exception['message'];
  657. $isExpected = (($exp = end($this->_expected)) && ($exp === true || $exp == $message || (
  658. Validator::isRegex($exp) && preg_match($exp, $message)
  659. )));
  660. if ($isExpected) {
  661. return array_pop($this->_expected);
  662. }
  663. $initFrame = current($exception['trace']) + array('class' => '-', 'function' => '-');
  664. foreach ($exception['trace'] as $frame) {
  665. if (isset($scopedFrame)) {
  666. break;
  667. }
  668. if (!class_exists('lithium\analysis\Inspector')) {
  669. continue;
  670. }
  671. if (isset($frame['class']) && in_array($frame['class'], Inspector::parents($this))) {
  672. $scopedFrame = $frame;
  673. }
  674. }
  675. if (class_exists('lithium\analysis\Debugger')) {
  676. $exception['trace'] = Debugger::trace(array(
  677. 'trace' => $exception['trace'],
  678. 'format' => '{:functionRef}, line {:line}',
  679. 'includeScope' => false,
  680. 'scope' => array_filter(array(
  681. 'functionRef' => __NAMESPACE__ . '\{closure}',
  682. 'line' => $lineFlag
  683. ))
  684. ));
  685. }
  686. $this->_result('exception', $exception + array(
  687. 'class' => $initFrame['class'],
  688. 'method' => $initFrame['function']
  689. ));
  690. }
  691. /**
  692. * Compare the expected with the result. If `$result` is null `$expected` equals `$type`
  693. * and `$result` equals `$expected`.
  694. *
  695. * @param string $type The type of comparison either `'identical'` or `'equal'` (default).
  696. * @param mixed $expected The expected value.
  697. * @param mixed $result An optional result value, defaults to `null`
  698. * @param string $trace An optional trace used internally to track arrays and objects,
  699. * defaults to `null`.
  700. * @return array Data with the keys `trace'`, `'expected'` and `'result'`.
  701. */
  702. protected function _compare($type, $expected, $result = null, $trace = null) {
  703. $compareTypes = function($expected, $result, $trace) {
  704. $types = array('expected' => gettype($expected), 'result' => gettype($result));
  705. if ($types['expected'] !== $types['result']) {
  706. $expected = trim("({$types['expected']}) " . print_r($expected, true));
  707. $result = trim("({$types['result']}) " . print_r($result, true));
  708. return compact('trace', 'expected', 'result');
  709. }
  710. };
  711. if ($types = $compareTypes($expected, $result, $trace)) {
  712. return $types;
  713. }
  714. $data = array();
  715. if (!is_scalar($expected)) {
  716. foreach ($expected as $key => $value) {
  717. $newTrace = "{$trace}[{$key}]";
  718. $isObject = false;
  719. if (is_object($expected)) {
  720. $isObject = true;
  721. $expected = (array) $expected;
  722. $result = (array) $result;
  723. }
  724. if (!array_key_exists($key, $result)) {
  725. $trace = (!$key) ? null : $newTrace;
  726. $expected = (!$key) ? $expected : $value;
  727. $result = ($key) ? null : $result;
  728. return compact('trace', 'expected', 'result');
  729. }
  730. $check = $result[$key];
  731. if ($isObject) {
  732. $newTrace = ($trace) ? "{$trace}->{$key}" : $key;
  733. $expected = (object) $expected;
  734. $result = (object) $result;
  735. }
  736. if ($type === 'identical') {
  737. if ($value === $check) {
  738. if ($types = $compareTypes($value, $check, $trace)) {
  739. return $types;
  740. }
  741. continue;
  742. }
  743. if ($check === array()) {
  744. $trace = $newTrace;
  745. return compact('trace', 'expected', 'result');
  746. }
  747. if (is_string($check)) {
  748. $trace = $newTrace;
  749. $expected = $value;
  750. $result = $check;
  751. return compact('trace', 'expected', 'result');
  752. }
  753. } else {
  754. if ($value == $check) {
  755. if ($types = $compareTypes($value, $check, $trace)) {
  756. return $types;
  757. }
  758. continue;
  759. }
  760. if (!is_array($value)) {
  761. $trace = $newTrace;
  762. return compact('trace', 'expected', 'result');
  763. }
  764. }
  765. $compare = $this->_compare($type, $value, $check, $newTrace);
  766. if ($compare !== true) {
  767. $data[] = $compare;
  768. }
  769. }
  770. if (!empty($data)) {
  771. return $data;
  772. }
  773. }
  774. if (!is_scalar($result)) {
  775. $data = $this->_compare($type, $result, $expected);
  776. if (!empty($data)) {
  777. return array(
  778. 'trace' => $data['trace'],
  779. 'expected' => $data['result'],
  780. 'result' => $data['expected']
  781. );
  782. }
  783. }
  784. if ((($type === 'identical') ? $expected === $result : $expected == $result)) {
  785. if ($types = $compareTypes($expected, $result, $trace)) {
  786. return $types;
  787. }
  788. return true;
  789. }
  790. return compact('trace', 'expected', 'result');
  791. }
  792. /**
  793. * Returns a basic message for the data returned from `_result()`.
  794. *
  795. * @see lithium\test\Unit::assert()
  796. * @see lithium\test\Unit::_result()
  797. * @param array $data The data to use for creating the message.
  798. * @param string $message The string prepended to the generate message in the current scope.
  799. * @return string
  800. */
  801. protected function _message(&$data = array(), $message = null) {
  802. if (!empty($data[0])) {
  803. foreach ($data as $key => $value) {
  804. $message = (!empty($data[$key][0])) ? $message : null;
  805. $message .= $this->_message($value, $message);
  806. unset($data[$key]);
  807. }
  808. return $message;
  809. }
  810. $defaults = array('trace' => null, 'expected' => null, 'result' => null);
  811. $result = (array) $data + $defaults;
  812. $message = null;
  813. if (!empty($result['trace'])) {
  814. $message = sprintf("trace: %s\n", $result['trace']);
  815. }
  816. if (is_object($result['expected'])) {
  817. $result['expected'] = get_object_vars($result['expected']);
  818. }
  819. if (is_object($result['result'])) {
  820. $result['result'] = get_object_vars($result['result']);
  821. }
  822. return $message . sprintf("expected: %s\nresult: %s\n",
  823. var_export($result['expected'], true),
  824. var_export($result['result'], true)
  825. );
  826. }
  827. /**
  828. * Generates all permutation of an array $items and returns them in a new array.
  829. *
  830. * @param array $items An array of items
  831. * @param array $perms
  832. * @return array
  833. */
  834. protected function _arrayPermute($items, $perms = array()) {
  835. static $permuted;
  836. if (empty($perms)) {
  837. $permuted = array();
  838. }
  839. if (empty($items)) {
  840. $permuted[] = $perms;
  841. return;
  842. }
  843. $numItems = count($items) - 1;
  844. for ($i = $numItems; $i >= 0; --$i) {
  845. $newItems = $items;
  846. $newPerms = $perms;
  847. list($tmp) = array_splice($newItems, $i, 1);
  848. array_unshift($newPerms, $tmp);
  849. $this->_arrayPermute($newItems, $newPerms);
  850. }
  851. return $permuted;
  852. }
  853. /**
  854. * Removes everything from `resources/tmp/tests` directory.
  855. * Call from inside of your test method or `tearDown()`.
  856. *
  857. * @param string $path path to directory of contents to remove
  858. * if first character is NOT `/` prepend `LITHIUM_APP_PATH/resources/tmp/`
  859. * @return void
  860. */
  861. protected function _cleanUp($path = null) {
  862. $resources = Libraries::get(true, 'resources');
  863. $path = $path ?: $resources . '/tmp/tests';
  864. $path = preg_match('/^\w:|^\//', $path) ? $path : $resources . '/tmp/' . $path;
  865. if (!is_dir($path)) {
  866. return;
  867. }
  868. $dirs = new RecursiveDirectoryIterator($path);
  869. $iterator = new RecursiveIteratorIterator($dirs, RecursiveIteratorIterator::CHILD_FIRST);
  870. foreach ($iterator as $item) {
  871. if ($item->getPathname() === "{$path}/empty" || $iterator->isDot()) {
  872. continue;
  873. }
  874. ($item->isDir()) ? rmdir($item->getPathname()) : unlink($item->getPathname());
  875. }
  876. }
  877. /**
  878. * Returns the current results
  879. *
  880. * @return array The Results, currently
  881. */
  882. public function results() {
  883. return $this->_results;
  884. }
  885. /**
  886. * Checks for a working internet connection.
  887. *
  888. * This method is used to check for a working connection to lithify.me, both
  889. * testing for proper dns resolution and reading the actual URL.
  890. *
  891. * @param array $options Override the default URI to check.
  892. * @return boolean True if a network connection is established, false otherwise.
  893. */
  894. protected function _hasNetwork($config = array()) {
  895. $defaults = array(
  896. 'scheme' => 'http',
  897. 'host' => 'lithify.me'
  898. );
  899. $config += $defaults;
  900. $url = "{$config['scheme']}://{$config['host']}";
  901. $failed = false;
  902. set_error_handler(function($errno, $errstr) use (&$failed) {
  903. $failed = true;
  904. });
  905. $dnsCheck = dns_check_record($config['host'], "ANY");
  906. $fileCheck = fopen($url, "r");
  907. restore_error_handler();
  908. return !$failed;
  909. }
  910. }
  911. ?>