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