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