Make HHBBC validate that all symbols are unique
[hiphop-php.git] / hphp / test / run.php
blob419ce35189b699218b7164d8f6133ee8c50eee14
1 <?hh
2 /**
3 * Run the test suites in various configurations.
4 */
6 const TIMEOUT_SECONDS = 300;
8 // NOTE: The "HPHP_HOME" environment variable can be set (to ".../fbcode"), to
9 // define "hphp_home()" and (indirectly) "test_dir()". Otherwise, we will use
10 // "__DIR__" as "test_dir()", and its grandparent directory for "hphp_home()"
11 // (unless we are testing a dso extensions).
13 <<__Memoize>>
14 function is_testing_dso_extension() {
15 $home = getenv("HPHP_HOME");
16 if ($home is string) {
17 return false;
19 // detecting if we're running outside of the hhvm codebase.
20 return !is_file(__DIR__."/../../hphp/test/run.php");
23 <<__Memoize>>
24 function hphp_home() {
25 $home = getenv("HPHP_HOME");
26 if ($home is string) {
27 return realpath($home);
29 if (is_testing_dso_extension()) {
30 return realpath(__DIR__);
32 return realpath(__DIR__."/../..");
35 <<__Memoize>>
36 function test_dir(): string {
37 $home = getenv("HPHP_HOME");
38 if ($home is string) {
39 return realpath($home)."/hphp/test";
41 return __DIR__;
44 function get_expect_file_and_type($test, $options) {
45 $types = varray[
46 'expect',
47 'expectf',
48 'expectregex',
49 'hhvm.expect',
50 'hhvm.expectf',
52 if (isset($options['repo'])) {
53 if (file_exists($test . '.hhbbc_assert')) {
54 return varray[$test . '.hhbbc_assert', 'expectf'];
56 foreach ($types as $type) {
57 $fname = "$test.$type-repo";
58 if (file_exists($fname)) {
59 return varray[$fname, $type];
63 foreach ($types as $type) {
64 $fname = "$test.$type";
65 if (file_exists($fname)) {
66 return varray[$fname, $type];
69 return varray[null, null];
72 function multi_request_modes() {
73 return varray['retranslate-all',
74 'recycle-tc',
75 'jit-serialize',
76 'cli-server'];
79 function has_multi_request_mode($options) {
80 foreach (multi_request_modes() as $option) {
81 if (isset($options[$option])) return true;
83 return false;
86 function test_repo($options, $test) {
87 if (isset($options['repo-out'])) {
88 $test = $options['repo-out'] . '/' . str_replace('/', '.', $test);
90 return "$test.repo";
93 function jit_serialize_option(string $cmd, $test, $options, $serialize) {
94 $serialized = test_repo($options, $test) . "/jit.dump";
95 $cmds = explode(' -- ', $cmd, 2);
96 $cmds[0] .=
97 ' --count=' . ($serialize ? $options['jit-serialize'] + 1 : 1) .
98 " -vEval.JitSerdesFile=" . $serialized .
99 " -vEval.JitSerdesMode=" . ($serialize ? 'Serialize' : 'DeserializeOrFail').
100 ($serialize ? " -vEval.JitSerializeOptProfRequests=1" : '');
101 if (isset($options['jitsample']) && $serialize) {
102 $cmds[0] .= ' -vDeploymentId="' . $options['jitsample'] . '-serialize"';
104 return implode(' -- ', $cmds);
107 function usage() {
108 $argv = \HH\global_get('argv');
109 return "usage: {$argv[0]} [-m jit|interp] [-r] <test/directories>";
112 function help() {
113 $argv = \HH\global_get('argv');
114 $ztestexample = 'test/zend/good/*/*z*.php'; // sep. for syntax highlighting.
115 $help = <<<EOT
118 This is the hhvm test-suite runner. For more detailed documentation,
119 see hphp/test/README.md.
121 The test argument may be a path to a php test file, a directory name, or
122 one of a few pre-defined suite names that this script knows about.
124 If you work with hhvm a lot, you might consider a bash alias:
126 alias ht="path/to/hphp/test/run"
128 Examples:
130 # Quick tests in JIT mode:
131 % {$argv[0]} test/quick
133 # Slow tests in interp mode:
134 % {$argv[0]} -m interp test/slow
136 # PHP specification tests in JIT mode:
137 % {$argv[0]} test/slow/spec
139 # Slow closure tests in JIT mode:
140 % {$argv[0]} test/slow/closure
142 # Slow closure tests in JIT mode with RepoAuthoritative:
143 % {$argv[0]} -r test/slow/closure
145 # Slow array tests, in RepoAuthoritative:
146 % {$argv[0]} -r test/slow/array
148 # Zend tests with a "z" in their name:
149 % {$argv[0]} $ztestexample
151 # Quick tests in JIT mode with some extra runtime options:
152 % {$argv[0]} test/quick -a '-vEval.JitMaxTranslations=120 -vEval.HHIRRefcountOpts=0'
154 # All quick tests except debugger
155 % {$argv[0]} -e debugger test/quick
157 # All tests except those containing a string of 3 digits
158 % {$argv[0]} -E '/\d{3}/' all
160 # All tests whose name containing pdo_mysql
161 % {$argv[0]} -i pdo_mysql -m jit -r zend
163 # Print all the standard tests
164 % {$argv[0]} --list-tests
166 # Use a specific HHVM binary
167 % {$argv[0]} -b ~/code/hhvm/hphp/hhvm/hhvm
168 % {$argv[0]} --hhvm-binary-path ~/code/hhvm/hphp/hhvm/hhvm
170 # Use retranslate all. Run the test n times, then run retranslate all, then
171 # run the test n more on the new code.
172 % {$argv[0]} --retranslate-all 2 quick
174 # Use jit-serialize. Run the test n times, then run retranslate all, run the
175 # test once more, serialize all profile data, and then restart hhvm, load the
176 # serialized state and run retranslate-all before starting the test.
177 % {$argv[0]} --jit-serialize 2 -r quick
178 EOT;
179 return usage().$help;
182 function error($message) {
183 print "$message\n";
184 exit(1);
187 function success($message) {
188 print "$message\n";
189 exit(0);
192 // If a user-supplied path is provided, let's make sure we have a valid
193 // executable. Returns canonicanalized path or exits.
194 function check_executable(string $path): string {
195 $rpath = realpath($path);
196 if ($rpath === false || !is_executable($rpath)) {
197 error("Provided HHVM executable ($path) is not an executable file.\n" .
198 "If using HHVM_BIN, make sure that is set correctly.");
201 $output = varray[];
202 $return_var = -1;
203 exec($rpath . " --version 2> /dev/null", inout $output, inout $return_var);
204 if (strpos(implode($output), "HipHop ") !== 0) {
205 error("Provided file ($rpath) is not an HHVM executable.\n" .
206 "If using HHVM_BIN, make sure that is set correctly.");
209 return $rpath;
212 function hhvm_binary_routes() {
213 return darray[
214 "buck" => "/buck-out/gen/hphp/hhvm/hhvm",
215 "cmake" => "/hphp/hhvm"
219 function hh_codegen_binary_routes() {
220 return darray[
221 "buck" => "/buck-out/bin/hphp/hack/src/hh_single_compile",
222 "cmake" => "/hphp/hack/bin"
226 // For Facebook: We have several build systems, and we can use any of them in
227 // the same code repo. If multiple binaries exist, we want the onus to be on
228 // the user to specify a particular one because before we chose the buck one
229 // by default and that could cause unexpected results.
230 function check_for_multiple_default_binaries() {
231 // Env var we use in testing that'll pick which build system to use.
232 if (getenv("FBCODE_BUILD_TOOL") !== false) {
233 return;
236 $home = hphp_home();
237 $found = varray[];
238 foreach (hhvm_binary_routes() as $path) {
239 $abs_path = $home . $path . "/hhvm";
240 if (file_exists($abs_path)) {
241 $found[] = $abs_path;
245 if (count($found) <= 1) {
246 return;
249 $msg = "Multiple binaries exist in this repo. \n";
250 foreach ($found as $bin) {
251 $msg .= " - " . $bin . "\n";
253 $msg .= "Are you in fbcode? If so, remove a binary \n"
254 . "or use the --hhvm-binary-path option to the test runner. \n"
255 . "e.g. test/run --hhvm-binary-path /path/to/binary slow\n";
256 error($msg);
259 function hhvm_path() {
260 $file = "";
261 if (getenv("HHVM_BIN") !== false) {
262 $file = realpath(getenv("HHVM_BIN"));
263 } else {
264 $file = bin_root().'/hhvm';
267 if (!is_file($file)) {
268 if (is_testing_dso_extension()) {
269 $output = null;
270 $return_var = -1;
271 exec("which hhvm 2> /dev/null", inout $output, inout $return_var);
272 if (isset($output[0]) && $output[0]) {
273 return $output[0];
275 error("You need to specify hhvm bin with env HHVM_BIN");
278 error("$file doesn't exist. Did you forget to build first?");
280 return rel_path($file);
283 function hh_codegen_cmd($options) {
284 $cmd = hh_codegen_path();
285 if (isset($options['hackc'])) {
286 $cmd .= ' --daemon';
289 return $cmd;
292 function bin_root() {
293 if (getenv("HHVM_BIN") !== false) {
294 return dirname(realpath(getenv("HHVM_BIN")));
297 $home = hphp_home();
298 $env_tool = getenv("FBCODE_BUILD_TOOL");
299 $routes = hhvm_binary_routes();
301 if ($env_tool !== false) {
302 return $home . $routes[$env_tool];
305 foreach ($routes as $_ => $path) {
306 $dir = $home . $path;
307 if (is_dir($dir)) {
308 return $dir;
312 return $home . $routes["cmake"];
315 function hh_codegen_path() {
316 $file = "";
317 if (getenv("HH_CODEGEN_BIN") !== false) {
318 $file = realpath(getenv("HH_CODEGEN_BIN"));
319 } else {
320 $file = hh_codegen_bin_root().'/hh_single_compile.opt';
322 if (!is_file($file)) {
323 error("$file doesn't exist. Did you forget to build first?");
325 return rel_path($file);
328 function hh_codegen_bin_root() {
329 $home = hphp_home();
330 $env_tool = getenv("FBCODE_BUILD_TOOL");
331 $routes = hh_codegen_binary_routes();
333 if ($env_tool !== false) {
334 return $home . $routes[$env_tool];
337 foreach ($routes as $_ => $path) {
338 $dir = $home . $path;
339 if (is_dir($dir)) {
340 return $dir;
344 return $home . $routes["cmake"];
347 function verify_hhbc() {
348 if (getenv("VERIFY_HHBC") !== false) {
349 return getenv($env_hhbc);
351 return bin_root().'/verify.hhbc';
354 function read_opts_file($file) {
355 if ($file === null || !file_exists($file)) {
356 return "";
359 $fp = fopen($file, "r");
361 $contents = "";
362 while ($line = fgets($fp)) {
363 // Compress out white space.
364 $line = preg_replace('/\s+/', ' ', $line);
366 // Discard simple line oriented ; and # comments to end of line
367 // Comments at end of line (after payload) are not allowed.
368 $line = preg_replace('/^ *;.*$/', ' ', $line);
369 $line = preg_replace('/^ *#.*$/', ' ', $line);
371 // Substitute in the directory name
372 $line = str_replace('__DIR__', dirname($file), $line);
374 $contents .= $line;
376 fclose($fp);
377 return $contents;
380 // http://stackoverflow.com/questions/2637945/
381 function rel_path($to) {
382 $from = explode('/', getcwd().'/');
383 $to = explode('/', $to);
384 $from_len = count($from);
385 $to_len = count($to);
387 // find first non-matching dir.
388 for ($d = 0; $d < $from_len; ++$d) {
389 if ($d >= $to_len || $from[$d] !== $to[$d])
390 break;
393 $relPath = vec[];
395 // get number of remaining dirs in $from.
396 $remaining = $from_len - $d - 1;
397 if ($remaining > 0) {
398 // add traversals up to first matching dir.
399 while ($remaining-- > 0) $relPath[] = '..';
400 } else {
401 $relPath[] = '.';
403 while ($d < $to_len) $relPath[] = $to[$d++];
404 return implode('/', $relPath);
407 function get_options($argv) {
408 # Options marked * affect test behavior, and need to be reported by list_tests
409 $parameters = darray[
410 '*env:' => '',
411 'exclude:' => 'e:',
412 'exclude-pattern:' => 'E:',
413 'exclude-recorded-failures:' => 'x:',
414 'include:' => 'i:',
415 'include-pattern:' => 'I:',
416 '*repo' => 'r',
417 '*repo-single' => '',
418 '*repo-separate' => '',
419 '*repo-threads:' => '',
420 '*repo-out:' => '',
421 '*hhbbc2' => '',
422 '*mode:' => 'm:',
423 '*server' => 's',
424 '*cli-server' => 'S',
425 'shuffle' => '',
426 'help' => 'h',
427 'verbose' => 'v',
428 'testpilot' => '',
429 'threads:' => '',
430 '*args:' => 'a:',
431 'log' => 'l',
432 'failure-file:' => '',
433 '*wholecfg' => '',
434 '*hhas-round-trip' => '',
435 'color' => 'c',
436 'no-fun' => '',
437 'cores' => '',
438 'dump-tc' => '',
439 'no-clean' => '',
440 'list-tests' => '',
441 '*recycle-tc:' => '',
442 '*retranslate-all:' => '',
443 '*jit-serialize:' => '',
444 '*hhvm-binary-path:' => 'b:',
445 '*vendor:' => '',
446 'record-failures:' => '',
447 '*hackc' => '',
448 '*hack-only' => '',
449 '*ignore-oids' => '',
450 'jitsample:' => '',
451 '*hh_single_type_check:' => '',
452 'write-to-checkout' => '',
454 $options = darray[];
455 $files = varray[];
456 $recorded = varray[];
459 * '-' argument causes all future arguments to be treated as filenames, even
460 * if they would otherwise match a valid option. Otherwise, arguments starting
461 * with '-' MUST match a valid option.
463 $force_file = false;
465 for ($i = 1; $i < count($argv); $i++) {
466 $arg = $argv[$i];
468 if (strlen($arg) == 0) {
469 continue;
470 } else if ($force_file) {
471 $files[] = $arg;
472 } else if ($arg === '-') {
473 $forcefile = true;
474 } else if ($arg[0] === '-') {
475 $found = false;
477 foreach ($parameters as $long => $short) {
478 if ($arg == '-'.str_replace(':', '', $short) ||
479 $arg == '--'.str_replace(varray[':', '*'], varray['', ''], $long)) {
480 $record = substr($long, 0, 1) === '*';
481 if ($record) $recorded[] = $arg;
482 if (substr($long, -1, 1) == ':') {
483 $value = $argv[++$i];
484 if ($record) $recorded[] = $value;
485 } else {
486 $value = true;
488 $options[str_replace(varray[':', '*'], varray['', ''], $long)] = $value;
489 $found = true;
490 break;
494 if (!$found) {
495 error(sprintf("Invalid argument: '%s'\nSee {$argv[0]} --help", $arg));
497 } else {
498 $files[] = $arg;
502 \HH\global_set('recorded_options', $recorded);
504 if (isset($options['repo-out']) && !is_dir($options['repo-out'])) {
505 if (!mkdir($options['repo-out']) && !is_dir($options['repo-out'])) {
506 echo "Unable to create repo-out dir " . $options['repo-out'] . "\n";
507 exit(1);
510 if (isset($options['hhbbc2'])) {
511 $options['repo-separate'] = true;
512 if (isset($options['repo']) || isset($options['repo-single'])) {
513 echo "repo-single/repo and hhbbc2 are mutually exclusive options\n";
514 exit(1);
516 if (isset($options['mode'])) {
517 echo "hhbbc2 doesn't support modes; it compares hhas, doesn't run code\n";
518 exit(1);
522 if (isset($options['repo-single']) || isset($options['repo-separate'])) {
523 $options['repo'] = true;
524 } else if (isset($options['repo'])) {
525 // if only repo was set, then it means repo single
526 $options['repo-single'] = true;
529 if (isset($options['jit-serialize'])) {
530 if (!isset($options['repo'])) {
531 echo "jit-serialize only works in repo mode\n";
532 exit(1);
534 if (isset($options['mode']) && $options['mode'] != 'jit') {
535 echo "jit-serialize only works in jit mode\n";
536 exit(1);
540 if (isset($options['repo']) && isset($options['hhas-round-trip'])) {
541 echo "repo and hhas-round-trip are mutually exclusive options\n";
542 exit(1);
545 $multi_request_modes = array_filter(multi_request_modes(),
546 function($x) use ($options) {
547 return isset($options[$x]);
549 if (count($multi_request_modes) > 1) {
550 echo "The options\n -", implode("\n -", $multi_request_modes),
551 "\nare mutually exclusive options\n";
552 exit(1);
555 if (isset($options['write-to-checkout'])) {
556 Status::$write_to_checkout = true;
559 return varray[$options, $files];
563 * Return the path to $test relative to $base, or false if $base does not
564 * contain test.
566 function canonical_path_from_base($test, $base) {
567 $full = realpath($test);
568 if (substr($full, 0, strlen($base)) === $base) {
569 return substr($full, strlen($base) + 1);
571 $dirstat = stat($base);
572 if (!is_array($dirstat)) return false;
573 for ($p = dirname($full); $p && $p !== "/"; $p = dirname($p)) {
574 $s = stat($p);
575 if (!is_array($s)) continue;
576 if ($s['ino'] === $dirstat['ino'] && $s['dev'] === $dirstat['dev']) {
577 return substr($full, strlen($p) + 1);
580 return false;
583 function canonical_path($test) {
584 $attempt = canonical_path_from_base($test, test_dir());
585 if ($attempt === false) {
586 return canonical_path_from_base($test, hphp_home());
587 } else {
588 return $attempt;
593 * We support some 'special' file names, that just know where the test
594 * suites are, to avoid typing 'hphp/test/foo'.
596 function find_test_files($file) {
597 $mappage = darray[
598 'quick' => 'hphp/test/quick',
599 'slow' => 'hphp/test/slow',
600 'debugger' => 'hphp/test/server/debugger/tests',
601 'http' => 'hphp/test/server/http/tests',
602 'fastcgi' => 'hphp/test/server/fastcgi/tests',
603 'zend' => 'hphp/test/zend/good',
604 'facebook' => 'hphp/facebook/test',
606 // subset of slow we run with CLI server too
607 'slow_ext_hsl' => 'hphp/test/slow/ext_hsl',
609 // Subsets of zend tests.
610 'zend_ext' => 'hphp/test/zend/good/ext',
611 'zend_ext_am' => 'hphp/test/zend/good/ext/[a-m]*',
612 'zend_ext_nz' => 'hphp/test/zend/good/ext/[n-z]*',
613 'zend_Zend' => 'hphp/test/zend/good/Zend',
614 'zend_tests' => 'hphp/test/zend/good/tests',
617 $pattern = $mappage[$file] ?? null;
618 if ($pattern is nonnull) {
619 $pattern = hphp_home().'/'.$pattern;
620 $matches = glob($pattern);
621 if (count($matches) === 0) {
622 error(
623 "Convenience test name '$file' is recognized but does not match ".
624 "any test files (pattern = '$pattern')",
627 return $matches;
630 return varray[$file];
633 // Some tests have to be run together in the same test bucket, serially, one
634 // after other in order to avoid races and other collisions.
635 function serial_only_tests($tests) {
636 if (is_testing_dso_extension()) {
637 return varray[];
639 // Add a <testname>.php.serial file to make your test run in the serial
640 // bucket.
641 $serial_tests = array_filter(
642 $tests,
643 function($test) {
644 return file_exists($test . '.serial');
647 return $serial_tests;
650 // NOTE: If "files" is very long, then the shell may reject the desired
651 // "find" command (especially because "escapeshellarg()" adds two single
652 // quote characters to each file), so we split "files" into chunks below.
653 function exec_find(mixed $files, string $extra): mixed {
654 $results = varray[];
655 foreach (array_chunk($files, 500) as $chunk) {
656 $efa = implode(' ', array_map(fun('escapeshellarg'), $chunk));
657 $output = shell_exec("find $efa $extra");
658 foreach (explode("\n", $output) as $result) {
659 // Collect the (non-empty) results, which should all be file paths.
660 if ($result !== "") $results[] = $result;
663 return $results;
666 function find_tests($files, darray $options = null) {
667 if (!$files) {
668 $files = varray['quick'];
670 if ($files == varray['all']) {
671 $files = varray['quick', 'slow', 'zend', 'fastcgi'];
672 if (is_dir(hphp_home() . '/hphp/facebook/test')) {
673 $files[] = 'facebook';
676 $ft = varray[];
677 foreach ($files as $file) {
678 $ft = array_merge($ft, find_test_files($file));
680 $files = $ft;
681 foreach ($files as $idx => $file) {
682 if (!@stat($file)) {
683 error("Not valid file or directory: '$file'");
685 $file = preg_replace(',//+,', '/', realpath($file));
686 $file = preg_replace(',^'.getcwd().'/,', '', $file);
687 $files[$idx] = $file;
689 $tests = exec_find(
690 $files,
691 "'(' " .
692 "-name '*.php' " .
693 "-o -name '*.hack' " .
694 "-o -name '*.hackpartial' " .
695 "-o -name '*.hhas' " .
696 "-o -name '*.php.type-errors' " .
697 "-o -name '*.hack.type-errors' " .
698 "-o -name '*.hackpartial.type-errors' " .
699 "')' " .
700 "-not -regex '.*round_trip[.]hhas'"
702 if (!$tests) {
703 error("Could not find any tests associated with your options.\n" .
704 "Make sure your test path is correct and that you have " .
705 "the right expect files for the tests you are trying to run.\n" .
706 usage());
708 asort(inout $tests);
709 $tests = array_filter($tests);
710 if ($options['exclude'] ?? false) {
711 $exclude = $options['exclude'];
712 $tests = array_filter($tests, function($test) use ($exclude) {
713 return (false === strpos($test, $exclude));
716 if ($options['exclude-pattern'] ?? false) {
717 $exclude = $options['exclude-pattern'];
718 $tests = array_filter($tests, function($test) use ($exclude) {
719 return !preg_match($exclude, $test);
722 if ($options['exclude-recorded-failures'] ?? false) {
723 $exclude_file = $options['exclude-recorded-failures'];
724 $exclude = file($exclude_file, FILE_IGNORE_NEW_LINES);
725 $tests = array_filter($tests, function($test) use ($exclude) {
726 return (false === in_array(canonical_path($test), $exclude));
729 if ($options['include'] ?? false) {
730 $include = $options['include'];
731 $tests = array_filter($tests, function($test) use ($include) {
732 return (false !== strpos($test, $include));
735 if ($options['include-pattern'] ?? false) {
736 $include = $options['include-pattern'];
737 $tests = array_filter($tests, function($test) use ($include) {
738 return preg_match($include, $test);
741 return $tests;
744 function list_tests($files, $options) {
745 $args = implode(' ', \HH\global_get('recorded_options'));
747 // Disable escaping of test info when listing. We check if the environment
748 // variable is set so we can make the change in a backwards compatible way.
749 $escape_info = getenv("LISTING_NO_ESCAPE") === false;
751 foreach (find_tests($files, $options) as $test) {
752 $test_info = Status::jsonEncode(
753 darray['args' => $args, 'name' => $test],
755 if ($escape_info) {
756 print str_replace('\\', '\\\\', $test_info)."\n";
757 } else {
758 print $test_info."\n";
763 function find_test_ext($test, $ext, $configName='config') {
764 if (is_file("{$test}.{$ext}")) {
765 return "{$test}.{$ext}";
767 return find_file_for_dir(dirname($test), "{$configName}.{$ext}");
770 function find_file_for_dir($dir, $name) {
771 // Handle the case where the $dir might come in as '.' because you
772 // are running the test runner on a file from the same directory as
773 // the test e.g., './mytest.php'. dirname() will give you the '.' when
774 // you actually have a lot of path to traverse upwards like
775 // /home/you/code/tests/mytest.php. Use realpath() to get that.
776 $dir = realpath($dir);
777 while ($dir !== '/' && is_dir($dir)) {
778 $file = "$dir/$name";
779 if (is_file($file)) {
780 return $file;
782 $dir = dirname($dir);
784 $file = test_dir().'/'.$name;
785 if (file_exists($file)) {
786 return $file;
788 return null;
791 function find_debug_config($test, $name) {
792 $debug_config = find_file_for_dir(dirname($test), $name);
793 if ($debug_config !== null) {
794 return "-m debug --debug-config ".$debug_config;
796 return "";
799 function mode_cmd($options): varray<string> {
800 $repo_args = '';
801 if (!isset($options['repo'])) {
802 // Set the non-repo-mode shared repo.
803 // When in repo mode, we set our own central path.
804 $repo_args = "-vRepo.Local.Mode=-- -vRepo.Central.Path=".verify_hhbc();
806 $interp_args = "$repo_args -vEval.Jit=0";
807 $jit_args = "$repo_args -vEval.Jit=true";
808 $mode = idx($options, 'mode', '');
809 switch ($mode) {
810 case '':
811 case 'jit':
812 return varray[$jit_args];
813 case 'interp':
814 return varray[$interp_args];
815 case 'interp,jit':
816 return varray[$interp_args, $jit_args];
817 default:
818 error("-m must be one of jit | interp | interp,jit. Got: '$mode'");
822 function extra_args($options): string {
823 $args = $options['args'] ?? '';
825 $vendor = $options['vendor'] ?? null;
826 if ($vendor !== null) {
827 $args .= ' -d auto_prepend_file=';
828 $args .= escapeshellarg($vendor.'/hh_autoload.php');
830 return $args;
833 function hhvm_cmd_impl(
834 $options,
835 $config,
836 $autoload_db_prefix,
837 ...$extra_args
838 ): varray<string> {
839 $cmds = varray[];
840 foreach (mode_cmd($options) as $mode_num => $mode) {
841 $args = varray[
842 hhvm_path(),
843 '-c',
844 $config,
845 // EnableArgsInBacktraces disables most of HHBBC's DCE optimizations.
846 // In order to test those optimizations (which are part of a normal prod
847 // configuration) we turn this flag off by default.
848 '-vEval.EnableArgsInBacktraces=false',
849 '-vEval.EnableIntrinsicsExtension=true',
850 '-vEval.HHIRInliningIgnoreHints=false',
851 '-vEval.HHIRAlwaysInterpIgnoreHint=false',
852 $mode,
853 isset($options['wholecfg']) ? '-vEval.JitPGORegionSelector=wholecfg' : '',
855 // load/store counters don't work on Ivy Bridge so disable for tests
856 '-vEval.ProfileHWEnable=false',
858 // use a fixed path for embedded data
859 '-vEval.HackCompilerExtractPath='
860 .escapeshellarg(bin_root().'/hackc_%{schema}'),
861 '-vEval.EmbeddedDataExtractPath='
862 .escapeshellarg(bin_root().'/hhvm_%{type}_%{buildid}'),
863 extra_args($options),
866 if ($autoload_db_prefix !== null) {
867 $args[] = '-vAutoload.DBPath='.escapeshellarg("$autoload_db_prefix.$mode_num");
870 if (isset($options['hackc'])) {
871 $args[] = '-vEval.HackCompilerCommand="'.hh_codegen_cmd($options).'"';
872 $args[] = '-vEval.HackCompilerUseEmbedded=false';
875 if (isset($options['retranslate-all'])) {
876 $args[] = '--count='.($options['retranslate-all'] * 2);
877 $args[] = '-vEval.JitPGO=true';
878 $args[] = '-vEval.JitRetranslateAllRequest='.$options['retranslate-all'];
879 // Set to timeout. We want requests to trigger retranslate all.
880 $args[] = '-vEval.JitRetranslateAllSeconds=' . TIMEOUT_SECONDS;
883 if (isset($options['recycle-tc'])) {
884 $args[] = '--count='.$options['recycle-tc'];
885 $args[] = '-vEval.StressUnitCacheFreq=1';
886 $args[] = '-vEval.EnableReusableTC=true';
889 if (isset($options['jit-serialize'])) {
890 $args[] = '-vEval.JitPGO=true';
891 $args[] = '-vEval.JitRetranslateAllRequest='.$options['jit-serialize'];
892 // Set to timeout. We want requests to trigger retranslate all.
893 $args[] = '-vEval.JitRetranslateAllSeconds=' . TIMEOUT_SECONDS;
896 if (isset($options['hhas-round-trip'])) {
897 $args[] = '-vEval.AllowHhas=1';
898 $args[] = '-vEval.LoadFilepathFromUnitCache=1';
901 if (!isset($options['cores'])) {
902 $args[] = '-vResourceLimit.CoreFileSize=0';
905 if (isset($options['dump-tc'])) {
906 $args[] = '-vEval.DumpIR=1';
907 $args[] = '-vEval.DumpTC=1';
910 if (isset($options['hh_single_type_check'])) {
911 $args[] = '--hh_single_type_check='.$options['hh_single_type_check'];
914 $cmds[] = implode(' ', array_merge($args, $extra_args));
916 return $cmds;
919 function repo_separate($options, $test) {
920 return isset($options['repo-separate']) &&
921 !file_exists($test . ".hhbbc_opts");
924 // Return the command and the env to run it in.
925 function hhvm_cmd(
926 $options,
927 $test,
928 $test_run = null,
929 $is_temp_file = false
930 ): (varray<string>, darray<string, mixed>) {
931 if ($test_run === null) {
932 $test_run = $test;
934 // hdf support is only temporary until we fully migrate to ini
935 // Discourage broad use.
936 $hdf_suffix = ".use.for.ini.migration.testing.only.hdf";
937 $hdf = file_exists($test.$hdf_suffix)
938 ? '-c ' . $test . $hdf_suffix
939 : "";
940 $cmds = hhvm_cmd_impl(
941 $options,
942 find_test_ext($test, 'ini'),
943 Status::getTestTmpPath($test, 'autoloadDB'),
944 $hdf,
945 find_debug_config($test, 'hphpd.ini'),
946 read_opts_file(find_test_ext($test, 'opts')),
947 '--file',
948 escapeshellarg($test_run),
949 $is_temp_file ? " --temp-file" : ""
952 $cmd = "";
954 if (file_exists($test.'.verify')) {
955 $cmd .= " -m verify";
958 if (isset($options['cli-server'])) {
959 $config = find_file_for_dir(dirname($test), 'config.ini');
960 $socket = $options['servers']['configs'][$config]->server['cli-socket'];
961 $cmd .= ' -vEval.UseRemoteUnixServer=only';
962 $cmd .= ' -vEval.UnixServerPath='.$socket;
963 $cmd .= ' --count=3';
966 // Special support for tests that require a path to the current
967 // test directory for things like prepend_file and append_file
968 // testing.
969 if (file_exists($test.'.ini')) {
970 $contents = file_get_contents($test.'.ini');
971 if (strpos($contents, '{PWD}') !== false) {
972 $test_ini = tempnam('/tmp', $test).'.ini';
973 file_put_contents($test_ini,
974 str_replace('{PWD}', dirname($test), $contents));
975 $cmd .= " -c $test_ini";
978 if ($hdf !== "") {
979 $contents = file_get_contents($test.$hdf_suffix);
980 if (strpos($contents, '{PWD}') !== false) {
981 $test_hdf = tempnam('/tmp', $test).$hdf_suffix;
982 file_put_contents($test_hdf,
983 str_replace('{PWD}', dirname($test), $contents));
984 $cmd .= " -c $test_hdf";
988 if (isset($options['repo'])) {
989 $repo_suffix = repo_separate($options, $test) ? 'hhbbc' : 'hhbc';
991 $program = isset($options['hackc']) ? "hackc" : "hhvm";
992 $hhbbc_repo = '"' . test_repo($options, $test) . "/$program.$repo_suffix\"";
993 $cmd .= ' -vRepo.Authoritative=true -vRepo.Commit=0';
994 $cmd .= " -vRepo.Central.Path=$hhbbc_repo";
997 if (isset($options['jitsample'])) {
998 $cmd .= ' -vDeploymentId="' . $options['jitsample'] . '"';
999 $cmd .= ' --instance-id="' . $test . '"';
1000 $cmd .= ' -vEval.JitSampleRate=1';
1001 $cmd .= " -vScribe.Tables.hhvm_jit.include.*=instance_id";
1002 $cmd .= " -vScribe.Tables.hhvm_jit.include.*=deployment_id";
1005 $env = $_ENV;
1007 // Apply the --env option
1008 if (isset($options['env'])) {
1009 foreach (explode(",", $options['env']) as $arg) {
1010 $i = strpos($arg, '=');
1011 if ($i) {
1012 $key = substr($arg, 0, $i);
1013 $val = substr($arg, $i + 1);
1014 $env[$key] = $val;
1015 } else {
1016 unset($env[$arg]);
1021 $in = find_test_ext($test, 'in');
1022 if ($in !== null) {
1023 $cmd .= ' < ' . escapeshellarg($in);
1024 // If we're piping the input into the command then setup a simple
1025 // dumb terminal so hhvm doesn't try to control it and pollute the
1026 // output with control characters, which could change depending on
1027 // a wide variety of terminal settings.
1028 $env["TERM"] = "dumb";
1031 foreach ($cmds as $idx => $_) {
1032 $cmds[$idx] .= $cmd;
1035 return tuple($cmds, $env);
1038 function hphp_cmd($options, $test, $program): string {
1039 $extra_args = preg_replace("/-v\s*/", "-vRuntime.", extra_args($options));
1041 $compiler_args = "";
1042 if (isset($options['hackc'])) {
1043 $hh_single_compile = hh_codegen_path();
1044 $compiler_args = implode(" ", varray[
1045 '-vRuntime.Eval.HackCompilerUseEmbedded=false',
1046 "-vRuntime.Eval.HackCompilerInheritConfig=true",
1047 "-vRuntime.Eval.HackCompilerCommand=\"{$hh_single_compile} --daemon --dump-symbol-refs\""
1051 $hdf_suffix = ".use.for.ini.migration.testing.only.hdf";
1052 $hdf = file_exists($test.$hdf_suffix)
1053 ? '-c ' . $test . $hdf_suffix
1054 : "";
1056 if ($hdf !== "") {
1057 $contents = file_get_contents($test.$hdf_suffix);
1058 if (strpos($contents, '{PWD}') !== false) {
1059 $test_hdf = tempnam('/tmp', $test).$hdf_suffix;
1060 file_put_contents($test_hdf,
1061 str_replace('{PWD}', dirname($test), $contents));
1062 $hdf = " -c $test_hdf";
1066 return implode(" ", varray[
1067 hhvm_path(),
1068 '--hphp',
1069 '-vUseHHBBC='. (repo_separate($options, $test) ? 'false' : 'true'),
1070 '--config',
1071 find_test_ext($test, 'ini', 'hphp_config'),
1072 $hdf,
1073 '-vRuntime.ResourceLimit.CoreFileSize=0',
1074 '-vRuntime.Eval.EnableIntrinsicsExtension=true',
1075 '-vRuntime.Eval.EnableArgsInBacktraces=true',
1076 '-vRuntime.Eval.HackCompilerExtractPath='
1077 .escapeshellarg(bin_root().'/hackc_%{schema}'),
1078 '-vParserThreadCount=' . ($options['repo-threads'] ?? 1),
1079 '--nofork=1 -thhbc -l1 -k1',
1080 '-o "' . test_repo($options, $test) . '"',
1081 "--program $program.hhbc \"$test\"",
1082 "-vRuntime.Repo.Local.Mode=rw -vRuntime.Repo.Local.Path=".verify_hhbc(),
1083 $extra_args,
1084 $compiler_args,
1085 read_opts_file("$test.hphp_opts"),
1089 function hhbbc_cmd($options, $test, $program) {
1090 $test_repo = test_repo($options, $test);
1091 return implode(" ", varray[
1092 hhvm_path(),
1093 '--hhbbc',
1094 '--no-logging',
1095 '--no-cores',
1096 '--parallel-num-threads=' . ($options['repo-threads'] ?? 1),
1097 '--hack-compiler-extract-path='
1098 .escapeshellarg(bin_root().'/hackc_%{schema}'),
1099 read_opts_file("$test.hhbbc_opts"),
1100 "-o \"$test_repo/$program.hhbbc\" \"$test_repo/$program.hhbc\"",
1104 // Execute $cmd and return its output, including any stacktrace.log
1105 // file it generated.
1106 function exec_with_stack($cmd) {
1107 $pipes = null;
1108 $proc = proc_open($cmd,
1109 darray[0 => varray['pipe', 'r'],
1110 1 => varray['pipe', 'w'],
1111 2 => varray['pipe', 'w']], inout $pipes);
1112 fclose($pipes[0]);
1113 $s = '';
1114 $all_selects_failed=true;
1115 $end = microtime(true) + TIMEOUT_SECONDS;
1116 $timedout = false;
1117 while (true) {
1118 $now = microtime(true);
1119 if ($now >= $end) break;
1120 $read = varray[$pipes[1], $pipes[2]];
1121 $write = null;
1122 $except = null;
1123 $available = @stream_select(
1124 inout $read,
1125 inout $write,
1126 inout $except,
1127 (int)($end - $now),
1129 if ($available === false) {
1130 usleep(1000);
1131 $s .= "select failed:\n" . print_r(error_get_last(), true);
1132 continue;
1134 $all_selects_failed=false;
1135 if ($available === 0) continue;
1136 # var_dump($read);
1137 foreach ($read as $pipe) {
1138 $t = fread($pipe, 4096);
1139 # var_dump($t);
1140 if ($t === false) continue;
1141 $s .= $t;
1143 if (feof($pipes[1]) && feof($pipes[2])) break;
1145 fclose($pipes[1]);
1146 fclose($pipes[2]);
1147 while (true) {
1148 $status = proc_get_status($proc);
1149 if (!$status['running']) break;
1150 $now = microtime(true);
1151 if ($now >= $end) {
1152 $timedout = true;
1153 $output = null;
1154 $return_var = -1;
1155 exec('pkill -P ' . $status['pid'] . ' 2> /dev/null', inout $output, inout $return_var);
1156 posix_kill($status['pid'], SIGTERM);
1158 usleep(1000);
1160 proc_close($proc);
1161 if ($timedout) {
1162 if ($all_selects_failed) {
1163 return "All selects failed running `$cmd'\n\n$s";
1165 return "Timed out running `$cmd'\n\n$s";
1167 if (!$status['exitcode']) return true;
1168 $pid = $status['pid'];
1169 $stack =
1170 @file_get_contents("/tmp/stacktrace.$pid.log") ?:
1171 @file_get_contents("/var/tmp/cores/stacktrace.$pid.log");
1172 if ($stack !== false) {
1173 $s .= "\n" . $stack;
1175 return "Running `$cmd' failed (".$status['exitcode']."):\n\n$s";
1178 function repo_mode_compile($options, $test, $program) {
1179 $hphp = hphp_cmd($options, $test, $program);
1180 $result = exec_with_stack($hphp);
1181 if ($result === true && repo_separate($options, $test)) {
1182 $hhbbc = hhbbc_cmd($options, $test, $program);
1183 $result = exec_with_stack($hhbbc);
1185 if ($result === true) return true;
1186 Status::writeDiff($test, $result);
1190 // Minimal support for sending messages between processes over named pipes.
1192 // Non-buffered pipe writes of up to 512 bytes (PIPE_BUF) are atomic.
1194 // Packet format:
1195 // 8 byte zero-padded hex pid
1196 // 4 byte zero-padded hex type
1197 // 4 byte zero-padded hex body size
1198 // N byte string body
1200 // NOTE: The first call to "getInput()" or "getOutput()" in any process will
1201 // block until some other process calls the other method.
1203 class Queue {
1204 // The path to the FIFO, until destroyed.
1205 private ?string $path = null;
1207 // TODO: Use proper types.
1208 private mixed $input = null;
1209 private mixed $output = null;
1211 // Pipes writes are atomic up to 512 bytes (up to 4096 bytes on linux),
1212 // and we use a 16 byte header, leaving this many bytes available for
1213 // each chunk of "body" (see "$partials").
1214 const int CHUNK = 512 - 16;
1216 // If a message "body" is larger than CHUNK bytes, then writers must break
1217 // it into chunks, and send all but the last chunk with type 0. The reader
1218 // collects those chunks in this Map (indexed by pid), until the final chunk
1219 // is received, and the chunks can be reassembled.
1220 private Map<int, Vector<string>> $partials = Map {};
1223 // NOTE: Only certain directories support "posix_mkfifo()".
1224 public function __construct(?string $dir = null): void {
1225 $path = \tempnam($dir ?? \sys_get_temp_dir(), "queue.mkfifo.");
1226 \unlink($path);
1227 if (!\posix_mkfifo($path, 0700)) {
1228 throw new \Exception("Failed to create FIFO at '$path'");
1230 $this->path = $path;
1233 private function getInput(): mixed {
1234 $input = $this->input;
1235 if ($input is null) {
1236 $path = $this->path;
1237 if ($path is null) {
1238 throw new \Exception("Missing FIFO path");
1240 $input = \fopen($path, "r");
1241 $this->input = $input;
1243 return $input;
1246 private function getOutput(): mixed {
1247 $output = $this->output;
1248 if ($output is null) {
1249 $path = $this->path;
1250 if ($path is null) {
1251 throw new \Exception("Missing FIFO path");
1253 $output = \fopen($path, "a");
1254 $this->output = $output;
1256 return $output;
1259 private function validate(int $pid, int $type, int $blen): void {
1260 if ($pid < 0 || $pid >= (1 << 22)) {
1261 throw new \Exception("Illegal pid $pid");
1263 if ($type < 0 || $type >= 0x10000) {
1264 throw new \Exception("Illegal type $type");
1266 if ($blen < 0 || $blen > static::CHUNK) {
1267 throw new \Exception("Illegal blen $blen");
1271 // Read one packet header or body.
1272 private function read(int $n): string {
1273 $input = $this->getInput();
1274 $result = "";
1275 while (\strlen($result) < $n) {
1276 $r = fread($input, $n - \strlen($result));
1277 if ($r is string) {
1278 $result .= $r;
1279 } else {
1280 throw new \Exception("Failed to read $n bytes");
1283 return $result;
1286 // Receive one raw message (pid, type, body).
1287 public function receive(): (int, int, string) {
1288 $type = null;
1289 $body = "";
1290 while (true) {
1291 $header = $this->read(16);
1292 $pid = intval(substr($header, 0, 8) as string, 16);
1293 $type = intval(substr($header, 8, 4) as string, 16);
1294 $blen = intval(substr($header, 12, 4) as string, 16);
1295 $this->validate($pid, $type, $blen);
1296 $body = $this->read($blen);
1297 if ($type === 0) {
1298 $this->partials[$pid] ??= Vector {};
1299 $this->partials[$pid][] = $body;
1300 } else {
1301 $chunks = $this->partials[$pid] ?? null;
1302 if ($chunks is nonnull) {
1303 $chunks[] = $body;
1304 $body = \join("", $chunks);
1305 unset($this->partials[$pid]);
1307 return tuple($pid, $type, $body);
1312 // Receive one message (pid, type, message).
1313 // Note that the raw body is processed using "unserialize()".
1314 public function receiveMessage(): (int, int, mixed) {
1315 list($pid, $type, $body) = $this->receive();
1316 $msg = unserialize($body);
1317 return tuple($pid, $type, $msg);
1320 private function write(int $pid, int $type, string $body): void {
1321 $output = $this->getOutput();
1322 $blen = \strlen($body);
1323 $this->validate($pid, $type, $blen);
1324 $packet = sprintf("%08x%04x%04x%s", $pid, $type, $blen, $body);
1325 $n = \strlen($packet);
1326 if ($n !== 16 + $blen) {
1327 throw new \Exception("Illegal packet");
1329 // NOTE: Hack's "fwrite()" is never buffered, which is especially
1330 // critical for pipe writes, to ensure that they are actually atomic.
1331 // See the documentation for "PlainFile::writeImpl()". But just in
1332 // case, we add an explicit "fflush()" below.
1333 $bytes_out = fwrite($output, $packet, $n);
1334 if ($bytes_out !== $n) {
1335 throw new \Exception(
1336 "Failed to write $n bytes; only $bytes_out were written"
1339 fflush($output);
1342 // Send one raw message.
1343 public function send(int $type, string $body): void {
1344 $pid = \posix_getpid();
1345 $blen = \strlen($body);
1346 $chunk = static::CHUNK;
1347 if ($blen > $chunk) {
1348 for ($i = 0; $i + $chunk < $blen; $i += $chunk) {
1349 $this->write($pid, 0, \substr($body, $i, $chunk) as string);
1351 $this->write($pid, $type, \substr($body, $i) as string);
1352 } else {
1353 $this->write($pid, $type, $body);
1357 // Send one message.
1358 // Note that the raw body is computed using "serialize()".
1359 public function sendMessage(int $type, mixed $msg): void {
1360 $body = serialize($msg);
1361 $this->send($type, $body);
1364 function destroy(): void {
1365 if ($this->input is nonnull) {
1366 fclose($this->input);
1367 $this->input = null;
1369 if ($this->output is nonnull) {
1370 fclose($this->output);
1371 $this->output = null;
1373 if ($this->path is nonnull) {
1374 \unlink($this->path);
1375 $this->path = null;
1380 enum TempDirRemove: int {
1381 ALWAYS = 0;
1382 ON_RUN_SUCCESS = 1;
1383 NEVER = 2;
1386 class Status {
1387 private static $results = varray[];
1388 private static $mode = 0;
1390 private static $use_color = false;
1392 public static $nofork = false;
1393 private static ?Queue $queue = null;
1394 private static $killed = false;
1395 public static TempDirRemove $temp_dir_remove = TempDirRemove::ALWAYS;
1396 private static int $return_value = 255;
1398 private static $overall_start_time = 0;
1399 private static $overall_end_time = 0;
1401 private static $tmpdir = "";
1402 public static $write_to_checkout = false;
1404 public static $passed = 0;
1405 public static $skipped = 0;
1406 public static $skip_reasons = darray[];
1407 public static $failed = 0;
1409 const MODE_NORMAL = 0;
1410 const MODE_VERBOSE = 1;
1411 const MODE_TESTPILOT = 3;
1412 const MODE_RECORD_FAILURES = 4;
1414 const MSG_STARTED = 7;
1415 const MSG_FINISHED = 1;
1416 const MSG_TEST_PASS = 2;
1417 const MSG_TEST_FAIL = 4;
1418 const MSG_TEST_SKIP = 5;
1419 const MSG_SERVER_RESTARTED = 6;
1421 const RED = 31;
1422 const GREEN = 32;
1423 const YELLOW = 33;
1424 const BLUE = 34;
1426 public static function createTmpDir(): void {
1427 // TODO: one day we should have hack-accessible mkdtemp
1428 self::$tmpdir = tempnam(sys_get_temp_dir(), "hphp-test-");
1429 unlink(self::$tmpdir);
1430 mkdir(self::$tmpdir);
1433 public static function getRunTmpDir(): string {
1434 return self::$tmpdir;
1437 // Return a path in the run tmpdir that's unique to this test and ext.
1438 // Remember to teach clean_intermediate_files to clean up all the exts you use
1439 public static function getTestTmpPath(string $test, string $ext): string {
1440 return self::$tmpdir . '/' . $test . '.' . $ext;
1443 // Similar to getTestTmpPath, but if we're run with --write-to-checkout
1444 // then we put the files next to the test instead of in the tmpdir.
1445 public static function getTestOutputPath(string $test, string $ext): string {
1446 if (self::$write_to_checkout) {
1447 return "$test.$ext";
1449 return static::getTestTmpPath($test, $ext);
1452 public static function createTestTmpDir(string $test): string {
1453 $test_temp_dir = self::getTestTmpPath($test, 'tmpdir');
1454 @mkdir($test_temp_dir, 0777, true);
1455 return $test_temp_dir;
1458 public static function writeDiff(string $test, string $diff): void {
1459 $path = Status::getTestOutputPath($test, 'diff');
1460 @mkdir(dirname($path), 0777, true);
1461 file_put_contents($path, $diff);
1464 public static function diffForTest(string $test): string {
1465 $diff = @file_get_contents(Status::getTestOutputPath($test, 'diff'));
1466 return $diff === false ? '' : $diff;
1469 public static function removeDirectory($dir) {
1470 $files = scandir($dir);
1471 foreach ($files as $file) {
1472 if ($file == '.' || $file == '..') {
1473 continue;
1475 $path = $dir . "/" . $file;
1476 if (is_dir($path)) {
1477 self::removeDirectory($path);
1478 } else {
1479 unlink($path);
1482 rmdir($dir);
1485 // This is similar to removeDirectory but it only removes empty directores
1486 // and won't enter directories whose names end with '.tmpdir'. This allows
1487 // us to clean up paths like test/quick/vec in our run's temporary directory
1488 // if all the tests in them passed, but it leaves test tmpdirs of failed
1489 // tests (that we didn't remove with clean_intermediate_files because the
1490 // test failed) and directores under them alone even if they're empty.
1491 public static function removeEmptyTestParentDirs($dir): bool {
1492 $is_now_empty = true;
1493 $files = scandir($dir);
1494 foreach ($files as $file) {
1495 if ($file == '.' || $file == '..') {
1496 continue;
1498 if (strrpos($file, '.tmpdir') === (strlen($file) - strlen('.tmpdir'))) {
1499 $is_now_empty = false;
1500 continue;
1502 $path = $dir . "/" . $file;
1503 if (!is_dir($path)) {
1504 $is_now_empty = false;
1505 continue;
1507 if (self::removeEmptyTestParentDirs($path)) {
1508 rmdir($path);
1509 } else {
1510 $is_now_empty = false;
1513 return $is_now_empty;
1516 public static function setMode($mode) {
1517 self::$mode = $mode;
1520 public static function getMode() {
1521 return self::$mode;
1524 public static function setUseColor($use) {
1525 self::$use_color = $use;
1528 public static function addTestTimesSerial($results) {
1529 $time = 0.0;
1530 foreach ($results as $result) {
1531 $time += $result['time'];
1533 return $time;
1536 public static function getOverallStartTime() {
1537 return self::$overall_start_time;
1540 public static function getOverallEndTime() {
1541 return self::$overall_end_time;
1544 public static function started() {
1545 self::send(self::MSG_STARTED, null);
1546 self::$overall_start_time = microtime(true);
1549 public static function finished(int $return_value) {
1550 self::$overall_end_time = microtime(true);
1551 self::$return_value = $return_value;
1552 self::send(self::MSG_FINISHED, null);
1555 public static function destroy(): void {
1556 if (!self::$killed) {
1557 self::$killed = true;
1558 if (self::$queue !== null) {
1559 self::$queue->destroy();
1560 self::$queue = null;
1562 switch (self::$temp_dir_remove) {
1563 case TempDirRemove::NEVER:
1564 break;
1565 case TempDirRemove::ON_RUN_SUCCESS:
1566 if (self::$return_value !== 0) {
1567 self::removeEmptyTestParentDirs(self::$tmpdir);
1568 break;
1570 // FALLTHROUGH
1571 case TempDirRemove::ALWAYS:
1572 self::removeDirectory(self::$tmpdir);
1577 public static function destroyFromSignal($_signo): void {
1578 self::destroy();
1581 public static function registerCleanup(bool $no_clean) {
1582 if (self::getMode() === self::MODE_TESTPILOT ||
1583 self::getMode() === self::MODE_RECORD_FAILURES) {
1584 self::$temp_dir_remove = TempDirRemove::ALWAYS;
1585 } else if ($no_clean) {
1586 self::$temp_dir_remove = TempDirRemove::NEVER;
1587 } else {
1588 self::$temp_dir_remove = TempDirRemove::ON_RUN_SUCCESS;
1590 register_shutdown_function(class_meth(self::class, 'destroy'));
1591 pcntl_signal(SIGTERM, class_meth(self::class, 'destroyFromSignal'));
1592 pcntl_signal(SIGINT, class_meth(self::class, 'destroyFromSignal'));
1595 public static function serverRestarted() {
1596 self::send(self::MSG_SERVER_RESTARTED, null);
1599 public static function pass($test, $time, $stime, $etime) {
1600 self::$results[] = darray['name' => $test,
1601 'status' => 'passed',
1602 'start_time' => $stime,
1603 'end_time' => $etime,
1604 'time' => $time];
1605 self::send(self::MSG_TEST_PASS, varray[$test, $time, $stime, $etime]);
1608 public static function skip($test, $reason, $time, $stime, $etime) {
1609 self::$results[] = darray[
1610 'name' => $test,
1611 /* testpilot needs a positive response for every test run, report
1612 * that this test isn't relevant so it can silently drop. */
1613 'status' => self::getMode() === self::MODE_TESTPILOT
1614 ? 'not_relevant'
1615 : 'skipped',
1616 'start_time' => $stime,
1617 'end_time' => $etime,
1618 'time' => $time,
1620 self::send(self::MSG_TEST_SKIP,
1621 varray[$test, $reason, $time, $stime, $etime]);
1624 public static function fail($test, $time, $stime, $etime, $diff) {
1625 self::$results[] = darray[
1626 'name' => $test,
1627 'status' => 'failed',
1628 'details' => self::utf8Sanitize($diff),
1629 'start_time' => $stime,
1630 'end_time' => $etime,
1631 'time' => $time
1633 self::send(self::MSG_TEST_FAIL, varray[$test, $time, $stime, $etime]);
1636 public static function handle_message($type, $message) {
1637 switch ($type) {
1638 case Status::MSG_STARTED:
1639 break;
1641 case Status::MSG_FINISHED:
1642 return false;
1644 case Status::MSG_SERVER_RESTARTED:
1645 switch (Status::getMode()) {
1646 case Status::MODE_NORMAL:
1647 if (!Status::hasCursorControl()) {
1648 Status::sayColor(Status::RED, 'x');
1650 break;
1652 case Status::MODE_VERBOSE:
1653 Status::sayColor("$test ", Status::YELLOW, "failed",
1654 " to talk to server\n");
1655 break;
1657 case Status::MODE_TESTPILOT:
1658 break;
1660 case Status::MODE_RECORD_FAILURES:
1661 break;
1664 case Status::MSG_TEST_PASS:
1665 self::$passed++;
1666 list($test, $time, $stime, $etime) = $message;
1667 switch (Status::getMode()) {
1668 case Status::MODE_NORMAL:
1669 if (!Status::hasCursorControl()) {
1670 Status::sayColor(Status::GREEN, '.');
1672 break;
1674 case Status::MODE_VERBOSE:
1675 Status::sayColor("$test ", Status::GREEN,
1676 sprintf("passed (%.2fs)\n", $time));
1677 break;
1679 case Status::MODE_TESTPILOT:
1680 Status::sayTestpilot($test, 'passed', $stime, $etime);
1681 break;
1683 case Status::MODE_RECORD_FAILURES:
1684 break;
1686 break;
1688 case Status::MSG_TEST_SKIP:
1689 self::$skipped++;
1690 list($test, $reason, $time, $stime, $etime) = $message;
1691 self::$skip_reasons[$reason] ??= 0;
1692 self::$skip_reasons[$reason]++;
1694 switch (Status::getMode()) {
1695 case Status::MODE_NORMAL:
1696 if (!Status::hasCursorControl()) {
1697 Status::sayColor(Status::YELLOW, 's');
1699 break;
1701 case Status::MODE_VERBOSE:
1702 Status::sayColor("$test ", Status::YELLOW, "skipped");
1704 if ($reason !== null) {
1705 Status::sayColor(" - reason: $reason");
1707 Status::sayColor(sprintf(" (%.2fs)\n", $time));
1708 break;
1710 case Status::MODE_TESTPILOT:
1711 Status::sayTestpilot($test, 'not_relevant', $stime, $etime);
1712 break;
1714 case Status::MODE_RECORD_FAILURES:
1715 break;
1717 break;
1719 case Status::MSG_TEST_FAIL:
1720 self::$failed++;
1721 list($test, $time, $stime, $etime) = $message;
1722 switch (Status::getMode()) {
1723 case Status::MODE_NORMAL:
1724 if (Status::hasCursorControl()) {
1725 print "\033[2K\033[1G";
1727 $diff = Status::diffForTest($test);
1728 Status::sayColor(Status::RED, "\nFAILED",
1729 ": $test\n$diff\n");
1730 break;
1732 case Status::MODE_VERBOSE:
1733 Status::sayColor("$test ", Status::RED,
1734 sprintf("FAILED (%.2fs)\n", $time));
1735 break;
1737 case Status::MODE_TESTPILOT:
1738 Status::sayTestpilot($test, 'failed', $stime, $etime);
1739 break;
1741 case Status::MODE_RECORD_FAILURES:
1742 break;
1744 break;
1746 default:
1747 error("Unknown message $type");
1749 return true;
1752 private static function send($type, $msg) {
1753 if (self::$killed) {
1754 return;
1756 if (self::$nofork) {
1757 self::handle_message($type, $msg);
1758 return;
1760 self::getQueue()->sendMessage($type, $msg);
1764 * Takes a variable number of string arguments. If color output is enabled
1765 * and any one of the arguments is preceded by an integer (see the color
1766 * constants above), that argument will be given the indicated color.
1768 public static function sayColor(...$args) {
1769 $n = count($args);
1770 for ($i = 0; $i < $n;) {
1771 $color = null;
1772 $str = $args[$i++];
1773 if (is_integer($str)) {
1774 $color = $str;
1775 if (self::$use_color) {
1776 print "\033[0;{$color}m";
1778 $str = $args[$i++];
1781 print $str;
1783 if (self::$use_color && !is_null($color)) {
1784 print "\033[0m";
1789 public static function sayTestpilot($test, $status, $stime, $etime) {
1790 $start = darray['op' => 'start', 'test' => $test];
1791 $end = darray['op' => 'test_done', 'test' => $test, 'status' => $status,
1792 'start_time' => $stime, 'end_time' => $etime];
1793 if ($status == 'failed') {
1794 $end['details'] = self::utf8Sanitize(Status::diffForTest($test));
1796 self::say($start, $end);
1799 public static function getResults() {
1800 return self::$results;
1803 /** Output is in the format expected by JsonTestRunner. */
1804 public static function say(...$args) {
1805 $data = array_map(
1806 $row ==> self::jsonEncode($row) . "\n",
1807 $args
1809 fwrite(STDERR, implode("", $data));
1812 public static function hasCursorControl() {
1813 // for runs on hudson-ci.org (aka jenkins).
1814 if (getenv("HUDSON_URL")) {
1815 return false;
1817 // for runs on travis-ci.org
1818 if (getenv("TRAVIS")) {
1819 return false;
1821 $stty = self::getSTTY();
1822 if (!$stty) {
1823 return false;
1825 return strpos($stty, 'erase = <undef>') === false;
1828 <<__Memoize>>
1829 public static function getSTTY() {
1830 $descriptorspec = darray[1 => varray["pipe", "w"], 2 => varray["pipe", "w"]];
1831 $pipes = null;
1832 $process = proc_open(
1833 'stty -a', $descriptorspec, inout $pipes, null, null,
1834 darray['suppress_errors' => true]
1836 $stty = stream_get_contents($pipes[1]);
1837 proc_close($process);
1838 return $stty;
1841 public static function utf8Sanitize($str) {
1842 if (!is_string($str)) {
1843 // We sometimes get called with the
1844 // return value of file_get_contents()
1845 // when fgc() has failed.
1846 return '';
1849 return UConverter::transcode($str, 'UTF-8', 'UTF-8');
1852 public static function jsonEncode($data) {
1853 // JSON_UNESCAPED_SLASHES is Zend 5.4+.
1854 if (defined("JSON_UNESCAPED_SLASHES")) {
1855 return json_encode($data, JSON_UNESCAPED_SLASHES);
1858 $json = json_encode($data);
1859 return str_replace('\\/', '/', $json);
1862 public static function getQueue() {
1863 if (!self::$queue) {
1864 if (self::$killed) error("Killed!");
1865 self::$queue = new Queue(self::$tmpdir);
1867 return self::$queue;
1871 function clean_intermediate_files($test, $options) {
1872 if (isset($options['no-clean'])) {
1873 return;
1875 $exts = varray[
1876 // normal test output will go here if we're run with --write-to-checkout
1877 'out',
1878 'diff',
1879 // repo mode tests
1880 'repo',
1881 // tests in --hhas-round-trip mode
1882 'round_trip.hhas',
1883 // tests in --hhbbc2 mode
1884 'before.round_trip.hhas',
1885 'after.round_trip.hhas',
1887 foreach ($exts as $ext) {
1888 if ($ext == 'repo') {
1889 $file = test_repo($options, $test);
1890 } else {
1891 $file = "$test.$ext";
1893 if (is_dir($file)) {
1894 Status::removeDirectory($file);
1895 } else if (file_exists($file)) {
1896 unlink($file);
1899 $tmp_exts = varray[
1900 // normal test output goes here by default
1901 'out',
1902 'diff',
1903 // scratch directory the test may write to
1904 'tmpdir',
1905 // temporary autoloader DB and associated cruft
1906 // We have at most two modes for now - see hhvm_cmd_impl
1907 'autoloadDB.0',
1908 'autoloadDB.0-journal',
1909 'autoloadDB.0-shm',
1910 'autoloadDB.0-wal',
1911 'autoloadDB.1',
1912 'autoloadDB.1-journal',
1913 'autoloadDB.1-shm',
1914 'autoloadDB.1-wal',
1916 foreach ($tmp_exts as $ext) {
1917 $file = Status::getTestTmpPath($test, $ext);
1918 if (is_dir($file)) {
1919 Status::removeDirectory($file);
1920 } else if (file_exists($file)) {
1921 unlink($file);
1926 function run($options, $tests, $bad_test_file) {
1927 foreach ($tests as $test) {
1928 run_and_lock_test($options, $test);
1930 file_put_contents($bad_test_file, json_encode(Status::getResults()));
1931 foreach (Status::getResults() as $result) {
1932 if ($result['status'] == 'failed') {
1933 return 1;
1936 return 0;
1939 function is_hack_file($options, $test) {
1940 if (substr($test, -3) === '.hh') return true;
1942 $file = fopen($test, 'r');
1943 if ($file === false) return false;
1945 // Skip lines that are a shebang or whitespace.
1946 while (($line = fgets($file)) !== false) {
1947 $line = trim($line);
1948 if ($line === '' || substr($line, 0, 2) === '#!') continue;
1949 // Allow partial and strict, but don't count decl files as Hack code
1950 if ($line === '<?hh' || $line === '<?hh //strict') return true;
1951 break;
1953 fclose($file);
1955 return false;
1958 function skip_test($options, $test, $run_skipif = true): ?string {
1959 if (isset($options['hack-only']) &&
1960 substr($test, -5) !== '.hhas' &&
1961 !is_hack_file($options, $test)) {
1962 return 'skip-hack-only';
1965 if ((isset($options['cli-server']) || isset($options['server'])) &&
1966 !can_run_server_test($test, $options)) {
1967 return 'skip-server';
1970 if (isset($options['hhas-round-trip']) && substr($test, -5) === ".hhas") {
1971 return 'skip-hhas';
1974 if (isset($options['hhbbc2']) || isset($options['hhas-round-trip'])) {
1975 $no_hhas_tag = 'nodumphhas';
1976 if (file_exists("$test.$no_hhas_tag") ||
1977 file_exists(dirname($test).'/'.$no_hhas_tag)) {
1978 return 'skip-nodumphhas';
1980 if (file_exists($test . ".verify")) {
1981 return 'skip-verify';
1985 if (has_multi_request_mode($options) || isset($options['repo']) ||
1986 isset($options['server'])) {
1987 if (file_exists($test . ".verify")) {
1988 return 'skip-verify';
1990 if (find_debug_config($test, 'hphpd.ini')) {
1991 return 'skip-debugger';
1995 if (!$run_skipif) return null;
1996 $skipif_test = find_test_ext($test, 'skipif');
1997 if (!$skipif_test) {
1998 return null;
2001 // For now, run the .skipif in non-repo since building a repo for it is hard.
2002 $options_without_repo = $options;
2003 unset($options_without_repo['repo']);
2005 list($hhvm, $_) = hhvm_cmd($options_without_repo, $test, $skipif_test);
2006 // running .skipif, arbitrarily picking a mode
2007 $hhvm = $hhvm[0];
2009 $descriptorspec = darray[
2010 0 => varray["pipe", "r"],
2011 1 => varray["pipe", "w"],
2012 2 => varray["pipe", "w"],
2014 $pipes = null;
2015 $process = proc_open("$hhvm $test 2>&1", $descriptorspec, inout $pipes);
2016 if (!is_resource($process)) {
2017 // This is weird. We can't run HHVM but we probably shouldn't skip the test
2018 // since on a broken build everything will show up as skipped and give you a
2019 // SHIPIT.
2020 return null;
2023 fclose($pipes[0]);
2024 $output = stream_get_contents($pipes[1]);
2025 fclose($pipes[1]);
2026 proc_close($process);
2028 // The standard php5 .skipif semantics is if the .skipif outputs ANYTHING
2029 // then it should be skipped. This is a poor design, but I'll just add a
2030 // small blacklist of things that are really bad if they are output so we
2031 // surface the errors in the tests themselves.
2032 if (stripos($output, 'segmentation fault') !== false) {
2033 return null;
2036 return strlen($output) === 0 ? null : 'skip-skipif';
2039 function comp_line($l1, $l2, $is_reg) {
2040 if ($is_reg) {
2041 return preg_match('/^'. $l1 . '$/s', $l2);
2042 } else {
2043 return !strcmp($l1, $l2);
2047 function count_array_diff($ar1, $ar2, $is_reg, $idx1, $idx2, $cnt1, $cnt2,
2048 $steps) {
2049 $equal = 0;
2051 while ($idx1 < $cnt1 && $idx2 < $cnt2 && comp_line($ar1[$idx1], $ar2[$idx2],
2052 $is_reg)) {
2053 $idx1++;
2054 $idx2++;
2055 $equal++;
2056 $steps--;
2058 if (--$steps > 0) {
2059 $eq1 = 0;
2060 $st = $steps / 2;
2062 for ($ofs1 = $idx1 + 1; $ofs1 < $cnt1 && $st-- > 0; $ofs1++) {
2063 $eq = @count_array_diff($ar1, $ar2, $is_reg, $ofs1, $idx2, $cnt1,
2064 $cnt2, $st);
2066 if ($eq > $eq1) {
2067 $eq1 = $eq;
2071 $eq2 = 0;
2072 $st = $steps;
2074 for ($ofs2 = $idx2 + 1; $ofs2 < $cnt2 && $st-- > 0; $ofs2++) {
2075 $eq = @count_array_diff($ar1, $ar2, $is_reg, $idx1, $ofs2, $cnt1, $cnt2, $st);
2076 if ($eq > $eq2) {
2077 $eq2 = $eq;
2081 if ($eq1 > $eq2) {
2082 $equal += $eq1;
2083 } else if ($eq2 > 0) {
2084 $equal += $eq2;
2088 return $equal;
2091 function generate_array_diff($ar1, $ar2, $is_reg, $w) {
2092 $idx1 = 0; $cnt1 = @count($ar1);
2093 $idx2 = 0; $cnt2 = @count($ar2);
2094 $old1 = darray[];
2095 $old2 = darray[];
2097 while ($idx1 < $cnt1 && $idx2 < $cnt2) {
2098 if (comp_line($ar1[$idx1], $ar2[$idx2], $is_reg)) {
2099 $idx1++;
2100 $idx2++;
2101 continue;
2102 } else {
2103 $c1 = @count_array_diff($ar1, $ar2, $is_reg, $idx1+1, $idx2, $cnt1,
2104 $cnt2, 10);
2105 $c2 = @count_array_diff($ar1, $ar2, $is_reg, $idx1, $idx2+1, $cnt1,
2106 $cnt2, 10);
2108 if ($c1 > $c2) {
2109 $old1[$idx1] = sprintf("%03d- ", $idx1+1) . $w[$idx1++];
2110 } else if ($c2 > 0) {
2111 $old2[$idx2] = sprintf("%03d+ ", $idx2+1) . $ar2[$idx2++];
2112 } else {
2113 $old1[$idx1] = sprintf("%03d- ", $idx1+1) . $w[$idx1++];
2114 $old2[$idx2] = sprintf("%03d+ ", $idx2+1) . $ar2[$idx2++];
2119 $diff = varray[];
2120 $old1_keys = array_keys($old1);
2121 $old2_keys = array_keys($old2);
2122 $old1_values = array_values($old1);
2123 $old2_values = array_values($old2);
2124 // these start at -2 so $l1 + 1 and $l2 + 1 are not valid indices
2125 $l1 = -2;
2126 $l2 = -2;
2127 $iter1 = 0; $end1 = count($old1);
2128 $iter2 = 0; $end2 = count($old2);
2130 while ($iter1 < $end1 || $iter2 < $end2) {
2131 $k1 = $iter1 < $end1 ? $old1_keys[$iter1] : null;
2132 $k2 = $iter2 < $end2 ? $old2_keys[$iter2] : null;
2133 if ($k1 == $l1 + 1 || $k2 === null) {
2134 $l1 = $k1;
2135 $diff[] = $old1_values[$iter1++];
2136 } else if ($k2 == $l2 + 1 || $k1 === null) {
2137 $l2 = $k2;
2138 $diff[] = $old2_values[$iter2++];
2139 } else if ($k1 < $k2) {
2140 $l1 = $k1;
2141 $diff[] = $old1_values[$iter1++];
2142 } else {
2143 $l2 = $k2;
2144 $diff[] = $old2_values[$iter2++];
2148 while ($idx1 < $cnt1) {
2149 $diff[] = sprintf("%03d- ", $idx1 + 1) . $w[$idx1++];
2152 while ($idx2 < $cnt2) {
2153 $diff[] = sprintf("%03d+ ", $idx2 + 1) . $ar2[$idx2++];
2156 return $diff;
2159 function generate_diff($wanted, $wanted_re, $output)
2161 $m = null;
2162 $w = explode("\n", $wanted);
2163 $o = explode("\n", $output);
2164 if (is_null($wanted_re)) {
2165 $r = $w;
2166 } else {
2167 if (preg_match_with_matches('/^\((.*)\)\{(\d+)\}$/s', $wanted_re, inout $m)) {
2168 $t = explode("\n", $m[1]);
2169 $r = varray[];
2170 $w2 = varray[];
2171 for ($i = 0; $i < $m[2]; $i++) {
2172 foreach ($t as $v) {
2173 $r[] = $v;
2175 foreach ($w as $v) {
2176 $w2[] = $v;
2179 $w = $wanted === $wanted_re ? $r : $w2;
2180 } else {
2181 $r = explode("\n", $wanted_re);
2184 $diff = generate_array_diff($r, $o, !is_null($wanted_re), $w);
2186 return implode("\r\n", $diff);
2189 function dump_hhas_cmd(string $hhvm_cmd, $test, $hhas_file) {
2190 $dump_flags = implode(' ', varray[
2191 '-vEval.AllowHhas=true',
2192 '-vEval.DumpHhas=1',
2193 '-vEval.DumpHhasToFile='.escapeshellarg($hhas_file),
2194 '-vEval.LoadFilepathFromUnitCache=0',
2196 $cmd = str_replace(' -- ', " $dump_flags -- ", $hhvm_cmd);
2197 if ($cmd == $hhvm_cmd) $cmd .= " $dump_flags";
2198 return $cmd;
2201 function dump_hhas_to_temp(string $hhvm_cmd, $test) {
2202 $temp_file = $test . '.round_trip.hhas';
2203 $cmd = dump_hhas_cmd($hhvm_cmd, $test, $temp_file);
2204 $ret = -1;
2205 system("$cmd &> /dev/null", inout $ret);
2206 return $ret === 0 ? $temp_file : false;
2209 const SERVER_EXCLUDE_PATHS = vec[
2210 'quick/xenon/',
2211 'slow/streams/',
2212 'slow/ext_mongo/',
2213 'slow/ext_oauth/',
2214 'slow/ext_vsdebug/',
2215 'zend/good/ext/standard/tests/array/',
2217 const HHAS_EXT = '.hhas';
2218 function can_run_server_test($test, $options) {
2219 // explicitly disabled
2220 if (is_file("$test.noserver") ||
2221 (is_file("$test.nowebserver") && isset($options['server']))) {
2222 return false;
2225 // has its own config
2226 if (find_test_ext($test, 'opts') || is_file("$test.ini") ||
2227 is_file("$test.use.for.ini.migration.testing.only.hdf")) {
2228 return false;
2231 // we can't run repo only tests in server modes
2232 if (is_file("$test.onlyrepo") || is_file("$test.onlyjumpstart")) {
2233 return false;
2236 foreach (SERVER_EXCLUDE_PATHS as $path) {
2237 if (strpos($test, $path) !== false) return false;
2240 // don't run hhas tests in server modes
2241 if (strrpos($test, HHAS_EXT) === (strlen($test) - strlen(HHAS_EXT))) {
2242 return false;
2245 return true;
2248 const SERVER_TIMEOUT = 45;
2249 function run_config_server($options, $test) {
2250 invariant(can_run_server_test($test, $options), "skip_test should have skipped this");
2252 Status::createTestTmpDir($test); // force it to be created
2253 $config = find_file_for_dir(dirname($test), 'config.ini');
2254 $port = $options['servers']['configs'][$config]->server['port'];
2255 $ch = curl_init("localhost:$port/$test");
2256 curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
2257 curl_setopt($ch, CURLOPT_TIMEOUT, SERVER_TIMEOUT);
2258 curl_setopt($ch, CURLOPT_BINARYTRANSFER, true);
2259 $output = curl_exec($ch);
2260 if ($output is string) {
2261 $output = trim($output);
2262 } else {
2263 $output = "Error talking to server: " . curl_error($ch);
2265 curl_close($ch);
2267 return run_config_post(varray[$output, ''], $test, $options);
2270 function run_config_cli(
2271 $options,
2272 $test,
2273 string $cmd,
2274 darray<string, mixed> $cmd_env,
2276 $cmd = timeout_prefix() . $cmd;
2278 $cmd_env['HPHP_TEST_TMPDIR'] = Status::createTestTmpDir($test);
2279 if (isset($options['log'])) {
2280 $cmd_env['TRACE'] = 'printir:1';
2281 $cmd_env['HPHP_TRACE_FILE'] = $test . '.log';
2284 $descriptorspec = darray[
2285 0 => varray["pipe", "r"],
2286 1 => varray["pipe", "w"],
2287 2 => varray["pipe", "w"],
2289 $pipes = null;
2290 $process = proc_open(
2291 "$cmd 2>&1", $descriptorspec, inout $pipes, null, $cmd_env
2293 if (!is_resource($process)) {
2294 Status::writeDiff($test, "Couldn't invoke $cmd");
2295 return false;
2298 fclose($pipes[0]);
2299 $output = stream_get_contents($pipes[1]);
2300 $output = trim($output);
2301 $stderr = stream_get_contents($pipes[2]);
2302 fclose($pipes[1]);
2303 fclose($pipes[2]);
2304 proc_close($process);
2306 return varray[$output, $stderr];
2309 function replace_object_resource_ids($str, $replacement) {
2310 $str = preg_replace(
2311 '/(object\([^)]+\)#)\d+/', '\1'.$replacement, $str
2313 return preg_replace(
2314 '/resource\(\d+\)/', "resource($replacement)", $str
2318 function run_config_post($outputs, $test, $options) {
2319 $output = $outputs[0];
2320 $stderr = $outputs[1];
2321 file_put_contents(Status::getTestOutputPath($test, 'out'), $output);
2323 $check_hhbbc_error = isset($options['repo'])
2324 && file_exists($test . '.hhbbc_assert');
2326 // hhvm redirects errors to stdout, so anything on stderr is really bad.
2327 if ($stderr && !$check_hhbbc_error) {
2328 Status::writeDiff(
2329 $test,
2330 "Test failed because the process wrote on stderr:\n$stderr"
2332 return false;
2335 // Needed for testing non-hhvm binaries that don't actually run the code
2336 // e.g. parser/test/parse_tester.cpp.
2337 if ($output == "FORCE PASS") {
2338 return true;
2341 $repeats = 0;
2342 if (!$check_hhbbc_error) {
2343 if (isset($options['retranslate-all'])) {
2344 $repeats = $options['retranslate-all'] * 2;
2347 if (isset($options['recycle-tc'])) {
2348 $repeats = $options['recycle-tc'];
2351 if (isset($options['cli-server'])) {
2352 $repeats = 3;
2356 list($file, $type) = get_expect_file_and_type($test, $options);
2357 if ($file === null || $type === null) {
2358 Status::writeDiff(
2359 $test,
2360 "No $test.expect, $test.expectf, $test.hhvm.expect, " .
2361 "$test.hhvm.expectf, or $test.expectregex. " .
2362 "If $test is meant to be included by other tests, " .
2363 "use a different file extension.\n"
2365 return false;
2368 if ($type === 'expect' || $type === 'hhvm.expect') {
2369 $wanted = trim(file_get_contents($file));
2370 if (isset($options['ignore-oids']) || isset($options['repo'])) {
2371 $output = replace_object_resource_ids($output, 'n');
2372 $wanted = replace_object_resource_ids($wanted, 'n');
2375 if (!$repeats) {
2376 $passed = !strcmp($output, $wanted);
2377 if (!$passed) {
2378 Status::writeDiff($test, generate_diff($wanted, null, $output));
2380 return $passed;
2382 $wanted_re = preg_quote($wanted, '/');
2383 } else if ($type === 'expectf' || $type === 'hhvm.expectf') {
2384 $wanted = trim(file_get_contents($file));
2385 if (isset($options['ignore-oids']) || isset($options['repo'])) {
2386 $wanted = replace_object_resource_ids($wanted, '%d');
2388 $wanted_re = $wanted;
2390 // do preg_quote, but miss out any %r delimited sections.
2391 $temp = "";
2392 $r = "%r";
2393 $startOffset = 0;
2394 $length = strlen($wanted_re);
2395 while ($startOffset < $length) {
2396 $start = strpos($wanted_re, $r, $startOffset);
2397 if ($start !== false) {
2398 // we have found a start tag.
2399 $end = strpos($wanted_re, $r, $start+2);
2400 if ($end === false) {
2401 // unbalanced tag, ignore it.
2402 $end = $start = $length;
2404 } else {
2405 // no more %r sections.
2406 $start = $end = $length;
2408 // quote a non re portion of the string.
2409 $temp = $temp.preg_quote(substr($wanted_re, $startOffset,
2410 ($start - $startOffset)), '/');
2411 // add the re unquoted.
2412 if ($end > $start) {
2413 $temp = $temp.'('.substr($wanted_re, $start+2, ($end - $start-2)).')';
2415 $startOffset = $end + 2;
2417 $wanted_re = $temp;
2419 $wanted_re = str_replace(
2420 varray['%binary_string_optional%'],
2421 'string',
2422 $wanted_re
2424 $wanted_re = str_replace(
2425 varray['%unicode_string_optional%'],
2426 'string',
2427 $wanted_re
2429 $wanted_re = str_replace(
2430 varray['%unicode\|string%', '%string\|unicode%'],
2431 'string',
2432 $wanted_re
2434 $wanted_re = str_replace(
2435 varray['%u\|b%', '%b\|u%'],
2437 $wanted_re
2439 // Stick to basics.
2440 $wanted_re = str_replace('%e', '\\' . DIRECTORY_SEPARATOR, $wanted_re);
2441 $wanted_re = str_replace('%s', '[^\r\n]+', $wanted_re);
2442 $wanted_re = str_replace('%S', '[^\r\n]*', $wanted_re);
2443 $wanted_re = str_replace('%a', '.+', $wanted_re);
2444 $wanted_re = str_replace('%A', '.*', $wanted_re);
2445 $wanted_re = str_replace('%w', '\s*', $wanted_re);
2446 $wanted_re = str_replace('%i', '[+-]?\d+', $wanted_re);
2447 $wanted_re = str_replace('%d', '\d+', $wanted_re);
2448 $wanted_re = str_replace('%x', '[0-9a-fA-F]+', $wanted_re);
2449 // %f allows two points "-.0.0" but that is the best *simple* expression.
2450 $wanted_re = str_replace('%f', '[+-]?\.?\d+\.?\d*(?:[Ee][+-]?\d+)?',
2451 $wanted_re);
2452 $wanted_re = str_replace('%c', '.', $wanted_re);
2453 // must be last.
2454 $wanted_re = str_replace('%%', '%%?', $wanted_re);
2456 // Normalize newlines.
2457 $wanted_re = preg_replace("/(\r\n?|\n)/", "\n", $wanted_re);
2458 $output = preg_replace("/(\r\n?|\n)/", "\n", $output);
2459 } else if ($type === 'expectregex') {
2460 $wanted_re = trim(file_get_contents($file));
2461 } else {
2462 throw new Exception("Unsupported expect file type: ".$type);
2465 if ($repeats) {
2466 $wanted_re = "($wanted_re\s*)".'{'.$repeats.'}';
2468 if (!isset($wanted)) $wanted = $wanted_re;
2469 $passed = @preg_match("/^$wanted_re\$/s", $output);
2470 if ($passed) return true;
2471 if ($passed === false && $repeats) {
2472 // $repeats can cause the regex to become too big, and fail
2473 // to compile.
2474 return 'skip-repeats-fail';
2476 $diff = generate_diff($wanted_re, $wanted_re, $output);
2477 if ($passed === false && $diff === "") {
2478 // the preg match failed, probably because the regex was too complex,
2479 // but since the line by line diff came up empty, we're fine
2480 return true;
2482 Status::writeDiff($test, $diff);
2483 return false;
2486 function timeout_prefix() {
2487 if (is_executable('/usr/bin/timeout')) {
2488 return '/usr/bin/timeout ' . TIMEOUT_SECONDS . ' ';
2489 } else {
2490 return hphp_home() . '/hphp/tools/timeout.sh -t ' . TIMEOUT_SECONDS . ' ';
2494 function run_foreach_config(
2495 $options,
2496 $test,
2497 varray<string> $cmds,
2498 darray<string, mixed> $cmd_env,
2500 invariant(count($cmds) > 0, "run_foreach_config: no modes");
2501 foreach ($cmds as $cmd) {
2502 $outputs = run_config_cli($options, $test, $cmd, $cmd_env);
2503 if ($outputs === false) return false;
2504 $result = run_config_post($outputs, $test, $options);
2505 if (!$result) return $result;
2507 return $result;
2510 function run_and_lock_test($options, $test) {
2511 $stime = time();
2512 $time = microtime(true);
2513 $failmsg = "";
2514 $status = false;
2515 $lock = fopen($test, 'r');
2516 $wouldblock = false;
2517 if (!$lock || !flock($lock, LOCK_EX, inout $wouldblock)) {
2518 $failmsg = "Failed to lock test";
2519 if ($lock) fclose($lock);
2520 $lock = null;
2521 } else {
2522 $status = run_test($options, $test);
2524 $time = microtime(true) - $time;
2525 $etime = time();
2526 if ($lock) {
2527 if ($status) {
2528 clean_intermediate_files($test, $options);
2529 } else if ($failmsg === '') {
2530 $failmsg = Status::diffForTest($test);
2531 if (!$failmsg) $failmsg = "Test failed with empty diff";
2533 if (!flock($lock, LOCK_UN, inout $wouldblock)) {
2534 if ($failmsg !== '') $failmsg .= "\n";
2535 $failmsg .= "Failed to release test lock";
2536 $status = false;
2538 if (!fclose($lock)) {
2539 if ($failmsg !== '') $failmsg .= "\n";
2540 $failmsg .= "Failed to close lock file";
2541 $status = false;
2544 if ($status === false) {
2545 invariant($failmsg !== '', "test failed with empty failmsg");
2546 Status::fail($test, $time, $stime, $etime, $failmsg);
2547 } else if ($status === true) {
2548 invariant($failmsg === '', "test passed with non-empty failmsg $failmsg");
2549 Status::pass($test, $time, $stime, $etime);
2550 } else if ($status is string) {
2551 invariant(
2552 preg_match('/^skip-[\w-]+$/', $status),
2553 "invalid skip status $status"
2555 invariant($failmsg === '', "test skipped with non-empty failmsg $failmsg");
2556 Status::skip($test, substr($status, 5), $time, $stime, $etime);
2557 } else {
2558 invariant_violation("invalid status type " . gettype($status));
2562 function run_test($options, $test) {
2563 $skip_reason = skip_test($options, $test);
2564 if ($skip_reason !== null) return $skip_reason;
2566 list($hhvm, $hhvm_env) = hhvm_cmd($options, $test);
2568 if (preg_grep('/ --count[ =][0-9]+ .* --count[ =][0-9]+( |$)/', $hhvm)) {
2569 // we got --count from 2 sources (e.g. .opts file and multi_request_mode)
2570 // this can't work so skip the test
2571 return 'skip-count';
2572 } else if (isset($options['jit-serialize'])) {
2573 // jit-serialize adds the --count option later, so even 1 --count in the
2574 // command means we have to skip
2575 if (preg_grep('/ --count[ =][0-9]+( |$)/', $hhvm)) {
2576 return 'skip-count';
2580 if (isset($options['repo'])) {
2581 if (file_exists($test.'.norepo')) {
2582 return 'skip-norepo';
2584 if (file_exists($test.'.onlyjumpstart') &&
2585 (!isset($options['jit-serialize']) || $options['jit-serialize'] < 1)) {
2586 return 'skip-onlyjumpstart';
2589 $test_repo = test_repo($options, $test);
2590 $hphp_hhvm_repo = "$test_repo/hhvm.hhbc";
2591 $hhbbc_hhvm_repo = "$test_repo/hhvm.hhbbc";
2592 $hphp_hackc_repo = "$test_repo/hackc.hhbc";
2593 $hhbbc_hackc_repo = "$test_repo/hackc.hhbbc";
2594 shell_exec("rm -f \"$hphp_hhvm_repo\" \"$hhbbc_hhvm_repo\" \"$hphp_hackc_repo\" \"$hhbbc_hackc_repo\" ");
2596 $program = isset($options['hackc']) ? "hackc" : "hhvm";
2598 if (file_exists($test . '.hhbbc_assert')) {
2599 $hphp = hphp_cmd($options, $test, $program);
2600 if (repo_separate($options, $test)) {
2601 $result = exec_with_stack($hphp);
2602 if ($result !== true) return false;
2603 $hhbbc = hhbbc_cmd($options, $test, $program);
2604 return run_foreach_config($options, $test, varray[$hhbbc], $hhvm_env);
2605 } else {
2606 return run_foreach_config($options, $test, varray[$hphp], $hhvm_env);
2610 if (!repo_mode_compile($options, $test, $program)) {
2611 return false;
2614 if (isset($options['hhbbc2'])) {
2615 invariant(
2616 count($hhvm) === 1,
2617 "get_options forbids modes because we're not runnig code"
2619 $hhas_temp1 = dump_hhas_to_temp($hhvm[0], "$test.before");
2620 if ($hhas_temp1 === false) {
2621 Status::writeDiff($test, "dumping hhas after first hhbbc pass failed");
2622 return false;
2624 shell_exec("mv $test_repo/$program.hhbbc $test_repo/$program.hhbc");
2625 $hhbbc = hhbbc_cmd($options, $test, $program);
2626 $result = exec_with_stack($hhbbc);
2627 if ($result !== true) {
2628 Status::writeDiff($test, $result);
2629 return false;
2631 $hhas_temp2 = dump_hhas_to_temp($hhvm[0], "$test.after");
2632 if ($hhas_temp2 === false) {
2633 Status::writeDiff($test, "dumping hhas after second hhbbc pass failed");
2634 return false;
2636 $diff = shell_exec("diff $hhas_temp1 $hhas_temp2");
2637 if (trim($diff) !== '') {
2638 Status::writeDiff($test, $diff);
2639 return false;
2643 if (isset($options['jit-serialize'])) {
2644 invariant(count($hhvm) === 1, 'get_options enforces jit mode only');
2645 $cmd = jit_serialize_option($hhvm[0], $test, $options, true);
2646 $outputs = run_config_cli($options, $test, $cmd, $hhvm_env);
2647 if ($outputs === false) return false;
2648 $hhvm[0] = jit_serialize_option($hhvm[0], $test, $options, false);
2651 return run_foreach_config($options, $test, $hhvm, $hhvm_env);
2654 if (file_exists($test.'.onlyrepo')) {
2655 return 'skip-onlyrepo';
2657 if (file_exists($test.'.onlyjumpstart')) {
2658 return 'skip-onlyjumpstart';
2661 if (isset($options['hhas-round-trip'])) {
2662 invariant(substr($test, -5) !== ".hhas", "skip_test should have skipped");
2663 // dumping hhas, not running code so arbitrarily picking a mode
2664 $hhas_temp = dump_hhas_to_temp($hhvm[0], $test);
2665 if ($hhas_temp === false) {
2666 $err = "system failed: " .
2667 dump_hhas_cmd($hhvm[0], $test, $test.'.round_trip.hhas') .
2668 "\n";
2669 Status::writeDiff($test, $err);
2670 return false;
2672 list($hhvm, $hhvm_env) = hhvm_cmd($options, $test, $hhas_temp);
2675 if (isset($options['server'])) {
2676 return run_config_server($options, $test);
2678 return run_foreach_config($options, $test, $hhvm, $hhvm_env);
2681 function num_cpus() {
2682 switch(PHP_OS) {
2683 case 'Linux':
2684 $data = file('/proc/stat');
2685 $cores = 0;
2686 foreach($data as $line) {
2687 if (preg_match('/^cpu[0-9]/', $line)) {
2688 $cores++;
2691 return $cores;
2692 case 'Darwin':
2693 case 'FreeBSD':
2694 $output = null;
2695 $return_var = -1;
2696 return exec('sysctl -n hw.ncpu', inout $output, inout $return_var);
2698 return 2; // default when we don't know how to detect.
2701 function make_header($str) {
2702 return "\n\033[0;33m".$str."\033[0m\n";
2705 function print_commands($tests, $options) {
2706 if (isset($options['verbose'])) {
2707 print make_header("Run these by hand:");
2708 } else {
2709 $test = $tests[0];
2710 print make_header("Run $test by hand:");
2711 $tests = varray[$test];
2714 foreach ($tests as $test) {
2715 list($commands, $_) = hhvm_cmd($options, $test);
2716 if (!isset($options['repo'])) {
2717 foreach ($commands as $c) {
2718 print "$c\n";
2720 continue;
2723 // How to run it with hhbbc:
2724 $program = isset($options['hackc']) ? "hackc" : "hhvm";
2725 $hhbbc_cmds = hphp_cmd($options, $test, $program)."\n";
2726 if (repo_separate($options, $test)) {
2727 $hhbbc_cmd = hhbbc_cmd($options, $test, $program)."\n";
2728 $hhbbc_cmds .= $hhbbc_cmd;
2729 if (isset($options['hhbbc2'])) {
2730 foreach ($commands as $c) {
2731 $hhbbc_cmds .=
2732 $c." -vEval.DumpHhas=1 > $test.before.round_trip.hhas\n";
2734 $hhbbc_cmds .=
2735 "mv $test_repo/$program.hhbbc $test_repo/$program.hhbc\n";
2736 $hhbbc_cmds .= $hhbbc_cmd;
2737 foreach ($commands as $c) {
2738 $hhbbc_cmds .=
2739 $c." -vEval.DumpHhas=1 > $test.after.round_trip.hhas\n";
2741 $hhbbc_cmds .=
2742 "diff $test.before.round_trip.hhas $test.after.round_trip.hhas\n";
2745 if (isset($options['jit-serialize'])) {
2746 invariant(count($commands) === 1, 'get_options enforces jit mode only');
2747 $hhbbc_cmds .=
2748 jit_serialize_option($commands[0], $test, $options, true) . "\n";
2749 $commands[0] = jit_serialize_option($commands[0], $test, $options, false);
2751 foreach ($commands as $c) {
2752 $hhbbc_cmds .= $c."\n";
2754 print "$hhbbc_cmds\n";
2758 // This runs only in the "printer" child.
2759 function msg_loop($num_tests, $queue) {
2760 $do_progress =
2762 Status::getMode() === Status::MODE_NORMAL ||
2763 Status::getMode() === Status::MODE_RECORD_FAILURES
2764 ) &&
2765 Status::hasCursorControl();
2767 if ($do_progress) {
2768 $stty = strtolower(Status::getSTTY());
2769 $matches = null;
2770 if (preg_match_with_matches("/columns ([0-9]+);/", $stty, inout $matches) ||
2771 // because BSD has to be different
2772 preg_match_with_matches("/([0-9]+) columns;/", $stty, inout $matches)) {
2773 $cols = $matches[1];
2774 } else {
2775 $do_progress = false;
2779 while (true) {
2780 list($pid, $type, $message) = $queue->receiveMessage();
2781 if (!Status::handle_message($type, $message)) break;
2783 if ($do_progress) {
2784 $total_run = (Status::$skipped + Status::$failed + Status::$passed);
2785 $bar_cols = ($cols - 45);
2787 $passed_ticks = round($bar_cols * (Status::$passed / $num_tests));
2788 $skipped_ticks = round($bar_cols * (Status::$skipped / $num_tests));
2789 $failed_ticks = round($bar_cols * (Status::$failed / $num_tests));
2791 $fill = $bar_cols - ($passed_ticks + $skipped_ticks + $failed_ticks);
2792 if ($fill < 0) $fill = 0;
2794 $fill = str_repeat('-', (int)$fill);
2796 $passed_ticks = str_repeat('#', (int)$passed_ticks);
2797 $skipped_ticks = str_repeat('#', (int)$skipped_ticks);
2798 $failed_ticks = str_repeat('#', (int)$failed_ticks);
2800 echo
2801 "\033[2K\033[1G[",
2802 "\033[0;32m$passed_ticks",
2803 "\033[33m$skipped_ticks",
2804 "\033[31m$failed_ticks",
2805 "\033[0m$fill] ($total_run/$num_tests) ",
2806 "(", Status::$skipped, " skipped,", Status::$failed, " failed)";
2810 if ($do_progress) {
2811 print "\033[2K\033[1G";
2812 if (Status::$skipped > 0) {
2813 print Status::$skipped ." tests \033[1;33mskipped\033[0m\n";
2814 $reasons = Status::$skip_reasons;
2815 arsort(inout $reasons);
2816 Status::$skip_reasons = $reasons;
2817 foreach (Status::$skip_reasons as $reason => $count) {
2818 printf("%12s: %d\n", $reason, $count);
2824 function print_success($tests, $results, $options) {
2825 // We didn't run any tests, not even skipped. Clowntown!
2826 if (!$tests) {
2827 print "\nCLOWNTOWN: No tests!\n";
2828 if (!($options['no-fun'] ?? false)) {
2829 print <<<CLOWN
2832 /*\\
2833 /_*_\\
2834 {('o')}
2835 C{{([^*^])}}D
2836 [ * ]
2837 / Y \\
2838 _\\__|__/_
2839 (___/ \\___)
2840 CLOWN
2841 ."\n\n";
2844 /* Emacs' syntax highlighting gets confused by that clown and this comment
2845 * resets whatever state got messed up. */
2846 return;
2848 $ran_tests = false;
2849 foreach ($results as $result) {
2850 // The result here will either be skipped or passed (since failed is
2851 // handled in print_failure.
2852 if ($result['status'] == 'passed') {
2853 $ran_tests = true;
2854 break;
2857 // We just had skipped tests
2858 if (!$ran_tests) {
2859 print "\nSKIP-ALOO: Only skipped tests!\n";
2860 if (!($options['no-fun'] ?? false)) {
2861 print <<<SKIPPER
2865 / ,"
2866 .-------.--- /
2867 "._ __.-/ o. o\
2868 " ( Y )
2872 .-" |
2873 / _ \ \
2874 / `. ". ) /' )
2875 Y )( / /(,/
2876 ,| / )
2877 ( | / /
2878 " \_ (__ (__
2879 "-._,)--._,)
2880 SKIPPER
2881 ."\n\n";
2884 /* Emacs' syntax highlighting may get confused by the skipper and this
2885 * rcomment esets whatever state got messed up. */
2886 return;
2888 print "\nAll tests passed.\n";
2889 if (!($options['no-fun'] ?? false)) {
2890 print <<<SHIP
2891 | | |
2892 )_) )_) )_)
2893 )___))___))___)\
2894 )____)____)_____)\\
2895 _____|____|____|____\\\__
2896 ---------\ SHIP IT /---------
2897 ^^^^^ ^^^^^^^^^^^^^^^^^^^^^
2898 ^^^^ ^^^^ ^^^ ^^
2899 ^^^^ ^^^
2900 SHIP
2901 ."\n";
2903 if ($options['failure-file'] ?? false) {
2904 @unlink($options['failure-file']);
2906 if (isset($options['verbose'])) {
2907 print_commands($tests, $options);
2911 function print_failure($argv, $results, $options) {
2912 $failed = varray[];
2913 $passed = varray[];
2914 foreach ($results as $result) {
2915 if ($result['status'] === 'failed') {
2916 $failed[] = $result['name'];
2918 if ($result['status'] === 'passed') {
2919 $passed[] = $result['name'];
2922 sort(inout $failed);
2924 $failing_tests_file = ($options['failure-file'] ?? false)
2925 ? $options['failure-file']
2926 : Status::getRunTmpDir() . '/test-failures';
2927 file_put_contents($failing_tests_file, implode("\n", $failed)."\n");
2928 if ($passed) {
2929 $passing_tests_file = ($options['success-file'] ?? false)
2930 ? $options['success-file']
2931 : Status::getRunTmpDir() . '/tests-passed';
2932 file_put_contents($passing_tests_file, implode("\n", $passed)."\n");
2935 print "\n".count($failed)." tests failed\n";
2936 if (!($options['no-fun'] ?? false)) {
2937 // Unicode for table-flipping emoticon
2938 // https://knowyourmeme.com/memes/flipping-tables
2939 print "(\u{256F}\u{00B0}\u{25A1}\u{00B0}\u{FF09}\u{256F}\u{FE35} \u{253B}";
2940 print "\u{2501}\u{253B}\n";
2943 print_commands($failed, $options);
2945 print make_header("See failed test output and expectations:");
2946 foreach ($failed as $n => $test) {
2947 if ($n !== 0) print "\n";
2948 print 'cat ' . Status::getTestOutputPath($test, 'diff') . "\n";
2949 print 'cat ' . Status::getTestOutputPath($test, 'out') . "\n";
2950 $expect_file = get_expect_file_and_type($test, $options)[0];
2951 if ($expect_file is null) {
2952 print "# no expect file found for $test\n";
2953 } else {
2954 print "cat $expect_file\n";
2957 // only print 3 tests worth unless verbose is on
2958 if ($n === 2 && !isset($options['verbose'])) {
2959 $remaining = count($failed) - 1 - $n;
2960 if ($remaining > 0) {
2961 print make_header("... and $remaining more.");
2963 break;
2967 if ($passed) {
2968 print make_header(
2969 'For xargs, lists of failed and passed tests are available using:'
2971 print 'cat '.$failing_tests_file."\n";
2972 print 'cat '.$passing_tests_file."\n";
2973 } else {
2974 print make_header('For xargs, list of failures is available using:').
2975 'cat '.$failing_tests_file."\n";
2978 print
2979 make_header("Re-run just the failing tests:") .
2980 str_replace("run.php", "run", $argv[0]) . ' ' .
2981 implode(' ', \HH\global_get('recorded_options')) .
2982 sprintf(' $(cat %s)%s', $failing_tests_file, "\n");
2985 function port_is_listening($port) {
2986 $socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
2987 return @socket_connect($socket, 'localhost', $port);
2990 function find_open_port() {
2991 for ($i = 0; $i < 50; ++$i) {
2992 $port = rand(1024, 65535);
2993 if (!port_is_listening($port)) return $port;
2996 error("Couldn't find an open port");
2999 function start_server_proc($options, $config, $port) {
3000 if (isset($options['cli-server'])) {
3001 $cli_sock = tempnam(sys_get_temp_dir(), 'hhvm-cli-');
3002 } else {
3003 // still want to test that an unwritable socket works...
3004 $cli_sock = '/var/run/hhvm-cli.sock';
3006 $threads = get_num_threads($options, PHP_INT_MAX);
3007 $thread_option = isset($options['cli-server'])
3008 ? '-vEval.UnixServerWorkers='.$threads
3009 : '-vServer.ThreadCount='.$threads;
3010 $prelude = isset($options['server'])
3011 ? '-vEval.PreludePath=' . Status::getRunTmpDir() . '/server-prelude.php'
3012 : "";
3013 $command = hhvm_cmd_impl(
3014 $options,
3015 $config,
3016 null, // we do not pass Autoload.DBPath to the server process
3017 '-m', 'server',
3018 "-vServer.Port=$port",
3019 "-vServer.Type=proxygen",
3020 "-vAdminServer.Port=0",
3021 $thread_option,
3022 '-vServer.ExitOnBindFail=1',
3023 '-vServer.RequestTimeoutSeconds='.SERVER_TIMEOUT,
3024 '-vPageletServer.ThreadCount=0',
3025 '-vLog.UseRequestLog=1',
3026 '-vLog.File=/dev/null',
3027 $prelude,
3029 // The server will unlink the temp file
3030 '-vEval.UnixServerPath='.$cli_sock,
3032 // This ensures we actually jit everything:
3033 '-vEval.JitRequireWriteLease=1',
3035 // The default test config uses a small TC but we'll be running thousands
3036 // of tests against the same process:
3037 '-vEval.JitASize=142606336',
3038 '-vEval.JitAProfSize=251658240',
3039 '-vEval.JitAColdSize=201326592',
3040 '-vEval.JitAFrozenSize=251658240',
3041 '-vEval.JitGlobalDataSize=32000000',
3043 // load/store counters don't work on Ivy Bridge so disable for tests
3044 '-vEval.ProfileHWEnable=false'
3046 if (count($command) !== 1) {
3047 error("Can't run multi-mode tests in server mode");
3049 $command = $command[0];
3050 if (getenv('HHVM_TEST_SERVER_LOG')) {
3051 echo "Starting server '$command'\n";
3054 $descriptors = darray[
3055 0 => varray['file', '/dev/null', 'r'],
3056 1 => varray['file', '/dev/null', 'w'],
3057 2 => varray['file', '/dev/null', 'w'],
3060 $dummy = null;
3061 $proc = proc_open($command, $descriptors, inout $dummy);
3062 if (!$proc) {
3063 error("Failed to start server process");
3065 $status = proc_get_status($proc);
3066 $status['proc'] = $proc;
3067 $status['port'] = $port;
3068 $status['config'] = $config;
3069 $status['cli-socket'] = $cli_sock;
3070 return $status;
3073 final class ServerRef {
3074 public function __construct(public $server) {
3079 * For each config file in $configs, start up a server on a randomly-determined
3080 * port. Return value is an array mapping pids and config files to arrays of
3081 * information about the server.
3083 function start_servers($options, $configs) {
3084 if (isset($options['server'])) {
3085 $prelude = <<<'EOT'
3086 <?hh
3087 <<__EntryPoint>> function UNIQUE_NAME_I_DONT_EXIST_IN_ANY_TEST(): void {
3088 putenv("HPHP_TEST_TMPDIR=BASEDIR{$_SERVER['SCRIPT_NAME']}.tmpdir");
3090 EOT;
3091 file_put_contents(
3092 Status::getRunTmpDir() . '/server-prelude.php',
3093 str_replace('BASEDIR', Status::getRunTmpDir(), $prelude),
3097 $starting = varray[];
3098 foreach ($configs as $config) {
3099 $starting[] = start_server_proc($options, $config, find_open_port());
3102 $start_time = microtime(true);
3103 $servers = darray['pids' => darray[], 'configs' => darray[]];
3105 // Wait for all servers to come up.
3106 while (count($starting) > 0) {
3107 $still_starting = varray[];
3109 foreach ($starting as $server) {
3110 $new_status = proc_get_status($server['proc']);
3112 if (!$new_status['running']) {
3113 if ($new_status['exitcode'] === 0) {
3114 error("Server exited prematurely but without error");
3117 // We lost a race. Try another port.
3118 if (getenv('HHVM_TEST_SERVER_LOG')) {
3119 echo "\n\nLost connection race on port $port. Trying another.\n\n";
3121 $still_starting[] =
3122 start_server_proc($options, $server['config'], find_open_port());
3123 } else if (!port_is_listening($server['port'])) {
3124 $still_starting[] = $server;
3125 } else {
3126 $ref = new ServerRef($server);
3127 $servers['pids'][$server['pid']] = $ref;
3128 $servers['configs'][$server['config']] = $ref;
3132 $starting = $still_starting;
3133 $max_time = 10;
3134 if (microtime(true) - $start_time > $max_time) {
3135 error("Servers took more than $max_time seconds to come up");
3138 // Take a short nap and try again.
3139 usleep(100000);
3142 $elapsed = microtime(true) - $start_time;
3143 printf("Started %d servers in %.1f seconds\n\n", count($configs), $elapsed);
3144 return $servers;
3147 function get_num_threads($options, $tests) {
3148 if (isset($options['threads'])) {
3149 $threads = (int)$options['threads'];
3150 if ((string)$threads !== $options['threads'] || $threads < 1) {
3151 error("--threads must be an integer >= 1");
3153 } else {
3154 $threads = isset($options['server']) || isset($options['cli-server'])
3155 ? num_cpus() * 2 : num_cpus();
3157 return min(count($tests), $threads);
3160 function runner_precheck() {
3161 // basic checking for runner.
3162 if (!((bool)$_SERVER ?? false) || !((bool)$_ENV ?? false)) {
3163 echo "Warning: \$_SERVER/\$_ENV variables not available, please check \n" .
3164 "your ini setting: variables_order, it should have both 'E' and 'S'\n";
3168 function main($argv) {
3169 runner_precheck();
3171 ini_set('pcre.backtrack_limit', PHP_INT_MAX);
3173 list($options, $files) = get_options($argv);
3174 if (isset($options['help'])) {
3175 error(help());
3177 if (isset($options['list-tests'])) {
3178 success(list_tests($files, $options));
3181 $tests = find_tests($files, $options);
3182 if (isset($options['shuffle'])) {
3183 shuffle($tests);
3186 // Explicit path given by --hhvm-binary-path takes priority. Then, if an
3187 // HHVM_BIN env var exists, and the file it points to exists, that trumps
3188 // any default hhvm executable path.
3189 if (isset($options['hhvm-binary-path'])) {
3190 $binary_path = check_executable($options['hhvm-binary-path']);
3191 putenv("HHVM_BIN=" . $binary_path);
3192 } else if (getenv("HHVM_BIN") !== false) {
3193 $binary_path = check_executable(getenv("HHVM_BIN"));
3194 } else {
3195 check_for_multiple_default_binaries();
3196 $binary_path = hhvm_path();
3199 if (isset($options['verbose'])) {
3200 print "You are using the binary located at: " . $binary_path . "\n";
3203 Status::createTmpDir();
3205 $servers = null;
3206 if (isset($options['server']) || isset($options['cli-server'])) {
3207 if (isset($options['server']) && isset($options['cli-server'])) {
3208 error("Server mode and CLI Server mode are mutually exclusive");
3210 if (isset($options['repo'])) {
3211 error("Server mode repo tests are not supported");
3214 /* We need to start up a separate server process for each config file
3215 * found. */
3216 $configs = keyset[];
3217 foreach ($tests as $test) {
3218 $config = find_file_for_dir(dirname($test), 'config.ini');
3219 if (!$config) {
3220 error("Couldn't find config file for $test");
3222 if (array_key_exists($config, $configs)) continue;
3223 if (skip_test($options, $test, false) !== null) continue;
3224 $configs[] = $config;
3227 $max_configs = 30;
3228 if (count($configs) > $max_configs) {
3229 error("More than $max_configs unique config files will be needed to run ".
3230 "the tests you specified. They may not be a good fit for server ".
3231 "mode. (".count($configs)." required)");
3234 $servers = $options['servers'] = start_servers($options, $configs);
3237 // Try to construct the buckets so the test results are ready in
3238 // approximately alphabetical order.
3239 // Get the serial tests to be in their own bucket later.
3240 $serial_tests = serial_only_tests($tests);
3242 // If we have no serial tests, we can use the maximum number of allowed
3243 // threads for the test running. If we have some, we save one thread for
3244 // the serial bucket. However if we only have one thread, we don't split
3245 // out serial tests.
3246 $parallel_threads = get_num_threads($options, $tests);
3247 if ($parallel_threads === 1) {
3248 $test_buckets = varray[$tests];
3249 } else {
3250 if (count($serial_tests) > 0) {
3251 // reserve a thread for serial tests
3252 $parallel_threads--;
3255 $test_buckets = varray[];
3256 for ($i = 0; $i < $parallel_threads; $i++) {
3257 $test_buckets[] = varray[];
3260 $i = 0;
3261 foreach ($tests as $test) {
3262 if (!in_array($test, $serial_tests)) {
3263 $test_buckets[$i][] = $test;
3264 $i = ($i + 1) % $parallel_threads;
3268 if (count($serial_tests) > 0) {
3269 // The last bucket is serial.
3270 $test_buckets[] = $serial_tests;
3274 // Remember that the serial tests are also in the tests array too,
3275 // so they are part of the total count.
3276 if (!isset($options['testpilot'])) {
3277 print "Running ".count($tests)." tests in ".
3278 count($test_buckets)." threads (" . count($serial_tests) .
3279 " in serial)\n";
3282 if (isset($options['verbose'])) {
3283 Status::setMode(Status::MODE_VERBOSE);
3285 if (isset($options['testpilot'])) {
3286 Status::setMode(Status::MODE_TESTPILOT);
3288 if (isset($options['record-failures'])) {
3289 Status::setMode(Status::MODE_RECORD_FAILURES);
3291 Status::setUseColor(isset($options['color']) ? true : posix_isatty(STDOUT));
3293 Status::$nofork = count($tests) == 1 && !$servers;
3295 if (!Status::$nofork) {
3296 // Create the Queue before any children are forked.
3297 $queue = Status::getQueue();
3299 // Fork a "printer" child to process status messages.
3300 $printer_pid = pcntl_fork();
3301 if ($printer_pid == -1) {
3302 error("failed to fork");
3303 } else if ($printer_pid == 0) {
3304 msg_loop(count($tests), $queue);
3305 return 0;
3309 // NOTE: This unblocks the Queue (if needed).
3310 Status::started();
3312 // Fork "worker" children (if needed).
3313 $children = darray[];
3314 // A poor man's shared memory.
3315 $bad_test_files = varray[];
3316 if (Status::$nofork) {
3317 Status::registerCleanup(isset($options['no-clean']));
3318 $bad_test_file = tempnam('/tmp', 'test-run-');
3319 $bad_test_files[] = $bad_test_file;
3320 invariant(count($test_buckets) === 1, "nofork was set erroneously");
3321 $return_value = run($options, $test_buckets[0], $bad_test_file);
3322 } else {
3323 foreach ($test_buckets as $test_bucket) {
3324 $bad_test_file = tempnam('/tmp', 'test-run-');
3325 $bad_test_files[] = $bad_test_file;
3326 $pid = pcntl_fork();
3327 if ($pid == -1) {
3328 error('could not fork');
3329 } else if ($pid) {
3330 $children[$pid] = $pid;
3331 } else {
3332 exit(run($options, $test_bucket, $bad_test_file));
3336 // Make sure to clean up on exit, or on SIGTERM/SIGINT.
3337 // Do this here so no children inherit this.
3338 Status::registerCleanup(isset($options['no-clean']));
3340 // Have the parent wait for all forked children to exit.
3341 $return_value = 0;
3342 while (count($children) && $printer_pid != 0) {
3343 $status = null;
3344 $pid = pcntl_wait(inout $status);
3345 if (!pcntl_wifexited($status) && !pcntl_wifsignaled($status)) {
3346 error("Unexpected exit status from child");
3349 if ($pid == $printer_pid) {
3350 // We should be finishing up soon.
3351 $printer_pid = 0;
3352 } else if ($servers && isset($servers['pids'][$pid])) {
3353 // A server crashed. Restart it.
3354 if (getenv('HHVM_TEST_SERVER_LOG')) {
3355 echo "\nServer $pid crashed. Restarting.\n";
3357 Status::serverRestarted();
3358 $ref = $servers['pids'][$pid];
3359 $ref->server =
3360 start_server_proc($options, $ref->server['config'], $ref->server['port']);
3362 // Unset the old $pid entry and insert the new one.
3363 unset($servers['pids'][$pid]);
3364 $servers['pids'][$ref->server['pid']] = $ref;
3365 } elseif (isset($children[$pid])) {
3366 unset($children[$pid]);
3367 $return_value |= pcntl_wexitstatus($status);
3368 } // Else, ignorable signal
3372 Status::finished($return_value);
3374 // Wait for the printer child to die, if needed.
3375 if (!Status::$nofork && $printer_pid != 0) {
3376 $status = 0;
3377 $pid = pcntl_waitpid($printer_pid, inout $status);
3378 if (!pcntl_wifexited($status) && !pcntl_wifsignaled($status)) {
3379 error("Unexpected exit status from child");
3383 // Kill the servers.
3384 if ($servers) {
3385 foreach ($servers['pids'] as $ref) {
3386 proc_terminate($ref->server['proc']);
3387 proc_close($ref->server['proc']);
3391 // aggregate results
3392 $results = darray[];
3393 foreach ($bad_test_files as $bad_test_file) {
3394 $json = json_decode(file_get_contents($bad_test_file), true);
3395 if (!is_array($json)) {
3396 error(
3397 "\nNo JSON output was received from a test thread. ".
3398 "Either you killed it, or it might be a bug in the test script."
3401 $results = array_merge($results, $json);
3402 unlink($bad_test_file);
3405 // print results
3406 if (isset($options['record-failures'])) {
3407 $fail_file = $options['record-failures'];
3408 $failed_tests = varray[];
3409 $prev_failing = varray[];
3410 if (file_exists($fail_file)) {
3411 $prev_failing = explode("\n", file_get_contents($fail_file));
3414 $new_fails = 0;
3415 $new_passes = 0;
3416 foreach ($results as $r) {
3417 if (!isset($r['name']) || !isset($r['status'])) continue;
3418 $test = canonical_path($r['name']);
3419 $status = $r['status'];
3420 if ($status === 'passed' && in_array($test, $prev_failing)) {
3421 $new_passes++;
3422 continue;
3424 if ($status !== 'failed') continue;
3425 if (!in_array($test, $prev_failing)) $new_fails++;
3426 $failed_tests[] = $test;
3428 printf(
3429 "Recording %d tests as failing.\n".
3430 "There are %d new failing tests, and %d new passing tests.\n",
3431 count($failed_tests), $new_fails, $new_passes
3433 sort(inout $failed_tests);
3434 file_put_contents($fail_file, implode("\n", $failed_tests));
3435 } else if (isset($options['testpilot'])) {
3436 Status::say(darray['op' => 'all_done', 'results' => $results]);
3437 return $return_value;
3438 } else if (!$return_value) {
3439 print_success($tests, $results, $options);
3440 } else {
3441 print_failure($argv, $results, $options);
3444 Status::sayColor("\nTotal time for all executed tests as run: ",
3445 Status::BLUE,
3446 sprintf("%.2fs\n",
3447 Status::getOverallEndTime() -
3448 Status::getOverallStartTime()));
3449 Status::sayColor("Total time for all executed tests if run serially: ",
3450 Status::BLUE,
3451 sprintf("%.2fs\n",
3452 Status::addTestTimesSerial($results)));
3454 return $return_value;
3457 <<__EntryPoint>>
3458 function run_main(): void {
3459 exit(main(\HH\global_get('argv')));