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