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