fix Typing[4110] errors by refactoring Options
[hiphop-php.git] / hphp / test / run.php
blob165d5271e0caefce0a252a01fc791831fafab7c0
1 <?hh
2 /**
3 * Run the test suites in various configurations.
4 */
6 const int TIMEOUT_SECONDS = 300;
8 function get_argv(): vec<string> {
9 return \HH\FIXME\UNSAFE_CAST<vec<mixed>,vec<string>>(
10 \HH\global_get('argv') as vec<_>
14 function mtime(): float {
15 return microtime(true) as float;
18 function safe_realpath(string $path): string {
19 return realpath($path) as string;
22 function safe_implode(string $delim, mixed $strs): string {
23 return implode($delim, $strs) as string;
26 // NOTE: The "HPHP_HOME" environment variable can be set (to ".../fbcode"), to
27 // define "hphp_home()" and (indirectly) "test_dir()". Otherwise, we will use
28 // "__DIR__" as "test_dir()", and its grandparent directory for "hphp_home()"
29 // (unless we are testing a dso extensions).
31 <<__Memoize>>
32 function is_testing_dso_extension(): bool {
33 $home = getenv("HPHP_HOME");
34 if ($home is string) {
35 return false;
37 // detecting if we're running outside of the hhvm codebase.
38 return !is_file(__DIR__."/../../hphp/test/run.php");
41 <<__Memoize>>
42 function hphp_home(): string {
43 $home = getenv("HPHP_HOME");
44 if ($home is string) {
45 return safe_realpath($home);
47 if (is_testing_dso_extension()) {
48 return safe_realpath(__DIR__);
50 return safe_realpath(__DIR__."/../..");
53 <<__Memoize>>
54 function test_dir(): string {
55 $home = getenv("HPHP_HOME");
56 if ($home is string) {
57 return safe_realpath($home)."/hphp/test";
59 return __DIR__;
62 function get_expect_file_and_type(
63 string $test,
64 Options $options,
65 ): vec<?string> {
66 $types = vec[
67 'expect',
68 'expectf',
69 'expectregex',
70 'hhvm.expect',
71 'hhvm.expectf',
73 if ($options->repo) {
74 if (file_exists($test . '.hphpc_assert')) {
75 return vec[$test . '.hphpc_assert', 'expectf'];
77 if (file_exists($test . '.hhbbc_assert')) {
78 return vec[$test . '.hhbbc_assert', 'expectf'];
80 foreach ($types as $type) {
81 $fname = "$test.$type-repo";
82 if (file_exists($fname)) {
83 return vec[$fname, $type];
88 foreach ($types as $type) {
89 $fname = "$test.$type";
90 if (file_exists($fname)) {
91 return vec[$fname, $type];
94 return vec[null, null];
97 function multi_request_modes(Options $options): vec<string> {
98 $r = vec[];
99 if ($options->retranslate_all is nonnull) $r []= 'retranslate-all';
100 if ($options->recycle_tc is nonnull) $r []= 'recycle-tc';
101 if ($options->jit_serialize is nonnull) $r []= 'jit-serialize';
102 if ($options->cli_server) $r []= 'cli-server';
103 return $r;
106 function has_multi_request_mode(Options $options): bool {
107 return count(multi_request_modes($options)) != 0;
110 function test_repo(Options $options, string $test): string {
111 if ($options->repo_out is nonnull) {
112 return $options->repo_out . '/' . str_replace('/', '.', $test) . '.repo';
114 return Status::getTestTmpPath($test, 'repo');
117 function jit_serialize_option(
118 string $cmd, string $test, Options $options, bool $serialize,
119 ): string {
120 $serialized = test_repo($options, $test) . "/jit.dump";
121 $cmds = explode(' -- ', $cmd, 2);
122 $jit_serialize = (int)($options->jit_serialize ?? 0);
123 $cmds[0] .=
124 ' --count=' . ($serialize ? $jit_serialize + 1 : 1) .
125 " -vEval.JitSerdesFile=\"" . $serialized . "\"" .
126 " -vEval.JitSerdesMode=" . ($serialize ? 'Serialize' : 'DeserializeOrFail') .
127 ($serialize ? " -vEval.JitSerializeOptProfRequests=" . $jit_serialize : '');
128 if ($options->jitsample is nonnull && $serialize) {
129 $cmds[0] .= ' -vDeploymentId="' . $options->jitsample . '-serialize"';
131 return safe_implode(' -- ', $cmds);
134 function usage(): string {
135 $argv = get_argv();
136 return "usage: {$argv[0]} [-m jit|interp] [-r] <test/directories>";
139 function help(): string {
140 $argv = get_argv();
141 $ztestexample = 'test/zend/good/*/*z*.php'; // sep. for syntax highlighting.
142 $help = <<<EOT
145 This is the hhvm test-suite runner. For more detailed documentation,
146 see hphp/test/README.md.
148 The test argument may be a path to a php test file, a directory name, or
149 one of a few pre-defined suite names that this script knows about.
151 If you work with hhvm a lot, you might consider a bash alias:
153 alias ht="path/to/hphp/test/run"
155 Examples:
157 # Quick tests in JIT mode:
158 % {$argv[0]} test/quick
160 # Slow tests in interp mode:
161 % {$argv[0]} -m interp test/slow
163 # PHP specification tests in JIT mode:
164 % {$argv[0]} test/slow/spec
166 # Slow closure tests in JIT mode:
167 % {$argv[0]} test/slow/closure
169 # Slow closure tests in JIT mode with RepoAuthoritative:
170 % {$argv[0]} -r test/slow/closure
172 # Slow array tests, in RepoAuthoritative:
173 % {$argv[0]} -r test/slow/array
175 # Zend tests with a "z" in their name:
176 % {$argv[0]} $ztestexample
178 # Quick tests in JIT mode with some extra runtime options:
179 % {$argv[0]} test/quick -a '-vEval.JitMaxTranslations=120 -vEval.HHIRRefcountOpts=0'
181 # Quick tests in JIT mode with RepoAuthoritative and an extra compile-time option:
182 % {$argv[0]} test/quick -r --compiler-args '--parse-on-demand=false'
184 # All quick tests except debugger
185 % {$argv[0]} -e debugger test/quick
187 # All tests except those containing a string of 3 digits
188 % {$argv[0]} -E '/\d{3}/' all
190 # All tests whose name containing pdo_mysql
191 % {$argv[0]} -i pdo_mysql -m jit -r zend
193 # Print all the standard tests
194 % {$argv[0]} --list-tests
196 # Use a specific HHVM binary
197 % {$argv[0]} -b ~/code/hhvm/hphp/hhvm/hhvm
198 % {$argv[0]} --hhvm-binary-path ~/code/hhvm/hphp/hhvm/hhvm
200 # Use retranslate all. Run the test n times, then run retranslate all, then
201 # run the test n more on the new code.
202 % {$argv[0]} --retranslate-all 2 quick
204 # Use jit-serialize. Run the test n times, then run retranslate all, run the
205 # test once more, serialize all profile data, and then restart hhvm, load the
206 # serialized state and run retranslate-all before starting the test.
207 % {$argv[0]} --jit-serialize 2 -r quick
208 EOT;
210 return usage().$help;
213 function error(string $message): noreturn {
214 print "$message\n";
215 exit(1);
218 function success(): noreturn {
219 // ISSUE: Is this newline important?
220 print "\n";
221 exit(0);
224 // If a user-supplied path is provided, let's make sure we have a valid
225 // executable. Returns canonicanalized path or exits.
226 function check_executable(string $path): string {
227 $rpath = realpath($path);
228 if ($rpath === false || !is_executable($rpath)) {
229 error("Provided HHVM executable ($path) is not an executable file.\n" .
230 "If using HHVM_BIN, make sure that is set correctly.");
233 $output = vec[];
234 $return_var = -1;
235 exec($rpath . " --version 2> /dev/null", inout $output, inout $return_var);
236 // ISSUE: Should just use "starts_with".
237 if (strpos(safe_implode("", $output), "HipHop ") !== 0) {
238 error("Provided file ($rpath) is not an HHVM executable.\n" .
239 "If using HHVM_BIN, make sure that is set correctly.");
242 return $rpath;
245 function hhvm_binary_routes(): dict<string, string> {
246 return dict[
247 "buck" => "/buck-out/gen/hphp/hhvm/hhvm",
248 "cmake" => "/hphp/hhvm"
252 function hh_codegen_binary_routes(): dict<string, string> {
253 return dict[
254 "buck" => "/buck-out/bin/hphp/hack/src/hh_single_compile",
255 "cmake" => "/hphp/hack/bin"
259 // For Facebook: We have several build systems, and we can use any of them in
260 // the same code repo. If multiple binaries exist, we want the onus to be on
261 // the user to specify a particular one because before we chose the buck one
262 // by default and that could cause unexpected results.
263 function check_for_multiple_default_binaries(): void {
264 // Env var we use in testing that'll pick which build system to use.
265 if (getenv("FBCODE_BUILD_TOOL") !== false) {
266 return;
269 $home = hphp_home();
270 $found = vec[];
271 foreach (hhvm_binary_routes() as $path) {
272 $abs_path = $home . $path . "/hhvm";
273 if (file_exists($abs_path)) {
274 $found[] = $abs_path;
278 if (count($found) <= 1) {
279 return;
282 $msg = "Multiple binaries exist in this repo. \n";
283 foreach ($found as $bin) {
284 $msg .= " - " . $bin . "\n";
286 $msg .= "Are you in fbcode? If so, remove a binary \n"
287 . "or use the --hhvm-binary-path option to the test runner. \n"
288 . "e.g. test/run --hhvm-binary-path /path/to/binary slow\n";
289 error($msg);
292 function hhvm_path(): string {
293 $file = "";
294 $hhvm_bin = getenv("HHVM_BIN");
295 if ($hhvm_bin is string) {
296 $file = safe_realpath($hhvm_bin);
297 } else {
298 $file = bin_root().'/hhvm';
301 if (!is_file($file)) {
302 if (is_testing_dso_extension()) {
303 $output = null;
304 $return_var = -1;
305 exec("which hhvm 2> /dev/null", inout $output, inout $return_var);
306 if (isset($output[0]) && $output[0]) {
307 return $output[0];
309 error("You need to specify hhvm bin with env HHVM_BIN");
312 error("$file doesn't exist. Did you forget to build first?");
314 return rel_path($file);
317 function bin_root(): string {
318 $hhvm_bin = getenv("HHVM_BIN");
319 if ($hhvm_bin is string) {
320 return dirname(safe_realpath($hhvm_bin));
323 $home = hphp_home();
324 $env_tool = getenv("FBCODE_BUILD_TOOL");
325 $routes = hhvm_binary_routes();
327 if ($env_tool !== false) {
328 return $home . $routes[$env_tool];
331 foreach ($routes as $_ => $path) {
332 $dir = $home . $path;
333 if (is_dir($dir)) {
334 return $dir;
338 return $home . $routes["cmake"];
341 function hh_codegen_path(): string {
342 $file = "";
343 $hh = getenv("HH_CODEGEN_BIN");
344 if ($hh is string) {
345 $file = safe_realpath($hh);
346 } else {
347 $file = hh_codegen_bin_root().'/hh_single_compile.opt';
349 if (!is_file($file)) {
350 error("$file doesn't exist. Did you forget to build first?");
352 return rel_path($file);
355 function hh_codegen_bin_root(): string {
356 $home = hphp_home();
357 $env_tool = getenv("FBCODE_BUILD_TOOL");
358 $routes = hh_codegen_binary_routes();
360 if ($env_tool !== false) {
361 return $home . $routes[$env_tool];
364 foreach ($routes as $_ => $path) {
365 $dir = $home . $path;
366 if (is_dir($dir)) {
367 return $dir;
371 return $home . $routes["cmake"];
374 function unit_cache_file(): string {
375 return Status::getTmpPathFile('unit-cache.sql');
378 function read_opts_file(?string $file): string {
379 if ($file is null || !file_exists($file)) {
380 return "";
382 $fp = fopen($file, "r");
383 invariant($fp is resource, "%s", __METHOD__);
385 $contents = "";
386 for ($line = fgets($fp); $line; $line = fgets($fp)) {
387 // Compress out white space.
388 $line = preg_replace('/\s+/', ' ', $line);
390 // Discard simple line oriented ; and # comments to end of line
391 // Comments at end of line (after payload) are not allowed.
392 $line = preg_replace('/^ *;.*$/', ' ', $line);
393 $line = preg_replace('/^ *#.*$/', ' ', $line);
395 // Substitute in the directory name
396 $line = str_replace('__DIR__', dirname($file), $line);
398 $contents .= $line;
400 fclose($fp);
401 return $contents;
404 // http://stackoverflow.com/questions/2637945/
405 function rel_path(string $to): string {
406 $from = explode('/', getcwd().'/');
407 $to = explode('/', $to);
408 $from_len = count($from);
409 $to_len = count($to);
411 // find first non-matching dir.
412 for ($d = 0; $d < $from_len; ++$d) {
413 if ($d >= $to_len || $from[$d] !== $to[$d])
414 break;
417 $relPath = vec[];
419 // get number of remaining dirs in $from.
420 $remaining = $from_len - $d - 1;
421 if ($remaining > 0) {
422 // add traversals up to first matching dir.
423 do {
424 $relPath[] = '..';
425 $remaining--;
426 } while ($remaining > 0);
427 } else {
428 $relPath[] = '.';
430 while ($d < $to_len) {
431 $relPath[] = $to[$d];
432 $d++;
434 return safe_implode('/', $relPath);
437 // Keep this in sync with the dict in get_options() below.
438 // Options taking a value (with a trailing `:` in the dict key)
439 // should be ?string. Otherwise they should be bool.
440 final class Options {
441 public ?string $env;
442 public ?string $exclude;
443 public ?string $exclude_pattern;
444 public ?string $exclude_recorded_failures;
445 public ?string $include;
446 public ?string $include_pattern;
447 public bool $repo = false;
448 public bool $split_hphpc = false;
449 public bool $repo_single = false;
450 public bool $repo_separate = false;
451 public ?string $repo_threads;
452 public ?string $repo_out;
453 public bool $hhbbc2 = false;
454 public ?string $mode;
455 public bool $server = false;
456 public bool $cli_server = false;
457 public bool $shuffle = false;
458 public bool $help = false;
459 public bool $verbose = false;
460 public bool $testpilot = false;
461 public ?string $threads;
462 public ?string $args;
463 public ?string $compiler_args;
464 public bool $log = false;
465 public ?string $failure_file;
466 public bool $wholecfg = false;
467 public bool $hhas_round_trip = false;
468 public bool $color = false;
469 public bool $no_fun = false;
470 public bool $no_skipif = false;
471 public bool $cores = false;
472 public bool $dump_tc = false;
473 public bool $no_clean = false;
474 public bool $list_tests = false;
475 public ?string $recycle_tc;
476 public ?string $retranslate_all;
477 public ?string $jit_serialize;
478 public ?string $hhvm_binary_path;
479 public ?string $vendor;
480 public ?string $record_failures;
481 public ?string $ignore_oids;
482 public ?string $jitsample;
483 public ?string $hh_single_type_check;
484 public bool $write_to_checkout = false;
485 public bool $bespoke = false;
486 public bool $lazyclass = false;
488 // Additional state added for convenience since Options is plumbed
489 // around almost everywhere.
490 public ?Servers $servers = null;
493 function get_options(
494 vec<string> $argv,
495 ): (Options, vec<string>) {
496 // Options marked * affect test behavior, and need to be reported by list_tests.
497 // Options with a trailing : take a value.
498 $parameters = dict[
499 '*env:' => '',
500 'exclude:' => 'e:',
501 'exclude-pattern:' => 'E:',
502 'exclude-recorded-failures:' => 'x:',
503 'include:' => 'i:',
504 'include-pattern:' => 'I:',
505 '*repo' => 'r',
506 '*split-hphpc' => '',
507 '*repo-single' => '',
508 '*repo-separate' => '',
509 '*repo-threads:' => '',
510 '*repo-out:' => '',
511 '*hhbbc2' => '',
512 '*mode:' => 'm:',
513 '*server' => 's',
514 '*cli-server' => 'S',
515 'shuffle' => '',
516 'help' => 'h',
517 'verbose' => 'v',
518 'testpilot' => '',
519 'threads:' => '',
520 '*args:' => 'a:',
521 '*compiler-args:' => '',
522 'log' => 'l',
523 'failure-file:' => '',
524 '*wholecfg' => '',
525 '*hhas-round-trip' => '',
526 'color' => 'c',
527 'no-fun' => '',
528 'no-skipif' => '',
529 'cores' => '',
530 'dump-tc' => '',
531 'no-clean' => '',
532 'list-tests' => '',
533 '*recycle-tc:' => '',
534 '*retranslate-all:' => '',
535 '*jit-serialize:' => '',
536 '*hhvm-binary-path:' => 'b:',
537 '*vendor:' => '',
538 'record-failures:' => '',
539 '*ignore-oids' => '',
540 'jitsample:' => '',
541 '*hh_single_type_check:' => '',
542 'write-to-checkout' => '',
543 'bespoke' => '',
544 'lazyclass' => '',
546 $options = new Options() as dynamic;
547 $files = vec[];
548 $recorded = vec[];
551 * '-' argument causes all future arguments to be treated as filenames, even
552 * if they would otherwise match a valid option. Otherwise, arguments starting
553 * with '-' MUST match a valid option.
555 $force_file = false;
557 for ($i = 1; $i < count($argv); $i++) {
558 $arg = $argv[$i];
560 if (strlen($arg) === 0) {
561 continue;
562 } else if ($force_file) {
563 $files[] = $arg;
564 } else if ($arg === '-') {
565 $forcefile = true;
566 } else if ($arg[0] === '-') {
567 $found = false;
569 foreach ($parameters as $long => $short) {
570 if ($arg == '-'.str_replace(':', '', $short) ||
571 $arg == '--'.str_replace(vec[':', '*'], vec['', ''], $long)) {
572 $record = substr($long, 0, 1) === '*';
573 if ($record) $recorded[] = $arg;
574 if (substr($long, -1, 1) === ':') {
575 $i++;
576 $value = $argv[$i];
577 if ($record) $recorded[] = $value;
578 } else {
579 $value = true;
581 $name = str_replace(vec[':', '*', '-'], vec['', '', '_'], $long);
582 $options->{$name} = $value;
583 $found = true;
584 break;
588 if (!$found) {
589 $msg = sprintf("Invalid argument: '%s'\nSee %s --help", $arg, $argv[0]);
590 error($msg as string);
592 } else {
593 $files[] = $arg;
596 $options = $options as Options;
598 \HH\global_set('recorded_options', $recorded);
600 $repo_out = $options->repo_out;
601 if ($repo_out is string && !is_dir($repo_out)) {
602 if (!mkdir($repo_out) && !is_dir($repo_out)) {
603 echo "Unable to create repo-out dir " . $repo_out . "\n";
604 exit(1);
607 if ($options->hhbbc2) {
608 $options->repo_separate = true;
609 if ($options->repo || $options->repo_single) {
610 echo "repo-single/repo and hhbbc2 are mutually exclusive options\n";
611 exit(1);
613 if (isset($options['mode'])) {
614 echo "hhbbc2 doesn't support modes; it compares hhas, doesn't run code\n";
615 exit(1);
619 if ($options->repo_single || $options->repo_separate) {
620 $options->repo = true;
621 } else if ($options->repo) {
622 // if only repo was set, then it means repo single
623 $options->repo_single = true;
626 if ($options->jit_serialize is nonnull) {
627 if (!$options->repo) {
628 echo "jit-serialize only works in repo mode\n";
629 exit(1);
631 if ($options->mode is nonnull && $options->mode !== 'jit') {
632 echo "jit-serialize only works in jit mode\n";
633 exit(1);
637 if ($options->split_hphpc) {
638 if (!$options->repo) {
639 echo "split-hphpc only works in repo mode\n";
640 exit(1);
642 if (!$options->repo_separate) {
643 echo "split-hphpc only works in repo-separate mode\n";
644 exit(1);
648 if ($options->repo && $options->hhas_round_trip) {
649 echo "repo and hhas-round-trip are mutually exclusive options\n";
650 exit(1);
653 $multi_request_modes = multi_request_modes($options);
654 if (count($multi_request_modes) > 1) {
655 echo "The options\n -", safe_implode("\n -", $multi_request_modes),
656 "\nare mutually exclusive options\n";
657 exit(1);
660 if ($options->write_to_checkout) {
661 Status::$write_to_checkout = true;
664 return tuple($options, $files);
668 * Return the path to $test relative to $base, or false if $base does not
669 * contain test.
671 function canonical_path_from_base(string $test, string $base): mixed {
672 $full = safe_realpath($test);
673 if (substr($full, 0, strlen($base)) === $base) {
674 return substr($full, strlen($base) + 1);
676 $dirstat = stat($base);
677 if (!is_dict($dirstat)) return false;
678 for ($p = dirname($full); $p && $p !== "/"; $p = dirname($p)) {
679 $s = stat($p);
680 if (!is_dict($s)) continue;
681 if ($s['ino'] === $dirstat['ino'] && $s['dev'] === $dirstat['dev']) {
682 return substr($full, strlen($p) + 1);
685 return false;
688 // ISSUE: This should probably return string.
689 function canonical_path(string $test): mixed {
690 $attempt = canonical_path_from_base($test, test_dir());
691 if ($attempt === false) {
692 // ISSUE: What if this is false?
693 return canonical_path_from_base($test, hphp_home());
694 } else {
695 return $attempt;
700 * We support some 'special' file names, that just know where the test
701 * suites are, to avoid typing 'hphp/test/foo'.
703 function find_test_files(string $file): vec<string>{
704 $mappage = dict[
705 'quick' => 'hphp/test/quick',
706 'slow' => 'hphp/test/slow',
707 'debugger' => 'hphp/test/server/debugger/tests',
708 'http' => 'hphp/test/server/http/tests',
709 'fastcgi' => 'hphp/test/server/fastcgi/tests',
710 'zend' => 'hphp/test/zend/good',
711 'facebook' => 'hphp/facebook/test',
712 'taint' => 'hphp/test/taint',
714 // subset of slow we run with CLI server too
715 'slow_ext_hsl' => 'hphp/test/slow/ext_hsl',
717 // Subsets of zend tests.
718 'zend_ext' => 'hphp/test/zend/good/ext',
719 'zend_ext_am' => 'hphp/test/zend/good/ext/[a-m]*',
720 'zend_ext_nz' => 'hphp/test/zend/good/ext/[n-z]*',
721 'zend_Zend' => 'hphp/test/zend/good/Zend',
722 'zend_tests' => 'hphp/test/zend/good/tests',
725 $pattern = $mappage[$file] ?? null;
726 if ($pattern is nonnull) {
727 $pattern = hphp_home().'/'.$pattern;
728 $matches = glob($pattern);
729 if (count($matches) === 0) {
730 error(
731 "Convenience test name '$file' is recognized but does not match ".
732 "any test files (pattern = '$pattern')",
735 return $matches;
738 return vec[$file];
741 // Some tests have to be run together in the same test bucket, serially, one
742 // after other in order to avoid races and other collisions.
743 function serial_only_tests(vec<string> $tests): vec<string> {
744 if (is_testing_dso_extension()) {
745 return vec[];
747 // Add a <testname>.php.serial file to make your test run in the serial
748 // bucket.
749 $serial_tests = vec(array_filter(
750 $tests,
751 function($test) {
752 return file_exists($test . '.serial');
755 return $serial_tests;
758 // NOTE: If "files" is very long, then the shell may reject the desired
759 // "find" command (especially because "escapeshellarg()" adds two single
760 // quote characters to each file), so we split "files" into chunks below.
761 function exec_find(vec<string> $files, string $extra): vec<string> {
762 $results = vec[];
763 foreach (array_chunk($files, 500) as $chunk) {
764 $efa = implode(' ', array_map(
765 $line ==> escapeshellarg($line as string),
766 $chunk as dict<_, _>,
768 $output = shell_exec("find $efa $extra");
769 foreach (explode("\n", $output) as $result) {
770 // Collect the (non-empty) results, which should all be file paths.
771 if ($result !== "") $results[] = $result;
774 return $results;
777 function find_tests(
778 vec<string> $files,
779 Options $options,
780 ): vec<string> {
781 if (!$files) {
782 $files = vec['quick'];
784 if ($files == vec['all']) {
785 $files = vec['quick', 'slow', 'zend', 'fastcgi', 'http', 'debugger'];
786 if (is_dir(hphp_home() . '/hphp/facebook/test')) {
787 $files[] = 'facebook';
790 $ft = vec[];
791 foreach ($files as $file) {
792 $ft = array_merge($ft, find_test_files($file));
794 $files = vec[];
795 foreach ($ft as $file) {
796 if (!@stat($file)) {
797 error("Not valid file or directory: '$file'");
799 $file = preg_replace(',//+,', '/', safe_realpath($file));
800 $file = preg_replace(',^'.getcwd().'/,', '', $file);
801 $files[] = $file;
803 $tests = exec_find(
804 $files,
805 "'(' " .
806 "-name '*.php' " .
807 "-o -name '*.hack' " .
808 "-o -name '*.hackpartial' " .
809 "-o -name '*.hhas' " .
810 "-o -name '*.php.type-errors' " .
811 "-o -name '*.hack.type-errors' " .
812 "-o -name '*.hackpartial.type-errors' " .
813 "')' " .
814 "-not -regex '.*round_trip[.]hhas'"
816 if (!$tests) {
817 error("Could not find any tests associated with your options.\n" .
818 "Make sure your test path is correct and that you have " .
819 "the right expect files for the tests you are trying to run.\n" .
820 usage());
822 asort(inout $tests);
823 $tests = vec(array_filter($tests));
824 if ($options->exclude is nonnull) {
825 $exclude = $options->exclude;
826 $tests = vec(array_filter($tests, function($test) use ($exclude) {
827 return (false === strpos($test, $exclude));
828 }));
830 if ($options->exclude_pattern is nonnull) {
831 $exclude = $options->exclude_pattern;
832 $tests = vec(array_filter($tests, function($test) use ($exclude) {
833 return !preg_match($exclude, $test);
834 }));
836 if ($options->exclude_recorded_failures is nonnull) {
837 $exclude_file = $options->exclude_recorded_failures;
838 $exclude = file($exclude_file, FILE_IGNORE_NEW_LINES);
839 $tests = vec(array_filter($tests, function($test) use ($exclude) {
840 return (false === in_array(canonical_path($test), $exclude));
841 }));
843 if ($options->include is nonnull) {
844 $include = $options->include;
845 $tests = vec(array_filter($tests, function($test) use ($include) {
846 return (false !== strpos($test, $include));
847 }));
849 if ($options->include_pattern is nonnull) {
850 $include = $options->include_pattern;
851 $tests = vec(array_filter($tests, function($test) use ($include) {
852 return (bool)preg_match($include, $test);
853 }));
855 return $tests;
858 function list_tests(vec<string> $files, Options $options): void {
859 $args = safe_implode(' ', \HH\global_get('recorded_options'));
861 // Disable escaping of test info when listing. We check if the environment
862 // variable is set so we can make the change in a backwards compatible way.
863 $escape_info = getenv("LISTING_NO_ESCAPE") === false;
865 foreach (find_tests($files, $options) as $test) {
866 $test_info = Status::jsonEncode(
867 dict['args' => $args, 'name' => $test],
869 if ($escape_info) {
870 print str_replace('\\', '\\\\', $test_info)."\n";
871 } else {
872 print $test_info."\n";
877 function find_test_ext(
878 string $test,
879 string $ext,
880 string $configName='config',
881 ): ?string {
882 if (is_file("{$test}.{$ext}")) {
883 return "{$test}.{$ext}";
885 return find_file_for_dir(dirname($test), "{$configName}.{$ext}");
888 function find_file_for_dir(string $dir, string $name): ?string {
889 // Handle the case where the $dir might come in as '.' because you
890 // are running the test runner on a file from the same directory as
891 // the test e.g., './mytest.php'. dirname() will give you the '.' when
892 // you actually have a lot of path to traverse upwards like
893 // /home/you/code/tests/mytest.php. Use realpath() to get that.
894 $dir = realpath($dir);
895 while ($dir !== '/' && is_dir($dir)) {
896 $file = "$dir/$name";
897 if (is_file($file)) {
898 return $file;
900 $dir = dirname($dir);
902 $file = test_dir().'/'.$name;
903 if (file_exists($file)) {
904 return $file;
906 return null;
909 function find_debug_config(string $test, string $name): string {
910 $debug_config = find_file_for_dir(dirname($test), $name);
911 if ($debug_config is nonnull) {
912 return "-m debug --debug-config ".$debug_config;
914 return "";
917 function mode_cmd(Options $options): vec<string> {
918 $repo_args = '';
919 if (!$options->repo) {
920 $repo_args = "-vUnitFileCache.Path=".unit_cache_file();
922 $interp_args = "$repo_args -vEval.Jit=0";
923 $jit_args = "$repo_args -vEval.Jit=true";
924 $mode = $options->mode ?? '';
925 switch ($mode) {
926 case '':
927 case 'jit':
928 return vec[$jit_args];
929 case 'interp':
930 return vec[$interp_args];
931 case 'interp,jit':
932 return vec[$interp_args, $jit_args];
933 default:
934 error("-m must be one of jit | interp | interp,jit. Got: '$mode'");
938 function extra_args(Options $options): string {
939 $args = $options->args ?? '';
941 if ($options->vendor is nonnull) {
942 $args .= ' -d auto_prepend_file=';
943 $args .= escapeshellarg($options->vendor.'/hh_autoload.php');
946 if ($options->lazyclass) {
947 $args .= ' -vEval.EmitClassPointers=2';
948 $args .= ' -vEval.ClassPassesClassname=true';
950 return $args;
953 function extra_compiler_args(Options $options): string {
954 return $options->compiler_args ?? '';
957 function hhvm_cmd_impl(
958 Options $options,
959 string $config,
960 ?string $autoload_db_prefix,
961 string ...$extra_args
962 ): vec<string> {
963 $cmds = vec[];
964 foreach (mode_cmd($options) as $mode_num => $mode) {
965 $args = vec[
966 hhvm_path(),
967 '-c',
968 $config,
969 // EnableArgsInBacktraces disables most of HHBBC's DCE optimizations.
970 // In order to test those optimizations (which are part of a normal prod
971 // configuration) we turn this flag off by default.
972 '-vEval.EnableArgsInBacktraces=false',
973 '-vEval.EnableIntrinsicsExtension=true',
974 '-vEval.HHIRInliningIgnoreHints=false',
975 '-vEval.HHIRAlwaysInterpIgnoreHint=false',
976 '-vEval.FoldLazyClassKeys=false',
977 '-vEval.CoeffectEnforcementLevels.zoned=1',
978 $mode,
979 $options->wholecfg ? '-vEval.JitPGORegionSelector=wholecfg' : '',
981 // load/store counters don't work on Ivy Bridge so disable for tests
982 '-vEval.ProfileHWEnable=false',
984 // use a fixed path for embedded data
985 '-vEval.EmbeddedDataExtractPath='
986 .escapeshellarg(bin_root().'/hhvm_%{type}_%{buildid}'),
988 // Stick to a single thread for retranslate-all
989 '-vEval.JitWorkerThreads=1',
990 '-vEval.JitWorkerThreadsForSerdes=1',
992 extra_args($options),
995 if ($autoload_db_prefix is nonnull) {
996 $args[] =
997 '-vAutoload.DB.Path='.escapeshellarg("$autoload_db_prefix.$mode_num");
1000 if ($options->retranslate_all is nonnull) {
1001 $args[] = '--count='.((int)$options->retranslate_all * 2);
1002 $args[] = '-vEval.JitPGO=true';
1003 $args[] = '-vEval.JitRetranslateAllRequest='.$options->retranslate_all;
1004 // Set to timeout. We want requests to trigger retranslate all.
1005 $args[] = '-vEval.JitRetranslateAllSeconds=' . TIMEOUT_SECONDS;
1008 if ($options->recycle_tc is nonnull) {
1009 $args[] = '--count='.$options->recycle_tc;
1010 $args[] = '-vEval.StressUnitCacheFreq=1';
1011 $args[] = '-vEval.EnableReusableTC=true';
1014 if ($options->jit_serialize is nonnull) {
1015 $args[] = '-vEval.JitPGO=true';
1016 $args[] = '-vEval.JitRetranslateAllRequest='.$options->jit_serialize;
1017 // Set to timeout. We want requests to trigger retranslate all.
1018 $args[] = '-vEval.JitRetranslateAllSeconds=' . TIMEOUT_SECONDS;
1021 if ($options->hhas_round_trip) {
1022 $args[] = '-vEval.AllowHhas=1';
1023 $args[] = '-vEval.LoadFilepathFromUnitCache=1';
1026 if (!$options->cores) {
1027 $args[] = '-vResourceLimit.CoreFileSize=0';
1030 if ($options->dump_tc) {
1031 $args[] = '-vEval.DumpIR=1';
1032 $args[] = '-vEval.DumpTC=1';
1035 if ($options->hh_single_type_check is nonnull) {
1036 $args[] = '--hh_single_type_check='.$options->hh_single_type_check;
1039 if ($options->bespoke) {
1040 $args[] = '-vEval.BespokeArrayLikeMode=1';
1041 $args[] = '-vServer.APC.MemModelTreadmill=true';
1044 $cmds[] = safe_implode(' ', array_merge($args, $extra_args));
1046 return $cmds;
1049 function repo_separate(Options $options, string $test): bool {
1050 return $options->repo_separate &&
1051 !file_exists($test . ".hhbbc_opts");
1054 // Return the command and the env to run it in.
1055 function hhvm_cmd(
1056 Options $options,
1057 string $test,
1058 ?string $test_run = null,
1059 bool $is_temp_file = false
1060 ): (vec<string>, dict<string, mixed>) {
1061 $test_run ??= $test;
1062 // hdf support is only temporary until we fully migrate to ini
1063 // Discourage broad use.
1064 $hdf_suffix = ".use.for.ini.migration.testing.only.hdf";
1065 $hdf = file_exists($test.$hdf_suffix)
1066 ? '-c ' . $test . $hdf_suffix
1067 : "";
1068 $extra_opts = read_opts_file(find_test_ext($test, 'opts'));
1069 $config = find_test_ext($test, 'ini');
1070 invariant($config is nonnull, "%s", __METHOD__);
1071 $cmds = hhvm_cmd_impl(
1072 $options,
1073 $config,
1074 Status::getTestTmpPath($test, 'autoloadDB'),
1075 $hdf,
1076 find_debug_config($test, 'hphpd.ini'),
1077 $extra_opts,
1078 $is_temp_file ? " --temp-file" : "",
1079 '--file',
1080 escapeshellarg($test_run),
1083 $cmd = "";
1085 if (file_exists($test.'.verify')) {
1086 $cmd .= " -m verify";
1089 if ($options->cli_server) {
1090 $config = find_file_for_dir(dirname($test), 'config.ini');
1091 $servers = $options->servers as Servers;
1092 $server = $servers->configs[$config ?? ''];
1093 $socket = $server->cli_socket;
1094 $cmd .= ' -vEval.UseRemoteUnixServer=only';
1095 $cmd .= ' -vEval.UnixServerPath='.$socket;
1096 $cmd .= ' --count=3';
1099 // Special support for tests that require a path to the current
1100 // test directory for things like prepend_file and append_file
1101 // testing.
1102 if (file_exists($test.'.ini')) {
1103 $contents = file_get_contents($test.'.ini');
1104 if (strpos($contents, '{PWD}') !== false) {
1105 $test_ini = tempnam('/tmp', $test).'.ini';
1106 file_put_contents($test_ini,
1107 str_replace('{PWD}', dirname($test), $contents));
1108 $cmd .= " -c $test_ini";
1111 if ($hdf !== "") {
1112 $contents = file_get_contents($test.$hdf_suffix);
1113 if (strpos($contents, '{PWD}') !== false) {
1114 $test_hdf = tempnam('/tmp', $test).$hdf_suffix;
1115 file_put_contents($test_hdf,
1116 str_replace('{PWD}', dirname($test), $contents));
1117 $cmd .= " -c $test_hdf";
1121 if ($options->repo) {
1122 $repo_suffix = repo_separate($options, $test) ? 'hhbbc' : 'hhbc';
1124 $program = "hhvm";
1125 $hhbbc_repo =
1126 "\"" . test_repo($options, $test) . "/$program.$repo_suffix\"";
1127 $cmd .= ' -vRepo.Authoritative=true';
1128 $cmd .= " -vRepo.Path=$hhbbc_repo";
1131 if ($options->jitsample is nonnull) {
1132 $cmd .= ' -vDeploymentId="' . $options->jitsample . '"';
1133 $cmd .= ' --instance-id="' . $test . '"';
1134 $cmd .= ' -vEval.JitSampleRate=1';
1135 $cmd .= " -vScribe.Tables.hhvm_jit.include.*=instance_id";
1136 $cmd .= " -vScribe.Tables.hhvm_jit.include.*=deployment_id";
1139 $env = \HH\FIXME\UNSAFE_CAST<dict<arraykey,mixed>,dict<string,mixed>>(
1140 \HH\global_get('_ENV') as dict<_, _>
1142 $env['LC_ALL'] = 'C';
1143 $env['INPUTRC'] = test_dir().'/inputrc';
1145 // Apply the --env option.
1146 if ($options->env is nonnull) {
1147 foreach (explode(",", $options->env) as $arg) {
1148 $i = strpos($arg, '=');
1149 if ($i) {
1150 $key = substr($arg, 0, $i);
1151 $val = substr($arg, $i + 1);
1152 $env[$key] = $val;
1153 } else {
1154 unset($env[$arg]);
1159 $in = find_test_ext($test, 'in');
1160 if ($in is nonnull) {
1161 $cmd .= ' < ' . escapeshellarg($in);
1162 // If we're piping the input into the command then setup a simple
1163 // dumb terminal so hhvm doesn't try to control it and pollute the
1164 // output with control characters, which could change depending on
1165 // a wide variety of terminal settings.
1166 $env["TERM"] = "dumb";
1169 foreach ($cmds as $idx => $_) {
1170 $cmds[$idx] .= $cmd;
1173 return tuple($cmds, $env);
1176 function hphp_cmd(
1177 Options $options,
1178 string $test,
1179 string $program,
1180 ): string {
1181 // Transform extra_args like "-vName=Value" into "-vRuntime.Name=Value".
1182 $extra_args =
1183 preg_replace("/(^-v|\s+-v)\s*/", "$1Runtime.", extra_args($options));
1185 $compiler_args = extra_compiler_args($options);
1187 $hdf_suffix = ".use.for.ini.migration.testing.only.hdf";
1188 $hdf = file_exists($test.$hdf_suffix)
1189 ? '-c ' . $test . $hdf_suffix
1190 : "";
1192 if ($hdf !== "") {
1193 $contents = file_get_contents($test.$hdf_suffix);
1194 if (strpos($contents, '{PWD}') !== false) {
1195 $test_hdf = tempnam('/tmp', $test).$hdf_suffix;
1196 file_put_contents($test_hdf,
1197 str_replace('{PWD}', dirname($test), $contents));
1198 $hdf = " -c $test_hdf";
1202 return safe_implode(" ", vec[
1203 hphpc_path($options),
1204 '--hphp',
1205 '-vUseHHBBC='. (repo_separate($options, $test) ? 'false' : 'true'),
1206 '--config',
1207 find_test_ext($test, 'ini', 'hphp_config'),
1208 $hdf,
1209 '-vRuntime.ResourceLimit.CoreFileSize=0',
1210 '-vRuntime.Eval.EnableIntrinsicsExtension=true',
1211 '-vRuntime.Eval.EnableArgsInBacktraces=true',
1212 '-vRuntime.Eval.FoldLazyClassKeys=false',
1213 '-vRuntime.Eval.CoeffectEnforcementLevels.zoned=1',
1214 '-vParserThreadCount=' . ($options->repo_threads ?? 1),
1215 '--nofork=1 -thhbc -l1 -k1',
1216 '-o "' . test_repo($options, $test) . '"',
1217 "--program $program.hhbc \"$test\"",
1218 "-vRuntime.UnitFileCache.Path=".unit_cache_file(),
1219 $extra_args,
1220 $compiler_args,
1221 read_opts_file("$test.hphp_opts"),
1225 function hphpc_path(Options $options): string {
1226 if ($options->split_hphpc) {
1227 $file = "";
1228 $file = bin_root().'/hphpc';
1230 if (!is_file($file)) {
1231 error("$file doesn't exist. Did you forget to build first?");
1233 return rel_path($file);
1234 } else {
1235 return hhvm_path();
1239 function hhbbc_cmd(
1240 Options $options, string $test, string $program,
1241 ): string {
1242 $test_repo = test_repo($options, $test);
1243 return safe_implode(" ", vec[
1244 hphpc_path($options),
1245 '--hhbbc',
1246 '--no-logging',
1247 '--no-cores',
1248 '--parallel-num-threads=' . ($options->repo_threads ?? 1),
1249 '--parallel-final-threads=' . ($options->repo_threads ?? 1),
1250 read_opts_file("$test.hhbbc_opts"),
1251 "-o \"$test_repo/$program.hhbbc\" \"$test_repo/$program.hhbc\"",
1255 // Execute $cmd and return its output on failure, including any stacktrace.log
1256 // file it generated. Return null on success.
1257 function exec_with_stack(string $cmd): ?string {
1258 $pipes = null;
1259 $proc = proc_open($cmd,
1260 dict[0 => vec['pipe', 'r'],
1261 1 => vec['pipe', 'w'],
1262 2 => vec['pipe', 'w']], inout $pipes);
1263 fclose($pipes[0]);
1264 $s = '';
1265 $all_selects_failed=true;
1266 $end = mtime() + TIMEOUT_SECONDS;
1267 $timedout = false;
1268 while (true) {
1269 $now = mtime();
1270 if ($now >= $end) break;
1271 $read = vec[$pipes[1], $pipes[2]];
1272 $write = null;
1273 $except = null;
1274 $available = @stream_select(
1275 inout $read,
1276 inout $write,
1277 inout $except,
1278 (int)($end - $now),
1280 if ($available === false) {
1281 usleep(1000);
1282 $s .= "select failed:\n" . print_r(error_get_last(), true);
1283 continue;
1285 $all_selects_failed=false;
1286 if ($available === 0) continue;
1287 foreach ($read as $pipe) {
1288 $t = fread($pipe, 4096);
1289 if ($t === false) continue;
1290 $s .= $t;
1292 if (feof($pipes[1]) && feof($pipes[2])) break;
1294 fclose($pipes[1]);
1295 fclose($pipes[2]);
1296 while (true) {
1297 $status = proc_get_status($proc);
1298 if (!$status['running']) break;
1299 $now = mtime();
1300 if ($now >= $end) {
1301 $timedout = true;
1302 $output = null;
1303 $return_var = -1;
1304 exec('pkill -P ' . $status['pid'] . ' 2> /dev/null', inout $output, inout $return_var);
1305 posix_kill($status['pid'], SIGTERM);
1307 usleep(1000);
1309 proc_close($proc);
1310 if ($timedout) {
1311 if ($all_selects_failed) {
1312 return "All selects failed running `$cmd'\n\n$s";
1314 return "Timed out running `$cmd'\n\n$s";
1316 if (
1317 !$status['exitcode'] &&
1318 !preg_match('/\\b(error|exception|fatal)\\b/', $s)
1320 return null;
1322 $pid = $status['pid'];
1323 $stack =
1324 @file_get_contents("/tmp/stacktrace.$pid.log") ?:
1325 @file_get_contents("/var/tmp/cores/stacktrace.$pid.log");
1326 if ($stack !== false) {
1327 $s .= "\n" . $stack;
1329 return "Running `$cmd' failed (".$status['exitcode']."):\n\n$s";
1332 function repo_mode_compile(
1333 Options $options, string $test, string $program,
1334 ): bool {
1335 $hphp = hphp_cmd($options, $test, $program);
1336 $result = exec_with_stack($hphp);
1337 if ($result is null && repo_separate($options, $test)) {
1338 $hhbbc = hhbbc_cmd($options, $test, $program);
1339 $result = exec_with_stack($hhbbc);
1341 if ($result is null) return true;
1342 Status::writeDiff($test, $result);
1343 return false;
1347 // Minimal support for sending messages between processes over named pipes.
1349 // Non-buffered pipe writes of up to 512 bytes (PIPE_BUF) are atomic.
1351 // Packet format:
1352 // 8 byte zero-padded hex pid
1353 // 4 byte zero-padded hex type
1354 // 4 byte zero-padded hex body size
1355 // N byte string body
1357 // NOTE: The first call to "getInput()" or "getOutput()" in any process will
1358 // block until some other process calls the other method.
1360 class Queue {
1361 // The path to the FIFO, until destroyed.
1362 private ?string $path = null;
1364 private ?resource $input = null;
1365 private ?resource $output = null;
1367 // Pipes writes are atomic up to 512 bytes (up to 4096 bytes on linux),
1368 // and we use a 16 byte header, leaving this many bytes available for
1369 // each chunk of "body" (see "$partials").
1370 const int CHUNK = 512 - 16;
1372 // If a message "body" is larger than CHUNK bytes, then writers must break
1373 // it into chunks, and send all but the last chunk with type 0. The reader
1374 // collects those chunks in this Map (indexed by pid), until the final chunk
1375 // is received, and the chunks can be reassembled.
1376 private Map<int, Vector<string>> $partials = Map {};
1379 // NOTE: Only certain directories support "posix_mkfifo()".
1380 public function __construct(?string $dir = null): void {
1381 $path = \tempnam($dir ?? \sys_get_temp_dir(), "queue.mkfifo.");
1382 \unlink($path);
1383 if (!\posix_mkfifo($path, 0700)) {
1384 throw new \Exception("Failed to create FIFO at '$path'");
1386 $this->path = $path;
1389 private function getInput(): resource {
1390 $input = $this->input;
1391 if ($input is null) {
1392 $path = $this->path;
1393 if ($path is null) {
1394 throw new \Exception("Missing FIFO path");
1396 $input = \fopen($path, "r");
1397 $this->input = $input;
1399 return $input;
1402 private function getOutput(): resource {
1403 $output = $this->output;
1404 if ($output is null) {
1405 $path = $this->path;
1406 if ($path is null) {
1407 throw new \Exception("Missing FIFO path");
1409 $output = \fopen($path, "a");
1410 $this->output = $output;
1412 return $output;
1415 private function validate(int $pid, int $type, int $blen): void {
1416 if ($pid < 0 || $pid >= (1 << 22)) {
1417 throw new \Exception("Illegal pid $pid");
1419 if ($type < 0 || $type >= 0x10000) {
1420 throw new \Exception("Illegal type $type");
1422 if ($blen < 0 || $blen > static::CHUNK) {
1423 throw new \Exception("Illegal blen $blen");
1427 // Read one packet header or body.
1428 private function read(int $n): string {
1429 $input = $this->getInput();
1430 $result = "";
1431 while (\strlen($result) < $n) {
1432 $r = fread($input, $n - \strlen($result));
1433 if ($r is string) {
1434 $result .= $r;
1435 } else {
1436 throw new \Exception("Failed to read $n bytes");
1439 return $result;
1442 // Receive one raw message (pid, type, body).
1443 public function receive(): (int, int, string) {
1444 $type = null;
1445 $body = "";
1446 while (true) {
1447 $header = $this->read(16);
1448 $pid = intval(substr($header, 0, 8) as string, 16);
1449 $type = intval(substr($header, 8, 4) as string, 16);
1450 $blen = intval(substr($header, 12, 4) as string, 16);
1451 $this->validate($pid, $type, $blen);
1452 $body = $this->read($blen);
1453 if ($type === 0) {
1454 $this->partials[$pid] ??= Vector {};
1455 $this->partials[$pid][] = $body;
1456 } else {
1457 $chunks = $this->partials[$pid] ?? null;
1458 if ($chunks is nonnull) {
1459 $chunks[] = $body;
1460 $body = \implode("", $chunks);
1461 $this->partials->removeKey($pid);
1463 return tuple($pid, $type, $body);
1468 // Receive one message (pid, type, message).
1469 // Note that the raw body is processed using "unserialize()".
1470 public function receiveMessage(): (int, int, ?Message) {
1471 list($pid, $type, $body) = $this->receive();
1472 $msg = unserialize($body) as ?Message;
1473 return tuple($pid, $type, $msg);
1476 private function write(int $pid, int $type, string $body): void {
1477 $output = $this->getOutput();
1478 $blen = \strlen($body);
1479 $this->validate($pid, $type, $blen);
1480 $packet = sprintf("%08x%04x%04x%s", $pid, $type, $blen, $body);
1481 $n = \strlen($packet);
1482 if ($n !== 16 + $blen) {
1483 throw new \Exception("Illegal packet");
1485 // NOTE: Hack's "fwrite()" is never buffered, which is especially
1486 // critical for pipe writes, to ensure that they are actually atomic.
1487 // See the documentation for "PlainFile::writeImpl()". But just in
1488 // case, we add an explicit "fflush()" below.
1489 $bytes_out = fwrite($output, $packet, $n);
1490 if ($bytes_out !== $n) {
1491 throw new \Exception(
1492 "Failed to write $n bytes; only $bytes_out were written"
1495 fflush($output);
1498 // Send one serialized message.
1499 public function send(int $type, string $body): void {
1500 $pid = \posix_getpid();
1501 $blen = \strlen($body);
1502 $chunk = static::CHUNK;
1503 if ($blen > $chunk) {
1504 for ($i = 0; $i + $chunk < $blen; $i += $chunk) {
1505 $this->write($pid, 0, \substr($body, $i, $chunk) as string);
1507 $this->write($pid, $type, \substr($body, $i) as string);
1508 } else {
1509 $this->write($pid, $type, $body);
1513 // Send one message after serializing it.
1514 public function sendMessage(int $type, ?Message $msg): void {
1515 $body = serialize($msg);
1516 $this->send($type, $body);
1519 public function destroy(): void {
1520 if ($this->input is nonnull) {
1521 fclose($this->input);
1522 $this->input = null;
1524 if ($this->output is nonnull) {
1525 fclose($this->output);
1526 $this->output = null;
1528 if ($this->path is nonnull) {
1529 \unlink($this->path);
1530 $this->path = null;
1535 final class Message {
1536 public function __construct(
1537 public string $test,
1538 public float $time,
1539 public int $stime,
1540 public int $etime,
1541 public ?string $reason = null,
1546 enum TempDirRemove: int {
1547 ALWAYS = 0;
1548 ON_RUN_SUCCESS = 1;
1549 NEVER = 2;
1552 type TestResult = shape(
1553 'name' => string,
1554 'status' => string,
1555 'start_time' => int,
1556 'end_time' => int,
1557 'time' => float,
1558 ?'details' => string,
1561 final class Status {
1562 private static vec<TestResult> $results = vec[];
1563 private static int $mode = 0;
1565 private static bool $use_color = false;
1567 public static bool $nofork = false;
1568 private static ?Queue $queue = null;
1569 private static bool $killed = false;
1570 public static TempDirRemove $temp_dir_remove = TempDirRemove::ALWAYS;
1571 private static int $return_value = 255;
1573 private static float $overall_start_time = 0.0;
1574 private static float $overall_end_time = 0.0;
1576 private static string $tmpdir = "";
1577 public static bool $write_to_checkout = false;
1579 public static int $passed = 0;
1580 public static int $skipped = 0;
1581 public static dict<string, int> $skip_reasons = dict[];
1582 public static int $failed = 0;
1584 const int MODE_NORMAL = 0;
1585 const int MODE_VERBOSE = 1;
1586 const int MODE_TESTPILOT = 3;
1587 const int MODE_RECORD_FAILURES = 4;
1589 const int MSG_STARTED = 7;
1590 const int MSG_FINISHED = 1;
1591 const int MSG_TEST_PASS = 2;
1592 const int MSG_TEST_FAIL = 4;
1593 const int MSG_TEST_SKIP = 5;
1594 const int MSG_SERVER_RESTARTED = 6;
1596 const int RED = 31;
1597 const int GREEN = 32;
1598 const int YELLOW = 33;
1599 const int BLUE = 34;
1601 public static function createTmpDir(): void {
1602 $parent = sys_get_temp_dir();
1603 if (substr($parent, -1) !== "/") {
1604 $parent .= "/";
1606 self::$tmpdir = HH\Lib\_Private\_OS\mkdtemp($parent . 'hphp-test-XXXXXX');
1609 public static function getRunTmpDir(): string {
1610 return self::$tmpdir;
1613 // Return a path in the run tmpdir that's unique to this test and ext.
1614 // Remember to teach clean_intermediate_files to clean up all the exts you use
1615 public static function getTestTmpPath(string $test, string $ext): string {
1616 return self::$tmpdir . '/' . $test . '.' . $ext;
1619 public static function getTmpPathFile(string $filename): string {
1620 return self::$tmpdir . '/' . $filename;
1623 // Similar to getTestTmpPath, but if we're run with --write-to-checkout
1624 // then we put the files next to the test instead of in the tmpdir.
1625 public static function getTestOutputPath(string $test, string $ext): string {
1626 if (self::$write_to_checkout) {
1627 return "$test.$ext";
1629 return static::getTestTmpPath($test, $ext);
1632 public static function createTestTmpDir(string $test): string {
1633 $test_temp_dir = self::getTestTmpPath($test, 'tmpdir');
1634 @mkdir($test_temp_dir, 0777, true);
1635 return $test_temp_dir;
1638 public static function writeDiff(string $test, string $diff): void {
1639 $path = Status::getTestOutputPath($test, 'diff');
1640 @mkdir(dirname($path), 0777, true);
1641 file_put_contents($path, $diff);
1644 public static function diffForTest(string $test): string {
1645 $diff = @file_get_contents(Status::getTestOutputPath($test, 'diff'));
1646 return $diff === false ? '' : $diff;
1649 public static function removeDirectory(string $dir): void {
1650 $files = scandir($dir);
1651 foreach ($files as $file) {
1652 if ($file === '.' || $file === '..') {
1653 continue;
1655 $path = $dir . "/" . $file;
1656 if (is_dir($path)) {
1657 self::removeDirectory($path);
1658 } else {
1659 unlink($path);
1662 rmdir($dir);
1665 // This is similar to removeDirectory but it only removes empty directores
1666 // and won't enter directories whose names end with '.tmpdir'. This allows
1667 // us to clean up paths like test/quick/vec in our run's temporary directory
1668 // if all the tests in them passed, but it leaves test tmpdirs of failed
1669 // tests (that we didn't remove with clean_intermediate_files because the
1670 // test failed) and directores under them alone even if they're empty.
1671 public static function removeEmptyTestParentDirs(string $dir): bool {
1672 $is_now_empty = true;
1673 $files = scandir($dir);
1674 foreach ($files as $file) {
1675 if ($file === '.' || $file === '..') {
1676 continue;
1678 if (strrpos($file, '.tmpdir') === (strlen($file) - strlen('.tmpdir'))) {
1679 $is_now_empty = false;
1680 continue;
1682 $path = $dir . "/" . $file;
1683 if (!is_dir($path)) {
1684 $is_now_empty = false;
1685 continue;
1687 if (self::removeEmptyTestParentDirs($path)) {
1688 rmdir($path);
1689 } else {
1690 $is_now_empty = false;
1693 return $is_now_empty;
1696 public static function setMode(int $mode): void {
1697 self::$mode = $mode;
1700 public static function getMode(): int {
1701 return self::$mode;
1704 public static function setUseColor(bool $use): void {
1705 self::$use_color = $use;
1708 public static function addTestTimesSerial(
1709 dict<string, TestResult> $results,
1710 ): float {
1711 $time = 0.0;
1712 foreach ($results as $result) {
1713 $time += $result['time'];
1715 return $time;
1718 public static function getOverallStartTime(): float {
1719 return self::$overall_start_time;
1722 public static function getOverallEndTime(): float {
1723 return self::$overall_end_time;
1726 public static function started(): void {
1727 self::send(self::MSG_STARTED, null);
1728 self::$overall_start_time = mtime();
1731 public static function finished(int $return_value): void {
1732 self::$overall_end_time = mtime();
1733 self::$return_value = $return_value;
1734 self::send(self::MSG_FINISHED, null);
1737 public static function destroy(): void {
1738 if (!self::$killed) {
1739 self::$killed = true;
1740 if (self::$queue is nonnull) {
1741 self::$queue->destroy();
1742 self::$queue = null;
1744 switch (self::$temp_dir_remove) {
1745 case TempDirRemove::NEVER:
1746 break;
1747 case TempDirRemove::ON_RUN_SUCCESS:
1748 if (self::$return_value !== 0) {
1749 self::removeEmptyTestParentDirs(self::$tmpdir);
1750 break;
1752 // FALLTHROUGH
1753 case TempDirRemove::ALWAYS:
1754 self::removeDirectory(self::$tmpdir);
1759 public static function destroyFromSignal(int $_signo): void {
1760 self::destroy();
1763 public static function registerCleanup(bool $no_clean): void {
1764 if (self::getMode() === self::MODE_TESTPILOT ||
1765 self::getMode() === self::MODE_RECORD_FAILURES) {
1766 self::$temp_dir_remove = TempDirRemove::ALWAYS;
1767 } else if ($no_clean) {
1768 self::$temp_dir_remove = TempDirRemove::NEVER;
1769 } else {
1770 self::$temp_dir_remove = TempDirRemove::ON_RUN_SUCCESS;
1772 register_shutdown_function(self::destroy<>);
1773 pcntl_signal(SIGTERM, self::destroyFromSignal<>);
1774 pcntl_signal(SIGINT, self::destroyFromSignal<>);
1777 public static function serverRestarted(): void {
1778 self::send(self::MSG_SERVER_RESTARTED, null);
1781 public static function pass(
1782 string $test, float $time, int $stime, int $etime,
1783 ): void {
1784 self::$results[] = shape(
1785 'name' => $test,
1786 'status' => 'passed',
1787 'start_time' => $stime,
1788 'end_time' => $etime,
1789 'time' => $time
1791 self::send(
1792 self::MSG_TEST_PASS,
1793 new Message($test, $time, $stime, $etime),
1797 public static function skip(
1798 string $test, string $reason, float $time, int $stime, int $etime,
1799 ): void {
1800 self::$results[] = shape(
1801 'name' => $test,
1802 /* testpilot needs a positive response for every test run, report
1803 * that this test isn't relevant so it can silently drop. */
1804 'status' => self::getMode() === self::MODE_TESTPILOT
1805 ? 'not_relevant'
1806 : 'skipped',
1807 'start_time' => $stime,
1808 'end_time' => $etime,
1809 'time' => $time,
1811 self::send(
1812 self::MSG_TEST_SKIP,
1813 new Message($test, $time, $stime, $etime, $reason),
1817 public static function fail(
1818 string $test, float $time, int $stime, int $etime, string $diff,
1819 ): void {
1820 self::$results[] = shape(
1821 'name' => $test,
1822 'status' => 'failed',
1823 'start_time' => $stime,
1824 'end_time' => $etime,
1825 'time' => $time,
1826 'details' => self::utf8Sanitize($diff),
1828 self::send(
1829 self::MSG_TEST_FAIL,
1830 new Message($test, $time, $stime, $etime),
1834 public static function handle_message(int $type, ?Message $message): bool {
1835 switch ($type) {
1836 case Status::MSG_STARTED:
1837 break;
1839 case Status::MSG_FINISHED:
1840 return false;
1842 case Status::MSG_SERVER_RESTARTED:
1843 switch (Status::getMode()) {
1844 case Status::MODE_NORMAL:
1845 if (!Status::hasCursorControl()) {
1846 Status::sayColor(Status::RED, 'x');
1848 break;
1850 case Status::MODE_VERBOSE:
1851 Status::sayColor(
1852 Status::YELLOW,
1853 "failed to talk to server\n"
1855 break;
1857 case Status::MODE_TESTPILOT:
1858 break;
1860 case Status::MODE_RECORD_FAILURES:
1861 break;
1863 break;
1865 case Status::MSG_TEST_PASS:
1866 self::$passed++;
1867 invariant($message is nonnull, "%s", __METHOD__);
1868 switch (Status::getMode()) {
1869 case Status::MODE_NORMAL:
1870 if (!Status::hasCursorControl()) {
1871 Status::sayColor(Status::GREEN, '.');
1873 break;
1875 case Status::MODE_VERBOSE:
1876 Status::sayColor(
1877 $message->test." ",
1878 Status::GREEN,
1879 sprintf("passed (%.2fs)\n", $message->time),
1881 break;
1883 case Status::MODE_TESTPILOT:
1884 Status::sayTestpilot(
1885 $message->test,
1886 'passed',
1887 $message->stime,
1888 $message->etime,
1889 $message->time,
1891 break;
1893 case Status::MODE_RECORD_FAILURES:
1894 break;
1896 break;
1898 case Status::MSG_TEST_SKIP:
1899 self::$skipped++;
1900 invariant($message is nonnull, "%s", __METHOD__);
1901 $reason = $message->reason;
1902 invariant($reason is nonnull, "%s", __METHOD__);
1903 self::$skip_reasons[$reason] ??= 0;
1904 self::$skip_reasons[$reason]++;
1906 switch (Status::getMode()) {
1907 case Status::MODE_NORMAL:
1908 if (!Status::hasCursorControl()) {
1909 Status::sayColor(Status::YELLOW, 's');
1911 break;
1913 case Status::MODE_VERBOSE:
1914 Status::sayColor($message->test." ", Status::YELLOW, "skipped");
1916 if ($reason is nonnull) {
1917 Status::sayColor(" - reason: $reason");
1919 Status::sayColor(sprintf(" (%.2fs)\n", $message->time));
1920 break;
1922 case Status::MODE_TESTPILOT:
1923 Status::sayTestpilot(
1924 $message->test,
1925 'not_relevant',
1926 $message->stime,
1927 $message->etime,
1928 $message->time,
1930 break;
1932 case Status::MODE_RECORD_FAILURES:
1933 break;
1935 break;
1937 case Status::MSG_TEST_FAIL:
1938 self::$failed++;
1939 invariant($message is nonnull, "%s", __METHOD__);
1940 switch (Status::getMode()) {
1941 case Status::MODE_NORMAL:
1942 if (Status::hasCursorControl()) {
1943 print "\033[2K\033[1G";
1945 $diff = Status::diffForTest($message->test);
1946 $test = $message->test;
1947 Status::sayColor(
1948 Status::RED,
1949 "\nFAILED: $test\n$diff\n",
1951 break;
1953 case Status::MODE_VERBOSE:
1954 Status::sayColor(
1955 $message->test." ",
1956 Status::RED,
1957 sprintf("FAILED (%.2fs)\n", $message->time),
1959 break;
1961 case Status::MODE_TESTPILOT:
1962 Status::sayTestpilot(
1963 $message->test,
1964 'failed',
1965 $message->stime,
1966 $message->etime,
1967 $message->time,
1969 break;
1971 case Status::MODE_RECORD_FAILURES:
1972 break;
1974 break;
1976 default:
1977 error("Unknown message $type");
1979 return true;
1982 private static function send(int $type, ?Message $msg): void {
1983 if (self::$killed) {
1984 return;
1986 if (self::$nofork) {
1987 self::handle_message($type, $msg);
1988 return;
1990 self::getQueue()->sendMessage($type, $msg);
1994 * Takes a variable number of string or int arguments. If color output is
1995 * enabled and any one of the arguments is preceded by an integer (see the
1996 * color constants above), that argument will be given the indicated color.
1998 public static function sayColor(arraykey ...$args): void {
1999 $n = count($args);
2000 for ($i = 0; $i < $n;) {
2001 $arg = $args[$i];
2002 $i++;
2003 if ($arg is int) {
2004 $color = $arg;
2005 if (self::$use_color) {
2006 print "\033[0;{$color}m";
2008 $arg = $args[$i];
2009 $i++;
2010 print $arg;
2011 if (self::$use_color) {
2012 print "\033[0m";
2014 } else {
2015 print $arg;
2020 public static function sayTestpilot(
2021 string $test, string $status, int $stime, int $etime, float $time,
2022 ): void {
2023 $start = dict['op' => 'start', 'test' => $test];
2024 $end = dict['op' => 'test_done', 'test' => $test, 'status' => $status,
2025 'start_time' => $stime, 'end_time' => $etime, 'time' => $time];
2026 if ($status === 'failed') {
2027 $end['details'] = self::utf8Sanitize(Status::diffForTest($test));
2029 self::say($start, $end);
2032 public static function getResults(): vec<TestResult> {
2033 return self::$results;
2036 /** Output is in the format expected by JsonTestRunner. */
2037 public static function say(dict<string, mixed> ...$args): void {
2038 $data = array_map(
2039 $row ==> self::jsonEncode($row) . "\n",
2040 $args
2042 fwrite(STDERR, safe_implode("", $data));
2045 public static function hasCursorControl(): bool {
2046 // for runs on hudson-ci.org (aka jenkins).
2047 if (getenv("HUDSON_URL")) {
2048 return false;
2050 // for runs on travis-ci.org
2051 if (getenv("TRAVIS")) {
2052 return false;
2054 $stty = self::getSTTY();
2055 if (!$stty) {
2056 return false;
2058 return strpos($stty, 'erase = <undef>') === false;
2061 <<__Memoize>>
2062 public static function getSTTY(): string {
2063 $descriptorspec = dict[1 => vec["pipe", "w"], 2 => vec["pipe", "w"]];
2064 $pipes = null;
2065 $process = proc_open(
2066 'stty -a', $descriptorspec, inout $pipes, null, null,
2067 dict['suppress_errors' => true]
2069 $stty = stream_get_contents($pipes[1]);
2070 proc_close($process);
2071 return $stty;
2074 public static function utf8Sanitize(string $str): string {
2075 return UConverter::transcode($str, 'UTF-8', 'UTF-8');
2078 public static function jsonEncode(mixed $data): string {
2079 return json_encode($data, JSON_UNESCAPED_SLASHES);
2082 public static function getQueue(): Queue {
2083 if (!self::$queue) {
2084 if (self::$killed) error("Killed!");
2085 self::$queue = new Queue(self::$tmpdir);
2087 return self::$queue;
2091 function clean_intermediate_files(string $test, Options $options): void {
2092 if ($options->no_clean) {
2093 return;
2095 if ($options->write_to_checkout) {
2096 // in --write-to-checkout mode, normal test output goes next to the test
2097 $exts = vec[
2098 'out',
2099 'diff',
2101 foreach ($exts as $ext) {
2102 $file = "$test.$ext";
2103 if (file_exists($file)) {
2104 unlink($file);
2108 $tmp_exts = vec[
2109 // normal test output goes here by default
2110 'out',
2111 'diff',
2112 // scratch directory the test may write to
2113 'tmpdir',
2114 // tests in --hhas-round-trip mode
2115 'round_trip.hhas',
2116 // tests in --hhbbc2 mode
2117 'before.round_trip.hhas',
2118 'after.round_trip.hhas',
2119 // temporary autoloader DB and associated cruft
2120 // We have at most two modes for now - see hhvm_cmd_impl
2121 'autoloadDB.0',
2122 'autoloadDB.0-journal',
2123 'autoloadDB.0-shm',
2124 'autoloadDB.0-wal',
2125 'autoloadDB.1',
2126 'autoloadDB.1-journal',
2127 'autoloadDB.1-shm',
2128 'autoloadDB.1-wal',
2130 foreach ($tmp_exts as $ext) {
2131 $file = Status::getTestTmpPath($test, $ext);
2132 if (is_dir($file)) {
2133 Status::removeDirectory($file);
2134 } else if (file_exists($file)) {
2135 unlink($file);
2138 // repo mode uses a directory that may or may not be in the run's tmpdir
2139 $repo = test_repo($options, $test);
2140 if (is_dir($repo)) {
2141 Status::removeDirectory($repo);
2145 function child_main(
2146 Options $options,
2147 vec<string> $tests,
2148 string $json_results_file,
2149 ): int {
2150 foreach ($tests as $test) {
2151 run_and_log_test($options, $test);
2153 $results = Status::getResults();
2154 file_put_contents($json_results_file, json_encode($results));
2155 foreach ($results as $result) {
2156 if ($result['status'] === 'failed') {
2157 return 1;
2160 return 0;
2164 * The runif feature is similar in spirit to skipif, but instead of allowing
2165 * one to run arbitrary code it can only skip based on pre-defined reasons
2166 * understood by the test runner.
2168 * The .runif file should consist of one or more lines made up of words
2169 * separated by spaces, optionally followed by a comment starting with #.
2170 * Empty lines (or lines with only comments) are ignored. The first word
2171 * determines the interpretation of the rest. The .runif file will allow the
2172 * test to run if all the non-empty lines 'match'.
2174 * Currently supported operations:
2175 * os [not] <os_name> # matches if we are (or are not) on the named OS
2176 * file <path> # matches if the file at the (possibly relative) path exists
2177 * euid [not] root # matches if we are (or are not) running as root (euid==0)
2178 * extension <extension_name> # matches if the named extension is available
2179 * function <function_name> # matches if the named function is available
2180 * class <class_name> # matches if the named class is available
2181 * method <class_name> <method name> # matches if the method is available
2182 * const <constant_name> # matches if the named constant is available
2183 * # matches if any named locale is available for the named LC_* category
2184 * locale LC_<something> <locale name>[ <another locale name>]
2186 * Several functions in this implementation return RunifResult. Valid sets of
2187 * keys are:
2188 * valid, error # valid will be false
2189 * valid, match # valid will be true, match will be true
2190 * valid, match, skip_reason # valid will be true, match will be false
2192 type RunifResult = shape(
2193 'valid' => bool, // was the runif file valid
2194 ?'error' => string, // if !valid, what was the problem
2195 ?'match' => bool, // did the line match/did all the lines in the file match
2196 ?'skip_reason' => string, // if !match, the skip reason to use
2199 <<__Memoize>>
2200 function runif_canonical_os(): string {
2201 if (PHP_OS === 'Linux' || PHP_OS === 'Darwin') return PHP_OS;
2202 if (substr(PHP_OS, 0, 3) === 'WIN') return 'WIN';
2203 invariant_violation('add proper canonicalization for your OS');
2206 function runif_known_os(string $match_os): bool {
2207 switch ($match_os) {
2208 case 'Linux':
2209 case 'Darwin':
2210 case 'WIN':
2211 return true;
2212 default:
2213 return false;
2217 function runif_os_matches(vec<string> $words): RunifResult {
2218 if (count($words) === 2) {
2219 if ($words[0] !== 'not') {
2220 return shape('valid' => false, 'error' => "malformed 'os' match");
2222 $match_os = $words[1];
2223 $invert = true;
2224 } else if (count($words) === 1) {
2225 $match_os = $words[0];
2226 $invert = false;
2227 } else {
2228 return shape('valid' => false, 'error' => "malformed 'os' match");
2230 if (!runif_known_os($match_os)) {
2231 return shape('valid' => false, 'error' => "unknown os '$match_os'");
2233 $matches = (runif_canonical_os() === $match_os);
2234 if ($matches !== $invert) return shape('valid' => true, 'match' => true);
2235 return shape(
2236 'valid' => true,
2237 'match' => false,
2238 'skip_reason' => 'skip-runif-os-' . safe_implode('-', $words)
2242 function runif_file_matches(vec<string> $words): RunifResult {
2243 /* This implementation has a trade-off. On the one hand, we could get more
2244 * accurate results if we do the check in a process with the same configs as
2245 * the test via runif_test_for_feature (e.g. if config differences make a
2246 * file we can see invisible to the test). However, this check was added to
2247 * skip tests where the test configs depend on a file that may be absent, in
2248 * which case hhvm configured the same way as the test cannot run. By doing
2249 * the check ourselves we can successfully skip such tests.
2251 if (count($words) !== 1) {
2252 return shape('valid' => false, 'error' => "malformed 'file' match");
2254 if (file_exists($words[0])) {
2255 return shape('valid' => true, 'match' => true);
2257 return shape(
2258 'valid' => true,
2259 'match' => false,
2260 'skip_reason' => 'skip-runif-file',
2264 function runif_test_for_feature(
2265 Options $options,
2266 string $test,
2267 string $bool_expression,
2268 ): bool {
2269 $tmp = tempnam(sys_get_temp_dir(), 'test-run-runif-');
2270 file_put_contents(
2271 $tmp,
2272 "<?hh\n" .
2273 "<<__EntryPoint>> function main(): void {\n" .
2274 " echo ($bool_expression) as bool ? 'PRESENT' : 'ABSENT';\n" .
2275 "}\n",
2278 // Run the check in non-repo mode to avoid building the repo (same features
2279 // should be available). Pick the mode arbitrarily for the same reason.
2280 $options_without_repo = clone $options;
2281 $options_without_repo->repo = false;
2282 list($hhvm, $_) = hhvm_cmd($options_without_repo, $test, $tmp, true);
2283 $hhvm = $hhvm[0];
2284 // Remove any --count <n> from the command
2285 $hhvm = preg_replace('/ --count[ =]\d+/', '', $hhvm);
2286 // some tests set open_basedir to a restrictive value, override to permissive
2287 $hhvm .= ' -dopen_basedir= ';
2289 $result = shell_exec("$hhvm 2>&1");
2290 invariant ($result !== false, 'shell_exec in runif_test_for_feature failed');
2291 $result = trim($result);
2292 if ($result === 'ABSENT') return false;
2293 if ($result === 'PRESENT') return true;
2294 invariant_violation(
2295 "unexpected output from shell_exec in runif_test_for_feature: '%s'",
2296 $result
2300 function runif_euid_matches(
2301 Options $options,
2302 string $test,
2303 vec<string> $words,
2304 ): RunifResult {
2305 if (count($words) === 2) {
2306 if ($words[0] !== 'not' || $words[1] !== 'root') {
2307 return shape('valid' => false, 'error' => "malformed 'euid' match");
2309 $invert = true;
2310 } else if (count($words) === 1) {
2311 if ($words[0] !== 'root') {
2312 return shape('valid' => false, 'error' => "malformed 'euid' match");
2314 $invert = false;
2315 } else {
2316 return shape('valid' => false, 'error' => "malformed 'euid' match");
2318 $matches = runif_test_for_feature($options, $test, 'posix_geteuid() === 0');
2319 if ($matches !== $invert) return shape('valid' => true, 'match' => true);
2320 return shape(
2321 'valid' => true,
2322 'match' => false,
2323 'skip_reason' => 'skip-runif-euid-' . safe_implode('-', $words)
2327 function runif_extension_matches(
2328 Options $options,
2329 string $test,
2330 vec<string> $words,
2331 ): RunifResult {
2332 if (count($words) !== 1) {
2333 return shape('valid' => false, 'error' => "malformed 'extension' match");
2335 if (runif_test_for_feature($options, $test, "extension_loaded('{$words[0]}')")) {
2336 return shape('valid' => true, 'match' => true);
2338 return shape(
2339 'valid' => true,
2340 'match' => false,
2341 'skip_reason' => 'skip-runif-extension-' . $words[0]
2345 function runif_function_matches(
2346 Options $options,
2347 string $test,
2348 vec<string> $words,
2349 ): RunifResult {
2350 if (count($words) !== 1) {
2351 return shape('valid' => false, 'error' => "malformed 'function' match");
2353 if (runif_test_for_feature($options, $test, "function_exists('{$words[0]}')")) {
2354 return shape('valid' => true, 'match' => true);
2356 return shape(
2357 'valid' => true,
2358 'match' => false,
2359 'skip_reason' => 'skip-runif-function-' . $words[0]
2363 function runif_class_matches(
2364 Options $options,
2365 string $test,
2366 vec<string> $words,
2367 ): RunifResult {
2368 if (count($words) !== 1) {
2369 return shape('valid' => false, 'error' => "malformed 'class' match");
2371 if (runif_test_for_feature($options, $test, "class_exists('{$words[0]}')")) {
2372 return shape('valid' => true, 'match' => true);
2374 return shape(
2375 'valid' => true,
2376 'match' => false,
2377 'skip_reason' => 'skip-runif-class-' . $words[0]
2381 function runif_method_matches(
2382 Options $options,
2383 string $test,
2384 vec<string> $words,
2385 ): RunifResult {
2386 if (count($words) !== 2) {
2387 return shape('valid' => false, 'error' => "malformed 'method' match");
2389 if (runif_test_for_feature($options, $test,
2390 "method_exists('{$words[0]}', '{$words[1]}')")) {
2391 return shape('valid' => true, 'match' => true);
2393 return shape(
2394 'valid' => true,
2395 'match' => false,
2396 'skip_reason' => 'skip-runif-method-' . $words[0] . '-' . $words[1],
2400 function runif_const_matches(
2401 Options $options,
2402 string $test,
2403 vec<string> $words,
2404 ): RunifResult {
2405 if (count($words) !== 1) {
2406 return shape('valid' => false, 'error' => "malformed 'const' match");
2408 if (runif_test_for_feature($options, $test, "defined('{$words[0]}')")) {
2409 return shape('valid' => true, 'match' => true);
2411 return shape(
2412 'valid' => true,
2413 'match' => false,
2414 'skip_reason' => 'skip-runif-const-' . $words[0]
2418 function runif_locale_matches(
2419 Options $options,
2420 string $test,
2421 vec<string> $words,
2422 ): RunifResult {
2423 if (count($words) < 2) {
2424 return shape('valid' => false, 'error' => "malformed 'locale' match");
2426 $category = array_shift(inout $words);
2427 if (!preg_match('/^LC_[A-Z]+$/', $category)) {
2428 return shape('valid' => false, 'error' => "bad locale category '$category'");
2430 $locale_args = safe_implode(', ', array_map($word ==> "'$word'", $words));
2431 $matches = runif_test_for_feature(
2432 $options,
2433 $test,
2434 "defined('$category') && (false !== setlocale($category, $locale_args))",
2436 if ($matches) {
2437 return shape('valid' => true, 'match' => true);
2439 return shape(
2440 'valid' => true,
2441 'match' => false,
2442 'skip_reason' => 'skip-runif-locale',
2446 function runif_should_skip_test(
2447 Options $options,
2448 string $test,
2449 ): RunifResult {
2450 $runif_path = find_test_ext($test, 'runif');
2451 if (!$runif_path) return shape('valid' => true, 'match' => true);
2453 $file_empty = true;
2454 $contents = file($runif_path, FILE_IGNORE_NEW_LINES);
2455 foreach ($contents as $line) {
2456 $line = preg_replace('/[#].*$/', '', $line); // remove comment
2457 $line = trim($line);
2458 if ($line === '') continue;
2459 $file_empty = false;
2461 $words = preg_split('/ +/', $line);
2462 if (count($words) < 2) {
2463 return shape('valid' => false, 'error' => "malformed line '$line'");
2465 foreach ($words as $word) {
2466 if (!preg_match('|^[\w/.-]+$|', $word)) {
2467 return shape(
2468 'valid' => false,
2469 'error' => "bad word '$word' in line '$line'",
2474 $type = array_shift(inout $words);
2475 $words = vec($words); // array_shift always promotes to dict :-\
2476 switch ($type) {
2477 case 'os':
2478 $result = runif_os_matches($words);
2479 break;
2480 case 'file':
2481 $result = runif_file_matches($words);
2482 break;
2483 case 'euid':
2484 $result = runif_euid_matches($options, $test, $words);
2485 break;
2486 case 'extension':
2487 $result = runif_extension_matches($options, $test, $words);
2488 break;
2489 case 'function':
2490 $result = runif_function_matches($options, $test, $words);
2491 break;
2492 case 'class':
2493 $result = runif_class_matches($options, $test, $words);
2494 break;
2495 case 'method':
2496 $result = runif_method_matches($options, $test, $words);
2497 break;
2498 case 'const':
2499 $result = runif_const_matches($options, $test, $words);
2500 break;
2501 case 'locale':
2502 $result = runif_locale_matches($options, $test, $words);
2503 break;
2504 default:
2505 return shape('valid' => false, 'error' => "bad match type '$type'");
2507 if (!$result['valid'] || !Shapes::idx($result, 'match', false)) {
2508 return $result;
2511 if ($file_empty) return shape('valid' => false, 'error' => 'empty runif file');
2512 return shape('valid' => true, 'match' => true);
2515 function should_skip_test_simple(
2516 Options $options,
2517 string $test,
2518 ): ?string {
2519 if (($options->cli_server || $options->server) &&
2520 !can_run_server_test($test, $options)) {
2521 return 'skip-server';
2524 if ($options->hhas_round_trip && substr($test, -5) === ".hhas") {
2525 return 'skip-hhas';
2528 if ($options->hhbbc2 || $options->hhas_round_trip) {
2529 $no_hhas_tag = 'nodumphhas';
2530 if (file_exists("$test.$no_hhas_tag") ||
2531 file_exists(dirname($test).'/'.$no_hhas_tag)) {
2532 return 'skip-nodumphhas';
2534 if (file_exists($test . ".verify")) {
2535 return 'skip-verify';
2539 if (has_multi_request_mode($options) || $options->repo ||
2540 $options->server) {
2541 if (file_exists($test . ".verify")) {
2542 return 'skip-verify';
2544 $no_multireq_tag = "nomultireq";
2545 if (file_exists("$test.$no_multireq_tag") ||
2546 file_exists(dirname($test).'/'.$no_multireq_tag)) {
2547 return 'skip-multi-req';
2549 if (find_debug_config($test, 'hphpd.ini')) {
2550 return 'skip-debugger';
2554 $no_bespoke_tag = "nobespoke";
2555 if ($options->bespoke &&
2556 file_exists("$test.$no_bespoke_tag")) {
2557 // Skip due to changes in array identity
2558 return 'skip-bespoke';
2561 $no_lazyclass_tag = "nolazyclass";
2562 if ($options->lazyclass &&
2563 file_exists("$test.$no_lazyclass_tag")) {
2564 return 'skip-lazyclass';
2567 $no_jitserialize_tag = "nojitserialize";
2568 if ($options->jit_serialize is nonnull &&
2569 file_exists("$test.$no_jitserialize_tag")) {
2570 return 'skip-jit-serialize';
2573 return null;
2576 function skipif_should_skip_test(
2577 Options $options,
2578 string $test,
2579 ): RunifResult {
2580 $skipif_test = find_test_ext($test, 'skipif');
2581 if (!$skipif_test) {
2582 return shape('valid' => true, 'match' => true);
2585 // Run the .skipif in non-repo mode since building a repo for it is
2586 // inconvenient and the same features should be available. Pick the mode
2587 // arbitrarily for the same reason.
2588 $options_without_repo = clone $options;
2589 $options_without_repo->repo = false;
2590 list($hhvm, $_) = hhvm_cmd($options_without_repo, $test, $skipif_test);
2591 $hhvm = $hhvm[0];
2592 // Remove any --count <n> from the command
2593 $hhvm = preg_replace('/ --count[ =]\d+/', '', $hhvm);
2595 $descriptorspec = dict[
2596 0 => vec["pipe", "r"],
2597 1 => vec["pipe", "w"],
2598 2 => vec["pipe", "w"],
2600 $pipes = null;
2601 $process = proc_open("$hhvm $test 2>&1", $descriptorspec, inout $pipes);
2602 if (!is_resource($process)) {
2603 return shape(
2604 'valid' => false,
2605 'error' => 'proc_open failed while running skipif'
2609 fclose($pipes[0]);
2610 $output = trim(stream_get_contents($pipes[1]));
2611 fclose($pipes[1]);
2612 proc_close($process);
2614 // valid output is empty or a single line starting with 'skip'
2615 // everything else must result in a test failure
2616 if ($output === '') {
2617 return shape('valid' => true, 'match' => true);
2619 if (preg_match('/^skip.*$/', $output)) {
2620 return shape(
2621 'valid' => true,
2622 'match' => false,
2623 'skip_reason' => 'skip-skipif',
2626 return shape('valid' => false, 'error' => "invalid skipif output '$output'");
2629 function comp_line(string $l1, string $l2, bool $is_reg): bool {
2630 if ($is_reg) {
2631 return (bool)preg_match('/^'. $l1 . '$/s', $l2);
2632 } else {
2633 return !strcmp($l1, $l2);
2637 function count_array_diff(
2638 vec<string> $ar1, vec<string> $ar2, bool $is_reg,
2639 int $idx1, int $idx2, int $cnt1, int $cnt2, num $steps,
2640 ): int {
2641 $equal = 0;
2643 while ($idx1 < $cnt1 && $idx2 < $cnt2 && comp_line($ar1[$idx1], $ar2[$idx2],
2644 $is_reg)) {
2645 $idx1++;
2646 $idx2++;
2647 $equal++;
2648 $steps--;
2650 $steps--;
2651 if ($steps > 0) {
2652 $eq1 = 0;
2653 $st = $steps / 2;
2655 for ($ofs1 = $idx1 + 1; $ofs1 < $cnt1 && $st > 0; $ofs1++) {
2656 $st--;
2657 $eq = @count_array_diff($ar1, $ar2, $is_reg, $ofs1, $idx2, $cnt1,
2658 $cnt2, $st);
2660 if ($eq > $eq1) {
2661 $eq1 = $eq;
2665 $eq2 = 0;
2666 $st = $steps;
2668 for ($ofs2 = $idx2 + 1; $ofs2 < $cnt2 && $st > 0; $ofs2++) {
2669 $st--;
2670 $eq = @count_array_diff($ar1, $ar2, $is_reg, $idx1, $ofs2, $cnt1, $cnt2, $st);
2671 if ($eq > $eq2) {
2672 $eq2 = $eq;
2676 if ($eq1 > $eq2) {
2677 $equal += $eq1;
2678 } else if ($eq2 > 0) {
2679 $equal += $eq2;
2683 return $equal;
2686 function generate_array_diff(
2687 vec<string> $ar1,
2688 vec<string> $ar2,
2689 bool $is_reg,
2690 vec<string> $w,
2691 ): vec<string> {
2692 $idx1 = 0; $cnt1 = @count($ar1);
2693 $idx2 = 0; $cnt2 = @count($ar2);
2694 $old1 = dict[];
2695 $old2 = dict[];
2697 while ($idx1 < $cnt1 && $idx2 < $cnt2) {
2698 if (comp_line($ar1[$idx1], $ar2[$idx2], $is_reg)) {
2699 $idx1++;
2700 $idx2++;
2701 continue;
2702 } else {
2703 $c1 = @count_array_diff($ar1, $ar2, $is_reg, $idx1+1, $idx2, $cnt1,
2704 $cnt2, 10);
2705 $c2 = @count_array_diff($ar1, $ar2, $is_reg, $idx1, $idx2+1, $cnt1,
2706 $cnt2, 10);
2708 if ($c1 > $c2) {
2709 $old1[$idx1+1] = sprintf("%03d- ", $idx1+1) . $w[$idx1];
2710 $idx1++;
2711 } else if ($c2 > 0) {
2712 $old2[$idx2+1] = sprintf("%03d+ ", $idx2+1) . $ar2[$idx2];
2713 $idx2++;
2714 } else {
2715 $old1[$idx1+1] = sprintf("%03d- ", $idx1+1) . $w[$idx1];
2716 $old2[$idx2+1] = sprintf("%03d+ ", $idx2+1) . $ar2[$idx2];
2717 $idx1++;
2718 $idx2++;
2723 $diff = vec[];
2724 $old1_keys = array_keys($old1);
2725 $old2_keys = array_keys($old2);
2726 $old1_values = array_values($old1);
2727 $old2_values = array_values($old2);
2728 // these start at -2 so $l1 + 1 and $l2 + 1 are not valid indices
2729 $l1 = -2;
2730 $l2 = -2;
2731 $iter1 = 0; $end1 = count($old1);
2732 $iter2 = 0; $end2 = count($old2);
2734 while ($iter1 < $end1 || $iter2 < $end2) {
2735 $k1 = $iter1 < $end1 ? $old1_keys[$iter1] : -2;
2736 $k2 = $iter2 < $end2 ? $old2_keys[$iter2] : -2;
2737 if ($k1 === $l1 + 1 || $iter2 >= $end2) {
2738 $l1 = $k1;
2739 $diff[] = $old1_values[$iter1];
2740 $iter1++;
2741 } else if ($k2 === $l2 + 1 || $iter1 >= $end1) {
2742 $l2 = $k2;
2743 $diff[] = $old2_values[$iter2];
2744 $iter2++;
2745 } else if ($k1 < $k2) {
2746 $l1 = $k1;
2747 $diff[] = $old1_values[$iter1];
2748 $iter1++;
2749 } else {
2750 $l2 = $k2;
2751 $diff[] = $old2_values[$iter2];
2752 $iter2++;
2756 while ($idx1 < $cnt1) {
2757 $diff[] = sprintf("%03d- ", $idx1 + 1) . $w[$idx1];
2758 $idx1++;
2761 while ($idx2 < $cnt2) {
2762 $diff[] = sprintf("%03d+ ", $idx2 + 1) . $ar2[$idx2];
2763 $idx2++;
2766 return $diff;
2769 function generate_diff(
2770 string $wanted,
2771 ?string $wanted_re,
2772 string $output
2773 ): string {
2774 $m = null;
2775 $w = explode("\n", $wanted);
2776 $o = explode("\n", $output);
2777 if (is_null($wanted_re)) {
2778 $r = $w;
2779 } else {
2780 if (preg_match_with_matches('/^\((.*)\)\{(\d+)\}$/s', $wanted_re, inout $m)) {
2781 $t = explode("\n", $m[1]);
2782 $r = vec[];
2783 $w2 = vec[];
2784 for ($i = 0; $i < (int)$m[2]; $i++) {
2785 foreach ($t as $v) {
2786 $r[] = $v;
2788 foreach ($w as $v) {
2789 $w2[] = $v;
2792 $w = $wanted === $wanted_re ? $r : $w2;
2793 } else {
2794 $r = explode("\n", $wanted_re);
2797 $diff = generate_array_diff($r, $o, !is_null($wanted_re), $w);
2799 return safe_implode("\r\n", $diff);
2802 function dump_hhas_cmd(
2803 string $hhvm_cmd, string $test, string $hhas_file,
2804 ): string {
2805 $dump_flags = safe_implode(' ', vec[
2806 '-vEval.AllowHhas=true',
2807 '-vEval.DumpHhas=1',
2808 '-vEval.DumpHhasToFile='.escapeshellarg($hhas_file),
2809 '-vEval.LoadFilepathFromUnitCache=0',
2811 $cmd = str_replace(' -- ', " $dump_flags -- ", $hhvm_cmd);
2812 if ($cmd === $hhvm_cmd) $cmd .= " $dump_flags";
2813 return $cmd;
2816 function dump_hhas_to_temp(string $hhvm_cmd, string $test): ?string {
2817 $temp_file = Status::getTestTmpPath($test, 'round_trip.hhas');
2818 $cmd = dump_hhas_cmd($hhvm_cmd, $test, $temp_file);
2819 $ret = -1;
2820 system("$cmd &> /dev/null", inout $ret);
2821 return $ret === 0 ? $temp_file : null;
2824 const vec<string> SERVER_EXCLUDE_PATHS = vec[
2825 'quick/xenon/',
2826 'slow/streams/',
2827 'slow/ext_mongo/',
2828 'slow/ext_oauth/',
2829 'slow/ext_vsdebug/',
2830 'zend/good/ext/standard/tests/array/',
2833 const string HHAS_EXT = '.hhas';
2835 function can_run_server_test(string $test, Options $options): bool {
2836 // explicitly disabled
2837 if (is_file("$test.noserver") ||
2838 (is_file("$test.nowebserver") && $options->server)) {
2839 return false;
2842 // has its own config
2843 if (find_test_ext($test, 'opts') || is_file("$test.ini") ||
2844 is_file("$test.use.for.ini.migration.testing.only.hdf")) {
2845 return false;
2848 // we can't run repo only tests in server modes
2849 if (is_file("$test.onlyrepo") || is_file("$test.onlyjumpstart")) {
2850 return false;
2853 foreach (SERVER_EXCLUDE_PATHS as $path) {
2854 if (strpos($test, $path) !== false) return false;
2857 // don't run hhas tests in server modes
2858 if (strrpos($test, HHAS_EXT) === (strlen($test) - strlen(HHAS_EXT))) {
2859 return false;
2862 return true;
2865 const int SERVER_TIMEOUT = 45;
2867 function run_config_server(Options $options, string $test): mixed {
2868 invariant(
2869 can_run_server_test($test, $options),
2870 'should_skip_test_simple should have skipped this',
2873 Status::createTestTmpDir($test); // force it to be created
2874 $config = find_file_for_dir(dirname($test), 'config.ini') ?? '';
2875 $servers = $options->servers as Servers;
2876 $port = $servers->configs[$config]->port;
2877 $ch = curl_init("localhost:$port/$test");
2878 curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
2879 curl_setopt($ch, CURLOPT_TIMEOUT, SERVER_TIMEOUT);
2880 curl_setopt($ch, CURLOPT_BINARYTRANSFER, true);
2881 $output = curl_exec($ch);
2882 if ($output is string) {
2883 $output = trim($output);
2884 } else {
2885 $output = "Error talking to server: " . curl_error($ch);
2887 curl_close($ch);
2889 return run_config_post(tuple($output, ''), $test, $options);
2892 function run_config_cli(
2893 Options $options,
2894 string $test,
2895 string $cmd,
2896 dict<string, mixed> $cmd_env,
2897 ): ?(string, string) {
2898 $cmd = timeout_prefix() . $cmd;
2900 if ($options->repo && $options->repo_out is null) {
2901 // we already created it in run_test
2902 $cmd_env['HPHP_TEST_TMPDIR'] = Status::getTestTmpPath($test, 'tmpdir');
2903 } else {
2904 $cmd_env['HPHP_TEST_TMPDIR'] = Status::createTestTmpDir($test);
2906 $cmd_env['HPHP_TEST_SOURCE_FILE'] = $test;
2907 if ($options->log) {
2908 $cmd_env['TRACE'] = 'printir:1';
2909 $cmd_env['HPHP_TRACE_FILE'] = $test . '.log';
2912 $descriptorspec = dict[
2913 0 => vec["pipe", "r"],
2914 1 => vec["pipe", "w"],
2915 2 => vec["pipe", "w"],
2917 $pipes = null;
2918 $process = proc_open(
2919 "$cmd 2>&1", $descriptorspec, inout $pipes, null, $cmd_env
2921 if (!is_resource($process)) {
2922 Status::writeDiff($test, "Couldn't invoke $cmd");
2923 return null;
2926 fclose($pipes[0]);
2927 $output = stream_get_contents($pipes[1]);
2928 $output = trim($output);
2929 $stderr = stream_get_contents($pipes[2]);
2930 fclose($pipes[1]);
2931 fclose($pipes[2]);
2932 proc_close($process);
2934 return tuple($output, $stderr);
2937 function replace_object_resource_ids(string $str, string $replacement): string {
2938 $str = preg_replace(
2939 '/(object\([^)]+\)#)\d+/', '\1'.$replacement, $str
2941 return preg_replace(
2942 '/resource\(\d+\)/', "resource($replacement)", $str
2946 // NOTE: Returns "(string | bool)".
2947 function run_config_post(
2948 (string, string) $outputs,
2949 string $test,
2950 Options $options,
2951 ): mixed {
2952 list($output, $stderr) = $outputs;
2953 file_put_contents(Status::getTestOutputPath($test, 'out'), $output);
2955 $check_hhbbc_error = $options->repo
2956 && (file_exists($test . '.hhbbc_assert') ||
2957 file_exists($test . '.hphpc_assert'));
2959 // hhvm redirects errors to stdout, so anything on stderr is really bad.
2960 if ($stderr && !$check_hhbbc_error) {
2961 Status::writeDiff(
2962 $test,
2963 "Test failed because the process wrote on stderr:\n$stderr"
2965 return false;
2968 $repeats = 0;
2969 if (!$check_hhbbc_error) {
2970 if ($options->retranslate_all is nonnull) {
2971 $repeats = (int)$options->retranslate_all * 2;
2974 if ($options->recycle_tc is nonnull) {
2975 $repeats = (int)$options->recycle_tc;
2978 if ($options->cli_server) {
2979 $repeats = 3;
2983 list($file, $type) = get_expect_file_and_type($test, $options);
2984 if ($file is null || $type is null) {
2985 Status::writeDiff(
2986 $test,
2987 "No $test.expect, $test.expectf, $test.hhvm.expect, " .
2988 "$test.hhvm.expectf, or $test.expectregex. " .
2989 "If $test is meant to be included by other tests, " .
2990 "use a different file extension.\n"
2992 return false;
2995 $wanted = null;
2996 if ($type === 'expect' || $type === 'hhvm.expect') {
2997 $wanted = trim(file_get_contents($file));
2998 if ($options->ignore_oids || $options->repo) {
2999 $output = replace_object_resource_ids($output, 'n');
3000 $wanted = replace_object_resource_ids($wanted, 'n');
3003 if (!$repeats) {
3004 $passed = !strcmp($output, $wanted);
3005 if (!$passed) {
3006 Status::writeDiff($test, generate_diff($wanted, null, $output));
3008 return $passed;
3010 $wanted_re = preg_quote($wanted, '/');
3011 } else if ($type === 'expectf' || $type === 'hhvm.expectf') {
3012 $wanted = trim(file_get_contents($file));
3013 if ($options->ignore_oids || $options->repo) {
3014 $wanted = replace_object_resource_ids($wanted, '%d');
3016 $wanted_re = $wanted;
3018 // do preg_quote, but miss out any %r delimited sections.
3019 $temp = "";
3020 $r = "%r";
3021 $startOffset = 0;
3022 $length = strlen($wanted_re);
3023 while ($startOffset < $length) {
3024 $start = strpos($wanted_re, $r, $startOffset);
3025 if ($start !== false) {
3026 // we have found a start tag.
3027 $end = strpos($wanted_re, $r, $start+2);
3028 if ($end === false) {
3029 // unbalanced tag, ignore it.
3030 $start = $length;
3031 $end = $length;
3033 } else {
3034 // no more %r sections.
3035 $start = $length;
3036 $end = $length;
3038 // quote a non re portion of the string.
3039 $temp = $temp.preg_quote(substr($wanted_re, $startOffset,
3040 ($start - $startOffset)), '/');
3041 // add the re unquoted.
3042 if ($end > $start) {
3043 $temp = $temp.'('.substr($wanted_re, $start+2, ($end - $start-2)).')';
3045 $startOffset = $end + 2;
3047 $wanted_re = $temp;
3049 $wanted_re = str_replace(
3050 vec['%binary_string_optional%'],
3051 'string',
3052 $wanted_re
3054 $wanted_re = str_replace(
3055 vec['%unicode_string_optional%'],
3056 'string',
3057 $wanted_re
3059 $wanted_re = str_replace(
3060 vec['%unicode\|string%', '%string\|unicode%'],
3061 'string',
3062 $wanted_re
3064 $wanted_re = str_replace(
3065 vec['%u\|b%', '%b\|u%'],
3067 $wanted_re
3069 // Stick to basics.
3070 $wanted_re = str_replace('%e', '\\' . DIRECTORY_SEPARATOR, $wanted_re);
3071 $wanted_re = str_replace('%s', '[^\r\n]+', $wanted_re);
3072 $wanted_re = str_replace('%S', '[^\r\n]*', $wanted_re);
3073 $wanted_re = str_replace('%a', '.+', $wanted_re);
3074 $wanted_re = str_replace('%A', '.*', $wanted_re);
3075 $wanted_re = str_replace('%w', '\s*', $wanted_re);
3076 $wanted_re = str_replace('%i', '[+-]?\d+', $wanted_re);
3077 $wanted_re = str_replace('%d', '\d+', $wanted_re);
3078 $wanted_re = str_replace('%x', '[0-9a-fA-F]+', $wanted_re);
3079 // %f allows two points "-.0.0" but that is the best *simple* expression.
3080 $wanted_re = str_replace('%f', '[+-]?\.?\d+\.?\d*(?:[Ee][+-]?\d+)?',
3081 $wanted_re);
3082 $wanted_re = str_replace('%c', '.', $wanted_re);
3083 // must be last.
3084 $wanted_re = str_replace('%%', '%%?', $wanted_re);
3086 // Normalize newlines.
3087 $wanted_re = preg_replace("/(\r\n?|\n)/", "\n", $wanted_re);
3088 $output = preg_replace("/(\r\n?|\n)/", "\n", $output);
3089 } else if ($type === 'expectregex') {
3090 $wanted_re = trim(file_get_contents($file));
3091 } else {
3092 throw new Exception("Unsupported expect file type: ".$type);
3095 if ($repeats) {
3096 $wanted_re = "($wanted_re\s*)".'{'.$repeats.'}';
3098 if ($wanted is null) $wanted = $wanted_re;
3099 $passed = @preg_match("/^$wanted_re\$/s", $output);
3100 if ($passed) return true;
3101 if ($passed === false && $repeats) {
3102 // $repeats can cause the regex to become too big, and fail
3103 // to compile.
3104 return 'skip-repeats-fail';
3106 $diff = generate_diff($wanted_re, $wanted_re, $output);
3107 if ($passed === false && $diff === "") {
3108 // the preg match failed, probably because the regex was too complex,
3109 // but since the line by line diff came up empty, we're fine
3110 return true;
3112 Status::writeDiff($test, $diff);
3113 return false;
3116 function timeout_prefix(): string {
3117 if (is_executable('/usr/bin/timeout')) {
3118 return '/usr/bin/timeout ' . TIMEOUT_SECONDS . ' ';
3119 } else {
3120 return hphp_home() . '/hphp/tools/timeout.sh -t ' . TIMEOUT_SECONDS . ' ';
3124 // NOTE: Returns "(string | bool)".
3125 function run_foreach_config(
3126 Options $options,
3127 string $test,
3128 vec<string> $cmds,
3129 dict<string, mixed> $cmd_env,
3130 ): mixed {
3131 invariant(count($cmds) > 0, "run_foreach_config: no modes");
3132 $result = false;
3133 foreach ($cmds as $cmd) {
3134 $outputs = run_config_cli($options, $test, $cmd, $cmd_env);
3135 if ($outputs is null) return false;
3136 $result = run_config_post($outputs, $test, $options);
3137 if (!$result) return $result;
3139 return $result;
3142 function run_and_log_test(Options $options, string $test): void {
3143 $stime = time();
3144 $time = mtime();
3145 $status = run_test($options, $test);
3146 $time = mtime() - $time;
3147 $etime = time();
3149 if ($status === false) {
3150 $diff = Status::diffForTest($test);
3151 if ($diff === '') {
3152 $diff = 'Test failed with empty diff';
3154 Status::fail($test, $time, $stime, $etime, $diff);
3155 } else if ($status === true) {
3156 Status::pass($test, $time, $stime, $etime);
3157 clean_intermediate_files($test, $options);
3158 } else if ($status is string) {
3159 invariant(
3160 preg_match('/^skip-[\w-]+$/', $status),
3161 "invalid skip status %s",
3162 $status
3164 Status::skip($test, substr($status, 5), $time, $stime, $etime);
3165 clean_intermediate_files($test, $options);
3166 } else {
3167 invariant_violation("invalid status type %s", gettype($status));
3171 // NOTE: Returns "(string | bool)".
3172 function run_test(Options $options, string $test): mixed {
3173 $skip_reason = should_skip_test_simple($options, $test);
3174 if ($skip_reason is nonnull) return $skip_reason;
3176 if (!$options->no_skipif) {
3177 $result = runif_should_skip_test($options, $test);
3178 if (!$result['valid']) {
3179 invariant(Shapes::keyExists($result, 'error'), 'missing runif error');
3180 Status::writeDiff($test, 'Invalid .runif file: ' . $result['error']);
3181 return false;
3183 if (!($result['match'] ?? false)) {
3184 invariant(Shapes::keyExists($result, 'skip_reason'), 'missing skip_reason');
3185 return $result['skip_reason'];
3188 $result = skipif_should_skip_test($options, $test);
3189 if (!$result['valid']) {
3190 invariant(Shapes::keyExists($result, 'error'), 'missing skipif error');
3191 Status::writeDiff($test, $result['error']);
3192 return false;
3194 if (!($result['match'] ?? false)) {
3195 invariant(Shapes::keyExists($result, 'skip_reason'), 'missing skip_reason');
3196 return $result['skip_reason'];
3200 list($hhvm, $hhvm_env) = hhvm_cmd($options, $test);
3202 if (preg_grep('/ --count[ =][0-9]+ .* --count[ =][0-9]+( |$)/', $hhvm)) {
3203 // we got --count from 2 sources (e.g. .opts file and multi_request_mode)
3204 // this can't work so skip the test
3205 return 'skip-count';
3206 } else if ($options->jit_serialize is nonnull) {
3207 // jit-serialize adds the --count option later, so even 1 --count in the
3208 // command means we have to skip
3209 if (preg_grep('/ --count[ =][0-9]+( |$)/', $hhvm)) {
3210 return 'skip-count';
3214 if ($options->repo) {
3215 if (file_exists($test.'.norepo')) {
3216 return 'skip-norepo';
3218 if (file_exists($test.'.onlyjumpstart') &&
3219 ($options->jit_serialize is null || (int)$options->jit_serialize < 1)) {
3220 return 'skip-onlyjumpstart';
3223 $test_repo = test_repo($options, $test);
3224 if ($options->repo_out is nonnull) {
3225 // we may need to clean up after a previous run
3226 $repo_files = vec['hhvm.hhbc', 'hhvm.hhbbc'];
3227 foreach ($repo_files as $repo_file) {
3228 @unlink("$test_repo/$repo_file");
3230 } else {
3231 // create tmpdir now so that we can write repos
3232 Status::createTestTmpDir($test);
3235 $program = "hhvm";
3237 if (file_exists($test . '.hphpc_assert')) {
3238 $hphp = hphp_cmd($options, $test, $program);
3239 return run_foreach_config($options, $test, vec[$hphp], $hhvm_env);
3240 } else if (file_exists($test . '.hhbbc_assert')) {
3241 $hphp = hphp_cmd($options, $test, $program);
3242 if (repo_separate($options, $test)) {
3243 $result = exec_with_stack($hphp);
3244 if ($result is string) return false;
3245 $hhbbc = hhbbc_cmd($options, $test, $program);
3246 return run_foreach_config($options, $test, vec[$hhbbc], $hhvm_env);
3247 } else {
3248 return run_foreach_config($options, $test, vec[$hphp], $hhvm_env);
3252 if (!repo_mode_compile($options, $test, $program)) {
3253 return false;
3256 if ($options->hhbbc2) {
3257 invariant(
3258 count($hhvm) === 1,
3259 "get_options forbids modes because we're not runnig code"
3261 // create tmpdir now so that we can write hhas
3262 Status::createTestTmpDir($test);
3263 $hhas_temp1 = dump_hhas_to_temp($hhvm[0], "$test.before");
3264 if ($hhas_temp1 is null) {
3265 Status::writeDiff($test, "dumping hhas after first hhbbc pass failed");
3266 return false;
3268 shell_exec("mv $test_repo/$program.hhbbc $test_repo/$program.hhbc");
3269 $hhbbc = hhbbc_cmd($options, $test, $program);
3270 $result = exec_with_stack($hhbbc);
3271 if ($result is string) {
3272 Status::writeDiff($test, $result);
3273 return false;
3275 $hhas_temp2 = dump_hhas_to_temp($hhvm[0], "$test.after");
3276 if ($hhas_temp2 is null) {
3277 Status::writeDiff($test, "dumping hhas after second hhbbc pass failed");
3278 return false;
3280 $diff = shell_exec("diff $hhas_temp1 $hhas_temp2");
3281 if (trim($diff) !== '') {
3282 Status::writeDiff($test, $diff);
3283 return false;
3287 if ($options->jit_serialize is nonnull) {
3288 invariant(count($hhvm) === 1, 'get_options enforces jit mode only');
3289 $cmd = jit_serialize_option($hhvm[0], $test, $options, true);
3290 $outputs = run_config_cli($options, $test, $cmd, $hhvm_env);
3291 if ($outputs is null) return false;
3292 $cmd = jit_serialize_option($hhvm[0], $test, $options, true);
3293 $outputs = run_config_cli($options, $test, $cmd, $hhvm_env);
3294 if ($outputs is null) return false;
3295 $hhvm[0] = jit_serialize_option($hhvm[0], $test, $options, false);
3298 return run_foreach_config($options, $test, $hhvm, $hhvm_env);
3301 if (file_exists($test.'.onlyrepo')) {
3302 return 'skip-onlyrepo';
3304 if (file_exists($test.'.onlyjumpstart')) {
3305 return 'skip-onlyjumpstart';
3308 if ($options->hhas_round_trip) {
3309 invariant(
3310 substr($test, -5) !== ".hhas",
3311 'should_skip_test_simple should have skipped this',
3313 // create tmpdir now so that we can write hhas
3314 Status::createTestTmpDir($test);
3315 // dumping hhas, not running code so arbitrarily picking a mode
3316 $hhas_temp = dump_hhas_to_temp($hhvm[0], $test);
3317 if ($hhas_temp is null) {
3318 $err = "system failed: " .
3319 dump_hhas_cmd($hhvm[0], $test,
3320 Status::getTestTmpPath($test, 'round_trip.hhas')) .
3321 "\n";
3322 Status::writeDiff($test, $err);
3323 return false;
3325 list($hhvm, $hhvm_env) = hhvm_cmd($options, $test, $hhas_temp);
3328 if ($options->server) {
3329 return run_config_server($options, $test);
3331 return run_foreach_config($options, $test, $hhvm, $hhvm_env);
3334 function num_cpus(): int {
3335 switch (PHP_OS) {
3336 case 'Linux':
3337 $data = file('/proc/stat');
3338 $cores = 0;
3339 foreach($data as $line) {
3340 if (preg_match('/^cpu[0-9]/', $line)) {
3341 $cores++;
3344 return $cores;
3345 case 'Darwin':
3346 case 'FreeBSD':
3347 $output = null;
3348 $return_var = -1;
3349 return (int)exec('sysctl -n hw.ncpu', inout $output, inout $return_var);
3351 return 2; // default when we don't know how to detect.
3354 function make_header(string $str): string {
3355 return "\n\033[0;33m".$str."\033[0m\n";
3358 function print_commands(
3359 vec<string> $tests,
3360 Options $options,
3361 ): void {
3362 if ($options->verbose) {
3363 print make_header("Run these by hand:");
3364 } else {
3365 $test = $tests[0];
3366 print make_header("Run $test by hand:");
3367 $tests = vec[$test];
3370 foreach ($tests as $test) {
3371 list($commands, $_) = hhvm_cmd($options, $test);
3372 if (!$options->repo) {
3373 foreach ($commands as $c) {
3374 print "$c\n";
3376 continue;
3379 // How to run it with hhbbc:
3380 $program = "hhvm";
3381 $hhbbc_cmds = hphp_cmd($options, $test, $program)."\n";
3382 if (repo_separate($options, $test)) {
3383 $hhbbc_cmd = hhbbc_cmd($options, $test, $program)."\n";
3384 $hhbbc_cmds .= $hhbbc_cmd;
3385 if ($options->hhbbc2) {
3386 foreach ($commands as $c) {
3387 $hhbbc_cmds .=
3388 $c." -vEval.DumpHhas=1 > $test.before.round_trip.hhas\n";
3390 $test_repo = test_repo($options, $test);
3391 $hhbbc_cmds .=
3392 "mv $test_repo/$program.hhbbc $test_repo/$program.hhbc\n";
3393 $hhbbc_cmds .= $hhbbc_cmd;
3394 foreach ($commands as $c) {
3395 $hhbbc_cmds .=
3396 $c." -vEval.DumpHhas=1 > $test.after.round_trip.hhas\n";
3398 $hhbbc_cmds .=
3399 "diff $test.before.round_trip.hhas $test.after.round_trip.hhas\n";
3402 if ($options->jit_serialize is nonnull) {
3403 invariant(count($commands) === 1, 'get_options enforces jit mode only');
3404 $hhbbc_cmds .=
3405 jit_serialize_option($commands[0], $test, $options, true) . "\n";
3406 $hhbbc_cmds .=
3407 jit_serialize_option($commands[0], $test, $options, true) . "\n";
3408 $commands[0] = jit_serialize_option($commands[0], $test, $options, false);
3410 foreach ($commands as $c) {
3411 $hhbbc_cmds .= $c."\n";
3413 print "$hhbbc_cmds\n";
3417 // This runs only in the "printer" child.
3418 function msg_loop(int $num_tests, Queue $queue): void {
3419 $cols = null;
3420 $do_progress =
3422 Status::getMode() === Status::MODE_NORMAL ||
3423 Status::getMode() === Status::MODE_RECORD_FAILURES
3424 ) &&
3425 Status::hasCursorControl();
3426 if ($do_progress) {
3427 $stty = strtolower(Status::getSTTY());
3428 $matches = vec[];
3429 if (preg_match_with_matches("/columns ([0-9]+);/", $stty, inout $matches) ||
3430 // because BSD has to be different
3431 preg_match_with_matches("/([0-9]+) columns;/", $stty, inout $matches)) {
3432 $cols = (int)$matches[1];
3436 while (true) {
3437 list($pid, $type, $message) = $queue->receiveMessage();
3438 if (!Status::handle_message($type, $message)) break;
3440 if ($cols is nonnull) {
3441 $total_run = (Status::$skipped + Status::$failed + Status::$passed);
3442 $bar_cols = $cols - 45;
3444 $passed_ticks = (int)round($bar_cols * (Status::$passed / $num_tests));
3445 $skipped_ticks = (int)round($bar_cols * (Status::$skipped / $num_tests));
3446 $failed_ticks = (int)round($bar_cols * (Status::$failed / $num_tests));
3448 $fill = $bar_cols - ($passed_ticks + $skipped_ticks + $failed_ticks);
3449 if ($fill < 0) $fill = 0;
3451 $passed_ticks = str_repeat('#', $passed_ticks);
3452 $skipped_ticks = str_repeat('#', $skipped_ticks);
3453 $failed_ticks = str_repeat('#', $failed_ticks);
3454 $fill = str_repeat('-', (int)$fill);
3456 echo
3457 "\033[2K\033[1G[",
3458 "\033[0;32m$passed_ticks",
3459 "\033[33m$skipped_ticks",
3460 "\033[31m$failed_ticks",
3461 "\033[0m$fill] ($total_run/$num_tests) ",
3462 "(", Status::$skipped, " skipped,", Status::$failed, " failed)";
3466 if ($cols is nonnull) {
3467 print "\033[2K\033[1G";
3468 if (Status::$skipped > 0) {
3469 print Status::$skipped ." tests \033[1;33mskipped\033[0m\n";
3470 $reasons = Status::$skip_reasons;
3471 arsort(inout $reasons);
3472 Status::$skip_reasons = $reasons as dict<_, _>;
3473 foreach (Status::$skip_reasons as $reason => $count) {
3474 printf("%12s: %d\n", $reason, $count);
3480 function print_success(
3481 vec<string> $tests,
3482 dict<string, TestResult> $results,
3483 Options $options,
3484 ): void {
3485 // We didn't run any tests, not even skipped. Clowntown!
3486 if (!$tests) {
3487 print "\nCLOWNTOWN: No tests!\n";
3488 if (!$options->no_fun) {
3489 print_clown();
3491 return;
3493 $ran_tests = false;
3494 foreach ($results as $result) {
3495 // The result here will either be skipped or passed (since failed is
3496 // handled in print_failure.
3497 if ($result['status'] === 'passed') {
3498 $ran_tests = true;
3499 break;
3502 // We just had skipped tests
3503 if (!$ran_tests) {
3504 print "\nSKIP-ALOO: Only skipped tests!\n";
3505 if (!$options->no_fun) {
3506 print_skipper();
3508 return;
3510 print "\nAll tests passed.\n";
3511 if (!$options->no_fun) {
3512 print_ship();
3514 if ($options->failure_file is nonnull) {
3515 @unlink($options->failure_file);
3517 if ($options->verbose) {
3518 print_commands($tests, $options);
3522 function print_failure(
3523 vec<string> $argv,
3524 dict<string, TestResult> $results,
3525 Options $options,
3526 ): void {
3527 $failed = vec[];
3528 $passed = vec[];
3529 foreach ($results as $result) {
3530 if ($result['status'] === 'failed') {
3531 $failed[] = $result['name'];
3532 } else if ($result['status'] === 'passed') {
3533 $passed[] = $result['name'];
3536 sort(inout $failed);
3538 $failing_tests_file = $options->failure_file ??
3539 Status::getRunTmpDir() . '/test-failures';
3540 file_put_contents($failing_tests_file, safe_implode("\n", $failed)."\n");
3541 if ($passed) {
3542 $passing_tests_file = Status::getRunTmpDir() . '/tests-passed';
3543 file_put_contents($passing_tests_file, safe_implode("\n", $passed)."\n");
3544 } else {
3545 $passing_tests_file = "";
3548 print "\n".count($failed)." tests failed\n";
3549 if (!$options->no_fun) {
3550 // Unicode for table-flipping emoticon
3551 // https://knowyourmeme.com/memes/flipping-tables
3552 print "(\u{256F}\u{00B0}\u{25A1}\u{00B0}\u{FF09}\u{256F}\u{FE35} \u{253B}";
3553 print "\u{2501}\u{253B}\n";
3556 print_commands($failed, $options);
3558 print make_header("See failed test output and expectations:");
3559 foreach ($failed as $n => $test) {
3560 if ($n !== 0) print "\n";
3561 print 'cat ' . Status::getTestOutputPath($test, 'diff') . "\n";
3562 print 'cat ' . Status::getTestOutputPath($test, 'out') . "\n";
3563 $expect_file = get_expect_file_and_type($test, $options)[0];
3564 if ($expect_file is null) {
3565 print "# no expect file found for $test\n";
3566 } else {
3567 print "cat $expect_file\n";
3570 // only print 3 tests worth unless verbose is on
3571 if ($n === 2 && !$options->verbose) {
3572 $remaining = count($failed) - 1 - $n;
3573 if ($remaining > 0) {
3574 print make_header("... and $remaining more.");
3576 break;
3580 if ($passed) {
3581 print make_header(
3582 'For xargs, lists of failed and passed tests are available using:'
3584 print 'cat '.$failing_tests_file."\n";
3585 print 'cat '.$passing_tests_file."\n";
3586 } else {
3587 print make_header('For xargs, list of failures is available using:').
3588 'cat '.$failing_tests_file."\n";
3591 print
3592 make_header("Re-run just the failing tests:") .
3593 str_replace("run.php", "run", $argv[0]) . ' ' .
3594 safe_implode(' ', \HH\global_get('recorded_options')) .
3595 sprintf(' $(cat %s)%s', $failing_tests_file, "\n");
3598 function port_is_listening(int $port): bool {
3599 $socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
3600 return @socket_connect($socket, 'localhost', $port);
3603 function find_open_port(): int {
3604 for ($i = 0; $i < 50; ++$i) {
3605 $port = rand(1024, 65535);
3606 if (!port_is_listening($port)) return $port;
3609 error("Couldn't find an open port");
3612 function start_server_proc(
3613 Options $options,
3614 string $config,
3615 int $port,
3616 ): Server {
3617 if ($options->cli_server) {
3618 $cli_sock = tempnam(sys_get_temp_dir(), 'hhvm-cli-');
3619 } else {
3620 // still want to test that an unwritable socket works...
3621 $cli_sock = '/var/run/hhvm-cli.sock';
3623 $threads = get_num_threads($options);
3624 $thread_option = $options->cli_server
3625 ? '-vEval.UnixServerWorkers='.$threads
3626 : '-vServer.ThreadCount='.$threads;
3627 $prelude = $options->server
3628 ? '-vEval.PreludePath=' . Status::getRunTmpDir() . '/server-prelude.php'
3629 : "";
3630 $command = hhvm_cmd_impl(
3631 $options,
3632 $config,
3633 null, // we do not pass Autoload.DB.Path to the server process
3634 '-m', 'server',
3635 "-vServer.Port=$port",
3636 "-vServer.Type=proxygen",
3637 "-vAdminServer.Port=0",
3638 $thread_option,
3639 '-vServer.ExitOnBindFail=1',
3640 '-vServer.RequestTimeoutSeconds='.SERVER_TIMEOUT,
3641 '-vPageletServer.ThreadCount=0',
3642 '-vLog.UseRequestLog=1',
3643 '-vLog.File=/dev/null',
3644 $prelude,
3646 // The server will unlink the temp file
3647 '-vEval.UnixServerPath='.$cli_sock,
3649 // This ensures we actually jit everything:
3650 '-vEval.JitRequireWriteLease=1',
3652 // The default test config uses a small TC but we'll be running thousands
3653 // of tests against the same process:
3654 '-vEval.JitASize=394264576',
3655 '-vEval.JitAColdSize=201326592',
3656 '-vEval.JitAFrozenSize=251658240',
3657 '-vEval.JitGlobalDataSize=32000000',
3659 // load/store counters don't work on Ivy Bridge so disable for tests
3660 '-vEval.ProfileHWEnable=false'
3662 if (count($command) !== 1) {
3663 error("Can't run multi-mode tests in server mode");
3665 $command = $command[0];
3666 if (getenv('HHVM_TEST_SERVER_LOG')) {
3667 echo "Starting server '$command'\n";
3670 $descriptors = dict[
3671 0 => vec['file', '/dev/null', 'r'],
3672 1 => vec['file', '/dev/null', 'w'],
3673 2 => vec['file', '/dev/null', 'w'],
3676 $dummy = null;
3677 $proc = proc_open($command, $descriptors, inout $dummy);
3678 if (!$proc) {
3679 error("Failed to start server process");
3681 // NOTE: This is a "dict<string, mixed>".
3682 $status = proc_get_status($proc);
3683 $pid = $status['pid'] as int;
3684 $server = new Server($proc, $pid, $port, $config, $cli_sock);
3685 return $server;
3688 final class Server {
3689 public function __construct(
3690 public resource $proc,
3691 public int $pid,
3692 public int $port,
3693 public string $config,
3694 public string $cli_socket,
3699 final class Servers {
3700 public dict<int, Server> $pids = dict[];
3701 public dict<string, Server> $configs = dict[];
3704 // For each config file in $configs, start up a server on a randomly-determined
3705 // port.
3706 function start_servers(
3707 Options $options,
3708 keyset<string> $configs,
3709 ): Servers {
3710 if ($options->server) {
3711 $prelude = <<<'EOT'
3712 <?hh
3713 <<__EntryPoint>> function UNIQUE_NAME_I_DONT_EXIST_IN_ANY_TEST(): void {
3714 putenv("HPHP_TEST_TMPDIR=BASEDIR{$_SERVER['SCRIPT_NAME']}.tmpdir");
3716 EOT;
3717 file_put_contents(
3718 Status::getRunTmpDir() . '/server-prelude.php',
3719 str_replace('BASEDIR', Status::getRunTmpDir(), $prelude),
3723 $starting = vec[];
3724 foreach ($configs as $config) {
3725 $starting[] = start_server_proc($options, $config, find_open_port());
3728 $start_time = mtime();
3729 $servers = new Servers();
3731 // Wait for all servers to come up.
3732 while (count($starting) > 0) {
3733 $still_starting = vec[];
3735 foreach ($starting as $server) {
3736 $config = $server->config;
3737 $pid = $server->pid;
3738 $port = $server->port;
3739 $proc = $server->proc;
3741 $new_status = proc_get_status($proc);
3743 if (!$new_status['running']) {
3744 if ($new_status['exitcode'] === 0) {
3745 error("Server exited prematurely but without error");
3748 // We lost a race. Try another port.
3749 if (getenv('HHVM_TEST_SERVER_LOG')) {
3750 echo "\n\nLost connection race on port $port. Trying another.\n\n";
3752 $port = find_open_port();
3753 $still_starting[] = start_server_proc($options, $config, $port);
3754 } else if (!port_is_listening($port)) {
3755 $still_starting[] = $server;
3756 } else {
3757 $servers->pids[$pid] = $server;
3758 $servers->configs[$config] = $server;
3762 $starting = $still_starting;
3763 $max_time = 10;
3764 if (mtime() - $start_time > $max_time) {
3765 error("Servers took more than $max_time seconds to come up");
3768 // Take a short nap and try again.
3769 usleep(100000);
3772 $elapsed = mtime() - $start_time;
3773 printf("Started %d servers in %.1f seconds\n\n", count($configs), $elapsed);
3774 return $servers;
3777 function get_num_threads(Options $options): int {
3778 if ($options->threads is nonnull) {
3779 $threads = (int)$options->threads;
3780 if ((string)$threads !== $options->threads || $threads < 1) {
3781 error("--threads must be an integer >= 1");
3783 } else {
3784 $threads = $options->server || $options->cli_server
3785 ? num_cpus() * 2 : num_cpus();
3787 return $threads;
3790 function runner_precheck(): void {
3791 // Basic checking for runner.
3792 $server = HH\global_get('_SERVER');
3793 $env = HH\global_get('_ENV');
3794 if (!((bool)$server ?? false) || !((bool)$env ?? false)) {
3795 echo "Warning: \$_SERVER/\$_ENV variables not available, please check \n" .
3796 "your ini setting: variables_order, it should have both 'E' and 'S'\n";
3800 function main(vec<string> $argv): int {
3801 runner_precheck();
3803 ini_set('pcre.backtrack_limit', PHP_INT_MAX);
3805 list($options, $files) = get_options($argv);
3806 if ($options->help) {
3807 error(help());
3809 if ($options->list_tests) {
3810 list_tests($files, $options);
3811 success();
3814 $tests = find_tests($files, $options);
3815 if ($options->shuffle) {
3816 shuffle(inout $tests);
3819 // Explicit path given by --hhvm-binary-path takes priority. Then, if an
3820 // HHVM_BIN env var exists, and the file it points to exists, that trumps
3821 // any default hhvm executable path.
3822 if ($options->hhvm_binary_path is nonnull) {
3823 $binary_path = check_executable($options->hhvm_binary_path);
3824 putenv("HHVM_BIN=" . $binary_path);
3825 } else if (getenv("HHVM_BIN") !== false) {
3826 $binary_path = check_executable(getenv("HHVM_BIN"));
3827 } else {
3828 check_for_multiple_default_binaries();
3829 $binary_path = hhvm_path();
3832 if ($options->verbose) {
3833 print "You are using the binary located at: " . $binary_path . "\n";
3836 Status::createTmpDir();
3838 $servers = null;
3839 if ($options->server || $options->cli_server) {
3840 if ($options->server && $options->cli_server) {
3841 error("Server mode and CLI Server mode are mutually exclusive");
3843 if ($options->repo) {
3844 error("Server mode repo tests are not supported");
3847 /* We need to start up a separate server process for each config file
3848 * found. */
3849 $configs = keyset[];
3850 foreach ($tests as $test) {
3851 $config = find_file_for_dir(dirname($test), 'config.ini');
3852 if (!$config) {
3853 error("Couldn't find config file for $test");
3855 if (array_key_exists($config, $configs)) continue;
3856 if (should_skip_test_simple($options, $test) is nonnull) continue;
3857 $configs[] = $config;
3860 $max_configs = 30;
3861 if (count($configs) > $max_configs) {
3862 error("More than $max_configs unique config files will be needed to run ".
3863 "the tests you specified. They may not be a good fit for server ".
3864 "mode. (".count($configs)." required)");
3867 $servers = start_servers($options, $configs);
3868 $options->servers = $servers;
3871 // Try to construct the buckets so the test results are ready in
3872 // approximately alphabetical order.
3873 // Get the serial tests to be in their own bucket later.
3874 $serial_tests = serial_only_tests($tests);
3876 // If we have no serial tests, we can use the maximum number of allowed
3877 // threads for the test running. If we have some, we save one thread for
3878 // the serial bucket. However if we only have one thread, we don't split
3879 // out serial tests.
3880 $parallel_threads = min(get_num_threads($options), \count($tests)) as int;
3881 if ($parallel_threads === 1) {
3882 $test_buckets = vec[$tests];
3883 } else {
3884 if (count($serial_tests) > 0) {
3885 // reserve a thread for serial tests
3886 $parallel_threads--;
3889 $test_buckets = vec[];
3890 for ($i = 0; $i < $parallel_threads; $i++) {
3891 $test_buckets[] = vec[];
3894 $i = 0;
3895 foreach ($tests as $test) {
3896 if (!in_array($test, $serial_tests)) {
3897 $test_buckets[$i][] = $test;
3898 $i = ($i + 1) % $parallel_threads;
3902 if (count($serial_tests) > 0) {
3903 // The last bucket is serial.
3904 $test_buckets[] = $serial_tests;
3908 // Remember that the serial tests are also in the tests array too,
3909 // so they are part of the total count.
3910 if (!$options->testpilot) {
3911 print "Running ".count($tests)." tests in ".
3912 count($test_buckets)." threads (" . count($serial_tests) .
3913 " in serial)\n";
3916 if ($options->verbose) {
3917 Status::setMode(Status::MODE_VERBOSE);
3919 if ($options->testpilot) {
3920 Status::setMode(Status::MODE_TESTPILOT);
3922 if ($options->record_failures is nonnull) {
3923 Status::setMode(Status::MODE_RECORD_FAILURES);
3925 Status::setUseColor($options->color || posix_isatty(STDOUT));
3927 Status::$nofork = count($tests) === 1 && !$servers;
3929 if (!Status::$nofork) {
3930 // Create the Queue before any children are forked.
3931 $queue = Status::getQueue();
3933 // Fork a "printer" child to process status messages.
3934 $printer_pid = pcntl_fork();
3935 if ($printer_pid === -1) {
3936 error("failed to fork");
3937 } else if ($printer_pid === 0) {
3938 msg_loop(count($tests), $queue);
3939 return 0;
3941 } else {
3942 // Satisfy the type-checker.
3943 $printer_pid = -1;
3946 // NOTE: This unblocks the Queue (if needed).
3947 Status::started();
3949 // Fork "worker" children (if needed).
3950 $children = dict[];
3951 // We write results as json in each child and collate them at the end
3952 $json_results_files = vec[];
3953 if (Status::$nofork) {
3954 Status::registerCleanup($options->no_clean);
3955 $json_results_file = tempnam('/tmp', 'test-run-');
3956 $json_results_files[] = $json_results_file;
3957 invariant(count($test_buckets) === 1, "nofork was set erroneously");
3958 $return_value = child_main($options, $test_buckets[0], $json_results_file);
3959 } else {
3960 foreach ($test_buckets as $test_bucket) {
3961 $json_results_file = tempnam('/tmp', 'test-run-');
3962 $json_results_files[] = $json_results_file;
3963 $pid = pcntl_fork();
3964 if ($pid === -1) {
3965 error('could not fork');
3966 } else if ($pid) {
3967 $children[$pid] = $pid;
3968 } else {
3969 invariant($test_bucket is vec<_>, "%s", __METHOD__);
3970 exit(child_main($options, $test_bucket, $json_results_file));
3974 // Make sure to clean up on exit, or on SIGTERM/SIGINT.
3975 // Do this here so no children inherit this.
3976 Status::registerCleanup($options->no_clean);
3978 // Have the parent wait for all forked children to exit.
3979 $return_value = 0;
3980 while (count($children) && $printer_pid !== 0) {
3981 $status = null;
3982 $pid = pcntl_wait(inout $status);
3983 if (!pcntl_wifexited($status) && !pcntl_wifsignaled($status)) {
3984 error("Unexpected exit status from child");
3987 if ($pid === $printer_pid) {
3988 // We should be finishing up soon.
3989 $printer_pid = 0;
3990 } else if ($servers && isset($servers->pids[$pid])) {
3991 // A server crashed. Restart it.
3992 if (getenv('HHVM_TEST_SERVER_LOG')) {
3993 echo "\nServer $pid crashed. Restarting.\n";
3995 Status::serverRestarted();
3996 $server = $servers->pids[$pid];
3997 $server = start_server_proc($options, $server->config, $server->port);
3999 // Unset the old $pid entry and insert the new one.
4000 unset($servers->pids[$pid]);
4001 $pid = $server->pid;
4002 $servers->pids[$pid] = $server;
4003 } elseif (isset($children[$pid])) {
4004 unset($children[$pid]);
4005 $return_value |= pcntl_wexitstatus($status);
4006 } // Else, ignorable signal
4010 Status::finished($return_value);
4012 // Wait for the printer child to die, if needed.
4013 if (!Status::$nofork && $printer_pid !== 0) {
4014 $status = 0;
4015 $pid = pcntl_waitpid($printer_pid, inout $status);
4016 $status = $status as int;
4017 if (!pcntl_wifexited($status) && !pcntl_wifsignaled($status)) {
4018 error("Unexpected exit status from child");
4022 // Kill the servers.
4023 if ($servers) {
4024 foreach ($servers->pids as $server) {
4025 proc_terminate($server->proc);
4026 proc_close($server->proc);
4030 // Aggregate results.
4031 $results = dict[];
4032 foreach ($json_results_files as $json_results_file) {
4033 $contents = file_get_contents($json_results_file);
4034 $json = json_decode($contents, true);
4035 if (!is_dict($json)) {
4036 error(
4037 "\nNo JSON output was received from a test thread. ".
4038 "Either you killed it, or it might be a bug in the test script.",
4041 $results = array_merge($results, $json);
4042 unlink($json_results_file);
4045 // Print results.
4046 if ($options->record_failures is nonnull) {
4047 $fail_file = $options->record_failures;
4048 $failed_tests = vec[];
4049 $prev_failing = vec[];
4050 if (file_exists($fail_file)) {
4051 $prev_failing = explode("\n", file_get_contents($fail_file));
4054 $new_fails = 0;
4055 $new_passes = 0;
4056 foreach ($results as $r) {
4057 if (!isset($r['name']) || !isset($r['status'])) continue;
4058 $test = canonical_path($r['name']);
4059 $status = $r['status'];
4060 if ($status === 'passed' && in_array($test, $prev_failing)) {
4061 $new_passes++;
4062 continue;
4064 if ($status !== 'failed') continue;
4065 if (!in_array($test, $prev_failing)) $new_fails++;
4066 $failed_tests[] = $test;
4068 printf(
4069 "Recording %d tests as failing.\n".
4070 "There are %d new failing tests, and %d new passing tests.\n",
4071 count($failed_tests), $new_fails, $new_passes
4073 sort(inout $failed_tests);
4074 file_put_contents($fail_file, safe_implode("\n", $failed_tests));
4075 } else if ($options->testpilot) {
4076 Status::say(dict['op' => 'all_done', 'results' => $results]);
4077 return $return_value;
4078 } else if (!$return_value) {
4079 print_success($tests, $results, $options);
4080 } else {
4081 print_failure($argv, $results, $options);
4084 Status::sayColor("\nTotal time for all executed tests as run: ",
4085 Status::BLUE,
4086 sprintf("%.2fs\n",
4087 Status::getOverallEndTime() -
4088 Status::getOverallStartTime()));
4089 Status::sayColor("Total time for all executed tests if run serially: ",
4090 Status::BLUE,
4091 sprintf("%.2fs\n",
4092 Status::addTestTimesSerial($results)));
4094 return $return_value;
4097 <<__EntryPoint>>
4098 function run_main(): void {
4099 exit(main(get_argv()));
4102 // NOTE: Inline ASCII art moved to end-of-file to avoid confusing emacs.
4104 function print_clown(): void {
4105 print <<<CLOWN
4108 /*\\
4109 /_*_\\
4110 {('o')}
4111 C{{([^*^])}}D
4112 [ * ]
4113 / Y \\
4114 _\\__|__/_
4115 (___/ \\___)
4116 CLOWN
4117 ."\n\n";
4120 function print_skipper(): void {
4121 print <<<SKIPPER
4125 / ,"
4126 .-------.--- /
4127 "._ __.-/ o. o\
4128 " ( Y )
4132 .-" |
4133 / _ \ \
4134 / `. ". ) /' )
4135 Y )( / /(,/
4136 ,| / )
4137 ( | / /
4138 " \_ (__ (__
4139 "-._,)--._,)
4140 SKIPPER
4141 ."\n\n";
4144 function print_ship(): void {
4145 print <<<SHIP
4146 | | |
4147 )_) )_) )_)
4148 )___))___))___)\
4149 )____)____)_____)\\
4150 _____|____|____|____\\\__
4151 ---------\ SHIP IT /---------
4152 ^^^^^ ^^^^^^^^^^^^^^^^^^^^^
4153 ^^^^ ^^^^ ^^^ ^^
4154 ^^^^ ^^^
4155 SHIP
4156 ."\n";