PageRenderTime 52ms CodeModel.GetById 14ms RepoModel.GetById 0ms app.codeStats 0ms

/Tester/Framework/Assert.php

https://github.com/juzna/tester
PHP | 509 lines | 313 code | 73 blank | 123 comment | 58 complexity | 2171fa4eb056759b2ac6c05c23ceec89 MD5 | raw file
Possible License(s): BSD-3-Clause
  1. <?php
  2. /**
  3. * This file is part of the Nette Tester.
  4. *
  5. * Copyright (c) 2009 David Grudl (http://davidgrudl.com)
  6. *
  7. * For the full copyright and license information, please view
  8. * the file license.txt that was distributed with this source code.
  9. *
  10. * @package Nette\Test
  11. */
  12. /**
  13. * Assertion test helpers.
  14. *
  15. * @author David Grudl
  16. * @package Nette\Test
  17. */
  18. class Assert
  19. {
  20. /**
  21. * Checks assertion. Values must be exactly the same.
  22. * @param mixed expected
  23. * @param mixed actual
  24. * @return void
  25. */
  26. public static function same($expected, $actual)
  27. {
  28. if ($actual !== $expected) {
  29. self::log($expected, $actual);
  30. throw new AssertException('Failed asserting that ' . self::dump($actual) . ' is identical to expected ' . self::dump($expected));
  31. }
  32. }
  33. /**
  34. * Checks assertion. The identity of objects and the order of keys in the arrays are ignored.
  35. * @param mixed expected
  36. * @param mixed actual
  37. * @return void
  38. */
  39. public static function equal($expected, $actual)
  40. {
  41. if (!self::compare($expected, $actual)) {
  42. self::log($expected, $actual);
  43. throw new AssertException('Failed asserting that ' . self::dump($actual) . ' is equal to expected ' . self::dump($expected));
  44. }
  45. }
  46. /**
  47. * Checks TRUE assertion.
  48. * @param mixed actual
  49. * @return void
  50. */
  51. public static function true($actual)
  52. {
  53. if ($actual !== TRUE) {
  54. throw new AssertException('Failed asserting that ' . self::dump($actual) . ' is TRUE');
  55. }
  56. }
  57. /**
  58. * Checks FALSE assertion.
  59. * @param mixed actual
  60. * @return void
  61. */
  62. public static function false($actual)
  63. {
  64. if ($actual !== FALSE) {
  65. throw new AssertException('Failed asserting that ' . self::dump($actual) . ' is FALSE');
  66. }
  67. }
  68. /**
  69. * Checks NULL assertion.
  70. * @param mixed actual
  71. * @return void
  72. */
  73. public static function null($actual)
  74. {
  75. if ($actual !== NULL) {
  76. throw new AssertException('Failed asserting that ' . self::dump($actual) . ' is NULL');
  77. }
  78. }
  79. /**
  80. * Checks exception assertion.
  81. * @param string class
  82. * @param string message
  83. * @param Exception
  84. * @return void
  85. */
  86. public static function exception($class, $message, $actual)
  87. {
  88. if (!($actual instanceof $class)) {
  89. throw new AssertException('Failed asserting that ' . get_class($actual) . " is an instance of class $class");
  90. }
  91. if ($message) {
  92. self::match($message, $actual->getMessage());
  93. }
  94. }
  95. /**
  96. * Checks if the function throws exception.
  97. * @param callable
  98. * @param string class
  99. * @param string message
  100. * @return void
  101. */
  102. public static function throws($function, $class, $message = NULL)
  103. {
  104. try {
  105. call_user_func($function);
  106. throw new AssertException('Expected exception');
  107. } catch (Exception $e) {
  108. Assert::exception($class, $message, $e);
  109. }
  110. }
  111. /**
  112. * Checks if the function throws exception.
  113. * @param callable
  114. * @param int
  115. * @param string message
  116. * @return void
  117. */
  118. public static function error($function, $level, $message = NULL)
  119. {
  120. $catched = NULL;
  121. set_error_handler(function($severity, $message, $file, $line) use (& $catched) {
  122. if (($severity & error_reporting()) === $severity) {
  123. if ($catched) {
  124. echo "\nUnexpected error $message in $file:$line";
  125. exit(TestJob::CODE_FAIL);
  126. }
  127. $catched = array($severity, $message);
  128. }
  129. });
  130. call_user_func($function);
  131. restore_error_handler();
  132. if (!$catched) {
  133. throw new AssertException('Expected error');
  134. }
  135. if ($catched[0] !== $level) {
  136. $consts = get_defined_constants(TRUE);
  137. foreach ($consts['Core'] as $name => $val) {
  138. if ($catched[0] === $val && substr($name, 0, 2) === 'E_') {
  139. $catched[0] = $name;
  140. }
  141. if ($level === $val && substr($name, 0, 2) === 'E_') {
  142. $level = $name;
  143. }
  144. }
  145. throw new AssertException('Failed asserting that ' . $catched[0] . ' is ' . $level);
  146. }
  147. if ($message) {
  148. self::match($message, $catched[1]);
  149. }
  150. }
  151. /**
  152. * Failed assertion
  153. * @return void
  154. */
  155. public static function fail($message)
  156. {
  157. throw new AssertException($message);
  158. }
  159. /**
  160. * Initializes shutdown handler.
  161. * @return void
  162. */
  163. public static function handler($handler)
  164. {
  165. ob_start();
  166. register_shutdown_function($handler);
  167. }
  168. /**
  169. * Compares two structures. Ignores the identity of objects and the order of keys in the arrays.
  170. * @return bool
  171. */
  172. public static function compare($expected, $actual)
  173. {
  174. if (is_object($expected) && is_object($actual) && get_class($expected) === get_class($actual)) {
  175. $expected = (array) $expected;
  176. $actual = (array) $actual;
  177. }
  178. if (is_array($expected) && is_array($actual)) {
  179. $arr1 = array_keys($expected);
  180. sort($arr1);
  181. $arr2 = array_keys($actual);
  182. sort($arr2);
  183. if ($arr1 !== $arr2) {
  184. return FALSE;
  185. }
  186. foreach ($expected as $key => $value) {
  187. if (!self::compare($value, $actual[$key])) {
  188. return FALSE;
  189. }
  190. }
  191. return TRUE;
  192. }
  193. return $expected === $actual;
  194. }
  195. /**
  196. * Compares results using mask:
  197. * %a% one or more of anything except the end of line characters
  198. * %a?% zero or more of anything except the end of line characters
  199. * %A% one or more of anything including the end of line characters
  200. * %A?% zero or more of anything including the end of line characters
  201. * %s% one or more white space characters except the end of line characters
  202. * %s?% zero or more white space characters except the end of line characters
  203. * %S% one or more of characters except the white space
  204. * %S?% zero or more of characters except the white space
  205. * %c% a single character of any sort (except the end of line)
  206. * %d% one or more digits
  207. * %d?% zero or more digits
  208. * %i% signed integer value
  209. * %f% floating point number
  210. * %h% one or more HEX digits
  211. * %ns% PHP namespace
  212. * %[..]% reg-exp
  213. * @param string
  214. * @param string
  215. * @return bool
  216. */
  217. public static function match($expected, $actual)
  218. {
  219. $expected = rtrim(preg_replace("#[\t ]+\n#", "\n", str_replace("\r\n", "\n", $expected)));
  220. $actual = rtrim(preg_replace("#[\t ]+\n#", "\n", str_replace("\r\n", "\n", $actual)));
  221. $re = strtr($expected, array(
  222. '%a%' => '[^\r\n]+', // one or more of anything except the end of line characters
  223. '%a?%'=> '[^\r\n]*', // zero or more of anything except the end of line characters
  224. '%A%' => '.+', // one or more of anything including the end of line characters
  225. '%A?%'=> '.*', // zero or more of anything including the end of line characters
  226. '%s%' => '[\t ]+', // one or more white space characters except the end of line characters
  227. '%s?%'=> '[\t ]*', // zero or more white space characters except the end of line characters
  228. '%S%' => '\S+', // one or more of characters except the white space
  229. '%S?%'=> '\S*', // zero or more of characters except the white space
  230. '%c%' => '[^\r\n]', // a single character of any sort (except the end of line)
  231. '%d%' => '[0-9]+', // one or more digits
  232. '%d?%'=> '[0-9]*', // zero or more digits
  233. '%i%' => '[+-]?[0-9]+', // signed integer value
  234. '%f%' => '[+-]?\.?\d+\.?\d*(?:[Ee][+-]?\d+)?', // floating point number
  235. '%h%' => '[0-9a-fA-F]+',// one or more HEX digits
  236. '%ns%'=> '(?:[_0-9a-zA-Z\\\\]+\\\\|N)?',// PHP namespace
  237. '%ds%'=> '[\\\\/]', // directory separator
  238. '%[^' => '[^', // reg-exp
  239. '%[' => '[', // reg-exp
  240. ']%' => ']+', // reg-exp
  241. '%(' => '(?:', // reg-exp
  242. ')%' => ')', // reg-exp
  243. ')?%' => ')?', // reg-exp
  244. '.' => '\.', '\\' => '\\\\', '+' => '\+', '*' => '\*', '?' => '\?', '[' => '\[', '^' => '\^', // preg quote
  245. ']' => '\]', '$' => '\$', '(' => '\(', ')' => '\)', '{' => '\{', '}' => '\}', '=' => '\=', '!' => '\!',
  246. '>' => '\>', '<' => '\<', '|' => '\|', ':' => '\:', '-' => '\-', "\x00" => '\000', '#' => '\#',
  247. ));
  248. $old = ini_set('pcre.backtrack_limit', '5000000');
  249. $res = preg_match("#^$re$#s", $actual);
  250. ini_set('pcre.backtrack_limit', $old);
  251. if ($res === FALSE || preg_last_error()) {
  252. throw new Exception("Error while executing regular expression. (PREG Error Code " . preg_last_error() . ")");
  253. }
  254. if (!$res) {
  255. self::log($expected, $actual);
  256. throw new AssertException('Failed asserting that ' . self::dump($actual) . ' matches expected ' . self::dump($expected));
  257. }
  258. }
  259. /**
  260. * Dumps information about a variable in readable format.
  261. * @param mixed variable to dump
  262. * @return void
  263. */
  264. private static function dump($var)
  265. {
  266. static $tableUtf, $tableBin, $reBinary = '#[^\x09\x0A\x0D\x20-\x7E\xA0-\x{10FFFF}]#u';
  267. if ($tableUtf === NULL) {
  268. foreach (range("\x00", "\xFF") as $ch) {
  269. if (ord($ch) < 32 && strpos("\r\n\t", $ch) === FALSE) {
  270. $tableUtf[$ch] = $tableBin[$ch] = '\\x' . str_pad(dechex(ord($ch)), 2, '0', STR_PAD_LEFT);
  271. } elseif (ord($ch) < 127) {
  272. $tableUtf[$ch] = $tableBin[$ch] = $ch;
  273. } else {
  274. $tableUtf[$ch] = $ch; $tableBin[$ch] = '\\x' . dechex(ord($ch));
  275. }
  276. }
  277. $tableBin["\\"] = '\\\\';
  278. $tableBin["\r"] = '\\r';
  279. $tableBin["\n"] = '\\n';
  280. $tableBin["\t"] = '\\t';
  281. $tableUtf['\\x'] = $tableBin['\\x'] = '\\\\x';
  282. }
  283. if (is_bool($var)) {
  284. return $var ? 'TRUE' : 'FALSE';
  285. } elseif ($var === NULL) {
  286. return 'NULL';
  287. } elseif (is_int($var)) {
  288. return "$var";
  289. } elseif (is_float($var)) {
  290. $var = var_export($var, TRUE);
  291. return strpos($var, '.') === FALSE ? $var . '.0' : $var;
  292. } elseif (is_string($var)) {
  293. if ($cut = @iconv_strlen($var, 'UTF-8') > 100) {
  294. $var = iconv_substr($var, 0, 100, 'UTF-8');
  295. } elseif ($cut = strlen($var) > 100) {
  296. $var = substr($var, 0, 100);
  297. }
  298. return '"' . strtr($var, preg_match($reBinary, $var) || preg_last_error() ? $tableBin : $tableUtf) . '"' . ($cut ? ' ...' : '');
  299. } elseif (is_array($var)) {
  300. return "array(" . count($var) . ")";
  301. } elseif ($var instanceof Exception) {
  302. return 'Exception ' . get_class($var) . ': ' . ($var->getCode() ? '#' . $var->getCode() . ' ' : '') . $var->getMessage();
  303. } elseif (is_object($var)) {
  304. $arr = (array) $var;
  305. return "object(" . get_class($var) . ") (" . count($arr) . ")";
  306. } elseif (is_resource($var)) {
  307. return "resource(" . get_resource_type($var) . ")";
  308. } else {
  309. return "unknown type";
  310. }
  311. }
  312. /**
  313. * Dumps variable in PHP format.
  314. * @param mixed variable to dump
  315. * @return void
  316. */
  317. private static function dumpPhp(&$var, $level = 0)
  318. {
  319. if (is_float($var)) {
  320. $var = var_export($var, TRUE);
  321. return strpos($var, '.') === FALSE ? $var . '.0' : $var;
  322. } elseif (is_bool($var)) {
  323. return $var ? 'TRUE' : 'FALSE';
  324. } elseif (is_string($var) && (preg_match('#[^\x09\x20-\x7E\xA0-\x{10FFFF}]#u', $var) || preg_last_error())) {
  325. static $table;
  326. if ($table === NULL) {
  327. foreach (range("\x00", "\xFF") as $ch) {
  328. $table[$ch] = ord($ch) < 32 || ord($ch) >= 127
  329. ? '\\x' . str_pad(dechex(ord($ch)), 2, '0', STR_PAD_LEFT)
  330. : $ch;
  331. }
  332. $table["\r"] = '\r';
  333. $table["\n"] = '\n';
  334. $table["\t"] = '\t';
  335. $table['$'] = '\\$';
  336. $table['\\'] = '\\\\';
  337. $table['"'] = '\\"';
  338. }
  339. return '"' . strtr($var, $table) . '"';
  340. } elseif (is_array($var)) {
  341. $s = '';
  342. $space = str_repeat("\t", $level);
  343. static $marker;
  344. if ($marker === NULL) {
  345. $marker = uniqid("\x00", TRUE);
  346. }
  347. if (empty($var)) {
  348. } elseif ($level > 50 || isset($var[$marker])) {
  349. throw new \Exception('Nesting level too deep or recursive dependency.');
  350. } else {
  351. $s .= "\n";
  352. $var[$marker] = TRUE;
  353. $counter = 0;
  354. foreach ($var as $k => &$v) {
  355. if ($k !== $marker) {
  356. $s .= "$space\t" . ($k === $counter ? '' : self::dumpPhp($k) . " => ") . self::dumpPhp($v, $level + 1) . ",\n";
  357. $counter = is_int($k) ? max($k + 1, $counter) : $counter;
  358. }
  359. }
  360. unset($var[$marker]);
  361. $s .= $space;
  362. }
  363. return "array($s)";
  364. } elseif (is_object($var)) {
  365. $arr = (array) $var;
  366. $s = '';
  367. $space = str_repeat("\t", $level);
  368. static $list = array();
  369. if (empty($arr)) {
  370. } elseif ($level > 50 || in_array($var, $list, TRUE)) {
  371. throw new \Exception('Nesting level too deep or recursive dependency.');
  372. } else {
  373. $s .= "\n";
  374. $list[] = $var;
  375. foreach ($arr as $k => &$v) {
  376. if ($k[0] === "\x00") {
  377. $k = substr($k, strrpos($k, "\x00") + 1);
  378. }
  379. $s .= "$space\t" . self::dumpPhp($k) . " => " . self::dumpPhp($v, $level + 1) . ",\n";
  380. }
  381. array_pop($list);
  382. $s .= $space;
  383. }
  384. return get_class($var) === 'stdClass'
  385. ? "(object) array($s)"
  386. : get_class($var) . "::__set_state(array($s))";
  387. } else {
  388. return var_export($var, TRUE);
  389. }
  390. }
  391. /**
  392. * Logs big variables to file.
  393. * @param mixed
  394. * @param mixed
  395. * @return void
  396. */
  397. private static function log($expected, $actual)
  398. {
  399. $trace = debug_backtrace();
  400. $item = end($trace);
  401. // in case of shutdown handler, we want to skip inner-code blocks
  402. // and debugging calls (e.g. those of Nette\Diagnostics\Debugger)
  403. // to get correct path to test file (which is the only purpose of this)
  404. while (!isset($item['file']) || substr($item['file'], -5) !== '.phpt') {
  405. $item = prev($trace);
  406. if ($item === FALSE) {
  407. return;
  408. }
  409. }
  410. $file = dirname($item['file']) . '/output/' . basename($item['file'], '.phpt');
  411. if (is_object($expected) || is_array($expected) || (is_string($expected) && strlen($expected) > 80)) {
  412. @mkdir(dirname($file)); // @ - directory may already exist
  413. file_put_contents($file . '.expected', is_string($expected) ? $expected : self::dumpPhp($expected));
  414. }
  415. if (is_object($actual) || is_array($actual) || (is_string($actual) && strlen($actual) > 80)) {
  416. @mkdir(dirname($file)); // @ - directory may already exist
  417. file_put_contents($file . '.actual', is_string($actual) ? $actual : self::dumpPhp($actual));
  418. }
  419. }
  420. }
  421. /**
  422. * Assertion exception.
  423. *
  424. * @author David Grudl
  425. * @package Nette\Tester
  426. */
  427. class AssertException extends \Exception
  428. {
  429. }