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'
55 function error($message) {
60 function hphp_home() {
61 return realpath(__DIR__
.'/../..');
64 function idx($array, $key, $default = null) {
65 return isset($array[$key]) ?
$array[$key] : $default;
68 function idx_file($array, $key, $default = null) {
69 $file = is_file(idx($array, $key)) ?
realpath($array[$key]) : $default;
70 if (!is_file($file)) {
71 error("$file doesn't exist");
73 return rel_path($file);
77 $dir = hphp_home() . '/' . idx($_ENV, 'FBMAKE_BIN_ROOT', '_bin');
84 function verify_hhbc() {
85 return idx($_ENV, 'VERIFY_HHBC', bin_root().'/verify.hhbc');
88 function read_file($file) {
89 return file_exists($file) ?
90 preg_replace('/\s+/', ' ', (file_get_contents($file))) : "";
93 // http://stackoverflow.com/questions/2637945/
94 function rel_path($to) {
95 $from = explode('/', getcwd().'/');
96 $to = explode('/', $to);
99 foreach($from as $depth => $dir) {
100 // find first non-matching dir
101 if($dir === $to[$depth]) {
102 // ignore this directory
103 array_shift($relPath);
105 // get number of remaining dirs to $from
106 $remaining = count($from) - $depth;
108 // add traversals up to first matching dir
109 $padLength = (count($relPath) +
$remaining - 1) * -1;
110 $relPath = array_pad($relPath, $padLength, '..');
113 $relPath[0] = './' . $relPath[0];
117 return implode('/', $relPath);
120 function get_options($argv) {
133 for ($i = 1; $i < count($argv); $i++
) {
136 if ($arg && $arg[0] == '-') {
137 foreach ($parameters as $long => $short) {
138 if ($arg == '-'.str_replace(':', '', $short) ||
139 $arg == '--'.str_replace(':', '', $long)) {
140 if (substr($long, -1, 1) == ':') {
141 $value = $argv[++
$i];
145 $options[str_replace(':', '', $long)] = $value;
151 if (!$found && $arg) {
155 return array($options, $files);
159 * We support some 'special' file names, that just know where the test
160 * suites are, to avoid typing 'hphp/test/foo'.
162 function map_convenience_filename($file) {
164 'quick' => 'hphp/test/quick',
165 'slow' => 'hphp/test/slow',
166 'zend' => 'hphp/test/zend/good',
167 'zend_bad' => 'hphp/test/zend/bad',
168 'facebook' => 'hphp/facebook/test',
172 if (!preg_match('/([^\/]*)/', $file, $m) ||
173 !isset($mappage[$m[1]])) {
176 return hphp_home().'/'.$mappage[$m[1]];
179 function find_tests($files) {
181 $files = array('quick');
183 if ($files == array('all')) {
184 $files = array('quick', 'slow', 'zend');
186 foreach ($files as &$file) {
187 $file = map_convenience_filename($file);
189 error("Not valid file or directory: '$file'");
191 $file = preg_replace(',//+,', '/', realpath($file));
192 $file = preg_replace(',^'.getcwd().'/,', '', $file);
194 $files = implode(' ', $files);
195 $tests = explode("\n", shell_exec(
196 "find $files -name '*.php' -o -name '*.hhas'"
202 return array_filter($tests);
205 function find_config($test, $name) {
206 return find_config_for_dir(dirname($test), $name);
209 function find_config_for_dir($dir, $name) {
210 while ($dir && stat($dir)) {
211 $config = "$dir/$name";
212 if (is_file($config)) {
215 $dir = substr($dir, 0, strrpos($dir, '/'));
217 return __DIR__
.'/'.$name;
220 function find_debug_config($test, $name) {
221 $debug_config = find_config_for_dir(dirname($test), $name);
222 if (is_file($debug_config)) {
223 return "-m debug --debug-config ".$debug_config;
228 function mode_cmd($options) {
229 $repo_args = "-vRepo.Local.Mode=-- -vRepo.Central.Path=".verify_hhbc();
230 $jit_args = "$repo_args -vEval.Jit=true ".
231 "-vEval.JitEnableRenameFunction=true";
232 $mode = idx($options, 'mode');
238 return "$repo_args -vEval.Jit=0";
240 error("-m must be one of jit | interp. Got: '$mode'");
244 function extra_args($options) {
245 return idx($options, 'args', '');
248 function hhvm_cmd($options, $test) {
249 $cmd = implode(" ", array(
250 idx_file($_ENV, 'HHVM_BIN', bin_root().'/hphp/hhvm/hhvm'),
252 find_config($test, 'config.hdf'),
253 find_debug_config($test, 'hphpd.hdf'),
255 '-vEval.EnableArgsInBacktraces=true',
256 read_file("$test.opts"),
257 extra_args($options),
261 if (file_exists("$test.in")) {
262 $cmd .= " <$test.in";
267 function hphp_cmd($options, $test) {
268 return implode(" ", array(
269 idx_file($_ENV, 'HPHP_BIN', bin_root().'/hphp/hhvm/hphp'),
271 find_config($test, 'hphp_config.hdf'),
272 read_file("$test.hphp_opts"),
273 "-thhbc -l0 -k1 -o $test.repo $test",
278 private static $results = array();
279 private static $mode = 0;
281 const MODE_NORMAL
= 0;
282 const MODE_VERBOSE
= 1;
283 const MODE_FBMAKE
= 2;
285 public static function setMode($mode) {
289 public static function pass($test) {
290 array_push(self
::$results, array('name' => $test, 'status' => 'passed'));
291 switch (self
::$mode) {
292 case self
::MODE_NORMAL
:
293 if (self
::hasColor()) {
294 print "\033[1;32m.\033[0m";
299 case self
::MODE_VERBOSE
:
300 if (self
::hasColor()) {
301 print "$test \033[1;32mpassed\033[0m\n";
303 print "$test passed";
306 case self
::MODE_FBMAKE
:
307 self
::sayFBMake($test, 'passed');
312 public static function fail($test) {
313 array_push(self
::$results, array(
315 'status' => 'failed',
316 'details' => @file_get_contents
("$test.diff")
318 switch (self
::$mode) {
319 case self
::MODE_NORMAL
:
320 $diff = @file_get_contents
($test.'.diff');
321 if (self
::hasColor()) {
322 print "\n\033[0;31m$test\033[0m\n$diff";
324 print "\nFAILED: $test\n$diff";
327 case self
::MODE_VERBOSE
:
328 if (self
::hasColor()) {
329 print "$test \033[0;31mFAILED\033[0m\n";
331 print "$test FAILED\n";
334 case self
::MODE_FBMAKE
:
335 self
::sayFBMake($test, 'failed');
340 private static function sayFBMake($test, $status) {
341 $start = array('op' => 'start', 'test' => $test);
342 $end = array('op' => 'test_done', 'test' => $test, 'status' => $status);
343 if ($status == 'failed') {
344 $end['details'] = @file_get_contents
("$test.diff");
346 self
::say($start, $end);
349 public static function getResults() {
350 return self
::$results;
353 /** Output is in the format expected by JsonTestRunner. */
354 public static function say(/* ... */) {
355 $data = array_map(function($row) {
356 return json_encode($row, JSON_UNESCAPED_SLASHES
) . "\n";
358 fwrite(STDERR
, implode("", $data));
361 private static function hasColor() {
362 return posix_isatty(STDOUT
);
366 function run($options, $tests, $bad_test_file) {
367 if (isset($options['verbose'])) {
368 Status
::setMode(Status
::MODE_VERBOSE
);
370 if (isset($options['fbmake'])) {
371 Status
::setMode(Status
::MODE_FBMAKE
);
373 foreach ($tests as $test) {
374 $status = run_test($options, $test);
381 file_put_contents($bad_test_file, json_encode(Status
::getResults()));
382 foreach (Status
::getResults() as $result) {
383 if ($result['status'] == 'failed') {
390 function run_test($options, $test) {
391 $hhvm = hhvm_cmd($options, $test);
393 if (isset($options['repo'])) {
394 if (strpos($test, '.hhas') !== false ||
395 strpos($hhvm, '-m debug') != false ||
is_file($test.'.norepo')) {
396 # We don't have a way to skip, I guess run non-repo?
398 unlink("$test.repo/hhvm.hhbc");
399 $hphp = hphp_cmd($options, $test);
400 $output .= shell_exec("$hphp 2>&1");
401 $hhvm .= " -vRepo.Authoritative=true ".
402 "-vRepo.Central.Path=$test.repo/hhvm.hhbc";
406 $descriptorspec = array(
407 0 => array("pipe", "r"),
408 1 => array("pipe", "w"),
409 2 => array("pipe", "w"),
411 $process = proc_open("$hhvm 2>&1", $descriptorspec, $pipes);
412 if (!is_resource($process)) {
413 file_put_contents("$test.diff", "Couldn't invoke $hhvm");
418 $output .= stream_get_contents($pipes[1]);
419 file_put_contents("$test.out", $output);
422 // hhvm redirects errors to stdout, so anything on stderr is really bad
423 $stderr = stream_get_contents($pipes[2]);
427 "Test failed because the process wrote on stderr:\n$stderr"
432 proc_close($process);
434 // Needed for testing non-hhvm binaries that don't actually run the code
435 // e.g. util/parser/test/parse_tester.cpp
436 if ($output == "FORCE PASS") {
440 if (file_exists("$test.expect")) {
441 $diff_cmds = "--text -u";
442 exec("diff --text -u $test.expect $test.out > $test.diff 2>&1",
447 } else if (file_exists("$test.expectf")) {
448 $wanted_re = file_get_contents("$test.expectf");
450 // do preg_quote, but miss out any %r delimited sections
454 $length = strlen($wanted_re);
455 while($startOffset < $length) {
456 $start = strpos($wanted_re, $r, $startOffset);
457 if ($start !== false) {
458 // we have found a start tag
459 $end = strpos($wanted_re, $r, $start+
2);
460 if ($end === false) {
461 // unbalanced tag, ignore it.
462 $end = $start = $length;
465 // no more %r sections
466 $start = $end = $length;
468 // quote a non re portion of the string
469 $temp = $temp.preg_quote(substr($wanted_re, $startOffset,
470 ($start - $startOffset)), '/');
471 // add the re unquoted.
473 $temp = $temp.'('.substr($wanted_re, $start+
2, ($end - $start-2)).')';
475 $startOffset = $end +
2;
479 $wanted_re = str_replace(
480 array('%binary_string_optional%'),
484 $wanted_re = str_replace(
485 array('%unicode_string_optional%'),
489 $wanted_re = str_replace(
490 array('%unicode\|string%', '%string\|unicode%'),
494 $wanted_re = str_replace(
495 array('%u\|b%', '%b\|u%'),
500 $wanted_re = str_replace('%e', '\\' . DIRECTORY_SEPARATOR
, $wanted_re);
501 $wanted_re = str_replace('%s', '[^\r\n]+', $wanted_re);
502 $wanted_re = str_replace('%S', '[^\r\n]*', $wanted_re);
503 $wanted_re = str_replace('%a', '.+', $wanted_re);
504 $wanted_re = str_replace('%A', '.*', $wanted_re);
505 $wanted_re = str_replace('%w', '\s*', $wanted_re);
506 $wanted_re = str_replace('%i', '[+-]?\d+', $wanted_re);
507 $wanted_re = str_replace('%d', '\d+', $wanted_re);
508 $wanted_re = str_replace('%x', '[0-9a-fA-F]+', $wanted_re);
509 $wanted_re = str_replace('%f', '[+-]?\.?\d+\.?\d*(?:[Ee][+-]?\d+)?',
511 $wanted_re = str_replace('%c', '.', $wanted_re);
512 // %f allows two points "-.0.0" but that is the best *simple* expression
514 # a poor man's aide for debugging
515 shell_exec("diff --text -u $test.expectf $test.out > $test.diff 2>&1");
517 return preg_match("/^$wanted_re\$/s", $output);
519 } else if (file_exists("$test.expectregex")) {
520 $wanted_re = file_get_contents("$test.expectregex");
522 # a poor man's aide for debugging
523 shell_exec("diff --text -u $test.expectregex $test.out > $test.diff 2>&1");
525 return preg_match("/^$wanted_re\$/s", $output);
529 function num_cpus() {
530 $data = file('/proc/stat');
532 foreach($data as $line) {
533 if (preg_match('/^cpu[0-9]/', $line)) {
540 function main($argv) {
542 ini_set('pcre.backtrack_limit', PHP_INT_MAX
);
544 list($options, $files) = get_options($argv);
545 if (isset($options['help'])) {
548 $tests = find_tests($files);
550 $threads = min(count($tests), idx($options, 'threads', num_cpus() +
1));
552 if (!isset($options['fbmake'])) {
553 print "Running ".count($tests)." tests in $threads threads\n";
556 # Try to construct the buckets so the test results are ready in approximately
558 $test_buckets = array();
560 foreach ($tests as $test) {
561 $test_buckets[$i][] = $test;
562 $i = ($i +
1) %
$threads;
565 # Spawn off worker threads
567 # A poor man's shared memory
568 $bad_test_files = array();
569 for ($i = 0; $i < $threads; $i++
) {
570 $bad_test_file = tempnam('/tmp', 'test-run-');
571 $bad_test_files[] = $bad_test_file;
574 error('could not fork');
578 exit(run($options, $test_buckets[$i], $bad_test_file));
584 foreach ($children as $child) {
585 pcntl_waitpid($child, $status);
586 $return_value |
= pcntl_wexitstatus($status);
590 foreach ($bad_test_files as $bad_test_file) {
591 $json = json_decode(file_get_contents($bad_test_file), true);
592 if (!is_array($json)) {
594 "A test thread didn't send json to the controller. WTF. ".
595 "If your code isn't horribly broken, please complain loudly"
598 $results = array_merge($results, $json);
600 if (isset($options['fbmake'])) {
601 Status
::say(array('op' => 'all_done', 'results' => $results));
603 if (!$return_value) {
604 print "\nAll tests passed.\n\n".<<<SHIP
609 _____|____|____|____\\\__
610 ---------\ SHIP IT
/---------
611 ^^^^^ ^^^^^^^^^^^^^^^^^^^^^
618 foreach ($results as $result) {
619 if ($result['status'] == 'failed') {
620 $failed[] = $result['name'];
624 $header_start = "\n\033[0;33m";
625 $header_end = "\033[0m\n";
626 print "\n".count($failed)." tests failed\n";
628 print $header_start."See the diffs:".$header_end.
629 implode("\n", array_map(
630 function($test) { return 'cat '.$test.'.diff'; },
633 print $header_start."Run these by hand:".$header_end;
635 foreach ($failed as $test) {
636 $command = hhvm_cmd($options, $test);
637 if (isset($options['repo'])) {
638 $command .= " -vRepo.Authoritative=true ";
639 $command = str_replace(verify_hhbc(), "$test.repo/hhvm.hhbc",
641 $command = hphp_cmd($options, $test)."\n".$command."\n";
646 print $header_start."Re-run just the failing tests:".$header_end.
647 "$argv[0] ".implode(' ', $failed)."\n";
651 return $return_value;