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
51 function error($message) {
56 function hphp_home() {
57 return realpath(__DIR__
.'/../..');
60 function idx($array, $key, $default = null) {
61 return isset($array[$key]) ?
$array[$key] : $default;
64 function idx_file($array, $key, $default = null) {
65 $file = is_file(idx($array, $key)) ?
realpath($array[$key]) : $default;
66 if (!is_file($file)) {
67 error("$file doesn't exist");
69 return rel_path($file);
73 $dir = hphp_home() . '/' . idx($_ENV, 'FBMAKE_BIN_ROOT', '_bin');
80 function verify_hhbc() {
81 return idx($_ENV, 'VERIFY_HHBC', bin_root().'/verify.hhbc');
84 function read_file($file) {
85 return file_exists($file) ?
preg_replace('/\s+/', ' ', (file_get_contents($file))) : "";
88 // http://stackoverflow.com/questions/2637945/
89 function rel_path($to) {
90 $from = explode('/', getcwd().'/');
91 $to = explode('/', $to);
94 foreach($from as $depth => $dir) {
95 // find first non-matching dir
96 if($dir === $to[$depth]) {
97 // ignore this directory
98 array_shift($relPath);
100 // get number of remaining dirs to $from
101 $remaining = count($from) - $depth;
103 // add traversals up to first matching dir
104 $padLength = (count($relPath) +
$remaining - 1) * -1;
105 $relPath = array_pad($relPath, $padLength, '..');
108 $relPath[0] = './' . $relPath[0];
112 return implode('/', $relPath);
115 function get_options($argv) {
127 for ($i = 1; $i < count($argv); $i++
) {
130 if ($arg && $arg[0] == '-') {
131 foreach ($parameters as $long => $short) {
132 if ($arg == '-'.str_replace(':', '', $short) ||
133 $arg == '--'.str_replace(':', '', $long)) {
134 if (substr($long, -1, 1) == ':') {
135 $value = $argv[++
$i];
139 $options[str_replace(':', '', $long)] = $value;
145 if (!$found && $arg) {
149 return array($options, $files);
153 * We support some 'special' file names, that just know where the test
154 * suites are, to avoid typing 'hphp/test/foo'.
156 function map_convenience_filename($file) {
158 'quick' => 'hphp/test/quick',
159 'slow' => 'hphp/test/slow',
160 'zend' => 'hphp/test/zend/good',
161 'zend_bad' => 'hphp/test/zend/bad',
162 'facebook' => 'hphp/facebook/test',
166 if (!preg_match('/([^\/]*)/', $file, $m) ||
167 !isset($mappage[$m[1]])) {
170 return hphp_home().'/'.$mappage[$m[1]];
173 function find_tests($files) {
175 $files = array('quick');
177 if ($files == array('all')) {
178 $files = array('quick', 'slow', 'zend');
180 foreach ($files as &$file) {
181 $file = map_convenience_filename($file);
183 error("Not valid file or directory: '$file'");
185 $file = preg_replace(',//+,', '/', realpath($file));
186 $file = preg_replace(',^'.getcwd().'/,', '', $file);
188 $files = implode(' ', $files);
189 $tests = explode("\n", shell_exec("find $files -name '*.php' -o -name '*.hhas'"));
194 return array_filter($tests);
197 function find_config($test, $name) {
198 return find_config_for_dir(dirname($test), $name);
201 function find_config_for_dir($dir, $name) {
202 while ($dir && stat($dir)) {
203 $config = "$dir/$name";
204 if (is_file($config)) {
207 $dir = substr($dir, 0, strrpos($dir, '/'));
209 return __DIR__
.'/'.$name;
212 function find_debug_config($test, $name) {
213 $debug_config = find_config_for_dir(dirname($test), $name);
214 if (is_file($debug_config)) {
215 return "-m debug --debug-config ".$debug_config;
220 function mode_cmd($options) {
221 $repo_args = "-v Repo.Local.Mode=-- -v Repo.Central.Path=".verify_hhbc();
222 $jit_args = "$repo_args -v Eval.Jit=true -v Eval.JitEnableRenameFunction=true";
223 $mode = idx($options, 'mode');
229 return "$repo_args -vEval.Jit=0";
231 error("-m must be one of jit | interp. Got: '$mode'");
235 function hhvm_cmd($options, $test) {
236 $cmd = implode(" ", array(
237 idx_file($_ENV, 'HHVM_BIN', bin_root().'/hphp/hhvm/hhvm'),
239 find_config($test, 'config.hdf'),
240 find_debug_config($test, 'hphpd.hdf'),
242 '-v Eval.EnableArgsInBacktraces=true',
243 read_file("$test.opts"),
247 if (file_exists("$test.in")) {
248 $cmd .= " <$test.in";
253 function hphp_cmd($options, $test) {
254 return implode(" ", array(
255 idx_file($_ENV, 'HPHP_BIN', bin_root().'/hphp/hhvm/hphp'),
257 find_config($test, 'hphp_config.hdf'),
258 read_file("$test.hphp_opts"),
259 "-thhbc -l0 -k1 -o $test.repo $test",
264 private static $results = array();
265 private static $mode = 0;
267 const MODE_NORMAL
= 0;
268 const MODE_VERBOSE
= 1;
269 const MODE_FBMAKE
= 2;
271 public static function setMode($mode) {
275 public static function pass($test) {
276 array_push(self
::$results, array('name' => $test, 'status' => 'passed'));
277 switch (self
::$mode) {
278 case self
::MODE_NORMAL
:
279 if (self
::hasColor()) {
280 print "\033[1;32m.\033[0m";
285 case self
::MODE_VERBOSE
:
286 if (self
::hasColor()) {
287 print "$test \033[1;32mpassed\033[0m\n";
289 print "$test passed";
292 case self
::MODE_FBMAKE
:
293 self
::sayFBMake($test, 'passed');
298 public static function fail($test) {
299 array_push(self
::$results, array(
301 'status' => 'failed',
302 'details' => @file_get_contents
("$test.diff")
304 switch (self
::$mode) {
305 case self
::MODE_NORMAL
:
306 $diff = @file_get_contents
($test.'.diff');
307 if (self
::hasColor()) {
308 print "\n\033[0;31m$test\033[0m\n$diff";
310 print "\nFAILED: $test\n$diff";
313 case self
::MODE_VERBOSE
:
314 if (self
::hasColor()) {
315 print "$test \033[0;31mFAILED\033[0m\n";
317 print "$test FAILED\n";
320 case self
::MODE_FBMAKE
:
321 self
::sayFBMake($test, 'failed');
326 private static function sayFBMake($test, $status) {
327 $start = array('op' => 'start', 'test' => $test);
328 $end = array('op' => 'test_done', 'test' => $test, 'status' => $status);
329 if ($status == 'failed') {
330 $end['details'] = @file_get_contents
("$test.diff");
332 self
::say($start, $end);
335 public static function getResults() {
336 return self
::$results;
339 /** Output is in the format expected by JsonTestRunner. */
340 public static function say(/* ... */) {
341 $data = array_map(function($row) {
342 return json_encode($row, JSON_UNESCAPED_SLASHES
) . "\n";
344 fwrite(STDERR
, implode("", $data));
347 private static function hasColor() {
348 return posix_isatty(STDOUT
);
352 function run($options, $tests, $bad_test_file) {
353 if (isset($options['verbose'])) {
354 Status
::setMode(Status
::MODE_VERBOSE
);
356 if (isset($options['fbmake'])) {
357 Status
::setMode(Status
::MODE_FBMAKE
);
359 foreach ($tests as $test) {
360 $status = run_test($options, $test);
367 file_put_contents($bad_test_file, json_encode(Status
::getResults()));
368 foreach (Status
::getResults() as $result) {
369 if ($result['status'] == 'failed') {
376 function run_test($options, $test) {
377 $hhvm = hhvm_cmd($options, $test);
379 if (isset($options['repo'])) {
380 if (strpos($test, '.hhas') !== false ||
strpos($hhvm, '-m debug') != false ||
is_file($test.'.norepo')) {
381 # We don't have a way to skip, I guess run non-repo?
383 unlink("$test.repo/hhvm.hhbc");
384 $hphp = hphp_cmd($options, $test);
385 $output .= shell_exec("$hphp 2>&1");
386 $hhvm .= " -v Repo.Authoritative=true -v Repo.Central.Path=$test.repo/hhvm.hhbc";
390 $descriptorspec = array(
391 0 => array("pipe", "r"),
392 1 => array("pipe", "w"),
393 2 => array("pipe", "w"),
395 $process = proc_open("$hhvm 2>&1", $descriptorspec, $pipes);
396 if (!is_resource($process)) {
397 file_put_contents("$test.diff", "Couldn't invoke $hhvm");
402 $output .= stream_get_contents($pipes[1]);
403 file_put_contents("$test.out", $output);
406 // hhvm redirects errors to stdout, so anything on stderr is really bad
407 $stderr = stream_get_contents($pipes[2]);
411 "Test failed because the process wrote on stderr:\n$stderr"
416 proc_close($process);
418 // Needed for testing non-hhvm binaries that don't actually run the code
419 // e.g. util/parser/test/parse_tester.cpp
420 if ($output == "FORCE PASS") {
424 if (file_exists("$test.expect")) {
425 $diff_cmds = "--text -u";
426 exec("diff --text -u $test.expect $test.out > $test.diff 2>&1", $_, $status);
430 } else if (file_exists("$test.expectf")) {
431 $wanted_re = file_get_contents("$test.expectf");
433 // do preg_quote, but miss out any %r delimited sections
437 $length = strlen($wanted_re);
438 while($startOffset < $length) {
439 $start = strpos($wanted_re, $r, $startOffset);
440 if ($start !== false) {
441 // we have found a start tag
442 $end = strpos($wanted_re, $r, $start+
2);
443 if ($end === false) {
444 // unbalanced tag, ignore it.
445 $end = $start = $length;
448 // no more %r sections
449 $start = $end = $length;
451 // quote a non re portion of the string
452 $temp = $temp . preg_quote(substr($wanted_re, $startOffset, ($start - $startOffset)), '/');
453 // add the re unquoted.
455 $temp = $temp . '(' . substr($wanted_re, $start+
2, ($end - $start-2)). ')';
457 $startOffset = $end +
2;
461 $wanted_re = str_replace(
462 array('%binary_string_optional%'),
466 $wanted_re = str_replace(
467 array('%unicode_string_optional%'),
471 $wanted_re = str_replace(
472 array('%unicode\|string%', '%string\|unicode%'),
476 $wanted_re = str_replace(
477 array('%u\|b%', '%b\|u%'),
482 $wanted_re = str_replace('%e', '\\' . DIRECTORY_SEPARATOR
, $wanted_re);
483 $wanted_re = str_replace('%s', '[^\r\n]+', $wanted_re);
484 $wanted_re = str_replace('%S', '[^\r\n]*', $wanted_re);
485 $wanted_re = str_replace('%a', '.+', $wanted_re);
486 $wanted_re = str_replace('%A', '.*', $wanted_re);
487 $wanted_re = str_replace('%w', '\s*', $wanted_re);
488 $wanted_re = str_replace('%i', '[+-]?\d+', $wanted_re);
489 $wanted_re = str_replace('%d', '\d+', $wanted_re);
490 $wanted_re = str_replace('%x', '[0-9a-fA-F]+', $wanted_re);
491 $wanted_re = str_replace('%f', '[+-]?\.?\d+\.?\d*(?:[Ee][+-]?\d+)?', $wanted_re);
492 $wanted_re = str_replace('%c', '.', $wanted_re);
493 // %f allows two points "-.0.0" but that is the best *simple* expression
495 # a poor man's aide for debugging
496 shell_exec("diff --text -u $test.expectf $test.out > $test.diff 2>&1");
498 return preg_match("/^$wanted_re\$/s", $output);
500 } else if (file_exists("$test.expectregex")) {
501 $wanted_re = file_get_contents("$test.expectregex");
503 # a poor man's aide for debugging
504 shell_exec("diff --text -u $test.expectregex $test.out > $test.diff 2>&1");
506 return preg_match("/^$wanted_re\$/s", $output);
510 function num_cpus() {
511 $data = file('/proc/stat');
513 foreach($data as $line) {
514 if (preg_match('/^cpu[0-9]/', $line)) {
521 function main($argv) {
523 ini_set('pcre.backtrack_limit', PHP_INT_MAX
);
525 list($options, $files) = get_options($argv);
526 if (isset($options['help'])) {
529 $tests = find_tests($files);
531 $threads = min(count($tests), idx($options, 'threads', num_cpus() +
1));
533 if (!isset($options['fbmake'])) {
534 print "Running ".count($tests)." tests in $threads threads\n";
537 # Try to construct the buckets so the test results are ready in approximately
539 $test_buckets = array();
541 foreach ($tests as $test) {
542 $test_buckets[$i][] = $test;
543 $i = ($i +
1) %
$threads;
546 # Spawn off worker threads
548 # A poor man's shared memory
549 $bad_test_files = array();
550 for ($i = 0; $i < $threads; $i++
) {
551 $bad_test_file = tempnam('/tmp', 'test-run-');
552 $bad_test_files[] = $bad_test_file;
555 error('could not fork');
559 exit(run($options, $test_buckets[$i], $bad_test_file));
565 foreach ($children as $child) {
566 pcntl_waitpid($child, $status);
567 $return_value |
= pcntl_wexitstatus($status);
571 foreach ($bad_test_files as $bad_test_file) {
572 $json = json_decode(file_get_contents($bad_test_file), true);
573 if (!is_array($json)) {
575 "A test thread didn't send json to the controller. WTF. ".
576 "If your code isn't horribly broken, please complain loudly"
579 $results = array_merge($results, $json);
581 if (isset($options['fbmake'])) {
582 Status
::say(array('op' => 'all_done', 'results' => $results));
584 if (!$return_value) {
585 print "\nAll tests passed.\n\n".<<<SHIP
590 _____|____|____|____\\\__
591 ---------\ SHIP IT
/---------
592 ^^^^^ ^^^^^^^^^^^^^^^^^^^^^
599 foreach ($results as $result) {
600 if ($result['status'] == 'failed') {
601 $failed[] = $result['name'];
605 $header_start = "\n\033[0;33m";
606 $header_end = "\033[0m\n";
607 print "\n".count($failed)." tests failed\n";
609 print $header_start."See the diffs:".$header_end.
610 implode("\n", array_map(
611 function($test) { return 'cat '.$test.'.diff'; },
614 print $header_start."Run these by hand:".$header_end;
616 foreach ($failed as $test) {
617 $command = hhvm_cmd($options, $test);
618 if (isset($options['repo'])) {
619 $command .= " -v Repo.Authoritative=true ";
620 $command = str_replace(verify_hhbc(), "$test.repo/hhvm.hhbc", $command);
621 $command = hphp_cmd($options, $test)."\n".$command."\n";
626 print $header_start."Re-run just the failing tests:".$header_end.
627 "$argv[0] ".implode(' ', $failed)."\n";
631 return $return_value;