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