Arquivos
hhvm/hphp/test/run
T
Paul Tarjan 9519cefd00 fix parser tests
I was quite stupid in my rewrite and totally broke this. Wow, it runs fast!

I had to switch to `HHVM_BIN` as the env var, since it seems we force `HHVM` to 1 all the time...
2013-06-03 23:54:38 -07:00

592 linhas
16 KiB
PHP
Arquivo Executável

#!/usr/bin/env php
<?php
/**
* Run the test suites in various configurations.
*/
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/fbcode/hphp/test/run"
Examples:
# Quick tests in JIT mode:
% $argv[0] test/quick
# Slow tests in interp mode:
% $argv[0] -m interp test/slow
# 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
EOT;
return usage().$help;
}
function error($message) {
print "$message\n";
exit(1);
}
function hphp_home() {
return realpath(__DIR__.'/../..');
}
function idx($array, $key, $default = null) {
return isset($array[$key]) ? $array[$key] : $default;
}
function idx_file($array, $key, $default = null) {
$file = is_file(idx($array, $key)) ? realpath($array[$key]) : $default;
if (!is_file($file)) {
error("$file doesn't exist");
}
return rel_path($file);
}
function bin_root() {
$dir = hphp_home() . '/' . idx($_ENV, 'FBMAKE_BIN_ROOT', '_bin');
return is_dir($dir) ?
$dir : # fbmake
hphp_home() # github
;
}
function verify_hhbc() {
return idx($_ENV, 'VERIFY_HHBC', bin_root().'/verify.hhbc');
}
function read_file($file) {
return file_exists($file) ? preg_replace('/\s+/', ' ', (file_get_contents($file))) : "";
}
// 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(
'repo' => 'r',
'mode:' => 'm:',
'server' => '',
'help' => 'h',
'verbose' => 'v',
'fbmake' => '',
'threads:' => '',
);
$options = array();
$files = array();
for ($i = 1; $i < count($argv); $i++) {
$arg = $argv[$i];
$found = false;
if ($arg && $arg[0] == '-') {
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 && $arg) {
$files[] = $arg;
}
}
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 map_convenience_filename($file) {
$mappage = array(
'quick' => 'hphp/test/quick',
'slow' => 'hphp/test/slow',
'zend' => 'hphp/test/zend/good',
'zend_bad' => 'hphp/test/zend/bad',
'facebook' => 'hphp/facebook/test',
);
$m = null;
if (!preg_match('/([^\/]*)/', $file, $m) ||
!isset($mappage[$m[1]])) {
return $file;
}
return hphp_home().'/'.$mappage[$m[1]];
}
function find_tests($files) {
if (!$files) {
$files = array('quick');
}
if ($files == array('all')) {
$files = array('quick', 'slow', 'zend');
}
foreach ($files as &$file) {
$file = map_convenience_filename($file);
if (!@stat($file)) {
error("Not valid file or directory: '$file'");
}
$file = preg_replace(',//+,', '/', realpath($file));
$file = preg_replace(',^'.getcwd().'/,', '', $file);
}
$files = implode(' ', $files);
$tests = explode("\n", shell_exec("find $files -name '*.php' -o -name '*.hhas'"));
if (!$tests) {
error(usage());
}
asort($tests);
return array_filter($tests);
}
function find_config($test, $name) {
return find_config_for_dir(dirname($test), $name);
}
function find_config_for_dir($dir, $name) {
while ($dir && stat($dir)) {
$config = "$dir/$name";
if (is_file($config)) {
return $config;
}
$dir = substr($dir, 0, strrpos($dir, '/'));
}
return __DIR__.'/'.$name;
}
function mode_cmd($options) {
$repo_args = "-v Repo.Local.Mode=-- -v Repo.Central.Path=".verify_hhbc();
$jit_args = "$repo_args -v Eval.Jit=true -v Eval.JitEnableRenameFunction=true";
$mode = idx($options, 'mode');
switch ($mode) {
case '':
case 'jit':
return "$jit_args";
case 'interp':
return "$repo_args -vEval.Jit=0";
default:
error("-m must be one of jit | interp. Got: '$mode'");
}
}
function hhvm_cmd($options, $test) {
$cmd = implode(" ", array(
idx_file($_ENV, 'HHVM_BIN', bin_root().'/hphp/hhvm/hhvm'),
'--config',
find_config($test, 'config.hdf'),
mode_cmd($options),
'-v Eval.EnableArgsInBacktraces=true',
read_file("$test.opts"),
'--file',
$test
));
if (file_exists("$test.in")) {
$cmd .= " <$test.in";
}
return $cmd;
}
function hphp_cmd($options, $test) {
return implode(" ", array(
idx_file($_ENV, 'HPHP_BIN', bin_root().'/hphp/hhvm/hphp'),
'--config',
find_config($test, 'hphp_config.hdf'),
read_file("$test.hphp_opts"),
"-thhbc -l0 -k1 -o $test.repo $test",
));
}
class Status {
private static $results = array();
private static $mode = 0;
const MODE_NORMAL = 0;
const MODE_VERBOSE = 1;
const MODE_FBMAKE = 2;
public static function setMode($mode) {
self::$mode = $mode;
}
public static function pass($test) {
array_push(self::$results, array('name' => $test, 'status' => 'passed'));
switch (self::$mode) {
case self::MODE_NORMAL:
if (self::hasColor()) {
print "\033[1;32m.\033[0m";
} else {
print '.';
}
break;
case self::MODE_VERBOSE:
if (self::hasColor()) {
print "$test \033[1;32mpassed\033[0m\n";
} else {
print "$test passed";
}
break;
case self::MODE_FBMAKE:
self::sayFBMake($test, 'passed');
break;
}
}
public static function fail($test) {
array_push(self::$results, array('name' => $test, 'status' => 'failed'));
switch (self::$mode) {
case self::MODE_NORMAL:
$diff = file_get_contents($test.'.diff');
if (self::hasColor()) {
print "\n\033[0;31m$test\033[0m\n$diff";
} else {
print "\nFAILED: $test\n$diff";
}
break;
case self::MODE_VERBOSE:
if (self::hasColor()) {
print "$test \033[0;31mFAILED\033[0m\n";
} else {
print "$test FAILED\n";
}
break;
case self::MODE_FBMAKE:
self::sayFBMake($test, 'failed');
break;
}
}
private static function sayFBMake($test, $status) {
$start = array('op' => 'start', 'test' => $test);
$end = array('op' => 'test_done', 'test' => $test, 'status' => $status);
if ($status == 'failed') {
$end['details'] = @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 json_encode($row, JSON_UNESCAPED_SLASHES) . "\n";
}, func_get_args());
fwrite(STDERR, implode("", $data));
}
private static function hasColor() {
return posix_isatty(STDOUT);
}
}
function run($options, $tests, $bad_test_file) {
if (isset($options['verbose'])) {
Status::setMode(Status::MODE_VERBOSE);
}
if (isset($options['fbmake'])) {
Status::setMode(Status::MODE_FBMAKE);
}
foreach ($tests as $test) {
$status = run_test($options, $test);
if ($status) {
Status::pass($test);
} else {
Status::fail($test);
}
}
file_put_contents($bad_test_file, json_encode(Status::getResults()));
foreach (Status::getResults() as $result) {
if ($result['status'] == 'failed') {
return 1;
}
}
return 0;
}
function run_test($options, $test) {
$hhvm = hhvm_cmd($options, $test);
if (isset($options['repo'])) {
if (strpos($test, '.hhas') !== false || is_file($test.'.norepo')) {
# We don't have a way to skip, I guess run non-repo?
} else {
unlink("$test.repo/hhvm.hhbc");
$hphp = hphp_cmd($options, $test);
shell_exec($hphp);
$opts .= " -v Repo.Authoritative=true -v Repo.Central.Path=$test.repo/hhvm.hhbc";
}
}
$descriptorspec = array(
0 => array("pipe", "r"),
1 => array("pipe", "w"),
2 => array("pipe", "w"),
);
$process = proc_open("$hhvm 2>&1", $descriptorspec, $pipes);
if (is_resource($process)) {
fclose($pipes[0]);
$output = stream_get_contents($pipes[1]);
fclose($pipes[1]);
proc_close($process);
}
file_put_contents("$test.out", $output);
// Needed for testing non-hhvm binaries that don't actually run the code
// e.g. util/parser/test/parse_tester.cpp
if ($output == "FORCE PASS") {
return true;
}
if (file_exists("$test.expect")) {
$diff_cmds = "--text -u";
exec("diff --text -u $test.expect $test.out > $test.diff 2>&1", $_, $status);
// unix 0 == success
return !$status;
} else if (file_exists("$test.expectf")) {
$wanted_re = file_get_contents("$test.expectf");
// 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);
$wanted_re = str_replace('%f', '[+-]?\.?\d+\.?\d*(?:[Ee][+-]?\d+)?', $wanted_re);
$wanted_re = str_replace('%c', '.', $wanted_re);
// %f allows two points "-.0.0" but that is the best *simple* expression
# a poor mans aide for debugging
shell_exec("diff --text -u $test.expectf $test.out > $test.diff 2>&1");
return preg_match("/^$wanted_re\$/s", $output);
} else if (file_exists("$test.expectregex")) {
$wanted_re = file_get_contents("$test.expectregex");
# a poor mans aide for debugging
shell_exec("diff --text -u $test.expectf $test.out > $test.diff 2>&1");
return preg_match("/^$wanted_re\$/s", $output);
}
}
function main($argv) {
ini_set('pcre.backtrack_limit', PHP_INT_MAX);
ini_set('pcre.recursion_limit', PHP_INT_MAX);
list($options, $files) = get_options($argv);
if (isset($options['help'])) {
error(help());
}
$tests = find_tests($files);
$threads = min(count($tests), idx($options, 'threads', 20));
if (!isset($options['fbmake'])) {
print "Running ".count($tests)." tests in $threads threads\n";
}
# Try to construct the buckets so the test results are ready in approximately
# alphabetical order
$test_buckets = array();
$i = 0;
foreach ($tests as $test) {
$test_buckets[$i][] = $test;
$i = ($i + 1) % $threads;
}
# Spawn off worker threads
$children = array();
# A poor man's shared memory
$bad_test_files = array();
for ($i = 0; $i < $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;
} else {
exit(run($options, $test_buckets[$i], $bad_test_file));
}
}
# Wait for the kids
$return_value = 0;
foreach ($children as $child) {
pcntl_waitpid($child, $status);
$return_value |= pcntl_wexitstatus($status);
}
$results = array();
foreach ($bad_test_files as $bad_test_file) {
$results = array_merge($results,
json_decode(file_get_contents($bad_test_file), true));
}
if (isset($options['fbmake'])) {
Status::say(array('op' => 'all_done', 'results' => $results));
} else {
if (!$return_value) {
print "\nAll tests passed.\n\n".<<<SHIP
| | |
)_) )_) )_)
)___))___))___)\
)____)____)_____)\\
_____|____|____|____\\\__
---------\ SHIP IT /---------
^^^^^ ^^^^^^^^^^^^^^^^^^^^^
^^^^ ^^^^ ^^^ ^^
^^^^ ^^^
SHIP
."\n";
} else {
$failed = array();
foreach ($results as $result) {
if ($result['status'] == 'failed') {
$failed[] = $result['name'];
}
}
asort($failed);
$header_start = "\n\033[0;33m";
$header_end = "\033[0m\n";
print "\n".count($failed)." tests failed\n";
print $header_start."See the diffs:".$header_end.
implode("\n", array_map(
function($test) { return 'cat '.$test.'.diff'; },
$failed))."\n";
print $header_start."Run these by hand:".$header_end;
foreach ($failed as $test) {
$command = hhvm_cmd($options, $test);
if (isset($options['repo'])) {
$command .= " -v Repo.Authoritative=true ";
$command = str_replace(verify_hhbc(), "$test.repo/hhvm.hhbc", $command);
$command = hphp_cmd($options, $test)."\n".$command."\n";
}
print "$command\n";
}
print $header_start."Re-run just the failing tests:".$header_end.
"$argv[0] ".implode(' ', $failed)."\n";
}
}
return $return_value;
}
exit(main($argv));