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