PageRenderTime 63ms CodeModel.GetById 18ms RepoModel.GetById 0ms app.codeStats 1ms

/hphp/test/run

http://github.com/facebook/hiphop-php
PHP | 2563 lines | 1986 code | 250 blank | 327 comment | 340 complexity | 01170663f231c4a4d7d55fde22cf6d9f MD5 | raw file
Possible License(s): LGPL-2.1, BSD-2-Clause, BSD-3-Clause, MPL-2.0-no-copyleft-exception, MIT, LGPL-2.0, Apache-2.0
  1. #!/usr/bin/env php
  2. <?php
  3. /**
  4. * Run the test suites in various configurations.
  5. */
  6. function is_testing_dso_extension() {
  7. // detecting if we're running outside of the hhvm codebase.
  8. return !is_file(__DIR__ . "/../../hphp/test/run");
  9. }
  10. function get_expect_file_and_type($test, $options) {
  11. // .typechecker files are for typechecker (hh_server --check) test runs.
  12. $types = null;
  13. if (isset($options['typechecker'])) {
  14. $types = array('typechecker.expect', 'typechecker.expectf');
  15. } else {
  16. $types = array('expect', 'hhvm.expect', 'expectf', 'hhvm.expectf',
  17. 'expectregex');
  18. }
  19. if (isset($options['repo'])) {
  20. foreach ($types as $type) {
  21. $fname = "$test.$type-repo";
  22. if (file_exists($fname)) {
  23. return array($fname, $type);
  24. }
  25. }
  26. }
  27. foreach ($types as $type) {
  28. $fname = "$test.$type";
  29. if (file_exists($fname)) {
  30. return array($fname, $type);
  31. }
  32. }
  33. return array(null, null);
  34. }
  35. function usage() {
  36. global $argv;
  37. return "usage: $argv[0] [-m jit|interp] [-r] <test/directories>";
  38. }
  39. function help() {
  40. global $argv;
  41. $ztestexample = 'test/zend/good/*/*z*.php'; // sep. for syntax highlighting.
  42. $help = <<<EOT
  43. This is the hhvm test-suite runner. For more detailed documentation,
  44. see hphp/test/README.md.
  45. The test argument may be a path to a php test file, a directory name, or
  46. one of a few pre-defined suite names that this script knows about.
  47. If you work with hhvm a lot, you might consider a bash alias:
  48. alias ht="path/to/hphp/test/run"
  49. Examples:
  50. # Quick tests in JIT mode:
  51. % $argv[0] test/quick
  52. # Slow tests in interp mode:
  53. % $argv[0] -m interp test/slow
  54. # PHP specificaion tests in JIT mode:
  55. % $argv[0] test/spec
  56. # Slow closure tests in JIT mode:
  57. % $argv[0] test/slow/closure
  58. # Slow closure tests in JIT mode with RepoAuthoritative:
  59. % $argv[0] -r test/slow/closure
  60. # Slow array tests, in RepoAuthoritative:
  61. % $argv[0] -r test/slow/array
  62. # Zend tests with a "z" in their name:
  63. % $argv[0] $ztestexample
  64. # Quick tests in JIT mode with some extra runtime options:
  65. % $argv[0] test/quick -a '-vEval.JitMaxTranslations=120 -vEval.HHIRRefcountOpts=0'
  66. # All quick tests except debugger
  67. % $argv[0] -e debugger test/quick
  68. # All tests except those containing a string of 3 digits
  69. % $argv[0] -E '/\d{3}/' all
  70. # All tests whose name containing pdo_mysql
  71. % $argv[0] -i pdo_mysql -m jit -r zend
  72. # Print all the standard tests
  73. % $argv[0] --list-tests
  74. # Use a specific HHVM binary
  75. % $argv[0] -b ~/code/hhvm/hphp/hhvm/hhvm
  76. % $argv[0] --hhvm-binary-path ~/code/hhvm/hphp/hhvm/hhvm
  77. # Use relocation to run tests in the same thread. e.g, 6 times in the same thread,
  78. # where the 3 specifies a random relocation for the 3rd request and the test is
  79. # run 3 * 2 times.
  80. % $argv[0] --relocate 3 test/quick/silencer.php
  81. # Run the Hack typechecker against quick typechecker.expect[f] files
  82. # Could explcitly use quick here too
  83. # $argv[0] --typechecker
  84. # Run the Hack typechecker against typechecker.expect[f] files in the slow
  85. # directory
  86. # $argv[0] --typechecker slow
  87. # Run the Hack typechecker against the typechecker.expect[f] file in this test
  88. # $argv[0] --typechecker test/slow/test_runner_typechecker_mode/basic.php
  89. # Use a specific typechecker binary
  90. # $argv[0] --hhserver-binary-path ~/code/hhvm/hphp/hack/bin/hh_server --typechecker .
  91. EOT;
  92. return usage().$help;
  93. }
  94. function error($message) {
  95. print "$message\n";
  96. exit(1);
  97. }
  98. // If a user-supplied path is provided, let's make sure we have a valid
  99. // executable.
  100. function check_executable($path, $typechecker) {
  101. $type = $typechecker ? "HH_SERVER" : "HHVM";
  102. $rpath = realpath($path);
  103. $msg = "Provided ".$type." executable (".$path.") is not a file.\n"
  104. . "If using ".$type."_BIN, make sure that is set correctly.";
  105. if (!is_file($rpath)) {
  106. error($msg);
  107. }
  108. $output = array();
  109. exec($rpath . " --help", $output);
  110. $str = implode($output);
  111. $msg = "Provided file (".$rpath.") is not a/an ".$type." executable.\n"
  112. . "If using ".$type."_BIN, make sure that is set correctly.";
  113. if (strpos($str, "Usage") !== 0) {
  114. error($msg);
  115. }
  116. }
  117. function hhvm_binary_routes() {
  118. $routes = array(
  119. "fbbuild" => "/_bin/hphp/hhvm",
  120. "buck" => "/buck-out/gen/hphp/hhvm/hhvm",
  121. "cmake" => "/hphp/hhvm"
  122. );
  123. $env_root = getenv("FBMAKE_BIN_ROOT");
  124. if ($env_root !== false) {
  125. $routes["fbbuild"] = "/" . $env_root . "/hphp/hhvm";
  126. }
  127. return $routes;
  128. }
  129. function hh_server_binary_routes() {
  130. return array(
  131. "fbbuild" => "/_bin/hphp/hack/src",
  132. "buck" => "/buck-out/gen/hphp/hack/src/hh_server",
  133. "cmake" => "/hphp/hack/bin"
  134. );
  135. $env_root = getenv("FBMAKE_BIN_ROOT");
  136. if ($env_root !== false) {
  137. $routes["fbbuild"] = "/" . $env_root . "/hphp/hack/src";
  138. }
  139. return $routes;
  140. }
  141. // For Facebook: We have several build systems, and we can use any of them in
  142. // the same code repo. If multiple binaries exist, we want the onus to be on
  143. // the user to specify a particular one because before we chose the fbmake one
  144. // by default and that could cause unexpected results.
  145. function check_for_multiple_default_binaries($typechecker) {
  146. // Env var we use in testing that'll pick which build system to use.
  147. if (getenv("FBCODE_BUILD_TOOL") !== false) {
  148. return;
  149. }
  150. $home = hphp_home();
  151. $routes = $typechecker ? hh_server_binary_routes() : hhvm_binary_routes();
  152. $binary = $typechecker ? "hh_server" : "hhvm";
  153. $found = array();
  154. foreach ($routes as $_ => $path) {
  155. $abs_path = $home . $path . "/" . $binary;
  156. if (file_exists($abs_path)) {
  157. $found[] = $abs_path;
  158. }
  159. }
  160. if (count($found) <= 1) {
  161. return;
  162. }
  163. $path_option = $typechecker ? "--hhserver-binary-path" : "--hhvm-binary-path";
  164. $msg = "Multiple binaries exist in this repo. \n";
  165. foreach ($found as $bin) {
  166. $msg .= " - " . $bin . "\n";
  167. }
  168. $msg .= "Are you in fbcode? If so, remove a binary \n"
  169. . "or use the " . $path_option . " option to the test runner. \n"
  170. . "e.g., test/run ";
  171. if ($typechecker) {
  172. $msg .= "--typechecker";
  173. }
  174. $msg .= " " . $path_option . " /path/to/binary slow\n";
  175. error($msg);
  176. }
  177. function hphp_home() {
  178. if (is_testing_dso_extension()) {
  179. return realpath(__DIR__);
  180. }
  181. return realpath(__DIR__.'/../..');
  182. }
  183. function idx($array, $key, $default = null) {
  184. return isset($array[$key]) ? $array[$key] : $default;
  185. }
  186. function hhvm_path() {
  187. $file = "";
  188. if (getenv("HHVM_BIN") !== false) {
  189. $file = realpath(getenv("HHVM_BIN"));
  190. } else {
  191. $file = bin_root().'/hhvm';
  192. }
  193. if (!is_file($file)) {
  194. if (is_testing_dso_extension()) {
  195. exec("which hhvm", $output);
  196. if (isset($output[0]) && $output[0]) {
  197. return $output[0];
  198. }
  199. error("You need to specify hhvm bin with env HHVM_BIN");
  200. }
  201. error("$file doesn't exist. Did you forget to build first?");
  202. }
  203. return rel_path($file);
  204. }
  205. function bin_root() {
  206. if (getenv("HHVM_BIN") !== false) {
  207. return dirname(realpath(getenv("HHVM_BIN")));
  208. }
  209. $home = hphp_home();
  210. $env_tool = getenv("FBCODE_BUILD_TOOL");
  211. $routes = hhvm_binary_routes();
  212. if ($env_tool !== false) {
  213. return $home . $routes[$env_tool];
  214. }
  215. foreach ($routes as $_ => $path) {
  216. $dir = $home . $path;
  217. if (is_dir($dir)) {
  218. return $dir;
  219. }
  220. }
  221. return $home . $routes["cmake"];
  222. }
  223. function hh_server_path() {
  224. $file = "";
  225. if (getenv("HH_SERVER_BIN") !== false) {
  226. $file = realpath(getenv("HH_SERVER_BIN"));
  227. } else {
  228. $file = hh_server_bin_root().'/hh_server';
  229. }
  230. if (!is_file($file)) {
  231. error("$file doesn't exist. Did you forget to build first?");
  232. }
  233. return rel_path($file);
  234. }
  235. function hh_server_bin_root() {
  236. if (getenv("HH_SERVER_BIN") !== false) {
  237. return dirname(realpath(getenv("HH_SERVER_BIN")));
  238. }
  239. $home = hphp_home();
  240. $env_tool = getenv("FBCODE_BUILD_TOOL");
  241. $routes = hh_server_binary_routes();
  242. if ($env_tool !== false) {
  243. return $home . $routes[$env_tool];
  244. }
  245. foreach ($routes as $_ => $path) {
  246. $dir = $home . $path;
  247. if (is_dir($dir)) {
  248. return $dir;
  249. }
  250. }
  251. return $home . $routes["cmake"];
  252. }
  253. function verify_hhbc() {
  254. if (getenv("VERIFY_HHBC") !== false) {
  255. return getenv($env_hhbc);
  256. }
  257. return bin_root().'/verify.hhbc';
  258. }
  259. function read_opts_file($file) {
  260. if (!file_exists($file)) {
  261. return "";
  262. }
  263. $fp = fopen($file, "r");
  264. $contents = "";
  265. while ($line = fgets($fp)) {
  266. // Compress out white space.
  267. $line = preg_replace('/\s+/', ' ', $line);
  268. // Discard simple line oriented ; and # comments to end of line
  269. // Comments at end of line (after payload) are not allowed.
  270. $line = preg_replace('/^ *;.*$/', ' ', $line);
  271. $line = preg_replace('/^ *#.*$/', ' ', $line);
  272. // Substitute in the directory name
  273. $line = str_replace('__DIR__', dirname($file), $line);
  274. $contents .= $line;
  275. }
  276. fclose($fp);
  277. return $contents;
  278. }
  279. // http://stackoverflow.com/questions/2637945/
  280. function rel_path($to) {
  281. $from = explode('/', getcwd().'/');
  282. $to = explode('/', $to);
  283. $relPath = $to;
  284. foreach ($from as $depth => $dir) {
  285. // find first non-matching dir.
  286. if ($dir === $to[$depth]) {
  287. // ignore this directory.
  288. array_shift($relPath);
  289. } else {
  290. // get number of remaining dirs to $from.
  291. $remaining = count($from) - $depth;
  292. if ($remaining > 1) {
  293. // add traversals up to first matching dir.
  294. $padLength = (count($relPath) + $remaining - 1) * -1;
  295. $relPath = array_pad($relPath, $padLength, '..');
  296. break;
  297. } else {
  298. $relPath[0] = './' . $relPath[0];
  299. }
  300. }
  301. }
  302. return implode('/', $relPath);
  303. }
  304. function get_options($argv) {
  305. $parameters = array(
  306. 'exclude:' => 'e:',
  307. 'exclude-pattern:' => 'E:',
  308. 'include:' => 'i:',
  309. 'include-pattern:' => 'I:',
  310. 'repo' => 'r',
  311. 'mode:' => 'm:',
  312. 'server' => 's',
  313. 'shuffle' => '',
  314. 'help' => 'h',
  315. 'verbose' => 'v',
  316. 'fbmake' => '',
  317. 'testpilot' => '',
  318. 'threads:' => '',
  319. 'args:' => 'a:',
  320. 'log' => 'l',
  321. 'failure-file:' => '',
  322. 'arm' => '',
  323. 'wholecfg' => '',
  324. 'hhas-round-trip' => '',
  325. 'color' => 'c',
  326. 'no-fun' => '',
  327. 'cores' => '',
  328. 'no-clean' => '',
  329. 'list-tests' => '',
  330. 'relocate:' => '',
  331. 'recycle-tc:' => '',
  332. 'hhvm-binary-path:' => 'b:',
  333. 'typechecker' => '',
  334. 'hhserver-binary-path:' => '',
  335. );
  336. $options = array();
  337. $files = array();
  338. /*
  339. * '-' argument causes all future arguments to be treated as filenames, even
  340. * if they would otherwise match a valid option. Otherwise, arguments starting
  341. * with '-' MUST match a valid option.
  342. */
  343. $force_file = false;
  344. for ($i = 1; $i < count($argv); $i++) {
  345. $arg = $argv[$i];
  346. if (strlen($arg) == 0) {
  347. continue;
  348. } else if ($force_file) {
  349. $files[] = $arg;
  350. } else if ($arg === '-') {
  351. $forcefile = true;
  352. } else if ($arg[0] === '-') {
  353. $found = false;
  354. foreach ($parameters as $long => $short) {
  355. if ($arg == '-'.str_replace(':', '', $short) ||
  356. $arg == '--'.str_replace(':', '', $long)) {
  357. if (substr($long, -1, 1) == ':') {
  358. $value = $argv[++$i];
  359. } else {
  360. $value = true;
  361. }
  362. $options[str_replace(':', '', $long)] = $value;
  363. $found = true;
  364. break;
  365. }
  366. }
  367. if (!$found) {
  368. error(sprintf("Invalid argument: '%s'\nSee $argv[0] --help", $arg));
  369. }
  370. } else {
  371. $files[] = $arg;
  372. }
  373. }
  374. if (isset($options['repo']) && isset($options['hhas-round-trip'])) {
  375. echo "repo and hhas-round-trip are mutually exclusive options\n";
  376. exit(1);
  377. }
  378. if (isset($options['relocate']) && isset($options['recycle-tc'])) {
  379. echo "relocate and recycle-tc are mutually exclusive options\n";
  380. exit(1);
  381. }
  382. return array($options, $files);
  383. }
  384. /*
  385. * We support some 'special' file names, that just know where the test
  386. * suites are, to avoid typing 'hphp/test/foo'.
  387. */
  388. function find_test_files($file) {
  389. $mappage = array(
  390. 'quick' => 'hphp/test/quick',
  391. 'slow' => 'hphp/test/slow',
  392. 'spec' => 'hphp/test/spec',
  393. 'debugger' => 'hphp/test/server/debugger/tests',
  394. 'http' => 'hphp/test/server/http/tests',
  395. 'fastcgi' => 'hphp/test/server/fastcgi/tests',
  396. 'zend' => 'hphp/test/zend/good',
  397. 'facebook' => 'hphp/facebook/test',
  398. // Subsets of zend tests.
  399. 'zend_ext' => 'hphp/test/zend/good/ext',
  400. 'zend_ext_am' => 'hphp/test/zend/good/ext/[a-m]*',
  401. 'zend_ext_nz' => 'hphp/test/zend/good/ext/[n-z]*',
  402. 'zend_Zend' => 'hphp/test/zend/good/Zend',
  403. 'zend_tests' => 'hphp/test/zend/good/tests',
  404. 'zend_bad' => 'hphp/test/zend/bad',
  405. );
  406. if (isset($mappage[$file])) {
  407. $matches = glob(hphp_home().'/'.$mappage[$file]);
  408. if (count($matches) == 0) {
  409. error(sprintf(
  410. "Convenience test name '%s' is recognized but does not match any test ".
  411. "files (pattern = '%s', hphp_home = '%s')",
  412. $file, $mappage[$file], hphp_home()));
  413. }
  414. return $matches;
  415. } else {
  416. return array($file, );
  417. }
  418. }
  419. // Some tests have to be run together in the same test bucket, serially, one
  420. // after other in order to avoid races and other collisions.
  421. function serial_only_tests($tests) {
  422. if (is_testing_dso_extension()) {
  423. return array();
  424. }
  425. // Add a <testname>.php.serial file to make your test run in the serial
  426. // bucket.
  427. $serial_tests = array_filter(
  428. $tests,
  429. function($test) {
  430. return file_exists($test . '.serial');
  431. }
  432. );
  433. return $serial_tests;
  434. }
  435. function find_tests($files, array $options = null) {
  436. if (!$files) {
  437. $files = array('quick');
  438. }
  439. if ($files == array('all')) {
  440. $files = array('quick', 'slow', 'spec', 'zend', 'fastcgi');
  441. }
  442. $ft = array();
  443. foreach ($files as $file) {
  444. $ft = array_merge($ft, find_test_files($file));
  445. }
  446. $files = $ft;
  447. foreach ($files as &$file) {
  448. if (!@stat($file)) {
  449. error("Not valid file or directory: '$file'");
  450. }
  451. $file = preg_replace(',//+,', '/', realpath($file));
  452. $file = preg_replace(',^'.getcwd().'/,', '', $file);
  453. }
  454. $files = array_map('escapeshellarg', $files);
  455. $files = implode(' ', $files);
  456. if (isset($options['typechecker'])) {
  457. $tests = explode("\n", shell_exec(
  458. "find $files -name '*.php' -o -name '*.php.type-errors'"
  459. ));
  460. // The above will get all the php files. Now filter out only the ones
  461. // that have a .hhconfig associated with it.
  462. $tests = array_filter(
  463. $tests,
  464. function($test) {
  465. return (file_exists($test . '.typechecker.expect') ||
  466. file_exists($test . '.typechecker.expectf')) &&
  467. file_exists($test . '.hhconfig');
  468. }
  469. );
  470. } else {
  471. $tests = explode("\n", shell_exec(
  472. "find $files -name '*.php' -o -name '*.php.type-errors' " .
  473. "-o -name '*.hhas' | grep -v round_trip.hhas"
  474. ));
  475. $tests = array_filter(
  476. $tests,
  477. function($test) {
  478. return file_exists($test . '.expect') ||
  479. file_exists($test . '.expectf') ||
  480. file_exists($test . '.hhvm.expect') ||
  481. file_exists($test . '.hhvm.expectf');
  482. }
  483. );
  484. }
  485. if (!$tests) {
  486. error("Could not find any tests associated with your options.\n" .
  487. "Make sure your test path is correct and that you have " .
  488. "the right expect files for the tests you are trying to run.\n" .
  489. usage());
  490. }
  491. asort($tests);
  492. $tests = array_filter($tests);
  493. if (!empty($options['exclude'])) {
  494. $exclude = $options['exclude'];
  495. $tests = array_filter($tests, function($test) use ($exclude) {
  496. return (false === strpos($test, $exclude));
  497. });
  498. }
  499. if (!empty($options['exclude-pattern'])) {
  500. $exclude = $options['exclude-pattern'];
  501. $tests = array_filter($tests, function($test) use ($exclude) {
  502. return !preg_match($exclude, $test);
  503. });
  504. }
  505. if (!empty($options['include'])) {
  506. $include = $options['include'];
  507. $tests = array_filter($tests, function($test) use ($include) {
  508. return (false !== strpos($test, $include));
  509. });
  510. }
  511. if (!empty($options['include-pattern'])) {
  512. $include = $options['include-pattern'];
  513. $tests = array_filter($tests, function($test) use ($include) {
  514. return preg_match($include, $test);
  515. });
  516. }
  517. return $tests;
  518. }
  519. function list_tests($files, $options) {
  520. $args = array();
  521. $mode = idx($options, 'mode', '');
  522. switch ($mode) {
  523. case '':
  524. case 'jit':
  525. $args[] = '-m jit';
  526. break;
  527. case 'interp';
  528. $args[] = '-m interp';
  529. break;
  530. default:
  531. throw new Exception("Unsupported mode for listing tests: ".$mode);
  532. }
  533. if (isset($options['repo'])) {
  534. $args[] = '-r';
  535. }
  536. if (isset($options['relocate'])) {
  537. $args[] = '--relocate';
  538. $args[] = $options['relocate'];
  539. }
  540. if (isset($options['recycle-tc'])) {
  541. $args[] = '--recycle-tc';
  542. $args[] = $options['recycle-tc'];
  543. }
  544. foreach (find_tests($files, $options) as $test) {
  545. print Status::jsonEncode(array(
  546. 'args' => implode(' ', $args),
  547. 'name' => $test,
  548. ))."\n";
  549. }
  550. }
  551. function find_test_ext($test, $ext) {
  552. if (is_file("{$test}.{$ext}")) {
  553. return "{$test}.{$ext}";
  554. }
  555. return find_file_for_dir(dirname($test), "config.{$ext}");
  556. }
  557. function find_file($test, $name) {
  558. return find_file_for_dir(dirname($test), $name);
  559. }
  560. function find_file_for_dir($dir, $name) {
  561. // Handle the case where the $dir might come in as '.' because you
  562. // are running the test runner on a file from the same directory as
  563. // the test e.g., './mytest.php'. dirname() will give you the '.' when
  564. // you actually have a lot of path to traverse upwards like
  565. // /home/you/code/tests/mytest.php. Use realpath() to get that.
  566. $dir = realpath($dir);
  567. while ($dir !== '/' && is_dir($dir)) {
  568. $file = "$dir/$name";
  569. if (is_file($file)) {
  570. return $file;
  571. }
  572. $dir = dirname($dir);
  573. }
  574. $file = __DIR__.'/'.$name;
  575. if (file_exists($file)) {
  576. return $file;
  577. }
  578. return null;
  579. }
  580. function find_debug_config($test, $name) {
  581. $debug_config = find_file_for_dir(dirname($test), $name);
  582. if ($debug_config !== null) {
  583. return "-m debug --debug-config ".$debug_config;
  584. }
  585. return "";
  586. }
  587. function mode_cmd($options) {
  588. $repo_args = '';
  589. if (!isset($options['repo'])) {
  590. // Set the non-repo-mode shared repo.
  591. // When in repo mode, we set our own central path.
  592. $repo_args = "-vRepo.Local.Mode=-- -vRepo.Central.Path=".verify_hhbc();
  593. }
  594. $jit_args = "$repo_args -vEval.Jit=true";
  595. $mode = idx($options, 'mode', '');
  596. switch ($mode) {
  597. case '':
  598. case 'jit':
  599. return "$jit_args";
  600. case 'pgo':
  601. return $jit_args.
  602. ' -vEval.JitPGO=1'.
  603. ' -vEval.JitPGORegionSelector=hottrace'.
  604. ' -vEval.JitPGOHotOnly=0';
  605. case 'interp':
  606. return "$repo_args -vEval.Jit=0";
  607. default:
  608. error("-m must be one of jit | pgo | interp. Got: '$mode'");
  609. }
  610. }
  611. function extra_args($options) {
  612. return idx($options, 'args', '');
  613. }
  614. function hhvm_cmd_impl() {
  615. $args = func_get_args();
  616. $options = array_shift($args);
  617. $config = array_shift($args);
  618. $extra_args = $args;
  619. $args = array(
  620. hhvm_path(),
  621. '-c',
  622. $config,
  623. '-vEval.EnableArgsInBacktraces=true',
  624. '-vEval.EnableIntrinsicsExtension=true',
  625. mode_cmd($options),
  626. isset($options['arm']) ? '-vEval.SimulateARM=1' : '',
  627. isset($options['wholecfg']) ? '-vEval.JitPGORegionSelector=wholecfg' : '',
  628. extra_args($options),
  629. );
  630. if (isset($options['relocate'])) {
  631. $args[] = '--count='.($options['relocate'] * 2);
  632. $args[] = '-vEval.JitAHotSize=6000000';
  633. $args[] = '-vEval.HotFuncCount=0';
  634. $args[] = '-vEval.PerfRelocate='.$options['relocate'];
  635. }
  636. if (isset($options['recycle-tc'])) {
  637. $args[] = '--count='.$options['recycle-tc'];
  638. $args[] = '-vEval.StressUnitCacheFreq=1';
  639. $args[] = '-vEval.EnableReusableTC=true';
  640. }
  641. if (!isset($options['cores'])) {
  642. $args[] = '-vResourceLimit.CoreFileSize=0';
  643. }
  644. return implode(' ', array_merge($args, $extra_args));
  645. }
  646. // Return the command and the env to run it in.
  647. function hhvm_cmd($options, $test, $test_run = null) {
  648. if ($test_run === null) {
  649. $test_run = $test;
  650. }
  651. // hdf support is only temporary until we fully migrate to ini
  652. // Discourage broad use.
  653. $hdf_suffix = ".use.for.ini.migration.testing.only.hdf";
  654. $hdf = file_exists($test.$hdf_suffix)
  655. ? '-c ' . $test . $hdf_suffix
  656. : "";
  657. $cmd = hhvm_cmd_impl(
  658. $options,
  659. find_test_ext($test, 'ini'),
  660. $hdf,
  661. find_debug_config($test, 'hphpd.ini'),
  662. read_opts_file(find_test_ext($test, 'opts')),
  663. '--file',
  664. escapeshellarg($test_run)
  665. );
  666. // Special support for tests that require a path to the current
  667. // test directory for things like prepend_file and append_file
  668. // testing.
  669. if (file_exists($test.'.ini')) {
  670. $contents = file_get_contents($test.'.ini');
  671. if (strpos($contents, '{PWD}') !== false) {
  672. $test_ini = tempnam('/tmp', $test).'.ini';
  673. file_put_contents($test_ini,
  674. str_replace('{PWD}', dirname($test), $contents));
  675. $cmd .= " -c $test_ini";
  676. }
  677. }
  678. if ($hdf !== "") {
  679. $contents = file_get_contents($test.$hdf_suffix);
  680. if (strpos($contents, '{PWD}') !== false) {
  681. $test_hdf = tempnam('/tmp', $test).$hdf_suffix;
  682. file_put_contents($test_hdf,
  683. str_replace('{PWD}', dirname($test), $contents));
  684. $cmd .= " -c $test_hdf";
  685. }
  686. }
  687. if (isset($options['repo'])) {
  688. $hhbbc_repo = "\"$test.repo/hhvm.hhbbc\"";
  689. $cmd .= ' -vRepo.Authoritative=true -vRepo.Commit=0';
  690. $cmd .= " -vRepo.Central.Path=$hhbbc_repo";
  691. }
  692. // Command line arguments
  693. $cli_args = find_test_ext($test, 'cli_args');
  694. if ($cli_args !== null) {
  695. $cmd .= " " . file_get_contents($cli_args);
  696. }
  697. $env = $_ENV;
  698. // If there's an <test name>.env file then inject the contents of that into
  699. // the test environment.
  700. $env_file = find_test_ext($test, 'env');
  701. if ($env_file !== null) {
  702. $extra_env = explode("\n", trim(file_get_contents($env_file)));
  703. foreach ($extra_env as $arg) {
  704. $i = strpos($arg, '=');
  705. if ($i) {
  706. $key = substr($arg, 0, $i);
  707. $val = substr($arg, $i + 1);
  708. $env[$key] = $val;
  709. } else {
  710. unset($env[$arg]);
  711. }
  712. }
  713. }
  714. $in = find_test_ext($test, 'in');
  715. if ($in !== null) {
  716. $cmd .= ' < ' . escapeshellarg($in);
  717. // If we're piping the input into the command then setup a simple
  718. // dumb terminal so hhvm doesn't try to control it and pollute the
  719. // output with control characters, which could change depending on
  720. // a wide variety of terminal settings.
  721. $env["TERM"] = "dumb";
  722. }
  723. return array($cmd, $env);
  724. }
  725. function hphp_cmd($options, $test) {
  726. $extra_args = preg_replace("/-v\s*/", "-vRuntime.", extra_args($options));
  727. return implode(" ", array(
  728. "HHVM_DISABLE_HHBBC2=1",
  729. hhvm_path(),
  730. '--hphp',
  731. '--config',
  732. find_file($test, 'hphp_config.ini'),
  733. read_opts_file("$test.hphp_opts"),
  734. "-thhbc -l0 -k1 -o \"$test.repo\" \"$test\"",
  735. $extra_args
  736. ));
  737. }
  738. function hhbbc_cmd($options, $test) {
  739. return implode(" ", array(
  740. hhvm_path(),
  741. '--hhbbc',
  742. '--no-logging',
  743. '--parallel-num-threads=1',
  744. read_opts_file("$test.hhbbc_opts"),
  745. "-o \"$test.repo/hhvm.hhbbc\" \"$test.repo/hhvm.hhbc\"",
  746. ));
  747. }
  748. function hh_server_cmd($options, $test) {
  749. // In order to run hh_server --check on only one file, we copy all of the
  750. // files associated with the test to a temporary directory, rename the
  751. // basename($test_file).hhconfig file to just .hhconfig and set the command
  752. // appropriately.
  753. $temp_dir = '/tmp/hh-test-runner-'.bin2hex(random_bytes(16));
  754. mkdir($temp_dir);
  755. foreach (glob($test . '*') as $test_file) {
  756. copy($test_file, $temp_dir . '/' . basename($test_file));
  757. if (strpos($test_file, '.hhconfig') !== false) {
  758. rename(
  759. $temp_dir . '/' . basename($test) . '.hhconfig',
  760. $temp_dir . '/.hhconfig'
  761. );
  762. } else if (strpos($test_file, '.type-errors') !== false) {
  763. // In order to actually run hh_server --check successfully, all files
  764. // named *.php.type-errors have to be renamed *.php
  765. rename(
  766. $temp_dir . '/' . basename($test_file),
  767. $temp_dir . '/' . str_replace('.type-errors', '', basename($test_file))
  768. );
  769. }
  770. }
  771. // Just copy all the .php.inc files, even if they are not related since
  772. // unrelated ones will be ignored anyway. This just makes it easier to
  773. // start with instead of doing a search inside the test file for requires
  774. // and includes and extracting it.
  775. foreach (glob(dirname($test) . "/*.inc.php") as $inc_file) {
  776. copy($inc_file, $temp_dir . '/' . basename($inc_file));
  777. }
  778. $cmd = hh_server_path() . ' --check ' . $temp_dir;
  779. return array($cmd, ' ', $temp_dir);
  780. }
  781. class Status {
  782. private static $results = array();
  783. private static $mode = 0;
  784. private static $use_color = false;
  785. private static $queue = null;
  786. private static $killed = false;
  787. public static $key;
  788. private static $overall_start_time = 0;
  789. private static $overall_end_time = 0;
  790. private static $tempdir = "";
  791. const MODE_NORMAL = 0;
  792. const MODE_VERBOSE = 1;
  793. const MODE_FBMAKE = 2;
  794. const MODE_TESTPILOT = 3;
  795. const MSG_STARTED = 7;
  796. const MSG_FINISHED = 1;
  797. const MSG_TEST_PASS = 2;
  798. const MSG_TEST_FAIL = 4;
  799. const MSG_TEST_SKIP = 5;
  800. const MSG_SERVER_RESTARTED = 6;
  801. const RED = 31;
  802. const GREEN = 32;
  803. const YELLOW = 33;
  804. const BLUE = 34;
  805. const PASS_SERVER = 0;
  806. const SKIP_SERVER = 1;
  807. const PASS_CLI = 2;
  808. private static function getTempDir() {
  809. self::$tempdir = sys_get_temp_dir();
  810. // Apparently some systems might not put the trailing slash
  811. if (substr(self::$tempdir, -1) !== "/") {
  812. self::$tempdir .= "/";
  813. }
  814. self::$tempdir .= substr( md5(rand()), 0, 8);
  815. mkdir(self::$tempdir);
  816. }
  817. // Since we run the tests in forked processes, state is not shared
  818. // So we cannot keep a static variable adding individual test times.
  819. // But we can put the times files and add the values later.
  820. public static function setTestTime($time) {
  821. file_put_contents(tempnam(self::$tempdir, "trun"), $time);
  822. }
  823. // The total time running the tests if they were run serially.
  824. public static function addTestTimesSerial() {
  825. $time = 0;
  826. $files = scandir(self::$tempdir);
  827. foreach ($files as $file) {
  828. if (strpos($file, 'trun') === 0) {
  829. $time += floatval(file_get_contents(self::$tempdir . "/" . $file));
  830. unlink(self::$tempdir . "/" . $file);
  831. }
  832. }
  833. return $time;
  834. }
  835. public static function setMode($mode) {
  836. self::$mode = $mode;
  837. }
  838. public static function setUseColor($use) {
  839. self::$use_color = $use;
  840. }
  841. public static function getMode() {
  842. return self::$mode;
  843. }
  844. public static function getOverallStartTime() {
  845. return self::$overall_start_time;
  846. }
  847. public static function getOverallEndTime() {
  848. return self::$overall_end_time;
  849. }
  850. public static function started() {
  851. self::getTempDir();
  852. self::send(self::MSG_STARTED, null);
  853. self::$overall_start_time = microtime(true);
  854. }
  855. public static function finished() {
  856. self::$overall_end_time = microtime(true);
  857. self::send(self::MSG_FINISHED, null);
  858. }
  859. public static function killQueue() {
  860. if (!self::$killed) {
  861. msg_remove_queue(self::$queue);
  862. self::$queue = null;
  863. self::$killed = true;
  864. }
  865. }
  866. public static function pass($test, $detail, $time, $stime, $etime) {
  867. array_push(self::$results, array('name' => $test,
  868. 'status' => 'passed',
  869. 'start_time' => $stime,
  870. 'end_time' => $etime,
  871. 'time' => $time));
  872. $how = $detail === 'pass-server' ? self::PASS_SERVER :
  873. ($detail === 'skip-server' ? self::SKIP_SERVER : self::PASS_CLI);
  874. self::send(self::MSG_TEST_PASS, array($test, $how, $time, $stime, $etime));
  875. }
  876. public static function skip($test, $reason, $time, $stime, $etime) {
  877. if (self::getMode() === self::MODE_FBMAKE) {
  878. /* Intentionally supress skips */
  879. } elseif (self::getMode() === self::MODE_TESTPILOT) {
  880. /* testpilot needs a positive response for every test run, report
  881. * that this test isn't relevant so it can silently drop. */
  882. array_push(self::$results, array('name' => $test,
  883. 'status' => 'not_relevant',
  884. 'start_time' => $stime,
  885. 'end_time' => $etime,
  886. 'time' => $time));
  887. } else {
  888. array_push(self::$results, array('name' => $test,
  889. 'status' => 'skipped',
  890. 'start_time' => $stime,
  891. 'end_time' => $etime,
  892. 'time' => $time));
  893. }
  894. self::send(self::MSG_TEST_SKIP,
  895. array($test, $reason, $time, $stime, $etime));
  896. }
  897. public static function fail($test, $time, $stime, $etime) {
  898. array_push(self::$results, array(
  899. 'name' => $test,
  900. 'status' => 'failed',
  901. 'details' => self::utf8Sanitize(@file_get_contents("$test.diff")),
  902. 'start_time' => $stime,
  903. 'end_time' => $etime,
  904. 'time' => $time
  905. ));
  906. self::send(self::MSG_TEST_FAIL, array($test, $time, $stime, $etime));
  907. }
  908. public static function serverRestarted() {
  909. self::send(self::MSG_SERVER_RESTARTED, null);
  910. }
  911. private static function send($type, $msg) {
  912. if (self::$killed) {
  913. return;
  914. }
  915. msg_send(self::getQueue(), $type, $msg);
  916. }
  917. /**
  918. * Takes a variable number of string arguments. If color output is enabled
  919. * and any one of the arguments is preceded by an integer (see the color
  920. * constants above), that argument will be given the indicated color.
  921. */
  922. public static function sayColor() {
  923. $args = func_get_args();
  924. while (count($args)) {
  925. $color = null;
  926. $str = array_shift($args);
  927. if (is_integer($str)) {
  928. $color = $str;
  929. if (self::$use_color) {
  930. print "\033[0;${color}m";
  931. }
  932. $str = array_shift($args);
  933. }
  934. print $str;
  935. if (self::$use_color && !is_null($color)) {
  936. print "\033[0m";
  937. }
  938. }
  939. }
  940. public static function sayFBMake($test, $status, $stime, $etime) {
  941. $start = array('op' => 'start', 'test' => $test);
  942. $end = array('op' => 'test_done', 'test' => $test, 'status' => $status,
  943. 'start_time' => $stime, 'end_time' => $etime);
  944. if ($status == 'failed') {
  945. $end['details'] = self::utf8Sanitize(@file_get_contents("$test.diff"));
  946. }
  947. self::say($start, $end);
  948. }
  949. public static function getResults() {
  950. return self::$results;
  951. }
  952. /** Output is in the format expected by JsonTestRunner. */
  953. public static function say(/* ... */) {
  954. $data = array_map(function($row) {
  955. return self::jsonEncode($row) . "\n";
  956. }, func_get_args());
  957. fwrite(STDERR, implode("", $data));
  958. }
  959. public static function hasCursorControl() {
  960. // for runs on hudson-ci.org (aka jenkins).
  961. if (getenv("HUDSON_URL")) {
  962. return false;
  963. }
  964. // for runs on travis-ci.org
  965. if (getenv("TRAVIS")) {
  966. return false;
  967. }
  968. $stty = self::getSTTY();
  969. if (!$stty) {
  970. return false;
  971. }
  972. return strpos($stty, 'erase = <undef>') === false;
  973. }
  974. public static function getSTTY() {
  975. $descriptorspec = array(1 => array("pipe", "w"), 2 => array("pipe", "w"));
  976. $process = proc_open(
  977. 'stty -a', $descriptorspec, $pipes, null, null,
  978. array('suppress_errors' => true)
  979. );
  980. $stty = stream_get_contents($pipes[1]);
  981. proc_close($process);
  982. return $stty;
  983. }
  984. public static function utf8Sanitize($str) {
  985. if (!is_string($str)) {
  986. // We sometimes get called with the
  987. // return value of file_get_contents()
  988. // when fgc() has failed.
  989. return '';
  990. }
  991. if (class_exists('UConverter')) {
  992. return UConverter::transcode($str, 'UTF-8', 'UTF-8');
  993. }
  994. // UConverter is PHP5.5 or later.
  995. // Do the equivalent using a slower user-space implementation.
  996. $ret = '';
  997. $len = strlen($str);
  998. $pos = 0;
  999. while ($pos < $len) {
  1000. $c = $str[$pos];
  1001. $co = ord($c);
  1002. if ($co < 0x80) {
  1003. // U+0000 - U+007F ASCII
  1004. $ret .= $c;
  1005. ++$pos;
  1006. } elseif ((($co & 0xE0) == 0xC0) &&
  1007. (($len - $pos) > 1) &&
  1008. ((ord($str[$pos+1]) & 0xC0) == 0x80)) {
  1009. // U+0080 - U+07FF Lower Basic Multilingual Plane
  1010. $ret .= substr($str, $pos, 2);
  1011. $pos += 2;
  1012. } elseif ((($co & 0xF0) == 0xE0) &&
  1013. (($len - $pos) > 2) &&
  1014. ((ord($str[$pos+1]) & 0xC0) == 0x80) &&
  1015. ((ord($str[$pos+2]) & 0xC0) == 0x80)) {
  1016. // U+0800 - U+FFFF Upper Basic Multilingual Plane
  1017. $ret .= substr($str, $pos, 3);
  1018. $pos += 3;
  1019. } elseif ((($co & 0xF8) == 0xF0) &&
  1020. (($len - $pos) > 3) &&
  1021. ((ord($str[$pos+1]) & 0xC0) == 0x80) &&
  1022. ((ord($str[$pos+2]) & 0xC0) == 0x80) &&
  1023. ((ord($str[$pos+3]) & 0xC0) == 0x80)) {
  1024. // U+010000 - U+10FFFF Supplementary Multilingual Planes
  1025. $ret .= substr($str, $pos, 4);
  1026. $pos += 4;
  1027. } else {
  1028. // Invalid UTF8
  1029. $ret .= "\xEF\xBF\xBD"; // U+FFFD REPLACEMENT CHARACTER
  1030. ++$pos;
  1031. }
  1032. }
  1033. return $ret;
  1034. }
  1035. public static function jsonEncode($data) {
  1036. // JSON_UNESCAPED_SLASHES is Zend 5.4+.
  1037. if (defined("JSON_UNESCAPED_SLASHES")) {
  1038. return json_encode($data, JSON_UNESCAPED_SLASHES);
  1039. }
  1040. $json = json_encode($data);
  1041. return str_replace('\\/', '/', $json);
  1042. }
  1043. public static function getQueue() {
  1044. if (!self::$queue) {
  1045. self::$queue = msg_get_queue(self::$key);
  1046. }
  1047. return self::$queue;
  1048. }
  1049. }
  1050. function clean_intermediate_files($test, $options) {
  1051. if (isset($options['no-clean'])) {
  1052. return;
  1053. }
  1054. $exts = array('out', 'diff', 'repo');
  1055. foreach ($exts as $ext) {
  1056. $file = "$test.$ext";
  1057. if (file_exists($file)) {
  1058. if (is_dir($file)) {
  1059. foreach(new RecursiveIteratorIterator(new
  1060. RecursiveDirectoryIterator($file, FilesystemIterator::SKIP_DOTS),
  1061. RecursiveIteratorIterator::CHILD_FIRST) as $path) {
  1062. $path->isDir()
  1063. ? rmdir($path->getPathname())
  1064. : unlink($path->getPathname());
  1065. }
  1066. rmdir($file);
  1067. } else {
  1068. unlink($file);
  1069. }
  1070. }
  1071. }
  1072. }
  1073. function run($options, $tests, $bad_test_file) {
  1074. foreach ($tests as $test) {
  1075. $stime = time();
  1076. $time = microtime(true);
  1077. $status = run_and_lock_test($options, $test);
  1078. $time = microtime(true) - $time;
  1079. $etime = time();
  1080. Status::setTestTime($time);
  1081. if ($status === 'skip') {
  1082. Status::skip($test, null, $time, $stime, $etime);
  1083. clean_intermediate_files($test, $options);
  1084. } else if ($status === 'skip-norepo') {
  1085. Status::skip($test, 'norepo', $time, $stime, $etime);
  1086. clean_intermediate_files($test, $options);
  1087. } else if ($status === 'skip-onlyrepo') {
  1088. Status::skip($test, 'onlyrepo', $time, $stime, $etime);
  1089. clean_intermediate_files($test, $options);
  1090. } else if ($status) {
  1091. Status::pass($test, $status, $time, $stime, $etime);
  1092. clean_intermediate_files($test, $options);
  1093. } else {
  1094. Status::fail($test, $time, $stime, $etime);
  1095. }
  1096. }
  1097. file_put_contents($bad_test_file, json_encode(Status::getResults()));
  1098. foreach (Status::getResults() as $result) {
  1099. if ($result['status'] == 'failed') {
  1100. return 1;
  1101. }
  1102. }
  1103. return 0;
  1104. }
  1105. function skip_test($options, $test) {
  1106. $skipif_test = find_test_ext($test, 'skipif');
  1107. if (!$skipif_test) {
  1108. return false;
  1109. }
  1110. // For now, run the .skipif in non-repo since building a repo for it is hard.
  1111. $options_without_repo = $options;
  1112. unset($options_without_repo['repo']);
  1113. list($hhvm, $_) = hhvm_cmd($options_without_repo, $test, $skipif_test);
  1114. $descriptorspec = array(
  1115. 0 => array("pipe", "r"),
  1116. 1 => array("pipe", "w"),
  1117. 2 => array("pipe", "w"),
  1118. );
  1119. $pipes = null;
  1120. $process = proc_open("$hhvm $test 2>&1", $descriptorspec, $pipes);
  1121. if (!is_resource($process)) {
  1122. // This is weird. We can't run HHVM but we probably shouldn't skip the test
  1123. // since on a broken build everything will show up as skipped and give you a
  1124. // SHIPIT.
  1125. return false;
  1126. }
  1127. fclose($pipes[0]);
  1128. $output = stream_get_contents($pipes[1]);
  1129. fclose($pipes[1]);
  1130. proc_close($process);
  1131. // The standard php5 .skipif semantics is if the .skipif outputs ANYTHING
  1132. // then it should be skipped. This is a poor design, but I'll just add a
  1133. // small blacklist of things that are really bad if they are output so we
  1134. // surface the errors in the tests themselves.
  1135. if (stripos($output, 'segmentation fault') !== false) {
  1136. return false;
  1137. }
  1138. return strlen($output) != 0;
  1139. }
  1140. function comp_line($l1, $l2, $is_reg) {
  1141. if ($is_reg) {
  1142. return preg_match('/^'. $l1 . '$/s', $l2);
  1143. } else {
  1144. return !strcmp($l1, $l2);
  1145. }
  1146. }
  1147. function count_array_diff($ar1, $ar2, $is_reg, $w, $idx1, $idx2, $cnt1, $cnt2,
  1148. $steps) {
  1149. $equal = 0;
  1150. while ($idx1 < $cnt1 && $idx2 < $cnt2 && comp_line($ar1[$idx1], $ar2[$idx2],
  1151. $is_reg)) {
  1152. $idx1++;
  1153. $idx2++;
  1154. $equal++;
  1155. $steps--;
  1156. }
  1157. if (--$steps > 0) {
  1158. $eq1 = 0;
  1159. $st = $steps / 2;
  1160. for ($ofs1 = $idx1 + 1; $ofs1 < $cnt1 && $st-- > 0; $ofs1++) {
  1161. $eq = @count_array_diff($ar1, $ar2, $is_reg, $w, $ofs1, $idx2, $cnt1,
  1162. $cnt2, $st);
  1163. if ($eq > $eq1) {
  1164. $eq1 = $eq;
  1165. }
  1166. }
  1167. $eq2 = 0;
  1168. $st = $steps;
  1169. for ($ofs2 = $idx2 + 1; $ofs2 < $cnt2 && $st-- > 0; $ofs2++) {
  1170. $eq = @count_array_diff($ar1, $ar2, $is_reg, $w, $idx1, $ofs2, $cnt1, $cnt2, $st);
  1171. if ($eq > $eq2) {
  1172. $eq2 = $eq;
  1173. }
  1174. }
  1175. if ($eq1 > $eq2) {
  1176. $equal += $eq1;
  1177. } else if ($eq2 > 0) {
  1178. $equal += $eq2;
  1179. }
  1180. }
  1181. return $equal;
  1182. }
  1183. function generate_array_diff($ar1, $ar2, $is_reg, $w) {
  1184. $idx1 = 0; $ofs1 = 0; $cnt1 = @count($ar1);
  1185. $idx2 = 0; $ofs2 = 0; $cnt2 = @count($ar2);
  1186. $diff = array();
  1187. $old1 = array();
  1188. $old2 = array();
  1189. while ($idx1 < $cnt1 && $idx2 < $cnt2) {
  1190. if (comp_line($ar1[$idx1], $ar2[$idx2], $is_reg)) {
  1191. $idx1++;
  1192. $idx2++;
  1193. continue;
  1194. } else {
  1195. $c1 = @count_array_diff($ar1, $ar2, $is_reg, $w, $idx1+1, $idx2, $cnt1,
  1196. $cnt2, 10);
  1197. $c2 = @count_array_diff($ar1, $ar2, $is_reg, $w, $idx1, $idx2+1, $cnt1,
  1198. $cnt2, 10);
  1199. if ($c1 > $c2) {
  1200. $old1[$idx1] = sprintf("%03d- ", $idx1+1) . $w[$idx1++];
  1201. $last = 1;
  1202. } else if ($c2 > 0) {
  1203. $old2[$idx2] = sprintf("%03d+ ", $idx2+1) . $ar2[$idx2++];
  1204. $last = 2;
  1205. } else {
  1206. $old1[$idx1] = sprintf("%03d- ", $idx1+1) . $w[$idx1++];
  1207. $old2[$idx2] = sprintf("%03d+ ", $idx2+1) . $ar2[$idx2++];
  1208. }
  1209. }
  1210. }
  1211. reset($old1); $k1 = key($old1); $l1 = -2;
  1212. reset($old2); $k2 = key($old2); $l2 = -2;
  1213. while ($k1 !== null || $k2 !== null) {
  1214. if ($k1 == $l1 + 1 || $k2 === null) {
  1215. $l1 = $k1;
  1216. $diff[] = current($old1);
  1217. $k1 = next($old1) ? key($old1) : null;
  1218. } else if ($k2 == $l2 + 1 || $k1 === null) {
  1219. $l2 = $k2;
  1220. $diff[] = current($old2);
  1221. $k2 = next($old2) ? key($old2) : null;
  1222. } else if ($k1 < $k2) {
  1223. $l1 = $k1;
  1224. $diff[] = current($old1);
  1225. $k1 = next($old1) ? key($old1) : null;
  1226. } else {
  1227. $l2 = $k2;
  1228. $diff[] = current($old2);
  1229. $k2 = next($old2) ? key($old2) : null;
  1230. }
  1231. }
  1232. while ($idx1 < $cnt1) {
  1233. $diff[] = sprintf("%03d- ", $idx1 + 1) . $w[$idx1++];
  1234. }
  1235. while ($idx2 < $cnt2) {
  1236. $diff[] = sprintf("%03d+ ", $idx2 + 1) . $ar2[$idx2++];
  1237. }
  1238. return $diff;
  1239. }
  1240. function generate_diff($wanted, $wanted_re, $output)
  1241. {
  1242. $w = explode("\n", $wanted);
  1243. $o = explode("\n", $output);
  1244. if (is_null($wanted_re)) {
  1245. $r = $w;
  1246. } else {
  1247. if (preg_match('/^\((.*)\)\{(\d+)\}$/s', $wanted_re, $m)) {
  1248. $t = explode("\n", $m[1]);
  1249. $r = array();
  1250. $w2 = array();
  1251. for ($i = 0; $i < $m[2]; $i++) {
  1252. foreach ($t as $v) {
  1253. $r[] = $v;
  1254. }
  1255. foreach ($w as $v) {
  1256. $w2[] = $v;
  1257. }
  1258. }
  1259. $w = $wanted === $wanted_re ? $r : $w2;
  1260. } else {
  1261. $r = explode("\n", $wanted_re);
  1262. }
  1263. }
  1264. $diff = generate_array_diff($r, $o, !is_null($wanted_re), $w);
  1265. return implode("\r\n", $diff);
  1266. }
  1267. function dump_hhas_to_temp($hhvm_cmd, $test) {
  1268. $tmp_file = $test . '.round_trip.hhas';
  1269. system("$hhvm_cmd -vEval.DumpHhas=1 > $tmp_file", $ret);
  1270. if ($ret) { echo "system failed\n"; exit(1); }
  1271. return $tmp_file;
  1272. }
  1273. const HHAS_EXT = '.hhas';
  1274. function can_run_server_test($test) {
  1275. return
  1276. !is_file("$test.noserver") &&
  1277. !find_test_ext($test, 'opts') &&
  1278. !is_file("$test.ini") &&
  1279. !is_file("$test.onlyrepo") &&
  1280. strpos($test, 'quick/debugger') === false &&
  1281. strpos($test, 'quick/xenon') === false &&
  1282. strpos($test, 'slow/streams/') === false &&
  1283. strpos($test, 'slow/ext_mongo/') === false &&
  1284. strpos($test, 'slow/ext_oauth/') === false &&
  1285. strpos($test, 'slow/ext_yaml/') === false &&
  1286. strpos($test, 'slow/debugger/') === false &&
  1287. strpos($test, 'slow/type_profiler/debugger/') === false &&
  1288. strpos($test, 'zend/good/ext/standard/tests/array/') === false &&
  1289. strpos($test, 'zend/good/ext/ftp') === false &&
  1290. strrpos($test, HHAS_EXT) !== (strlen($test) - strlen(HHAS_EXT))
  1291. ;
  1292. }
  1293. const SERVER_TIMEOUT = 45;
  1294. function run_config_server($options, $test) {
  1295. if (!isset($options['server']) || !can_run_server_test($test)) {
  1296. return null;
  1297. }
  1298. $config = find_file_for_dir(dirname($test), 'config.ini');
  1299. $port = $options['servers']['configs'][$config]['port'];
  1300. $ch = curl_init("localhost:$port/$test");
  1301. curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
  1302. curl_setopt($ch, CURLOPT_TIMEOUT, SERVER_TIMEOUT);
  1303. curl_setopt($ch, CURLOPT_BINARYTRANSFER, true);
  1304. $output = curl_exec($ch);
  1305. if ($output === false) {
  1306. // The server probably crashed so fall back to cli to determine if this was
  1307. // the test that caused the crash. Our parent process will see that the
  1308. // server died and restart it.
  1309. if (getenv('HHVM_TEST_SERVER_LOG')) {
  1310. printf("Curl failed: %d\n", curl_errno($ch));
  1311. }
  1312. return null;
  1313. }
  1314. curl_close($ch);
  1315. $output = trim($output);
  1316. return array($output, '');
  1317. }
  1318. function run_config_cli($options, $test, $cmd, $cmd_env) {
  1319. if (isset($options['log']) && !isset($options['typechecker'])) {
  1320. $cmd_env['TRACE'] = 'printir:1';
  1321. $cmd_env['HPHP_TRACE_FILE'] = $test . '.log';
  1322. }
  1323. $descriptorspec = array(
  1324. 0 => array("pipe", "r"),
  1325. 1 => array("pipe", "w"),
  1326. 2 => array("pipe", "w"),
  1327. );
  1328. $pipes = null;
  1329. if (isset($options['typechecker'])) {
  1330. $process = proc_open(
  1331. "$cmd 2>/dev/null", $descriptorspec, $pipes, null, $cmd_env
  1332. );
  1333. } else {
  1334. $process = proc_open("$cmd 2>&1", $descriptorspec, $pipes, null, $cmd_env);
  1335. }
  1336. if (!is_resource($process)) {
  1337. file_put_contents("$test.diff", "Couldn't invoke $cmd");
  1338. return false;
  1339. }
  1340. fclose($pipes[0]);
  1341. $output = stream_get_contents($pipes[1]);
  1342. $output = trim($output);
  1343. $stderr = stream_get_contents($pipes[2]);
  1344. fclose($pipes[1]);
  1345. fclose($pipes[2]);
  1346. proc_close($process);
  1347. return array($output, $stderr);
  1348. }
  1349. function run_config_post($outputs, $test, $options) {
  1350. $output = $outputs[0];
  1351. $stderr = $outputs[1];
  1352. file_put_contents("$test.out", $output);
  1353. // hhvm redirects errors to stdout, so anything on stderr is really bad.
  1354. if ($stderr) {
  1355. file_put_contents(
  1356. "$test.diff",
  1357. "Test failed because the process wrote on stderr:\n$stderr"
  1358. );
  1359. return false;
  1360. }
  1361. // Needed for testing non-hhvm binaries that don't actually run the code
  1362. // e.g. parser/test/parse_tester.cpp.
  1363. if ($output == "FORCE PASS") {
  1364. return true;
  1365. }
  1366. $repeats = 0;
  1367. if (isset($options['relocate'])) {
  1368. $repeats = $options['relocate'] * 2;
  1369. }
  1370. if (isset($options['recycle-tc'])) {
  1371. $repeats = $options['recycle-tc'];
  1372. }
  1373. list($file, $type) = get_expect_file_and_type($test, $options);
  1374. if ($file === null || $type === null) {
  1375. file_put_contents(
  1376. "$test.diff", "No $test.expect, $test.expectf, " .
  1377. "$test.hhvm.expect, $test.hhvm.expectf, " .
  1378. "$test.typechecker.expect, $test.typechecker.expectf, " .
  1379. "nor $test.expectregex. Is $test even a test?"
  1380. );
  1381. return false;
  1382. }
  1383. $is_tc = isset($options['typechecker']);
  1384. if ((!$is_tc && ($type === 'expect' || $type === 'hhvm.expect')) ||
  1385. ($is_tc && $type === 'typechecker.expect')) {
  1386. $wanted = trim(file_get_contents($file));
  1387. if (!$repeats) {
  1388. $passed = !strcmp($output, $wanted);
  1389. if (!$passed) {
  1390. file_put_contents("$test.diff", generate_diff($wanted, null, $output));
  1391. }
  1392. return $passed;
  1393. }
  1394. $wanted_re = preg_quote($wanted);
  1395. } else if ((!$is_tc && ($type === 'expectf' || $type === 'hhvm.expectf')) ||
  1396. ($is_tc && $type === 'typechecker.expectf')) {
  1397. $wanted = trim(file_get_contents($file));
  1398. $wanted_re = $wanted;
  1399. // do preg_quote, but miss out any %r delimited sections.
  1400. $temp = "";
  1401. $r = "%r";
  1402. $startOffset = 0;
  1403. $length = strlen($wanted_re);
  1404. while($startOffset < $length) {
  1405. $start = strpos($wanted_re, $r, $startOffset);
  1406. if ($start !== false) {
  1407. // we have found a start tag.
  1408. $end = strpos($wanted_re, $r, $start+2);
  1409. if ($end === false) {
  1410. // unbalanced tag, ignore it.
  1411. $end = $start = $length;
  1412. }
  1413. } else {
  1414. // no more %r sections.
  1415. $start = $end = $length;
  1416. }
  1417. // quote a non re portion of the string.
  1418. $temp = $temp.preg_quote(substr($wanted_re, $startOffset,
  1419. ($start - $startOffset)), '/');
  1420. // add the re unquoted.
  1421. if ($end > $start) {
  1422. $temp = $temp.'('.substr($wanted_re, $start+2, ($end - $start-2)).')';
  1423. }
  1424. $startOffset = $end + 2;
  1425. }
  1426. $wanted_re = $temp;
  1427. $wanted_re = str_replace(
  1428. array('%binary_string_optional%'),
  1429. 'string',
  1430. $wanted_re
  1431. );
  1432. $wanted_re = str_replace(
  1433. array('%unicode_string_optional%'),
  1434. 'string',
  1435. $wanted_re
  1436. );
  1437. $wanted_re = str_replace(
  1438. array('%unicode\|string%', '%string\|unicode%'),
  1439. 'string',
  1440. $wanted_re
  1441. );
  1442. $wanted_re = str_replace(
  1443. array('%u\|b%', '%b\|u%'),
  1444. '',
  1445. $wanted_re
  1446. );
  1447. // Stick to basics.
  1448. $wanted_re = str_replace('%e', '\\' . DIRECTORY_SEPARATOR, $wanted_re);
  1449. $wanted_re = str_replace('%s', '[^\r\n]+', $wanted_re);
  1450. $wanted_re = str_replace('%S', '[^\r\n]*', $wanted_re);
  1451. $wanted_re = str_replace('%a', '.+', $wanted_re);
  1452. $wanted_re = str_replace('%A', '.*', $wanted_re);
  1453. $wanted_re = str_replace('%w', '\s*', $wanted_re);
  1454. $wanted_re = str_replace('%i', '[+-]?\d+', $wanted_re);
  1455. $wanted_re = str_replace('%d', '\d+', $wanted_re);
  1456. $wanted_re = str_replace('%x', '[0-9a-fA-F]+', $wanted_re);
  1457. // %f allows two points "-.0.0" but that is the best *simple* expression.
  1458. $wanted_re = str_replace('%f', '[+-]?\.?\d+\.?\d*(?:[Ee][+-]?\d+)?',
  1459. $wanted_re);
  1460. $wanted_re = str_replace('%c', '.', $wanted_re);
  1461. // must be last.
  1462. $wanted_re = str_replace('%%', '%%?', $wanted_re);
  1463. // Normalize newlines.
  1464. $wanted_re = preg_replace("/(\r\n?|\n)/", "\n", $wanted_re);
  1465. $output = preg_replace("/(\r\n?|\n)/", "\n", $output);
  1466. } else if (!$is_tc && $type === 'expectregex') {
  1467. $wanted_re = trim(file_get_contents($file));
  1468. } else {
  1469. throw new Exception("Unsupported expect file type: ".$type);
  1470. }
  1471. if ($repeats) {
  1472. $wanted_re = "($wanted_re\s*)".'{'.$repeats.'}';
  1473. }
  1474. if (!isset($wanted)) $wanted = $wanted_re;
  1475. $passed = @preg_match("/^$wanted_re\$/s", $output);
  1476. if ($passed === false && $repeats) {
  1477. // $repeats can cause the regex to become too big, and fail
  1478. // to compile.
  1479. return 'skip';
  1480. }
  1481. if (!$passed) {
  1482. file_put_contents("$test.diff",
  1483. generate_diff($wanted_re, $wanted_re, $output));
  1484. }
  1485. return $passed;
  1486. }
  1487. function run_one_config($options, $test, $cmd, $cmd_env) {
  1488. $outputs = run_config_cli($options, $test, $cmd, $cmd_env);
  1489. if ($outputs === false) return false;
  1490. return run_config_post($outputs, $test, $options);
  1491. }
  1492. function run_and_lock_test($options, $test) {
  1493. $lock = fopen($test, 'r');
  1494. if (!flock($lock, LOCK_EX)) return false;
  1495. if (isset($options['typechecker'])) {
  1496. $result = run_typechecker_test($options, $test);
  1497. } else {
  1498. $result = run_test($options, $test);
  1499. }
  1500. if (!flock($lock, LOCK_UN)) return false;
  1501. fclose($lock);
  1502. return $result;
  1503. }
  1504. function run_typechecker_test($options, $test) {
  1505. if (skip_test($options, $test)) return 'skip';
  1506. if (!file_exists($test . ".hhconfig")) return 'skip';
  1507. list($hh_server, $hh_server_env, $temp_dir) = hh_server_cmd($options, $test);
  1508. if (is_executable('/usr/bin/timeout')) {
  1509. $hh_server = '/usr/bin/timeout 300 ' . $hh_server;
  1510. } else {
  1511. $hh_server = __DIR__.'/../tools/timeout.sh -t 300 '. $hh_server;
  1512. }
  1513. $result = run_one_config($options, $test, $hh_server, $hh_server_env);
  1514. // Remove the temporary directory.
  1515. if (!isset($options['no-clean'])) {
  1516. shell_exec('rm -rf ' . $temp_dir);
  1517. }
  1518. return $result;
  1519. }
  1520. function run_test($options, $test) {
  1521. if (skip_test($options, $test)) return 'skip';
  1522. $test_ext = pathinfo($test, PATHINFO_EXTENSION);
  1523. list($hhvm, $hhvm_env) = hhvm_cmd($options, $test);
  1524. if ((isset($options['relocate']) || isset($options['recycle-tc'])) &&
  1525. preg_match('/ --count[ =].* --count[ =]/', $hhvm)) {
  1526. return 'skip';
  1527. }
  1528. if (is_executable('/usr/bin/timeout')) {
  1529. $hhvm = '/usr/bin/timeout 300 '.$hhvm;
  1530. } else {
  1531. $hhvm = __DIR__.'/../tools/timeout.sh -t 300 '.$hhvm;
  1532. }
  1533. if (isset($options['repo'])) {
  1534. if (strpos($hhvm, '-m debug') !== false ||
  1535. file_exists($test.'.norepo')) {
  1536. return 'skip-norepo';
  1537. }
  1538. $hphp_repo = "$test.repo/hhvm.hhbc";
  1539. $hhbbc_repo = "$test.repo/hhvm.hhbbc";
  1540. shell_exec("rm -f \"$hphp_repo\" \"$hhbbc_repo\" ");
  1541. $hphp = hphp_cmd($options, $test);
  1542. $hhbbc = hhbbc_cmd($options, $test);
  1543. shell_exec("$hphp 2>&1");
  1544. shell_exec("$hhbbc 2>&1");
  1545. return run_one_config($options, $test, $hhvm, $hhvm_env);
  1546. }
  1547. if (file_exists($test.'.onlyrepo')) {
  1548. return 'skip-onlyrepo';
  1549. }
  1550. if (isset($options['hhas-round-trip'])) {
  1551. $hhas_temp = dump_hhas_to_temp($hhvm, $test);
  1552. list($hhvm, $hhvm_env) = hhvm_cmd($options, $hhas_temp);
  1553. }
  1554. if ($outputs = run_config_server($options, $test)) {
  1555. return run_config_post($outputs, $test, $options) ? 'pass-server'
  1556. : (run_one_config($options, $test, $hhvm, $hhvm_env) ? 'skip-server'
  1557. : false);
  1558. }
  1559. return run_one_config($options, $test, $hhvm, $hhvm_env);
  1560. }
  1561. function num_cpus() {
  1562. switch(PHP_OS) {
  1563. case 'Linux':
  1564. $data = file('/proc/stat');
  1565. $cores = 0;
  1566. foreach($data as $line) {
  1567. if (preg_match('/^cpu[0-9]/', $line)) {
  1568. $cores++;
  1569. }
  1570. }
  1571. return $cores;
  1572. case 'Darwin':
  1573. case 'FreeBSD':
  1574. return exec('sysctl -n hw.ncpu');
  1575. }
  1576. return 2; // default when we don't know how to detect.
  1577. }
  1578. function make_header($str) {
  1579. return "\n\033[0;33m".$str."\033[0m\n";
  1580. }
  1581. function print_commands($tests, $options) {
  1582. print make_header("Run these by hand:");
  1583. foreach ($tests as $test) {
  1584. if (isset($options['typechecker'])) {
  1585. list($command, $_, ) = hh_server_cmd($options, $test);
  1586. } else {
  1587. list($command, $_) = hhvm_cmd($options, $test);
  1588. }
  1589. if (!isset($options['repo'])) {
  1590. print "$command\n";
  1591. continue;
  1592. }
  1593. // How to run it with hhbbc:
  1594. $hhbbc_cmds = hphp_cmd($options, $test)."\n";
  1595. $hhbbc_cmds .= hhbbc_cmd($options, $test)."\n";
  1596. $hhbbc_cmds .= $command."\n";
  1597. print "$hhbbc_cmds\n";
  1598. }
  1599. }
  1600. function msg_loop($num_tests, $queue) {
  1601. $passed = 0;
  1602. $skipped = 0;
  1603. $failed = 0;
  1604. $do_progress = Status::getMode() == Status::MODE_NORMAL &&
  1605. Status::hasCursorControl();
  1606. if ($do_progress) {
  1607. $stty = strtolower(Status::getSTTY());
  1608. preg_match_all("/columns ([0-9]+);/", $stty, $output);
  1609. if (!isset($output[1][0])) {
  1610. // because BSD has to be different
  1611. preg_match_all("/([0-9]+) columns;/", $stty, $output);
  1612. }
  1613. if (!isset($output[1][0])) {
  1614. $do_progress = false;
  1615. } else {
  1616. $cols = $output[1][0];
  1617. }
  1618. }
  1619. while (true) {
  1620. if (!msg_receive($queue, 0, $type, 1024, $message)) {
  1621. error("msg_receive failed");
  1622. }
  1623. switch ($type) {
  1624. case Status::MSG_STARTED:
  1625. break;
  1626. case Status::MSG_FINISHED:
  1627. break 2;
  1628. case Status::MSG_SERVER_RESTARTED:
  1629. switch (Status::getMode()) {
  1630. case Status::MODE_NORMAL:
  1631. if (!Status::hasCursorControl()) {
  1632. Status::sayColor(Status::RED, 'x');
  1633. }
  1634. break;
  1635. case Status::MODE_VERBOSE:
  1636. Status::sayColor("$test ", Status::YELLOW, "failed",
  1637. " to talk to server\n");
  1638. break;
  1639. case Status::MODE_FBMAKE:
  1640. break;
  1641. case Status::MODE_TESTPILOT:
  1642. break;
  1643. }
  1644. case Status::MSG_TEST_PASS:
  1645. $passed++;
  1646. list($test, $how, $time, $stime, $etime) = $message;
  1647. switch (Status::getMode()) {
  1648. case Status::MODE_NORMAL:
  1649. if (!Status::hasCursorControl()) {
  1650. if ($how == Status::SKIP_SERVER) {
  1651. Status::sayColor(Status::RED, '.');
  1652. } else {
  1653. Status::sayColor(Status::GREEN,
  1654. $how == Status::PASS_SERVER ? ',' : '.');
  1655. }
  1656. }
  1657. break;
  1658. case Status::MODE_VERBOSE:
  1659. Status::sayColor("$test ", Status::GREEN,
  1660. sprintf("passed (%.2fs)\n", $time));
  1661. break;
  1662. case Status::MODE_FBMAKE:
  1663. Status::sayFBMake($test, 'passed', $stime, $etime);
  1664. break;
  1665. case Status::MODE_TESTPILOT:
  1666. Status::sayFBMake($test, 'passed', $stime, $etime);
  1667. break;
  1668. }
  1669. break;
  1670. case Status::MSG_TEST_SKIP:
  1671. $skipped++;
  1672. list($test, $reason, $time, $stime, $etime) = $message;
  1673. switch (Status::getMode()) {
  1674. case Status::MODE_NORMAL:
  1675. if (!Status::hasCursorControl()) {
  1676. Status::sayColor(Status::YELLOW, 's');
  1677. }
  1678. break;
  1679. case Status::MODE_VERBOSE:
  1680. Status::sayColor("$test ", Status::YELLOW, "skipped");
  1681. if ($reason !== null) {
  1682. Status::sayColor(" - $reason");
  1683. }
  1684. Status::sayColor(sprintf(" (%.2fs)\n", $time));
  1685. break;
  1686. case Status::MODE_FBMAKE:
  1687. /* Intentionally discard result */
  1688. break;
  1689. case Status::MODE_TESTPILOT:
  1690. Status::sayFBMake($test, 'not_relevant', $stime, $etime);
  1691. break;
  1692. }
  1693. break;
  1694. case Status::MSG_TEST_FAIL:
  1695. $failed++;
  1696. list($test, $time, $stime, $etime) = $message;
  1697. switch (Status::getMode()) {
  1698. case Status::MODE_NORMAL:
  1699. if (Status::hasCursorControl()) {
  1700. print "\033[2K\033[1G";
  1701. }
  1702. $diff = (string)@file_get_contents($test.'.diff');
  1703. Status::sayColor(Status::RED, "\nFAILED",
  1704. ": $test\n$diff\n");
  1705. break;
  1706. case Status::MODE_VERBOSE:
  1707. Status::sayColor("$test ", Status::RED,
  1708. sprintf("FAILED (%.2fs)\n", $time));
  1709. break;
  1710. case Status::MODE_FBMAKE:
  1711. Status::sayFBMake($test, 'failed', $stime, $etime);
  1712. break;
  1713. case Status::MODE_TESTPILOT:
  1714. Status::sayFBMake($test, 'failed', $stime, $etime);
  1715. break;
  1716. }
  1717. break;
  1718. default:
  1719. error("Unknown message $type");
  1720. }
  1721. if ($do_progress) {
  1722. $total_run = ($skipped + $failed + $passed);
  1723. $bar_cols = ($cols - 45);
  1724. $passed_ticks = round($bar_cols * ($passed / $num_tests));
  1725. $skipped_ticks = round($bar_cols * ($skipped / $num_tests));
  1726. $failed_ticks = round($bar_cols * ($failed / $num_tests));
  1727. $fill = $bar_cols - ($passed_ticks + $skipped_ticks + $failed_ticks);
  1728. if ($fill < 0) $fill = 0;
  1729. $fill = str_repeat('-', $fill);
  1730. $passed_ticks = str_repeat('#', $passed_ticks);
  1731. $skipped_ticks = str_repeat('#', $skipped_ticks);
  1732. $failed_ticks = str_repeat('#', $failed_ticks);
  1733. print "\033[2K\033[1G[".
  1734. "\033[0;32m$passed_ticks".
  1735. "\033[33m$skipped_ticks".
  1736. "\033[31m$failed_ticks".
  1737. "\033[0m$fill] ($total_run/$num_tests) ".
  1738. "($skipped skipped, $failed failed)";
  1739. }
  1740. }
  1741. if ($do_progress) {
  1742. print "\033[2K\033[1G";
  1743. if ($skipped > 0) {
  1744. print "$skipped tests \033[1;33mskipped\033[0m\n";
  1745. }
  1746. }
  1747. }
  1748. function print_success($tests, $results, $options) {
  1749. // We didn't run any tests, not even skipped. Clowntown!
  1750. if (!$tests) {
  1751. print "\nCLOWNTOWN: No tests!\n";
  1752. if (empty($options['no-fun'])) {
  1753. print <<<CLOWN
  1754. _
  1755. {_}
  1756. /*\\
  1757. /_*_\\
  1758. {('o')}
  1759. C{{([^*^])}}D
  1760. [ * ]
  1761. / Y \\
  1762. _\\__|__/_
  1763. (___/ \\___)
  1764. CLOWN
  1765. ."\n\n";
  1766. }
  1767. /* Emacs' syntax highlighting gets confused by that clown and this comment
  1768. * resets whatever state got messed up. */
  1769. return;
  1770. }
  1771. $ran_tests = false;
  1772. foreach ($results as $result) {
  1773. // The result here will either be skipped or passed (since failed is
  1774. // handled in print_failure.
  1775. if ($result['status'] == 'passed') {
  1776. $ran_tests = true;
  1777. break;
  1778. }
  1779. }
  1780. // We just had skipped tests
  1781. if (!$ran_tests) {
  1782. print "\nSKIP-ALOO: Only skipped tests!\n";
  1783. if (empty($options['no-fun'])) {
  1784. print <<<SKIPPER
  1785. .".
  1786. / |
  1787. / /
  1788. / ,"
  1789. .-------.--- /
  1790. "._ __.-/ o. o\
  1791. " ( Y )
  1792. ) /
  1793. / (
  1794. / Y
  1795. .-" |
  1796. / _ \ \
  1797. / `. ". ) /' )
  1798. Y )( / /(,/
  1799. ,| / )
  1800. ( | / /
  1801. " \_ (__ (__
  1802. "-._,)--._,)
  1803. SKIPPER
  1804. ."\n\n";
  1805. }
  1806. /* Emacs' syntax highlighting may get confused by the skipper and this
  1807. * rcomment esets whatever state got messed up. */
  1808. return;
  1809. }
  1810. print "\nAll tests passed.\n";
  1811. if (empty($options['no-fun'])) {
  1812. print <<<SHIP
  1813. | | |
  1814. )_) )_) )_)
  1815. )___))___))___)\
  1816. )____)____)_____)\\
  1817. _____|____|____|____\\\__
  1818. ---------\ SHIP IT /---------
  1819. ^^^^^ ^^^^^^^^^^^^^^^^^^^^^
  1820. ^^^^ ^^^^ ^^^ ^^
  1821. ^^^^ ^^^
  1822. SHIP
  1823. ."\n";
  1824. }
  1825. if (isset($options['verbose'])) {
  1826. print_commands($tests, $options);
  1827. }
  1828. }
  1829. function print_failure($argv, $results, $options) {
  1830. $failed = array();
  1831. $passed = array();
  1832. foreach ($results as $result) {
  1833. if ($result['status'] === 'failed') {
  1834. $failed[] = $result['name'];
  1835. }
  1836. if ($result['status'] === 'passed') {
  1837. $passed[] = $result['name'];
  1838. }
  1839. }
  1840. asort($failed);
  1841. print "\n".count($failed)." tests failed\n";
  1842. if (empty($options['no-fun'])) {
  1843. print "(╯°□°)╯︵ ┻━┻\n";
  1844. }
  1845. print make_header("See the diffs:").
  1846. implode("\n", array_map(
  1847. function($test) { return 'cat '.$test.'.diff'; },
  1848. $failed))."\n";
  1849. $failing_tests_file = !empty($options['failure-file'])
  1850. ? $options['failure-file']
  1851. : tempnam('/tmp', 'test-failures');
  1852. file_put_contents($failing_tests_file, implode("\n", $failed)."\n");
  1853. print make_header('For xargs, list of failures is available using:').
  1854. 'cat '.$failing_tests_file."\n";
  1855. if (!empty($passed)) {
  1856. $passing_tests_file = !empty($options['success-file'])
  1857. ? $options['success-file']
  1858. : tempnam('/tmp', 'tests-passed');
  1859. file_put_contents($passing_tests_file, implode("\n", $passed)."\n");
  1860. print make_header('For xargs, list of passed tests is available using:').
  1861. 'cat '.$passing_tests_file."\n";
  1862. }
  1863. print_commands($failed, $options);
  1864. $rerun = make_header("Re-run just the failing tests:") . $argv[0];
  1865. foreach ($options as $option => $value) {
  1866. if ($option === "threads") {
  1867. // e.g., For a small number of failed tests, we don't need max threads.
  1868. $rerun .= " --" . $option . ' ' . get_num_threads($options, $failed);
  1869. } else if ($option === "typechecker" ||
  1870. $option === "verbose" ||
  1871. $option === "repo") {
  1872. // The escapeshellarg($value) of these is 1, but there is no real value
  1873. // associated with these two options.
  1874. $rerun .= " --" . $option;
  1875. } else {
  1876. $rerun .= " --" . $option . ' ' . escapeshellarg($value);
  1877. }
  1878. }
  1879. $rerun .= ' ' . implode(' ', array_map('escapeshellarg', $failed)) . "\n";
  1880. print $rerun;
  1881. }
  1882. function port_is_listening($port) {
  1883. $socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
  1884. return @socket_connect($socket, 'localhost', $port);
  1885. }
  1886. function find_open_port() {
  1887. for ($i = 0; $i < 50; ++$i) {
  1888. $port = rand(1024, 65535);
  1889. if (!port_is_listening($port)) return $port;
  1890. }
  1891. error("Couldn't find an open port");
  1892. }
  1893. function start_server_proc($options, $config, $port) {
  1894. $threads = $options['threads'];
  1895. $command = hhvm_cmd_impl(
  1896. $options,
  1897. $config,
  1898. '-m', 'server',
  1899. "-vServer.Port=$port",
  1900. "-vServer.Type=proxygen",
  1901. "-vAdminServer.Port=0",
  1902. "-vServer.ThreadCount=$threads",
  1903. '-vServer.ExitOnBindFail=1',
  1904. '-vServer.RequestTimeoutSeconds='.SERVER_TIMEOUT,
  1905. '-vPageletServer.ThreadCount=0',
  1906. '-vLog.UseRequestLog=1',
  1907. '-vLog.File=/dev/null',
  1908. // This ensures we actually jit everything:
  1909. '-vEval.JitRequireWriteLease=1',
  1910. // The default test config uses a small TC but we'll be running thousands
  1911. // of tests against the same process:
  1912. '-vEval.JitASize=100000000',
  1913. '-vEval.JitGlobalDataSize=32000000'
  1914. );
  1915. if (getenv('HHVM_TEST_SERVER_LOG')) {
  1916. echo "Starting server '$command'\n";
  1917. }
  1918. $descriptors = array(
  1919. 0 => array('file', '/dev/null', 'r'),
  1920. 1 => array('file', '/dev/null', 'w'),
  1921. 2 => array('file', '/dev/null', 'w'),
  1922. );
  1923. $proc = proc_open($command, $descriptors, $dummy);
  1924. if (!$proc) {
  1925. error("Failed to start server process");
  1926. }
  1927. $status = proc_get_status($proc);
  1928. $status['proc'] = $proc;
  1929. $status['port'] = $port;
  1930. $status['config'] = $config;
  1931. return $status;
  1932. }
  1933. /*
  1934. * For each config file in $configs, start up a server on a randomly-determined
  1935. * port. Return value is an array mapping pids and config files to arrays of
  1936. * information about the server.
  1937. */
  1938. function start_servers($options, $configs) {
  1939. $starting = array();
  1940. foreach ($configs as $config) {
  1941. $starting[] = start_server_proc($options, $config, find_open_port());
  1942. }
  1943. $start_time = microtime(true);
  1944. $servers = array('pids' => array(), 'configs' => array());
  1945. // Wait for all servers to come up.
  1946. while (count($starting) > 0) {
  1947. $still_starting = array();
  1948. foreach ($starting as $server) {
  1949. $new_status = proc_get_status($server['proc']);
  1950. if (!$new_status['running']) {
  1951. if ($new_status['exitcode'] === 0) {
  1952. error("Server exited prematurely but without error");
  1953. }
  1954. // We lost a race. Try another port.
  1955. if (getenv('HHVM_TEST_SERVER_LOG')) {
  1956. echo "\n\nLost connection race on port $port. Trying another.\n\n";
  1957. }
  1958. $still_starting[] =
  1959. start_server_proc($options, $server['config'], find_open_port());
  1960. } else if (!port_is_listening($server['port'])) {
  1961. $still_starting[] = $server;
  1962. } else {
  1963. $servers['pids'][$server['pid']] =& $server;
  1964. $servers['configs'][$server['config']] =& $server;
  1965. unset($server);
  1966. }
  1967. }
  1968. $starting = $still_starting;
  1969. $max_time = 10;
  1970. if (microtime(true) - $start_time > $max_time) {
  1971. error("Servers took more than $max_time seconds to come up");
  1972. }
  1973. // Take a short nap and try again.
  1974. usleep(100000);
  1975. }
  1976. $elapsed = microtime(true) - $start_time;
  1977. printf("Started %d servers in %.1f seconds\n\n", count($configs), $elapsed);
  1978. return $servers;
  1979. }
  1980. function drain_queue($queue) {
  1981. while (@msg_receive($queue, 0, $type, 1024, $message, true,
  1982. MSG_IPC_NOWAIT | MSG_NOERROR));
  1983. }
  1984. function get_num_threads($options, $tests) {
  1985. $cpus = isset($options['server']) ? num_cpus() * 2 : num_cpus();
  1986. return min(count($tests), idx($options, 'threads', $cpus));
  1987. }
  1988. function runner_precheck() {
  1989. // basic checking for runner.
  1990. if (empty($_SERVER) || empty($_ENV)) {
  1991. echo "Warning: \$_SERVER/\$_ENV variables not available, please check \n" .
  1992. "your ini setting: variables_order, it should have both 'E' and 'S'\n";
  1993. }
  1994. }
  1995. function main($argv) {
  1996. runner_precheck();
  1997. ini_set('pcre.backtrack_limit', PHP_INT_MAX);
  1998. list($options, $files) = get_options($argv);
  1999. if (isset($options['help'])) {
  2000. error(help());
  2001. }
  2002. if (isset($options['list-tests'])) {
  2003. error(list_tests($files, $options));
  2004. }
  2005. $tests = find_tests($files, $options);
  2006. if (isset($options['shuffle'])) {
  2007. shuffle($tests);
  2008. }
  2009. if (isset($options['repo']) && isset($options['typechecker'])) {
  2010. error("Repo mode and typechecker mode are not compatible");
  2011. }
  2012. if (isset($options['hhvm-binary-path']) &&
  2013. isset($options['typechecker'])) {
  2014. error("Did you mean to set the hh_server binary path instead?");
  2015. }
  2016. if (isset($options['hhserver-binary-path']) &&
  2017. !isset($options['typechecker'])) {
  2018. error("hh_server binary path set, but not --typechecker");
  2019. }
  2020. if (isset($options['hhvm-binary-path']) &&
  2021. isset($options['hhserver-binary-path'])) {
  2022. error("Need to choose one of the two binaries to run");
  2023. }
  2024. $binary_path = "";
  2025. $typechecker = false;
  2026. if (isset($options['hhvm-binary-path'])) {
  2027. check_executable($options['hhvm-binary-path'], false);
  2028. $binary_path = realpath($options['hhvm-binary-path']);
  2029. putenv("HHVM_BIN=" . $binary_path);
  2030. } else if (isset($options['hhserver-binary-path'])) {
  2031. check_executable($options['hhserver-binary-path'], true);
  2032. $binary_path = realpath($options['hhserver-binary-path']);
  2033. $typechecker = true;
  2034. putenv("HH_SERVER_BIN=" . $binary_path);
  2035. } else if (isset($options['typechecker'])) {
  2036. $typechecker = true;
  2037. }
  2038. // Explicit path given by --hhvm-binary-path or --hhserver-binary-path
  2039. // takes priority (see above)
  2040. // Then, if an HHVM_BIN or HH_SERVER env var exists, and the file it
  2041. // points to exists, that trumps any default hhvm / typechecker executable
  2042. // path.
  2043. if ($binary_path === "") {
  2044. if (!$typechecker) {
  2045. if (getenv("HHVM_BIN") !== false) {
  2046. $binary_path = realpath(getenv("HHVM_BIN"));
  2047. check_executable($binary_path, false);
  2048. } else {
  2049. check_for_multiple_default_binaries(false);
  2050. $binary_path = hhvm_path();
  2051. }
  2052. } else {
  2053. if (getenv("HH_SERVER_BIN") !== false) {
  2054. $binary_path = realpath(getenv("HH_SERVER_BIN"));
  2055. check_executable($binary_path, true);
  2056. } else {
  2057. check_for_multiple_default_binaries(true);
  2058. $binary_path = hh_server_path();
  2059. }
  2060. }
  2061. }
  2062. if (isset($options['verbose'])) {
  2063. print "You are using the binary located at: " . $binary_path . "\n";
  2064. }
  2065. $options['threads'] = get_num_threads($options, $tests);
  2066. $servers = null;
  2067. if (isset($options['server'])) {
  2068. if (isset($options['repo']) || isset($options['typechecker'])) {
  2069. error("Server mode repo tests are not supported");
  2070. }
  2071. $configs = array();
  2072. /* We need to start up a separate server process for each config file
  2073. * found. */
  2074. foreach ($tests as $test) {
  2075. if (!can_run_server_test($test)) continue;
  2076. $config = find_file_for_dir(dirname($test), 'config.ini');
  2077. if (!$config) {
  2078. error("Couldn't find config file for $test");
  2079. }
  2080. $configs[$config] = $config;
  2081. }
  2082. $max_configs = 20;
  2083. if (count($configs) > $max_configs) {
  2084. error("More than $max_configs unique config files will be needed to run ".
  2085. "the tests you specified. They may not be a good fit for server ".
  2086. "mode.");
  2087. }
  2088. $servers = $options['servers'] = start_servers($options, $configs);
  2089. }
  2090. // Try to construct the buckets so the test results are ready in
  2091. // approximately alphabetical order.
  2092. $test_buckets = array();
  2093. $i = 0;
  2094. // Get the serial tests to be in their own bucket later.
  2095. $serial_tests = serial_only_tests($tests);
  2096. // If we have no serial tests, we can use the maximum number of allowed
  2097. // threads for the test running. If we have some, we save one thread for
  2098. // the serial bucket.
  2099. $parallel_threads = count($serial_tests) > 0
  2100. ? $options['threads'] - 1
  2101. : $options['threads'];
  2102. foreach ($tests as $test) {
  2103. if (!in_array($test, $serial_tests)) {
  2104. $test_buckets[$i][] = $test;
  2105. $i = ($i + 1) % $parallel_threads;
  2106. }
  2107. }
  2108. if (count($serial_tests) > 0) {
  2109. // The last bucket is serial.
  2110. // If the number of parallel tests didn't equal the actual number of
  2111. // parallel threads because the number of serial tests reduced it enough,
  2112. // then our next bucket is just $i; otherwise it is final available
  2113. // thread. For example, we have 13 total tests which initially gave us
  2114. // 13 parallel threads, but then we find out 3 are serial, so the parallel
  2115. // tests would only fill 9 parallel buckets (< 12). The next one would be
  2116. // 10 for the 3 serial. Now if we have 40 total tests which gave us 32
  2117. // parallel threads and 4 serial tests, then all of possible parallel
  2118. // buckets (31) would be filled regardless; so the serial bucket is what
  2119. // would have been the last parallel thread (32).
  2120. // $i got bumped to the next bucket at the end of the parallel test loop
  2121. // above, so no $i++ here.
  2122. $i = count($tests) - count($serial_tests) < $parallel_threads
  2123. ? $i // we didn't fill all the parallel buckets, so use next one in line
  2124. : $options['threads'] - 1; // all parallel filled; last thread; 0 indexed
  2125. foreach ($serial_tests as $test) {
  2126. $test_buckets[$i][] = $test;
  2127. }
  2128. }
  2129. // If our total number of test buckets didn't overflow back to 0 above
  2130. // when we % against the number of threads (because we didn't have that
  2131. // many tests for this run), then just set the threads to how many
  2132. // buckets we actually have to make calculations below correct.
  2133. if (count($test_buckets) < $options['threads']) {
  2134. $options['threads'] = count($test_buckets);
  2135. }
  2136. // Remember that the serial tests are also in the tests array too,
  2137. // so they are part of the total count.
  2138. if (!isset($options['fbmake']) && !isset($options['testpilot'])) {
  2139. print "Running ".count($tests)." tests in ".
  2140. $options['threads']." threads (" . count($serial_tests) .
  2141. " in serial)\n";
  2142. }
  2143. if (isset($options['verbose'])) {
  2144. Status::setMode(Status::MODE_VERBOSE);
  2145. }
  2146. if (isset($options['fbmake'])) {
  2147. Status::setMode(Status::MODE_FBMAKE);
  2148. }
  2149. if (isset($options['testpilot'])) {
  2150. Status::setMode(Status::MODE_TESTPILOT);
  2151. }
  2152. Status::setUseColor(isset($options['color']) ? true : posix_isatty(STDOUT));
  2153. Status::$key = rand();
  2154. $queue = Status::getQueue();
  2155. drain_queue($queue);
  2156. Status::started();
  2157. // Spawn off worker threads.
  2158. $children = array();
  2159. // A poor man's shared memory.
  2160. $bad_test_files = array();
  2161. for ($i = 0; $i < $options['threads']; $i++) {
  2162. $bad_test_file = tempnam('/tmp', 'test-run-');
  2163. $bad_test_files[] = $bad_test_file;
  2164. $pid = pcntl_fork();
  2165. if ($pid == -1) {
  2166. error('could not fork');
  2167. } else if ($pid) {
  2168. $children[$pid] = $pid;
  2169. } else {
  2170. exit(run($options, $test_buckets[$i], $bad_test_file));
  2171. }
  2172. }
  2173. // Fork off a child to receive messages and print status, and have the parent
  2174. // wait for all children to exit.
  2175. $printer_pid = pcntl_fork();
  2176. if ($printer_pid == -1) {
  2177. error("failed to fork");
  2178. } else if ($printer_pid == 0) {
  2179. msg_loop(count($tests), $queue);
  2180. return 0;
  2181. }
  2182. // In case we exit in a crazy way, have the parent blow up the queue.
  2183. // Do this here so no children inherit this.
  2184. $kill_queue = function() { Status::killQueue(); };
  2185. register_shutdown_function($kill_queue);
  2186. pcntl_signal(SIGTERM, $kill_queue);
  2187. pcntl_signal(SIGINT, $kill_queue);
  2188. $return_value = 0;
  2189. while (count($children) && $printer_pid != 0) {
  2190. $pid = pcntl_wait($status);
  2191. if (!pcntl_wifexited($status) && !pcntl_wifsignaled($status)) {
  2192. error("Unexpected exit status from child");
  2193. }
  2194. if ($pid == $printer_pid) {
  2195. // We should be finishing up soon.
  2196. $printer_pid = 0;
  2197. } else if (isset($servers['pids'][$pid])) {
  2198. // A server crashed. Restart it.
  2199. if (getenv('HHVM_TEST_SERVER_LOG')) {
  2200. echo "\nServer $pid crashed. Restarting.\n";
  2201. }
  2202. Status::serverRestarted();
  2203. $server =& $servers['pids'][$pid];
  2204. $server = start_server_proc($options, $server['config'], $server['port']);
  2205. // Unset the old $pid entry and insert the new one.
  2206. unset($servers['pids'][$pid]);
  2207. $servers['pids'][$server['pid']] =& $server;
  2208. unset($server);
  2209. } elseif (isset($children[$pid])) {
  2210. unset($children[$pid]);
  2211. $return_value |= pcntl_wexitstatus($status);
  2212. } // Else, ignorable signal
  2213. }
  2214. Status::finished();
  2215. // Kill the server.
  2216. if ($servers) {
  2217. foreach ($servers['pids'] as $server) {
  2218. proc_terminate($server['proc']);
  2219. proc_close($server['proc']);
  2220. }
  2221. }
  2222. $results = array();
  2223. foreach ($bad_test_files as $bad_test_file) {
  2224. $json = json_decode(file_get_contents($bad_test_file), true);
  2225. if (!is_array($json)) {
  2226. error(
  2227. "\nNo JSON output was received from a test thread. ".
  2228. "Either you killed it, or it might be a bug in the test script."
  2229. );
  2230. }
  2231. $results = array_merge($results, $json);
  2232. unlink($bad_test_file);
  2233. }
  2234. if (isset($options['fbmake']) || isset($options['testpilot'])) {
  2235. Status::say(array('op' => 'all_done', 'results' => $results));
  2236. } else if (!$return_value) {
  2237. print_success($tests, $results, $options);
  2238. } else {
  2239. print_failure($argv, $results, $options);
  2240. }
  2241. if (!isset($options['fbmake'])) {
  2242. Status::sayColor("\nTotal time for all executed tests as run: ",
  2243. Status::BLUE,
  2244. sprintf("%.2fs\n",
  2245. Status::getOverallEndTime() -
  2246. Status::getOverallStartTime()));
  2247. Status::sayColor("Total time for all executed tests if run serially: ",
  2248. Status::BLUE,
  2249. sprintf("%.2fs\n",
  2250. Status::addTestTimesSerial()));
  2251. }
  2252. return $return_value;
  2253. }
  2254. exit(main($argv));