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