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