4 * Run the test suites in various configurations.
9 return "usage: $argv[0] [-m jit|interp] [-r] <test/directories>";
14 $ztestexample = 'test/zend/good/*/*z*.php'; // sep. for syntax highlighting
18 This is the hhvm test-suite runner. For more detailed documentation,
19 see hphp/test/README.md.
21 The test argument may be a path to a php test file, a directory name, or
22 one of a few pre-defined suite names that this script knows about.
24 If you work with hhvm a lot, you might consider a bash alias:
26 alias ht="path/to/fbcode/hphp/test/run"
30 # Quick tests in JIT mode:
33 # Slow tests in interp mode:
34 % $argv[0] -m interp test/slow
36 # Slow closure tests in JIT mode:
37 % $argv[0] test/slow/closure
39 # Slow closure tests in JIT mode with RepoAuthoritative:
40 % $argv[0] -r test/slow/closure
42 # Slow array tests, in RepoAuthoritative:
43 % $argv[0] -r test/slow/array
45 # Zend tests with a "z" in their name:
46 % $argv[0] $ztestexample
48 # Quick tests in JIT mode with some extra runtime options:
49 % $argv[0] test/quick -a '-vEval.JitMaxTranslations=120 -vEval.HHIRJumpOpts=0'
51 # All quick tests except debugger
52 % $argv[0] -e debugger test/quick
54 # All tests except those containing a string of 3 digits
55 % $argv[0] -E '/\d{3}/' all
57 # All tests whose name containing pdo_mysql
58 % $argv[0] -i pdo_mysql -m jit -r zend
64 function error($message) {
69 function hphp_home() {
70 return realpath(__DIR__
.'/../..');
73 function idx($array, $key, $default = null) {
74 return isset($array[$key]) ?
$array[$key] : $default;
77 function idx_file($array, $key, $default = null) {
78 $file = is_file(idx($array, $key)) ?
realpath($array[$key]) : $default;
79 if (!is_file($file)) {
80 error("$file doesn't exist. Did you forget to build first?");
82 return rel_path($file);
86 $dir = hphp_home() . '/' . idx($_ENV, 'FBMAKE_BIN_ROOT', '_bin');
93 function verify_hhbc() {
94 return idx($_ENV, 'VERIFY_HHBC', bin_root().'/verify.hhbc');
97 function read_file($file) {
98 return file_exists($file) ?
99 str_replace('__DIR__', dirname($file),
100 preg_replace('/\s+/', ' ', (file_get_contents($file))))
104 // http://stackoverflow.com/questions/2637945/
105 function rel_path($to) {
106 $from = explode('/', getcwd().'/');
107 $to = explode('/', $to);
110 foreach($from as $depth => $dir) {
111 // find first non-matching dir
112 if($dir === $to[$depth]) {
113 // ignore this directory
114 array_shift($relPath);
116 // get number of remaining dirs to $from
117 $remaining = count($from) - $depth;
119 // add traversals up to first matching dir
120 $padLength = (count($relPath) +
$remaining - 1) * -1;
121 $relPath = array_pad($relPath, $padLength, '..');
124 $relPath[0] = './' . $relPath[0];
128 return implode('/', $relPath);
131 function get_options($argv) {
134 'exclude-pattern:' => 'E:',
136 'include-pattern:' => 'I:',
146 'failure-file:' => '',
148 'hhas-round-trip' => '',
152 for ($i = 1; $i < count($argv); $i++
) {
155 if ($arg && $arg[0] == '-') {
156 foreach ($parameters as $long => $short) {
157 if ($arg == '-'.str_replace(':', '', $short) ||
158 $arg == '--'.str_replace(':', '', $long)) {
159 if (substr($long, -1, 1) == ':') {
160 $value = $argv[++
$i];
164 $options[str_replace(':', '', $long)] = $value;
170 if (!$found && $arg) {
175 if (isset($options['repo']) && isset($options['hhas-round-trip'])) {
176 echo "repo and hhas-round-trip are mutually exclusive options\n";
180 return array($options, $files);
184 * We support some 'special' file names, that just know where the test
185 * suites are, to avoid typing 'hphp/test/foo'.
187 function map_convenience_filename($file) {
189 'quick' => 'hphp/test/quick',
190 'slow' => 'hphp/test/slow',
191 'debugger' => 'hphp/test/server/debugger/tests',
192 'zend' => 'hphp/test/zend/good',
193 'facebook' => 'hphp/facebook/test',
195 // Subsets of zend tests.
196 'zend_ext' => 'hphp/test/zend/good/ext',
197 'zend_Zend' => 'hphp/test/zend/good/Zend',
198 'zend_tests' => 'hphp/test/zend/good/tests',
199 'zend_bad' => 'hphp/test/zend/bad',
202 if (!isset($mappage[$file])) {
205 return hphp_home().'/'.$mappage[$file];
208 function find_tests($files, array $options = null) {
210 $files = array('quick');
212 if ($files == array('all')) {
213 $files = array('quick', 'slow', 'zend');
215 foreach ($files as &$file) {
216 $file = map_convenience_filename($file);
218 error("Not valid file or directory: '$file'");
220 $file = preg_replace(',//+,', '/', realpath($file));
221 $file = preg_replace(',^'.getcwd().'/,', '', $file);
223 $files = implode(' ', $files);
224 $tests = explode("\n", shell_exec(
225 "find $files -name '*.php' -o -name '*.hhas' | grep -v round_trip.hhas"
231 $tests = array_filter($tests);
232 if (!empty($options['exclude'])) {
233 $exclude = $options['exclude'];
234 $tests = array_filter($tests, function($test) use ($exclude) {
235 return (false === strpos($test, $exclude));
238 if (!empty($options['exclude-pattern'])) {
239 $exclude = $options['exclude-pattern'];
240 $tests = array_filter($tests, function($test) use ($exclude) {
241 return !preg_match($exclude, $test);
244 if (!empty($options['include'])) {
245 $include = $options['include'];
246 $tests = array_filter($tests, function($test) use ($include) {
247 return (false !== strpos($test, $include));
250 if (!empty($options['include-pattern'])) {
251 $include = $options['include-pattern'];
252 $tests = array_filter($tests, function($test) use ($include) {
253 return preg_match($include, $test);
259 function find_test_ext($test, $ext) {
260 if (is_file("{$test}.{$ext}")) {
261 return "{$test}.{$ext}";
263 return find_file_for_dir(dirname($test), "config.{$ext}");
266 function find_file($test, $name) {
267 return find_file_for_dir(dirname($test), $name);
270 function find_file_for_dir($dir, $name) {
271 while (($dir !== '.' && $dir !== '/') && is_dir($dir)) {
272 $file = "$dir/$name";
273 if (is_file($file)) {
276 $dir = dirname($dir);
278 $file = __DIR__
.'/'.$name;
279 if (file_exists($file)) {
285 function find_debug_config($test, $name) {
286 $debug_config = find_file_for_dir(dirname($test), $name);
287 if ($debug_config !== null) {
288 return "-m debug --debug-config ".$debug_config;
293 function mode_cmd($options) {
294 $repo_args = "-vRepo.Local.Mode=-- -vRepo.Central.Path=".verify_hhbc();
295 $jit_args = "$repo_args -vEval.Jit=true";
296 $mode = idx($options, 'mode', '');
303 return "$jit_args -vEval.JitPGO=1 -vEval.JitRegionSelector=hottrace ".
304 "-vEval.JitPGOHotOnly=0";
306 return "$repo_args -vEval.Jit=0";
308 error("-m must be one of jit | pgo | interp | automain. Got: '$mode'");
312 function extra_args($options) {
313 return idx($options, 'args', '');
316 function hhvm_path() {
317 return idx_file($_ENV, 'HHVM_BIN', bin_root().'/hphp/hhvm/hhvm');
320 // Return the command and the env to run it in.
321 function hhvm_cmd($options, $test, $test_run = null) {
322 $use_automain = false;
323 if ($test_run === null) {
324 $use_automain = 'automain' === idx($options, 'mode')
325 && 'php' === pathinfo($test, PATHINFO_EXTENSION
);
326 $test_run = $use_automain
327 ? __DIR__
.'/pseudomain_wrapper.php'
330 $cmd = implode(' ', array(
333 find_test_ext($test, 'hdf'),
334 find_debug_config($test, 'hphpd.hdf'),
336 '-vEval.EnableArgsInBacktraces=true',
337 read_file(find_test_ext($test, 'opts')),
338 isset($options['arm']) ?
'-vEval.SimulateARM=1' : '',
339 extra_args($options),
340 '-vResourceLimit.CoreFileSize=0',
342 escapeshellarg($test_run),
343 $use_automain ?
escapeshellarg($test) : ''
345 if (file_exists($test.'.ini')) {
346 $cmd .= " -vServer.IniFile=$test.ini";
349 $in = find_test_ext($test, 'in');
351 $cmd .= ' < ' . escapeshellarg($in);
352 // If we're piping the input into the command then setup a simple
353 // dumb terminal so hhvm doesn't try to control it and pollute the
354 // output with control characters, which could change depending on
355 // a wide variety of terminal settings.
356 $env["TERM"] = "dumb";
358 return array($cmd, $env);
361 function hphp_cmd($options, $test) {
362 return implode(" ", array(
366 find_file($test, 'hphp_config.hdf'),
367 read_file("$test.hphp_opts"),
368 "-thhbc -l0 -k1 -o $test.repo $test",
372 function hhbbc_cmd($options, $test) {
373 return implode(" ", array(
377 '--parallel-num-threads=1',
378 read_file("$test.hhbbc_opts"),
379 "-o $test.repo/hhvm.hhbbc $test.repo/hhvm.hhbc",
384 private static $results = array();
385 private static $mode = 0;
387 const MODE_NORMAL
= 0;
388 const MODE_VERBOSE
= 1;
389 const MODE_FBMAKE
= 2;
391 public static function setMode($mode) {
395 public static function pass($test) {
396 array_push(self
::$results, array('name' => $test, 'status' => 'passed'));
397 switch (self
::$mode) {
398 case self
::MODE_NORMAL
:
399 if (self
::hasColor()) {
400 print "\033[1;32m.\033[0m";
405 case self
::MODE_VERBOSE
:
406 if (self
::hasColor()) {
407 print "$test \033[1;32mpassed\033[0m\n";
409 print "$test passed\n";
412 case self
::MODE_FBMAKE
:
413 self
::sayFBMake($test, 'passed');
418 public static function skip($test, $reason = null) {
419 switch (self
::$mode) {
420 case self
::MODE_NORMAL
:
421 if (self
::hasColor()) {
422 print "\033[1;33ms\033[0m";
427 case self
::MODE_VERBOSE
:
428 if (self
::hasColor()) {
429 if ($reason !== null) {
430 print "$test \033[1;33mskipped\033[0m ";
431 print "\033[1;31m$reason\033[0m\n";
433 print "$test \033[1;33mskipped\033[0m\n";
436 if ($reason !== null) {
437 print "$test skipped\n";
439 print "$test skipped - $reason\n";
443 case self
::MODE_FBMAKE
:
449 public static function fail($test) {
450 array_push(self
::$results, array(
452 'status' => 'failed',
453 'details' => (string)@file_get_contents
("$test.diff")
455 switch (self
::$mode) {
456 case self
::MODE_NORMAL
:
457 $diff = (string)@file_get_contents
($test.'.diff');
458 if (self
::hasColor()) {
459 print "\n\033[0;31m$test\033[0m\n$diff";
461 print "\nFAILED: $test\n$diff";
464 case self
::MODE_VERBOSE
:
465 if (self
::hasColor()) {
466 print "$test \033[0;31mFAILED\033[0m\n";
468 print "$test FAILED\n";
471 case self
::MODE_FBMAKE
:
472 self
::sayFBMake($test, 'failed');
477 private static function sayFBMake($test, $status) {
478 $start = array('op' => 'start', 'test' => $test);
479 $end = array('op' => 'test_done', 'test' => $test, 'status' => $status);
480 if ($status == 'failed') {
481 $end['details'] = (string)@file_get_contents
("$test.diff");
483 self
::say($start, $end);
486 public static function getResults() {
487 return self
::$results;
490 /** Output is in the format expected by JsonTestRunner. */
491 public static function say(/* ... */) {
492 $data = array_map(function($row) {
493 return self
::jsonEncode($row) . "\n";
495 fwrite(STDERR
, implode("", $data));
498 private static function hasColor() {
499 return posix_isatty(STDOUT
);
502 public static function jsonEncode($data) {
503 // JSON_UNESCAPED_SLASHES is Zend 5.4+
504 if (defined("JSON_UNESCAPED_SLASHES")) {
505 return json_encode($data, JSON_UNESCAPED_SLASHES
);
508 $json = json_encode($data);
509 return str_replace('\\/', '/', $json);
513 function run($options, $tests, $bad_test_file) {
514 if (isset($options['verbose'])) {
515 Status
::setMode(Status
::MODE_VERBOSE
);
517 if (isset($options['fbmake'])) {
518 Status
::setMode(Status
::MODE_FBMAKE
);
520 foreach ($tests as $test) {
521 $status = run_test($options, $test);
522 if ($status === 'skip') {
524 } else if ($status === 'skip-norepo') {
525 Status
::skip($test, 'norepo');
526 } else if ($status === 'skip-onlyrepo') {
527 Status
::skip($test, 'onlyrepo');
528 } else if ($status) {
534 file_put_contents($bad_test_file, json_encode(Status
::getResults()));
535 foreach (Status
::getResults() as $result) {
536 if ($result['status'] == 'failed') {
543 function skip_test($options, $test) {
544 $skipif_test = find_test_ext($test, 'skipif');
549 list($hhvm, $_) = hhvm_cmd($options, $test, $skipif_test);
550 $out = shell_exec($hhvm . ' 2> /dev/null');
552 return (bool)strlen($out);
555 function dump_hhas_to_temp($hhvm_cmd, $test) {
556 $tmp_file = $test . '.round_trip.hhas';
557 system("$hhvm_cmd -vEval.DumpHhas=1 > $tmp_file", $ret);
558 if ($ret) { echo "system failed\n"; exit(1); }
562 function run_one_config($options, $test, $hhvm, $hhvm_env) {
563 $descriptorspec = array(
564 0 => array("pipe", "r"),
565 1 => array("pipe", "w"),
566 2 => array("pipe", "w"),
568 if (isset($options['log'])) {
569 $hhvm_env['TRACE'] = 'printir:1';
570 $hhvm_env['HPHP_TRACE_FILE'] = $test . '.log';
573 $process = proc_open("$hhvm 2>&1", $descriptorspec, $pipes, null, $hhvm_env);
574 if (!is_resource($process)) {
575 file_put_contents("$test.diff", "Couldn't invoke $hhvm");
580 $output .= trim(stream_get_contents($pipes[1]));
581 file_put_contents("$test.out", $output);
584 // hhvm redirects errors to stdout, so anything on stderr is really bad
585 $stderr = stream_get_contents($pipes[2]);
589 "Test failed because the process wrote on stderr:\n$stderr"
594 proc_close($process);
596 // Needed for testing non-hhvm binaries that don't actually run the code
597 // e.g. parser/test/parse_tester.cpp
598 if ($output == "FORCE PASS") {
602 if (file_exists("$test.expect")) {
603 $diff_cmds = "--text -u";
604 file_put_contents("$test.expect-trimmed",
605 trim(file_get_contents("$test.expect")));
606 exec("diff --text -u $test.expect-trimmed $test.out > $test.diff 2>&1",
609 unlink("$test.expect-trimmed");
611 } else if (file_exists("$test.expectf")) {
612 $wanted_re = trim(file_get_contents("$test.expectf"));
614 // do preg_quote, but miss out any %r delimited sections
618 $length = strlen($wanted_re);
619 while($startOffset < $length) {
620 $start = strpos($wanted_re, $r, $startOffset);
621 if ($start !== false) {
622 // we have found a start tag
623 $end = strpos($wanted_re, $r, $start+
2);
624 if ($end === false) {
625 // unbalanced tag, ignore it.
626 $end = $start = $length;
629 // no more %r sections
630 $start = $end = $length;
632 // quote a non re portion of the string
633 $temp = $temp.preg_quote(substr($wanted_re, $startOffset,
634 ($start - $startOffset)), '/');
635 // add the re unquoted.
637 $temp = $temp.'('.substr($wanted_re, $start+
2, ($end - $start-2)).')';
639 $startOffset = $end +
2;
643 $wanted_re = str_replace(
644 array('%binary_string_optional%'),
648 $wanted_re = str_replace(
649 array('%unicode_string_optional%'),
653 $wanted_re = str_replace(
654 array('%unicode\|string%', '%string\|unicode%'),
658 $wanted_re = str_replace(
659 array('%u\|b%', '%b\|u%'),
664 $wanted_re = str_replace('%e', '\\' . DIRECTORY_SEPARATOR
, $wanted_re);
665 $wanted_re = str_replace('%s', '[^\r\n]+', $wanted_re);
666 $wanted_re = str_replace('%S', '[^\r\n]*', $wanted_re);
667 $wanted_re = str_replace('%a', '.+', $wanted_re);
668 $wanted_re = str_replace('%A', '.*', $wanted_re);
669 $wanted_re = str_replace('%w', '\s*', $wanted_re);
670 $wanted_re = str_replace('%i', '[+-]?\d+', $wanted_re);
671 $wanted_re = str_replace('%d', '\d+', $wanted_re);
672 $wanted_re = str_replace('%x', '[0-9a-fA-F]+', $wanted_re);
673 $wanted_re = str_replace('%f', '[+-]?\.?\d+\.?\d*(?:[Ee][+-]?\d+)?',
675 $wanted_re = str_replace('%c', '.', $wanted_re);
676 // %f allows two points "-.0.0" but that is the best *simple* expression
678 # a poor man's aide for debugging
679 shell_exec("diff --text -u $test.expectf $test.out > $test.diff 2>&1");
681 // Normalize newlines
682 $wanted_re = preg_replace("/(\r\n?|\n)/", "\n", $wanted_re);
683 $output = preg_replace("/(\r\n?|\n)/", "\n", $output);
685 return preg_match("/^$wanted_re\$/s", $output);
687 } else if (file_exists("$test.expectregex")) {
688 $wanted_re = trim(file_get_contents("$test.expectregex"));
690 # a poor man's aide for debugging
691 shell_exec("diff --text -u $test.expectregex $test.out > $test.diff 2>&1");
693 return preg_match("/^$wanted_re\$/s", $output);
697 function run_test($options, $test) {
698 if (skip_test($options, $test)) return 'skip';
700 $test_ext = pathinfo($test, PATHINFO_EXTENSION
);
701 list($hhvm, $hhvm_env) = hhvm_cmd($options, $test);
703 $hhvm = __DIR__
.'/../tools/timeout.sh -t 300 '.$hhvm;
706 if (isset($options['repo'])) {
707 if ($test_ext === 'hhas' ||
708 strpos($hhvm, '-m debug') !== false ||
709 file_exists($test.'.norepo')) {
710 return 'skip-norepo';
713 $repodb1 = "$test.repo/hhvm.hhbc";
714 $repodb2 = "$test.repo/hhvm.hhbbc";
715 if (file_exists($repodb1)) unlink($repodb1);
716 if (file_exists($repodb2)) unlink($repodb2);
717 $hphp = hphp_cmd($options, $test);
718 $hhbbc = hhbbc_cmd($options, $test);
719 $output .= shell_exec("$hphp 2>&1");
720 $output .= shell_exec("$hhbbc 2>&1");
721 $hhvm .= ' -vRepo.Authoritative=true -vRepo.Commit=0 ';
722 $hhvm1 = $hhvm."-vRepo.Central.Path=$repodb1";
723 $hhvm2 = $hhvm."-vRepo.Central.Path=$repodb2";
725 if (!($ret = run_one_config($options, $test, $hhvm1, $hhvm_env))) {
728 return run_one_config($options, $test, $hhvm2, $hhvm_env);
731 if (file_exists($test.'.onlyrepo')) {
732 return 'skip-onlyrepo';
734 if (isset($options['hhas-round-trip'])) {
735 $hhas_temp = dump_hhas_to_temp($hhvm, $test);
736 list($hhvm, $hhvm_env) = hhvm_cmd($options, $hhas_temp);
739 return run_one_config($options, $test, $hhvm, $hhvm_env);
742 function num_cpus() {
745 $data = file('/proc/stat');
747 foreach($data as $line) {
748 if (preg_match('/^cpu[0-9]/', $line)) {
755 return exec('sysctl -n hw.ncpu');
757 return 2; // default when we don't know how to detect
760 function make_header($str) {
761 return "\n\033[0;33m".$str."\033[0m\n";
764 function print_commands($tests, $options) {
765 print make_header("Run these by hand:");
767 foreach ($tests as $test) {
768 list($command, $_) = hhvm_cmd($options, $test);
769 if (!isset($options['repo'])) {
774 // How to run without hhbbc:
775 $command .= " -vRepo.Authoritative=true ";
776 $hphpc_hhvm = str_replace(verify_hhbc(), "$test.repo/hhvm.hhbc",
778 $hphpc_cmds = hphp_cmd($options, $test)."\n";
779 $hphpc_cmds .= $hphpc_hhvm."\n";
780 print "$hphpc_cmds\n\n";
782 // How to run it with hhbbc:
783 $hhbbc_hhvm = str_replace(verify_hhbc(), "$test.repo/hhvm.hhbbc",
785 $hhbbc_cmds = hphp_cmd($options, $test)."\n";
786 $hhbbc_cmds .= hhbbc_cmd($options, $test)."\n";
787 $hhbbc_cmds .= $hhbbc_hhvm."\n";
788 print "$hhbbc_cmds\n";
793 function print_success($tests, $options) {
794 print "\nAll tests passed.\n\n".<<<SHIP
799 _____|____|____|____\\\__
800 ---------\ SHIP IT
/---------
801 ^^^^^ ^^^^^^^^^^^^^^^^^^^^^
806 if (isset($options['verbose'])) {
807 print_commands($tests, $options);
811 function print_failure($argv, $results, $options) {
813 foreach ($results as $result) {
814 if ($result['status'] == 'failed') {
815 $failed[] = $result['name'];
819 print "\n".count($failed)." tests failed\n";
821 print make_header("See the diffs:").
822 implode("\n", array_map(
823 function($test) { return 'cat '.$test.'.diff'; },
826 $failing_tests_file = !empty($options['failure-file'])
827 ?
$options['failure-file']
828 : tempnam('/tmp', 'test-failures');
829 file_put_contents($failing_tests_file, implode("\n", $failed)."\n");
830 print make_header('For xargs, list of failures is available using:').
831 'cat '.$failing_tests_file."\n";
833 print_commands($failed, $options);
835 print make_header("Re-run just the failing tests:").
836 $argv[0].' '.implode(' ', $failed)."\n";
838 if (idx($options, 'mode') == 'automain') {
840 'Automain caveat: wrapper script may change semantics');
841 print 'The automain wrapper script ('.__DIR__
.'/pseudomain_wrapper.php)'
842 .' may have changed behavior'."\n"
843 .'(e.g. extra frames in backtraces) by moving code from '
844 .'pseudo-main to file top-level and adding a wrapper function.'
849 function main($argv) {
850 ini_set('pcre.backtrack_limit', PHP_INT_MAX
);
852 list($options, $files) = get_options($argv);
853 if (isset($options['help'])) {
856 $tests = find_tests($files, $options);
858 hhvm_path(); // check that binary exists
860 $threads = min(count($tests), idx($options, 'threads', num_cpus() +
1));
862 if (!isset($options['fbmake'])) {
863 print "Running ".count($tests)." tests in $threads threads\n";
866 // Try to construct the buckets so the test results are ready in
867 // approximately alphabetical order
868 $test_buckets = array();
870 foreach ($tests as $test) {
871 $test_buckets[$i][] = $test;
872 $i = ($i +
1) %
$threads;
875 // Spawn off worker threads
877 // A poor man's shared memory
878 $bad_test_files = array();
879 for ($i = 0; $i < $threads; $i++
) {
880 $bad_test_file = tempnam('/tmp', 'test-run-');
881 $bad_test_files[] = $bad_test_file;
884 error('could not fork');
888 exit(run($options, $test_buckets[$i], $bad_test_file));
894 foreach ($children as $child) {
895 pcntl_waitpid($child, $status);
896 $return_value |
= pcntl_wexitstatus($status);
900 foreach ($bad_test_files as $bad_test_file) {
901 $json = json_decode(file_get_contents($bad_test_file), true);
902 if (!is_array($json)) {
904 "No JSON output was received from a test thread. ".
905 "This might be a bug in the test script."
908 $results = array_merge($results, $json);
911 if (isset($options['fbmake'])) {
912 Status
::say(array('op' => 'all_done', 'results' => $results));
913 return $return_value;
916 if (!$return_value) {
917 print_success($tests, $options);
918 return $return_value;
921 print_failure($argv, $results, $options);
922 return $return_value;