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