/test/framework/reduce/Reduce.php
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
- <?php
- /*
- * phc -- the open source PHP compiler
- * See doc/license/README.license for licensing information
- *
- * Automatically reduce test cases using phc and delta-debugging.
- */
- /*
- * Introduction:
- *
- * The reduce class automatically reduces PHP test cases quickly (using the
- * delta-debugging algorithm). Given an input program, it will remove as many
- * statements as it can, checking for each statement that the bug is not
- * removed. It uses callbacks to check that the bug is not removed, so it is
- * flexible to nearly any problem.
- *
- * Usage:
- *
- *
- * $filename = "test_case.php";
- * $test_prog = file_get_contents ($filename);
- * try
- * {
- * $reduce = new Reduce ();
- * $reduce->set_checking_function ("my_checking_function");
- * $reduced_prog = $reduce->run_on_php ($test_prog);
- *
- * file_put_contents ("$filename.reduced", $reduced_prog);
- * }
- * catch (ReduceException e)
- * {
- * ...
- * }
- *
- * function my_checking_function ()
- * {
- * $bug_kept_in = (bool)(...);
- * return $bug_kept_in;
- * }
- *
- * Options:
- *
- * The basic usage is described above. But it can be tailored through a number of methods:
- *
- * - set_checking_function ($callback)
- * Set the function used for checking if the reduce step was
- * successful. Its parameters are:
- * $program - the PHP program to check
- * It returns true if the redcue step was successful (ie if the bug
- * was kept in), false otherwise.
- *
- * - set_run_command_function ($callback)
- * Set the function used for running shell commands. Its parameters are:
- * $command - the command to be run
- * $stdin - the input to it
- * It should return false for failure, else it should return
- * array ($stdout, $stderr, $exit_code)
- * If this function is not provided, an exception will be thrown
- *
- *
- * The following functions are more-or-less required, though you can survive without them.
- *
- * - set_debug_function ($callback)
- * Set the function used for debug messages. Its parameters are:
- * $level - 0-3 representing importance of message (lower is better)
- * $message - string message
- * Its return value is ignored.
- * If this function is not provided, print() will be used.
- *
- * - set_dump_function ($callback)
- * Set the function used for dumping intermediate files. Its parameters are:
- * $level - 0-3 representing importance of message (lower is better)
- * $message - string message
- * Its return value is ignored.
- * If this function is not provided, nothing will happen.
- *
- *
- *
- * These methods are optional.
- *
- * - set_comment ($comment);
- * Add a comment to top of the reduced program
- * If the comment is not called, no comment will be added.
- *
- *
- * phc is required for usage. To set phc's configuration, use
- *
- * - set_phc ($phc)
- * Set the location of the phc executable
- * Defaults to "phc" (ie using the PATH)
- *
- * - set_plugin_path ($path)
- * Set the path used by phc's plugins
- * Defaults to "."
- *
- *
- */
- # This attempts to reduce a test case to a minimal test case. For a
- # program of N statements, it attempts to reduce it by N/2 statements
- # chunks, then by N/4 statement chunks, and so on, while the progrmam
- # still fails.
- # Complexity:
- # TODO Actually, this is wrong, since each term has a larger
- # complexity, because of k.
- #
- # There are N - K possible chunks for chunk size K. The number of
- # iterations is (N-N/2) + (N-N/4) + ... + (N-1), which is log_2(N)
- # steps of size N (the second term converges to N, so we discard it).
- # So the number of iterations is O (N log N). Each iteration leads to
- # a traversal of each statement, giving worst case complexity of O
- # (N^2 log N).
- # Approach:
- #
- # prog = input_file
- # N = num_statements (prog)
- # for each k = N/2; k > 0; k /=2
- # for each i = 0; i < N -k; i++
- # new_prog = prog.reduce (i, k)
- # if (reduce_successful (new_prog))
- # prog = new_prog
- # goto start;
- class ReduceException extends Exception
- {
- function __construct ($message)
- {
- Exception::__construct ($message);
- }
- }
- class Reduce
- {
- function __construct ()
- {
- // defaults
- $this->set_phc ("phc");
- $this->set_pass ("AST-to-HIR");
- $this->set_plugin_path (".");
- }
- /*
- * Methods to set configuration
- */
- function set_comment ($comment)
- {
- $this->comment = $comment;
- }
- function set_plugin_path ($plugin_path)
- {
- $this->plugin_path = $plugin_path;
- }
- function set_run_command_function ($callback)
- {
- $this->run_command_function = $callback;
- }
- function set_debug_function ($callback)
- {
- $this->debug_function = $callback;
- }
- function set_dump_function ($callback)
- {
- $this->dump_function = $callback;
- }
- function set_pass ($passname)
- {
- $this->pass = $passname;
- }
- function set_checking_function ($callback)
- {
- $this->checking_function = $callback;
- }
- function set_phc ($phc)
- {
- $this->phc = $phc;
- }
- /*
- * User-overridable utility methods
- */
- function debug ($level, $message)
- {
- // Call a user-providec debug function, if provided
- if (isset ($this->debug_function))
- {
- call_user_func ($this->debug_function, $level, $message);
- }
- else
- {
- $this->warn_once ("Warning: no user-defined debug() function provided. Consider "
- ."adding one via set_debug_function ()");
- print "$level: $message\n";
- }
- }
- // Returns array ($stdout, $stderr, $exit_code)
- function run_command ($command, $stdin = NULL)
- {
- if (isset ($this->run_command_function))
- {
- $result = call_user_func ($this->run_command_function, $command, $stdin);
- // Check the result
- if ( is_array ($result)
- && count ($result) == 3
- && is_string ($result[0])
- && is_string ($result[1])
- && is_numeric ($result[2]))
- return $result;
- else
- throw new ReduceException ("Result of run_command function has the wrong structure");
- }
- else
- {
- $this->warn_once ("Warning: no user-defined run_command () function provided. Consider "
- ."adding one via set_run_command_function ()");
- throw new ReduceException ("No run_command function provided");
- }
- }
- function warn_once ($message)
- {
- static $cache = array ();
- if (isset ($cache[$message]))
- return;
- $cache[$message] = true;
- trigger_error (E_WARNING, $message);
- }
- function dump ($suffix, $output)
- {
- // Call a user-providec dump function, if provided
- if (isset ($this->dump_function))
- {
- call_user_func ($this->dump_function, $suffix, $output);
- }
- else
- {
- $this->warn_once ("Warning: no user-defined dump() function provided. Consider "
- ."adding one via set_dump_function ()");
- }
- }
- function check ($program)
- {
- if (isset ($this->checking_function))
- {
- return call_user_func ($this->checking_function, $program);
- }
- else
- {
- throw new ReduceException ("No checking function present - add one using set_checking_function ()");
- }
- }
- /*
- * Methods used as part of the algorithm
- */
- function add_comment ($xprogram)
- {
- if (!isset ($this->comment))
- return $xprogram;
- $this->debug (2, "Adding comment");
- $command = "{$this->phc} "
- ."--read-xml={$this->pass} "
- ."--run={$this->plugin_path}/plugins/tools/add_comment.la "
- ."--r-option=\"Reduced by: $this->comment\" "
- ."--dump-xml={$this->plugin_path}/plugins/tools/add_comment.la ";
- return $this->run_safe ($command, $xprogram);
- }
- function reduce_step ($xprogram, $start, $num)
- {
- $this->debug (2, "Reducing");
- $out = $this->run_safe (
- "$this->phc"
- ." --read-xml=$this->pass"
- ." --run=$this->plugin_path/plugins/tools/reduce_statements.la"
- ." --r-option=$start:$num"
- ." --dump-xml=$this->plugin_path/plugins/tools/reduce_statements.la",
- $xprogram);
- return $out;
- }
- function convert ($xprogram, $upper)
- {
- $this->debug (2, "Converting to PHP from XML");
- $command = "$this->phc"
- ." --read-xml=$this->pass"
- ." --dump=$this->pass";
- if ($upper && $this->pass == "mir")
- {
- $this->debug (2, "Uppering");
- $command .= " --convert-uppered";
- }
- return $this->run_safe ($command, $xprogram);
- }
- function count_statements ($xprogram)
- {
- $this->debug (2, "Counting statements");
- $out = $this->run_safe (
- "{$this->phc}"
- . " --read-xml={$this->plugin_path}/plugins/tutorials/count_statements_easy.la"
- . " --run={$this->plugin_path}/plugins/tutorials/count_statements_easy.la"
- , $xprogram);
- $this->debug (2, "Output is: $out");
- if (!preg_match ("/(\d+) statements found/", $out, $matched))
- throw ReduceException ("No statement string found");
- return $matched[1];
- }
- function has_syntax_errors ($program)
- {
- $this->debug (2, "Checking syntax errors");
- list ($out, $err, $exit) = $this->run_command ("php -l", $program);
- if ($exit || $err) // if the reduced case causes a PHP error, ignore.
- {
- $this->debug (1, "Syntax error detected: Skip. (out: $out, exit code: $exit, error: $err)");
- return true;
- }
- return false;
- }
- /* Check outputs */
- function run_safe ($command, $stdin = NULL)
- {
- list ($out, $err, $exit) = $this->run_command ($command, $stdin);
- if ($exit !== 0 || $err !== "")
- throw new ReduceException ("Error ($exit): $err");
- return $out;
- }
- # Reduce and test the program, passed as XML in $xprogram. Reduce it
- # starting from the $start'th statement, by $num statements. Return false if
- # the program couldnt reduce, or couldnt be tested, or the reduced program
- # otherwise.
- function do_main_step ($xprogram, $start, $num)
- {
- $this->num_steps++;
- # Reduce
- $this->debug (1, "Attempting to reduce by $num statements, starting at statement $start");
- $xnew_program = $this->reduce_step ($xprogram, $start, $num);
- $pnew_program = $this->convert ($xnew_program, 0); // converted to PHP
- $unew_program = $this->convert ($xnew_program, 1); // uppered
- $id = "{$num}_$start";
- $this->dump ("xreduced_$id", $xnew_program);
- $this->dump ("preduced_$id", $pnew_program);
- $this->dump ("ureduced_$id", $unew_program);
- if ($xprogram == $xnew_program && $num != 0)
- {
- // this would happen if we dont remove any statements
- // if $num == 0, then this is intentional
- $this->debug (1, "The two programs are identical. Skip.");
- return false;
- }
- if ($this->has_syntax_errors ($unew_program))
- return false;
- // Check if the new program is successful (has kept the bug in)
- if ($this->check ($unew_program))
- {
- $this->debug (2, "Success, bug kept in");
- $this->dump ("xsuccess_$id", $xnew_program);
- $this->dump ("psuccess_$id", $pnew_program);
- $this->dump ("usuccess_$id", $unew_program);
- return $xnew_program;
- }
- $this->debug (2, "Bug removed. Skip.");
- return false;
- }
- /*
- * The reduction algorithm itself
- */
- function run_on_php ($program)
- {
- $this->debug (2, "Getting initial XML input");
- $command = "{$this->phc} --dump-xml=$this->pass";
- $out = $this->run_safe ($command, $program);
- if (substr ($out, 0, 5) != "<?xml")
- throw new ReduceException ("Cannot convert input file into XML: $out");
- return $this->run_on_xml ($out);
- }
- function run_on_xml ($xprogram)
- {
- $this->num_steps = 0;
- $N = $this->count_statements ($xprogram);
- $original = $N;
- $this->debug (1, "$N statements");
- if ($N == 0)
- throw new ReduceException ("No statements found");
- # confirm that we can find the bug automatically
- if (!$this->do_main_step ($xprogram, 0, 0))
- throw new ReduceException ("Program does not appear to have a bug");
- for ($k = (int)($N/2); $k >= 1; $k = (int)($k/2))
- {
- // RESTART:
- for ($i = 0; $i <= ($N-$k); $i += $k)
- {
- $result = $this->do_main_step ($xprogram, $i, $k);
- if ($result !== false)
- {
- $xprogram = $result;
- $N = $this->count_statements ($xprogram);
- $k = $N; // the iteration will divide $N by 2
- $this->debug (1, "Success, program reduced to $N statements");
- $this->debug (1, ""); // put a blank line in the debug
- continue 2;
- }
- $this->debug (1, ""); // put a blank line in the debug
- }
- }
- // we're done, it wont reduce any further
- $this->debug (0, "Reduced from $original to $N statements in $this->num_steps steps.");
- $xprogram = $this->add_comment ($xprogram);
- $pprogram = $this->convert ($xprogram, 0); // converted to PHP
- $this->dump ("reduced", $pprogram);
- $this->dump ("xreduced", $xprogram);
- return $pprogram;
- }
- }