2 require_once __DIR__
."/Framework.php";
3 require_once __DIR__
.'/Options.php';
4 require_once __DIR__
.'/PHPUnitPatterns.php';
5 require_once __DIR__
.'/Statuses.php';
6 require_once __DIR__
.'/Colors.php';
7 require_once __DIR__
.'/utils.php';
8 require_once __DIR__
.'/ProxyInformation.php';
11 // Name could be the name of the single test file for a given framework,
12 // or the actual framework name if we are running in serial, for example
15 private string $test_information = "";
16 private string $error_information = "";
17 private string $fatal_information = "";
18 private string $diff_information = "";
19 private string $stat_information = "";
20 // Each PHPUnit process gets its' own tempdir to avoid race conditions
21 // between multiple runs
22 private ?
string $temp_dir;
24 private array<resource> $pipes = [];
25 private ?
resource $process = null;
26 private string $actual_test_command = "";
28 public function __construct(public Framework
$framework, string $p = "") {
29 $this->name
= $p === "" ?
$this->framework
->getName() : $p;
32 public function run(): int {
33 chdir($this->framework
->getTestPath());
38 if ($this->initialize()) {
39 while (!(feof($this->pipes
[1]))) {
40 $line = $this->getLine();
44 if ($this->isBlankLine($line)) {
47 // e.g. PHPUnit 3.7.28 by Sebastian Bergmann.
48 // Even if there are three lines of prologue, this will keep
49 // continuing before we call analyzeTest
50 if ($this->isPrologue($line)) {
51 $pretest_data = false;
54 // e.g. \nWarning: HipHop currently does not support circular
55 // reference collection
56 // e.g. No headers testing
57 // e.g. Please install runkit and enable runkit.internal_override!
59 if ($this->checkForWarnings($line)) {
60 $this->error_information
.= "PRETEST WARNING FOR ".
61 $this->name
.PHP_EOL
.$line.PHP_EOL
.
62 $this->getTestRunStr($this->name
, "RUN TEST FILE: ").PHP_EOL
;
66 if ($this->isStop($line)) {
67 // If we have finished the tests, then we are just printing any error
68 // info and getting the final stats
69 $this->printPostTestInfo();
73 // We have gotten through the prologue and any blank lines
74 // and we should be at tests now.
75 $tn_matches = array();
76 if (preg_match($this->framework
->getTestNamePattern(), $line,
77 &$tn_matches) === 1) {
78 // If analyzeTest returns false, then we have most likely
79 // hit a fatal. So we bail the run.
80 if(!$this->analyzeTest($tn_matches[0])) {
83 } else if ($this->checkForWarnings($line)) {
84 // We have a warning after the tests have supposedly started
85 // but we really don't have a test to examine.
87 // PHPUnit 3.7.28 by Sebastian Bergmann.
88 // The Xdebug extension is not loaded. No code coverage will be gen
89 // \nNotice: Use of undefined constant DRIZZLE_CON_NONE
90 $line = remove_string_from_text($line, __DIR__
, "");
91 $this->error_information
.= PHP_EOL
.$line.PHP_EOL
.
92 $this->getTestRunStr($this->name
,"RUN TEST FILE: ").
95 } else if ($this->checkForFatals($line)) {
96 // We have a fatal after the tests have supposedly started
97 // but we really don't have a test to examine.
99 // PHPUnit 3.7.28 by Sebastian Bergmann.
100 // The Xdebug extension is not loaded. No code coverage will be gen
101 // \nFatal error: Call to undefined function mysqli_report
102 $line = remove_string_from_text($line, __DIR__
, "");
103 $this->fatal_information
.= PHP_EOL
.$this->name
.
104 PHP_EOL
.$line.PHP_EOL
.PHP_EOL
.
105 $this->getTestRunStr($this->name
, "RUN TEST FILE: ").
111 $ret_val = $this->finalize();
114 error_and_exit("Could not open process to run test ".$this->name
.
115 " for framework ".$this->framework
->getName());
121 private function analyzeTest(string $test): bool {
122 verbose("Analyzing test: ".$test.PHP_EOL
);
123 // If we hit a fatal or something, we will stop the overall test running
124 // for this particular test sequence
125 $continue_testing = true;
126 // We have the test. Now just get the incoming data unitl we find some
127 // sort of status data
129 $status = $this->getLine();
130 if ($status !== null) {
131 // No user specific information in status. Replace with empty string
132 $status = remove_string_from_text($status, __DIR__
, "");
134 if ($status === null) {
135 $status = Statuses
::UNKNOWN
;
136 $this->fatal_information
.= $test.PHP_EOL
.$status.PHP_EOL
.
137 $this->getTestRunStr($test, "RUN TEST FILE: ").PHP_EOL
.PHP_EOL
;
138 $this->stat_information
= $this->name
.PHP_EOL
.$status.PHP_EOL
;
139 $continue_testing = false;
141 } else if ($status === Statuses
::TIMEOUT
) {
142 $this->fatal_information
.= $test.PHP_EOL
.$status.PHP_EOL
.
143 $this->getTestRunStr($test, "RUN TEST FILE: ").PHP_EOL
.PHP_EOL
;
144 $this->stat_information
= $this->name
.PHP_EOL
.$status.PHP_EOL
;
145 $continue_testing = false;
147 } else if ($this->checkForFatals($status)) {
148 $this->fatal_information
.= $test.PHP_EOL
.$status.PHP_EOL
.PHP_EOL
.
149 $this->getTestRunStr($test, "RUN TEST FILE: ").PHP_EOL
.PHP_EOL
;
150 $status = Statuses
::FATAL
;
151 $this->stat_information
= $this->name
.PHP_EOL
.$status.PHP_EOL
;
152 $continue_testing = false;
154 } else if ($this->checkForWarnings($status)) {
155 // Warnings are special. We may get one or more warnings, but then
156 // a real test status will come afterwards.
157 $this->error_information
.= $test.PHP_EOL
.$status.PHP_EOL
.PHP_EOL
.
158 $this->getTestRunStr($test, "RUN TEST FILE: ").PHP_EOL
.PHP_EOL
;
161 } while (!feof($this->pipes
[1]) &&
162 preg_match(PHPUnitPatterns
::STATUS_CODE_PATTERN
,
164 // Test names should have all characters before and including __DIR__
165 // removed, so that specific user info is not added
166 $test = rtrim($test, PHP_EOL
);
167 $test = remove_string_from_text($test, __DIR__
, null);
168 $this->test_information
.= $test.PHP_EOL
;
169 $this->processStatus(nullthrows($status), $test);
171 return $continue_testing;
174 private function processStatus(string $status, string $test): void
{
175 // May have this if we reached the end of the file or if something
176 // wasn't printed out in optimized mode that may have been printed
178 if ($status === "" ||
$status === null) {
179 $status = Statuses
::UNKNOWN
;
180 } else if ($status !== Statuses
::UNKNOWN
&& $status !== Statuses
::TIMEOUT
&&
181 $status !== Statuses
::FATAL
) {
182 // Otherwise we have, Fail, Error, Incomplete, Skip, Pass (.)
183 // First Char In case we had "F 252 / 364 (69 %)"
184 $status = $status[0];
187 $this->test_information
.= $status.PHP_EOL
;
189 $fbmake_name = fbmake_test_name($this->framework
, $test);
190 fbmake_json(Map
{'op' => 'start', 'test' => $fbmake_name});
194 'test' => $fbmake_name,
195 })->setAll(fbmake_result_json($this->framework
, $test, $status))
197 $statuses = $this->framework
->getCurrentTestStatuses();
199 if ($statuses !== null &&
200 $statuses->containsKey($test)) {
201 if ($status === $statuses[$test]) {
202 // FIX: posix_isatty(STDOUT) was always returning false, even
203 // though can print in color. Check this out later.
204 human(Colors
::GREEN
.Statuses
::PASS
.Colors
::NONE
);
206 // Red if we go from pass to something else
207 if ($statuses[$test] === '.') {
208 human(Colors
::RED
.Statuses
::FAIL
.Colors
::NONE
);
209 // Green if we go from something else to pass
210 } else if ($status === '.') {
211 human(Colors
::GREEN
.Statuses
::FAIL
.Colors
::NONE
);
212 // Blue if we go from something "faily" to something "faily"
215 human(Colors
::BLUE
.Statuses
::FAIL
.Colors
::NONE
);
217 human(PHP_EOL
."Different status in ".$this->framework
->getName().
218 " for test ".$test." was ".
220 " and now is ".$status.PHP_EOL
);
221 $this->diff_information
.= "----------------------".PHP_EOL
.
222 $test.PHP_EOL
.PHP_EOL
.
223 $this->getTestRunStr($test, "RUN TEST FILE: ").PHP_EOL
.PHP_EOL
.
224 "EXPECTED: ".$statuses[$test].
225 PHP_EOL
.">>>>>>>".PHP_EOL
.
226 "ACTUAL: ".$status.PHP_EOL
.PHP_EOL
;
229 // This is either the first time we run the unit tests, and all pass
230 // because we are establishing a baseline. OR we have run the tests
231 // before, but we are having an issue getting to the actual tests
232 // (e.g., yii is one test suite that has behaved this way).
233 if ($statuses !== null) {
234 human(Colors
::LIGHTBLUE
.Statuses
::FAIL
.Colors
::NONE
);
235 human(PHP_EOL
."Different status in ".$this->framework
->getName().
236 " for test ".$test.PHP_EOL
);
237 $this->diff_information
.= "----------------------".PHP_EOL
.
238 "Maybe haven't see this test before: ".$test.PHP_EOL
.PHP_EOL
.
239 $this->getTestRunStr($test, "RUN TEST FILE: ").PHP_EOL
.PHP_EOL
;
241 human(Colors
::GRAY
.Statuses
::PASS
.Colors
::NONE
);
246 private function getLine(): ?
string {
247 if (feof($this->pipes
[1])) {
250 if (!$this->checkReadStream()) {
251 return Statuses
::TIMEOUT
;
256 $part = stream_get_line($this->pipes
[1], 4096, PHP_EOL
);
257 if ($part === false ||
$part === null) {
259 // We never read anything, so there is no line here.
262 // We've buffered some output already. Exit the loop and return
263 // that to the caller.
268 if (strlen($part) < 4096) {
273 $line = remove_color_codes($line);
277 // Post test information are error/failure information and the final passing
278 // stats for the test
279 private function printPostTestInfo(): void
{
283 $post_stat_fatal = false;
285 // Throw out any initial blank lines
287 $line = $this->getLine();
288 } while ($line === "" && $line !== null);
290 // Now that we have our first non-blank line, print out the test information
291 // until we have our final stats
292 while ($line !== null) {
293 // Don't print out any of the PHPUnit Patterns to the errors file.
294 // Just print out pertinent error information.
296 // There was 1 failure: <---- Don't print
298 // 1) Assetic\Test\Asset\HttpAssetTest::testGetLastModified <---- print
299 if (preg_match(PHPUnitPatterns
::TESTS_OK_SKIPPED_INC_PATTERN
,
301 preg_match(PHPUnitPatterns
::NUM_ERRORS_FAILURES_PATTERN
,
303 preg_match(PHPUnitPatterns
::FAILURES_HEADER_PATTERN
,
305 preg_match(PHPUnitPatterns
::NUM_SKIPS_INC_PATTERN
,
308 // throw out any blank lines after these pattern
309 $line = $this->getLine();
310 } while ($line === "" && $line !== null);
314 // If we hit what we think is the final stats based on the pattern of the
315 // line, make sure this is the case. The final stats will generally be
316 // the last line before we hit null returned from line retrieval. The
317 // only cases where this would not be true is if, for some rare reason,
318 // stat information is part of the information provided for a
319 // given test error -- or -- we have hit a fatal at the very end of
320 // running PHPUnit. For that fatal case, we handle that a bit differently.
321 if (preg_match(PHPUnitPatterns
::TESTS_OK_PATTERN
, $line) === 1 ||
322 preg_match(PHPUnitPatterns
::TESTS_FAILURE_PATTERN
, $line) === 1 ||
323 preg_match(PHPUnitPatterns
::NO_TESTS_EXECUTED_PATTERN
,
326 $line = $this->getLine();
327 if ($line === null) {
328 $final_stats = $prev_line;
330 } else if ($line === "") {
331 // FIX ME: The above $line === null check is all I should need, but
332 // but getLine() is not cooperating. Not sure if getLine() problem or
333 // a PHPUnit output thing, but even when I am at the final stat line
334 // pattern, sometimes it takes me two getLine() calls to hit
335 // $line === null because I first get $line === "".
336 // So...save the current position. Read ahead. If null, we are done.
337 // Otherwise, print $prev_line, go back to where we were and the
338 // current blank line now stored in $line, will be printed down
340 $curPos = ftell($this->pipes
[1]);
341 if ($this->getLine() === null) {
342 $final_stats = $prev_line;
345 $this->error_information
.= $prev_line.PHP_EOL
;
346 fseek($this->pipes
[1], $curPos);
348 } else if ($this->checkForFatals($line) ||
349 $this->checkForWarnings($line)) {
350 // Sometimes when PHPUnit is done printing its post test info, hhvm
351 // fatals. This is not good, but it currently happens nonetheless. Here
355 // Tests: 3, Assertions: 9, Failures: 2. <--- EXPECTED LAST LINE (STATS)
356 // Core dumped: Segmentation fault <--- But, we can get this and below
357 // /home/joelm/bin/hhvm: line 1: 28417 Segmentation fault
358 $final_stats = $prev_line;
359 $post_stat_fatal = true;
362 $this->error_information
.= $prev_line.PHP_EOL
;
366 $this->error_information
.= $line.PHP_EOL
;
367 if (preg_match($this->framework
->getTestNamePattern(), $line,
369 $print_blanks = true;
370 $this->error_information
.= PHP_EOL
.
371 $this->getTestRunStr($matches[0],
375 $line = $this->getLine();
378 if ($post_stat_fatal) {
379 $this->fatal_information
.= "POST-TEST FATAL/WARNING FOR ".
380 $this->name
.PHP_EOL
.PHP_EOL
.
381 $this->getTestRunStr($this->name
, "RUN TEST FILE: ").
383 while ($line !== null) {
384 $this->fatal_information
.= $line.PHP_EOL
;
385 $line = $this->getLine();
387 // Add a newline to the fatal file if we had a post-test fatal for better
389 $this->fatal_information
.= PHP_EOL
;
392 // If we have no final stats, assume some sort of fatal for this test.
393 // If we have "No tests executed", assume a skip
394 // Otherwise, print the final stats.
395 $this->stat_information
= $this->name
.PHP_EOL
;
396 if ($final_stats === null) {
397 $this->stat_information
.= Statuses
::FATAL
.PHP_EOL
;
398 } else if (preg_match(PHPUnitPatterns
::NO_TESTS_EXECUTED_PATTERN
,
399 $final_stats) === 1) {
400 $this->stat_information
.= Statuses
::SKIP
.PHP_EOL
;
402 $this->stat_information
.= $final_stats.PHP_EOL
;
406 private function isStop(string $line) {
407 return preg_match(PHPUnitPatterns
::STOP_PARSING_PATTERN
, $line) === 1;
410 private function isPrologue(string $line) {
411 return preg_match(PHPUnitPatterns
::HEADER_PATTERN
, $line) === 1 ||
412 preg_match(PHPUnitPatterns
::CONFIG_FILE_PATTERN
, $line) === 1 ||
413 preg_match(PHPUnitPatterns
::XDEBUG_PATTERN
, $line) === 1;
416 private function isBlankLine(string $line): bool {
417 return $line === "" ||
$line === PHP_EOL
;
420 private function initialize(): bool {
421 $this->actual_test_command
= $this->framework
->getTestCommand($this->name
);
422 verbose("Command: ".$this->actual_test_command
."\n");
424 $descriptorspec = array(
425 0 => array("pipe", "r"),
426 1 => array("pipe", "w"),
427 2 => array("pipe", "w"),
431 // Use the proxies in case the test needs access to the outside world
432 $env = array_merge($env, nullthrows(ProxyInformation
::$proxies)->toArray());
433 if ($this->framework
->getEnvVars() !== null) {
434 $env = array_merge($env,
435 nullthrows($this->framework
->getEnvVars())->toArray());
437 $temp_dir = sys_get_temp_dir().'/hhvm_oss_'.
438 uniqid($this->framework
->getName(), true);
440 $this->temp_dir
= $temp_dir;
441 $env['TMPDIR'] = $temp_dir;
442 $env['HHVM_NO_DEFAULT_CONFIGS'] = true;
443 $env['HHVM_CONFIG_FILE'] = $this->framework
->getHHVMConfigFile();
445 $this->process
= proc_open($this->actual_test_command
, $descriptorspec,
446 &$this->pipes
, $this->framework
->getTestPath(),
448 return is_resource($this->process
);
451 private function finalize(): int {
452 fclose($this->pipes
[0]);
453 fclose($this->pipes
[1]);
454 fclose($this->pipes
[2]);
456 $temp_dir = $this->temp_dir
;
457 $this->temp_dir
= null;
458 if ($temp_dir !== null) {
459 if (!file_exists($temp_dir)) {
461 'Temp directory already deleted in test '.$this->name
464 remove_dir_recursive($temp_dir);
467 return proc_close($this->process
) === -1 ?
-1 : 0;
470 private function checkReadStream(): bool {
471 $r = array($this->pipes
[1]);
474 $s = stream_select(&$r, &$w, &$e, Options
::$timeout);
475 // If stream_select returns 0, then there is no more data or we have
476 // timed out. If it returns false, then something else bad happened.
477 return !($s === 0 ||
$s === false);
480 private function outputData(): void
{
481 file_put_contents($this->framework
->getOutFile(), $this->test_information
,
482 FILE_APPEND | LOCK_EX
);
483 file_put_contents($this->framework
->getErrorsFile(),
484 $this->error_information
, FILE_APPEND | LOCK_EX
);
485 file_put_contents($this->framework
->getDiffFile(), $this->diff_information
,
486 FILE_APPEND | LOCK_EX
);
487 file_put_contents($this->framework
->getStatsFile(), $this->stat_information
,
488 FILE_APPEND | LOCK_EX
);
489 file_put_contents($this->framework
->getFatalsFile(),
490 $this->fatal_information
, FILE_APPEND | LOCK_EX
);
494 private function checkForFatals(string $line): bool {
495 return preg_match(PHPUnitPatterns
::FATAL_PATTERN
, $line) === 1;
498 private function checkForWarnings(string $line): bool {
499 return preg_match(PHPUnitPatterns
::WARNING_PATTERN
, $line) === 1 ||
500 preg_match(PHPUnitPatterns
::PHPUNIT_EXCEPTION_WITH_WARNING
,
504 private function getTestRunStr(string $test, string $prologue = "",
505 string $epilogue = ""): string {
506 $test_run = $prologue.
507 " cd ".$this->framework
->getTestPath()." && ";
508 // If the test that is coming in to this function is an individual test,
509 // as opposed to a file, then we can use the --filter option to make the
510 // run string have even more specificity.
511 if (preg_match($this->framework
->getTestNamePattern(), $test)) {
512 // If we are running this framework with individual test mode
513 // (e.g., --by-test), then --filter already exists. We also don't want to
514 // add --filter to .phpt style tests (e.g. Pear).
515 if (strpos($this->actual_test_command
, "--filter") === false &&
516 strpos($test, ".phpt") === false) {
517 // The string after the last space in actual_test_command is
518 // the file that is run in phpunit. Remove the file and replace
519 // with --filter <individual test>. This will also get rid of any
520 // 2>&1 that may exist as well, which we do not want.
523 // hhvm -v Eval.Jit=true phpunit --debug 'ConverterTest.php'
525 // hhvm -v Eval.Jit=true phpunit --debug 'ConverterTest::MyTest'
526 $t = rtrim(str_replace("2>&1", "", $this->actual_test_command
));
527 $lastspace = strrpos($t, ' ');
528 $t = substr($this->actual_test_command
, 0, $lastspace);
529 // For --filter, the namespaces need to be separated by \\
530 $test = str_replace("\\", "\\\\", $test);
531 $t .= " --filter '".$test."'";
533 } else if (!$this->framework
->isParallel()) {
534 // If a framework is not being run in parallel (e.g., it is being run like
535 // normal phpunit for the entire framework), then the actual_test_command
536 // would not contain the individual test by default. It is being run like
537 // this, for example, from the test root directory:
541 // Pear is a current example of this behavior.
542 $test_run .= rtrim(str_replace("2>&1", "", $this->actual_test_command
));
543 // Re-add __DIR__ if not there so we have a full test path to run
544 $test_run .= strpos($test, __DIR__
) !== 0
545 ?
" ".__DIR__
."/".$test
548 $test_run .= rtrim(str_replace("2>&1", "", $this->actual_test_command
));
551 // $test is not a XXX::YYY style test, but is instead a file that is already
552 // part of the actual_test_comand
553 $test_run .= rtrim(str_replace("2>&1", "", $this->actual_test_command
));
555 return $test_run.$epilogue;