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