PageRenderTime 827ms CodeModel.GetById 57ms app.highlight 7ms RepoModel.GetById 1ms app.codeStats 0ms

/hphp/test/run

http://github.com/facebook/hiphop-php
PHP | 2563 lines | 1986 code | 250 blank | 327 comment | 340 complexity | 01170663f231c4a4d7d55fde22cf6d9f MD5 | raw file

Large files files are truncated, but you can click here to view the full file

   1#!/usr/bin/env php
   2<?php
   3/**
   4* Run the test suites in various configurations.
   5*/
   6
   7function is_testing_dso_extension() {
   8  // detecting if we're running outside of the hhvm codebase.
   9  return !is_file(__DIR__ . "/../../hphp/test/run");
  10}
  11
  12function get_expect_file_and_type($test, $options) {
  13  // .typechecker files are for typechecker (hh_server --check) test runs.
  14  $types = null;
  15  if (isset($options['typechecker'])) {
  16    $types = array('typechecker.expect', 'typechecker.expectf');
  17  } else {
  18    $types = array('expect', 'hhvm.expect', 'expectf', 'hhvm.expectf',
  19                   'expectregex');
  20  }
  21  if (isset($options['repo'])) {
  22    foreach ($types as $type) {
  23      $fname = "$test.$type-repo";
  24      if (file_exists($fname)) {
  25        return array($fname, $type);
  26      }
  27    }
  28  }
  29  foreach ($types as $type) {
  30    $fname = "$test.$type";
  31    if (file_exists($fname)) {
  32      return array($fname, $type);
  33    }
  34  }
  35  return array(null, null);
  36}
  37
  38function usage() {
  39  global $argv;
  40  return "usage: $argv[0] [-m jit|interp] [-r] <test/directories>";
  41}
  42
  43function help() {
  44  global $argv;
  45  $ztestexample = 'test/zend/good/*/*z*.php'; // sep. for syntax highlighting.
  46  $help = <<<EOT
  47
  48
  49This is the hhvm test-suite runner.  For more detailed documentation,
  50see hphp/test/README.md.
  51
  52The test argument may be a path to a php test file, a directory name, or
  53one of a few pre-defined suite names that this script knows about.
  54
  55If you work with hhvm a lot, you might consider a bash alias:
  56
  57   alias ht="path/to/hphp/test/run"
  58
  59Examples:
  60
  61  # Quick tests in JIT mode:
  62  % $argv[0] test/quick
  63
  64  # Slow tests in interp mode:
  65  % $argv[0] -m interp test/slow
  66
  67  # PHP specificaion tests in JIT mode:
  68  % $argv[0] test/spec
  69
  70  # Slow closure tests in JIT mode:
  71  % $argv[0] test/slow/closure
  72
  73  # Slow closure tests in JIT mode with RepoAuthoritative:
  74  % $argv[0] -r test/slow/closure
  75
  76  # Slow array tests, in RepoAuthoritative:
  77  % $argv[0] -r test/slow/array
  78
  79  # Zend tests with a "z" in their name:
  80  % $argv[0] $ztestexample
  81
  82  # Quick tests in JIT mode with some extra runtime options:
  83  % $argv[0] test/quick -a '-vEval.JitMaxTranslations=120 -vEval.HHIRRefcountOpts=0'
  84
  85  # All quick tests except debugger
  86  % $argv[0] -e debugger test/quick
  87
  88  # All tests except those containing a string of 3 digits
  89  % $argv[0] -E '/\d{3}/' all
  90
  91  # All tests whose name containing pdo_mysql
  92  % $argv[0] -i pdo_mysql -m jit -r zend
  93
  94  # Print all the standard tests
  95  % $argv[0] --list-tests
  96
  97  # Use a specific HHVM binary
  98  % $argv[0] -b ~/code/hhvm/hphp/hhvm/hhvm
  99  % $argv[0] --hhvm-binary-path ~/code/hhvm/hphp/hhvm/hhvm
 100
 101  # Use relocation to run tests in the same thread. e.g, 6 times in the same thread,
 102  # where the 3 specifies a random relocation for the 3rd request and the test is
 103  # run 3 * 2 times.
 104  % $argv[0] --relocate 3 test/quick/silencer.php
 105
 106  # Run the Hack typechecker against quick typechecker.expect[f] files
 107  # Could explcitly use quick here too
 108  # $argv[0] --typechecker
 109
 110  # Run the Hack typechecker against typechecker.expect[f] files in the slow
 111  # directory
 112  # $argv[0] --typechecker slow
 113
 114  # Run the Hack typechecker against the typechecker.expect[f] file in this test
 115  # $argv[0] --typechecker test/slow/test_runner_typechecker_mode/basic.php
 116
 117  # Use a specific typechecker binary
 118  # $argv[0] --hhserver-binary-path ~/code/hhvm/hphp/hack/bin/hh_server --typechecker .
 119
 120EOT;
 121  return usage().$help;
 122}
 123
 124function error($message) {
 125  print "$message\n";
 126  exit(1);
 127}
 128
 129// If a user-supplied path is provided, let's make sure we have a valid
 130// executable.
 131function check_executable($path, $typechecker) {
 132  $type = $typechecker ? "HH_SERVER" : "HHVM";
 133  $rpath = realpath($path);
 134  $msg = "Provided ".$type." executable (".$path.") is not a file.\n"
 135       . "If using ".$type."_BIN, make sure that is set correctly.";
 136  if (!is_file($rpath)) {
 137    error($msg);
 138  }
 139  $output = array();
 140  exec($rpath . " --help", $output);
 141  $str = implode($output);
 142  $msg = "Provided file (".$rpath.") is not a/an ".$type." executable.\n"
 143       . "If using ".$type."_BIN, make sure that is set correctly.";
 144  if (strpos($str, "Usage") !== 0) {
 145    error($msg);
 146  }
 147}
 148
 149function hhvm_binary_routes() {
 150  $routes = array(
 151    "fbbuild" => "/_bin/hphp/hhvm",
 152    "buck"    => "/buck-out/gen/hphp/hhvm/hhvm",
 153    "cmake"   => "/hphp/hhvm"
 154  );
 155
 156  $env_root = getenv("FBMAKE_BIN_ROOT");
 157  if ($env_root !== false) {
 158    $routes["fbbuild"] = "/" . $env_root . "/hphp/hhvm";
 159  }
 160  return $routes;
 161}
 162
 163function hh_server_binary_routes() {
 164  return array(
 165    "fbbuild" => "/_bin/hphp/hack/src",
 166    "buck"    => "/buck-out/gen/hphp/hack/src/hh_server",
 167    "cmake"   => "/hphp/hack/bin"
 168  );
 169  $env_root = getenv("FBMAKE_BIN_ROOT");
 170  if ($env_root !== false) {
 171    $routes["fbbuild"] = "/" . $env_root . "/hphp/hack/src";
 172  }
 173  return $routes;
 174}
 175
 176// For Facebook: We have several build systems, and we can use any of them in
 177// the same code repo.  If multiple binaries exist, we want the onus to be on
 178// the user to specify a particular one because before we chose the fbmake one
 179// by default and that could cause unexpected results.
 180function check_for_multiple_default_binaries($typechecker) {
 181  // Env var we use in testing that'll pick which build system to use.
 182  if (getenv("FBCODE_BUILD_TOOL") !== false) {
 183    return;
 184  }
 185
 186  $home = hphp_home();
 187  $routes = $typechecker ? hh_server_binary_routes() : hhvm_binary_routes();
 188  $binary = $typechecker ? "hh_server" : "hhvm";
 189
 190  $found = array();
 191  foreach ($routes as $_ => $path) {
 192    $abs_path = $home . $path . "/" . $binary;
 193    if (file_exists($abs_path)) {
 194      $found[] = $abs_path;
 195    }
 196  }
 197
 198  if (count($found) <= 1) {
 199    return;
 200  }
 201
 202  $path_option = $typechecker ? "--hhserver-binary-path" : "--hhvm-binary-path";
 203
 204  $msg = "Multiple binaries exist in this repo. \n";
 205  foreach ($found as $bin) {
 206    $msg .= " - " . $bin . "\n";
 207  }
 208  $msg .= "Are you in fbcode?  If so, remove a binary \n"
 209    . "or use the " . $path_option . " option to the test runner. \n"
 210    . "e.g., test/run ";
 211  if ($typechecker) {
 212    $msg .= "--typechecker";
 213  }
 214  $msg .= " " . $path_option . " /path/to/binary slow\n";
 215  error($msg);
 216}
 217
 218function hphp_home() {
 219  if (is_testing_dso_extension()) {
 220    return realpath(__DIR__);
 221  }
 222  return realpath(__DIR__.'/../..');
 223}
 224
 225function idx($array, $key, $default = null) {
 226  return isset($array[$key]) ? $array[$key] : $default;
 227}
 228
 229function hhvm_path() {
 230  $file = "";
 231  if (getenv("HHVM_BIN") !== false) {
 232    $file = realpath(getenv("HHVM_BIN"));
 233  } else {
 234    $file = bin_root().'/hhvm';
 235  }
 236
 237  if (!is_file($file)) {
 238    if (is_testing_dso_extension()) {
 239      exec("which hhvm", $output);
 240      if (isset($output[0]) && $output[0]) {
 241        return $output[0];
 242      }
 243      error("You need to specify hhvm bin with env HHVM_BIN");
 244    }
 245
 246    error("$file doesn't exist. Did you forget to build first?");
 247  }
 248  return rel_path($file);
 249}
 250
 251function bin_root() {
 252  if (getenv("HHVM_BIN") !== false) {
 253    return dirname(realpath(getenv("HHVM_BIN")));
 254  }
 255
 256  $home = hphp_home();
 257  $env_tool = getenv("FBCODE_BUILD_TOOL");
 258  $routes = hhvm_binary_routes();
 259
 260  if ($env_tool !== false) {
 261    return $home . $routes[$env_tool];
 262  }
 263
 264  foreach ($routes as $_ => $path) {
 265    $dir = $home . $path;
 266    if (is_dir($dir)) {
 267      return $dir;
 268    }
 269  }
 270
 271  return $home . $routes["cmake"];
 272}
 273
 274function hh_server_path() {
 275  $file = "";
 276  if (getenv("HH_SERVER_BIN") !== false) {
 277    $file = realpath(getenv("HH_SERVER_BIN"));
 278  } else {
 279    $file = hh_server_bin_root().'/hh_server';
 280  }
 281  if (!is_file($file)) {
 282    error("$file doesn't exist. Did you forget to build first?");
 283  }
 284  return rel_path($file);
 285}
 286
 287function hh_server_bin_root() {
 288  if (getenv("HH_SERVER_BIN") !== false) {
 289    return dirname(realpath(getenv("HH_SERVER_BIN")));
 290  }
 291
 292  $home = hphp_home();
 293  $env_tool = getenv("FBCODE_BUILD_TOOL");
 294  $routes = hh_server_binary_routes();
 295
 296  if ($env_tool !== false) {
 297    return $home . $routes[$env_tool];
 298  }
 299
 300  foreach ($routes as $_ => $path) {
 301    $dir = $home . $path;
 302    if (is_dir($dir)) {
 303      return $dir;
 304    }
 305  }
 306
 307  return $home . $routes["cmake"];
 308}
 309
 310function verify_hhbc() {
 311  if (getenv("VERIFY_HHBC") !== false) {
 312    return getenv($env_hhbc);
 313  }
 314  return bin_root().'/verify.hhbc';
 315}
 316
 317function read_opts_file($file) {
 318  if (!file_exists($file)) {
 319    return "";
 320  }
 321
 322  $fp = fopen($file, "r");
 323
 324  $contents = "";
 325  while ($line = fgets($fp)) {
 326    // Compress out white space.
 327    $line = preg_replace('/\s+/', ' ', $line);
 328
 329    // Discard simple line oriented ; and # comments to end of line
 330    // Comments at end of line (after payload) are not allowed.
 331    $line = preg_replace('/^ *;.*$/', ' ', $line);
 332    $line = preg_replace('/^ *#.*$/', ' ', $line);
 333
 334    // Substitute in the directory name
 335    $line = str_replace('__DIR__', dirname($file), $line);
 336
 337    $contents .= $line;
 338  }
 339  fclose($fp);
 340  return $contents;
 341}
 342
 343// http://stackoverflow.com/questions/2637945/
 344function rel_path($to) {
 345  $from     = explode('/', getcwd().'/');
 346  $to       = explode('/', $to);
 347  $relPath  = $to;
 348
 349  foreach ($from as $depth => $dir) {
 350    // find first non-matching dir.
 351    if ($dir === $to[$depth]) {
 352      // ignore this directory.
 353      array_shift($relPath);
 354    } else {
 355      // get number of remaining dirs to $from.
 356      $remaining = count($from) - $depth;
 357      if ($remaining > 1) {
 358        // add traversals up to first matching dir.
 359        $padLength = (count($relPath) + $remaining - 1) * -1;
 360        $relPath = array_pad($relPath, $padLength, '..');
 361        break;
 362      } else {
 363        $relPath[0] = './' . $relPath[0];
 364      }
 365    }
 366  }
 367  return implode('/', $relPath);
 368}
 369
 370function get_options($argv) {
 371  $parameters = array(
 372    'exclude:' => 'e:',
 373    'exclude-pattern:' => 'E:',
 374    'include:' => 'i:',
 375    'include-pattern:' => 'I:',
 376    'repo' => 'r',
 377    'mode:' => 'm:',
 378    'server' => 's',
 379    'shuffle' => '',
 380    'help' => 'h',
 381    'verbose' => 'v',
 382    'fbmake' => '',
 383    'testpilot' => '',
 384    'threads:' => '',
 385    'args:' => 'a:',
 386    'log' => 'l',
 387    'failure-file:' => '',
 388    'arm' => '',
 389    'wholecfg' => '',
 390    'hhas-round-trip' => '',
 391    'color' => 'c',
 392    'no-fun' => '',
 393    'cores' => '',
 394    'no-clean' => '',
 395    'list-tests' => '',
 396    'relocate:' => '',
 397    'recycle-tc:' => '',
 398    'hhvm-binary-path:' => 'b:',
 399    'typechecker' => '',
 400    'hhserver-binary-path:' => '',
 401  );
 402  $options = array();
 403  $files = array();
 404
 405  /*
 406   * '-' argument causes all future arguments to be treated as filenames, even
 407   * if they would otherwise match a valid option. Otherwise, arguments starting
 408   * with '-' MUST match a valid option.
 409   */
 410  $force_file = false;
 411
 412  for ($i = 1; $i < count($argv); $i++) {
 413    $arg = $argv[$i];
 414
 415    if (strlen($arg) == 0) {
 416      continue;
 417    } else if ($force_file) {
 418      $files[] = $arg;
 419    } else if ($arg === '-') {
 420      $forcefile = true;
 421    } else if ($arg[0] === '-') {
 422      $found = false;
 423
 424      foreach ($parameters as $long => $short) {
 425        if ($arg == '-'.str_replace(':', '', $short) ||
 426            $arg == '--'.str_replace(':', '', $long)) {
 427          if (substr($long, -1, 1) == ':') {
 428            $value = $argv[++$i];
 429          } else {
 430            $value = true;
 431          }
 432          $options[str_replace(':', '', $long)] = $value;
 433          $found = true;
 434          break;
 435        }
 436      }
 437
 438      if (!$found) {
 439        error(sprintf("Invalid argument: '%s'\nSee $argv[0] --help", $arg));
 440      }
 441    } else {
 442      $files[] = $arg;
 443    }
 444  }
 445
 446  if (isset($options['repo']) && isset($options['hhas-round-trip'])) {
 447    echo "repo and hhas-round-trip are mutually exclusive options\n";
 448    exit(1);
 449  }
 450
 451  if (isset($options['relocate']) && isset($options['recycle-tc'])) {
 452    echo "relocate and recycle-tc are mutually exclusive options\n";
 453    exit(1);
 454  }
 455
 456  return array($options, $files);
 457}
 458
 459/*
 460 * We support some 'special' file names, that just know where the test
 461 * suites are, to avoid typing 'hphp/test/foo'.
 462 */
 463function find_test_files($file) {
 464  $mappage = array(
 465    'quick'      => 'hphp/test/quick',
 466    'slow'       => 'hphp/test/slow',
 467    'spec'       => 'hphp/test/spec',
 468    'debugger'   => 'hphp/test/server/debugger/tests',
 469    'http'       => 'hphp/test/server/http/tests',
 470    'fastcgi'    => 'hphp/test/server/fastcgi/tests',
 471    'zend'       => 'hphp/test/zend/good',
 472    'facebook'   => 'hphp/facebook/test',
 473
 474    // Subsets of zend tests.
 475    'zend_ext'    => 'hphp/test/zend/good/ext',
 476    'zend_ext_am' => 'hphp/test/zend/good/ext/[a-m]*',
 477    'zend_ext_nz' => 'hphp/test/zend/good/ext/[n-z]*',
 478    'zend_Zend'   => 'hphp/test/zend/good/Zend',
 479    'zend_tests'  => 'hphp/test/zend/good/tests',
 480    'zend_bad'    => 'hphp/test/zend/bad',
 481  );
 482
 483  if (isset($mappage[$file])) {
 484    $matches = glob(hphp_home().'/'.$mappage[$file]);
 485    if (count($matches) == 0) {
 486      error(sprintf(
 487        "Convenience test name '%s' is recognized but does not match any test ".
 488        "files (pattern = '%s', hphp_home = '%s')",
 489        $file, $mappage[$file], hphp_home()));
 490    }
 491    return $matches;
 492  } else {
 493    return array($file, );
 494  }
 495}
 496
 497// Some tests have to be run together in the same test bucket, serially, one
 498// after other in order to avoid races and other collisions.
 499function serial_only_tests($tests) {
 500  if (is_testing_dso_extension()) {
 501    return array();
 502  }
 503  // Add a <testname>.php.serial file to make your test run in the serial
 504  // bucket.
 505  $serial_tests = array_filter(
 506    $tests,
 507    function($test) {
 508      return file_exists($test . '.serial');
 509    }
 510  );
 511  return $serial_tests;
 512}
 513
 514function find_tests($files, array $options = null) {
 515  if (!$files) {
 516    $files = array('quick');
 517  }
 518  if ($files == array('all')) {
 519    $files = array('quick', 'slow', 'spec', 'zend', 'fastcgi');
 520  }
 521  $ft = array();
 522  foreach ($files as $file) {
 523    $ft = array_merge($ft, find_test_files($file));
 524  }
 525  $files = $ft;
 526  foreach ($files as &$file) {
 527    if (!@stat($file)) {
 528      error("Not valid file or directory: '$file'");
 529    }
 530    $file = preg_replace(',//+,', '/', realpath($file));
 531    $file = preg_replace(',^'.getcwd().'/,', '', $file);
 532  }
 533  $files = array_map('escapeshellarg', $files);
 534  $files = implode(' ', $files);
 535  if (isset($options['typechecker'])) {
 536    $tests = explode("\n", shell_exec(
 537      "find $files -name '*.php' -o -name '*.php.type-errors'"
 538    ));
 539    // The above will get all the php files. Now filter out only the ones
 540    // that have a .hhconfig associated with it.
 541    $tests = array_filter(
 542      $tests,
 543      function($test) {
 544        return (file_exists($test . '.typechecker.expect') ||
 545                file_exists($test . '.typechecker.expectf')) &&
 546                file_exists($test . '.hhconfig');
 547      }
 548    );
 549  } else {
 550    $tests = explode("\n", shell_exec(
 551      "find $files -name '*.php' -o -name '*.php.type-errors' " .
 552      "-o -name '*.hhas' | grep -v round_trip.hhas"
 553    ));
 554    $tests = array_filter(
 555      $tests,
 556      function($test) {
 557        return file_exists($test . '.expect') ||
 558               file_exists($test . '.expectf') ||
 559               file_exists($test . '.hhvm.expect') ||
 560               file_exists($test . '.hhvm.expectf');
 561      }
 562    );
 563  }
 564  if (!$tests) {
 565    error("Could not find any tests associated with your options.\n" .
 566          "Make sure your test path is correct and that you have " .
 567          "the right expect files for the tests you are trying to run.\n" .
 568          usage());
 569  }
 570  asort($tests);
 571  $tests = array_filter($tests);
 572  if (!empty($options['exclude'])) {
 573    $exclude = $options['exclude'];
 574    $tests = array_filter($tests, function($test) use ($exclude) {
 575      return (false === strpos($test, $exclude));
 576    });
 577  }
 578  if (!empty($options['exclude-pattern'])) {
 579    $exclude = $options['exclude-pattern'];
 580    $tests = array_filter($tests, function($test) use ($exclude) {
 581      return !preg_match($exclude, $test);
 582    });
 583  }
 584  if (!empty($options['include'])) {
 585    $include = $options['include'];
 586    $tests = array_filter($tests, function($test) use ($include) {
 587      return (false !== strpos($test, $include));
 588    });
 589  }
 590  if (!empty($options['include-pattern'])) {
 591    $include = $options['include-pattern'];
 592    $tests = array_filter($tests, function($test) use ($include) {
 593      return preg_match($include, $test);
 594    });
 595  }
 596  return $tests;
 597}
 598
 599function list_tests($files, $options) {
 600  $args = array();
 601  $mode = idx($options, 'mode', '');
 602  switch ($mode) {
 603    case '':
 604    case 'jit':
 605      $args[] = '-m jit';
 606      break;
 607    case 'interp';
 608      $args[] = '-m interp';
 609      break;
 610    default:
 611      throw new Exception("Unsupported mode for listing tests: ".$mode);
 612  }
 613
 614  if (isset($options['repo'])) {
 615    $args[] = '-r';
 616  }
 617
 618  if (isset($options['relocate'])) {
 619    $args[] = '--relocate';
 620    $args[] = $options['relocate'];
 621  }
 622
 623  if (isset($options['recycle-tc'])) {
 624    $args[] = '--recycle-tc';
 625    $args[] = $options['recycle-tc'];
 626  }
 627
 628  foreach (find_tests($files, $options) as $test) {
 629    print Status::jsonEncode(array(
 630      'args' => implode(' ', $args),
 631      'name' => $test,
 632    ))."\n";
 633  }
 634}
 635
 636function find_test_ext($test, $ext) {
 637  if (is_file("{$test}.{$ext}")) {
 638    return "{$test}.{$ext}";
 639  }
 640  return find_file_for_dir(dirname($test), "config.{$ext}");
 641}
 642
 643function find_file($test, $name) {
 644  return find_file_for_dir(dirname($test), $name);
 645}
 646
 647function find_file_for_dir($dir, $name) {
 648  // Handle the case where the $dir might come in as '.' because you
 649  // are running the test runner on a file from the same directory as
 650  // the test e.g., './mytest.php'. dirname() will give you the '.' when
 651  // you actually have a lot of path to traverse upwards like
 652  // /home/you/code/tests/mytest.php. Use realpath() to get that.
 653  $dir = realpath($dir);
 654  while ($dir !== '/' && is_dir($dir)) {
 655    $file = "$dir/$name";
 656    if (is_file($file)) {
 657      return $file;
 658    }
 659    $dir = dirname($dir);
 660  }
 661  $file = __DIR__.'/'.$name;
 662  if (file_exists($file)) {
 663    return $file;
 664  }
 665  return null;
 666}
 667
 668function find_debug_config($test, $name) {
 669  $debug_config = find_file_for_dir(dirname($test), $name);
 670  if ($debug_config !== null) {
 671    return "-m debug --debug-config ".$debug_config;
 672  }
 673  return "";
 674}
 675
 676function mode_cmd($options) {
 677  $repo_args = '';
 678  if (!isset($options['repo'])) {
 679    // Set the non-repo-mode shared repo.
 680    // When in repo mode, we set our own central path.
 681    $repo_args = "-vRepo.Local.Mode=-- -vRepo.Central.Path=".verify_hhbc();
 682  }
 683  $jit_args = "$repo_args -vEval.Jit=true";
 684  $mode = idx($options, 'mode', '');
 685  switch ($mode) {
 686    case '':
 687    case 'jit':
 688      return "$jit_args";
 689    case 'pgo':
 690      return $jit_args.
 691        ' -vEval.JitPGO=1'.
 692        ' -vEval.JitPGORegionSelector=hottrace'.
 693        ' -vEval.JitPGOHotOnly=0';
 694    case 'interp':
 695      return "$repo_args -vEval.Jit=0";
 696    default:
 697      error("-m must be one of jit | pgo | interp. Got: '$mode'");
 698  }
 699}
 700
 701function extra_args($options) {
 702  return idx($options, 'args', '');
 703}
 704
 705function hhvm_cmd_impl() {
 706  $args = func_get_args();
 707  $options = array_shift($args);
 708  $config = array_shift($args);
 709  $extra_args = $args;
 710  $args = array(
 711    hhvm_path(),
 712    '-c',
 713    $config,
 714    '-vEval.EnableArgsInBacktraces=true',
 715    '-vEval.EnableIntrinsicsExtension=true',
 716    mode_cmd($options),
 717    isset($options['arm']) ? '-vEval.SimulateARM=1' : '',
 718    isset($options['wholecfg']) ? '-vEval.JitPGORegionSelector=wholecfg' : '',
 719    extra_args($options),
 720  );
 721
 722  if (isset($options['relocate'])) {
 723    $args[] = '--count='.($options['relocate'] * 2);
 724    $args[] = '-vEval.JitAHotSize=6000000';
 725    $args[] = '-vEval.HotFuncCount=0';
 726    $args[] = '-vEval.PerfRelocate='.$options['relocate'];
 727  }
 728
 729  if (isset($options['recycle-tc'])) {
 730    $args[] = '--count='.$options['recycle-tc'];
 731    $args[] = '-vEval.StressUnitCacheFreq=1';
 732    $args[] = '-vEval.EnableReusableTC=true';
 733  }
 734
 735  if (!isset($options['cores'])) {
 736    $args[] = '-vResourceLimit.CoreFileSize=0';
 737  }
 738
 739  return implode(' ', array_merge($args, $extra_args));
 740}
 741
 742// Return the command and the env to run it in.
 743function hhvm_cmd($options, $test, $test_run = null) {
 744  if ($test_run === null) {
 745    $test_run = $test;
 746  }
 747  // hdf support is only temporary until we fully migrate to ini
 748  // Discourage broad use.
 749  $hdf_suffix = ".use.for.ini.migration.testing.only.hdf";
 750  $hdf = file_exists($test.$hdf_suffix)
 751       ? '-c ' . $test . $hdf_suffix
 752       : "";
 753  $cmd = hhvm_cmd_impl(
 754    $options,
 755    find_test_ext($test, 'ini'),
 756    $hdf,
 757    find_debug_config($test, 'hphpd.ini'),
 758    read_opts_file(find_test_ext($test, 'opts')),
 759    '--file',
 760    escapeshellarg($test_run)
 761  );
 762
 763  // Special support for tests that require a path to the current
 764  // test directory for things like prepend_file and append_file
 765  // testing.
 766  if (file_exists($test.'.ini')) {
 767    $contents = file_get_contents($test.'.ini');
 768    if (strpos($contents, '{PWD}') !== false) {
 769      $test_ini = tempnam('/tmp', $test).'.ini';
 770      file_put_contents($test_ini,
 771                        str_replace('{PWD}', dirname($test), $contents));
 772      $cmd .= " -c $test_ini";
 773    }
 774  }
 775  if ($hdf !== "") {
 776    $contents = file_get_contents($test.$hdf_suffix);
 777    if (strpos($contents, '{PWD}') !== false) {
 778      $test_hdf = tempnam('/tmp', $test).$hdf_suffix;
 779      file_put_contents($test_hdf,
 780                        str_replace('{PWD}', dirname($test), $contents));
 781      $cmd .= " -c $test_hdf";
 782    }
 783  }
 784
 785  if (isset($options['repo'])) {
 786    $hhbbc_repo = "\"$test.repo/hhvm.hhbbc\"";
 787    $cmd .= ' -vRepo.Authoritative=true -vRepo.Commit=0';
 788    $cmd .= " -vRepo.Central.Path=$hhbbc_repo";
 789  }
 790
 791  // Command line arguments
 792  $cli_args = find_test_ext($test, 'cli_args');
 793  if ($cli_args !== null) {
 794    $cmd .= " " . file_get_contents($cli_args);
 795  }
 796
 797  $env = $_ENV;
 798  // If there's an <test name>.env file then inject the contents of that into
 799  // the test environment.
 800  $env_file = find_test_ext($test, 'env');
 801  if ($env_file !== null) {
 802    $extra_env = explode("\n", trim(file_get_contents($env_file)));
 803    foreach ($extra_env as $arg) {
 804      $i = strpos($arg, '=');
 805      if ($i) {
 806        $key = substr($arg, 0, $i);
 807        $val = substr($arg, $i + 1);
 808        $env[$key] = $val;
 809      } else {
 810        unset($env[$arg]);
 811      }
 812    }
 813  }
 814
 815  $in = find_test_ext($test, 'in');
 816  if ($in !== null) {
 817    $cmd .= ' < ' . escapeshellarg($in);
 818    // If we're piping the input into the command then setup a simple
 819    // dumb terminal so hhvm doesn't try to control it and pollute the
 820    // output with control characters, which could change depending on
 821    // a wide variety of terminal settings.
 822    $env["TERM"] = "dumb";
 823  }
 824
 825  return array($cmd, $env);
 826}
 827
 828function hphp_cmd($options, $test) {
 829  $extra_args = preg_replace("/-v\s*/", "-vRuntime.", extra_args($options));
 830  return implode(" ", array(
 831    "HHVM_DISABLE_HHBBC2=1",
 832    hhvm_path(),
 833    '--hphp',
 834    '--config',
 835    find_file($test, 'hphp_config.ini'),
 836    read_opts_file("$test.hphp_opts"),
 837    "-thhbc -l0 -k1 -o \"$test.repo\" \"$test\"",
 838    $extra_args
 839  ));
 840}
 841
 842function hhbbc_cmd($options, $test) {
 843  return implode(" ", array(
 844    hhvm_path(),
 845    '--hhbbc',
 846    '--no-logging',
 847    '--parallel-num-threads=1',
 848    read_opts_file("$test.hhbbc_opts"),
 849    "-o \"$test.repo/hhvm.hhbbc\" \"$test.repo/hhvm.hhbc\"",
 850  ));
 851}
 852
 853function hh_server_cmd($options, $test) {
 854  // In order to run hh_server --check on only one file, we copy all of the
 855  // files associated with the test to a temporary directory, rename the
 856  // basename($test_file).hhconfig file to just .hhconfig and set the command
 857  // appropriately.
 858  $temp_dir = '/tmp/hh-test-runner-'.bin2hex(random_bytes(16));
 859  mkdir($temp_dir);
 860  foreach (glob($test . '*') as $test_file) {
 861    copy($test_file, $temp_dir . '/' . basename($test_file));
 862    if (strpos($test_file, '.hhconfig') !== false) {
 863      rename(
 864        $temp_dir . '/' . basename($test) . '.hhconfig',
 865        $temp_dir . '/.hhconfig'
 866      );
 867    } else if (strpos($test_file, '.type-errors') !== false) {
 868      // In order to actually run hh_server --check successfully, all files
 869      // named *.php.type-errors have to be renamed *.php
 870      rename(
 871        $temp_dir . '/' . basename($test_file),
 872        $temp_dir . '/' . str_replace('.type-errors', '', basename($test_file))
 873      );
 874    }
 875  }
 876  // Just copy all the .php.inc files, even if they are not related since
 877  // unrelated ones will be ignored anyway. This just makes it easier to
 878  // start with instead of doing a search inside the test file for requires
 879  // and includes and extracting it.
 880  foreach (glob(dirname($test) . "/*.inc.php") as $inc_file) {
 881    copy($inc_file, $temp_dir . '/' . basename($inc_file));
 882  }
 883  $cmd = hh_server_path() .  ' --check ' . $temp_dir;
 884  return array($cmd, ' ', $temp_dir);
 885}
 886
 887class Status {
 888  private static $results = array();
 889  private static $mode = 0;
 890
 891  private static $use_color = false;
 892
 893  private static $queue = null;
 894  private static $killed = false;
 895  public static $key;
 896
 897  private static $overall_start_time = 0;
 898  private static $overall_end_time = 0;
 899
 900  private static $tempdir = "";
 901
 902  const MODE_NORMAL = 0;
 903  const MODE_VERBOSE = 1;
 904  const MODE_FBMAKE = 2;
 905  const MODE_TESTPILOT = 3;
 906
 907  const MSG_STARTED = 7;
 908  const MSG_FINISHED = 1;
 909  const MSG_TEST_PASS = 2;
 910  const MSG_TEST_FAIL = 4;
 911  const MSG_TEST_SKIP = 5;
 912  const MSG_SERVER_RESTARTED = 6;
 913
 914  const RED = 31;
 915  const GREEN = 32;
 916  const YELLOW = 33;
 917  const BLUE = 34;
 918
 919  const PASS_SERVER = 0;
 920  const SKIP_SERVER = 1;
 921  const PASS_CLI = 2;
 922
 923  private static function getTempDir() {
 924    self::$tempdir = sys_get_temp_dir();
 925    // Apparently some systems might not put the trailing slash
 926    if (substr(self::$tempdir, -1) !== "/") {
 927      self::$tempdir .= "/";
 928    }
 929    self::$tempdir .= substr( md5(rand()), 0, 8);
 930    mkdir(self::$tempdir);
 931  }
 932
 933  // Since we run the tests in forked processes, state is not shared
 934  // So we cannot keep a static variable adding individual test times.
 935  // But we can put the times files and add the values later.
 936  public static function setTestTime($time) {
 937    file_put_contents(tempnam(self::$tempdir, "trun"), $time);
 938  }
 939
 940  // The total time running the tests if they were run serially.
 941  public static function addTestTimesSerial() {
 942    $time = 0;
 943    $files = scandir(self::$tempdir);
 944    foreach ($files as $file) {
 945      if (strpos($file, 'trun') === 0) {
 946        $time += floatval(file_get_contents(self::$tempdir . "/" . $file));
 947        unlink(self::$tempdir . "/" . $file);
 948      }
 949    }
 950    return $time;
 951  }
 952
 953  public static function setMode($mode) {
 954    self::$mode = $mode;
 955  }
 956
 957  public static function setUseColor($use) {
 958    self::$use_color = $use;
 959  }
 960
 961  public static function getMode() {
 962    return self::$mode;
 963  }
 964
 965  public static function getOverallStartTime() {
 966    return self::$overall_start_time;
 967  }
 968
 969  public static function getOverallEndTime() {
 970    return self::$overall_end_time;
 971  }
 972
 973  public static function started() {
 974    self::getTempDir();
 975    self::send(self::MSG_STARTED, null);
 976    self::$overall_start_time = microtime(true);
 977  }
 978
 979  public static function finished() {
 980    self::$overall_end_time = microtime(true);
 981    self::send(self::MSG_FINISHED, null);
 982  }
 983
 984  public static function killQueue() {
 985    if (!self::$killed) {
 986      msg_remove_queue(self::$queue);
 987      self::$queue = null;
 988      self::$killed = true;
 989    }
 990  }
 991
 992  public static function pass($test, $detail, $time, $stime, $etime) {
 993    array_push(self::$results, array('name' => $test,
 994                                     'status' => 'passed',
 995                                     'start_time' => $stime,
 996                                     'end_time' => $etime,
 997                                     'time' => $time));
 998    $how = $detail === 'pass-server' ? self::PASS_SERVER :
 999      ($detail === 'skip-server' ? self::SKIP_SERVER : self::PASS_CLI);
1000    self::send(self::MSG_TEST_PASS, array($test, $how, $time, $stime, $etime));
1001  }
1002
1003  public static function skip($test, $reason, $time, $stime, $etime) {
1004    if (self::getMode() === self::MODE_FBMAKE) {
1005      /* Intentionally supress skips */
1006    } elseif (self::getMode() === self::MODE_TESTPILOT) {
1007      /* testpilot needs a positive response for every test run, report
1008       * that this test isn't relevant so it can silently drop. */
1009      array_push(self::$results, array('name' => $test,
1010                                       'status' => 'not_relevant',
1011                                       'start_time' => $stime,
1012                                       'end_time' => $etime,
1013                                       'time' => $time));
1014    } else {
1015      array_push(self::$results, array('name' => $test,
1016                                       'status' => 'skipped',
1017                                       'start_time' => $stime,
1018                                       'end_time' => $etime,
1019                                       'time' => $time));
1020    }
1021    self::send(self::MSG_TEST_SKIP,
1022               array($test, $reason, $time, $stime, $etime));
1023  }
1024
1025  public static function fail($test, $time, $stime, $etime) {
1026    array_push(self::$results, array(
1027      'name' => $test,
1028      'status' => 'failed',
1029      'details' => self::utf8Sanitize(@file_get_contents("$test.diff")),
1030      'start_time' => $stime,
1031      'end_time' => $etime,
1032      'time' => $time
1033    ));
1034    self::send(self::MSG_TEST_FAIL, array($test, $time, $stime, $etime));
1035  }
1036
1037  public static function serverRestarted() {
1038    self::send(self::MSG_SERVER_RESTARTED, null);
1039  }
1040
1041  private static function send($type, $msg) {
1042    if (self::$killed) {
1043      return;
1044    }
1045    msg_send(self::getQueue(), $type, $msg);
1046  }
1047
1048  /**
1049   * Takes a variable number of string arguments. If color output is enabled
1050   * and any one of the arguments is preceded by an integer (see the color
1051   * constants above), that argument will be given the indicated color.
1052   */
1053  public static function sayColor() {
1054    $args = func_get_args();
1055    while (count($args)) {
1056      $color = null;
1057      $str = array_shift($args);
1058      if (is_integer($str)) {
1059        $color = $str;
1060        if (self::$use_color) {
1061          print "\033[0;${color}m";
1062        }
1063        $str = array_shift($args);
1064      }
1065
1066      print $str;
1067
1068      if (self::$use_color && !is_null($color)) {
1069        print "\033[0m";
1070      }
1071    }
1072  }
1073
1074  public static function sayFBMake($test, $status, $stime, $etime) {
1075    $start = array('op' => 'start', 'test' => $test);
1076    $end = array('op' => 'test_done', 'test' => $test, 'status' => $status,
1077                 'start_time' => $stime, 'end_time' => $etime);
1078    if ($status == 'failed') {
1079      $end['details'] = self::utf8Sanitize(@file_get_contents("$test.diff"));
1080    }
1081    self::say($start, $end);
1082  }
1083
1084  public static function getResults() {
1085    return self::$results;
1086  }
1087
1088  /** Output is in the format expected by JsonTestRunner. */
1089  public static function say(/* ... */) {
1090    $data = array_map(function($row) {
1091      return self::jsonEncode($row) . "\n";
1092    }, func_get_args());
1093    fwrite(STDERR, implode("", $data));
1094  }
1095
1096  public static function hasCursorControl() {
1097    // for runs on hudson-ci.org (aka jenkins).
1098    if (getenv("HUDSON_URL")) {
1099      return false;
1100    }
1101    // for runs on travis-ci.org
1102    if (getenv("TRAVIS")) {
1103      return false;
1104    }
1105    $stty = self::getSTTY();
1106    if (!$stty) {
1107      return false;
1108    }
1109    return strpos($stty, 'erase = <undef>') === false;
1110  }
1111
1112  public static function getSTTY() {
1113    $descriptorspec = array(1 => array("pipe", "w"), 2 => array("pipe", "w"));
1114    $process = proc_open(
1115      'stty -a', $descriptorspec, $pipes, null, null,
1116      array('suppress_errors' => true)
1117    );
1118    $stty = stream_get_contents($pipes[1]);
1119    proc_close($process);
1120    return $stty;
1121  }
1122
1123  public static function utf8Sanitize($str) {
1124    if (!is_string($str)) {
1125      // We sometimes get called with the
1126      // return value of file_get_contents()
1127      // when fgc() has failed.
1128      return '';
1129    }
1130
1131    if (class_exists('UConverter')) {
1132      return UConverter::transcode($str, 'UTF-8', 'UTF-8');
1133    }
1134
1135    // UConverter is PHP5.5 or later.
1136    // Do the equivalent using a slower user-space implementation.
1137    $ret = '';
1138    $len = strlen($str);
1139    $pos = 0;
1140    while ($pos < $len) {
1141      $c = $str[$pos];
1142      $co = ord($c);
1143      if ($co < 0x80) {
1144        // U+0000 - U+007F ASCII
1145        $ret .= $c;
1146        ++$pos;
1147      } elseif ((($co & 0xE0) == 0xC0) &&
1148                (($len - $pos) > 1) &&
1149                ((ord($str[$pos+1]) & 0xC0) == 0x80)) {
1150        // U+0080 - U+07FF Lower Basic Multilingual Plane
1151        $ret .= substr($str, $pos, 2);
1152        $pos += 2;
1153      } elseif ((($co & 0xF0) == 0xE0) &&
1154                (($len - $pos) > 2) &&
1155                ((ord($str[$pos+1]) & 0xC0) == 0x80) &&
1156                ((ord($str[$pos+2]) & 0xC0) == 0x80)) {
1157        // U+0800 - U+FFFF Upper Basic Multilingual Plane
1158        $ret .= substr($str, $pos, 3);
1159        $pos += 3;
1160      } elseif ((($co & 0xF8) == 0xF0) &&
1161                (($len - $pos) > 3) &&
1162                ((ord($str[$pos+1]) & 0xC0) == 0x80) &&
1163                ((ord($str[$pos+2]) & 0xC0) == 0x80) &&
1164                ((ord($str[$pos+3]) & 0xC0) == 0x80)) {
1165        // U+010000 - U+10FFFF Supplementary Multilingual Planes
1166        $ret .= substr($str, $pos, 4);
1167        $pos += 4;
1168      } else {
1169        // Invalid UTF8
1170        $ret .= "\xEF\xBF\xBD"; // U+FFFD REPLACEMENT CHARACTER
1171        ++$pos;
1172      }
1173    }
1174    return $ret;
1175  }
1176
1177  public static function jsonEncode($data) {
1178    // JSON_UNESCAPED_SLASHES is Zend 5.4+.
1179    if (defined("JSON_UNESCAPED_SLASHES")) {
1180      return json_encode($data, JSON_UNESCAPED_SLASHES);
1181    }
1182
1183    $json = json_encode($data);
1184    return str_replace('\\/', '/', $json);
1185  }
1186
1187  public static function getQueue() {
1188    if (!self::$queue) {
1189      self::$queue = msg_get_queue(self::$key);
1190    }
1191    return self::$queue;
1192  }
1193}
1194
1195function clean_intermediate_files($test, $options) {
1196  if (isset($options['no-clean'])) {
1197    return;
1198  }
1199  $exts = array('out', 'diff', 'repo');
1200  foreach ($exts as $ext) {
1201    $file = "$test.$ext";
1202    if (file_exists($file)) {
1203      if (is_dir($file)) {
1204        foreach(new RecursiveIteratorIterator(new
1205            RecursiveDirectoryIterator($file, FilesystemIterator::SKIP_DOTS),
1206            RecursiveIteratorIterator::CHILD_FIRST) as $path) {
1207          $path->isDir()
1208          ? rmdir($path->getPathname())
1209          : unlink($path->getPathname());
1210        }
1211        rmdir($file);
1212      } else {
1213        unlink($file);
1214      }
1215    }
1216  }
1217}
1218
1219function run($options, $tests, $bad_test_file) {
1220  foreach ($tests as $test) {
1221    $stime = time();
1222    $time = microtime(true);
1223    $status = run_and_lock_test($options, $test);
1224    $time = microtime(true) - $time;
1225    $etime = time();
1226    Status::setTestTime($time);
1227    if ($status === 'skip') {
1228      Status::skip($test, null, $time, $stime, $etime);
1229      clean_intermediate_files($test, $options);
1230    } else if ($status === 'skip-norepo') {
1231      Status::skip($test, 'norepo', $time, $stime, $etime);
1232      clean_intermediate_files($test, $options);
1233    } else if ($status === 'skip-onlyrepo') {
1234      Status::skip($test, 'onlyrepo', $time, $stime, $etime);
1235      clean_intermediate_files($test, $options);
1236    } else if ($status) {
1237      Status::pass($test, $status, $time, $stime, $etime);
1238      clean_intermediate_files($test, $options);
1239    } else {
1240      Status::fail($test, $time, $stime, $etime);
1241    }
1242  }
1243  file_put_contents($bad_test_file, json_encode(Status::getResults()));
1244  foreach (Status::getResults() as $result) {
1245    if ($result['status'] == 'failed') {
1246      return 1;
1247    }
1248  }
1249  return 0;
1250}
1251
1252function skip_test($options, $test) {
1253  $skipif_test = find_test_ext($test, 'skipif');
1254  if (!$skipif_test) {
1255    return false;
1256  }
1257
1258  // For now, run the .skipif in non-repo since building a repo for it is hard.
1259  $options_without_repo = $options;
1260  unset($options_without_repo['repo']);
1261
1262  list($hhvm, $_) = hhvm_cmd($options_without_repo, $test, $skipif_test);
1263  $descriptorspec = array(
1264    0 => array("pipe", "r"),
1265    1 => array("pipe", "w"),
1266    2 => array("pipe", "w"),
1267  );
1268  $pipes = null;
1269  $process = proc_open("$hhvm $test 2>&1", $descriptorspec, $pipes);
1270  if (!is_resource($process)) {
1271    // This is weird. We can't run HHVM but we probably shouldn't skip the test
1272    // since on a broken build everything will show up as skipped and give you a
1273    // SHIPIT.
1274    return false;
1275  }
1276
1277  fclose($pipes[0]);
1278  $output = stream_get_contents($pipes[1]);
1279  fclose($pipes[1]);
1280  proc_close($process);
1281
1282  // The standard php5 .skipif semantics is if the .skipif outputs ANYTHING
1283  // then it should be skipped. This is a poor design, but I'll just add a
1284  // small blacklist of things that are really bad if they are output so we
1285  // surface the errors in the tests themselves.
1286  if (stripos($output, 'segmentation fault') !== false) {
1287    return false;
1288  }
1289
1290  return strlen($output) != 0;
1291}
1292
1293function comp_line($l1, $l2, $is_reg) {
1294  if ($is_reg) {
1295    return preg_match('/^'. $l1 . '$/s', $l2);
1296  } else {
1297    return !strcmp($l1, $l2);
1298  }
1299}
1300
1301function count_array_diff($ar1, $ar2, $is_reg, $w, $idx1, $idx2, $cnt1, $cnt2,
1302                          $steps) {
1303  $equal = 0;
1304
1305  while ($idx1 < $cnt1 && $idx2 < $cnt2 && comp_line($ar1[$idx1], $ar2[$idx2],
1306                                                     $is_reg)) {
1307    $idx1++;
1308    $idx2++;
1309    $equal++;
1310    $steps--;
1311  }
1312  if (--$steps > 0) {
1313    $eq1 = 0;
1314    $st = $steps / 2;
1315
1316    for ($ofs1 = $idx1 + 1; $ofs1 < $cnt1 && $st-- > 0; $ofs1++) {
1317      $eq = @count_array_diff($ar1, $ar2, $is_reg, $w, $ofs1, $idx2, $cnt1,
1318                              $cnt2, $st);
1319
1320      if ($eq > $eq1) {
1321        $eq1 = $eq;
1322      }
1323    }
1324
1325    $eq2 = 0;
1326    $st = $steps;
1327
1328    for ($ofs2 = $idx2 + 1; $ofs2 < $cnt2 && $st-- > 0; $ofs2++) {
1329      $eq = @count_array_diff($ar1, $ar2, $is_reg, $w, $idx1, $ofs2, $cnt1, $cnt2, $st);
1330      if ($eq > $eq2) {
1331        $eq2 = $eq;
1332      }
1333    }
1334
1335    if ($eq1 > $eq2) {
1336      $equal += $eq1;
1337    } else if ($eq2 > 0) {
1338      $equal += $eq2;
1339    }
1340  }
1341
1342  return $equal;
1343}
1344
1345function generate_array_diff($ar1, $ar2, $is_reg, $w) {
1346  $idx1 = 0; $ofs1 = 0; $cnt1 = @count($ar1);
1347  $idx2 = 0; $ofs2 = 0; $cnt2 = @count($ar2);
1348  $diff = array();
1349  $old1 = array();
1350  $old2 = array();
1351
1352  while ($idx1 < $cnt1 && $idx2 < $cnt2) {
1353
1354    if (comp_line($ar1[$idx1], $ar2[$idx2], $is_reg)) {
1355      $idx1++;
1356      $idx2++;
1357      continue;
1358    } else {
1359
1360      $c1 = @count_array_diff($ar1, $ar2, $is_reg, $w, $idx1+1, $idx2, $cnt1,
1361                              $cnt2, 10);
1362      $c2 = @count_array_diff($ar1, $ar2, $is_reg, $w, $idx1, $idx2+1, $cnt1,
1363                              $cnt2, 10);
1364
1365      if ($c1 > $c2) {
1366        $old1[$idx1] = sprintf("%03d- ", $idx1+1) . $w[$idx1++];
1367        $last = 1;
1368      } else if ($c2 > 0) {
1369        $old2[$idx2] = sprintf("%03d+ ", $idx2+1) . $ar2[$idx2++];
1370        $last = 2;
1371      } else {
1372        $old1[$idx1] = sprintf("%03d- ", $idx1+1) . $w[$idx1++];
1373        $old2[$idx2] = sprintf("%03d+ ", $idx2+1) . $ar2[$idx2++];
1374      }
1375    }
1376  }
1377
1378  reset($old1); $k1 = key($old1); $l1 = -2;
1379  reset($old2); $k2 = key($old2); $l2 = -2;
1380
1381  while ($k1 !== null || $k2 !== null) {
1382
1383    if ($k1 == $l1 + 1 || $k2 === null) {
1384      $l1 = $k1;
1385      $diff[] = current($old1);
1386      $k1 = next($old1) ? key($old1) : null;
1387    } else if ($k2 == $l2 + 1 || $k1 === null) {
1388      $l2 = $k2;
1389      $diff[] = current($old2);
1390      $k2 = next($old2) ? key($old2) : null;
1391    } else if ($k1 < $k2) {
1392      $l1 = $k1;
1393      $diff[] = current($old1);
1394      $k1 = next($old1) ? key($old1) : null;
1395    } else {
1396      $l2 = $k2;
1397      $diff[] = current($old2);
1398      $k2 = next($old2) ? key($old2) : null;
1399    }
1400  }
1401
1402  while ($idx1 < $cnt1) {
1403    $diff[] = sprintf("%03d- ", $idx1 + 1) . $w[$idx1++];
1404  }
1405
1406  while ($idx2 < $cnt2) {
1407    $diff[] = sprintf("%03d+ ", $idx2 + 1) . $ar2[$idx2++];
1408  }
1409
1410  return $diff;
1411}
1412
1413function generate_diff($wanted, $wanted_re, $output)
1414{
1415  $w = explode("\n", $wanted);
1416  $o = explode("\n", $output);
1417  if (is_null($wanted_re)) {
1418    $r = $w;
1419  } else {
1420    if (preg_match('/^\((.*)\)\{(\d+)\}$/s', $wanted_re, $m)) {
1421      $t = explode("\n", $m[1]);
1422      $r = array();
1423      $w2 = array();
1424      for ($i = 0; $i < $m[2]; $i++) {
1425        foreach ($t as $v) {
1426          $r[] = $v;
1427        }
1428        foreach ($w as $v) {
1429          $w2[] = $v;
1430        }
1431      }
1432      $w = $wanted === $wanted_re ? $r : $w2;
1433    } else {
1434      $r = explode("\n", $wanted_re);
1435    }
1436  }
1437  $diff = generate_array_diff($r, $o, !is_null($wanted_re), $w);
1438
1439  return implode("\r\n", $diff);
1440}
1441
1442function dump_hhas_to_temp($hhvm_cmd, $test) {
1443  $tmp_file = $test . '.round_trip.hhas';
1444  system("$hhvm_cmd -vEval.DumpHhas=1 > $tmp_file", $ret);
1445  if ($ret) { echo "system failed\n"; exit(1); }
1446  return $tmp_file;
1447}
1448
1449const HHAS_EXT = '.hhas';
1450function can_run_server_test($test) {
1451  return
1452    !is_file("$test.noserver") &&
1453    !find_test_ext($test, 'opts') &&
1454    !is_file("$test.ini") &&
1455    !is_file("$test.onlyrepo") &&
1456    strpos($test, 'quick/debugger') === false &&
1457    strpos($test, 'quick/xenon') === false &&
1458    strpos($test, 'slow/streams/') === false &&
1459    strpos($test, 'slow/ext_mongo/') === false &&
1460    strpos($test, 'slow/ext_oauth/') === false &&
1461    strpos($test, 'slow/ext_yaml/') === false &&
1462    strpos($test, 'slow/debugger/') === false &&
1463    strpos($test, 'slow/type_profiler/debugger/') === false &&
1464    strpos($test, 'zend/good/ext/standard/tests/array/') === false &&
1465    strpos($test, 'zend/good/ext/ftp') === false &&
1466    strrpos($test, HHAS_EXT) !== (strlen($test) - strlen(HHAS_EXT))
1467    ;
1468}
1469
1470const SERVER_TIMEOUT = 45;
1471function run_config_server($options, $test) {
1472  if (!isset($options['server']) || !can_run_server_test($test)) {
1473    return null;
1474  }
1475
1476  $config = find_file_for_dir(dirname($test), 'config.ini');
1477  $port = $options['servers']['configs'][$config]['port'];
1478  $ch = curl_init("localhost:$port/$test");
1479  curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
1480  curl_setopt($ch, CURLOPT_TIMEOUT, SERVER_TIMEOUT);
1481  curl_setopt($ch, CURLOPT_BINARYTRANSFER, true);
1482  $output = curl_exec($ch);
1483  if ($output === false) {
1484    // The server probably crashed so fall back to cli to determine if this was
1485    // the test that caused the crash. Our parent process will see that the
1486    // server died and restart it.
1487    if (getenv('HHVM_TEST_SERVER_LOG')) {
1488      printf("Curl failed: %d\n", curl_errno($ch));
1489    }
1490    return null;
1491  }
1492  curl_close($ch);
1493  $output = trim($output);
1494
1495  return array($output, '');
1496}
1497
1498function run_config_cli($options, $test, $cmd, $cmd_env) {
1499  if (isset($options['log']) && !isset($options['typechecker'])) {
1500    $cmd_env['TRACE'] = 'printir:1';
1501    $cmd_env['HPHP_TRACE_FILE'] = $test . '.log';
1502  }
1503
1504  $descriptorspec = array(
1505    0 => array("pipe", "r"),
1506    1 => array("pipe", "w"),
1507    2 => array("pipe", "w"),
1508  );
1509  $pipes = null;
1510  if (isset($options['typechecker'])) {
1511    $process = proc_open(
1512      "$cmd 2>/dev/null", $descriptorspec, $pipes, null, $cmd_env
1513    );
1514  } else {
1515    $process = proc_open("$cmd 2>&1", $descriptorspec, $pipes, null, $cmd_env);
1516  }
1517  if (!is_resource($process)) {
1518    file_put_contents("$test.diff", "Couldn't invoke $cmd");
1519    return false;
1520  }
1521
1522  fclose($pipes[0]);
1523  $output = stream_get_contents($pipes[1]);
1524  $output = trim($output);
1525  $stderr = stream_get_contents($pipes[2]);
1526  fclose($pipes[1]);
1527  fclose($pipes[2]);
1528  proc_close($process);
1529
1530  return array($output, $stderr);
1531}
1532
1533function run_config_post($outputs, $test, $options) {
1534  $output = $outputs[0];
1535  $stderr = $outputs[1];
1536  file_put_contents("$test.out", $output);
1537
1538  // hhvm redirects errors to stdout, so anything on stderr is really bad.
1539  if ($stderr) {
1540    file_put_contents(
1541      "$test.diff",
1542      "Test failed because the process wrote on stderr:\n$stderr"
1543    );
1544    return false;
1545  }
1546
1547  // Needed for testing non-hhvm binaries that don't actually run the code
1548  // e.g. parser/test/parse_tester.cpp.
1549  if ($output == "FORCE PASS") {
1550    return true;
1551  }
1552
1553  $repeats = 0;
1554
1555  if (isset($options['relocate'])) {
1556    $repeats = $options['relocate'] * 2;
1557  }
1558
1559  if (isset($options['recycle-tc'])) {
1560    $repeats = $options['recycle-tc'];
1561  }
1562
1563  list($file, $type) = get_expect_file_and_type($test, $options);
1564  if ($file === null || $type === null) {
1565    file_put_contents(
1566      "$test.diff", "No $test.expect, $test.expectf, " .
1567      "$test.hhvm.expect, $test.hhvm.expectf, " .
1568      "$test.typechecker.expect, $test.typechecker.expectf, " .
1569      "nor $test.expectregex. Is $test even a test?"
1570    );
1571    return false;
1572  }
1573
1574  $is_tc = isset($options['typechecker']);
1575  if ((!$is_tc && ($type === 'expect' || $type === 'hhvm.expect')) ||
1576      ($is_tc && $type === 'typechecker.expect')) {
1577    $wanted = trim(file_get_contents($file));
1578    if (!$repeats) {
1579      $passed = !strcmp($output, $wanted);
1580      if (!$passed) {
1581        file_put_contents("$test.diff", generate_diff($wanted, null, $output));
1582      }
1583      return $passed;
1584    }
1585    $wanted_re = preg_quote($wanted);
1586  } else if ((!$is_tc && ($type === 'expectf' || $type === 'hhvm.expectf')) ||
1587             ($is_tc && $type === 'typechecker.expectf')) {
1588    $wanted = trim(file_get_contents($file));
1589    $wanted_re = $wanted;
1590
1591    // do preg_quote, but miss out any %r delimited sections.
1592    $temp = "";
1593    $r = "%r";
1594    $startOffset = 0;
1595    $length = strlen($wanted_re);
1596    while($startOffset < $length) {
1597      $start = strpos($wanted_re, $r, $startOffset);
1598      if ($start !== false) {
1599        // we have found a start tag.
1600        $end = strpos($wanted_re, $r, $start+2);
1601        if ($end === false) {
1602          // unbalanced tag, ignore it.
1603          $end = $start = $length;
1604        }
1605      } else {
1606        // no more %r sections.
1607        $start = $end = $length;
1608      }
1609      // quote a non re portion of the string.
1610      $temp = $temp.preg_quote(substr($wanted_re, $startOffset,
1611                                      ($start - $startOffset)),  '/');
1612      // add the re unquoted.
1613      if ($end > $start) {
1614        $temp = $temp.'('.substr($wanted_re, $start+2, ($end - $start-2)).')';
1615      }
1616      $startOffset = $end + 2;
1617    }
1618    $wanted_re = $temp;
1619
1620    $wanted_re = str_replace(
1621      array('%binary_string_optional%'),
1622      'string',
1623      $wanted_re
1624    );
1625    $wanted_re = str_replace(
1626      array('%unicode_string_optional%'),
1627      'string',
1628      $wanted_re
1629    );
1630    $wanted_re = str_replace(
1631      array('%unicode\|string%', '%string\|unicode%'),
1632      'string',
1633      $wanted_re
1634    );
1635    $wanted_re = str_replace(
1636      array('%u\|b%', '%b\|u%'),
1637      '',
1638      $wanted_re
1639    );
1640    // Stick to basics.
1641    $wanted_re = str_replace('%e', '\\' . DIRECTORY_SEPARATOR, $wanted_re);
1642    $wanted_re = str_replace('%s', '[^\r\n]+', $wanted_re);
1643    $wanted_re = str_replace('%S', '[^\r\n]*', $wanted_re);
1644    $wanted_re = str_replace('%a', '.+', $wanted_re);
1645    $wanted_re = str_replace('%A', '.*', $wanted_re);
1646    $wanted_re = str_replace('%w', '\s*', $wanted_re);
1647    $wanted_re = str_replace('%i', '[+-]?\d+', $wanted_re);
1648    $wanted_re = str_replace('%d', '\d+', $wanted_re);
1649    $wanted_re = str_replace('%x', '[0-9a-fA-F]+', $wanted_re);
1650    // %f allows two points "-.0.0" but that is the best *simple* expression.
1651    $wanted_re = str_replace('%f', '[+-]?\.?\d+\.?\d*(?:[Ee][+-]?\d+)?',
1652                             $wanted_re);
1653    $wanted_re = str_replace('%c', '.', $wanted_re);
1654    // must be last.
1655    $wanted_re = str_replace('%%', '%%?', $wanted_re);
1656
1657    // Normalize newlines.
1658    $wanted_re = preg_replace("/(\r\n?|\n)/", "\n", $wanted_re);
1659    $output    = preg_replace("/(\r\n?|\n)/", "\n", $output);
1660  } else if (!$is_tc && $type === 'expectregex') {
1661    $wanted_re = trim(file_get_contents($file));
1662  } else {
1663    throw new Exception("Unsupported expect file type: ".$type);
1664  }
1665
1666  if ($repeats) {
1667    $wanted_re = "($wanted_re\s*)".'{'.$repeats.'}';
1668  }
1669  if (!isset($wanted)) $wanted = $wanted_re;
1670  $passed = @preg_match("/^$wanted_re\$/s", $output);
1671  if ($passed === false && $repeats) {
1672    // $repeats can cause the regex to become too big, and fail
1673    // to compile.
1674    return 'skip';
1675  }
1676  if (!$passed) {
1677    file_put_contents("$test.diff",
1678                      generate_diff($wanted_re, $wanted_re, $output));
1679  }
1680  return $passed;
1681}
1682
1683function run_one_config($options, $test, $cmd, $cmd_env) {
1684  $outputs = run_config_cli($options, $test, $cmd, $cmd_env);
1685  if ($outputs === false) return false;
1686  return run_config_post($outputs, $test, $options);
1687}
1688
1689function run_and_lock_test($options, $test) {
1690  $lock = fopen($test, 'r');
1691  if (!flock($lock, LOCK_EX)) return false;
1692  if (isset($options['typechecker'])) {
1693    $result = run_typechecker_test($options, $test);
1694  } else {
1695    $result = run_test($options, $test);
1696  }
1697  if (!flock($lock, LOCK_UN)) return false;
1698  fclose($lock);
1699  return $result;
1700}
1701
1702function run_typechecker_test($options, $test) {
1703  if (skip_test($options, $test)) return 'skip';
1704  if (!file_exists($test . ".hhconfig")) return 'skip';
1705  list($hh_server, $hh_server_env, $temp_dir) = hh_server_cmd($options, $test);
1706  if (is_executable('/usr/bin/timeout')) {
1707    $hh_server = '/usr/bin/timeout 300 ' . $hh_server;
1708  } else {
1709    $hh_server = __DIR__.'/../tools/timeout.sh -t 300 '. $hh_server;
1710  }
1711  $result =  run_one_config($options, $test, $hh_server, $hh_server_env);
1712  // Remove the temporary directory.
1713  if (!isset($options['no-clean'])) {
1714    shell_exec('rm -rf ' . $temp_dir);
1715  }
1716  return $result;
1717}
1718
1719function run_test($options, $test) {
1720  if (skip_test($options, $test…

Large files files are truncated, but you can click here to view the full file