PageRenderTime 48ms CodeModel.GetById 18ms RepoModel.GetById 0ms app.codeStats 0ms

/hphp/test/frameworks/utils.php

http://github.com/facebook/hiphop-php
PHP | 464 lines | 383 code | 34 blank | 47 comment | 34 complexity | afaf8099edc83115f8baf5dfeceeaade 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. <?hh
  2. require_once __DIR__.'/SortedIterator.php';
  3. require_once __DIR__.'/Options.php';
  4. class TimeoutException extends Exception {
  5. }
  6. # There's an outer timeout of 300s; this number must be less than that (with
  7. # fudge factor)
  8. const INSTALL_TIMEOUT_SECS = 240;
  9. const NETWORK_RETRIES = 1;
  10. // For determining number of processes
  11. function num_cpus() {
  12. switch(PHP_OS) {
  13. case 'Linux':
  14. $data = file('/proc/stat');
  15. $cores = 0;
  16. foreach($data as $line) {
  17. if (preg_match('/^cpu[0-9]/', $line)) {
  18. $cores++;
  19. }
  20. }
  21. return $cores;
  22. case 'Darwin':
  23. case 'FreeBSD':
  24. return exec('sysctl -n hw.ncpu');
  25. }
  26. return 2; // default when we don't know how to detect
  27. }
  28. function remove_dir_recursive(string $root_dir) {
  29. $files = new RecursiveIteratorIterator(
  30. new RecursiveDirectoryIterator(
  31. $root_dir,
  32. RecursiveDirectoryIterator::SKIP_DOTS),
  33. RecursiveIteratorIterator::CHILD_FIRST);
  34. foreach ($files as $fileinfo) {
  35. if ($fileinfo->isDir()) {
  36. rmdir($fileinfo->getPathname());
  37. } else {
  38. unlink($fileinfo->getPathname());
  39. }
  40. }
  41. rmdir($root_dir);
  42. }
  43. function any_dir_empty_one_level(string $dir): bool {
  44. $files = scandir($dir);
  45. // Get rid of any "." and ".." to check
  46. unset($files[array_search(".",$files)]);
  47. unset($files[array_search("..",$files)]);
  48. foreach ($files as $file) {
  49. if (is_dir($dir."/".$file)) {
  50. // Empty dir will have . and ..
  51. if (count(scandir($dir."/".$file)) <= 2) {
  52. return true;
  53. }
  54. }
  55. }
  56. return false;
  57. }
  58. // Start from self and work down tree. Find first occurrence closest to root
  59. // Works on files or directories.
  60. //
  61. // Note: Wanted to use builtin SPL for this, but it seems like the order cannot
  62. // be guaranteed with their iterators. So found and used a sorted iterator class
  63. // and sorted by the full path including file name.
  64. function find_first_file_recursive(Set $filenames, string $root_dir,
  65. bool $just_path_to_file): ?string {
  66. $dit = new RecursiveDirectoryIterator($root_dir,
  67. RecursiveDirectoryIterator::SKIP_DOTS);
  68. $rit = new RecursiveIteratorIterator($dit);
  69. $sit = new SortedIterator($rit);
  70. foreach ($sit as $fileinfo) {
  71. if ($filenames->contains($fileinfo->getFilename())) {
  72. return $just_path_to_file
  73. ? $fileinfo->getPath()
  74. : $fileinfo->getPathname();
  75. }
  76. }
  77. return null;
  78. }
  79. function find_all_files(string $pattern, string $root_dir,
  80. string $exclude_file_pattern,
  81. ?Set<string> $exclude_dirs = null): ?Set<string> {
  82. if (!file_exists($root_dir)) {
  83. return null;
  84. }
  85. $files = Set {};
  86. $dit = new RecursiveDirectoryIterator($root_dir,
  87. RecursiveDirectoryIterator::SKIP_DOTS);
  88. $rit = new RecursiveIteratorIterator($dit);
  89. $sit = new SortedIterator($rit);
  90. foreach ($sit as $fileinfo) {
  91. if (preg_match($pattern, $fileinfo->getFilename()) === 1 &&
  92. preg_match($exclude_file_pattern, $fileinfo->getFilename()) === 0 &&
  93. strstr($fileinfo->getPath(), '/vendor/') === false &&
  94. !nullthrows($exclude_dirs)->contains(dirname($fileinfo->getPath()))) {
  95. $files[] = $fileinfo->getPathname();
  96. }
  97. }
  98. return $files;
  99. }
  100. function find_all_files_containing_text(
  101. string $text,
  102. string $root_dir,
  103. string $exclude_file_pattern,
  104. ?Set<string> $exclude_dirs = null,
  105. ): ?Set<string> {
  106. if (!file_exists($root_dir)) {
  107. return null;
  108. }
  109. $files = Set {};
  110. $dit = new RecursiveDirectoryIterator($root_dir,
  111. RecursiveDirectoryIterator::SKIP_DOTS);
  112. $rit = new RecursiveIteratorIterator($dit);
  113. $sit = new SortedIterator($rit);
  114. foreach ($sit as $fileinfo) {
  115. if (strpos(file_get_contents($fileinfo->getPathname()), $text) !== false &&
  116. preg_match($exclude_file_pattern, $fileinfo->getFilename()) === 0 &&
  117. strstr($fileinfo->getPath(), '/vendor/') === false &&
  118. !nullthrows($exclude_dirs)->contains(dirname($fileinfo->getPath()))) {
  119. $files[] = $fileinfo->getPathname();
  120. }
  121. }
  122. return $files;
  123. }
  124. function command_exists(string $cmd): bool {
  125. $ret = shell_exec("which $cmd");
  126. return !empty($ret);
  127. }
  128. /**
  129. * Print if output format is for humans
  130. */
  131. function human(string $msg): void {
  132. if (
  133. (Options::$output_format === OutputFormat::HUMAN) ||
  134. (Options::$output_format === OutputFormat::HUMAN_VERBOSE)
  135. ) {
  136. print $msg;
  137. }
  138. }
  139. function fbmake_json(Map<string, mixed> $data) {
  140. if (Options::$output_format === OutputFormat::FBMAKE) {
  141. // Yep, really. STDERR. If you put it on STDOUT instead, 'All tests passed.'
  142. fprintf(STDERR, "%s\n", json_encode($data));
  143. }
  144. }
  145. function fbmake_test_name(Framework $framework, string $test) {
  146. return $framework->getName().'/'.$test;
  147. }
  148. function fbmake_result_json(
  149. Framework $framework,
  150. string $test,
  151. string $status
  152. ): Map<string, mixed> {
  153. if (Options::$output_format !== OutputFormat::FBMAKE) {
  154. return Map { };
  155. }
  156. $expected = $framework->getCurrentTestStatuses();
  157. if ($expected && $expected->containsKey($test)) {
  158. $expected = $expected[$test];
  159. if ($expected === $status) {
  160. return Map {
  161. 'status' => 'passed',
  162. 'details' => 'Matched expected status: '.$status,
  163. };
  164. }
  165. return Map {
  166. 'status' => 'failed',
  167. 'details' => 'Expected '.$expected.', got '.$status,
  168. };
  169. }
  170. return Map {
  171. 'status' => 'failed',
  172. 'details' => 'Unknown test - updated expect file needed?',
  173. };
  174. }
  175. /**
  176. * Print output if verbose mode is on. This implies that the output format
  177. * is human-readable.
  178. */
  179. function verbose(string $msg): void {
  180. if (Options::$output_format === OutputFormat::HUMAN_VERBOSE) {
  181. print $msg;
  182. }
  183. }
  184. /**
  185. * Print output if format is human readable, but not not verbose.
  186. */
  187. function not_verbose(string $msg): void {
  188. if (Options::$output_format === OutputFormat::HUMAN) {
  189. print $msg;
  190. }
  191. }
  192. function remove_color_codes(string $line): string {
  193. // Get rid of codes like ^[[31;31m that may get output to the results file.
  194. // 0x1B is the hex code for the escape sequence ^[
  195. $color_escape_code_pattern = "/\x1B\[([0-9]{1,2}(;[0-9]{1,2})?)?[m|K]/";
  196. return preg_replace($color_escape_code_pattern, "", $line);
  197. }
  198. /*
  199. e.g., remove_string_from_text($dir, __DIR__, null);
  200. /data/users/joelm/fbcode/hphp/test/frameworks/frameworks/pear-core/tests
  201. /PEAR_Command_Channels/channel-update/test_remotefile.phpt
  202. becomes
  203. frameworks/pear-core/tests//PEAR_Command_Channels/
  204. channel-update/test_remotefile.phpt
  205. */
  206. function remove_string_from_text(string $text, string $str,
  207. ?string $replace = null): string {
  208. if (($pos = strpos($text, $str)) !== false) {
  209. return $replace === null
  210. ? substr($text, $pos + strlen(__DIR__) + 1)
  211. : substr_replace($text, $replace, $pos, strlen(__DIR__) + 1);
  212. }
  213. return $text;
  214. }
  215. function get_subclasses_of(string $parent): Vector {
  216. $result = Vector {};
  217. foreach (get_declared_classes() as $class) {
  218. if (is_subclass_of($class, $parent)) {
  219. $result[] = strtolower($class);
  220. }
  221. }
  222. sort($result);
  223. return $result;
  224. }
  225. function get_runtime_build(bool $use_php = false): string {
  226. $executable = '';
  227. $command = '';
  228. // FIX: Should we try to install a vanilla php binary here instead of
  229. // relying on user to specify a path? Should we try to determine if php
  230. // is already installed via a $PATH variable?
  231. if (Options::$php_path !== null) {
  232. if (!file_exists(Options::$php_path)) {
  233. error_and_exit("PHP build does not exists. Are you sure your path is ".
  234. "right?");
  235. }
  236. $executable = Options::$php_path;
  237. $command = $executable;
  238. } else {
  239. $fbcode_root_dir = __DIR__.'/../../..';
  240. $oss_root_dir = __DIR__.'/../..';
  241. $buck_root_dir = __DIR__.'/../../..';
  242. // See if we are using an internal development build
  243. if ((file_exists($fbcode_root_dir."/_bin"))) {
  244. $executable = $fbcode_root_dir;
  245. $executable .= $use_php ? "/_bin/hphp/hhvm/php" : "/_bin/hphp/hhvm/hhvm";
  246. } else if (file_exists($buck_root_dir."/buck-out/gen")) {
  247. // Maybe we're using a buck build.
  248. $executable = $buck_root_dir;
  249. $executable .= $use_php
  250. ? "/buck-out/gen/hphp/hhvm/symlinks=php/php"
  251. : "/buck-out/gen/hphp/hhvm/hhvm/hhvm";
  252. } else if (file_exists($oss_root_dir."/hhvm")) {
  253. // Maybe we are in OSS land trying this script.
  254. // Pear won't run correctly unless a 'php' executable exists.
  255. // This may be a Pear thing, a PHPUnit running phpt thing, or
  256. // or something else. Until we know for sure, let's just create
  257. // a php symlink to hhvm
  258. if (!file_exists($oss_root_dir.'/hhvm/php')) {
  259. symlink($oss_root_dir."/hhvm/hhvm", $oss_root_dir."/hhvm/php");
  260. }
  261. $executable = $oss_root_dir."/hhvm";
  262. $executable .= $use_php ? "/php" : "/hhvm";
  263. } else {
  264. error_and_exit("HHVM build doesn't exist. Did you build yet?");
  265. }
  266. $command = $executable;
  267. if (!$use_php) {
  268. $repo_loc = tempnam('/tmp', 'framework-test');
  269. $repo_args = " -v Repo.Local.Mode=-- -v Repo.Central.Path=".$repo_loc;
  270. $command .= $repo_args.
  271. " --config ".__DIR__."/php.ini";
  272. }
  273. }
  274. invariant(
  275. file_exists($executable),
  276. $executable.' does not exist'
  277. );
  278. invariant(
  279. is_executable($executable),
  280. $executable.' is not executable'
  281. );
  282. return nullthrows($command);
  283. }
  284. function error_and_exit(
  285. string $message,
  286. string $fbmake_action = 'skipped',
  287. ): void {
  288. if (Options::$output_format === OutputFormat::FBMAKE) {
  289. fprintf(
  290. STDERR,
  291. "%s\n",
  292. json_encode(
  293. [
  294. 'op' => 'test_done',
  295. 'test' => 'framework test setup',
  296. 'status' => $fbmake_action,
  297. 'details' => 'ERROR: '.$message,
  298. ],
  299. /* assoc array = */ true,
  300. )
  301. );
  302. exit(0);
  303. }
  304. fprintf(STDERR, "ERROR: %s\n", $message);
  305. exit(1);
  306. }
  307. // Include all PHP files in a directory
  308. function include_all_php($folder){
  309. foreach (glob("{$folder}/*.php") as $filename) {
  310. require_once $filename;
  311. }
  312. }
  313. // This will run processes that will get the test infra dependencies
  314. // (e.g. PHPUnit), frameworks and framework dependencies.
  315. function run_install(
  316. string $proc,
  317. string $path,
  318. ?Map $env = null,
  319. int $retries = NETWORK_RETRIES
  320. ): ?int {
  321. // We need to output something every once in a while - if we go quiet, fbmake
  322. // kills us.
  323. for ($try = 1; $try <= $retries; ++$try) {
  324. $test_name = $proc.' - attempt '.$try;
  325. try {
  326. fbmake_json(Map {'op' => 'start', 'test' => $test_name});
  327. $result = run_install_impl($proc, $path, $env);
  328. fbmake_json(
  329. Map {'op' => 'test_done', 'test' => $test_name, 'status' => 'passed' }
  330. );
  331. return $result;
  332. } catch (TimeoutException $e) {
  333. verbose((string) $e);
  334. remove_dir_recursive(nullthrows($path));
  335. fbmake_json(
  336. Map {'op' => 'test_done', 'test' => $test_name, 'status' => 'skipped' }
  337. );
  338. }
  339. }
  340. error_and_exit('Retries exceeded: '.$proc);
  341. return null; // unrechable, but make the typechecker happy.
  342. }
  343. function run_install_impl(string $proc, string $path, ?Map $env): ?int
  344. {
  345. verbose("Running: $proc\n");
  346. $descriptorspec = array(
  347. 0 => array("pipe", "r"),
  348. 1 => array("pipe", "w"),
  349. 2 => array("pipe", "w"),
  350. );
  351. $env_arr = null; // $_ENV will passed in by default if this is null
  352. if ($env !== null) {
  353. $env_arr = array_merge($_ENV, $env->toArray());
  354. }
  355. // If you have this set, it probably points to hhvm objects, not OSS
  356. // objects. Misses here seem to be a huge slowdown, causing problems with
  357. // fbmake timeouts.
  358. if (
  359. $env_arr !== null &&
  360. array_key_exists('GIT_ALTERNATE_OBJECT_DIRECTORIES', $env_arr)
  361. ) {
  362. unset($env_arr['GIT_ALTERNATE_OBJECT_DIRECTORIES']);
  363. }
  364. $pipes = null;
  365. $process = proc_open($proc, $descriptorspec, $pipes, $path, $env_arr);
  366. assert($pipes !== null);
  367. if (is_resource($process)) {
  368. fclose($pipes[0]);
  369. $start_time = microtime(true);
  370. $read = [$pipes[1]];
  371. $write = [];
  372. $except = $read;
  373. $ready = null;
  374. $done_by = time() + INSTALL_TIMEOUT_SECS;
  375. while ($done_by > time()) {
  376. $remaining = $done_by - time();
  377. $ready = stream_select(
  378. $read, $write, $except,
  379. $remaining > 0 ? $remaining : 1
  380. );
  381. if ($ready === 0) {
  382. proc_terminate($process);
  383. throw new TimeoutException("Hit timeout reading from proc: ".$proc);
  384. }
  385. if (feof($pipes[1])) {
  386. break;
  387. }
  388. $block = fread($pipes[1], 8096);
  389. verbose($block);
  390. if ((microtime(true) - $start_time) > 1) {
  391. not_verbose('.');
  392. $start_time = microtime(true);
  393. }
  394. }
  395. verbose(stream_get_contents($pipes[2]));
  396. fclose($pipes[1]);
  397. $ret = proc_close($process);
  398. verbose("Returned status $ret\n");
  399. return $ret;
  400. }
  401. verbose("Couldn't proc_open: $proc\n");
  402. return null;
  403. }
  404. function nullthrows<T>(?T $x, ?string $message = null): T {
  405. if ($x !== null) {
  406. return $x;
  407. }
  408. if ($message === null) {
  409. $message = 'Unexpected null';
  410. }
  411. throw new Exception($message);
  412. }
  413. // Use this instead of unlink to avoid warnings
  414. function delete_file(?string $path): void {
  415. if ($path !== null && file_exists($path)) {
  416. unlink($path);
  417. }
  418. }