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

/tests/NetteTest/NetteTestCase.php

https://github.com/DocX/nette
PHP | 399 lines | 218 code | 78 blank | 103 comment | 16 complexity | 5767e8d744ec3d82383013087de6824b MD5 | raw file
Possible License(s): BSD-3-Clause
  1. <?php
  2. /**
  3. * Nette Framework
  4. *
  5. * Copyright (c) 2004, 2009 David Grudl (http://davidgrudl.com)
  6. *
  7. * This source file is subject to the "Nette license" that is bundled
  8. * with this package in the file license.txt.
  9. *
  10. * For more information please see http://nettephp.com
  11. *
  12. * @copyright Copyright (c) 2004, 2009 David Grudl
  13. * @license http://nettephp.com/license Nette license
  14. * @link http://nettephp.com
  15. * @category Nette
  16. * @package Nette\Test
  17. */
  18. /**
  19. * Single test case.
  20. *
  21. * @author David Grudl
  22. * @package Nette\Test
  23. */
  24. class NetteTestCase
  25. {
  26. /** @var string test file */
  27. private $file;
  28. /** @var array test file multiparts */
  29. private $sections;
  30. /** @var string test output */
  31. private $output;
  32. /** @var string output headers in raw format */
  33. private $headers;
  34. /** @var string PHP-CGI command line */
  35. private $cmdLine;
  36. /** @var string PHP-CGI command line */
  37. private $phpVersion;
  38. /** @var array */
  39. private static $cachedPhp;
  40. /**
  41. * @param string test file name
  42. * @param string PHP-CGI command line
  43. * @return void
  44. */
  45. public function __construct($testFile)
  46. {
  47. $this->file = (string) $testFile;
  48. $this->sections = self::parseSections($this->file);
  49. }
  50. /**
  51. * Runs single test.
  52. * @return void
  53. */
  54. public function run()
  55. {
  56. // pre-skip?
  57. $options = $this->sections['options'];
  58. if (isset($options['skip'])) {
  59. throw new NetteTestCaseException('Skipped.', NetteTestCaseException::SKIPPED);
  60. } elseif (isset($options['phpversion']) && version_compare($options['phpversion'], $this->phpVersion, '>')) {
  61. throw new NetteTestCaseException("Requires PHP version $options[phpversion].", NetteTestCaseException::SKIPPED);
  62. }
  63. $this->execute();
  64. $output = $this->output;
  65. $headers = array_change_key_case(self::parseLines($this->headers, ':'), CASE_LOWER);
  66. $tests = 0;
  67. // post-skip?
  68. if (isset($headers['x-nette-test-skip'])) {
  69. throw new NetteTestCaseException('Skipped.', NetteTestCaseException::SKIPPED);
  70. }
  71. // compare output
  72. $expectedOutput = $this->getExpectedOutput();
  73. if ($expectedOutput !== NULL) {
  74. $tests++;
  75. $binary = (bool) preg_match('#[\x00-\x08\x0B\x0C\x0E-\x1F]#', $output);
  76. if ($binary) {
  77. if ($expectedOutput !== $output) {
  78. throw new NetteTestCaseException("Binary output doesn't match.");
  79. }
  80. } else {
  81. $trim = isset($this->sections['expect']);
  82. $output = self::normalize($output, $trim);
  83. $expectedOutput = self::normalize($expectedOutput, $trim);
  84. if (!$this->compare($output, $expectedOutput)) {
  85. throw new NetteTestCaseException("Output doesn't match.");
  86. }
  87. }
  88. }
  89. // compare headers
  90. $expectedHeaders = $this->getExpectedHeaders();
  91. if ($expectedHeaders !== NULL) {
  92. $tests++;
  93. $expectedHeaders = self::normalize($expectedHeaders, TRUE);
  94. $expectedHeaders = array_change_key_case(self::parseLines($expectedHeaders, ':'), CASE_LOWER);
  95. foreach ($expectedHeaders as $name => $header) {
  96. if (!isset($headers[$name])) {
  97. throw new NetteTestCaseException("Missing header '$name'.");
  98. } elseif (!$this->compare($headers[$name], $header)) {
  99. throw new NetteTestCaseException("Header '$name' doesn't match.");
  100. }
  101. }
  102. }
  103. if (!$tests) {
  104. throw new NetteTestCaseException("Missing EXPECT and/or EXPECTHEADERS section.");
  105. }
  106. }
  107. /**
  108. * Sets PHP command line.
  109. * @param string
  110. * @param string
  111. * @return NetteTestCase provides a fluent interface
  112. */
  113. public function setPHP($executable, $args)
  114. {
  115. if (isset(self::$cachedPhp[$executable])) {
  116. $this->phpVersion = self::$cachedPhp[$executable];
  117. } else {
  118. exec(escapeshellarg($executable) . ' -v', $output, $res);
  119. if ($res !== 0 && $res !== 255) {
  120. throw new Exception("Unable to execute '$executable'.");
  121. }
  122. if (!preg_match('#^PHP (\S+).*cgi#i', $output[0], $matches)) {
  123. throw new Exception("Unable to detect PHP version (output: $output[0]).");
  124. }
  125. $this->phpVersion = self::$cachedPhp[$executable] = $matches[1];
  126. }
  127. $this->cmdLine = escapeshellarg($executable) . " $args";
  128. return $this;
  129. }
  130. /**
  131. * Execute test.
  132. * @return array
  133. */
  134. private function execute()
  135. {
  136. $this->headers = $this->output = NULL;
  137. $tempFile = tempnam('', 'tmp');
  138. if (!$tempFile) {
  139. throw new Exception("Unable to create temporary file.");
  140. }
  141. $command = $this->cmdLine;
  142. if (isset($this->sections['options']['phpini'])) {
  143. foreach (explode(';', $this->sections['options']['phpini']) as $item) {
  144. $command .= " -d " . escapeshellarg(trim($item));
  145. }
  146. }
  147. $command .= ' ' . escapeshellarg($this->file) . ' > ' . escapeshellarg($tempFile);
  148. chdir(dirname($this->file));
  149. exec($command, $foo, $res);
  150. if ($res === 255) {
  151. // exit_status 255 => parse or fatal error
  152. } elseif ($res !== 0) {
  153. throw new Exception("Unable to execute '$command'.");
  154. }
  155. $this->output = file_get_contents($tempFile);
  156. unlink($tempFile);
  157. list($this->headers, $this->output) = explode("\r\n\r\n", $this->output, 2); // CGI
  158. }
  159. /**
  160. * Returns test file section.
  161. * @return string
  162. */
  163. public function getSection($name)
  164. {
  165. return isset($this->sections[$name]) ? $this->sections[$name] : NULL;
  166. }
  167. /**
  168. * Returns test name.
  169. * @return string
  170. */
  171. public function getName()
  172. {
  173. return $this->sections['options']['name'];
  174. }
  175. /**
  176. * Returns test output.
  177. * @return string
  178. */
  179. public function getOutput()
  180. {
  181. return $this->output;
  182. }
  183. /**
  184. * Returns output headers.
  185. * @return string
  186. */
  187. public function getHeaders()
  188. {
  189. return $this->headers;
  190. }
  191. /**
  192. * Returns expected output.
  193. * @return string
  194. */
  195. public function getExpectedOutput()
  196. {
  197. if (isset($this->sections['expect'])) {
  198. return $this->sections['expect'];
  199. } elseif (is_file($expFile = str_replace('.phpt', '', $this->file) . '.expect')) {
  200. return file_get_contents($expFile);
  201. } else {
  202. return NULL;
  203. }
  204. }
  205. /**
  206. * Returns expected headers.
  207. * @return string
  208. */
  209. public function getExpectedHeaders()
  210. {
  211. return $this->getSection('expectheaders');
  212. }
  213. /********************* helpers ****************d*g**/
  214. /**
  215. * Splits file into sections.
  216. * @param string file
  217. * @return array
  218. */
  219. public static function parseSections($testFile)
  220. {
  221. $content = file_get_contents($testFile);
  222. $sections = array(
  223. 'options' => array(),
  224. );
  225. // phpDoc
  226. $phpDoc = preg_match('#^/\*\*(.*?)\*/#ms', $content, $matches) ? trim($matches[1]) : '';
  227. preg_match_all('#^\s*\*\s*@(\S+)(.*)#mi', $phpDoc, $matches, PREG_SET_ORDER);
  228. foreach ($matches as $match) {
  229. $sections['options'][$match[1]] = isset($match[2]) ? trim($match[2]) : TRUE;
  230. }
  231. $sections['options']['name'] = preg_match('#^\s*\*\s*TEST:(.*)#mi', $phpDoc, $matches) ? trim($matches[1]) : $testFile;
  232. // file parts
  233. $tmp = preg_split('#^-{3,}([^\s-]+)-{1,}(?:\r?\n|$)#m', $content, -1, PREG_SPLIT_DELIM_CAPTURE);
  234. $i = 1;
  235. while (isset($tmp[$i])) {
  236. $sections[strtolower($tmp[$i])] = $tmp[$i+1];
  237. $i += 2;
  238. }
  239. return $sections;
  240. }
  241. /**
  242. * Splits HTTP headers into array.
  243. * @param string
  244. * @param string
  245. * @return array
  246. */
  247. public static function parseLines($raw, $separator)
  248. {
  249. $headers = array();
  250. foreach (explode("\r\n", $raw) as $header) {
  251. $a = strpos($header, $separator);
  252. if ($a !== FALSE) {
  253. $headers[trim(substr($header, 0, $a))] = (string) trim(substr($header, $a + 2));
  254. }
  255. }
  256. return $headers;
  257. }
  258. /**
  259. * Compares results.
  260. * @param string
  261. * @param string
  262. * @return bool
  263. */
  264. public static function compare($left, $right)
  265. {
  266. $right = strtr($right, array(
  267. '%a%' => '[^\r\n]+', // one or more of anything except the end of line characters
  268. '%a?%'=> '[^\r\n]*', // zero or more of anything except the end of line characters
  269. '%A%' => '.+', // one or more of anything including the end of line characters
  270. '%A?%'=> '.*', // zero or more of anything including the end of line characters
  271. '%s%' => '[\t ]+', // one or more white space characters except the end of line characters
  272. '%s?%'=> '[\t ]*', // zero or more white space characters except the end of line characters
  273. '%S%' => '\S+', // one or more of characters except the white space
  274. '%S?%'=> '\S*', // zero or more of characters except the white space
  275. '%c%' => '[^\r\n]', // a single character of any sort (except the end of line)
  276. '%d%' => '[0-9]+', // one or more digits
  277. '%d?%'=> '[0-9]*', // zero or more digits
  278. '%i%' => '[+-]?[0-9]+', // signed integer value
  279. '%f%' => '[+-]?\.?\d+\.?\d*(?:[Ee][+-]?\d+)?', // floating point number
  280. '%h%' => '[0-9a-fA-F]+',// one or more HEX digits
  281. '%ns%'=> '(?:[_0-9a-zA-Z\\\\]+\\\\|N)?',// PHP namespace
  282. '%[^' => '[^', // reg-exp
  283. '%[' => '[', // reg-exp
  284. ']%' => ']+', // reg-exp
  285. '.' => '\.', '\\' => '\\\\', '+' => '\+', '*' => '\*', '?' => '\?', '[' => '\[', '^' => '\^', ']' => '\]', '$' => '\$', '(' => '\(', ')' => '\)', // preg quote
  286. '{' => '\{', '}' => '\}', '=' => '\=', '!' => '\!', '>' => '\>', '<' => '\<', '|' => '\|', ':' => '\:', '-' => '\-', "\x00" => '\000', '#' => '\#', // preg quote
  287. ));
  288. return (bool) preg_match("#^$right$#s", $left);
  289. }
  290. /**
  291. * Normalizes whitespace
  292. * @param string
  293. * @param bool
  294. * @return string
  295. */
  296. public static function normalize($s, $trim = FALSE)
  297. {
  298. $s = str_replace("\n", PHP_EOL, str_replace("\r\n", "\n", $s)); // normalize EOL
  299. if ($trim) {
  300. $s = preg_replace("#[\t ]+(\r?\n)#", '$1', $s); // multiline right trim
  301. $s = rtrim($s); // ending trim
  302. }
  303. return $s;
  304. }
  305. }
  306. /**
  307. * Single test exception.
  308. *
  309. * @author David Grudl
  310. * @package Nette\Test
  311. */
  312. class NetteTestCaseException extends Exception
  313. {
  314. const SKIPPED = 1;
  315. }