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