Updating submodules
[hiphop-php.git] / hphp / test / run.php
blobe6a8e9c5a0094d0818d02f419b3f259ad96b839e
1 <?hh
2 /**
3 * Run the test suites in various configurations.
4 */
6 use namespace HH\Lib\C;
8 const int TIMEOUT_SECONDS = 300;
10 function get_argv(): vec<string> {
11 return \HH\FIXME\UNSAFE_CAST<vec<mixed>,vec<string>>(
12 \HH\global_get('argv') as vec<_>
16 function mtime(): float {
17 return microtime(true) as float;
20 // The "HPHP_HOME" environment variable can be set (to ".../fbcode"), to
21 // define "hphp_home()" and (indirectly) "test_dir()". Otherwise, we will use
22 // "__DIR__" as "test_dir()", and its grandparent directory for "hphp_home()"
23 // (unless we are testing a dso extensions).
25 <<__Memoize>>
26 function is_testing_dso_extension(): bool {
27 $home = getenv("HPHP_HOME");
28 if ($home is string) {
29 return false;
31 // detecting if we're running outside of the hhvm codebase.
32 return !is_file(__DIR__."/../../hphp/test/run.php");
35 <<__Memoize>>
36 function hphp_home(): string {
37 $home = getenv("HPHP_HOME");
38 if ($home is string) {
39 return realpath($home);
41 if (is_testing_dso_extension()) {
42 return realpath(__DIR__);
44 return realpath(__DIR__."/../..");
47 <<__Memoize>>
48 function test_dir(): string {
49 $home = getenv("HPHP_HOME");
50 if ($home is string) {
51 return realpath($home)."/hphp/test";
53 return __DIR__;
56 function get_expect_file_and_type(
57 string $test,
58 Options $options,
59 ): vec<?string> {
60 $types = vec[
61 'expect',
62 'expectf',
63 'expectregex',
64 'hhvm.expect',
65 'hhvm.expectf',
67 if ($options->repo) {
68 if (file_exists($test . '.hphpc_assert')) {
69 return vec[$test . '.hphpc_assert', 'expectf'];
71 if (file_exists($test . '.hhbbc_assert')) {
72 return vec[$test . '.hhbbc_assert', 'expectf'];
74 foreach ($types as $type) {
75 $fname = "$test.$type-repo";
76 if (file_exists($fname)) {
77 return vec[$fname, $type];
82 foreach ($types as $type) {
83 $fname = "$test.$type";
84 if (file_exists($fname)) {
85 return vec[$fname, $type];
88 return vec[null, null];
91 function multi_request_modes(Options $options): vec<string> {
92 $r = vec[];
93 if ($options->retranslate_all is nonnull) $r []= 'retranslate-all';
94 if ($options->recycle_tc is nonnull) $r []= 'recycle-tc';
95 if ($options->jit_serialize is nonnull) $r []= 'jit-serialize';
96 if ($options->cli_server) $r []= 'cli-server';
97 return $r;
100 function has_multi_request_mode(Options $options): bool {
101 return count(multi_request_modes($options)) != 0;
104 function test_repo(Options $options, string $test): string {
105 if ($options->repo_out is nonnull) {
106 return $options->repo_out . '/' . str_replace('/', '.', $test) . '.repo';
108 return Status::getTestTmpPath($test, 'repo');
111 function jit_serialize_option(
112 string $cmd, string $test, Options $options, bool $serialize,
113 ): string {
114 $serialized = test_repo($options, $test) . "/jit.dump";
115 $cmds = explode(' -- ', $cmd, 2);
116 $jit_serialize = (int)($options->jit_serialize ?? 0);
117 $cmds[0] .=
118 ' --count=' . ($serialize ? $jit_serialize + 1 : 1) .
119 " -vEval.JitSerdesFile=\"" . $serialized . "\"" .
120 " -vEval.JitSerdesMode=" . ($serialize ? 'Serialize' : 'DeserializeOrFail') .
121 ($serialize ? " -vEval.JitSerializeOptProfRequests=" . $jit_serialize : '');
122 if ($options->jitsample is nonnull && $serialize) {
123 $cmds[0] .= ' -vDeploymentId="' . $options->jitsample . '-serialize"';
125 return implode(' -- ', $cmds);
128 function usage(): string {
129 $argv = get_argv();
130 return "usage: {$argv[0]} [-m jit|interp] [-r] <test/directories>";
133 function help(): string {
134 $argv = get_argv();
135 $ztestexample = 'test/zend/good/*/*z*.php'; // sep. for syntax highlighting.
136 $help = <<<EOT
139 This is the hhvm test-suite runner. For more detailed documentation,
140 see hphp/test/README.md.
142 The test argument may be a path to a php test file, a directory name, or
143 one of a few pre-defined suite names that this script knows about.
145 If you work with hhvm a lot, you might consider a bash alias:
147 alias htest="path/to/hphp/test/run"
149 Examples:
151 # Quick tests in JIT mode:
152 % {$argv[0]} test/quick
154 # Slow tests in interp mode:
155 % {$argv[0]} -m interp test/slow
157 # PHP specification tests in JIT mode:
158 % {$argv[0]} test/slow/spec
160 # Slow closure tests in JIT mode:
161 % {$argv[0]} test/slow/closure
163 # Slow closure tests in JIT mode with RepoAuthoritative:
164 % {$argv[0]} -r test/slow/closure
166 # Slow array tests, in RepoAuthoritative:
167 % {$argv[0]} -r test/slow/array
169 # Zend tests with a "z" in their name:
170 % {$argv[0]} $ztestexample
172 # Quick tests in JIT mode with some extra runtime options:
173 % {$argv[0]} test/quick -a '-vEval.JitMaxTranslations=120 -vEval.HHIRRefcountOpts=0'
175 # Quick tests in JIT mode with RepoAuthoritative and an extra compile-time option:
176 % {$argv[0]} test/quick -r --compiler-args '--log=4'
178 # All quick tests except debugger
179 % {$argv[0]} -e debugger test/quick
181 # All tests except those containing a string of 3 digits
182 % {$argv[0]} -E '/\d{3}/' all
184 # All tests whose name containing pdo_mysql
185 % {$argv[0]} -i pdo_mysql -m jit -r zend
187 # Print all the standard tests
188 % {$argv[0]} --list-tests
190 # Use a specific HHVM binary
191 % {$argv[0]} -b ~/code/hhvm/hphp/hhvm/hhvm
192 % {$argv[0]} --hhvm-binary-path ~/code/hhvm/hphp/hhvm/hhvm
194 # Use retranslate all. Run the test n times, then run retranslate all, then
195 # run the test n more on the new code.
196 % {$argv[0]} --retranslate-all 2 quick
198 # Use jit-serialize. Run the test n times, then run retranslate all, run the
199 # test once more, serialize all profile data, and then restart hhvm, load the
200 # serialized state and run retranslate-all before starting the test.
201 % {$argv[0]} --jit-serialize 2 -r quick
202 EOT;
204 return usage().$help;
207 function error(string $message): noreturn {
208 print "$message\n";
209 exit(1);
212 // If a user-supplied path is provided, let's make sure we have a valid
213 // executable. Returns canonicanalized path or exits.
214 function check_executable(string $path): string {
215 $rpath = realpath($path);
216 if ($rpath === false || !is_executable($rpath)) {
217 error("Provided HHVM executable ($path) is not an executable file.\n" .
218 "If using HHVM_BIN, make sure that is set correctly.");
221 $output = vec[];
222 $return_var = -1;
223 exec($rpath . " --version 2> /dev/null", inout $output, inout $return_var);
224 if (strpos(implode("", $output), "HipHop ") !== 0) {
225 error("Provided file ($rpath) is not an HHVM executable.\n" .
226 "If using HHVM_BIN, make sure that is set correctly.");
229 return $rpath;
232 function hhvm_binary_routes(): dict<string, string> {
233 return dict[
234 "buck" => "/buck-out/gen/hphp/hhvm/hhvm",
235 "buck2" => "/../buck-out/v2/gen/fbcode/hphp/hhvm/out",
236 "cmake" => "/hphp/hhvm"
240 function hh_codegen_binary_routes(): dict<string, string> {
241 return dict[
242 "buck" => "/buck-out/bin/hphp/hack/src/hh_single_compile",
243 "cmake" => "/hphp/hack/bin"
247 // For Facebook: We have several build systems, and we can use any of them in
248 // the same code repo. If multiple binaries exist, we want the onus to be on
249 // the user to specify a particular one because before we chose the buck one
250 // by default and that could cause unexpected results.
251 function check_for_multiple_default_binaries(): void {
252 // Env var we use in testing that'll pick which build system to use.
253 if (getenv("FBCODE_BUILD_TOOL") !== false) {
254 return;
257 $home = hphp_home();
258 $found = vec[];
259 foreach (hhvm_binary_routes() as $path) {
260 $abs_path = $home . $path . "/hhvm";
261 if (file_exists($abs_path)) {
262 $found[] = $abs_path;
266 if (count($found) <= 1) {
267 return;
270 $msg = "Multiple binaries exist in this repo. \n";
271 foreach ($found as $bin) {
272 $msg .= " - " . $bin . "\n";
274 $msg .= "Are you in fbcode? If so, remove a binary \n"
275 . "or use the --hhvm-binary-path option to the test runner. \n"
276 . "e.g. test/run --hhvm-binary-path /path/to/binary slow\n";
277 error($msg);
280 function hhvm_path(): string {
281 $file = "";
282 $hhvm_bin = getenv("HHVM_BIN");
283 if ($hhvm_bin is string) {
284 $file = realpath($hhvm_bin);
285 } else {
286 $file = bin_root().'/hhvm';
289 if (!is_file($file)) {
290 if (is_testing_dso_extension()) {
291 $output = null;
292 $return_var = -1;
293 exec("which hhvm 2> /dev/null", inout $output, inout $return_var);
294 if (isset($output[0]) && $output[0]) {
295 return $output[0];
297 error("You need to specify hhvm bin with env HHVM_BIN");
300 error("$file doesn't exist. Did you forget to build first?");
302 return rel_path($file);
305 function bin_root(): string {
306 $hhvm_bin = getenv("HHVM_BIN");
307 if ($hhvm_bin is string) {
308 return dirname(realpath($hhvm_bin));
311 $home = hphp_home();
312 $env_tool = getenv("FBCODE_BUILD_TOOL");
313 $routes = hhvm_binary_routes();
315 if ($env_tool !== false) {
316 return $home . $routes[$env_tool];
319 foreach ($routes as $_ => $path) {
320 $dir = $home . $path;
321 if (is_dir($dir)) {
322 return $dir;
326 return $home . $routes["cmake"];
329 function unit_cache_file(): string {
330 return Status::getTmpPathFile('unit-cache.sql');
333 function read_opts_file(?string $file): string {
334 if ($file is null || !file_exists($file)) {
335 return "";
337 $fp = fopen($file, "r");
338 invariant($fp is resource, "%s", __METHOD__);
340 $contents = "";
341 for ($line = fgets($fp); $line; $line = fgets($fp)) {
342 // Compress out white space.
343 $line = preg_replace('/\s+/', ' ', $line);
345 // Discard simple line oriented ; and # comments to end of line
346 // Comments at end of line (after payload) are not allowed.
347 $line = preg_replace('/^ *;.*$/', ' ', $line);
348 $line = preg_replace('/^ *#.*$/', ' ', $line);
350 // Substitute in the directory name
351 $line = str_replace('__DIR__', dirname($file), $line);
353 $contents .= $line;
355 fclose($fp);
356 return $contents;
359 // http://stackoverflow.com/questions/2637945/
360 function rel_path(string $to): string {
361 $from = explode('/', getcwd().'/');
362 $to = explode('/', $to);
363 $from_len = count($from);
364 $to_len = count($to);
366 // find first non-matching dir.
367 for ($d = 0; $d < $from_len; ++$d) {
368 if ($d >= $to_len || $from[$d] !== $to[$d])
369 break;
372 $relPath = vec[];
374 // get number of remaining dirs in $from.
375 $remaining = $from_len - $d - 1;
376 if ($remaining > 0) {
377 // add traversals up to first matching dir.
378 do {
379 $relPath[] = '..';
380 $remaining--;
381 } while ($remaining > 0);
382 } else {
383 $relPath[] = '.';
385 while ($d < $to_len) {
386 $relPath[] = $to[$d];
387 $d++;
389 return implode('/', $relPath);
392 // Keep this in sync with the dict in get_options() below.
393 // Options taking a value (with a trailing `:` in the dict key)
394 // should be ?string. Otherwise they should be bool.
395 final class Options {
396 public ?string $env;
397 public ?string $exclude;
398 public ?string $exclude_pattern;
399 public ?string $exclude_recorded_failures;
400 public ?string $include;
401 public ?string $include_pattern;
402 public bool $repo = false;
403 public bool $split_hphpc = false;
404 public bool $repo_single = false;
405 public bool $repo_separate = false;
406 public ?string $repo_threads;
407 public ?string $repo_out;
408 public bool $hhbbc2 = false;
409 public ?string $mode;
410 public bool $server = false;
411 public bool $cli_server = false;
412 public bool $shuffle = false;
413 public bool $help = false;
414 public bool $verbose = false;
415 public bool $testpilot = false;
416 public ?string $threads;
417 public ?string $args;
418 public ?string $compiler_args;
419 public bool $log = false;
420 public ?string $failure_file;
421 public bool $wholecfg = false;
422 public bool $hhas_round_trip = false;
423 public bool $verify_hackc_translator = false;
424 public bool $color = false;
425 public bool $no_fun = false;
426 public bool $no_skipif = false;
427 public bool $cores = false;
428 public bool $dump_tc = false;
429 public bool $no_clean = false;
430 public bool $list_tests = false;
431 public ?string $recycle_tc;
432 public ?string $retranslate_all;
433 public ?string $jit_serialize;
434 public ?string $hhvm_binary_path;
435 public ?string $working_dir;
436 public ?string $vendor;
437 public ?string $record_failures;
438 public ?string $ignore_oids;
439 public ?string $jitsample;
440 public ?string $hh_single_type_check;
441 public bool $write_to_checkout = false;
442 public bool $bespoke = false;
444 // Additional state added for convenience since Options is plumbed
445 // around almost everywhere.
446 public ?Servers $servers = null;
449 function get_options(
450 vec<string> $argv,
451 ): (Options, vec<string>) {
452 // Options marked * affect test behavior, and need to be reported by list_tests.
453 // Options with a trailing : take a value.
454 $parameters = dict[
455 '*env:' => '',
456 'exclude:' => 'e:',
457 'exclude-pattern:' => 'E:',
458 'exclude-recorded-failures:' => 'x:',
459 'include:' => 'i:',
460 'include-pattern:' => 'I:',
461 '*repo' => 'r',
462 '*split-hphpc' => '',
463 '*repo-single' => '',
464 '*repo-separate' => '',
465 '*repo-threads:' => '',
466 '*repo-out:' => '',
467 '*hhbbc2' => '',
468 '*mode:' => 'm:',
469 '*server' => 's',
470 '*cli-server' => 'S',
471 'shuffle' => '',
472 'help' => 'h',
473 'verbose' => 'v',
474 'testpilot' => '',
475 'threads:' => '',
476 '*args:' => 'a:',
477 '*compiler-args:' => '',
478 'log' => 'l',
479 'failure-file:' => '',
480 '*wholecfg' => '',
481 '*hhas-round-trip' => '',
482 '*verify-hackc-translator' => '',
483 'color' => 'c',
484 'no-fun' => '',
485 'no-skipif' => '',
486 'cores' => '',
487 'dump-tc' => '',
488 'no-clean' => '',
489 'list-tests' => '',
490 '*recycle-tc:' => '',
491 '*retranslate-all:' => '',
492 '*jit-serialize:' => '',
493 '*hhvm-binary-path:' => 'b:',
494 '*working-dir:' => 'w:',
495 '*vendor:' => '',
496 'record-failures:' => '',
497 '*ignore-oids' => '',
498 'jitsample:' => '',
499 '*hh_single_type_check:' => '',
500 'write-to-checkout' => '',
501 'bespoke' => '',
503 $options = new Options() as dynamic;
504 $files = vec[];
505 $recorded = vec[];
508 * '-' argument causes all future arguments to be treated as filenames, even
509 * if they would otherwise match a valid option. Otherwise, arguments starting
510 * with '-' MUST match a valid option.
512 $force_file = false;
514 for ($i = 1; $i < count($argv); $i++) {
515 $arg = $argv[$i];
517 if (strlen($arg) === 0) {
518 continue;
519 } else if ($force_file) {
520 $files[] = $arg;
521 } else if ($arg === '-') {
522 $forcefile = true;
523 } else if ($arg[0] === '-') {
524 $found = false;
526 foreach ($parameters as $long => $short) {
527 if ($arg == '-'.str_replace(':', '', $short) ||
528 $arg == '--'.str_replace(vec[':', '*'], vec['', ''], $long)) {
529 $record = substr($long, 0, 1) === '*';
530 if ($record) $recorded[] = $arg;
531 if (substr($long, -1, 1) === ':') {
532 $i++;
533 $value = $argv[$i];
534 if ($record) $recorded[] = $value;
535 } else {
536 $value = true;
538 $name = str_replace(vec[':', '*', '-'], vec['', '', '_'], $long);
539 $options->{$name} = $value;
540 $found = true;
541 break;
545 if (!$found) {
546 $msg = sprintf("Invalid argument: '%s'\nSee %s --help", $arg, $argv[0]);
547 error($msg as string);
549 } else {
550 $files[] = $arg;
553 $options = $options as Options;
555 \HH\global_set('recorded_options', $recorded);
557 $repo_out = $options->repo_out;
558 if ($repo_out is string && !is_dir($repo_out)) {
559 if (!mkdir($repo_out) && !is_dir($repo_out)) {
560 error("Unable to create repo-out dir " . $repo_out);
563 if ($options->hhbbc2) {
564 $options->repo_separate = true;
565 if ($options->repo || $options->repo_single) {
566 error("repo-single/repo and hhbbc2 are mutually exclusive options");
568 if (isset($options['mode'])) {
569 error("hhbbc2 doesn't support modes; it compares hhas, doesn't run code");
573 if ($options->repo_single || $options->repo_separate) {
574 $options->repo = true;
575 } else if ($options->repo) {
576 // if only repo was set, then it means repo single
577 $options->repo_single = true;
580 if ($options->jit_serialize is nonnull) {
581 if (!$options->repo) {
582 error("jit-serialize only works in repo mode");
584 if ($options->mode is nonnull && $options->mode !== 'jit') {
585 error("jit-serialize only works in jit mode");
589 if ($options->split_hphpc) {
590 if (!$options->repo) {
591 error("split-hphpc only works in repo mode");
593 if (!$options->repo_separate) {
594 error("split-hphpc only works in repo-separate mode");
598 if ($options->repo && $options->hhas_round_trip) {
599 error("repo and hhas-round-trip are mutually exclusive options");
602 $multi_request_modes = multi_request_modes($options);
603 if (count($multi_request_modes) > 1) {
604 error("The options\n -" . implode("\n -", $multi_request_modes) .
605 "\nare mutually exclusive options");
608 if ($options->write_to_checkout) {
609 Status::$write_to_checkout = true;
612 return tuple($options, $files);
616 * Return the path to $test relative to $base, or false if $base does not
617 * contain test.
619 function canonical_path_from_base(string $test, string $base): mixed {
620 $full = realpath($test);
621 if (substr($full, 0, strlen($base)) === $base) {
622 return substr($full, strlen($base) + 1);
624 $dirstat = stat($base);
625 if (!is_dict($dirstat)) return false;
626 for ($p = dirname($full); $p && $p !== "/"; $p = dirname($p)) {
627 $s = stat($p);
628 if (!is_dict($s)) continue;
629 if ($s['ino'] === $dirstat['ino'] && $s['dev'] === $dirstat['dev']) {
630 return substr($full, strlen($p) + 1);
633 return false;
636 function canonical_path(string $test): mixed {
637 $attempt = canonical_path_from_base($test, test_dir());
638 if ($attempt === false) {
639 return canonical_path_from_base($test, hphp_home());
640 } else {
641 return $attempt;
646 * We support some 'special' file names, that just know where the test
647 * suites are, to avoid typing 'hphp/test/foo'.
649 function find_test_files(string $file): vec<string>{
650 $mappage = dict[
651 'quick' => 'hphp/test/quick',
652 'slow' => 'hphp/test/slow',
653 'debugger' => 'hphp/test/server/debugger/tests',
654 'http' => 'hphp/test/server/http/tests',
655 'fastcgi' => 'hphp/test/server/fastcgi/tests',
656 'zend' => 'hphp/test/zend/good',
657 'facebook' => 'hphp/facebook/test',
658 'taint' => 'hphp/test/taint',
660 // subset of slow we run with CLI server too
661 'slow_ext_hsl' => 'hphp/test/slow/ext_hsl',
663 // Subsets of zend tests.
664 'zend_ext' => 'hphp/test/zend/good/ext',
665 'zend_ext_am' => 'hphp/test/zend/good/ext/[a-m]*',
666 'zend_ext_nz' => 'hphp/test/zend/good/ext/[n-z]*',
667 'zend_Zend' => 'hphp/test/zend/good/Zend',
668 'zend_tests' => 'hphp/test/zend/good/tests',
671 $pattern = $mappage[$file] ?? null;
672 if ($pattern is nonnull) {
673 $pattern = hphp_home().'/'.$pattern;
674 $matches = glob($pattern);
675 if (count($matches) === 0) {
676 error(
677 "Convenience test name '$file' is recognized but does not match ".
678 "any test files (pattern = '$pattern')",
681 return $matches;
684 return vec[$file];
687 // Some tests have to be run together in the same test bucket, serially, one
688 // after other in order to avoid races and other collisions.
689 function serial_only_tests(vec<string> $tests): vec<string> {
690 if (is_testing_dso_extension()) {
691 return vec[];
693 // Add a <testname>.php.serial file to make your test run in the serial
694 // bucket.
695 $serial_tests = vec(array_filter(
696 $tests,
697 function($test) {
698 return file_exists($test . '.serial');
701 return $serial_tests;
704 // If "files" is very long, then the shell may reject the desired
705 // "find" command (especially because "escapeshellarg()" adds two single
706 // quote characters to each file), so we split "files" into chunks below.
707 function exec_find(vec<string> $files, string $extra): vec<string> {
708 $results = vec[];
709 foreach (array_chunk($files, 500) as $chunk) {
710 $efa = implode(' ', array_map(
711 $line ==> escapeshellarg($line as string),
712 $chunk as dict<_, _>,
714 $output = shell_exec("find $efa $extra");
715 foreach (explode("\n", $output) as $result) {
716 // Collect the (non-empty) results, which should all be file paths.
717 if ($result !== "") $results[] = $result;
720 return $results;
723 function is_facebook_build(Options $options): bool {
724 if (!is_dir(hphp_home() . '/hphp/facebook/test')) return false;
726 // We want to test for the presence of an extension in the build, so turn off
727 // a bunch of features in order to do this as simply and reliably as possible.
728 $simplified_options = clone $options;
729 $simplified_options->repo = false;
730 $simplified_options->server = false;
731 $simplified_options->cli_server = false;
732 // Use a bogus test name so we don't find any config overrides
733 $result = runif_extension_matches(
734 $simplified_options,
735 'not_a_real_test.php',
736 vec['facebook'],
738 if (!$result['valid']) {
739 invariant(Shapes::keyExists($result, 'error'), 'RunifResult contract');
740 invariant_violation(
741 "is_facebook_build is calling runif_extension_matches incorrectly: %s",
742 $result['error'],
745 invariant(Shapes::keyExists($result, 'match'), 'RunifResult contract');
746 return $result['match'];
749 function find_tests(
750 vec<string> $files,
751 Options $options,
752 ): vec<string> {
753 if (!$files) {
754 $files = vec['quick'];
756 if ($files == vec['all']) {
757 $files = vec['quick', 'slow', 'zend', 'fastcgi', 'http', 'debugger'];
758 if (is_facebook_build($options)) {
759 $files[] = 'facebook';
762 $ft = vec[];
763 foreach ($files as $file) {
764 $ft = array_merge($ft, find_test_files($file));
766 $files = vec[];
767 foreach ($ft as $file) {
768 if (!@stat($file)) {
769 error("Not valid file or directory: '$file'");
771 $file = preg_replace(',//+,', '/', realpath($file));
772 $file = preg_replace(',^'.getcwd().'/,', '', $file);
773 $files[] = $file;
775 $tests = exec_find(
776 $files,
777 "'(' " .
778 "-name '*.php' " .
779 "-o -name '*.hack' " .
780 "-o -name '*.hackpartial' " .
781 "-o -name '*.hhas' " .
782 "-o -name '*.php.type-errors' " .
783 "-o -name '*.hack.type-errors' " .
784 "-o -name '*.hackpartial.type-errors' " .
785 "')' " .
786 "-not -regex '.*round_trip[.]hhas'"
788 if (!$tests) {
789 error("Could not find any tests associated with your options.\n" .
790 "Make sure your test path is correct and that you have " .
791 "the right expect files for the tests you are trying to run.\n" .
792 usage());
794 asort(inout $tests);
795 $tests = vec(array_filter($tests));
796 if ($options->exclude is nonnull) {
797 $exclude = $options->exclude;
798 $tests = vec(array_filter($tests, function($test) use ($exclude) {
799 return (false === strpos($test, $exclude));
800 }));
802 if ($options->exclude_pattern is nonnull) {
803 $exclude = $options->exclude_pattern;
804 $tests = vec(array_filter($tests, function($test) use ($exclude) {
805 return !preg_match($exclude, $test);
806 }));
808 if ($options->exclude_recorded_failures is nonnull) {
809 $exclude_file = $options->exclude_recorded_failures;
810 $exclude = file($exclude_file, FILE_IGNORE_NEW_LINES);
811 $tests = vec(array_filter($tests, function($test) use ($exclude) {
812 return (false === in_array(canonical_path($test), $exclude));
813 }));
815 if ($options->include is nonnull) {
816 $include = $options->include;
817 $tests = vec(array_filter($tests, function($test) use ($include) {
818 return (false !== strpos($test, $include));
819 }));
821 if ($options->include_pattern is nonnull) {
822 $include = $options->include_pattern;
823 $tests = vec(array_filter($tests, function($test) use ($include) {
824 return (bool)preg_match($include, $test);
825 }));
827 return $tests;
830 function list_tests(vec<string> $files, Options $options): void {
831 $args = implode(' ', \HH\global_get('recorded_options'));
833 // Disable escaping of test info when listing. We check if the environment
834 // variable is set so we can make the change in a backwards compatible way.
835 $escape_info = getenv("LISTING_NO_ESCAPE") === false;
837 foreach (find_tests($files, $options) as $test) {
838 $test_info = Status::jsonEncode(
839 dict['args' => $args, 'name' => $test],
841 if ($escape_info) {
842 print str_replace('\\', '\\\\', $test_info)."\n";
843 } else {
844 print $test_info."\n";
849 function find_test_ext(
850 string $test,
851 string $ext,
852 string $configName='config',
853 ): ?string {
854 if (is_file("{$test}.{$ext}")) {
855 return "{$test}.{$ext}";
857 return find_file_for_dir(dirname($test), "{$configName}.{$ext}");
860 function find_file_for_dir(string $dir, string $name): ?string {
861 // Handle the case where the $dir might come in as '.' because you
862 // are running the test runner on a file from the same directory as
863 // the test e.g., './mytest.php'. dirname() will give you the '.' when
864 // you actually have a lot of path to traverse upwards like
865 // /home/you/code/tests/mytest.php. Use realpath() to get that.
866 $dir = realpath($dir);
867 while ($dir !== '/' && is_dir($dir)) {
868 $file = "$dir/$name";
869 if (is_file($file)) {
870 return $file;
872 $dir = dirname($dir);
874 $file = test_dir().'/'.$name;
875 if (file_exists($file)) {
876 return $file;
878 return null;
881 function find_debug_config(string $test, string $name): string {
882 $debug_config = find_file_for_dir(dirname($test), $name);
883 if ($debug_config is nonnull) {
884 return "-m debug --debug-config ".$debug_config;
886 return "";
889 function mode_cmd(Options $options): vec<string> {
890 $repo_args = '';
891 if (!$options->repo) {
892 $repo_args = "-vUnitFileCache.Path=".unit_cache_file();
894 $interp_args = "$repo_args -vEval.Jit=0";
895 $jit_args = "$repo_args -vEval.Jit=true";
896 $mode = $options->mode ?? '';
897 switch ($mode) {
898 case '':
899 case 'jit':
900 return vec[$jit_args];
901 case 'interp':
902 return vec[$interp_args];
903 case 'interp,jit':
904 return vec[$interp_args, $jit_args];
905 default:
906 error("-m must be one of jit | interp | interp,jit. Got: '$mode'");
910 function extra_args(Options $options): string {
911 $args = $options->args ?? '';
913 if ($options->vendor is nonnull) {
914 $args .= ' -d auto_prepend_file=';
915 $args .= escapeshellarg($options->vendor.'/hh_autoload.php');
918 return $args;
921 function extra_compiler_args(Options $options): string {
922 return $options->compiler_args ?? '';
925 function hhvm_cmd_impl(
926 Options $options,
927 string $config,
928 ?string $autoload_db_prefix,
929 string ...$extra_args
930 ): vec<string> {
931 $cmds = vec[];
932 foreach (mode_cmd($options) as $mode_num => $mode) {
933 $args = vec[
934 hhvm_path(),
935 '-c',
936 $config,
937 // EnableArgsInBacktraces disables most of HHBBC's DCE optimizations.
938 // In order to test those optimizations (which are part of a normal prod
939 // configuration) we turn this flag off by default.
940 '-vEval.EnableArgsInBacktraces=false',
941 '-vEval.EnableIntrinsicsExtension=true',
942 '-vEval.HHIRInliningIgnoreHints=false',
943 '-vEval.HHIRAlwaysInterpIgnoreHint=false',
944 '-vEval.FoldLazyClassKeys=false',
945 '-vEval.EnableLogBridge=false',
946 $mode,
947 $options->wholecfg ? '-vEval.JitPGORegionSelector=wholecfg' : '',
949 // load/store counters don't work on Ivy Bridge so disable for tests
950 '-vEval.ProfileHWEnable=false',
952 // use a fixed path for embedded data
953 '-vEval.EmbeddedDataExtractPath='
954 .escapeshellarg(bin_root().'/hhvm_%{type}_%{buildid}'),
956 // Stick to a single thread for retranslate-all
957 '-vEval.JitWorkerThreads=1',
958 '-vEval.JitWorkerThreadsForSerdes=1',
960 extra_args($options),
963 if ($autoload_db_prefix is nonnull) {
964 $args[] =
965 '-vAutoload.DB.Path='.escapeshellarg("$autoload_db_prefix.$mode_num");
968 if ($options->retranslate_all is nonnull) {
969 $args[] = '--count='.((int)$options->retranslate_all * 2);
970 $args[] = '-vEval.JitPGO=true';
971 $args[] = '-vEval.JitRetranslateAllRequest='.$options->retranslate_all;
972 // Set to timeout. We want requests to trigger retranslate all.
973 $args[] = '-vEval.JitRetranslateAllSeconds=' . TIMEOUT_SECONDS;
976 if ($options->recycle_tc is nonnull) {
977 $args[] = '--count='.$options->recycle_tc;
978 $args[] = '-vEval.StressUnitCacheFreq=1';
979 $args[] = '-vEval.EnableReusableTC=true';
982 if ($options->jit_serialize is nonnull) {
983 $args[] = '-vEval.JitPGO=true';
984 $args[] = '-vEval.JitRetranslateAllRequest='.$options->jit_serialize;
985 // Set to timeout. We want requests to trigger retranslate all.
986 $args[] = '-vEval.JitRetranslateAllSeconds=' . TIMEOUT_SECONDS;
989 if ($options->hhas_round_trip) {
990 $args[] = '-vEval.AllowHhas=1';
991 $args[] = '-vEval.LoadFilepathFromUnitCache=1';
994 if ($options->verify_hackc_translator) {
995 $args[] = '-vEval.VerifyTranslateHackC=1';
998 if (!$options->cores) {
999 $args[] = '-vResourceLimit.CoreFileSize=0';
1002 if ($options->dump_tc) {
1003 $args[] = '-vEval.DumpIR=2';
1004 $args[] = '-vEval.DumpTC=1';
1007 if ($options->hh_single_type_check is nonnull) {
1008 $args[] = '--hh_single_type_check='.$options->hh_single_type_check;
1011 if ($options->bespoke) {
1012 $args[] = '-vEval.BespokeArrayLikeMode=1';
1013 $args[] = '-vServer.APC.MemModelTreadmill=true';
1016 $cmds[] = implode(' ', array_merge($args, $extra_args));
1018 return $cmds;
1021 function repo_separate(Options $options, string $test): bool {
1022 return $options->repo_separate &&
1023 !file_exists($test . ".hhbbc_opts");
1026 // Return the command and the env to run it in.
1027 function hhvm_cmd(
1028 Options $options,
1029 string $test,
1030 ?string $test_run = null,
1031 bool $is_temp_file = false
1032 ): (vec<string>, dict<string, mixed>) {
1033 $test_run ??= $test;
1034 // hdf support is only temporary until we fully migrate to ini
1035 // Discourage broad use.
1036 $hdf_suffix = ".use.for.ini.migration.testing.only.hdf";
1037 $hdf = file_exists($test.$hdf_suffix)
1038 ? '-c ' . $test . $hdf_suffix
1039 : "";
1040 $extra_opts = read_opts_file(find_test_ext($test, 'opts'));
1041 $config = find_test_ext($test, 'ini');
1042 invariant($config is nonnull, "%s", __METHOD__);
1043 $cmds = hhvm_cmd_impl(
1044 $options,
1045 $config,
1046 Status::getTestTmpPath($test, 'autoloadDB'),
1047 $hdf,
1048 find_debug_config($test, 'hphpd.ini'),
1049 $extra_opts,
1050 $is_temp_file ? " --temp-file" : "",
1051 '--file',
1052 escapeshellarg($test_run),
1055 $cmd = "";
1057 if (file_exists($test.'.verify')) {
1058 $cmd .= " -m verify";
1061 if ($options->cli_server) {
1062 $config = find_file_for_dir(dirname($test), 'config.ini');
1063 $servers = $options->servers as Servers;
1064 $server = $servers->configs[$config ?? ''];
1065 $socket = $server->cli_socket;
1066 $cmd .= ' -vEval.UseRemoteUnixServer=only';
1067 $cmd .= ' -vEval.UnixServerPath='.$socket;
1068 $cmd .= ' --count=3';
1071 // Special support for tests that require a path to the current
1072 // test directory for things like prepend_file and append_file
1073 // testing.
1074 if (file_exists($test.'.ini')) {
1075 $contents = file_get_contents($test.'.ini');
1076 if (strpos($contents, '{PWD}') !== false) {
1077 $test_ini = tempnam('/tmp', $test).'.ini';
1078 file_put_contents($test_ini,
1079 str_replace('{PWD}', dirname($test), $contents));
1080 $cmd .= " -c $test_ini";
1083 if ($hdf !== "") {
1084 $contents = file_get_contents($test.$hdf_suffix);
1085 if (strpos($contents, '{PWD}') !== false) {
1086 $test_hdf = tempnam('/tmp', $test).$hdf_suffix;
1087 file_put_contents($test_hdf,
1088 str_replace('{PWD}', dirname($test), $contents));
1089 $cmd .= " -c $test_hdf";
1093 if ($options->repo) {
1094 $repo_suffix = repo_separate($options, $test) ? 'hhbbc' : 'hhbc';
1096 $program = "hhvm";
1097 $hhbbc_repo =
1098 "\"" . test_repo($options, $test) . "/hhvm.$repo_suffix\"";
1099 $cmd .= ' -vRepo.Authoritative=true';
1100 $cmd .= " -vRepo.Path=$hhbbc_repo";
1103 if ($options->jitsample is nonnull) {
1104 $cmd .= ' -vDeploymentId="' . $options->jitsample . '"';
1105 $cmd .= ' --instance-id="' . $test . '"';
1106 $cmd .= ' -vEval.JitSampleRate=1';
1107 $cmd .= " -vScribe.Tables.hhvm_jit.include.*=instance_id";
1108 $cmd .= " -vScribe.Tables.hhvm_jit.include.*=deployment_id";
1111 $env = \HH\FIXME\UNSAFE_CAST<dict<arraykey,mixed>,dict<string,mixed>>(
1112 \HH\global_get('_ENV') as dict<_, _>
1114 $env['LC_ALL'] = 'C';
1115 $env['INPUTRC'] = test_dir().'/inputrc';
1117 // Apply the --env option.
1118 if ($options->env is nonnull) {
1119 foreach (explode(",", $options->env) as $arg) {
1120 $i = strpos($arg, '=');
1121 if ($i) {
1122 $key = substr($arg, 0, $i);
1123 $val = substr($arg, $i + 1);
1124 $env[$key] = $val;
1125 } else {
1126 unset($env[$arg]);
1131 $in = find_test_ext($test, 'in');
1132 if ($in is nonnull) {
1133 $cmd .= ' < ' . escapeshellarg($in);
1134 // If we're piping the input into the command then setup a simple
1135 // dumb terminal so hhvm doesn't try to control it and pollute the
1136 // output with control characters, which could change depending on
1137 // a wide variety of terminal settings.
1138 $env["TERM"] = "dumb";
1141 foreach ($cmds as $idx => $_) {
1142 $cmds[$idx] .= $cmd;
1145 return tuple($cmds, $env);
1148 function hphp_cmd(
1149 Options $options,
1150 string $test,
1151 string $program,
1152 ): string {
1153 // Transform extra_args like "-vName=Value" into "-vRuntime.Name=Value".
1154 $extra_args =
1155 preg_replace("/(^-v|\s+-v)\s*/", "$1Runtime.", extra_args($options));
1157 $compiler_args = extra_compiler_args($options);
1159 $hdf_suffix = ".use.for.ini.migration.testing.only.hdf";
1160 $hdf = file_exists($test.$hdf_suffix)
1161 ? '-c ' . $test . $hdf_suffix
1162 : "";
1164 if ($hdf !== "") {
1165 $contents = file_get_contents($test.$hdf_suffix);
1166 if (strpos($contents, '{PWD}') !== false) {
1167 $test_hdf = tempnam('/tmp', $test).$hdf_suffix;
1168 file_put_contents($test_hdf,
1169 str_replace('{PWD}', dirname($test), $contents));
1170 $hdf = " -c $test_hdf";
1174 return implode(" ", vec[
1175 hphpc_path($options),
1176 '--hphp',
1177 '-vUseHHBBC='. (repo_separate($options, $test) ? 'false' : 'true'),
1178 '--config',
1179 find_test_ext($test, 'ini', 'hphp_config'),
1180 $hdf,
1181 '-vRuntime.ResourceLimit.CoreFileSize=0',
1182 '-vRuntime.Eval.EnableIntrinsicsExtension=true',
1183 // EnableArgsInBacktraces disables most of HHBBC's DCE optimizations.
1184 // In order to test those optimizations (which are part of a normal prod
1185 // configuration) we turn this flag off by default.
1186 '-vRuntime.Eval.EnableArgsInBacktraces=false',
1187 '-vRuntime.Eval.FoldLazyClassKeys=false',
1188 '-vRuntime.Eval.EnableLogBridge=false',
1189 '-vParserThreadCount=' . ($options->repo_threads ?? 1),
1190 '-l1',
1191 '-o "' . test_repo($options, $test) . '"',
1192 "\"$test\"",
1193 "-vExternWorker.WorkingDir=".Status::getTestTmpPath($test, 'work'),
1194 $extra_args,
1195 $compiler_args,
1196 read_opts_file(find_test_ext($test, 'hphp_opts')),
1200 function hphpc_path(Options $options): string {
1201 if ($options->split_hphpc) {
1202 $file = "";
1203 $file = bin_root().'/hphpc';
1205 if (!is_file($file)) {
1206 error("$file doesn't exist. Did you forget to build first?");
1208 return rel_path($file);
1209 } else {
1210 return hhvm_path();
1214 function hhbbc_cmd(
1215 Options $options, string $test, string $program,
1216 ): string {
1217 $test_repo = test_repo($options, $test);
1218 return implode(" ", vec[
1219 hphpc_path($options),
1220 '--hhbbc',
1221 '--no-logging',
1222 '--no-cores',
1223 '--parallel-num-threads=' . ($options->repo_threads ?? 1),
1224 '--parallel-final-threads=' . ($options->repo_threads ?? 1),
1225 '--extern-worker-working-dir=' . Status::getTestTmpPath($test, 'work'),
1226 read_opts_file("$test.hhbbc_opts"),
1227 "-o \"$test_repo/hhvm.hhbbc\" \"$test_repo/hhvm.hhbc\"",
1231 // Execute $cmd and return its output on failure, including any stacktrace.log
1232 // file it generated. Return null on success.
1233 function exec_with_stack(string $cmd): ?string {
1234 $pipes = null;
1235 $proc = proc_open($cmd,
1236 dict[0 => vec['pipe', 'r'],
1237 1 => vec['pipe', 'w'],
1238 2 => vec['pipe', 'w']], inout $pipes);
1239 $pipes as nonnull;
1240 fclose($pipes[0]);
1241 $s = '';
1242 $all_selects_failed=true;
1243 $end = mtime() + TIMEOUT_SECONDS;
1244 $timedout = false;
1245 while (true) {
1246 $now = mtime();
1247 if ($now >= $end) break;
1248 $read = vec[$pipes[1], $pipes[2]];
1249 $write = null;
1250 $except = null;
1251 $available = @stream_select(
1252 inout $read,
1253 inout $write,
1254 inout $except,
1255 (int)($end - $now),
1257 if ($available === false) {
1258 usleep(1000);
1259 $s .= "select failed:\n" . print_r(error_get_last(), true);
1260 continue;
1262 $all_selects_failed=false;
1263 if ($available === 0) continue;
1264 $read as nonnull;
1265 foreach ($read as $pipe) {
1266 $t = fread($pipe, 4096);
1267 if ($t === false) continue;
1268 $s .= $t;
1270 if (feof($pipes[1]) && feof($pipes[2])) break;
1272 fclose($pipes[1]);
1273 fclose($pipes[2]);
1274 while (true) {
1275 $status = proc_get_status($proc);
1276 if (!$status['running']) break;
1277 $now = mtime();
1278 if ($now >= $end) {
1279 $timedout = true;
1280 $output = null;
1281 $return_var = -1;
1282 exec('pkill -P ' . $status['pid'] . ' 2> /dev/null', inout $output, inout $return_var);
1283 posix_kill($status['pid'], SIGTERM);
1285 usleep(1000);
1287 proc_close($proc);
1288 if ($timedout) {
1289 if ($all_selects_failed) {
1290 return "All selects failed running `$cmd'\n\n$s";
1292 return "Timed out running `$cmd'\n\n$s";
1294 if (
1295 !$status['exitcode'] &&
1296 !preg_match('/\\b(error|exception|fatal)\\b/', $s)
1298 return null;
1300 $pid = $status['pid'];
1301 $stack =
1302 @file_get_contents("/tmp/stacktrace.$pid.log") ?:
1303 @file_get_contents("/var/tmp/cores/stacktrace.$pid.log");
1304 if ($stack !== false) {
1305 $s .= "\n" . $stack;
1307 return "Running `$cmd' failed (".$status['exitcode']."):\n\n$s";
1310 function repo_mode_compile(
1311 Options $options, string $test, string $program,
1312 ): bool {
1313 $hphp = hphp_cmd($options, $test, $program);
1314 $result = exec_with_stack($hphp);
1315 if ($result is null && repo_separate($options, $test)) {
1316 $hhbbc = hhbbc_cmd($options, $test, $program);
1317 $result = exec_with_stack($hhbbc);
1319 if ($result is null) return true;
1320 Status::writeDiff($test, $result);
1321 return false;
1325 // Minimal support for sending messages between processes over named pipes.
1327 // Non-buffered pipe writes of up to 512 bytes (PIPE_BUF) are atomic.
1329 // Packet format:
1330 // 8 byte zero-padded hex pid
1331 // 4 byte zero-padded hex type
1332 // 4 byte zero-padded hex body size
1333 // N byte string body
1335 // The first call to "getInput()" or "getOutput()" in any process will
1336 // block until some other process calls the other method.
1338 class Queue {
1339 // The path to the FIFO, until destroyed.
1340 private ?string $path = null;
1342 private ?resource $input = null;
1343 private ?resource $output = null;
1345 // Pipes writes are atomic up to 512 bytes (up to 4096 bytes on linux),
1346 // and we use a 16 byte header, leaving this many bytes available for
1347 // each chunk of "body" (see "$partials").
1348 const int CHUNK = 512 - 16;
1350 // If a message "body" is larger than CHUNK bytes, then writers must break
1351 // it into chunks, and send all but the last chunk with type 0. The reader
1352 // collects those chunks in this Map (indexed by pid), until the final chunk
1353 // is received, and the chunks can be reassembled.
1354 private Map<int, Vector<string>> $partials = Map {};
1356 public function __construct(?string $dir = null): void {
1357 $path = \tempnam($dir ?? \sys_get_temp_dir(), "queue.mkfifo.");
1358 \unlink($path);
1359 if (!\posix_mkfifo($path, 0700)) {
1360 // Only certain directories support "posix_mkfifo()".
1361 throw new \Exception("Failed to create FIFO at '$path'");
1363 $this->path = $path;
1366 private function getInput(): resource {
1367 $input = $this->input;
1368 if ($input is null) {
1369 $path = $this->path;
1370 if ($path is null) {
1371 throw new \Exception("Missing FIFO path");
1373 $input = \fopen($path, "r");
1374 $this->input = $input;
1376 return $input;
1379 private function getOutput(): resource {
1380 $output = $this->output;
1381 if ($output is null) {
1382 $path = $this->path;
1383 if ($path is null) {
1384 throw new \Exception("Missing FIFO path");
1386 $output = \fopen($path, "a");
1387 $this->output = $output;
1389 return $output;
1392 private function validate(int $pid, int $type, int $blen): void {
1393 if ($pid < 0 || $pid >= (1 << 22)) {
1394 throw new \Exception("Illegal pid $pid");
1396 if ($type < 0 || $type >= 0x10000) {
1397 throw new \Exception("Illegal type $type");
1399 if ($blen < 0 || $blen > static::CHUNK) {
1400 throw new \Exception("Illegal blen $blen");
1404 // Read one packet header or body.
1405 private function read(int $n): string {
1406 $input = $this->getInput();
1407 $result = "";
1408 while (\strlen($result) < $n) {
1409 $r = fread($input, $n - \strlen($result));
1410 if ($r is string) {
1411 $result .= $r;
1412 } else {
1413 throw new \Exception("Failed to read $n bytes");
1416 return $result;
1419 // Receive one raw message (pid, type, body).
1420 public function receive(): (int, int, string) {
1421 $type = null;
1422 $body = "";
1423 while (true) {
1424 $header = $this->read(16);
1425 $pid = intval(substr($header, 0, 8) as string, 16);
1426 $type = intval(substr($header, 8, 4) as string, 16);
1427 $blen = intval(substr($header, 12, 4) as string, 16);
1428 $this->validate($pid, $type, $blen);
1429 $body = $this->read($blen);
1430 if ($type === 0) {
1431 $this->partials[$pid] ??= Vector {};
1432 $this->partials[$pid][] = $body;
1433 } else {
1434 $chunks = $this->partials[$pid] ?? null;
1435 if ($chunks is nonnull) {
1436 $chunks[] = $body;
1437 $body = \implode("", $chunks);
1438 $this->partials->removeKey($pid);
1440 return tuple($pid, $type, $body);
1445 // Receive one message (pid, type, message).
1446 // Note that the raw body is processed using "unserialize()".
1447 public function receiveMessage(): (int, int, ?Message) {
1448 list($pid, $type, $body) = $this->receive();
1449 $msg = unserialize($body) as ?Message;
1450 return tuple($pid, $type, $msg);
1453 private function write(int $pid, int $type, string $body): void {
1454 $output = $this->getOutput();
1455 $blen = \strlen($body);
1456 $this->validate($pid, $type, $blen);
1457 $packet = sprintf("%08x%04x%04x%s", $pid, $type, $blen, $body);
1458 $n = \strlen($packet);
1459 if ($n !== 16 + $blen) {
1460 throw new \Exception("Illegal packet");
1462 // Hack's "fwrite()" is never buffered, which is especially
1463 // critical for pipe writes, to ensure that they are actually atomic.
1464 // See the documentation for "PlainFile::writeImpl()". But just in
1465 // case, we add an explicit "fflush()" below.
1466 $bytes_out = fwrite($output, $packet, $n);
1467 if ($bytes_out !== $n) {
1468 throw new \Exception(
1469 "Failed to write $n bytes; only $bytes_out were written"
1472 fflush($output);
1475 // Send one serialized message.
1476 public function send(int $type, string $body): void {
1477 $pid = \posix_getpid();
1478 $blen = \strlen($body);
1479 $chunk = static::CHUNK;
1480 if ($blen > $chunk) {
1481 for ($i = 0; $i + $chunk < $blen; $i += $chunk) {
1482 $this->write($pid, 0, \substr($body, $i, $chunk) as string);
1484 $this->write($pid, $type, \substr($body, $i) as string);
1485 } else {
1486 $this->write($pid, $type, $body);
1490 // Send one message after serializing it.
1491 public function sendMessage(int $type, ?Message $msg): void {
1492 $body = serialize($msg);
1493 $this->send($type, $body);
1496 public function destroy(): void {
1497 if ($this->input is nonnull) {
1498 fclose($this->input);
1499 $this->input = null;
1501 if ($this->output is nonnull) {
1502 fclose($this->output);
1503 $this->output = null;
1505 if ($this->path is nonnull) {
1506 \unlink($this->path);
1507 $this->path = null;
1512 final class Message {
1513 public function __construct(
1514 public string $test,
1515 public float $time,
1516 public int $stime,
1517 public int $etime,
1518 public ?string $reason = null,
1523 enum TempDirRemove: int {
1524 ALWAYS = 0;
1525 ON_RUN_SUCCESS = 1;
1526 NEVER = 2;
1529 type TestResult = shape(
1530 'name' => string,
1531 'status' => string,
1532 'start_time' => int,
1533 'end_time' => int,
1534 'time' => float,
1535 ?'details' => string,
1538 final class Status {
1539 private static vec<TestResult> $results = vec[];
1540 private static int $mode = 0;
1542 private static bool $use_color = false;
1544 public static bool $nofork = false;
1545 private static ?Queue $queue = null;
1546 private static bool $killed = false;
1547 public static TempDirRemove $temp_dir_remove = TempDirRemove::ALWAYS;
1548 private static int $return_value = 255;
1550 private static float $overall_start_time = 0.0;
1551 private static float $overall_end_time = 0.0;
1553 private static string $tmpdir = "";
1554 public static bool $write_to_checkout = false;
1556 public static int $passed = 0;
1557 public static int $skipped = 0;
1558 public static dict<string, int> $skip_reasons = dict[];
1559 public static int $failed = 0;
1561 const int MODE_NORMAL = 0;
1562 const int MODE_VERBOSE = 1;
1563 const int MODE_TESTPILOT = 3;
1564 const int MODE_RECORD_FAILURES = 4;
1566 const int MSG_STARTED = 7;
1567 const int MSG_FINISHED = 1;
1568 const int MSG_TEST_PASS = 2;
1569 const int MSG_TEST_FAIL = 4;
1570 const int MSG_TEST_SKIP = 5;
1571 const int MSG_SERVER_RESTARTED = 6;
1573 const int RED = 31;
1574 const int GREEN = 32;
1575 const int YELLOW = 33;
1576 const int BLUE = 34;
1578 public static function createTmpDir(?string $working_dir): void {
1579 $parent = $working_dir ?? sys_get_temp_dir();
1580 if (substr($parent, -1) !== "/") {
1581 $parent .= "/";
1583 self::$tmpdir = HH\Lib\_Private\_OS\mkdtemp($parent . 'hphp-test-XXXXXX');
1586 public static function getRunTmpDir(): string {
1587 return self::$tmpdir;
1590 // Return a path in the run tmpdir that's unique to this test and ext.
1591 // Remember to teach clean_intermediate_files to clean up all the exts you use
1592 public static function getTestTmpPath(string $test, string $ext): string {
1593 return self::$tmpdir . '/' . $test . '.' . $ext;
1596 public static function getTmpPathFile(string $filename): string {
1597 return self::$tmpdir . '/' . $filename;
1600 // Similar to getTestTmpPath, but if we're run with --write-to-checkout
1601 // then we put the files next to the test instead of in the tmpdir.
1602 public static function getTestOutputPath(string $test, string $ext): string {
1603 if (self::$write_to_checkout) {
1604 return "$test.$ext";
1606 return static::getTestTmpPath($test, $ext);
1609 public static function createTestTmpDir(string $test): string {
1610 $test_temp_dir = self::getTestTmpPath($test, 'tmpdir');
1611 @mkdir($test_temp_dir, 0777, true);
1612 return $test_temp_dir;
1615 public static function writeDiff(string $test, string $diff): void {
1616 $path = Status::getTestOutputPath($test, 'diff');
1617 @mkdir(dirname($path), 0777, true);
1618 file_put_contents($path, $diff);
1621 public static function diffForTest(string $test): string {
1622 $diff = @file_get_contents(Status::getTestOutputPath($test, 'diff'));
1623 return $diff === false ? '' : $diff;
1626 public static function removeDirectory(string $dir): void {
1627 $files = scandir($dir);
1628 foreach ($files as $file) {
1629 if ($file === '.' || $file === '..') {
1630 continue;
1632 $path = $dir . "/" . $file;
1633 if (is_dir($path)) {
1634 self::removeDirectory($path);
1635 } else {
1636 unlink($path);
1639 rmdir($dir);
1642 // This is similar to removeDirectory but it only removes empty directores
1643 // and won't enter directories whose names end with '.tmpdir'. This allows
1644 // us to clean up paths like test/quick/vec in our run's temporary directory
1645 // if all the tests in them passed, but it leaves test tmpdirs of failed
1646 // tests (that we didn't remove with clean_intermediate_files because the
1647 // test failed) and directores under them alone even if they're empty.
1648 public static function removeEmptyTestParentDirs(string $dir): bool {
1649 $is_now_empty = true;
1650 $files = scandir($dir);
1651 foreach ($files as $file) {
1652 if ($file === '.' || $file === '..') {
1653 continue;
1655 if (strrpos($file, '.tmpdir') === (strlen($file) - strlen('.tmpdir'))) {
1656 $is_now_empty = false;
1657 continue;
1659 $path = $dir . "/" . $file;
1660 if (!is_dir($path)) {
1661 $is_now_empty = false;
1662 continue;
1664 if (self::removeEmptyTestParentDirs($path)) {
1665 rmdir($path);
1666 } else {
1667 $is_now_empty = false;
1670 return $is_now_empty;
1673 public static function setMode(int $mode): void {
1674 self::$mode = $mode;
1677 public static function getMode(): int {
1678 return self::$mode;
1681 public static function setUseColor(bool $use): void {
1682 self::$use_color = $use;
1685 public static function addTestTimesSerial(
1686 dict<string, TestResult> $results,
1687 ): float {
1688 $time = 0.0;
1689 foreach ($results as $result) {
1690 $time += $result['time'];
1692 return $time;
1695 public static function getOverallStartTime(): float {
1696 return self::$overall_start_time;
1699 public static function getOverallEndTime(): float {
1700 return self::$overall_end_time;
1703 public static function started(): void {
1704 self::send(self::MSG_STARTED, null);
1705 self::$overall_start_time = mtime();
1708 public static function finished(int $return_value): void {
1709 self::$overall_end_time = mtime();
1710 self::$return_value = $return_value;
1711 self::send(self::MSG_FINISHED, null);
1714 public static function destroy(): void {
1715 if (!self::$killed) {
1716 self::$killed = true;
1717 if (self::$queue is nonnull) {
1718 self::$queue->destroy();
1719 self::$queue = null;
1721 switch (self::$temp_dir_remove) {
1722 case TempDirRemove::NEVER:
1723 break;
1724 case TempDirRemove::ON_RUN_SUCCESS:
1725 if (self::$return_value !== 0) {
1726 self::removeEmptyTestParentDirs(self::$tmpdir);
1727 break;
1729 // FALLTHROUGH
1730 case TempDirRemove::ALWAYS:
1731 self::removeDirectory(self::$tmpdir);
1736 public static function destroyFromSignal(int $_signo): void {
1737 self::destroy();
1740 public static function registerCleanup(bool $no_clean): void {
1741 if (self::getMode() === self::MODE_TESTPILOT ||
1742 self::getMode() === self::MODE_RECORD_FAILURES) {
1743 self::$temp_dir_remove = TempDirRemove::ALWAYS;
1744 } else if ($no_clean) {
1745 self::$temp_dir_remove = TempDirRemove::NEVER;
1746 } else {
1747 self::$temp_dir_remove = TempDirRemove::ON_RUN_SUCCESS;
1749 register_shutdown_function(self::destroy<>);
1750 pcntl_signal(SIGTERM, self::destroyFromSignal<>);
1751 pcntl_signal(SIGINT, self::destroyFromSignal<>);
1754 public static function serverRestarted(): void {
1755 self::send(self::MSG_SERVER_RESTARTED, null);
1758 public static function pass(
1759 string $test, float $time, int $stime, int $etime,
1760 ): void {
1761 self::$results[] = shape(
1762 'name' => $test,
1763 'status' => 'passed',
1764 'start_time' => $stime,
1765 'end_time' => $etime,
1766 'time' => $time
1768 self::send(
1769 self::MSG_TEST_PASS,
1770 new Message($test, $time, $stime, $etime),
1774 public static function skip(
1775 string $test, string $reason, float $time, int $stime, int $etime,
1776 ): void {
1777 self::$results[] = shape(
1778 'name' => $test,
1779 /* testpilot needs a positive response for every test run, report
1780 * that this test isn't relevant so it can silently drop. */
1781 'status' => self::getMode() === self::MODE_TESTPILOT
1782 ? 'not_relevant'
1783 : 'skipped',
1784 'start_time' => $stime,
1785 'end_time' => $etime,
1786 'time' => $time,
1788 self::send(
1789 self::MSG_TEST_SKIP,
1790 new Message($test, $time, $stime, $etime, $reason),
1794 public static function fail(
1795 string $test, float $time, int $stime, int $etime, string $diff,
1796 ): void {
1797 self::$results[] = shape(
1798 'name' => $test,
1799 'status' => 'failed',
1800 'start_time' => $stime,
1801 'end_time' => $etime,
1802 'time' => $time,
1803 'details' => self::utf8Sanitize($diff),
1805 self::send(
1806 self::MSG_TEST_FAIL,
1807 new Message($test, $time, $stime, $etime),
1811 public static function handle_message(int $type, ?Message $message): bool {
1812 switch ($type) {
1813 case Status::MSG_STARTED:
1814 break;
1816 case Status::MSG_FINISHED:
1817 return false;
1819 case Status::MSG_SERVER_RESTARTED:
1820 switch (Status::getMode()) {
1821 case Status::MODE_NORMAL:
1822 if (!Status::hasCursorControl()) {
1823 Status::sayColor(Status::RED, 'x');
1825 break;
1827 case Status::MODE_VERBOSE:
1828 Status::sayColor(
1829 Status::YELLOW,
1830 "failed to talk to server\n"
1832 break;
1834 case Status::MODE_TESTPILOT:
1835 break;
1837 case Status::MODE_RECORD_FAILURES:
1838 break;
1840 break;
1842 case Status::MSG_TEST_PASS:
1843 self::$passed++;
1844 invariant($message is nonnull, "%s", __METHOD__);
1845 switch (Status::getMode()) {
1846 case Status::MODE_NORMAL:
1847 if (!Status::hasCursorControl()) {
1848 Status::sayColor(Status::GREEN, '.');
1850 break;
1852 case Status::MODE_VERBOSE:
1853 Status::sayColor(
1854 $message->test." ",
1855 Status::GREEN,
1856 sprintf("passed (%.2fs)\n", $message->time),
1858 break;
1860 case Status::MODE_TESTPILOT:
1861 Status::sayTestpilot(
1862 $message->test,
1863 'passed',
1864 $message->stime,
1865 $message->etime,
1866 $message->time,
1868 break;
1870 case Status::MODE_RECORD_FAILURES:
1871 break;
1873 break;
1875 case Status::MSG_TEST_SKIP:
1876 self::$skipped++;
1877 invariant($message is nonnull, "%s", __METHOD__);
1878 $reason = $message->reason;
1879 invariant($reason is nonnull, "%s", __METHOD__);
1880 self::$skip_reasons[$reason] ??= 0;
1881 self::$skip_reasons[$reason]++;
1883 switch (Status::getMode()) {
1884 case Status::MODE_NORMAL:
1885 if (!Status::hasCursorControl()) {
1886 Status::sayColor(Status::YELLOW, 's');
1888 break;
1890 case Status::MODE_VERBOSE:
1891 Status::sayColor($message->test." ", Status::YELLOW, "skipped");
1893 if ($reason is nonnull) {
1894 Status::sayColor(" - reason: $reason");
1896 Status::sayColor(sprintf(" (%.2fs)\n", $message->time));
1897 break;
1899 case Status::MODE_TESTPILOT:
1900 Status::sayTestpilot(
1901 $message->test,
1902 'not_relevant',
1903 $message->stime,
1904 $message->etime,
1905 $message->time,
1907 break;
1909 case Status::MODE_RECORD_FAILURES:
1910 break;
1912 break;
1914 case Status::MSG_TEST_FAIL:
1915 self::$failed++;
1916 invariant($message is nonnull, "%s", __METHOD__);
1917 switch (Status::getMode()) {
1918 case Status::MODE_NORMAL:
1919 if (Status::hasCursorControl()) {
1920 print "\033[2K\033[1G";
1922 $diff = Status::diffForTest($message->test);
1923 $test = $message->test;
1924 Status::sayColor(
1925 Status::RED,
1926 "\nFAILED: $test\n$diff\n",
1928 break;
1930 case Status::MODE_VERBOSE:
1931 Status::sayColor(
1932 $message->test." ",
1933 Status::RED,
1934 sprintf("FAILED (%.2fs)\n", $message->time),
1936 break;
1938 case Status::MODE_TESTPILOT:
1939 Status::sayTestpilot(
1940 $message->test,
1941 'failed',
1942 $message->stime,
1943 $message->etime,
1944 $message->time,
1946 break;
1948 case Status::MODE_RECORD_FAILURES:
1949 break;
1951 break;
1953 default:
1954 error("Unknown message $type");
1956 return true;
1959 private static function send(int $type, ?Message $msg): void {
1960 if (self::$killed) {
1961 return;
1963 if (self::$nofork) {
1964 self::handle_message($type, $msg);
1965 return;
1967 self::getQueue()->sendMessage($type, $msg);
1971 * Takes a variable number of string or int arguments. If color output is
1972 * enabled and any one of the arguments is preceded by an integer (see the
1973 * color constants above), that argument will be given the indicated color.
1975 public static function sayColor(arraykey ...$args): void {
1976 $n = count($args);
1977 for ($i = 0; $i < $n;) {
1978 $arg = $args[$i];
1979 $i++;
1980 if ($arg is int) {
1981 $color = $arg;
1982 if (self::$use_color) {
1983 print "\033[0;{$color}m";
1985 $arg = $args[$i];
1986 $i++;
1987 print $arg;
1988 if (self::$use_color) {
1989 print "\033[0m";
1991 } else {
1992 print $arg;
1997 public static function sayTestpilot(
1998 string $test, string $status, int $stime, int $etime, float $time,
1999 ): void {
2000 $start = dict['op' => 'start', 'test' => $test];
2001 $end = dict['op' => 'test_done', 'test' => $test, 'status' => $status,
2002 'start_time' => $stime, 'end_time' => $etime, 'time' => $time];
2003 if ($status === 'failed') {
2004 $end['details'] = self::utf8Sanitize(Status::diffForTest($test));
2006 self::say($start, $end);
2009 public static function getResults(): vec<TestResult> {
2010 return self::$results;
2013 /** Output is in the format expected by JsonTestRunner. */
2014 public static function say(dict<string, mixed> ...$args): void {
2015 $data = array_map(
2016 $row ==> self::jsonEncode($row) . "\n",
2017 $args
2019 fwrite(STDERR, implode("", $data));
2022 public static function hasCursorControl(): bool {
2023 // for runs on hudson-ci.org (aka jenkins).
2024 if (getenv("HUDSON_URL")) {
2025 return false;
2027 // for runs on travis-ci.org
2028 if (getenv("TRAVIS")) {
2029 return false;
2031 $stty = self::getSTTY();
2032 if (!$stty) {
2033 return false;
2035 return strpos($stty, 'erase = <undef>') === false;
2038 <<__Memoize>>
2039 public static function getSTTY(): string {
2040 $descriptorspec = dict[1 => vec["pipe", "w"], 2 => vec["pipe", "w"]];
2041 $pipes = null;
2042 $process = proc_open(
2043 'stty -a', $descriptorspec, inout $pipes, null, null,
2044 dict['suppress_errors' => true]
2046 $pipes as nonnull;
2047 $stty = stream_get_contents($pipes[1]);
2048 proc_close($process);
2049 return $stty;
2052 public static function utf8Sanitize(string $str): string {
2053 return UConverter::transcode($str, 'UTF-8', 'UTF-8');
2056 public static function jsonEncode(mixed $data): string {
2057 return json_encode($data, JSON_UNESCAPED_SLASHES);
2060 public static function getQueue(): Queue {
2061 if (!self::$queue) {
2062 if (self::$killed) error("Killed!");
2063 self::$queue = new Queue(self::$tmpdir);
2065 return self::$queue;
2069 function clean_intermediate_files(string $test, Options $options): void {
2070 if ($options->no_clean) {
2071 return;
2073 if ($options->write_to_checkout) {
2074 // in --write-to-checkout mode, normal test output goes next to the test
2075 $exts = vec[
2076 'out',
2077 'diff',
2079 foreach ($exts as $ext) {
2080 $file = "$test.$ext";
2081 if (file_exists($file)) {
2082 unlink($file);
2086 $tmp_exts = vec[
2087 // normal test output goes here by default
2088 'out',
2089 'diff',
2090 // scratch directory the test may write to
2091 'tmpdir',
2092 // tests in --hhas-round-trip mode
2093 'round_trip.hhas',
2094 // tests in --hhbbc2 mode
2095 'before.round_trip.hhas',
2096 'after.round_trip.hhas',
2097 // temporary autoloader DB and associated cruft
2098 // We have at most two modes for now - see hhvm_cmd_impl
2099 'autoloadDB.0',
2100 'autoloadDB.0-journal',
2101 'autoloadDB.0-shm',
2102 'autoloadDB.0-wal',
2103 'autoloadDB.1',
2104 'autoloadDB.1-journal',
2105 'autoloadDB.1-shm',
2106 'autoloadDB.1-wal',
2108 foreach ($tmp_exts as $ext) {
2109 $file = Status::getTestTmpPath($test, $ext);
2110 if (is_dir($file)) {
2111 Status::removeDirectory($file);
2112 } else if (file_exists($file)) {
2113 unlink($file);
2116 // repo mode uses a directory that may or may not be in the run's tmpdir
2117 $repo = test_repo($options, $test);
2118 if (is_dir($repo)) {
2119 Status::removeDirectory($repo);
2123 function child_main(
2124 Options $options,
2125 vec<string> $tests,
2126 string $json_results_file,
2127 ): int {
2128 foreach ($tests as $test) {
2129 run_and_log_test($options, $test);
2131 $results = Status::getResults();
2132 file_put_contents($json_results_file, json_encode($results));
2133 foreach ($results as $result) {
2134 if ($result['status'] === 'failed') {
2135 return 1;
2138 return 0;
2142 * The runif feature is similar in spirit to skipif, but instead of allowing
2143 * one to run arbitrary code it can only skip based on pre-defined reasons
2144 * understood by the test runner.
2146 * The .runif file should consist of one or more lines made up of words
2147 * separated by spaces, optionally followed by a comment starting with #.
2148 * Empty lines (or lines with only comments) are ignored. The first word
2149 * determines the interpretation of the rest. The .runif file will allow the
2150 * test to run if all the non-empty lines 'match'.
2152 * Currently supported operations:
2153 * os [not] <os_name> # matches if we are (or are not) on the named OS
2154 * file <path> # matches if the file at the (possibly relative) path exists
2155 * euid [not] root # matches if we are (or are not) running as root (euid==0)
2156 * extension <extension_name> # matches if the named extension is available
2157 * function <function_name> # matches if the named function is available
2158 * class <class_name> # matches if the named class is available
2159 * method <class_name> <method name> # matches if the method is available
2160 * const <constant_name> # matches if the named constant is available
2161 * # matches if any named locale is available for the named LC_* category
2162 * locale LC_<something> <locale name>[ <another locale name>]
2164 * Several functions in this implementation return RunifResult. Valid sets of
2165 * keys are:
2166 * valid, error # valid will be false
2167 * valid, match # valid will be true, match will be true
2168 * valid, match, skip_reason # valid will be true, match will be false
2170 type RunifResult = shape(
2171 'valid' => bool, // was the runif file valid
2172 ?'error' => string, // if !valid, what was the problem
2173 ?'match' => bool, // did the line match/did all the lines in the file match
2174 ?'skip_reason' => string, // if !match, the skip reason to use
2177 <<__Memoize>>
2178 function runif_canonical_os(): string {
2179 if (PHP_OS === 'Linux' || PHP_OS === 'Darwin') return PHP_OS;
2180 if (substr(PHP_OS, 0, 3) === 'WIN') return 'WIN';
2181 invariant_violation('add proper canonicalization for your OS');
2184 function runif_known_os(string $match_os): bool {
2185 switch ($match_os) {
2186 case 'Linux':
2187 case 'Darwin':
2188 case 'WIN':
2189 return true;
2190 default:
2191 return false;
2195 function runif_os_matches(vec<string> $words): RunifResult {
2196 if (count($words) === 2) {
2197 if ($words[0] !== 'not') {
2198 return shape('valid' => false, 'error' => "malformed 'os' match");
2200 $match_os = $words[1];
2201 $invert = true;
2202 } else if (count($words) === 1) {
2203 $match_os = $words[0];
2204 $invert = false;
2205 } else {
2206 return shape('valid' => false, 'error' => "malformed 'os' match");
2208 if (!runif_known_os($match_os)) {
2209 return shape('valid' => false, 'error' => "unknown os '$match_os'");
2211 $matches = (runif_canonical_os() === $match_os);
2212 if ($matches !== $invert) return shape('valid' => true, 'match' => true);
2213 return shape(
2214 'valid' => true,
2215 'match' => false,
2216 'skip_reason' => 'skip-runif-os-' . implode('-', $words)
2220 function runif_file_matches(vec<string> $words): RunifResult {
2221 /* This implementation has a trade-off. On the one hand, we could get more
2222 * accurate results if we do the check in a process with the same configs as
2223 * the test via runif_test_for_feature (e.g. if config differences make a
2224 * file we can see invisible to the test). However, this check was added to
2225 * skip tests where the test configs depend on a file that may be absent, in
2226 * which case hhvm configured the same way as the test cannot run. By doing
2227 * the check ourselves we can successfully skip such tests.
2229 if (count($words) !== 1) {
2230 return shape('valid' => false, 'error' => "malformed 'file' match");
2232 if (file_exists($words[0])) {
2233 return shape('valid' => true, 'match' => true);
2235 return shape(
2236 'valid' => true,
2237 'match' => false,
2238 'skip_reason' => 'skip-runif-file',
2242 function runif_test_for_feature(
2243 Options $options,
2244 string $test,
2245 string $bool_expression,
2246 ): bool {
2247 $tmp = tempnam(sys_get_temp_dir(), 'test-run-runif-');
2248 file_put_contents(
2249 $tmp,
2250 "<?hh\n" .
2251 "<<__EntryPoint>> function main(): void {\n" .
2252 " echo ($bool_expression) as bool ? 'PRESENT' : 'ABSENT';\n" .
2253 "}\n",
2256 // Run the check in non-repo mode to avoid building the repo (same features
2257 // should be available). Pick the mode arbitrarily for the same reason.
2258 $options_without_repo = clone $options;
2259 $options_without_repo->repo = false;
2260 list($hhvm, $_) = hhvm_cmd($options_without_repo, $test, $tmp, true);
2261 $hhvm = $hhvm[0];
2262 // Remove any --count <n> from the command
2263 $hhvm = preg_replace('/ --count[ =]\d+/', '', $hhvm);
2264 // some tests set open_basedir to a restrictive value, override to permissive
2265 $hhvm .= ' -dopen_basedir= ';
2267 $result = shell_exec("$hhvm 2>&1");
2268 invariant ($result !== false, 'shell_exec in runif_test_for_feature failed');
2269 $result = trim($result);
2270 if ($result === 'ABSENT') return false;
2271 if ($result === 'PRESENT') return true;
2272 invariant_violation(
2273 "unexpected output from shell_exec in runif_test_for_feature: '%s'",
2274 $result
2278 function runif_euid_matches(
2279 Options $options,
2280 string $test,
2281 vec<string> $words,
2282 ): RunifResult {
2283 if (count($words) === 2) {
2284 if ($words[0] !== 'not' || $words[1] !== 'root') {
2285 return shape('valid' => false, 'error' => "malformed 'euid' match");
2287 $invert = true;
2288 } else if (count($words) === 1) {
2289 if ($words[0] !== 'root') {
2290 return shape('valid' => false, 'error' => "malformed 'euid' match");
2292 $invert = false;
2293 } else {
2294 return shape('valid' => false, 'error' => "malformed 'euid' match");
2296 $matches = runif_test_for_feature($options, $test, 'posix_geteuid() === 0');
2297 if ($matches !== $invert) return shape('valid' => true, 'match' => true);
2298 return shape(
2299 'valid' => true,
2300 'match' => false,
2301 'skip_reason' => 'skip-runif-euid-' . implode('-', $words)
2305 function runif_extension_matches(
2306 Options $options,
2307 string $test,
2308 vec<string> $words,
2309 ): RunifResult {
2310 if (count($words) !== 1) {
2311 return shape('valid' => false, 'error' => "malformed 'extension' match");
2313 if (runif_test_for_feature($options, $test, "extension_loaded('{$words[0]}')")) {
2314 return shape('valid' => true, 'match' => true);
2316 return shape(
2317 'valid' => true,
2318 'match' => false,
2319 'skip_reason' => 'skip-runif-extension-' . $words[0]
2323 function runif_function_matches(
2324 Options $options,
2325 string $test,
2326 vec<string> $words,
2327 ): RunifResult {
2328 if (count($words) !== 1) {
2329 return shape('valid' => false, 'error' => "malformed 'function' match");
2331 if (runif_test_for_feature($options, $test, "function_exists('{$words[0]}')")) {
2332 return shape('valid' => true, 'match' => true);
2334 return shape(
2335 'valid' => true,
2336 'match' => false,
2337 'skip_reason' => 'skip-runif-function-' . $words[0]
2341 function runif_class_matches(
2342 Options $options,
2343 string $test,
2344 vec<string> $words,
2345 ): RunifResult {
2346 if (count($words) !== 1) {
2347 return shape('valid' => false, 'error' => "malformed 'class' match");
2349 if (runif_test_for_feature($options, $test, "class_exists('{$words[0]}')")) {
2350 return shape('valid' => true, 'match' => true);
2352 return shape(
2353 'valid' => true,
2354 'match' => false,
2355 'skip_reason' => 'skip-runif-class-' . $words[0]
2359 function runif_method_matches(
2360 Options $options,
2361 string $test,
2362 vec<string> $words,
2363 ): RunifResult {
2364 if (count($words) !== 2) {
2365 return shape('valid' => false, 'error' => "malformed 'method' match");
2367 if (runif_test_for_feature($options, $test,
2368 "method_exists('{$words[0]}', '{$words[1]}')")) {
2369 return shape('valid' => true, 'match' => true);
2371 return shape(
2372 'valid' => true,
2373 'match' => false,
2374 'skip_reason' => 'skip-runif-method-' . $words[0] . '-' . $words[1],
2378 function runif_const_matches(
2379 Options $options,
2380 string $test,
2381 vec<string> $words,
2382 ): RunifResult {
2383 if (count($words) !== 1) {
2384 return shape('valid' => false, 'error' => "malformed 'const' match");
2386 if (runif_test_for_feature($options, $test, "defined('{$words[0]}')")) {
2387 return shape('valid' => true, 'match' => true);
2389 return shape(
2390 'valid' => true,
2391 'match' => false,
2392 'skip_reason' => 'skip-runif-const-' . $words[0]
2396 function runif_locale_matches(
2397 Options $options,
2398 string $test,
2399 vec<string> $words,
2400 ): RunifResult {
2401 if (count($words) < 2) {
2402 return shape('valid' => false, 'error' => "malformed 'locale' match");
2404 $category = array_shift(inout $words);
2405 if (!preg_match('/^LC_[A-Z]+$/', $category)) {
2406 return shape('valid' => false, 'error' => "bad locale category '$category'");
2408 $locale_args = implode(', ', array_map($word ==> "'$word'", $words));
2409 $matches = runif_test_for_feature(
2410 $options,
2411 $test,
2412 "defined('$category') && (false !== setlocale($category, $locale_args))",
2414 if ($matches) {
2415 return shape('valid' => true, 'match' => true);
2417 return shape(
2418 'valid' => true,
2419 'match' => false,
2420 'skip_reason' => 'skip-runif-locale',
2424 function runif_should_skip_test(
2425 Options $options,
2426 string $test,
2427 ): RunifResult {
2428 $runif_path = find_test_ext($test, 'runif');
2429 if (!$runif_path) return shape('valid' => true, 'match' => true);
2431 $file_empty = true;
2432 $contents = file($runif_path, FILE_IGNORE_NEW_LINES);
2433 foreach ($contents as $line) {
2434 $line = preg_replace('/[#].*$/', '', $line); // remove comment
2435 $line = trim($line);
2436 if ($line === '') continue;
2437 $file_empty = false;
2439 $words = preg_split('/ +/', $line);
2440 if (count($words) < 2) {
2441 return shape('valid' => false, 'error' => "malformed line '$line'");
2443 foreach ($words as $word) {
2444 if (!preg_match('|^[\w/.-]+$|', $word)) {
2445 return shape(
2446 'valid' => false,
2447 'error' => "bad word '$word' in line '$line'",
2452 $type = array_shift(inout $words);
2453 $words = vec($words); // array_shift always promotes to dict :-\
2454 switch ($type) {
2455 case 'os':
2456 $result = runif_os_matches($words);
2457 break;
2458 case 'file':
2459 $result = runif_file_matches($words);
2460 break;
2461 case 'euid':
2462 $result = runif_euid_matches($options, $test, $words);
2463 break;
2464 case 'extension':
2465 $result = runif_extension_matches($options, $test, $words);
2466 break;
2467 case 'function':
2468 $result = runif_function_matches($options, $test, $words);
2469 break;
2470 case 'class':
2471 $result = runif_class_matches($options, $test, $words);
2472 break;
2473 case 'method':
2474 $result = runif_method_matches($options, $test, $words);
2475 break;
2476 case 'const':
2477 $result = runif_const_matches($options, $test, $words);
2478 break;
2479 case 'locale':
2480 $result = runif_locale_matches($options, $test, $words);
2481 break;
2482 default:
2483 return shape('valid' => false, 'error' => "bad match type '$type'");
2485 if (!$result['valid'] || !Shapes::idx($result, 'match', false)) {
2486 return $result;
2489 if ($file_empty) return shape('valid' => false, 'error' => 'empty runif file');
2490 return shape('valid' => true, 'match' => true);
2493 function should_skip_test_simple(
2494 Options $options,
2495 string $test,
2496 ): ?string {
2497 if (($options->cli_server || $options->server) &&
2498 !can_run_server_test($test, $options)) {
2499 return 'skip-server';
2502 if ($options->hhas_round_trip && substr($test, -5) === ".hhas") {
2503 return 'skip-hhas';
2506 if ($options->hhbbc2 || $options->hhas_round_trip) {
2507 $no_hhas_tag = 'nodumphhas';
2508 if (file_exists("$test.$no_hhas_tag") ||
2509 file_exists(dirname($test).'/'.$no_hhas_tag)) {
2510 return 'skip-nodumphhas';
2512 if (file_exists($test . ".verify")) {
2513 return 'skip-verify';
2517 if (has_multi_request_mode($options) || $options->repo ||
2518 $options->server) {
2519 if (file_exists($test . ".verify")) {
2520 return 'skip-verify';
2522 $no_multireq_tag = "nomultireq";
2523 if (file_exists("$test.$no_multireq_tag") ||
2524 file_exists(dirname($test).'/'.$no_multireq_tag)) {
2525 return 'skip-multi-req';
2527 if (find_debug_config($test, 'hphpd.ini')) {
2528 return 'skip-debugger';
2532 $no_bespoke_tag = "nobespoke";
2533 if ($options->bespoke &&
2534 file_exists("$test.$no_bespoke_tag")) {
2535 // Skip due to changes in array identity
2536 return 'skip-bespoke';
2539 $no_jitserialize_tag = "nojitserialize";
2540 if ($options->jit_serialize is nonnull &&
2541 file_exists("$test.$no_jitserialize_tag")) {
2542 return 'skip-jit-serialize';
2545 return null;
2548 function skipif_should_skip_test(
2549 Options $options,
2550 string $test,
2551 ): RunifResult {
2552 $skipif_test = find_test_ext($test, 'skipif');
2553 if (!$skipif_test) {
2554 return shape('valid' => true, 'match' => true);
2557 // Run the .skipif in non-repo mode since building a repo for it is
2558 // inconvenient and the same features should be available. Pick the mode
2559 // arbitrarily for the same reason.
2560 $options_without_repo = clone $options;
2561 $options_without_repo->repo = false;
2562 list($hhvm, $_) = hhvm_cmd($options_without_repo, $test, $skipif_test);
2563 $hhvm = $hhvm[0];
2564 // Remove any --count <n> from the command
2565 $hhvm = preg_replace('/ --count[ =]\d+/', '', $hhvm);
2567 $descriptorspec = dict[
2568 0 => vec["pipe", "r"],
2569 1 => vec["pipe", "w"],
2570 2 => vec["pipe", "w"],
2572 $pipes = null;
2573 $process = proc_open("$hhvm $test 2>&1", $descriptorspec, inout $pipes);
2574 if (!is_resource($process)) {
2575 return shape(
2576 'valid' => false,
2577 'error' => 'proc_open failed while running skipif'
2581 $pipes as nonnull;
2582 fclose($pipes[0]);
2583 $output = trim(stream_get_contents($pipes[1]));
2584 fclose($pipes[1]);
2585 proc_close($process);
2587 // valid output is empty or a single line starting with 'skip'
2588 // everything else must result in a test failure
2589 if ($output === '') {
2590 return shape('valid' => true, 'match' => true);
2592 if (preg_match('/^skip.*$/', $output)) {
2593 return shape(
2594 'valid' => true,
2595 'match' => false,
2596 'skip_reason' => 'skip-skipif',
2599 return shape('valid' => false, 'error' => "invalid skipif output '$output'");
2602 function comp_line(string $l1, string $l2, bool $is_reg): bool {
2603 if ($is_reg) {
2604 return (bool)preg_match('/^'. $l1 . '$/s', $l2);
2605 } else {
2606 return !strcmp($l1, $l2);
2610 function count_array_diff(
2611 vec<string> $ar1, vec<string> $ar2, bool $is_reg,
2612 int $idx1, int $idx2, int $cnt1, int $cnt2, num $steps,
2613 ): int {
2614 $equal = 0;
2616 while ($idx1 < $cnt1 && $idx2 < $cnt2 && comp_line($ar1[$idx1], $ar2[$idx2],
2617 $is_reg)) {
2618 $idx1++;
2619 $idx2++;
2620 $equal++;
2621 $steps--;
2623 $steps--;
2624 if ($steps > 0) {
2625 $eq1 = 0;
2626 $st = $steps / 2;
2628 for ($ofs1 = $idx1 + 1; $ofs1 < $cnt1 && $st > 0; $ofs1++) {
2629 $st--;
2630 $eq = @count_array_diff($ar1, $ar2, $is_reg, $ofs1, $idx2, $cnt1,
2631 $cnt2, $st);
2633 if ($eq > $eq1) {
2634 $eq1 = $eq;
2638 $eq2 = 0;
2639 $st = $steps;
2641 for ($ofs2 = $idx2 + 1; $ofs2 < $cnt2 && $st > 0; $ofs2++) {
2642 $st--;
2643 $eq = @count_array_diff($ar1, $ar2, $is_reg, $idx1, $ofs2, $cnt1, $cnt2, $st);
2644 if ($eq > $eq2) {
2645 $eq2 = $eq;
2649 if ($eq1 > $eq2) {
2650 $equal += $eq1;
2651 } else if ($eq2 > 0) {
2652 $equal += $eq2;
2656 return $equal;
2659 function generate_array_diff(
2660 vec<string> $ar1,
2661 vec<string> $ar2,
2662 bool $is_reg,
2663 vec<string> $w,
2664 ): vec<string> {
2665 $idx1 = 0; $cnt1 = @count($ar1);
2666 $idx2 = 0; $cnt2 = @count($ar2);
2667 $old1 = dict[];
2668 $old2 = dict[];
2670 while ($idx1 < $cnt1 && $idx2 < $cnt2) {
2671 if (comp_line($ar1[$idx1], $ar2[$idx2], $is_reg)) {
2672 $idx1++;
2673 $idx2++;
2674 continue;
2675 } else {
2676 $c1 = @count_array_diff($ar1, $ar2, $is_reg, $idx1+1, $idx2, $cnt1,
2677 $cnt2, 10);
2678 $c2 = @count_array_diff($ar1, $ar2, $is_reg, $idx1, $idx2+1, $cnt1,
2679 $cnt2, 10);
2681 if ($c1 > $c2) {
2682 $old1[$idx1+1] = sprintf("%03d- ", $idx1+1) . $w[$idx1];
2683 $idx1++;
2684 } else if ($c2 > 0) {
2685 $old2[$idx2+1] = sprintf("%03d+ ", $idx2+1) . $ar2[$idx2];
2686 $idx2++;
2687 } else {
2688 $old1[$idx1+1] = sprintf("%03d- ", $idx1+1) . $w[$idx1];
2689 $old2[$idx2+1] = sprintf("%03d+ ", $idx2+1) . $ar2[$idx2];
2690 $idx1++;
2691 $idx2++;
2696 $diff = vec[];
2697 $old1_keys = array_keys($old1);
2698 $old2_keys = array_keys($old2);
2699 $old1_values = array_values($old1);
2700 $old2_values = array_values($old2);
2701 // these start at -2 so $l1 + 1 and $l2 + 1 are not valid indices
2702 $l1 = -2;
2703 $l2 = -2;
2704 $iter1 = 0; $end1 = count($old1);
2705 $iter2 = 0; $end2 = count($old2);
2707 while ($iter1 < $end1 || $iter2 < $end2) {
2708 $k1 = $iter1 < $end1 ? $old1_keys[$iter1] : -2;
2709 $k2 = $iter2 < $end2 ? $old2_keys[$iter2] : -2;
2710 if ($k1 === $l1 + 1 || $iter2 >= $end2) {
2711 $l1 = $k1;
2712 $diff[] = $old1_values[$iter1];
2713 $iter1++;
2714 } else if ($k2 === $l2 + 1 || $iter1 >= $end1) {
2715 $l2 = $k2;
2716 $diff[] = $old2_values[$iter2];
2717 $iter2++;
2718 } else if ($k1 < $k2) {
2719 $l1 = $k1;
2720 $diff[] = $old1_values[$iter1];
2721 $iter1++;
2722 } else {
2723 $l2 = $k2;
2724 $diff[] = $old2_values[$iter2];
2725 $iter2++;
2729 while ($idx1 < $cnt1) {
2730 $diff[] = sprintf("%03d- ", $idx1 + 1) . $w[$idx1];
2731 $idx1++;
2734 while ($idx2 < $cnt2) {
2735 $diff[] = sprintf("%03d+ ", $idx2 + 1) . $ar2[$idx2];
2736 $idx2++;
2739 return $diff;
2742 function generate_diff(
2743 string $wanted,
2744 ?string $wanted_re,
2745 string $output
2746 ): string {
2747 $m = null;
2748 $w = explode("\n", $wanted);
2749 $o = explode("\n", $output);
2750 if (is_null($wanted_re)) {
2751 $r = $w;
2752 } else {
2753 if (preg_match_with_matches('/^\((.*)\)\{(\d+)\}$/s', $wanted_re, inout $m)) {
2754 $t = explode("\n", $m[1] as string);
2755 $r = vec[];
2756 $w2 = vec[];
2757 for ($i = 0; $i < (int)$m[2]; $i++) {
2758 foreach ($t as $v) {
2759 $r[] = $v;
2761 foreach ($w as $v) {
2762 $w2[] = $v;
2765 $w = $wanted === $wanted_re ? $r : $w2;
2766 } else {
2767 $r = explode("\n", $wanted_re);
2770 $diff = generate_array_diff($r, $o, !is_null($wanted_re), $w);
2772 return implode("\r\n", $diff);
2775 function dump_hhas_cmd(
2776 string $hhvm_cmd, string $test, string $hhas_file,
2777 ): string {
2778 $dump_flags = implode(' ', vec[
2779 '-vEval.AllowHhas=true',
2780 '-vEval.DumpHhas=1',
2781 '-vEval.DumpHhasToFile='.escapeshellarg($hhas_file),
2782 '-vEval.LoadFilepathFromUnitCache=0',
2784 $cmd = str_replace(' -- ', " $dump_flags -- ", $hhvm_cmd);
2785 if ($cmd === $hhvm_cmd) $cmd .= " $dump_flags";
2786 return $cmd;
2789 function dump_hhas_to_temp(string $hhvm_cmd, string $test): ?string {
2790 $temp_file = Status::getTestTmpPath($test, 'round_trip.hhas');
2791 $cmd = dump_hhas_cmd($hhvm_cmd, $test, $temp_file);
2792 $ret = -1;
2793 system("$cmd &> /dev/null", inout $ret);
2794 return $ret === 0 ? $temp_file : null;
2797 const vec<string> SERVER_EXCLUDE_PATHS = vec[
2798 'quick/xenon/',
2799 'slow/streams/',
2800 'slow/ext_mongo/',
2801 'slow/ext_oauth/',
2802 'slow/ext_vsdebug/',
2803 'zend/good/ext/standard/tests/array/',
2806 const string HHAS_EXT = '.hhas';
2808 function can_run_server_test(string $test, Options $options): bool {
2809 // explicitly disabled
2810 if (is_file("$test.noserver") ||
2811 (is_file("$test.nowebserver") && $options->server)) {
2812 return false;
2815 // has its own config
2816 if (find_test_ext($test, 'opts') || is_file("$test.ini") ||
2817 is_file("$test.use.for.ini.migration.testing.only.hdf")) {
2818 return false;
2821 // we can't run repo only tests in server modes
2822 if (is_file("$test.onlyrepo") || is_file("$test.onlyjumpstart")) {
2823 return false;
2826 foreach (SERVER_EXCLUDE_PATHS as $path) {
2827 if (strpos($test, $path) !== false) return false;
2830 // don't run hhas tests in server modes
2831 if (strrpos($test, HHAS_EXT) === (strlen($test) - strlen(HHAS_EXT))) {
2832 return false;
2835 return true;
2838 const int SERVER_TIMEOUT = 45;
2840 function run_config_server(Options $options, string $test): mixed {
2841 invariant(
2842 can_run_server_test($test, $options),
2843 'should_skip_test_simple should have skipped this',
2846 Status::createTestTmpDir($test); // force it to be created
2847 $config = find_file_for_dir(dirname($test), 'config.ini') ?? '';
2848 $servers = $options->servers as Servers;
2849 $port = $servers->configs[$config]->port;
2850 $ch = curl_init("localhost:$port/$test");
2851 curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
2852 curl_setopt($ch, CURLOPT_TIMEOUT, SERVER_TIMEOUT);
2853 curl_setopt($ch, CURLOPT_BINARYTRANSFER, true);
2854 $output = curl_exec($ch);
2855 if ($output is string) {
2856 $output = trim($output);
2857 } else {
2858 $output = "Error talking to server: " . curl_error($ch);
2860 curl_close($ch);
2862 return run_config_post(tuple($output, ''), $test, $options);
2865 function run_config_cli(
2866 Options $options,
2867 string $test,
2868 string $cmd,
2869 dict<string, mixed> $cmd_env,
2870 ): ?(string, string) {
2871 $cmd = timeout_prefix() . $cmd;
2873 if ($options->repo && $options->repo_out is null) {
2874 // we already created it in run_test
2875 $cmd_env['HPHP_TEST_TMPDIR'] = Status::getTestTmpPath($test, 'tmpdir');
2876 } else {
2877 $cmd_env['HPHP_TEST_TMPDIR'] = Status::createTestTmpDir($test);
2879 $cmd_env['HPHP_TEST_SOURCE_FILE'] = $test;
2880 if ($options->log) {
2881 $cmd_env['TRACE'] = 'printir:1';
2882 $cmd_env['HPHP_TRACE_FILE'] = $test . '.log';
2885 $descriptorspec = dict[
2886 0 => vec["pipe", "r"],
2887 1 => vec["pipe", "w"],
2888 2 => vec["pipe", "w"],
2890 $pipes = null;
2891 $process = proc_open(
2892 "$cmd 2>&1", $descriptorspec, inout $pipes, null, $cmd_env
2894 if (!is_resource($process)) {
2895 Status::writeDiff($test, "Couldn't invoke $cmd");
2896 return null;
2899 $pipes as nonnull;
2900 fclose($pipes[0]);
2901 $output = stream_get_contents($pipes[1]);
2902 $output = trim($output);
2903 $stderr = stream_get_contents($pipes[2]);
2904 fclose($pipes[1]);
2905 fclose($pipes[2]);
2906 proc_close($process);
2908 return tuple($output, $stderr);
2911 function replace_object_resource_ids(string $str, string $replacement): string {
2912 $str = preg_replace(
2913 '/(object\([^)]+\)#)\d+/', '\1'.$replacement, $str
2915 return preg_replace(
2916 '/resource\(\d+\)/', "resource($replacement)", $str
2920 function run_config_post(
2921 (string, string) $outputs,
2922 string $test,
2923 Options $options,
2924 ): mixed {
2925 list($output, $stderr) = $outputs;
2926 file_put_contents(Status::getTestOutputPath($test, 'out'), $output);
2928 $check_hhbbc_error = $options->repo
2929 && (file_exists($test . '.hhbbc_assert') ||
2930 file_exists($test . '.hphpc_assert'));
2932 // hhvm redirects errors to stdout, so anything on stderr is really bad.
2933 if ($stderr && !$check_hhbbc_error) {
2934 Status::writeDiff(
2935 $test,
2936 "Test failed because the process wrote on stderr:\n$stderr"
2938 return false;
2941 $repeats = 0;
2942 if (!$check_hhbbc_error) {
2943 if ($options->retranslate_all is nonnull) {
2944 $repeats = (int)$options->retranslate_all * 2;
2947 if ($options->recycle_tc is nonnull) {
2948 $repeats = (int)$options->recycle_tc;
2951 if ($options->cli_server) {
2952 $repeats = 3;
2956 list($file, $type) = get_expect_file_and_type($test, $options);
2957 if ($file is null || $type is null) {
2958 Status::writeDiff(
2959 $test,
2960 "No $test.expect, $test.expectf, $test.hhvm.expect, " .
2961 "$test.hhvm.expectf, or $test.expectregex. " .
2962 "If $test is meant to be included by other tests, " .
2963 "use a different file extension.\n"
2965 return false;
2968 $wanted = null;
2969 if ($type === 'expect' || $type === 'hhvm.expect') {
2970 $wanted = trim(file_get_contents($file));
2971 if ($options->ignore_oids || $options->repo) {
2972 $output = replace_object_resource_ids($output, 'n');
2973 $wanted = replace_object_resource_ids($wanted, 'n');
2976 if (!$repeats) {
2977 $passed = !strcmp($output, $wanted);
2978 if (!$passed) {
2979 Status::writeDiff($test, generate_diff($wanted, null, $output));
2981 return $passed;
2983 $wanted_re = preg_quote($wanted, '/');
2984 } else if ($type === 'expectf' || $type === 'hhvm.expectf') {
2985 $wanted = trim(file_get_contents($file));
2986 if ($options->ignore_oids || $options->repo) {
2987 $wanted = replace_object_resource_ids($wanted, '%d');
2989 $wanted_re = $wanted;
2991 // do preg_quote, but miss out any %r delimited sections.
2992 $temp = "";
2993 $r = "%r";
2994 $startOffset = 0;
2995 $length = strlen($wanted_re);
2996 while ($startOffset < $length) {
2997 $start = strpos($wanted_re, $r, $startOffset);
2998 if ($start !== false) {
2999 // we have found a start tag.
3000 $end = strpos($wanted_re, $r, $start+2);
3001 if ($end === false) {
3002 // unbalanced tag, ignore it.
3003 $start = $length;
3004 $end = $length;
3006 } else {
3007 // no more %r sections.
3008 $start = $length;
3009 $end = $length;
3011 // quote a non re portion of the string.
3012 $temp = $temp.preg_quote(substr($wanted_re, $startOffset,
3013 ($start - $startOffset)), '/');
3014 // add the re unquoted.
3015 if ($end > $start) {
3016 $temp = $temp.'('.substr($wanted_re, $start+2, ($end - $start-2)).')';
3018 $startOffset = $end + 2;
3020 $wanted_re = $temp;
3022 $wanted_re = str_replace(
3023 vec['%binary_string_optional%'],
3024 'string',
3025 $wanted_re
3027 $wanted_re = str_replace(
3028 vec['%unicode_string_optional%'],
3029 'string',
3030 $wanted_re
3032 $wanted_re = str_replace(
3033 vec['%unicode\|string%', '%string\|unicode%'],
3034 'string',
3035 $wanted_re
3037 $wanted_re = str_replace(
3038 vec['%u\|b%', '%b\|u%'],
3040 $wanted_re
3042 // Stick to basics.
3043 $wanted_re = str_replace('%e', '\\' . DIRECTORY_SEPARATOR, $wanted_re);
3044 $wanted_re = str_replace('%s', '[^\r\n]+', $wanted_re);
3045 $wanted_re = str_replace('%S', '[^\r\n]*', $wanted_re);
3046 $wanted_re = str_replace('%a', '.+', $wanted_re);
3047 $wanted_re = str_replace('%A', '.*', $wanted_re);
3048 $wanted_re = str_replace('%w', '\s*', $wanted_re);
3049 $wanted_re = str_replace('%i', '[+-]?\d+', $wanted_re);
3050 $wanted_re = str_replace('%d', '\d+', $wanted_re);
3051 $wanted_re = str_replace('%x', '[0-9a-fA-F]+', $wanted_re);
3052 // %f allows two points "-.0.0" but that is the best *simple* expression.
3053 $wanted_re = str_replace('%f', '[+-]?\.?\d+\.?\d*(?:[Ee][+-]?\d+)?',
3054 $wanted_re);
3055 $wanted_re = str_replace('%c', '.', $wanted_re);
3056 // must be last.
3057 $wanted_re = str_replace('%%', '%%?', $wanted_re);
3059 // Normalize newlines.
3060 $wanted_re = preg_replace("/(\r\n?|\n)/", "\n", $wanted_re);
3061 $output = preg_replace("/(\r\n?|\n)/", "\n", $output);
3062 } else if ($type === 'expectregex') {
3063 $wanted_re = trim(file_get_contents($file));
3064 } else {
3065 throw new Exception("Unsupported expect file type: ".$type);
3068 if ($repeats) {
3069 $wanted_re = "($wanted_re\s*)".'{'.$repeats.'}';
3071 if ($wanted is null) $wanted = $wanted_re;
3072 $passed = @preg_match("/^$wanted_re\$/s", $output);
3073 if ($passed) return true;
3074 if ($passed === false && $repeats) {
3075 // $repeats can cause the regex to become too big, and fail
3076 // to compile.
3077 return 'skip-repeats-fail';
3079 $diff = generate_diff($wanted_re, $wanted_re, $output);
3080 if ($passed === false && $diff === "") {
3081 // the preg match failed, probably because the regex was too complex,
3082 // but since the line by line diff came up empty, we're fine
3083 return true;
3085 Status::writeDiff($test, $diff);
3086 return false;
3089 function timeout_prefix(): string {
3090 if (is_executable('/usr/bin/timeout')) {
3091 return '/usr/bin/timeout ' . TIMEOUT_SECONDS . ' ';
3092 } else {
3093 return hphp_home() . '/hphp/tools/timeout.sh -t ' . TIMEOUT_SECONDS . ' ';
3097 function run_foreach_config(
3098 Options $options,
3099 string $test,
3100 vec<string> $cmds,
3101 dict<string, mixed> $cmd_env,
3102 ): mixed {
3103 invariant(count($cmds) > 0, "run_foreach_config: no modes");
3104 $result = false;
3105 foreach ($cmds as $cmd) {
3106 $outputs = run_config_cli($options, $test, $cmd, $cmd_env);
3107 if ($outputs is null) return false;
3108 $result = run_config_post($outputs, $test, $options);
3109 if (!$result) return $result;
3111 return $result;
3114 function run_and_log_test(Options $options, string $test): void {
3115 $stime = time();
3116 $time = mtime();
3117 $status = run_test($options, $test);
3118 $time = mtime() - $time;
3119 $etime = time();
3121 if ($status === false) {
3122 $diff = Status::diffForTest($test);
3123 if ($diff === '') {
3124 $diff = 'Test failed with empty diff';
3126 Status::fail($test, $time, $stime, $etime, $diff);
3127 } else if ($status === true) {
3128 Status::pass($test, $time, $stime, $etime);
3129 clean_intermediate_files($test, $options);
3130 } else if ($status is string) {
3131 invariant(
3132 preg_match('/^skip-[\w-]+$/', $status),
3133 "invalid skip status %s",
3134 $status
3136 Status::skip($test, substr($status, 5), $time, $stime, $etime);
3137 clean_intermediate_files($test, $options);
3138 } else {
3139 invariant_violation("invalid status type %s", gettype($status));
3143 // Returns "(string | bool)".
3144 function run_test(Options $options, string $test): mixed {
3145 $skip_reason = should_skip_test_simple($options, $test);
3146 if ($skip_reason is nonnull) return $skip_reason;
3148 if (!$options->no_skipif) {
3149 $result = runif_should_skip_test($options, $test);
3150 if (!$result['valid']) {
3151 invariant(Shapes::keyExists($result, 'error'), 'missing runif error');
3152 Status::writeDiff($test, 'Invalid .runif file: ' . $result['error']);
3153 return false;
3155 if (!($result['match'] ?? false)) {
3156 invariant(Shapes::keyExists($result, 'skip_reason'), 'missing skip_reason');
3157 return $result['skip_reason'];
3160 $result = skipif_should_skip_test($options, $test);
3161 if (!$result['valid']) {
3162 invariant(Shapes::keyExists($result, 'error'), 'missing skipif error');
3163 Status::writeDiff($test, $result['error']);
3164 return false;
3166 if (!($result['match'] ?? false)) {
3167 invariant(Shapes::keyExists($result, 'skip_reason'), 'missing skip_reason');
3168 return $result['skip_reason'];
3172 list($hhvm, $hhvm_env) = hhvm_cmd($options, $test);
3174 if (preg_grep('/ --count[ =][0-9]+ .* --count[ =][0-9]+( |$)/', $hhvm)) {
3175 // we got --count from 2 sources (e.g. .opts file and multi_request_mode)
3176 // this can't work so skip the test
3177 return 'skip-count';
3178 } else if ($options->jit_serialize is nonnull) {
3179 // jit-serialize adds the --count option later, so even 1 --count in the
3180 // command means we have to skip
3181 if (preg_grep('/ --count[ =][0-9]+( |$)/', $hhvm)) {
3182 return 'skip-count';
3186 if ($options->repo) {
3187 if (file_exists($test.'.norepo')) {
3188 return 'skip-norepo';
3190 if (file_exists($test.'.onlyjumpstart') &&
3191 ($options->jit_serialize is null || (int)$options->jit_serialize < 1)) {
3192 return 'skip-onlyjumpstart';
3195 $test_repo = test_repo($options, $test);
3196 if ($options->repo_out is nonnull) {
3197 // we may need to clean up after a previous run
3198 $repo_files = vec['hhvm.hhbc', 'hhvm.hhbbc'];
3199 foreach ($repo_files as $repo_file) {
3200 @unlink("$test_repo/$repo_file");
3202 } else {
3203 // create tmpdir now so that we can write repos
3204 Status::createTestTmpDir($test);
3207 $program = "hhvm";
3209 if (file_exists($test . '.hphpc_assert')) {
3210 $hphp = hphp_cmd($options, $test, $program);
3211 return run_foreach_config($options, $test, vec[$hphp], $hhvm_env);
3212 } else if (file_exists($test . '.hhbbc_assert')) {
3213 $hphp = hphp_cmd($options, $test, $program);
3214 if (repo_separate($options, $test)) {
3215 $result = exec_with_stack($hphp);
3216 if ($result is string) return false;
3217 $hhbbc = hhbbc_cmd($options, $test, $program);
3218 return run_foreach_config($options, $test, vec[$hhbbc], $hhvm_env);
3219 } else {
3220 return run_foreach_config($options, $test, vec[$hphp], $hhvm_env);
3224 if (!repo_mode_compile($options, $test, $program)) {
3225 return false;
3228 if ($options->hhbbc2) {
3229 invariant(
3230 count($hhvm) === 1,
3231 "get_options forbids modes because we're not runnig code"
3233 // create tmpdir now so that we can write hhas
3234 Status::createTestTmpDir($test);
3235 $hhas_temp1 = dump_hhas_to_temp($hhvm[0], "$test.before");
3236 if ($hhas_temp1 is null) {
3237 Status::writeDiff($test, "dumping hhas after first hhbbc pass failed");
3238 return false;
3240 shell_exec("mv $test_repo/hhvm.hhbbc $test_repo/hhvm.hhbc");
3241 $hhbbc = hhbbc_cmd($options, $test, $program);
3242 $result = exec_with_stack($hhbbc);
3243 if ($result is string) {
3244 Status::writeDiff($test, $result);
3245 return false;
3247 $hhas_temp2 = dump_hhas_to_temp($hhvm[0], "$test.after");
3248 if ($hhas_temp2 is null) {
3249 Status::writeDiff($test, "dumping hhas after second hhbbc pass failed");
3250 return false;
3252 $diff = shell_exec("diff $hhas_temp1 $hhas_temp2");
3253 if (trim($diff) !== '') {
3254 Status::writeDiff($test, $diff);
3255 return false;
3259 if ($options->jit_serialize is nonnull) {
3260 invariant(count($hhvm) === 1, 'get_options enforces jit mode only');
3261 $cmd = jit_serialize_option($hhvm[0], $test, $options, true);
3262 $outputs = run_config_cli($options, $test, $cmd, $hhvm_env);
3263 if ($outputs is null) return false;
3264 $cmd = jit_serialize_option($hhvm[0], $test, $options, true);
3265 $outputs = run_config_cli($options, $test, $cmd, $hhvm_env);
3266 if ($outputs is null) return false;
3267 $hhvm[0] = jit_serialize_option($hhvm[0], $test, $options, false);
3270 return run_foreach_config($options, $test, $hhvm, $hhvm_env);
3273 if (file_exists($test.'.onlyrepo')) {
3274 return 'skip-onlyrepo';
3276 if (file_exists($test.'.onlyjumpstart')) {
3277 return 'skip-onlyjumpstart';
3280 if ($options->hhas_round_trip) {
3281 invariant(
3282 substr($test, -5) !== ".hhas",
3283 'should_skip_test_simple should have skipped this',
3285 // create tmpdir now so that we can write hhas
3286 Status::createTestTmpDir($test);
3287 // dumping hhas, not running code so arbitrarily picking a mode
3288 $hhas_temp = dump_hhas_to_temp($hhvm[0], $test);
3289 if ($hhas_temp is null) {
3290 $err = "system failed: " .
3291 dump_hhas_cmd($hhvm[0], $test,
3292 Status::getTestTmpPath($test, 'round_trip.hhas')) .
3293 "\n";
3294 Status::writeDiff($test, $err);
3295 return false;
3297 list($hhvm, $hhvm_env) = hhvm_cmd($options, $test, $hhas_temp);
3300 if ($options->server) {
3301 return run_config_server($options, $test);
3303 return run_foreach_config($options, $test, $hhvm, $hhvm_env);
3306 function num_cpus(): int {
3307 switch (PHP_OS) {
3308 case 'Linux':
3309 $data = file('/proc/stat');
3310 $cores = 0;
3311 foreach($data as $line) {
3312 if (preg_match('/^cpu[0-9]/', $line)) {
3313 $cores++;
3316 return $cores;
3317 case 'Darwin':
3318 case 'FreeBSD':
3319 $output = null;
3320 $return_var = -1;
3321 return (int)exec('sysctl -n hw.ncpu', inout $output, inout $return_var);
3323 return 2; // default when we don't know how to detect.
3326 function make_header(string $str): string {
3327 return "\n\033[0;33m".$str."\033[0m\n";
3330 function print_commands(
3331 vec<string> $tests,
3332 Options $options,
3333 ): void {
3334 if (C\count($tests) === 0) {
3335 print make_header(
3336 "Test run failed with no failed tests; did a worker process die?"
3338 } else if ($options->verbose) {
3339 print make_header("Run these by hand:");
3340 } else {
3341 $test = $tests[0];
3342 print make_header("Run $test by hand:");
3343 $tests = vec[$test];
3346 foreach ($tests as $test) {
3347 list($commands, $_) = hhvm_cmd($options, $test);
3348 if (!$options->repo) {
3349 foreach ($commands as $c) {
3350 print "$c\n";
3352 continue;
3355 // How to run it with hhbbc:
3356 $program = "hhvm";
3357 $hhbbc_cmds = hphp_cmd($options, $test, $program)."\n";
3358 if (repo_separate($options, $test)) {
3359 $hhbbc_cmd = hhbbc_cmd($options, $test, $program)."\n";
3360 $hhbbc_cmds .= $hhbbc_cmd;
3361 if ($options->hhbbc2) {
3362 foreach ($commands as $c) {
3363 $hhbbc_cmds .=
3364 $c." -vEval.DumpHhas=1 > $test.before.round_trip.hhas\n";
3366 $test_repo = test_repo($options, $test);
3367 $hhbbc_cmds .=
3368 "mv $test_repo/hhvm.hhbbc $test_repo/hhvm.hhbc\n";
3369 $hhbbc_cmds .= $hhbbc_cmd;
3370 foreach ($commands as $c) {
3371 $hhbbc_cmds .=
3372 $c." -vEval.DumpHhas=1 > $test.after.round_trip.hhas\n";
3374 $hhbbc_cmds .=
3375 "diff $test.before.round_trip.hhas $test.after.round_trip.hhas\n";
3378 if ($options->jit_serialize is nonnull) {
3379 invariant(count($commands) === 1, 'get_options enforces jit mode only');
3380 $hhbbc_cmds .=
3381 jit_serialize_option($commands[0], $test, $options, true) . "\n";
3382 $hhbbc_cmds .=
3383 jit_serialize_option($commands[0], $test, $options, true) . "\n";
3384 $commands[0] = jit_serialize_option($commands[0], $test, $options, false);
3386 foreach ($commands as $c) {
3387 $hhbbc_cmds .= $c."\n";
3389 print "$hhbbc_cmds\n";
3393 // This runs only in the "printer" child.
3394 function msg_loop(int $num_tests, Queue $queue): void {
3395 $cols = null;
3396 $do_progress =
3397 $num_tests > 0 &&
3399 Status::getMode() === Status::MODE_NORMAL ||
3400 Status::getMode() === Status::MODE_RECORD_FAILURES
3401 ) &&
3402 Status::hasCursorControl();
3403 if ($do_progress) {
3404 $stty = strtolower(Status::getSTTY());
3405 $matches = vec[];
3406 if (preg_match_with_matches("/columns ([0-9]+);/", $stty, inout $matches) ||
3407 // because BSD has to be different
3408 preg_match_with_matches("/([0-9]+) columns;/", $stty, inout $matches)) {
3409 $cols = (int)$matches[1];
3413 while (true) {
3414 list($pid, $type, $message) = $queue->receiveMessage();
3415 if (!Status::handle_message($type, $message)) break;
3417 if ($cols is nonnull) {
3418 $total_run = (Status::$skipped + Status::$failed + Status::$passed);
3419 $bar_cols = $cols - 45;
3421 $passed_ticks = (int)round($bar_cols * (Status::$passed / $num_tests));
3422 $skipped_ticks = (int)round($bar_cols * (Status::$skipped / $num_tests));
3423 $failed_ticks = (int)round($bar_cols * (Status::$failed / $num_tests));
3425 $fill = $bar_cols - ($passed_ticks + $skipped_ticks + $failed_ticks);
3426 if ($fill < 0) $fill = 0;
3428 $passed_ticks = str_repeat('#', $passed_ticks);
3429 $skipped_ticks = str_repeat('#', $skipped_ticks);
3430 $failed_ticks = str_repeat('#', $failed_ticks);
3431 $fill = str_repeat('-', (int)$fill);
3433 echo
3434 "\033[2K\033[1G[",
3435 "\033[0;32m$passed_ticks",
3436 "\033[33m$skipped_ticks",
3437 "\033[31m$failed_ticks",
3438 "\033[0m$fill] ($total_run/$num_tests) ",
3439 "(", Status::$skipped, " skipped,", Status::$failed, " failed)";
3443 if ($cols is nonnull) {
3444 print "\033[2K\033[1G";
3445 if (Status::$skipped > 0) {
3446 print Status::$skipped ." tests \033[1;33mskipped\033[0m\n";
3447 $reasons = Status::$skip_reasons;
3448 arsort(inout $reasons);
3449 Status::$skip_reasons = $reasons as dict<_, _>;
3450 foreach (Status::$skip_reasons as $reason => $count) {
3451 printf("%12s: %d\n", $reason, $count);
3457 function print_success(
3458 vec<string> $tests,
3459 dict<string, TestResult> $results,
3460 Options $options,
3461 ): void {
3462 // We didn't run any tests, not even skipped. Clowntown!
3463 if (!$tests) {
3464 print "\nCLOWNTOWN: No tests!\n";
3465 if (!$options->no_fun) {
3466 print_clown();
3468 return;
3470 $ran_tests = false;
3471 foreach ($results as $result) {
3472 // The result here will either be skipped or passed (since failed is
3473 // handled in print_failure.
3474 if ($result['status'] === 'passed') {
3475 $ran_tests = true;
3476 break;
3479 // We just had skipped tests
3480 if (!$ran_tests) {
3481 print "\nSKIP-ALOO: Only skipped tests!\n";
3482 if (!$options->no_fun) {
3483 print_skipper();
3485 return;
3487 print "\nAll tests passed.\n";
3488 if (!$options->no_fun) {
3489 print_ship();
3491 if ($options->failure_file is nonnull) {
3492 @unlink($options->failure_file);
3494 if ($options->verbose) {
3495 print_commands($tests, $options);
3499 function print_failure(
3500 vec<string> $argv,
3501 dict<string, TestResult> $results,
3502 Options $options,
3503 ): void {
3504 $failed = vec[];
3505 $passed = vec[];
3506 foreach ($results as $result) {
3507 if ($result['status'] === 'failed') {
3508 $failed[] = $result['name'];
3509 } else if ($result['status'] === 'passed') {
3510 $passed[] = $result['name'];
3513 sort(inout $failed);
3515 $failing_tests_file = $options->failure_file ??
3516 Status::getRunTmpDir() . '/test-failures';
3517 file_put_contents($failing_tests_file, implode("\n", $failed)."\n");
3518 if ($passed) {
3519 $passing_tests_file = Status::getRunTmpDir() . '/tests-passed';
3520 file_put_contents($passing_tests_file, implode("\n", $passed)."\n");
3521 } else {
3522 $passing_tests_file = "";
3525 print "\n".count($failed)." tests failed\n";
3526 if (!$options->no_fun) {
3527 // Unicode for table-flipping emoticon
3528 // https://knowyourmeme.com/memes/flipping-tables
3529 print "(\u{256F}\u{00B0}\u{25A1}\u{00B0}\u{FF09}\u{256F}\u{FE35} \u{253B}";
3530 print "\u{2501}\u{253B}\n";
3533 print_commands($failed, $options);
3535 print make_header("See failed test output and expectations:");
3536 foreach ($failed as $n => $test) {
3537 if ($n !== 0) print "\n";
3538 print 'cat ' . Status::getTestOutputPath($test, 'diff') . "\n";
3539 print 'cat ' . Status::getTestOutputPath($test, 'out') . "\n";
3540 $expect_file = get_expect_file_and_type($test, $options)[0];
3541 if ($expect_file is null) {
3542 print "# no expect file found for $test\n";
3543 } else {
3544 print "cat $expect_file\n";
3547 // only print 3 tests worth unless verbose is on
3548 if ($n === 2 && !$options->verbose) {
3549 $remaining = count($failed) - 1 - $n;
3550 if ($remaining > 0) {
3551 print make_header("... and $remaining more.");
3553 break;
3557 if ($passed) {
3558 print make_header(
3559 'For xargs, lists of failed and passed tests are available using:'
3561 print 'cat '.$failing_tests_file."\n";
3562 print 'cat '.$passing_tests_file."\n";
3563 } else {
3564 print make_header('For xargs, list of failures is available using:').
3565 'cat '.$failing_tests_file."\n";
3568 print
3569 make_header("Re-run just the failing tests:") .
3570 str_replace("run.php", "run", $argv[0]) . ' ' .
3571 implode(' ', \HH\global_get('recorded_options')) .
3572 sprintf(' $(cat %s)%s', $failing_tests_file, "\n");
3575 function port_is_listening(int $port): bool {
3576 $socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
3577 return @socket_connect($socket, 'localhost', $port);
3580 function find_open_port(): int {
3581 for ($i = 0; $i < 50; ++$i) {
3582 $port = rand(1024, 65535);
3583 if (!port_is_listening($port)) return $port;
3586 error("Couldn't find an open port");
3589 function start_server_proc(
3590 Options $options,
3591 string $config,
3592 int $port,
3593 ): Server {
3594 if ($options->cli_server) {
3595 $cli_sock = tempnam(sys_get_temp_dir(), 'hhvm-cli-');
3596 } else {
3597 // still want to test that an unwritable socket works...
3598 $cli_sock = '/var/run/hhvm-cli.sock';
3600 $threads = get_num_threads($options);
3601 $thread_option = $options->cli_server
3602 ? '-vEval.UnixServerWorkers='.$threads
3603 : '-vServer.ThreadCount='.$threads;
3604 $prelude = $options->server
3605 ? '-vEval.PreludePath=' . Status::getRunTmpDir() . '/server-prelude.php'
3606 : "";
3607 $command = hhvm_cmd_impl(
3608 $options,
3609 $config,
3610 null, // we do not pass Autoload.DB.Path to the server process
3611 '-m', 'server',
3612 "-vServer.Port=$port",
3613 "-vServer.Type=proxygen",
3614 "-vAdminServer.Port=0",
3615 $thread_option,
3616 '-vServer.ExitOnBindFail=1',
3617 '-vServer.RequestTimeoutSeconds='.SERVER_TIMEOUT,
3618 '-vPageletServer.ThreadCount=0',
3619 '-vLog.UseRequestLog=1',
3620 '-vLog.File=/dev/null',
3621 $prelude,
3623 // The server will unlink the temp file
3624 '-vEval.UnixServerPath='.$cli_sock,
3626 // This ensures we actually jit everything:
3627 '-vEval.JitRequireWriteLease=1',
3629 // The default test config uses a small TC but we'll be running thousands
3630 // of tests against the same process:
3631 '-vEval.JitASize=394264576',
3632 '-vEval.JitAColdSize=201326592',
3633 '-vEval.JitAFrozenSize=251658240',
3634 '-vEval.JitGlobalDataSize=32000000',
3636 // load/store counters don't work on Ivy Bridge so disable for tests
3637 '-vEval.ProfileHWEnable=false'
3639 if (count($command) !== 1) {
3640 error("Can't run multi-mode tests in server mode");
3642 $command = $command[0];
3643 if (getenv('HHVM_TEST_SERVER_LOG')) {
3644 echo "Starting server '$command'\n";
3647 $descriptors = dict[
3648 0 => vec['file', '/dev/null', 'r'],
3649 1 => vec['file', '/dev/null', 'w'],
3650 2 => vec['file', '/dev/null', 'w'],
3653 $dummy = null;
3654 $proc = proc_open($command, $descriptors, inout $dummy);
3655 if (!$proc) {
3656 error("Failed to start server process");
3658 $status = proc_get_status($proc); // dict<string, mixed>
3659 $pid = $status['pid'] as int;
3660 $server = new Server($proc, $pid, $port, $config, $cli_sock);
3661 return $server;
3664 final class Server {
3665 public function __construct(
3666 public resource $proc,
3667 public int $pid,
3668 public int $port,
3669 public string $config,
3670 public string $cli_socket,
3675 final class Servers {
3676 public dict<int, Server> $pids = dict[];
3677 public dict<string, Server> $configs = dict[];
3680 // For each config file in $configs, start up a server on a randomly-determined
3681 // port.
3682 function start_servers(
3683 Options $options,
3684 keyset<string> $configs,
3685 ): Servers {
3686 if ($options->server) {
3687 $prelude = <<<'EOT'
3688 <?hh
3689 <<__EntryPoint>> function UNIQUE_NAME_I_DONT_EXIST_IN_ANY_TEST(): void {
3690 putenv("HPHP_TEST_TMPDIR=BASEDIR{$_SERVER['SCRIPT_NAME']}.tmpdir");
3692 EOT;
3693 file_put_contents(
3694 Status::getRunTmpDir() . '/server-prelude.php',
3695 str_replace('BASEDIR', Status::getRunTmpDir(), $prelude),
3699 $starting = vec[];
3700 foreach ($configs as $config) {
3701 $starting[] = start_server_proc($options, $config, find_open_port());
3704 $start_time = mtime();
3705 $servers = new Servers();
3707 // Wait for all servers to come up.
3708 while (count($starting) > 0) {
3709 $still_starting = vec[];
3711 foreach ($starting as $server) {
3712 $config = $server->config;
3713 $pid = $server->pid;
3714 $port = $server->port;
3715 $proc = $server->proc;
3717 $new_status = proc_get_status($proc);
3719 if (!$new_status['running']) {
3720 if ($new_status['exitcode'] === 0) {
3721 error("Server exited prematurely but without error");
3724 // We lost a race. Try another port.
3725 if (getenv('HHVM_TEST_SERVER_LOG')) {
3726 echo "\n\nLost connection race on port $port. Trying another.\n\n";
3728 $port = find_open_port();
3729 $still_starting[] = start_server_proc($options, $config, $port);
3730 } else if (!port_is_listening($port)) {
3731 $still_starting[] = $server;
3732 } else {
3733 $servers->pids[$pid] = $server;
3734 $servers->configs[$config] = $server;
3738 $starting = $still_starting;
3739 $max_time = 10;
3740 if (mtime() - $start_time > $max_time) {
3741 error("Servers took more than $max_time seconds to come up");
3744 // Take a short nap and try again.
3745 usleep(100000);
3748 $elapsed = mtime() - $start_time;
3749 printf("Started %d servers in %.1f seconds\n\n", count($configs), $elapsed);
3750 return $servers;
3753 function get_num_threads(Options $options): int {
3754 if ($options->threads is nonnull) {
3755 $threads = (int)$options->threads;
3756 if ((string)$threads !== $options->threads || $threads < 1) {
3757 error("--threads must be an integer >= 1");
3759 } else {
3760 $threads = $options->server || $options->cli_server
3761 ? num_cpus() * 2 : num_cpus();
3763 return $threads;
3766 function runner_precheck(): void {
3767 // Basic checking for runner.
3768 $server = HH\global_get('_SERVER');
3769 $env = HH\global_get('_ENV');
3770 if (!((bool)$server ?? false) || !((bool)$env ?? false)) {
3771 echo "Warning: \$_SERVER/\$_ENV variables not available, please check \n" .
3772 "your ini setting: variables_order, it should have both 'E' and 'S'\n";
3776 function main(vec<string> $argv): int {
3777 runner_precheck();
3779 ini_set('pcre.backtrack_limit', PHP_INT_MAX);
3781 list($options, $files) = get_options($argv);
3782 if ($options->help) {
3783 error(help());
3786 Status::createTmpDir($options->working_dir);
3788 if ($options->list_tests) {
3789 list_tests($files, $options);
3790 print "\n";
3791 exit(0);
3794 $tests = find_tests($files, $options);
3795 if ($options->shuffle) {
3796 shuffle(inout $tests);
3799 // Explicit path given by --hhvm-binary-path takes priority. Then, if an
3800 // HHVM_BIN env var exists, and the file it points to exists, that trumps
3801 // any default hhvm executable path.
3802 if ($options->hhvm_binary_path is nonnull) {
3803 $binary_path = check_executable($options->hhvm_binary_path);
3804 putenv("HHVM_BIN=" . $binary_path);
3805 } else if (getenv("HHVM_BIN") !== false) {
3806 $binary_path = check_executable(getenv("HHVM_BIN"));
3807 } else {
3808 check_for_multiple_default_binaries();
3809 $binary_path = hhvm_path();
3812 if ($options->verbose) {
3813 print "You are using the binary located at: " . $binary_path . "\n";
3816 $servers = null;
3817 if ($options->server || $options->cli_server) {
3818 if ($options->server && $options->cli_server) {
3819 error("Server mode and CLI Server mode are mutually exclusive");
3821 if ($options->repo) {
3822 error("Server mode repo tests are not supported");
3825 /* We need to start up a separate server process for each config file
3826 * found. */
3827 $configs = keyset[];
3828 foreach ($tests as $test) {
3829 $config = find_file_for_dir(dirname($test), 'config.ini');
3830 if (!$config) {
3831 error("Couldn't find config file for $test");
3833 if (array_key_exists($config, $configs)) continue;
3834 if (should_skip_test_simple($options, $test) is nonnull) continue;
3835 $configs[] = $config;
3838 $max_configs = 30;
3839 if (count($configs) > $max_configs) {
3840 error("More than $max_configs unique config files will be needed to run ".
3841 "the tests you specified. They may not be a good fit for server ".
3842 "mode. (".count($configs)." required)");
3845 $servers = start_servers($options, $configs);
3846 $options->servers = $servers;
3849 // Try to construct the buckets so the test results are ready in
3850 // approximately alphabetical order.
3851 // Get the serial tests to be in their own bucket later.
3852 $serial_tests = serial_only_tests($tests);
3854 // If we have no serial tests, we can use the maximum number of allowed
3855 // threads for the test running. If we have some, we save one thread for
3856 // the serial bucket. However if we only have one thread, we don't split
3857 // out serial tests.
3858 $parallel_threads = min(get_num_threads($options), \count($tests)) as int;
3859 if ($parallel_threads === 1) {
3860 $test_buckets = vec[$tests];
3861 } else {
3862 if (count($serial_tests) > 0) {
3863 // reserve a thread for serial tests
3864 $parallel_threads--;
3867 $test_buckets = vec[];
3868 for ($i = 0; $i < $parallel_threads; $i++) {
3869 $test_buckets[] = vec[];
3872 $i = 0;
3873 foreach ($tests as $test) {
3874 if (!in_array($test, $serial_tests)) {
3875 $test_buckets[$i][] = $test;
3876 $i = ($i + 1) % $parallel_threads;
3880 if (count($serial_tests) > 0) {
3881 // The last bucket is serial.
3882 $test_buckets[] = $serial_tests;
3886 // Remember that the serial tests are also in the tests array too,
3887 // so they are part of the total count.
3888 if (!$options->testpilot) {
3889 print "Running ".count($tests)." tests in ".
3890 count($test_buckets)." threads (" . count($serial_tests) .
3891 " in serial)\n";
3894 if ($options->verbose) {
3895 Status::setMode(Status::MODE_VERBOSE);
3897 if ($options->testpilot) {
3898 Status::setMode(Status::MODE_TESTPILOT);
3900 if ($options->record_failures is nonnull) {
3901 Status::setMode(Status::MODE_RECORD_FAILURES);
3903 Status::setUseColor($options->color || posix_isatty(STDOUT));
3905 Status::$nofork = count($tests) === 1 && !$servers;
3907 if (!Status::$nofork) {
3908 // Create the Queue before any children are forked.
3909 $queue = Status::getQueue();
3911 // Fork a "printer" child to process status messages.
3912 $printer_pid = pcntl_fork();
3913 if ($printer_pid === -1) {
3914 error("failed to fork");
3915 } else if ($printer_pid === 0) {
3916 msg_loop(count($tests), $queue);
3917 return 0;
3919 } else {
3920 // Satisfy the type-checker.
3921 $printer_pid = -1;
3924 // Unblock the Queue (if needed).
3925 Status::started();
3927 // Fork "worker" children (if needed).
3928 $children = dict[];
3929 // We write results as json in each child and collate them at the end
3930 $json_results_files = vec[];
3931 if (Status::$nofork) {
3932 Status::registerCleanup($options->no_clean);
3933 $json_results_file = tempnam('/tmp', 'test-run-');
3934 $json_results_files[] = $json_results_file;
3935 invariant(count($test_buckets) === 1, "nofork was set erroneously");
3936 $return_value = child_main($options, $test_buckets[0], $json_results_file);
3937 } else {
3938 foreach ($test_buckets as $test_bucket) {
3939 $json_results_file = tempnam('/tmp', 'test-run-');
3940 $json_results_files[] = $json_results_file;
3941 $pid = pcntl_fork();
3942 if ($pid === -1) {
3943 error('could not fork');
3944 } else if ($pid) {
3945 $children[$pid] = $pid;
3946 } else {
3947 invariant($test_bucket is vec<_>, "%s", __METHOD__);
3948 exit(child_main($options, $test_bucket, $json_results_file));
3952 // Make sure to clean up on exit, or on SIGTERM/SIGINT.
3953 // Do this here so no children inherit this.
3954 Status::registerCleanup($options->no_clean);
3956 // Have the parent wait for all forked children to exit.
3957 $return_value = 0;
3958 while (count($children) && $printer_pid !== 0) {
3959 $status = null;
3960 $pid = pcntl_wait(inout $status);
3961 if (pcntl_wifexited($status as nonnull)) {
3962 $bad_end = pcntl_wexitstatus($status) !== 0;
3963 } else if (pcntl_wifsignaled($status)) {
3964 $bad_end = true;
3965 } else {
3966 error("Unexpected exit status from child");
3969 if ($pid === $printer_pid) {
3970 // We should be finishing up soon.
3971 $printer_pid = 0;
3972 if ($bad_end) {
3973 // Don't consider the run successful if the printer worker died
3974 $return_value = 1;
3976 } else if ($servers && isset($servers->pids[$pid])) {
3977 // A server crashed. Restart it.
3978 // We intentionally ignore $bad_end here because we expect this to
3979 // show up as a test failure in whatever test was running on the server
3980 // when it crashed. TODO(alexeyt): assert $bad_end === true?
3981 if (getenv('HHVM_TEST_SERVER_LOG')) {
3982 echo "\nServer $pid crashed. Restarting.\n";
3984 Status::serverRestarted();
3985 $server = $servers->pids[$pid];
3986 $server = start_server_proc($options, $server->config, $server->port);
3988 // Unset the old $pid entry and insert the new one.
3989 unset($servers->pids[$pid]);
3990 $pid = $server->pid;
3991 $servers->pids[$pid] = $server;
3992 } else if (isset($children[$pid])) {
3993 unset($children[$pid]);
3994 if ($bad_end) {
3995 // If any worker process dies we should fail the test run
3996 $return_value = 1;
3998 } else {
3999 error("Got status for child that we didn't know we had with pid $pid");
4004 Status::finished($return_value);
4006 // Wait for the printer child to exit, if needed.
4007 if (!Status::$nofork && $printer_pid !== 0) {
4008 $status = 0;
4009 $pid = pcntl_waitpid($printer_pid, inout $status);
4010 $status = $status as int;
4011 if (pcntl_wifexited($status)) {
4012 if (pcntl_wexitstatus($status) !== 0) {
4013 // Don't consider the run successful if the printer worker died
4014 $return_value = 1;
4016 } else if (pcntl_wifsignaled($status)) {
4017 // Don't consider the run successful if the printer worker died
4018 $return_value = 1;
4019 } else {
4020 error("Unexpected exit status from child");
4024 // Kill the servers.
4025 if ($servers) {
4026 foreach ($servers->pids as $server) {
4027 proc_terminate($server->proc);
4028 proc_close($server->proc);
4032 // Aggregate results.
4033 $results = dict[];
4034 foreach ($json_results_files as $json_results_file) {
4035 $contents = file_get_contents($json_results_file);
4036 $json = json_decode($contents, true);
4037 if (!is_dict($json)) {
4038 error(
4039 "\nNo JSON output was received from a test thread. ".
4040 "Either you killed it, or it might be a bug in the test script.",
4043 $results = array_merge($results, $json);
4044 unlink($json_results_file);
4047 // Print results.
4048 if ($options->record_failures is nonnull) {
4049 $fail_file = $options->record_failures;
4050 $failed_tests = vec[];
4051 $prev_failing = vec[];
4052 if (file_exists($fail_file)) {
4053 $prev_failing = explode("\n", file_get_contents($fail_file));
4056 $new_fails = 0;
4057 $new_passes = 0;
4058 foreach ($results as $r) {
4059 if (!isset($r['name']) || !isset($r['status'])) continue;
4060 $test = canonical_path($r['name']);
4061 $status = $r['status'];
4062 if ($status === 'passed' && in_array($test, $prev_failing)) {
4063 $new_passes++;
4064 continue;
4066 if ($status !== 'failed') continue;
4067 if (!in_array($test, $prev_failing)) $new_fails++;
4068 $failed_tests[] = $test;
4070 printf(
4071 "Recording %d tests as failing.\n".
4072 "There are %d new failing tests, and %d new passing tests.\n",
4073 count($failed_tests), $new_fails, $new_passes
4075 sort(inout $failed_tests);
4076 file_put_contents($fail_file, implode("\n", $failed_tests));
4077 } else if ($options->testpilot) {
4078 Status::say(dict['op' => 'all_done', 'results' => $results]);
4079 return $return_value;
4080 } else if (!$return_value) {
4081 print_success($tests, $results, $options);
4082 } else {
4083 print_failure($argv, $results, $options);
4086 Status::sayColor("\nTotal time for all executed tests as run: ",
4087 Status::BLUE,
4088 sprintf("%.2fs\n",
4089 Status::getOverallEndTime() -
4090 Status::getOverallStartTime()));
4091 Status::sayColor("Total time for all executed tests if run serially: ",
4092 Status::BLUE,
4093 sprintf("%.2fs\n",
4094 Status::addTestTimesSerial($results)));
4096 return $return_value;
4099 <<__EntryPoint>>
4100 function run_main(): void {
4101 exit(main(get_argv()));
4104 // Inline ASCII art moved to end-of-file to avoid confusing emacs.
4106 function print_clown(): void {
4107 print <<<CLOWN
4110 /*\\
4111 /_*_\\
4112 {('o')}
4113 C{{([^*^])}}D
4114 [ * ]
4115 / Y \\
4116 _\\__|__/_
4117 (___/ \\___)
4118 CLOWN
4119 ."\n\n";
4122 function print_skipper(): void {
4123 print <<<SKIPPER
4127 / ,"
4128 .-------.--- /
4129 "._ __.-/ o. o\
4130 " ( Y )
4134 .-" |
4135 / _ \ \
4136 / `. ". ) /' )
4137 Y )( / /(,/
4138 ,| / )
4139 ( | / /
4140 " \_ (__ (__
4141 "-._,)--._,)
4142 SKIPPER
4143 ."\n\n";
4146 function print_ship(): void {
4147 print <<<SHIP
4148 | | |
4149 )_) )_) )_)
4150 )___))___))___)\
4151 )____)____)_____)\\
4152 _____|____|____|____\\\__
4153 ---------\ SHIP IT /---------
4154 ^^^^^ ^^^^^^^^^^^^^^^^^^^^^
4155 ^^^^ ^^^^ ^^^ ^^
4156 ^^^^ ^^^
4157 SHIP
4158 ."\n";