Revert "Revert "Update and lock framework test dependencies""
[hiphop-php.git] / hphp / test / frameworks / run.php
bloba01289bb661b1cf64647c7a4ce6dd48c78cb5fcc
1 #!/usr/bin/env php
2 <?hh
3 /*
4 * This script allows us to more easily test the key OSS PHP frameworks
5 * that is helping bring HHVM closer to parity.
7 * Key features:
9 * - Autodownload of frameworks, so we don't have to add 3 GB of frameworks to
10 * our official repo. This can be a bit flaky due to our proxy; so we
11 * we will see how this works out moving forward. If the framework does not
12 * exist in your local repo, it gets downloaded to your dev box. The
13 * .gitignore will ensure that the frameworks aren't added to the official
14 * repo.
16 * - Run a single test suite, all of the test suites or a custom set of
17 * multiple test suites. Currently, all of the available tests live in the
18 * code itself. See the help for the syntax on how to run the tests in each
19 * available mode.
21 * - Multiple test suites are run in separate processes, making the entire
22 * testing process a bit faster. This also helps us prepare for
23 * incorporating this with our official test suite.
25 * - The creation (and appending to) a summary file that lists all frameworks
26 * run and the pass percentage (or fatal) of each framework. The impetus for
27 * this file is the "OSS Parity" snapshot on our team TV screen.
29 * - Raw statistics for each test suite are put in a raw results file for
30 * examination.
32 * - Diff files showing the tests names and results that are different than
33 * expected for the test suite.
35 * - Error files showing all the errors and failures from running the test,
36 * suite or the fatal if the framework fatals.
38 * - Timeout option for running individual tests. There is a default of 60
39 * seconds to run each test, but this can be shortened or lengthened as
40 * desired.
42 * - Enhanced data output by the script to include "diff" type information
43 * about why a passing percentage is different from a previous run,
44 * particularly from a regression perspective. For example, what tests
45 * caused the regression.
47 * Comment about the frameworks:
49 * - Have a 'git_commit' field to ensure consistency across test runs as we
50 * may have different download times for people, as well as redownloads.
51 * The latest SHA at the time was used for the value.
53 * - In order to get frameworks to work correctly, may need to grab more code
54 * via some sort of pull request:
56 * - pull: The code we need is in a dir that doesn't affect the
57 * primary branch or SHA (e.g., 'vendor') and we can just do
58 * a 'git pull' since any branch or HEAD change doesn't matter
59 * - submodule: The code we are adding may be in the root framework dir
60 * so that can affect the framework branch or SHA. If we
61 * pull/merge, the HEAD SHA changes. (FIX IF THIS DOESN'T
62 * HAVE TO BE THE CASE). And, if that happens, we will always
63 * be redownloading the framework since the SHA is different
64 * than what we expect. Use a submodule/move technique.
66 * - Clowny tests that fail both Zend and HHVM (#WTFPear). We treat these
67 * tests as no-ops with respect to calculation.
69 * - Blacklist tests that are causing problems with this script running. E.g,
70 * deadlocks
72 * Future enhancements:
74 * - Integrate the testing with our current "test/run" style infrastructure
75 * for official pass/fail statistics when we have diffs.
77 * - Special case frameworks that don't use PHPUnit (e.g. Thinkup).
79 * - Decide whether proxy-based code needs to be solidified better or whether
80 * we should just add the frameworks to our repo officially.
85 require_once __DIR__.'/../../../hphp/tools/command_line_lib.php';
86 require_once __DIR__.'/utils.php';
87 require_once __DIR__.'/Colors.php';
88 require_once __DIR__.'/ProxyInformation.php';
89 require_once __DIR__.'/PHPUnitPatterns.php';
90 require_once __DIR__.'/Framework.php';
91 require_once __DIR__.'/Runner.php';
92 require_once __DIR__.'/Options.php';
93 require_once __DIR__.'/spyc/Spyc.php';
95 function prepare(Set $available_frameworks, Set $framework_class_overrides,
96 Vector $passed_frameworks): Vector {
97 get_unit_testing_infra_dependencies();
99 if (Options::$all) {
100 // At this point, $framework_names should be empty if we are in --all mode.
101 if (!($passed_frameworks->isEmpty())) {
102 error_and_exit("Do not specify both --all and individual frameworks to ".
103 "run at same time.\n");
105 // Test all frameworks
106 $passed_frameworks = $available_frameworks->toVector();
107 } else if (Options::$allexcept) {
108 // Run all the frameworks, except the ones we listed.
109 $passed_frameworks = Vector::fromItems(array_diff(
110 $available_frameworks->toVector(),
111 $passed_frameworks));
112 } else if ($passed_frameworks->isEmpty()) {
113 error_and_exit(usage());
116 // So it is easier to keep tabs on our progress when running ps or something.
117 // Since I get all the tests of a framework by foreaching over the frameworks
118 // vector, and then append those tests to a tests vector and then foreach the
119 // test vector to bucketize them, this will allow us to basically run the
120 // framework tests alphabetically.
121 sort($passed_frameworks);
122 $frameworks = Vector {};
123 foreach ($passed_frameworks as $name) {
124 $name = trim(strtolower($name));
125 if ($available_frameworks->contains($name)) {
126 $uname = ucfirst($name);
127 if ($framework_class_overrides->contains($name)) {
128 $frameworks[] = new $uname($name);
129 } else {
130 $frameworks[] = new Framework($name);
135 if ($frameworks->isEmpty()) {
136 error_and_exit(usage());
139 return $frameworks;
142 function fork_buckets(Traversable $data, (function(array):int) $callback): int {
143 $num_threads = min(count($data), num_cpus() + 1);
144 if (Options::$num_threads !== -1) {
145 $num_threads = min(count($data), Options::$num_threads);
148 // Create some data buckets proportional to the number of threads
149 $data_buckets = array();
150 $i = 0;
151 foreach ($data as $d) {
152 $data_buckets[$i][] = $d;
153 $i = ($i + 1) % $num_threads;
156 $children = Vector {};
157 for ($i = 0; $i < $num_threads; $i++) {
158 $pid = pcntl_fork();
159 if ($pid === -1) {
160 error_and_exit('Issues creating threads for data');
161 } else if ($pid) {
162 $children[] = $pid;
163 } else {
164 exit($callback($data_buckets[$i]));
168 $thread_ret_val = 0;
169 $status = -1;
170 foreach($children as $child) {
171 pcntl_waitpid($child, $status);
172 $thread_ret_val |= pcntl_wexitstatus($status);
175 return $thread_ret_val;
178 function run_tests(Vector $frameworks): void {
179 if ($frameworks->isEmpty()) {
180 error_and_exit("No frameworks available on which to run tests");
183 /***********************************
184 * Initial preparation
185 **********************************/
186 $summary_file = tempnam("/tmp", "oss-fw-test-summary");
187 $all_tests = Vector {};
189 foreach($frameworks as $framework) {
190 $framework->clean();
191 if (file_exists($framework->getExpectFile())) {
192 $framework->prepareCurrentTestStatuses(
193 PHPUnitPatterns::STATUS_CODE_PATTERN,
194 PHPUnitPatterns::STOP_PARSING_PATTERN);
195 human(Colors::YELLOW.$framework->getName().Colors::NONE.": running. ".
196 "Comparing against ".count($framework->getCurrentTestStatuses()).
197 " tests\n");
198 $run_msg = "Comparing test suite with previous run. ".
199 Colors::GREEN."Green . (dot) ".Colors::NONE.
200 "means test result same as previous. ".
201 "A ".Colors::GREEN." green F ".Colors::NONE.
202 "means we have gone from fail to pass. ".
203 "A ".Colors::RED." red F ".Colors::NONE.
204 "means we have gone from pass to fail. ".
205 "A ".Colors::BLUE." blue F ".Colors::NONE.
206 "means we have gone from one type of fail to another type ".
207 "of fail (e.g., F to E, or I to F). ".
208 "A ".Colors::LIGHTBLUE." light blue F ".Colors::NONE.
209 "means we are having trouble accessing the tests from the ".
210 "expected run and can't get a proper status".PHP_EOL;
211 verbose($run_msg);
212 } else {
213 human("Establishing baseline statuses for ".$framework->getName().
214 " with gray dots...\n");
217 // If we are running the tests for the framework in parallel, then let's
218 // get all the test for that framework and add each to our tests
219 // vector; otherwise, we are just going to add the framework to run
220 // serially and use its global phpunit test run command to run the entire
221 // suite (just like a normal phpunit run outside this framework).
222 if ($framework->isParallel() && !Options::$as_phpunit) {
223 foreach($framework->getTests() as $test) {
224 $st = new Runner($framework, $test);
225 $all_tests->add($st);
227 } else {
228 $st = new Runner($framework);
229 $all_tests->add($st);
233 /*************************************
234 * Run the test suite
235 ************************************/
236 human("Beginning the unit tests.....\n");
237 if ($all_tests->isEmpty()) {
238 error_and_exit("No tests found to run");
241 fork_buckets($all_tests, fun('run_test_bucket'));
243 /****************************************
244 * All tests complete. Create results for
245 * the framework that just ran.
246 ****************************************/
247 $all_tests_success = true;
248 $diff_frameworks = Vector {};
249 foreach ($frameworks as $framework) {
250 $pct = $framework->getPassPercentage();
251 $encoded_result = json_encode(array($framework->getName() => $pct));
252 if (!(file_exists($summary_file))) {
253 file_put_contents($summary_file, $encoded_result);
254 } else {
255 $file_data = file_get_contents($summary_file);
256 $decoded_results = json_decode($file_data, true);
257 $decoded_results[$framework->getName()] = $pct;
258 file_put_contents($summary_file, json_encode($decoded_results));
261 // If the first baseline run, make both the same. Otherwise, see if we have
262 // a diff file. If not, then all is good. If not, thumbs down because there
263 // was a difference between what we ran and what we expected.
264 Framework::sortFile($framework->getOutFile());
265 if (!file_exists($framework->getExpectFile())) {
266 copy($framework->getOutFile(), $framework->getExpectFile());
267 } else if (file_get_contents($framework->getExpectFile()) !==
268 file_get_contents($framework->getOutFile())) {
269 $all_tests_success = false;
272 if (filesize($framework->getDiffFile()) > 0) {
273 $diff_frameworks[] = $framework;
277 if ($all_tests_success) {
278 $msg = <<<THUMBSUP
279 All tests ran as expected.
285 __\ \ _____
286 (____) -|
287 (____)| |
288 (____).__|
289 (___)__.|_____
291 THUMBSUP;
292 human($msg);
293 } else {
294 $msg = <<<THUMBSDOWN
295 All tests did not run as expected. Either some statuses were
296 different or the number of tests run didn't match the number of
297 tests expected to run
300 ______
301 (( ____ \-
302 (( _____
303 ((_____
304 ((____ ----
306 (_((
308 THUMBSDOWN;
309 human($msg);
312 // Print the diffs
313 if (
314 (Options::$output_format === OutputFormat::HUMAN) ||
315 (Options::$output_format === OutputFormat::HUMAN_VERBOSE)
317 foreach($diff_frameworks as $framework) {
318 print_diffs($framework);
322 // Print out summary information
323 print_summary_information($summary_file);
325 // Update any git hashes in case --latest or --latest-record was used and we
326 // changed the hashes currently in frameworks.json. Use md5 of the original
327 // and current maps to see if we are different
328 if (md5(serialize(Options::$original_framework_info)) !==
329 md5(serialize(Options::$framework_info))) {
330 human("Updating frameworks.json because some hashes have been updated");
331 file_put_contents(__DIR__."/frameworks.yaml",
332 Spyc::YAMLDump(Options::$framework_info));
336 function run_test_bucket(array $test_bucket): int {
337 $result = 0;
338 foreach($test_bucket as $test) {
339 $result = $test->run();
341 return $result;
344 function get_unit_testing_infra_dependencies(): void {
345 // Install composer.phar. If it exists, but it is nearing that
346 // 30 day old mark, resintall it anyway.
347 if (!(file_exists(__DIR__."/composer.phar")) ||
348 (time() - filectime(__DIR__."/composer.phar")) >= 29*24*60*60) {
349 human("Getting composer.phar....\n");
350 unlink(__DIR__."/composer.phar");
351 $comp_url = "http://getcomposer.org/composer.phar";
352 $get_composer_command = "curl ".$comp_url." -o ".
353 __DIR__."/composer.phar 2>&1";
354 $ret = run_install($get_composer_command, __DIR__,
355 ProxyInformation::$proxies);
356 if ($ret !== 0) {
357 error_and_exit("Could not download composer. Script stopping\n");
361 // Quick hack to make sure we get the latest phpunit binary from composer
362 $md5_file = __DIR__."/composer.json.md5";
363 $json_file_contents = file_get_contents(__DIR__."/composer.json");
364 $vendor_dir = __DIR__."/vendor";
365 $lock_file = __DIR__."/composer.lock";
366 if (!file_exists($md5_file) ||
367 file_get_contents($md5_file) !== md5($json_file_contents)) {
368 human("\nUpdated composer.json found. Updating phpunit binary.\n");
369 if (file_exists($vendor_dir)) {
370 remove_dir_recursive($vendor_dir);
372 unlink($lock_file);
373 file_put_contents($md5_file, md5($json_file_contents));
376 // Install phpunit from composer.json located in __DIR__
377 $phpunit_binary = __DIR__."/vendor/bin/phpunit";
378 if (!(file_exists($phpunit_binary))) {
379 human("\nDownloading PHPUnit in order to run tests. There may be an ".
380 "output delay while the download begins.\n");
381 // Use the timeout to avoid curl SlowTimer timeouts and problems
382 $phpunit_install_command = get_runtime_build()." ".
383 "-v ResourceLimit.SocketDefaultTimeout=30 ".
384 __DIR__.
385 "/composer.phar install --dev --verbose 2>&1";
386 $ret = run_install($phpunit_install_command, __DIR__,
387 ProxyInformation::$proxies);
388 if ($ret !== 0) {
389 error_and_exit("Could not install PHPUnit. Script stopping\n");
395 function print_diffs(Framework $framework): void {
396 $diff = $framework->getDiffFile();
397 // The file may not exist or the file may not have anything in it
398 // since there is no diff (i.e., all tests for that particular
399 // framework ran as expected). Either way, don't print anything
400 // out for those cases.
401 if (file_exists($diff) &&
402 ($contents = file_get_contents($diff)) !== "") {
403 print PHP_EOL."********* ".strtoupper($framework->getName()).
404 " **********".PHP_EOL;
405 print $contents;
409 function print_summary_information(string $summary_file): void {
410 if (file_exists($summary_file)
411 && ($contents = file_get_contents($summary_file)) !== "") {
412 $decoded_results = json_decode($contents, true);
413 ksort($decoded_results);
415 switch (Options::$output_format) {
416 case OutputFormat::CSV:
417 if (Options::$csv_header) {
418 $print_str = str_pad("date,", 20);
419 foreach ($decoded_results as $key => $value) {
420 $print_str .= str_pad($key.",", 20);
422 print rtrim($print_str, " ,") . PHP_EOL;
424 $print_str = str_pad(date("Y/m/d-G:i:s").",", 20);
425 foreach ($decoded_results as $key => $value) {
426 $print_str .= str_pad($value.",", 20);
428 print rtrim($print_str, " ,") . PHP_EOL;
429 break;
430 case OutputFormat::FBMAKE:
431 break;
432 default:
433 print PHP_EOL."ALL TESTS COMPLETE!".PHP_EOL;
434 print "SUMMARY:".PHP_EOL;
435 foreach ($decoded_results as $key => $value) {
436 print $key."=".$value.PHP_EOL;
438 print PHP_EOL;
439 print "To run differing tests (if they exist), see above for the".PHP_EOL;
440 print "commands or the results/.diff file. To run erroring or".PHP_EOL;
441 print "fataling tests see results/.errors and results/.fatals".PHP_EOL;
442 print "files, respectively".PHP_EOL;
443 break;
445 } else {
446 human("\nNO SUMMARY INFO AVAILABLE!\n");
450 function help(): void {
451 $intro = <<<INTRO
452 oss_framework_test_script
454 This script runs one or more open source (oss) framework unit tests
455 via PHPUnit. Run one or multiple tests by explicitly naming them at the
456 command line, or specify --all to run all available tests.
458 You will see various forms of output. The first time running a test suite
459 for a framework, gray dots will appear to set the baseline for future runs.
460 On subsequent runs, you will see green dots for tests that have the same
461 status as the previous run. If something changes, you will see a red F if
462 a test went from pass to something else. You will see a green F if a test
463 went from something else to pass. You will see a blue F if a test stayed
464 in the failing range, but went from something like E to I or F to S.
466 The summary for a test show the overall pass percentage of the unit
467 test suite, irrespective of previous runs. The output and diff files for
468 a test suite will show what tests pass or fail, and have why they failed.
470 INTRO;
472 $examples = <<<EXAMPLES
473 Usage:
475 # Run all framework tests.
476 % hhvm run.php --all
478 # Run all framework tests using another PHP binary
479 % hhvm run.php --all --with-php ~/php55/bin/php
481 # Run all framework tests forcing the download of all the frameworks and
482 # creating new expected output files for all of the frameworks
483 % hhvm run.php --all --redownload --record
485 # Run all framework tests with a timeout per individual test (in secs).
486 % hhvm run.php --all --timeout 30
488 # Run one test.
489 % hhvm run.php composer
491 # Run multiple tests.
492 % hhvm run.php composer assetic paris
494 # Run all tests except a few.
495 % hhvm run.php --allexcept pear symfony
497 # Run multiple tests with timeout for each individual test (in seconds).
498 # Tests must come after the -- options
499 % hhvm run.php --timeout 30 composer assetic
501 # Run multiple tests with timeout for each individual test (in seconds) and
502 # with verbose messages. Tests must come after the -- options.
503 % hhvm run.php --timeout 30 --verbose composer assetic
505 # Run all the tests, but only produce a machine readable csv
506 # for data extraction into entities like charts.
507 % hhvm run.php --all --csv
509 # Run all the tests, but only produce a machine readable csv
510 # for data extraction into entities like charts, including a header row.
511 % hhvm run.php --all --csv --csvheader
513 # Run tests with the runner as they would be run with phpunit
514 % hhvm run.php --as-phpunit paris
515 % hhvm run.php --as-phpunit --all (THIS WILL BE SLOW AND COULD FATAL TOO)
517 # Display help.
518 % hhvm run.php --help
521 EXAMPLES;
523 $run_msg = "When comparing test suites with previous runs: ".PHP_EOL;
524 $run_msg .= Colors::GREEN."Green . (dot) ".Colors::NONE;
525 $run_msg .= "means test result same as previous. ".PHP_EOL;
526 $run_msg .= "A ".Colors::GREEN." green F ".Colors::NONE;
527 $run_msg .= "means we have gone from fail to pass. ".PHP_EOL;
528 $run_msg .= "A ".Colors::RED." red F ".Colors::NONE;
529 $run_msg .= "means we have gone from pass to fail. ".PHP_EOL;
530 $run_msg .= "A ".Colors::BLUE." blue F ".Colors::NONE;
531 $run_msg .= "means we have gone from one type of fail to another type ";
532 $run_msg .= "of fail (e.g., F to E, or I to F). ".PHP_EOL;
533 $run_msg .= "A ".Colors::LIGHTBLUE." light blue F ".Colors::NONE;
534 $run_msg .= "means we are having trouble accessing the tests from the ";
535 $run_msg .= "expected run and can't get a proper status.".PHP_EOL;
537 display_help($intro, oss_test_option_map());
538 print $examples;
539 print $run_msg;
543 function get_available_frameworks(): array<string> {
544 return array_keys(Spyc::YAMLLoad(__DIR__.'/frameworks.yaml'));
547 function usage(): string {
548 $msg = "Specify frameworks to run, use --all or use --allexcept. ";
549 $msg .= "Available frameworks are: ".PHP_EOL;
550 $msg .= implode(PHP_EOL, get_available_frameworks());
551 return $msg;
554 function oss_test_option_map(): OptionInfoMap {
555 return Map {
556 'help' => Pair {'h', "Print help message"},
557 'all' => Pair {'a', "Run tests of all frameworks. The ".
558 "frameworks to be run are hardcoded ".
559 "in a Map in this code."},
560 'allexcept' => Pair {'e', "Run all tests of all frameworks ".
561 "except for the ones listed. The ".
562 "tests must be at the end of the ".
563 "command argument list."},
564 'flakey' => Pair {'', 'Include tests that intermittently '.
565 'fail.'},
566 'timeout:' => Pair {'', "The maximum amount of time, in secs, ".
567 "to allow a individual test to run.".
568 "Default is 60 seconds."},
569 'verbose' => Pair {'v', "For a lot of messages about what is ".
570 "going on."},
571 'with-php:' => Pair {'', "Use php to run the tests ".
572 "Currently, php must be installed ".
573 "and the path to the php binary".
574 "specified."},
575 'install-only' => Pair {'', "Download and install the framework, ".
576 "but don't run any tests."},
577 'redownload' => Pair {'', "Forces a redownload of the framework ".
578 "code and dependencies. This uses ".
579 "the current git hash associated with ".
580 "the current download."},
581 'record' => Pair {'', "Forces a new expect file for the ".
582 "framework test suite"},
583 'latest-record' => Pair {'', "Forces a complete update of a ".
584 "framework. The code is updated with ".
585 "the latest and greatest hash of the ".
586 "current branch (e.g., master) and ".
587 "the expect file is updated."},
588 'latest' => Pair {'', "Forces framework code to be updated ".
589 "with the latest and greatest hash of ".
590 "the current branch (e.g., master)."},
591 'csv' => Pair {'', "Just create the machine readable ".
592 "summary CSV for parsing and chart ".
593 "display."},
594 'csvheader' => Pair {'', "Add a header line for the summary ".
595 "CSV which includes the framework ".
596 "names."},
597 'fbmake' => Pair {'', "Output a stream of JSON objects that ".
598 "Facebook's test systems understand"},
599 'by-file' => Pair {'f', "DEFAULT: Run tests for a framework ".
600 "on a per test file basis, as ".
601 "opposed to a an individual test ".
602 "basis."},
603 'by-single-test' => Pair {'s', "Run tests for a framework on a ".
604 "individual test basis, as ".
605 "opposed to a test file basis. This ".
606 "basically means the --filter option ".
607 "is being used for phpunit"},
608 'as-phpunit' => Pair {'', "Run tests for a framework just ".
609 "like it would be run normally with ".
610 "PHPUnit"},
611 'numthreads:' => Pair {'', "The exact number of threads to use ".
612 "when running framework tests in ".
613 "parallel"},
614 'isolate' => Pair {'', "Try to make tests that have ".
615 "external dependencies automatically ".
616 "fail"},
617 'toran-proxy:' => Pair {'', "URL of Toran Proxy to use for ".
618 "dependencies"},
622 function main(array $argv): void {
623 $options = parse_options(oss_test_option_map());
624 if ($options->containsKey('help')) {
625 help();
626 return;
628 // Parse all the options passed to run.php and come out with a list of
629 // frameworks passed into test (or --all or --allexcept)
630 $passed_frameworks = Options::parse($options, $argv);
631 $available_frameworks = new Set(array_keys(Options::$framework_info));
632 include_all_php(__DIR__."/framework_class_overrides");
633 $framework_class_overrides = get_subclasses_of("Framework")->toSet();
634 $frameworks = prepare($available_frameworks, $framework_class_overrides,
635 $passed_frameworks);
637 if (Options::$run_tests) {
638 run_tests($frameworks);
642 main($argv);