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