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

/vendor/nette/tester/src/Framework/Dumper.php

https://gitlab.com/kubinos/writeoff
PHP | 365 lines | 273 code | 54 blank | 38 comment | 61 complexity | 291382b37d9ac0077006d8432afe0238 MD5 | raw file
  1. <?php
  2. /**
  3. * This file is part of the Nette Tester.
  4. * Copyright (c) 2009 David Grudl (https://davidgrudl.com)
  5. */
  6. namespace Tester;
  7. /**
  8. * Dumps PHP variables.
  9. */
  10. class Dumper
  11. {
  12. public static $maxLength = 70;
  13. public static $maxDepth = 10;
  14. public static $dumpDir = 'output';
  15. public static $maxPathSegments = 3;
  16. /**
  17. * Dumps information about a variable in readable format.
  18. * @param mixed variable to dump
  19. * @return string
  20. */
  21. public static function toLine($var)
  22. {
  23. static $table;
  24. if ($table === NULL) {
  25. foreach (array_merge(range("\x00", "\x1F"), range("\x7F", "\xFF")) as $ch) {
  26. $table[$ch] = '\x' . str_pad(dechex(ord($ch)), 2, '0', STR_PAD_LEFT);
  27. }
  28. $table['\\'] = '\\\\';
  29. $table["\r"] = '\r';
  30. $table["\n"] = '\n';
  31. $table["\t"] = '\t';
  32. }
  33. if (is_bool($var)) {
  34. return $var ? 'TRUE' : 'FALSE';
  35. } elseif ($var === NULL) {
  36. return 'NULL';
  37. } elseif (is_int($var)) {
  38. return "$var";
  39. } elseif (is_float($var)) {
  40. if (!is_finite($var)) {
  41. return var_export($var, TRUE);
  42. }
  43. $var = str_replace(',', '.', "$var");
  44. return strpos($var, '.') === FALSE ? $var . '.0' : $var;
  45. } elseif (is_string($var)) {
  46. if (preg_match('#^(.{' . self::$maxLength . '}).#su', $var, $m)) {
  47. $var = "$m[1]...";
  48. } elseif (strlen($var) > self::$maxLength) {
  49. $var = substr($var, 0, self::$maxLength) . '...';
  50. }
  51. return (preg_match('#[^\x09\x0A\x0D\x20-\x7E\xA0-\x{10FFFF}]#u', $var) || preg_last_error() ? '"' . strtr($var, $table) . '"' : "'$var'");
  52. } elseif (is_array($var)) {
  53. $out = '';
  54. $counter = 0;
  55. foreach ($var as $k => & $v) {
  56. $out .= ($out === '' ? '' : ', ');
  57. if (strlen($out) > self::$maxLength) {
  58. $out .= '...';
  59. break;
  60. }
  61. $out .= ($k === $counter ? '' : self::toLine($k) . ' => ')
  62. . (is_array($v) ? 'array(...)' : self::toLine($v));
  63. $counter = is_int($k) ? max($k + 1, $counter) : $counter;
  64. }
  65. return "array($out)";
  66. } elseif ($var instanceof \Exception || $var instanceof \Throwable) {
  67. return 'Exception ' . get_class($var) . ': ' . ($var->getCode() ? '#' . $var->getCode() . ' ' : '') . $var->getMessage();
  68. } elseif (is_object($var)) {
  69. return self::objectToLine($var);
  70. } elseif (is_resource($var)) {
  71. return 'resource(' . get_resource_type($var) . ')';
  72. } else {
  73. return 'unknown type';
  74. }
  75. }
  76. /**
  77. * Formats object to line.
  78. * @param object
  79. * @return string
  80. */
  81. private static function objectToLine($object)
  82. {
  83. $line = get_class($object);
  84. if ($object instanceof \DateTime || $object instanceof \DateTimeInterface) {
  85. $line .= '(' . $object->format('Y-m-d H:i:s O') . ')';
  86. }
  87. return $line . '(#' . substr(md5(spl_object_hash($object)), 0, 4) . ')';
  88. }
  89. /**
  90. * Dumps variable in PHP format.
  91. * @param mixed variable to dump
  92. * @return string
  93. */
  94. public static function toPhp($var)
  95. {
  96. return self::_toPhp($var);
  97. }
  98. /**
  99. * @return string
  100. */
  101. private static function _toPhp(&$var, & $list = array(), $level = 0, & $line = 1)
  102. {
  103. if (is_float($var)) {
  104. $var = str_replace(',', '.', "$var");
  105. return strpos($var, '.') === FALSE ? $var . '.0' : $var;
  106. } elseif (is_bool($var)) {
  107. return $var ? 'TRUE' : 'FALSE';
  108. } elseif (is_string($var) && (preg_match('#[^\x09\x20-\x7E\xA0-\x{10FFFF}]#u', $var) || preg_last_error())) {
  109. static $table;
  110. if ($table === NULL) {
  111. foreach (array_merge(range("\x00", "\x1F"), range("\x7F", "\xFF")) as $ch) {
  112. $table[$ch] = '\x' . str_pad(dechex(ord($ch)), 2, '0', STR_PAD_LEFT);
  113. }
  114. $table['\\'] = '\\\\';
  115. $table["\r"] = '\r';
  116. $table["\n"] = '\n';
  117. $table["\t"] = '\t';
  118. $table['$'] = '\$';
  119. $table['"'] = '\"';
  120. }
  121. return '"' . strtr($var, $table) . '"';
  122. } elseif (is_array($var)) {
  123. $space = str_repeat("\t", $level);
  124. static $marker;
  125. if ($marker === NULL) {
  126. $marker = uniqid("\x00", TRUE);
  127. }
  128. if (empty($var)) {
  129. $out = '';
  130. } elseif ($level > self::$maxDepth || isset($var[$marker])) {
  131. return '/* Nesting level too deep or recursive dependency */';
  132. } else {
  133. $out = "\n$space";
  134. $outShort = '';
  135. $var[$marker] = TRUE;
  136. $oldLine = $line;
  137. $line++;
  138. $counter = 0;
  139. foreach ($var as $k => &$v) {
  140. if ($k !== $marker) {
  141. $item = ($k === $counter ? '' : self::_toPhp($k, $list, $level + 1, $line) . ' => ') . self::_toPhp($v, $list, $level + 1, $line);
  142. $counter = is_int($k) ? max($k + 1, $counter) : $counter;
  143. $outShort .= ($outShort === '' ? '' : ', ') . $item;
  144. $out .= "\t$item,\n$space";
  145. $line++;
  146. }
  147. }
  148. unset($var[$marker]);
  149. if (strpos($outShort, "\n") === FALSE && strlen($outShort) < self::$maxLength) {
  150. $line = $oldLine;
  151. $out = $outShort;
  152. }
  153. }
  154. return 'array(' . $out . ')';
  155. } elseif ($var instanceof \Closure) {
  156. $rc = new \ReflectionFunction($var);
  157. return "/* Closure defined in file {$rc->getFileName()} on line {$rc->getStartLine()} */";
  158. } elseif (is_object($var)) {
  159. if (PHP_VERSION_ID >= 70000 && ($rc = new \ReflectionObject($var)) && $rc->isAnonymous()) {
  160. return "/* Anonymous class defined in file {$rc->getFileName()} on line {$rc->getStartLine()} */";
  161. }
  162. $arr = (array) $var;
  163. $space = str_repeat("\t", $level);
  164. $class = get_class($var);
  165. $used = & $list[spl_object_hash($var)];
  166. if (empty($arr)) {
  167. $out = '';
  168. } elseif ($used) {
  169. return "/* $class dumped on line $used */";
  170. } elseif ($level > self::$maxDepth) {
  171. return '/* Nesting level too deep */';
  172. } else {
  173. $out = "\n";
  174. $used = $line;
  175. $line++;
  176. foreach ($arr as $k => &$v) {
  177. if ($k[0] === "\x00") {
  178. $k = substr($k, strrpos($k, "\x00") + 1);
  179. }
  180. $out .= "$space\t" . self::_toPhp($k, $list, $level + 1, $line) . ' => ' . self::_toPhp($v, $list, $level + 1, $line) . ",\n";
  181. $line++;
  182. }
  183. $out .= $space;
  184. }
  185. return $class === 'stdClass'
  186. ? "(object) array($out)"
  187. : "$class::__set_state(array($out))";
  188. } elseif (is_resource($var)) {
  189. return '/* resource ' . get_resource_type($var) . ' */';
  190. } else {
  191. $res = var_export($var, TRUE);
  192. $line += substr_count($res, "\n");
  193. return $res;
  194. }
  195. }
  196. /**
  197. * @param \Exception|\Throwable
  198. * @internal
  199. */
  200. public static function dumpException($e)
  201. {
  202. $trace = $e->getTrace();
  203. array_splice($trace, 0, $e instanceof \ErrorException ? 1 : 0, array(array('file' => $e->getFile(), 'line' => $e->getLine())));
  204. $testFile = NULL;
  205. foreach (array_reverse($trace) as $item) {
  206. if (isset($item['file'])) { // in case of shutdown handler, we want to skip inner-code blocks and debugging calls
  207. $testFile = $item['file'];
  208. break;
  209. }
  210. }
  211. if ($e instanceof AssertException) {
  212. $expected = $e->expected;
  213. $actual = $e->actual;
  214. if (is_object($expected) || is_array($expected) || (is_string($expected) && strlen($expected) > self::$maxLength)
  215. || is_object($actual) || is_array($actual) || (is_string($actual) && strlen($actual) > self::$maxLength)
  216. ) {
  217. $args = isset($_SERVER['argv'][1])
  218. ? '.[' . implode(' ', preg_replace(array('#^-*(.{1,20}).*#i', '#[^=a-z0-9. -]+#i'), array('$1', '-'), array_slice($_SERVER['argv'], 1))) . ']'
  219. : '';
  220. $stored[] = self::saveOutput($testFile, $expected, $args . '.expected');
  221. $stored[] = self::saveOutput($testFile, $actual, $args . '.actual');
  222. }
  223. if ((is_string($actual) && is_string($expected))) {
  224. for ($i = 0; $i < strlen($actual) && isset($expected[$i]) && $actual[$i] === $expected[$i]; $i++);
  225. $i = max(0, min($i, max(strlen($actual), strlen($expected)) - self::$maxLength + 3));
  226. for (; $i && $i < count($actual) && $actual[$i - 1] >= "\x80" && $actual[$i] >= "\x80" && $actual[$i] < "\xC0"; $i--);
  227. if ($i) {
  228. $expected = substr_replace($expected, '...', 0, $i);
  229. $actual = substr_replace($actual, '...', 0, $i);
  230. }
  231. }
  232. $message = 'Failed: ' . $e->origMessage;
  233. if (((is_string($actual) && is_string($expected)) || (is_array($actual) && is_array($expected)))
  234. && preg_match('#^(.*)(%\d)(.*)(%\d.*)\z#s', $message, $m)
  235. ) {
  236. if (($delta = strlen($m[1]) - strlen($m[3])) >= 3) {
  237. $message = "$m[1]$m[2]\n" . str_repeat(' ', $delta - 3) . "...$m[3]$m[4]";
  238. } else {
  239. $message = "$m[1]$m[2]$m[3]\n" . str_repeat(' ', strlen($m[1]) - 4) . "... $m[4]";
  240. }
  241. }
  242. $message = strtr($message, array(
  243. '%1' => self::color('yellow') . self::toLine($actual) . self::color('white'),
  244. '%2' => self::color('yellow') . self::toLine($expected) . self::color('white'),
  245. ));
  246. } else {
  247. $message = ($e instanceof \ErrorException ? Helpers::errorTypeToString($e->getSeverity()) : get_class($e))
  248. . ': ' . preg_replace('#[\x00-\x09\x0B-\x1F]+#', ' ', $e->getMessage());
  249. }
  250. $s = self::color('white', $message) . "\n\n"
  251. . (isset($stored) ? 'diff ' . Helpers::escapeArg($stored[0]) . ' ' . Helpers::escapeArg($stored[1]) . "\n\n" : '');
  252. foreach ($trace as $item) {
  253. $item += array('file' => NULL, 'class' => NULL, 'type' => NULL, 'function' => NULL);
  254. if ($e instanceof AssertException && $item['file'] === __DIR__ . DIRECTORY_SEPARATOR . 'Assert.php') {
  255. continue;
  256. }
  257. $s .= 'in '
  258. . ($item['file']
  259. ? (
  260. ($item['file'] === $testFile ? self::color('white') : '')
  261. . implode(DIRECTORY_SEPARATOR, array_slice(explode(DIRECTORY_SEPARATOR, $item['file']), -self::$maxPathSegments))
  262. . "($item[line])" . self::color('gray') . ' '
  263. )
  264. : '[internal function]'
  265. )
  266. . $item['class'] . $item['type']
  267. . (isset($item['function']) ? $item['function'] . '()' : '')
  268. . self::color() . "\n";
  269. }
  270. if ($e->getPrevious()) {
  271. $s .= "\n(previous) " . static::dumpException($e->getPrevious());
  272. }
  273. return $s;
  274. }
  275. /**
  276. * Dumps data to folder 'output'.
  277. * @return string
  278. * @internal
  279. */
  280. public static function saveOutput($testFile, $content, $suffix = '')
  281. {
  282. $path = self::$dumpDir . DIRECTORY_SEPARATOR . basename($testFile, '.phpt') . $suffix;
  283. if (!preg_match('#/|\w:#A', self::$dumpDir)) {
  284. $path = dirname($testFile) . DIRECTORY_SEPARATOR . $path;
  285. }
  286. @mkdir(dirname($path)); // @ - directory may already exist
  287. file_put_contents($path, is_string($content) ? $content : (self::toPhp($content) . "\n"));
  288. return $path;
  289. }
  290. /**
  291. * Applies color to string.
  292. * @return string
  293. */
  294. public static function color($color = NULL, $s = NULL)
  295. {
  296. static $colors = array(
  297. 'black' => '0;30', 'gray' => '1;30', 'silver' => '0;37', 'white' => '1;37',
  298. 'navy' => '0;34', 'blue' => '1;34', 'green' => '0;32', 'lime' => '1;32',
  299. 'teal' => '0;36', 'aqua' => '1;36', 'maroon' => '0;31', 'red' => '1;31',
  300. 'purple' => '0;35', 'fuchsia' => '1;35', 'olive' => '0;33', 'yellow' => '1;33',
  301. NULL => '0',
  302. );
  303. $c = explode('/', $color);
  304. return "\033[" . $colors[$c[0]] . (empty($c[1]) ? '' : ';4' . substr($colors[$c[1]], -1))
  305. . 'm' . $s . ($s === NULL ? '' : "\033[0m");
  306. }
  307. public static function removeColors($s)
  308. {
  309. return preg_replace('#\033\[[\d;]+m#', '', $s);
  310. }
  311. }