xref: /PHP-8.1/run-tests.php (revision 23126374)
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: 39621a39aa6663a90ce59fe1b47be01fdf7c23fd $ */
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: 39621a39aa6663a90ce59fe1b47be01fdf7c23fd $' . "\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 ($output !== '') {
2244            show_result("BORK", $output, $tested_file, 'reason: invalid output from SKIPIF', $temp_filenames);
2245            $PHP_FAILED_TESTS['BORKED'][] = [
2246                'name' => $file,
2247                'test_name' => '',
2248                'output' => '',
2249                'diff' => '',
2250                'info' => "$output [$file]",
2251            ];
2252
2253            $junit->markTestAs('BORK', $shortname, $tested, null, $output);
2254            return 'BORKED';
2255        }
2256    }
2257
2258    if (!extension_loaded("zlib") && $test->hasAnySections("GZIP_POST", "DEFLATE_POST")) {
2259        $message = "ext/zlib required";
2260        show_result('SKIP', $tested, $tested_file, "reason: $message", $temp_filenames);
2261        $junit->markTestAs('SKIP', $shortname, $tested, null, $message);
2262        return 'SKIPPED';
2263    }
2264
2265    if ($test->hasSection('REDIRECTTEST')) {
2266        $test_files = [];
2267
2268        $IN_REDIRECT = eval($test->getSection('REDIRECTTEST'));
2269        $IN_REDIRECT['via'] = "via [$shortname]\n\t";
2270        $IN_REDIRECT['dir'] = realpath(dirname($file));
2271        $IN_REDIRECT['prefix'] = $tested;
2272        $IN_REDIRECT['EXTENSIONS'] = $extensions;
2273
2274        if (!empty($IN_REDIRECT['TESTS'])) {
2275            if (is_array($org_file)) {
2276                $test_files[] = $org_file[1];
2277            } else {
2278                $GLOBALS['test_files'] = $test_files;
2279                find_files($IN_REDIRECT['TESTS']);
2280
2281                foreach ($GLOBALS['test_files'] as $f) {
2282                    $test_files[] = [$f, $file];
2283                }
2284            }
2285            $test_cnt += count($test_files) - 1;
2286            $test_idx--;
2287
2288            show_redirect_start($IN_REDIRECT['TESTS'], $tested, $tested_file);
2289
2290            // set up environment
2291            $redirenv = array_merge($environment, $IN_REDIRECT['ENV']);
2292            $redirenv['REDIR_TEST_DIR'] = realpath($IN_REDIRECT['TESTS']) . DIRECTORY_SEPARATOR;
2293
2294            usort($test_files, "test_sort");
2295            run_all_tests($test_files, $redirenv, $tested);
2296
2297            show_redirect_ends($IN_REDIRECT['TESTS'], $tested, $tested_file);
2298
2299            // a redirected test never fails
2300            $IN_REDIRECT = false;
2301
2302            $junit->markTestAs('PASS', $shortname, $tested);
2303            return 'REDIR';
2304        } else {
2305            $bork_info = "Redirect info must contain exactly one TEST string to be used as redirect directory.";
2306            show_result("BORK", $bork_info, '', '', $temp_filenames);
2307            $PHP_FAILED_TESTS['BORKED'][] = [
2308                'name' => $file,
2309                'test_name' => '',
2310                'output' => '',
2311                'diff' => '',
2312                'info' => "$bork_info [$file]",
2313            ];
2314        }
2315    }
2316
2317    if (is_array($org_file) || $test->hasSection('REDIRECTTEST')) {
2318        if (is_array($org_file)) {
2319            $file = $org_file[0];
2320        }
2321
2322        $bork_info = "Redirected test did not contain redirection info";
2323        show_result("BORK", $bork_info, '', '', $temp_filenames);
2324        $PHP_FAILED_TESTS['BORKED'][] = [
2325            'name' => $file,
2326            'test_name' => '',
2327            'output' => '',
2328            'diff' => '',
2329            'info' => "$bork_info [$file]",
2330        ];
2331
2332        $junit->markTestAs('BORK', $shortname, $tested, null, $bork_info);
2333
2334        return 'BORKED';
2335    }
2336
2337    // We've satisfied the preconditions - run the test!
2338    if ($test->hasSection('FILE')) {
2339        show_file_block('php', $test->getSection('FILE'), 'TEST');
2340        save_text($test_file, $test->getSection('FILE'), $temp_file);
2341    } else {
2342        $test_file = $temp_file = "";
2343    }
2344
2345    if ($test->hasSection('GET')) {
2346        $query_string = trim($test->getSection('GET'));
2347    } else {
2348        $query_string = '';
2349    }
2350
2351    $env['REDIRECT_STATUS'] = '1';
2352    if (empty($env['QUERY_STRING'])) {
2353        $env['QUERY_STRING'] = $query_string;
2354    }
2355    if (empty($env['PATH_TRANSLATED'])) {
2356        $env['PATH_TRANSLATED'] = $test_file;
2357    }
2358    if (empty($env['SCRIPT_FILENAME'])) {
2359        $env['SCRIPT_FILENAME'] = $test_file;
2360    }
2361
2362    if ($test->hasSection('COOKIE')) {
2363        $env['HTTP_COOKIE'] = trim($test->getSection('COOKIE'));
2364    } else {
2365        $env['HTTP_COOKIE'] = '';
2366    }
2367
2368    $args = $test->hasSection('ARGS') ? ' -- ' . $test->getSection('ARGS') : '';
2369
2370    if ($preload && !empty($test_file)) {
2371        save_text($preload_filename, "<?php opcache_compile_file('$test_file');");
2372        $local_pass_options = $pass_options;
2373        unset($pass_options);
2374        $pass_options = $local_pass_options;
2375        $pass_options .= " -d opcache.preload=" . $preload_filename;
2376    }
2377
2378    if ($test->sectionNotEmpty('POST_RAW')) {
2379        $post = trim($test->getSection('POST_RAW'));
2380        $raw_lines = explode("\n", $post);
2381
2382        $request = '';
2383        $started = false;
2384
2385        foreach ($raw_lines as $line) {
2386            if (empty($env['CONTENT_TYPE']) && preg_match('/^Content-Type:(.*)/i', $line, $res)) {
2387                $env['CONTENT_TYPE'] = trim(str_replace("\r", '', $res[1]));
2388                continue;
2389            }
2390
2391            if ($started) {
2392                $request .= "\n";
2393            }
2394
2395            $started = true;
2396            $request .= $line;
2397        }
2398
2399        $env['CONTENT_LENGTH'] = strlen($request);
2400        $env['REQUEST_METHOD'] = 'POST';
2401
2402        if (empty($request)) {
2403            $junit->markTestAs('BORK', $shortname, $tested, null, 'empty $request');
2404            return 'BORKED';
2405        }
2406
2407        save_text($tmp_post, $request);
2408        $cmd = "$php $pass_options $ini_settings -f \"$test_file\"$cmdRedirect < \"$tmp_post\"";
2409    } elseif ($test->sectionNotEmpty('PUT')) {
2410        $post = trim($test->getSection('PUT'));
2411        $raw_lines = explode("\n", $post);
2412
2413        $request = '';
2414        $started = false;
2415
2416        foreach ($raw_lines as $line) {
2417            if (empty($env['CONTENT_TYPE']) && preg_match('/^Content-Type:(.*)/i', $line, $res)) {
2418                $env['CONTENT_TYPE'] = trim(str_replace("\r", '', $res[1]));
2419                continue;
2420            }
2421
2422            if ($started) {
2423                $request .= "\n";
2424            }
2425
2426            $started = true;
2427            $request .= $line;
2428        }
2429
2430        $env['CONTENT_LENGTH'] = strlen($request);
2431        $env['REQUEST_METHOD'] = 'PUT';
2432
2433        if (empty($request)) {
2434            $junit->markTestAs('BORK', $shortname, $tested, null, 'empty $request');
2435            return 'BORKED';
2436        }
2437
2438        save_text($tmp_post, $request);
2439        $cmd = "$php $pass_options $ini_settings -f \"$test_file\"$cmdRedirect < \"$tmp_post\"";
2440    } elseif ($test->sectionNotEmpty('POST')) {
2441        $post = trim($test->getSection('POST'));
2442        $content_length = strlen($post);
2443        save_text($tmp_post, $post);
2444
2445        $env['REQUEST_METHOD'] = 'POST';
2446        if (empty($env['CONTENT_TYPE'])) {
2447            $env['CONTENT_TYPE'] = 'application/x-www-form-urlencoded';
2448        }
2449
2450        if (empty($env['CONTENT_LENGTH'])) {
2451            $env['CONTENT_LENGTH'] = $content_length;
2452        }
2453
2454        $cmd = "$php $pass_options $ini_settings -f \"$test_file\"$cmdRedirect < \"$tmp_post\"";
2455    } elseif ($test->sectionNotEmpty('GZIP_POST')) {
2456        $post = trim($test->getSection('GZIP_POST'));
2457        $post = gzencode($post, 9, FORCE_GZIP);
2458        $env['HTTP_CONTENT_ENCODING'] = 'gzip';
2459
2460        save_text($tmp_post, $post);
2461        $content_length = strlen($post);
2462
2463        $env['REQUEST_METHOD'] = 'POST';
2464        $env['CONTENT_TYPE'] = 'application/x-www-form-urlencoded';
2465        $env['CONTENT_LENGTH'] = $content_length;
2466
2467        $cmd = "$php $pass_options $ini_settings -f \"$test_file\"$cmdRedirect < \"$tmp_post\"";
2468    } elseif ($test->sectionNotEmpty('DEFLATE_POST')) {
2469        $post = trim($test->getSection('DEFLATE_POST'));
2470        $post = gzcompress($post, 9);
2471        $env['HTTP_CONTENT_ENCODING'] = 'deflate';
2472        save_text($tmp_post, $post);
2473        $content_length = strlen($post);
2474
2475        $env['REQUEST_METHOD'] = 'POST';
2476        $env['CONTENT_TYPE'] = 'application/x-www-form-urlencoded';
2477        $env['CONTENT_LENGTH'] = $content_length;
2478
2479        $cmd = "$php $pass_options $ini_settings -f \"$test_file\"$cmdRedirect < \"$tmp_post\"";
2480    } else {
2481        $env['REQUEST_METHOD'] = 'GET';
2482        $env['CONTENT_TYPE'] = '';
2483        $env['CONTENT_LENGTH'] = '';
2484
2485        $repeat_option = $num_repeats > 1 ? "--repeat $num_repeats" : "";
2486        $cmd = "$php $pass_options $repeat_option $ini_settings -f \"$test_file\" $args$cmdRedirect";
2487    }
2488
2489    $orig_cmd = $cmd;
2490    if ($valgrind) {
2491        $env['USE_ZEND_ALLOC'] = '0';
2492        $env['ZEND_DONT_UNLOAD_MODULES'] = 1;
2493
2494        $cmd = $valgrind->wrapCommand($cmd, $memcheck_filename, strpos($test_file, "pcre") !== false);
2495    }
2496
2497    if ($DETAILED) {
2498        echo "
2499CONTENT_LENGTH  = " . $env['CONTENT_LENGTH'] . "
2500CONTENT_TYPE    = " . $env['CONTENT_TYPE'] . "
2501PATH_TRANSLATED = " . $env['PATH_TRANSLATED'] . "
2502QUERY_STRING    = " . $env['QUERY_STRING'] . "
2503REDIRECT_STATUS = " . $env['REDIRECT_STATUS'] . "
2504REQUEST_METHOD  = " . $env['REQUEST_METHOD'] . "
2505SCRIPT_FILENAME = " . $env['SCRIPT_FILENAME'] . "
2506HTTP_COOKIE     = " . $env['HTTP_COOKIE'] . "
2507COMMAND $cmd
2508";
2509    }
2510
2511    $junit->startTimer($shortname);
2512    $hrtime = hrtime();
2513    $startTime = $hrtime[0] * 1000000000 + $hrtime[1];
2514
2515    $stdin = $test->hasSection('STDIN') ? $test->getSection('STDIN') : null;
2516    $out = system_with_timeout($cmd, $env, $stdin, $captureStdIn, $captureStdOut, $captureStdErr);
2517
2518    $junit->stopTimer($shortname);
2519    $hrtime = hrtime();
2520    $time = $hrtime[0] * 1000000000 + $hrtime[1] - $startTime;
2521    if ($time >= $slow_min_ms * 1000000) {
2522        $PHP_FAILED_TESTS['SLOW'][] = [
2523            'name' => $file,
2524            'test_name' => $tested . " [$tested_file]",
2525            'output' => '',
2526            'diff' => '',
2527            'info' => $time / 1000000000,
2528        ];
2529    }
2530
2531    if ($test->sectionNotEmpty('CLEAN') && (!$no_clean || $cfg['keep']['clean'])) {
2532        show_file_block('clean', $test->getSection('CLEAN'));
2533        save_text($test_clean, trim($test->getSection('CLEAN')), $temp_clean);
2534
2535        if (!$no_clean) {
2536            $extra = !IS_WINDOWS ?
2537                "unset REQUEST_METHOD; unset QUERY_STRING; unset PATH_TRANSLATED; unset SCRIPT_FILENAME; unset REQUEST_METHOD;" : "";
2538            system_with_timeout("$extra $php $pass_options $extra_options -q $orig_ini_settings $no_file_cache \"$test_clean\"", $env);
2539        }
2540
2541        if (!$cfg['keep']['clean']) {
2542            @unlink($test_clean);
2543        }
2544    }
2545
2546    $leaked = false;
2547    $passed = false;
2548
2549    if ($valgrind) { // leak check
2550        $leaked = filesize($memcheck_filename) > 0;
2551
2552        if (!$leaked) {
2553            @unlink($memcheck_filename);
2554        }
2555    }
2556
2557    if ($num_repeats > 1) {
2558        // In repeat mode, retain the output before the first execution,
2559        // and of the last execution. Do this early, because the trimming below
2560        // makes the newline handling complicated.
2561        $separator1 = "Executing for the first time...\n";
2562        $separator1_pos = strpos($out, $separator1);
2563        if ($separator1_pos !== false) {
2564            $separator2 = "Finished execution, repeating...\n";
2565            $separator2_pos = strrpos($out, $separator2);
2566            if ($separator2_pos !== false) {
2567                $out = substr($out, 0, $separator1_pos)
2568                     . substr($out, $separator2_pos + strlen($separator2));
2569            } else {
2570                $out = substr($out, 0, $separator1_pos)
2571                     . substr($out, $separator1_pos + strlen($separator1));
2572            }
2573        }
2574    }
2575
2576    // Does the output match what is expected?
2577    $output = preg_replace("/\r\n/", "\n", trim($out));
2578
2579    /* when using CGI, strip the headers from the output */
2580    $headers = [];
2581
2582    if (!empty($uses_cgi) && preg_match("/^(.*?)\r?\n\r?\n(.*)/s", $out, $match)) {
2583        $output = trim($match[2]);
2584        $rh = preg_split("/[\n\r]+/", $match[1]);
2585
2586        foreach ($rh as $line) {
2587            if (strpos($line, ':') !== false) {
2588                $line = explode(':', $line, 2);
2589                $headers[trim($line[0])] = trim($line[1]);
2590            }
2591        }
2592    }
2593
2594    $failed_headers = false;
2595
2596    if ($test->hasSection('EXPECTHEADERS')) {
2597        $want = [];
2598        $wanted_headers = [];
2599        $lines = preg_split("/[\n\r]+/", $test->getSection('EXPECTHEADERS'));
2600
2601        foreach ($lines as $line) {
2602            if (strpos($line, ':') !== false) {
2603                $line = explode(':', $line, 2);
2604                $want[trim($line[0])] = trim($line[1]);
2605                $wanted_headers[] = trim($line[0]) . ': ' . trim($line[1]);
2606            }
2607        }
2608
2609        $output_headers = [];
2610
2611        foreach ($want as $k => $v) {
2612            if (isset($headers[$k])) {
2613                $output_headers[] = $k . ': ' . $headers[$k];
2614            }
2615
2616            if (!isset($headers[$k]) || $headers[$k] != $v) {
2617                $failed_headers = true;
2618            }
2619        }
2620
2621        ksort($wanted_headers);
2622        $wanted_headers = implode("\n", $wanted_headers);
2623        ksort($output_headers);
2624        $output_headers = implode("\n", $output_headers);
2625    }
2626
2627    show_file_block('out', $output);
2628
2629    if ($preload) {
2630        $output = trim(preg_replace("/\n?Warning: Can't preload [^\n]*\n?/", "", $output));
2631    }
2632
2633    if ($test->hasAnySections('EXPECTF', 'EXPECTREGEX')) {
2634        if ($test->hasSection('EXPECTF')) {
2635            $wanted = trim($test->getSection('EXPECTF'));
2636        } else {
2637            $wanted = trim($test->getSection('EXPECTREGEX'));
2638        }
2639
2640        show_file_block('exp', $wanted);
2641        $wanted_re = preg_replace('/\r\n/', "\n", $wanted);
2642
2643        if ($test->hasSection('EXPECTF')) {
2644            // do preg_quote, but miss out any %r delimited sections
2645            $temp = "";
2646            $r = "%r";
2647            $startOffset = 0;
2648            $length = strlen($wanted_re);
2649            while ($startOffset < $length) {
2650                $start = strpos($wanted_re, $r, $startOffset);
2651                if ($start !== false) {
2652                    // we have found a start tag
2653                    $end = strpos($wanted_re, $r, $start + 2);
2654                    if ($end === false) {
2655                        // unbalanced tag, ignore it.
2656                        $end = $start = $length;
2657                    }
2658                } else {
2659                    // no more %r sections
2660                    $start = $end = $length;
2661                }
2662                // quote a non re portion of the string
2663                $temp .= preg_quote(substr($wanted_re, $startOffset, $start - $startOffset), '/');
2664                // add the re unquoted.
2665                if ($end > $start) {
2666                    $temp .= '(' . substr($wanted_re, $start + 2, $end - $start - 2) . ')';
2667                }
2668                $startOffset = $end + 2;
2669            }
2670            $wanted_re = $temp;
2671
2672            // Stick to basics
2673            $wanted_re = str_replace('%e', '\\' . DIRECTORY_SEPARATOR, $wanted_re);
2674            $wanted_re = str_replace('%s', '[^\r\n]+', $wanted_re);
2675            $wanted_re = str_replace('%S', '[^\r\n]*', $wanted_re);
2676            $wanted_re = str_replace('%a', '.+', $wanted_re);
2677            $wanted_re = str_replace('%A', '.*', $wanted_re);
2678            $wanted_re = str_replace('%w', '\s*', $wanted_re);
2679            $wanted_re = str_replace('%i', '[+-]?\d+', $wanted_re);
2680            $wanted_re = str_replace('%d', '\d+', $wanted_re);
2681            $wanted_re = str_replace('%x', '[0-9a-fA-F]+', $wanted_re);
2682            $wanted_re = str_replace('%f', '[+-]?\.?\d+\.?\d*(?:[Ee][+-]?\d+)?', $wanted_re);
2683            $wanted_re = str_replace('%c', '.', $wanted_re);
2684            $wanted_re = str_replace('%0', '\x00', $wanted_re);
2685            // %f allows two points "-.0.0" but that is the best *simple* expression
2686        }
2687
2688        if (preg_match("/^$wanted_re\$/s", $output)) {
2689            $passed = true;
2690            if (!$cfg['keep']['php'] && !$leaked) {
2691                @unlink($test_file);
2692                @unlink($preload_filename);
2693            }
2694            @unlink($tmp_post);
2695
2696            if (!$leaked && !$failed_headers) {
2697                if ($test->hasSection('XFAIL')) {
2698                    $warn = true;
2699                    $info = " (warn: XFAIL section but test passes)";
2700                } elseif ($test->hasSection('XLEAK')) {
2701                    $warn = true;
2702                    $info = " (warn: XLEAK section but test passes)";
2703                } elseif ($retried) {
2704                    $warn = true;
2705                    $info = " (warn: Test passed on retry attempt)";
2706                } else {
2707                    show_result("PASS", $tested, $tested_file, '', $temp_filenames);
2708                    $junit->markTestAs('PASS', $shortname, $tested);
2709                    return 'PASSED';
2710                }
2711            }
2712        }
2713    } else {
2714        $wanted = trim($test->getSection('EXPECT'));
2715        $wanted = preg_replace('/\r\n/', "\n", $wanted);
2716        show_file_block('exp', $wanted);
2717
2718        // compare and leave on success
2719        if (!strcmp($output, $wanted)) {
2720            $passed = true;
2721
2722            if (!$cfg['keep']['php'] && !$leaked) {
2723                @unlink($test_file);
2724                @unlink($preload_filename);
2725            }
2726            @unlink($tmp_post);
2727
2728            if (!$leaked && !$failed_headers) {
2729                if ($test->hasSection('XFAIL')) {
2730                    $warn = true;
2731                    $info = " (warn: XFAIL section but test passes)";
2732                } elseif ($test->hasSection('XLEAK')) {
2733                    $warn = true;
2734                    $info = " (warn: XLEAK section but test passes)";
2735                } elseif ($retried) {
2736                    $warn = true;
2737                    $info = " (warn: Test passed on retry attempt)";
2738                } else {
2739                    show_result("PASS", $tested, $tested_file, '', $temp_filenames);
2740                    $junit->markTestAs('PASS', $shortname, $tested);
2741                    return 'PASSED';
2742                }
2743            }
2744        }
2745
2746        $wanted_re = null;
2747    }
2748    if (!$passed && !$retried && error_may_be_retried($test, $output)) {
2749        $retried = true;
2750        goto retry;
2751    }
2752
2753    // Test failed so we need to report details.
2754    if ($failed_headers) {
2755        $passed = false;
2756        $wanted = $wanted_headers . "\n--HEADERS--\n" . $wanted;
2757        $output = $output_headers . "\n--HEADERS--\n" . $output;
2758
2759        if (isset($wanted_re)) {
2760            $wanted_re = preg_quote($wanted_headers . "\n--HEADERS--\n", '/') . $wanted_re;
2761        }
2762    }
2763
2764    if ($leaked) {
2765        $restype[] = $test->hasSection('XLEAK') ?
2766                        'XLEAK' : 'LEAK';
2767    }
2768
2769    if ($warn) {
2770        $restype[] = 'WARN';
2771    }
2772
2773    if (!$passed) {
2774        if ($test->hasSection('XFAIL')) {
2775            $restype[] = 'XFAIL';
2776            $info = '  XFAIL REASON: ' . rtrim($test->getSection('XFAIL'));
2777        } elseif ($test->hasSection('XLEAK')) {
2778            $restype[] = 'XLEAK';
2779            $info = '  XLEAK REASON: ' . rtrim($test->getSection('XLEAK'));
2780        } else {
2781            $restype[] = 'FAIL';
2782        }
2783    }
2784
2785    if (!$passed) {
2786        // write .exp
2787        if (strpos($log_format, 'E') !== false && file_put_contents($exp_filename, $wanted) === false) {
2788            error("Cannot create expected test output - $exp_filename");
2789        }
2790
2791        // write .out
2792        if (strpos($log_format, 'O') !== false && file_put_contents($output_filename, $output) === false) {
2793            error("Cannot create test output - $output_filename");
2794        }
2795
2796        // write .diff
2797        $diff = generate_diff($wanted, $wanted_re, $output);
2798        if (is_array($IN_REDIRECT)) {
2799            $orig_shortname = str_replace(TEST_PHP_SRCDIR . '/', '', $file);
2800            $diff = "# original source file: $orig_shortname\n" . $diff;
2801        }
2802        show_file_block('diff', $diff);
2803        if (strpos($log_format, 'D') !== false && file_put_contents($diff_filename, $diff) === false) {
2804            error("Cannot create test diff - $diff_filename");
2805        }
2806
2807        // write .log
2808        if (strpos($log_format, 'L') !== false && file_put_contents($log_filename, "
2809---- EXPECTED OUTPUT
2810$wanted
2811---- ACTUAL OUTPUT
2812$output
2813---- FAILED
2814") === false) {
2815            error("Cannot create test log - $log_filename");
2816            error_report($file, $log_filename, $tested);
2817        }
2818    }
2819
2820    if (!$passed || $leaked) {
2821        // write .sh
2822        if (strpos($log_format, 'S') !== false) {
2823            $env_lines = [];
2824            foreach ($env as $env_var => $env_val) {
2825                $env_lines[] = "export $env_var=" . escapeshellarg($env_val ?? "");
2826            }
2827            $exported_environment = $env_lines ? "\n" . implode("\n", $env_lines) . "\n" : "";
2828            $sh_script = <<<SH
2829#!/bin/sh
2830{$exported_environment}
2831case "$1" in
2832"gdb")
2833    gdb --args {$orig_cmd}
2834    ;;
2835"valgrind")
2836    USE_ZEND_ALLOC=0 valgrind $2 ${orig_cmd}
2837    ;;
2838"rr")
2839    rr record $2 ${orig_cmd}
2840    ;;
2841*)
2842    {$orig_cmd}
2843    ;;
2844esac
2845SH;
2846            if (file_put_contents($sh_filename, $sh_script) === false) {
2847                error("Cannot create test shell script - $sh_filename");
2848            }
2849            chmod($sh_filename, 0755);
2850        }
2851    }
2852
2853    if ($valgrind && $leaked && $cfg["show"]["mem"]) {
2854        show_file_block('mem', file_get_contents($memcheck_filename));
2855    }
2856
2857    show_result(implode('&', $restype), $tested, $tested_file, $info, $temp_filenames);
2858
2859    foreach ($restype as $type) {
2860        $PHP_FAILED_TESTS[$type . 'ED'][] = [
2861            'name' => $file,
2862            'test_name' => (is_array($IN_REDIRECT) ? $IN_REDIRECT['via'] : '') . $tested . " [$tested_file]",
2863            'output' => $output_filename,
2864            'diff' => $diff_filename,
2865            'info' => $info,
2866        ];
2867    }
2868
2869    $diff = empty($diff) ? '' : preg_replace('/\e/', '<esc>', $diff);
2870
2871    $junit->markTestAs($restype, $shortname, $tested, null, $info, $diff);
2872
2873    return $restype[0] . 'ED';
2874}
2875
2876function is_flaky(TestFile $test): bool
2877{
2878    if ($test->hasSection('FLAKY')) {
2879        return true;
2880    }
2881    if (!$test->hasSection('FILE')) {
2882        return false;
2883    }
2884    $file = $test->getSection('FILE');
2885    $flaky_functions = [
2886        'disk_free_space',
2887        'hrtime',
2888        'microtime',
2889        'sleep',
2890        'usleep',
2891    ];
2892    $regex = '(\b(' . implode('|', $flaky_functions) . ')\()i';
2893    return preg_match($regex, $file) === 1;
2894}
2895
2896function is_flaky_output(string $output): bool
2897{
2898    $messages = [
2899        '404: page not found',
2900        'address already in use',
2901        'connection refused',
2902        'deadlock',
2903        'mailbox already exists',
2904        'timed out',
2905    ];
2906    $regex = '(\b(' . implode('|', $messages) . ')\b)i';
2907    return preg_match($regex, $output) === 1;
2908}
2909
2910function error_may_be_retried(TestFile $test, string $output): bool
2911{
2912    return is_flaky_output($output)
2913        || is_flaky($test);
2914}
2915
2916/**
2917 * @return bool|int
2918 */
2919function comp_line(string $l1, string $l2, bool $is_reg)
2920{
2921    if ($is_reg) {
2922        return preg_match('/^' . $l1 . '$/s', $l2);
2923    } else {
2924        return !strcmp($l1, $l2);
2925    }
2926}
2927
2928function count_array_diff(
2929    array $ar1,
2930    array $ar2,
2931    bool $is_reg,
2932    array $w,
2933    int $idx1,
2934    int $idx2,
2935    int $cnt1,
2936    int $cnt2,
2937    int $steps
2938): int {
2939    $equal = 0;
2940
2941    while ($idx1 < $cnt1 && $idx2 < $cnt2 && comp_line($ar1[$idx1], $ar2[$idx2], $is_reg)) {
2942        $idx1++;
2943        $idx2++;
2944        $equal++;
2945        $steps--;
2946    }
2947    if (--$steps > 0) {
2948        $eq1 = 0;
2949        $st = $steps / 2;
2950
2951        for ($ofs1 = $idx1 + 1; $ofs1 < $cnt1 && $st-- > 0; $ofs1++) {
2952            $eq = @count_array_diff($ar1, $ar2, $is_reg, $w, $ofs1, $idx2, $cnt1, $cnt2, $st);
2953
2954            if ($eq > $eq1) {
2955                $eq1 = $eq;
2956            }
2957        }
2958
2959        $eq2 = 0;
2960        $st = $steps;
2961
2962        for ($ofs2 = $idx2 + 1; $ofs2 < $cnt2 && $st-- > 0; $ofs2++) {
2963            $eq = @count_array_diff($ar1, $ar2, $is_reg, $w, $idx1, $ofs2, $cnt1, $cnt2, $st);
2964            if ($eq > $eq2) {
2965                $eq2 = $eq;
2966            }
2967        }
2968
2969        if ($eq1 > $eq2) {
2970            $equal += $eq1;
2971        } elseif ($eq2 > 0) {
2972            $equal += $eq2;
2973        }
2974    }
2975
2976    return $equal;
2977}
2978
2979function generate_array_diff(array $ar1, array $ar2, bool $is_reg, array $w): array
2980{
2981    global $context_line_count;
2982    $idx1 = 0;
2983    $cnt1 = @count($ar1);
2984    $idx2 = 0;
2985    $cnt2 = @count($ar2);
2986    $diff = [];
2987    $old1 = [];
2988    $old2 = [];
2989    $number_len = max(3, strlen((string)max($cnt1 + 1, $cnt2 + 1)));
2990    $line_number_spec = '%0' . $number_len . 'd';
2991
2992    /** Mapping from $idx2 to $idx1, including indexes of idx2 that are identical to idx1 as well as entries that don't have matches */
2993    $mapping = [];
2994
2995    while ($idx1 < $cnt1 && $idx2 < $cnt2) {
2996        $mapping[$idx2] = $idx1;
2997        if (comp_line($ar1[$idx1], $ar2[$idx2], $is_reg)) {
2998            $idx1++;
2999            $idx2++;
3000            continue;
3001        } else {
3002            $c1 = @count_array_diff($ar1, $ar2, $is_reg, $w, $idx1 + 1, $idx2, $cnt1, $cnt2, 10);
3003            $c2 = @count_array_diff($ar1, $ar2, $is_reg, $w, $idx1, $idx2 + 1, $cnt1, $cnt2, 10);
3004
3005            if ($c1 > $c2) {
3006                $old1[$idx1] = sprintf("{$line_number_spec}- ", $idx1 + 1) . $w[$idx1++];
3007            } elseif ($c2 > 0) {
3008                $old2[$idx2] = sprintf("{$line_number_spec}+ ", $idx2 + 1) . $ar2[$idx2++];
3009            } else {
3010                $old1[$idx1] = sprintf("{$line_number_spec}- ", $idx1 + 1) . $w[$idx1++];
3011                $old2[$idx2] = sprintf("{$line_number_spec}+ ", $idx2 + 1) . $ar2[$idx2++];
3012            }
3013            $last_printed_context_line = $idx1;
3014        }
3015    }
3016    $mapping[$idx2] = $idx1;
3017
3018    reset($old1);
3019    $k1 = key($old1);
3020    $l1 = -2;
3021    reset($old2);
3022    $k2 = key($old2);
3023    $l2 = -2;
3024    $old_k1 = -1;
3025    $add_context_lines = function (int $new_k1) use (&$old_k1, &$diff, $w, $context_line_count, $number_len) {
3026        if ($old_k1 >= $new_k1 || !$context_line_count) {
3027            return;
3028        }
3029        $end = $new_k1 - 1;
3030        $range_end = min($end, $old_k1 + $context_line_count);
3031        if ($old_k1 >= 0) {
3032            while ($old_k1 < $range_end) {
3033                $diff[] = str_repeat(' ', $number_len + 2) . $w[$old_k1++];
3034            }
3035        }
3036        if ($end - $context_line_count > $old_k1) {
3037            $old_k1 = $end - $context_line_count;
3038            if ($old_k1 > 0) {
3039                // Add a '--' to mark sections where the common areas were truncated
3040                $diff[] = '--';
3041            }
3042        }
3043        $old_k1 = max($old_k1, 0);
3044        while ($old_k1 < $end) {
3045            $diff[] = str_repeat(' ', $number_len + 2) . $w[$old_k1++];
3046        }
3047        $old_k1 = $new_k1;
3048    };
3049
3050    while ($k1 !== null || $k2 !== null) {
3051        if ($k1 == $l1 + 1 || $k2 === null) {
3052            $add_context_lines($k1);
3053            $l1 = $k1;
3054            $diff[] = current($old1);
3055            $old_k1 = $k1;
3056            $k1 = next($old1) ? key($old1) : null;
3057        } elseif ($k2 == $l2 + 1 || $k1 === null) {
3058            $add_context_lines($mapping[$k2]);
3059            $l2 = $k2;
3060            $diff[] = current($old2);
3061            $k2 = next($old2) ? key($old2) : null;
3062        } elseif ($k1 < $mapping[$k2]) {
3063            $add_context_lines($k1);
3064            $l1 = $k1;
3065            $diff[] = current($old1);
3066            $k1 = next($old1) ? key($old1) : null;
3067        } else {
3068            $add_context_lines($mapping[$k2]);
3069            $l2 = $k2;
3070            $diff[] = current($old2);
3071            $k2 = next($old2) ? key($old2) : null;
3072        }
3073    }
3074
3075    while ($idx1 < $cnt1) {
3076        $add_context_lines($idx1 + 1);
3077        $diff[] = sprintf("{$line_number_spec}- ", $idx1 + 1) . $w[$idx1++];
3078    }
3079
3080    while ($idx2 < $cnt2) {
3081        if (isset($mapping[$idx2])) {
3082            $add_context_lines($mapping[$idx2] + 1);
3083        }
3084        $diff[] = sprintf("{$line_number_spec}+ ", $idx2 + 1) . $ar2[$idx2++];
3085    }
3086    $add_context_lines(min($old_k1 + $context_line_count + 1, $cnt1 + 1));
3087    if ($context_line_count && $old_k1 < $cnt1 + 1) {
3088        // Add a '--' to mark sections where the common areas were truncated
3089        $diff[] = '--';
3090    }
3091
3092    return $diff;
3093}
3094
3095function generate_diff(string $wanted, ?string $wanted_re, string $output): string
3096{
3097    $w = explode("\n", $wanted);
3098    $o = explode("\n", $output);
3099    $r = is_null($wanted_re) ? $w : explode("\n", $wanted_re);
3100    $diff = generate_array_diff($r, $o, !is_null($wanted_re), $w);
3101
3102    return implode(PHP_EOL, $diff);
3103}
3104
3105function error(string $message): void
3106{
3107    echo "ERROR: {$message}\n";
3108    exit(1);
3109}
3110
3111function settings2array(array $settings, &$ini_settings): void
3112{
3113    foreach ($settings as $setting) {
3114        if (strpos($setting, '=') !== false) {
3115            $setting = explode("=", $setting, 2);
3116            $name = trim($setting[0]);
3117            $value = trim($setting[1]);
3118
3119            if ($name == 'extension' || $name == 'zend_extension') {
3120                if (!isset($ini_settings[$name])) {
3121                    $ini_settings[$name] = [];
3122                }
3123
3124                $ini_settings[$name][] = $value;
3125            } else {
3126                $ini_settings[$name] = $value;
3127            }
3128        }
3129    }
3130}
3131
3132function settings2params(array $ini_settings): string
3133{
3134    $settings = '';
3135
3136    foreach ($ini_settings as $name => $value) {
3137        if (is_array($value)) {
3138            foreach ($value as $val) {
3139                $val = addslashes($val);
3140                $settings .= " -d \"$name=$val\"";
3141            }
3142        } else {
3143            if (IS_WINDOWS && !empty($value) && $value[0] == '"') {
3144                $len = strlen($value);
3145
3146                if ($value[$len - 1] == '"') {
3147                    $value[0] = "'";
3148                    $value[$len - 1] = "'";
3149                }
3150            } else {
3151                $value = addslashes($value);
3152            }
3153
3154            $settings .= " -d \"$name=$value\"";
3155        }
3156    }
3157
3158    return $settings;
3159}
3160
3161function compute_summary(): void
3162{
3163    global $n_total, $test_results, $ignored_by_ext, $sum_results, $percent_results;
3164
3165    $n_total = count($test_results);
3166    $n_total += $ignored_by_ext;
3167    $sum_results = [
3168        'PASSED' => 0,
3169        'WARNED' => 0,
3170        'SKIPPED' => 0,
3171        'FAILED' => 0,
3172        'BORKED' => 0,
3173        'LEAKED' => 0,
3174        'XFAILED' => 0,
3175        'XLEAKED' => 0
3176    ];
3177
3178    foreach ($test_results as $v) {
3179        $sum_results[$v]++;
3180    }
3181
3182    $sum_results['SKIPPED'] += $ignored_by_ext;
3183    $percent_results = [];
3184
3185    foreach ($sum_results as $v => $n) {
3186        $percent_results[$v] = (100.0 * $n) / $n_total;
3187    }
3188}
3189
3190function get_summary(bool $show_ext_summary): string
3191{
3192    global $exts_skipped, $exts_tested, $n_total, $sum_results, $percent_results, $end_time, $start_time, $failed_test_summary, $PHP_FAILED_TESTS, $valgrind;
3193
3194    $x_total = $n_total - $sum_results['SKIPPED'] - $sum_results['BORKED'];
3195
3196    if ($x_total) {
3197        $x_warned = (100.0 * $sum_results['WARNED']) / $x_total;
3198        $x_failed = (100.0 * $sum_results['FAILED']) / $x_total;
3199        $x_xfailed = (100.0 * $sum_results['XFAILED']) / $x_total;
3200        $x_xleaked = (100.0 * $sum_results['XLEAKED']) / $x_total;
3201        $x_leaked = (100.0 * $sum_results['LEAKED']) / $x_total;
3202        $x_passed = (100.0 * $sum_results['PASSED']) / $x_total;
3203    } else {
3204        $x_warned = $x_failed = $x_passed = $x_leaked = $x_xfailed = $x_xleaked = 0;
3205    }
3206
3207    $summary = '';
3208
3209    if ($show_ext_summary) {
3210        $summary .= '
3211=====================================================================
3212TEST RESULT SUMMARY
3213---------------------------------------------------------------------
3214Exts skipped    : ' . sprintf('%4d', $exts_skipped) . '
3215Exts tested     : ' . sprintf('%4d', $exts_tested) . '
3216---------------------------------------------------------------------
3217';
3218    }
3219
3220    $summary .= '
3221Number of tests : ' . sprintf('%4d', $n_total) . '          ' . sprintf('%8d', $x_total);
3222
3223    if ($sum_results['BORKED']) {
3224        $summary .= '
3225Tests borked    : ' . sprintf('%4d (%5.1f%%)', $sum_results['BORKED'], $percent_results['BORKED']) . ' --------';
3226    }
3227
3228    $summary .= '
3229Tests skipped   : ' . sprintf('%4d (%5.1f%%)', $sum_results['SKIPPED'], $percent_results['SKIPPED']) . ' --------
3230Tests warned    : ' . sprintf('%4d (%5.1f%%)', $sum_results['WARNED'], $percent_results['WARNED']) . ' ' . sprintf('(%5.1f%%)', $x_warned) . '
3231Tests failed    : ' . sprintf('%4d (%5.1f%%)', $sum_results['FAILED'], $percent_results['FAILED']) . ' ' . sprintf('(%5.1f%%)', $x_failed);
3232
3233    if ($sum_results['XFAILED']) {
3234        $summary .= '
3235Expected fail   : ' . sprintf('%4d (%5.1f%%)', $sum_results['XFAILED'], $percent_results['XFAILED']) . ' ' . sprintf('(%5.1f%%)', $x_xfailed);
3236    }
3237
3238    if ($valgrind) {
3239        $summary .= '
3240Tests leaked    : ' . sprintf('%4d (%5.1f%%)', $sum_results['LEAKED'], $percent_results['LEAKED']) . ' ' . sprintf('(%5.1f%%)', $x_leaked);
3241        if ($sum_results['XLEAKED']) {
3242            $summary .= '
3243Expected leak   : ' . sprintf('%4d (%5.1f%%)', $sum_results['XLEAKED'], $percent_results['XLEAKED']) . ' ' . sprintf('(%5.1f%%)', $x_xleaked);
3244        }
3245    }
3246
3247    $summary .= '
3248Tests passed    : ' . sprintf('%4d (%5.1f%%)', $sum_results['PASSED'], $percent_results['PASSED']) . ' ' . sprintf('(%5.1f%%)', $x_passed) . '
3249---------------------------------------------------------------------
3250Time taken      : ' . sprintf('%4d seconds', $end_time - $start_time) . '
3251=====================================================================
3252';
3253    $failed_test_summary = '';
3254
3255    if (count($PHP_FAILED_TESTS['SLOW'])) {
3256        usort($PHP_FAILED_TESTS['SLOW'], function (array $a, array $b): int {
3257            return $a['info'] < $b['info'] ? 1 : -1;
3258        });
3259
3260        $failed_test_summary .= '
3261=====================================================================
3262SLOW TEST SUMMARY
3263---------------------------------------------------------------------
3264';
3265        foreach ($PHP_FAILED_TESTS['SLOW'] as $failed_test_data) {
3266            $failed_test_summary .= sprintf('(%.3f s) ', $failed_test_data['info']) . $failed_test_data['test_name'] . "\n";
3267        }
3268        $failed_test_summary .= "=====================================================================\n";
3269    }
3270
3271    if (count($PHP_FAILED_TESTS['XFAILED'])) {
3272        $failed_test_summary .= '
3273=====================================================================
3274EXPECTED FAILED TEST SUMMARY
3275---------------------------------------------------------------------
3276';
3277        foreach ($PHP_FAILED_TESTS['XFAILED'] as $failed_test_data) {
3278            $failed_test_summary .= $failed_test_data['test_name'] . $failed_test_data['info'] . "\n";
3279        }
3280        $failed_test_summary .= "=====================================================================\n";
3281    }
3282
3283    if (count($PHP_FAILED_TESTS['BORKED'])) {
3284        $failed_test_summary .= '
3285=====================================================================
3286BORKED TEST SUMMARY
3287---------------------------------------------------------------------
3288';
3289        foreach ($PHP_FAILED_TESTS['BORKED'] as $failed_test_data) {
3290            $failed_test_summary .= $failed_test_data['info'] . "\n";
3291        }
3292
3293        $failed_test_summary .= "=====================================================================\n";
3294    }
3295
3296    if (count($PHP_FAILED_TESTS['FAILED'])) {
3297        $failed_test_summary .= '
3298=====================================================================
3299FAILED TEST SUMMARY
3300---------------------------------------------------------------------
3301';
3302        foreach ($PHP_FAILED_TESTS['FAILED'] as $failed_test_data) {
3303            $failed_test_summary .= $failed_test_data['test_name'] . $failed_test_data['info'] . "\n";
3304        }
3305        $failed_test_summary .= "=====================================================================\n";
3306    }
3307    if (count($PHP_FAILED_TESTS['WARNED'])) {
3308        $failed_test_summary .= '
3309=====================================================================
3310WARNED TEST SUMMARY
3311---------------------------------------------------------------------
3312';
3313        foreach ($PHP_FAILED_TESTS['WARNED'] as $failed_test_data) {
3314            $failed_test_summary .= $failed_test_data['test_name'] . $failed_test_data['info'] . "\n";
3315        }
3316
3317        $failed_test_summary .= "=====================================================================\n";
3318    }
3319
3320    if (count($PHP_FAILED_TESTS['LEAKED'])) {
3321        $failed_test_summary .= '
3322=====================================================================
3323LEAKED TEST SUMMARY
3324---------------------------------------------------------------------
3325';
3326        foreach ($PHP_FAILED_TESTS['LEAKED'] as $failed_test_data) {
3327            $failed_test_summary .= $failed_test_data['test_name'] . $failed_test_data['info'] . "\n";
3328        }
3329
3330        $failed_test_summary .= "=====================================================================\n";
3331    }
3332
3333    if (count($PHP_FAILED_TESTS['XLEAKED'])) {
3334        $failed_test_summary .= '
3335=====================================================================
3336EXPECTED LEAK TEST SUMMARY
3337---------------------------------------------------------------------
3338';
3339        foreach ($PHP_FAILED_TESTS['XLEAKED'] as $failed_test_data) {
3340            $failed_test_summary .= $failed_test_data['test_name'] . $failed_test_data['info'] . "\n";
3341        }
3342
3343        $failed_test_summary .= "=====================================================================\n";
3344    }
3345
3346    if ($failed_test_summary && !getenv('NO_PHPTEST_SUMMARY')) {
3347        $summary .= $failed_test_summary;
3348    }
3349
3350    return $summary;
3351}
3352
3353function show_start($start_time): void
3354{
3355    echo "TIME START " . date('Y-m-d H:i:s', $start_time) . "\n=====================================================================\n";
3356}
3357
3358function show_end($end_time): void
3359{
3360    echo "=====================================================================\nTIME END " . date('Y-m-d H:i:s', $end_time) . "\n";
3361}
3362
3363function show_summary(): void
3364{
3365    echo get_summary(true);
3366}
3367
3368function show_redirect_start(string $tests, string $tested, string $tested_file): void
3369{
3370    global $SHOW_ONLY_GROUPS;
3371
3372    if (!$SHOW_ONLY_GROUPS || in_array('REDIRECT', $SHOW_ONLY_GROUPS)) {
3373        echo "REDIRECT $tests ($tested [$tested_file]) begin\n";
3374    } else {
3375        clear_show_test();
3376    }
3377}
3378
3379function show_redirect_ends(string $tests, string $tested, string $tested_file): void
3380{
3381    global $SHOW_ONLY_GROUPS;
3382
3383    if (!$SHOW_ONLY_GROUPS || in_array('REDIRECT', $SHOW_ONLY_GROUPS)) {
3384        echo "REDIRECT $tests ($tested [$tested_file]) done\n";
3385    } else {
3386        clear_show_test();
3387    }
3388}
3389
3390function show_test(int $test_idx, string $shortname): void
3391{
3392    global $test_cnt;
3393    global $line_length;
3394
3395    $str = "TEST $test_idx/$test_cnt [$shortname]\r";
3396    $line_length = strlen($str);
3397    echo $str;
3398    flush();
3399}
3400
3401function clear_show_test(): void
3402{
3403    global $line_length;
3404    // Parallel testing
3405    global $workerID;
3406
3407    if (!$workerID && isset($line_length)) {
3408        // Write over the last line to avoid random trailing chars on next echo
3409        echo str_repeat(" ", $line_length), "\r";
3410    }
3411}
3412
3413function parse_conflicts(string $text): array
3414{
3415    // Strip comments
3416    $text = preg_replace('/#.*/', '', $text);
3417    return array_map('trim', explode("\n", trim($text)));
3418}
3419
3420function show_result(
3421    string $result,
3422    string $tested,
3423    string $tested_file,
3424    string $extra = '',
3425    ?array $temp_filenames = null
3426): void {
3427    global $SHOW_ONLY_GROUPS, $colorize;
3428
3429    if (!$SHOW_ONLY_GROUPS || in_array($result, $SHOW_ONLY_GROUPS)) {
3430        if ($colorize) {
3431            /* Use ANSI escape codes for coloring test result */
3432            switch ( $result ) {
3433                case 'PASS': // Light Green
3434                    $color = "\e[1;32m{$result}\e[0m"; break;
3435                case 'FAIL':
3436                case 'BORK':
3437                case 'LEAK':
3438                case 'LEAK&FAIL':
3439                    // Light Red
3440                    $color = "\e[1;31m{$result}\e[0m"; break;
3441                default: // Yellow
3442                    $color = "\e[1;33m{$result}\e[0m"; break;
3443            }
3444
3445            echo "$color $tested [$tested_file] $extra\n";
3446        } else {
3447            echo "$result $tested [$tested_file] $extra\n";
3448        }
3449    } elseif (!$SHOW_ONLY_GROUPS) {
3450        clear_show_test();
3451    }
3452
3453}
3454
3455class BorkageException extends Exception
3456{
3457}
3458
3459class JUnit
3460{
3461    private bool $enabled = true;
3462    private $fp = null;
3463    private array $suites = [];
3464    private array $rootSuite = self::EMPTY_SUITE + ['name' => 'php'];
3465
3466    private const EMPTY_SUITE = [
3467        'test_total' => 0,
3468        'test_pass' => 0,
3469        'test_fail' => 0,
3470        'test_error' => 0,
3471        'test_skip' => 0,
3472        'test_warn' => 0,
3473        'files' => [],
3474        'execution_time' => 0,
3475    ];
3476
3477    /**
3478     * @throws Exception
3479     */
3480    public function __construct(array $env, int $workerID)
3481    {
3482        // Check whether a junit log is wanted.
3483        $fileName = $env['TEST_PHP_JUNIT'] ?? null;
3484        if (empty($fileName)) {
3485            $this->enabled = false;
3486            return;
3487        }
3488        if (!$workerID && !$this->fp = fopen($fileName, 'w')) {
3489            throw new Exception("Failed to open $fileName for writing.");
3490        }
3491    }
3492
3493    public function isEnabled(): bool
3494    {
3495        return $this->enabled;
3496    }
3497
3498    public function clear(): void
3499    {
3500        $this->rootSuite = self::EMPTY_SUITE + ['name' => 'php'];
3501        $this->suites = [];
3502    }
3503
3504    public function saveXML(): void
3505    {
3506        if (!$this->enabled) {
3507            return;
3508        }
3509
3510        $xml = '<' . '?' . 'xml version="1.0" encoding="UTF-8"' . '?' . '>' . PHP_EOL;
3511        $xml .= sprintf(
3512            '<testsuites name="%s" tests="%s" failures="%d" errors="%d" skip="%d" time="%s">' . PHP_EOL,
3513            $this->rootSuite['name'],
3514            $this->rootSuite['test_total'],
3515            $this->rootSuite['test_fail'],
3516            $this->rootSuite['test_error'],
3517            $this->rootSuite['test_skip'],
3518            $this->rootSuite['execution_time']
3519        );
3520        $xml .= $this->getSuitesXML();
3521        $xml .= '</testsuites>';
3522        fwrite($this->fp, $xml);
3523    }
3524
3525    private function getSuitesXML(string $suite_name = '')
3526    {
3527        // FIXME: $suite_name gets overwritten
3528        $result = '';
3529
3530        foreach ($this->suites as $suite_name => $suite) {
3531            $result .= sprintf(
3532                '<testsuite name="%s" tests="%s" failures="%d" errors="%d" skip="%d" time="%s">' . PHP_EOL,
3533                $suite['name'],
3534                $suite['test_total'],
3535                $suite['test_fail'],
3536                $suite['test_error'],
3537                $suite['test_skip'],
3538                $suite['execution_time']
3539            );
3540
3541            if (!empty($suite_name)) {
3542                foreach ($suite['files'] as $file) {
3543                    $result .= $this->rootSuite['files'][$file]['xml'];
3544                }
3545            }
3546
3547            $result .= '</testsuite>' . PHP_EOL;
3548        }
3549
3550        return $result;
3551    }
3552
3553    public function markTestAs(
3554        $type,
3555        string $file_name,
3556        string $test_name,
3557        ?int $time = null,
3558        string $message = '',
3559        string $details = ''
3560    ): void {
3561        if (!$this->enabled) {
3562            return;
3563        }
3564
3565        $suite = $this->getSuiteName($file_name);
3566
3567        $this->record($suite, 'test_total');
3568
3569        $time = $time ?? $this->getTimer($file_name);
3570        $this->record($suite, 'execution_time', $time);
3571
3572        $escaped_details = htmlspecialchars($details, ENT_QUOTES, 'UTF-8');
3573        $escaped_details = preg_replace_callback('/[\0-\x08\x0B\x0C\x0E-\x1F]/', function ($c) {
3574            return sprintf('[[0x%02x]]', ord($c[0]));
3575        }, $escaped_details);
3576        $escaped_message = htmlspecialchars($message, ENT_QUOTES, 'UTF-8');
3577
3578        $escaped_test_name = htmlspecialchars($file_name . ' (' . $test_name . ')', ENT_QUOTES);
3579        $this->rootSuite['files'][$file_name]['xml'] = "<testcase name='$escaped_test_name' time='$time'>\n";
3580
3581        if (is_array($type)) {
3582            $output_type = $type[0] . 'ED';
3583            $temp = array_intersect(['XFAIL', 'XLEAK', 'FAIL', 'WARN'], $type);
3584            $type = reset($temp);
3585        } else {
3586            $output_type = $type . 'ED';
3587        }
3588
3589        if ('PASS' == $type || 'XFAIL' == $type || 'XLEAK' == $type) {
3590            $this->record($suite, 'test_pass');
3591        } elseif ('BORK' == $type) {
3592            $this->record($suite, 'test_error');
3593            $this->rootSuite['files'][$file_name]['xml'] .= "<error type='$output_type' message='$escaped_message'/>\n";
3594        } elseif ('SKIP' == $type) {
3595            $this->record($suite, 'test_skip');
3596            $this->rootSuite['files'][$file_name]['xml'] .= "<skipped>$escaped_message</skipped>\n";
3597        } elseif ('WARN' == $type) {
3598            $this->record($suite, 'test_warn');
3599            $this->rootSuite['files'][$file_name]['xml'] .= "<warning>$escaped_message</warning>\n";
3600        } elseif ('FAIL' == $type) {
3601            $this->record($suite, 'test_fail');
3602            $this->rootSuite['files'][$file_name]['xml'] .= "<failure type='$output_type' message='$escaped_message'>$escaped_details</failure>\n";
3603        } else {
3604            $this->record($suite, 'test_error');
3605            $this->rootSuite['files'][$file_name]['xml'] .= "<error type='$output_type' message='$escaped_message'>$escaped_details</error>\n";
3606        }
3607
3608        $this->rootSuite['files'][$file_name]['xml'] .= "</testcase>\n";
3609    }
3610
3611    private function record(string $suite, string $param, $value = 1): void
3612    {
3613        $this->rootSuite[$param] += $value;
3614        $this->suites[$suite][$param] += $value;
3615    }
3616
3617    private function getTimer(string $file_name)
3618    {
3619        if (!$this->enabled) {
3620            return 0;
3621        }
3622
3623        if (isset($this->rootSuite['files'][$file_name]['total'])) {
3624            return number_format($this->rootSuite['files'][$file_name]['total'], 4);
3625        }
3626
3627        return 0;
3628    }
3629
3630    public function startTimer(string $file_name): void
3631    {
3632        if (!$this->enabled) {
3633            return;
3634        }
3635
3636        if (!isset($this->rootSuite['files'][$file_name]['start'])) {
3637            $this->rootSuite['files'][$file_name]['start'] = microtime(true);
3638
3639            $suite = $this->getSuiteName($file_name);
3640            $this->initSuite($suite);
3641            $this->suites[$suite]['files'][$file_name] = $file_name;
3642        }
3643    }
3644
3645    public function getSuiteName(string $file_name): string
3646    {
3647        return $this->pathToClassName(dirname($file_name));
3648    }
3649
3650    private function pathToClassName(string $file_name): string
3651    {
3652        if (!$this->enabled) {
3653            return '';
3654        }
3655
3656        $ret = $this->rootSuite['name'];
3657        $_tmp = [];
3658
3659        // lookup whether we're in the PHP source checkout
3660        $max = 5;
3661        if (is_file($file_name)) {
3662            $dir = dirname(realpath($file_name));
3663        } else {
3664            $dir = realpath($file_name);
3665        }
3666        do {
3667            array_unshift($_tmp, basename($dir));
3668            $chk = $dir . DIRECTORY_SEPARATOR . "main" . DIRECTORY_SEPARATOR . "php_version.h";
3669            $dir = dirname($dir);
3670        } while (!file_exists($chk) && --$max > 0);
3671        if (file_exists($chk)) {
3672            if ($max) {
3673                array_shift($_tmp);
3674            }
3675            foreach ($_tmp as $p) {
3676                $ret .= "." . preg_replace(",[^a-z0-9]+,i", ".", $p);
3677            }
3678            return $ret;
3679        }
3680
3681        return $this->rootSuite['name'] . '.' . str_replace([DIRECTORY_SEPARATOR, '-'], '.', $file_name);
3682    }
3683
3684    public function initSuite(string $suite_name): void
3685    {
3686        if (!$this->enabled) {
3687            return;
3688        }
3689
3690        if (!empty($this->suites[$suite_name])) {
3691            return;
3692        }
3693
3694        $this->suites[$suite_name] = self::EMPTY_SUITE + ['name' => $suite_name];
3695    }
3696
3697    /**
3698     * @throws Exception
3699     */
3700    public function stopTimer(string $file_name): void
3701    {
3702        if (!$this->enabled) {
3703            return;
3704        }
3705
3706        if (!isset($this->rootSuite['files'][$file_name]['start'])) {
3707            throw new Exception("Timer for $file_name was not started!");
3708        }
3709
3710        if (!isset($this->rootSuite['files'][$file_name]['total'])) {
3711            $this->rootSuite['files'][$file_name]['total'] = 0;
3712        }
3713
3714        $start = $this->rootSuite['files'][$file_name]['start'];
3715        $this->rootSuite['files'][$file_name]['total'] += microtime(true) - $start;
3716        unset($this->rootSuite['files'][$file_name]['start']);
3717    }
3718
3719    public function mergeResults(?JUnit $other): void
3720    {
3721        if (!$this->enabled || !$other) {
3722            return;
3723        }
3724
3725        $this->mergeSuites($this->rootSuite, $other->rootSuite);
3726        foreach ($other->suites as $name => $suite) {
3727            if (!isset($this->suites[$name])) {
3728                $this->suites[$name] = $suite;
3729                continue;
3730            }
3731
3732            $this->mergeSuites($this->suites[$name], $suite);
3733        }
3734    }
3735
3736    private function mergeSuites(array &$dest, array $source): void
3737    {
3738        $dest['test_total'] += $source['test_total'];
3739        $dest['test_pass']  += $source['test_pass'];
3740        $dest['test_fail']  += $source['test_fail'];
3741        $dest['test_error'] += $source['test_error'];
3742        $dest['test_skip']  += $source['test_skip'];
3743        $dest['test_warn']  += $source['test_warn'];
3744        $dest['execution_time'] += $source['execution_time'];
3745        $dest['files'] += $source['files'];
3746    }
3747}
3748
3749class SkipCache
3750{
3751    private bool $enable;
3752    private bool $keepFile;
3753
3754    private array $skips = [];
3755    private array $extensions = [];
3756
3757    private int $hits = 0;
3758    private int $misses = 0;
3759    private int $extHits = 0;
3760    private int $extMisses = 0;
3761
3762    public function __construct(bool $enable, bool $keepFile)
3763    {
3764        $this->enable = $enable;
3765        $this->keepFile = $keepFile;
3766    }
3767
3768    public function checkSkip(string $php, string $code, string $checkFile, string $tempFile, array $env): string
3769    {
3770        // Extension tests frequently use something like <?php require 'skipif.inc';
3771        // for skip checks. This forces us to cache per directory to avoid pollution.
3772        $dir = dirname($checkFile);
3773        $key = "$php => $dir";
3774
3775        if (isset($this->skips[$key][$code])) {
3776            $this->hits++;
3777            if ($this->keepFile) {
3778                save_text($checkFile, $code, $tempFile);
3779            }
3780            return $this->skips[$key][$code];
3781        }
3782
3783        save_text($checkFile, $code, $tempFile);
3784        $result = trim(system_with_timeout("$php \"$checkFile\"", $env));
3785        if (strpos($result, 'nocache') === 0) {
3786            $result = '';
3787        } else if ($this->enable) {
3788            $this->skips[$key][$code] = $result;
3789        }
3790        $this->misses++;
3791
3792        if (!$this->keepFile) {
3793            @unlink($checkFile);
3794        }
3795
3796        return $result;
3797    }
3798
3799    public function getExtensions(string $php): array
3800    {
3801        if (isset($this->extensions[$php])) {
3802            $this->extHits++;
3803            return $this->extensions[$php];
3804        }
3805
3806        $extDir = `$php -d display_errors=0 -r "echo ini_get('extension_dir');"`;
3807        $extensions = explode(",", `$php -d display_errors=0 -r "echo implode(',', get_loaded_extensions());"`);
3808        $extensions = array_map('strtolower', $extensions);
3809        if (in_array('zend opcache', $extensions)) {
3810            $extensions[] = 'opcache';
3811        }
3812
3813        $result = [$extDir, $extensions];
3814        $this->extensions[$php] = $result;
3815        $this->extMisses++;
3816
3817        return $result;
3818    }
3819
3820//    public function __destruct()
3821//    {
3822//        echo "Skips: {$this->hits} hits, {$this->misses} misses.\n";
3823//        echo "Extensions: {$this->extHits} hits, {$this->extMisses} misses.\n";
3824//        echo "Cache distribution:\n";
3825//
3826//        foreach ($this->skips as $php => $cache) {
3827//            echo "$php: " . count($cache) . "\n";
3828//        }
3829//    }
3830}
3831
3832class RuntestsValgrind
3833{
3834    protected $version = '';
3835    protected $header = '';
3836    protected $version_3_8_0 = false;
3837    protected $tool = null;
3838
3839    public function getVersion(): string
3840    {
3841        return $this->version;
3842    }
3843
3844    public function getHeader(): string
3845    {
3846        return $this->header;
3847    }
3848
3849    public function __construct(array $environment, string $tool = 'memcheck')
3850    {
3851        $this->tool = $tool;
3852        $header = system_with_timeout("valgrind --tool={$this->tool} --version", $environment);
3853        if (!$header) {
3854            error("Valgrind returned no version info for {$this->tool}, cannot proceed.\n".
3855                  "Please check if Valgrind is installed and the tool is named correctly.");
3856        }
3857        $count = 0;
3858        $version = preg_replace("/valgrind-(\d+)\.(\d+)\.(\d+)([.\w_-]+)?(\s+)/", '$1.$2.$3', $header, 1, $count);
3859        if ($count != 1) {
3860            error("Valgrind returned invalid version info (\"{$header}\") for {$this->tool}, cannot proceed.");
3861        }
3862        $this->version = $version;
3863        $this->header = sprintf(
3864            "%s (%s)", trim($header), $this->tool);
3865        $this->version_3_8_0 = version_compare($version, '3.8.0', '>=');
3866    }
3867
3868    public function wrapCommand(string $cmd, string $memcheck_filename, bool $check_all): string
3869    {
3870        $vcmd = "valgrind -q --tool={$this->tool} --trace-children=yes";
3871        if ($check_all) {
3872            $vcmd .= ' --smc-check=all';
3873        }
3874
3875        /* --vex-iropt-register-updates=allregs-at-mem-access is necessary for phpdbg watchpoint tests */
3876        if ($this->version_3_8_0) {
3877            return "$vcmd --vex-iropt-register-updates=allregs-at-mem-access --log-file=$memcheck_filename $cmd";
3878        }
3879        return "$vcmd --vex-iropt-precise-memory-exns=yes --log-file=$memcheck_filename $cmd";
3880    }
3881}
3882
3883class TestFile
3884{
3885    private string $fileName;
3886
3887    private array $sections = ['TEST' => ''];
3888
3889    private const ALLOWED_SECTIONS = [
3890        'EXPECT', 'EXPECTF', 'EXPECTREGEX', 'EXPECTREGEX_EXTERNAL', 'EXPECT_EXTERNAL', 'EXPECTF_EXTERNAL', 'EXPECTHEADERS',
3891        'POST', 'POST_RAW', 'GZIP_POST', 'DEFLATE_POST', 'PUT', 'GET', 'COOKIE', 'ARGS',
3892        'FILE', 'FILEEOF', 'FILE_EXTERNAL', 'REDIRECTTEST',
3893        'CAPTURE_STDIO', 'STDIN', 'CGI', 'PHPDBG',
3894        'INI', 'ENV', 'EXTENSIONS',
3895        'SKIPIF', 'XFAIL', 'XLEAK', 'CLEAN',
3896        'CREDITS', 'DESCRIPTION', 'CONFLICTS', 'WHITESPACE_SENSITIVE',
3897        'FLAKY',
3898    ];
3899
3900    /**
3901     * @throws BorkageException
3902     */
3903    public function __construct(string $fileName, bool $inRedirect)
3904    {
3905        $this->fileName = $fileName;
3906
3907        $this->readFile();
3908        $this->validateAndProcess($inRedirect);
3909    }
3910
3911    public function hasSection(string $name): bool
3912    {
3913        return isset($this->sections[$name]);
3914    }
3915
3916    public function hasAllSections(string ...$names): bool
3917    {
3918        foreach ($names as $section) {
3919            if (!isset($this->sections[$section])) {
3920                return false;
3921            }
3922        }
3923
3924        return true;
3925    }
3926
3927    public function hasAnySections(string ...$names): bool
3928    {
3929        foreach ($names as $section) {
3930            if (isset($this->sections[$section])) {
3931                return true;
3932            }
3933        }
3934
3935        return false;
3936    }
3937
3938    public function sectionNotEmpty(string $name): bool
3939    {
3940        return !empty($this->sections[$name]);
3941    }
3942
3943    /**
3944     * @throws Exception
3945     */
3946    public function getSection(string $name): string
3947    {
3948        if (!isset($this->sections[$name])) {
3949            throw new Exception("Section $name not found");
3950        }
3951        return $this->sections[$name];
3952    }
3953
3954    public function getName(): string
3955    {
3956        return trim($this->getSection('TEST'));
3957    }
3958
3959    public function isCGI(): bool
3960    {
3961        return $this->hasSection('CGI')
3962            || $this->sectionNotEmpty('GET')
3963            || $this->sectionNotEmpty('POST')
3964            || $this->sectionNotEmpty('GZIP_POST')
3965            || $this->sectionNotEmpty('DEFLATE_POST')
3966            || $this->sectionNotEmpty('POST_RAW')
3967            || $this->sectionNotEmpty('PUT')
3968            || $this->sectionNotEmpty('COOKIE')
3969            || $this->sectionNotEmpty('EXPECTHEADERS');
3970    }
3971
3972    /**
3973     * TODO Refactor to make it not needed
3974     */
3975    public function setSection(string $name, string $value): void
3976    {
3977        $this->sections[$name] = $value;
3978    }
3979
3980    /**
3981     * Load the sections of the test file
3982     * @throws BorkageException
3983     */
3984    private function readFile(): void
3985    {
3986        $fp = fopen($this->fileName, "rb") or error("Cannot open test file: {$this->fileName}");
3987
3988        if (!feof($fp)) {
3989            $line = fgets($fp);
3990
3991            if ($line === false) {
3992                throw new BorkageException("cannot read test");
3993            }
3994        } else {
3995            throw new BorkageException("empty test [{$this->fileName}]");
3996        }
3997        if (strncmp('--TEST--', $line, 8)) {
3998            throw new BorkageException("tests must start with --TEST-- [{$this->fileName}]");
3999        }
4000
4001        $section = 'TEST';
4002        $secfile = false;
4003        $secdone = false;
4004
4005        while (!feof($fp)) {
4006            $line = fgets($fp);
4007
4008            if ($line === false) {
4009                break;
4010            }
4011
4012            // Match the beginning of a section.
4013            if (preg_match('/^--([_A-Z]+)--/', $line, $r)) {
4014                $section = (string) $r[1];
4015
4016                if (isset($this->sections[$section]) && $this->sections[$section]) {
4017                    throw new BorkageException("duplicated $section section");
4018                }
4019
4020                // check for unknown sections
4021                if (!in_array($section, self::ALLOWED_SECTIONS)) {
4022                    throw new BorkageException('Unknown section "' . $section . '"');
4023                }
4024
4025                $this->sections[$section] = '';
4026                $secfile = $section == 'FILE' || $section == 'FILEEOF' || $section == 'FILE_EXTERNAL';
4027                $secdone = false;
4028                continue;
4029            }
4030
4031            // Add to the section text.
4032            if (!$secdone) {
4033                $this->sections[$section] .= $line;
4034            }
4035
4036            // End of actual test?
4037            if ($secfile && preg_match('/^===DONE===\s*$/', $line)) {
4038                $secdone = true;
4039            }
4040        }
4041
4042        fclose($fp);
4043    }
4044
4045    /**
4046     * @throws BorkageException
4047     */
4048    private function validateAndProcess(bool $inRedirect): void
4049    {
4050        // the redirect section allows a set of tests to be reused outside of
4051        // a given test dir
4052        if ($this->hasSection('REDIRECTTEST')) {
4053            if ($inRedirect) {
4054                throw new BorkageException("Can't redirect a test from within a redirected test");
4055            }
4056            return;
4057        }
4058        if (!$this->hasSection('PHPDBG') && $this->hasSection('FILE') + $this->hasSection('FILEEOF') + $this->hasSection('FILE_EXTERNAL') != 1) {
4059            throw new BorkageException("missing section --FILE--");
4060        }
4061
4062        if ($this->hasSection('FILEEOF')) {
4063            $this->sections['FILE'] = preg_replace("/[\r\n]+$/", '', $this->sections['FILEEOF']);
4064            unset($this->sections['FILEEOF']);
4065        }
4066
4067        foreach (['FILE', 'EXPECT', 'EXPECTF', 'EXPECTREGEX'] as $prefix) {
4068            // For grepping: FILE_EXTERNAL, EXPECT_EXTERNAL, EXPECTF_EXTERNAL, EXPECTREGEX_EXTERNAL
4069            $key = $prefix . '_EXTERNAL';
4070
4071            if ($this->hasSection($key)) {
4072                // don't allow tests to retrieve files from anywhere but this subdirectory
4073                $dir = dirname($this->fileName);
4074                $fileName = $dir . '/' . trim(str_replace('..', '', $this->getSection($key)));
4075
4076                if (file_exists($fileName)) {
4077                    $this->sections[$prefix] = file_get_contents($fileName);
4078                } else {
4079                    throw new BorkageException("could not load --" . $key . "-- " . $dir . '/' . trim($fileName));
4080                }
4081            }
4082        }
4083
4084        if (($this->hasSection('EXPECT') + $this->hasSection('EXPECTF') + $this->hasSection('EXPECTREGEX')) != 1) {
4085            throw new BorkageException("missing section --EXPECT--, --EXPECTF-- or --EXPECTREGEX--");
4086        }
4087
4088        if ($this->hasSection('PHPDBG') && !$this->hasSection('STDIN')) {
4089            $this->sections['STDIN'] = $this->sections['PHPDBG'] . "\n";
4090        }
4091    }
4092}
4093
4094function init_output_buffers(): void
4095{
4096    // Delete as much output buffers as possible.
4097    while (@ob_end_clean()) {
4098    }
4099
4100    if (ob_get_level()) {
4101        echo "Not all buffers were deleted.\n";
4102    }
4103}
4104
4105function check_proc_open_function_exists(): void
4106{
4107    if (!function_exists('proc_open')) {
4108        echo <<<NO_PROC_OPEN_ERROR
4109
4110+-----------------------------------------------------------+
4111|                       ! ERROR !                           |
4112| The test-suite requires that proc_open() is available.    |
4113| Please check if you disabled it in php.ini.               |
4114+-----------------------------------------------------------+
4115
4116NO_PROC_OPEN_ERROR;
4117        exit(1);
4118    }
4119}
4120
4121function bless_failed_tests(array $failedTests): void
4122{
4123    if (empty($failedTests)) {
4124        return;
4125    }
4126    $args = [
4127        PHP_BINARY,
4128        __DIR__ . '/scripts/dev/bless_tests.php',
4129    ];
4130    foreach ($failedTests as $test) {
4131        $args[] = $test['name'];
4132    }
4133    proc_open($args, [], $pipes);
4134}
4135
4136main();
4137