1#!/usr/bin/env php 2<?php 3/* 4 +----------------------------------------------------------------------+ 5 | Copyright (c) The PHP Group | 6 +----------------------------------------------------------------------+ 7 | This source file is subject to version 3.01 of the PHP license, | 8 | that is bundled with this package in the file LICENSE, and is | 9 | available through the world-wide-web at the following url: | 10 | https://php.net/license/3_01.txt | 11 | If you did not receive a copy of the PHP license and are unable to | 12 | obtain it through the world-wide-web, please send a note to | 13 | license@php.net so we can mail you a copy immediately. | 14 +----------------------------------------------------------------------+ 15 | Authors: Ilia Alshanetsky <iliaa@php.net> | 16 | Preston L. Bannister <pbannister@php.net> | 17 | Marcus Boerger <helly@php.net> | 18 | Derick Rethans <derick@php.net> | 19 | Sander Roobol <sander@php.net> | 20 | Andrea Faulds <ajf@ajf.me> | 21 | (based on version by: Stig Bakken <ssb@php.net>) | 22 | (based on the PHP 3 test framework by Rasmus Lerdorf) | 23 +----------------------------------------------------------------------+ 24 */ 25 26/* $Id: 070229ec1d6aa866f9962ef442e2a9ffd30a999f $ */ 27 28/* Temporary variables while this file is being refactored. */ 29/** @var ?JUnit */ 30$junit = null; 31 32/* End temporary variables. */ 33 34/* Let there be no top-level code beyond this point: 35 * Only functions and classes, thanks! 36 * 37 * Minimum required PHP version: 7.4.0 38 */ 39 40function show_usage(): void 41{ 42 echo <<<HELP 43Synopsis: 44 php run-tests.php [options] [files] [directories] 45 46Options: 47 -j<workers> Run up to <workers> simultaneous testing processes in parallel for 48 quicker testing on systems with multiple logical processors. 49 Note that this is experimental feature. 50 51 -l <file> Read the testfiles to be executed from <file>. After the test 52 has finished all failed tests are written to the same <file>. 53 If the list is empty and no further test is specified then 54 all tests are executed (same as: -r <file> -w <file>). 55 56 -r <file> Read the testfiles to be executed from <file>. 57 58 -w <file> Write a list of all failed tests to <file>. 59 60 -a <file> Same as -w but append rather then truncating <file>. 61 62 -W <file> Write a list of all tests and their result status to <file>. 63 64 -c <file> Look for php.ini in directory <file> or use <file> as ini. 65 66 -n Pass -n option to the php binary (Do not use a php.ini). 67 68 -d foo=bar Pass -d option to the php binary (Define INI entry foo 69 with value 'bar'). 70 71 -g Comma separated list of groups to show during test run 72 (possible values: PASS, FAIL, XFAIL, XLEAK, SKIP, BORK, WARN, LEAK, REDIRECT). 73 74 -m Test for memory leaks with Valgrind (equivalent to -M memcheck). 75 76 -M <tool> Test for errors with Valgrind tool. 77 78 -p <php> Specify PHP executable to run. 79 80 -P Use PHP_BINARY as PHP executable to run (default). 81 82 -q Quiet, no user interaction (same as environment NO_INTERACTION). 83 84 -s <file> Write output to <file>. 85 86 -x Sets 'SKIP_SLOW_TESTS' environmental variable. 87 88 --offline Sets 'SKIP_ONLINE_TESTS' environmental variable. 89 90 --verbose 91 -v Verbose mode. 92 93 --help 94 -h This Help. 95 96 --temp-source <sdir> --temp-target <tdir> [--temp-urlbase <url>] 97 Write temporary files to <tdir> by replacing <sdir> from the 98 filenames to generate with <tdir>. In general you want to make 99 <sdir> the path to your source files and <tdir> some patch in 100 your web page hierarchy with <url> pointing to <tdir>. 101 102 --keep-[all|php|skip|clean] 103 Do not delete 'all' files, 'php' test file, 'skip' or 'clean' 104 file. 105 106 --set-timeout <n> 107 Set timeout for individual tests, where <n> is the number of 108 seconds. The default value is 60 seconds, or 300 seconds when 109 testing for memory leaks. 110 111 --context <n> 112 Sets the number of lines of surrounding context to print for diffs. 113 The default value is 3. 114 115 --show-[all|php|skip|clean|exp|diff|out|mem] 116 Show 'all' files, 'php' test file, 'skip' or 'clean' file. You 117 can also use this to show the output 'out', the expected result 118 'exp', the difference between them 'diff' or the valgrind log 119 'mem'. The result types get written independent of the log format, 120 however 'diff' only exists when a test fails. 121 122 --show-slow <n> 123 Show all tests that took longer than <n> milliseconds to run. 124 125 --no-clean Do not execute clean section if any. 126 127 --color 128 --no-color Do/Don't colorize the result type in the test result. 129 130 --repeat [n] 131 Run the tests multiple times in the same process and check the 132 output of the last execution (CLI SAPI only). 133 134 --bless Bless failed tests using scripts/dev/bless_tests.php. 135 136HELP; 137} 138 139/** 140 * One function to rule them all, one function to find them, one function to 141 * bring them all and in the darkness bind them. 142 * This is the entry point and exit point überfunction. It contains all the 143 * code that was previously found at the top level. It could and should be 144 * refactored to be smaller and more manageable. 145 */ 146function main(): void 147{ 148 /* This list was derived in a naïve mechanical fashion. If a member 149 * looks like it doesn't belong, it probably doesn't; cull at will. 150 */ 151 global $DETAILED, $PHP_FAILED_TESTS, $SHOW_ONLY_GROUPS, $argc, $argv, $cfg, 152 $cfgfiles, $cfgtypes, $conf_passed, $end_time, $environment, 153 $exts_skipped, $exts_tested, $exts_to_test, $failed_tests_file, 154 $ignored_by_ext, $ini_overwrites, $is_switch, $colorize, 155 $just_save_results, $log_format, $matches, $no_clean, $no_file_cache, 156 $optionals, $output_file, $pass_option_n, $pass_options, 157 $pattern_match, $php, $php_cgi, $phpdbg, $preload, $redir_tests, 158 $repeat, $result_tests_file, $slow_min_ms, $start_time, $switch, 159 $temp_source, $temp_target, $test_cnt, $test_dirs, 160 $test_files, $test_idx, $test_list, $test_results, $testfile, 161 $user_tests, $valgrind, $sum_results, $shuffle, $file_cache, $num_repeats, 162 $bless; 163 // Parallel testing 164 global $workers, $workerID; 165 global $context_line_count; 166 167 // Temporary for the duration of refactoring 168 /** @var JUnit */ 169 global $junit; 170 171 define('IS_WINDOWS', substr(PHP_OS, 0, 3) == "WIN"); 172 173 $workerID = 0; 174 if (getenv("TEST_PHP_WORKER")) { 175 $workerID = intval(getenv("TEST_PHP_WORKER")); 176 run_worker(); 177 return; 178 } 179 180 define('INIT_DIR', getcwd()); 181 182 // Change into the PHP source directory. 183 if (getenv('TEST_PHP_SRCDIR')) { 184 @chdir(getenv('TEST_PHP_SRCDIR')); 185 } 186 187 define('TEST_PHP_SRCDIR', getcwd()); 188 189 check_proc_open_function_exists(); 190 191 // If timezone is not set, use UTC. 192 if (ini_get('date.timezone') == '') { 193 date_default_timezone_set('UTC'); 194 } 195 196 // Delete some security related environment variables 197 putenv('SSH_CLIENT=deleted'); 198 putenv('SSH_AUTH_SOCK=deleted'); 199 putenv('SSH_TTY=deleted'); 200 putenv('SSH_CONNECTION=deleted'); 201 202 set_time_limit(0); 203 204 ini_set('pcre.backtrack_limit', PHP_INT_MAX); 205 206 init_output_buffers(); 207 208 error_reporting(E_ALL); 209 210 $environment = $_ENV ?? []; 211 212 // Some configurations like php.ini-development set variables_order="GPCS" 213 // not "EGPCS", in which case $_ENV is NOT populated. Detect if the $_ENV 214 // was empty and handle it by explicitly populating through getenv(). 215 if (empty($environment)) { 216 $environment = getenv(); 217 } 218 219 if (empty($environment['TEMP'])) { 220 $environment['TEMP'] = sys_get_temp_dir(); 221 222 if (empty($environment['TEMP'])) { 223 // For example, OpCache on Windows will fail in this case because 224 // child processes (for tests) will not get a TEMP variable, so 225 // GetTempPath() will fallback to c:\windows, while GetTempPath() 226 // will return %TEMP% for parent (likely a different path). The 227 // parent will initialize the OpCache in that path, and child will 228 // fail to reattach to the OpCache because it will be using the 229 // wrong path. 230 die("TEMP environment is NOT set"); 231 } else { 232 if (count($environment) == 1) { 233 // Not having other environment variables, only having TEMP, is 234 // probably ok, but strange and may make a difference in the 235 // test pass rate, so warn the user. 236 echo "WARNING: Only 1 environment variable will be available to tests(TEMP environment variable)" . PHP_EOL; 237 } 238 } 239 } 240 241 if (IS_WINDOWS && empty($environment["SystemRoot"])) { 242 $environment["SystemRoot"] = getenv("SystemRoot"); 243 } 244 245 $php = null; 246 $php_cgi = null; 247 $phpdbg = null; 248 249 if (getenv('TEST_PHP_LOG_FORMAT')) { 250 $log_format = strtoupper(getenv('TEST_PHP_LOG_FORMAT')); 251 } else { 252 $log_format = 'LEODS'; 253 } 254 255 // Check whether a detailed log is wanted. 256 if (getenv('TEST_PHP_DETAILED')) { 257 $DETAILED = getenv('TEST_PHP_DETAILED'); 258 } else { 259 $DETAILED = 0; 260 } 261 262 $junit = new JUnit($environment, $workerID); 263 264 if (getenv('SHOW_ONLY_GROUPS')) { 265 $SHOW_ONLY_GROUPS = explode(",", getenv('SHOW_ONLY_GROUPS')); 266 } else { 267 $SHOW_ONLY_GROUPS = []; 268 } 269 270 // Check whether user test dirs are requested. 271 if (getenv('TEST_PHP_USER')) { 272 $user_tests = explode(',', getenv('TEST_PHP_USER')); 273 } else { 274 $user_tests = []; 275 } 276 277 $exts_to_test = []; 278 $ini_overwrites = [ 279 'output_handler=', 280 'open_basedir=', 281 'disable_functions=', 282 'output_buffering=Off', 283 'error_reporting=' . E_ALL, 284 'display_errors=1', 285 'display_startup_errors=1', 286 'log_errors=0', 287 'html_errors=0', 288 'track_errors=0', 289 'report_memleaks=1', 290 'report_zend_debug=0', 291 'docref_root=', 292 'docref_ext=.html', 293 'error_prepend_string=', 294 'error_append_string=', 295 'auto_prepend_file=', 296 'auto_append_file=', 297 'ignore_repeated_errors=0', 298 'precision=14', 299 'serialize_precision=-1', 300 'memory_limit=128M', 301 'opcache.fast_shutdown=0', 302 'opcache.file_update_protection=0', 303 'opcache.revalidate_freq=0', 304 'opcache.jit_hot_loop=1', 305 'opcache.jit_hot_func=1', 306 'opcache.jit_hot_return=1', 307 'opcache.jit_hot_side_exit=1', 308 'zend.assertions=1', 309 'zend.exception_ignore_args=0', 310 'zend.exception_string_param_max_len=15', 311 'short_open_tag=0', 312 ]; 313 314 $no_file_cache = '-d opcache.file_cache= -d opcache.file_cache_only=0'; 315 316 define('PHP_QA_EMAIL', 'qa-reports@lists.php.net'); 317 define('QA_SUBMISSION_PAGE', 'http://qa.php.net/buildtest-process.php'); 318 define('QA_REPORTS_PAGE', 'http://qa.php.net/reports'); 319 define('TRAVIS_CI', (bool) getenv('TRAVIS')); 320 321 // Determine the tests to be run. 322 323 $test_files = []; 324 $redir_tests = []; 325 $test_results = []; 326 $PHP_FAILED_TESTS = [ 327 'BORKED' => [], 328 'FAILED' => [], 329 'WARNED' => [], 330 'LEAKED' => [], 331 'XFAILED' => [], 332 'XLEAKED' => [], 333 'SLOW' => [] 334 ]; 335 336 // If parameters given assume they represent selected tests to run. 337 $result_tests_file = false; 338 $failed_tests_file = false; 339 $pass_option_n = false; 340 $pass_options = ''; 341 342 $output_file = INIT_DIR . '/php_test_results_' . date('Ymd_Hi') . '.txt'; 343 344 $just_save_results = false; 345 $valgrind = null; 346 $temp_source = null; 347 $temp_target = null; 348 $conf_passed = null; 349 $no_clean = false; 350 $colorize = true; 351 if (function_exists('sapi_windows_vt100_support') && !sapi_windows_vt100_support(STDOUT, true)) { 352 $colorize = false; 353 } 354 if (array_key_exists('NO_COLOR', $environment)) { 355 $colorize = false; 356 } 357 $selected_tests = false; 358 $slow_min_ms = INF; 359 $preload = false; 360 $file_cache = null; 361 $shuffle = false; 362 $bless = false; 363 $workers = null; 364 $context_line_count = 3; 365 $num_repeats = 1; 366 367 $cfgtypes = ['show', 'keep']; 368 $cfgfiles = ['skip', 'php', 'clean', 'out', 'diff', 'exp', 'mem']; 369 $cfg = []; 370 371 foreach ($cfgtypes as $type) { 372 $cfg[$type] = []; 373 374 foreach ($cfgfiles as $file) { 375 $cfg[$type][$file] = false; 376 } 377 } 378 379 if (!isset($argc, $argv) || !$argc) { 380 $argv = [__FILE__]; 381 $argc = 1; 382 } 383 384 if (getenv('TEST_PHP_ARGS')) { 385 $argv = array_merge($argv, explode(' ', getenv('TEST_PHP_ARGS'))); 386 $argc = count($argv); 387 } 388 389 for ($i = 1; $i < $argc; $i++) { 390 $is_switch = false; 391 $switch = substr($argv[$i], 1, 1); 392 $repeat = substr($argv[$i], 0, 1) == '-'; 393 394 while ($repeat) { 395 if (!$is_switch) { 396 $switch = substr($argv[$i], 1, 1); 397 } 398 399 $is_switch = true; 400 401 if ($repeat) { 402 foreach ($cfgtypes as $type) { 403 if (strpos($switch, '--' . $type) === 0) { 404 foreach ($cfgfiles as $file) { 405 if ($switch == '--' . $type . '-' . $file) { 406 $cfg[$type][$file] = true; 407 $is_switch = false; 408 break; 409 } 410 } 411 } 412 } 413 } 414 415 if (!$is_switch) { 416 $is_switch = true; 417 break; 418 } 419 420 $repeat = false; 421 422 switch ($switch) { 423 case 'j': 424 $workers = substr($argv[$i], 2); 425 if (!preg_match('/^\d+$/', $workers) || $workers == 0) { 426 error("'$workers' is not a valid number of workers, try e.g. -j16 for 16 workers"); 427 } 428 $workers = intval($workers, 10); 429 // Don't use parallel testing infrastructure if there is only one worker. 430 if ($workers === 1) { 431 $workers = null; 432 } 433 break; 434 case 'r': 435 case 'l': 436 $test_list = file($argv[++$i]); 437 if ($test_list) { 438 foreach ($test_list as $test) { 439 $matches = []; 440 if (preg_match('/^#.*\[(.*)\]\:\s+(.*)$/', $test, $matches)) { 441 $redir_tests[] = [$matches[1], $matches[2]]; 442 } else { 443 if (strlen($test)) { 444 $test_files[] = trim($test); 445 } 446 } 447 } 448 } 449 if ($switch != 'l') { 450 break; 451 } 452 $i--; 453 // no break 454 case 'w': 455 $failed_tests_file = fopen($argv[++$i], 'w+t'); 456 break; 457 case 'a': 458 $failed_tests_file = fopen($argv[++$i], 'a+t'); 459 break; 460 case 'W': 461 $result_tests_file = fopen($argv[++$i], 'w+t'); 462 break; 463 case 'c': 464 $conf_passed = $argv[++$i]; 465 break; 466 case 'd': 467 $ini_overwrites[] = $argv[++$i]; 468 break; 469 case 'g': 470 $SHOW_ONLY_GROUPS = explode(",", $argv[++$i]); 471 break; 472 //case 'h' 473 case '--keep-all': 474 foreach ($cfgfiles as $file) { 475 $cfg['keep'][$file] = true; 476 } 477 break; 478 //case 'l' 479 case 'm': 480 $valgrind = new RuntestsValgrind($environment); 481 break; 482 case 'M': 483 $valgrind = new RuntestsValgrind($environment, $argv[++$i]); 484 break; 485 case 'n': 486 if (!$pass_option_n) { 487 $pass_options .= ' -n'; 488 } 489 $pass_option_n = true; 490 break; 491 case 'e': 492 $pass_options .= ' -e'; 493 break; 494 case '--preload': 495 $preload = true; 496 $environment['SKIP_PRELOAD'] = 1; 497 break; 498 case '--file-cache-prime': 499 $file_cache = 'prime'; 500 break; 501 case '--file-cache-use': 502 $file_cache = 'use'; 503 break; 504 case '--no-clean': 505 $no_clean = true; 506 break; 507 case '--color': 508 $colorize = true; 509 break; 510 case '--no-color': 511 $colorize = false; 512 break; 513 case 'p': 514 $php = $argv[++$i]; 515 putenv("TEST_PHP_EXECUTABLE=$php"); 516 $environment['TEST_PHP_EXECUTABLE'] = $php; 517 break; 518 case 'P': 519 $php = PHP_BINARY; 520 putenv("TEST_PHP_EXECUTABLE=$php"); 521 $environment['TEST_PHP_EXECUTABLE'] = $php; 522 break; 523 case 'q': 524 putenv('NO_INTERACTION=1'); 525 $environment['NO_INTERACTION'] = 1; 526 break; 527 //case 'r' 528 case 's': 529 $output_file = $argv[++$i]; 530 $just_save_results = true; 531 break; 532 case '--set-timeout': 533 $timeout = $argv[++$i] ?? ''; 534 if (!preg_match('/^\d+$/', $timeout)) { 535 error("'$timeout' is not a valid number of seconds, try e.g. --set-timeout 60 for 1 minute"); 536 } 537 $environment['TEST_TIMEOUT'] = intval($timeout, 10); 538 break; 539 case '--context': 540 $context_line_count = $argv[++$i] ?? ''; 541 if (!preg_match('/^\d+$/', $context_line_count)) { 542 error("'$context_line_count' is not a valid number of lines of context, try e.g. --context 3 for 3 lines"); 543 } 544 $context_line_count = intval($context_line_count, 10); 545 break; 546 case '--show-all': 547 foreach ($cfgfiles as $file) { 548 $cfg['show'][$file] = true; 549 } 550 break; 551 case '--show-slow': 552 $slow_min_ms = $argv[++$i] ?? ''; 553 if (!preg_match('/^\d+$/', $slow_min_ms)) { 554 error("'$slow_min_ms' is not a valid number of milliseconds, try e.g. --show-slow 1000 for 1 second"); 555 } 556 $slow_min_ms = intval($slow_min_ms, 10); 557 break; 558 case '--temp-source': 559 $temp_source = $argv[++$i]; 560 break; 561 case '--temp-target': 562 $temp_target = $argv[++$i]; 563 break; 564 case 'v': 565 case '--verbose': 566 $DETAILED = true; 567 break; 568 case 'x': 569 $environment['SKIP_SLOW_TESTS'] = 1; 570 break; 571 case '--offline': 572 $environment['SKIP_ONLINE_TESTS'] = 1; 573 break; 574 case '--shuffle': 575 $shuffle = true; 576 break; 577 case '--asan': 578 case '--msan': 579 $environment['USE_ZEND_ALLOC'] = 0; 580 $environment['USE_TRACKED_ALLOC'] = 1; 581 $environment['SKIP_ASAN'] = 1; 582 $environment['SKIP_PERF_SENSITIVE'] = 1; 583 if ($switch === '--msan') { 584 $environment['SKIP_MSAN'] = 1; 585 $environment['MSAN_OPTIONS'] = 'intercept_tls_get_addr=0'; 586 } 587 588 $lsanSuppressions = __DIR__ . '/.github/lsan-suppressions.txt'; 589 if (file_exists($lsanSuppressions)) { 590 $environment['LSAN_OPTIONS'] = 'suppressions=' . $lsanSuppressions 591 . ':print_suppressions=0'; 592 } 593 break; 594 case '--repeat': 595 $num_repeats = (int) $argv[++$i]; 596 $environment['SKIP_REPEAT'] = 1; 597 break; 598 case '--bless': 599 $bless = true; 600 break; 601 //case 'w' 602 case '-': 603 // repeat check with full switch 604 $switch = $argv[$i]; 605 if ($switch != '-') { 606 $repeat = true; 607 } 608 break; 609 case '--version': 610 echo '$Id: 070229ec1d6aa866f9962ef442e2a9ffd30a999f $' . "\n"; 611 exit(1); 612 613 default: 614 echo "Illegal switch '$switch' specified!\n"; 615 // no break 616 case 'h': 617 case '-help': 618 case '--help': 619 show_usage(); 620 exit(1); 621 } 622 } 623 624 if (!$is_switch) { 625 $selected_tests = true; 626 $testfile = realpath($argv[$i]); 627 628 if (!$testfile && strpos($argv[$i], '*') !== false && function_exists('glob')) { 629 if (substr($argv[$i], -5) == '.phpt') { 630 $pattern_match = glob($argv[$i]); 631 } else { 632 if (preg_match("/\*$/", $argv[$i])) { 633 $pattern_match = glob($argv[$i] . '.phpt'); 634 } else { 635 die('Cannot find test file "' . $argv[$i] . '".' . PHP_EOL); 636 } 637 } 638 639 if (is_array($pattern_match)) { 640 $test_files = array_merge($test_files, $pattern_match); 641 } 642 } else { 643 if (is_dir($testfile)) { 644 find_files($testfile); 645 } else { 646 if (substr($testfile, -5) == '.phpt') { 647 $test_files[] = $testfile; 648 } else { 649 die('Cannot find test file "' . $argv[$i] . '".' . PHP_EOL); 650 } 651 } 652 } 653 } 654 } 655 656 if ($selected_tests && count($test_files) === 0) { 657 echo "No tests found.\n"; 658 return; 659 } 660 661 if (!$php) { 662 $php = getenv('TEST_PHP_EXECUTABLE'); 663 } 664 if (!$php) { 665 $php = PHP_BINARY; 666 } 667 668 if (!$php_cgi) { 669 $php_cgi = getenv('TEST_PHP_CGI_EXECUTABLE'); 670 } 671 if (!$php_cgi) { 672 $php_cgi = get_binary($php, 'php-cgi', 'sapi/cgi/php-cgi'); 673 } 674 675 if (!$phpdbg) { 676 $phpdbg = getenv('TEST_PHPDBG_EXECUTABLE'); 677 } 678 if (!$phpdbg) { 679 $phpdbg = get_binary($php, 'phpdbg', 'sapi/phpdbg/phpdbg'); 680 } 681 682 putenv("TEST_PHP_EXECUTABLE=$php"); 683 $environment['TEST_PHP_EXECUTABLE'] = $php; 684 putenv("TEST_PHP_CGI_EXECUTABLE=$php_cgi"); 685 $environment['TEST_PHP_CGI_EXECUTABLE'] = $php_cgi; 686 putenv("TEST_PHPDBG_EXECUTABLE=$phpdbg"); 687 $environment['TEST_PHPDBG_EXECUTABLE'] = $phpdbg; 688 689 if ($conf_passed !== null) { 690 if (IS_WINDOWS) { 691 $pass_options .= " -c " . escapeshellarg($conf_passed); 692 } else { 693 $pass_options .= " -c '" . realpath($conf_passed) . "'"; 694 } 695 } 696 697 $test_files = array_unique($test_files); 698 $test_files = array_merge($test_files, $redir_tests); 699 700 // Run selected tests. 701 $test_cnt = count($test_files); 702 703 verify_config(); 704 write_information(); 705 706 if ($test_cnt) { 707 putenv('NO_INTERACTION=1'); 708 usort($test_files, "test_sort"); 709 $start_time = time(); 710 711 echo "Running selected tests.\n"; 712 713 $test_idx = 0; 714 run_all_tests($test_files, $environment); 715 $end_time = time(); 716 717 if ($failed_tests_file) { 718 fclose($failed_tests_file); 719 } 720 721 if ($result_tests_file) { 722 fclose($result_tests_file); 723 } 724 725 if (0 == count($test_results)) { 726 echo "No tests were run.\n"; 727 return; 728 } 729 730 compute_summary(); 731 echo "====================================================================="; 732 echo get_summary(false); 733 734 if ($output_file != '' && $just_save_results) { 735 save_or_mail_results(); 736 } 737 } else { 738 // Compile a list of all test files (*.phpt). 739 $test_files = []; 740 $exts_tested = count($exts_to_test); 741 $exts_skipped = 0; 742 $ignored_by_ext = 0; 743 sort($exts_to_test); 744 $test_dirs = []; 745 $optionals = ['Zend', 'tests', 'ext', 'sapi']; 746 747 foreach ($optionals as $dir) { 748 if (is_dir($dir)) { 749 $test_dirs[] = $dir; 750 } 751 } 752 753 // Convert extension names to lowercase 754 foreach ($exts_to_test as $key => $val) { 755 $exts_to_test[$key] = strtolower($val); 756 } 757 758 foreach ($test_dirs as $dir) { 759 find_files(TEST_PHP_SRCDIR . "/{$dir}", $dir == 'ext'); 760 } 761 762 foreach ($user_tests as $dir) { 763 find_files($dir, $dir == 'ext'); 764 } 765 766 $test_files = array_unique($test_files); 767 usort($test_files, "test_sort"); 768 769 $start_time = time(); 770 show_start($start_time); 771 772 $test_cnt = count($test_files); 773 $test_idx = 0; 774 run_all_tests($test_files, $environment); 775 $end_time = time(); 776 777 if ($failed_tests_file) { 778 fclose($failed_tests_file); 779 } 780 781 if ($result_tests_file) { 782 fclose($result_tests_file); 783 } 784 785 // Summarize results 786 787 if (0 == count($test_results)) { 788 echo "No tests were run.\n"; 789 return; 790 } 791 792 compute_summary(); 793 794 show_end($end_time); 795 show_summary(); 796 797 save_or_mail_results(); 798 } 799 800 $junit->saveXML(); 801 if ($bless) { 802 bless_failed_tests($PHP_FAILED_TESTS['FAILED']); 803 } 804 if (getenv('REPORT_EXIT_STATUS') !== '0' && getenv('REPORT_EXIT_STATUS') !== 'no' && 805 ($sum_results['FAILED'] || $sum_results['BORKED'] || $sum_results['LEAKED'])) { 806 exit(1); 807 } 808} 809 810if (!function_exists("hrtime")) { 811 /** 812 * @return array|float|int 813 */ 814 function hrtime(bool $as_num = false) 815 { 816 $t = microtime(true); 817 818 if ($as_num) { 819 return $t * 1000000000; 820 } 821 822 $s = floor($t); 823 return [0 => $s, 1 => ($t - $s) * 1000000000]; 824 } 825} 826 827function verify_config(): void 828{ 829 global $php; 830 831 if (empty($php) || !file_exists($php)) { 832 error('environment variable TEST_PHP_EXECUTABLE must be set to specify PHP executable!'); 833 } 834 835 if (!is_executable($php)) { 836 error("invalid PHP executable specified by TEST_PHP_EXECUTABLE = $php"); 837 } 838} 839 840function write_information(): void 841{ 842 global $php, $php_cgi, $phpdbg, $php_info, $user_tests, $ini_overwrites, $pass_options, $exts_to_test, $valgrind, $no_file_cache; 843 844 // Get info from php 845 $info_file = __DIR__ . '/run-test-info.php'; 846 @unlink($info_file); 847 $php_info = '<?php echo " 848PHP_SAPI : " , PHP_SAPI , " 849PHP_VERSION : " , phpversion() , " 850ZEND_VERSION: " , zend_version() , " 851PHP_OS : " , PHP_OS , " - " , php_uname() , " 852INI actual : " , realpath(get_cfg_var("cfg_file_path")) , " 853More .INIs : " , (function_exists(\'php_ini_scanned_files\') ? str_replace("\n","", php_ini_scanned_files()) : "** not determined **"); ?>'; 854 save_text($info_file, $php_info); 855 $info_params = []; 856 settings2array($ini_overwrites, $info_params); 857 $info_params = settings2params($info_params); 858 $php_info = `$php $pass_options $info_params $no_file_cache "$info_file"`; 859 define('TESTED_PHP_VERSION', `$php -n -r "echo PHP_VERSION;"`); 860 861 if ($php_cgi && $php != $php_cgi) { 862 $php_info_cgi = `$php_cgi $pass_options $info_params $no_file_cache -q "$info_file"`; 863 $php_info_sep = "\n---------------------------------------------------------------------"; 864 $php_cgi_info = "$php_info_sep\nPHP : $php_cgi $php_info_cgi$php_info_sep"; 865 } else { 866 $php_cgi_info = ''; 867 } 868 869 if ($phpdbg) { 870 $phpdbg_info = `$phpdbg $pass_options $info_params $no_file_cache -qrr "$info_file"`; 871 $php_info_sep = "\n---------------------------------------------------------------------"; 872 $phpdbg_info = "$php_info_sep\nPHP : $phpdbg $phpdbg_info$php_info_sep"; 873 } else { 874 $phpdbg_info = ''; 875 } 876 877 if (function_exists('opcache_invalidate')) { 878 opcache_invalidate($info_file, true); 879 } 880 @unlink($info_file); 881 882 // load list of enabled and loadable extensions 883 save_text($info_file, <<<'PHP' 884 <?php 885 echo str_replace("Zend OPcache", "opcache", implode(",", get_loaded_extensions())); 886 $ext_dir = ini_get("extension_dir"); 887 foreach (scandir($ext_dir) as $file) { 888 if (!preg_match('/^(?:php_)?([_a-zA-Z0-9]+)\.(?:so|dll)$/', $file, $matches)) { 889 continue; 890 } 891 $ext = $matches[1]; 892 if (!extension_loaded($ext) && @dl($file)) { 893 echo ",", $ext; 894 } 895 } 896 ?> 897 PHP); 898 $exts_to_test = explode(',', `$php $pass_options $info_params $no_file_cache "$info_file"`); 899 // check for extensions that need special handling and regenerate 900 $info_params_ex = [ 901 'session' => ['session.auto_start=0'], 902 'tidy' => ['tidy.clean_output=0'], 903 'zlib' => ['zlib.output_compression=Off'], 904 'xdebug' => ['xdebug.mode=off'], 905 ]; 906 907 foreach ($info_params_ex as $ext => $ini_overwrites_ex) { 908 if (in_array($ext, $exts_to_test)) { 909 $ini_overwrites = array_merge($ini_overwrites, $ini_overwrites_ex); 910 } 911 } 912 913 if (function_exists('opcache_invalidate')) { 914 opcache_invalidate($info_file, true); 915 } 916 @unlink($info_file); 917 918 // Write test context information. 919 echo " 920===================================================================== 921PHP : $php $php_info $php_cgi_info $phpdbg_info 922CWD : " . TEST_PHP_SRCDIR . " 923Extra dirs : "; 924 foreach ($user_tests as $test_dir) { 925 echo "{$test_dir}\n "; 926 } 927 echo " 928VALGRIND : " . ($valgrind ? $valgrind->getHeader() : 'Not used') . " 929===================================================================== 930"; 931} 932 933function save_or_mail_results(): void 934{ 935 global $sum_results, $just_save_results, $failed_test_summary, 936 $PHP_FAILED_TESTS, $php, $output_file; 937 938 /* We got failed Tests, offer the user to send an e-mail to QA team, unless NO_INTERACTION is set */ 939 if (!getenv('NO_INTERACTION') && !TRAVIS_CI) { 940 $fp = fopen("php://stdin", "r+"); 941 if ($sum_results['FAILED'] || $sum_results['BORKED'] || $sum_results['WARNED'] || $sum_results['LEAKED']) { 942 echo "\nYou may have found a problem in PHP."; 943 } 944 echo "\nThis report can be automatically sent to the PHP QA team at\n"; 945 echo QA_REPORTS_PAGE . " and http://news.php.net/php.qa.reports\n"; 946 echo "This gives us a better understanding of PHP's behavior.\n"; 947 echo "If you don't want to send the report immediately you can choose\n"; 948 echo "option \"s\" to save it. You can then email it to " . PHP_QA_EMAIL . " later.\n"; 949 echo "Do you want to send this report now? [Yns]: "; 950 flush(); 951 952 $user_input = fgets($fp, 10); 953 $just_save_results = (!empty($user_input) && strtolower($user_input[0]) === 's'); 954 } 955 956 if ($just_save_results || !getenv('NO_INTERACTION') || TRAVIS_CI) { 957 if ($just_save_results || TRAVIS_CI || strlen(trim($user_input)) == 0 || strtolower($user_input[0]) == 'y') { 958 /* 959 * Collect information about the host system for our report 960 * Fetch phpinfo() output so that we can see the PHP environment 961 * Make an archive of all the failed tests 962 * Send an email 963 */ 964 if ($just_save_results) { 965 $user_input = 's'; 966 } 967 968 /* Ask the user to provide an email address, so that QA team can contact the user */ 969 if (TRAVIS_CI) { 970 $user_email = 'travis at php dot net'; 971 } elseif (!strncasecmp($user_input, 'y', 1) || strlen(trim($user_input)) == 0) { 972 echo "\nPlease enter your email address.\n(Your address will be mangled so that it will not go out on any\nmailinglist in plain text): "; 973 flush(); 974 $user_email = trim(fgets($fp, 1024)); 975 $user_email = str_replace("@", " at ", str_replace(".", " dot ", $user_email)); 976 } 977 978 $failed_tests_data = ''; 979 $sep = "\n" . str_repeat('=', 80) . "\n"; 980 $failed_tests_data .= $failed_test_summary . "\n"; 981 $failed_tests_data .= get_summary(true) . "\n"; 982 983 if ($sum_results['FAILED']) { 984 foreach ($PHP_FAILED_TESTS['FAILED'] as $test_info) { 985 $failed_tests_data .= $sep . $test_info['name'] . $test_info['info']; 986 $failed_tests_data .= $sep . file_get_contents(realpath($test_info['output'])); 987 $failed_tests_data .= $sep . file_get_contents(realpath($test_info['diff'])); 988 $failed_tests_data .= $sep . "\n\n"; 989 } 990 $status = "failed"; 991 } else { 992 $status = "success"; 993 } 994 995 $failed_tests_data .= "\n" . $sep . 'BUILD ENVIRONMENT' . $sep; 996 $failed_tests_data .= "OS:\n" . PHP_OS . " - " . php_uname() . "\n\n"; 997 $ldd = $autoconf = $sys_libtool = $libtool = $compiler = 'N/A'; 998 999 if (!IS_WINDOWS) { 1000 /* If PHP_AUTOCONF is set, use it; otherwise, use 'autoconf'. */ 1001 if (getenv('PHP_AUTOCONF')) { 1002 $autoconf = shell_exec(getenv('PHP_AUTOCONF') . ' --version'); 1003 } else { 1004 $autoconf = shell_exec('autoconf --version'); 1005 } 1006 1007 /* Always use the generated libtool - Mac OSX uses 'glibtool' */ 1008 $libtool = shell_exec(INIT_DIR . '/libtool --version'); 1009 1010 /* Use shtool to find out if there is glibtool present (MacOSX) */ 1011 $sys_libtool_path = shell_exec(__DIR__ . '/build/shtool path glibtool libtool'); 1012 1013 if ($sys_libtool_path) { 1014 $sys_libtool = shell_exec(str_replace("\n", "", $sys_libtool_path) . ' --version'); 1015 } 1016 1017 /* Try the most common flags for 'version' */ 1018 $flags = ['-v', '-V', '--version']; 1019 $cc_status = 0; 1020 1021 foreach ($flags as $flag) { 1022 system(getenv('CC') . " $flag >/dev/null 2>&1", $cc_status); 1023 if ($cc_status == 0) { 1024 $compiler = shell_exec(getenv('CC') . " $flag 2>&1"); 1025 break; 1026 } 1027 } 1028 1029 $ldd = shell_exec("ldd $php 2>/dev/null"); 1030 } 1031 1032 $failed_tests_data .= "Autoconf:\n$autoconf\n"; 1033 $failed_tests_data .= "Bundled Libtool:\n$libtool\n"; 1034 $failed_tests_data .= "System Libtool:\n$sys_libtool\n"; 1035 $failed_tests_data .= "Compiler:\n$compiler\n"; 1036 $failed_tests_data .= "Bison:\n" . shell_exec('bison --version 2>/dev/null') . "\n"; 1037 $failed_tests_data .= "Libraries:\n$ldd\n"; 1038 $failed_tests_data .= "\n"; 1039 1040 if (isset($user_email)) { 1041 $failed_tests_data .= "User's E-mail: " . $user_email . "\n\n"; 1042 } 1043 1044 $failed_tests_data .= $sep . "PHPINFO" . $sep; 1045 $failed_tests_data .= shell_exec($php . ' -ddisplay_errors=stderr -dhtml_errors=0 -i 2> /dev/null'); 1046 1047 if (($just_save_results || !mail_qa_team($failed_tests_data, $status)) && !TRAVIS_CI) { 1048 file_put_contents($output_file, $failed_tests_data); 1049 1050 if (!$just_save_results) { 1051 echo "\nThe test script was unable to automatically send the report to PHP's QA Team\n"; 1052 } 1053 1054 echo "Please send " . $output_file . " to " . PHP_QA_EMAIL . " manually, thank you.\n"; 1055 } elseif (!getenv('NO_INTERACTION') && !TRAVIS_CI) { 1056 fwrite($fp, "\nThank you for helping to make PHP better.\n"); 1057 fclose($fp); 1058 } 1059 } 1060 } 1061} 1062 1063function get_binary(string $php, string $sapi, string $sapi_path): ?string 1064{ 1065 $dir = dirname($php); 1066 if (IS_WINDOWS && file_exists("$dir/$sapi.exe")) { 1067 return realpath("$dir/$sapi.exe"); 1068 } 1069 // Sources tree 1070 if (file_exists("$dir/../../$sapi_path")) { 1071 return realpath("$dir/../../$sapi_path"); 1072 } 1073 // Installation tree, preserve command prefix/suffix 1074 $inst = str_replace('php', $sapi, basename($php)); 1075 if (file_exists("$dir/$inst")) { 1076 return realpath("$dir/$inst"); 1077 } 1078 return null; 1079} 1080 1081function find_files(string $dir, bool $is_ext_dir = false, bool $ignore = false): void 1082{ 1083 global $test_files, $exts_to_test, $ignored_by_ext, $exts_skipped; 1084 1085 $o = opendir($dir) or error("cannot open directory: $dir"); 1086 1087 while (($name = readdir($o)) !== false) { 1088 if (is_dir("{$dir}/{$name}") && !in_array($name, ['.', '..', '.svn'])) { 1089 $skip_ext = ($is_ext_dir && !in_array(strtolower($name), $exts_to_test)); 1090 if ($skip_ext) { 1091 $exts_skipped++; 1092 } 1093 find_files("{$dir}/{$name}", false, $ignore || $skip_ext); 1094 } 1095 1096 // Cleanup any left-over tmp files from last run. 1097 if (substr($name, -4) == '.tmp') { 1098 @unlink("$dir/$name"); 1099 continue; 1100 } 1101 1102 // Otherwise we're only interested in *.phpt files. 1103 if (substr($name, -5) == '.phpt') { 1104 if ($ignore) { 1105 $ignored_by_ext++; 1106 } else { 1107 $testfile = realpath("{$dir}/{$name}"); 1108 $test_files[] = $testfile; 1109 } 1110 } 1111 } 1112 1113 closedir($o); 1114} 1115 1116/** 1117 * @param array|string $name 1118 */ 1119function test_name($name): string 1120{ 1121 if (is_array($name)) { 1122 return $name[0] . ':' . $name[1]; 1123 } else { 1124 return $name; 1125 } 1126} 1127/** 1128 * @param array|string $a 1129 * @param array|string $b 1130 */ 1131function test_sort($a, $b): int 1132{ 1133 $a = test_name($a); 1134 $b = test_name($b); 1135 1136 $ta = strpos($a, TEST_PHP_SRCDIR . "/tests") === 0 ? 1 + (strpos($a, 1137 TEST_PHP_SRCDIR . "/tests/run-test") === 0 ? 1 : 0) : 0; 1138 $tb = strpos($b, TEST_PHP_SRCDIR . "/tests") === 0 ? 1 + (strpos($b, 1139 TEST_PHP_SRCDIR . "/tests/run-test") === 0 ? 1 : 0) : 0; 1140 1141 if ($ta == $tb) { 1142 return strcmp($a, $b); 1143 } else { 1144 return $tb - $ta; 1145 } 1146} 1147 1148// 1149// Send Email to QA Team 1150// 1151 1152function mail_qa_team(string $data, bool $status = false): bool 1153{ 1154 $url_bits = parse_url(QA_SUBMISSION_PAGE); 1155 1156 if ($proxy = getenv('http_proxy')) { 1157 $proxy = parse_url($proxy); 1158 $path = $url_bits['host'] . $url_bits['path']; 1159 $host = $proxy['host']; 1160 if (empty($proxy['port'])) { 1161 $proxy['port'] = 80; 1162 } 1163 $port = $proxy['port']; 1164 } else { 1165 $path = $url_bits['path']; 1166 $host = $url_bits['host']; 1167 $port = empty($url_bits['port']) ? 80 : $port = $url_bits['port']; 1168 } 1169 1170 $data = "php_test_data=" . urlencode(base64_encode(str_replace("\00", '[0x0]', $data))); 1171 $data_length = strlen($data); 1172 1173 $fs = fsockopen($host, $port, $errno, $errstr, 10); 1174 1175 if (!$fs) { 1176 return false; 1177 } 1178 1179 $php_version = urlencode(TESTED_PHP_VERSION); 1180 1181 echo "\nPosting to " . QA_SUBMISSION_PAGE . "\n"; 1182 fwrite($fs, "POST " . $path . "?status=$status&version=$php_version HTTP/1.1\r\n"); 1183 fwrite($fs, "Host: " . $host . "\r\n"); 1184 fwrite($fs, "User-Agent: QA Browser 0.1\r\n"); 1185 fwrite($fs, "Content-Type: application/x-www-form-urlencoded\r\n"); 1186 fwrite($fs, "Content-Length: " . $data_length . "\r\n\r\n"); 1187 fwrite($fs, $data); 1188 fwrite($fs, "\r\n\r\n"); 1189 fclose($fs); 1190 1191 return true; 1192} 1193 1194// 1195// Write the given text to a temporary file, and return the filename. 1196// 1197 1198function save_text(string $filename, string $text, ?string $filename_copy = null): void 1199{ 1200 global $DETAILED; 1201 1202 if ($filename_copy && $filename_copy != $filename) { 1203 if (file_put_contents($filename_copy, $text) === false) { 1204 error("Cannot open file '" . $filename_copy . "' (save_text)"); 1205 } 1206 } 1207 1208 if (file_put_contents($filename, $text) === false) { 1209 error("Cannot open file '" . $filename . "' (save_text)"); 1210 } 1211 1212 if (1 < $DETAILED) { 1213 echo " 1214FILE $filename {{{ 1215$text 1216}}} 1217"; 1218 } 1219} 1220 1221// 1222// Write an error in a format recognizable to Emacs or MSVC. 1223// 1224 1225function error_report(string $testname, string $logname, string $tested): void 1226{ 1227 $testname = realpath($testname); 1228 $logname = realpath($logname); 1229 1230 switch (strtoupper(getenv('TEST_PHP_ERROR_STYLE'))) { 1231 case 'MSVC': 1232 echo $testname . "(1) : $tested\n"; 1233 echo $logname . "(1) : $tested\n"; 1234 break; 1235 case 'EMACS': 1236 echo $testname . ":1: $tested\n"; 1237 echo $logname . ":1: $tested\n"; 1238 break; 1239 } 1240} 1241 1242/** 1243 * @return false|string 1244 */ 1245function system_with_timeout( 1246 string $commandline, 1247 ?array $env = null, 1248 ?string $stdin = null, 1249 bool $captureStdIn = true, 1250 bool $captureStdOut = true, 1251 bool $captureStdErr = true 1252) { 1253 global $valgrind; 1254 1255 $data = ''; 1256 1257 $bin_env = []; 1258 foreach ((array) $env as $key => $value) { 1259 $bin_env[$key] = $value; 1260 } 1261 1262 $descriptorspec = []; 1263 if ($captureStdIn) { 1264 $descriptorspec[0] = ['pipe', 'r']; 1265 } 1266 if ($captureStdOut) { 1267 $descriptorspec[1] = ['pipe', 'w']; 1268 } 1269 if ($captureStdErr) { 1270 $descriptorspec[2] = ['pipe', 'w']; 1271 } 1272 $proc = proc_open($commandline, $descriptorspec, $pipes, TEST_PHP_SRCDIR, $bin_env, ['suppress_errors' => true]); 1273 1274 if (!$proc) { 1275 return false; 1276 } 1277 1278 if ($captureStdIn) { 1279 if (!is_null($stdin)) { 1280 fwrite($pipes[0], $stdin); 1281 } 1282 fclose($pipes[0]); 1283 unset($pipes[0]); 1284 } 1285 1286 $timeout = $valgrind ? 300 : ($env['TEST_TIMEOUT'] ?? 60); 1287 /* ASAN can cause a ~2-3x slowdown. */ 1288 if (isset($env['SKIP_ASAN'])) { 1289 $timeout *= 3; 1290 } 1291 1292 while (true) { 1293 /* hide errors from interrupted syscalls */ 1294 $r = $pipes; 1295 $w = null; 1296 $e = null; 1297 1298 $n = @stream_select($r, $w, $e, $timeout); 1299 1300 if ($n === false) { 1301 break; 1302 } elseif ($n === 0) { 1303 /* timed out */ 1304 $data .= "\n ** ERROR: process timed out **\n"; 1305 proc_terminate($proc, 9); 1306 return $data; 1307 } elseif ($n > 0) { 1308 if ($captureStdOut) { 1309 $line = fread($pipes[1], 8192); 1310 } elseif ($captureStdErr) { 1311 $line = fread($pipes[2], 8192); 1312 } else { 1313 $line = ''; 1314 } 1315 if (strlen($line) == 0) { 1316 /* EOF */ 1317 break; 1318 } 1319 $data .= $line; 1320 } 1321 } 1322 1323 $stat = proc_get_status($proc); 1324 1325 if ($stat['signaled']) { 1326 $data .= "\nTermsig=" . $stat['stopsig'] . "\n"; 1327 } 1328 if ($stat["exitcode"] > 128 && $stat["exitcode"] < 160) { 1329 $data .= "\nTermsig=" . ($stat["exitcode"] - 128) . "\n"; 1330 } else if (defined('PHP_WINDOWS_VERSION_MAJOR') && (($stat["exitcode"] >> 28) & 0b1111) === 0b1100) { 1331 // https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-erref/87fba13e-bf06-450e-83b1-9241dc81e781 1332 $data .= "\nTermsig=" . $stat["exitcode"] . "\n"; 1333 } 1334 1335 proc_close($proc); 1336 return $data; 1337} 1338 1339/** 1340 * @param string|array|null $redir_tested 1341 */ 1342function run_all_tests(array $test_files, array $env, $redir_tested = null): void 1343{ 1344 global $test_results, $failed_tests_file, $result_tests_file, $php, $test_idx, $file_cache; 1345 global $preload; 1346 // Parallel testing 1347 global $PHP_FAILED_TESTS, $workers, $workerID, $workerSock; 1348 1349 if ($file_cache !== null || $preload) { 1350 /* Automatically skip opcache tests in --file-cache and --preload mode, 1351 * because opcache generally expects these to run under a default configuration. */ 1352 $test_files = array_filter($test_files, function($test) use($preload) { 1353 if (!is_string($test)) { 1354 return true; 1355 } 1356 if (false !== strpos($test, 'ext/opcache')) { 1357 return false; 1358 } 1359 if ($preload && false !== strpos($test, 'ext/zend_test/tests/observer')) { 1360 return false; 1361 } 1362 return true; 1363 }); 1364 } 1365 1366 /* Ignore -jN if there is only one file to analyze. */ 1367 if ($workers !== null && count($test_files) > 1 && !$workerID) { 1368 run_all_tests_parallel($test_files, $env, $redir_tested); 1369 return; 1370 } 1371 1372 foreach ($test_files as $name) { 1373 if (is_array($name)) { 1374 $index = "# $name[1]: $name[0]"; 1375 1376 if ($redir_tested) { 1377 $name = $name[0]; 1378 } 1379 } elseif ($redir_tested) { 1380 $index = "# $redir_tested: $name"; 1381 } else { 1382 $index = $name; 1383 } 1384 $test_idx++; 1385 1386 if ($workerID) { 1387 $PHP_FAILED_TESTS = ['BORKED' => [], 'FAILED' => [], 'WARNED' => [], 'LEAKED' => [], 'XFAILED' => [], 'XLEAKED' => [], 'SLOW' => []]; 1388 ob_start(); 1389 } 1390 1391 $result = run_test($php, $name, $env); 1392 if ($workerID) { 1393 $resultText = ob_get_clean(); 1394 } 1395 1396 if (!is_array($name) && $result != 'REDIR') { 1397 if ($workerID) { 1398 send_message($workerSock, [ 1399 "type" => "test_result", 1400 "name" => $name, 1401 "index" => $index, 1402 "result" => $result, 1403 "text" => $resultText, 1404 "PHP_FAILED_TESTS" => $PHP_FAILED_TESTS 1405 ]); 1406 continue; 1407 } 1408 1409 $test_results[$index] = $result; 1410 if ($failed_tests_file && ($result == 'XFAILED' || $result == 'XLEAKED' || $result == 'FAILED' || $result == 'WARNED' || $result == 'LEAKED')) { 1411 fwrite($failed_tests_file, "$index\n"); 1412 } 1413 if ($result_tests_file) { 1414 fwrite($result_tests_file, "$result\t$index\n"); 1415 } 1416 } 1417 } 1418} 1419 1420/** The heart of parallel testing. 1421 * @param string|array|null $redir_tested 1422 */ 1423function run_all_tests_parallel(array $test_files, array $env, $redir_tested): void 1424{ 1425 global $workers, $test_idx, $test_cnt, $test_results, $failed_tests_file, $result_tests_file, $PHP_FAILED_TESTS, $shuffle, $SHOW_ONLY_GROUPS, $valgrind; 1426 1427 global $junit; 1428 1429 // The PHP binary running run-tests.php, and run-tests.php itself 1430 // This PHP executable is *not* necessarily the same as the tested version 1431 $thisPHP = PHP_BINARY; 1432 $thisScript = __FILE__; 1433 1434 $workerProcs = []; 1435 $workerSocks = []; 1436 1437 // Each test may specify a list of conflict keys. While a test that conflicts with 1438 // key K is running, no other test that conflicts with K may run. Conflict keys are 1439 // specified either in the --CONFLICTS-- section, or CONFLICTS file inside a directory. 1440 $dirConflictsWith = []; 1441 $fileConflictsWith = []; 1442 $sequentialTests = []; 1443 foreach ($test_files as $i => $file) { 1444 $contents = file_get_contents($file); 1445 if (preg_match('/^--CONFLICTS--(.+?)^--/ms', $contents, $matches)) { 1446 $conflicts = parse_conflicts($matches[1]); 1447 } else { 1448 // Cache per-directory conflicts in a separate map, so we compute these only once. 1449 $dir = dirname($file); 1450 if (!isset($dirConflictsWith[$dir])) { 1451 $dirConflicts = []; 1452 if (file_exists($dir . '/CONFLICTS')) { 1453 $contents = file_get_contents($dir . '/CONFLICTS'); 1454 $dirConflicts = parse_conflicts($contents); 1455 } 1456 $dirConflictsWith[$dir] = $dirConflicts; 1457 } 1458 $conflicts = $dirConflictsWith[$dir]; 1459 } 1460 1461 // For tests conflicting with "all", no other tests may run in parallel. We'll run these 1462 // tests separately at the end, when only one worker is left. 1463 if (in_array('all', $conflicts, true)) { 1464 $sequentialTests[] = $file; 1465 unset($test_files[$i]); 1466 } 1467 1468 $fileConflictsWith[$file] = $conflicts; 1469 } 1470 1471 // Some tests assume that they are executed in a certain order. We will be popping from 1472 // $test_files, so reverse its order here. This makes sure that order is preserved at least 1473 // for tests with a common conflict key. 1474 $test_files = array_reverse($test_files); 1475 1476 // To discover parallelization issues it is useful to randomize the test order. 1477 if ($shuffle) { 1478 shuffle($test_files); 1479 } 1480 1481 // Don't start more workers than test files. 1482 $workers = max(1, min($workers, count($test_files))); 1483 1484 echo "Spawning $workers workers... "; 1485 1486 // We use sockets rather than STDIN/STDOUT for comms because on Windows, 1487 // those can't be non-blocking for some reason. 1488 $listenSock = stream_socket_server("tcp://127.0.0.1:0") or error("Couldn't create socket on localhost."); 1489 $sockName = stream_socket_get_name($listenSock, false); 1490 // PHP is terrible and returns IPv6 addresses not enclosed by [] 1491 $portPos = strrpos($sockName, ":"); 1492 $sockHost = substr($sockName, 0, $portPos); 1493 if (false !== strpos($sockHost, ":")) { 1494 $sockHost = "[$sockHost]"; 1495 } 1496 $sockPort = substr($sockName, $portPos + 1); 1497 $sockUri = "tcp://$sockHost:$sockPort"; 1498 $totalFileCount = count($test_files); 1499 1500 $startTime = microtime(true); 1501 for ($i = 1; $i <= $workers; $i++) { 1502 $proc = proc_open( 1503 [$thisPHP, $thisScript], 1504 [], // Inherit our stdin, stdout and stderr 1505 $pipes, 1506 null, 1507 $GLOBALS['environment'] + [ 1508 "TEST_PHP_WORKER" => $i, 1509 "TEST_PHP_URI" => $sockUri, 1510 ], 1511 [ 1512 "suppress_errors" => true, 1513 'create_new_console' => true, 1514 ] 1515 ); 1516 if ($proc === false) { 1517 kill_children($workerProcs); 1518 error("Failed to spawn worker $i"); 1519 } 1520 $workerProcs[$i] = $proc; 1521 } 1522 1523 for ($i = 1; $i <= $workers; $i++) { 1524 $workerSock = stream_socket_accept($listenSock, 5); 1525 if ($workerSock === false) { 1526 kill_children($workerProcs); 1527 error("Failed to accept connection from worker."); 1528 } 1529 1530 $greeting = base64_encode(serialize([ 1531 "type" => "hello", 1532 "GLOBALS" => $GLOBALS, 1533 "constants" => [ 1534 "INIT_DIR" => INIT_DIR, 1535 "TEST_PHP_SRCDIR" => TEST_PHP_SRCDIR, 1536 "PHP_QA_EMAIL" => PHP_QA_EMAIL, 1537 "QA_SUBMISSION_PAGE" => QA_SUBMISSION_PAGE, 1538 "QA_REPORTS_PAGE" => QA_REPORTS_PAGE, 1539 "TRAVIS_CI" => TRAVIS_CI 1540 ] 1541 ])) . "\n"; 1542 1543 stream_set_timeout($workerSock, 5); 1544 if (fwrite($workerSock, $greeting) === false) { 1545 kill_children($workerProcs); 1546 error("Failed to send greeting to worker."); 1547 } 1548 1549 $rawReply = fgets($workerSock); 1550 if ($rawReply === false) { 1551 kill_children($workerProcs); 1552 error("Failed to read greeting reply from worker."); 1553 } 1554 1555 $reply = unserialize(base64_decode($rawReply)); 1556 if (!$reply || $reply["type"] !== "hello_reply") { 1557 kill_children($workerProcs); 1558 error("Greeting reply from worker unexpected or could not be decoded: '$rawReply'"); 1559 } 1560 1561 stream_set_timeout($workerSock, 0); 1562 stream_set_blocking($workerSock, false); 1563 1564 $workerID = $reply["workerID"]; 1565 $workerSocks[$workerID] = $workerSock; 1566 } 1567 printf("Done in %.2fs\n", microtime(true) - $startTime); 1568 echo "=====================================================================\n"; 1569 echo "\n"; 1570 1571 $rawMessageBuffers = []; 1572 $testsInProgress = 0; 1573 1574 // Map from conflict key to worker ID. 1575 $activeConflicts = []; 1576 // Tests waiting due to conflicts. Map from conflict key to array. 1577 $waitingTests = []; 1578 1579escape: 1580 while ($test_files || $sequentialTests || $testsInProgress > 0) { 1581 $toRead = array_values($workerSocks); 1582 $toWrite = null; 1583 $toExcept = null; 1584 if (stream_select($toRead, $toWrite, $toExcept, 10)) { 1585 foreach ($toRead as $workerSock) { 1586 $i = array_search($workerSock, $workerSocks); 1587 if ($i === false) { 1588 kill_children($workerProcs); 1589 error("Could not find worker stdout in array of worker stdouts, THIS SHOULD NOT HAPPEN."); 1590 } 1591 if (feof($workerSock)) { 1592 kill_children($workerProcs); 1593 error("Worker $i died unexpectedly"); 1594 } 1595 while (false !== ($rawMessage = fgets($workerSock))) { 1596 // work around fgets truncating things 1597 if (($rawMessageBuffers[$i] ?? '') !== '') { 1598 $rawMessage = $rawMessageBuffers[$i] . $rawMessage; 1599 $rawMessageBuffers[$i] = ''; 1600 } 1601 if (substr($rawMessage, -1) !== "\n") { 1602 $rawMessageBuffers[$i] = $rawMessage; 1603 continue; 1604 } 1605 1606 $message = unserialize(base64_decode($rawMessage)); 1607 if (!$message) { 1608 kill_children($workerProcs); 1609 $stuff = fread($workerSock, 65536); 1610 error("Could not decode message from worker $i: '$rawMessage$stuff'"); 1611 } 1612 1613 switch ($message["type"]) { 1614 case "tests_finished": 1615 $testsInProgress--; 1616 foreach ($activeConflicts as $key => $workerId) { 1617 if ($workerId === $i) { 1618 unset($activeConflicts[$key]); 1619 if (isset($waitingTests[$key])) { 1620 while ($test = array_pop($waitingTests[$key])) { 1621 $test_files[] = $test; 1622 } 1623 unset($waitingTests[$key]); 1624 } 1625 } 1626 } 1627 $junit->mergeResults($message["junit"]); 1628 // no break 1629 case "ready": 1630 // Schedule sequential tests only once we are down to one worker. 1631 if (count($workerProcs) === 1 && $sequentialTests) { 1632 $test_files = array_merge($test_files, $sequentialTests); 1633 $sequentialTests = []; 1634 } 1635 // Batch multiple tests to reduce communication overhead. 1636 // - When valgrind is used, communication overhead is relatively small, 1637 // so just use a batch size of 1. 1638 // - If this is running a small enough number of tests, 1639 // reduce the batch size to give batches to more workers. 1640 $files = []; 1641 $maxBatchSize = $valgrind ? 1 : ($shuffle ? 4 : 32); 1642 $averageFilesPerWorker = max(1, (int) ceil($totalFileCount / count($workerProcs))); 1643 $batchSize = min($maxBatchSize, $averageFilesPerWorker); 1644 while (count($files) <= $batchSize && $file = array_pop($test_files)) { 1645 foreach ($fileConflictsWith[$file] as $conflictKey) { 1646 if (isset($activeConflicts[$conflictKey])) { 1647 $waitingTests[$conflictKey][] = $file; 1648 continue 2; 1649 } 1650 } 1651 $files[] = $file; 1652 } 1653 if ($files) { 1654 foreach ($files as $file) { 1655 foreach ($fileConflictsWith[$file] as $conflictKey) { 1656 $activeConflicts[$conflictKey] = $i; 1657 } 1658 } 1659 $testsInProgress++; 1660 send_message($workerSocks[$i], [ 1661 "type" => "run_tests", 1662 "test_files" => $files, 1663 "env" => $env, 1664 "redir_tested" => $redir_tested 1665 ]); 1666 } else { 1667 proc_terminate($workerProcs[$i]); 1668 unset($workerProcs[$i]); 1669 unset($workerSocks[$i]); 1670 goto escape; 1671 } 1672 break; 1673 case "test_result": 1674 list($name, $index, $result, $resultText) = [$message["name"], $message["index"], $message["result"], $message["text"]]; 1675 foreach ($message["PHP_FAILED_TESTS"] as $category => $tests) { 1676 $PHP_FAILED_TESTS[$category] = array_merge($PHP_FAILED_TESTS[$category], $tests); 1677 } 1678 $test_idx++; 1679 1680 if (!$SHOW_ONLY_GROUPS) { 1681 clear_show_test(); 1682 } 1683 1684 echo $resultText; 1685 1686 if (!$SHOW_ONLY_GROUPS) { 1687 show_test($test_idx, count($workerProcs) . "/$workers concurrent test workers running"); 1688 } 1689 1690 if (!is_array($name) && $result != 'REDIR') { 1691 $test_results[$index] = $result; 1692 1693 if ($failed_tests_file && ($result == 'XFAILED' || $result == 'XLEAKED' || $result == 'FAILED' || $result == 'WARNED' || $result == 'LEAKED')) { 1694 fwrite($failed_tests_file, "$index\n"); 1695 } 1696 if ($result_tests_file) { 1697 fwrite($result_tests_file, "$result\t$index\n"); 1698 } 1699 } 1700 break; 1701 case "error": 1702 kill_children($workerProcs); 1703 error("Worker $i reported error: $message[msg]"); 1704 break; 1705 case "php_error": 1706 kill_children($workerProcs); 1707 $error_consts = [ 1708 'E_ERROR', 1709 'E_WARNING', 1710 'E_PARSE', 1711 'E_NOTICE', 1712 'E_CORE_ERROR', 1713 'E_CORE_WARNING', 1714 'E_COMPILE_ERROR', 1715 'E_COMPILE_WARNING', 1716 'E_USER_ERROR', 1717 'E_USER_WARNING', 1718 'E_USER_NOTICE', 1719 'E_STRICT', // TODO Cleanup when removed from Zend Engine. 1720 'E_RECOVERABLE_ERROR', 1721 'E_DEPRECATED', 1722 'E_USER_DEPRECATED' 1723 ]; 1724 $error_consts = array_combine(array_map('constant', $error_consts), $error_consts); 1725 error("Worker $i reported unexpected {$error_consts[$message['errno']]}: $message[errstr] in $message[errfile] on line $message[errline]"); 1726 // no break 1727 default: 1728 kill_children($workerProcs); 1729 error("Unrecognised message type '$message[type]' from worker $i"); 1730 } 1731 } 1732 } 1733 } 1734 } 1735 1736 if (!$SHOW_ONLY_GROUPS) { 1737 clear_show_test(); 1738 } 1739 1740 kill_children($workerProcs); 1741 1742 if ($testsInProgress < 0) { 1743 error("$testsInProgress test batches “in progress”, which is less than zero. THIS SHOULD NOT HAPPEN."); 1744 } 1745} 1746 1747function send_message($stream, array $message): void 1748{ 1749 $blocking = stream_get_meta_data($stream)["blocked"]; 1750 stream_set_blocking($stream, true); 1751 fwrite($stream, base64_encode(serialize($message)) . "\n"); 1752 stream_set_blocking($stream, $blocking); 1753} 1754 1755function kill_children(array $children): void 1756{ 1757 foreach ($children as $child) { 1758 if ($child) { 1759 proc_terminate($child); 1760 } 1761 } 1762} 1763 1764function run_worker(): void 1765{ 1766 global $workerID, $workerSock; 1767 1768 global $junit; 1769 1770 $sockUri = getenv("TEST_PHP_URI"); 1771 1772 $workerSock = stream_socket_client($sockUri, $_, $_, 5) or error("Couldn't connect to $sockUri"); 1773 1774 $greeting = fgets($workerSock); 1775 $greeting = unserialize(base64_decode($greeting)) or die("Could not decode greeting\n"); 1776 if ($greeting["type"] !== "hello") { 1777 error("Unexpected greeting of type $greeting[type]"); 1778 } 1779 1780 set_error_handler(function (int $errno, string $errstr, string $errfile, int $errline) use ($workerSock): bool { 1781 if (error_reporting() & $errno) { 1782 send_message($workerSock, compact('errno', 'errstr', 'errfile', 'errline') + [ 1783 'type' => 'php_error' 1784 ]); 1785 } 1786 1787 return true; 1788 }); 1789 1790 foreach ($greeting["GLOBALS"] as $var => $value) { 1791 if ($var !== "workerID" && $var !== "workerSock" && $var !== "GLOBALS") { 1792 $GLOBALS[$var] = $value; 1793 } 1794 } 1795 foreach ($greeting["constants"] as $const => $value) { 1796 define($const, $value); 1797 } 1798 1799 send_message($workerSock, [ 1800 "type" => "hello_reply", 1801 "workerID" => $workerID 1802 ]); 1803 1804 send_message($workerSock, [ 1805 "type" => "ready" 1806 ]); 1807 1808 while (($command = fgets($workerSock))) { 1809 $command = unserialize(base64_decode($command)); 1810 1811 switch ($command["type"]) { 1812 case "run_tests": 1813 run_all_tests($command["test_files"], $command["env"], $command["redir_tested"]); 1814 send_message($workerSock, [ 1815 "type" => "tests_finished", 1816 "junit" => $junit->isEnabled() ? $junit : null, 1817 ]); 1818 $junit->clear(); 1819 break; 1820 default: 1821 send_message($workerSock, [ 1822 "type" => "error", 1823 "msg" => "Unrecognised message type: $command[type]" 1824 ]); 1825 break 2; 1826 } 1827 } 1828} 1829 1830// 1831// Show file or result block 1832// 1833function show_file_block(string $file, string $block, ?string $section = null): void 1834{ 1835 global $cfg; 1836 global $colorize; 1837 1838 if ($cfg['show'][$file]) { 1839 if (is_null($section)) { 1840 $section = strtoupper($file); 1841 } 1842 if ($section === 'DIFF' && $colorize) { 1843 // '-' is Light Red for removal, '+' is Light Green for addition 1844 $block = preg_replace('/^[0-9]+\-\s.*$/m', "\e[1;31m\\0\e[0m", $block); 1845 $block = preg_replace('/^[0-9]+\+\s.*$/m', "\e[1;32m\\0\e[0m", $block); 1846 } 1847 1848 echo "\n========" . $section . "========\n"; 1849 echo rtrim($block); 1850 echo "\n========DONE========\n"; 1851 } 1852} 1853 1854function skip_test(string $tested, string $tested_file, string $shortname, string $reason) { 1855 global $junit; 1856 1857 show_result('SKIP', $tested, $tested_file, "reason: $reason"); 1858 $junit->initSuite($junit->getSuiteName($shortname)); 1859 $junit->markTestAs('SKIP', $shortname, $tested, 0, $reason); 1860 return 'SKIPPED'; 1861} 1862 1863// 1864// Run an individual test case. 1865// 1866/** 1867 * @param string|array $file 1868 */ 1869function run_test(string $php, $file, array $env): string 1870{ 1871 global $log_format, $ini_overwrites, $PHP_FAILED_TESTS; 1872 global $pass_options, $DETAILED, $IN_REDIRECT, $test_cnt, $test_idx; 1873 global $valgrind, $temp_source, $temp_target, $cfg, $environment; 1874 global $no_clean; 1875 global $SHOW_ONLY_GROUPS; 1876 global $no_file_cache; 1877 global $slow_min_ms; 1878 global $preload, $file_cache; 1879 global $num_repeats; 1880 // Parallel testing 1881 global $workerID; 1882 1883 // Temporary 1884 /** @var JUnit */ 1885 global $junit; 1886 1887 static $skipCache; 1888 if (!$skipCache) { 1889 $enableSkipCache = !($env['DISABLE_SKIP_CACHE'] ?? '0'); 1890 $skipCache = new SkipCache($enableSkipCache, $cfg['keep']['skip']); 1891 } 1892 1893 $retried = false; 1894retry: 1895 1896 $temp_filenames = null; 1897 $org_file = $file; 1898 $orig_php = $php; 1899 1900 if (isset($env['TEST_PHP_CGI_EXECUTABLE'])) { 1901 $php_cgi = $env['TEST_PHP_CGI_EXECUTABLE']; 1902 } 1903 1904 if (isset($env['TEST_PHPDBG_EXECUTABLE'])) { 1905 $phpdbg = $env['TEST_PHPDBG_EXECUTABLE']; 1906 } 1907 1908 if (is_array($file)) { 1909 $file = $file[0]; 1910 } 1911 1912 if ($DETAILED) { 1913 echo " 1914================= 1915TEST $file 1916"; 1917 } 1918 1919 $shortname = str_replace(TEST_PHP_SRCDIR . '/', '', $file); 1920 $tested_file = $shortname; 1921 1922 try { 1923 $test = new TestFile($file, (bool)$IN_REDIRECT); 1924 } catch (BorkageException $ex) { 1925 show_result("BORK", $ex->getMessage(), $tested_file); 1926 $PHP_FAILED_TESTS['BORKED'][] = [ 1927 'name' => $file, 1928 'test_name' => '', 1929 'output' => '', 1930 'diff' => '', 1931 'info' => "{$ex->getMessage()} [$file]", 1932 ]; 1933 1934 $junit->markTestAs('BORK', $shortname, $tested_file, 0, $ex->getMessage()); 1935 return 'BORKED'; 1936 } 1937 1938 $tested = $test->getName(); 1939 1940 if ($test->hasSection('FILE_EXTERNAL')) { 1941 if ($num_repeats > 1) { 1942 return skip_test($tested, $tested_file, $shortname, 'Test with FILE_EXTERNAL might not be repeatable'); 1943 } 1944 } 1945 1946 if ($test->hasSection('CAPTURE_STDIO')) { 1947 $capture = $test->getSection('CAPTURE_STDIO'); 1948 $captureStdIn = stripos($capture, 'STDIN') !== false; 1949 $captureStdOut = stripos($capture, 'STDOUT') !== false; 1950 $captureStdErr = stripos($capture, 'STDERR') !== false; 1951 } else { 1952 $captureStdIn = true; 1953 $captureStdOut = true; 1954 $captureStdErr = true; 1955 } 1956 if ($captureStdOut && $captureStdErr) { 1957 $cmdRedirect = ' 2>&1'; 1958 } else { 1959 $cmdRedirect = ''; 1960 } 1961 1962 /* For GET/POST/PUT tests, check if cgi sapi is available and if it is, use it. */ 1963 if ($test->isCGI()) { 1964 if (!$php_cgi) { 1965 return skip_test($tested, $tested_file, $shortname, 'CGI not available'); 1966 } 1967 $php = $php_cgi . ' -C '; 1968 $uses_cgi = true; 1969 if ($num_repeats > 1) { 1970 return skip_test($tested, $tested_file, $shortname, 'CGI does not support --repeat'); 1971 } 1972 } 1973 1974 /* For phpdbg tests, check if phpdbg sapi is available and if it is, use it. */ 1975 $extra_options = ''; 1976 if ($test->hasSection('PHPDBG')) { 1977 if (isset($phpdbg)) { 1978 $php = $phpdbg . ' -qIb'; 1979 1980 // Additional phpdbg command line options for sections that need to 1981 // be run straight away. For example, EXTENSIONS, SKIPIF, CLEAN. 1982 $extra_options = '-rr'; 1983 } else { 1984 return skip_test($tested, $tested_file, $shortname, 'phpdbg not available'); 1985 } 1986 if ($num_repeats > 1) { 1987 return skip_test($tested, $tested_file, $shortname, 'phpdbg does not support --repeat'); 1988 } 1989 } 1990 1991 foreach (['CLEAN', 'STDIN', 'CAPTURE_STDIO'] as $section) { 1992 if ($test->hasSection($section)) { 1993 if ($num_repeats > 1) { 1994 return skip_test($tested, $tested_file, $shortname, "Test with $section might not be repeatable"); 1995 } 1996 } 1997 } 1998 1999 if (!$SHOW_ONLY_GROUPS && !$workerID) { 2000 show_test($test_idx, $shortname); 2001 } 2002 2003 if (is_array($IN_REDIRECT)) { 2004 $temp_dir = $test_dir = $IN_REDIRECT['dir']; 2005 } else { 2006 $temp_dir = $test_dir = realpath(dirname($file)); 2007 } 2008 2009 if ($temp_source && $temp_target) { 2010 $temp_dir = str_replace($temp_source, $temp_target, $temp_dir); 2011 } 2012 2013 $main_file_name = basename($file, 'phpt'); 2014 2015 $diff_filename = $temp_dir . DIRECTORY_SEPARATOR . $main_file_name . 'diff'; 2016 $log_filename = $temp_dir . DIRECTORY_SEPARATOR . $main_file_name . 'log'; 2017 $exp_filename = $temp_dir . DIRECTORY_SEPARATOR . $main_file_name . 'exp'; 2018 $output_filename = $temp_dir . DIRECTORY_SEPARATOR . $main_file_name . 'out'; 2019 $memcheck_filename = $temp_dir . DIRECTORY_SEPARATOR . $main_file_name . 'mem'; 2020 $sh_filename = $temp_dir . DIRECTORY_SEPARATOR . $main_file_name . 'sh'; 2021 $temp_file = $temp_dir . DIRECTORY_SEPARATOR . $main_file_name . 'php'; 2022 $test_file = $test_dir . DIRECTORY_SEPARATOR . $main_file_name . 'php'; 2023 $temp_skipif = $temp_dir . DIRECTORY_SEPARATOR . $main_file_name . 'skip.php'; 2024 $test_skipif = $test_dir . DIRECTORY_SEPARATOR . $main_file_name . 'skip.php'; 2025 $temp_clean = $temp_dir . DIRECTORY_SEPARATOR . $main_file_name . 'clean.php'; 2026 $test_clean = $test_dir . DIRECTORY_SEPARATOR . $main_file_name . 'clean.php'; 2027 $preload_filename = $temp_dir . DIRECTORY_SEPARATOR . $main_file_name . 'preload.php'; 2028 $tmp_post = $temp_dir . DIRECTORY_SEPARATOR . $main_file_name . 'post'; 2029 $tmp_relative_file = str_replace(__DIR__ . DIRECTORY_SEPARATOR, '', $test_file) . 't'; 2030 2031 if ($temp_source && $temp_target) { 2032 $temp_skipif .= 's'; 2033 $temp_file .= 's'; 2034 $temp_clean .= 's'; 2035 $copy_file = $temp_dir . DIRECTORY_SEPARATOR . basename(is_array($file) ? $file[1] : $file) . '.phps'; 2036 2037 if (!is_dir(dirname($copy_file))) { 2038 mkdir(dirname($copy_file), 0777, true) or error("Cannot create output directory - " . dirname($copy_file)); 2039 } 2040 2041 if ($test->hasSection('FILE')) { 2042 save_text($copy_file, $test->getSection('FILE')); 2043 } 2044 2045 $temp_filenames = [ 2046 'file' => $copy_file, 2047 'diff' => $diff_filename, 2048 'log' => $log_filename, 2049 'exp' => $exp_filename, 2050 'out' => $output_filename, 2051 'mem' => $memcheck_filename, 2052 'sh' => $sh_filename, 2053 'php' => $temp_file, 2054 'skip' => $temp_skipif, 2055 'clean' => $temp_clean 2056 ]; 2057 } 2058 2059 if (is_array($IN_REDIRECT)) { 2060 $tested = $IN_REDIRECT['prefix'] . ' ' . $tested; 2061 $tested_file = $tmp_relative_file; 2062 $shortname = str_replace(TEST_PHP_SRCDIR . '/', '', $tested_file); 2063 } 2064 2065 // unlink old test results 2066 @unlink($diff_filename); 2067 @unlink($log_filename); 2068 @unlink($exp_filename); 2069 @unlink($output_filename); 2070 @unlink($memcheck_filename); 2071 @unlink($sh_filename); 2072 @unlink($temp_file); 2073 @unlink($test_file); 2074 @unlink($temp_skipif); 2075 @unlink($test_skipif); 2076 @unlink($tmp_post); 2077 @unlink($temp_clean); 2078 @unlink($test_clean); 2079 @unlink($preload_filename); 2080 2081 // Reset environment from any previous test. 2082 $env['REDIRECT_STATUS'] = ''; 2083 $env['QUERY_STRING'] = ''; 2084 $env['PATH_TRANSLATED'] = ''; 2085 $env['SCRIPT_FILENAME'] = ''; 2086 $env['REQUEST_METHOD'] = ''; 2087 $env['CONTENT_TYPE'] = ''; 2088 $env['CONTENT_LENGTH'] = ''; 2089 $env['TZ'] = ''; 2090 2091 if ($test->sectionNotEmpty('ENV')) { 2092 $env_str = str_replace('{PWD}', dirname($file), $test->getSection('ENV')); 2093 foreach (explode("\n", $env_str) as $e) { 2094 $e = explode('=', trim($e), 2); 2095 2096 if (!empty($e[0]) && isset($e[1])) { 2097 $env[$e[0]] = $e[1]; 2098 } 2099 } 2100 } 2101 2102 // Default ini settings 2103 $ini_settings = $workerID ? ['opcache.cache_id' => "worker$workerID"] : []; 2104 2105 // Additional required extensions 2106 $extensions = []; 2107 if ($test->hasSection('EXTENSIONS')) { 2108 $extensions = preg_split("/[\n\r]+/", trim($test->getSection('EXTENSIONS'))); 2109 } 2110 if (is_array($IN_REDIRECT) && $IN_REDIRECT['EXTENSIONS'] != []) { 2111 $extensions = array_merge($extensions, $IN_REDIRECT['EXTENSIONS']); 2112 } 2113 2114 /* Load required extensions */ 2115 if ($extensions != []) { 2116 $ext_params = []; 2117 settings2array($ini_overwrites, $ext_params); 2118 $ext_params = settings2params($ext_params); 2119 [$ext_dir, $loaded] = $skipCache->getExtensions("$orig_php $pass_options $extra_options $ext_params $no_file_cache"); 2120 $ext_prefix = IS_WINDOWS ? "php_" : ""; 2121 $missing = []; 2122 foreach ($extensions as $req_ext) { 2123 if (!in_array(strtolower($req_ext), $loaded)) { 2124 if ($req_ext == 'opcache' || $req_ext == 'xdebug') { 2125 $ext_file = $ext_dir . DIRECTORY_SEPARATOR . $ext_prefix . $req_ext . '.' . PHP_SHLIB_SUFFIX; 2126 $ini_settings['zend_extension'][] = $ext_file; 2127 } else { 2128 $ext_file = $ext_dir . DIRECTORY_SEPARATOR . $ext_prefix . $req_ext . '.' . PHP_SHLIB_SUFFIX; 2129 $ini_settings['extension'][] = $ext_file; 2130 } 2131 if (!is_readable($ext_file)) { 2132 $missing[] = $req_ext; 2133 } 2134 } 2135 } 2136 if ($missing) { 2137 $message = 'Required extension' . (count($missing) > 1 ? 's' : '') 2138 . ' missing: ' . implode(', ', $missing); 2139 return skip_test($tested, $tested_file, $shortname, $message); 2140 } 2141 } 2142 2143 // additional ini overwrites 2144 //$ini_overwrites[] = 'setting=value'; 2145 settings2array($ini_overwrites, $ini_settings); 2146 2147 $orig_ini_settings = settings2params($ini_settings); 2148 2149 if ($file_cache !== null) { 2150 $ini_settings['opcache.file_cache'] = '/tmp'; 2151 // Make sure warnings still show up on the second run. 2152 $ini_settings['opcache.record_warnings'] = '1'; 2153 // File cache is currently incompatible with JIT. 2154 $ini_settings['opcache.jit'] = '0'; 2155 if ($file_cache === 'use') { 2156 // Disable timestamp validation in order to fetch from file cache, 2157 // even though all the files are re-created. 2158 $ini_settings['opcache.validate_timestamps'] = '0'; 2159 } 2160 } else if ($num_repeats > 1) { 2161 // Make sure warnings still show up on the second run. 2162 $ini_settings['opcache.record_warnings'] = '1'; 2163 } 2164 2165 // Any special ini settings 2166 // these may overwrite the test defaults... 2167 if ($test->hasSection('INI')) { 2168 $ini = str_replace('{PWD}', dirname($file), $test->getSection('INI')); 2169 $ini = str_replace('{TMP}', sys_get_temp_dir(), $ini); 2170 $replacement = IS_WINDOWS ? '"' . PHP_BINARY . ' -r \"while ($in = fgets(STDIN)) echo $in;\" > $1"' : 'tee $1 >/dev/null'; 2171 $ini = preg_replace('/{MAIL:(\S+)}/', $replacement, $ini); 2172 settings2array(preg_split("/[\n\r]+/", $ini), $ini_settings); 2173 2174 if (isset($ini_settings['opcache.opt_debug_level'])) { 2175 if ($num_repeats > 1) { 2176 return skip_test($tested, $tested_file, $shortname, 'opt_debug_level tests are not repeatable'); 2177 } 2178 } 2179 } 2180 2181 $ini_settings = settings2params($ini_settings); 2182 2183 $env['TEST_PHP_EXTRA_ARGS'] = $pass_options . ' ' . $ini_settings; 2184 2185 // Check if test should be skipped. 2186 $info = ''; 2187 $warn = false; 2188 2189 if ($test->sectionNotEmpty('SKIPIF')) { 2190 show_file_block('skip', $test->getSection('SKIPIF')); 2191 $extra = !IS_WINDOWS ? 2192 "unset REQUEST_METHOD; unset QUERY_STRING; unset PATH_TRANSLATED; unset SCRIPT_FILENAME; unset REQUEST_METHOD;" : ""; 2193 2194 if ($valgrind) { 2195 $env['USE_ZEND_ALLOC'] = '0'; 2196 $env['ZEND_DONT_UNLOAD_MODULES'] = 1; 2197 } 2198 2199 $junit->startTimer($shortname); 2200 2201 $startTime = microtime(true); 2202 $commandLine = "$extra $php $pass_options $extra_options -q $orig_ini_settings $no_file_cache -d display_errors=1 -d display_startup_errors=0"; 2203 $output = $skipCache->checkSkip($commandLine, $test->getSection('SKIPIF'), $test_skipif, $temp_skipif, $env); 2204 2205 $time = microtime(true) - $startTime; 2206 $junit->stopTimer($shortname); 2207 2208 if ($time > $slow_min_ms / 1000) { 2209 $PHP_FAILED_TESTS['SLOW'][] = [ 2210 'name' => $file, 2211 'test_name' => 'SKIPIF of ' . $tested . " [$tested_file]", 2212 'output' => '', 2213 'diff' => '', 2214 'info' => $time, 2215 ]; 2216 } 2217 2218 if (!$cfg['keep']['skip']) { 2219 @unlink($test_skipif); 2220 } 2221 2222 if (!strncasecmp('skip', $output, 4)) { 2223 if (preg_match('/^skip\s*(.+)/i', $output, $m)) { 2224 show_result('SKIP', $tested, $tested_file, "reason: $m[1]", $temp_filenames); 2225 } else { 2226 show_result('SKIP', $tested, $tested_file, '', $temp_filenames); 2227 } 2228 2229 $message = !empty($m[1]) ? $m[1] : ''; 2230 $junit->markTestAs('SKIP', $shortname, $tested, null, $message); 2231 return 'SKIPPED'; 2232 } 2233 2234 2235 if (!strncasecmp('info', $output, 4) && preg_match('/^info\s*(.+)/i', $output, $m)) { 2236 $info = " (info: $m[1])"; 2237 } elseif (!strncasecmp('warn', $output, 4) && preg_match('/^warn\s+(.+)/i', $output, $m)) { 2238 $warn = true; /* only if there is a reason */ 2239 $info = " (warn: $m[1])"; 2240 } elseif (!strncasecmp('xfail', $output, 5)) { 2241 // Pretend we have an XFAIL section 2242 $test->setSection('XFAIL', ltrim(substr($output, 5))); 2243 } elseif (!strncasecmp('flaky', $output, 5)) { 2244 // Pretend we have a FLAKY section 2245 $test->setSection('FLAKY', ltrim(substr($output, 5))); 2246 } elseif ($output !== '') { 2247 show_result("BORK", $output, $tested_file, 'reason: invalid output from SKIPIF', $temp_filenames); 2248 $PHP_FAILED_TESTS['BORKED'][] = [ 2249 'name' => $file, 2250 'test_name' => '', 2251 'output' => '', 2252 'diff' => '', 2253 'info' => "$output [$file]", 2254 ]; 2255 2256 $junit->markTestAs('BORK', $shortname, $tested, null, $output); 2257 return 'BORKED'; 2258 } 2259 } 2260 2261 if (!extension_loaded("zlib") && $test->hasAnySections("GZIP_POST", "DEFLATE_POST")) { 2262 $message = "ext/zlib required"; 2263 show_result('SKIP', $tested, $tested_file, "reason: $message", $temp_filenames); 2264 $junit->markTestAs('SKIP', $shortname, $tested, null, $message); 2265 return 'SKIPPED'; 2266 } 2267 2268 if ($test->hasSection('REDIRECTTEST')) { 2269 $test_files = []; 2270 2271 $IN_REDIRECT = eval($test->getSection('REDIRECTTEST')); 2272 $IN_REDIRECT['via'] = "via [$shortname]\n\t"; 2273 $IN_REDIRECT['dir'] = realpath(dirname($file)); 2274 $IN_REDIRECT['prefix'] = $tested; 2275 $IN_REDIRECT['EXTENSIONS'] = $extensions; 2276 2277 if (!empty($IN_REDIRECT['TESTS'])) { 2278 if (is_array($org_file)) { 2279 $test_files[] = $org_file[1]; 2280 } else { 2281 $GLOBALS['test_files'] = $test_files; 2282 find_files($IN_REDIRECT['TESTS']); 2283 2284 foreach ($GLOBALS['test_files'] as $f) { 2285 $test_files[] = [$f, $file]; 2286 } 2287 } 2288 $test_cnt += count($test_files) - 1; 2289 $test_idx--; 2290 2291 show_redirect_start($IN_REDIRECT['TESTS'], $tested, $tested_file); 2292 2293 // set up environment 2294 $redirenv = array_merge($environment, $IN_REDIRECT['ENV']); 2295 $redirenv['REDIR_TEST_DIR'] = realpath($IN_REDIRECT['TESTS']) . DIRECTORY_SEPARATOR; 2296 2297 usort($test_files, "test_sort"); 2298 run_all_tests($test_files, $redirenv, $tested); 2299 2300 show_redirect_ends($IN_REDIRECT['TESTS'], $tested, $tested_file); 2301 2302 // a redirected test never fails 2303 $IN_REDIRECT = false; 2304 2305 $junit->markTestAs('PASS', $shortname, $tested); 2306 return 'REDIR'; 2307 } else { 2308 $bork_info = "Redirect info must contain exactly one TEST string to be used as redirect directory."; 2309 show_result("BORK", $bork_info, '', '', $temp_filenames); 2310 $PHP_FAILED_TESTS['BORKED'][] = [ 2311 'name' => $file, 2312 'test_name' => '', 2313 'output' => '', 2314 'diff' => '', 2315 'info' => "$bork_info [$file]", 2316 ]; 2317 } 2318 } 2319 2320 if (is_array($org_file) || $test->hasSection('REDIRECTTEST')) { 2321 if (is_array($org_file)) { 2322 $file = $org_file[0]; 2323 } 2324 2325 $bork_info = "Redirected test did not contain redirection info"; 2326 show_result("BORK", $bork_info, '', '', $temp_filenames); 2327 $PHP_FAILED_TESTS['BORKED'][] = [ 2328 'name' => $file, 2329 'test_name' => '', 2330 'output' => '', 2331 'diff' => '', 2332 'info' => "$bork_info [$file]", 2333 ]; 2334 2335 $junit->markTestAs('BORK', $shortname, $tested, null, $bork_info); 2336 2337 return 'BORKED'; 2338 } 2339 2340 // We've satisfied the preconditions - run the test! 2341 if ($test->hasSection('FILE')) { 2342 show_file_block('php', $test->getSection('FILE'), 'TEST'); 2343 save_text($test_file, $test->getSection('FILE'), $temp_file); 2344 } else { 2345 $test_file = $temp_file = ""; 2346 } 2347 2348 if ($test->hasSection('GET')) { 2349 $query_string = trim($test->getSection('GET')); 2350 } else { 2351 $query_string = ''; 2352 } 2353 2354 $env['REDIRECT_STATUS'] = '1'; 2355 if (empty($env['QUERY_STRING'])) { 2356 $env['QUERY_STRING'] = $query_string; 2357 } 2358 if (empty($env['PATH_TRANSLATED'])) { 2359 $env['PATH_TRANSLATED'] = $test_file; 2360 } 2361 if (empty($env['SCRIPT_FILENAME'])) { 2362 $env['SCRIPT_FILENAME'] = $test_file; 2363 } 2364 2365 if ($test->hasSection('COOKIE')) { 2366 $env['HTTP_COOKIE'] = trim($test->getSection('COOKIE')); 2367 } else { 2368 $env['HTTP_COOKIE'] = ''; 2369 } 2370 2371 $args = $test->hasSection('ARGS') ? ' -- ' . $test->getSection('ARGS') : ''; 2372 2373 if ($preload && !empty($test_file)) { 2374 save_text($preload_filename, "<?php opcache_compile_file('$test_file');"); 2375 $local_pass_options = $pass_options; 2376 unset($pass_options); 2377 $pass_options = $local_pass_options; 2378 $pass_options .= " -d opcache.preload=" . $preload_filename; 2379 } 2380 2381 if ($test->sectionNotEmpty('POST_RAW')) { 2382 $post = trim($test->getSection('POST_RAW')); 2383 $raw_lines = explode("\n", $post); 2384 2385 $request = ''; 2386 $started = false; 2387 2388 foreach ($raw_lines as $line) { 2389 if (empty($env['CONTENT_TYPE']) && preg_match('/^Content-Type:(.*)/i', $line, $res)) { 2390 $env['CONTENT_TYPE'] = trim(str_replace("\r", '', $res[1])); 2391 continue; 2392 } 2393 2394 if ($started) { 2395 $request .= "\n"; 2396 } 2397 2398 $started = true; 2399 $request .= $line; 2400 } 2401 2402 $env['CONTENT_LENGTH'] = strlen($request); 2403 $env['REQUEST_METHOD'] = 'POST'; 2404 2405 if (empty($request)) { 2406 $junit->markTestAs('BORK', $shortname, $tested, null, 'empty $request'); 2407 return 'BORKED'; 2408 } 2409 2410 save_text($tmp_post, $request); 2411 $cmd = "$php $pass_options $ini_settings -f \"$test_file\"$cmdRedirect < \"$tmp_post\""; 2412 } elseif ($test->sectionNotEmpty('PUT')) { 2413 $post = trim($test->getSection('PUT')); 2414 $raw_lines = explode("\n", $post); 2415 2416 $request = ''; 2417 $started = false; 2418 2419 foreach ($raw_lines as $line) { 2420 if (empty($env['CONTENT_TYPE']) && preg_match('/^Content-Type:(.*)/i', $line, $res)) { 2421 $env['CONTENT_TYPE'] = trim(str_replace("\r", '', $res[1])); 2422 continue; 2423 } 2424 2425 if ($started) { 2426 $request .= "\n"; 2427 } 2428 2429 $started = true; 2430 $request .= $line; 2431 } 2432 2433 $env['CONTENT_LENGTH'] = strlen($request); 2434 $env['REQUEST_METHOD'] = 'PUT'; 2435 2436 if (empty($request)) { 2437 $junit->markTestAs('BORK', $shortname, $tested, null, 'empty $request'); 2438 return 'BORKED'; 2439 } 2440 2441 save_text($tmp_post, $request); 2442 $cmd = "$php $pass_options $ini_settings -f \"$test_file\"$cmdRedirect < \"$tmp_post\""; 2443 } elseif ($test->sectionNotEmpty('POST')) { 2444 $post = trim($test->getSection('POST')); 2445 $content_length = strlen($post); 2446 save_text($tmp_post, $post); 2447 2448 $env['REQUEST_METHOD'] = 'POST'; 2449 if (empty($env['CONTENT_TYPE'])) { 2450 $env['CONTENT_TYPE'] = 'application/x-www-form-urlencoded'; 2451 } 2452 2453 if (empty($env['CONTENT_LENGTH'])) { 2454 $env['CONTENT_LENGTH'] = $content_length; 2455 } 2456 2457 $cmd = "$php $pass_options $ini_settings -f \"$test_file\"$cmdRedirect < \"$tmp_post\""; 2458 } elseif ($test->sectionNotEmpty('GZIP_POST')) { 2459 $post = trim($test->getSection('GZIP_POST')); 2460 $post = gzencode($post, 9, FORCE_GZIP); 2461 $env['HTTP_CONTENT_ENCODING'] = 'gzip'; 2462 2463 save_text($tmp_post, $post); 2464 $content_length = strlen($post); 2465 2466 $env['REQUEST_METHOD'] = 'POST'; 2467 $env['CONTENT_TYPE'] = 'application/x-www-form-urlencoded'; 2468 $env['CONTENT_LENGTH'] = $content_length; 2469 2470 $cmd = "$php $pass_options $ini_settings -f \"$test_file\"$cmdRedirect < \"$tmp_post\""; 2471 } elseif ($test->sectionNotEmpty('DEFLATE_POST')) { 2472 $post = trim($test->getSection('DEFLATE_POST')); 2473 $post = gzcompress($post, 9); 2474 $env['HTTP_CONTENT_ENCODING'] = 'deflate'; 2475 save_text($tmp_post, $post); 2476 $content_length = strlen($post); 2477 2478 $env['REQUEST_METHOD'] = 'POST'; 2479 $env['CONTENT_TYPE'] = 'application/x-www-form-urlencoded'; 2480 $env['CONTENT_LENGTH'] = $content_length; 2481 2482 $cmd = "$php $pass_options $ini_settings -f \"$test_file\"$cmdRedirect < \"$tmp_post\""; 2483 } else { 2484 $env['REQUEST_METHOD'] = 'GET'; 2485 $env['CONTENT_TYPE'] = ''; 2486 $env['CONTENT_LENGTH'] = ''; 2487 2488 $repeat_option = $num_repeats > 1 ? "--repeat $num_repeats" : ""; 2489 $cmd = "$php $pass_options $repeat_option $ini_settings -f \"$test_file\" $args$cmdRedirect"; 2490 } 2491 2492 $orig_cmd = $cmd; 2493 if ($valgrind) { 2494 $env['USE_ZEND_ALLOC'] = '0'; 2495 $env['ZEND_DONT_UNLOAD_MODULES'] = 1; 2496 2497 $cmd = $valgrind->wrapCommand($cmd, $memcheck_filename, strpos($test_file, "pcre") !== false); 2498 } 2499 2500 if ($DETAILED) { 2501 echo " 2502CONTENT_LENGTH = " . $env['CONTENT_LENGTH'] . " 2503CONTENT_TYPE = " . $env['CONTENT_TYPE'] . " 2504PATH_TRANSLATED = " . $env['PATH_TRANSLATED'] . " 2505QUERY_STRING = " . $env['QUERY_STRING'] . " 2506REDIRECT_STATUS = " . $env['REDIRECT_STATUS'] . " 2507REQUEST_METHOD = " . $env['REQUEST_METHOD'] . " 2508SCRIPT_FILENAME = " . $env['SCRIPT_FILENAME'] . " 2509HTTP_COOKIE = " . $env['HTTP_COOKIE'] . " 2510COMMAND $cmd 2511"; 2512 } 2513 2514 $junit->startTimer($shortname); 2515 $hrtime = hrtime(); 2516 $startTime = $hrtime[0] * 1000000000 + $hrtime[1]; 2517 2518 $stdin = $test->hasSection('STDIN') ? $test->getSection('STDIN') : null; 2519 $out = system_with_timeout($cmd, $env, $stdin, $captureStdIn, $captureStdOut, $captureStdErr); 2520 2521 $junit->stopTimer($shortname); 2522 $hrtime = hrtime(); 2523 $time = $hrtime[0] * 1000000000 + $hrtime[1] - $startTime; 2524 if ($time >= $slow_min_ms * 1000000) { 2525 $PHP_FAILED_TESTS['SLOW'][] = [ 2526 'name' => $file, 2527 'test_name' => $tested . " [$tested_file]", 2528 'output' => '', 2529 'diff' => '', 2530 'info' => $time / 1000000000, 2531 ]; 2532 } 2533 2534 if ($test->sectionNotEmpty('CLEAN') && (!$no_clean || $cfg['keep']['clean'])) { 2535 show_file_block('clean', $test->getSection('CLEAN')); 2536 save_text($test_clean, trim($test->getSection('CLEAN')), $temp_clean); 2537 2538 if (!$no_clean) { 2539 $extra = !IS_WINDOWS ? 2540 "unset REQUEST_METHOD; unset QUERY_STRING; unset PATH_TRANSLATED; unset SCRIPT_FILENAME; unset REQUEST_METHOD;" : ""; 2541 system_with_timeout("$extra $php $pass_options $extra_options -q $orig_ini_settings $no_file_cache \"$test_clean\"", $env); 2542 } 2543 2544 if (!$cfg['keep']['clean']) { 2545 @unlink($test_clean); 2546 } 2547 } 2548 2549 $leaked = false; 2550 $passed = false; 2551 2552 if ($valgrind) { // leak check 2553 $leaked = filesize($memcheck_filename) > 0; 2554 2555 if (!$leaked) { 2556 @unlink($memcheck_filename); 2557 } 2558 } 2559 2560 if ($num_repeats > 1) { 2561 // In repeat mode, retain the output before the first execution, 2562 // and of the last execution. Do this early, because the trimming below 2563 // makes the newline handling complicated. 2564 $separator1 = "Executing for the first time...\n"; 2565 $separator1_pos = strpos($out, $separator1); 2566 if ($separator1_pos !== false) { 2567 $separator2 = "Finished execution, repeating...\n"; 2568 $separator2_pos = strrpos($out, $separator2); 2569 if ($separator2_pos !== false) { 2570 $out = substr($out, 0, $separator1_pos) 2571 . substr($out, $separator2_pos + strlen($separator2)); 2572 } else { 2573 $out = substr($out, 0, $separator1_pos) 2574 . substr($out, $separator1_pos + strlen($separator1)); 2575 } 2576 } 2577 } 2578 2579 // Does the output match what is expected? 2580 $output = preg_replace("/\r\n/", "\n", trim($out)); 2581 2582 /* when using CGI, strip the headers from the output */ 2583 $headers = []; 2584 2585 if (!empty($uses_cgi) && preg_match("/^(.*?)\r?\n\r?\n(.*)/s", $out, $match)) { 2586 $output = trim($match[2]); 2587 $rh = preg_split("/[\n\r]+/", $match[1]); 2588 2589 foreach ($rh as $line) { 2590 if (strpos($line, ':') !== false) { 2591 $line = explode(':', $line, 2); 2592 $headers[trim($line[0])] = trim($line[1]); 2593 } 2594 } 2595 } 2596 2597 $failed_headers = false; 2598 2599 if ($test->hasSection('EXPECTHEADERS')) { 2600 $want = []; 2601 $wanted_headers = []; 2602 $lines = preg_split("/[\n\r]+/", $test->getSection('EXPECTHEADERS')); 2603 2604 foreach ($lines as $line) { 2605 if (strpos($line, ':') !== false) { 2606 $line = explode(':', $line, 2); 2607 $want[trim($line[0])] = trim($line[1]); 2608 $wanted_headers[] = trim($line[0]) . ': ' . trim($line[1]); 2609 } 2610 } 2611 2612 $output_headers = []; 2613 2614 foreach ($want as $k => $v) { 2615 if (isset($headers[$k])) { 2616 $output_headers[] = $k . ': ' . $headers[$k]; 2617 } 2618 2619 if (!isset($headers[$k]) || $headers[$k] != $v) { 2620 $failed_headers = true; 2621 } 2622 } 2623 2624 ksort($wanted_headers); 2625 $wanted_headers = implode("\n", $wanted_headers); 2626 ksort($output_headers); 2627 $output_headers = implode("\n", $output_headers); 2628 } 2629 2630 show_file_block('out', $output); 2631 2632 if ($preload) { 2633 $output = trim(preg_replace("/\n?Warning: Can't preload [^\n]*\n?/", "", $output)); 2634 } 2635 2636 if ($test->hasAnySections('EXPECTF', 'EXPECTREGEX')) { 2637 if ($test->hasSection('EXPECTF')) { 2638 $wanted = trim($test->getSection('EXPECTF')); 2639 } else { 2640 $wanted = trim($test->getSection('EXPECTREGEX')); 2641 } 2642 2643 show_file_block('exp', $wanted); 2644 $wanted_re = preg_replace('/\r\n/', "\n", $wanted); 2645 2646 if ($test->hasSection('EXPECTF')) { 2647 // do preg_quote, but miss out any %r delimited sections 2648 $temp = ""; 2649 $r = "%r"; 2650 $startOffset = 0; 2651 $length = strlen($wanted_re); 2652 while ($startOffset < $length) { 2653 $start = strpos($wanted_re, $r, $startOffset); 2654 if ($start !== false) { 2655 // we have found a start tag 2656 $end = strpos($wanted_re, $r, $start + 2); 2657 if ($end === false) { 2658 // unbalanced tag, ignore it. 2659 $end = $start = $length; 2660 } 2661 } else { 2662 // no more %r sections 2663 $start = $end = $length; 2664 } 2665 // quote a non re portion of the string 2666 $temp .= preg_quote(substr($wanted_re, $startOffset, $start - $startOffset), '/'); 2667 // add the re unquoted. 2668 if ($end > $start) { 2669 $temp .= '(' . substr($wanted_re, $start + 2, $end - $start - 2) . ')'; 2670 } 2671 $startOffset = $end + 2; 2672 } 2673 $wanted_re = $temp; 2674 2675 // Stick to basics 2676 $wanted_re = str_replace('%e', '\\' . DIRECTORY_SEPARATOR, $wanted_re); 2677 $wanted_re = str_replace('%s', '[^\r\n]+', $wanted_re); 2678 $wanted_re = str_replace('%S', '[^\r\n]*', $wanted_re); 2679 $wanted_re = str_replace('%a', '.+', $wanted_re); 2680 $wanted_re = str_replace('%A', '.*', $wanted_re); 2681 $wanted_re = str_replace('%w', '\s*', $wanted_re); 2682 $wanted_re = str_replace('%i', '[+-]?\d+', $wanted_re); 2683 $wanted_re = str_replace('%d', '\d+', $wanted_re); 2684 $wanted_re = str_replace('%x', '[0-9a-fA-F]+', $wanted_re); 2685 $wanted_re = str_replace('%f', '[+-]?\.?\d+\.?\d*(?:[Ee][+-]?\d+)?', $wanted_re); 2686 $wanted_re = str_replace('%c', '.', $wanted_re); 2687 $wanted_re = str_replace('%0', '\x00', $wanted_re); 2688 // %f allows two points "-.0.0" but that is the best *simple* expression 2689 } 2690 2691 if (preg_match("/^$wanted_re\$/s", $output)) { 2692 $passed = true; 2693 if (!$cfg['keep']['php'] && !$leaked) { 2694 @unlink($test_file); 2695 @unlink($preload_filename); 2696 } 2697 @unlink($tmp_post); 2698 2699 if (!$leaked && !$failed_headers) { 2700 if ($test->hasSection('XFAIL')) { 2701 $warn = true; 2702 $info = " (warn: XFAIL section but test passes)"; 2703 } elseif ($test->hasSection('XLEAK')) { 2704 $warn = true; 2705 $info = " (warn: XLEAK section but test passes)"; 2706 } elseif ($retried) { 2707 $warn = true; 2708 $info = " (warn: Test passed on retry attempt)"; 2709 } else { 2710 show_result("PASS", $tested, $tested_file, '', $temp_filenames); 2711 $junit->markTestAs('PASS', $shortname, $tested); 2712 return 'PASSED'; 2713 } 2714 } 2715 } 2716 } else { 2717 $wanted = trim($test->getSection('EXPECT')); 2718 $wanted = preg_replace('/\r\n/', "\n", $wanted); 2719 show_file_block('exp', $wanted); 2720 2721 // compare and leave on success 2722 if (!strcmp($output, $wanted)) { 2723 $passed = true; 2724 2725 if (!$cfg['keep']['php'] && !$leaked) { 2726 @unlink($test_file); 2727 @unlink($preload_filename); 2728 } 2729 @unlink($tmp_post); 2730 2731 if (!$leaked && !$failed_headers) { 2732 if ($test->hasSection('XFAIL')) { 2733 $warn = true; 2734 $info = " (warn: XFAIL section but test passes)"; 2735 } elseif ($test->hasSection('XLEAK')) { 2736 $warn = true; 2737 $info = " (warn: XLEAK section but test passes)"; 2738 } elseif ($retried) { 2739 $warn = true; 2740 $info = " (warn: Test passed on retry attempt)"; 2741 } else { 2742 show_result("PASS", $tested, $tested_file, '', $temp_filenames); 2743 $junit->markTestAs('PASS', $shortname, $tested); 2744 return 'PASSED'; 2745 } 2746 } 2747 } 2748 2749 $wanted_re = null; 2750 } 2751 if (!$passed && !$retried && error_may_be_retried($test, $output)) { 2752 $retried = true; 2753 goto retry; 2754 } 2755 2756 // Test failed so we need to report details. 2757 if ($failed_headers) { 2758 $passed = false; 2759 $wanted = $wanted_headers . "\n--HEADERS--\n" . $wanted; 2760 $output = $output_headers . "\n--HEADERS--\n" . $output; 2761 2762 if (isset($wanted_re)) { 2763 $wanted_re = preg_quote($wanted_headers . "\n--HEADERS--\n", '/') . $wanted_re; 2764 } 2765 } 2766 2767 if ($leaked) { 2768 $restype[] = $test->hasSection('XLEAK') ? 2769 'XLEAK' : 'LEAK'; 2770 } 2771 2772 if ($warn) { 2773 $restype[] = 'WARN'; 2774 } 2775 2776 if (!$passed) { 2777 if ($test->hasSection('XFAIL')) { 2778 $restype[] = 'XFAIL'; 2779 $info = ' XFAIL REASON: ' . rtrim($test->getSection('XFAIL')); 2780 } elseif ($test->hasSection('XLEAK')) { 2781 $restype[] = 'XLEAK'; 2782 $info = ' XLEAK REASON: ' . rtrim($test->getSection('XLEAK')); 2783 } else { 2784 $restype[] = 'FAIL'; 2785 } 2786 } 2787 2788 if (!$passed) { 2789 // write .exp 2790 if (strpos($log_format, 'E') !== false && file_put_contents($exp_filename, $wanted) === false) { 2791 error("Cannot create expected test output - $exp_filename"); 2792 } 2793 2794 // write .out 2795 if (strpos($log_format, 'O') !== false && file_put_contents($output_filename, $output) === false) { 2796 error("Cannot create test output - $output_filename"); 2797 } 2798 2799 // write .diff 2800 $diff = generate_diff($wanted, $wanted_re, $output); 2801 if (is_array($IN_REDIRECT)) { 2802 $orig_shortname = str_replace(TEST_PHP_SRCDIR . '/', '', $file); 2803 $diff = "# original source file: $orig_shortname\n" . $diff; 2804 } 2805 show_file_block('diff', $diff); 2806 if (strpos($log_format, 'D') !== false && file_put_contents($diff_filename, $diff) === false) { 2807 error("Cannot create test diff - $diff_filename"); 2808 } 2809 2810 // write .log 2811 if (strpos($log_format, 'L') !== false && file_put_contents($log_filename, " 2812---- EXPECTED OUTPUT 2813$wanted 2814---- ACTUAL OUTPUT 2815$output 2816---- FAILED 2817") === false) { 2818 error("Cannot create test log - $log_filename"); 2819 error_report($file, $log_filename, $tested); 2820 } 2821 } 2822 2823 if (!$passed || $leaked) { 2824 // write .sh 2825 if (strpos($log_format, 'S') !== false) { 2826 $env_lines = []; 2827 foreach ($env as $env_var => $env_val) { 2828 $env_lines[] = "export $env_var=" . escapeshellarg($env_val ?? ""); 2829 } 2830 $exported_environment = $env_lines ? "\n" . implode("\n", $env_lines) . "\n" : ""; 2831 $sh_script = <<<SH 2832#!/bin/sh 2833{$exported_environment} 2834case "$1" in 2835"gdb") 2836 gdb --args {$orig_cmd} 2837 ;; 2838"valgrind") 2839 USE_ZEND_ALLOC=0 valgrind $2 ${orig_cmd} 2840 ;; 2841"rr") 2842 rr record $2 ${orig_cmd} 2843 ;; 2844*) 2845 {$orig_cmd} 2846 ;; 2847esac 2848SH; 2849 if (file_put_contents($sh_filename, $sh_script) === false) { 2850 error("Cannot create test shell script - $sh_filename"); 2851 } 2852 chmod($sh_filename, 0755); 2853 } 2854 } 2855 2856 if ($valgrind && $leaked && $cfg["show"]["mem"]) { 2857 show_file_block('mem', file_get_contents($memcheck_filename)); 2858 } 2859 2860 show_result(implode('&', $restype), $tested, $tested_file, $info, $temp_filenames); 2861 2862 foreach ($restype as $type) { 2863 $PHP_FAILED_TESTS[$type . 'ED'][] = [ 2864 'name' => $file, 2865 'test_name' => (is_array($IN_REDIRECT) ? $IN_REDIRECT['via'] : '') . $tested . " [$tested_file]", 2866 'output' => $output_filename, 2867 'diff' => $diff_filename, 2868 'info' => $info, 2869 ]; 2870 } 2871 2872 $diff = empty($diff) ? '' : preg_replace('/\e/', '<esc>', $diff); 2873 2874 $junit->markTestAs($restype, $shortname, $tested, null, $info, $diff); 2875 2876 return $restype[0] . 'ED'; 2877} 2878 2879function is_flaky(TestFile $test): bool 2880{ 2881 if ($test->hasSection('FLAKY')) { 2882 return true; 2883 } 2884 if (!$test->hasSection('FILE')) { 2885 return false; 2886 } 2887 $file = $test->getSection('FILE'); 2888 $flaky_functions = [ 2889 'disk_free_space', 2890 'hrtime', 2891 'microtime', 2892 'sleep', 2893 'usleep', 2894 ]; 2895 $regex = '(\b(' . implode('|', $flaky_functions) . ')\()i'; 2896 return preg_match($regex, $file) === 1; 2897} 2898 2899function is_flaky_output(string $output): bool 2900{ 2901 $messages = [ 2902 '404: page not found', 2903 'address already in use', 2904 'connection refused', 2905 'deadlock', 2906 'mailbox already exists', 2907 'timed out', 2908 ]; 2909 $regex = '(\b(' . implode('|', $messages) . ')\b)i'; 2910 return preg_match($regex, $output) === 1; 2911} 2912 2913function error_may_be_retried(TestFile $test, string $output): bool 2914{ 2915 return is_flaky_output($output) 2916 || is_flaky($test); 2917} 2918 2919/** 2920 * @return bool|int 2921 */ 2922function comp_line(string $l1, string $l2, bool $is_reg) 2923{ 2924 if ($is_reg) { 2925 return preg_match('/^' . $l1 . '$/s', $l2); 2926 } else { 2927 return !strcmp($l1, $l2); 2928 } 2929} 2930 2931function count_array_diff( 2932 array $ar1, 2933 array $ar2, 2934 bool $is_reg, 2935 array $w, 2936 int $idx1, 2937 int $idx2, 2938 int $cnt1, 2939 int $cnt2, 2940 int $steps 2941): int { 2942 $equal = 0; 2943 2944 while ($idx1 < $cnt1 && $idx2 < $cnt2 && comp_line($ar1[$idx1], $ar2[$idx2], $is_reg)) { 2945 $idx1++; 2946 $idx2++; 2947 $equal++; 2948 $steps--; 2949 } 2950 if (--$steps > 0) { 2951 $eq1 = 0; 2952 $st = $steps / 2; 2953 2954 for ($ofs1 = $idx1 + 1; $ofs1 < $cnt1 && $st-- > 0; $ofs1++) { 2955 $eq = @count_array_diff($ar1, $ar2, $is_reg, $w, $ofs1, $idx2, $cnt1, $cnt2, $st); 2956 2957 if ($eq > $eq1) { 2958 $eq1 = $eq; 2959 } 2960 } 2961 2962 $eq2 = 0; 2963 $st = $steps; 2964 2965 for ($ofs2 = $idx2 + 1; $ofs2 < $cnt2 && $st-- > 0; $ofs2++) { 2966 $eq = @count_array_diff($ar1, $ar2, $is_reg, $w, $idx1, $ofs2, $cnt1, $cnt2, $st); 2967 if ($eq > $eq2) { 2968 $eq2 = $eq; 2969 } 2970 } 2971 2972 if ($eq1 > $eq2) { 2973 $equal += $eq1; 2974 } elseif ($eq2 > 0) { 2975 $equal += $eq2; 2976 } 2977 } 2978 2979 return $equal; 2980} 2981 2982function generate_array_diff(array $ar1, array $ar2, bool $is_reg, array $w): array 2983{ 2984 global $context_line_count; 2985 $idx1 = 0; 2986 $cnt1 = @count($ar1); 2987 $idx2 = 0; 2988 $cnt2 = @count($ar2); 2989 $diff = []; 2990 $old1 = []; 2991 $old2 = []; 2992 $number_len = max(3, strlen((string)max($cnt1 + 1, $cnt2 + 1))); 2993 $line_number_spec = '%0' . $number_len . 'd'; 2994 2995 /** Mapping from $idx2 to $idx1, including indexes of idx2 that are identical to idx1 as well as entries that don't have matches */ 2996 $mapping = []; 2997 2998 while ($idx1 < $cnt1 && $idx2 < $cnt2) { 2999 $mapping[$idx2] = $idx1; 3000 if (comp_line($ar1[$idx1], $ar2[$idx2], $is_reg)) { 3001 $idx1++; 3002 $idx2++; 3003 continue; 3004 } else { 3005 $c1 = @count_array_diff($ar1, $ar2, $is_reg, $w, $idx1 + 1, $idx2, $cnt1, $cnt2, 10); 3006 $c2 = @count_array_diff($ar1, $ar2, $is_reg, $w, $idx1, $idx2 + 1, $cnt1, $cnt2, 10); 3007 3008 if ($c1 > $c2) { 3009 $old1[$idx1] = sprintf("{$line_number_spec}- ", $idx1 + 1) . $w[$idx1++]; 3010 } elseif ($c2 > 0) { 3011 $old2[$idx2] = sprintf("{$line_number_spec}+ ", $idx2 + 1) . $ar2[$idx2++]; 3012 } else { 3013 $old1[$idx1] = sprintf("{$line_number_spec}- ", $idx1 + 1) . $w[$idx1++]; 3014 $old2[$idx2] = sprintf("{$line_number_spec}+ ", $idx2 + 1) . $ar2[$idx2++]; 3015 } 3016 $last_printed_context_line = $idx1; 3017 } 3018 } 3019 $mapping[$idx2] = $idx1; 3020 3021 reset($old1); 3022 $k1 = key($old1); 3023 $l1 = -2; 3024 reset($old2); 3025 $k2 = key($old2); 3026 $l2 = -2; 3027 $old_k1 = -1; 3028 $add_context_lines = function (int $new_k1) use (&$old_k1, &$diff, $w, $context_line_count, $number_len) { 3029 if ($old_k1 >= $new_k1 || !$context_line_count) { 3030 return; 3031 } 3032 $end = $new_k1 - 1; 3033 $range_end = min($end, $old_k1 + $context_line_count); 3034 if ($old_k1 >= 0) { 3035 while ($old_k1 < $range_end) { 3036 $diff[] = str_repeat(' ', $number_len + 2) . $w[$old_k1++]; 3037 } 3038 } 3039 if ($end - $context_line_count > $old_k1) { 3040 $old_k1 = $end - $context_line_count; 3041 if ($old_k1 > 0) { 3042 // Add a '--' to mark sections where the common areas were truncated 3043 $diff[] = '--'; 3044 } 3045 } 3046 $old_k1 = max($old_k1, 0); 3047 while ($old_k1 < $end) { 3048 $diff[] = str_repeat(' ', $number_len + 2) . $w[$old_k1++]; 3049 } 3050 $old_k1 = $new_k1; 3051 }; 3052 3053 while ($k1 !== null || $k2 !== null) { 3054 if ($k1 == $l1 + 1 || $k2 === null) { 3055 $add_context_lines($k1); 3056 $l1 = $k1; 3057 $diff[] = current($old1); 3058 $old_k1 = $k1; 3059 $k1 = next($old1) ? key($old1) : null; 3060 } elseif ($k2 == $l2 + 1 || $k1 === null) { 3061 $add_context_lines($mapping[$k2]); 3062 $l2 = $k2; 3063 $diff[] = current($old2); 3064 $k2 = next($old2) ? key($old2) : null; 3065 } elseif ($k1 < $mapping[$k2]) { 3066 $add_context_lines($k1); 3067 $l1 = $k1; 3068 $diff[] = current($old1); 3069 $k1 = next($old1) ? key($old1) : null; 3070 } else { 3071 $add_context_lines($mapping[$k2]); 3072 $l2 = $k2; 3073 $diff[] = current($old2); 3074 $k2 = next($old2) ? key($old2) : null; 3075 } 3076 } 3077 3078 while ($idx1 < $cnt1) { 3079 $add_context_lines($idx1 + 1); 3080 $diff[] = sprintf("{$line_number_spec}- ", $idx1 + 1) . $w[$idx1++]; 3081 } 3082 3083 while ($idx2 < $cnt2) { 3084 if (isset($mapping[$idx2])) { 3085 $add_context_lines($mapping[$idx2] + 1); 3086 } 3087 $diff[] = sprintf("{$line_number_spec}+ ", $idx2 + 1) . $ar2[$idx2++]; 3088 } 3089 $add_context_lines(min($old_k1 + $context_line_count + 1, $cnt1 + 1)); 3090 if ($context_line_count && $old_k1 < $cnt1 + 1) { 3091 // Add a '--' to mark sections where the common areas were truncated 3092 $diff[] = '--'; 3093 } 3094 3095 return $diff; 3096} 3097 3098function generate_diff(string $wanted, ?string $wanted_re, string $output): string 3099{ 3100 $w = explode("\n", $wanted); 3101 $o = explode("\n", $output); 3102 $r = is_null($wanted_re) ? $w : explode("\n", $wanted_re); 3103 $diff = generate_array_diff($r, $o, !is_null($wanted_re), $w); 3104 3105 return implode(PHP_EOL, $diff); 3106} 3107 3108function error(string $message): void 3109{ 3110 echo "ERROR: {$message}\n"; 3111 exit(1); 3112} 3113 3114function settings2array(array $settings, &$ini_settings): void 3115{ 3116 foreach ($settings as $setting) { 3117 if (strpos($setting, '=') !== false) { 3118 $setting = explode("=", $setting, 2); 3119 $name = trim($setting[0]); 3120 $value = trim($setting[1]); 3121 3122 if ($name == 'extension' || $name == 'zend_extension') { 3123 if (!isset($ini_settings[$name])) { 3124 $ini_settings[$name] = []; 3125 } 3126 3127 $ini_settings[$name][] = $value; 3128 } else { 3129 $ini_settings[$name] = $value; 3130 } 3131 } 3132 } 3133} 3134 3135function settings2params(array $ini_settings): string 3136{ 3137 $settings = ''; 3138 3139 foreach ($ini_settings as $name => $value) { 3140 if (is_array($value)) { 3141 foreach ($value as $val) { 3142 $val = addslashes($val); 3143 $settings .= " -d \"$name=$val\""; 3144 } 3145 } else { 3146 if (IS_WINDOWS && !empty($value) && $value[0] == '"') { 3147 $len = strlen($value); 3148 3149 if ($value[$len - 1] == '"') { 3150 $value[0] = "'"; 3151 $value[$len - 1] = "'"; 3152 } 3153 } else { 3154 $value = addslashes($value); 3155 } 3156 3157 $settings .= " -d \"$name=$value\""; 3158 } 3159 } 3160 3161 return $settings; 3162} 3163 3164function compute_summary(): void 3165{ 3166 global $n_total, $test_results, $ignored_by_ext, $sum_results, $percent_results; 3167 3168 $n_total = count($test_results); 3169 $n_total += $ignored_by_ext; 3170 $sum_results = [ 3171 'PASSED' => 0, 3172 'WARNED' => 0, 3173 'SKIPPED' => 0, 3174 'FAILED' => 0, 3175 'BORKED' => 0, 3176 'LEAKED' => 0, 3177 'XFAILED' => 0, 3178 'XLEAKED' => 0 3179 ]; 3180 3181 foreach ($test_results as $v) { 3182 $sum_results[$v]++; 3183 } 3184 3185 $sum_results['SKIPPED'] += $ignored_by_ext; 3186 $percent_results = []; 3187 3188 foreach ($sum_results as $v => $n) { 3189 $percent_results[$v] = (100.0 * $n) / $n_total; 3190 } 3191} 3192 3193function get_summary(bool $show_ext_summary): string 3194{ 3195 global $exts_skipped, $exts_tested, $n_total, $sum_results, $percent_results, $end_time, $start_time, $failed_test_summary, $PHP_FAILED_TESTS, $valgrind; 3196 3197 $x_total = $n_total - $sum_results['SKIPPED'] - $sum_results['BORKED']; 3198 3199 if ($x_total) { 3200 $x_warned = (100.0 * $sum_results['WARNED']) / $x_total; 3201 $x_failed = (100.0 * $sum_results['FAILED']) / $x_total; 3202 $x_xfailed = (100.0 * $sum_results['XFAILED']) / $x_total; 3203 $x_xleaked = (100.0 * $sum_results['XLEAKED']) / $x_total; 3204 $x_leaked = (100.0 * $sum_results['LEAKED']) / $x_total; 3205 $x_passed = (100.0 * $sum_results['PASSED']) / $x_total; 3206 } else { 3207 $x_warned = $x_failed = $x_passed = $x_leaked = $x_xfailed = $x_xleaked = 0; 3208 } 3209 3210 $summary = ''; 3211 3212 if ($show_ext_summary) { 3213 $summary .= ' 3214===================================================================== 3215TEST RESULT SUMMARY 3216--------------------------------------------------------------------- 3217Exts skipped : ' . sprintf('%4d', $exts_skipped) . ' 3218Exts tested : ' . sprintf('%4d', $exts_tested) . ' 3219--------------------------------------------------------------------- 3220'; 3221 } 3222 3223 $summary .= ' 3224Number of tests : ' . sprintf('%4d', $n_total) . ' ' . sprintf('%8d', $x_total); 3225 3226 if ($sum_results['BORKED']) { 3227 $summary .= ' 3228Tests borked : ' . sprintf('%4d (%5.1f%%)', $sum_results['BORKED'], $percent_results['BORKED']) . ' --------'; 3229 } 3230 3231 $summary .= ' 3232Tests skipped : ' . sprintf('%4d (%5.1f%%)', $sum_results['SKIPPED'], $percent_results['SKIPPED']) . ' -------- 3233Tests warned : ' . sprintf('%4d (%5.1f%%)', $sum_results['WARNED'], $percent_results['WARNED']) . ' ' . sprintf('(%5.1f%%)', $x_warned) . ' 3234Tests failed : ' . sprintf('%4d (%5.1f%%)', $sum_results['FAILED'], $percent_results['FAILED']) . ' ' . sprintf('(%5.1f%%)', $x_failed); 3235 3236 if ($sum_results['XFAILED']) { 3237 $summary .= ' 3238Expected fail : ' . sprintf('%4d (%5.1f%%)', $sum_results['XFAILED'], $percent_results['XFAILED']) . ' ' . sprintf('(%5.1f%%)', $x_xfailed); 3239 } 3240 3241 if ($valgrind) { 3242 $summary .= ' 3243Tests leaked : ' . sprintf('%4d (%5.1f%%)', $sum_results['LEAKED'], $percent_results['LEAKED']) . ' ' . sprintf('(%5.1f%%)', $x_leaked); 3244 if ($sum_results['XLEAKED']) { 3245 $summary .= ' 3246Expected leak : ' . sprintf('%4d (%5.1f%%)', $sum_results['XLEAKED'], $percent_results['XLEAKED']) . ' ' . sprintf('(%5.1f%%)', $x_xleaked); 3247 } 3248 } 3249 3250 $summary .= ' 3251Tests passed : ' . sprintf('%4d (%5.1f%%)', $sum_results['PASSED'], $percent_results['PASSED']) . ' ' . sprintf('(%5.1f%%)', $x_passed) . ' 3252--------------------------------------------------------------------- 3253Time taken : ' . sprintf('%4d seconds', $end_time - $start_time) . ' 3254===================================================================== 3255'; 3256 $failed_test_summary = ''; 3257 3258 if (count($PHP_FAILED_TESTS['SLOW'])) { 3259 usort($PHP_FAILED_TESTS['SLOW'], function (array $a, array $b): int { 3260 return $a['info'] < $b['info'] ? 1 : -1; 3261 }); 3262 3263 $failed_test_summary .= ' 3264===================================================================== 3265SLOW TEST SUMMARY 3266--------------------------------------------------------------------- 3267'; 3268 foreach ($PHP_FAILED_TESTS['SLOW'] as $failed_test_data) { 3269 $failed_test_summary .= sprintf('(%.3f s) ', $failed_test_data['info']) . $failed_test_data['test_name'] . "\n"; 3270 } 3271 $failed_test_summary .= "=====================================================================\n"; 3272 } 3273 3274 if (count($PHP_FAILED_TESTS['XFAILED'])) { 3275 $failed_test_summary .= ' 3276===================================================================== 3277EXPECTED FAILED TEST SUMMARY 3278--------------------------------------------------------------------- 3279'; 3280 foreach ($PHP_FAILED_TESTS['XFAILED'] as $failed_test_data) { 3281 $failed_test_summary .= $failed_test_data['test_name'] . $failed_test_data['info'] . "\n"; 3282 } 3283 $failed_test_summary .= "=====================================================================\n"; 3284 } 3285 3286 if (count($PHP_FAILED_TESTS['BORKED'])) { 3287 $failed_test_summary .= ' 3288===================================================================== 3289BORKED TEST SUMMARY 3290--------------------------------------------------------------------- 3291'; 3292 foreach ($PHP_FAILED_TESTS['BORKED'] as $failed_test_data) { 3293 $failed_test_summary .= $failed_test_data['info'] . "\n"; 3294 } 3295 3296 $failed_test_summary .= "=====================================================================\n"; 3297 } 3298 3299 if (count($PHP_FAILED_TESTS['FAILED'])) { 3300 $failed_test_summary .= ' 3301===================================================================== 3302FAILED TEST SUMMARY 3303--------------------------------------------------------------------- 3304'; 3305 foreach ($PHP_FAILED_TESTS['FAILED'] as $failed_test_data) { 3306 $failed_test_summary .= $failed_test_data['test_name'] . $failed_test_data['info'] . "\n"; 3307 } 3308 $failed_test_summary .= "=====================================================================\n"; 3309 } 3310 if (count($PHP_FAILED_TESTS['WARNED'])) { 3311 $failed_test_summary .= ' 3312===================================================================== 3313WARNED TEST SUMMARY 3314--------------------------------------------------------------------- 3315'; 3316 foreach ($PHP_FAILED_TESTS['WARNED'] as $failed_test_data) { 3317 $failed_test_summary .= $failed_test_data['test_name'] . $failed_test_data['info'] . "\n"; 3318 } 3319 3320 $failed_test_summary .= "=====================================================================\n"; 3321 } 3322 3323 if (count($PHP_FAILED_TESTS['LEAKED'])) { 3324 $failed_test_summary .= ' 3325===================================================================== 3326LEAKED TEST SUMMARY 3327--------------------------------------------------------------------- 3328'; 3329 foreach ($PHP_FAILED_TESTS['LEAKED'] as $failed_test_data) { 3330 $failed_test_summary .= $failed_test_data['test_name'] . $failed_test_data['info'] . "\n"; 3331 } 3332 3333 $failed_test_summary .= "=====================================================================\n"; 3334 } 3335 3336 if (count($PHP_FAILED_TESTS['XLEAKED'])) { 3337 $failed_test_summary .= ' 3338===================================================================== 3339EXPECTED LEAK TEST SUMMARY 3340--------------------------------------------------------------------- 3341'; 3342 foreach ($PHP_FAILED_TESTS['XLEAKED'] as $failed_test_data) { 3343 $failed_test_summary .= $failed_test_data['test_name'] . $failed_test_data['info'] . "\n"; 3344 } 3345 3346 $failed_test_summary .= "=====================================================================\n"; 3347 } 3348 3349 if ($failed_test_summary && !getenv('NO_PHPTEST_SUMMARY')) { 3350 $summary .= $failed_test_summary; 3351 } 3352 3353 return $summary; 3354} 3355 3356function show_start($start_time): void 3357{ 3358 echo "TIME START " . date('Y-m-d H:i:s', $start_time) . "\n=====================================================================\n"; 3359} 3360 3361function show_end($end_time): void 3362{ 3363 echo "=====================================================================\nTIME END " . date('Y-m-d H:i:s', $end_time) . "\n"; 3364} 3365 3366function show_summary(): void 3367{ 3368 echo get_summary(true); 3369} 3370 3371function show_redirect_start(string $tests, string $tested, string $tested_file): void 3372{ 3373 global $SHOW_ONLY_GROUPS; 3374 3375 if (!$SHOW_ONLY_GROUPS || in_array('REDIRECT', $SHOW_ONLY_GROUPS)) { 3376 echo "REDIRECT $tests ($tested [$tested_file]) begin\n"; 3377 } else { 3378 clear_show_test(); 3379 } 3380} 3381 3382function show_redirect_ends(string $tests, string $tested, string $tested_file): void 3383{ 3384 global $SHOW_ONLY_GROUPS; 3385 3386 if (!$SHOW_ONLY_GROUPS || in_array('REDIRECT', $SHOW_ONLY_GROUPS)) { 3387 echo "REDIRECT $tests ($tested [$tested_file]) done\n"; 3388 } else { 3389 clear_show_test(); 3390 } 3391} 3392 3393function show_test(int $test_idx, string $shortname): void 3394{ 3395 global $test_cnt; 3396 global $line_length; 3397 3398 $str = "TEST $test_idx/$test_cnt [$shortname]\r"; 3399 $line_length = strlen($str); 3400 echo $str; 3401 flush(); 3402} 3403 3404function clear_show_test(): void 3405{ 3406 global $line_length; 3407 // Parallel testing 3408 global $workerID; 3409 3410 if (!$workerID && isset($line_length)) { 3411 // Write over the last line to avoid random trailing chars on next echo 3412 echo str_repeat(" ", $line_length), "\r"; 3413 } 3414} 3415 3416function parse_conflicts(string $text): array 3417{ 3418 // Strip comments 3419 $text = preg_replace('/#.*/', '', $text); 3420 return array_map('trim', explode("\n", trim($text))); 3421} 3422 3423function show_result( 3424 string $result, 3425 string $tested, 3426 string $tested_file, 3427 string $extra = '', 3428 ?array $temp_filenames = null 3429): void { 3430 global $SHOW_ONLY_GROUPS, $colorize; 3431 3432 if (!$SHOW_ONLY_GROUPS || in_array($result, $SHOW_ONLY_GROUPS)) { 3433 if ($colorize) { 3434 /* Use ANSI escape codes for coloring test result */ 3435 switch ( $result ) { 3436 case 'PASS': // Light Green 3437 $color = "\e[1;32m{$result}\e[0m"; break; 3438 case 'FAIL': 3439 case 'BORK': 3440 case 'LEAK': 3441 case 'LEAK&FAIL': 3442 // Light Red 3443 $color = "\e[1;31m{$result}\e[0m"; break; 3444 default: // Yellow 3445 $color = "\e[1;33m{$result}\e[0m"; break; 3446 } 3447 3448 echo "$color $tested [$tested_file] $extra\n"; 3449 } else { 3450 echo "$result $tested [$tested_file] $extra\n"; 3451 } 3452 } elseif (!$SHOW_ONLY_GROUPS) { 3453 clear_show_test(); 3454 } 3455 3456} 3457 3458class BorkageException extends Exception 3459{ 3460} 3461 3462class JUnit 3463{ 3464 private bool $enabled = true; 3465 private $fp = null; 3466 private array $suites = []; 3467 private array $rootSuite = self::EMPTY_SUITE + ['name' => 'php']; 3468 3469 private const EMPTY_SUITE = [ 3470 'test_total' => 0, 3471 'test_pass' => 0, 3472 'test_fail' => 0, 3473 'test_error' => 0, 3474 'test_skip' => 0, 3475 'test_warn' => 0, 3476 'files' => [], 3477 'execution_time' => 0, 3478 ]; 3479 3480 /** 3481 * @throws Exception 3482 */ 3483 public function __construct(array $env, int $workerID) 3484 { 3485 // Check whether a junit log is wanted. 3486 $fileName = $env['TEST_PHP_JUNIT'] ?? null; 3487 if (empty($fileName)) { 3488 $this->enabled = false; 3489 return; 3490 } 3491 if (!$workerID && !$this->fp = fopen($fileName, 'w')) { 3492 throw new Exception("Failed to open $fileName for writing."); 3493 } 3494 } 3495 3496 public function isEnabled(): bool 3497 { 3498 return $this->enabled; 3499 } 3500 3501 public function clear(): void 3502 { 3503 $this->rootSuite = self::EMPTY_SUITE + ['name' => 'php']; 3504 $this->suites = []; 3505 } 3506 3507 public function saveXML(): void 3508 { 3509 if (!$this->enabled) { 3510 return; 3511 } 3512 3513 $xml = '<' . '?' . 'xml version="1.0" encoding="UTF-8"' . '?' . '>' . PHP_EOL; 3514 $xml .= sprintf( 3515 '<testsuites name="%s" tests="%s" failures="%d" errors="%d" skip="%d" time="%s">' . PHP_EOL, 3516 $this->rootSuite['name'], 3517 $this->rootSuite['test_total'], 3518 $this->rootSuite['test_fail'], 3519 $this->rootSuite['test_error'], 3520 $this->rootSuite['test_skip'], 3521 $this->rootSuite['execution_time'] 3522 ); 3523 $xml .= $this->getSuitesXML(); 3524 $xml .= '</testsuites>'; 3525 fwrite($this->fp, $xml); 3526 } 3527 3528 private function getSuitesXML(string $suite_name = '') 3529 { 3530 // FIXME: $suite_name gets overwritten 3531 $result = ''; 3532 3533 foreach ($this->suites as $suite_name => $suite) { 3534 $result .= sprintf( 3535 '<testsuite name="%s" tests="%s" failures="%d" errors="%d" skip="%d" time="%s">' . PHP_EOL, 3536 $suite['name'], 3537 $suite['test_total'], 3538 $suite['test_fail'], 3539 $suite['test_error'], 3540 $suite['test_skip'], 3541 $suite['execution_time'] 3542 ); 3543 3544 if (!empty($suite_name)) { 3545 foreach ($suite['files'] as $file) { 3546 $result .= $this->rootSuite['files'][$file]['xml']; 3547 } 3548 } 3549 3550 $result .= '</testsuite>' . PHP_EOL; 3551 } 3552 3553 return $result; 3554 } 3555 3556 public function markTestAs( 3557 $type, 3558 string $file_name, 3559 string $test_name, 3560 ?int $time = null, 3561 string $message = '', 3562 string $details = '' 3563 ): void { 3564 if (!$this->enabled) { 3565 return; 3566 } 3567 3568 $suite = $this->getSuiteName($file_name); 3569 3570 $this->record($suite, 'test_total'); 3571 3572 $time = $time ?? $this->getTimer($file_name); 3573 $this->record($suite, 'execution_time', $time); 3574 3575 $escaped_details = htmlspecialchars($details, ENT_QUOTES, 'UTF-8'); 3576 $escaped_details = preg_replace_callback('/[\0-\x08\x0B\x0C\x0E-\x1F]/', function ($c) { 3577 return sprintf('[[0x%02x]]', ord($c[0])); 3578 }, $escaped_details); 3579 $escaped_message = htmlspecialchars($message, ENT_QUOTES, 'UTF-8'); 3580 3581 $escaped_test_name = htmlspecialchars($file_name . ' (' . $test_name . ')', ENT_QUOTES); 3582 $this->rootSuite['files'][$file_name]['xml'] = "<testcase name='$escaped_test_name' time='$time'>\n"; 3583 3584 if (is_array($type)) { 3585 $output_type = $type[0] . 'ED'; 3586 $temp = array_intersect(['XFAIL', 'XLEAK', 'FAIL', 'WARN'], $type); 3587 $type = reset($temp); 3588 } else { 3589 $output_type = $type . 'ED'; 3590 } 3591 3592 if ('PASS' == $type || 'XFAIL' == $type || 'XLEAK' == $type) { 3593 $this->record($suite, 'test_pass'); 3594 } elseif ('BORK' == $type) { 3595 $this->record($suite, 'test_error'); 3596 $this->rootSuite['files'][$file_name]['xml'] .= "<error type='$output_type' message='$escaped_message'/>\n"; 3597 } elseif ('SKIP' == $type) { 3598 $this->record($suite, 'test_skip'); 3599 $this->rootSuite['files'][$file_name]['xml'] .= "<skipped>$escaped_message</skipped>\n"; 3600 } elseif ('WARN' == $type) { 3601 $this->record($suite, 'test_warn'); 3602 $this->rootSuite['files'][$file_name]['xml'] .= "<warning>$escaped_message</warning>\n"; 3603 } elseif ('FAIL' == $type) { 3604 $this->record($suite, 'test_fail'); 3605 $this->rootSuite['files'][$file_name]['xml'] .= "<failure type='$output_type' message='$escaped_message'>$escaped_details</failure>\n"; 3606 } else { 3607 $this->record($suite, 'test_error'); 3608 $this->rootSuite['files'][$file_name]['xml'] .= "<error type='$output_type' message='$escaped_message'>$escaped_details</error>\n"; 3609 } 3610 3611 $this->rootSuite['files'][$file_name]['xml'] .= "</testcase>\n"; 3612 } 3613 3614 private function record(string $suite, string $param, $value = 1): void 3615 { 3616 $this->rootSuite[$param] += $value; 3617 $this->suites[$suite][$param] += $value; 3618 } 3619 3620 private function getTimer(string $file_name) 3621 { 3622 if (!$this->enabled) { 3623 return 0; 3624 } 3625 3626 if (isset($this->rootSuite['files'][$file_name]['total'])) { 3627 return number_format($this->rootSuite['files'][$file_name]['total'], 4); 3628 } 3629 3630 return 0; 3631 } 3632 3633 public function startTimer(string $file_name): void 3634 { 3635 if (!$this->enabled) { 3636 return; 3637 } 3638 3639 if (!isset($this->rootSuite['files'][$file_name]['start'])) { 3640 $this->rootSuite['files'][$file_name]['start'] = microtime(true); 3641 3642 $suite = $this->getSuiteName($file_name); 3643 $this->initSuite($suite); 3644 $this->suites[$suite]['files'][$file_name] = $file_name; 3645 } 3646 } 3647 3648 public function getSuiteName(string $file_name): string 3649 { 3650 return $this->pathToClassName(dirname($file_name)); 3651 } 3652 3653 private function pathToClassName(string $file_name): string 3654 { 3655 if (!$this->enabled) { 3656 return ''; 3657 } 3658 3659 $ret = $this->rootSuite['name']; 3660 $_tmp = []; 3661 3662 // lookup whether we're in the PHP source checkout 3663 $max = 5; 3664 if (is_file($file_name)) { 3665 $dir = dirname(realpath($file_name)); 3666 } else { 3667 $dir = realpath($file_name); 3668 } 3669 do { 3670 array_unshift($_tmp, basename($dir)); 3671 $chk = $dir . DIRECTORY_SEPARATOR . "main" . DIRECTORY_SEPARATOR . "php_version.h"; 3672 $dir = dirname($dir); 3673 } while (!file_exists($chk) && --$max > 0); 3674 if (file_exists($chk)) { 3675 if ($max) { 3676 array_shift($_tmp); 3677 } 3678 foreach ($_tmp as $p) { 3679 $ret .= "." . preg_replace(",[^a-z0-9]+,i", ".", $p); 3680 } 3681 return $ret; 3682 } 3683 3684 return $this->rootSuite['name'] . '.' . str_replace([DIRECTORY_SEPARATOR, '-'], '.', $file_name); 3685 } 3686 3687 public function initSuite(string $suite_name): void 3688 { 3689 if (!$this->enabled) { 3690 return; 3691 } 3692 3693 if (!empty($this->suites[$suite_name])) { 3694 return; 3695 } 3696 3697 $this->suites[$suite_name] = self::EMPTY_SUITE + ['name' => $suite_name]; 3698 } 3699 3700 /** 3701 * @throws Exception 3702 */ 3703 public function stopTimer(string $file_name): void 3704 { 3705 if (!$this->enabled) { 3706 return; 3707 } 3708 3709 if (!isset($this->rootSuite['files'][$file_name]['start'])) { 3710 throw new Exception("Timer for $file_name was not started!"); 3711 } 3712 3713 if (!isset($this->rootSuite['files'][$file_name]['total'])) { 3714 $this->rootSuite['files'][$file_name]['total'] = 0; 3715 } 3716 3717 $start = $this->rootSuite['files'][$file_name]['start']; 3718 $this->rootSuite['files'][$file_name]['total'] += microtime(true) - $start; 3719 unset($this->rootSuite['files'][$file_name]['start']); 3720 } 3721 3722 public function mergeResults(?JUnit $other): void 3723 { 3724 if (!$this->enabled || !$other) { 3725 return; 3726 } 3727 3728 $this->mergeSuites($this->rootSuite, $other->rootSuite); 3729 foreach ($other->suites as $name => $suite) { 3730 if (!isset($this->suites[$name])) { 3731 $this->suites[$name] = $suite; 3732 continue; 3733 } 3734 3735 $this->mergeSuites($this->suites[$name], $suite); 3736 } 3737 } 3738 3739 private function mergeSuites(array &$dest, array $source): void 3740 { 3741 $dest['test_total'] += $source['test_total']; 3742 $dest['test_pass'] += $source['test_pass']; 3743 $dest['test_fail'] += $source['test_fail']; 3744 $dest['test_error'] += $source['test_error']; 3745 $dest['test_skip'] += $source['test_skip']; 3746 $dest['test_warn'] += $source['test_warn']; 3747 $dest['execution_time'] += $source['execution_time']; 3748 $dest['files'] += $source['files']; 3749 } 3750} 3751 3752class SkipCache 3753{ 3754 private bool $enable; 3755 private bool $keepFile; 3756 3757 private array $skips = []; 3758 private array $extensions = []; 3759 3760 private int $hits = 0; 3761 private int $misses = 0; 3762 private int $extHits = 0; 3763 private int $extMisses = 0; 3764 3765 public function __construct(bool $enable, bool $keepFile) 3766 { 3767 $this->enable = $enable; 3768 $this->keepFile = $keepFile; 3769 } 3770 3771 public function checkSkip(string $php, string $code, string $checkFile, string $tempFile, array $env): string 3772 { 3773 // Extension tests frequently use something like <?php require 'skipif.inc'; 3774 // for skip checks. This forces us to cache per directory to avoid pollution. 3775 $dir = dirname($checkFile); 3776 $key = "$php => $dir"; 3777 3778 if (isset($this->skips[$key][$code])) { 3779 $this->hits++; 3780 if ($this->keepFile) { 3781 save_text($checkFile, $code, $tempFile); 3782 } 3783 return $this->skips[$key][$code]; 3784 } 3785 3786 save_text($checkFile, $code, $tempFile); 3787 $result = trim(system_with_timeout("$php \"$checkFile\"", $env)); 3788 if (strpos($result, 'nocache') === 0) { 3789 $result = ''; 3790 } else if ($this->enable) { 3791 $this->skips[$key][$code] = $result; 3792 } 3793 $this->misses++; 3794 3795 if (!$this->keepFile) { 3796 @unlink($checkFile); 3797 } 3798 3799 return $result; 3800 } 3801 3802 public function getExtensions(string $php): array 3803 { 3804 if (isset($this->extensions[$php])) { 3805 $this->extHits++; 3806 return $this->extensions[$php]; 3807 } 3808 3809 $extDir = `$php -d display_errors=0 -r "echo ini_get('extension_dir');"`; 3810 $extensions = explode(",", `$php -d display_errors=0 -r "echo implode(',', get_loaded_extensions());"`); 3811 $extensions = array_map('strtolower', $extensions); 3812 if (in_array('zend opcache', $extensions)) { 3813 $extensions[] = 'opcache'; 3814 } 3815 3816 $result = [$extDir, $extensions]; 3817 $this->extensions[$php] = $result; 3818 $this->extMisses++; 3819 3820 return $result; 3821 } 3822 3823// public function __destruct() 3824// { 3825// echo "Skips: {$this->hits} hits, {$this->misses} misses.\n"; 3826// echo "Extensions: {$this->extHits} hits, {$this->extMisses} misses.\n"; 3827// echo "Cache distribution:\n"; 3828// 3829// foreach ($this->skips as $php => $cache) { 3830// echo "$php: " . count($cache) . "\n"; 3831// } 3832// } 3833} 3834 3835class RuntestsValgrind 3836{ 3837 protected $version = ''; 3838 protected $header = ''; 3839 protected $version_3_8_0 = false; 3840 protected $tool = null; 3841 3842 public function getVersion(): string 3843 { 3844 return $this->version; 3845 } 3846 3847 public function getHeader(): string 3848 { 3849 return $this->header; 3850 } 3851 3852 public function __construct(array $environment, string $tool = 'memcheck') 3853 { 3854 $this->tool = $tool; 3855 $header = system_with_timeout("valgrind --tool={$this->tool} --version", $environment); 3856 if (!$header) { 3857 error("Valgrind returned no version info for {$this->tool}, cannot proceed.\n". 3858 "Please check if Valgrind is installed and the tool is named correctly."); 3859 } 3860 $count = 0; 3861 $version = preg_replace("/valgrind-(\d+)\.(\d+)\.(\d+)([.\w_-]+)?(\s+)/", '$1.$2.$3', $header, 1, $count); 3862 if ($count != 1) { 3863 error("Valgrind returned invalid version info (\"{$header}\") for {$this->tool}, cannot proceed."); 3864 } 3865 $this->version = $version; 3866 $this->header = sprintf( 3867 "%s (%s)", trim($header), $this->tool); 3868 $this->version_3_8_0 = version_compare($version, '3.8.0', '>='); 3869 } 3870 3871 public function wrapCommand(string $cmd, string $memcheck_filename, bool $check_all): string 3872 { 3873 $vcmd = "valgrind -q --tool={$this->tool} --trace-children=yes"; 3874 if ($check_all) { 3875 $vcmd .= ' --smc-check=all'; 3876 } 3877 3878 /* --vex-iropt-register-updates=allregs-at-mem-access is necessary for phpdbg watchpoint tests */ 3879 if ($this->version_3_8_0) { 3880 return "$vcmd --vex-iropt-register-updates=allregs-at-mem-access --log-file=$memcheck_filename $cmd"; 3881 } 3882 return "$vcmd --vex-iropt-precise-memory-exns=yes --log-file=$memcheck_filename $cmd"; 3883 } 3884} 3885 3886class TestFile 3887{ 3888 private string $fileName; 3889 3890 private array $sections = ['TEST' => '']; 3891 3892 private const ALLOWED_SECTIONS = [ 3893 'EXPECT', 'EXPECTF', 'EXPECTREGEX', 'EXPECTREGEX_EXTERNAL', 'EXPECT_EXTERNAL', 'EXPECTF_EXTERNAL', 'EXPECTHEADERS', 3894 'POST', 'POST_RAW', 'GZIP_POST', 'DEFLATE_POST', 'PUT', 'GET', 'COOKIE', 'ARGS', 3895 'FILE', 'FILEEOF', 'FILE_EXTERNAL', 'REDIRECTTEST', 3896 'CAPTURE_STDIO', 'STDIN', 'CGI', 'PHPDBG', 3897 'INI', 'ENV', 'EXTENSIONS', 3898 'SKIPIF', 'XFAIL', 'XLEAK', 'CLEAN', 3899 'CREDITS', 'DESCRIPTION', 'CONFLICTS', 'WHITESPACE_SENSITIVE', 3900 'FLAKY', 3901 ]; 3902 3903 /** 3904 * @throws BorkageException 3905 */ 3906 public function __construct(string $fileName, bool $inRedirect) 3907 { 3908 $this->fileName = $fileName; 3909 3910 $this->readFile(); 3911 $this->validateAndProcess($inRedirect); 3912 } 3913 3914 public function hasSection(string $name): bool 3915 { 3916 return isset($this->sections[$name]); 3917 } 3918 3919 public function hasAllSections(string ...$names): bool 3920 { 3921 foreach ($names as $section) { 3922 if (!isset($this->sections[$section])) { 3923 return false; 3924 } 3925 } 3926 3927 return true; 3928 } 3929 3930 public function hasAnySections(string ...$names): bool 3931 { 3932 foreach ($names as $section) { 3933 if (isset($this->sections[$section])) { 3934 return true; 3935 } 3936 } 3937 3938 return false; 3939 } 3940 3941 public function sectionNotEmpty(string $name): bool 3942 { 3943 return !empty($this->sections[$name]); 3944 } 3945 3946 /** 3947 * @throws Exception 3948 */ 3949 public function getSection(string $name): string 3950 { 3951 if (!isset($this->sections[$name])) { 3952 throw new Exception("Section $name not found"); 3953 } 3954 return $this->sections[$name]; 3955 } 3956 3957 public function getName(): string 3958 { 3959 return trim($this->getSection('TEST')); 3960 } 3961 3962 public function isCGI(): bool 3963 { 3964 return $this->hasSection('CGI') 3965 || $this->sectionNotEmpty('GET') 3966 || $this->sectionNotEmpty('POST') 3967 || $this->sectionNotEmpty('GZIP_POST') 3968 || $this->sectionNotEmpty('DEFLATE_POST') 3969 || $this->sectionNotEmpty('POST_RAW') 3970 || $this->sectionNotEmpty('PUT') 3971 || $this->sectionNotEmpty('COOKIE') 3972 || $this->sectionNotEmpty('EXPECTHEADERS'); 3973 } 3974 3975 /** 3976 * TODO Refactor to make it not needed 3977 */ 3978 public function setSection(string $name, string $value): void 3979 { 3980 $this->sections[$name] = $value; 3981 } 3982 3983 /** 3984 * Load the sections of the test file 3985 * @throws BorkageException 3986 */ 3987 private function readFile(): void 3988 { 3989 $fp = fopen($this->fileName, "rb") or error("Cannot open test file: {$this->fileName}"); 3990 3991 if (!feof($fp)) { 3992 $line = fgets($fp); 3993 3994 if ($line === false) { 3995 throw new BorkageException("cannot read test"); 3996 } 3997 } else { 3998 throw new BorkageException("empty test [{$this->fileName}]"); 3999 } 4000 if (strncmp('--TEST--', $line, 8)) { 4001 throw new BorkageException("tests must start with --TEST-- [{$this->fileName}]"); 4002 } 4003 4004 $section = 'TEST'; 4005 $secfile = false; 4006 $secdone = false; 4007 4008 while (!feof($fp)) { 4009 $line = fgets($fp); 4010 4011 if ($line === false) { 4012 break; 4013 } 4014 4015 // Match the beginning of a section. 4016 if (preg_match('/^--([_A-Z]+)--/', $line, $r)) { 4017 $section = (string) $r[1]; 4018 4019 if (isset($this->sections[$section]) && $this->sections[$section]) { 4020 throw new BorkageException("duplicated $section section"); 4021 } 4022 4023 // check for unknown sections 4024 if (!in_array($section, self::ALLOWED_SECTIONS)) { 4025 throw new BorkageException('Unknown section "' . $section . '"'); 4026 } 4027 4028 $this->sections[$section] = ''; 4029 $secfile = $section == 'FILE' || $section == 'FILEEOF' || $section == 'FILE_EXTERNAL'; 4030 $secdone = false; 4031 continue; 4032 } 4033 4034 // Add to the section text. 4035 if (!$secdone) { 4036 $this->sections[$section] .= $line; 4037 } 4038 4039 // End of actual test? 4040 if ($secfile && preg_match('/^===DONE===\s*$/', $line)) { 4041 $secdone = true; 4042 } 4043 } 4044 4045 fclose($fp); 4046 } 4047 4048 /** 4049 * @throws BorkageException 4050 */ 4051 private function validateAndProcess(bool $inRedirect): void 4052 { 4053 // the redirect section allows a set of tests to be reused outside of 4054 // a given test dir 4055 if ($this->hasSection('REDIRECTTEST')) { 4056 if ($inRedirect) { 4057 throw new BorkageException("Can't redirect a test from within a redirected test"); 4058 } 4059 return; 4060 } 4061 if (!$this->hasSection('PHPDBG') && $this->hasSection('FILE') + $this->hasSection('FILEEOF') + $this->hasSection('FILE_EXTERNAL') != 1) { 4062 throw new BorkageException("missing section --FILE--"); 4063 } 4064 4065 if ($this->hasSection('FILEEOF')) { 4066 $this->sections['FILE'] = preg_replace("/[\r\n]+$/", '', $this->sections['FILEEOF']); 4067 unset($this->sections['FILEEOF']); 4068 } 4069 4070 foreach (['FILE', 'EXPECT', 'EXPECTF', 'EXPECTREGEX'] as $prefix) { 4071 // For grepping: FILE_EXTERNAL, EXPECT_EXTERNAL, EXPECTF_EXTERNAL, EXPECTREGEX_EXTERNAL 4072 $key = $prefix . '_EXTERNAL'; 4073 4074 if ($this->hasSection($key)) { 4075 // don't allow tests to retrieve files from anywhere but this subdirectory 4076 $dir = dirname($this->fileName); 4077 $fileName = $dir . '/' . trim(str_replace('..', '', $this->getSection($key))); 4078 4079 if (file_exists($fileName)) { 4080 $this->sections[$prefix] = file_get_contents($fileName); 4081 } else { 4082 throw new BorkageException("could not load --" . $key . "-- " . $dir . '/' . trim($fileName)); 4083 } 4084 } 4085 } 4086 4087 if (($this->hasSection('EXPECT') + $this->hasSection('EXPECTF') + $this->hasSection('EXPECTREGEX')) != 1) { 4088 throw new BorkageException("missing section --EXPECT--, --EXPECTF-- or --EXPECTREGEX--"); 4089 } 4090 4091 if ($this->hasSection('PHPDBG') && !$this->hasSection('STDIN')) { 4092 $this->sections['STDIN'] = $this->sections['PHPDBG'] . "\n"; 4093 } 4094 } 4095} 4096 4097function init_output_buffers(): void 4098{ 4099 // Delete as much output buffers as possible. 4100 while (@ob_end_clean()) { 4101 } 4102 4103 if (ob_get_level()) { 4104 echo "Not all buffers were deleted.\n"; 4105 } 4106} 4107 4108function check_proc_open_function_exists(): void 4109{ 4110 if (!function_exists('proc_open')) { 4111 echo <<<NO_PROC_OPEN_ERROR 4112 4113+-----------------------------------------------------------+ 4114| ! ERROR ! | 4115| The test-suite requires that proc_open() is available. | 4116| Please check if you disabled it in php.ini. | 4117+-----------------------------------------------------------+ 4118 4119NO_PROC_OPEN_ERROR; 4120 exit(1); 4121 } 4122} 4123 4124function bless_failed_tests(array $failedTests): void 4125{ 4126 if (empty($failedTests)) { 4127 return; 4128 } 4129 $args = [ 4130 PHP_BINARY, 4131 __DIR__ . '/scripts/dev/bless_tests.php', 4132 ]; 4133 foreach ($failedTests as $test) { 4134 $args[] = $test['name']; 4135 } 4136 proc_open($args, [], $pipes); 4137} 4138 4139main(); 4140