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

/test/framework/reduce/Reduce.php

http://phc.googlecode.com/
PHP | 490 lines | 254 code | 87 blank | 149 comment | 34 complexity | 9163fedb7925478b582198b31b640bbe MD5 | raw file
Possible License(s): GPL-2.0, 0BSD, BSD-3-Clause, Unlicense, MPL-2.0-no-copyleft-exception, LGPL-2.1
  1. <?php
  2. /*
  3. * phc -- the open source PHP compiler
  4. * See doc/license/README.license for licensing information
  5. *
  6. * Automatically reduce test cases using phc and delta-debugging.
  7. */
  8. /*
  9. * Introduction:
  10. *
  11. * The reduce class automatically reduces PHP test cases quickly (using the
  12. * delta-debugging algorithm). Given an input program, it will remove as many
  13. * statements as it can, checking for each statement that the bug is not
  14. * removed. It uses callbacks to check that the bug is not removed, so it is
  15. * flexible to nearly any problem.
  16. *
  17. * Usage:
  18. *
  19. *
  20. * $filename = "test_case.php";
  21. * $test_prog = file_get_contents ($filename);
  22. * try
  23. * {
  24. * $reduce = new Reduce ();
  25. * $reduce->set_checking_function ("my_checking_function");
  26. * $reduced_prog = $reduce->run_on_php ($test_prog);
  27. *
  28. * file_put_contents ("$filename.reduced", $reduced_prog);
  29. * }
  30. * catch (ReduceException e)
  31. * {
  32. * ...
  33. * }
  34. *
  35. * function my_checking_function ()
  36. * {
  37. * $bug_kept_in = (bool)(...);
  38. * return $bug_kept_in;
  39. * }
  40. *
  41. * Options:
  42. *
  43. * The basic usage is described above. But it can be tailored through a number of methods:
  44. *
  45. * - set_checking_function ($callback)
  46. * Set the function used for checking if the reduce step was
  47. * successful. Its parameters are:
  48. * $program - the PHP program to check
  49. * It returns true if the redcue step was successful (ie if the bug
  50. * was kept in), false otherwise.
  51. *
  52. * - set_run_command_function ($callback)
  53. * Set the function used for running shell commands. Its parameters are:
  54. * $command - the command to be run
  55. * $stdin - the input to it
  56. * It should return false for failure, else it should return
  57. * array ($stdout, $stderr, $exit_code)
  58. * If this function is not provided, an exception will be thrown
  59. *
  60. *
  61. * The following functions are more-or-less required, though you can survive without them.
  62. *
  63. * - set_debug_function ($callback)
  64. * Set the function used for debug messages. Its parameters are:
  65. * $level - 0-3 representing importance of message (lower is better)
  66. * $message - string message
  67. * Its return value is ignored.
  68. * If this function is not provided, print() will be used.
  69. *
  70. * - set_dump_function ($callback)
  71. * Set the function used for dumping intermediate files. Its parameters are:
  72. * $level - 0-3 representing importance of message (lower is better)
  73. * $message - string message
  74. * Its return value is ignored.
  75. * If this function is not provided, nothing will happen.
  76. *
  77. *
  78. *
  79. * These methods are optional.
  80. *
  81. * - set_comment ($comment);
  82. * Add a comment to top of the reduced program
  83. * If the comment is not called, no comment will be added.
  84. *
  85. *
  86. * phc is required for usage. To set phc's configuration, use
  87. *
  88. * - set_phc ($phc)
  89. * Set the location of the phc executable
  90. * Defaults to "phc" (ie using the PATH)
  91. *
  92. * - set_plugin_path ($path)
  93. * Set the path used by phc's plugins
  94. * Defaults to "."
  95. *
  96. *
  97. */
  98. # This attempts to reduce a test case to a minimal test case. For a
  99. # program of N statements, it attempts to reduce it by N/2 statements
  100. # chunks, then by N/4 statement chunks, and so on, while the progrmam
  101. # still fails.
  102. # Complexity:
  103. # TODO Actually, this is wrong, since each term has a larger
  104. # complexity, because of k.
  105. #
  106. # There are N - K possible chunks for chunk size K. The number of
  107. # iterations is (N-N/2) + (N-N/4) + ... + (N-1), which is log_2(N)
  108. # steps of size N (the second term converges to N, so we discard it).
  109. # So the number of iterations is O (N log N). Each iteration leads to
  110. # a traversal of each statement, giving worst case complexity of O
  111. # (N^2 log N).
  112. # Approach:
  113. #
  114. # prog = input_file
  115. # N = num_statements (prog)
  116. # for each k = N/2; k > 0; k /=2
  117. # for each i = 0; i < N -k; i++
  118. # new_prog = prog.reduce (i, k)
  119. # if (reduce_successful (new_prog))
  120. # prog = new_prog
  121. # goto start;
  122. class ReduceException extends Exception
  123. {
  124. function __construct ($message)
  125. {
  126. Exception::__construct ($message);
  127. }
  128. }
  129. class Reduce
  130. {
  131. function __construct ()
  132. {
  133. // defaults
  134. $this->set_phc ("phc");
  135. $this->set_pass ("AST-to-HIR");
  136. $this->set_plugin_path (".");
  137. }
  138. /*
  139. * Methods to set configuration
  140. */
  141. function set_comment ($comment)
  142. {
  143. $this->comment = $comment;
  144. }
  145. function set_plugin_path ($plugin_path)
  146. {
  147. $this->plugin_path = $plugin_path;
  148. }
  149. function set_run_command_function ($callback)
  150. {
  151. $this->run_command_function = $callback;
  152. }
  153. function set_debug_function ($callback)
  154. {
  155. $this->debug_function = $callback;
  156. }
  157. function set_dump_function ($callback)
  158. {
  159. $this->dump_function = $callback;
  160. }
  161. function set_pass ($passname)
  162. {
  163. $this->pass = $passname;
  164. }
  165. function set_checking_function ($callback)
  166. {
  167. $this->checking_function = $callback;
  168. }
  169. function set_phc ($phc)
  170. {
  171. $this->phc = $phc;
  172. }
  173. /*
  174. * User-overridable utility methods
  175. */
  176. function debug ($level, $message)
  177. {
  178. // Call a user-providec debug function, if provided
  179. if (isset ($this->debug_function))
  180. {
  181. call_user_func ($this->debug_function, $level, $message);
  182. }
  183. else
  184. {
  185. $this->warn_once ("Warning: no user-defined debug() function provided. Consider "
  186. ."adding one via set_debug_function ()");
  187. print "$level: $message\n";
  188. }
  189. }
  190. // Returns array ($stdout, $stderr, $exit_code)
  191. function run_command ($command, $stdin = NULL)
  192. {
  193. if (isset ($this->run_command_function))
  194. {
  195. $result = call_user_func ($this->run_command_function, $command, $stdin);
  196. // Check the result
  197. if ( is_array ($result)
  198. && count ($result) == 3
  199. && is_string ($result[0])
  200. && is_string ($result[1])
  201. && is_numeric ($result[2]))
  202. return $result;
  203. else
  204. throw new ReduceException ("Result of run_command function has the wrong structure");
  205. }
  206. else
  207. {
  208. $this->warn_once ("Warning: no user-defined run_command () function provided. Consider "
  209. ."adding one via set_run_command_function ()");
  210. throw new ReduceException ("No run_command function provided");
  211. }
  212. }
  213. function warn_once ($message)
  214. {
  215. static $cache = array ();
  216. if (isset ($cache[$message]))
  217. return;
  218. $cache[$message] = true;
  219. trigger_error (E_WARNING, $message);
  220. }
  221. function dump ($suffix, $output)
  222. {
  223. // Call a user-providec dump function, if provided
  224. if (isset ($this->dump_function))
  225. {
  226. call_user_func ($this->dump_function, $suffix, $output);
  227. }
  228. else
  229. {
  230. $this->warn_once ("Warning: no user-defined dump() function provided. Consider "
  231. ."adding one via set_dump_function ()");
  232. }
  233. }
  234. function check ($program)
  235. {
  236. if (isset ($this->checking_function))
  237. {
  238. return call_user_func ($this->checking_function, $program);
  239. }
  240. else
  241. {
  242. throw new ReduceException ("No checking function present - add one using set_checking_function ()");
  243. }
  244. }
  245. /*
  246. * Methods used as part of the algorithm
  247. */
  248. function add_comment ($xprogram)
  249. {
  250. if (!isset ($this->comment))
  251. return $xprogram;
  252. $this->debug (2, "Adding comment");
  253. $command = "{$this->phc} "
  254. ."--read-xml={$this->pass} "
  255. ."--run={$this->plugin_path}/plugins/tools/add_comment.la "
  256. ."--r-option=\"Reduced by: $this->comment\" "
  257. ."--dump-xml={$this->plugin_path}/plugins/tools/add_comment.la ";
  258. return $this->run_safe ($command, $xprogram);
  259. }
  260. function reduce_step ($xprogram, $start, $num)
  261. {
  262. $this->debug (2, "Reducing");
  263. $out = $this->run_safe (
  264. "$this->phc"
  265. ." --read-xml=$this->pass"
  266. ." --run=$this->plugin_path/plugins/tools/reduce_statements.la"
  267. ." --r-option=$start:$num"
  268. ." --dump-xml=$this->plugin_path/plugins/tools/reduce_statements.la",
  269. $xprogram);
  270. return $out;
  271. }
  272. function convert ($xprogram, $upper)
  273. {
  274. $this->debug (2, "Converting to PHP from XML");
  275. $command = "$this->phc"
  276. ." --read-xml=$this->pass"
  277. ." --dump=$this->pass";
  278. if ($upper && $this->pass == "mir")
  279. {
  280. $this->debug (2, "Uppering");
  281. $command .= " --convert-uppered";
  282. }
  283. return $this->run_safe ($command, $xprogram);
  284. }
  285. function count_statements ($xprogram)
  286. {
  287. $this->debug (2, "Counting statements");
  288. $out = $this->run_safe (
  289. "{$this->phc}"
  290. . " --read-xml={$this->plugin_path}/plugins/tutorials/count_statements_easy.la"
  291. . " --run={$this->plugin_path}/plugins/tutorials/count_statements_easy.la"
  292. , $xprogram);
  293. $this->debug (2, "Output is: $out");
  294. if (!preg_match ("/(\d+) statements found/", $out, $matched))
  295. throw ReduceException ("No statement string found");
  296. return $matched[1];
  297. }
  298. function has_syntax_errors ($program)
  299. {
  300. $this->debug (2, "Checking syntax errors");
  301. list ($out, $err, $exit) = $this->run_command ("php -l", $program);
  302. if ($exit || $err) // if the reduced case causes a PHP error, ignore.
  303. {
  304. $this->debug (1, "Syntax error detected: Skip. (out: $out, exit code: $exit, error: $err)");
  305. return true;
  306. }
  307. return false;
  308. }
  309. /* Check outputs */
  310. function run_safe ($command, $stdin = NULL)
  311. {
  312. list ($out, $err, $exit) = $this->run_command ($command, $stdin);
  313. if ($exit !== 0 || $err !== "")
  314. throw new ReduceException ("Error ($exit): $err");
  315. return $out;
  316. }
  317. # Reduce and test the program, passed as XML in $xprogram. Reduce it
  318. # starting from the $start'th statement, by $num statements. Return false if
  319. # the program couldnt reduce, or couldnt be tested, or the reduced program
  320. # otherwise.
  321. function do_main_step ($xprogram, $start, $num)
  322. {
  323. $this->num_steps++;
  324. # Reduce
  325. $this->debug (1, "Attempting to reduce by $num statements, starting at statement $start");
  326. $xnew_program = $this->reduce_step ($xprogram, $start, $num);
  327. $pnew_program = $this->convert ($xnew_program, 0); // converted to PHP
  328. $unew_program = $this->convert ($xnew_program, 1); // uppered
  329. $id = "{$num}_$start";
  330. $this->dump ("xreduced_$id", $xnew_program);
  331. $this->dump ("preduced_$id", $pnew_program);
  332. $this->dump ("ureduced_$id", $unew_program);
  333. if ($xprogram == $xnew_program && $num != 0)
  334. {
  335. // this would happen if we dont remove any statements
  336. // if $num == 0, then this is intentional
  337. $this->debug (1, "The two programs are identical. Skip.");
  338. return false;
  339. }
  340. if ($this->has_syntax_errors ($unew_program))
  341. return false;
  342. // Check if the new program is successful (has kept the bug in)
  343. if ($this->check ($unew_program))
  344. {
  345. $this->debug (2, "Success, bug kept in");
  346. $this->dump ("xsuccess_$id", $xnew_program);
  347. $this->dump ("psuccess_$id", $pnew_program);
  348. $this->dump ("usuccess_$id", $unew_program);
  349. return $xnew_program;
  350. }
  351. $this->debug (2, "Bug removed. Skip.");
  352. return false;
  353. }
  354. /*
  355. * The reduction algorithm itself
  356. */
  357. function run_on_php ($program)
  358. {
  359. $this->debug (2, "Getting initial XML input");
  360. $command = "{$this->phc} --dump-xml=$this->pass";
  361. $out = $this->run_safe ($command, $program);
  362. if (substr ($out, 0, 5) != "<?xml")
  363. throw new ReduceException ("Cannot convert input file into XML: $out");
  364. return $this->run_on_xml ($out);
  365. }
  366. function run_on_xml ($xprogram)
  367. {
  368. $this->num_steps = 0;
  369. $N = $this->count_statements ($xprogram);
  370. $original = $N;
  371. $this->debug (1, "$N statements");
  372. if ($N == 0)
  373. throw new ReduceException ("No statements found");
  374. # confirm that we can find the bug automatically
  375. if (!$this->do_main_step ($xprogram, 0, 0))
  376. throw new ReduceException ("Program does not appear to have a bug");
  377. for ($k = (int)($N/2); $k >= 1; $k = (int)($k/2))
  378. {
  379. // RESTART:
  380. for ($i = 0; $i <= ($N-$k); $i += $k)
  381. {
  382. $result = $this->do_main_step ($xprogram, $i, $k);
  383. if ($result !== false)
  384. {
  385. $xprogram = $result;
  386. $N = $this->count_statements ($xprogram);
  387. $k = $N; // the iteration will divide $N by 2
  388. $this->debug (1, "Success, program reduced to $N statements");
  389. $this->debug (1, ""); // put a blank line in the debug
  390. continue 2;
  391. }
  392. $this->debug (1, ""); // put a blank line in the debug
  393. }
  394. }
  395. // we're done, it wont reduce any further
  396. $this->debug (0, "Reduced from $original to $N statements in $this->num_steps steps.");
  397. $xprogram = $this->add_comment ($xprogram);
  398. $pprogram = $this->convert ($xprogram, 0); // converted to PHP
  399. $this->dump ("reduced", $pprogram);
  400. $this->dump ("xreduced", $xprogram);
  401. return $pprogram;
  402. }
  403. }