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