1#!/usr/bin/env php 2<?php 3/* 4 +----------------------------------------------------------------------+ 5 | Copyright (c) The PHP Group | 6 +----------------------------------------------------------------------+ 7 | This source file is subject to version 3.01 of the PHP license, | 8 | that is bundled with this package in the file LICENSE, and is | 9 | available through the world-wide-web at the following url: | 10 | https://www.php.net/license/3_01.txt | 11 | If you did not receive a copy of the PHP license and are unable to | 12 | obtain it through the world-wide-web, please send a note to | 13 | license@php.net so we can mail you a copy immediately. | 14 +----------------------------------------------------------------------+ 15 | Authors: Ilia Alshanetsky <iliaa@php.net> | 16 | Preston L. Bannister <pbannister@php.net> | 17 | Marcus Boerger <helly@php.net> | 18 | Derick Rethans <derick@php.net> | 19 | Sander Roobol <sander@php.net> | 20 | Andrea Faulds <ajf@ajf.me> | 21 | (based on version by: Stig Bakken <ssb@php.net>) | 22 | (based on the PHP 3 test framework by Rasmus Lerdorf) | 23 +----------------------------------------------------------------------+ 24 */ 25 26/* Let there be no top-level code beyond this point: 27 * Only functions and classes, thanks! 28 * 29 * Minimum required PHP version: 7.4.0 30 */ 31 32function show_usage(): void 33{ 34 echo <<<HELP 35Synopsis: 36 php run-tests.php [options] [files] [directories] 37 38Options: 39 -j<workers> Run up to <workers> simultaneous testing processes in parallel for 40 quicker testing on systems with multiple logical processors. 41 Note that this is experimental feature. 42 43 -l <file> Read the testfiles to be executed from <file>. After the test 44 has finished all failed tests are written to the same <file>. 45 If the list is empty and no further test is specified then 46 all tests are executed (same as: -r <file> -w <file>). 47 48 -r <file> Read the testfiles to be executed from <file>. 49 50 -w <file> Write a list of all failed tests to <file>. 51 52 -a <file> Same as -w but append rather then truncating <file>. 53 54 -W <file> Write a list of all tests and their result status to <file>. 55 56 -c <file> Look for php.ini in directory <file> or use <file> as ini. 57 58 -n Pass -n option to the php binary (Do not use a php.ini). 59 60 -d foo=bar Pass -d option to the php binary (Define INI entry foo 61 with value 'bar'). 62 63 -g Comma separated list of groups to show during test run 64 (possible values: PASS, FAIL, XFAIL, XLEAK, SKIP, BORK, WARN, LEAK, REDIRECT). 65 66 -m Test for memory leaks with Valgrind (equivalent to -M memcheck). 67 68 -M <tool> Test for errors with Valgrind tool. 69 70 -p <php> Specify PHP executable to run. 71 72 -P Use PHP_BINARY as PHP executable to run (default). 73 74 -q Quiet, no user interaction (same as environment NO_INTERACTION). 75 76 -s <file> Write output to <file>. 77 78 -x Sets 'SKIP_SLOW_TESTS' environment variable. 79 80 --online Prevents setting the 'SKIP_ONLINE_TESTS' environment variable. 81 82 --offline Sets 'SKIP_ONLINE_TESTS' environment variable (default). 83 84 --verbose 85 -v Verbose mode. 86 87 --help 88 -h This Help. 89 90 --temp-source <sdir> --temp-target <tdir> [--temp-urlbase <url>] 91 Write temporary files to <tdir> by replacing <sdir> from the 92 filenames to generate with <tdir>. In general you want to make 93 <sdir> the path to your source files and <tdir> some patch in 94 your web page hierarchy with <url> pointing to <tdir>. 95 96 --keep-[all|php|skip|clean] 97 Do not delete 'all' files, 'php' test file, 'skip' or 'clean' 98 file. 99 100 --set-timeout <n> 101 Set timeout for individual tests, where <n> is the number of 102 seconds. The default value is 60 seconds, or 300 seconds when 103 testing for memory leaks. 104 105 --context <n> 106 Sets the number of lines of surrounding context to print for diffs. 107 The default value is 3. 108 109 --show-[all|php|skip|clean|exp|diff|out|mem] 110 Show 'all' files, 'php' test file, 'skip' or 'clean' file. You 111 can also use this to show the output 'out', the expected result 112 'exp', the difference between them 'diff' or the valgrind log 113 'mem'. The result types get written independent of the log format, 114 however 'diff' only exists when a test fails. 115 116 --show-slow <n> 117 Show all tests that took longer than <n> milliseconds to run. 118 119 --no-clean Do not execute clean section if any. 120 121 --color 122 --no-color Do/Don't colorize the result type in the test result. 123 124 --progress 125 --no-progress Do/Don't show the current progress. 126 127 --repeat [n] 128 Run the tests multiple times in the same process and check the 129 output of the last execution (CLI SAPI only). 130 131 --bless Bless failed tests using scripts/dev/bless_tests.php. 132 133HELP; 134} 135 136/** 137 * One function to rule them all, one function to find them, one function to 138 * bring them all and in the darkness bind them. 139 * This is the entry point and exit point überfunction. It contains all the 140 * code that was previously found at the top level. It could and should be 141 * refactored to be smaller and more manageable. 142 */ 143function main(): void 144{ 145 /* This list was derived in a naïve mechanical fashion. If a member 146 * looks like it doesn't belong, it probably doesn't; cull at will. 147 */ 148 global $DETAILED, $PHP_FAILED_TESTS, $SHOW_ONLY_GROUPS, $argc, $argv, $cfg, 149 $end_time, $environment, 150 $exts_skipped, $exts_tested, $exts_to_test, $failed_tests_file, 151 $ignored_by_ext, $ini_overwrites, $colorize, 152 $log_format, $no_clean, $no_file_cache, 153 $pass_options, $php, $php_cgi, $preload, 154 $result_tests_file, $slow_min_ms, $start_time, 155 $temp_source, $temp_target, $test_cnt, 156 $test_files, $test_idx, $test_results, $testfile, 157 $valgrind, $sum_results, $shuffle, $file_cache, $num_repeats, 158 $show_progress; 159 // Parallel testing 160 global $workers, $workerID; 161 global $context_line_count; 162 163 // Temporary for the duration of refactoring 164 /** @var JUnit $junit */ 165 global $junit; 166 167 define('IS_WINDOWS', substr(PHP_OS, 0, 3) == "WIN"); 168 169 $workerID = 0; 170 if (getenv("TEST_PHP_WORKER")) { 171 $workerID = intval(getenv("TEST_PHP_WORKER")); 172 run_worker(); 173 return; 174 } 175 176 define('INIT_DIR', getcwd()); 177 178 // Change into the PHP source directory. 179 if (getenv('TEST_PHP_SRCDIR')) { 180 @chdir(getenv('TEST_PHP_SRCDIR')); 181 } 182 183 define('TEST_PHP_SRCDIR', getcwd()); 184 185 check_proc_open_function_exists(); 186 187 // If timezone is not set, use UTC. 188 if (ini_get('date.timezone') == '') { 189 date_default_timezone_set('UTC'); 190 } 191 192 // Delete some security related environment variables 193 putenv('SSH_CLIENT=deleted'); 194 putenv('SSH_AUTH_SOCK=deleted'); 195 putenv('SSH_TTY=deleted'); 196 putenv('SSH_CONNECTION=deleted'); 197 198 set_time_limit(0); 199 200 ini_set('pcre.backtrack_limit', PHP_INT_MAX); 201 202 init_output_buffers(); 203 204 error_reporting(E_ALL); 205 206 $environment = $_ENV ?? []; 207 208 // Some configurations like php.ini-development set variables_order="GPCS" 209 // not "EGPCS", in which case $_ENV is NOT populated. Detect if the $_ENV 210 // was empty and handle it by explicitly populating through getenv(). 211 if (empty($environment)) { 212 $environment = getenv(); 213 } 214 215 if (empty($environment['TEMP'])) { 216 $environment['TEMP'] = sys_get_temp_dir(); 217 218 if (empty($environment['TEMP'])) { 219 // For example, OpCache on Windows will fail in this case because 220 // child processes (for tests) will not get a TEMP variable, so 221 // GetTempPath() will fallback to c:\windows, while GetTempPath() 222 // will return %TEMP% for parent (likely a different path). The 223 // parent will initialize the OpCache in that path, and child will 224 // fail to reattach to the OpCache because it will be using the 225 // wrong path. 226 die("TEMP environment is NOT set"); 227 } 228 229 if (count($environment) == 1) { 230 // Not having other environment variables, only having TEMP, is 231 // probably ok, but strange and may make a difference in the 232 // test pass rate, so warn the user. 233 echo "WARNING: Only 1 environment variable will be available to tests(TEMP environment variable)" , PHP_EOL; 234 } 235 } 236 237 if (IS_WINDOWS && empty($environment["SystemRoot"])) { 238 $environment["SystemRoot"] = getenv("SystemRoot"); 239 } 240 241 if (getenv('TEST_PHP_LOG_FORMAT')) { 242 $log_format = strtoupper(getenv('TEST_PHP_LOG_FORMAT')); 243 } else { 244 $log_format = 'LEODS'; 245 } 246 247 // Check whether a detailed log is wanted. 248 if (getenv('TEST_PHP_DETAILED')) { 249 $DETAILED = getenv('TEST_PHP_DETAILED'); 250 } else { 251 $DETAILED = 0; 252 } 253 254 $junit = new JUnit($environment, $workerID); 255 256 if (getenv('SHOW_ONLY_GROUPS')) { 257 $SHOW_ONLY_GROUPS = explode(",", getenv('SHOW_ONLY_GROUPS')); 258 } else { 259 $SHOW_ONLY_GROUPS = []; 260 } 261 262 // Check whether user test dirs are requested. 263 $user_tests = []; 264 if (getenv('TEST_PHP_USER')) { 265 $user_tests = explode(',', getenv('TEST_PHP_USER')); 266 } 267 268 $exts_to_test = []; 269 $ini_overwrites = [ 270 'output_handler=', 271 'open_basedir=', 272 'disable_functions=', 273 'output_buffering=Off', 274 'error_reporting=' . E_ALL, 275 'display_errors=1', 276 'display_startup_errors=1', 277 'log_errors=0', 278 'html_errors=0', 279 'track_errors=0', 280 'report_memleaks=1', 281 'report_zend_debug=0', 282 'docref_root=', 283 'docref_ext=.html', 284 'error_prepend_string=', 285 'error_append_string=', 286 'auto_prepend_file=', 287 'auto_append_file=', 288 'ignore_repeated_errors=0', 289 'precision=14', 290 'serialize_precision=-1', 291 'memory_limit=128M', 292 'opcache.fast_shutdown=0', 293 'opcache.file_update_protection=0', 294 'opcache.revalidate_freq=0', 295 'opcache.jit_hot_loop=1', 296 'opcache.jit_hot_func=1', 297 'opcache.jit_hot_return=1', 298 'opcache.jit_hot_side_exit=1', 299 'opcache.jit_max_root_traces=100000', 300 'opcache.jit_max_side_traces=100000', 301 'opcache.jit_max_exit_counters=100000', 302 'opcache.protect_memory=1', 303 'zend.assertions=1', 304 'zend.exception_ignore_args=0', 305 'zend.exception_string_param_max_len=15', 306 'short_open_tag=0', 307 ]; 308 309 $no_file_cache = '-d opcache.file_cache= -d opcache.file_cache_only=0'; 310 311 // Determine the tests to be run. 312 313 $test_files = []; 314 $redir_tests = []; 315 $test_results = []; 316 $PHP_FAILED_TESTS = [ 317 'BORKED' => [], 318 'FAILED' => [], 319 'WARNED' => [], 320 'LEAKED' => [], 321 'XFAILED' => [], 322 'XLEAKED' => [], 323 'SLOW' => [] 324 ]; 325 326 // If parameters given assume they represent selected tests to run. 327 $result_tests_file = false; 328 $failed_tests_file = false; 329 $pass_option_n = false; 330 $pass_options = ''; 331 332 $output_file = INIT_DIR . '/php_test_results_' . date('Ymd_Hi') . '.txt'; 333 334 $just_save_results = false; 335 $valgrind = null; 336 $temp_source = null; 337 $temp_target = null; 338 $conf_passed = null; 339 $no_clean = false; 340 $colorize = true; 341 if (function_exists('sapi_windows_vt100_support') && !sapi_windows_vt100_support(STDOUT, true)) { 342 $colorize = false; 343 } 344 if (array_key_exists('NO_COLOR', $environment)) { 345 $colorize = false; 346 } 347 $selected_tests = false; 348 $slow_min_ms = INF; 349 $preload = false; 350 $file_cache = null; 351 $shuffle = false; 352 $bless = false; 353 $workers = null; 354 $context_line_count = 3; 355 $num_repeats = 1; 356 $show_progress = true; 357 $ignored_by_ext = []; 358 $online = null; 359 360 $cfgtypes = ['show', 'keep']; 361 $cfgfiles = ['skip', 'php', 'clean', 'out', 'diff', 'exp', 'mem']; 362 $cfg = []; 363 364 foreach ($cfgtypes as $type) { 365 $cfg[$type] = []; 366 367 foreach ($cfgfiles as $file) { 368 $cfg[$type][$file] = false; 369 } 370 } 371 372 if (!isset($argc, $argv) || !$argc) { 373 $argv = [__FILE__]; 374 $argc = 1; 375 } 376 377 if (getenv('TEST_PHP_ARGS')) { 378 $argv = array_merge($argv, explode(' ', getenv('TEST_PHP_ARGS'))); 379 $argc = count($argv); 380 } 381 382 for ($i = 1; $i < $argc; $i++) { 383 $is_switch = false; 384 $switch = substr($argv[$i], 1, 1); 385 $repeat = substr($argv[$i], 0, 1) == '-'; 386 387 while ($repeat) { 388 if (!$is_switch) { 389 $switch = substr($argv[$i], 1, 1); 390 } 391 392 $is_switch = true; 393 394 foreach ($cfgtypes as $type) { 395 if (strpos($switch, '--' . $type) === 0) { 396 foreach ($cfgfiles as $file) { 397 if ($switch == '--' . $type . '-' . $file) { 398 $cfg[$type][$file] = true; 399 $is_switch = false; 400 break; 401 } 402 } 403 } 404 } 405 406 if (!$is_switch) { 407 $is_switch = true; 408 break; 409 } 410 411 $repeat = false; 412 413 switch ($switch) { 414 case 'j': 415 $workers = substr($argv[$i], 2); 416 if ($workers == 0 || !preg_match('/^\d+$/', $workers)) { 417 error("'$workers' is not a valid number of workers, try e.g. -j16 for 16 workers"); 418 } 419 $workers = intval($workers, 10); 420 // Don't use parallel testing infrastructure if there is only one worker. 421 if ($workers === 1) { 422 $workers = null; 423 } 424 break; 425 case 'r': 426 case 'l': 427 $test_list = file($argv[++$i]); 428 if ($test_list) { 429 foreach ($test_list as $test) { 430 $matches = []; 431 if (preg_match('/^#.*\[(.*)\]\:\s+(.*)$/', $test, $matches)) { 432 $redir_tests[] = [$matches[1], $matches[2]]; 433 } elseif (strlen($test)) { 434 $test_files[] = trim($test); 435 } 436 } 437 } 438 if ($switch != 'l') { 439 break; 440 } 441 $i--; 442 // no break 443 case 'w': 444 $failed_tests_file = fopen($argv[++$i], 'w+t'); 445 break; 446 case 'a': 447 $failed_tests_file = fopen($argv[++$i], 'a+t'); 448 break; 449 case 'W': 450 $result_tests_file = fopen($argv[++$i], 'w+t'); 451 break; 452 case 'c': 453 $conf_passed = $argv[++$i]; 454 break; 455 case 'd': 456 $ini_overwrites[] = $argv[++$i]; 457 break; 458 case 'g': 459 $SHOW_ONLY_GROUPS = explode(",", $argv[++$i]); 460 break; 461 case '--keep-all': 462 foreach ($cfgfiles as $file) { 463 $cfg['keep'][$file] = true; 464 } 465 break; 466 case 'm': 467 $valgrind = new RuntestsValgrind($environment); 468 break; 469 case 'M': 470 $valgrind = new RuntestsValgrind($environment, $argv[++$i]); 471 break; 472 case 'n': 473 if (!$pass_option_n) { 474 $pass_options .= ' -n'; 475 } 476 $pass_option_n = true; 477 break; 478 case 'e': 479 $pass_options .= ' -e'; 480 break; 481 case '--preload': 482 $preload = true; 483 $environment['SKIP_PRELOAD'] = 1; 484 break; 485 case '--file-cache-prime': 486 $file_cache = 'prime'; 487 break; 488 case '--file-cache-use': 489 $file_cache = 'use'; 490 break; 491 case '--no-clean': 492 $no_clean = true; 493 break; 494 case '--color': 495 $colorize = true; 496 break; 497 case '--no-color': 498 $colorize = false; 499 break; 500 case 'p': 501 $php = $argv[++$i]; 502 putenv("TEST_PHP_EXECUTABLE=$php"); 503 $environment['TEST_PHP_EXECUTABLE'] = $php; 504 break; 505 case 'P': 506 $php = PHP_BINARY; 507 putenv("TEST_PHP_EXECUTABLE=$php"); 508 $environment['TEST_PHP_EXECUTABLE'] = $php; 509 break; 510 case 'q': 511 putenv('NO_INTERACTION=1'); 512 $environment['NO_INTERACTION'] = 1; 513 break; 514 case 's': 515 $output_file = $argv[++$i]; 516 $just_save_results = true; 517 break; 518 case '--set-timeout': 519 $timeout = $argv[++$i] ?? ''; 520 if (!preg_match('/^\d+$/', $timeout)) { 521 error("'$timeout' is not a valid number of seconds, try e.g. --set-timeout 60 for 1 minute"); 522 } 523 $environment['TEST_TIMEOUT'] = intval($timeout, 10); 524 break; 525 case '--context': 526 $context_line_count = $argv[++$i] ?? ''; 527 if (!preg_match('/^\d+$/', $context_line_count)) { 528 error("'$context_line_count' is not a valid number of lines of context, try e.g. --context 3 for 3 lines"); 529 } 530 $context_line_count = intval($context_line_count, 10); 531 break; 532 case '--show-all': 533 foreach ($cfgfiles as $file) { 534 $cfg['show'][$file] = true; 535 } 536 break; 537 case '--show-slow': 538 $slow_min_ms = $argv[++$i] ?? ''; 539 if (!preg_match('/^\d+$/', $slow_min_ms)) { 540 error("'$slow_min_ms' is not a valid number of milliseconds, try e.g. --show-slow 1000 for 1 second"); 541 } 542 $slow_min_ms = intval($slow_min_ms, 10); 543 break; 544 case '--temp-source': 545 $temp_source = $argv[++$i]; 546 break; 547 case '--temp-target': 548 $temp_target = $argv[++$i]; 549 break; 550 case 'v': 551 case '--verbose': 552 $DETAILED = true; 553 break; 554 case 'x': 555 $environment['SKIP_SLOW_TESTS'] = 1; 556 break; 557 case '--online': 558 $online = true; 559 break; 560 case '--offline': 561 $online = false; 562 break; 563 case '--shuffle': 564 $shuffle = true; 565 break; 566 case '--asan': 567 case '--msan': 568 $environment['USE_ZEND_ALLOC'] = 0; 569 $environment['USE_TRACKED_ALLOC'] = 1; 570 $environment['SKIP_ASAN'] = 1; 571 $environment['SKIP_PERF_SENSITIVE'] = 1; 572 if ($switch === '--msan') { 573 $environment['SKIP_MSAN'] = 1; 574 $environment['MSAN_OPTIONS'] = 'intercept_tls_get_addr=0'; 575 } 576 577 $lsanSuppressions = __DIR__ . '/.github/lsan-suppressions.txt'; 578 if (file_exists($lsanSuppressions)) { 579 $environment['LSAN_OPTIONS'] = 'suppressions=' . $lsanSuppressions 580 . ':print_suppressions=0'; 581 } 582 break; 583 case '--repeat': 584 $num_repeats = (int) $argv[++$i]; 585 $environment['SKIP_REPEAT'] = 1; 586 break; 587 case '--bless': 588 $bless = true; 589 break; 590 case '-': 591 // repeat check with full switch 592 $switch = $argv[$i]; 593 if ($switch != '-') { 594 $repeat = true; 595 } 596 break; 597 case '--progress': 598 $show_progress = true; 599 break; 600 case '--no-progress': 601 $show_progress = false; 602 break; 603 case '--version': 604 echo '$Id: 258eae1fb2da984c478745eb3fd0a9a639710ec3 $' . "\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['XFAILED'])) { 3074 $failed_test_summary .= ' 3075===================================================================== 3076EXPECTED FAILED TEST SUMMARY 3077--------------------------------------------------------------------- 3078'; 3079 foreach ($PHP_FAILED_TESTS['XFAILED'] as $failed_test_data) { 3080 $failed_test_summary .= $failed_test_data['test_name'] . $failed_test_data['info'] . "\n"; 3081 } 3082 $failed_test_summary .= "=====================================================================\n"; 3083 } 3084 3085 if (count($PHP_FAILED_TESTS['BORKED'])) { 3086 $failed_test_summary .= ' 3087===================================================================== 3088BORKED TEST SUMMARY 3089--------------------------------------------------------------------- 3090'; 3091 foreach ($PHP_FAILED_TESTS['BORKED'] as $failed_test_data) { 3092 $failed_test_summary .= $failed_test_data['info'] . "\n"; 3093 } 3094 3095 $failed_test_summary .= "=====================================================================\n"; 3096 } 3097 3098 if (count($PHP_FAILED_TESTS['FAILED'])) { 3099 $failed_test_summary .= ' 3100===================================================================== 3101FAILED TEST SUMMARY 3102--------------------------------------------------------------------- 3103'; 3104 foreach ($PHP_FAILED_TESTS['FAILED'] as $failed_test_data) { 3105 $failed_test_summary .= $failed_test_data['test_name'] . $failed_test_data['info'] . "\n"; 3106 } 3107 $failed_test_summary .= "=====================================================================\n"; 3108 } 3109 if (count($PHP_FAILED_TESTS['WARNED'])) { 3110 $failed_test_summary .= ' 3111===================================================================== 3112WARNED TEST SUMMARY 3113--------------------------------------------------------------------- 3114'; 3115 foreach ($PHP_FAILED_TESTS['WARNED'] as $failed_test_data) { 3116 $failed_test_summary .= $failed_test_data['test_name'] . $failed_test_data['info'] . "\n"; 3117 } 3118 3119 $failed_test_summary .= "=====================================================================\n"; 3120 } 3121 3122 if (count($PHP_FAILED_TESTS['LEAKED'])) { 3123 $failed_test_summary .= ' 3124===================================================================== 3125LEAKED TEST SUMMARY 3126--------------------------------------------------------------------- 3127'; 3128 foreach ($PHP_FAILED_TESTS['LEAKED'] as $failed_test_data) { 3129 $failed_test_summary .= $failed_test_data['test_name'] . $failed_test_data['info'] . "\n"; 3130 } 3131 3132 $failed_test_summary .= "=====================================================================\n"; 3133 } 3134 3135 if (count($PHP_FAILED_TESTS['XLEAKED'])) { 3136 $failed_test_summary .= ' 3137===================================================================== 3138EXPECTED LEAK TEST SUMMARY 3139--------------------------------------------------------------------- 3140'; 3141 foreach ($PHP_FAILED_TESTS['XLEAKED'] as $failed_test_data) { 3142 $failed_test_summary .= $failed_test_data['test_name'] . $failed_test_data['info'] . "\n"; 3143 } 3144 3145 $failed_test_summary .= "=====================================================================\n"; 3146 } 3147 3148 if ($failed_test_summary && !getenv('NO_PHPTEST_SUMMARY')) { 3149 $summary .= $failed_test_summary; 3150 } 3151 3152 return $summary; 3153} 3154 3155function show_start(int $start_timestamp): void 3156{ 3157 echo "TIME START " . date('Y-m-d H:i:s', $start_timestamp) . "\n=====================================================================\n"; 3158} 3159 3160function show_end(int $start_timestamp, int|float $start_time, int|float $end_time): void 3161{ 3162 echo "=====================================================================\nTIME END " . date('Y-m-d H:i:s', $start_timestamp + (int)(($end_time - $start_time)/1e9)) . "\n"; 3163} 3164 3165function show_summary(): void 3166{ 3167 echo get_summary(true); 3168} 3169 3170function show_redirect_start(string $tests, string $tested, string $tested_file): void 3171{ 3172 global $SHOW_ONLY_GROUPS, $show_progress; 3173 3174 if (!$SHOW_ONLY_GROUPS || in_array('REDIRECT', $SHOW_ONLY_GROUPS)) { 3175 echo "REDIRECT $tests ($tested [$tested_file]) begin\n"; 3176 } elseif ($show_progress) { 3177 clear_show_test(); 3178 } 3179} 3180 3181function show_redirect_ends(string $tests, string $tested, string $tested_file): void 3182{ 3183 global $SHOW_ONLY_GROUPS, $show_progress; 3184 3185 if (!$SHOW_ONLY_GROUPS || in_array('REDIRECT', $SHOW_ONLY_GROUPS)) { 3186 echo "REDIRECT $tests ($tested [$tested_file]) done\n"; 3187 } elseif ($show_progress) { 3188 clear_show_test(); 3189 } 3190} 3191 3192function show_test(int $test_idx, string $shortname): void 3193{ 3194 global $test_cnt; 3195 global $line_length; 3196 3197 $str = "TEST $test_idx/$test_cnt [$shortname]\r"; 3198 $line_length = strlen($str); 3199 echo $str; 3200 flush(); 3201} 3202 3203function clear_show_test(): void 3204{ 3205 global $line_length; 3206 // Parallel testing 3207 global $workerID; 3208 3209 if (!$workerID && isset($line_length)) { 3210 // Write over the last line to avoid random trailing chars on next echo 3211 echo str_repeat(" ", $line_length), "\r"; 3212 } 3213} 3214 3215function parse_conflicts(string $text): array 3216{ 3217 // Strip comments 3218 $text = preg_replace('/#.*/', '', $text); 3219 return array_map('trim', explode("\n", trim($text))); 3220} 3221 3222function show_result( 3223 string $result, 3224 string $tested, 3225 string $tested_file, 3226 string $extra = '' 3227): void { 3228 global $SHOW_ONLY_GROUPS, $colorize, $show_progress; 3229 3230 if (!$SHOW_ONLY_GROUPS || in_array($result, $SHOW_ONLY_GROUPS)) { 3231 if ($colorize) { 3232 /* Use ANSI escape codes for coloring test result */ 3233 switch ( $result ) { 3234 case 'PASS': // Light Green 3235 $color = "\e[1;32m{$result}\e[0m"; break; 3236 case 'FAIL': 3237 case 'BORK': 3238 case 'LEAK': 3239 case 'LEAK&FAIL': 3240 // Light Red 3241 $color = "\e[1;31m{$result}\e[0m"; break; 3242 default: // Yellow 3243 $color = "\e[1;33m{$result}\e[0m"; break; 3244 } 3245 3246 echo "$color $tested [$tested_file] $extra\n"; 3247 } else { 3248 echo "$result $tested [$tested_file] $extra\n"; 3249 } 3250 } elseif ($show_progress) { 3251 clear_show_test(); 3252 } 3253} 3254 3255class BorkageException extends Exception 3256{ 3257} 3258 3259class JUnit 3260{ 3261 private bool $enabled = true; 3262 private $fp = null; 3263 private array $suites = []; 3264 private array $rootSuite = self::EMPTY_SUITE + ['name' => 'php']; 3265 3266 private const EMPTY_SUITE = [ 3267 'test_total' => 0, 3268 'test_pass' => 0, 3269 'test_fail' => 0, 3270 'test_error' => 0, 3271 'test_skip' => 0, 3272 'test_warn' => 0, 3273 'files' => [], 3274 'execution_time' => 0, 3275 ]; 3276 3277 /** 3278 * @throws Exception 3279 */ 3280 public function __construct(array $env, int $workerID) 3281 { 3282 // Check whether a junit log is wanted. 3283 $fileName = $env['TEST_PHP_JUNIT'] ?? null; 3284 if (empty($fileName)) { 3285 $this->enabled = false; 3286 return; 3287 } 3288 if (!$workerID && !$this->fp = fopen($fileName, 'w')) { 3289 throw new Exception("Failed to open $fileName for writing."); 3290 } 3291 } 3292 3293 public function isEnabled(): bool 3294 { 3295 return $this->enabled; 3296 } 3297 3298 public function clear(): void 3299 { 3300 $this->rootSuite = self::EMPTY_SUITE + ['name' => 'php']; 3301 $this->suites = []; 3302 } 3303 3304 public function saveXML(): void 3305 { 3306 if (!$this->enabled) { 3307 return; 3308 } 3309 3310 $xml = '<' . '?' . 'xml version="1.0" encoding="UTF-8"' . '?' . '>' . PHP_EOL; 3311 $xml .= sprintf( 3312 '<testsuites name="%s" tests="%s" failures="%d" errors="%d" skip="%d" time="%s">' . PHP_EOL, 3313 $this->rootSuite['name'], 3314 $this->rootSuite['test_total'], 3315 $this->rootSuite['test_fail'], 3316 $this->rootSuite['test_error'], 3317 $this->rootSuite['test_skip'], 3318 $this->rootSuite['execution_time'] 3319 ); 3320 $xml .= $this->getSuitesXML(); 3321 $xml .= '</testsuites>'; 3322 fwrite($this->fp, $xml); 3323 } 3324 3325 private function getSuitesXML(): string 3326 { 3327 $result = ''; 3328 3329 foreach ($this->suites as $suite_name => $suite) { 3330 $result .= sprintf( 3331 '<testsuite name="%s" tests="%s" failures="%d" errors="%d" skip="%d" time="%s">' . PHP_EOL, 3332 $suite['name'], 3333 $suite['test_total'], 3334 $suite['test_fail'], 3335 $suite['test_error'], 3336 $suite['test_skip'], 3337 $suite['execution_time'] 3338 ); 3339 3340 if (!empty($suite_name)) { 3341 foreach ($suite['files'] as $file) { 3342 $result .= $this->rootSuite['files'][$file]['xml']; 3343 } 3344 } 3345 3346 $result .= '</testsuite>' . PHP_EOL; 3347 } 3348 3349 return $result; 3350 } 3351 3352 public function markTestAs( 3353 $type, 3354 string $file_name, 3355 string $test_name, 3356 ?int $time = null, 3357 string $message = '', 3358 string $details = '' 3359 ): void { 3360 if (!$this->enabled) { 3361 return; 3362 } 3363 3364 $suite = $this->getSuiteName($file_name); 3365 3366 $this->record($suite, 'test_total'); 3367 3368 $time = $time ?? $this->getTimer($file_name); 3369 $this->record($suite, 'execution_time', $time); 3370 3371 $escaped_details = htmlspecialchars($details, ENT_QUOTES, 'UTF-8'); 3372 $escaped_details = preg_replace_callback('/[\0-\x08\x0B\x0C\x0E-\x1F]/', function ($c) { 3373 return sprintf('[[0x%02x]]', ord($c[0])); 3374 }, $escaped_details); 3375 $escaped_message = htmlspecialchars($message, ENT_QUOTES, 'UTF-8'); 3376 3377 $escaped_test_name = htmlspecialchars($file_name . ' (' . $test_name . ')', ENT_QUOTES); 3378 $this->rootSuite['files'][$file_name]['xml'] = "<testcase name='$escaped_test_name' time='$time'>\n"; 3379 3380 if (is_array($type)) { 3381 $output_type = $type[0] . 'ED'; 3382 $temp = array_intersect(['XFAIL', 'XLEAK', 'FAIL', 'WARN'], $type); 3383 $type = reset($temp); 3384 } else { 3385 $output_type = $type . 'ED'; 3386 } 3387 3388 if ('PASS' == $type || 'XFAIL' == $type || 'XLEAK' == $type) { 3389 $this->record($suite, 'test_pass'); 3390 } elseif ('BORK' == $type) { 3391 $this->record($suite, 'test_error'); 3392 $this->rootSuite['files'][$file_name]['xml'] .= "<error type='$output_type' message='$escaped_message'/>\n"; 3393 } elseif ('SKIP' == $type) { 3394 $this->record($suite, 'test_skip'); 3395 $this->rootSuite['files'][$file_name]['xml'] .= "<skipped>$escaped_message</skipped>\n"; 3396 } elseif ('WARN' == $type) { 3397 $this->record($suite, 'test_warn'); 3398 $this->rootSuite['files'][$file_name]['xml'] .= "<warning>$escaped_message</warning>\n"; 3399 } elseif ('FAIL' == $type) { 3400 $this->record($suite, 'test_fail'); 3401 $this->rootSuite['files'][$file_name]['xml'] .= "<failure type='$output_type' message='$escaped_message'>$escaped_details</failure>\n"; 3402 } else { 3403 $this->record($suite, 'test_error'); 3404 $this->rootSuite['files'][$file_name]['xml'] .= "<error type='$output_type' message='$escaped_message'>$escaped_details</error>\n"; 3405 } 3406 3407 $this->rootSuite['files'][$file_name]['xml'] .= "</testcase>\n"; 3408 } 3409 3410 private function record(string $suite, string $param, $value = 1): void 3411 { 3412 $this->rootSuite[$param] += $value; 3413 $this->suites[$suite][$param] += $value; 3414 } 3415 3416 private function getTimer(string $file_name) 3417 { 3418 if (!$this->enabled) { 3419 return 0; 3420 } 3421 3422 if (isset($this->rootSuite['files'][$file_name]['total'])) { 3423 return number_format($this->rootSuite['files'][$file_name]['total'], 4); 3424 } 3425 3426 return 0; 3427 } 3428 3429 public function startTimer(string $file_name): void 3430 { 3431 if (!$this->enabled) { 3432 return; 3433 } 3434 3435 if (!isset($this->rootSuite['files'][$file_name]['start'])) { 3436 $this->rootSuite['files'][$file_name]['start'] = microtime(true); 3437 3438 $suite = $this->getSuiteName($file_name); 3439 $this->initSuite($suite); 3440 $this->suites[$suite]['files'][$file_name] = $file_name; 3441 } 3442 } 3443 3444 public function getSuiteName(string $file_name): string 3445 { 3446 return $this->pathToClassName(dirname($file_name)); 3447 } 3448 3449 private function pathToClassName(string $file_name): string 3450 { 3451 if (!$this->enabled) { 3452 return ''; 3453 } 3454 3455 $ret = $this->rootSuite['name']; 3456 $_tmp = []; 3457 3458 // lookup whether we're in the PHP source checkout 3459 $max = 5; 3460 if (is_file($file_name)) { 3461 $dir = dirname(realpath($file_name)); 3462 } else { 3463 $dir = realpath($file_name); 3464 } 3465 do { 3466 array_unshift($_tmp, basename($dir)); 3467 $chk = $dir . DIRECTORY_SEPARATOR . "main" . DIRECTORY_SEPARATOR . "php_version.h"; 3468 $dir = dirname($dir); 3469 } while (!file_exists($chk) && --$max > 0); 3470 if (file_exists($chk)) { 3471 if ($max) { 3472 array_shift($_tmp); 3473 } 3474 foreach ($_tmp as $p) { 3475 $ret .= "." . preg_replace(",[^a-z0-9]+,i", ".", $p); 3476 } 3477 return $ret; 3478 } 3479 3480 return $this->rootSuite['name'] . '.' . str_replace([DIRECTORY_SEPARATOR, '-'], '.', $file_name); 3481 } 3482 3483 public function initSuite(string $suite_name): void 3484 { 3485 if (!$this->enabled) { 3486 return; 3487 } 3488 3489 if (!empty($this->suites[$suite_name])) { 3490 return; 3491 } 3492 3493 $this->suites[$suite_name] = self::EMPTY_SUITE + ['name' => $suite_name]; 3494 } 3495 3496 /** 3497 * @throws Exception 3498 */ 3499 public function stopTimer(string $file_name): void 3500 { 3501 if (!$this->enabled) { 3502 return; 3503 } 3504 3505 if (!isset($this->rootSuite['files'][$file_name]['start'])) { 3506 throw new Exception("Timer for $file_name was not started!"); 3507 } 3508 3509 if (!isset($this->rootSuite['files'][$file_name]['total'])) { 3510 $this->rootSuite['files'][$file_name]['total'] = 0; 3511 } 3512 3513 $start = $this->rootSuite['files'][$file_name]['start']; 3514 $this->rootSuite['files'][$file_name]['total'] += microtime(true) - $start; 3515 unset($this->rootSuite['files'][$file_name]['start']); 3516 } 3517 3518 public function mergeResults(?JUnit $other): void 3519 { 3520 if (!$this->enabled || !$other) { 3521 return; 3522 } 3523 3524 $this->mergeSuites($this->rootSuite, $other->rootSuite); 3525 foreach ($other->suites as $name => $suite) { 3526 if (!isset($this->suites[$name])) { 3527 $this->suites[$name] = $suite; 3528 continue; 3529 } 3530 3531 $this->mergeSuites($this->suites[$name], $suite); 3532 } 3533 } 3534 3535 private function mergeSuites(array &$dest, array $source): void 3536 { 3537 $dest['test_total'] += $source['test_total']; 3538 $dest['test_pass'] += $source['test_pass']; 3539 $dest['test_fail'] += $source['test_fail']; 3540 $dest['test_error'] += $source['test_error']; 3541 $dest['test_skip'] += $source['test_skip']; 3542 $dest['test_warn'] += $source['test_warn']; 3543 $dest['execution_time'] += $source['execution_time']; 3544 $dest['files'] += $source['files']; 3545 } 3546} 3547 3548class SkipCache 3549{ 3550 private bool $enable; 3551 private bool $keepFile; 3552 3553 private array $skips = []; 3554 private array $extensions = []; 3555 3556 private int $hits = 0; 3557 private int $misses = 0; 3558 private int $extHits = 0; 3559 private int $extMisses = 0; 3560 3561 public function __construct(bool $enable, bool $keepFile) 3562 { 3563 $this->enable = $enable; 3564 $this->keepFile = $keepFile; 3565 } 3566 3567 public function checkSkip(string $php, string $code, string $checkFile, string $tempFile, array $env): string 3568 { 3569 // Extension tests frequently use something like <?php require 'skipif.inc'; 3570 // for skip checks. This forces us to cache per directory to avoid pollution. 3571 $dir = dirname($checkFile); 3572 $key = "$php => $dir"; 3573 3574 if (isset($this->skips[$key][$code])) { 3575 $this->hits++; 3576 if ($this->keepFile) { 3577 save_text($checkFile, $code, $tempFile); 3578 } 3579 return $this->skips[$key][$code]; 3580 } 3581 3582 save_text($checkFile, $code, $tempFile); 3583 $result = trim(system_with_timeout("$php \"$checkFile\"", $env)); 3584 if (strpos($result, 'nocache') === 0) { 3585 $result = ''; 3586 } else if ($this->enable) { 3587 $this->skips[$key][$code] = $result; 3588 } 3589 $this->misses++; 3590 3591 if (!$this->keepFile) { 3592 @unlink($checkFile); 3593 } 3594 3595 return $result; 3596 } 3597 3598 public function getExtensions(string $php): array 3599 { 3600 if (isset($this->extensions[$php])) { 3601 $this->extHits++; 3602 return $this->extensions[$php]; 3603 } 3604 3605 $extDir = shell_exec("$php -d display_errors=0 -r \"echo ini_get('extension_dir');\""); 3606 $extensionsNames = explode(",", shell_exec("$php -d display_errors=0 -r \"echo implode(',', get_loaded_extensions());\"")); 3607 $extensions = remap_loaded_extensions_names($extensionsNames); 3608 3609 $result = [$extDir, $extensions]; 3610 $this->extensions[$php] = $result; 3611 $this->extMisses++; 3612 3613 return $result; 3614 } 3615} 3616 3617class RuntestsValgrind 3618{ 3619 protected string $header; 3620 protected bool $version_3_8_0; 3621 protected string $tool; 3622 3623 public function getHeader(): string 3624 { 3625 return $this->header; 3626 } 3627 3628 public function __construct(array $environment, string $tool = 'memcheck') 3629 { 3630 $this->tool = $tool; 3631 $header = system_with_timeout("valgrind --tool={$this->tool} --version", $environment); 3632 if (!$header) { 3633 error("Valgrind returned no version info for {$this->tool}, cannot proceed.\n". 3634 "Please check if Valgrind is installed and the tool is named correctly."); 3635 } 3636 $count = 0; 3637 $version = preg_replace("/valgrind-(\d+)\.(\d+)\.(\d+)([.\w_-]+)?(\s+)/", '$1.$2.$3', $header, 1, $count); 3638 if ($count != 1) { 3639 error("Valgrind returned invalid version info (\"{$header}\") for {$this->tool}, cannot proceed."); 3640 } 3641 $this->header = sprintf("%s (%s)", trim($header), $this->tool); 3642 $this->version_3_8_0 = version_compare($version, '3.8.0', '>='); 3643 } 3644 3645 public function wrapCommand(string $cmd, string $memcheck_filename, bool $check_all): string 3646 { 3647 $vcmd = "valgrind -q --tool={$this->tool} --trace-children=yes"; 3648 if ($check_all) { 3649 $vcmd .= ' --smc-check=all'; 3650 } 3651 3652 /* --vex-iropt-register-updates=allregs-at-mem-access is necessary for phpdbg watchpoint tests */ 3653 if ($this->version_3_8_0) { 3654 return "$vcmd --vex-iropt-register-updates=allregs-at-mem-access --log-file=$memcheck_filename $cmd"; 3655 } 3656 return "$vcmd --vex-iropt-precise-memory-exns=yes --log-file=$memcheck_filename $cmd"; 3657 } 3658} 3659 3660class TestFile 3661{ 3662 private string $fileName; 3663 3664 private array $sections = ['TEST' => '']; 3665 3666 private const ALLOWED_SECTIONS = [ 3667 'EXPECT', 'EXPECTF', 'EXPECTREGEX', 'EXPECTREGEX_EXTERNAL', 'EXPECT_EXTERNAL', 'EXPECTF_EXTERNAL', 'EXPECTHEADERS', 3668 'POST', 'POST_RAW', 'GZIP_POST', 'DEFLATE_POST', 'PUT', 'GET', 'COOKIE', 'ARGS', 3669 'FILE', 'FILEEOF', 'FILE_EXTERNAL', 'REDIRECTTEST', 3670 'CAPTURE_STDIO', 'STDIN', 'CGI', 'PHPDBG', 3671 'INI', 'ENV', 'EXTENSIONS', 3672 'SKIPIF', 'XFAIL', 'XLEAK', 'CLEAN', 3673 'CREDITS', 'DESCRIPTION', 'CONFLICTS', 'WHITESPACE_SENSITIVE', 3674 'FLAKY', 3675 ]; 3676 3677 /** 3678 * @throws BorkageException 3679 */ 3680 public function __construct(string $fileName, bool $inRedirect) 3681 { 3682 $this->fileName = $fileName; 3683 3684 $this->readFile(); 3685 $this->validateAndProcess($inRedirect); 3686 } 3687 3688 public function hasSection(string $name): bool 3689 { 3690 return isset($this->sections[$name]); 3691 } 3692 3693 public function hasAnySections(string ...$names): bool 3694 { 3695 foreach ($names as $section) { 3696 if (isset($this->sections[$section])) { 3697 return true; 3698 } 3699 } 3700 3701 return false; 3702 } 3703 3704 public function sectionNotEmpty(string $name): bool 3705 { 3706 return !empty($this->sections[$name]); 3707 } 3708 3709 /** 3710 * @throws Exception 3711 */ 3712 public function getSection(string $name): string 3713 { 3714 if (!isset($this->sections[$name])) { 3715 throw new Exception("Section $name not found"); 3716 } 3717 return $this->sections[$name]; 3718 } 3719 3720 public function getName(): string 3721 { 3722 return trim($this->getSection('TEST')); 3723 } 3724 3725 public function isCGI(): bool 3726 { 3727 return $this->hasSection('CGI') 3728 || $this->sectionNotEmpty('GET') 3729 || $this->sectionNotEmpty('POST') 3730 || $this->sectionNotEmpty('GZIP_POST') 3731 || $this->sectionNotEmpty('DEFLATE_POST') 3732 || $this->sectionNotEmpty('POST_RAW') 3733 || $this->sectionNotEmpty('PUT') 3734 || $this->sectionNotEmpty('COOKIE') 3735 || $this->sectionNotEmpty('EXPECTHEADERS'); 3736 } 3737 3738 /** 3739 * TODO Refactor to make it not needed 3740 */ 3741 public function setSection(string $name, string $value): void 3742 { 3743 $this->sections[$name] = $value; 3744 } 3745 3746 /** 3747 * Load the sections of the test file 3748 * @throws BorkageException 3749 */ 3750 private function readFile(): void 3751 { 3752 $fp = fopen($this->fileName, "rb") or error("Cannot open test file: {$this->fileName}"); 3753 3754 if (!feof($fp)) { 3755 $line = fgets($fp); 3756 3757 if ($line === false) { 3758 throw new BorkageException("cannot read test"); 3759 } 3760 } else { 3761 throw new BorkageException("empty test [{$this->fileName}]"); 3762 } 3763 if (strncmp('--TEST--', $line, 8)) { 3764 throw new BorkageException("tests must start with --TEST-- [{$this->fileName}]"); 3765 } 3766 3767 $section = 'TEST'; 3768 $secfile = false; 3769 $secdone = false; 3770 3771 while (!feof($fp)) { 3772 $line = fgets($fp); 3773 3774 if ($line === false) { 3775 break; 3776 } 3777 3778 // Match the beginning of a section. 3779 if (preg_match('/^--([_A-Z]+)--/', $line, $r)) { 3780 $section = $r[1]; 3781 3782 if (isset($this->sections[$section]) && $this->sections[$section]) { 3783 throw new BorkageException("duplicated $section section"); 3784 } 3785 3786 // check for unknown sections 3787 if (!in_array($section, self::ALLOWED_SECTIONS)) { 3788 throw new BorkageException('Unknown section "' . $section . '"'); 3789 } 3790 3791 $this->sections[$section] = ''; 3792 $secfile = $section == 'FILE' || $section == 'FILEEOF' || $section == 'FILE_EXTERNAL'; 3793 $secdone = false; 3794 continue; 3795 } 3796 3797 // Add to the section text. 3798 if (!$secdone) { 3799 $this->sections[$section] .= $line; 3800 } 3801 3802 // End of actual test? 3803 if ($secfile && preg_match('/^===DONE===\s*$/', $line)) { 3804 $secdone = true; 3805 } 3806 } 3807 3808 fclose($fp); 3809 } 3810 3811 /** 3812 * @throws BorkageException 3813 */ 3814 private function validateAndProcess(bool $inRedirect): void 3815 { 3816 // the redirect section allows a set of tests to be reused outside of 3817 // a given test dir 3818 if ($this->hasSection('REDIRECTTEST')) { 3819 if ($inRedirect) { 3820 throw new BorkageException("Can't redirect a test from within a redirected test"); 3821 } 3822 return; 3823 } 3824 if (!$this->hasSection('PHPDBG') && $this->hasSection('FILE') + $this->hasSection('FILEEOF') + $this->hasSection('FILE_EXTERNAL') != 1) { 3825 throw new BorkageException("missing section --FILE--"); 3826 } 3827 3828 if ($this->hasSection('FILEEOF')) { 3829 $this->sections['FILE'] = preg_replace("/[\r\n]+$/", '', $this->sections['FILEEOF']); 3830 unset($this->sections['FILEEOF']); 3831 } 3832 3833 foreach (['FILE', 'EXPECT', 'EXPECTF', 'EXPECTREGEX'] as $prefix) { 3834 // For grepping: FILE_EXTERNAL, EXPECT_EXTERNAL, EXPECTF_EXTERNAL, EXPECTREGEX_EXTERNAL 3835 $key = $prefix . '_EXTERNAL'; 3836 3837 if ($this->hasSection($key)) { 3838 // don't allow tests to retrieve files from anywhere but this subdirectory 3839 $dir = dirname($this->fileName); 3840 $fileName = $dir . '/' . trim(str_replace('..', '', $this->getSection($key))); 3841 3842 if (file_exists($fileName)) { 3843 $this->sections[$prefix] = file_get_contents($fileName); 3844 } else { 3845 throw new BorkageException("could not load --" . $key . "-- " . $dir . '/' . trim($fileName)); 3846 } 3847 } 3848 } 3849 3850 if (($this->hasSection('EXPECT') + $this->hasSection('EXPECTF') + $this->hasSection('EXPECTREGEX')) != 1) { 3851 throw new BorkageException("missing section --EXPECT--, --EXPECTF-- or --EXPECTREGEX--"); 3852 } 3853 3854 if ($this->hasSection('PHPDBG') && !$this->hasSection('STDIN')) { 3855 $this->sections['STDIN'] = $this->sections['PHPDBG'] . "\n"; 3856 } 3857 } 3858} 3859 3860function init_output_buffers(): void 3861{ 3862 // Delete as much output buffers as possible. 3863 while (@ob_end_clean()) { 3864 } 3865 3866 if (ob_get_level()) { 3867 echo "Not all buffers were deleted.\n"; 3868 } 3869} 3870 3871function check_proc_open_function_exists(): void 3872{ 3873 if (!function_exists('proc_open')) { 3874 echo <<<NO_PROC_OPEN_ERROR 3875 3876+-----------------------------------------------------------+ 3877| ! ERROR ! | 3878| The test-suite requires that proc_open() is available. | 3879| Please check if you disabled it in php.ini. | 3880+-----------------------------------------------------------+ 3881 3882NO_PROC_OPEN_ERROR; 3883 exit(1); 3884 } 3885} 3886 3887function bless_failed_tests(array $failedTests): void 3888{ 3889 if (empty($failedTests)) { 3890 return; 3891 } 3892 $args = [ 3893 PHP_BINARY, 3894 __DIR__ . '/scripts/dev/bless_tests.php', 3895 ]; 3896 foreach ($failedTests as $test) { 3897 $args[] = $test['name']; 3898 } 3899 proc_open($args, [], $pipes); 3900} 3901 3902/* 3903 * BSD 3-Clause License 3904 * 3905 * Copyright (c) 2002-2023, Sebastian Bergmann 3906 * All rights reserved. 3907 * 3908 * This file is part of sebastian/diff. 3909 * https://github.com/sebastianbergmann/diff 3910 */ 3911 3912final class Differ 3913{ 3914 public const OLD = 0; 3915 public const ADDED = 1; 3916 public const REMOVED = 2; 3917 private DiffOutputBuilder $outputBuilder; 3918 private $isEqual; 3919 3920 public function __construct(callable $isEqual) 3921 { 3922 $this->outputBuilder = new DiffOutputBuilder; 3923 $this->isEqual = $isEqual; 3924 } 3925 3926 public function diff(array $from, array $to): string 3927 { 3928 $diff = $this->diffToArray($from, $to); 3929 3930 return $this->outputBuilder->getDiff($diff); 3931 } 3932 3933 public function diffToArray(array $from, array $to): array 3934 { 3935 $fromLine = 1; 3936 $toLine = 1; 3937 3938 [$from, $to, $start, $end] = $this->getArrayDiffParted($from, $to); 3939 3940 $common = $this->calculateCommonSubsequence(array_values($from), array_values($to)); 3941 $diff = []; 3942 3943 foreach ($start as $token) { 3944 $diff[] = [$token, self::OLD]; 3945 $fromLine++; 3946 $toLine++; 3947 } 3948 3949 reset($from); 3950 reset($to); 3951 3952 foreach ($common as $token) { 3953 while (!empty($from) && !($this->isEqual)(reset($from), $token)) { 3954 $diff[] = [array_shift($from), self::REMOVED, $fromLine++]; 3955 } 3956 3957 while (!empty($to) && !($this->isEqual)($token, reset($to))) { 3958 $diff[] = [array_shift($to), self::ADDED, $toLine++]; 3959 } 3960 3961 $diff[] = [$token, self::OLD]; 3962 $fromLine++; 3963 $toLine++; 3964 3965 array_shift($from); 3966 array_shift($to); 3967 } 3968 3969 while (($token = array_shift($from)) !== null) { 3970 $diff[] = [$token, self::REMOVED, $fromLine++]; 3971 } 3972 3973 while (($token = array_shift($to)) !== null) { 3974 $diff[] = [$token, self::ADDED, $toLine++]; 3975 } 3976 3977 foreach ($end as $token) { 3978 $diff[] = [$token, self::OLD]; 3979 } 3980 3981 return $diff; 3982 } 3983 3984 private function getArrayDiffParted(array &$from, array &$to): array 3985 { 3986 $start = []; 3987 $end = []; 3988 3989 reset($to); 3990 3991 foreach ($from as $k => $v) { 3992 $toK = key($to); 3993 3994 if (($this->isEqual)($toK, $k) && ($this->isEqual)($v, $to[$k])) { 3995 $start[$k] = $v; 3996 3997 unset($from[$k], $to[$k]); 3998 } else { 3999 break; 4000 } 4001 } 4002 4003 end($from); 4004 end($to); 4005 4006 do { 4007 $fromK = key($from); 4008 $toK = key($to); 4009 4010 if (null === $fromK || null === $toK || !($this->isEqual)(current($from), current($to))) { 4011 break; 4012 } 4013 4014 prev($from); 4015 prev($to); 4016 4017 $end = [$fromK => $from[$fromK]] + $end; 4018 unset($from[$fromK], $to[$toK]); 4019 } while (true); 4020 4021 return [$from, $to, $start, $end]; 4022 } 4023 4024 public function calculateCommonSubsequence(array $from, array $to): array 4025 { 4026 $cFrom = count($from); 4027 $cTo = count($to); 4028 4029 if ($cFrom === 0) { 4030 return []; 4031 } 4032 4033 if ($cFrom === 1) { 4034 foreach ($to as $toV) { 4035 if (($this->isEqual)($from[0], $toV)) { 4036 return [$toV]; 4037 } 4038 } 4039 4040 return []; 4041 } 4042 4043 $i = (int) ($cFrom / 2); 4044 $fromStart = array_slice($from, 0, $i); 4045 $fromEnd = array_slice($from, $i); 4046 $llB = $this->commonSubsequenceLength($fromStart, $to); 4047 $llE = $this->commonSubsequenceLength(array_reverse($fromEnd), array_reverse($to)); 4048 $jMax = 0; 4049 $max = 0; 4050 4051 for ($j = 0; $j <= $cTo; $j++) { 4052 $m = $llB[$j] + $llE[$cTo - $j]; 4053 4054 if ($m >= $max) { 4055 $max = $m; 4056 $jMax = $j; 4057 } 4058 } 4059 4060 $toStart = array_slice($to, 0, $jMax); 4061 $toEnd = array_slice($to, $jMax); 4062 4063 return array_merge( 4064 $this->calculateCommonSubsequence($fromStart, $toStart), 4065 $this->calculateCommonSubsequence($fromEnd, $toEnd) 4066 ); 4067 } 4068 4069 private function commonSubsequenceLength(array $from, array $to): array 4070 { 4071 $current = array_fill(0, count($to) + 1, 0); 4072 $cFrom = count($from); 4073 $cTo = count($to); 4074 4075 for ($i = 0; $i < $cFrom; $i++) { 4076 $prev = $current; 4077 4078 for ($j = 0; $j < $cTo; $j++) { 4079 if (($this->isEqual)($from[$i], $to[$j])) { 4080 $current[$j + 1] = $prev[$j] + 1; 4081 } else { 4082 $current[$j + 1] = max($current[$j], $prev[$j + 1]); 4083 } 4084 } 4085 } 4086 4087 return $current; 4088 } 4089} 4090 4091class DiffOutputBuilder 4092{ 4093 public function getDiff(array $diffs): string 4094 { 4095 global $context_line_count; 4096 $i = 0; 4097 $number_len = max(3, strlen((string)count($diffs))); 4098 $line_number_spec = '%0' . $number_len . 'd'; 4099 $buffer = fopen('php://memory', 'r+b'); 4100 while ($i < count($diffs)) { 4101 // Find next difference 4102 $next = $i; 4103 while ($next < count($diffs)) { 4104 if ($diffs[$next][1] !== Differ::OLD) { 4105 break; 4106 } 4107 $next++; 4108 } 4109 // Found no more differentiating rows, we're done 4110 if ($next === count($diffs)) { 4111 if (($i - 1) < count($diffs)) { 4112 fwrite($buffer, "--\n"); 4113 } 4114 break; 4115 } 4116 // Print separator if necessary 4117 if ($i < ($next - $context_line_count)) { 4118 fwrite($buffer, "--\n"); 4119 $i = $next - $context_line_count; 4120 } 4121 // Print leading context 4122 while ($i < $next) { 4123 fwrite($buffer, str_repeat(' ', $number_len + 2)); 4124 fwrite($buffer, $diffs[$i][0]); 4125 fwrite($buffer, "\n"); 4126 $i++; 4127 } 4128 // Print differences 4129 while ($i < count($diffs) && $diffs[$i][1] !== Differ::OLD) { 4130 fwrite($buffer, sprintf($line_number_spec, $diffs[$i][2])); 4131 switch ($diffs[$i][1]) { 4132 case Differ::ADDED: 4133 fwrite($buffer, '+ '); 4134 break; 4135 case Differ::REMOVED: 4136 fwrite($buffer, '- '); 4137 break; 4138 } 4139 fwrite($buffer, $diffs[$i][0]); 4140 fwrite($buffer, "\n"); 4141 $i++; 4142 } 4143 // Print trailing context 4144 $afterContext = min($i + $context_line_count, count($diffs)); 4145 while ($i < $afterContext && $diffs[$i][1] === Differ::OLD) { 4146 fwrite($buffer, str_repeat(' ', $number_len + 2)); 4147 fwrite($buffer, $diffs[$i][0]); 4148 fwrite($buffer, "\n"); 4149 $i++; 4150 } 4151 } 4152 4153 $diff = stream_get_contents($buffer, -1, 0); 4154 fclose($buffer); 4155 4156 return $diff; 4157 } 4158} 4159 4160main(); 4161