Don't use Aast.Any when marking expressions as type Tany
[hiphop-php.git] / hphp / test / run.php
blobe3e5202d513d3424e61f74e91090502f776fb39f
1 <?hh
2 /**
3 * Run the test suites in various configurations.
4 */
6 const TIMEOUT_SECONDS = 300;
8 function is_testing_dso_extension() {
9 // detecting if we're running outside of the hhvm codebase.
10 return !is_file(__DIR__ . "/../../hphp/test/run");
13 function get_expect_file_and_type($test, $options) {
14 // .typechecker files are for typechecker (hh_server --check) test runs.
15 $types = null;
16 if (isset($options['typechecker'])) {
17 $types = array('typechecker.expect', 'typechecker.expectf');
18 } else {
19 $types = array('expect', 'hhvm.expect', 'expectf', 'hhvm.expectf',
20 'expectregex');
22 if (isset($options['repo'])) {
23 if (file_exists($test . '.hhbbc_assert')) {
24 return array($test . '.hhbbc_assert', 'expectf');
26 foreach ($types as $type) {
27 $fname = "$test.$type-repo";
28 if (file_exists($fname)) {
29 return array($fname, $type);
33 foreach ($types as $type) {
34 $fname = "$test.$type";
35 if (file_exists($fname)) {
36 return array($fname, $type);
39 return array(null, null);
42 function multi_request_modes() {
43 return array('relocate',
44 'retranslate-all',
45 'recycle-tc',
46 'jit-serialize',
47 'cli-server');
50 function has_multi_request_mode($options) {
51 foreach (multi_request_modes() as $option) {
52 if (isset($options[$option])) return true;
54 return false;
57 function jit_serialize_option($cmd, $test, $options, $serialize) {
58 $serialized = "$test.repo/jit.dump";
59 $cmds = explode(' -- ', $cmd, 2);
60 $cmds[0] .=
61 ' --count=' . ($serialize ? $options['jit-serialize'] : 1) .
62 " -vEval.JitSerdesFile=" . $serialized .
63 " -vEval.JitSerdesMode=" . ($serialize ? 'Serialize' : 'DeserializeOrFail');
64 if (isset($options['jitsample']) && $serialize) {
65 $cmds[0] .= ' -vDeploymentId="' . $options['jitsample'] . '-serialize"';
67 return implode(' -- ', $cmds);
70 function usage() {
71 $argv = $GLOBALS['argv'];
72 return "usage: {$argv[0]} [-m jit|interp] [-r] <test/directories>";
75 function help() {
76 $argv = $GLOBALS['argv'];
77 $ztestexample = 'test/zend/good/*/*z*.php'; // sep. for syntax highlighting.
78 $help = <<<EOT
81 This is the hhvm test-suite runner. For more detailed documentation,
82 see hphp/test/README.md.
84 The test argument may be a path to a php test file, a directory name, or
85 one of a few pre-defined suite names that this script knows about.
87 If you work with hhvm a lot, you might consider a bash alias:
89 alias ht="path/to/hphp/test/run"
91 Examples:
93 # Quick tests in JIT mode:
94 % {$argv[0]} test/quick
96 # Slow tests in interp mode:
97 % {$argv[0]} -m interp test/slow
99 # PHP specification tests in JIT mode:
100 % {$argv[0]} test/slow/spec
102 # Slow closure tests in JIT mode:
103 % {$argv[0]} test/slow/closure
105 # Slow closure tests in JIT mode with RepoAuthoritative:
106 % {$argv[0]} -r test/slow/closure
108 # Slow array tests, in RepoAuthoritative:
109 % {$argv[0]} -r test/slow/array
111 # Zend tests with a "z" in their name:
112 % {$argv[0]} $ztestexample
114 # Quick tests in JIT mode with some extra runtime options:
115 % {$argv[0]} test/quick -a '-vEval.JitMaxTranslations=120 -vEval.HHIRRefcountOpts=0'
117 # All quick tests except debugger
118 % {$argv[0]} -e debugger test/quick
120 # All tests except those containing a string of 3 digits
121 % {$argv[0]} -E '/\d{3}/' all
123 # All tests whose name containing pdo_mysql
124 % {$argv[0]} -i pdo_mysql -m jit -r zend
126 # Print all the standard tests
127 % {$argv[0]} --list-tests
129 # Use a specific HHVM binary
130 % {$argv[0]} -b ~/code/hhvm/hphp/hhvm/hhvm
131 % {$argv[0]} --hhvm-binary-path ~/code/hhvm/hphp/hhvm/hhvm
133 # Use live relocation to run tests in the same thread. e.g, 6 times in the same
134 # thread, where the 3 specifies a random relocation for the 3rd request and the
135 # test is run 3 * 2 times.
136 % {$argv[0]} --relocate 3 test/quick/silencer.php
138 # Use retranslate all. Run the test n times, then run retranslate all, then
139 # run the test n more on the new code.
140 % {$argv[0]} --retranslate-all 2 quick
142 # Use jit-serialize. Run the test n times, then run retranslate all,
143 # serialize the state, and then restart hhvm, load the serialized state and
144 # run retranslate-all before starting the test.
145 % {$argv[0]} --jit-serialize 2 -r quick
147 # Run the Hack typechecker against quick typechecker.expect[f] files
148 # Could explcitly use quick here too
149 # {$argv[0]} --typechecker
151 # Run the Hack typechecker against typechecker.expect[f] files in the slow
152 # directory
153 # {$argv[0]} --typechecker slow
155 # Run the Hack typechecker against the typechecker.expect[f] file in this test
156 # {$argv[0]} --typechecker test/slow/test_runner_typechecker_mode/basic.php
158 # Use a specific typechecker binary
159 # {$argv[0]} --hhserver-binary-path ~/code/hhvm/hphp/hack/bin/hh_server --typechecker .
161 EOT;
162 return usage().$help;
165 function error($message) {
166 print "$message\n";
167 exit(1);
170 // If a user-supplied path is provided, let's make sure we have a valid
171 // executable.
172 function check_executable($path, $typechecker) {
173 $type = $typechecker ? "HH_SERVER" : "HHVM";
174 $rpath = realpath($path);
175 $msg = "Provided ".$type." executable (".$path.") is not a file.\n"
176 . "If using ".$type."_BIN, make sure that is set correctly.";
177 if (!is_file($rpath)) {
178 error($msg);
180 $output = array();
181 exec($rpath . " --help 2> /dev/null", &$output);
182 $str = implode($output);
183 $msg = "Provided file (".$rpath.") is not a/an ".$type." executable.\n"
184 . "If using ".$type."_BIN, make sure that is set correctly.";
185 if (strpos($str, "Usage") !== 0) {
186 error($msg);
190 function hhvm_binary_routes() {
191 return array(
192 "buck" => "/buck-out/gen/hphp/hhvm/hhvm",
193 "cmake" => "/hphp/hhvm"
197 function hh_server_binary_routes() {
198 return array(
199 "buck" => "/buck-out/gen/hphp/hack/src/hh_server",
200 "cmake" => "/hphp/hack/bin"
204 function hh_codegen_binary_routes() {
205 return array(
206 "buck" => "/buck-out/bin/hphp/hack/src/hh_single_compile",
207 "cmake" => "/hphp/hack/bin"
211 // For Facebook: We have several build systems, and we can use any of them in
212 // the same code repo. If multiple binaries exist, we want the onus to be on
213 // the user to specify a particular one because before we chose the buck one
214 // by default and that could cause unexpected results.
215 function check_for_multiple_default_binaries($typechecker) {
216 // Env var we use in testing that'll pick which build system to use.
217 if (getenv("FBCODE_BUILD_TOOL") !== false) {
218 return;
221 $home = hphp_home();
222 $routes = $typechecker ? hh_server_binary_routes() : hhvm_binary_routes();
223 $binary = $typechecker ? "hh_server" : "hhvm";
225 $found = array();
226 foreach ($routes as $_ => $path) {
227 $abs_path = $home . $path . "/" . $binary;
228 if (file_exists($abs_path)) {
229 $found[] = $abs_path;
233 if (count($found) <= 1) {
234 return;
237 $path_option = $typechecker ? "--hhserver-binary-path" : "--hhvm-binary-path";
239 $msg = "Multiple binaries exist in this repo. \n";
240 foreach ($found as $bin) {
241 $msg .= " - " . $bin . "\n";
243 $msg .= "Are you in fbcode? If so, remove a binary \n"
244 . "or use the " . $path_option . " option to the test runner. \n"
245 . "e.g., test/run ";
246 if ($typechecker) {
247 $msg .= "--typechecker";
249 $msg .= " " . $path_option . " /path/to/binary slow\n";
250 error($msg);
253 function hphp_home() {
254 if (is_testing_dso_extension()) {
255 return realpath(__DIR__);
257 return realpath(__DIR__.'/../..');
260 function idx($array, $key, $default = null) {
261 return isset($array[$key]) ? $array[$key] : $default;
264 function hhvm_path() {
265 $file = "";
266 if (getenv("HHVM_BIN") !== false) {
267 $file = realpath(getenv("HHVM_BIN"));
268 } else {
269 $file = bin_root().'/hhvm';
272 if (!is_file($file)) {
273 if (is_testing_dso_extension()) {
274 exec("which hhvm 2> /dev/null", &$output);
275 if (isset($output[0]) && $output[0]) {
276 return $output[0];
278 error("You need to specify hhvm bin with env HHVM_BIN");
281 error("$file doesn't exist. Did you forget to build first?");
283 return rel_path($file);
286 function hh_codegen_cmd($options) {
287 $cmd = hh_codegen_path();
288 $cmd .= ' -v Hack.Compiler.SourceMapping=1 ';
289 if (isset($options['hackc'])) {
290 $cmd .= ' --daemon';
293 return $cmd;
296 function bin_root() {
297 if (getenv("HHVM_BIN") !== false) {
298 return dirname(realpath(getenv("HHVM_BIN")));
301 $home = hphp_home();
302 $env_tool = getenv("FBCODE_BUILD_TOOL");
303 $routes = hhvm_binary_routes();
305 if ($env_tool !== false) {
306 return $home . $routes[$env_tool];
309 foreach ($routes as $_ => $path) {
310 $dir = $home . $path;
311 if (is_dir($dir)) {
312 return $dir;
316 return $home . $routes["cmake"];
319 function hh_server_path() {
320 $file = "";
321 if (getenv("HH_SERVER_BIN") !== false) {
322 $file = realpath(getenv("HH_SERVER_BIN"));
323 } else {
324 $file = hh_server_bin_root().'/hh_server';
326 if (!is_file($file)) {
327 error("$file doesn't exist. Did you forget to build first?");
329 return rel_path($file);
332 function hh_server_bin_root() {
333 if (getenv("HH_SERVER_BIN") !== false) {
334 return dirname(realpath(getenv("HH_SERVER_BIN")));
337 $home = hphp_home();
338 $env_tool = getenv("FBCODE_BUILD_TOOL");
339 $routes = hh_server_binary_routes();
341 if ($env_tool !== false) {
342 return $home . $routes[$env_tool];
345 foreach ($routes as $_ => $path) {
346 $dir = $home . $path;
347 if (is_dir($dir)) {
348 return $dir;
352 return $home . $routes["cmake"];
355 function hh_codegen_path() {
356 $file = "";
357 if (getenv("HH_CODEGEN_BIN") !== false) {
358 $file = realpath(getenv("HH_CODEGEN_BIN"));
359 } else {
360 $file = hh_codegen_bin_root().'/hh_single_compile.opt';
362 if (!is_file($file)) {
363 error("$file doesn't exist. Did you forget to build first?");
365 return rel_path($file);
368 function hh_codegen_bin_root() {
369 $home = hphp_home();
370 $env_tool = getenv("FBCODE_BUILD_TOOL");
371 $routes = hh_codegen_binary_routes();
373 if ($env_tool !== false) {
374 return $home . $routes[$env_tool];
377 foreach ($routes as $_ => $path) {
378 $dir = $home . $path;
379 if (is_dir($dir)) {
380 return $dir;
384 return $home . $routes["cmake"];
387 function verify_hhbc() {
388 if (getenv("VERIFY_HHBC") !== false) {
389 return getenv($env_hhbc);
391 return bin_root().'/verify.hhbc';
394 function read_opts_file($file) {
395 if ($file === null || !file_exists($file)) {
396 return "";
399 $fp = fopen($file, "r");
401 $contents = "";
402 while ($line = fgets($fp)) {
403 // Compress out white space.
404 $line = preg_replace('/\s+/', ' ', $line);
406 // Discard simple line oriented ; and # comments to end of line
407 // Comments at end of line (after payload) are not allowed.
408 $line = preg_replace('/^ *;.*$/', ' ', $line);
409 $line = preg_replace('/^ *#.*$/', ' ', $line);
411 // Substitute in the directory name
412 $line = str_replace('__DIR__', dirname($file), $line);
414 $contents .= $line;
416 fclose($fp);
417 return $contents;
420 // http://stackoverflow.com/questions/2637945/
421 function rel_path($to) {
422 $from = explode('/', getcwd().'/');
423 $to = explode('/', $to);
424 $from_len = count($from);
425 $to_len = count($to);
427 // find first non-matching dir.
428 for ($d = 0; $d < $from_len; ++$d) {
429 if ($d >= $to_len || $from[$d] !== $to[$d])
430 break;
433 $relPath = vec[];
435 // get number of remaining dirs in $from.
436 $remaining = $from_len - $d - 1;
437 if ($remaining > 0) {
438 // add traversals up to first matching dir.
439 while ($remaining-- > 0) $relPath[] = '..';
440 } else {
441 $relPath[] = '.';
443 while ($d < $to_len) $relPath[] = $to[$d++];
444 return implode('/', $relPath);
447 function get_options($argv) {
448 # Options marked * affect test behavior, and need to be reported by list_tests
449 $parameters = array(
450 '*env:' => '',
451 'exclude:' => 'e:',
452 'exclude-pattern:' => 'E:',
453 'exclude-recorded-failures:' => 'x:',
454 'include:' => 'i:',
455 'include-pattern:' => 'I:',
456 '*repo' => 'r',
457 '*repo-single' => '',
458 '*repo-separate' => '',
459 '*repo-threads:' => '',
460 '*hhbbc2' => '',
461 '*mode:' => 'm:',
462 '*server' => 's',
463 '*cli-server' => 'S',
464 'shuffle' => '',
465 'help' => 'h',
466 'verbose' => 'v',
467 'testpilot' => '',
468 'threads:' => '',
469 '*args:' => 'a:',
470 'log' => 'l',
471 'failure-file:' => '',
472 '*wholecfg' => '',
473 '*hhas-round-trip' => '',
474 'color' => 'c',
475 'no-fun' => '',
476 'cores' => '',
477 'dump-tc' => '',
478 'no-clean' => '',
479 'list-tests' => '',
480 '*relocate:' => '',
481 '*recycle-tc:' => '',
482 '*retranslate-all:' => '',
483 '*jit-serialize:' => '',
484 '*hhvm-binary-path:' => 'b:',
485 '*typechecker' => '',
486 '*vendor:' => '',
487 '*hhserver-binary-path:' => '',
488 'record-failures:' => '',
489 '*hackc' => '',
490 '*srcloc' => '',
491 '*hack-only' => '',
492 '*ignore-oids' => '',
493 'jitsample:' => '',
494 '*hh_single_type_check:' => '',
496 $options = array();
497 $files = array();
498 $recorded = array();
501 * '-' argument causes all future arguments to be treated as filenames, even
502 * if they would otherwise match a valid option. Otherwise, arguments starting
503 * with '-' MUST match a valid option.
505 $force_file = false;
507 for ($i = 1; $i < count($argv); $i++) {
508 $arg = $argv[$i];
510 if (strlen($arg) == 0) {
511 continue;
512 } else if ($force_file) {
513 $files[] = $arg;
514 } else if ($arg === '-') {
515 $forcefile = true;
516 } else if ($arg[0] === '-') {
517 $found = false;
519 foreach ($parameters as $long => $short) {
520 if ($arg == '-'.str_replace(':', '', $short) ||
521 $arg == '--'.str_replace(array(':', '*'), array('', ''), $long)) {
522 $record = substr($long, 0, 1) === '*';
523 if ($record) $recorded[] = $arg;
524 if (substr($long, -1, 1) == ':') {
525 $value = $argv[++$i];
526 if ($record) $recorded[] = $value;
527 } else {
528 $value = true;
530 $options[str_replace(array(':', '*'), array('', ''), $long)] = $value;
531 $found = true;
532 break;
536 if (!$found) {
537 error(sprintf("Invalid argument: '%s'\nSee {$argv[0]} --help", $arg));
539 } else {
540 $files[] = $arg;
544 $GLOBALS['recorded_options'] = $recorded;
546 if (isset($options['hhbbc2'])) {
547 $options['repo-separate'] = true;
548 if (isset($options['repo']) || isset($options['repo-single'])) {
549 echo "repo-single/repo and hhbbc2 are mutually exclusive options\n";
550 exit(1);
554 if (isset($options['repo-single']) || isset($options['repo-separate'])) {
555 $options['repo'] = true;
556 } else if (isset($options['repo'])) {
557 // if only repo was set, then it means repo single
558 $options['repo-single'] = true;
561 if (isset($options['jit-serialize'])) {
562 if (!isset($options['repo'])) {
563 echo "jit-serialize only works in repo mode\n";
564 exit(1);
566 if (isset($options['mode']) && $options['mode'] != 'jit') {
567 echo "jit-serialize only works in jit mode\n";
568 exit(1);
572 if (isset($options['repo']) && isset($options['hhas-round-trip'])) {
573 echo "repo and hhas-round-trip are mutually exclusive options\n";
574 exit(1);
577 $multi_request_modes = array_filter(multi_request_modes(),
578 function($x) use ($options) {
579 return isset($options[$x]);
581 if (count($multi_request_modes) > 1) {
582 echo "The options\n -", implode("\n -", $multi_request_modes),
583 "\nare mutually exclusive options\n";
584 exit(1);
587 return array($options, $files);
591 * Return the path to $test relative to __DIR__, or false if __DIR__ does not
592 * contain test.
594 function canonical_path_from_base($test, $base) {
595 $full = realpath($test);
596 if (substr($full, 0, strlen($base)) === $base) {
597 return substr($full, strlen($base) + 1);
599 $dirstat = stat($base);
600 if (!is_array($dirstat)) return false;
601 for ($p = dirname($full); $p && $p !== "/"; $p = dirname($p)) {
602 $s = stat($p);
603 if (!is_array($s)) continue;
604 if ($s['ino'] === $dirstat['ino'] && $s['dev'] === $dirstat['dev']) {
605 return substr($full, strlen($p) + 1);
608 return false;
611 function canonical_path($test) {
612 $attempt = canonical_path_from_base($test,__DIR__);
613 if ($attempt === false) {
614 return canonical_path_from_base($test, hphp_home());
615 } else {
616 return $attempt;
621 * We support some 'special' file names, that just know where the test
622 * suites are, to avoid typing 'hphp/test/foo'.
624 function find_test_files($file) {
625 $mappage = array(
626 'quick' => 'hphp/test/quick',
627 'slow' => 'hphp/test/slow',
628 'debugger' => 'hphp/test/server/debugger/tests',
629 'http' => 'hphp/test/server/http/tests',
630 'fastcgi' => 'hphp/test/server/fastcgi/tests',
631 'zend' => 'hphp/test/zend/good',
632 'facebook' => 'hphp/facebook/test',
634 // Subsets of zend tests.
635 'zend_ext' => 'hphp/test/zend/good/ext',
636 'zend_ext_am' => 'hphp/test/zend/good/ext/[a-m]*',
637 'zend_ext_nz' => 'hphp/test/zend/good/ext/[n-z]*',
638 'zend_Zend' => 'hphp/test/zend/good/Zend',
639 'zend_tests' => 'hphp/test/zend/good/tests',
642 if (isset($mappage[$file])) {
643 $matches = glob(hphp_home().'/'.$mappage[$file]);
644 if (count($matches) == 0) {
645 error(sprintf(
646 "Convenience test name '%s' is recognized but does not match any test ".
647 "files (pattern = '%s', hphp_home = '%s')",
648 $file, $mappage[$file], hphp_home()));
650 return $matches;
651 } else {
652 return array($file, );
656 // Some tests have to be run together in the same test bucket, serially, one
657 // after other in order to avoid races and other collisions.
658 function serial_only_tests($tests) {
659 if (is_testing_dso_extension()) {
660 return array();
662 // Add a <testname>.php.serial file to make your test run in the serial
663 // bucket.
664 $serial_tests = array_filter(
665 $tests,
666 function($test) {
667 return file_exists($test . '.serial');
670 return $serial_tests;
673 function find_tests($files, array $options = null) {
674 if (!$files) {
675 $files = array('quick');
677 if ($files == array('all')) {
678 $files = array('quick', 'slow', 'zend', 'fastcgi');
679 if (is_dir(hphp_home() . '/hphp/facebook/test')) {
680 $files[] = 'facebook';
683 $ft = array();
684 foreach ($files as $file) {
685 $ft = array_merge($ft, find_test_files($file));
687 $files = $ft;
688 foreach ($files as $idx => $file) {
689 if (!@stat($file)) {
690 error("Not valid file or directory: '$file'");
692 $file = preg_replace(',//+,', '/', realpath($file));
693 $file = preg_replace(',^'.getcwd().'/,', '', $file);
694 $files[$idx] = $file;
696 $files = array_map('escapeshellarg', $files);
697 $files = implode(' ', $files);
698 if (isset($options['typechecker'])) {
699 $tests = explode("\n", shell_exec(
700 "find $files ".
701 "-name '*.php' ".
702 "-o -name '*.php.type-errors' ".
703 "-o -name '*.hack' ".
704 "-o -name '*.hack.type-errors'"
706 // The above will get all the php files. Now filter out only the ones
707 // that have a .hhconfig associated with it.
708 $tests = array_filter(
709 $tests,
710 function($test) {
711 return (file_exists($test . '.typechecker.expect') ||
712 file_exists($test . '.typechecker.expectf')) &&
713 file_exists($test . '.hhconfig');
716 } else {
717 $tests = explode("\n", shell_exec(
718 "find $files '(' " .
719 "-name '*.php' " .
720 "-o -name '*.hack' " .
721 "-o -name '*.js' " .
722 "-o -name '*.php.type-errors' " .
723 "-o -name '*.hack.type-errors' " .
724 "-o -name '*.hhas' " .
725 "')' " .
726 "-not -regex '.*round_trip[.]hhas'"
729 if (!$tests) {
730 error("Could not find any tests associated with your options.\n" .
731 "Make sure your test path is correct and that you have " .
732 "the right expect files for the tests you are trying to run.\n" .
733 usage());
735 asort(&$tests);
736 $tests = array_filter($tests);
737 if ($options['exclude'] ?? false) {
738 $exclude = $options['exclude'];
739 $tests = array_filter($tests, function($test) use ($exclude) {
740 return (false === strpos($test, $exclude));
743 if ($options['exclude-pattern'] ?? false) {
744 $exclude = $options['exclude-pattern'];
745 $tests = array_filter($tests, function($test) use ($exclude) {
746 return !preg_match($exclude, $test);
749 if ($options['exclude-recorded-failures'] ?? false) {
750 $exclude_file = $options['exclude-recorded-failures'];
751 $exclude = file($exclude_file, FILE_IGNORE_NEW_LINES);
752 $tests = array_filter($tests, function($test) use ($exclude) {
753 return (false === in_array(canonical_path($test), $exclude));
756 if ($options['include'] ?? false) {
757 $include = $options['include'];
758 $tests = array_filter($tests, function($test) use ($include) {
759 return (false !== strpos($test, $include));
762 if ($options['include-pattern'] ?? false) {
763 $include = $options['include-pattern'];
764 $tests = array_filter($tests, function($test) use ($include) {
765 return preg_match($include, $test);
768 return $tests;
771 function list_tests($files, $options) {
772 $args = implode(' ', $GLOBALS['recorded_options']);
774 foreach (find_tests($files, $options) as $test) {
775 print str_replace('\\', '\\\\',
776 Status::jsonEncode(
777 array('args' => $args, 'name' => $test)
779 )."\n";
783 function find_test_ext($test, $ext, $configName='config') {
784 if (is_file("{$test}.{$ext}")) {
785 return "{$test}.{$ext}";
787 return find_file_for_dir(dirname($test), "{$configName}.{$ext}");
790 function find_file_for_dir($dir, $name) {
791 // Handle the case where the $dir might come in as '.' because you
792 // are running the test runner on a file from the same directory as
793 // the test e.g., './mytest.php'. dirname() will give you the '.' when
794 // you actually have a lot of path to traverse upwards like
795 // /home/you/code/tests/mytest.php. Use realpath() to get that.
796 $dir = realpath($dir);
797 while ($dir !== '/' && is_dir($dir)) {
798 $file = "$dir/$name";
799 if (is_file($file)) {
800 return $file;
802 $dir = dirname($dir);
804 $file = __DIR__.'/'.$name;
805 if (file_exists($file)) {
806 return $file;
808 return null;
811 function find_debug_config($test, $name) {
812 $debug_config = find_file_for_dir(dirname($test), $name);
813 if ($debug_config !== null) {
814 return "-m debug --debug-config ".$debug_config;
816 return "";
819 function mode_cmd($options) {
820 $repo_args = '';
821 if (!isset($options['repo'])) {
822 // Set the non-repo-mode shared repo.
823 // When in repo mode, we set our own central path.
824 $repo_args = "-vRepo.Local.Mode=-- -vRepo.Central.Path=".verify_hhbc();
826 $jit_args = "$repo_args -vEval.Jit=true";
827 $mode = idx($options, 'mode', '');
828 switch ($mode) {
829 case '':
830 case 'jit':
831 return "$jit_args";
832 case 'pgo':
833 return $jit_args.
834 ' -vEval.JitPGO=1'.
835 ' -vEval.JitPGORegionSelector=hotcfg';
836 case 'interp':
837 return "$repo_args -vEval.Jit=0";
838 case 'interp,jit':
839 return array("$repo_args -vEval.Jit=0", $jit_args);
840 default:
841 error("-m must be one of jit | pgo | interp | interp,jit. Got: '$mode'");
845 function extra_args($options): string {
846 $args = $options['args'] ?? '';
848 $vendor = $options['vendor'] ?? null;
849 if ($vendor !== null) {
850 $args .= ' -d auto_prepend_file=';
851 $args .= escapeshellarg($vendor.'/hh_autoload.php');
853 return $args;
856 function hhvm_cmd_impl($options, $config, $test, ...$extra_args) {
857 $modes = (array)mode_cmd($options);
859 $cmds = array();
860 foreach ($modes as $mode_num => $mode) {
861 $args = array(
862 hhvm_path(),
863 '-c',
864 $config,
865 '-vEval.EnableArgsInBacktraces=true',
866 '-vEval.EnableIntrinsicsExtension=true',
867 '-vEval.HHIRInliningIgnoreHints=false',
868 '-vAutoload.DBPath='.escapeshellarg("$test.$mode_num.autoloadDB"),
869 $mode,
870 isset($options['wholecfg']) ? '-vEval.JitPGORegionSelector=wholecfg' : '',
872 // load/store counters don't work on Ivy Bridge so disable for tests
873 '-vEval.ProfileHWEnable=false',
875 // use a fixed path for embedded data
876 '-vEval.HackCompilerExtractPath='
877 .escapeshellarg(bin_root().'/hackc_%{schema}'),
878 '-vEval.EmbeddedDataExtractPath='
879 .escapeshellarg(bin_root().'/hhvm_%{type}_%{buildid}'),
880 extra_args($options),
883 if (isset($options['hackc'])) {
884 $args[] = '-vEval.HackCompilerCommand="'.hh_codegen_cmd($options).'"';
885 $args[] = '-vEval.HackCompilerUseEmbedded=false';
888 if (isset($options['relocate'])) {
889 $args[] = '--count='.($options['relocate'] * 2);
890 $args[] = '-vEval.JitAHotSize=6000000';
891 $args[] = '-vEval.PerfRelocate='.$options['relocate'];
894 if (isset($options['retranslate-all'])) {
895 $args[] = '--count='.($options['retranslate-all'] * 2);
896 $args[] = '-vEval.JitPGO=true';
897 $args[] = '-vEval.JitRetranslateAllRequest='.$options['retranslate-all'];
898 // Set to timeout. We want requests to trigger retranslate all.
899 $args[] = '-vEval.JitRetranslateAllSeconds=' . TIMEOUT_SECONDS;
902 if (isset($options['recycle-tc'])) {
903 $args[] = '--count='.$options['recycle-tc'];
904 $args[] = '-vEval.StressUnitCacheFreq=1';
905 $args[] = '-vEval.EnableReusableTC=true';
908 if (isset($options['jit-serialize'])) {
909 $args[] = '-vEval.JitPGO=true';
910 $args[] = '-vEval.JitRetranslateAllRequest='.$options['jit-serialize'];
911 // Set to timeout. We want requests to trigger retranslate all.
912 $args[] = '-vEval.JitRetranslateAllSeconds=' . TIMEOUT_SECONDS;
915 if (isset($options['hhas-round-trip'])) {
916 $args[] = '-vEval.AllowHhas=1';
917 $args[] = '-vEval.LoadFilepathFromUnitCache=1';
920 if (!isset($options['cores'])) {
921 $args[] = '-vResourceLimit.CoreFileSize=0';
924 if (isset($options['dump-tc'])) {
925 $args[] = '-vEval.DumpIR=1';
926 $args[] = '-vEval.DumpTC=1';
929 if (isset($options['hh_single_type_check'])) {
930 $args[] = '--hh_single_type_check='.$options['hh_single_type_check'];
933 $cmds[] = implode(' ', array_merge($args, $extra_args));
935 if (count($cmds) != 1) return $cmds;
936 return $cmds[0];
939 function repo_separate($options, $test) {
940 return isset($options['repo-separate']) &&
941 !file_exists($test . ".hhbbc_opts");
944 // Return the command and the env to run it in.
945 function hhvm_cmd($options, $test, $test_run = null, $is_temp_file = false) {
946 if ($test_run === null) {
947 $test_run = $test;
949 // hdf support is only temporary until we fully migrate to ini
950 // Discourage broad use.
951 $hdf_suffix = ".use.for.ini.migration.testing.only.hdf";
952 $hdf = file_exists($test.$hdf_suffix)
953 ? '-c ' . $test . $hdf_suffix
954 : "";
955 $cmds = hhvm_cmd_impl(
956 $options,
957 find_test_ext($test, 'ini'),
958 $test,
959 $hdf,
960 find_debug_config($test, 'hphpd.ini'),
961 read_opts_file(find_test_ext($test, 'opts')),
962 '--file',
963 escapeshellarg($test_run),
964 $is_temp_file ? " --temp-file" : ""
967 $cmd = "";
969 if (file_exists($test.'.verify')) {
970 $cmd .= " -m verify";
973 if (isset($options['cli-server'])) {
974 $config = find_file_for_dir(dirname($test), 'config.ini');
975 $socket = $options['servers']['configs'][$config]->server['cli-socket'];
976 $cmd .= ' -vEval.UseRemoteUnixServer=only';
977 $cmd .= ' -vEval.UnixServerPath='.$socket;
978 $cmd .= ' --count=3';
981 // Special support for tests that require a path to the current
982 // test directory for things like prepend_file and append_file
983 // testing.
984 if (file_exists($test.'.ini')) {
985 $contents = file_get_contents($test.'.ini');
986 if (strpos($contents, '{PWD}') !== false) {
987 $test_ini = tempnam('/tmp', $test).'.ini';
988 file_put_contents($test_ini,
989 str_replace('{PWD}', dirname($test), $contents));
990 $cmd .= " -c $test_ini";
993 if ($hdf !== "") {
994 $contents = file_get_contents($test.$hdf_suffix);
995 if (strpos($contents, '{PWD}') !== false) {
996 $test_hdf = tempnam('/tmp', $test).$hdf_suffix;
997 file_put_contents($test_hdf,
998 str_replace('{PWD}', dirname($test), $contents));
999 $cmd .= " -c $test_hdf";
1003 if (isset($options['repo'])) {
1004 $repo_suffix = repo_separate($options, $test) ? 'hhbbc' : 'hhbc';
1006 $program = isset($options['hackc']) ? "hackc" : "hhvm";
1007 $hhbbc_repo = "\"$test.repo/$program.$repo_suffix\"";
1008 $cmd .= ' -vRepo.Authoritative=true -vRepo.Commit=0';
1009 $cmd .= " -vRepo.Central.Path=$hhbbc_repo";
1012 if (isset($options['jitsample'])) {
1013 $cmd .= ' -vDeploymentId="' . $options['jitsample'] . '"';
1014 $cmd .= ' --instance-id="' . $test . '"';
1015 $cmd .= ' -vEval.JitSampleRate=1';
1016 $cmd .= " -vScribe.Tables.hhvm_jit.include.*=instance_id";
1017 $cmd .= " -vScribe.Tables.hhvm_jit.include.*=deployment_id";
1020 // Command line arguments
1021 $cli_args = find_test_ext($test, 'cli_args');
1022 if ($cli_args !== null) {
1023 $cmd .= " -- " . trim(file_get_contents($cli_args));
1026 $env = $_ENV;
1027 $extra_env = array();
1029 // Apply the --env option
1030 if (isset($options['env'])) {
1031 $extra_env = array_merge($extra_env,
1032 explode(",", $options['env']));
1035 // If there's an <test name>.env file then inject the contents of that into
1036 // the test environment.
1037 $env_file = find_test_ext($test, 'env');
1038 if ($env_file !== null) {
1039 $extra_env = array_merge($extra_env,
1040 explode("\n", trim(file_get_contents($env_file))));
1043 if ($extra_env) {
1044 foreach ($extra_env as $arg) {
1045 $i = strpos($arg, '=');
1046 if ($i) {
1047 $key = substr($arg, 0, $i);
1048 $val = substr($arg, $i + 1);
1049 $env[$key] = $val;
1050 } else {
1051 unset($env[$arg]);
1056 $in = find_test_ext($test, 'in');
1057 if ($in !== null) {
1058 $cmd .= ' < ' . escapeshellarg($in);
1059 // If we're piping the input into the command then setup a simple
1060 // dumb terminal so hhvm doesn't try to control it and pollute the
1061 // output with control characters, which could change depending on
1062 // a wide variety of terminal settings.
1063 $env["TERM"] = "dumb";
1066 if (is_array($cmds)) {
1067 foreach ($cmds as $idx => $_) {
1068 $cmds[$idx] .= $cmd;
1070 $cmd = $cmds;
1071 } else {
1072 $cmd = $cmds . $cmd;
1075 return array($cmd, $env);
1078 function hphp_cmd($options, $test, $program) {
1079 $extra_args = preg_replace("/-v\s*/", "-vRuntime.", extra_args($options));
1081 $compiler_args = "";
1082 if (isset($options['hackc'])) {
1083 $hh_single_compile = hh_codegen_path();
1084 $compiler_args = implode(" ", array(
1085 '-vRuntime.Eval.HackCompilerUseEmbedded=false',
1086 "-vRuntime.Eval.HackCompilerInheritConfig=true",
1087 "-vRuntime.Eval.HackCompilerCommand=\"{$hh_single_compile} -v Hack.Compiler.SourceMapping=1 --daemon --dump-symbol-refs\""
1091 return implode(" ", array(
1092 hhvm_path(),
1093 '--hphp',
1094 '-vUseHHBBC='. (repo_separate($options, $test) ? 'false' : 'true'),
1095 '--config',
1096 find_test_ext($test, 'ini', 'hphp_config'),
1097 '-vRuntime.ResourceLimit.CoreFileSize=0',
1098 '-vRuntime.Eval.EnableIntrinsicsExtension=true',
1099 '-vRuntime.Eval.EnableArgsInBacktraces=true',
1100 '-vRuntime.Eval.HackCompilerExtractPath='
1101 .escapeshellarg(bin_root().'/hackc_%{schema}'),
1102 '-vParserThreadCount=' . ($options['repo-threads'] ?? 1),
1103 '--nofork=1 -thhbc -l1 -k1',
1104 "-o \"$test.repo\" --program $program.hhbc \"$test\"",
1105 "-vRuntime.Repo.Local.Mode=rw -vRuntime.Repo.Local.Path=".verify_hhbc(),
1106 $extra_args,
1107 $compiler_args,
1108 read_opts_file("$test.hphp_opts"),
1112 function hhbbc_cmd($options, $test, $program) {
1113 return implode(" ", array(
1114 hhvm_path(),
1115 '--hhbbc',
1116 '--no-logging',
1117 '--no-cores',
1118 '--parallel-num-threads=' . ($options['repo-threads'] ?? 1),
1119 '--hack-compiler-extract-path='
1120 .escapeshellarg(bin_root().'/hackc_%{schema}'),
1121 read_opts_file("$test.hhbbc_opts"),
1122 "-o \"$test.repo/$program.hhbbc\" \"$test.repo/$program.hhbc\"",
1126 // Execute $cmd and return its output, including any stacktrace.log
1127 // file it generated.
1128 function exec_with_stack($cmd) {
1129 $proc = proc_open($cmd,
1130 array(0 => array('pipe', 'r'),
1131 1 => array('pipe', 'w'),
1132 2 => array('pipe', 'w')), &$pipes);
1133 fclose($pipes[0]);
1134 $s = '';
1135 $all_selects_failed=true;
1136 $end = microtime(true) + TIMEOUT_SECONDS;
1137 $timedout = false;
1138 while (true) {
1139 $now = microtime(true);
1140 if ($now >= $end) break;
1141 $read = array($pipes[1], $pipes[2]);
1142 $write = null;
1143 $except = null;
1144 $available = @stream_select(&$read, &$write, &$except, (int)($end - $now));
1145 if ($available === false) {
1146 usleep(1000);
1147 $s .= "select failed:\n" . print_r(error_get_last(), true);
1148 continue;
1150 $all_selects_failed=false;
1151 if ($available === 0) continue;
1152 # var_dump($read);
1153 foreach ($read as $pipe) {
1154 $t = fread($pipe, 4096);
1155 # var_dump($t);
1156 if ($t === false) continue;
1157 $s .= $t;
1159 if (feof($pipes[1]) && feof($pipes[2])) break;
1161 fclose($pipes[1]);
1162 fclose($pipes[2]);
1163 while (true) {
1164 $status = proc_get_status($proc);
1165 if (!$status['running']) break;
1166 $now = microtime(true);
1167 if ($now >= $end) {
1168 $timedout = true;
1169 exec('pkill -P ' . $status['pid'] . ' 2> /dev/null');
1170 posix_kill($status['pid'], SIGTERM);
1172 usleep(1000);
1174 proc_close($proc);
1175 if ($timedout) {
1176 if ($all_selects_failed) {
1177 return "All selects failed running `$cmd'\n\n$s";
1179 return "Timed out running `$cmd'\n\n$s";
1181 if (!$status['exitcode']) return true;
1182 $pid = $status['pid'];
1183 $stack =
1184 @file_get_contents("/tmp/stacktrace.$pid.log") ?:
1185 @file_get_contents("/var/tmp/cores/stacktrace.$pid.log");
1186 if ($stack !== false) {
1187 $s .= "\n" . $stack;
1189 return "Running `$cmd' failed (".$status['exitcode']."):\n\n$s";
1192 function repo_mode_compile($options, $test, $program) {
1193 $hphp = hphp_cmd($options, $test, $program);
1194 $result = exec_with_stack($hphp);
1195 if ($result === true && repo_separate($options, $test)) {
1196 $hhbbc = hhbbc_cmd($options, $test, $program);
1197 $result = exec_with_stack($hhbbc);
1199 if ($result === true) return true;
1200 file_put_contents("$test.diff", $result);
1203 function hh_server_cmd($options, $test) {
1204 // In order to run hh_server --check on only one file, we copy all of the
1205 // files associated with the test to a temporary directory, rename the
1206 // basename($test_file).hhconfig file to just .hhconfig and set the command
1207 // appropriately.
1208 $temp_dir = '/tmp/hh-test-runner-'.bin2hex(random_bytes(16));
1209 mkdir($temp_dir);
1210 foreach (glob($test . '*') as $test_file) {
1211 copy($test_file, $temp_dir . '/' . basename($test_file));
1212 if (strpos($test_file, '.hhconfig') !== false) {
1213 rename(
1214 $temp_dir . '/' . basename($test) . '.hhconfig',
1215 $temp_dir . '/.hhconfig'
1217 } else if (strpos($test_file, '.type-errors') !== false) {
1218 // In order to actually run hh_server --check successfully, all files
1219 // named *.php.type-errors have to be renamed *.php
1220 rename(
1221 $temp_dir . '/' . basename($test_file),
1222 $temp_dir . '/' . str_replace('.type-errors', '', basename($test_file))
1226 // Just copy all the .php.inc files, even if they are not related since
1227 // unrelated ones will be ignored anyway. This just makes it easier to
1228 // start with instead of doing a search inside the test file for requires
1229 // and includes and extracting it.
1230 foreach (glob(dirname($test) . "/*.inc.php") as $inc_file) {
1231 copy($inc_file, $temp_dir . '/' . basename($inc_file));
1233 $cmd = hh_server_path() . ' --check ' . $temp_dir;
1235 $vendor = $options['vendor'] ?? null;
1236 if ($vendor !== null) {
1237 $f = fopen($temp_dir.'/.hhconfig', 'a+');
1238 if (!is_resource($f)) {
1239 throw new Exception('failed to open hhconfig for append');
1241 fprintf(
1243 "\nextra_paths=%s\n",
1244 $vendor
1246 fclose($f);
1248 return array($cmd, ' ', $temp_dir);
1251 class Status {
1252 private static $results = array();
1253 private static $mode = 0;
1255 private static $use_color = false;
1257 public static $nofork = false;
1258 private static $queue = null;
1259 private static $killed = false;
1260 public static $key;
1262 private static $overall_start_time = 0;
1263 private static $overall_end_time = 0;
1265 private static $tempdir = "";
1267 public static $passed = 0;
1268 public static $skipped = 0;
1269 public static $skip_reasons = array();
1270 public static $failed = 0;
1272 const MODE_NORMAL = 0;
1273 const MODE_VERBOSE = 1;
1274 const MODE_TESTPILOT = 3;
1275 const MODE_RECORD_FAILURES = 4;
1277 const MSG_STARTED = 7;
1278 const MSG_FINISHED = 1;
1279 const MSG_TEST_PASS = 2;
1280 const MSG_TEST_FAIL = 4;
1281 const MSG_TEST_SKIP = 5;
1282 const MSG_SERVER_RESTARTED = 6;
1284 const RED = 31;
1285 const GREEN = 32;
1286 const YELLOW = 33;
1287 const BLUE = 34;
1289 const PASS_SERVER = 0;
1290 const SKIP_SERVER = 1;
1291 const PASS_CLI = 2;
1293 private static function getTempDir() {
1294 self::$tempdir = sys_get_temp_dir();
1295 // Apparently some systems might not put the trailing slash
1296 if (substr(self::$tempdir, -1) !== "/") {
1297 self::$tempdir .= "/";
1299 self::$tempdir .= getmypid().'-'.rand();
1300 mkdir(self::$tempdir);
1303 public static function getTestTmpDir() {
1304 $test_tmp_dir = self::$tempdir . "/test-data";
1305 mkdir($test_tmp_dir);
1306 return $test_tmp_dir;
1309 private static function removeDirectory($dir) {
1310 $files = scandir($dir);
1311 foreach ($files as $file) {
1312 if ($file == '.' || $file == '..') {
1313 continue;
1315 $path = $dir . "/" . $file;
1316 if (is_dir($path)) {
1317 self::removeDirectory($path);
1318 } else {
1319 unlink($path);
1322 rmdir($dir);
1325 public static function removeTempDir() {
1326 self::removeDirectory(self::$tempdir);
1329 public static function setMode($mode) {
1330 self::$mode = $mode;
1333 public static function getMode() {
1334 return self::$mode;
1337 public static function setUseColor($use) {
1338 self::$use_color = $use;
1341 // Since we run the tests in forked processes, state is not shared
1342 // So we cannot keep a static variable adding individual test times.
1343 // But we can put the times files and add the values later.
1344 public static function setTestTime($time) {
1345 file_put_contents(tempnam(self::$tempdir, "trun"), $time);
1348 // The total time running the tests if they were run serially.
1349 public static function addTestTimesSerial() {
1350 $time = 0;
1351 $files = scandir(self::$tempdir);
1352 foreach ($files as $file) {
1353 if (strpos($file, 'trun') === 0) {
1354 $time += floatval(file_get_contents(self::$tempdir . "/" . $file));
1355 unlink(self::$tempdir . "/" . $file);
1358 return $time;
1361 public static function getOverallStartTime() {
1362 return self::$overall_start_time;
1365 public static function getOverallEndTime() {
1366 return self::$overall_end_time;
1369 public static function started() {
1370 self::getTempDir();
1371 self::send(self::MSG_STARTED, null);
1372 self::$overall_start_time = microtime(true);
1375 public static function finished() {
1376 self::$overall_end_time = microtime(true);
1377 self::send(self::MSG_FINISHED, null);
1380 public static function killQueue() {
1381 if (!self::$killed) {
1382 msg_remove_queue(self::$queue);
1383 self::$queue = null;
1384 self::$killed = true;
1388 public static function serverRestarted() {
1389 self::send(self::MSG_SERVER_RESTARTED, null);
1392 public static function pass($test, $detail, $time, $stime, $etime) {
1393 self::$results[] = array('name' => $test,
1394 'status' => 'passed',
1395 'start_time' => $stime,
1396 'end_time' => $etime,
1397 'time' => $time);
1398 $how = $detail === 'pass-server' ? self::PASS_SERVER :
1399 ($detail === 'skip-server' ? self::SKIP_SERVER : self::PASS_CLI);
1400 self::send(self::MSG_TEST_PASS, array($test, $how, $time, $stime, $etime));
1403 public static function skip($test, $reason, $time, $stime, $etime) {
1404 self::$results[] = array(
1405 'name' => $test,
1406 /* testpilot needs a positive response for every test run, report
1407 * that this test isn't relevant so it can silently drop. */
1408 'status' => self::getMode() === self::MODE_TESTPILOT
1409 ? 'not_relevant'
1410 : 'skipped',
1411 'start_time' => $stime,
1412 'end_time' => $etime,
1413 'time' => $time,
1415 self::send(self::MSG_TEST_SKIP,
1416 array($test, $reason, $time, $stime, $etime));
1419 public static function fail($test, $time, $stime, $etime, $diff) {
1420 self::$results[] = array(
1421 'name' => $test,
1422 'status' => 'failed',
1423 'details' => self::utf8Sanitize($diff),
1424 'start_time' => $stime,
1425 'end_time' => $etime,
1426 'time' => $time
1428 self::send(self::MSG_TEST_FAIL, array($test, $time, $stime, $etime));
1431 public static function handle_message($type, $message) {
1432 switch ($type) {
1433 case Status::MSG_STARTED:
1434 break;
1436 case Status::MSG_FINISHED:
1437 return false;
1439 case Status::MSG_SERVER_RESTARTED:
1440 switch (Status::getMode()) {
1441 case Status::MODE_NORMAL:
1442 if (!Status::hasCursorControl()) {
1443 Status::sayColor(Status::RED, 'x');
1445 break;
1447 case Status::MODE_VERBOSE:
1448 Status::sayColor("$test ", Status::YELLOW, "failed",
1449 " to talk to server\n");
1450 break;
1452 case Status::MODE_TESTPILOT:
1453 break;
1455 case Status::MODE_RECORD_FAILURES:
1456 break;
1459 case Status::MSG_TEST_PASS:
1460 self::$passed++;
1461 list($test, $how, $time, $stime, $etime) = $message;
1462 switch (Status::getMode()) {
1463 case Status::MODE_NORMAL:
1464 if (!Status::hasCursorControl()) {
1465 if ($how == Status::SKIP_SERVER) {
1466 Status::sayColor(Status::RED, '.');
1467 } else {
1468 Status::sayColor(Status::GREEN,
1469 $how == Status::PASS_SERVER ? ',' : '.');
1472 break;
1474 case Status::MODE_VERBOSE:
1475 Status::sayColor("$test ", Status::GREEN,
1476 sprintf("passed (%.2fs)\n", $time));
1477 break;
1479 case Status::MODE_TESTPILOT:
1480 Status::sayTestpilot($test, 'passed', $stime, $etime);
1481 break;
1483 case Status::MODE_RECORD_FAILURES:
1484 break;
1486 break;
1488 case Status::MSG_TEST_SKIP:
1489 self::$skipped++;
1490 list($test, $reason, $time, $stime, $etime) = $message;
1491 self::$skip_reasons[$reason]++;
1493 switch (Status::getMode()) {
1494 case Status::MODE_NORMAL:
1495 if (!Status::hasCursorControl()) {
1496 Status::sayColor(Status::YELLOW, 's');
1498 break;
1500 case Status::MODE_VERBOSE:
1501 Status::sayColor("$test ", Status::YELLOW, "skipped");
1503 if ($reason !== null) {
1504 Status::sayColor(" - $reason");
1506 Status::sayColor(sprintf(" (%.2fs)\n", $time));
1507 break;
1509 case Status::MODE_TESTPILOT:
1510 Status::sayTestpilot($test, 'not_relevant', $stime, $etime);
1511 break;
1513 case Status::MODE_RECORD_FAILURES:
1514 break;
1516 break;
1518 case Status::MSG_TEST_FAIL:
1519 self::$failed++;
1520 list($test, $time, $stime, $etime) = $message;
1521 switch (Status::getMode()) {
1522 case Status::MODE_NORMAL:
1523 if (Status::hasCursorControl()) {
1524 print "\033[2K\033[1G";
1526 $diff = (string)@file_get_contents($test.'.diff');
1527 Status::sayColor(Status::RED, "\nFAILED",
1528 ": $test\n$diff\n");
1529 break;
1531 case Status::MODE_VERBOSE:
1532 Status::sayColor("$test ", Status::RED,
1533 sprintf("FAILED (%.2fs)\n", $time));
1534 break;
1536 case Status::MODE_TESTPILOT:
1537 Status::sayTestpilot($test, 'failed', $stime, $etime);
1538 break;
1540 case Status::MODE_RECORD_FAILURES:
1541 break;
1543 break;
1545 default:
1546 error("Unknown message $type");
1548 return true;
1551 private static function send($type, $msg) {
1552 if (self::$killed) {
1553 return;
1555 if (self::$nofork) {
1556 self::handle_message($type, $msg);
1557 return;
1559 msg_send(self::getQueue(), $type, $msg);
1563 * Takes a variable number of string arguments. If color output is enabled
1564 * and any one of the arguments is preceded by an integer (see the color
1565 * constants above), that argument will be given the indicated color.
1567 public static function sayColor(...$args) {
1568 $n = count($args);
1569 for ($i = 0; $i < $n;) {
1570 $color = null;
1571 $str = $args[$i++];
1572 if (is_integer($str)) {
1573 $color = $str;
1574 if (self::$use_color) {
1575 print "\033[0;{$color}m";
1577 $str = $args[$i++];
1580 print $str;
1582 if (self::$use_color && !is_null($color)) {
1583 print "\033[0m";
1588 public static function sayTestpilot($test, $status, $stime, $etime) {
1589 $start = array('op' => 'start', 'test' => $test);
1590 $end = array('op' => 'test_done', 'test' => $test, 'status' => $status,
1591 'start_time' => $stime, 'end_time' => $etime);
1592 if ($status == 'failed') {
1593 $end['details'] = self::utf8Sanitize(@file_get_contents("$test.diff"));
1595 self::say($start, $end);
1598 public static function getResults() {
1599 return self::$results;
1602 /** Output is in the format expected by JsonTestRunner. */
1603 public static function say(...$args) {
1604 $data = array_map(
1605 $row ==> self::jsonEncode($row) . "\n",
1606 $args
1608 fwrite(STDERR, implode("", $data));
1611 public static function hasCursorControl() {
1612 // for runs on hudson-ci.org (aka jenkins).
1613 if (getenv("HUDSON_URL")) {
1614 return false;
1616 // for runs on travis-ci.org
1617 if (getenv("TRAVIS")) {
1618 return false;
1620 $stty = self::getSTTY();
1621 if (!$stty) {
1622 return false;
1624 return strpos($stty, 'erase = <undef>') === false;
1627 public static function getSTTY() {
1628 $descriptorspec = array(1 => array("pipe", "w"), 2 => array("pipe", "w"));
1629 $process = proc_open(
1630 'stty -a', $descriptorspec, &$pipes, null, null,
1631 array('suppress_errors' => true)
1633 $stty = stream_get_contents($pipes[1]);
1634 proc_close($process);
1635 return $stty;
1638 public static function utf8Sanitize($str) {
1639 if (!is_string($str)) {
1640 // We sometimes get called with the
1641 // return value of file_get_contents()
1642 // when fgc() has failed.
1643 return '';
1646 return UConverter::transcode($str, 'UTF-8', 'UTF-8');
1649 public static function jsonEncode($data) {
1650 // JSON_UNESCAPED_SLASHES is Zend 5.4+.
1651 if (defined("JSON_UNESCAPED_SLASHES")) {
1652 return json_encode($data, JSON_UNESCAPED_SLASHES);
1655 $json = json_encode($data);
1656 return str_replace('\\/', '/', $json);
1659 public static function getQueue() {
1660 if (!self::$queue) {
1661 self::$queue = msg_get_queue(self::$key);
1663 return self::$queue;
1667 function clean_intermediate_files($test, $options) {
1668 if (isset($options['no-clean'])) {
1669 return;
1671 $exts = array(
1672 // normal test output
1673 'out',
1674 'diff',
1675 // repo mode tests
1676 'repo',
1677 // tests in --hhas-round-trip mode
1678 'round_trip.hhas',
1679 // tests in --hhbbc2 mode
1680 'before.round_trip.hhas',
1681 'after.round_trip.hhas',
1682 // temporary autoloader DB and associated cruft
1683 // We have at most two modes for now - see hhvm_cmd_impl
1684 '0.autoloadDB',
1685 '0.autoloadDB-shm',
1686 '0.autoloadDB-wal',
1687 '1.autoloadDB',
1688 '1.autoloadDB-shm',
1689 '1.autoloadDB-wal',
1691 foreach ($exts as $ext) {
1692 $file = "$test.$ext";
1693 if (file_exists($file)) {
1694 if (is_dir($file)) {
1695 foreach(new RecursiveIteratorIterator(new
1696 RecursiveDirectoryIterator($file, FilesystemIterator::SKIP_DOTS),
1697 RecursiveIteratorIterator::CHILD_FIRST) as $path) {
1698 $path->isDir()
1699 ? rmdir($path->getPathname())
1700 : unlink($path->getPathname());
1702 rmdir($file);
1703 } else {
1704 unlink($file);
1710 function run($options, $tests, $bad_test_file) {
1711 foreach ($tests as $test) {
1712 run_and_lock_test($options, $test);
1714 file_put_contents($bad_test_file, json_encode(Status::getResults()));
1715 foreach (Status::getResults() as $result) {
1716 if ($result['status'] == 'failed') {
1717 return 1;
1720 return 0;
1723 function is_hack_file($options, $test) {
1724 if (substr($test, -3) === '.hh') return true;
1726 $file = fopen($test, 'r');
1727 if ($file === false) return false;
1729 // Skip lines that are a shebang or whitespace.
1730 while (($line = fgets($file)) !== false) {
1731 $line = trim($line);
1732 if ($line === '' || substr($line, 0, 2) === '#!') continue;
1733 // Allow partial and strict, but don't count decl files as Hack code
1734 if ($line === '<?hh' || $line === '<?hh //strict') return true;
1735 break;
1737 fclose($file);
1739 return false;
1742 function skip_test($options, $test) {
1743 if (isset($options['hack-only']) &&
1744 substr($test, -5) !== '.hhas' &&
1745 !is_hack_file($options, $test)) {
1746 return 'skip-hack-only';
1749 if (isset($options['cli-server']) && !can_run_server_test($test)) {
1750 return 'skip-server';
1753 $skipif_test = find_test_ext($test, 'skipif');
1754 if (!$skipif_test) {
1755 return false;
1758 // For now, run the .skipif in non-repo since building a repo for it is hard.
1759 $options_without_repo = $options;
1760 unset($options_without_repo['repo']);
1762 list($hhvm, $_) = hhvm_cmd($options_without_repo, $test, $skipif_test);
1763 if (is_array($hhvm)) $hhvm=$hhvm[0];
1765 $descriptorspec = array(
1766 0 => array("pipe", "r"),
1767 1 => array("pipe", "w"),
1768 2 => array("pipe", "w"),
1770 $pipes = null;
1771 $process = proc_open("$hhvm $test 2>&1", $descriptorspec, &$pipes);
1772 if (!is_resource($process)) {
1773 // This is weird. We can't run HHVM but we probably shouldn't skip the test
1774 // since on a broken build everything will show up as skipped and give you a
1775 // SHIPIT.
1776 return false;
1779 fclose($pipes[0]);
1780 $output = stream_get_contents($pipes[1]);
1781 fclose($pipes[1]);
1782 proc_close($process);
1784 // The standard php5 .skipif semantics is if the .skipif outputs ANYTHING
1785 // then it should be skipped. This is a poor design, but I'll just add a
1786 // small blacklist of things that are really bad if they are output so we
1787 // surface the errors in the tests themselves.
1788 if (stripos($output, 'segmentation fault') !== false) {
1789 return false;
1792 return strlen($output) === 0 ? false : 'skip-skipif';
1795 function comp_line($l1, $l2, $is_reg) {
1796 if ($is_reg) {
1797 return preg_match('/^'. $l1 . '$/s', $l2);
1798 } else {
1799 return !strcmp($l1, $l2);
1803 function count_array_diff($ar1, $ar2, $is_reg, $idx1, $idx2, $cnt1, $cnt2,
1804 $steps) {
1805 $equal = 0;
1807 while ($idx1 < $cnt1 && $idx2 < $cnt2 && comp_line($ar1[$idx1], $ar2[$idx2],
1808 $is_reg)) {
1809 $idx1++;
1810 $idx2++;
1811 $equal++;
1812 $steps--;
1814 if (--$steps > 0) {
1815 $eq1 = 0;
1816 $st = $steps / 2;
1818 for ($ofs1 = $idx1 + 1; $ofs1 < $cnt1 && $st-- > 0; $ofs1++) {
1819 $eq = @count_array_diff($ar1, $ar2, $is_reg, $ofs1, $idx2, $cnt1,
1820 $cnt2, $st);
1822 if ($eq > $eq1) {
1823 $eq1 = $eq;
1827 $eq2 = 0;
1828 $st = $steps;
1830 for ($ofs2 = $idx2 + 1; $ofs2 < $cnt2 && $st-- > 0; $ofs2++) {
1831 $eq = @count_array_diff($ar1, $ar2, $is_reg, $idx1, $ofs2, $cnt1, $cnt2, $st);
1832 if ($eq > $eq2) {
1833 $eq2 = $eq;
1837 if ($eq1 > $eq2) {
1838 $equal += $eq1;
1839 } else if ($eq2 > 0) {
1840 $equal += $eq2;
1844 return $equal;
1847 function generate_array_diff($ar1, $ar2, $is_reg, $w) {
1848 $idx1 = 0; $cnt1 = @count($ar1);
1849 $idx2 = 0; $cnt2 = @count($ar2);
1850 $old1 = array();
1851 $old2 = array();
1853 while ($idx1 < $cnt1 && $idx2 < $cnt2) {
1854 if (comp_line($ar1[$idx1], $ar2[$idx2], $is_reg)) {
1855 $idx1++;
1856 $idx2++;
1857 continue;
1858 } else {
1859 $c1 = @count_array_diff($ar1, $ar2, $is_reg, $idx1+1, $idx2, $cnt1,
1860 $cnt2, 10);
1861 $c2 = @count_array_diff($ar1, $ar2, $is_reg, $idx1, $idx2+1, $cnt1,
1862 $cnt2, 10);
1864 if ($c1 > $c2) {
1865 $old1[$idx1] = sprintf("%03d- ", $idx1+1) . $w[$idx1++];
1866 } else if ($c2 > 0) {
1867 $old2[$idx2] = sprintf("%03d+ ", $idx2+1) . $ar2[$idx2++];
1868 } else {
1869 $old1[$idx1] = sprintf("%03d- ", $idx1+1) . $w[$idx1++];
1870 $old2[$idx2] = sprintf("%03d+ ", $idx2+1) . $ar2[$idx2++];
1875 $diff = array();
1876 $old1_keys = array_keys($old1);
1877 $old2_keys = array_keys($old2);
1878 $old1_values = array_values($old1);
1879 $old2_values = array_values($old2);
1880 // these start at -2 so $l1 + 1 and $l2 + 1 are not valid indices
1881 $l1 = -2;
1882 $l2 = -2;
1883 $iter1 = 0; $end1 = count($old1);
1884 $iter2 = 0; $end2 = count($old2);
1886 while ($iter1 < $end1 || $iter2 < $end2) {
1887 $k1 = $iter1 < $end1 ? $old1_keys[$iter1] : null;
1888 $k2 = $iter2 < $end2 ? $old2_keys[$iter2] : null;
1889 if ($k1 == $l1 + 1 || $k2 === null) {
1890 $l1 = $k1;
1891 $diff[] = $old1_values[$iter1++];
1892 } else if ($k2 == $l2 + 1 || $k1 === null) {
1893 $l2 = $k2;
1894 $diff[] = $old2_values[$iter2++];
1895 } else if ($k1 < $k2) {
1896 $l1 = $k1;
1897 $diff[] = $old1_values[$iter1++];
1898 } else {
1899 $l2 = $k2;
1900 $diff[] = $old2_values[$iter2++];
1904 while ($idx1 < $cnt1) {
1905 $diff[] = sprintf("%03d- ", $idx1 + 1) . $w[$idx1++];
1908 while ($idx2 < $cnt2) {
1909 $diff[] = sprintf("%03d+ ", $idx2 + 1) . $ar2[$idx2++];
1912 return $diff;
1915 function generate_diff($wanted, $wanted_re, $output)
1917 $w = explode("\n", $wanted);
1918 $o = explode("\n", $output);
1919 if (is_null($wanted_re)) {
1920 $r = $w;
1921 } else {
1922 if (preg_match('/^\((.*)\)\{(\d+)\}$/s', $wanted_re, &$m)) {
1923 $t = explode("\n", $m[1]);
1924 $r = array();
1925 $w2 = array();
1926 for ($i = 0; $i < $m[2]; $i++) {
1927 foreach ($t as $v) {
1928 $r[] = $v;
1930 foreach ($w as $v) {
1931 $w2[] = $v;
1934 $w = $wanted === $wanted_re ? $r : $w2;
1935 } else {
1936 $r = explode("\n", $wanted_re);
1939 $diff = generate_array_diff($r, $o, !is_null($wanted_re), $w);
1941 return implode("\r\n", $diff);
1944 function dump_hhas_cmd($hhvm_cmd, $test, $hhas_file) {
1945 $dump_flags = implode(' ', array(
1946 '-vEval.AllowHhas=true',
1947 '-vEval.DumpHhas=1',
1948 '-vEval.DumpHhasToFile='.escapeshellarg($hhas_file),
1949 '-vEval.LoadFilepathFromUnitCache=0',
1951 $cmd = str_replace(' -- ', " $dump_flags -- ", $hhvm_cmd);
1952 if ($cmd == $hhvm_cmd) $cmd .= " $dump_flags";
1953 return $cmd;
1956 function dump_hhas_to_temp($hhvm_cmd, $test) {
1957 $temp_file = $test . '.round_trip.hhas';
1958 $cmd = dump_hhas_cmd($hhvm_cmd, $test, $temp_file);
1959 system("$cmd &> /dev/null", &$ret);
1960 return $ret === 0 ? $temp_file : false;
1963 const HHAS_EXT = '.hhas';
1964 function can_run_server_test($test) {
1965 return
1966 !is_file("$test.noserver") &&
1967 !find_test_ext($test, 'opts') &&
1968 !is_file("$test.ini") &&
1969 !is_file("$test.onlyrepo") &&
1970 !is_file("$test.use.for.ini.migration.testing.only.hdf") &&
1971 strpos($test, 'quick/debugger') === false &&
1972 strpos($test, 'quick/xenon') === false &&
1973 strpos($test, 'slow/streams/') === false &&
1974 strpos($test, 'slow/ext_mongo/') === false &&
1975 strpos($test, 'slow/ext_oauth/') === false &&
1976 strpos($test, 'slow/ext_vsdebug/') === false &&
1977 strpos($test, 'slow/ext_yaml/') === false &&
1978 strpos($test, 'slow/ext_xdebug/') === false &&
1979 strpos($test, 'slow/debugger/') === false &&
1980 strpos($test, 'slow/type_profiler/debugger/') === false &&
1981 strpos($test, 'zend/good/ext/standard/tests/array/') === false &&
1982 strpos($test, 'zend/good/ext/ftp') === false &&
1983 strrpos($test, HHAS_EXT) !== (strlen($test) - strlen(HHAS_EXT))
1987 const SERVER_TIMEOUT = 45;
1988 function run_config_server($options, $test) {
1989 if (!isset($options['server']) || !can_run_server_test($test)) {
1990 return null;
1993 $config = find_file_for_dir(dirname($test), 'config.ini');
1994 $port = $options['servers']['configs'][$config]->server['port'];
1995 $ch = curl_init("localhost:$port/$test");
1996 curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
1997 curl_setopt($ch, CURLOPT_TIMEOUT, SERVER_TIMEOUT);
1998 curl_setopt($ch, CURLOPT_BINARYTRANSFER, true);
1999 $output = curl_exec($ch);
2000 if ($output === false) {
2001 // The server probably crashed so fall back to cli to determine if this was
2002 // the test that caused the crash. Our parent process will see that the
2003 // server died and restart it.
2004 if (getenv('HHVM_TEST_SERVER_LOG')) {
2005 printf("Curl failed: %d\n", curl_errno($ch));
2007 return null;
2009 curl_close($ch);
2010 $output = trim($output);
2012 return array($output, '');
2015 function run_config_cli($options, $test, $cmd, $cmd_env) {
2016 if (isset($options['log']) && !isset($options['typechecker'])) {
2017 $cmd_env['TRACE'] = 'printir:1';
2018 $cmd_env['HPHP_TRACE_FILE'] = $test . '.log';
2021 $descriptorspec = array(
2022 0 => array("pipe", "r"),
2023 1 => array("pipe", "w"),
2024 2 => array("pipe", "w"),
2026 $pipes = null;
2027 if (isset($options['typechecker'])) {
2028 $process = proc_open(
2029 "$cmd 2>/dev/null", $descriptorspec, &$pipes, null, $cmd_env
2031 } else {
2032 $process = proc_open("$cmd 2>&1", $descriptorspec, &$pipes, null, $cmd_env);
2034 if (!is_resource($process)) {
2035 file_put_contents("$test.diff", "Couldn't invoke $cmd");
2036 return false;
2039 fclose($pipes[0]);
2040 $output = stream_get_contents($pipes[1]);
2041 $output = trim($output);
2042 $stderr = stream_get_contents($pipes[2]);
2043 fclose($pipes[1]);
2044 fclose($pipes[2]);
2045 proc_close($process);
2047 return array($output, $stderr);
2050 function replace_object_resource_ids($str, $replacement) {
2051 $str = preg_replace(
2052 '/(object\([^)]+\)#)\d+/', '\1'.$replacement, $str
2054 return preg_replace(
2055 '/resource\(\d+\)/', "resource($replacement)", $str
2059 function run_config_post($outputs, $test, $options) {
2060 $output = $outputs[0];
2061 $stderr = $outputs[1];
2062 file_put_contents("$test.out", $output);
2064 $error_ok = isset($options['repo']) && file_exists($test . '.hhbbc_assert');
2066 // hhvm redirects errors to stdout, so anything on stderr is really bad.
2067 if ($stderr && !$error_ok) {
2068 file_put_contents(
2069 "$test.diff",
2070 "Test failed because the process wrote on stderr:\n$stderr"
2072 return false;
2075 // Needed for testing non-hhvm binaries that don't actually run the code
2076 // e.g. parser/test/parse_tester.cpp.
2077 if ($output == "FORCE PASS") {
2078 return true;
2081 $repeats = 0;
2083 if (isset($options['relocate'])) {
2084 $repeats = $options['relocate'] * 2;
2087 if (isset($options['retranslate-all'])) {
2088 $repeats = $options['retranslate-all'] * 2;
2091 if (isset($options['recycle-tc'])) {
2092 $repeats = $options['recycle-tc'];
2095 if (isset($options['cli-server'])) {
2096 $repeats = 3;
2099 list($file, $type) = get_expect_file_and_type($test, $options);
2100 if ($file === null || $type === null) {
2101 file_put_contents(
2102 "$test.diff", "No $test.expect, $test.expectf, " .
2103 "$test.hhvm.expect, $test.hhvm.expectf, " .
2104 "$test.typechecker.expect, $test.typechecker.expectf, " .
2105 "nor $test.expectregex. If $test is meant to be included by other ".
2106 "tests, use a different file extension.\n"
2108 return false;
2111 $is_tc = isset($options['typechecker']);
2112 if ((!$is_tc && ($type === 'expect' || $type === 'hhvm.expect')) ||
2113 ($is_tc && $type === 'typechecker.expect')) {
2114 $wanted = trim(file_get_contents($file));
2115 if (isset($options['ignore-oids']) || isset($options['repo'])) {
2116 $output = replace_object_resource_ids($output, 'n');
2117 $wanted = replace_object_resource_ids($wanted, 'n');
2120 if (!$repeats) {
2121 $passed = !strcmp($output, $wanted);
2122 if (!$passed) {
2123 file_put_contents("$test.diff", generate_diff($wanted, null, $output));
2125 return $passed;
2127 $wanted_re = preg_quote($wanted, '/');
2128 } else if ((!$is_tc && ($type === 'expectf' || $type === 'hhvm.expectf')) ||
2129 ($is_tc && $type === 'typechecker.expectf')) {
2130 $wanted = trim(file_get_contents($file));
2131 if (isset($options['ignore-oids']) || isset($options['repo'])) {
2132 $wanted = replace_object_resource_ids($wanted, '%d');
2134 $wanted_re = $wanted;
2136 // do preg_quote, but miss out any %r delimited sections.
2137 $temp = "";
2138 $r = "%r";
2139 $startOffset = 0;
2140 $length = strlen($wanted_re);
2141 while ($startOffset < $length) {
2142 $start = strpos($wanted_re, $r, $startOffset);
2143 if ($start !== false) {
2144 // we have found a start tag.
2145 $end = strpos($wanted_re, $r, $start+2);
2146 if ($end === false) {
2147 // unbalanced tag, ignore it.
2148 $end = $start = $length;
2150 } else {
2151 // no more %r sections.
2152 $start = $end = $length;
2154 // quote a non re portion of the string.
2155 $temp = $temp.preg_quote(substr($wanted_re, $startOffset,
2156 ($start - $startOffset)), '/');
2157 // add the re unquoted.
2158 if ($end > $start) {
2159 $temp = $temp.'('.substr($wanted_re, $start+2, ($end - $start-2)).')';
2161 $startOffset = $end + 2;
2163 $wanted_re = $temp;
2165 $wanted_re = str_replace(
2166 array('%binary_string_optional%'),
2167 'string',
2168 $wanted_re
2170 $wanted_re = str_replace(
2171 array('%unicode_string_optional%'),
2172 'string',
2173 $wanted_re
2175 $wanted_re = str_replace(
2176 array('%unicode\|string%', '%string\|unicode%'),
2177 'string',
2178 $wanted_re
2180 $wanted_re = str_replace(
2181 array('%u\|b%', '%b\|u%'),
2183 $wanted_re
2185 // Stick to basics.
2186 $wanted_re = str_replace('%e', '\\' . DIRECTORY_SEPARATOR, $wanted_re);
2187 $wanted_re = str_replace('%s', '[^\r\n]+', $wanted_re);
2188 $wanted_re = str_replace('%S', '[^\r\n]*', $wanted_re);
2189 $wanted_re = str_replace('%a', '.+', $wanted_re);
2190 $wanted_re = str_replace('%A', '.*', $wanted_re);
2191 $wanted_re = str_replace('%w', '\s*', $wanted_re);
2192 $wanted_re = str_replace('%i', '[+-]?\d+', $wanted_re);
2193 $wanted_re = str_replace('%d', '\d+', $wanted_re);
2194 $wanted_re = str_replace('%x', '[0-9a-fA-F]+', $wanted_re);
2195 // %f allows two points "-.0.0" but that is the best *simple* expression.
2196 $wanted_re = str_replace('%f', '[+-]?\.?\d+\.?\d*(?:[Ee][+-]?\d+)?',
2197 $wanted_re);
2198 $wanted_re = str_replace('%c', '.', $wanted_re);
2199 // must be last.
2200 $wanted_re = str_replace('%%', '%%?', $wanted_re);
2202 // Normalize newlines.
2203 $wanted_re = preg_replace("/(\r\n?|\n)/", "\n", $wanted_re);
2204 $output = preg_replace("/(\r\n?|\n)/", "\n", $output);
2205 } else if (!$is_tc && $type === 'expectregex') {
2206 $wanted_re = trim(file_get_contents($file));
2207 } else {
2208 throw new Exception("Unsupported expect file type: ".$type);
2211 if ($repeats) {
2212 $wanted_re = "($wanted_re\s*)".'{'.$repeats.'}';
2214 if (!isset($wanted)) $wanted = $wanted_re;
2215 $passed = @preg_match("/^$wanted_re\$/s", $output);
2216 if ($passed === false && $repeats) {
2217 // $repeats can cause the regex to become too big, and fail
2218 // to compile.
2219 return 'skip-repeats-fail';
2221 if (!$passed) {
2222 $diff = generate_diff($wanted_re, $wanted_re, $output);
2223 if ($passed === false && $diff === "") {
2224 // the preg match failed, probably because the regex was too complex,
2225 // but since the line by line diff came up empty, we're fine
2226 $passed = 1;
2227 } else {
2228 file_put_contents("$test.diff", $diff);
2231 return $passed;
2234 function timeout_prefix() {
2235 if (is_executable('/usr/bin/timeout')) {
2236 return '/usr/bin/timeout ' . TIMEOUT_SECONDS . ' ';
2237 } else {
2238 return __DIR__.'/../tools/timeout.sh -t ' . TIMEOUT_SECONDS . ' ';
2242 function run_one_config($options, $test, $cmd, $cmd_env) {
2243 if (is_array($cmd)) {
2244 $result = 'skip-empty-cmd';
2245 foreach ($cmd as $c) {
2246 $result = run_one_config($options, $test, $c, $cmd_env);
2247 if (!$result) return $result;
2249 return $result;
2251 $cmd = timeout_prefix() . $cmd;
2252 $outputs = run_config_cli($options, $test, $cmd, $cmd_env);
2253 if ($outputs === false) return false;
2254 return run_config_post($outputs, $test, $options);
2257 function run_and_lock_test($options, $test) {
2258 $stime = time();
2259 $time = microtime(true);
2260 $failmsg = "";
2261 $status = false;
2262 $lock = fopen($test, 'r');
2263 if (!$lock || !flock($lock, LOCK_EX)) {
2264 $failmsg = "Failed to lock test";
2265 if ($lock) fclose($lock);
2266 $lock = null;
2267 } else {
2268 if (isset($options['typechecker'])) {
2269 $status = run_typechecker_test($options, $test);
2270 } else {
2271 $status = run_test($options, $test);
2274 $time = microtime(true) - $time;
2275 $etime = time();
2276 Status::setTestTime($time);
2277 if ($lock) {
2278 if ($status) {
2279 clean_intermediate_files($test, $options);
2280 } else if ($failmsg === '') {
2281 $failmsg = @file_get_contents("$test.diff");
2282 if (!$failmsg) $failmsg = "Test failed with empty diff";
2284 if (!flock($lock, LOCK_UN)) {
2285 if ($failmsg !== '') $failmsg .= "\n";
2286 $failmsg .= "Failed to release test lock";
2288 if (!fclose($lock)) {
2289 if ($failmsg !== '') $failmsg .= "\n";
2290 $failmsg .= "Failed to close lock file";
2293 if ($failmsg !== "") {
2294 Status::fail($test, $time, $stime, $etime, $failmsg);
2295 } else if (is_string($status) && substr($status, 0, 4) === 'skip') {
2296 if (strlen($status) > 5 && substr($status, 0, 5) === 'skip-') {
2297 Status::skip($test, substr($status, 5), $time, $stime, $etime);
2298 } else {
2299 Status::fail($test, $time, $stime, $etime, "invalid skip status $status");
2301 } else if ($status) {
2302 Status::pass($test, $status, $time, $stime, $etime);
2303 } else {
2304 Status::fail($test, $time, $stime, $etime, "Unknown failure");
2308 function run_typechecker_test($options, $test) {
2309 $skip_reason = skip_test($options, $test);
2310 if ($skip_reason !== false) return $skip_reason;
2311 if (!file_exists($test . ".hhconfig")) return 'skip-no-hhconfig';
2312 list($hh_server, $hh_server_env, $temp_dir) = hh_server_cmd($options, $test);
2313 $result = run_one_config($options, $test, $hh_server, $hh_server_env);
2314 // Remove the temporary directory.
2315 if (!isset($options['no-clean'])) {
2316 shell_exec('rm -rf ' . $temp_dir);
2318 return $result;
2321 function run_test($options, $test) {
2322 $skip_reason = skip_test($options, $test);
2323 if ($skip_reason !== false) return $skip_reason;
2325 // Skip tests that don't make sense in modes where we dump/compare hhas
2326 $no_hhas_tag = '.nodumphhas';
2327 $no_hhas = file_exists($test.$no_hhas_tag) ||
2328 file_exists(dirname($test).'/'.$no_hhas_tag);
2329 if ($no_hhas && (
2330 isset($options['hhbbc2']) ||
2331 isset($options['hhas-round-trip'])
2332 )) {
2333 return 'skip-nodumphhas';
2336 list($hhvm, $hhvm_env) = hhvm_cmd($options, $test);
2337 if (has_multi_request_mode($options)) {
2338 if (isset($options['jit-serialize']) || isset($options['cli-server'])) {
2339 if (preg_grep('/ --count[ =][0-9]+ /', (array)$hhvm)) {
2340 return 'skip-count';
2342 } else {
2343 if (preg_grep('/ --count[ =][0-9]+ .* --count[ =][0-9]+ /',
2344 (array)$hhvm)) {
2345 return 'skip-count';
2350 if (file_exists($test . ".verify") && (has_multi_request_mode($options) ||
2351 isset($options['repo']))) {
2352 return 'skip-verify';
2355 if (isset($options['repo'])) {
2356 if (preg_grep('/-m debug/', (array)$hhvm) || file_exists($test.'.norepo')) {
2357 return 'skip-norepo';
2360 $hphp_hhvm_repo = "$test.repo/hhvm.hhbc";
2361 $hhbbc_hhvm_repo = "$test.repo/hhvm.hhbbc";
2362 $hphp_hackc_repo = "$test.repo/hackc.hhbc";
2363 $hhbbc_hackc_repo = "$test.repo/hackc.hhbbc";
2364 shell_exec("rm -f \"$hphp_hhvm_repo\" \"$hhbbc_hhvm_repo\" \"$hphp_hackc_repo\" \"$hhbbc_hackc_repo\" ");
2366 $program = isset($options['hackc']) ? "hackc" : "hhvm";
2368 if (file_exists($test . '.hhbbc_assert')) {
2369 $hhvm = hphp_cmd($options, $test, $program);
2370 if (repo_separate($options, $test)) {
2371 $result = exec_with_stack($hhvm);
2372 if ($result !== true) return false;
2373 $hhvm = hhbbc_cmd($options, $test, $program);
2375 return run_one_config($options, $test, $hhvm, $hhvm_env);
2378 if (!repo_mode_compile($options, $test, $program)) {
2379 return false;
2382 if (isset($options['hhbbc2'])) {
2383 $hhas_temp1 = dump_hhas_to_temp($hhvm, "$test.before");
2384 if ($hhas_temp1 === false) {
2385 file_put_contents(
2386 "$test.diff",
2387 "dumping hhas after first hhbbc pass failed"
2389 return false;
2391 shell_exec("mv $test.repo/$program.hhbbc $test.repo/$program.hhbc");
2392 $hhbbc = hhbbc_cmd($options, $test, $program);
2393 $result = exec_with_stack($hhbbc);
2394 if ($result !== true) {
2395 file_put_contents("$test.diff", $result);
2396 return false;
2398 $hhas_temp2 = dump_hhas_to_temp($hhvm, "$test.after");
2399 if ($hhas_temp2 === false) {
2400 file_put_contents(
2401 "$test.diff",
2402 "dumping hhas after second hhbbc pass failed"
2404 return false;
2406 $diff = shell_exec("diff $hhas_temp1 $hhas_temp2 | wc -l");
2407 if (trim($diff) != '0') {
2408 shell_exec("diff $hhas_temp1 $hhas_temp2 > $test.diff");
2409 return false;
2413 if (isset($options['jit-serialize'])) {
2414 $cmd = timeout_prefix() .
2415 jit_serialize_option($hhvm, $test, $options, true);
2416 $outputs = run_config_cli($options, $test, $cmd, $hhvm_env);
2417 if ($outputs === false) return false;
2418 $hhvm = jit_serialize_option($hhvm, $test, $options, false);
2421 return run_one_config($options, $test, $hhvm, $hhvm_env);
2424 if (file_exists($test.'.onlyrepo')) {
2425 return 'skip-onlyrepo';
2428 if (isset($options['hhas-round-trip'])) {
2429 if (substr($test, -5) === ".hhas") return 'skip-hhas';
2430 $hhas_temp = dump_hhas_to_temp($hhvm, $test);
2431 if ($hhas_temp === false) {
2432 $err = "system failed: " .
2433 dump_hhas_cmd($hhvm, $test, $test.'.round_trip.hhas') .
2434 "\n";
2435 file_put_contents("$test.diff", $err);
2436 return false;
2438 list($hhvm, $hhvm_env) = hhvm_cmd($options, $test, $hhas_temp);
2441 if ($outputs = run_config_server($options, $test)) {
2442 return run_config_post($outputs, $test, $options) ? 'pass-server'
2443 : (run_one_config($options, $test, $hhvm, $hhvm_env) ? 'skip-server'
2444 : false);
2446 return run_one_config($options, $test, $hhvm, $hhvm_env);
2449 function num_cpus() {
2450 switch(PHP_OS) {
2451 case 'Linux':
2452 $data = file('/proc/stat');
2453 $cores = 0;
2454 foreach($data as $line) {
2455 if (preg_match('/^cpu[0-9]/', $line)) {
2456 $cores++;
2459 return $cores;
2460 case 'Darwin':
2461 case 'FreeBSD':
2462 return exec('sysctl -n hw.ncpu');
2464 return 2; // default when we don't know how to detect.
2467 function make_header($str) {
2468 return "\n\033[0;33m".$str."\033[0m\n";
2471 function print_commands($tests, $options) {
2472 print make_header("Run these by hand:");
2474 foreach ($tests as $test) {
2475 if (isset($options['typechecker'])) {
2476 list($command, $_, ) = hh_server_cmd($options, $test);
2477 } else {
2478 list($command, $_) = hhvm_cmd($options, $test);
2480 if (!isset($options['repo'])) {
2481 foreach ((array)$command as $c) {
2482 print "$c\n";
2484 continue;
2487 // How to run it with hhbbc:
2488 $program = isset($options['hackc']) ? "hackc" : "hhvm";
2489 $hhbbc_cmds = hphp_cmd($options, $test, $program)."\n";
2490 if (repo_separate($options, $test)) {
2491 $hhbbc_cmd = hhbbc_cmd($options, $test, $program)."\n";
2492 $hhbbc_cmds .= $hhbbc_cmd;
2493 if (isset($options['hhbbc2'])) {
2494 foreach ((array)$command as $c) {
2495 $hhbbc_cmds .=
2496 $c." -vEval.DumpHhas=1 > $test.before.round_trip.hhas\n";
2498 $hhbbc_cmds .=
2499 "mv $test.repo/$program.hhbbc $test.repo/$program.hhbc\n";
2500 $hhbbc_cmds .= $hhbbc_cmd;
2501 foreach ((array)$command as $c) {
2502 $hhbbc_cmds .=
2503 $c." -vEval.DumpHhas=1 > $test.after.round_trip.hhas\n";
2505 $hhbbc_cmds .=
2506 "diff $test.before.round_trip.hhas $test.after.round_trip.hhas\n";
2509 if (isset($options['jit-serialize'])) {
2510 $hhbbc_cmds .=
2511 jit_serialize_option($command, $test, $options, true) . "\n";
2512 $command = jit_serialize_option($command, $test, $options, false);
2514 foreach ((array)$command as $c) {
2515 $hhbbc_cmds .= $c."\n";
2517 print "$hhbbc_cmds\n";
2521 function msg_loop($num_tests, $queue) {
2522 $do_progress =
2524 Status::getMode() === Status::MODE_NORMAL ||
2525 Status::getMode() === Status::MODE_RECORD_FAILURES
2526 ) &&
2527 Status::hasCursorControl();
2529 if ($do_progress) {
2530 $stty = strtolower(Status::getSTTY());
2531 preg_match_all("/columns ([0-9]+);/", $stty, &$output);
2532 if (!isset($output[1][0])) {
2533 // because BSD has to be different
2534 preg_match_all("/([0-9]+) columns;/", $stty, &$output);
2536 if (!isset($output[1][0])) {
2537 $do_progress = false;
2538 } else {
2539 $cols = $output[1][0];
2543 while (true) {
2544 if (!msg_receive($queue, 0, &$type, 1024, &$message)) {
2545 error("msg_receive failed");
2548 if (!Status::handle_message($type, $message)) break;
2550 if ($do_progress) {
2551 $total_run = (Status::$skipped + Status::$failed + Status::$passed);
2552 $bar_cols = ($cols - 45);
2554 $passed_ticks = round($bar_cols * (Status::$passed / $num_tests));
2555 $skipped_ticks = round($bar_cols * (Status::$skipped / $num_tests));
2556 $failed_ticks = round($bar_cols * (Status::$failed / $num_tests));
2558 $fill = $bar_cols - ($passed_ticks + $skipped_ticks + $failed_ticks);
2559 if ($fill < 0) $fill = 0;
2561 $fill = str_repeat('-', (int)$fill);
2563 $passed_ticks = str_repeat('#', (int)$passed_ticks);
2564 $skipped_ticks = str_repeat('#', (int)$skipped_ticks);
2565 $failed_ticks = str_repeat('#', (int)$failed_ticks);
2567 echo
2568 "\033[2K\033[1G[",
2569 "\033[0;32m$passed_ticks",
2570 "\033[33m$skipped_ticks",
2571 "\033[31m$failed_ticks",
2572 "\033[0m$fill] ($total_run/$num_tests) ",
2573 "(", Status::$skipped, " skipped,", Status::$failed, " failed)";
2577 if ($do_progress) {
2578 print "\033[2K\033[1G";
2579 if (Status::$skipped > 0) {
2580 print Status::$skipped ." tests \033[1;33mskipped\033[0m\n";
2581 $reasons = Status::$skip_reasons;
2582 arsort(&$reasons);
2583 Status::$skip_reasons = $reasons;
2584 foreach (Status::$skip_reasons as $reason => $count) {
2585 printf("%12s: %d\n", $reason, $count);
2591 function print_success($tests, $results, $options) {
2592 // We didn't run any tests, not even skipped. Clowntown!
2593 if (!$tests) {
2594 print "\nCLOWNTOWN: No tests!\n";
2595 if (!($options['no-fun'] ?? false)) {
2596 print <<<CLOWN
2599 /*\\
2600 /_*_\\
2601 {('o')}
2602 C{{([^*^])}}D
2603 [ * ]
2604 / Y \\
2605 _\\__|__/_
2606 (___/ \\___)
2607 CLOWN
2608 ."\n\n";
2611 /* Emacs' syntax highlighting gets confused by that clown and this comment
2612 * resets whatever state got messed up. */
2613 return;
2615 $ran_tests = false;
2616 foreach ($results as $result) {
2617 // The result here will either be skipped or passed (since failed is
2618 // handled in print_failure.
2619 if ($result['status'] == 'passed') {
2620 $ran_tests = true;
2621 break;
2624 // We just had skipped tests
2625 if (!$ran_tests) {
2626 print "\nSKIP-ALOO: Only skipped tests!\n";
2627 if (!($options['no-fun'] ?? false)) {
2628 print <<<SKIPPER
2632 / ,"
2633 .-------.--- /
2634 "._ __.-/ o. o\
2635 " ( Y )
2639 .-" |
2640 / _ \ \
2641 / `. ". ) /' )
2642 Y )( / /(,/
2643 ,| / )
2644 ( | / /
2645 " \_ (__ (__
2646 "-._,)--._,)
2647 SKIPPER
2648 ."\n\n";
2651 /* Emacs' syntax highlighting may get confused by the skipper and this
2652 * rcomment esets whatever state got messed up. */
2653 return;
2655 print "\nAll tests passed.\n";
2656 if (!($options['no-fun'] ?? false)) {
2657 print <<<SHIP
2658 | | |
2659 )_) )_) )_)
2660 )___))___))___)\
2661 )____)____)_____)\\
2662 _____|____|____|____\\\__
2663 ---------\ SHIP IT /---------
2664 ^^^^^ ^^^^^^^^^^^^^^^^^^^^^
2665 ^^^^ ^^^^ ^^^ ^^
2666 ^^^^ ^^^
2667 SHIP
2668 ."\n";
2670 if ($options['failure-file'] ?? false) {
2671 @unlink($options['failure-file']);
2673 if (isset($options['verbose'])) {
2674 print_commands($tests, $options);
2678 function aggregate_srcloc_info($tests) {
2679 $all_missing_on_lhs = [];
2680 $all_missing_on_rhs = [];
2681 $all_mismatched_srcloc = [];
2683 foreach ($tests as $test) {
2684 $file = "$test.diff";
2685 $json_content = file_get_contents($file);
2686 $srcloc_info = json_decode($json_content, true);
2688 $mismatched_srcloc = $srcloc_info['mismatched_srcloc'];
2689 foreach ($mismatched_srcloc as $instr) {
2690 $all_mismatched_srcloc[] = $instr;
2692 $all_mismatched_srcloc = array_unique($all_mismatched_srcloc);
2693 sort(&$all_mismatched_srcloc);
2695 $missing_on_lhs_srcloc = $srcloc_info['missing_on_lhs_srcloc'];
2696 foreach ($missing_on_lhs_srcloc as $instr) {
2697 $all_missing_on_lhs[] = $instr;
2699 $all_missing_on_lhs = array_unique($all_missing_on_lhs);
2700 sort(&$all_missing_on_lhs);
2702 $missing_on_rhs_srcloc = $srcloc_info['missing_on_rhs_srcloc'];
2703 foreach ($missing_on_rhs_srcloc as $instr) {
2704 $all_missing_on_rhs[] = $instr;
2706 $all_missing_on_rhs = array_unique($all_missing_on_rhs);
2707 sort(&$all_missing_on_rhs);
2710 print "\nInstructions with mismatched srcloc:\n";
2711 print " - " . implode(",\n - ", $all_mismatched_srcloc) . "\n\n";
2712 print "Instructions with missing srcloc in HackC:\n";
2713 print " - " . implode(",\n - ", $all_missing_on_lhs) . "\n\n";
2714 print "Instructions with missing srcloc in HHVM:\n";
2715 print " - " . implode(",\n - ", $all_missing_on_rhs) . "\n\n";
2716 print "\n";
2719 function print_failure($argv, $results, $options) {
2720 $failed = array();
2721 $passed = array();
2722 foreach ($results as $result) {
2723 if ($result['status'] === 'failed') {
2724 $failed[] = $result['name'];
2726 if ($result['status'] === 'passed') {
2727 $passed[] = $result['name'];
2730 asort(&$failed);
2731 print "\n".count($failed)." tests failed\n";
2732 if (!($options['no-fun'] ?? false)) {
2733 // Unicode for table-flipping emoticon
2734 print "(\u{256F}\u{00B0}\u{25A1}\u{00B0}\u{FF09}\u{256F}\u{FE35} \u{253B}";
2735 print "\u{2501}\u{253B}\n";
2736 // TODO: Google indicates that this is some old emoji-thing relating to
2737 // table flipping... Maybe replace to stop other people spending time
2738 // trying to decipher it?
2739 // https://knowyourmeme.com/memes/flipping-tables
2742 if (isset($options['srcloc'])) {
2743 aggregate_srcloc_info ($failed);
2744 } else {
2745 print make_header("See the diffs:").
2746 implode("\n", array_map(
2747 function($test) { return 'cat '.$test.'.diff'; },
2748 $failed))."\n";
2750 $failing_tests_file = ($options['failure-file'] ?? false)
2751 ? $options['failure-file']
2752 : tempnam('/tmp', 'test-failures');
2753 file_put_contents($failing_tests_file, implode("\n", $failed)."\n");
2754 print make_header('For xargs, list of failures is available using:').
2755 'cat '.$failing_tests_file."\n";
2757 if ($passed ?? false) {
2758 $passing_tests_file = ($options['success-file'] ?? false)
2759 ? $options['success-file']
2760 : tempnam('/tmp', 'tests-passed');
2761 file_put_contents($passing_tests_file, implode("\n", $passed)."\n");
2762 print make_header('For xargs, list of passed tests is available using:').
2763 'cat '.$passing_tests_file."\n";
2766 print_commands($failed, $options);
2768 print
2769 make_header("Re-run just the failing tests:") .
2770 str_replace("run.php", "run", $argv[0]) . ' ' .
2771 implode(' ', $GLOBALS['recorded_options']) .
2772 sprintf(' $(cat %s)%s', $failing_tests_file, "\n");
2776 function port_is_listening($port) {
2777 $socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
2778 return @socket_connect($socket, 'localhost', $port);
2781 function find_open_port() {
2782 for ($i = 0; $i < 50; ++$i) {
2783 $port = rand(1024, 65535);
2784 if (!port_is_listening($port)) return $port;
2787 error("Couldn't find an open port");
2790 function start_server_proc($options, $config, $port) {
2791 if (isset($options['cli-server'])) {
2792 $cli_sock = tempnam(sys_get_temp_dir(), 'hhvm-cli-');
2793 } else {
2794 // still want to test that an unwritable socket works...
2795 $cli_sock = '/var/run/hhvm-cli.sock';
2797 $threads = $options['threads'];
2798 $thread_option = isset($options['cli-server'])
2799 ? '-vEval.UnixServerWorkers='.$threads
2800 : '-vServer.ThreadCount='.$threads;
2801 $command = hhvm_cmd_impl(
2802 $options,
2803 $config,
2804 '-m', 'server',
2805 "-vServer.Port=$port",
2806 "-vServer.Type=proxygen",
2807 "-vAdminServer.Port=0",
2808 $thread_option,
2809 '-vServer.ExitOnBindFail=1',
2810 '-vServer.RequestTimeoutSeconds='.SERVER_TIMEOUT,
2811 '-vPageletServer.ThreadCount=0',
2812 '-vLog.UseRequestLog=1',
2813 '-vLog.File=/dev/null',
2815 // The server will unlink the temp file
2816 '-vEval.UnixServerPath='.$cli_sock,
2818 // This ensures we actually jit everything:
2819 '-vEval.JitRequireWriteLease=1',
2821 // The default test config uses a small TC but we'll be running thousands
2822 // of tests against the same process:
2823 '-vEval.JitASize=142606336',
2824 '-vEval.JitAProfSize=251658240',
2825 '-vEval.JitAColdSize=201326592',
2826 '-vEval.JitAFrozenSize=251658240',
2827 '-vEval.JitGlobalDataSize=32000000',
2829 // load/store counters don't work on Ivy Bridge so disable for tests
2830 '-vEval.ProfileHWEnable=false'
2832 if (is_array($command)) {
2833 error("Can't run multi-mode tests in server mode");
2835 if (getenv('HHVM_TEST_SERVER_LOG')) {
2836 echo "Starting server '$command'\n";
2839 $descriptors = array(
2840 0 => array('file', '/dev/null', 'r'),
2841 1 => array('file', '/dev/null', 'w'),
2842 2 => array('file', '/dev/null', 'w'),
2845 $proc = proc_open($command, $descriptors, &$dummy);
2846 if (!$proc) {
2847 error("Failed to start server process");
2849 $status = proc_get_status($proc);
2850 $status['proc'] = $proc;
2851 $status['port'] = $port;
2852 $status['config'] = $config;
2853 $status['cli-socket'] = $cli_sock;
2854 return $status;
2857 final class ServerRef {
2858 public function __construct(public $server) {
2863 * For each config file in $configs, start up a server on a randomly-determined
2864 * port. Return value is an array mapping pids and config files to arrays of
2865 * information about the server.
2867 function start_servers($options, $configs) {
2868 $starting = array();
2869 foreach ($configs as $config) {
2870 $starting[] = start_server_proc($options, $config, find_open_port());
2873 $start_time = microtime(true);
2874 $servers = array('pids' => array(), 'configs' => array());
2876 // Wait for all servers to come up.
2877 while (count($starting) > 0) {
2878 $still_starting = array();
2880 foreach ($starting as $server) {
2881 $new_status = proc_get_status($server['proc']);
2883 if (!$new_status['running']) {
2884 if ($new_status['exitcode'] === 0) {
2885 error("Server exited prematurely but without error");
2888 // We lost a race. Try another port.
2889 if (getenv('HHVM_TEST_SERVER_LOG')) {
2890 echo "\n\nLost connection race on port $port. Trying another.\n\n";
2892 $still_starting[] =
2893 start_server_proc($options, $server['config'], find_open_port());
2894 } else if (!port_is_listening($server['port'])) {
2895 $still_starting[] = $server;
2896 } else {
2897 $ref = new ServerRef($server);
2898 $servers['pids'][$server['pid']] = $ref;
2899 $servers['configs'][$server['config']] = $ref;
2903 $starting = $still_starting;
2904 $max_time = 10;
2905 if (microtime(true) - $start_time > $max_time) {
2906 error("Servers took more than $max_time seconds to come up");
2909 // Take a short nap and try again.
2910 usleep(100000);
2913 $elapsed = microtime(true) - $start_time;
2914 printf("Started %d servers in %.1f seconds\n\n", count($configs), $elapsed);
2915 return $servers;
2918 function drain_queue($queue) {
2919 while (@msg_receive($queue, 0, &$type, 1024, &$message, true,
2920 MSG_IPC_NOWAIT | MSG_NOERROR));
2923 function get_num_threads($options, $tests) {
2924 if ($options['typechecker'] ?? false) {
2925 // hh_server spawns a child per CPU; things get flakey with CPU^2 forks
2926 return 1;
2928 $cpus = isset($options['server']) || isset($options['cli-server'])
2929 ? num_cpus() * 2 : num_cpus();
2930 return min(count($tests), idx($options, 'threads', $cpus));
2933 function runner_precheck() {
2934 // basic checking for runner.
2935 if (!((bool)$_SERVER ?? false) || !((bool)$_ENV ?? false)) {
2936 echo "Warning: \$_SERVER/\$_ENV variables not available, please check \n" .
2937 "your ini setting: variables_order, it should have both 'E' and 'S'\n";
2941 function main($argv) {
2942 runner_precheck();
2944 ini_set('pcre.backtrack_limit', PHP_INT_MAX);
2946 list($options, $files) = get_options($argv);
2947 if (isset($options['help'])) {
2948 error(help());
2950 if (isset($options['list-tests'])) {
2951 error(list_tests($files, $options));
2954 $tests = find_tests($files, $options);
2955 if (isset($options['shuffle'])) {
2956 shuffle($tests);
2959 if (isset($options['repo']) && isset($options['typechecker'])) {
2960 error("Repo mode and typechecker mode are not compatible");
2963 if (isset($options['hhvm-binary-path']) &&
2964 isset($options['typechecker'])) {
2965 error("Did you mean to set the hh_server binary path instead?");
2968 if (isset($options['hhserver-binary-path']) &&
2969 !isset($options['typechecker'])) {
2970 error("hh_server binary path set, but not --typechecker");
2973 if (isset($options['hhvm-binary-path']) &&
2974 isset($options['hhserver-binary-path'])) {
2975 error("Need to choose one of the two binaries to run");
2978 $binary_path = "";
2979 $typechecker = false;
2980 if (isset($options['hhvm-binary-path'])) {
2981 check_executable($options['hhvm-binary-path'], false);
2982 $binary_path = realpath($options['hhvm-binary-path']);
2983 putenv("HHVM_BIN=" . $binary_path);
2984 } else if (isset($options['hhserver-binary-path'])) {
2985 check_executable($options['hhserver-binary-path'], true);
2986 $binary_path = realpath($options['hhserver-binary-path']);
2987 $typechecker = true;
2988 putenv("HH_SERVER_BIN=" . $binary_path);
2989 } else if (isset($options['typechecker'])) {
2990 $typechecker = true;
2993 // Explicit path given by --hhvm-binary-path or --hhserver-binary-path
2994 // takes priority (see above)
2995 // Then, if an HHVM_BIN or HH_SERVER env var exists, and the file it
2996 // points to exists, that trumps any default hhvm / typechecker executable
2997 // path.
2998 if ($binary_path === "") {
2999 if (!$typechecker) {
3000 if (getenv("HHVM_BIN") !== false) {
3001 $binary_path = realpath(getenv("HHVM_BIN"));
3002 check_executable($binary_path, false);
3003 } else {
3004 check_for_multiple_default_binaries(false);
3005 $binary_path = hhvm_path();
3007 } else {
3008 if (getenv("HH_SERVER_BIN") !== false) {
3009 $binary_path = realpath(getenv("HH_SERVER_BIN"));
3010 check_executable($binary_path, true);
3011 } else {
3012 check_for_multiple_default_binaries(true);
3013 $binary_path = hh_server_path();
3018 if (isset($options['verbose'])) {
3019 print "You are using the binary located at: " . $binary_path . "\n";
3022 $options['threads'] = get_num_threads($options, $tests);
3024 $servers = null;
3025 if (isset($options['server']) || isset($options['cli-server'])) {
3026 if (isset($options['server']) && isset($options['cli-server'])) {
3027 error("Server mode and CLI Server mode are mutually exclusive");
3029 if (isset($options['repo']) || isset($options['typechecker'])) {
3030 error("Server mode repo tests are not supported");
3032 $configs = array();
3034 /* We need to start up a separate server process for each config file
3035 * found. */
3036 foreach ($tests as $test) {
3037 if (!can_run_server_test($test)) continue;
3038 $config = find_file_for_dir(dirname($test), 'config.ini');
3039 if (!$config) {
3040 error("Couldn't find config file for $test");
3042 $configs[$config] = $config;
3045 $max_configs = 30;
3046 if (count($configs) > $max_configs) {
3047 error("More than $max_configs unique config files will be needed to run ".
3048 "the tests you specified. They may not be a good fit for server ".
3049 "mode. (".count($configs)." required)");
3052 $servers = $options['servers'] = start_servers($options, $configs);
3055 // Try to construct the buckets so the test results are ready in
3056 // approximately alphabetical order.
3057 $test_buckets = array();
3058 $i = 0;
3060 // Get the serial tests to be in their own bucket later.
3061 $serial_tests = serial_only_tests($tests);
3063 // If we have no serial tests, we can use the maximum number of allowed
3064 // threads for the test running. If we have some, we save one thread for
3065 // the serial bucket.
3066 $parallel_threads = count($serial_tests) > 0
3067 ? $options['threads'] - 1
3068 : $options['threads'];
3070 foreach ($tests as $test) {
3071 if (!in_array($test, $serial_tests)) {
3072 if (!array_key_exists($i, $test_buckets)) {
3073 $test_buckets[$i] = array();
3075 $test_buckets[$i][] = $test;
3076 $i = ($i + 1) % $parallel_threads;
3080 if (count($serial_tests) > 0) {
3081 // The last bucket is serial.
3082 // If the number of parallel tests didn't equal the actual number of
3083 // parallel threads because the number of serial tests reduced it enough,
3084 // then our next bucket is just $i; otherwise it is final available
3085 // thread. For example, we have 13 total tests which initially gave us
3086 // 13 parallel threads, but then we find out 3 are serial, so the parallel
3087 // tests would only fill 9 parallel buckets (< 12). The next one would be
3088 // 10 for the 3 serial. Now if we have 40 total tests which gave us 32
3089 // parallel threads and 4 serial tests, then all of possible parallel
3090 // buckets (31) would be filled regardless; so the serial bucket is what
3091 // would have been the last parallel thread (32).
3092 // $i got bumped to the next bucket at the end of the parallel test loop
3093 // above, so no $i++ here.
3094 $i = count($tests) - count($serial_tests) < $parallel_threads
3095 ? $i // we didn't fill all the parallel buckets, so use next one in line
3096 : $options['threads'] - 1; // all parallel filled; last thread; 0 indexed
3097 foreach ($serial_tests as $test) {
3098 if (!array_key_exists($i, $test_buckets)) {
3099 $test_buckets[$i] = array();
3101 $test_buckets[$i][] = $test;
3105 // If our total number of test buckets didn't overflow back to 0 above
3106 // when we % against the number of threads (because we didn't have that
3107 // many tests for this run), then just set the threads to how many
3108 // buckets we actually have to make calculations below correct.
3109 if (count($test_buckets) < $options['threads']) {
3110 $options['threads'] = count($test_buckets);
3113 // Remember that the serial tests are also in the tests array too,
3114 // so they are part of the total count.
3115 if (!isset($options['testpilot'])) {
3116 print "Running ".count($tests)." tests in ".
3117 $options['threads']." threads (" . count($serial_tests) .
3118 " in serial)\n";
3121 if (isset($options['verbose'])) {
3122 Status::setMode(Status::MODE_VERBOSE);
3124 if (isset($options['testpilot'])) {
3125 Status::setMode(Status::MODE_TESTPILOT);
3127 if (isset($options['record-failures'])) {
3128 Status::setMode(Status::MODE_RECORD_FAILURES);
3130 Status::setUseColor(isset($options['color']) ? true : posix_isatty(STDOUT));
3132 Status::$key = rand();
3133 Status::$nofork = count($tests) == 1 && !$servers;
3134 if (!Status::$nofork) {
3135 $queue = Status::getQueue();
3136 drain_queue($queue);
3139 Status::started();
3140 $_ENV['HPHP_TEST_TMPDIR'] = Status::getTestTmpDir();
3142 // Spawn off worker threads.
3143 $children = array();
3144 // A poor man's shared memory.
3145 $bad_test_files = array();
3146 if (Status::$nofork) {
3147 $bad_test_file = tempnam('/tmp', 'test-run-');
3148 $bad_test_files[] = $bad_test_file;
3149 $return_value = run($options, $test_buckets[$i], $bad_test_file);
3150 } else {
3151 for ($i = 0; $i < $options['threads']; $i++) {
3152 $bad_test_file = tempnam('/tmp', 'test-run-');
3153 $bad_test_files[] = $bad_test_file;
3154 $pid = pcntl_fork();
3155 if ($pid == -1) {
3156 error('could not fork');
3157 } else if ($pid) {
3158 $children[$pid] = $pid;
3159 } else {
3160 exit(run($options, $test_buckets[$i], $bad_test_file));
3164 // Fork off a child to receive messages and print status, and have the parent
3165 // wait for all children to exit.
3166 $printer_pid = pcntl_fork();
3167 if ($printer_pid == -1) {
3168 error("failed to fork");
3169 } else if ($printer_pid == 0) {
3170 msg_loop(count($tests), $queue);
3171 return 0;
3174 // In case we exit in a crazy way, have the parent blow up the queue.
3175 // Do this here so no children inherit this.
3176 $kill_queue = function() { Status::killQueue(); };
3177 register_shutdown_function($kill_queue);
3178 pcntl_signal(SIGTERM, $kill_queue);
3179 pcntl_signal(SIGINT, $kill_queue);
3181 $return_value = 0;
3182 while (count($children) && $printer_pid != 0) {
3183 $pid = pcntl_wait(&$status);
3184 if (!pcntl_wifexited($status) && !pcntl_wifsignaled($status)) {
3185 error("Unexpected exit status from child");
3188 if ($pid == $printer_pid) {
3189 // We should be finishing up soon.
3190 $printer_pid = 0;
3191 } else if ($servers && isset($servers['pids'][$pid])) {
3192 // A server crashed. Restart it.
3193 if (getenv('HHVM_TEST_SERVER_LOG')) {
3194 echo "\nServer $pid crashed. Restarting.\n";
3196 Status::serverRestarted();
3197 $ref = $servers['pids'][$pid];
3198 $ref->server =
3199 start_server_proc($options, $ref->server['config'], $ref->server['port']);
3201 // Unset the old $pid entry and insert the new one.
3202 unset($servers['pids'][$pid]);
3203 $servers['pids'][$ref->server['pid']] = $ref;
3204 } elseif (isset($children[$pid])) {
3205 unset($children[$pid]);
3206 $return_value |= pcntl_wexitstatus($status);
3207 } // Else, ignorable signal
3211 Status::finished();
3213 // Kill the server.
3214 if ($servers) {
3215 foreach ($servers['pids'] as $ref) {
3216 proc_terminate($ref->server['proc']);
3217 proc_close($ref->server['proc']);
3221 // aggregate results
3222 $results = array();
3223 foreach ($bad_test_files as $bad_test_file) {
3224 $json = json_decode(file_get_contents($bad_test_file), true);
3225 if (!is_array($json)) {
3226 error(
3227 "\nNo JSON output was received from a test thread. ".
3228 "Either you killed it, or it might be a bug in the test script."
3231 $results = array_merge($results, $json);
3232 unlink($bad_test_file);
3235 // print results
3236 if (isset($options['record-failures'])) {
3237 $fail_file = $options['record-failures'];
3238 $failed_tests = array();
3239 $prev_failing = array();
3240 if (file_exists($fail_file)) {
3241 $prev_failing = explode("\n", file_get_contents($fail_file));
3244 $new_fails = 0;
3245 $new_passes = 0;
3246 foreach ($results as $r) {
3247 if (!isset($r['name']) || !isset($r['status'])) continue;
3248 $test = canonical_path($r['name']);
3249 $status = $r['status'];
3250 if ($status === 'passed' && in_array($test, $prev_failing)) {
3251 $new_passes++;
3252 continue;
3254 if ($status !== 'failed') continue;
3255 if (!in_array($test, $prev_failing)) $new_fails++;
3256 $failed_tests[] = $test;
3258 printf(
3259 "Recording %d tests as failing.\n".
3260 "There are %d new failing tests, and %d new passing tests.\n",
3261 count($failed_tests), $new_fails, $new_passes
3263 sort(&$failed_tests);
3264 file_put_contents($fail_file, implode("\n", $failed_tests));
3265 } else if (isset($options['testpilot'])) {
3266 Status::say(array('op' => 'all_done', 'results' => $results));
3267 return $return_value;
3268 } else if (!$return_value) {
3269 print_success($tests, $results, $options);
3270 } else {
3271 print_failure($argv, $results, $options);
3274 Status::sayColor("\nTotal time for all executed tests as run: ",
3275 Status::BLUE,
3276 sprintf("%.2fs\n",
3277 Status::getOverallEndTime() -
3278 Status::getOverallStartTime()));
3279 Status::sayColor("Total time for all executed tests if run serially: ",
3280 Status::BLUE,
3281 sprintf("%.2fs\n",
3282 Status::addTestTimesSerial()));
3284 Status::removeTempDir();
3286 return $return_value;
3289 <<__EntryPoint>>
3290 function run_main(): void {
3291 exit(main($GLOBALS['argv']));