#!/usr/bin/env php
<?php
/**
* Run the test suites in various configurations.
*/

function is_testing_dso_extension() {
  // detecting if we're running outside of the hhvm codebase.
  return !is_file(__DIR__ . "/../../hphp/test/run");
}

function get_expect_file_and_type($test, $options) {
  // .typechecker files are for typechecker (hh_server --check) test runs.
  $types = null;
  if (isset($options['typechecker'])) {
    $types = array('typechecker.expect', 'typechecker.expectf');
  } else {
    $types = array('expect', 'hhvm.expect', 'expectf', 'hhvm.expectf',
                   'expectregex');
  }
  if (isset($options['repo'])) {
    foreach ($types as $type) {
      $fname = "$test.$type-repo";
      if (file_exists($fname)) {
        return array($fname, $type);
      }
    }
  }
  foreach ($types as $type) {
    $fname = "$test.$type";
    if (file_exists($fname)) {
      return array($fname, $type);
    }
  }
  return array(null, null);
}

function usage() {
  global $argv;
  return "usage: $argv[0] [-m jit|interp] [-r] <test/directories>";
}

function help() {
  global $argv;
  $ztestexample = 'test/zend/good/*/*z*.php'; // sep. for syntax highlighting.
  $help = <<<EOT


This is the hhvm test-suite runner.  For more detailed documentation,
see hphp/test/README.md.

The test argument may be a path to a php test file, a directory name, or
one of a few pre-defined suite names that this script knows about.

If you work with hhvm a lot, you might consider a bash alias:

   alias ht="path/to/hphp/test/run"

Examples:

  # Quick tests in JIT mode:
  % $argv[0] test/quick

  # Slow tests in interp mode:
  % $argv[0] -m interp test/slow

  # PHP specificaion tests in JIT mode:
  % $argv[0] test/spec

  # Slow closure tests in JIT mode:
  % $argv[0] test/slow/closure

  # Slow closure tests in JIT mode with RepoAuthoritative:
  % $argv[0] -r test/slow/closure

  # Slow array tests, in RepoAuthoritative:
  % $argv[0] -r test/slow/array

  # Zend tests with a "z" in their name:
  % $argv[0] $ztestexample

  # Quick tests in JIT mode with some extra runtime options:
  % $argv[0] test/quick -a '-vEval.JitMaxTranslations=120 -vEval.HHIRRefcountOpts=0'

  # All quick tests except debugger
  % $argv[0] -e debugger test/quick

  # All tests except those containing a string of 3 digits
  % $argv[0] -E '/\d{3}/' all

  # All tests whose name containing pdo_mysql
  % $argv[0] -i pdo_mysql -m jit -r zend

  # Print all the standard tests
  % $argv[0] --list-tests

  # Use a specific HHVM binary
  % $argv[0] -b ~/code/hhvm/hphp/hhvm/hhvm
  % $argv[0] --hhvm-binary-path ~/code/hhvm/hphp/hhvm/hhvm

  # Use relocation to run tests in the same thread. e.g, 6 times in the same thread,
  # where the 3 specifies a random relocation for the 3rd request and the test is
  # run 3 * 2 times.
  % $argv[0] --relocate 3 test/quick/silencer.php

  # Run the Hack typechecker against quick typechecker.expect[f] files
  # Could explcitly use quick here too
  # $argv[0] --typechecker

  # Run the Hack typechecker against typechecker.expect[f] files in the slow
  # directory
  # $argv[0] --typechecker slow

  # Run the Hack typechecker against the typechecker.expect[f] file in this test
  # $argv[0] --typechecker test/slow/test_runner_typechecker_mode/basic.php
EOT;
  return usage().$help;
}

function error($message) {
  print "$message\n";
  exit(1);
}

// If a user-supplied path is provided, let's make sure we have a valid
// executable.
function check_hhvm_executable($path) {
  $path = realpath($path);
  $msg = "Provided HHVM executable (" . $path . ") is not a file.\n"
       . "If using HHVM_BIN, make sure that is set correctly.";
  if (!is_file($path)) {
    error($msg);
  }
  $output = array();
  exec($path . " --version", $output);
  $str = implode($output);
  $msg = "Provided file (" . $path . ") is not an HHVM executable.\n"
       . "If using HHVM_BIN, make sure that is set correctly.";
  if (strpos($str, "HipHop") === false && strpos($str, "HHVM") === false) {
    error($msg);
  }
}

function check_for_multiple_default_hhvm_binaries() {
  $dir = hphp_home() . "/";
  if (getenv("FBMAKE_BIN_ROOT")) {
    $dir .= getenv("FBMAKE_BIN_ROOT");
  } else {
    $dir .= "_bin";
  }
  // For Facebook: We have two make systems fbmake and cmake, and we
  // can use both in the same code repo. If two binaries exist, we
  // want the onus to be on the user to specify a particular one because
  // before we chose the fbmake one by default and that can cause unexpected
  // results.
  if (file_exists($dir.'/hphp/hhvm/hhvm') &&
      file_exists(hphp_home().'/hphp/hhvm/hhvm')) {
    $msg = "Two binaries exist in this repo. \n"
         . " - " . $dir . "/hphp/hhvm/hhvm\n"
         . " - " . hphp_home() . "/hphp/hhvm/hhvm\n"
         . "Are you in fbcode? If so, remove a binary \n"
         . "or use the --hhvm-binary-path option to the test runner. \n"
         . "e.g., test/run -b hphp/hhvm/hhvm quick";
    error($msg);
  }
}

function hphp_home() {
  if (is_testing_dso_extension()) {
    return realpath(__DIR__);
  }
  return realpath(__DIR__.'/../..');
}

function idx($array, $key, $default = null) {
  return isset($array[$key]) ? $array[$key] : $default;
}

function hhvm_path() {
  $file = "";
  if (getenv("HHVM_BIN") !== false) {
    $file = realpath(getenv("HHVM_BIN"));
  } else {
    $file = bin_root().'/hhvm';
  }

  if (!is_file($file)) {
    if (is_testing_dso_extension()) {
      exec("which hhvm", $output);
      if (isset($output[0]) && $output[0]) {
        return $output[0];
      }
      error("You need to specify hhvm bin with env HHVM_BIN");
    }

    error("$file doesn't exist. Did you forget to build first?");
  }
  return rel_path($file);
}

function bin_root() {
  if (getenv("HHVM_BIN") !== false) {
    return dirname(realpath(getenv("HHVM_BIN")));
  }

  $dir = hphp_home() . "/";
  if (getenv("FBMAKE_BIN_ROOT") !== false) {
    $dir .= getenv("FBMAKE_BIN_ROOT") . '/hphp/hhvm';
  } else {
    $dir .= "_bin/hphp/hhvm";
  }
  return is_dir($dir) ?
    $dir :      # fbmake
    hphp_home() . "/hphp/hhvm" # github / cmake
  ;
}

function hh_server_path() {
  $file = hh_server_bin_root().'/hh_server';
  if (!is_file($file)) {
    error("$file doesn't exist. Did you forget to build first?");
  }
  return rel_path($file);
}

function hh_server_bin_root() {
  $dir = hphp_home() . "/";
  if (getenv("FBMAKE_BIN_ROOT") !== false) {
    $dir .= getenv("FBMAKE_BIN_ROOT") . '/hphp/hack/src';
  } else {
    $dir .= "_bin/hphp/hack/src";
  }
  return is_dir($dir) ?
    $dir :      # fbmake
    hphp_home() . "/hphp/hack/bin" # github / cmake
  ;
}

function verify_hhbc() {
  if (getenv("VERIFY_HHBC") !== false) {
    return getenv($env_hhbc);
  }
  return bin_root().'/verify.hhbc';
}

function read_opts_file($file) {
  if (!file_exists($file)) {
    return "";
  }

  $fp = fopen($file, "r");

  $contents = "";
  while ($line = fgets($fp)) {
    // Compress out white space.
    $line = preg_replace('/\s+/', ' ', $line);

    // Discard simple line oriented ; and # comments to end of line
    // Comments at end of line (after payload) are not allowed.
    $line = preg_replace('/^ *;.*$/', ' ', $line);
    $line = preg_replace('/^ *#.*$/', ' ', $line);

    // Substitute in the directory name
    $line = str_replace('__DIR__', dirname($file), $line);

    $contents .= $line;
  }
  fclose($fp);
  return $contents;
}

// http://stackoverflow.com/questions/2637945/
function rel_path($to) {
  $from     = explode('/', getcwd().'/');
  $to       = explode('/', $to);
  $relPath  = $to;

  foreach ($from as $depth => $dir) {
    // find first non-matching dir.
    if ($dir === $to[$depth]) {
      // ignore this directory.
      array_shift($relPath);
    } else {
      // get number of remaining dirs to $from.
      $remaining = count($from) - $depth;
      if ($remaining > 1) {
        // add traversals up to first matching dir.
        $padLength = (count($relPath) + $remaining - 1) * -1;
        $relPath = array_pad($relPath, $padLength, '..');
        break;
      } else {
        $relPath[0] = './' . $relPath[0];
      }
    }
  }
  return implode('/', $relPath);
}

function get_options($argv) {
  $parameters = array(
    'exclude:' => 'e:',
    'exclude-pattern:' => 'E:',
    'include:' => 'i:',
    'include-pattern:' => 'I:',
    'repo' => 'r',
    'mode:' => 'm:',
    'server' => 's',
    'shuffle' => '',
    'help' => 'h',
    'verbose' => 'v',
    'fbmake' => '',
    'testpilot' => '',
    'threads:' => '',
    'args:' => 'a:',
    'log' => 'l',
    'failure-file:' => '',
    'arm' => '',
    'llvm' => '',
    'force-llvm' => '',
    'wholecfg' => '',
    'hhas-round-trip' => '',
    'color' => 'c',
    'no-fun' => '',
    'cores' => '',
    'no-clean' => '',
    'list-tests' => '',
    'relocate:' => '',
    'hhvm-binary-path:' => 'b:',
    'typechecker' => '',
  );
  $options = array();
  $files = array();

  /*
   * '-' argument causes all future arguments to be treated as filenames, even
   * if they would otherwise match a valid option. Otherwise, arguments starting
   * with '-' MUST match a valid option.
   */
  $force_file = false;

  for ($i = 1; $i < count($argv); $i++) {
    $arg = $argv[$i];

    if (strlen($arg) == 0) {
      continue;
    } else if ($force_file) {
      $files[] = $arg;
    } else if ($arg === '-') {
      $forcefile = true;
    } else if ($arg[0] === '-') {
      $found = false;

      foreach ($parameters as $long => $short) {
        if ($arg == '-'.str_replace(':', '', $short) ||
            $arg == '--'.str_replace(':', '', $long)) {
          if (substr($long, -1, 1) == ':') {
            $value = $argv[++$i];
          } else {
            $value = true;
          }
          $options[str_replace(':', '', $long)] = $value;
          $found = true;
          break;
        }
      }

      if (!$found) {
        error(sprintf("Invalid argument: '%s'\nSee $argv[0] --help", $arg));
      }
    } else {
      $files[] = $arg;
    }
  }

  if (isset($options['repo']) && isset($options['hhas-round-trip'])) {
    echo "repo and hhas-round-trip are mutually exclusive options\n";
    exit(1);
  }

  return array($options, $files);
}

/*
 * We support some 'special' file names, that just know where the test
 * suites are, to avoid typing 'hphp/test/foo'.
 */
function find_test_files($file) {
  $mappage = array(
    'quick'      => 'hphp/test/quick',
    'slow'       => 'hphp/test/slow',
    'spec'       => 'hphp/test/spec',
    'debugger'   => 'hphp/test/server/debugger/tests',
    'http'       => 'hphp/test/server/http/tests',
    'fastcgi'    => 'hphp/test/server/fastcgi/tests',
    'zend'       => 'hphp/test/zend/good',
    'facebook'   => 'hphp/facebook/test',

    // Subsets of zend tests.
    'zend_ext'    => 'hphp/test/zend/good/ext',
    'zend_ext_am' => 'hphp/test/zend/good/ext/[a-m]*',
    'zend_ext_nz' => 'hphp/test/zend/good/ext/[n-z]*',
    'zend_Zend'   => 'hphp/test/zend/good/Zend',
    'zend_tests'  => 'hphp/test/zend/good/tests',
    'zend_bad'    => 'hphp/test/zend/bad',
  );

  if (isset($mappage[$file])) {
    $matches = glob(hphp_home().'/'.$mappage[$file]);
    if (count($matches) == 0) {
      error(sprintf(
        "Convenience test name '%s' is recognized but does not match any test ".
        "files (pattern = '%s', hphp_home = '%s')",
        $file, $mappage[$file], hphp_home()));
    }
    return $matches;
  } else {
    return array($file, );
  }
}

// Some tests have to be run together in the same test bucket, serially, one
// after other in order to avoid races and other collisions.
function serial_only_tests($tests) {
  if (is_testing_dso_extension()) {
    return array();
  }
  // Add a <testname>.php.serial file to make your test run in the serial
  // bucket.
  $serial_tests = array_filter(
    $tests,
    function($test) {
      return file_exists($test . '.serial');
    }
  );
  return $serial_tests;
}

function find_tests($files, array $options = null) {
  if (!$files) {
    $files = array('quick');
  }
  if ($files == array('all')) {
    $files = array('quick', 'slow', 'spec', 'zend', 'fastcgi');
  }
  $ft = array();
  foreach ($files as $file) {
    $ft = array_merge($ft, find_test_files($file));
  }
  $files = $ft;
  foreach ($files as &$file) {
    if (!@stat($file)) {
      error("Not valid file or directory: '$file'");
    }
    $file = preg_replace(',//+,', '/', realpath($file));
    $file = preg_replace(',^'.getcwd().'/,', '', $file);
  }
  $files = array_map('escapeshellarg', $files);
  $files = implode(' ', $files);
  if (isset($options['typechecker'])) {
    $tests = explode("\n", shell_exec(
      "find $files -name '*.php' -o -name '*.php.type-errors'"
    ));
    // The above will get all the php files. Now filter out only the ones
    // that have a .hhconfig associated with it.
    $tests = array_filter(
      $tests,
      function($test) {
        return (file_exists($test . '.typechecker.expect') ||
                file_exists($test . '.typechecker.expectf')) &&
                file_exists($test . '.hhconfig');
      }
    );
  } else {
    $tests = explode("\n", shell_exec(
      "find $files -name '*.php' -o -name '*.php.type-errors' " .
      "-o -name '*.hhas' | grep -v round_trip.hhas"
    ));
    $tests = array_filter(
      $tests,
      function($test) {
        return file_exists($test . '.expect') ||
               file_exists($test . '.expectf') ||
               file_exists($test . '.hhvm.expect') ||
               file_exists($test . '.hhvm.expectf');
      }
    );
  }
  if (!$tests) {
    error("Could not find any tests associated with your options.\n" .
          "Make sure your test path is correct and that you have " .
          "the right expect files for the tests you are trying to run.\n" .
          usage());
  }
  asort($tests);
  $tests = array_filter($tests);
  if (!empty($options['exclude'])) {
    $exclude = $options['exclude'];
    $tests = array_filter($tests, function($test) use ($exclude) {
      return (false === strpos($test, $exclude));
    });
  }
  if (!empty($options['exclude-pattern'])) {
    $exclude = $options['exclude-pattern'];
    $tests = array_filter($tests, function($test) use ($exclude) {
      return !preg_match($exclude, $test);
    });
  }
  if (!empty($options['include'])) {
    $include = $options['include'];
    $tests = array_filter($tests, function($test) use ($include) {
      return (false !== strpos($test, $include));
    });
  }
  if (!empty($options['include-pattern'])) {
    $include = $options['include-pattern'];
    $tests = array_filter($tests, function($test) use ($include) {
      return preg_match($include, $test);
    });
  }
  return $tests;
}

function list_tests($files, $options) {
  $args = array();
  $mode = idx($options, 'mode', '');
  switch ($mode) {
    case '':
    case 'jit':
      $args[] = '-m jit';
      break;
    case 'interp';
      $args[] = '-m interp';
      break;
    default:
      throw new Exception("Unsupported mode for listing tests: ".$mode);
  }

  if (isset($options['repo'])) {
    $args[] = '-r';
  }

  if (isset($options['relocate'])) {
    $args[] = '--relocate';
    $args[] = $options['relocate'];
  }

  foreach (find_tests($files, $options) as $test) {
    print Status::jsonEncode(array(
      'args' => implode(' ', $args),
      'name' => $test,
    ))."\n";
  }
}

function find_test_ext($test, $ext) {
  if (is_file("{$test}.{$ext}")) {
    return "{$test}.{$ext}";
  }
  return find_file_for_dir(dirname($test), "config.{$ext}");
}

function find_file($test, $name) {
  return find_file_for_dir(dirname($test), $name);
}

function find_file_for_dir($dir, $name) {
  // Handle the case where the $dir might come in as '.' because you
  // are running the test runner on a file from the same directory as
  // the test e.g., './mytest.php'. dirname() will give you the '.' when
  // you actually have a lot of path to traverse upwards like
  // /home/you/code/tests/mytest.php. Use realpath() to get that.
  $dir = realpath($dir);
  while ($dir !== '/' && is_dir($dir)) {
    $file = "$dir/$name";
    if (is_file($file)) {
      return $file;
    }
    $dir = dirname($dir);
  }
  $file = __DIR__.'/'.$name;
  if (file_exists($file)) {
    return $file;
  }
  return null;
}

function find_debug_config($test, $name) {
  $debug_config = find_file_for_dir(dirname($test), $name);
  if ($debug_config !== null) {
    return "-m debug --debug-config ".$debug_config;
  }
  return "";
}

function mode_cmd($options) {
  $repo_args = '';
  if (!isset($options['repo'])) {
    // Set the non-repo-mode shared repo.
    // When in repo mode, we set our own central path.
    $repo_args = "-vRepo.Local.Mode=-- -vRepo.Central.Path=".verify_hhbc();
  }
  $jit_args = "$repo_args -vEval.Jit=true";
  $mode = idx($options, 'mode', '');
  switch ($mode) {
    case '':
    case 'jit':
      return "$jit_args";
    case 'llvm':
      return "$jit_args -vEval.JitLLVM=2";
    case 'pgo':
      return $jit_args.
        ' -vEval.JitPGO=1'.
        ' -vEval.JitPGORegionSelector=hottrace'.
        ' -vEval.JitPGOHotOnly=0';
    case 'interp':
      return "$repo_args -vEval.Jit=0";
    default:
      error("-m must be one of jit | pgo | interp. Got: '$mode'");
  }
}

function extra_args($options) {
  return idx($options, 'args', '');
}

function hhvm_cmd_impl() {
  $args = func_get_args();
  $options = array_shift($args);
  $config = array_shift($args);
  $extra_args = $args;
  $args = array(
    hhvm_path(),
    '-c',
    $config,
    '-vEval.EnableArgsInBacktraces=true',
    '-vEval.EnableIntrinsicsExtension=true',
    mode_cmd($options),
    isset($options['arm']) ? '-vEval.SimulateARM=1' : '',
    isset($options['llvm']) ? '-vEval.JitLLVM=2' : '',
    isset($options['wholecfg']) ? '-vEval.JitPGORegionSelector=wholecfg' : '',
    isset($options['require-llvm']) ? '-vEval.JitLLVM=3' : '',
    extra_args($options),
  );

  if (isset($options['relocate'])) {
    $args[] = '--count='.($options['relocate'] * 2);
    $args[] = '-vEval.JitAHotSize=6000000';
    $args[] = '-vEval.HotFuncCount=0';
    $args[] = '-vEval.PerfRelocate='.$options['relocate'];
  }

  if (!isset($options['cores'])) {
    $args[] = '-vResourceLimit.CoreFileSize=0';
  }

  return implode(' ', array_merge($args, $extra_args));
}

// Return the command and the env to run it in.
function hhvm_cmd($options, $test, $test_run = null) {
  if ($test_run === null) {
    $test_run = $test;
  }
  // hdf support is only temporary until we fully migrate to ini
  // Discourage broad use.
  $hdf_suffix = ".use.for.ini.migration.testing.only.hdf";
  $hdf = file_exists($test.$hdf_suffix)
       ? '-c ' . $test . $hdf_suffix
       : "";
  $cmd = hhvm_cmd_impl(
    $options,
    find_test_ext($test, 'ini'),
    $hdf,
    find_debug_config($test, 'hphpd.ini'),
    read_opts_file(find_test_ext($test, 'opts')),
    '--file',
    escapeshellarg($test_run)
  );

  // Special support for tests that require a path to the current
  // test directory for things like prepend_file and append_file
  // testing.
  if (file_exists($test.'.ini')) {
    $contents = file_get_contents($test.'.ini');
    if (strpos($contents, '{PWD}') !== false) {
      $test_ini = tempnam('/tmp', $test).'.ini';
      file_put_contents($test_ini,
                        str_replace('{PWD}', dirname($test), $contents));
      $cmd .= " -c $test_ini";
    }
  }
  if ($hdf !== "") {
    $contents = file_get_contents($test.$hdf_suffix);
    if (strpos($contents, '{PWD}') !== false) {
      $test_hdf = tempnam('/tmp', $test).$hdf_suffix;
      file_put_contents($test_hdf,
                        str_replace('{PWD}', dirname($test), $contents));
      $cmd .= " -c $test_hdf";
    }
  }

  if (isset($options['repo'])) {
    $hhbbc_repo = "\"$test.repo/hhvm.hhbbc\"";
    $cmd .= ' -vRepo.Authoritative=true -vRepo.Commit=0';
    $cmd .= " -vRepo.Central.Path=$hhbbc_repo";
  }

  // Command line arguments
  $cli_args = find_test_ext($test, 'cli_args');
  if ($cli_args !== null) {
    $cmd .= " " . file_get_contents($cli_args);
  }

  $env = $_ENV;
  $in = find_test_ext($test, 'in');
  if ($in !== null) {
    $cmd .= ' < ' . escapeshellarg($in);
    // If we're piping the input into the command then setup a simple
    // dumb terminal so hhvm doesn't try to control it and pollute the
    // output with control characters, which could change depending on
    // a wide variety of terminal settings.
    $env["TERM"] = "dumb";
  }

  return array($cmd, $env);
}

function hphp_cmd($options, $test) {
  $extra_args = preg_replace("/-v\s*/", "-vRuntime.", extra_args($options));
  return implode(" ", array(
    "HHVM_DISABLE_HHBBC2=1",
    hhvm_path(),
    '--hphp',
    '--config',
    find_file($test, 'hphp_config.ini'),
    read_opts_file("$test.hphp_opts"),
    "-thhbc -l0 -k1 -o \"$test.repo\" \"$test\"",
    $extra_args
  ));
}

function hhbbc_cmd($options, $test) {
  return implode(" ", array(
    hhvm_path(),
    '--hhbbc',
    '--no-logging',
    '--parallel-num-threads=1',
    read_opts_file("$test.hhbbc_opts"),
    "-o \"$test.repo/hhvm.hhbbc\" \"$test.repo/hhvm.hhbc\"",
  ));
}

function hh_server_cmd($options, $test) {
  // In order to run hh_server --check on only one file, we copy all of the
  // files associated with the test to a temporary directory, rename the
  // basename($test_file).hhconfig file to just .hhconfig and set the command
  // appropriately.
  $temp_dir = uniqid('/tmp/hh-test-runner-');
  mkdir($temp_dir);
  foreach (glob($test . '*') as $test_file) {
    copy($test_file, $temp_dir . '/' . basename($test_file));
    if (strpos($test_file, '.hhconfig') !== false) {
      rename(
        $temp_dir . '/' . basename($test) . '.hhconfig',
        $temp_dir . '/.hhconfig'
      );
    } else if (strpos($test_file, '.type-errors') !== false) {
      // In order to actually run hh_server --check successfully, all files
      // named *.php.type-errors have to be renamed *.php
      rename(
        $temp_dir . '/' . basename($test_file),
        $temp_dir . '/' . str_replace('.type-errors', '', basename($test_file))
      );
    }
  }
  // Just copy all the .php.inc files, even if they are not related since
  // unrelated ones will be ignored anyway. This just makes it easier to
  // start with instead of doing a search inside the test file for requires
  // and includes and extracting it.
  foreach (glob(dirname($test) . "/*.inc.php") as $inc_file) {
    copy($inc_file, $temp_dir . '/' . basename($inc_file));
  }
  $cmd = hh_server_path() .  ' --check ' . $temp_dir;
  return array($cmd, ' ', $temp_dir);
}

class Status {
  private static $results = array();
  private static $mode = 0;

  private static $use_color = false;

  private static $queue = null;
  private static $killed = false;
  public static $key;

  private static $overall_start_time = 0;
  private static $overall_end_time = 0;

  private static $tempdir = "";

  const MODE_NORMAL = 0;
  const MODE_VERBOSE = 1;
  const MODE_FBMAKE = 2;
  const MODE_TESTPILOT = 3;

  const MSG_STARTED = 7;
  const MSG_FINISHED = 1;
  const MSG_TEST_PASS = 2;
  const MSG_TEST_FAIL = 4;
  const MSG_TEST_SKIP = 5;
  const MSG_SERVER_RESTARTED = 6;

  const RED = 31;
  const GREEN = 32;
  const YELLOW = 33;
  const BLUE = 34;

  const PASS_SERVER = 0;
  const SKIP_SERVER = 1;
  const PASS_CLI = 2;

  private static function getTempDir() {
    self::$tempdir = sys_get_temp_dir();
    // Apparently some systems might not put the trailing slash
    if (substr(self::$tempdir, -1) !== "/") {
      self::$tempdir .= "/";
    }
    self::$tempdir .= substr( md5(rand()), 0, 8);
    mkdir(self::$tempdir);
  }

  // Since we run the tests in forked processes, state is not shared
  // So we cannot keep a static variable adding individual test times.
  // But we can put the times files and add the values later.
  public static function setTestTime($time) {
    file_put_contents(tempnam(self::$tempdir, "trun"), $time);
  }

  // The total time running the tests if they were run serially.
  public static function addTestTimesSerial() {
    $time = 0;
    $files = scandir(self::$tempdir);
    foreach ($files as $file) {
      if (strpos($file, 'trun') === 0) {
        $time += floatval(file_get_contents(self::$tempdir . "/" . $file));
        unlink(self::$tempdir . "/" . $file);
      }
    }
    return $time;
  }

  public static function setMode($mode) {
    self::$mode = $mode;
  }

  public static function setUseColor($use) {
    self::$use_color = $use;
  }

  public static function getMode() {
    return self::$mode;
  }

  public static function getOverallStartTime() {
    return self::$overall_start_time;
  }

  public static function getOverallEndTime() {
    return self::$overall_end_time;
  }

  public static function started() {
    self::getTempDir();
    self::send(self::MSG_STARTED, null);
    self::$overall_start_time = microtime(true);
  }

  public static function finished() {
    self::$overall_end_time = microtime(true);
    self::send(self::MSG_FINISHED, null);
  }

  public static function killQueue() {
    if (!self::$killed) {
      msg_remove_queue(self::$queue);
      self::$queue = null;
      self::$killed = true;
    }
  }

  public static function pass($test, $detail, $time, $stime, $etime) {
    array_push(self::$results, array('name' => $test,
                                     'status' => 'passed',
                                     'start_time' => $stime,
                                     'end_time' => $etime,
                                     'time' => $time));
    $how = $detail === 'pass-server' ? self::PASS_SERVER :
      ($detail === 'skip-server' ? self::SKIP_SERVER : self::PASS_CLI);
    self::send(self::MSG_TEST_PASS, array($test, $how, $time, $stime, $etime));
  }

  public static function skip($test, $reason, $time, $stime, $etime) {
    if (self::getMode() === self::MODE_FBMAKE) {
      /* Intentionally supress skips */
    } elseif (self::getMode() === self::MODE_TESTPILOT) {
      /* testpilot needs a positive response for every test run, report
       * that this test isn't relevant so it can silently drop. */
      array_push(self::$results, array('name' => $test,
                                       'status' => 'not_relevant',
                                       'start_time' => $stime,
                                       'end_time' => $etime,
                                       'time' => $time));
    } else {
      array_push(self::$results, array('name' => $test,
                                       'status' => 'skipped',
                                       'start_time' => $stime,
                                       'end_time' => $etime,
                                       'time' => $time));
    }
    self::send(self::MSG_TEST_SKIP,
               array($test, $reason, $time, $stime, $etime));
  }

  public static function fail($test, $time, $stime, $etime) {
    array_push(self::$results, array(
      'name' => $test,
      'status' => 'failed',
      'details' => self::utf8Sanitize(@file_get_contents("$test.diff")),
      'start_time' => $stime,
      'end_time' => $etime,
      'time' => $time
    ));
    self::send(self::MSG_TEST_FAIL, array($test, $time, $stime, $etime));
  }

  public static function serverRestarted() {
    self::send(self::MSG_SERVER_RESTARTED, null);
  }

  private static function send($type, $msg) {
    if (self::$killed) {
      return;
    }
    msg_send(self::getQueue(), $type, $msg);
  }

  /**
   * Takes a variable number of string arguments. If color output is enabled
   * and any one of the arguments is preceded by an integer (see the color
   * constants above), that argument will be given the indicated color.
   */
  public static function sayColor() {
    $args = func_get_args();
    while (count($args)) {
      $color = null;
      $str = array_shift($args);
      if (is_integer($str)) {
        $color = $str;
        if (self::$use_color) {
          print "\033[0;${color}m";
        }
        $str = array_shift($args);
      }

      print $str;

      if (self::$use_color && !is_null($color)) {
        print "\033[0m";
      }
    }
  }

  public static function sayFBMake($test, $status, $stime, $etime) {
    $start = array('op' => 'start', 'test' => $test);
    $end = array('op' => 'test_done', 'test' => $test, 'status' => $status,
                 'start_time' => $stime, 'end_time' => $etime);
    if ($status == 'failed') {
      $end['details'] = self::utf8Sanitize(@file_get_contents("$test.diff"));
    }
    self::say($start, $end);
  }

  public static function getResults() {
    return self::$results;
  }

  /** Output is in the format expected by JsonTestRunner. */
  public static function say(/* ... */) {
    $data = array_map(function($row) {
      return self::jsonEncode($row) . "\n";
    }, func_get_args());
    fwrite(STDERR, implode("", $data));
  }

  public static function hasCursorControl() {
    // for runs on hudson-ci.org (aka jenkins).
    if (getenv("HUDSON_URL")) {
      return false;
    }
    // for runs on travis-ci.org
    if (getenv("TRAVIS")) {
      return false;
    }
    $stty = self::getSTTY();
    if (!$stty) {
      return false;
    }
    return strpos($stty, 'erase = <undef>') === false;
  }

  public static function getSTTY() {
    $descriptorspec = array(1 => array("pipe", "w"), 2 => array("pipe", "w"));
    $process = proc_open(
      'stty -a', $descriptorspec, $pipes, null, null,
      array('suppress_errors' => true)
    );
    $stty = stream_get_contents($pipes[1]);
    proc_close($process);
    return $stty;
  }

  public static function utf8Sanitize($str) {
    if (!is_string($str)) {
      // We sometimes get called with the
      // return value of file_get_contents()
      // when fgc() has failed.
      return '';
    }

    if (class_exists('UConverter')) {
      return UConverter::transcode($str, 'UTF-8', 'UTF-8');
    }

    // UConverter is PHP5.5 or later.
    // Do the equivalent using a slower user-space implementation.
    $ret = '';
    $len = strlen($str);
    $pos = 0;
    while ($pos < $len) {
      $c = $str[$pos];
      $co = ord($c);
      if ($co < 0x80) {
        // U+0000 - U+007F ASCII
        $ret .= $c;
        ++$pos;
      } elseif ((($co & 0xE0) == 0xC0) &&
                (($len - $pos) > 1) &&
                ((ord($str[$pos+1]) & 0xC0) == 0x80)) {
        // U+0080 - U+07FF Lower Basic Multilingual Plane
        $ret .= substr($str, $pos, 2);
        $pos += 2;
      } elseif ((($co & 0xF0) == 0xE0) &&
                (($len - $pos) > 2) &&
                ((ord($str[$pos+1]) & 0xC0) == 0x80) &&
                ((ord($str[$pos+2]) & 0xC0) == 0x80)) {
        // U+0800 - U+FFFF Upper Basic Multilingual Plane
        $ret .= substr($str, $pos, 3);
        $pos += 3;
      } elseif ((($co & 0xF8) == 0xF0) &&
                (($len - $pos) > 3) &&
                ((ord($str[$pos+1]) & 0xC0) == 0x80) &&
                ((ord($str[$pos+2]) & 0xC0) == 0x80) &&
                ((ord($str[$pos+3]) & 0xC0) == 0x80)) {
        // U+010000 - U+10FFFF Supplementary Multilingual Planes
        $ret .= substr($str, $pos, 4);
        $pos += 4;
      } else {
        // Invalid UTF8
        $ret .= "\xEF\xBF\xBD"; // U+FFFD REPLACEMENT CHARACTER
        ++$pos;
      }
    }
    return $ret;
  }

  public static function jsonEncode($data) {
    // JSON_UNESCAPED_SLASHES is Zend 5.4+.
    if (defined("JSON_UNESCAPED_SLASHES")) {
      return json_encode($data, JSON_UNESCAPED_SLASHES);
    }

    $json = json_encode($data);
    return str_replace('\\/', '/', $json);
  }

  public static function getQueue() {
    if (!self::$queue) {
      self::$queue = msg_get_queue(self::$key);
    }
    return self::$queue;
  }
}

function clean_intermediate_files($test, $options) {
  if (isset($options['no-clean'])) {
    return;
  }
  $exts = array('out', 'diff', 'repo');
  foreach ($exts as $ext) {
    $file = "$test.$ext";
    if (file_exists($file)) {
      if (is_dir($file)) {
        foreach(new RecursiveIteratorIterator(new
            RecursiveDirectoryIterator($file, FilesystemIterator::SKIP_DOTS),
            RecursiveIteratorIterator::CHILD_FIRST) as $path) {
          $path->isDir()
          ? rmdir($path->getPathname())
          : unlink($path->getPathname());
        }
        rmdir($file);
      } else {
        unlink($file);
      }
    }
  }
}

function run($options, $tests, $bad_test_file) {
  foreach ($tests as $test) {
    $stime = time();
    $time = microtime(true);
    $status = run_and_lock_test($options, $test);
    $time = microtime(true) - $time;
    $etime = time();
    Status::setTestTime($time);
    if ($status === 'skip') {
      Status::skip($test, null, $time, $stime, $etime);
      clean_intermediate_files($test, $options);
    } else if ($status === 'skip-norepo') {
      Status::skip($test, 'norepo', $time, $stime, $etime);
      clean_intermediate_files($test, $options);
    } else if ($status === 'skip-onlyrepo') {
      Status::skip($test, 'onlyrepo', $time, $stime, $etime);
      clean_intermediate_files($test, $options);
    } else if ($status) {
      Status::pass($test, $status, $time, $stime, $etime);
      clean_intermediate_files($test, $options);
    } else {
      Status::fail($test, $time, $stime, $etime);
    }
  }
  file_put_contents($bad_test_file, json_encode(Status::getResults()));
  foreach (Status::getResults() as $result) {
    if ($result['status'] == 'failed') {
      return 1;
    }
  }
  return 0;
}

function skip_test($options, $test) {
  $skipif_test = find_test_ext($test, 'skipif');
  if (!$skipif_test) {
    return false;
  }

  // For now, run the .skipif in non-repo since building a repo for it is hard.
  $options_without_repo = $options;
  unset($options_without_repo['repo']);

  list($hhvm, $_) = hhvm_cmd($options_without_repo, $test, $skipif_test);
  $descriptorspec = array(
    0 => array("pipe", "r"),
    1 => array("pipe", "w"),
    2 => array("pipe", "w"),
  );
  $pipes = null;
  $process = proc_open("$hhvm $test 2>&1", $descriptorspec, $pipes);
  if (!is_resource($process)) {
    // This is weird. We can't run HHVM but we probably shouldn't skip the test
    // since on a broken build everything will show up as skipped and give you a
    // SHIPIT.
    return false;
  }

  fclose($pipes[0]);
  $output = stream_get_contents($pipes[1]);
  fclose($pipes[1]);
  proc_close($process);

  // The standard php5 .skipif semantics is if the .skipif outputs ANYTHING
  // then it should be skipped. This is a poor design, but I'll just add a
  // small blacklist of things that are really bad if they are output so we
  // surface the errors in the tests themselves.
  if (stripos($output, 'segmentation fault') !== false) {
    return false;
  }

  return strlen($output) != 0;
}

function comp_line($l1, $l2, $is_reg) {
  if ($is_reg) {
    return preg_match('/^'. $l1 . '$/s', $l2);
  } else {
    return !strcmp($l1, $l2);
  }
}

function count_array_diff($ar1, $ar2, $is_reg, $w, $idx1, $idx2, $cnt1, $cnt2,
                          $steps) {
  $equal = 0;

  while ($idx1 < $cnt1 && $idx2 < $cnt2 && comp_line($ar1[$idx1], $ar2[$idx2],
                                                     $is_reg)) {
    $idx1++;
    $idx2++;
    $equal++;
    $steps--;
  }
  if (--$steps > 0) {
    $eq1 = 0;
    $st = $steps / 2;

    for ($ofs1 = $idx1 + 1; $ofs1 < $cnt1 && $st-- > 0; $ofs1++) {
      $eq = @count_array_diff($ar1, $ar2, $is_reg, $w, $ofs1, $idx2, $cnt1,
                              $cnt2, $st);

      if ($eq > $eq1) {
        $eq1 = $eq;
      }
    }

    $eq2 = 0;
    $st = $steps;

    for ($ofs2 = $idx2 + 1; $ofs2 < $cnt2 && $st-- > 0; $ofs2++) {
      $eq = @count_array_diff($ar1, $ar2, $is_reg, $w, $idx1, $ofs2, $cnt1, $cnt2, $st);
      if ($eq > $eq2) {
        $eq2 = $eq;
      }
    }

    if ($eq1 > $eq2) {
      $equal += $eq1;
    } else if ($eq2 > 0) {
      $equal += $eq2;
    }
  }

  return $equal;
}

function generate_array_diff($ar1, $ar2, $is_reg, $w) {
  $idx1 = 0; $ofs1 = 0; $cnt1 = @count($ar1);
  $idx2 = 0; $ofs2 = 0; $cnt2 = @count($ar2);
  $diff = array();
  $old1 = array();
  $old2 = array();

  while ($idx1 < $cnt1 && $idx2 < $cnt2) {

    if (comp_line($ar1[$idx1], $ar2[$idx2], $is_reg)) {
      $idx1++;
      $idx2++;
      continue;
    } else {

      $c1 = @count_array_diff($ar1, $ar2, $is_reg, $w, $idx1+1, $idx2, $cnt1,
                              $cnt2, 10);
      $c2 = @count_array_diff($ar1, $ar2, $is_reg, $w, $idx1, $idx2+1, $cnt1,
                              $cnt2, 10);

      if ($c1 > $c2) {
        $old1[$idx1] = sprintf("%03d- ", $idx1+1) . $w[$idx1++];
        $last = 1;
      } else if ($c2 > 0) {
        $old2[$idx2] = sprintf("%03d+ ", $idx2+1) . $ar2[$idx2++];
        $last = 2;
      } else {
        $old1[$idx1] = sprintf("%03d- ", $idx1+1) . $w[$idx1++];
        $old2[$idx2] = sprintf("%03d+ ", $idx2+1) . $ar2[$idx2++];
      }
    }
  }

  reset($old1); $k1 = key($old1); $l1 = -2;
  reset($old2); $k2 = key($old2); $l2 = -2;

  while ($k1 !== null || $k2 !== null) {

    if ($k1 == $l1 + 1 || $k2 === null) {
      $l1 = $k1;
      $diff[] = current($old1);
      $k1 = next($old1) ? key($old1) : null;
    } else if ($k2 == $l2 + 1 || $k1 === null) {
      $l2 = $k2;
      $diff[] = current($old2);
      $k2 = next($old2) ? key($old2) : null;
    } else if ($k1 < $k2) {
      $l1 = $k1;
      $diff[] = current($old1);
      $k1 = next($old1) ? key($old1) : null;
    } else {
      $l2 = $k2;
      $diff[] = current($old2);
      $k2 = next($old2) ? key($old2) : null;
    }
  }

  while ($idx1 < $cnt1) {
    $diff[] = sprintf("%03d- ", $idx1 + 1) . $w[$idx1++];
  }

  while ($idx2 < $cnt2) {
    $diff[] = sprintf("%03d+ ", $idx2 + 1) . $ar2[$idx2++];
  }

  return $diff;
}

function generate_diff($wanted, $wanted_re, $output)
{
  $w = explode("\n", $wanted);
  $o = explode("\n", $output);
  if (is_null($wanted_re)) {
    $r = $w;
  } else {
    if (preg_match('/^\((.*)\)\{(\d+)\}$/s', $wanted_re, $m)) {
      $t = explode("\n", $m[1]);
      $r = array();
      $w2 = array();
      for ($i = 0; $i < $m[2]; $i++) {
        foreach ($t as $v) {
          $r[] = $v;
        }
        foreach ($w as $v) {
          $w2[] = $v;
        }
      }
      $w = $wanted === $wanted_re ? $r : $w2;
    } else {
      $r = explode("\n", $wanted_re);
    }
  }
  $diff = generate_array_diff($r, $o, !is_null($wanted_re), $w);

  return implode("\r\n", $diff);
}

function dump_hhas_to_temp($hhvm_cmd, $test) {
  $tmp_file = $test . '.round_trip.hhas';
  system("$hhvm_cmd -vEval.DumpHhas=1 > $tmp_file", $ret);
  if ($ret) { echo "system failed\n"; exit(1); }
  return $tmp_file;
}

const HHAS_EXT = '.hhas';
function can_run_server_test($test) {
  return
    !is_file("$test.noserver") &&
    !find_test_ext($test, 'opts') &&
    !is_file("$test.ini") &&
    !is_file("$test.onlyrepo") &&
    strpos($test, 'quick/debugger') === false &&
    strpos($test, 'quick/xenon') === false &&
    strpos($test, 'slow/streams/') === false &&
    strpos($test, 'slow/ext_mongo/') === false &&
    strpos($test, 'slow/ext_oauth/') === false &&
    strpos($test, 'slow/ext_yaml/') === false &&
    strpos($test, 'slow/debugger/') === false &&
    strpos($test, 'slow/type_profiler/debugger/') === false &&
    strpos($test, 'zend/good/ext/standard/tests/array/') === false &&
    strpos($test, 'zend/good/ext/ftp') === false &&
    strrpos($test, HHAS_EXT) !== (strlen($test) - strlen(HHAS_EXT))
    ;
}

const SERVER_TIMEOUT = 45;
function run_config_server($options, $test) {
  if (!isset($options['server']) || !can_run_server_test($test)) {
    return null;
  }

  $config = find_file_for_dir(dirname($test), 'config.ini');
  $port = $options['servers']['configs'][$config]['port'];
  $ch = curl_init("localhost:$port/$test");
  curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
  curl_setopt($ch, CURLOPT_TIMEOUT, SERVER_TIMEOUT);
  curl_setopt($ch, CURLOPT_BINARYTRANSFER, true);
  $output = curl_exec($ch);
  if ($output === false) {
    // The server probably crashed so fall back to cli to determine if this was
    // the test that caused the crash. Our parent process will see that the
    // server died and restart it.
    if (getenv('HHVM_TEST_SERVER_LOG')) {
      printf("Curl failed: %d\n", curl_errno($ch));
    }
    return null;
  }
  curl_close($ch);
  $output = trim($output);

  return array($output, '');
}

function run_config_cli($options, $test, $cmd, $cmd_env) {
  if (isset($options['log']) && !isset($options['typechecker'])) {
    $cmd_env['TRACE'] = 'printir:1';
    $cmd_env['HPHP_TRACE_FILE'] = $test . '.log';
  }

  $descriptorspec = array(
    0 => array("pipe", "r"),
    1 => array("pipe", "w"),
    2 => array("pipe", "w"),
  );
  $pipes = null;
  if (isset($options['typechecker'])) {
    $process = proc_open(
      "$cmd 2>/dev/null", $descriptorspec, $pipes, null, $cmd_env
    );
  } else {
    $process = proc_open("$cmd 2>&1", $descriptorspec, $pipes, null, $cmd_env);
  }
  if (!is_resource($process)) {
    file_put_contents("$test.diff", "Couldn't invoke $cmd");
    return false;
  }

  fclose($pipes[0]);
  $output = stream_get_contents($pipes[1]);
  $output = trim($output);
  $stderr = stream_get_contents($pipes[2]);
  fclose($pipes[1]);
  fclose($pipes[2]);
  proc_close($process);

  return array($output, $stderr);
}

function run_config_post($outputs, $test, $options) {
  $output = $outputs[0];
  $stderr = $outputs[1];
  file_put_contents("$test.out", $output);

  // hhvm redirects errors to stdout, so anything on stderr is really bad.
  if ($stderr) {
    file_put_contents(
      "$test.diff",
      "Test failed because the process wrote on stderr:\n$stderr"
    );
    return false;
  }

  // Needed for testing non-hhvm binaries that don't actually run the code
  // e.g. parser/test/parse_tester.cpp.
  if ($output == "FORCE PASS") {
    return true;
  }

  $repeats = isset($options['relocate']) ? $options['relocate'] * 2 : 0;

  list($file, $type) = get_expect_file_and_type($test, $options);
  if ($file === null || $type === null) {
    file_put_contents(
      "$test.diff", "No $test.expect, $test.expectf, " .
      "$test.hhvm.expect, $test.hhvm.expectf, " .
      "$test.typechecker.expect, $test.typechecker.expectf, " .
      "nor $test.expectregex. Is $test even a test?"
    );
    return false;
  }

  $is_tc = isset($options['typechecker']);
  if ((!$is_tc && ($type === 'expect' || $type === 'hhvm.expect')) ||
      ($is_tc && $type === 'typechecker.expect')) {
    $wanted = trim(file_get_contents($file));
    if (!$repeats) {
      $passed = !strcmp($output, $wanted);
      if (!$passed) {
        file_put_contents("$test.diff", generate_diff($wanted, null, $output));
      }
      return $passed;
    }
    $wanted_re = preg_quote($wanted);
  } else if ((!$is_tc && ($type === 'expectf' || $type === 'hhvm.expectf')) ||
             ($is_tc && $type === 'typechecker.expectf')) {
    $wanted = trim(file_get_contents($file));
    $wanted_re = $wanted;

    // do preg_quote, but miss out any %r delimited sections.
    $temp = "";
    $r = "%r";
    $startOffset = 0;
    $length = strlen($wanted_re);
    while($startOffset < $length) {
      $start = strpos($wanted_re, $r, $startOffset);
      if ($start !== false) {
        // we have found a start tag.
        $end = strpos($wanted_re, $r, $start+2);
        if ($end === false) {
          // unbalanced tag, ignore it.
          $end = $start = $length;
        }
      } else {
        // no more %r sections.
        $start = $end = $length;
      }
      // quote a non re portion of the string.
      $temp = $temp.preg_quote(substr($wanted_re, $startOffset,
                                      ($start - $startOffset)),  '/');
      // add the re unquoted.
      if ($end > $start) {
        $temp = $temp.'('.substr($wanted_re, $start+2, ($end - $start-2)).')';
      }
      $startOffset = $end + 2;
    }
    $wanted_re = $temp;

    $wanted_re = str_replace(
      array('%binary_string_optional%'),
      'string',
      $wanted_re
    );
    $wanted_re = str_replace(
      array('%unicode_string_optional%'),
      'string',
      $wanted_re
    );
    $wanted_re = str_replace(
      array('%unicode\|string%', '%string\|unicode%'),
      'string',
      $wanted_re
    );
    $wanted_re = str_replace(
      array('%u\|b%', '%b\|u%'),
      '',
      $wanted_re
    );
    // Stick to basics.
    $wanted_re = str_replace('%e', '\\' . DIRECTORY_SEPARATOR, $wanted_re);
    $wanted_re = str_replace('%s', '[^\r\n]+', $wanted_re);
    $wanted_re = str_replace('%S', '[^\r\n]*', $wanted_re);
    $wanted_re = str_replace('%a', '.+', $wanted_re);
    $wanted_re = str_replace('%A', '.*', $wanted_re);
    $wanted_re = str_replace('%w', '\s*', $wanted_re);
    $wanted_re = str_replace('%i', '[+-]?\d+', $wanted_re);
    $wanted_re = str_replace('%d', '\d+', $wanted_re);
    $wanted_re = str_replace('%x', '[0-9a-fA-F]+', $wanted_re);
    // %f allows two points "-.0.0" but that is the best *simple* expression.
    $wanted_re = str_replace('%f', '[+-]?\.?\d+\.?\d*(?:[Ee][+-]?\d+)?',
                             $wanted_re);
    $wanted_re = str_replace('%c', '.', $wanted_re);
    // must be last.
    $wanted_re = str_replace('%%', '%%?', $wanted_re);

    // Normalize newlines.
    $wanted_re = preg_replace("/(\r\n?|\n)/", "\n", $wanted_re);
    $output    = preg_replace("/(\r\n?|\n)/", "\n", $output);
  } else if (!$is_tc && $type === 'expectregex') {
    $wanted_re = trim(file_get_contents($file));
  } else {
    throw new Exception("Unsupported expect file type: ".$type);
  }

  if ($repeats) {
    $wanted_re = "($wanted_re\s*)".'{'.$repeats.'}';
  }
  if (!isset($wanted)) $wanted = $wanted_re;
  $passed = @preg_match("/^$wanted_re\$/s", $output);
  if ($passed === false && $repeats) {
    // $repeats can cause the regex to become too big, and fail
    // to compile.
    return 'skip';
  }
  if (!$passed) {
    file_put_contents("$test.diff",
                      generate_diff($wanted_re, $wanted_re, $output));
  }
  return $passed;
}

function run_one_config($options, $test, $cmd, $cmd_env) {
  $outputs = run_config_cli($options, $test, $cmd, $cmd_env);
  if ($outputs === false) return false;
  return run_config_post($outputs, $test, $options);
}

function run_and_lock_test($options, $test) {
  $lock = fopen($test, 'r');
  if (!flock($lock, LOCK_EX)) return false;
  if (isset($options['typechecker'])) {
    $result = run_typechecker_test($options, $test);
  } else {
    $result = run_test($options, $test);
  }
  if (!flock($lock, LOCK_UN)) return false;
  fclose($lock);
  return $result;
}

function run_typechecker_test($options, $test) {
  if (skip_test($options, $test)) return 'skip';
  if (!file_exists($test . ".hhconfig")) return 'skip';
  list($hh_server, $hh_server_env, $temp_dir) = hh_server_cmd($options, $test);
  if (is_executable('/usr/bin/timeout')) {
    $hh_server = '/usr/bin/timeout 300 ' . $hh_server;
  } else {
    $hh_server = __DIR__.'/../tools/timeout.sh -t 300 '. $hh_server;
  }
  $result =  run_one_config($options, $test, $hh_server, $hh_server_env);
  // Remove the temporary directory.
  if (!isset($options['no-clean'])) {
    shell_exec('rm -rf ' . $temp_dir);
  }
  return $result;
}

function run_test($options, $test) {
  if (skip_test($options, $test)) return 'skip';

  $test_ext = pathinfo($test, PATHINFO_EXTENSION);
  list($hhvm, $hhvm_env) = hhvm_cmd($options, $test);
  if (isset($options['relocate']) &&
      preg_match('/ --count[ =].* --count[ =]/', $hhvm)) {
    return 'skip';
  }

  if (is_executable('/usr/bin/timeout')) {
    $hhvm = '/usr/bin/timeout 300 '.$hhvm;
  } else {
    $hhvm = __DIR__.'/../tools/timeout.sh -t 300 '.$hhvm;
  }

  if (isset($options['repo'])) {
    if ($test_ext === 'hhas' ||
        strpos($hhvm, '-m debug') !== false ||
        file_exists($test.'.norepo')) {
      return 'skip-norepo';
    }

    $hphp_repo = "$test.repo/hhvm.hhbc";
    $hhbbc_repo = "$test.repo/hhvm.hhbbc";
    shell_exec("rm -f \"$hphp_repo\" \"$hhbbc_repo\" ");
    $hphp = hphp_cmd($options, $test);
    $hhbbc = hhbbc_cmd($options, $test);
    shell_exec("$hphp 2>&1");
    shell_exec("$hhbbc 2>&1");

    return run_one_config($options, $test, $hhvm, $hhvm_env);
  }

  if (file_exists($test.'.onlyrepo')) {
    return 'skip-onlyrepo';
  }
  if (isset($options['hhas-round-trip'])) {
    $hhas_temp = dump_hhas_to_temp($hhvm, $test);
    list($hhvm, $hhvm_env) = hhvm_cmd($options, $hhas_temp);
  }

  if ($outputs = run_config_server($options, $test)) {
    return run_config_post($outputs, $test, $options) ? 'pass-server'
      : (run_one_config($options, $test, $hhvm, $hhvm_env) ? 'skip-server'
         : false);
  }
  return run_one_config($options, $test, $hhvm, $hhvm_env);
}

function num_cpus() {
  switch(PHP_OS) {
    case 'Linux':
      $data = file('/proc/stat');
      $cores = 0;
      foreach($data as $line) {
        if (preg_match('/^cpu[0-9]/', $line)) {
          $cores++;
        }
      }
      return $cores;
    case 'Darwin':
    case 'FreeBSD':
      return exec('sysctl -n hw.ncpu');
  }
  return 2; // default when we don't know how to detect.
}

function make_header($str) {
  return "\n\033[0;33m".$str."\033[0m\n";
}

function print_commands($tests, $options) {
  print make_header("Run these by hand:");

  foreach ($tests as $test) {
    if (isset($options['typechecker'])) {
      list($command, $_, ) = hh_server_cmd($options, $test);
    } else {
      list($command, $_) = hhvm_cmd($options, $test);
    }
    if (!isset($options['repo'])) {
      print "$command\n";
      continue;
    }

    // How to run it with hhbbc:
    $hhbbc_cmds = hphp_cmd($options, $test)."\n";
    $hhbbc_cmds .= hhbbc_cmd($options, $test)."\n";
    $hhbbc_cmds .= $command."\n";
    print "$hhbbc_cmds\n";
  }
}

function msg_loop($num_tests, $queue) {
  $passed = 0;
  $skipped = 0;
  $failed = 0;

  $do_progress = Status::getMode() == Status::MODE_NORMAL &&
    Status::hasCursorControl();
  if ($do_progress) {
    $stty = strtolower(Status::getSTTY());
    preg_match_all("/columns ([0-9]+);/", $stty, $output);
    if (!isset($output[1][0])) {
      // because BSD has to be different
      preg_match_all("/([0-9]+) columns;/", $stty, $output);
    }
    if (!isset($output[1][0])) {
      $do_progress = false;
    } else {
      $cols = $output[1][0];
    }
  }

  while (true) {
    if (!msg_receive($queue, 0, $type, 1024, $message)) {
      error("msg_receive failed");
    }

    switch ($type) {
      case Status::MSG_STARTED:
        break;

      case Status::MSG_FINISHED:
        break 2;

      case Status::MSG_SERVER_RESTARTED:
        switch (Status::getMode()) {
          case Status::MODE_NORMAL:
            if (!Status::hasCursorControl()) {
              Status::sayColor(Status::RED, 'x');
            }
            break;

          case Status::MODE_VERBOSE:
            Status::sayColor("$test ", Status::YELLOW, "failed",
                             " to talk to server\n");
            break;

          case Status::MODE_FBMAKE:
            break;

          case Status::MODE_TESTPILOT:
            break;
        }

      case Status::MSG_TEST_PASS:
        $passed++;
        list($test, $how, $time, $stime, $etime) = $message;
        switch (Status::getMode()) {
          case Status::MODE_NORMAL:
            if (!Status::hasCursorControl()) {
              if ($how == Status::SKIP_SERVER) {
                Status::sayColor(Status::RED, '.');
              } else {
                Status::sayColor(Status::GREEN,
                                 $how == Status::PASS_SERVER ? ',' : '.');
              }
            }
            break;

          case Status::MODE_VERBOSE:
            Status::sayColor("$test ", Status::GREEN,
                             sprintf("passed (%.2fs)\n", $time));
            break;

          case Status::MODE_FBMAKE:
            Status::sayFBMake($test, 'passed', $stime, $etime);
            break;

          case Status::MODE_TESTPILOT:
            Status::sayFBMake($test, 'passed', $stime, $etime);
            break;
        }
        break;

      case Status::MSG_TEST_SKIP:
        $skipped++;
        list($test, $reason, $time, $stime, $etime) = $message;

        switch (Status::getMode()) {
          case Status::MODE_NORMAL:
            if (!Status::hasCursorControl()) {
              Status::sayColor(Status::YELLOW, 's');
            }
            break;

          case Status::MODE_VERBOSE:
            Status::sayColor("$test ", Status::YELLOW, "skipped");

            if ($reason !== null) {
              Status::sayColor(" - $reason");
            }
            Status::sayColor(sprintf(" (%.2fs)\n", $time));
            break;

          case Status::MODE_FBMAKE:
            /* Intentionally discard result */
            break;

          case Status::MODE_TESTPILOT:
            Status::sayFBMake($test, 'not_relevant', $stime, $etime);
            break;
        }
        break;

      case Status::MSG_TEST_FAIL:
        $failed++;
        list($test, $time, $stime, $etime) = $message;
        switch (Status::getMode()) {
          case Status::MODE_NORMAL:
            if (Status::hasCursorControl()) {
              print "\033[2K\033[1G";
            }
            $diff = (string)@file_get_contents($test.'.diff');
            Status::sayColor(Status::RED, "\nFAILED",
                             ": $test\n$diff\n");
            break;

          case Status::MODE_VERBOSE:
            Status::sayColor("$test ", Status::RED,
                             sprintf("FAILED (%.2fs)\n", $time));
            break;

          case Status::MODE_FBMAKE:
            Status::sayFBMake($test, 'failed', $stime, $etime);
            break;

          case Status::MODE_TESTPILOT:
            Status::sayFBMake($test, 'failed', $stime, $etime);
            break;
        }
        break;

      default:
        error("Unknown message $type");
    }

    if ($do_progress) {
      $total_run = ($skipped + $failed + $passed);
      $bar_cols = ($cols - 45);

      $passed_ticks  = round($bar_cols * ($passed  / $num_tests));
      $skipped_ticks = round($bar_cols * ($skipped / $num_tests));
      $failed_ticks  = round($bar_cols * ($failed  / $num_tests));

      $fill = $bar_cols - ($passed_ticks + $skipped_ticks + $failed_ticks);
      if ($fill < 0) $fill = 0;

      $fill = str_repeat('-', $fill);

      $passed_ticks = str_repeat('#',  $passed_ticks);
      $skipped_ticks = str_repeat('#', $skipped_ticks);
      $failed_ticks = str_repeat('#',  $failed_ticks);

      print "\033[2K\033[1G[".
        "\033[0;32m$passed_ticks".
        "\033[33m$skipped_ticks".
        "\033[31m$failed_ticks".
        "\033[0m$fill] ($total_run/$num_tests) ".
        "($skipped skipped, $failed failed)";
    }
  }

  if ($do_progress) {
    print "\033[2K\033[1G";
    if ($skipped > 0) {
      print "$skipped tests \033[1;33mskipped\033[0m\n";
    }
  }
}

function print_success($tests, $results, $options) {
  // We didn't run any tests, not even skipped. Clowntown!
  if (!$tests) {
    print "\nCLOWNTOWN: No tests!\n";
    if (empty($options['no-fun'])) {
      print <<<CLOWN
            _
           {_}
           /*\\
          /_*_\\
         {('o')}
      C{{([^*^])}}D
          [ * ]
         /  Y  \\
        _\\__|__/_
       (___/ \\___)
CLOWN
        ."\n\n";
    }

    /* Emacs' syntax highlighting gets confused by that clown and this comment
     * resets whatever state got messed up. */
    return;
  }
  $ran_tests = false;
  foreach ($results as $result) {
    // The result here will either be skipped or passed (since failed is
    // handled in print_failure.
    if ($result['status'] == 'passed') {
      $ran_tests = true;
      break;
    }
  }
  // We just had skipped tests
  if (!$ran_tests) {
    print "\nSKIP-ALOO: Only skipped tests!\n";
    if (empty($options['no-fun'])) {
      print <<<SKIPPER
                        .".
                         /  |
                        /  /
                       / ,"
           .-------.--- /
          "._ __.-/ o. o\
             "   (    Y  )
                  )     /
                 /     (
                /       Y
            .-"         |
           /  _     \    \
          /    `. ". ) /' )
         Y       )( / /(,/
        ,|      /     )
       ( |     /     /
        " \_  (__   (__
            "-._,)--._,)
SKIPPER
        ."\n\n";
    }

    /* Emacs' syntax highlighting may get confused by the skipper and this
     * rcomment esets whatever state got messed up. */
    return;
  }
  print "\nAll tests passed.\n";
  if (empty($options['no-fun'])) {
    print <<<SHIP
              |    |    |
             )_)  )_)  )_)
            )___))___))___)\
           )____)____)_____)\\
         _____|____|____|____\\\__
---------\      SHIP IT      /---------
  ^^^^^ ^^^^^^^^^^^^^^^^^^^^^
    ^^^^      ^^^^     ^^^    ^^
         ^^^^      ^^^
SHIP
      ."\n";
  }
  if (isset($options['verbose'])) {
    print_commands($tests, $options);
  }
}

function print_failure($argv, $results, $options) {
  $failed = array();
  $passed = array();
  foreach ($results as $result) {
    if ($result['status'] === 'failed') {
      $failed[] = $result['name'];
    }
    if ($result['status'] === 'passed') {
      $passed[] = $result['name'];
    }
  }
  asort($failed);
  print "\n".count($failed)." tests failed\n";
  if (empty($options['no-fun'])) {
    print "(╯°□°）╯︵ ┻━┻\n";
  }

  print make_header("See the diffs:").
    implode("\n", array_map(
      function($test) { return 'cat '.$test.'.diff'; },
    $failed))."\n";

  $failing_tests_file = !empty($options['failure-file'])
    ? $options['failure-file']
    : tempnam('/tmp', 'test-failures');
  file_put_contents($failing_tests_file, implode("\n", $failed)."\n");
  print make_header('For xargs, list of failures is available using:').
    'cat '.$failing_tests_file."\n";

  if (!empty($passed)) {
    $passing_tests_file = !empty($options['success-file'])
      ? $options['success-file']
      : tempnam('/tmp', 'tests-passed');
    file_put_contents($passing_tests_file, implode("\n", $passed)."\n");
    print make_header('For xargs, list of passed tests is available using:').
      'cat '.$passing_tests_file."\n";
  }

  print_commands($failed, $options);

  $rerun = make_header("Re-run just the failing tests:") . $argv[0];
  foreach ($options as $option => $value) {
    if ($option === "threads") {
      // e.g., For a small number of failed tests, we don't need max threads.
      $rerun .= " --" . $option . ' ' . get_num_threads($options, $failed);
    } else {
      $rerun .= " --" . $option . ' ' . escapeshellarg($value);
    }
  }
  $rerun .= ' ' . implode(' ', array_map('escapeshellarg', $failed)) . "\n";
  print $rerun;
}

function port_is_listening($port) {
  $socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
  return @socket_connect($socket, 'localhost', $port);
}

function find_open_port() {
  for ($i = 0; $i < 50; ++$i) {
    $port = rand(1024, 65535);
    if (!port_is_listening($port)) return $port;
  }

  error("Couldn't find an open port");
}

function start_server_impl($options, $config, $port) {
  $threads = $options['threads'];
  $command = hhvm_cmd_impl(
    $options,
    $config,
    '-m', 'server',
    "-vServer.Port=$port",
    "-vServer.Type=proxygen",
    "-vAdminServer.Port=0",
    "-vServer.ThreadCount=$threads",
    '-vServer.ExitOnBindFail=1',
    '-vServer.RequestTimeoutSeconds='.SERVER_TIMEOUT,
    '-vPageletServer.ThreadCount=0',
    '-vLog.UseRequestLog=1',
    '-vLog.File=/dev/null',

    // This ensures we actually jit everything:
    '-vEval.JitRequireWriteLease=1',

    // The default test config uses a small TC but we'll be running thousands
    // of tests against the same process:
    '-vEval.JitASize=100000000',
    '-vEval.JitGlobalDataSize=32000000'
  );
  if (getenv('HHVM_TEST_SERVER_LOG')) {
    echo "Starting server '$command'\n";
  }

  $descriptors = array(
    0 => array('file', '/dev/null', 'r'),
    1 => array('file', '/dev/null', 'w'),
    2 => array('file', '/dev/null', 'w'),
  );

  $proc = proc_open($command, $descriptors, $dummy);
  if (!$proc) {
    error("Failed to start server process");
  }
  $status = proc_get_status($proc);
  $status['proc'] = $proc;
  $status['port'] = $port;
  $status['config'] = $config;
  return $status;
}

function start_server($options, $config) {
  for ($i = 0; $i < 5; ++$i) {
    $port = find_open_port();
    $status = start_server_impl($options, $config, $port);

    // Wait for the server to start responding. If someone else bound to $port
    // between the call to find_open_port and the call to proc_open, then we
    // might start talking to them thinking it's our server.
    for ($j = 0; $j < 40; ++$j) {
      $new_status = proc_get_status($status['proc']);

      if (!$new_status['running']) {
        if ($new_status['exitcode'] === 0) {
          error("Server exited prematurely but without error");
        }

        // We lost a race. Try another port.
        if (getenv('HHVM_TEST_SERVER_LOG')) {
          echo "\n\nLost connection race on port $port. Trying another.\n\n";
        }
        continue 2;
      }

      if (port_is_listening($port)) {
        return $status;
      }

      usleep(250000);
    }
    error("Server took too long to come up");
  }

  error("Couldn't start server");
}

/*
 * For each config file in $configs, start up a server on a randomly-determined
 * port. Return value is an array mapping pids and config files to arrays of
 * information about the server.
 */
function start_servers($options, $configs) {
  $servers = array('pids' => array(), 'configs' => array());
  foreach ($configs as $config) {
    $server = start_server($options, $config);
    $servers['pids'][$server['pid']] =& $server;
    $servers['configs'][$server['config']] =& $server;
    unset($server);
  }
  return $servers;
}

function drain_queue($queue) {
  while (@msg_receive($queue, 0, $type, 1024, $message, true,
                      MSG_IPC_NOWAIT | MSG_NOERROR));
}

function get_num_threads($options, $tests) {
  $cpus = isset($options['server']) ? num_cpus() * 2 : num_cpus();
  return min(count($tests), idx($options, 'threads', $cpus));
}

function runner_precheck() {
  // basic checking for runner.
  if (empty($_SERVER) || empty($_ENV)) {
    echo "Warning: \$_SERVER/\$_ENV variables not available, please check \n" .
         "your ini setting: variables_order, it should have both 'E' and 'S'\n";
  }
}

function main($argv) {
  runner_precheck();

  ini_set('pcre.backtrack_limit', PHP_INT_MAX);

  list($options, $files) = get_options($argv);
  if (isset($options['help'])) {
    error(help());
  }
  if (isset($options['list-tests'])) {
    error(list_tests($files, $options));
  }

  $tests = find_tests($files, $options);
  if (isset($options['shuffle'])) {
    shuffle($tests);
  }

  if (isset($options['repo']) && isset($options['typechecker'])) {
    error("Repo mode and typechecker mode are not compatible");
  }

  $binary_path = "";
  if (isset($options['typechecker'])) {
    $binary_path = hh_server_path();
  } else {
    // Explicit path given by --hhvm-binary-path takes priority
    // Then, if an HHVM_BIN env var exists, and the file it points to exists,
    // that trumps any default hhvm executable path.
    if (isset($options['hhvm-binary-path'])) {
      $binary_path = realpath($options['hhvm-binary-path']);
      check_hhvm_executable($binary_path);
      putenv("HHVM_BIN=" . $binary_path);
    } else if (getenv("HHVM_BIN") !== false) {
      $binary_path = realpath(getenv("HHVM_BIN"));
      check_hhvm_executable($binary_path);
    } else {
      check_for_multiple_default_hhvm_binaries();
      $binary_path = hhvm_path();
    }
  }

  if (isset($options['verbose'])) {
    print "You are using the binary located at: " . $binary_path . "\n";
  }

  $options['threads'] = get_num_threads($options, $tests);

  $servers = null;
  if (isset($options['server'])) {
    if (isset($options['repo']) || isset($options['typechecker'])) {
      error("Server mode repo tests are not supported");
    }
    $configs = array();

    /* We need to start up a separate server process for each config file
     * found. */
    foreach ($tests as $test) {
      if (!can_run_server_test($test)) continue;
      $config = find_file_for_dir(dirname($test), 'config.ini');
      if (!$config) {
        error("Couldn't find config file for $test");
      }
      $configs[$config] = $config;
    }

    $max_configs = 10;
    if (count($configs) > $max_configs) {
      error("More than $max_configs unique config files will be needed to run ".
            "the tests you specified. They may not be a good fit for server ".
            "mode.");
    }

    $servers = $options['servers'] = start_servers($options, $configs);
  }

  // Try to construct the buckets so the test results are ready in
  // approximately alphabetical order.
  $test_buckets = array();
  $i = 0;

  // Get the serial tests to be in their own bucket later.
  $serial_tests = serial_only_tests($tests);

  // If we have no serial tests, we can use the maximum number of allowed
  // threads for the test running. If we have some, we save one thread for
  // the serial bucket.
  $parallel_threads = count($serial_tests) > 0
                    ? $options['threads'] - 1
                    : $options['threads'];

  foreach ($tests as $test) {
    if (!in_array($test, $serial_tests)) {
      $test_buckets[$i][] = $test;
      $i = ($i + 1) % $parallel_threads;
    }
  }

  if (count($serial_tests) > 0) {
    // The last bucket is serial.
    // If the number of parallel tests didn't equal the actual number of
    // parallel threads because the number of serial tests reduced it enough,
    // then our next bucket is just $i; otherwise it is final available
    // thread. For example, we have 13 total tests which initially gave us
    // 13 parallel threads, but then we find out 3 are serial, so the parallel
    // tests would only fill 9 parallel buckets (< 12). The next one would be
    // 10 for the 3 serial. Now if we have 40 total tests which gave us 32
    // parallel threads and 4 serial tests, then all of possible parallel
    // buckets (31) would be filled regardless; so the serial bucket is what
    // would have been the last parallel thread (32).
    // $i got bumped to the next bucket at the end of the parallel test loop
    // above, so no $i++ here.
    $i = count($tests) - count($serial_tests) < $parallel_threads
       ? $i // we didn't fill all the parallel buckets, so use next one in line
       : $options['threads'] - 1; // all parallel filled; last thread; 0 indexed
    foreach ($serial_tests as $test) {
        $test_buckets[$i][] = $test;
    }
  }

  // If our total number of test buckets didn't overflow back to 0 above
  // when we % against the number of threads (because we didn't have that
  // many tests for this run), then just set the threads to how many
  // buckets we actually have to make calculations below correct.
  if (count($test_buckets) < $options['threads']) {
    $options['threads'] = count($test_buckets);
  }

  // Remember that the serial tests are also in the tests array too,
  // so they are part of the total count.
  if (!isset($options['fbmake']) && !isset($options['testpilot'])) {
    print "Running ".count($tests)." tests in ".
      $options['threads']." threads (" . count($serial_tests) .
      " in serial)\n";
  }

  if (isset($options['verbose'])) {
    Status::setMode(Status::MODE_VERBOSE);
  }
  if (isset($options['fbmake'])) {
    Status::setMode(Status::MODE_FBMAKE);
  }
  if (isset($options['testpilot'])) {
    Status::setMode(Status::MODE_TESTPILOT);
  }
  Status::setUseColor(isset($options['color']) ? true : posix_isatty(STDOUT));

  Status::$key = rand();
  $queue = Status::getQueue();
  drain_queue($queue);

  Status::started();
  // Spawn off worker threads.
  $children = array();
  // A poor man's shared memory.
  $bad_test_files = array();
  for ($i = 0; $i < $options['threads']; $i++) {
    $bad_test_file = tempnam('/tmp', 'test-run-');
    $bad_test_files[] = $bad_test_file;
    $pid = pcntl_fork();
    if ($pid == -1) {
      error('could not fork');
    } else if ($pid) {
      $children[$pid] = $pid;
    } else {
      exit(run($options, $test_buckets[$i], $bad_test_file));
    }
  }

  // Fork off a child to receive messages and print status, and have the parent
  // wait for all children to exit.
  $printer_pid = pcntl_fork();
  if ($printer_pid == -1) {
    error("failed to fork");
  } else if ($printer_pid == 0) {
    msg_loop(count($tests), $queue);
    return 0;
  }

  // In case we exit in a crazy way, have the parent blow up the queue.
  // Do this here so no children inherit this.
  $kill_queue = function() { Status::killQueue(); };
  register_shutdown_function($kill_queue);
  pcntl_signal(SIGTERM, $kill_queue);
  pcntl_signal(SIGINT, $kill_queue);

  $return_value = 0;
  while (count($children) && $printer_pid != 0) {
    $pid = pcntl_wait($status);
    if (!pcntl_wifexited($status) && !pcntl_wifsignaled($status)) {
      error("Unexpected exit status from child");
    }

    if ($pid == $printer_pid) {
      // We should be finishing up soon.
      $printer_pid = 0;
    } else if (isset($servers['pids'][$pid])) {
      // A server crashed. Restart it.
      if (getenv('HHVM_TEST_SERVER_LOG')) {
        echo "\nServer $pid crashed. Restarting.\n";
      }
      Status::serverRestarted();
      $server =& $servers['pids'][$pid];
      $server = start_server_impl($options, $server['config'], $server['port']);

      // Unset the old $pid entry and insert the new one.
      unset($servers['pids'][$pid]);
      $servers['pids'][$server['pid']] =& $server;
      unset($server);
    } else {
      if (!isset($children[$pid])) {
        error("Unexpected child pid $pid from wait");
      }
      unset($children[$pid]);
      $return_value |= pcntl_wexitstatus($status);
    }
  }

  Status::finished();

  // Kill the server.
  if ($servers) {
    foreach ($servers['pids'] as $server) {
      proc_terminate($server['proc']);
      proc_close($server['proc']);
    }
  }

  $results = array();
  foreach ($bad_test_files as $bad_test_file) {
    $json = json_decode(file_get_contents($bad_test_file), true);
    if (!is_array($json)) {
      error(
        "\nNo JSON output was received from a test thread. ".
        "Either you killed it, or it might be a bug in the test script."
      );
    }
    $results = array_merge($results, $json);
    unlink($bad_test_file);
  }

  if (isset($options['fbmake']) || isset($options['testpilot'])) {
    Status::say(array('op' => 'all_done', 'results' => $results));
  } else if (!$return_value) {
    print_success($tests, $results, $options);
  } else {
    print_failure($argv, $results, $options);
  }


  Status::sayColor("\nTotal time for all executed tests as run: ",
                   Status::BLUE,
                   sprintf("%.2fs\n",
                   Status::getOverallEndTime() -
                   Status::getOverallStartTime()));
  Status::sayColor("Total time for all executed tests if run serially: ",
                   Status::BLUE,
                   sprintf("%.2fs\n",
                   Status::addTestTimesSerial()));

  return $return_value;
}

exit(main($argv));
