3 * Run the test suites in various configurations.
6 use namespace HH\Lib\C
;
8 const int TIMEOUT_SECONDS
= 300;
10 function get_argv(): vec
<string> {
11 return \HH\FIXME\UNSAFE_CAST
<vec
<mixed>,vec
<string>>(
12 \HH\
global_get('argv') as vec
<_
>
16 function mtime(): float {
17 return microtime(true) as float;
20 // The "HPHP_HOME" environment variable can be set (to ".../fbcode"), to
21 // define "hphp_home()" and (indirectly) "test_dir()". Otherwise, we will use
22 // "__DIR__" as "test_dir()", and its grandparent directory for "hphp_home()"
23 // (unless we are testing a dso extensions).
26 function is_testing_dso_extension(): bool {
27 $home = getenv("HPHP_HOME");
28 if ($home is
string) {
31 // detecting if we're running outside of the hhvm codebase.
32 return !is_file(__DIR__
."/../../hphp/test/run.php");
36 function hphp_home(): string {
37 $home = getenv("HPHP_HOME");
38 if ($home is
string) {
39 return realpath($home);
41 if (is_testing_dso_extension()) {
42 return realpath(__DIR__
);
44 return realpath(__DIR__
."/../..");
48 function test_dir(): string {
49 $home = getenv("HPHP_HOME");
50 if ($home is
string) {
51 return realpath($home)."/hphp/test";
56 function get_expect_file_and_type(
68 if (file_exists($test . '.hphpc_assert')) {
69 return vec
[$test . '.hphpc_assert', 'expectf'];
71 if (file_exists($test . '.hhbbc_assert')) {
72 return vec
[$test . '.hhbbc_assert', 'expectf'];
74 foreach ($types as $type) {
75 $fname = "$test.$type-repo";
76 if (file_exists($fname)) {
77 return vec
[$fname, $type];
82 foreach ($types as $type) {
83 $fname = "$test.$type";
84 if (file_exists($fname)) {
85 return vec
[$fname, $type];
88 return vec
[null, null];
91 function multi_request_modes(Options
$options): vec
<string> {
93 if ($options->retranslate_all is nonnull
) $r []= 'retranslate-all';
94 if ($options->recycle_tc is nonnull
) $r []= 'recycle-tc';
95 if ($options->jit_serialize is nonnull
) $r []= 'jit-serialize';
96 if ($options->cli_server
) $r []= 'cli-server';
100 function has_multi_request_mode(Options
$options): bool {
101 return count(multi_request_modes($options)) != 0;
104 function test_repo(Options
$options, string $test): string {
105 if ($options->repo_out is nonnull
) {
106 return $options->repo_out
. '/' . str_replace('/', '.', $test) . '.repo';
108 return Status
::getTestTmpPath($test, 'repo');
111 function jit_serialize_option(
112 string $cmd, string $test, Options
$options, bool $serialize,
114 $serialized = test_repo($options, $test) . "/jit.dump";
115 $cmds = explode(' -- ', $cmd, 2);
116 $jit_serialize = (int)($options->jit_serialize ??
0);
118 ' --count=' . ($serialize ?
$jit_serialize +
1 : 1) .
119 " -vEval.JitSerdesFile=\"" . $serialized . "\"" .
120 " -vEval.JitSerdesMode=" . ($serialize ?
'Serialize' : 'DeserializeOrFail') .
121 ($serialize ?
" -vEval.JitSerializeOptProfRequests=" . $jit_serialize : '');
122 if ($options->jitsample is nonnull
&& $serialize) {
123 $cmds[0] .= ' -vDeploymentId="' . $options->jitsample
. '-serialize"';
125 return implode(' -- ', $cmds);
128 function usage(): string {
130 return "usage: {$argv[0]} [-m jit|interp] [-r] <test/directories>";
133 function help(): string {
135 $ztestexample = 'test/zend/good/*/*z*.php'; // sep. for syntax highlighting.
139 This is the hhvm test-suite runner. For more detailed documentation,
140 see hphp/test/README.md.
142 The test argument may be a path to a php test file, a directory name, or
143 one of a few pre-defined suite names that this script knows about.
145 If you work with hhvm a lot, you might consider a bash alias:
147 alias ht="path/to/hphp/test/run"
151 # Quick tests in JIT mode:
152 % {$argv[0]} test/quick
154 # Slow tests in interp mode:
155 % {$argv[0]} -m interp test/slow
157 # PHP specification tests in JIT mode:
158 % {$argv[0]} test/slow/spec
160 # Slow closure tests in JIT mode:
161 % {$argv[0]} test/slow/closure
163 # Slow closure tests in JIT mode with RepoAuthoritative:
164 % {$argv[0]} -r test/slow/closure
166 # Slow array tests, in RepoAuthoritative:
167 % {$argv[0]} -r test/slow/array
169 # Zend tests with a "z" in their name:
170 % {$argv[0]} $ztestexample
172 # Quick tests in JIT mode with some extra runtime options:
173 % {$argv[0]} test/quick -a '-vEval.JitMaxTranslations=120 -vEval.HHIRRefcountOpts=0'
175 # Quick tests in JIT mode with RepoAuthoritative and an extra compile-time option:
176 % {$argv[0]} test/quick -r --compiler-args '--parse-on-demand=false'
178 # All quick tests except debugger
179 % {$argv[0]} -e debugger test/quick
181 # All tests except those containing a string of 3 digits
182 % {$argv[0]} -E '/\d{3}/' all
184 # All tests whose name containing pdo_mysql
185 % {$argv[0]} -i pdo_mysql -m jit -r zend
187 # Print all the standard tests
188 % {$argv[0]} --list-tests
190 # Use a specific HHVM binary
191 % {$argv[0]} -b ~/code/hhvm/hphp/hhvm/hhvm
192 % {$argv[0]} --hhvm-binary-path ~/code/hhvm/hphp/hhvm/hhvm
194 # Use retranslate all. Run the test n times, then run retranslate all, then
195 # run the test n more on the new code.
196 % {$argv[0]} --retranslate-all 2 quick
198 # Use jit-serialize. Run the test n times, then run retranslate all, run the
199 # test once more, serialize all profile data, and then restart hhvm, load the
200 # serialized state and run retranslate-all before starting the test.
201 % {$argv[0]} --jit-serialize 2 -r quick
204 return usage().$help;
207 function error(string $message): noreturn
{
212 // If a user-supplied path is provided, let's make sure we have a valid
213 // executable. Returns canonicanalized path or exits.
214 function check_executable(string $path): string {
215 $rpath = realpath($path);
216 if ($rpath === false ||
!is_executable($rpath)) {
217 error("Provided HHVM executable ($path) is not an executable file.\n" .
218 "If using HHVM_BIN, make sure that is set correctly.");
223 exec($rpath . " --version 2> /dev/null", inout
$output, inout
$return_var);
224 if (strpos(implode("", $output), "HipHop ") !== 0) {
225 error("Provided file ($rpath) is not an HHVM executable.\n" .
226 "If using HHVM_BIN, make sure that is set correctly.");
232 function hhvm_binary_routes(): dict
<string, string> {
234 "buck" => "/buck-out/gen/hphp/hhvm/hhvm",
235 "buck2" => "/../buck-out/v2/gen/fbcode/hphp/hhvm/out",
236 "cmake" => "/hphp/hhvm"
240 function hh_codegen_binary_routes(): dict
<string, string> {
242 "buck" => "/buck-out/bin/hphp/hack/src/hh_single_compile",
243 "cmake" => "/hphp/hack/bin"
247 // For Facebook: We have several build systems, and we can use any of them in
248 // the same code repo. If multiple binaries exist, we want the onus to be on
249 // the user to specify a particular one because before we chose the buck one
250 // by default and that could cause unexpected results.
251 function check_for_multiple_default_binaries(): void
{
252 // Env var we use in testing that'll pick which build system to use.
253 if (getenv("FBCODE_BUILD_TOOL") !== false) {
259 foreach (hhvm_binary_routes() as $path) {
260 $abs_path = $home . $path . "/hhvm";
261 if (file_exists($abs_path)) {
262 $found[] = $abs_path;
266 if (count($found) <= 1) {
270 $msg = "Multiple binaries exist in this repo. \n";
271 foreach ($found as $bin) {
272 $msg .= " - " . $bin . "\n";
274 $msg .= "Are you in fbcode? If so, remove a binary \n"
275 . "or use the --hhvm-binary-path option to the test runner. \n"
276 . "e.g. test/run --hhvm-binary-path /path/to/binary slow\n";
280 function hhvm_path(): string {
282 $hhvm_bin = getenv("HHVM_BIN");
283 if ($hhvm_bin is
string) {
284 $file = realpath($hhvm_bin);
286 $file = bin_root().'/hhvm';
289 if (!is_file($file)) {
290 if (is_testing_dso_extension()) {
293 exec("which hhvm 2> /dev/null", inout
$output, inout
$return_var);
294 if (isset($output[0]) && $output[0]) {
297 error("You need to specify hhvm bin with env HHVM_BIN");
300 error("$file doesn't exist. Did you forget to build first?");
302 return rel_path($file);
305 function bin_root(): string {
306 $hhvm_bin = getenv("HHVM_BIN");
307 if ($hhvm_bin is
string) {
308 return dirname(realpath($hhvm_bin));
312 $env_tool = getenv("FBCODE_BUILD_TOOL");
313 $routes = hhvm_binary_routes();
315 if ($env_tool !== false) {
316 return $home . $routes[$env_tool];
319 foreach ($routes as $_ => $path) {
320 $dir = $home . $path;
326 return $home . $routes["cmake"];
329 function unit_cache_file(): string {
330 return Status
::getTmpPathFile('unit-cache.sql');
333 function read_opts_file(?
string $file): string {
334 if ($file is
null ||
!file_exists($file)) {
337 $fp = fopen($file, "r");
338 invariant($fp is
resource, "%s", __METHOD__
);
341 for ($line = fgets($fp); $line; $line = fgets($fp)) {
342 // Compress out white space.
343 $line = preg_replace('/\s+/', ' ', $line);
345 // Discard simple line oriented ; and # comments to end of line
346 // Comments at end of line (after payload) are not allowed.
347 $line = preg_replace('/^ *;.*$/', ' ', $line);
348 $line = preg_replace('/^ *#.*$/', ' ', $line);
350 // Substitute in the directory name
351 $line = str_replace('__DIR__', dirname($file), $line);
359 // http://stackoverflow.com/questions/2637945/
360 function rel_path(string $to): string {
361 $from = explode('/', getcwd().'/');
362 $to = explode('/', $to);
363 $from_len = count($from);
364 $to_len = count($to);
366 // find first non-matching dir.
367 for ($d = 0; $d < $from_len; ++
$d) {
368 if ($d >= $to_len ||
$from[$d] !== $to[$d])
374 // get number of remaining dirs in $from.
375 $remaining = $from_len - $d - 1;
376 if ($remaining > 0) {
377 // add traversals up to first matching dir.
381 } while ($remaining > 0);
385 while ($d < $to_len) {
386 $relPath[] = $to[$d];
389 return implode('/', $relPath);
392 // Keep this in sync with the dict in get_options() below.
393 // Options taking a value (with a trailing `:` in the dict key)
394 // should be ?string. Otherwise they should be bool.
395 final class Options
{
397 public ?
string $exclude;
398 public ?
string $exclude_pattern;
399 public ?
string $exclude_recorded_failures;
400 public ?
string $include;
401 public ?
string $include_pattern;
402 public bool $repo = false;
403 public bool $split_hphpc = false;
404 public bool $repo_single = false;
405 public bool $repo_separate = false;
406 public ?
string $repo_threads;
407 public ?
string $repo_out;
408 public bool $hhbbc2 = false;
409 public ?
string $mode;
410 public bool $server = false;
411 public bool $cli_server = false;
412 public bool $shuffle = false;
413 public bool $help = false;
414 public bool $verbose = false;
415 public bool $testpilot = false;
416 public ?
string $threads;
417 public ?
string $args;
418 public ?
string $compiler_args;
419 public bool $log = false;
420 public ?
string $failure_file;
421 public bool $wholecfg = false;
422 public bool $hhas_round_trip = false;
423 public bool $color = false;
424 public bool $no_fun = false;
425 public bool $no_skipif = false;
426 public bool $cores = false;
427 public bool $dump_tc = false;
428 public bool $no_clean = false;
429 public bool $list_tests = false;
430 public ?
string $recycle_tc;
431 public ?
string $retranslate_all;
432 public ?
string $jit_serialize;
433 public ?
string $hhvm_binary_path;
434 public ?
string $vendor;
435 public ?
string $record_failures;
436 public ?
string $ignore_oids;
437 public ?
string $jitsample;
438 public ?
string $hh_single_type_check;
439 public bool $write_to_checkout = false;
440 public bool $bespoke = false;
441 public bool $lazyclass = false;
443 // Additional state added for convenience since Options is plumbed
444 // around almost everywhere.
445 public ?Servers
$servers = null;
448 function get_options(
450 ): (Options
, vec
<string>) {
451 // Options marked * affect test behavior, and need to be reported by list_tests.
452 // Options with a trailing : take a value.
456 'exclude-pattern:' => 'E:',
457 'exclude-recorded-failures:' => 'x:',
459 'include-pattern:' => 'I:',
461 '*split-hphpc' => '',
462 '*repo-single' => '',
463 '*repo-separate' => '',
464 '*repo-threads:' => '',
469 '*cli-server' => 'S',
476 '*compiler-args:' => '',
478 'failure-file:' => '',
480 '*hhas-round-trip' => '',
488 '*recycle-tc:' => '',
489 '*retranslate-all:' => '',
490 '*jit-serialize:' => '',
491 '*hhvm-binary-path:' => 'b:',
493 'record-failures:' => '',
494 '*ignore-oids' => '',
496 '*hh_single_type_check:' => '',
497 'write-to-checkout' => '',
500 $options = new Options() as dynamic
;
505 * '-' argument causes all future arguments to be treated as filenames, even
506 * if they would otherwise match a valid option. Otherwise, arguments starting
507 * with '-' MUST match a valid option.
511 for ($i = 1; $i < count($argv); $i++
) {
514 if (strlen($arg) === 0) {
516 } else if ($force_file) {
518 } else if ($arg === '-') {
520 } else if ($arg[0] === '-') {
523 foreach ($parameters as $long => $short) {
524 if ($arg == '-'.str_replace(':', '', $short) ||
525 $arg == '--'.str_replace(vec
[':', '*'], vec
['', ''], $long)) {
526 $record = substr($long, 0, 1) === '*';
527 if ($record) $recorded[] = $arg;
528 if (substr($long, -1, 1) === ':') {
531 if ($record) $recorded[] = $value;
535 $name = str_replace(vec
[':', '*', '-'], vec
['', '', '_'], $long);
536 $options->{$name} = $value;
543 $msg = sprintf("Invalid argument: '%s'\nSee %s --help", $arg, $argv[0]);
544 error($msg as string);
550 $options = $options as Options
;
552 \HH\
global_set('recorded_options', $recorded);
554 $repo_out = $options->repo_out
;
555 if ($repo_out is
string && !is_dir($repo_out)) {
556 if (!mkdir($repo_out) && !is_dir($repo_out)) {
557 error("Unable to create repo-out dir " . $repo_out);
560 if ($options->hhbbc2
) {
561 $options->repo_separate
= true;
562 if ($options->repo ||
$options->repo_single
) {
563 error("repo-single/repo and hhbbc2 are mutually exclusive options");
565 if (isset($options['mode'])) {
566 error("hhbbc2 doesn't support modes; it compares hhas, doesn't run code");
570 if ($options->repo_single ||
$options->repo_separate
) {
571 $options->repo
= true;
572 } else if ($options->repo
) {
573 // if only repo was set, then it means repo single
574 $options->repo_single
= true;
577 if ($options->jit_serialize is nonnull
) {
578 if (!$options->repo
) {
579 error("jit-serialize only works in repo mode");
581 if ($options->mode is nonnull
&& $options->mode
!== 'jit') {
582 error("jit-serialize only works in jit mode");
586 if ($options->split_hphpc
) {
587 if (!$options->repo
) {
588 error("split-hphpc only works in repo mode");
590 if (!$options->repo_separate
) {
591 error("split-hphpc only works in repo-separate mode");
595 if ($options->repo
&& $options->hhas_round_trip
) {
596 error("repo and hhas-round-trip are mutually exclusive options");
599 $multi_request_modes = multi_request_modes($options);
600 if (count($multi_request_modes) > 1) {
601 error("The options\n -" . implode("\n -", $multi_request_modes) .
602 "\nare mutually exclusive options");
605 if ($options->write_to_checkout
) {
606 Status
::$write_to_checkout = true;
609 return tuple($options, $files);
613 * Return the path to $test relative to $base, or false if $base does not
616 function canonical_path_from_base(string $test, string $base): mixed {
617 $full = realpath($test);
618 if (substr($full, 0, strlen($base)) === $base) {
619 return substr($full, strlen($base) +
1);
621 $dirstat = stat($base);
622 if (!is_dict($dirstat)) return false;
623 for ($p = dirname($full); $p && $p !== "/"; $p = dirname($p)) {
625 if (!is_dict($s)) continue;
626 if ($s['ino'] === $dirstat['ino'] && $s['dev'] === $dirstat['dev']) {
627 return substr($full, strlen($p) +
1);
633 function canonical_path(string $test): mixed {
634 $attempt = canonical_path_from_base($test, test_dir());
635 if ($attempt === false) {
636 return canonical_path_from_base($test, hphp_home());
643 * We support some 'special' file names, that just know where the test
644 * suites are, to avoid typing 'hphp/test/foo'.
646 function find_test_files(string $file): vec
<string>{
648 'quick' => 'hphp/test/quick',
649 'slow' => 'hphp/test/slow',
650 'debugger' => 'hphp/test/server/debugger/tests',
651 'http' => 'hphp/test/server/http/tests',
652 'fastcgi' => 'hphp/test/server/fastcgi/tests',
653 'zend' => 'hphp/test/zend/good',
654 'facebook' => 'hphp/facebook/test',
655 'taint' => 'hphp/test/taint',
657 // subset of slow we run with CLI server too
658 'slow_ext_hsl' => 'hphp/test/slow/ext_hsl',
660 // Subsets of zend tests.
661 'zend_ext' => 'hphp/test/zend/good/ext',
662 'zend_ext_am' => 'hphp/test/zend/good/ext/[a-m]*',
663 'zend_ext_nz' => 'hphp/test/zend/good/ext/[n-z]*',
664 'zend_Zend' => 'hphp/test/zend/good/Zend',
665 'zend_tests' => 'hphp/test/zend/good/tests',
668 $pattern = $mappage[$file] ??
null;
669 if ($pattern is nonnull
) {
670 $pattern = hphp_home().'/'.$pattern;
671 $matches = glob($pattern);
672 if (count($matches) === 0) {
674 "Convenience test name '$file' is recognized but does not match ".
675 "any test files (pattern = '$pattern')",
684 // Some tests have to be run together in the same test bucket, serially, one
685 // after other in order to avoid races and other collisions.
686 function serial_only_tests(vec
<string> $tests): vec
<string> {
687 if (is_testing_dso_extension()) {
690 // Add a <testname>.php.serial file to make your test run in the serial
692 $serial_tests = vec(array_filter(
695 return file_exists($test . '.serial');
698 return $serial_tests;
701 // If "files" is very long, then the shell may reject the desired
702 // "find" command (especially because "escapeshellarg()" adds two single
703 // quote characters to each file), so we split "files" into chunks below.
704 function exec_find(vec
<string> $files, string $extra): vec
<string> {
706 foreach (array_chunk($files, 500) as $chunk) {
707 $efa = implode(' ', array_map(
708 $line ==> escapeshellarg($line as string),
709 $chunk as dict
<_
, _
>,
711 $output = shell_exec("find $efa $extra");
712 foreach (explode("\n", $output) as $result) {
713 // Collect the (non-empty) results, which should all be file paths.
714 if ($result !== "") $results[] = $result;
725 $files = vec
['quick'];
727 if ($files == vec
['all']) {
728 $files = vec
['quick', 'slow', 'zend', 'fastcgi', 'http', 'debugger'];
729 if (is_dir(hphp_home() . '/hphp/facebook/test')) {
730 $files[] = 'facebook';
734 foreach ($files as $file) {
735 $ft = array_merge($ft, find_test_files($file));
738 foreach ($ft as $file) {
740 error("Not valid file or directory: '$file'");
742 $file = preg_replace(',//+,', '/', realpath($file));
743 $file = preg_replace(',^'.getcwd().'/,', '', $file);
750 "-o -name '*.hack' " .
751 "-o -name '*.hackpartial' " .
752 "-o -name '*.hhas' " .
753 "-o -name '*.php.type-errors' " .
754 "-o -name '*.hack.type-errors' " .
755 "-o -name '*.hackpartial.type-errors' " .
757 "-not -regex '.*round_trip[.]hhas'"
760 error("Could not find any tests associated with your options.\n" .
761 "Make sure your test path is correct and that you have " .
762 "the right expect files for the tests you are trying to run.\n" .
766 $tests = vec(array_filter($tests));
767 if ($options->exclude is nonnull
) {
768 $exclude = $options->exclude
;
769 $tests = vec(array_filter($tests, function($test) use ($exclude) {
770 return (false === strpos($test, $exclude));
773 if ($options->exclude_pattern is nonnull
) {
774 $exclude = $options->exclude_pattern
;
775 $tests = vec(array_filter($tests, function($test) use ($exclude) {
776 return !preg_match($exclude, $test);
779 if ($options->exclude_recorded_failures is nonnull
) {
780 $exclude_file = $options->exclude_recorded_failures
;
781 $exclude = file($exclude_file, FILE_IGNORE_NEW_LINES
);
782 $tests = vec(array_filter($tests, function($test) use ($exclude) {
783 return (false === in_array(canonical_path($test), $exclude));
786 if ($options->include is nonnull
) {
787 $include = $options->include;
788 $tests = vec(array_filter($tests, function($test) use ($include) {
789 return (false !== strpos($test, $include));
792 if ($options->include_pattern is nonnull
) {
793 $include = $options->include_pattern
;
794 $tests = vec(array_filter($tests, function($test) use ($include) {
795 return (bool)preg_match($include, $test);
801 function list_tests(vec
<string> $files, Options
$options): void
{
802 $args = implode(' ', \HH\
global_get('recorded_options'));
804 // Disable escaping of test info when listing. We check if the environment
805 // variable is set so we can make the change in a backwards compatible way.
806 $escape_info = getenv("LISTING_NO_ESCAPE") === false;
808 foreach (find_tests($files, $options) as $test) {
809 $test_info = Status
::jsonEncode(
810 dict
['args' => $args, 'name' => $test],
813 print str_replace('\\', '\\\\', $test_info)."\n";
815 print $test_info."\n";
820 function find_test_ext(
823 string $configName='config',
825 if (is_file("{$test}.{$ext}")) {
826 return "{$test}.{$ext}";
828 return find_file_for_dir(dirname($test), "{$configName}.{$ext}");
831 function find_file_for_dir(string $dir, string $name): ?
string {
832 // Handle the case where the $dir might come in as '.' because you
833 // are running the test runner on a file from the same directory as
834 // the test e.g., './mytest.php'. dirname() will give you the '.' when
835 // you actually have a lot of path to traverse upwards like
836 // /home/you/code/tests/mytest.php. Use realpath() to get that.
837 $dir = realpath($dir);
838 while ($dir !== '/' && is_dir($dir)) {
839 $file = "$dir/$name";
840 if (is_file($file)) {
843 $dir = dirname($dir);
845 $file = test_dir().'/'.$name;
846 if (file_exists($file)) {
852 function find_debug_config(string $test, string $name): string {
853 $debug_config = find_file_for_dir(dirname($test), $name);
854 if ($debug_config is nonnull
) {
855 return "-m debug --debug-config ".$debug_config;
860 function mode_cmd(Options
$options): vec
<string> {
862 if (!$options->repo
) {
863 $repo_args = "-vUnitFileCache.Path=".unit_cache_file();
865 $interp_args = "$repo_args -vEval.Jit=0";
866 $jit_args = "$repo_args -vEval.Jit=true";
867 $mode = $options->mode ??
'';
871 return vec
[$jit_args];
873 return vec
[$interp_args];
875 return vec
[$interp_args, $jit_args];
877 error("-m must be one of jit | interp | interp,jit. Got: '$mode'");
881 function extra_args(Options
$options): string {
882 $args = $options->args ??
'';
884 if ($options->vendor is nonnull
) {
885 $args .= ' -d auto_prepend_file=';
886 $args .= escapeshellarg($options->vendor
.'/hh_autoload.php');
892 function extra_compiler_args(Options
$options): string {
893 return $options->compiler_args ??
'';
896 function hhvm_cmd_impl(
899 ?
string $autoload_db_prefix,
900 string ...$extra_args
903 foreach (mode_cmd($options) as $mode_num => $mode) {
908 // EnableArgsInBacktraces disables most of HHBBC's DCE optimizations.
909 // In order to test those optimizations (which are part of a normal prod
910 // configuration) we turn this flag off by default.
911 '-vEval.EnableArgsInBacktraces=false',
912 '-vEval.EnableIntrinsicsExtension=true',
913 '-vEval.HHIRInliningIgnoreHints=false',
914 '-vEval.HHIRAlwaysInterpIgnoreHint=false',
915 '-vEval.FoldLazyClassKeys=false',
917 $options->wholecfg ?
'-vEval.JitPGORegionSelector=wholecfg' : '',
919 // load/store counters don't work on Ivy Bridge so disable for tests
920 '-vEval.ProfileHWEnable=false',
922 // use a fixed path for embedded data
923 '-vEval.EmbeddedDataExtractPath='
924 .escapeshellarg(bin_root().'/hhvm_%{type}_%{buildid}'),
926 // Stick to a single thread for retranslate-all
927 '-vEval.JitWorkerThreads=1',
928 '-vEval.JitWorkerThreadsForSerdes=1',
930 extra_args($options),
933 if ($autoload_db_prefix is nonnull
) {
935 '-vAutoload.DB.Path='.escapeshellarg("$autoload_db_prefix.$mode_num");
938 if ($options->retranslate_all is nonnull
) {
939 $args[] = '--count='.((int)$options->retranslate_all
* 2);
940 $args[] = '-vEval.JitPGO=true';
941 $args[] = '-vEval.JitRetranslateAllRequest='.$options->retranslate_all
;
942 // Set to timeout. We want requests to trigger retranslate all.
943 $args[] = '-vEval.JitRetranslateAllSeconds=' . TIMEOUT_SECONDS
;
946 if ($options->recycle_tc is nonnull
) {
947 $args[] = '--count='.$options->recycle_tc
;
948 $args[] = '-vEval.StressUnitCacheFreq=1';
949 $args[] = '-vEval.EnableReusableTC=true';
952 if ($options->jit_serialize is nonnull
) {
953 $args[] = '-vEval.JitPGO=true';
954 $args[] = '-vEval.JitRetranslateAllRequest='.$options->jit_serialize
;
955 // Set to timeout. We want requests to trigger retranslate all.
956 $args[] = '-vEval.JitRetranslateAllSeconds=' . TIMEOUT_SECONDS
;
959 if ($options->hhas_round_trip
) {
960 $args[] = '-vEval.AllowHhas=1';
961 $args[] = '-vEval.LoadFilepathFromUnitCache=1';
964 if (!$options->cores
) {
965 $args[] = '-vResourceLimit.CoreFileSize=0';
968 if ($options->dump_tc
) {
969 $args[] = '-vEval.DumpIR=1';
970 $args[] = '-vEval.DumpTC=1';
973 if ($options->hh_single_type_check is nonnull
) {
974 $args[] = '--hh_single_type_check='.$options->hh_single_type_check
;
977 if ($options->bespoke
) {
978 $args[] = '-vEval.BespokeArrayLikeMode=1';
979 $args[] = '-vServer.APC.MemModelTreadmill=true';
982 $cmds[] = implode(' ', array_merge($args, $extra_args));
987 function repo_separate(Options
$options, string $test): bool {
988 return $options->repo_separate
&&
989 !file_exists($test . ".hhbbc_opts");
992 // Return the command and the env to run it in.
996 ?
string $test_run = null,
997 bool $is_temp_file = false
998 ): (vec
<string>, dict
<string, mixed>) {
1000 // hdf support is only temporary until we fully migrate to ini
1001 // Discourage broad use.
1002 $hdf_suffix = ".use.for.ini.migration.testing.only.hdf";
1003 $hdf = file_exists($test.$hdf_suffix)
1004 ?
'-c ' . $test . $hdf_suffix
1006 $extra_opts = read_opts_file(find_test_ext($test, 'opts'));
1007 $config = find_test_ext($test, 'ini');
1008 invariant($config is nonnull
, "%s", __METHOD__
);
1009 $cmds = hhvm_cmd_impl(
1012 Status
::getTestTmpPath($test, 'autoloadDB'),
1014 find_debug_config($test, 'hphpd.ini'),
1016 $is_temp_file ?
" --temp-file" : "",
1018 escapeshellarg($test_run),
1023 if (file_exists($test.'.verify')) {
1024 $cmd .= " -m verify";
1027 if ($options->cli_server
) {
1028 $config = find_file_for_dir(dirname($test), 'config.ini');
1029 $servers = $options->servers
as Servers
;
1030 $server = $servers->configs
[$config ??
''];
1031 $socket = $server->cli_socket
;
1032 $cmd .= ' -vEval.UseRemoteUnixServer=only';
1033 $cmd .= ' -vEval.UnixServerPath='.$socket;
1034 $cmd .= ' --count=3';
1037 // Special support for tests that require a path to the current
1038 // test directory for things like prepend_file and append_file
1040 if (file_exists($test.'.ini')) {
1041 $contents = file_get_contents($test.'.ini');
1042 if (strpos($contents, '{PWD}') !== false) {
1043 $test_ini = tempnam('/tmp', $test).'.ini';
1044 file_put_contents($test_ini,
1045 str_replace('{PWD}', dirname($test), $contents));
1046 $cmd .= " -c $test_ini";
1050 $contents = file_get_contents($test.$hdf_suffix);
1051 if (strpos($contents, '{PWD}') !== false) {
1052 $test_hdf = tempnam('/tmp', $test).$hdf_suffix;
1053 file_put_contents($test_hdf,
1054 str_replace('{PWD}', dirname($test), $contents));
1055 $cmd .= " -c $test_hdf";
1059 if ($options->repo
) {
1060 $repo_suffix = repo_separate($options, $test) ?
'hhbbc' : 'hhbc';
1064 "\"" . test_repo($options, $test) . "/hhvm.$repo_suffix\"";
1065 $cmd .= ' -vRepo.Authoritative=true';
1066 $cmd .= " -vRepo.Path=$hhbbc_repo";
1069 if ($options->jitsample is nonnull
) {
1070 $cmd .= ' -vDeploymentId="' . $options->jitsample
. '"';
1071 $cmd .= ' --instance-id="' . $test . '"';
1072 $cmd .= ' -vEval.JitSampleRate=1';
1073 $cmd .= " -vScribe.Tables.hhvm_jit.include.*=instance_id";
1074 $cmd .= " -vScribe.Tables.hhvm_jit.include.*=deployment_id";
1077 $env = \HH\FIXME\UNSAFE_CAST
<dict
<arraykey
,mixed>,dict
<string,mixed>>(
1078 \HH\
global_get('_ENV') as dict
<_
, _
>
1080 $env['LC_ALL'] = 'C';
1081 $env['INPUTRC'] = test_dir().'/inputrc';
1083 // Apply the --env option.
1084 if ($options->env is nonnull
) {
1085 foreach (explode(",", $options->env
) as $arg) {
1086 $i = strpos($arg, '=');
1088 $key = substr($arg, 0, $i);
1089 $val = substr($arg, $i +
1);
1097 $in = find_test_ext($test, 'in');
1098 if ($in is nonnull
) {
1099 $cmd .= ' < ' . escapeshellarg($in);
1100 // If we're piping the input into the command then setup a simple
1101 // dumb terminal so hhvm doesn't try to control it and pollute the
1102 // output with control characters, which could change depending on
1103 // a wide variety of terminal settings.
1104 $env["TERM"] = "dumb";
1107 foreach ($cmds as $idx => $_) {
1108 $cmds[$idx] .= $cmd;
1111 return tuple($cmds, $env);
1119 // Transform extra_args like "-vName=Value" into "-vRuntime.Name=Value".
1121 preg_replace("/(^-v|\s+-v)\s*/", "$1Runtime.", extra_args($options));
1123 $compiler_args = extra_compiler_args($options);
1125 $hdf_suffix = ".use.for.ini.migration.testing.only.hdf";
1126 $hdf = file_exists($test.$hdf_suffix)
1127 ?
'-c ' . $test . $hdf_suffix
1131 $contents = file_get_contents($test.$hdf_suffix);
1132 if (strpos($contents, '{PWD}') !== false) {
1133 $test_hdf = tempnam('/tmp', $test).$hdf_suffix;
1134 file_put_contents($test_hdf,
1135 str_replace('{PWD}', dirname($test), $contents));
1136 $hdf = " -c $test_hdf";
1140 return implode(" ", vec
[
1141 hphpc_path($options),
1143 '-vUseHHBBC='. (repo_separate($options, $test) ?
'false' : 'true'),
1145 find_test_ext($test, 'ini', 'hphp_config'),
1147 '-vRuntime.ResourceLimit.CoreFileSize=0',
1148 '-vRuntime.Eval.EnableIntrinsicsExtension=true',
1149 '-vRuntime.Eval.EnableArgsInBacktraces=true',
1150 '-vRuntime.Eval.FoldLazyClassKeys=false',
1151 '-vParserThreadCount=' . ($options->repo_threads ??
1),
1153 '-o "' . test_repo($options, $test) . '"',
1155 "-vExternWorker.WorkingDir=".Status
::getTestTmpPath($test, 'work'),
1158 read_opts_file("$test.hphp_opts"),
1162 function hphpc_path(Options
$options): string {
1163 if ($options->split_hphpc
) {
1165 $file = bin_root().'/hphpc';
1167 if (!is_file($file)) {
1168 error("$file doesn't exist. Did you forget to build first?");
1170 return rel_path($file);
1177 Options
$options, string $test, string $program,
1179 $test_repo = test_repo($options, $test);
1180 return implode(" ", vec
[
1181 hphpc_path($options),
1185 '--parallel-num-threads=' . ($options->repo_threads ??
1),
1186 '--parallel-final-threads=' . ($options->repo_threads ??
1),
1187 read_opts_file("$test.hhbbc_opts"),
1188 "-o \"$test_repo/hhvm.hhbbc\" \"$test_repo/hhvm.hhbc\"",
1192 // Execute $cmd and return its output on failure, including any stacktrace.log
1193 // file it generated. Return null on success.
1194 function exec_with_stack(string $cmd): ?
string {
1196 $proc = proc_open($cmd,
1197 dict
[0 => vec
['pipe', 'r'],
1198 1 => vec
['pipe', 'w'],
1199 2 => vec
['pipe', 'w']], inout
$pipes);
1202 $all_selects_failed=true;
1203 $end = mtime() + TIMEOUT_SECONDS
;
1207 if ($now >= $end) break;
1208 $read = vec
[$pipes[1], $pipes[2]];
1211 $available = @stream_select
(
1217 if ($available === false) {
1219 $s .= "select failed:\n" . print_r(error_get_last(), true);
1222 $all_selects_failed=false;
1223 if ($available === 0) continue;
1224 foreach ($read as $pipe) {
1225 $t = fread($pipe, 4096);
1226 if ($t === false) continue;
1229 if (feof($pipes[1]) && feof($pipes[2])) break;
1234 $status = proc_get_status($proc);
1235 if (!$status['running']) break;
1241 exec('pkill -P ' . $status['pid'] . ' 2> /dev/null', inout
$output, inout
$return_var);
1242 posix_kill($status['pid'], SIGTERM
);
1248 if ($all_selects_failed) {
1249 return "All selects failed running `$cmd'\n\n$s";
1251 return "Timed out running `$cmd'\n\n$s";
1254 !$status['exitcode'] &&
1255 !preg_match('/\\b(error|exception|fatal)\\b/', $s)
1259 $pid = $status['pid'];
1261 @file_get_contents
("/tmp/stacktrace.$pid.log") ?
:
1262 @file_get_contents
("/var/tmp/cores/stacktrace.$pid.log");
1263 if ($stack !== false) {
1264 $s .= "\n" . $stack;
1266 return "Running `$cmd' failed (".$status['exitcode']."):\n\n$s";
1269 function repo_mode_compile(
1270 Options
$options, string $test, string $program,
1272 $hphp = hphp_cmd($options, $test, $program);
1273 $result = exec_with_stack($hphp);
1274 if ($result is
null && repo_separate($options, $test)) {
1275 $hhbbc = hhbbc_cmd($options, $test, $program);
1276 $result = exec_with_stack($hhbbc);
1278 if ($result is
null) return true;
1279 Status
::writeDiff($test, $result);
1284 // Minimal support for sending messages between processes over named pipes.
1286 // Non-buffered pipe writes of up to 512 bytes (PIPE_BUF) are atomic.
1289 // 8 byte zero-padded hex pid
1290 // 4 byte zero-padded hex type
1291 // 4 byte zero-padded hex body size
1292 // N byte string body
1294 // The first call to "getInput()" or "getOutput()" in any process will
1295 // block until some other process calls the other method.
1298 // The path to the FIFO, until destroyed.
1299 private ?
string $path = null;
1301 private ?
resource $input = null;
1302 private ?
resource $output = null;
1304 // Pipes writes are atomic up to 512 bytes (up to 4096 bytes on linux),
1305 // and we use a 16 byte header, leaving this many bytes available for
1306 // each chunk of "body" (see "$partials").
1307 const int CHUNK
= 512 - 16;
1309 // If a message "body" is larger than CHUNK bytes, then writers must break
1310 // it into chunks, and send all but the last chunk with type 0. The reader
1311 // collects those chunks in this Map (indexed by pid), until the final chunk
1312 // is received, and the chunks can be reassembled.
1313 private Map
<int, Vector
<string>> $partials = Map
{};
1315 public function __construct(?
string $dir = null): void
{
1316 $path = \tempnam
($dir ?? \
sys_get_temp_dir(), "queue.mkfifo.");
1318 if (!\
posix_mkfifo($path, 0700)) {
1319 // Only certain directories support "posix_mkfifo()".
1320 throw new \
Exception("Failed to create FIFO at '$path'");
1322 $this->path
= $path;
1325 private function getInput(): resource {
1326 $input = $this->input
;
1327 if ($input is
null) {
1328 $path = $this->path
;
1329 if ($path is
null) {
1330 throw new \
Exception("Missing FIFO path");
1332 $input = \fopen
($path, "r");
1333 $this->input
= $input;
1338 private function getOutput(): resource {
1339 $output = $this->output
;
1340 if ($output is
null) {
1341 $path = $this->path
;
1342 if ($path is
null) {
1343 throw new \
Exception("Missing FIFO path");
1345 $output = \fopen
($path, "a");
1346 $this->output
= $output;
1351 private function validate(int $pid, int $type, int $blen): void
{
1352 if ($pid < 0 ||
$pid >= (1 << 22)) {
1353 throw new \
Exception("Illegal pid $pid");
1355 if ($type < 0 ||
$type >= 0x10000) {
1356 throw new \
Exception("Illegal type $type");
1358 if ($blen < 0 ||
$blen > static::CHUNK
) {
1359 throw new \
Exception("Illegal blen $blen");
1363 // Read one packet header or body.
1364 private function read(int $n): string {
1365 $input = $this->getInput();
1367 while (\
strlen($result) < $n) {
1368 $r = fread($input, $n - \
strlen($result));
1372 throw new \
Exception("Failed to read $n bytes");
1378 // Receive one raw message (pid, type, body).
1379 public function receive(): (int, int, string) {
1383 $header = $this->read(16);
1384 $pid = intval(substr($header, 0, 8) as string, 16);
1385 $type = intval(substr($header, 8, 4) as string, 16);
1386 $blen = intval(substr($header, 12, 4) as string, 16);
1387 $this->validate($pid, $type, $blen);
1388 $body = $this->read($blen);
1390 $this->partials
[$pid] ??
= Vector
{};
1391 $this->partials
[$pid][] = $body;
1393 $chunks = $this->partials
[$pid] ??
null;
1394 if ($chunks is nonnull
) {
1396 $body = \
implode("", $chunks);
1397 $this->partials
->removeKey($pid);
1399 return tuple($pid, $type, $body);
1404 // Receive one message (pid, type, message).
1405 // Note that the raw body is processed using "unserialize()".
1406 public function receiveMessage(): (int, int, ?Message
) {
1407 list($pid, $type, $body) = $this->receive();
1408 $msg = unserialize($body) as ?Message
;
1409 return tuple($pid, $type, $msg);
1412 private function write(int $pid, int $type, string $body): void
{
1413 $output = $this->getOutput();
1414 $blen = \
strlen($body);
1415 $this->validate($pid, $type, $blen);
1416 $packet = sprintf("%08x%04x%04x%s", $pid, $type, $blen, $body);
1417 $n = \
strlen($packet);
1418 if ($n !== 16 +
$blen) {
1419 throw new \
Exception("Illegal packet");
1421 // Hack's "fwrite()" is never buffered, which is especially
1422 // critical for pipe writes, to ensure that they are actually atomic.
1423 // See the documentation for "PlainFile::writeImpl()". But just in
1424 // case, we add an explicit "fflush()" below.
1425 $bytes_out = fwrite($output, $packet, $n);
1426 if ($bytes_out !== $n) {
1427 throw new \
Exception(
1428 "Failed to write $n bytes; only $bytes_out were written"
1434 // Send one serialized message.
1435 public function send(int $type, string $body): void
{
1436 $pid = \
posix_getpid();
1437 $blen = \
strlen($body);
1438 $chunk = static::CHUNK
;
1439 if ($blen > $chunk) {
1440 for ($i = 0; $i +
$chunk < $blen; $i +
= $chunk) {
1441 $this->write($pid, 0, \
substr($body, $i, $chunk) as string);
1443 $this->write($pid, $type, \
substr($body, $i) as string);
1445 $this->write($pid, $type, $body);
1449 // Send one message after serializing it.
1450 public function sendMessage(int $type, ?Message
$msg): void
{
1451 $body = serialize($msg);
1452 $this->send($type, $body);
1455 public function destroy(): void
{
1456 if ($this->input is nonnull
) {
1457 fclose($this->input
);
1458 $this->input
= null;
1460 if ($this->output is nonnull
) {
1461 fclose($this->output
);
1462 $this->output
= null;
1464 if ($this->path is nonnull
) {
1465 \
unlink($this->path
);
1471 final class Message
{
1472 public function __construct(
1473 public string $test,
1477 public ?
string $reason = null,
1482 enum TempDirRemove
: int {
1488 type TestResult
= shape(
1491 'start_time' => int,
1494 ?
'details' => string,
1497 final class Status
{
1498 private static vec
<TestResult
> $results = vec
[];
1499 private static int $mode = 0;
1501 private static bool $use_color = false;
1503 public static bool $nofork = false;
1504 private static ?Queue
$queue = null;
1505 private static bool $killed = false;
1506 public static TempDirRemove
$temp_dir_remove = TempDirRemove
::ALWAYS
;
1507 private static int $return_value = 255;
1509 private static float $overall_start_time = 0.0;
1510 private static float $overall_end_time = 0.0;
1512 private static string $tmpdir = "";
1513 public static bool $write_to_checkout = false;
1515 public static int $passed = 0;
1516 public static int $skipped = 0;
1517 public static dict
<string, int> $skip_reasons = dict
[];
1518 public static int $failed = 0;
1520 const int MODE_NORMAL
= 0;
1521 const int MODE_VERBOSE
= 1;
1522 const int MODE_TESTPILOT
= 3;
1523 const int MODE_RECORD_FAILURES
= 4;
1525 const int MSG_STARTED
= 7;
1526 const int MSG_FINISHED
= 1;
1527 const int MSG_TEST_PASS
= 2;
1528 const int MSG_TEST_FAIL
= 4;
1529 const int MSG_TEST_SKIP
= 5;
1530 const int MSG_SERVER_RESTARTED
= 6;
1533 const int GREEN
= 32;
1534 const int YELLOW
= 33;
1535 const int BLUE
= 34;
1537 public static function createTmpDir(): void
{
1538 $parent = sys_get_temp_dir();
1539 if (substr($parent, -1) !== "/") {
1542 self
::$tmpdir = HH\Lib\_Private\_OS\
mkdtemp($parent . 'hphp-test-XXXXXX');
1545 public static function getRunTmpDir(): string {
1546 return self
::$tmpdir;
1549 // Return a path in the run tmpdir that's unique to this test and ext.
1550 // Remember to teach clean_intermediate_files to clean up all the exts you use
1551 public static function getTestTmpPath(string $test, string $ext): string {
1552 return self
::$tmpdir . '/' . $test . '.' . $ext;
1555 public static function getTmpPathFile(string $filename): string {
1556 return self
::$tmpdir . '/' . $filename;
1559 // Similar to getTestTmpPath, but if we're run with --write-to-checkout
1560 // then we put the files next to the test instead of in the tmpdir.
1561 public static function getTestOutputPath(string $test, string $ext): string {
1562 if (self
::$write_to_checkout) {
1563 return "$test.$ext";
1565 return static::getTestTmpPath($test, $ext);
1568 public static function createTestTmpDir(string $test): string {
1569 $test_temp_dir = self
::getTestTmpPath($test, 'tmpdir');
1570 @mkdir
($test_temp_dir, 0777, true);
1571 return $test_temp_dir;
1574 public static function writeDiff(string $test, string $diff): void
{
1575 $path = Status
::getTestOutputPath($test, 'diff');
1576 @mkdir
(dirname($path), 0777, true);
1577 file_put_contents($path, $diff);
1580 public static function diffForTest(string $test): string {
1581 $diff = @file_get_contents
(Status
::getTestOutputPath($test, 'diff'));
1582 return $diff === false ?
'' : $diff;
1585 public static function removeDirectory(string $dir): void
{
1586 $files = scandir($dir);
1587 foreach ($files as $file) {
1588 if ($file === '.' ||
$file === '..') {
1591 $path = $dir . "/" . $file;
1592 if (is_dir($path)) {
1593 self
::removeDirectory($path);
1601 // This is similar to removeDirectory but it only removes empty directores
1602 // and won't enter directories whose names end with '.tmpdir'. This allows
1603 // us to clean up paths like test/quick/vec in our run's temporary directory
1604 // if all the tests in them passed, but it leaves test tmpdirs of failed
1605 // tests (that we didn't remove with clean_intermediate_files because the
1606 // test failed) and directores under them alone even if they're empty.
1607 public static function removeEmptyTestParentDirs(string $dir): bool {
1608 $is_now_empty = true;
1609 $files = scandir($dir);
1610 foreach ($files as $file) {
1611 if ($file === '.' ||
$file === '..') {
1614 if (strrpos($file, '.tmpdir') === (strlen($file) - strlen('.tmpdir'))) {
1615 $is_now_empty = false;
1618 $path = $dir . "/" . $file;
1619 if (!is_dir($path)) {
1620 $is_now_empty = false;
1623 if (self
::removeEmptyTestParentDirs($path)) {
1626 $is_now_empty = false;
1629 return $is_now_empty;
1632 public static function setMode(int $mode): void
{
1633 self
::$mode = $mode;
1636 public static function getMode(): int {
1640 public static function setUseColor(bool $use): void
{
1641 self
::$use_color = $use;
1644 public static function addTestTimesSerial(
1645 dict
<string, TestResult
> $results,
1648 foreach ($results as $result) {
1649 $time +
= $result['time'];
1654 public static function getOverallStartTime(): float {
1655 return self
::$overall_start_time;
1658 public static function getOverallEndTime(): float {
1659 return self
::$overall_end_time;
1662 public static function started(): void
{
1663 self
::send(self
::MSG_STARTED
, null);
1664 self
::$overall_start_time = mtime();
1667 public static function finished(int $return_value): void
{
1668 self
::$overall_end_time = mtime();
1669 self
::$return_value = $return_value;
1670 self
::send(self
::MSG_FINISHED
, null);
1673 public static function destroy(): void
{
1674 if (!self
::$killed) {
1675 self
::$killed = true;
1676 if (self
::$queue is nonnull
) {
1677 self
::$queue->destroy();
1678 self
::$queue = null;
1680 switch (self
::$temp_dir_remove) {
1681 case TempDirRemove
::NEVER
:
1683 case TempDirRemove
::ON_RUN_SUCCESS
:
1684 if (self
::$return_value !== 0) {
1685 self
::removeEmptyTestParentDirs(self
::$tmpdir);
1689 case TempDirRemove
::ALWAYS
:
1690 self
::removeDirectory(self
::$tmpdir);
1695 public static function destroyFromSignal(int $_signo): void
{
1699 public static function registerCleanup(bool $no_clean): void
{
1700 if (self
::getMode() === self
::MODE_TESTPILOT ||
1701 self
::getMode() === self
::MODE_RECORD_FAILURES
) {
1702 self
::$temp_dir_remove = TempDirRemove
::ALWAYS
;
1703 } else if ($no_clean) {
1704 self
::$temp_dir_remove = TempDirRemove
::NEVER
;
1706 self
::$temp_dir_remove = TempDirRemove
::ON_RUN_SUCCESS
;
1708 register_shutdown_function(self
::destroy
<>);
1709 pcntl_signal(SIGTERM
, self
::destroyFromSignal
<>);
1710 pcntl_signal(SIGINT
, self
::destroyFromSignal
<>);
1713 public static function serverRestarted(): void
{
1714 self
::send(self
::MSG_SERVER_RESTARTED
, null);
1717 public static function pass(
1718 string $test, float $time, int $stime, int $etime,
1720 self
::$results[] = shape(
1722 'status' => 'passed',
1723 'start_time' => $stime,
1724 'end_time' => $etime,
1728 self
::MSG_TEST_PASS
,
1729 new Message($test, $time, $stime, $etime),
1733 public static function skip(
1734 string $test, string $reason, float $time, int $stime, int $etime,
1736 self
::$results[] = shape(
1738 /* testpilot needs a positive response for every test run, report
1739 * that this test isn't relevant so it can silently drop. */
1740 'status' => self
::getMode() === self
::MODE_TESTPILOT
1743 'start_time' => $stime,
1744 'end_time' => $etime,
1748 self
::MSG_TEST_SKIP
,
1749 new Message($test, $time, $stime, $etime, $reason),
1753 public static function fail(
1754 string $test, float $time, int $stime, int $etime, string $diff,
1756 self
::$results[] = shape(
1758 'status' => 'failed',
1759 'start_time' => $stime,
1760 'end_time' => $etime,
1762 'details' => self
::utf8Sanitize($diff),
1765 self
::MSG_TEST_FAIL
,
1766 new Message($test, $time, $stime, $etime),
1770 public static function handle_message(int $type, ?Message
$message): bool {
1772 case Status
::MSG_STARTED
:
1775 case Status
::MSG_FINISHED
:
1778 case Status
::MSG_SERVER_RESTARTED
:
1779 switch (Status
::getMode()) {
1780 case Status
::MODE_NORMAL
:
1781 if (!Status
::hasCursorControl()) {
1782 Status
::sayColor(Status
::RED
, 'x');
1786 case Status
::MODE_VERBOSE
:
1789 "failed to talk to server\n"
1793 case Status
::MODE_TESTPILOT
:
1796 case Status
::MODE_RECORD_FAILURES
:
1801 case Status
::MSG_TEST_PASS
:
1803 invariant($message is nonnull
, "%s", __METHOD__
);
1804 switch (Status
::getMode()) {
1805 case Status
::MODE_NORMAL
:
1806 if (!Status
::hasCursorControl()) {
1807 Status
::sayColor(Status
::GREEN
, '.');
1811 case Status
::MODE_VERBOSE
:
1815 sprintf("passed (%.2fs)\n", $message->time
),
1819 case Status
::MODE_TESTPILOT
:
1820 Status
::sayTestpilot(
1829 case Status
::MODE_RECORD_FAILURES
:
1834 case Status
::MSG_TEST_SKIP
:
1836 invariant($message is nonnull
, "%s", __METHOD__
);
1837 $reason = $message->reason
;
1838 invariant($reason is nonnull
, "%s", __METHOD__
);
1839 self
::$skip_reasons[$reason] ??
= 0;
1840 self
::$skip_reasons[$reason]++
;
1842 switch (Status
::getMode()) {
1843 case Status
::MODE_NORMAL
:
1844 if (!Status
::hasCursorControl()) {
1845 Status
::sayColor(Status
::YELLOW
, 's');
1849 case Status
::MODE_VERBOSE
:
1850 Status
::sayColor($message->test
." ", Status
::YELLOW
, "skipped");
1852 if ($reason is nonnull
) {
1853 Status
::sayColor(" - reason: $reason");
1855 Status
::sayColor(sprintf(" (%.2fs)\n", $message->time
));
1858 case Status
::MODE_TESTPILOT
:
1859 Status
::sayTestpilot(
1868 case Status
::MODE_RECORD_FAILURES
:
1873 case Status
::MSG_TEST_FAIL
:
1875 invariant($message is nonnull
, "%s", __METHOD__
);
1876 switch (Status
::getMode()) {
1877 case Status
::MODE_NORMAL
:
1878 if (Status
::hasCursorControl()) {
1879 print "\033[2K\033[1G";
1881 $diff = Status
::diffForTest($message->test
);
1882 $test = $message->test
;
1885 "\nFAILED: $test\n$diff\n",
1889 case Status
::MODE_VERBOSE
:
1893 sprintf("FAILED (%.2fs)\n", $message->time
),
1897 case Status
::MODE_TESTPILOT
:
1898 Status
::sayTestpilot(
1907 case Status
::MODE_RECORD_FAILURES
:
1913 error("Unknown message $type");
1918 private static function send(int $type, ?Message
$msg): void
{
1919 if (self
::$killed) {
1922 if (self
::$nofork) {
1923 self
::handle_message($type, $msg);
1926 self
::getQueue()->sendMessage($type, $msg);
1930 * Takes a variable number of string or int arguments. If color output is
1931 * enabled and any one of the arguments is preceded by an integer (see the
1932 * color constants above), that argument will be given the indicated color.
1934 public static function sayColor(arraykey
...$args): void
{
1936 for ($i = 0; $i < $n;) {
1941 if (self
::$use_color) {
1942 print "\033[0;{$color}m";
1947 if (self
::$use_color) {
1956 public static function sayTestpilot(
1957 string $test, string $status, int $stime, int $etime, float $time,
1959 $start = dict
['op' => 'start', 'test' => $test];
1960 $end = dict
['op' => 'test_done', 'test' => $test, 'status' => $status,
1961 'start_time' => $stime, 'end_time' => $etime, 'time' => $time];
1962 if ($status === 'failed') {
1963 $end['details'] = self
::utf8Sanitize(Status
::diffForTest($test));
1965 self
::say($start, $end);
1968 public static function getResults(): vec
<TestResult
> {
1969 return self
::$results;
1972 /** Output is in the format expected by JsonTestRunner. */
1973 public static function say(dict
<string, mixed> ...$args): void
{
1975 $row ==> self
::jsonEncode($row) . "\n",
1978 fwrite(STDERR
, implode("", $data));
1981 public static function hasCursorControl(): bool {
1982 // for runs on hudson-ci.org (aka jenkins).
1983 if (getenv("HUDSON_URL")) {
1986 // for runs on travis-ci.org
1987 if (getenv("TRAVIS")) {
1990 $stty = self
::getSTTY();
1994 return strpos($stty, 'erase = <undef>') === false;
1998 public static function getSTTY(): string {
1999 $descriptorspec = dict
[1 => vec
["pipe", "w"], 2 => vec
["pipe", "w"]];
2001 $process = proc_open(
2002 'stty -a', $descriptorspec, inout
$pipes, null, null,
2003 dict
['suppress_errors' => true]
2005 $stty = stream_get_contents($pipes[1]);
2006 proc_close($process);
2010 public static function utf8Sanitize(string $str): string {
2011 return UConverter
::transcode($str, 'UTF-8', 'UTF-8');
2014 public static function jsonEncode(mixed $data): string {
2015 return json_encode($data, JSON_UNESCAPED_SLASHES
);
2018 public static function getQueue(): Queue
{
2019 if (!self
::$queue) {
2020 if (self
::$killed) error("Killed!");
2021 self
::$queue = new Queue(self
::$tmpdir);
2023 return self
::$queue;
2027 function clean_intermediate_files(string $test, Options
$options): void
{
2028 if ($options->no_clean
) {
2031 if ($options->write_to_checkout
) {
2032 // in --write-to-checkout mode, normal test output goes next to the test
2037 foreach ($exts as $ext) {
2038 $file = "$test.$ext";
2039 if (file_exists($file)) {
2045 // normal test output goes here by default
2048 // scratch directory the test may write to
2050 // tests in --hhas-round-trip mode
2052 // tests in --hhbbc2 mode
2053 'before.round_trip.hhas',
2054 'after.round_trip.hhas',
2055 // temporary autoloader DB and associated cruft
2056 // We have at most two modes for now - see hhvm_cmd_impl
2058 'autoloadDB.0-journal',
2062 'autoloadDB.1-journal',
2066 foreach ($tmp_exts as $ext) {
2067 $file = Status
::getTestTmpPath($test, $ext);
2068 if (is_dir($file)) {
2069 Status
::removeDirectory($file);
2070 } else if (file_exists($file)) {
2074 // repo mode uses a directory that may or may not be in the run's tmpdir
2075 $repo = test_repo($options, $test);
2076 if (is_dir($repo)) {
2077 Status
::removeDirectory($repo);
2081 function child_main(
2084 string $json_results_file,
2086 foreach ($tests as $test) {
2087 run_and_log_test($options, $test);
2089 $results = Status
::getResults();
2090 file_put_contents($json_results_file, json_encode($results));
2091 foreach ($results as $result) {
2092 if ($result['status'] === 'failed') {
2100 * The runif feature is similar in spirit to skipif, but instead of allowing
2101 * one to run arbitrary code it can only skip based on pre-defined reasons
2102 * understood by the test runner.
2104 * The .runif file should consist of one or more lines made up of words
2105 * separated by spaces, optionally followed by a comment starting with #.
2106 * Empty lines (or lines with only comments) are ignored. The first word
2107 * determines the interpretation of the rest. The .runif file will allow the
2108 * test to run if all the non-empty lines 'match'.
2110 * Currently supported operations:
2111 * os [not] <os_name> # matches if we are (or are not) on the named OS
2112 * file <path> # matches if the file at the (possibly relative) path exists
2113 * euid [not] root # matches if we are (or are not) running as root (euid==0)
2114 * extension <extension_name> # matches if the named extension is available
2115 * function <function_name> # matches if the named function is available
2116 * class <class_name> # matches if the named class is available
2117 * method <class_name> <method name> # matches if the method is available
2118 * const <constant_name> # matches if the named constant is available
2119 * # matches if any named locale is available for the named LC_* category
2120 * locale LC_<something> <locale name>[ <another locale name>]
2122 * Several functions in this implementation return RunifResult. Valid sets of
2124 * valid, error # valid will be false
2125 * valid, match # valid will be true, match will be true
2126 * valid, match, skip_reason # valid will be true, match will be false
2128 type RunifResult
= shape(
2129 'valid' => bool, // was the runif file valid
2130 ?
'error' => string, // if !valid, what was the problem
2131 ?
'match' => bool, // did the line match/did all the lines in the file match
2132 ?
'skip_reason' => string, // if !match, the skip reason to use
2136 function runif_canonical_os(): string {
2137 if (PHP_OS
=== 'Linux' || PHP_OS
=== 'Darwin') return PHP_OS
;
2138 if (substr(PHP_OS
, 0, 3) === 'WIN') return 'WIN';
2139 invariant_violation('add proper canonicalization for your OS');
2142 function runif_known_os(string $match_os): bool {
2143 switch ($match_os) {
2153 function runif_os_matches(vec
<string> $words): RunifResult
{
2154 if (count($words) === 2) {
2155 if ($words[0] !== 'not') {
2156 return shape('valid' => false, 'error' => "malformed 'os' match");
2158 $match_os = $words[1];
2160 } else if (count($words) === 1) {
2161 $match_os = $words[0];
2164 return shape('valid' => false, 'error' => "malformed 'os' match");
2166 if (!runif_known_os($match_os)) {
2167 return shape('valid' => false, 'error' => "unknown os '$match_os'");
2169 $matches = (runif_canonical_os() === $match_os);
2170 if ($matches !== $invert) return shape('valid' => true, 'match' => true);
2174 'skip_reason' => 'skip-runif-os-' . implode('-', $words)
2178 function runif_file_matches(vec
<string> $words): RunifResult
{
2179 /* This implementation has a trade-off. On the one hand, we could get more
2180 * accurate results if we do the check in a process with the same configs as
2181 * the test via runif_test_for_feature (e.g. if config differences make a
2182 * file we can see invisible to the test). However, this check was added to
2183 * skip tests where the test configs depend on a file that may be absent, in
2184 * which case hhvm configured the same way as the test cannot run. By doing
2185 * the check ourselves we can successfully skip such tests.
2187 if (count($words) !== 1) {
2188 return shape('valid' => false, 'error' => "malformed 'file' match");
2190 if (file_exists($words[0])) {
2191 return shape('valid' => true, 'match' => true);
2196 'skip_reason' => 'skip-runif-file',
2200 function runif_test_for_feature(
2203 string $bool_expression,
2205 $tmp = tempnam(sys_get_temp_dir(), 'test-run-runif-');
2209 "<<__EntryPoint>> function main(): void {\n" .
2210 " echo ($bool_expression) as bool ? 'PRESENT' : 'ABSENT';\n" .
2214 // Run the check in non-repo mode to avoid building the repo (same features
2215 // should be available). Pick the mode arbitrarily for the same reason.
2216 $options_without_repo = clone $options;
2217 $options_without_repo->repo
= false;
2218 list($hhvm, $_) = hhvm_cmd($options_without_repo, $test, $tmp, true);
2220 // Remove any --count <n> from the command
2221 $hhvm = preg_replace('/ --count[ =]\d+/', '', $hhvm);
2222 // some tests set open_basedir to a restrictive value, override to permissive
2223 $hhvm .= ' -dopen_basedir= ';
2225 $result = shell_exec("$hhvm 2>&1");
2226 invariant ($result !== false, 'shell_exec in runif_test_for_feature failed');
2227 $result = trim($result);
2228 if ($result === 'ABSENT') return false;
2229 if ($result === 'PRESENT') return true;
2230 invariant_violation(
2231 "unexpected output from shell_exec in runif_test_for_feature: '%s'",
2236 function runif_euid_matches(
2241 if (count($words) === 2) {
2242 if ($words[0] !== 'not' ||
$words[1] !== 'root') {
2243 return shape('valid' => false, 'error' => "malformed 'euid' match");
2246 } else if (count($words) === 1) {
2247 if ($words[0] !== 'root') {
2248 return shape('valid' => false, 'error' => "malformed 'euid' match");
2252 return shape('valid' => false, 'error' => "malformed 'euid' match");
2254 $matches = runif_test_for_feature($options, $test, 'posix_geteuid() === 0');
2255 if ($matches !== $invert) return shape('valid' => true, 'match' => true);
2259 'skip_reason' => 'skip-runif-euid-' . implode('-', $words)
2263 function runif_extension_matches(
2268 if (count($words) !== 1) {
2269 return shape('valid' => false, 'error' => "malformed 'extension' match");
2271 if (runif_test_for_feature($options, $test, "extension_loaded('{$words[0]}')")) {
2272 return shape('valid' => true, 'match' => true);
2277 'skip_reason' => 'skip-runif-extension-' . $words[0]
2281 function runif_function_matches(
2286 if (count($words) !== 1) {
2287 return shape('valid' => false, 'error' => "malformed 'function' match");
2289 if (runif_test_for_feature($options, $test, "function_exists('{$words[0]}')")) {
2290 return shape('valid' => true, 'match' => true);
2295 'skip_reason' => 'skip-runif-function-' . $words[0]
2299 function runif_class_matches(
2304 if (count($words) !== 1) {
2305 return shape('valid' => false, 'error' => "malformed 'class' match");
2307 if (runif_test_for_feature($options, $test, "class_exists('{$words[0]}')")) {
2308 return shape('valid' => true, 'match' => true);
2313 'skip_reason' => 'skip-runif-class-' . $words[0]
2317 function runif_method_matches(
2322 if (count($words) !== 2) {
2323 return shape('valid' => false, 'error' => "malformed 'method' match");
2325 if (runif_test_for_feature($options, $test,
2326 "method_exists('{$words[0]}', '{$words[1]}')")) {
2327 return shape('valid' => true, 'match' => true);
2332 'skip_reason' => 'skip-runif-method-' . $words[0] . '-' . $words[1],
2336 function runif_const_matches(
2341 if (count($words) !== 1) {
2342 return shape('valid' => false, 'error' => "malformed 'const' match");
2344 if (runif_test_for_feature($options, $test, "defined('{$words[0]}')")) {
2345 return shape('valid' => true, 'match' => true);
2350 'skip_reason' => 'skip-runif-const-' . $words[0]
2354 function runif_locale_matches(
2359 if (count($words) < 2) {
2360 return shape('valid' => false, 'error' => "malformed 'locale' match");
2362 $category = array_shift(inout
$words);
2363 if (!preg_match('/^LC_[A-Z]+$/', $category)) {
2364 return shape('valid' => false, 'error' => "bad locale category '$category'");
2366 $locale_args = implode(', ', array_map($word ==> "'$word'", $words));
2367 $matches = runif_test_for_feature(
2370 "defined('$category') && (false !== setlocale($category, $locale_args))",
2373 return shape('valid' => true, 'match' => true);
2378 'skip_reason' => 'skip-runif-locale',
2382 function runif_should_skip_test(
2386 $runif_path = find_test_ext($test, 'runif');
2387 if (!$runif_path) return shape('valid' => true, 'match' => true);
2390 $contents = file($runif_path, FILE_IGNORE_NEW_LINES
);
2391 foreach ($contents as $line) {
2392 $line = preg_replace('/[#].*$/', '', $line); // remove comment
2393 $line = trim($line);
2394 if ($line === '') continue;
2395 $file_empty = false;
2397 $words = preg_split('/ +/', $line);
2398 if (count($words) < 2) {
2399 return shape('valid' => false, 'error' => "malformed line '$line'");
2401 foreach ($words as $word) {
2402 if (!preg_match('|^[\w/.-]+$|', $word)) {
2405 'error' => "bad word '$word' in line '$line'",
2410 $type = array_shift(inout
$words);
2411 $words = vec($words); // array_shift always promotes to dict :-\
2414 $result = runif_os_matches($words);
2417 $result = runif_file_matches($words);
2420 $result = runif_euid_matches($options, $test, $words);
2423 $result = runif_extension_matches($options, $test, $words);
2426 $result = runif_function_matches($options, $test, $words);
2429 $result = runif_class_matches($options, $test, $words);
2432 $result = runif_method_matches($options, $test, $words);
2435 $result = runif_const_matches($options, $test, $words);
2438 $result = runif_locale_matches($options, $test, $words);
2441 return shape('valid' => false, 'error' => "bad match type '$type'");
2443 if (!$result['valid'] ||
!Shapes
::idx($result, 'match', false)) {
2447 if ($file_empty) return shape('valid' => false, 'error' => 'empty runif file');
2448 return shape('valid' => true, 'match' => true);
2451 function should_skip_test_simple(
2455 if (($options->cli_server ||
$options->server
) &&
2456 !can_run_server_test($test, $options)) {
2457 return 'skip-server';
2460 if ($options->hhas_round_trip
&& substr($test, -5) === ".hhas") {
2464 if ($options->hhbbc2 ||
$options->hhas_round_trip
) {
2465 $no_hhas_tag = 'nodumphhas';
2466 if (file_exists("$test.$no_hhas_tag") ||
2467 file_exists(dirname($test).'/'.$no_hhas_tag)) {
2468 return 'skip-nodumphhas';
2470 if (file_exists($test . ".verify")) {
2471 return 'skip-verify';
2475 if (has_multi_request_mode($options) ||
$options->repo ||
2477 if (file_exists($test . ".verify")) {
2478 return 'skip-verify';
2480 $no_multireq_tag = "nomultireq";
2481 if (file_exists("$test.$no_multireq_tag") ||
2482 file_exists(dirname($test).'/'.$no_multireq_tag)) {
2483 return 'skip-multi-req';
2485 if (find_debug_config($test, 'hphpd.ini')) {
2486 return 'skip-debugger';
2490 $no_bespoke_tag = "nobespoke";
2491 if ($options->bespoke
&&
2492 file_exists("$test.$no_bespoke_tag")) {
2493 // Skip due to changes in array identity
2494 return 'skip-bespoke';
2497 $no_lazyclass_tag = "nolazyclass";
2498 if ($options->lazyclass
&&
2499 file_exists("$test.$no_lazyclass_tag")) {
2500 return 'skip-lazyclass';
2503 $no_jitserialize_tag = "nojitserialize";
2504 if ($options->jit_serialize is nonnull
&&
2505 file_exists("$test.$no_jitserialize_tag")) {
2506 return 'skip-jit-serialize';
2512 function skipif_should_skip_test(
2516 $skipif_test = find_test_ext($test, 'skipif');
2517 if (!$skipif_test) {
2518 return shape('valid' => true, 'match' => true);
2521 // Run the .skipif in non-repo mode since building a repo for it is
2522 // inconvenient and the same features should be available. Pick the mode
2523 // arbitrarily for the same reason.
2524 $options_without_repo = clone $options;
2525 $options_without_repo->repo
= false;
2526 list($hhvm, $_) = hhvm_cmd($options_without_repo, $test, $skipif_test);
2528 // Remove any --count <n> from the command
2529 $hhvm = preg_replace('/ --count[ =]\d+/', '', $hhvm);
2531 $descriptorspec = dict
[
2532 0 => vec
["pipe", "r"],
2533 1 => vec
["pipe", "w"],
2534 2 => vec
["pipe", "w"],
2537 $process = proc_open("$hhvm $test 2>&1", $descriptorspec, inout
$pipes);
2538 if (!is_resource($process)) {
2541 'error' => 'proc_open failed while running skipif'
2546 $output = trim(stream_get_contents($pipes[1]));
2548 proc_close($process);
2550 // valid output is empty or a single line starting with 'skip'
2551 // everything else must result in a test failure
2552 if ($output === '') {
2553 return shape('valid' => true, 'match' => true);
2555 if (preg_match('/^skip.*$/', $output)) {
2559 'skip_reason' => 'skip-skipif',
2562 return shape('valid' => false, 'error' => "invalid skipif output '$output'");
2565 function comp_line(string $l1, string $l2, bool $is_reg): bool {
2567 return (bool)preg_match('/^'. $l1 . '$/s', $l2);
2569 return !strcmp($l1, $l2);
2573 function count_array_diff(
2574 vec
<string> $ar1, vec
<string> $ar2, bool $is_reg,
2575 int $idx1, int $idx2, int $cnt1, int $cnt2, num
$steps,
2579 while ($idx1 < $cnt1 && $idx2 < $cnt2 && comp_line($ar1[$idx1], $ar2[$idx2],
2591 for ($ofs1 = $idx1 +
1; $ofs1 < $cnt1 && $st > 0; $ofs1++
) {
2593 $eq = @count_array_diff
($ar1, $ar2, $is_reg, $ofs1, $idx2, $cnt1,
2604 for ($ofs2 = $idx2 +
1; $ofs2 < $cnt2 && $st > 0; $ofs2++
) {
2606 $eq = @count_array_diff
($ar1, $ar2, $is_reg, $idx1, $ofs2, $cnt1, $cnt2, $st);
2614 } else if ($eq2 > 0) {
2622 function generate_array_diff(
2628 $idx1 = 0; $cnt1 = @count
($ar1);
2629 $idx2 = 0; $cnt2 = @count
($ar2);
2633 while ($idx1 < $cnt1 && $idx2 < $cnt2) {
2634 if (comp_line($ar1[$idx1], $ar2[$idx2], $is_reg)) {
2639 $c1 = @count_array_diff
($ar1, $ar2, $is_reg, $idx1+
1, $idx2, $cnt1,
2641 $c2 = @count_array_diff
($ar1, $ar2, $is_reg, $idx1, $idx2+
1, $cnt1,
2645 $old1[$idx1+
1] = sprintf("%03d- ", $idx1+
1) . $w[$idx1];
2647 } else if ($c2 > 0) {
2648 $old2[$idx2+
1] = sprintf("%03d+ ", $idx2+
1) . $ar2[$idx2];
2651 $old1[$idx1+
1] = sprintf("%03d- ", $idx1+
1) . $w[$idx1];
2652 $old2[$idx2+
1] = sprintf("%03d+ ", $idx2+
1) . $ar2[$idx2];
2660 $old1_keys = array_keys($old1);
2661 $old2_keys = array_keys($old2);
2662 $old1_values = array_values($old1);
2663 $old2_values = array_values($old2);
2664 // these start at -2 so $l1 + 1 and $l2 + 1 are not valid indices
2667 $iter1 = 0; $end1 = count($old1);
2668 $iter2 = 0; $end2 = count($old2);
2670 while ($iter1 < $end1 ||
$iter2 < $end2) {
2671 $k1 = $iter1 < $end1 ?
$old1_keys[$iter1] : -2;
2672 $k2 = $iter2 < $end2 ?
$old2_keys[$iter2] : -2;
2673 if ($k1 === $l1 +
1 ||
$iter2 >= $end2) {
2675 $diff[] = $old1_values[$iter1];
2677 } else if ($k2 === $l2 +
1 ||
$iter1 >= $end1) {
2679 $diff[] = $old2_values[$iter2];
2681 } else if ($k1 < $k2) {
2683 $diff[] = $old1_values[$iter1];
2687 $diff[] = $old2_values[$iter2];
2692 while ($idx1 < $cnt1) {
2693 $diff[] = sprintf("%03d- ", $idx1 +
1) . $w[$idx1];
2697 while ($idx2 < $cnt2) {
2698 $diff[] = sprintf("%03d+ ", $idx2 +
1) . $ar2[$idx2];
2705 function generate_diff(
2711 $w = explode("\n", $wanted);
2712 $o = explode("\n", $output);
2713 if (is_null($wanted_re)) {
2716 if (preg_match_with_matches('/^\((.*)\)\{(\d+)\}$/s', $wanted_re, inout
$m)) {
2717 $t = explode("\n", $m[1]);
2720 for ($i = 0; $i < (int)$m[2]; $i++
) {
2721 foreach ($t as $v) {
2724 foreach ($w as $v) {
2728 $w = $wanted === $wanted_re ?
$r : $w2;
2730 $r = explode("\n", $wanted_re);
2733 $diff = generate_array_diff($r, $o, !is_null($wanted_re), $w);
2735 return implode("\r\n", $diff);
2738 function dump_hhas_cmd(
2739 string $hhvm_cmd, string $test, string $hhas_file,
2741 $dump_flags = implode(' ', vec
[
2742 '-vEval.AllowHhas=true',
2743 '-vEval.DumpHhas=1',
2744 '-vEval.DumpHhasToFile='.escapeshellarg($hhas_file),
2745 '-vEval.LoadFilepathFromUnitCache=0',
2747 $cmd = str_replace(' -- ', " $dump_flags -- ", $hhvm_cmd);
2748 if ($cmd === $hhvm_cmd) $cmd .= " $dump_flags";
2752 function dump_hhas_to_temp(string $hhvm_cmd, string $test): ?
string {
2753 $temp_file = Status
::getTestTmpPath($test, 'round_trip.hhas');
2754 $cmd = dump_hhas_cmd($hhvm_cmd, $test, $temp_file);
2756 system("$cmd &> /dev/null", inout
$ret);
2757 return $ret === 0 ?
$temp_file : null;
2760 const vec
<string> SERVER_EXCLUDE_PATHS
= vec
[
2765 'slow/ext_vsdebug/',
2766 'zend/good/ext/standard/tests/array/',
2769 const string HHAS_EXT
= '.hhas';
2771 function can_run_server_test(string $test, Options
$options): bool {
2772 // explicitly disabled
2773 if (is_file("$test.noserver") ||
2774 (is_file("$test.nowebserver") && $options->server
)) {
2778 // has its own config
2779 if (find_test_ext($test, 'opts') ||
is_file("$test.ini") ||
2780 is_file("$test.use.for.ini.migration.testing.only.hdf")) {
2784 // we can't run repo only tests in server modes
2785 if (is_file("$test.onlyrepo") ||
is_file("$test.onlyjumpstart")) {
2789 foreach (SERVER_EXCLUDE_PATHS
as $path) {
2790 if (strpos($test, $path) !== false) return false;
2793 // don't run hhas tests in server modes
2794 if (strrpos($test, HHAS_EXT
) === (strlen($test) - strlen(HHAS_EXT
))) {
2801 const int SERVER_TIMEOUT
= 45;
2803 function run_config_server(Options
$options, string $test): mixed {
2805 can_run_server_test($test, $options),
2806 'should_skip_test_simple should have skipped this',
2809 Status
::createTestTmpDir($test); // force it to be created
2810 $config = find_file_for_dir(dirname($test), 'config.ini') ??
'';
2811 $servers = $options->servers
as Servers
;
2812 $port = $servers->configs
[$config]->port
;
2813 $ch = curl_init("localhost:$port/$test");
2814 curl_setopt($ch, CURLOPT_RETURNTRANSFER
, true);
2815 curl_setopt($ch, CURLOPT_TIMEOUT
, SERVER_TIMEOUT
);
2816 curl_setopt($ch, CURLOPT_BINARYTRANSFER
, true);
2817 $output = curl_exec($ch);
2818 if ($output is
string) {
2819 $output = trim($output);
2821 $output = "Error talking to server: " . curl_error($ch);
2825 return run_config_post(tuple($output, ''), $test, $options);
2828 function run_config_cli(
2832 dict
<string, mixed> $cmd_env,
2833 ): ?
(string, string) {
2834 $cmd = timeout_prefix() . $cmd;
2836 if ($options->repo
&& $options->repo_out is
null) {
2837 // we already created it in run_test
2838 $cmd_env['HPHP_TEST_TMPDIR'] = Status
::getTestTmpPath($test, 'tmpdir');
2840 $cmd_env['HPHP_TEST_TMPDIR'] = Status
::createTestTmpDir($test);
2842 $cmd_env['HPHP_TEST_SOURCE_FILE'] = $test;
2843 if ($options->log
) {
2844 $cmd_env['TRACE'] = 'printir:1';
2845 $cmd_env['HPHP_TRACE_FILE'] = $test . '.log';
2848 $descriptorspec = dict
[
2849 0 => vec
["pipe", "r"],
2850 1 => vec
["pipe", "w"],
2851 2 => vec
["pipe", "w"],
2854 $process = proc_open(
2855 "$cmd 2>&1", $descriptorspec, inout
$pipes, null, $cmd_env
2857 if (!is_resource($process)) {
2858 Status
::writeDiff($test, "Couldn't invoke $cmd");
2863 $output = stream_get_contents($pipes[1]);
2864 $output = trim($output);
2865 $stderr = stream_get_contents($pipes[2]);
2868 proc_close($process);
2870 return tuple($output, $stderr);
2873 function replace_object_resource_ids(string $str, string $replacement): string {
2874 $str = preg_replace(
2875 '/(object\([^)]+\)#)\d+/', '\1'.$replacement, $str
2877 return preg_replace(
2878 '/resource\(\d+\)/', "resource($replacement)", $str
2882 function run_config_post(
2883 (string, string) $outputs,
2887 list($output, $stderr) = $outputs;
2888 file_put_contents(Status
::getTestOutputPath($test, 'out'), $output);
2890 $check_hhbbc_error = $options->repo
2891 && (file_exists($test . '.hhbbc_assert') ||
2892 file_exists($test . '.hphpc_assert'));
2894 // hhvm redirects errors to stdout, so anything on stderr is really bad.
2895 if ($stderr && !$check_hhbbc_error) {
2898 "Test failed because the process wrote on stderr:\n$stderr"
2904 if (!$check_hhbbc_error) {
2905 if ($options->retranslate_all is nonnull
) {
2906 $repeats = (int)$options->retranslate_all
* 2;
2909 if ($options->recycle_tc is nonnull
) {
2910 $repeats = (int)$options->recycle_tc
;
2913 if ($options->cli_server
) {
2918 list($file, $type) = get_expect_file_and_type($test, $options);
2919 if ($file is
null ||
$type is
null) {
2922 "No $test.expect, $test.expectf, $test.hhvm.expect, " .
2923 "$test.hhvm.expectf, or $test.expectregex. " .
2924 "If $test is meant to be included by other tests, " .
2925 "use a different file extension.\n"
2931 if ($type === 'expect' ||
$type === 'hhvm.expect') {
2932 $wanted = trim(file_get_contents($file));
2933 if ($options->ignore_oids ||
$options->repo
) {
2934 $output = replace_object_resource_ids($output, 'n');
2935 $wanted = replace_object_resource_ids($wanted, 'n');
2939 $passed = !strcmp($output, $wanted);
2941 Status
::writeDiff($test, generate_diff($wanted, null, $output));
2945 $wanted_re = preg_quote($wanted, '/');
2946 } else if ($type === 'expectf' ||
$type === 'hhvm.expectf') {
2947 $wanted = trim(file_get_contents($file));
2948 if ($options->ignore_oids ||
$options->repo
) {
2949 $wanted = replace_object_resource_ids($wanted, '%d');
2951 $wanted_re = $wanted;
2953 // do preg_quote, but miss out any %r delimited sections.
2957 $length = strlen($wanted_re);
2958 while ($startOffset < $length) {
2959 $start = strpos($wanted_re, $r, $startOffset);
2960 if ($start !== false) {
2961 // we have found a start tag.
2962 $end = strpos($wanted_re, $r, $start+
2);
2963 if ($end === false) {
2964 // unbalanced tag, ignore it.
2969 // no more %r sections.
2973 // quote a non re portion of the string.
2974 $temp = $temp.preg_quote(substr($wanted_re, $startOffset,
2975 ($start - $startOffset)), '/');
2976 // add the re unquoted.
2977 if ($end > $start) {
2978 $temp = $temp.'('.substr($wanted_re, $start+
2, ($end - $start-2)).')';
2980 $startOffset = $end +
2;
2984 $wanted_re = str_replace(
2985 vec
['%binary_string_optional%'],
2989 $wanted_re = str_replace(
2990 vec
['%unicode_string_optional%'],
2994 $wanted_re = str_replace(
2995 vec
['%unicode\|string%', '%string\|unicode%'],
2999 $wanted_re = str_replace(
3000 vec
['%u\|b%', '%b\|u%'],
3005 $wanted_re = str_replace('%e', '\\' . DIRECTORY_SEPARATOR
, $wanted_re);
3006 $wanted_re = str_replace('%s', '[^\r\n]+', $wanted_re);
3007 $wanted_re = str_replace('%S', '[^\r\n]*', $wanted_re);
3008 $wanted_re = str_replace('%a', '.+', $wanted_re);
3009 $wanted_re = str_replace('%A', '.*', $wanted_re);
3010 $wanted_re = str_replace('%w', '\s*', $wanted_re);
3011 $wanted_re = str_replace('%i', '[+-]?\d+', $wanted_re);
3012 $wanted_re = str_replace('%d', '\d+', $wanted_re);
3013 $wanted_re = str_replace('%x', '[0-9a-fA-F]+', $wanted_re);
3014 // %f allows two points "-.0.0" but that is the best *simple* expression.
3015 $wanted_re = str_replace('%f', '[+-]?\.?\d+\.?\d*(?:[Ee][+-]?\d+)?',
3017 $wanted_re = str_replace('%c', '.', $wanted_re);
3019 $wanted_re = str_replace('%%', '%%?', $wanted_re);
3021 // Normalize newlines.
3022 $wanted_re = preg_replace("/(\r\n?|\n)/", "\n", $wanted_re);
3023 $output = preg_replace("/(\r\n?|\n)/", "\n", $output);
3024 } else if ($type === 'expectregex') {
3025 $wanted_re = trim(file_get_contents($file));
3027 throw new Exception("Unsupported expect file type: ".$type);
3031 $wanted_re = "($wanted_re\s*)".'{'.$repeats.'}';
3033 if ($wanted is
null) $wanted = $wanted_re;
3034 $passed = @preg_match
("/^$wanted_re\$/s", $output);
3035 if ($passed) return true;
3036 if ($passed === false && $repeats) {
3037 // $repeats can cause the regex to become too big, and fail
3039 return 'skip-repeats-fail';
3041 $diff = generate_diff($wanted_re, $wanted_re, $output);
3042 if ($passed === false && $diff === "") {
3043 // the preg match failed, probably because the regex was too complex,
3044 // but since the line by line diff came up empty, we're fine
3047 Status
::writeDiff($test, $diff);
3051 function timeout_prefix(): string {
3052 if (is_executable('/usr/bin/timeout')) {
3053 return '/usr/bin/timeout ' . TIMEOUT_SECONDS
. ' ';
3055 return hphp_home() . '/hphp/tools/timeout.sh -t ' . TIMEOUT_SECONDS
. ' ';
3059 function run_foreach_config(
3063 dict
<string, mixed> $cmd_env,
3065 invariant(count($cmds) > 0, "run_foreach_config: no modes");
3067 foreach ($cmds as $cmd) {
3068 $outputs = run_config_cli($options, $test, $cmd, $cmd_env);
3069 if ($outputs is
null) return false;
3070 $result = run_config_post($outputs, $test, $options);
3071 if (!$result) return $result;
3076 function run_and_log_test(Options
$options, string $test): void
{
3079 $status = run_test($options, $test);
3080 $time = mtime() - $time;
3083 if ($status === false) {
3084 $diff = Status
::diffForTest($test);
3086 $diff = 'Test failed with empty diff';
3088 Status
::fail($test, $time, $stime, $etime, $diff);
3089 } else if ($status === true) {
3090 Status
::pass($test, $time, $stime, $etime);
3091 clean_intermediate_files($test, $options);
3092 } else if ($status is
string) {
3094 preg_match('/^skip-[\w-]+$/', $status),
3095 "invalid skip status %s",
3098 Status
::skip($test, substr($status, 5), $time, $stime, $etime);
3099 clean_intermediate_files($test, $options);
3101 invariant_violation("invalid status type %s", gettype($status));
3105 // Returns "(string | bool)".
3106 function run_test(Options
$options, string $test): mixed {
3107 $skip_reason = should_skip_test_simple($options, $test);
3108 if ($skip_reason is nonnull
) return $skip_reason;
3110 if (!$options->no_skipif
) {
3111 $result = runif_should_skip_test($options, $test);
3112 if (!$result['valid']) {
3113 invariant(Shapes
::keyExists($result, 'error'), 'missing runif error');
3114 Status
::writeDiff($test, 'Invalid .runif file: ' . $result['error']);
3117 if (!($result['match'] ??
false)) {
3118 invariant(Shapes
::keyExists($result, 'skip_reason'), 'missing skip_reason');
3119 return $result['skip_reason'];
3122 $result = skipif_should_skip_test($options, $test);
3123 if (!$result['valid']) {
3124 invariant(Shapes
::keyExists($result, 'error'), 'missing skipif error');
3125 Status
::writeDiff($test, $result['error']);
3128 if (!($result['match'] ??
false)) {
3129 invariant(Shapes
::keyExists($result, 'skip_reason'), 'missing skip_reason');
3130 return $result['skip_reason'];
3134 list($hhvm, $hhvm_env) = hhvm_cmd($options, $test);
3136 if (preg_grep('/ --count[ =][0-9]+ .* --count[ =][0-9]+( |$)/', $hhvm)) {
3137 // we got --count from 2 sources (e.g. .opts file and multi_request_mode)
3138 // this can't work so skip the test
3139 return 'skip-count';
3140 } else if ($options->jit_serialize is nonnull
) {
3141 // jit-serialize adds the --count option later, so even 1 --count in the
3142 // command means we have to skip
3143 if (preg_grep('/ --count[ =][0-9]+( |$)/', $hhvm)) {
3144 return 'skip-count';
3148 if ($options->repo
) {
3149 if (file_exists($test.'.norepo')) {
3150 return 'skip-norepo';
3152 if (file_exists($test.'.onlyjumpstart') &&
3153 ($options->jit_serialize is
null ||
(int)$options->jit_serialize
< 1)) {
3154 return 'skip-onlyjumpstart';
3157 $test_repo = test_repo($options, $test);
3158 if ($options->repo_out is nonnull
) {
3159 // we may need to clean up after a previous run
3160 $repo_files = vec
['hhvm.hhbc', 'hhvm.hhbbc'];
3161 foreach ($repo_files as $repo_file) {
3162 @unlink
("$test_repo/$repo_file");
3165 // create tmpdir now so that we can write repos
3166 Status
::createTestTmpDir($test);
3171 if (file_exists($test . '.hphpc_assert')) {
3172 $hphp = hphp_cmd($options, $test, $program);
3173 return run_foreach_config($options, $test, vec
[$hphp], $hhvm_env);
3174 } else if (file_exists($test . '.hhbbc_assert')) {
3175 $hphp = hphp_cmd($options, $test, $program);
3176 if (repo_separate($options, $test)) {
3177 $result = exec_with_stack($hphp);
3178 if ($result is
string) return false;
3179 $hhbbc = hhbbc_cmd($options, $test, $program);
3180 return run_foreach_config($options, $test, vec
[$hhbbc], $hhvm_env);
3182 return run_foreach_config($options, $test, vec
[$hphp], $hhvm_env);
3186 if (!repo_mode_compile($options, $test, $program)) {
3190 if ($options->hhbbc2
) {
3193 "get_options forbids modes because we're not runnig code"
3195 // create tmpdir now so that we can write hhas
3196 Status
::createTestTmpDir($test);
3197 $hhas_temp1 = dump_hhas_to_temp($hhvm[0], "$test.before");
3198 if ($hhas_temp1 is
null) {
3199 Status
::writeDiff($test, "dumping hhas after first hhbbc pass failed");
3202 shell_exec("mv $test_repo/hhvm.hhbbc $test_repo/hhvm.hhbc");
3203 $hhbbc = hhbbc_cmd($options, $test, $program);
3204 $result = exec_with_stack($hhbbc);
3205 if ($result is
string) {
3206 Status
::writeDiff($test, $result);
3209 $hhas_temp2 = dump_hhas_to_temp($hhvm[0], "$test.after");
3210 if ($hhas_temp2 is
null) {
3211 Status
::writeDiff($test, "dumping hhas after second hhbbc pass failed");
3214 $diff = shell_exec("diff $hhas_temp1 $hhas_temp2");
3215 if (trim($diff) !== '') {
3216 Status
::writeDiff($test, $diff);
3221 if ($options->jit_serialize is nonnull
) {
3222 invariant(count($hhvm) === 1, 'get_options enforces jit mode only');
3223 $cmd = jit_serialize_option($hhvm[0], $test, $options, true);
3224 $outputs = run_config_cli($options, $test, $cmd, $hhvm_env);
3225 if ($outputs is
null) return false;
3226 $cmd = jit_serialize_option($hhvm[0], $test, $options, true);
3227 $outputs = run_config_cli($options, $test, $cmd, $hhvm_env);
3228 if ($outputs is
null) return false;
3229 $hhvm[0] = jit_serialize_option($hhvm[0], $test, $options, false);
3232 return run_foreach_config($options, $test, $hhvm, $hhvm_env);
3235 if (file_exists($test.'.onlyrepo')) {
3236 return 'skip-onlyrepo';
3238 if (file_exists($test.'.onlyjumpstart')) {
3239 return 'skip-onlyjumpstart';
3242 if ($options->hhas_round_trip
) {
3244 substr($test, -5) !== ".hhas",
3245 'should_skip_test_simple should have skipped this',
3247 // create tmpdir now so that we can write hhas
3248 Status
::createTestTmpDir($test);
3249 // dumping hhas, not running code so arbitrarily picking a mode
3250 $hhas_temp = dump_hhas_to_temp($hhvm[0], $test);
3251 if ($hhas_temp is
null) {
3252 $err = "system failed: " .
3253 dump_hhas_cmd($hhvm[0], $test,
3254 Status
::getTestTmpPath($test, 'round_trip.hhas')) .
3256 Status
::writeDiff($test, $err);
3259 list($hhvm, $hhvm_env) = hhvm_cmd($options, $test, $hhas_temp);
3262 if ($options->server
) {
3263 return run_config_server($options, $test);
3265 return run_foreach_config($options, $test, $hhvm, $hhvm_env);
3268 function num_cpus(): int {
3271 $data = file('/proc/stat');
3273 foreach($data as $line) {
3274 if (preg_match('/^cpu[0-9]/', $line)) {
3283 return (int)exec('sysctl -n hw.ncpu', inout
$output, inout
$return_var);
3285 return 2; // default when we don't know how to detect.
3288 function make_header(string $str): string {
3289 return "\n\033[0;33m".$str."\033[0m\n";
3292 function print_commands(
3296 if (C\
count($tests) === 0) {
3298 "Test run failed with no failed tests; did a worker process die?"
3300 } else if ($options->verbose
) {
3301 print make_header("Run these by hand:");
3304 print make_header("Run $test by hand:");
3305 $tests = vec
[$test];
3308 foreach ($tests as $test) {
3309 list($commands, $_) = hhvm_cmd($options, $test);
3310 if (!$options->repo
) {
3311 foreach ($commands as $c) {
3317 // How to run it with hhbbc:
3319 $hhbbc_cmds = hphp_cmd($options, $test, $program)."\n";
3320 if (repo_separate($options, $test)) {
3321 $hhbbc_cmd = hhbbc_cmd($options, $test, $program)."\n";
3322 $hhbbc_cmds .= $hhbbc_cmd;
3323 if ($options->hhbbc2
) {
3324 foreach ($commands as $c) {
3326 $c." -vEval.DumpHhas=1 > $test.before.round_trip.hhas\n";
3328 $test_repo = test_repo($options, $test);
3330 "mv $test_repo/hhvm.hhbbc $test_repo/hhvm.hhbc\n";
3331 $hhbbc_cmds .= $hhbbc_cmd;
3332 foreach ($commands as $c) {
3334 $c." -vEval.DumpHhas=1 > $test.after.round_trip.hhas\n";
3337 "diff $test.before.round_trip.hhas $test.after.round_trip.hhas\n";
3340 if ($options->jit_serialize is nonnull
) {
3341 invariant(count($commands) === 1, 'get_options enforces jit mode only');
3343 jit_serialize_option($commands[0], $test, $options, true) . "\n";
3345 jit_serialize_option($commands[0], $test, $options, true) . "\n";
3346 $commands[0] = jit_serialize_option($commands[0], $test, $options, false);
3348 foreach ($commands as $c) {
3349 $hhbbc_cmds .= $c."\n";
3351 print "$hhbbc_cmds\n";
3355 // This runs only in the "printer" child.
3356 function msg_loop(int $num_tests, Queue
$queue): void
{
3360 Status
::getMode() === Status
::MODE_NORMAL ||
3361 Status
::getMode() === Status
::MODE_RECORD_FAILURES
3363 Status
::hasCursorControl();
3365 $stty = strtolower(Status
::getSTTY());
3367 if (preg_match_with_matches("/columns ([0-9]+);/", $stty, inout
$matches) ||
3368 // because BSD has to be different
3369 preg_match_with_matches("/([0-9]+) columns;/", $stty, inout
$matches)) {
3370 $cols = (int)$matches[1];
3375 list($pid, $type, $message) = $queue->receiveMessage();
3376 if (!Status
::handle_message($type, $message)) break;
3378 if ($cols is nonnull
) {
3379 $total_run = (Status
::$skipped + Status
::$failed + Status
::$passed);
3380 $bar_cols = $cols - 45;
3382 $passed_ticks = (int)round($bar_cols * (Status
::$passed / $num_tests));
3383 $skipped_ticks = (int)round($bar_cols * (Status
::$skipped / $num_tests));
3384 $failed_ticks = (int)round($bar_cols * (Status
::$failed / $num_tests));
3386 $fill = $bar_cols - ($passed_ticks +
$skipped_ticks +
$failed_ticks);
3387 if ($fill < 0) $fill = 0;
3389 $passed_ticks = str_repeat('#', $passed_ticks);
3390 $skipped_ticks = str_repeat('#', $skipped_ticks);
3391 $failed_ticks = str_repeat('#', $failed_ticks);
3392 $fill = str_repeat('-', (int)$fill);
3396 "\033[0;32m$passed_ticks",
3397 "\033[33m$skipped_ticks",
3398 "\033[31m$failed_ticks",
3399 "\033[0m$fill] ($total_run/$num_tests) ",
3400 "(", Status
::$skipped, " skipped,", Status
::$failed, " failed)";
3404 if ($cols is nonnull
) {
3405 print "\033[2K\033[1G";
3406 if (Status
::$skipped > 0) {
3407 print Status
::$skipped ." tests \033[1;33mskipped\033[0m\n";
3408 $reasons = Status
::$skip_reasons;
3409 arsort(inout
$reasons);
3410 Status
::$skip_reasons = $reasons as dict
<_
, _
>;
3411 foreach (Status
::$skip_reasons as $reason => $count) {
3412 printf("%12s: %d\n", $reason, $count);
3418 function print_success(
3420 dict
<string, TestResult
> $results,
3423 // We didn't run any tests, not even skipped. Clowntown!
3425 print "\nCLOWNTOWN: No tests!\n";
3426 if (!$options->no_fun
) {
3432 foreach ($results as $result) {
3433 // The result here will either be skipped or passed (since failed is
3434 // handled in print_failure.
3435 if ($result['status'] === 'passed') {
3440 // We just had skipped tests
3442 print "\nSKIP-ALOO: Only skipped tests!\n";
3443 if (!$options->no_fun
) {
3448 print "\nAll tests passed.\n";
3449 if (!$options->no_fun
) {
3452 if ($options->failure_file is nonnull
) {
3453 @unlink
($options->failure_file
);
3455 if ($options->verbose
) {
3456 print_commands($tests, $options);
3460 function print_failure(
3462 dict
<string, TestResult
> $results,
3467 foreach ($results as $result) {
3468 if ($result['status'] === 'failed') {
3469 $failed[] = $result['name'];
3470 } else if ($result['status'] === 'passed') {
3471 $passed[] = $result['name'];
3474 sort(inout
$failed);
3476 $failing_tests_file = $options->failure_file ??
3477 Status
::getRunTmpDir() . '/test-failures';
3478 file_put_contents($failing_tests_file, implode("\n", $failed)."\n");
3480 $passing_tests_file = Status
::getRunTmpDir() . '/tests-passed';
3481 file_put_contents($passing_tests_file, implode("\n", $passed)."\n");
3483 $passing_tests_file = "";
3486 print "\n".count($failed)." tests failed\n";
3487 if (!$options->no_fun
) {
3488 // Unicode for table-flipping emoticon
3489 // https://knowyourmeme.com/memes/flipping-tables
3490 print "(\u{256F}\u{00B0}\u{25A1}\u{00B0}\u{FF09}\u{256F}\u{FE35} \u{253B}";
3491 print "\u{2501}\u{253B}\n";
3494 print_commands($failed, $options);
3496 print make_header("See failed test output and expectations:");
3497 foreach ($failed as $n => $test) {
3498 if ($n !== 0) print "\n";
3499 print 'cat ' . Status
::getTestOutputPath($test, 'diff') . "\n";
3500 print 'cat ' . Status
::getTestOutputPath($test, 'out') . "\n";
3501 $expect_file = get_expect_file_and_type($test, $options)[0];
3502 if ($expect_file is
null) {
3503 print "# no expect file found for $test\n";
3505 print "cat $expect_file\n";
3508 // only print 3 tests worth unless verbose is on
3509 if ($n === 2 && !$options->verbose
) {
3510 $remaining = count($failed) - 1 - $n;
3511 if ($remaining > 0) {
3512 print make_header("... and $remaining more.");
3520 'For xargs, lists of failed and passed tests are available using:'
3522 print 'cat '.$failing_tests_file."\n";
3523 print 'cat '.$passing_tests_file."\n";
3525 print make_header('For xargs, list of failures is available using:').
3526 'cat '.$failing_tests_file."\n";
3530 make_header("Re-run just the failing tests:") .
3531 str_replace("run.php", "run", $argv[0]) . ' ' .
3532 implode(' ', \HH\
global_get('recorded_options')) .
3533 sprintf(' $(cat %s)%s', $failing_tests_file, "\n");
3536 function port_is_listening(int $port): bool {
3537 $socket = socket_create(AF_INET
, SOCK_STREAM
, SOL_TCP
);
3538 return @socket_connect
($socket, 'localhost', $port);
3541 function find_open_port(): int {
3542 for ($i = 0; $i < 50; ++
$i) {
3543 $port = rand(1024, 65535);
3544 if (!port_is_listening($port)) return $port;
3547 error("Couldn't find an open port");
3550 function start_server_proc(
3555 if ($options->cli_server
) {
3556 $cli_sock = tempnam(sys_get_temp_dir(), 'hhvm-cli-');
3558 // still want to test that an unwritable socket works...
3559 $cli_sock = '/var/run/hhvm-cli.sock';
3561 $threads = get_num_threads($options);
3562 $thread_option = $options->cli_server
3563 ?
'-vEval.UnixServerWorkers='.$threads
3564 : '-vServer.ThreadCount='.$threads;
3565 $prelude = $options->server
3566 ?
'-vEval.PreludePath=' . Status
::getRunTmpDir() . '/server-prelude.php'
3568 $command = hhvm_cmd_impl(
3571 null, // we do not pass Autoload.DB.Path to the server process
3573 "-vServer.Port=$port",
3574 "-vServer.Type=proxygen",
3575 "-vAdminServer.Port=0",
3577 '-vServer.ExitOnBindFail=1',
3578 '-vServer.RequestTimeoutSeconds='.SERVER_TIMEOUT
,
3579 '-vPageletServer.ThreadCount=0',
3580 '-vLog.UseRequestLog=1',
3581 '-vLog.File=/dev/null',
3584 // The server will unlink the temp file
3585 '-vEval.UnixServerPath='.$cli_sock,
3587 // This ensures we actually jit everything:
3588 '-vEval.JitRequireWriteLease=1',
3590 // The default test config uses a small TC but we'll be running thousands
3591 // of tests against the same process:
3592 '-vEval.JitASize=394264576',
3593 '-vEval.JitAColdSize=201326592',
3594 '-vEval.JitAFrozenSize=251658240',
3595 '-vEval.JitGlobalDataSize=32000000',
3597 // load/store counters don't work on Ivy Bridge so disable for tests
3598 '-vEval.ProfileHWEnable=false'
3600 if (count($command) !== 1) {
3601 error("Can't run multi-mode tests in server mode");
3603 $command = $command[0];
3604 if (getenv('HHVM_TEST_SERVER_LOG')) {
3605 echo "Starting server '$command'\n";
3608 $descriptors = dict
[
3609 0 => vec
['file', '/dev/null', 'r'],
3610 1 => vec
['file', '/dev/null', 'w'],
3611 2 => vec
['file', '/dev/null', 'w'],
3615 $proc = proc_open($command, $descriptors, inout
$dummy);
3617 error("Failed to start server process");
3619 $status = proc_get_status($proc); // dict<string, mixed>
3620 $pid = $status['pid'] as int;
3621 $server = new Server($proc, $pid, $port, $config, $cli_sock);
3625 final class Server
{
3626 public function __construct(
3627 public resource $proc,
3630 public string $config,
3631 public string $cli_socket,
3636 final class Servers
{
3637 public dict
<int, Server
> $pids = dict
[];
3638 public dict
<string, Server
> $configs = dict
[];
3641 // For each config file in $configs, start up a server on a randomly-determined
3643 function start_servers(
3645 keyset
<string> $configs,
3647 if ($options->server
) {
3650 <<__EntryPoint>> function UNIQUE_NAME_I_DONT_EXIST_IN_ANY_TEST(): void {
3651 putenv("HPHP_TEST_TMPDIR=BASEDIR{$_SERVER['SCRIPT_NAME']}.tmpdir");
3655 Status
::getRunTmpDir() . '/server-prelude.php',
3656 str_replace('BASEDIR', Status
::getRunTmpDir(), $prelude),
3661 foreach ($configs as $config) {
3662 $starting[] = start_server_proc($options, $config, find_open_port());
3665 $start_time = mtime();
3666 $servers = new Servers();
3668 // Wait for all servers to come up.
3669 while (count($starting) > 0) {
3670 $still_starting = vec
[];
3672 foreach ($starting as $server) {
3673 $config = $server->config
;
3674 $pid = $server->pid
;
3675 $port = $server->port
;
3676 $proc = $server->proc
;
3678 $new_status = proc_get_status($proc);
3680 if (!$new_status['running']) {
3681 if ($new_status['exitcode'] === 0) {
3682 error("Server exited prematurely but without error");
3685 // We lost a race. Try another port.
3686 if (getenv('HHVM_TEST_SERVER_LOG')) {
3687 echo "\n\nLost connection race on port $port. Trying another.\n\n";
3689 $port = find_open_port();
3690 $still_starting[] = start_server_proc($options, $config, $port);
3691 } else if (!port_is_listening($port)) {
3692 $still_starting[] = $server;
3694 $servers->pids
[$pid] = $server;
3695 $servers->configs
[$config] = $server;
3699 $starting = $still_starting;
3701 if (mtime() - $start_time > $max_time) {
3702 error("Servers took more than $max_time seconds to come up");
3705 // Take a short nap and try again.
3709 $elapsed = mtime() - $start_time;
3710 printf("Started %d servers in %.1f seconds\n\n", count($configs), $elapsed);
3714 function get_num_threads(Options
$options): int {
3715 if ($options->threads is nonnull
) {
3716 $threads = (int)$options->threads
;
3717 if ((string)$threads !== $options->threads ||
$threads < 1) {
3718 error("--threads must be an integer >= 1");
3721 $threads = $options->server ||
$options->cli_server
3722 ?
num_cpus() * 2 : num_cpus();
3727 function runner_precheck(): void
{
3728 // Basic checking for runner.
3729 $server = HH\
global_get('_SERVER');
3730 $env = HH\
global_get('_ENV');
3731 if (!((bool)$server ??
false) ||
!((bool)$env ??
false)) {
3732 echo "Warning: \$_SERVER/\$_ENV variables not available, please check \n" .
3733 "your ini setting: variables_order, it should have both 'E' and 'S'\n";
3737 function main(vec
<string> $argv): int {
3740 ini_set('pcre.backtrack_limit', PHP_INT_MAX
);
3742 list($options, $files) = get_options($argv);
3743 if ($options->help
) {
3746 if ($options->list_tests
) {
3747 list_tests($files, $options);
3752 $tests = find_tests($files, $options);
3753 if ($options->shuffle
) {
3754 shuffle(inout
$tests);
3757 // Explicit path given by --hhvm-binary-path takes priority. Then, if an
3758 // HHVM_BIN env var exists, and the file it points to exists, that trumps
3759 // any default hhvm executable path.
3760 if ($options->hhvm_binary_path is nonnull
) {
3761 $binary_path = check_executable($options->hhvm_binary_path
);
3762 putenv("HHVM_BIN=" . $binary_path);
3763 } else if (getenv("HHVM_BIN") !== false) {
3764 $binary_path = check_executable(getenv("HHVM_BIN"));
3766 check_for_multiple_default_binaries();
3767 $binary_path = hhvm_path();
3770 if ($options->verbose
) {
3771 print "You are using the binary located at: " . $binary_path . "\n";
3774 Status
::createTmpDir();
3777 if ($options->server ||
$options->cli_server
) {
3778 if ($options->server
&& $options->cli_server
) {
3779 error("Server mode and CLI Server mode are mutually exclusive");
3781 if ($options->repo
) {
3782 error("Server mode repo tests are not supported");
3785 /* We need to start up a separate server process for each config file
3787 $configs = keyset
[];
3788 foreach ($tests as $test) {
3789 $config = find_file_for_dir(dirname($test), 'config.ini');
3791 error("Couldn't find config file for $test");
3793 if (array_key_exists($config, $configs)) continue;
3794 if (should_skip_test_simple($options, $test) is nonnull
) continue;
3795 $configs[] = $config;
3799 if (count($configs) > $max_configs) {
3800 error("More than $max_configs unique config files will be needed to run ".
3801 "the tests you specified. They may not be a good fit for server ".
3802 "mode. (".count($configs)." required)");
3805 $servers = start_servers($options, $configs);
3806 $options->servers
= $servers;
3809 // Try to construct the buckets so the test results are ready in
3810 // approximately alphabetical order.
3811 // Get the serial tests to be in their own bucket later.
3812 $serial_tests = serial_only_tests($tests);
3814 // If we have no serial tests, we can use the maximum number of allowed
3815 // threads for the test running. If we have some, we save one thread for
3816 // the serial bucket. However if we only have one thread, we don't split
3817 // out serial tests.
3818 $parallel_threads = min(get_num_threads($options), \
count($tests)) as int;
3819 if ($parallel_threads === 1) {
3820 $test_buckets = vec
[$tests];
3822 if (count($serial_tests) > 0) {
3823 // reserve a thread for serial tests
3824 $parallel_threads--;
3827 $test_buckets = vec
[];
3828 for ($i = 0; $i < $parallel_threads; $i++
) {
3829 $test_buckets[] = vec
[];
3833 foreach ($tests as $test) {
3834 if (!in_array($test, $serial_tests)) {
3835 $test_buckets[$i][] = $test;
3836 $i = ($i +
1) %
$parallel_threads;
3840 if (count($serial_tests) > 0) {
3841 // The last bucket is serial.
3842 $test_buckets[] = $serial_tests;
3846 // Remember that the serial tests are also in the tests array too,
3847 // so they are part of the total count.
3848 if (!$options->testpilot
) {
3849 print "Running ".count($tests)." tests in ".
3850 count($test_buckets)." threads (" . count($serial_tests) .
3854 if ($options->verbose
) {
3855 Status
::setMode(Status
::MODE_VERBOSE
);
3857 if ($options->testpilot
) {
3858 Status
::setMode(Status
::MODE_TESTPILOT
);
3860 if ($options->record_failures is nonnull
) {
3861 Status
::setMode(Status
::MODE_RECORD_FAILURES
);
3863 Status
::setUseColor($options->color ||
posix_isatty(STDOUT
));
3865 Status
::$nofork = count($tests) === 1 && !$servers;
3867 if (!Status
::$nofork) {
3868 // Create the Queue before any children are forked.
3869 $queue = Status
::getQueue();
3871 // Fork a "printer" child to process status messages.
3872 $printer_pid = pcntl_fork();
3873 if ($printer_pid === -1) {
3874 error("failed to fork");
3875 } else if ($printer_pid === 0) {
3876 msg_loop(count($tests), $queue);
3880 // Satisfy the type-checker.
3884 // Unblock the Queue (if needed).
3887 // Fork "worker" children (if needed).
3889 // We write results as json in each child and collate them at the end
3890 $json_results_files = vec
[];
3891 if (Status
::$nofork) {
3892 Status
::registerCleanup($options->no_clean
);
3893 $json_results_file = tempnam('/tmp', 'test-run-');
3894 $json_results_files[] = $json_results_file;
3895 invariant(count($test_buckets) === 1, "nofork was set erroneously");
3896 $return_value = child_main($options, $test_buckets[0], $json_results_file);
3898 foreach ($test_buckets as $test_bucket) {
3899 $json_results_file = tempnam('/tmp', 'test-run-');
3900 $json_results_files[] = $json_results_file;
3901 $pid = pcntl_fork();
3903 error('could not fork');
3905 $children[$pid] = $pid;
3907 invariant($test_bucket is vec
<_
>, "%s", __METHOD__
);
3908 exit(child_main($options, $test_bucket, $json_results_file));
3912 // Make sure to clean up on exit, or on SIGTERM/SIGINT.
3913 // Do this here so no children inherit this.
3914 Status
::registerCleanup($options->no_clean
);
3916 // Have the parent wait for all forked children to exit.
3918 while (count($children) && $printer_pid !== 0) {
3920 $pid = pcntl_wait(inout
$status);
3921 if (pcntl_wifexited($status)) {
3922 $bad_end = pcntl_wexitstatus($status) !== 0;
3923 } else if (pcntl_wifsignaled($status)) {
3926 error("Unexpected exit status from child");
3929 if ($pid === $printer_pid) {
3930 // We should be finishing up soon.
3933 // Don't consider the run successful if the printer worker died
3936 } else if ($servers && isset($servers->pids
[$pid])) {
3937 // A server crashed. Restart it.
3938 // We intentionally ignore $bad_end here because we expect this to
3939 // show up as a test failure in whatever test was running on the server
3940 // when it crashed. TODO(alexeyt): assert $bad_end === true?
3941 if (getenv('HHVM_TEST_SERVER_LOG')) {
3942 echo "\nServer $pid crashed. Restarting.\n";
3944 Status
::serverRestarted();
3945 $server = $servers->pids
[$pid];
3946 $server = start_server_proc($options, $server->config
, $server->port
);
3948 // Unset the old $pid entry and insert the new one.
3949 unset($servers->pids
[$pid]);
3950 $pid = $server->pid
;
3951 $servers->pids
[$pid] = $server;
3952 } else if (isset($children[$pid])) {
3953 unset($children[$pid]);
3955 // If any worker process dies we should fail the test run
3959 error("Got status for child that we didn't know we had with pid $pid");
3964 Status
::finished($return_value);
3966 // Wait for the printer child to exit, if needed.
3967 if (!Status
::$nofork && $printer_pid !== 0) {
3969 $pid = pcntl_waitpid($printer_pid, inout
$status);
3970 $status = $status as int;
3971 if (pcntl_wifexited($status)) {
3972 if (pcntl_wexitstatus($status) !== 0) {
3973 // Don't consider the run successful if the printer worker died
3976 } else if (pcntl_wifsignaled($status)) {
3977 // Don't consider the run successful if the printer worker died
3980 error("Unexpected exit status from child");
3984 // Kill the servers.
3986 foreach ($servers->pids
as $server) {
3987 proc_terminate($server->proc
);
3988 proc_close($server->proc
);
3992 // Aggregate results.
3994 foreach ($json_results_files as $json_results_file) {
3995 $contents = file_get_contents($json_results_file);
3996 $json = json_decode($contents, true);
3997 if (!is_dict($json)) {
3999 "\nNo JSON output was received from a test thread. ".
4000 "Either you killed it, or it might be a bug in the test script.",
4003 $results = array_merge($results, $json);
4004 unlink($json_results_file);
4008 if ($options->record_failures is nonnull
) {
4009 $fail_file = $options->record_failures
;
4010 $failed_tests = vec
[];
4011 $prev_failing = vec
[];
4012 if (file_exists($fail_file)) {
4013 $prev_failing = explode("\n", file_get_contents($fail_file));
4018 foreach ($results as $r) {
4019 if (!isset($r['name']) ||
!isset($r['status'])) continue;
4020 $test = canonical_path($r['name']);
4021 $status = $r['status'];
4022 if ($status === 'passed' && in_array($test, $prev_failing)) {
4026 if ($status !== 'failed') continue;
4027 if (!in_array($test, $prev_failing)) $new_fails++
;
4028 $failed_tests[] = $test;
4031 "Recording %d tests as failing.\n".
4032 "There are %d new failing tests, and %d new passing tests.\n",
4033 count($failed_tests), $new_fails, $new_passes
4035 sort(inout
$failed_tests);
4036 file_put_contents($fail_file, implode("\n", $failed_tests));
4037 } else if ($options->testpilot
) {
4038 Status
::say(dict
['op' => 'all_done', 'results' => $results]);
4039 return $return_value;
4040 } else if (!$return_value) {
4041 print_success($tests, $results, $options);
4043 print_failure($argv, $results, $options);
4046 Status
::sayColor("\nTotal time for all executed tests as run: ",
4049 Status
::getOverallEndTime() -
4050 Status
::getOverallStartTime()));
4051 Status
::sayColor("Total time for all executed tests if run serially: ",
4054 Status
::addTestTimesSerial($results)));
4056 return $return_value;
4060 function run_main(): void
{
4061 exit(main(get_argv()));
4064 // Inline ASCII art moved to end-of-file to avoid confusing emacs.
4066 function print_clown(): void
{
4082 function print_skipper(): void
{
4106 function print_ship(): void
{
4112 _____|____|____|____\\\__
4113 ---------\ SHIP IT
/---------
4114 ^^^^^ ^^^^^^^^^^^^^^^^^^^^^