PageRenderTime 29ms CodeModel.GetById 24ms RepoModel.GetById 1ms app.codeStats 0ms

/test/Unit.php

https://github.com/Daikoun/lithium
PHP | 1058 lines | 790 code | 61 blank | 207 comment | 80 complexity | dd366ef1bef0dc6836f99a11758944d3 MD5 | raw file
Possible License(s): BSD-3-Clause
  1. <?php
  2. /**
  3. * Lithium: the most rad php framework
  4. *
  5. * @copyright Copyright 2012, 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. * Generates a failed test with the passed message.
  215. *
  216. * @param string $message
  217. */
  218. public function fail($message = false) {
  219. $this->assert(false, $message);
  220. }
  221. /**
  222. * Checks that the actual result is equal, but not neccessarily identical, to the expected
  223. * result.
  224. *
  225. * @param mixed $expected
  226. * @param mixed $result
  227. * @param string|boolean $message
  228. */
  229. public function assertEqual($expected, $result, $message = false) {
  230. $data = ($expected != $result) ? $this->_compare('equal', $expected, $result) : null;
  231. $this->assert($expected == $result, $message, $data);
  232. }
  233. /**
  234. * Checks that the actual result and the expected result are not equal to each other.
  235. *
  236. * @param mixed $expected
  237. * @param mixed $result
  238. * @param string|boolean $message
  239. */
  240. public function assertNotEqual($expected, $result, $message = false) {
  241. $this->assert($result != $expected, $message, compact('expected', 'result'));
  242. }
  243. /**
  244. * Checks that the actual result and the expected result are identical.
  245. *
  246. * @param mixed $expected
  247. * @param mixed $result
  248. * @param string|boolean $message
  249. */
  250. public function assertIdentical($expected, $result, $message = false) {
  251. $data = ($expected !== $result) ? $this->_compare('identical', $expected, $result) : null;
  252. $this->assert($expected === $result, $message, $data);
  253. }
  254. /**
  255. * Checks that the result evaluates to true.
  256. *
  257. * For example:
  258. * {{{
  259. * $this->assertTrue('false', 'String has content');
  260. * }}}
  261. * {{{
  262. * $this->assertTrue(10, 'Non-Zero value');
  263. * }}}
  264. * {{{
  265. * $this->assertTrue(true, 'Boolean true');
  266. * }}}
  267. * all evaluate to true.
  268. *
  269. * @param mixed $result
  270. * @param string $message
  271. */
  272. public function assertTrue($result, $message = '{:message}') {
  273. $expected = true;
  274. $this->assert(!empty($result), $message, compact('expected', 'result'));
  275. }
  276. /**
  277. * Checks that the result evaluates to false.
  278. *
  279. * For example:
  280. * {{{
  281. * $this->assertFalse('', 'String is empty');
  282. * }}}
  283. *
  284. * {{{
  285. * $this->assertFalse(0, 'Zero value');
  286. * }}}
  287. *
  288. * {{{
  289. * $this->assertFalse(false, 'Boolean false');
  290. * }}}
  291. * all evaluate to false.
  292. *
  293. * @param mixed $result
  294. * @param string $message
  295. */
  296. public function assertFalse($result, $message = '{:message}') {
  297. $expected = false;
  298. $this->assert(empty($result), $message, compact('expected', 'result'));
  299. }
  300. /**
  301. * Checks if the result is null.
  302. *
  303. * @param mixed $result
  304. * @param string $message
  305. */
  306. public function assertNull($result, $message = '{:message}') {
  307. $expected = null;
  308. $this->assert($result === null, $message, compact('expected', 'result'));
  309. }
  310. /**
  311. * Checks that the regular expression `$expected` is not matched in the result.
  312. *
  313. * @param mixed $expected
  314. * @param mixed $result
  315. * @param string $message
  316. */
  317. public function assertNoPattern($expected, $result, $message = '{:message}') {
  318. $this->assert(!preg_match($expected, $result), $message, compact('expected', 'result'));
  319. }
  320. /**
  321. * Checks that the regular expression `$expected` is matched in the result.
  322. *
  323. * @param mixed $expected
  324. * @param mixed $result
  325. * @param string $message
  326. */
  327. public function assertPattern($expected, $result, $message = '{:message}') {
  328. $this->assert(!!preg_match($expected, $result), $message, compact('expected', 'result'));
  329. }
  330. /**
  331. * Takes an array $expected and generates a regex from it to match the provided $string.
  332. * Samples for $expected:
  333. *
  334. * Checks for an input tag with a name attribute (contains any non-empty value) and an id
  335. * attribute that contains 'my-input':
  336. * {{{
  337. * array('input' => array('name', 'id' => 'my-input'))
  338. * }}}
  339. *
  340. * Checks for two p elements with some text in them:
  341. * {{{
  342. * array(
  343. * array('p' => true),
  344. * 'textA',
  345. * '/p',
  346. * array('p' => true),
  347. * 'textB',
  348. * '/p'
  349. * )
  350. * }}}
  351. *
  352. * You can also specify a pattern expression as part of the attribute values, or the tag
  353. * being defined, if you prepend the value with preg: and enclose it with slashes, like so:
  354. * {{{
  355. * array(
  356. * array('input' => array('name', 'id' => 'preg:/FieldName\d+/')),
  357. * 'preg:/My\s+field/'
  358. * )
  359. * }}}
  360. *
  361. * Important: This function is very forgiving about whitespace and also accepts any
  362. * permutation of attribute order. It will also allow whitespaces between specified tags.
  363. *
  364. * @param string $string An HTML/XHTML/XML string
  365. * @param array $expected An array, see above
  366. * @return boolean
  367. */
  368. function assertTags($string, $expected) {
  369. $regex = array();
  370. $normalized = array();
  371. foreach ((array) $expected as $key => $val) {
  372. if (!is_numeric($key)) {
  373. $normalized[] = array($key => $val);
  374. } else {
  375. $normalized[] = $val;
  376. }
  377. }
  378. $i = 0;
  379. foreach ($normalized as $tags) {
  380. $i++;
  381. if (is_string($tags) && $tags{0} == '<') {
  382. $tags = array(substr($tags, 1) => array());
  383. } elseif (is_string($tags)) {
  384. $tagsTrimmed = preg_replace('/\s+/m', '', $tags);
  385. if (preg_match('/^\*?\//', $tags, $match) && $tagsTrimmed !== '//') {
  386. $prefix = array(null, null);
  387. if ($match[0] == '*/') {
  388. $prefix = array('Anything, ', '.*?');
  389. }
  390. $regex[] = array(
  391. sprintf('%sClose %s tag', $prefix[0], substr($tags, strlen($match[0]))),
  392. sprintf('%s<[\s]*\/[\s]*%s[\s]*>[\n\r]*', $prefix[1], substr(
  393. $tags, strlen($match[0])
  394. )),
  395. $i
  396. );
  397. continue;
  398. }
  399. if (!empty($tags) && preg_match('/^regex\:\/(.+)\/$/i', $tags, $matches)) {
  400. $tags = $matches[1];
  401. $type = 'Regex matches';
  402. } else {
  403. $tags = preg_quote($tags, '/');
  404. $type = 'Text equals';
  405. }
  406. $regex[] = array(sprintf('%s "%s"', $type, $tags), $tags, $i);
  407. continue;
  408. }
  409. foreach ($tags as $tag => $attributes) {
  410. $regex[] = array(
  411. sprintf('Open %s tag', $tag),
  412. sprintf('[\s]*<%s', preg_quote($tag, '/')),
  413. $i
  414. );
  415. if ($attributes === true) {
  416. $attributes = array();
  417. }
  418. $attrs = array();
  419. $explanations = array();
  420. foreach ($attributes as $attr => $val) {
  421. if (is_numeric($attr) && preg_match('/^regex\:\/(.+)\/$/i', $val, $matches)) {
  422. $attrs[] = $matches[1];
  423. $explanations[] = sprintf('Regex "%s" matches', $matches[1]);
  424. continue;
  425. } else {
  426. $quotes = '"';
  427. if (is_numeric($attr)) {
  428. $attr = $val;
  429. $val = '.+?';
  430. $explanations[] = sprintf('Attribute "%s" present', $attr);
  431. } elseif (
  432. !empty($val) && preg_match('/^regex\:\/(.+)\/$/i', $val, $matches)
  433. ) {
  434. $quotes = '"?';
  435. $val = $matches[1];
  436. $explanations[] = sprintf('Attribute "%s" matches "%s"', $attr, $val);
  437. } else {
  438. $explanations[] = sprintf('Attribute "%s" == "%s"', $attr, $val);
  439. $val = preg_quote($val, '/');
  440. }
  441. $attrs[] = '[\s]+' . preg_quote($attr, '/') . "={$quotes}{$val}{$quotes}";
  442. }
  443. }
  444. if ($attrs) {
  445. $permutations = $this->_arrayPermute($attrs);
  446. $permutationTokens = array();
  447. foreach ($permutations as $permutation) {
  448. $permutationTokens[] = join('', $permutation);
  449. }
  450. $regex[] = array(
  451. sprintf('%s', join(', ', $explanations)),
  452. $permutationTokens,
  453. $i
  454. );
  455. }
  456. $regex[] = array(sprintf('End %s tag', $tag), '[\s]*\/?[\s]*>[\n\r]*', $i);
  457. }
  458. }
  459. foreach ($regex as $i => $assertation) {
  460. list($description, $expressions, $itemNum) = $assertation;
  461. $matches = false;
  462. foreach ((array) $expressions as $expression) {
  463. if (preg_match(sprintf('/^%s/s', $expression), $string, $match)) {
  464. $matches = true;
  465. $string = substr($string, strlen($match[0]));
  466. break;
  467. }
  468. }
  469. if (!$matches) {
  470. $this->assert(false, sprintf(
  471. '- Item #%d / regex #%d failed: %s', $itemNum, $i, $description
  472. ));
  473. return false;
  474. }
  475. }
  476. return $this->assert(true);
  477. }
  478. /**
  479. * Assert that the code passed in a closure throws an exception matching the passed expected
  480. * exception.
  481. *
  482. * The value passed to `exepected` is either an exception class name or the expected message.
  483. *
  484. * @param mixed $expected A string indicating what the error text is expected to be. This can
  485. * be an exact string, a /-delimited regular expression, or true, indicating that
  486. * any error text is acceptable.
  487. * @param closure $closure A closure containing the code that should throw the exception.
  488. * @param string $message
  489. * @return boolean
  490. */
  491. public function assertException($expected, $closure, $message = '{:message}') {
  492. try {
  493. $closure();
  494. $message = sprintf('An exception "%s" was expected but not thrown.', $expected);
  495. return $this->assert(false, $message, compact('expected', 'result'));
  496. } catch (Exception $e) {
  497. $class = get_class($e);
  498. $eMessage = $e->getMessage();
  499. if (get_class($e) == $expected) {
  500. $result = $class;
  501. return $this->assert(true, $message, compact('expected', 'result'));
  502. }
  503. if ($eMessage == $expected) {
  504. $result = $eMessage;
  505. return $this->assert(true, $message, compact('expected', 'result'));
  506. }
  507. if (Validator::isRegex($expected) && preg_match($expected, $eMessage)) {
  508. $result = $eMessage;
  509. return $this->assert(true, $message, compact('expected', 'result'));
  510. }
  511. $message = sprintf(
  512. 'Exception "%s" was expected. Exception "%s" with message "%s" was thrown instead.',
  513. $expected, get_class($e), $eMessage);
  514. return $this->assert(false, $message);
  515. }
  516. }
  517. /**
  518. * Assert Cookie data is properly set in headers.
  519. *
  520. * The value passed to `exepected` is an array of the cookie data, with at least the key and
  521. * value expected, but can support any of the following keys:
  522. * - `key`: the expected key
  523. * - `value`: the expected value
  524. * - `path`: optionally specifiy a path
  525. * - `name`: optionally specify the cookie name
  526. * - `expires`: optionally assert a specific expire time
  527. *
  528. * @param array $expected
  529. * @param array $headers When empty, value of `headers_list()` is used.
  530. * @return boolean
  531. */
  532. public function assertCookie($expected, $headers = null) {
  533. $matched = $this->_cookieMatch($expected, $headers);
  534. if (!$matched['match']) {
  535. $message = sprintf('%s - Cookie not found in headers.', $matched['pattern']);
  536. $this->assert(false, $message, compact('expected', 'result'));
  537. return false;
  538. }
  539. return $this->assert(true, '%s');
  540. }
  541. /**
  542. * Assert Cookie data is *not* set in headers.
  543. *
  544. * The value passed to `expected` is an array of the cookie data, with at least the key and
  545. * value expected, but can support any of the following keys:
  546. * - `key`: the expected key
  547. * - `value`: the expected value
  548. * - `path`: optionally specify a path
  549. * - `name`: optionally specify the cookie name
  550. * - `expires`: optionally assert a specific expire time
  551. *
  552. * @param array $expected
  553. * @param array $headers When empty, value of `headers_list()` is used.
  554. * @return boolean
  555. */
  556. public function assertNoCookie($expected, $headers = null) {
  557. $matched = $this->_cookieMatch($expected, $headers);
  558. if ($matched['match']) {
  559. $message = sprintf('%s - Cookie found in headers.', $matched['pattern']);
  560. $this->assert(false, $message, compact('expected', 'result'));
  561. return false;
  562. }
  563. return $this->assert(true, '%s');
  564. }
  565. /**
  566. * Match an `$expected` cookie with the given headers. If no headers are provided, then
  567. * the value of `headers_list()` will be used.
  568. *
  569. * @param array $expected
  570. * @param array $headers When empty, value of `headers_list()` will be used.
  571. * @return boolean True if cookie is found, false otherwise.
  572. */
  573. protected function _cookieMatch($expected, $headers) {
  574. $defaults = array('path' => '/', 'name' => '[\w.-]+');
  575. $expected += $defaults;
  576. $headers = ($headers) ?: headers_list();
  577. $value = preg_quote(urlencode($expected['value']), '/');
  578. $key = explode('.', $expected['key']);
  579. $key = (count($key) == 1) ? '[' . current($key) . ']' : ('[' . join('][', $key) . ']');
  580. $key = preg_quote($key, '/');
  581. if (isset($expected['expires'])) {
  582. $date = gmdate('D, d-M-Y H:i:s \G\M\T', strtotime($expected['expires']));
  583. $expires = preg_quote($date, '/');
  584. } else {
  585. $expires = '(?:.+?)';
  586. }
  587. $path = preg_quote($expected['path'], '/');
  588. $pattern = "/^Set\-Cookie:\s{$expected['name']}$key=$value;";
  589. $pattern .= "\sexpires=$expires;\spath=$path/";
  590. $match = false;
  591. foreach ($headers as $header) {
  592. if (preg_match($pattern, $header)) {
  593. $match = true;
  594. continue;
  595. }
  596. }
  597. return compact('match', 'pattern');
  598. }
  599. /**
  600. * Used before a call to `assert*()` if you expect the test assertion to generate an exception
  601. * or PHP error. If no error or exception is thrown, a test failure will be reported. Can
  602. * be called multiple times per assertion, if more than one error is expected.
  603. *
  604. * @param mixed $message A string indicating what the error text is expected to be. This can
  605. * be an exact string, a /-delimited regular expression, or true, indicating that
  606. * any error text is acceptable.
  607. * @return void
  608. */
  609. public function expectException($message = true) {
  610. $this->_expected[] = $message;
  611. }
  612. /**
  613. * Reports test result messages.
  614. *
  615. * @param string $type The type of result being reported. Can be `'pass'`, `'fail'`, `'skip'`
  616. * or `'exception'`.
  617. * @param array $info An array of information about the test result. At a minimum, this should
  618. * contain a `'message'` key. Other possible keys are `'file'`, `'line'`,
  619. * `'class'`, `'method'`, `'assertion'` and `'data'`.
  620. * @param array $options Currently unimplemented.
  621. * @return void
  622. */
  623. protected function _result($type, $info, array $options = array()) {
  624. $info = (array('result' => $type) + $info);
  625. $defaults = array();
  626. $options += $defaults;
  627. if ($this->_reporter) {
  628. $filtered = $this->_reporter->__invoke($info);
  629. $info = is_array($filtered) ? $filtered : $info;
  630. }
  631. $this->_results[] = $info;
  632. }
  633. /**
  634. * Runs an individual test method, collecting results and catching exceptions along the way.
  635. *
  636. * @param string $method The name of the test method to run.
  637. * @param array $options
  638. * @return mixed
  639. * @filter
  640. */
  641. protected function _runTestMethod($method, $options) {
  642. try {
  643. $this->setUp();
  644. } catch (Exception $e) {
  645. $this->_handleException($e, __LINE__ - 2);
  646. return $this->_results;
  647. }
  648. $params = compact('options', 'method');
  649. $passed = $this->_filter(__CLASS__ . '::run', $params, function($self, $params, $chain) {
  650. try {
  651. $method = $params['method'];
  652. $lineFlag = __LINE__ + 1;
  653. $self->{$method}();
  654. } catch (Exception $e) {
  655. $self->invokeMethod('_handleException', array($e));
  656. }
  657. });
  658. foreach ($this->_expected as $expected) {
  659. $this->_result('fail', compact('method') + array(
  660. 'class' => get_class($this),
  661. 'message' => "Expected exception matching `{$expected}` uncaught.",
  662. 'data' => array(),
  663. 'file' => null,
  664. 'line' => null,
  665. 'assertion' => 'expectException'
  666. ));
  667. }
  668. $this->_expected = array();
  669. $this->tearDown();
  670. return $passed;
  671. }
  672. /**
  673. * Normalizes `Exception` objects and PHP error data into a single array format, and checks
  674. * each error against the list of expected errors (set using `expectException()`). If a match
  675. * is found, the expectation is removed from the stack and the error is ignored. If no match
  676. * is found, then the error data is logged to the test results.
  677. *
  678. * @see lithium\test\Unit::expectException()
  679. * @see lithium\test\Unit::_reportException()
  680. * @param mixed $exception An `Exception` object instance, or an array containing the following
  681. * keys: `'message'`, `'file'`, `'line'`, `'trace'` (in `debug_backtrace()`
  682. * format) and optionally `'code'` (error code number) and `'context'` (an array
  683. * of variables relevant to the scope of where the error occurred).
  684. * @param integer $lineFlag A flag used for determining the relevant scope of the call stack.
  685. * Set to the line number where test methods are called.
  686. * @return void
  687. */
  688. protected function _handleException($exception, $lineFlag = null) {
  689. $data = $exception;
  690. if (is_object($exception)) {
  691. $data = array();
  692. foreach (array('message', 'file', 'line', 'trace') as $key) {
  693. $method = 'get' . ucfirst($key);
  694. $data[$key] = $exception->{$method}();
  695. }
  696. $ref = $exception->getTrace();
  697. $ref = $ref[0] + array('class' => null);
  698. if ($ref['class'] == __CLASS__ && $ref['function'] == 'skipIf') {
  699. return $this->_result('skip', $data);
  700. }
  701. }
  702. return $this->_reportException($data, $lineFlag);
  703. }
  704. /**
  705. * Convert an exception object to an exception result array for test reporting.
  706. *
  707. * @param array $exception The exception data to report on. Statistics are gathered and
  708. * added to the reporting stack contained in `Unit::$_results`.
  709. * @param string $lineFlag
  710. * @return void
  711. * @todo Refactor so that reporters handle trace formatting.
  712. */
  713. protected function _reportException($exception, $lineFlag = null) {
  714. $message = $exception['message'];
  715. $isExpected = (($exp = end($this->_expected)) && ($exp === true || $exp == $message || (
  716. Validator::isRegex($exp) && preg_match($exp, $message)
  717. )));
  718. if ($isExpected) {
  719. return array_pop($this->_expected);
  720. }
  721. $initFrame = current($exception['trace']) + array('class' => '-', 'function' => '-');
  722. foreach ($exception['trace'] as $frame) {
  723. if (isset($scopedFrame)) {
  724. break;
  725. }
  726. if (!class_exists('lithium\analysis\Inspector')) {
  727. continue;
  728. }
  729. if (isset($frame['class']) && in_array($frame['class'], Inspector::parents($this))) {
  730. $scopedFrame = $frame;
  731. }
  732. }
  733. if (class_exists('lithium\analysis\Debugger')) {
  734. $exception['trace'] = Debugger::trace(array(
  735. 'trace' => $exception['trace'],
  736. 'format' => '{:functionRef}, line {:line}',
  737. 'includeScope' => false,
  738. 'scope' => array_filter(array(
  739. 'functionRef' => __NAMESPACE__ . '\{closure}',
  740. 'line' => $lineFlag
  741. ))
  742. ));
  743. }
  744. $this->_result('exception', $exception + array(
  745. 'class' => $initFrame['class'],
  746. 'method' => $initFrame['function']
  747. ));
  748. }
  749. /**
  750. * Compare the expected with the result. If `$result` is null `$expected` equals `$type`
  751. * and `$result` equals `$expected`.
  752. *
  753. * @param string $type The type of comparison either `'identical'` or `'equal'` (default).
  754. * @param mixed $expected The expected value.
  755. * @param mixed $result An optional result value, defaults to `null`
  756. * @param string $trace An optional trace used internally to track arrays and objects,
  757. * defaults to `null`.
  758. * @return array Data with the keys `trace'`, `'expected'` and `'result'`.
  759. */
  760. protected function _compare($type, $expected, $result = null, $trace = null) {
  761. $compareTypes = function($expected, $result, $trace) {
  762. $types = array('expected' => gettype($expected), 'result' => gettype($result));
  763. if ($types['expected'] !== $types['result']) {
  764. $expected = trim("({$types['expected']}) " . print_r($expected, true));
  765. $result = trim("({$types['result']}) " . print_r($result, true));
  766. return compact('trace', 'expected', 'result');
  767. }
  768. };
  769. if ($types = $compareTypes($expected, $result, $trace)) {
  770. return $types;
  771. }
  772. $data = array();
  773. if (!is_scalar($expected)) {
  774. foreach ($expected as $key => $value) {
  775. $newTrace = "{$trace}[{$key}]";
  776. $isObject = false;
  777. if (is_object($expected)) {
  778. $isObject = true;
  779. $expected = (array) $expected;
  780. $result = (array) $result;
  781. }
  782. if (!array_key_exists($key, $result)) {
  783. $trace = (!$key) ? null : $newTrace;
  784. $expected = (!$key) ? $expected : $value;
  785. $result = ($key) ? null : $result;
  786. return compact('trace', 'expected', 'result');
  787. }
  788. $check = $result[$key];
  789. if ($isObject) {
  790. $newTrace = ($trace) ? "{$trace}->{$key}" : $key;
  791. $expected = (object) $expected;
  792. $result = (object) $result;
  793. }
  794. if ($type === 'identical') {
  795. if ($value === $check) {
  796. if ($types = $compareTypes($value, $check, $trace)) {
  797. return $types;
  798. }
  799. continue;
  800. }
  801. if ($check === array()) {
  802. $trace = $newTrace;
  803. return compact('trace', 'expected', 'result');
  804. }
  805. if (is_string($check)) {
  806. $trace = $newTrace;
  807. $expected = $value;
  808. $result = $check;
  809. return compact('trace', 'expected', 'result');
  810. }
  811. } else {
  812. if ($value == $check) {
  813. if ($types = $compareTypes($value, $check, $trace)) {
  814. return $types;
  815. }
  816. continue;
  817. }
  818. if (!is_array($value)) {
  819. $trace = $newTrace;
  820. return compact('trace', 'expected', 'result');
  821. }
  822. }
  823. $compare = $this->_compare($type, $value, $check, $newTrace);
  824. if ($compare !== true) {
  825. $data[] = $compare;
  826. }
  827. }
  828. if (!empty($data)) {
  829. return $data;
  830. }
  831. }
  832. if (!is_scalar($result)) {
  833. $data = $this->_compare($type, $result, $expected);
  834. if (!empty($data)) {
  835. return array(
  836. 'trace' => $data['trace'],
  837. 'expected' => $data['result'],
  838. 'result' => $data['expected']
  839. );
  840. }
  841. }
  842. if ((($type === 'identical') ? $expected === $result : $expected == $result)) {
  843. if ($types = $compareTypes($expected, $result, $trace)) {
  844. return $types;
  845. }
  846. return true;
  847. }
  848. return compact('trace', 'expected', 'result');
  849. }
  850. /**
  851. * Returns a basic message for the data returned from `_result()`.
  852. *
  853. * @see lithium\test\Unit::assert()
  854. * @see lithium\test\Unit::_result()
  855. * @param array $data The data to use for creating the message.
  856. * @param string $message The string prepended to the generate message in the current scope.
  857. * @return string
  858. */
  859. protected function _message(&$data = array(), $message = null) {
  860. if (!empty($data[0])) {
  861. foreach ($data as $key => $value) {
  862. $message = (!empty($data[$key][0])) ? $message : null;
  863. $message .= $this->_message($value, $message);
  864. unset($data[$key]);
  865. }
  866. return $message;
  867. }
  868. $defaults = array('trace' => null, 'expected' => null, 'result' => null);
  869. $result = (array) $data + $defaults;
  870. $message = null;
  871. if (!empty($result['trace'])) {
  872. $message = sprintf("trace: %s\n", $result['trace']);
  873. }
  874. if (is_object($result['expected'])) {
  875. $result['expected'] = get_object_vars($result['expected']);
  876. }
  877. if (is_object($result['result'])) {
  878. $result['result'] = get_object_vars($result['result']);
  879. }
  880. return $message . sprintf("expected: %s\nresult: %s\n",
  881. var_export($result['expected'], true),
  882. var_export($result['result'], true)
  883. );
  884. }
  885. /**
  886. * Generates all permutation of an array $items and returns them in a new array.
  887. *
  888. * @param array $items An array of items
  889. * @param array $perms
  890. * @return array
  891. */
  892. protected function _arrayPermute($items, $perms = array()) {
  893. static $permuted;
  894. if (empty($perms)) {
  895. $permuted = array();
  896. }
  897. if (empty($items)) {
  898. $permuted[] = $perms;
  899. return;
  900. }
  901. $numItems = count($items) - 1;
  902. for ($i = $numItems; $i >= 0; --$i) {
  903. $newItems = $items;
  904. $newPerms = $perms;
  905. list($tmp) = array_splice($newItems, $i, 1);
  906. array_unshift($newPerms, $tmp);
  907. $this->_arrayPermute($newItems, $newPerms);
  908. }
  909. return $permuted;
  910. }
  911. /**
  912. * Removes everything from `resources/tmp/tests` directory.
  913. * Call from inside of your test method or `tearDown()`.
  914. *
  915. * @param string $path path to directory of contents to remove
  916. * if first character is NOT `/` prepend `LITHIUM_APP_PATH/resources/tmp/`
  917. * @return void
  918. */
  919. protected function _cleanUp($path = null) {
  920. $resources = Libraries::get(true, 'resources');
  921. $path = $path ?: $resources . '/tmp/tests';
  922. $path = preg_match('/^\w:|^\//', $path) ? $path : $resources . '/tmp/' . $path;
  923. if (!is_dir($path)) {
  924. return;
  925. }
  926. $dirs = new RecursiveDirectoryIterator($path);
  927. $iterator = new RecursiveIteratorIterator($dirs, RecursiveIteratorIterator::CHILD_FIRST);
  928. foreach ($iterator as $item) {
  929. if ($item->getPathname() === "{$path}/empty" || $iterator->isDot()) {
  930. continue;
  931. }
  932. ($item->isDir()) ? rmdir($item->getPathname()) : unlink($item->getPathname());
  933. }
  934. }
  935. /**
  936. * Returns the current results
  937. *
  938. * @return array The Results, currently
  939. */
  940. public function results() {
  941. return $this->_results;
  942. }
  943. /**
  944. * Checks for a working internet connection.
  945. *
  946. * This method is used to check for a working connection to google.com, both
  947. * testing for proper dns resolution and reading the actual URL.
  948. *
  949. * @param array $config Override the default URI to check.
  950. * @return boolean True if a network connection is established, false otherwise.
  951. */
  952. protected function _hasNetwork($config = array()) {
  953. $defaults = array(
  954. 'scheme' => 'http',
  955. 'host' => 'google.com'
  956. );
  957. $config += $defaults;
  958. $url = "{$config['scheme']}://{$config['host']}";
  959. $failed = false;
  960. set_error_handler(function($errno, $errstr) use (&$failed) {
  961. $failed = true;
  962. });
  963. $dnsCheck = dns_check_record($config['host'], "ANY");
  964. $fileCheck = fopen($url, "r");
  965. restore_error_handler();
  966. return !$failed;
  967. }
  968. }
  969. ?>