xref: /PHP-7.4/run-tests.php (revision a480bf80)
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: 19b7f5cb4015d00823d154fd0b256389c2d8d6cc $ */
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: 19b7f5cb4015d00823d154fd0b256389c2d8d6cc $' . "\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['BORKED'] || $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    } else if (defined('PHP_WINDOWS_VERSION_MAJOR') && (($stat["exitcode"] >> 28) & 0b1111) === 0b1100) {
1277        // https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-erref/87fba13e-bf06-450e-83b1-9241dc81e781
1278        $data .= "\nTermsig=" . $stat["exitcode"] . "\n";
1279    }
1280
1281    proc_close($proc);
1282    return $data;
1283}
1284
1285function run_all_tests($test_files, $env, $redir_tested = null)
1286{
1287    global $test_results, $failed_tests_file, $result_tests_file, $php, $test_idx;
1288    // Parallel testing
1289    global $PHP_FAILED_TESTS, $workers, $workerID, $workerSock;
1290
1291    if ($workers !== null && !$workerID) {
1292        run_all_tests_parallel($test_files, $env, $redir_tested);
1293        return;
1294    }
1295
1296    foreach ($test_files as $name) {
1297        if (is_array($name)) {
1298            $index = "# $name[1]: $name[0]";
1299
1300            if ($redir_tested) {
1301                $name = $name[0];
1302            }
1303        } else if ($redir_tested) {
1304            $index = "# $redir_tested: $name";
1305        } else {
1306            $index = $name;
1307        }
1308        $test_idx++;
1309
1310        if ($workerID) {
1311            $PHP_FAILED_TESTS = ['BORKED' => [], 'FAILED' => [], 'WARNED' => [], 'LEAKED' => [], 'XFAILED' => [], 'XLEAKED' => [], 'SLOW' => []];
1312            ob_start();
1313        }
1314
1315        $result = run_test($php, $name, $env);
1316        if ($workerID) {
1317            $resultText = ob_get_clean();
1318        }
1319
1320        if (!is_array($name) && $result != 'REDIR') {
1321            if ($workerID) {
1322                send_message($workerSock, [
1323                    "type" => "test_result",
1324                    "name" => $name,
1325                    "index" => $index,
1326                    "result" => $result,
1327                    "text" => $resultText,
1328                    "PHP_FAILED_TESTS" => $PHP_FAILED_TESTS
1329                ]);
1330                continue;
1331            }
1332
1333            $test_results[$index] = $result;
1334            if ($failed_tests_file && ($result == 'XFAILED' || $result == 'XLEAKED' || $result == 'FAILED' || $result == 'WARNED' || $result == 'LEAKED')) {
1335                fwrite($failed_tests_file, "$index\n");
1336            }
1337            if ($result_tests_file) {
1338                fwrite($result_tests_file, "$result\t$index\n");
1339            }
1340        }
1341    }
1342}
1343
1344/** The heart of parallel testing. */
1345function run_all_tests_parallel($test_files, $env, $redir_tested) {
1346    global $workers, $test_idx, $test_cnt, $test_results, $failed_tests_file, $result_tests_file, $PHP_FAILED_TESTS, $shuffle, $SHOW_ONLY_GROUPS;
1347
1348    // The PHP binary running run-tests.php, and run-tests.php itself
1349    // This PHP executable is *not* necessarily the same as the tested version
1350    $thisPHP = PHP_BINARY;
1351    $thisScript = __FILE__;
1352
1353    $workerProcs = [];
1354    $workerSocks = [];
1355
1356    echo "=====================================================================\n";
1357    echo "========= WELCOME TO THE FUTURE: run-tests PARALLEL EDITION =========\n";
1358    echo "=====================================================================\n";
1359
1360    // Each test may specify a list of conflict keys. While a test that conflicts with
1361    // key K is running, no other test that conflicts with K may run. Conflict keys are
1362    // specified either in the --CONFLICTS-- section, or CONFLICTS file inside a directory.
1363    $dirConflictsWith = [];
1364    $fileConflictsWith = [];
1365    $sequentialTests = [];
1366    foreach ($test_files as $i => $file) {
1367        $contents = file_get_contents($file);
1368        if (preg_match('/^--CONFLICTS--(.+?)^--/ms', $contents, $matches)) {
1369            $conflicts = parse_conflicts($matches[1]);
1370        } else {
1371            // Cache per-directory conflicts in a separate map, so we compute these only once.
1372            $dir = dirname($file);
1373            if (!isset($dirConflictsWith[$dir])) {
1374                $dirConflicts = [];
1375                if (file_exists($dir . '/CONFLICTS')) {
1376                    $contents = file_get_contents($dir . '/CONFLICTS');
1377                    $dirConflicts = parse_conflicts($contents);
1378                }
1379                $dirConflictsWith[$dir] = $dirConflicts;
1380            }
1381            $conflicts = $dirConflictsWith[$dir];
1382        }
1383
1384        // For tests conflicting with "all", no other tests may run in parallel. We'll run these
1385        // tests separately at the end, when only one worker is left.
1386        if (in_array('all', $conflicts, true)) {
1387            $sequentialTests[] = $file;
1388            unset($test_files[$i]);
1389        }
1390
1391        $fileConflictsWith[$file] = $conflicts;
1392    }
1393
1394    // Some tests assume that they are executed in a certain order. We will be popping from
1395    // $test_files, so reverse its order here. This makes sure that order is preserved at least
1396    // for tests with a common conflict key.
1397    $test_files = array_reverse($test_files);
1398
1399    // To discover parallelization issues it is useful to randomize the test order.
1400    if ($shuffle) {
1401        shuffle($test_files);
1402    }
1403
1404    echo "Spawning workers… ";
1405
1406    // We use sockets rather than STDIN/STDOUT for comms because on Windows,
1407    // those can't be non-blocking for some reason.
1408    $listenSock = stream_socket_server("tcp://127.0.0.1:0") or error("Couldn't create socket on localhost.");
1409    $sockName = stream_socket_get_name($listenSock, false);
1410    // PHP is terrible and returns IPv6 addresses not enclosed by []
1411    $portPos = strrpos($sockName, ":");
1412    $sockHost = substr($sockName, 0, $portPos);
1413    if (FALSE !== strpos($sockHost, ":")) {
1414        $sockHost = "[$sockHost]";
1415    }
1416    $sockPort = substr($sockName, $portPos + 1);
1417    $sockUri = "tcp://$sockHost:$sockPort";
1418
1419    for ($i = 1; $i <= $workers; $i++) {
1420        $proc = proc_open(
1421            $thisPHP . ' ' . escapeshellarg($thisScript),
1422            [], // Inherit our stdin, stdout and stderr
1423            $pipes,
1424            NULL,
1425            $_ENV + [
1426                "TEST_PHP_WORKER" => $i,
1427                "TEST_PHP_URI" => $sockUri,
1428            ],
1429            [
1430                "suppress_errors" => TRUE,
1431                'create_new_console' => TRUE,
1432            ]
1433        );
1434        if ($proc === FALSE) {
1435            kill_children($workerProcs);
1436            error("Failed to spawn worker $i");
1437        }
1438        $workerProcs[$i] = $proc;
1439
1440        $workerSock = stream_socket_accept($listenSock, 5);
1441        if ($workerSock === FALSE) {
1442            kill_children($workerProcs);
1443            error("Failed to accept connection from worker $i");
1444        }
1445
1446        $greeting = base64_encode(serialize([
1447            "type" => "hello",
1448            "workerID" => $i,
1449            "GLOBALS" => $GLOBALS,
1450            "constants" => [
1451                "INIT_DIR" => INIT_DIR,
1452                "TEST_PHP_SRCDIR" => TEST_PHP_SRCDIR,
1453                "PHP_QA_EMAIL" => PHP_QA_EMAIL,
1454                "QA_SUBMISSION_PAGE" => QA_SUBMISSION_PAGE,
1455                "QA_REPORTS_PAGE" => QA_REPORTS_PAGE,
1456                "TRAVIS_CI" => TRAVIS_CI
1457            ]
1458        ])) . "\n";
1459
1460        stream_set_timeout($workerSock, 5);
1461        if (fwrite($workerSock, $greeting) === FALSE) {
1462            kill_children($workerProcs);
1463            error("Failed to send greeting to worker $i.");
1464        }
1465
1466        $rawReply = fgets($workerSock);
1467        if ($rawReply === FALSE) {
1468            kill_children($workerProcs);
1469            error("Failed to read greeting reply from worker $i.");
1470        }
1471
1472        $reply = unserialize(base64_decode($rawReply));
1473        if (!$reply || $reply["type"] !== "hello_reply" || $reply["workerID"] !== $i) {
1474            kill_children($workerProcs);
1475            error("Greeting reply from worker $i unexpected or could not be decoded: '$rawReply'");
1476        }
1477
1478        stream_set_timeout($workerSock, 0);
1479        stream_set_blocking($workerSock, FALSE);
1480
1481        $workerSocks[$i] = $workerSock;
1482
1483        echo "$i ";
1484    }
1485    echo "… done!\n";
1486    echo "=====================================================================\n";
1487    echo "\n";
1488
1489    $rawMessageBuffers = [];
1490    $testsInProgress = 0;
1491
1492    // Map from conflict key to worker ID.
1493    $activeConflicts = [];
1494    // Tests waiting due to conflicts. Map from conflict key to array.
1495    $waitingTests = [];
1496
1497escape:
1498    while ($test_files || $sequentialTests || $testsInProgress > 0) {
1499        $toRead = array_values($workerSocks);
1500        $toWrite = NULL;
1501        $toExcept = NULL;
1502        if (stream_select($toRead, $toWrite, $toExcept, 10)) {
1503            foreach ($toRead as $workerSock) {
1504                $i = array_search($workerSock, $workerSocks);
1505                if ($i === FALSE) {
1506                    kill_children($workerProcs);
1507                    error("Could not find worker stdout in array of worker stdouts, THIS SHOULD NOT HAPPEN.");
1508                }
1509                while (FALSE !== ($rawMessage = fgets($workerSock))) {
1510                    // work around fgets truncating things
1511                    if (($rawMessageBuffers[$i] ?? '') !== '') {
1512                        $rawMessage = $rawMessageBuffers[$i] . $rawMessage;
1513                        $rawMessageBuffers[$i] = '';
1514                    }
1515                    if (substr($rawMessage, -1) !== "\n") {
1516                        $rawMessageBuffers[$i] = $rawMessage;
1517                        continue;
1518                    }
1519
1520                    $message = unserialize(base64_decode($rawMessage));
1521                    if (!$message) {
1522                        kill_children($workerProcs);
1523                        $stuff = fread($workerSock, 65536);
1524                        error("Could not decode message from worker $i: '$rawMessage$stuff'");
1525                    }
1526
1527                    switch ($message["type"]) {
1528                        case "tests_finished":
1529                            $testsInProgress--;
1530                            foreach ($activeConflicts as $key => $workerId) {
1531                                if ($workerId === $i) {
1532                                    unset($activeConflicts[$key]);
1533                                    if (isset($waitingTests[$key])) {
1534                                        while ($test = array_pop($waitingTests[$key])) {
1535                                            $test_files[] = $test;
1536                                        }
1537                                        unset($waitingTests[$key]);
1538                                    }
1539                                }
1540                            }
1541                            if (junit_enabled()) {
1542                                junit_merge_results($message["junit"]);
1543                            }
1544                            // intentional fall-through
1545                        case "ready":
1546                            // Schedule sequential tests only once we are down to one worker.
1547                            if (count($workerProcs) === 1 && $sequentialTests) {
1548                                $test_files = array_merge($test_files, $sequentialTests);
1549                                $sequentialTests = [];
1550                            }
1551                            // Batch multiple tests to reduce communication overhead.
1552                            $files = [];
1553                            $batchSize = $shuffle ? 4 : 32;
1554                            while (count($files) <= $batchSize && $file = array_pop($test_files)) {
1555                                foreach ($fileConflictsWith[$file] as $conflictKey) {
1556                                    if (isset($activeConflicts[$conflictKey])) {
1557                                        $waitingTests[$conflictKey][] = $file;
1558                                        continue 2;
1559                                    }
1560                                }
1561                                $files[] = $file;
1562                            }
1563                            if ($files) {
1564                                foreach ($files as $file) {
1565                                    foreach ($fileConflictsWith[$file] as $conflictKey) {
1566                                        $activeConflicts[$conflictKey] = $i;
1567                                    }
1568                                }
1569                                $testsInProgress++;
1570                                send_message($workerSocks[$i], [
1571                                    "type" => "run_tests",
1572                                    "test_files" => $files,
1573                                    "env" => $env,
1574                                    "redir_tested" => $redir_tested
1575                                ]);
1576                            } else {
1577                                proc_terminate($workerProcs[$i]);
1578                                unset($workerProcs[$i]);
1579                                unset($workerSocks[$i]);
1580                                goto escape;
1581                            }
1582                            break;
1583                        case "test_result":
1584                            list($name, $index, $result, $resultText) = [$message["name"], $message["index"], $message["result"], $message["text"]];
1585                            foreach ($message["PHP_FAILED_TESTS"] as $category => $tests) {
1586                                $PHP_FAILED_TESTS[$category] = array_merge($PHP_FAILED_TESTS[$category], $tests);
1587                            }
1588                            $test_idx++;
1589
1590                            if (!$SHOW_ONLY_GROUPS) {
1591                                clear_show_test();
1592                            }
1593
1594                            echo $resultText;
1595
1596                            if (!$SHOW_ONLY_GROUPS) {
1597                                show_test($test_idx, count($workerProcs) . "/$workers concurrent test workers running");
1598                            }
1599
1600                            if (!is_array($name) && $result != 'REDIR') {
1601                                $test_results[$index] = $result;
1602
1603                                if ($failed_tests_file && ($result == 'XFAILED' || $result == 'XLEAKED' || $result == 'FAILED' || $result == 'WARNED' || $result == 'LEAKED')) {
1604                                    fwrite($failed_tests_file, "$index\n");
1605                                }
1606                                if ($result_tests_file) {
1607                                    fwrite($result_tests_file, "$result\t$index\n");
1608                                }
1609                            }
1610                            break;
1611                        case "error":
1612                            kill_children($workerProcs);
1613                            error("Worker $i reported error: $message[msg]");
1614                            break;
1615                        case "php_error":
1616                            kill_children($workerProcs);
1617                            $error_consts = [
1618                                'E_ERROR',
1619                                'E_WARNING',
1620                                'E_PARSE',
1621                                'E_NOTICE',
1622                                'E_CORE_ERROR',
1623                                'E_CORE_WARNING',
1624                                'E_COMPILE_ERROR',
1625                                'E_COMPILE_WARNING',
1626                                'E_USER_ERROR',
1627                                'E_USER_WARNING',
1628                                'E_USER_NOTICE',
1629                                'E_STRICT', // TODO Cleanup when removed from Zend Engine.
1630                                'E_RECOVERABLE_ERROR',
1631                                'E_USER_DEPRECATED'
1632                            ];
1633                            $error_consts = array_combine(array_map('constant', $error_consts), $error_consts);
1634                            error("Worker $i reported unexpected {$error_consts[$message['errno']]}: $message[errstr] in $message[errfile] on line $message[errline]");
1635                        default:
1636                            kill_children($workerProcs);
1637                            error("Unrecognised message type '$message[type]' from worker $i");
1638                    }
1639                }
1640            }
1641        }
1642    }
1643
1644    if (!$SHOW_ONLY_GROUPS) {
1645        clear_show_test();
1646    }
1647
1648    kill_children($workerProcs);
1649
1650    if ($testsInProgress < 0) {
1651        error("$testsInProgress test batches “in progress”, which is less than zero. THIS SHOULD NOT HAPPEN.");
1652    }
1653}
1654
1655function send_message($stream, array $message) {
1656    $blocking = stream_get_meta_data($stream)["blocked"];
1657    stream_set_blocking($stream, true);
1658    fwrite($stream, base64_encode(serialize($message)) . "\n");
1659    stream_set_blocking($stream, $blocking);
1660}
1661
1662function kill_children(array $children) {
1663    foreach ($children as $child) {
1664        if ($child) {
1665            proc_terminate($child);
1666        }
1667    }
1668}
1669
1670function run_worker() {
1671    global $workerID, $workerSock;
1672
1673    $sockUri = getenv("TEST_PHP_URI");
1674
1675    $workerSock = stream_socket_client($sockUri, $_, $_, 5) or error("Couldn't connect to $sockUri");
1676
1677    $greeting = fgets($workerSock);
1678    $greeting = unserialize(base64_decode($greeting)) or die("Could not decode greeting\n");
1679    if ($greeting["type"] !== "hello" || $greeting["workerID"] !== $workerID) {
1680        error("Unexpected greeting of type $greeting[type] and for worker $greeting[workerID]");
1681    }
1682
1683    set_error_handler(function ($errno, $errstr, $errfile, $errline) use ($workerSock) {
1684        if (error_reporting() & $errno) {
1685            send_message($workerSock, compact('errno', 'errstr', 'errfile', 'errline') + [
1686                'type' => 'php_error'
1687            ]);
1688        }
1689
1690        return true;
1691    });
1692
1693    foreach ($greeting["GLOBALS"] as $var => $value) {
1694        if ($var !== "workerID" && $var !== "workerSock" && $var !== "GLOBALS") {
1695            $GLOBALS[$var] = $value;
1696        }
1697    }
1698    foreach ($greeting["constants"] as $const => $value) {
1699        define($const, $value);
1700    }
1701
1702    send_message($workerSock, [
1703        "type" => "hello_reply",
1704        "workerID" => $workerID
1705    ]);
1706
1707    send_message($workerSock, [
1708        "type" => "ready"
1709    ]);
1710
1711    while (($command = fgets($workerSock))) {
1712        $command = unserialize(base64_decode($command));
1713
1714        switch ($command["type"]) {
1715            case "run_tests":
1716                run_all_tests($command["test_files"], $command["env"], $command["redir_tested"]);
1717                send_message($workerSock, [
1718                    "type" => "tests_finished",
1719                    "junit" => junit_enabled() ? $GLOBALS['JUNIT'] : null,
1720                ]);
1721                junit_init();
1722                break;
1723            default:
1724                send_message($workerSock, [
1725                    "type" => "error",
1726                    "msg" => "Unrecognised message type: $command[type]"
1727                ]);
1728                break 2;
1729        }
1730    }
1731}
1732
1733//
1734//  Show file or result block
1735//
1736function show_file_block($file, $block, $section = null)
1737{
1738    global $cfg;
1739
1740    if ($cfg['show'][$file]) {
1741
1742        if (is_null($section)) {
1743            $section = strtoupper($file);
1744        }
1745
1746        echo "\n========" . $section . "========\n";
1747        echo rtrim($block);
1748        echo "\n========DONE========\n";
1749    }
1750}
1751
1752//
1753//  Run an individual test case.
1754//
1755function run_test($php, $file, $env)
1756{
1757    global $log_format, $ini_overwrites, $PHP_FAILED_TESTS;
1758    global $pass_options, $DETAILED, $IN_REDIRECT, $test_cnt, $test_idx;
1759    global $valgrind, $temp_source, $temp_target, $cfg, $environment;
1760    global $no_clean;
1761    global $SHOW_ONLY_GROUPS;
1762    global $no_file_cache;
1763    global $slow_min_ms;
1764    global $preload;
1765    // Parallel testing
1766    global $workerID;
1767    $temp_filenames = null;
1768    $org_file = $file;
1769
1770    if (isset($env['TEST_PHP_CGI_EXECUTABLE'])) {
1771        $php_cgi = $env['TEST_PHP_CGI_EXECUTABLE'];
1772    }
1773
1774    if (isset($env['TEST_PHPDBG_EXECUTABLE'])) {
1775        $phpdbg = $env['TEST_PHPDBG_EXECUTABLE'];
1776    }
1777
1778    if (is_array($file)) {
1779        $file = $file[0];
1780    }
1781
1782    if ($DETAILED) echo "
1783=================
1784TEST $file
1785";
1786
1787    // Load the sections of the test file.
1788    $section_text = array('TEST' => '');
1789
1790    $fp = fopen($file, "rb") or error("Cannot open test file: $file");
1791
1792    $bork_info = null;
1793
1794    if (!feof($fp)) {
1795        $line = fgets($fp);
1796
1797        if ($line === false) {
1798            $bork_info = "cannot read test";
1799        }
1800    } else {
1801        $bork_info = "empty test [$file]";
1802    }
1803    if ($bork_info === null && strncmp('--TEST--', $line, 8)) {
1804        $bork_info = "tests must start with --TEST-- [$file]";
1805    }
1806
1807    $section = 'TEST';
1808    $secfile = false;
1809    $secdone = false;
1810
1811    while (!feof($fp)) {
1812        $line = fgets($fp);
1813
1814        if ($line === false) {
1815            break;
1816        }
1817
1818        // Match the beginning of a section.
1819        if (preg_match('/^--([_A-Z]+)--/', $line, $r)) {
1820            $section = (string)$r[1];
1821
1822            if (isset($section_text[$section]) && $section_text[$section]) {
1823                $bork_info = "duplicated $section section";
1824            }
1825
1826            // check for unknown sections
1827            if (!in_array($section, array(
1828                'EXPECT', 'EXPECTF', 'EXPECTREGEX', 'EXPECTREGEX_EXTERNAL', 'EXPECT_EXTERNAL', 'EXPECTF_EXTERNAL', 'EXPECTHEADERS',
1829                'POST', 'POST_RAW', 'GZIP_POST', 'DEFLATE_POST', 'PUT', 'GET', 'COOKIE', 'ARGS',
1830                'FILE', 'FILEEOF', 'FILE_EXTERNAL', 'REDIRECTTEST',
1831                'CAPTURE_STDIO', 'STDIN', 'CGI', 'PHPDBG',
1832                'INI', 'ENV', 'EXTENSIONS',
1833                'SKIPIF', 'XFAIL', 'XLEAK', 'CLEAN',
1834                'CREDITS', 'DESCRIPTION', 'CONFLICTS', 'WHITESPACE_SENSITIVE',
1835            ))) {
1836                $bork_info = 'Unknown section "' . $section . '"';
1837            }
1838
1839            $section_text[$section] = '';
1840            $secfile = $section == 'FILE' || $section == 'FILEEOF' || $section == 'FILE_EXTERNAL';
1841            $secdone = false;
1842            continue;
1843        }
1844
1845        // Add to the section text.
1846        if (!$secdone) {
1847            $section_text[$section] .= $line;
1848        }
1849
1850        // End of actual test?
1851        if ($secfile && preg_match('/^===DONE===\s*$/', $line)) {
1852            $secdone = true;
1853        }
1854    }
1855
1856    // the redirect section allows a set of tests to be reused outside of
1857    // a given test dir
1858    if ($bork_info === null) {
1859        if (isset($section_text['REDIRECTTEST'])) {
1860
1861            if ($IN_REDIRECT) {
1862                $bork_info = "Can't redirect a test from within a redirected test";
1863            }
1864
1865        } else {
1866
1867            if (!isset($section_text['PHPDBG']) && isset($section_text['FILE']) + isset($section_text['FILEEOF']) + isset($section_text['FILE_EXTERNAL']) != 1) {
1868                $bork_info = "missing section --FILE--";
1869            }
1870
1871            if (isset($section_text['FILEEOF'])) {
1872                $section_text['FILE'] = preg_replace("/[\r\n]+$/", '', $section_text['FILEEOF']);
1873                unset($section_text['FILEEOF']);
1874            }
1875
1876            foreach (array('FILE', 'EXPECT', 'EXPECTF', 'EXPECTREGEX') as $prefix) {
1877                $key = $prefix . '_EXTERNAL';
1878
1879                if (isset($section_text[$key])) {
1880                    // don't allow tests to retrieve files from anywhere but this subdirectory
1881                    $section_text[$key] = dirname($file) . '/' . trim(str_replace('..', '', $section_text[$key]));
1882
1883                    if (file_exists($section_text[$key])) {
1884                        $section_text[$prefix] = file_get_contents($section_text[$key], FILE_BINARY);
1885                        unset($section_text[$key]);
1886                    } else {
1887                        $bork_info = "could not load --" . $key . "-- " . dirname($file) . '/' . trim($section_text[$key]);
1888                    }
1889                }
1890            }
1891
1892            if ((isset($section_text['EXPECT']) + isset($section_text['EXPECTF']) + isset($section_text['EXPECTREGEX'])) != 1) {
1893                $bork_info = "missing section --EXPECT--, --EXPECTF-- or --EXPECTREGEX--";
1894            }
1895        }
1896    }
1897    fclose($fp);
1898
1899    $shortname = str_replace(TEST_PHP_SRCDIR . '/', '', $file);
1900    $tested_file = $shortname;
1901
1902    if ($bork_info !== null) {
1903        show_result("BORK", $bork_info, $tested_file);
1904        $PHP_FAILED_TESTS['BORKED'][] = array(
1905            'name' => $file,
1906            'test_name' => '',
1907            'output' => '',
1908            'diff' => '',
1909            'info' => "$bork_info [$file]",
1910        );
1911
1912        junit_mark_test_as('BORK', $shortname, $tested_file, 0, $bork_info);
1913        return 'BORKED';
1914    }
1915
1916    if (isset($section_text['CAPTURE_STDIO'])) {
1917        $captureStdIn = stripos($section_text['CAPTURE_STDIO'], 'STDIN') !== false;
1918        $captureStdOut = stripos($section_text['CAPTURE_STDIO'], 'STDOUT') !== false;
1919        $captureStdErr = stripos($section_text['CAPTURE_STDIO'], 'STDERR') !== false;
1920    } else {
1921        $captureStdIn = true;
1922        $captureStdOut = true;
1923        $captureStdErr = true;
1924    }
1925    if ($captureStdOut && $captureStdErr) {
1926        $cmdRedirect = ' 2>&1';
1927    } else {
1928        $cmdRedirect = '';
1929    }
1930
1931    $tested = trim($section_text['TEST']);
1932
1933    /* For GET/POST/PUT tests, check if cgi sapi is available and if it is, use it. */
1934    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'])) {
1935        if (isset($php_cgi)) {
1936            $php = $php_cgi . ' -C ';
1937        } else if (!strncasecmp(PHP_OS, "win", 3) && file_exists(dirname($php) . "/php-cgi.exe")) {
1938            $php = realpath(dirname($php) . "/php-cgi.exe") . ' -C ';
1939        } else {
1940            if (file_exists(dirname($php) . "/../../sapi/cgi/php-cgi")) {
1941                $php = realpath(dirname($php) . "/../../sapi/cgi/php-cgi") . ' -C ';
1942            } else if (file_exists("./sapi/cgi/php-cgi")) {
1943                $php = realpath("./sapi/cgi/php-cgi") . ' -C ';
1944            } else if (file_exists(dirname($php) . "/php-cgi")) {
1945                $php = realpath(dirname($php) . "/php-cgi") . ' -C ';
1946            } else {
1947                show_result('SKIP', $tested, $tested_file, "reason: CGI not available");
1948
1949                junit_init_suite(junit_get_suitename_for($shortname));
1950                junit_mark_test_as('SKIP', $shortname, $tested, 0, 'CGI not available');
1951                return 'SKIPPED';
1952            }
1953        }
1954        $uses_cgi = true;
1955    }
1956
1957    /* For phpdbg tests, check if phpdbg sapi is available and if it is, use it. */
1958    $extra_options = '';
1959    if (array_key_exists('PHPDBG', $section_text)) {
1960        if (!isset($section_text['STDIN'])) {
1961            $section_text['STDIN'] = $section_text['PHPDBG'] . "\n";
1962        }
1963
1964        if (isset($phpdbg)) {
1965            $php = $phpdbg . ' -qIb';
1966
1967            // Additional phpdbg command line options for sections that need to
1968            // be run straight away. For example, EXTENSIONS, SKIPIF, CLEAN.
1969            $extra_options = '-rr';
1970        } else {
1971            show_result('SKIP', $tested, $tested_file, "reason: phpdbg not available");
1972
1973            junit_init_suite(junit_get_suitename_for($shortname));
1974            junit_mark_test_as('SKIP', $shortname, $tested, 0, 'phpdbg not available');
1975            return 'SKIPPED';
1976        }
1977    }
1978
1979    if (!$SHOW_ONLY_GROUPS && !$workerID) {
1980        show_test($test_idx, $shortname);
1981    }
1982
1983    if (is_array($IN_REDIRECT)) {
1984        $temp_dir = $test_dir = $IN_REDIRECT['dir'];
1985    } else {
1986        $temp_dir = $test_dir = realpath(dirname($file));
1987    }
1988
1989    if ($temp_source && $temp_target) {
1990        $temp_dir = str_replace($temp_source, $temp_target, $temp_dir);
1991    }
1992
1993    $main_file_name = basename($file, 'phpt');
1994
1995    $diff_filename = $temp_dir . DIRECTORY_SEPARATOR . $main_file_name . 'diff';
1996    $log_filename = $temp_dir . DIRECTORY_SEPARATOR . $main_file_name . 'log';
1997    $exp_filename = $temp_dir . DIRECTORY_SEPARATOR . $main_file_name . 'exp';
1998    $output_filename = $temp_dir . DIRECTORY_SEPARATOR . $main_file_name . 'out';
1999    $memcheck_filename = $temp_dir . DIRECTORY_SEPARATOR . $main_file_name . 'mem';
2000    $sh_filename = $temp_dir . DIRECTORY_SEPARATOR . $main_file_name . 'sh';
2001    $temp_file = $temp_dir . DIRECTORY_SEPARATOR . $main_file_name . 'php';
2002    $test_file = $test_dir . DIRECTORY_SEPARATOR . $main_file_name . 'php';
2003    $temp_skipif = $temp_dir . DIRECTORY_SEPARATOR . $main_file_name . 'skip.php';
2004    $test_skipif = $test_dir . DIRECTORY_SEPARATOR . $main_file_name . 'skip.php';
2005    $temp_clean = $temp_dir . DIRECTORY_SEPARATOR . $main_file_name . 'clean.php';
2006    $test_clean = $test_dir . DIRECTORY_SEPARATOR . $main_file_name . 'clean.php';
2007    $preload_filename = $temp_dir . DIRECTORY_SEPARATOR . $main_file_name . 'preload.php';
2008    $tmp_post = $temp_dir . DIRECTORY_SEPARATOR . $main_file_name . 'post';
2009    $tmp_relative_file = str_replace(__DIR__ . DIRECTORY_SEPARATOR, '', $test_file) . 't';
2010
2011    if ($temp_source && $temp_target) {
2012        $temp_skipif .= 's';
2013        $temp_file .= 's';
2014        $temp_clean .= 's';
2015        $copy_file = $temp_dir . DIRECTORY_SEPARATOR . basename(is_array($file) ? $file[1] : $file) . '.phps';
2016
2017        if (!is_dir(dirname($copy_file))) {
2018            mkdir(dirname($copy_file), 0777, true) or error("Cannot create output directory - " . dirname($copy_file));
2019        }
2020
2021        if (isset($section_text['FILE'])) {
2022            save_text($copy_file, $section_text['FILE']);
2023        }
2024
2025        $temp_filenames = array(
2026            'file' => $copy_file,
2027            'diff' => $diff_filename,
2028            'log' => $log_filename,
2029            'exp' => $exp_filename,
2030            'out' => $output_filename,
2031            'mem' => $memcheck_filename,
2032            'sh' => $sh_filename,
2033            'php' => $temp_file,
2034            'skip' => $temp_skipif,
2035            'clean' => $temp_clean
2036        );
2037    }
2038
2039    if (is_array($IN_REDIRECT)) {
2040        $tested = $IN_REDIRECT['prefix'] . ' ' . trim($section_text['TEST']);
2041        $tested_file = $tmp_relative_file;
2042        $shortname = str_replace(TEST_PHP_SRCDIR . '/', '', $tested_file);
2043    }
2044
2045    // unlink old test results
2046    @unlink($diff_filename);
2047    @unlink($log_filename);
2048    @unlink($exp_filename);
2049    @unlink($output_filename);
2050    @unlink($memcheck_filename);
2051    @unlink($sh_filename);
2052    @unlink($temp_file);
2053    @unlink($test_file);
2054    @unlink($temp_skipif);
2055    @unlink($test_skipif);
2056    @unlink($tmp_post);
2057    @unlink($temp_clean);
2058    @unlink($test_clean);
2059    @unlink($preload_filename);
2060
2061    // Reset environment from any previous test.
2062    $env['REDIRECT_STATUS'] = '';
2063    $env['QUERY_STRING'] = '';
2064    $env['PATH_TRANSLATED'] = '';
2065    $env['SCRIPT_FILENAME'] = '';
2066    $env['REQUEST_METHOD'] = '';
2067    $env['CONTENT_TYPE'] = '';
2068    $env['CONTENT_LENGTH'] = '';
2069    $env['TZ'] = '';
2070
2071    if (!empty($section_text['ENV'])) {
2072
2073        foreach (explode("\n", trim($section_text['ENV'])) as $e) {
2074            $e = explode('=', trim($e), 2);
2075
2076            if (!empty($e[0]) && isset($e[1])) {
2077                $env[$e[0]] = $e[1];
2078            }
2079        }
2080    }
2081
2082    // Default ini settings
2083    $ini_settings = $workerID ? array('opcache.cache_id' => "worker$workerID") : array();
2084
2085    // Additional required extensions
2086    if (array_key_exists('EXTENSIONS', $section_text)) {
2087        $ext_params = array();
2088        settings2array($ini_overwrites, $ext_params);
2089        $ext_params = settings2params($ext_params);
2090        $ext_dir = `$php $pass_options $extra_options $ext_params -d display_errors=0 -r "echo ini_get('extension_dir');"`;
2091        $extensions = preg_split("/[\n\r]+/", trim($section_text['EXTENSIONS']));
2092        $loaded = explode(",", `$php $pass_options $extra_options $ext_params -d display_errors=0 -r "echo implode(',', get_loaded_extensions());"`);
2093        $ext_prefix = substr(PHP_OS, 0, 3) === "WIN" ? "php_" : "";
2094        foreach ($extensions as $req_ext) {
2095            if (!in_array($req_ext, $loaded)) {
2096                if ($req_ext == 'opcache') {
2097                    $ini_settings['zend_extension'][] = $ext_dir . DIRECTORY_SEPARATOR . $ext_prefix . $req_ext . '.' . PHP_SHLIB_SUFFIX;
2098                } else {
2099                    $ini_settings['extension'][] = $ext_dir . DIRECTORY_SEPARATOR . $ext_prefix . $req_ext . '.' . PHP_SHLIB_SUFFIX;
2100                }
2101            }
2102        }
2103    }
2104
2105    // additional ini overwrites
2106    //$ini_overwrites[] = 'setting=value';
2107    settings2array($ini_overwrites, $ini_settings);
2108
2109    $orig_ini_settings = settings2params($ini_settings);
2110
2111    // Any special ini settings
2112    // these may overwrite the test defaults...
2113    if (array_key_exists('INI', $section_text)) {
2114        $section_text['INI'] = str_replace('{PWD}', dirname($file), $section_text['INI']);
2115        $section_text['INI'] = str_replace('{TMP}', sys_get_temp_dir(), $section_text['INI']);
2116        settings2array(preg_split("/[\n\r]+/", $section_text['INI']), $ini_settings);
2117    }
2118
2119    $ini_settings = settings2params($ini_settings);
2120
2121    $env['TEST_PHP_EXTRA_ARGS'] = $pass_options . ' ' . $ini_settings;
2122
2123    // Check if test should be skipped.
2124    $info = '';
2125    $warn = false;
2126
2127    if (array_key_exists('SKIPIF', $section_text)) {
2128
2129        if (trim($section_text['SKIPIF'])) {
2130            show_file_block('skip', $section_text['SKIPIF']);
2131            save_text($test_skipif, $section_text['SKIPIF'], $temp_skipif);
2132            $extra = substr(PHP_OS, 0, 3) !== "WIN" ?
2133                "unset REQUEST_METHOD; unset QUERY_STRING; unset PATH_TRANSLATED; unset SCRIPT_FILENAME; unset REQUEST_METHOD;" : "";
2134
2135            if ($valgrind) {
2136                $env['USE_ZEND_ALLOC'] = '0';
2137                $env['ZEND_DONT_UNLOAD_MODULES'] = 1;
2138            }
2139
2140            junit_start_timer($shortname);
2141
2142            $output = system_with_timeout("$extra $php $pass_options $extra_options -q $orig_ini_settings $no_file_cache -d display_errors=0 \"$test_skipif\"", $env);
2143
2144            junit_finish_timer($shortname);
2145
2146            if (!$cfg['keep']['skip']) {
2147                @unlink($test_skipif);
2148            }
2149
2150            if (!strncasecmp('skip', ltrim($output), 4)) {
2151
2152                if (preg_match('/^\s*skip\s*(.+)\s*/i', $output, $m)) {
2153                    show_result('SKIP', $tested, $tested_file, "reason: $m[1]", $temp_filenames);
2154                } else {
2155                    show_result('SKIP', $tested, $tested_file, '', $temp_filenames);
2156                }
2157
2158                if (!$cfg['keep']['skip']) {
2159                    @unlink($test_skipif);
2160                }
2161
2162                $message = !empty($m[1]) ? $m[1] : '';
2163                junit_mark_test_as('SKIP', $shortname, $tested, null, $message);
2164                return 'SKIPPED';
2165            }
2166
2167            if (!strncasecmp('info', ltrim($output), 4)) {
2168                if (preg_match('/^\s*info\s*(.+)\s*/i', $output, $m)) {
2169                    $info = " (info: $m[1])";
2170                }
2171            }
2172
2173            if (!strncasecmp('warn', ltrim($output), 4)) {
2174                if (preg_match('/^\s*warn\s*(.+)\s*/i', $output, $m)) {
2175                    $warn = true; /* only if there is a reason */
2176                    $info = " (warn: $m[1])";
2177                }
2178            }
2179
2180            if (!strncasecmp('xfail', ltrim($output), 5)) {
2181                // Pretend we have an XFAIL section
2182                $section_text['XFAIL'] = trim(substr(ltrim($output), 5));
2183            }
2184        }
2185    }
2186
2187    if (!extension_loaded("zlib")
2188        && (array_key_exists("GZIP_POST", $section_text)
2189        || array_key_exists("DEFLATE_POST", $section_text))) {
2190        $message = "ext/zlib required";
2191        show_result('SKIP', $tested, $tested_file, "reason: $message", $temp_filenames);
2192        junit_mark_test_as('SKIP', $shortname, $tested, null, $message);
2193        return 'SKIPPED';
2194    }
2195
2196    if (isset($section_text['REDIRECTTEST'])) {
2197        $test_files = array();
2198
2199        $IN_REDIRECT = eval($section_text['REDIRECTTEST']);
2200        $IN_REDIRECT['via'] = "via [$shortname]\n\t";
2201        $IN_REDIRECT['dir'] = realpath(dirname($file));
2202        $IN_REDIRECT['prefix'] = trim($section_text['TEST']);
2203
2204        if (!empty($IN_REDIRECT['TESTS'])) {
2205
2206            if (is_array($org_file)) {
2207                $test_files[] = $org_file[1];
2208            } else {
2209                $GLOBALS['test_files'] = $test_files;
2210                find_files($IN_REDIRECT['TESTS']);
2211
2212                foreach ($GLOBALS['test_files'] as $f) {
2213                    $test_files[] = array($f, $file);
2214                }
2215            }
2216            $test_cnt += count($test_files) - 1;
2217            $test_idx--;
2218
2219            show_redirect_start($IN_REDIRECT['TESTS'], $tested, $tested_file);
2220
2221            // set up environment
2222            $redirenv = array_merge($environment, $IN_REDIRECT['ENV']);
2223            $redirenv['REDIR_TEST_DIR'] = realpath($IN_REDIRECT['TESTS']) . DIRECTORY_SEPARATOR;
2224
2225            usort($test_files, "test_sort");
2226            run_all_tests($test_files, $redirenv, $tested);
2227
2228            show_redirect_ends($IN_REDIRECT['TESTS'], $tested, $tested_file);
2229
2230            // a redirected test never fails
2231            $IN_REDIRECT = false;
2232
2233            junit_mark_test_as('PASS', $shortname, $tested);
2234            return 'REDIR';
2235
2236        } else {
2237
2238            $bork_info = "Redirect info must contain exactly one TEST string to be used as redirect directory.";
2239            show_result("BORK", $bork_info, '', $temp_filenames);
2240            $PHP_FAILED_TESTS['BORKED'][] = array(
2241                'name' => $file,
2242                'test_name' => '',
2243                'output' => '',
2244                'diff' => '',
2245                'info' => "$bork_info [$file]",
2246            );
2247        }
2248    }
2249
2250    if (is_array($org_file) || isset($section_text['REDIRECTTEST'])) {
2251
2252        if (is_array($org_file)) {
2253            $file = $org_file[0];
2254        }
2255
2256        $bork_info = "Redirected test did not contain redirection info";
2257        show_result("BORK", $bork_info, '', $temp_filenames);
2258        $PHP_FAILED_TESTS['BORKED'][] = array(
2259            'name' => $file,
2260            'test_name' => '',
2261            'output' => '',
2262            'diff' => '',
2263            'info' => "$bork_info [$file]",
2264        );
2265
2266        junit_mark_test_as('BORK', $shortname, $tested, null, $bork_info);
2267
2268        return 'BORKED';
2269    }
2270
2271    // We've satisfied the preconditions - run the test!
2272    if (isset($section_text['FILE'])) {
2273        show_file_block('php', $section_text['FILE'], 'TEST');
2274        save_text($test_file, $section_text['FILE'], $temp_file);
2275    } else {
2276        $test_file = $temp_file = "";
2277    }
2278
2279    if (array_key_exists('GET', $section_text)) {
2280        $query_string = trim($section_text['GET']);
2281    } else {
2282        $query_string = '';
2283    }
2284
2285    $env['REDIRECT_STATUS'] = '1';
2286    if (empty($env['QUERY_STRING'])) {
2287        $env['QUERY_STRING'] = $query_string;
2288    }
2289    if (empty($env['PATH_TRANSLATED'])) {
2290        $env['PATH_TRANSLATED'] = $test_file;
2291    }
2292    if (empty($env['SCRIPT_FILENAME'])) {
2293        $env['SCRIPT_FILENAME'] = $test_file;
2294    }
2295
2296    if (array_key_exists('COOKIE', $section_text)) {
2297        $env['HTTP_COOKIE'] = trim($section_text['COOKIE']);
2298    } else {
2299        $env['HTTP_COOKIE'] = '';
2300    }
2301
2302    $args = isset($section_text['ARGS']) ? ' -- ' . $section_text['ARGS'] : '';
2303
2304    if ($preload && !empty($test_file)) {
2305        save_text($preload_filename, "<?php opcache_compile_file('$test_file');");
2306        $local_pass_options = $pass_options;
2307        unset($pass_options);
2308        $pass_options = $local_pass_options;
2309        $pass_options .= " -d opcache.preload=" . $preload_filename;
2310    }
2311
2312    if (array_key_exists('POST_RAW', $section_text) && !empty($section_text['POST_RAW'])) {
2313
2314        $post = trim($section_text['POST_RAW']);
2315        $raw_lines = explode("\n", $post);
2316
2317        $request = '';
2318        $started = false;
2319
2320        foreach ($raw_lines as $line) {
2321
2322            if (empty($env['CONTENT_TYPE']) && preg_match('/^Content-Type:(.*)/i', $line, $res)) {
2323                $env['CONTENT_TYPE'] = trim(str_replace("\r", '', $res[1]));
2324                continue;
2325            }
2326
2327            if ($started) {
2328                $request .= "\n";
2329            }
2330
2331            $started = true;
2332            $request .= $line;
2333        }
2334
2335        $env['CONTENT_LENGTH'] = strlen($request);
2336        $env['REQUEST_METHOD'] = 'POST';
2337
2338        if (empty($request)) {
2339            junit_mark_test_as('BORK', $shortname, $tested, null, 'empty $request');
2340            return 'BORKED';
2341        }
2342
2343        save_text($tmp_post, $request);
2344        $cmd = "$php $pass_options $ini_settings -f \"$test_file\"$cmdRedirect < \"$tmp_post\"";
2345
2346    } elseif (array_key_exists('PUT', $section_text) && !empty($section_text['PUT'])) {
2347
2348        $post = trim($section_text['PUT']);
2349        $raw_lines = explode("\n", $post);
2350
2351        $request = '';
2352        $started = false;
2353
2354        foreach ($raw_lines as $line) {
2355
2356            if (empty($env['CONTENT_TYPE']) && preg_match('/^Content-Type:(.*)/i', $line, $res)) {
2357                $env['CONTENT_TYPE'] = trim(str_replace("\r", '', $res[1]));
2358                continue;
2359            }
2360
2361            if ($started) {
2362                $request .= "\n";
2363            }
2364
2365            $started = true;
2366            $request .= $line;
2367        }
2368
2369        $env['CONTENT_LENGTH'] = strlen($request);
2370        $env['REQUEST_METHOD'] = 'PUT';
2371
2372        if (empty($request)) {
2373            junit_mark_test_as('BORK', $shortname, $tested, null, 'empty $request');
2374            return 'BORKED';
2375        }
2376
2377        save_text($tmp_post, $request);
2378        $cmd = "$php $pass_options $ini_settings -f \"$test_file\"$cmdRedirect < \"$tmp_post\"";
2379
2380    } else if (array_key_exists('POST', $section_text) && !empty($section_text['POST'])) {
2381
2382        $post = trim($section_text['POST']);
2383        $content_length = strlen($post);
2384        save_text($tmp_post, $post);
2385
2386        $env['REQUEST_METHOD'] = 'POST';
2387        if (empty($env['CONTENT_TYPE'])) {
2388            $env['CONTENT_TYPE'] = 'application/x-www-form-urlencoded';
2389        }
2390
2391        if (empty($env['CONTENT_LENGTH'])) {
2392            $env['CONTENT_LENGTH'] = $content_length;
2393        }
2394
2395        $cmd = "$php $pass_options $ini_settings -f \"$test_file\"$cmdRedirect < \"$tmp_post\"";
2396
2397    } else if (array_key_exists('GZIP_POST', $section_text) && !empty($section_text['GZIP_POST'])) {
2398
2399        $post = trim($section_text['GZIP_POST']);
2400        $post = gzencode($post, 9, FORCE_GZIP);
2401        $env['HTTP_CONTENT_ENCODING'] = 'gzip';
2402
2403        save_text($tmp_post, $post);
2404        $content_length = strlen($post);
2405
2406        $env['REQUEST_METHOD'] = 'POST';
2407        $env['CONTENT_TYPE'] = 'application/x-www-form-urlencoded';
2408        $env['CONTENT_LENGTH'] = $content_length;
2409
2410        $cmd = "$php $pass_options $ini_settings -f \"$test_file\"$cmdRedirect < \"$tmp_post\"";
2411
2412    } else if (array_key_exists('DEFLATE_POST', $section_text) && !empty($section_text['DEFLATE_POST'])) {
2413        $post = trim($section_text['DEFLATE_POST']);
2414        $post = gzcompress($post, 9);
2415        $env['HTTP_CONTENT_ENCODING'] = 'deflate';
2416        save_text($tmp_post, $post);
2417        $content_length = strlen($post);
2418
2419        $env['REQUEST_METHOD'] = 'POST';
2420        $env['CONTENT_TYPE'] = 'application/x-www-form-urlencoded';
2421        $env['CONTENT_LENGTH'] = $content_length;
2422
2423        $cmd = "$php $pass_options $ini_settings -f \"$test_file\"$cmdRedirect < \"$tmp_post\"";
2424
2425    } else {
2426
2427        $env['REQUEST_METHOD'] = 'GET';
2428        $env['CONTENT_TYPE'] = '';
2429        $env['CONTENT_LENGTH'] = '';
2430
2431        $cmd = "$php $pass_options $ini_settings -f \"$test_file\" $args$cmdRedirect";
2432    }
2433
2434    if ($valgrind) {
2435        $env['USE_ZEND_ALLOC'] = '0';
2436        $env['ZEND_DONT_UNLOAD_MODULES'] = 1;
2437
2438        $cmd = $valgrind->wrapCommand($cmd, $memcheck_filename, strpos($test_file, "pcre") !== false);
2439    }
2440
2441    if ($DETAILED) echo "
2442CONTENT_LENGTH  = " . $env['CONTENT_LENGTH'] . "
2443CONTENT_TYPE    = " . $env['CONTENT_TYPE'] . "
2444PATH_TRANSLATED = " . $env['PATH_TRANSLATED'] . "
2445QUERY_STRING    = " . $env['QUERY_STRING'] . "
2446REDIRECT_STATUS = " . $env['REDIRECT_STATUS'] . "
2447REQUEST_METHOD  = " . $env['REQUEST_METHOD'] . "
2448SCRIPT_FILENAME = " . $env['SCRIPT_FILENAME'] . "
2449HTTP_COOKIE     = " . $env['HTTP_COOKIE'] . "
2450COMMAND $cmd
2451";
2452
2453    junit_start_timer($shortname);
2454    $hrtime = hrtime();
2455    $startTime = $hrtime[0] * 1000000000 + $hrtime[1];
2456
2457    $out = system_with_timeout($cmd, $env, $section_text['STDIN'] ?? null, $captureStdIn, $captureStdOut, $captureStdErr);
2458
2459    junit_finish_timer($shortname);
2460    $hrtime = hrtime();
2461    $time = $hrtime[0] * 1000000000 + $hrtime[1] - $startTime;
2462    if ($time >= $slow_min_ms * 1000000) {
2463        $PHP_FAILED_TESTS['SLOW'][] = array(
2464            'name' => $file,
2465            'test_name' => (is_array($IN_REDIRECT) ? $IN_REDIRECT['via'] : '') . $tested . " [$tested_file]",
2466            'output' => '',
2467            'diff' => '',
2468            'info' => $time / 1000000000,
2469        );
2470    }
2471
2472    if (array_key_exists('CLEAN', $section_text) && (!$no_clean || $cfg['keep']['clean'])) {
2473
2474        if (trim($section_text['CLEAN'])) {
2475            show_file_block('clean', $section_text['CLEAN']);
2476            save_text($test_clean, trim($section_text['CLEAN']), $temp_clean);
2477
2478            if (!$no_clean) {
2479                $extra = substr(PHP_OS, 0, 3) !== "WIN" ?
2480                    "unset REQUEST_METHOD; unset QUERY_STRING; unset PATH_TRANSLATED; unset SCRIPT_FILENAME; unset REQUEST_METHOD;" : "";
2481                system_with_timeout("$extra $php $pass_options $extra_options -q $orig_ini_settings $no_file_cache \"$test_clean\"", $env);
2482            }
2483
2484            if (!$cfg['keep']['clean']) {
2485                @unlink($test_clean);
2486            }
2487        }
2488    }
2489
2490    @unlink($preload_filename);
2491
2492    $leaked = false;
2493    $passed = false;
2494
2495    if ($valgrind) { // leak check
2496        $leaked = filesize($memcheck_filename) > 0;
2497
2498        if (!$leaked) {
2499            @unlink($memcheck_filename);
2500        }
2501    }
2502
2503    // Does the output match what is expected?
2504    $output = preg_replace("/\r\n/", "\n", trim($out));
2505
2506    /* when using CGI, strip the headers from the output */
2507    $headers = array();
2508
2509    if (!empty($uses_cgi) && preg_match("/^(.*?)\r?\n\r?\n(.*)/s", $out, $match)) {
2510        $output = trim($match[2]);
2511        $rh = preg_split("/[\n\r]+/", $match[1]);
2512
2513        foreach ($rh as $line) {
2514            if (strpos($line, ':') !== false) {
2515                $line = explode(':', $line, 2);
2516                $headers[trim($line[0])] = trim($line[1]);
2517            }
2518        }
2519    }
2520
2521    $failed_headers = false;
2522
2523    if (isset($section_text['EXPECTHEADERS'])) {
2524        $want = array();
2525        $wanted_headers = array();
2526        $lines = preg_split("/[\n\r]+/", $section_text['EXPECTHEADERS']);
2527
2528        foreach ($lines as $line) {
2529            if (strpos($line, ':') !== false) {
2530                $line = explode(':', $line, 2);
2531                $want[trim($line[0])] = trim($line[1]);
2532                $wanted_headers[] = trim($line[0]) . ': ' . trim($line[1]);
2533            }
2534        }
2535
2536        $output_headers = array();
2537
2538        foreach ($want as $k => $v) {
2539
2540            if (isset($headers[$k])) {
2541                $output_headers[] = $k . ': ' . $headers[$k];
2542            }
2543
2544            if (!isset($headers[$k]) || $headers[$k] != $v) {
2545                $failed_headers = true;
2546            }
2547        }
2548
2549        ksort($wanted_headers);
2550        $wanted_headers = implode("\n", $wanted_headers);
2551        ksort($output_headers);
2552        $output_headers = implode("\n", $output_headers);
2553    }
2554
2555    show_file_block('out', $output);
2556
2557    if ($preload) {
2558        $output = trim(preg_replace("/\n?Warning: Can't preload [^\n]*\n?/", "", $output));
2559    }
2560
2561    if (isset($section_text['EXPECTF']) || isset($section_text['EXPECTREGEX'])) {
2562
2563        if (isset($section_text['EXPECTF'])) {
2564            $wanted = trim($section_text['EXPECTF']);
2565        } else {
2566            $wanted = trim($section_text['EXPECTREGEX']);
2567        }
2568
2569        show_file_block('exp', $wanted);
2570        $wanted_re = preg_replace('/\r\n/', "\n", $wanted);
2571
2572        if (isset($section_text['EXPECTF'])) {
2573
2574            // do preg_quote, but miss out any %r delimited sections
2575            $temp = "";
2576            $r = "%r";
2577            $startOffset = 0;
2578            $length = strlen($wanted_re);
2579            while ($startOffset < $length) {
2580                $start = strpos($wanted_re, $r, $startOffset);
2581                if ($start !== false) {
2582                    // we have found a start tag
2583                    $end = strpos($wanted_re, $r, $start + 2);
2584                    if ($end === false) {
2585                        // unbalanced tag, ignore it.
2586                        $end = $start = $length;
2587                    }
2588                } else {
2589                    // no more %r sections
2590                    $start = $end = $length;
2591                }
2592                // quote a non re portion of the string
2593                $temp .= preg_quote(substr($wanted_re, $startOffset, $start - $startOffset), '/');
2594                // add the re unquoted.
2595                if ($end > $start) {
2596                    $temp .= '(' . substr($wanted_re, $start + 2, $end - $start - 2) . ')';
2597                }
2598                $startOffset = $end + 2;
2599            }
2600            $wanted_re = $temp;
2601
2602            // Stick to basics
2603            $wanted_re = str_replace('%e', '\\' . DIRECTORY_SEPARATOR, $wanted_re);
2604            $wanted_re = str_replace('%s', '[^\r\n]+', $wanted_re);
2605            $wanted_re = str_replace('%S', '[^\r\n]*', $wanted_re);
2606            $wanted_re = str_replace('%a', '.+', $wanted_re);
2607            $wanted_re = str_replace('%A', '.*', $wanted_re);
2608            $wanted_re = str_replace('%w', '\s*', $wanted_re);
2609            $wanted_re = str_replace('%i', '[+-]?\d+', $wanted_re);
2610            $wanted_re = str_replace('%d', '\d+', $wanted_re);
2611            $wanted_re = str_replace('%x', '[0-9a-fA-F]+', $wanted_re);
2612            $wanted_re = str_replace('%f', '[+-]?\.?\d+\.?\d*(?:[Ee][+-]?\d+)?', $wanted_re);
2613            $wanted_re = str_replace('%c', '.', $wanted_re);
2614            // %f allows two points "-.0.0" but that is the best *simple* expression
2615        }
2616/* DEBUG YOUR REGEX HERE
2617        var_dump($wanted_re);
2618        print(str_repeat('=', 80) . "\n");
2619        var_dump($output);
2620         */
2621        if (preg_match("/^$wanted_re\$/s", $output)) {
2622            $passed = true;
2623            if (!$cfg['keep']['php']) {
2624                @unlink($test_file);
2625            }
2626            @unlink($tmp_post);
2627
2628            if (!$leaked && !$failed_headers) {
2629                if (isset($section_text['XFAIL'])) {
2630                    $warn = true;
2631                    $info = " (warn: XFAIL section but test passes)";
2632                } else if (isset($section_text['XLEAK'])) {
2633                    $warn = true;
2634                    $info = " (warn: XLEAK section but test passes)";
2635                } else {
2636                    show_result("PASS", $tested, $tested_file, '', $temp_filenames);
2637                    junit_mark_test_as('PASS', $shortname, $tested);
2638                    return 'PASSED';
2639                }
2640            }
2641        }
2642
2643    } else {
2644
2645        $wanted = trim($section_text['EXPECT']);
2646        $wanted = preg_replace('/\r\n/', "\n", $wanted);
2647        show_file_block('exp', $wanted);
2648
2649        // compare and leave on success
2650        if (!strcmp($output, $wanted)) {
2651            $passed = true;
2652
2653            if (!$cfg['keep']['php']) {
2654                @unlink($test_file);
2655            }
2656            @unlink($tmp_post);
2657
2658            if (!$leaked && !$failed_headers) {
2659                if (isset($section_text['XFAIL'])) {
2660                    $warn = true;
2661                    $info = " (warn: XFAIL section but test passes)";
2662                } elseif (isset($section_text['XLEAK'])) {
2663                    $warn = true;
2664                    $info = " (warn: XLEAK section but test passes)";
2665                } else {
2666                    show_result("PASS", $tested, $tested_file, '', $temp_filenames);
2667                    junit_mark_test_as('PASS', $shortname, $tested);
2668                    return 'PASSED';
2669                }
2670            }
2671        }
2672
2673        $wanted_re = null;
2674    }
2675
2676    // Test failed so we need to report details.
2677    if ($failed_headers) {
2678        $passed = false;
2679        $wanted = $wanted_headers . "\n--HEADERS--\n" . $wanted;
2680        $output = $output_headers . "\n--HEADERS--\n" . $output;
2681
2682        if (isset($wanted_re)) {
2683            $wanted_re = preg_quote($wanted_headers . "\n--HEADERS--\n", '/') . $wanted_re;
2684        }
2685    }
2686
2687    if ($leaked) {
2688        $restype[] = isset($section_text['XLEAK']) ?
2689                        'XLEAK' : 'LEAK';
2690    }
2691
2692    if ($warn) {
2693        $restype[] = 'WARN';
2694    }
2695
2696    if (!$passed) {
2697        if (isset($section_text['XFAIL'])) {
2698            $restype[] = 'XFAIL';
2699            $info = '  XFAIL REASON: ' . rtrim($section_text['XFAIL']);
2700        } else if (isset($section_text['XLEAK'])) {
2701            $restype[] = 'XLEAK';
2702            $info = '  XLEAK REASON: ' . rtrim($section_text['XLEAK']);
2703        } else {
2704            $restype[] = 'FAIL';
2705        }
2706    }
2707
2708    if (!$passed) {
2709
2710        // write .exp
2711        if (strpos($log_format, 'E') !== false && file_put_contents($exp_filename, $wanted, FILE_BINARY) === false) {
2712            error("Cannot create expected test output - $exp_filename");
2713        }
2714
2715        // write .out
2716        if (strpos($log_format, 'O') !== false && file_put_contents($output_filename, $output, FILE_BINARY) === false) {
2717            error("Cannot create test output - $output_filename");
2718        }
2719
2720        // write .diff
2721        $diff = generate_diff($wanted, $wanted_re, $output);
2722        if (is_array($IN_REDIRECT)) {
2723            $orig_shortname = str_replace(TEST_PHP_SRCDIR . '/', '', $file);
2724            $diff = "# original source file: $orig_shortname\n" . $diff;
2725        }
2726        show_file_block('diff', $diff);
2727        if (strpos($log_format, 'D') !== false && file_put_contents($diff_filename, $diff, FILE_BINARY) === false) {
2728            error("Cannot create test diff - $diff_filename");
2729        }
2730
2731        // write .sh
2732        if (strpos($log_format, 'S') !== false && file_put_contents($sh_filename, "#!/bin/sh
2733
2734{$cmd}
2735", FILE_BINARY) === false) {
2736            error("Cannot create test shell script - $sh_filename");
2737        }
2738        chmod($sh_filename, 0755);
2739
2740        // write .log
2741        if (strpos($log_format, 'L') !== false && file_put_contents($log_filename, "
2742---- EXPECTED OUTPUT
2743$wanted
2744---- ACTUAL OUTPUT
2745$output
2746---- FAILED
2747", FILE_BINARY) === false) {
2748            error("Cannot create test log - $log_filename");
2749            error_report($file, $log_filename, $tested);
2750        }
2751    }
2752
2753    if ($valgrind && $leaked && $cfg["show"]["mem"]) {
2754        show_file_block('mem', file_get_contents($memcheck_filename));
2755    }
2756
2757    show_result(implode('&', $restype), $tested, $tested_file, $info, $temp_filenames);
2758
2759    foreach ($restype as $type) {
2760        $PHP_FAILED_TESTS[$type . 'ED'][] = array(
2761            'name' => $file,
2762            'test_name' => (is_array($IN_REDIRECT) ? $IN_REDIRECT['via'] : '') . $tested . " [$tested_file]",
2763            'output' => $output_filename,
2764            'diff' => $diff_filename,
2765            'info' => $info,
2766        );
2767    }
2768
2769    $diff = empty($diff) ? '' : preg_replace('/\e/', '<esc>', $diff);
2770
2771    junit_mark_test_as($restype, $shortname, $tested, null, $info, $diff);
2772
2773    return $restype[0] . 'ED';
2774}
2775
2776function comp_line($l1, $l2, $is_reg)
2777{
2778    if ($is_reg) {
2779        return preg_match('/^' . $l1 . '$/s', $l2);
2780    } else {
2781        return !strcmp($l1, $l2);
2782    }
2783}
2784
2785function count_array_diff($ar1, $ar2, $is_reg, $w, $idx1, $idx2, $cnt1, $cnt2, $steps)
2786{
2787    $equal = 0;
2788
2789    while ($idx1 < $cnt1 && $idx2 < $cnt2 && comp_line($ar1[$idx1], $ar2[$idx2], $is_reg)) {
2790        $idx1++;
2791        $idx2++;
2792        $equal++;
2793        $steps--;
2794    }
2795    if (--$steps > 0) {
2796        $eq1 = 0;
2797        $st = $steps / 2;
2798
2799        for ($ofs1 = $idx1 + 1; $ofs1 < $cnt1 && $st-- > 0; $ofs1++) {
2800            $eq = @count_array_diff($ar1, $ar2, $is_reg, $w, $ofs1, $idx2, $cnt1, $cnt2, $st);
2801
2802            if ($eq > $eq1) {
2803                $eq1 = $eq;
2804            }
2805        }
2806
2807        $eq2 = 0;
2808        $st = $steps;
2809
2810        for ($ofs2 = $idx2 + 1; $ofs2 < $cnt2 && $st-- > 0; $ofs2++) {
2811            $eq = @count_array_diff($ar1, $ar2, $is_reg, $w, $idx1, $ofs2, $cnt1, $cnt2, $st);
2812            if ($eq > $eq2) {
2813                $eq2 = $eq;
2814            }
2815        }
2816
2817        if ($eq1 > $eq2) {
2818            $equal += $eq1;
2819        } else if ($eq2 > 0) {
2820            $equal += $eq2;
2821        }
2822    }
2823
2824    return $equal;
2825}
2826
2827function generate_array_diff($ar1, $ar2, $is_reg, $w)
2828{
2829    $idx1 = 0;
2830    $cnt1 = @count($ar1);
2831    $idx2 = 0;
2832    $cnt2 = @count($ar2);
2833    $diff = array();
2834    $old1 = array();
2835    $old2 = array();
2836
2837    while ($idx1 < $cnt1 && $idx2 < $cnt2) {
2838
2839        if (comp_line($ar1[$idx1], $ar2[$idx2], $is_reg)) {
2840            $idx1++;
2841            $idx2++;
2842            continue;
2843        } else {
2844
2845            $c1 = @count_array_diff($ar1, $ar2, $is_reg, $w, $idx1 + 1, $idx2, $cnt1, $cnt2, 10);
2846            $c2 = @count_array_diff($ar1, $ar2, $is_reg, $w, $idx1, $idx2 + 1, $cnt1, $cnt2, 10);
2847
2848            if ($c1 > $c2) {
2849                $old1[$idx1] = sprintf("%03d- ", $idx1 + 1) . $w[$idx1++];
2850            } else if ($c2 > 0) {
2851                $old2[$idx2] = sprintf("%03d+ ", $idx2 + 1) . $ar2[$idx2++];
2852            } else {
2853                $old1[$idx1] = sprintf("%03d- ", $idx1 + 1) . $w[$idx1++];
2854                $old2[$idx2] = sprintf("%03d+ ", $idx2 + 1) . $ar2[$idx2++];
2855            }
2856        }
2857    }
2858
2859    reset($old1);
2860    $k1 = key($old1);
2861    $l1 = -2;
2862    reset($old2);
2863    $k2 = key($old2);
2864    $l2 = -2;
2865
2866    while ($k1 !== null || $k2 !== null) {
2867
2868        if ($k1 == $l1 + 1 || $k2 === null) {
2869            $l1 = $k1;
2870            $diff[] = current($old1);
2871            $k1 = next($old1) ? key($old1) : null;
2872        } else if ($k2 == $l2 + 1 || $k1 === null) {
2873            $l2 = $k2;
2874            $diff[] = current($old2);
2875            $k2 = next($old2) ? key($old2) : null;
2876        } else if ($k1 < $k2) {
2877            $l1 = $k1;
2878            $diff[] = current($old1);
2879            $k1 = next($old1) ? key($old1) : null;
2880        } else {
2881            $l2 = $k2;
2882            $diff[] = current($old2);
2883            $k2 = next($old2) ? key($old2) : null;
2884        }
2885    }
2886
2887    while ($idx1 < $cnt1) {
2888        $diff[] = sprintf("%03d- ", $idx1 + 1) . $w[$idx1++];
2889    }
2890
2891    while ($idx2 < $cnt2) {
2892        $diff[] = sprintf("%03d+ ", $idx2 + 1) . $ar2[$idx2++];
2893    }
2894
2895    return $diff;
2896}
2897
2898function generate_diff($wanted, $wanted_re, $output)
2899{
2900    $w = explode("\n", $wanted);
2901    $o = explode("\n", $output);
2902    $r = is_null($wanted_re) ? $w : explode("\n", $wanted_re);
2903    $diff = generate_array_diff($r, $o, !is_null($wanted_re), $w);
2904
2905    return implode(PHP_EOL, $diff);
2906}
2907
2908function error($message)
2909{
2910    echo "ERROR: {$message}\n";
2911    exit(1);
2912}
2913
2914function settings2array($settings, &$ini_settings)
2915{
2916    foreach ($settings as $setting) {
2917
2918        if (strpos($setting, '=') !== false) {
2919            $setting = explode("=", $setting, 2);
2920            $name = trim($setting[0]);
2921            $value = trim($setting[1]);
2922
2923            if ($name == 'extension' || $name == 'zend_extension') {
2924
2925                if (!isset($ini_settings[$name])) {
2926                    $ini_settings[$name] = array();
2927                }
2928
2929                $ini_settings[$name][] = $value;
2930
2931            } else {
2932                $ini_settings[$name] = $value;
2933            }
2934        }
2935    }
2936}
2937
2938function settings2params($ini_settings)
2939{
2940    $settings = '';
2941
2942    foreach($ini_settings as $name => $value) {
2943
2944        if (is_array($value)) {
2945            foreach($value as $val) {
2946                $val = addslashes($val);
2947                $settings .= " -d \"$name=$val\"";
2948            }
2949        } else {
2950            if (substr(PHP_OS, 0, 3) == "WIN" && !empty($value) && $value[0] == '"') {
2951                $len = strlen($value);
2952
2953                if ($value[$len - 1] == '"') {
2954                    $value[0] = "'";
2955                    $value[$len - 1] = "'";
2956                }
2957            } else {
2958                $value = addslashes($value);
2959            }
2960
2961            $settings .= " -d \"$name=$value\"";
2962        }
2963    }
2964
2965    return $settings;
2966}
2967
2968function compute_summary()
2969{
2970    global $n_total, $test_results, $ignored_by_ext, $sum_results, $percent_results;
2971
2972    $n_total = count($test_results);
2973    $n_total += $ignored_by_ext;
2974    $sum_results = array(
2975        'PASSED' => 0,
2976        'WARNED' => 0,
2977        'SKIPPED' => 0,
2978        'FAILED' => 0,
2979        'BORKED' => 0,
2980        'LEAKED' => 0,
2981        'XFAILED' => 0,
2982        'XLEAKED' => 0
2983    );
2984
2985    foreach ($test_results as $v) {
2986        $sum_results[$v]++;
2987    }
2988
2989    $sum_results['SKIPPED'] += $ignored_by_ext;
2990    $percent_results = array();
2991
2992    foreach ($sum_results as $v => $n) {
2993        $percent_results[$v] = (100.0 * $n) / $n_total;
2994    }
2995}
2996
2997function get_summary($show_ext_summary, $show_html)
2998{
2999    global $exts_skipped, $exts_tested, $n_total, $sum_results, $percent_results, $end_time, $start_time, $failed_test_summary, $PHP_FAILED_TESTS, $valgrind;
3000
3001    $x_total = $n_total - $sum_results['SKIPPED'] - $sum_results['BORKED'];
3002
3003    if ($x_total) {
3004        $x_warned = (100.0 * $sum_results['WARNED']) / $x_total;
3005        $x_failed = (100.0 * $sum_results['FAILED']) / $x_total;
3006        $x_xfailed = (100.0 * $sum_results['XFAILED']) / $x_total;
3007        $x_xleaked = (100.0 * $sum_results['XLEAKED']) / $x_total;
3008        $x_leaked = (100.0 * $sum_results['LEAKED']) / $x_total;
3009        $x_passed = (100.0 * $sum_results['PASSED']) / $x_total;
3010    } else {
3011        $x_warned = $x_failed = $x_passed = $x_leaked = $x_xfailed = $x_xleaked = 0;
3012    }
3013
3014    $summary = '';
3015
3016    if ($show_html) {
3017        $summary .= "<pre>\n";
3018    }
3019
3020    if ($show_ext_summary) {
3021        $summary .= '
3022=====================================================================
3023TEST RESULT SUMMARY
3024---------------------------------------------------------------------
3025Exts skipped    : ' . sprintf('%4d', $exts_skipped) . '
3026Exts tested     : ' . sprintf('%4d', $exts_tested) . '
3027---------------------------------------------------------------------
3028';
3029    }
3030
3031    $summary .= '
3032Number of tests : ' . sprintf('%4d', $n_total) . '          ' . sprintf('%8d', $x_total);
3033
3034    if ($sum_results['BORKED']) {
3035        $summary .= '
3036Tests borked    : ' . sprintf('%4d (%5.1f%%)', $sum_results['BORKED'], $percent_results['BORKED']) . ' --------';
3037    }
3038
3039    $summary .= '
3040Tests skipped   : ' . sprintf('%4d (%5.1f%%)', $sum_results['SKIPPED'], $percent_results['SKIPPED']) . ' --------
3041Tests warned    : ' . sprintf('%4d (%5.1f%%)', $sum_results['WARNED'], $percent_results['WARNED']) . ' ' . sprintf('(%5.1f%%)', $x_warned) . '
3042Tests failed    : ' . sprintf('%4d (%5.1f%%)', $sum_results['FAILED'], $percent_results['FAILED']) . ' ' . sprintf('(%5.1f%%)', $x_failed);
3043
3044    if ($sum_results['XFAILED']) {
3045        $summary .= '
3046Expected fail   : ' . sprintf('%4d (%5.1f%%)', $sum_results['XFAILED'], $percent_results['XFAILED']) . ' ' . sprintf('(%5.1f%%)', $x_xfailed);
3047    }
3048
3049    if ($valgrind) {
3050        $summary .= '
3051Tests leaked    : ' . sprintf('%4d (%5.1f%%)', $sum_results['LEAKED'], $percent_results['LEAKED']) . ' ' . sprintf('(%5.1f%%)', $x_leaked);
3052        if ($sum_results['XLEAKED']) {
3053            $summary .= '
3054Expected leak   : ' . sprintf('%4d (%5.1f%%)', $sum_results['XLEAKED'], $percent_results['XLEAKED']) . ' ' . sprintf('(%5.1f%%)', $x_xleaked);
3055        }
3056    }
3057
3058    $summary .= '
3059Tests passed    : ' . sprintf('%4d (%5.1f%%)', $sum_results['PASSED'], $percent_results['PASSED']) . ' ' . sprintf('(%5.1f%%)', $x_passed) . '
3060---------------------------------------------------------------------
3061Time taken      : ' . sprintf('%4d seconds', $end_time - $start_time) . '
3062=====================================================================
3063';
3064    $failed_test_summary = '';
3065
3066    if (count($PHP_FAILED_TESTS['SLOW'])) {
3067        usort($PHP_FAILED_TESTS['SLOW'], function ($a, $b) {
3068            return $a['info'] < $b['info'] ? 1 : -1;
3069        });
3070
3071        $failed_test_summary .= '
3072=====================================================================
3073SLOW TEST SUMMARY
3074---------------------------------------------------------------------
3075';
3076        foreach ($PHP_FAILED_TESTS['SLOW'] as $failed_test_data) {
3077            $failed_test_summary .= sprintf('(%.3f s) ', $failed_test_data['info']) . $failed_test_data['test_name'] . "\n";
3078        }
3079        $failed_test_summary .= "=====================================================================\n";
3080    }
3081
3082    if (count($PHP_FAILED_TESTS['XFAILED'])) {
3083        $failed_test_summary .= '
3084=====================================================================
3085EXPECTED FAILED TEST SUMMARY
3086---------------------------------------------------------------------
3087';
3088        foreach ($PHP_FAILED_TESTS['XFAILED'] as $failed_test_data) {
3089            $failed_test_summary .= $failed_test_data['test_name'] . $failed_test_data['info'] . "\n";
3090        }
3091        $failed_test_summary .= "=====================================================================\n";
3092    }
3093
3094    if (count($PHP_FAILED_TESTS['BORKED'])) {
3095        $failed_test_summary .= '
3096=====================================================================
3097BORKED TEST SUMMARY
3098---------------------------------------------------------------------
3099';
3100        foreach ($PHP_FAILED_TESTS['BORKED'] as $failed_test_data) {
3101            $failed_test_summary .= $failed_test_data['info'] . "\n";
3102        }
3103
3104        $failed_test_summary .= "=====================================================================\n";
3105    }
3106
3107    if (count($PHP_FAILED_TESTS['FAILED'])) {
3108        $failed_test_summary .= '
3109=====================================================================
3110FAILED TEST SUMMARY
3111---------------------------------------------------------------------
3112';
3113        foreach ($PHP_FAILED_TESTS['FAILED'] as $failed_test_data) {
3114            $failed_test_summary .= $failed_test_data['test_name'] . $failed_test_data['info'] . "\n";
3115        }
3116        $failed_test_summary .= "=====================================================================\n";
3117    }
3118    if (count($PHP_FAILED_TESTS['WARNED'])) {
3119        $failed_test_summary .= '
3120=====================================================================
3121WARNED TEST SUMMARY
3122---------------------------------------------------------------------
3123';
3124        foreach ($PHP_FAILED_TESTS['WARNED'] as $failed_test_data) {
3125            $failed_test_summary .= $failed_test_data['test_name'] . $failed_test_data['info'] . "\n";
3126        }
3127
3128        $failed_test_summary .= "=====================================================================\n";
3129    }
3130
3131    if (count($PHP_FAILED_TESTS['LEAKED'])) {
3132        $failed_test_summary .= '
3133=====================================================================
3134LEAKED TEST SUMMARY
3135---------------------------------------------------------------------
3136';
3137        foreach ($PHP_FAILED_TESTS['LEAKED'] as $failed_test_data) {
3138            $failed_test_summary .= $failed_test_data['test_name'] . $failed_test_data['info'] . "\n";
3139        }
3140
3141        $failed_test_summary .= "=====================================================================\n";
3142    }
3143
3144    if (count($PHP_FAILED_TESTS['XLEAKED'])) {
3145        $failed_test_summary .= '
3146=====================================================================
3147EXPECTED LEAK TEST SUMMARY
3148---------------------------------------------------------------------
3149';
3150        foreach ($PHP_FAILED_TESTS['XLEAKED'] as $failed_test_data) {
3151            $failed_test_summary .= $failed_test_data['test_name'] . $failed_test_data['info'] . "\n";
3152        }
3153
3154        $failed_test_summary .= "=====================================================================\n";
3155    }
3156
3157    if ($failed_test_summary && !getenv('NO_PHPTEST_SUMMARY')) {
3158        $summary .= $failed_test_summary;
3159    }
3160
3161    if ($show_html) {
3162        $summary .= "</pre>";
3163    }
3164
3165    return $summary;
3166}
3167
3168function show_start($start_time)
3169{
3170    global $html_output, $html_file;
3171
3172    if ($html_output) {
3173        fwrite($html_file, "<h2>Time Start: " . date('Y-m-d H:i:s', $start_time) . "</h2>\n");
3174        fwrite($html_file, "<table>\n");
3175    }
3176
3177    echo "TIME START " . date('Y-m-d H:i:s', $start_time) . "\n=====================================================================\n";
3178}
3179
3180function show_end($end_time)
3181{
3182    global $html_output, $html_file;
3183
3184    if ($html_output) {
3185        fwrite($html_file, "</table>\n");
3186        fwrite($html_file, "<h2>Time End: " . date('Y-m-d H:i:s', $end_time) . "</h2>\n");
3187    }
3188
3189    echo "=====================================================================\nTIME END " . date('Y-m-d H:i:s', $end_time) . "\n";
3190}
3191
3192function show_summary()
3193{
3194    global $html_output, $html_file;
3195
3196    if ($html_output) {
3197        fwrite($html_file, "<hr/>\n" . get_summary(true, true));
3198    }
3199
3200    echo get_summary(true, false);
3201}
3202
3203function show_redirect_start($tests, $tested, $tested_file)
3204{
3205    global $html_output, $html_file, $line_length, $SHOW_ONLY_GROUPS;
3206
3207    if ($html_output) {
3208        fwrite($html_file, "<tr><td colspan='3'>---&gt; $tests ($tested [$tested_file]) begin</td></tr>\n");
3209    }
3210
3211    if (!$SHOW_ONLY_GROUPS || in_array('REDIRECT', $SHOW_ONLY_GROUPS)) {
3212        echo "REDIRECT $tests ($tested [$tested_file]) begin\n";
3213    } else {
3214        clear_show_test();
3215    }
3216}
3217
3218function show_redirect_ends($tests, $tested, $tested_file)
3219{
3220    global $html_output, $html_file, $line_length, $SHOW_ONLY_GROUPS;
3221
3222    if ($html_output) {
3223        fwrite($html_file, "<tr><td colspan='3'>---&gt; $tests ($tested [$tested_file]) done</td></tr>\n");
3224    }
3225
3226    if (!$SHOW_ONLY_GROUPS || in_array('REDIRECT', $SHOW_ONLY_GROUPS)) {
3227        echo "REDIRECT $tests ($tested [$tested_file]) done\n";
3228    } else {
3229        clear_show_test();
3230    }
3231}
3232
3233function show_test($test_idx, $shortname)
3234{
3235    global $test_cnt;
3236    global $line_length;
3237
3238    $str = "TEST $test_idx/$test_cnt [$shortname]\r";
3239    $line_length = strlen($str);
3240    echo $str;
3241    flush();
3242}
3243
3244function clear_show_test() {
3245    global $line_length;
3246    // Parallel testing
3247    global $workerID;
3248
3249    if (!$workerID) {
3250        // Write over the last line to avoid random trailing chars on next echo
3251        echo str_repeat(" ", $line_length), "\r";
3252    }
3253}
3254
3255function parse_conflicts(string $text) : array {
3256    // Strip comments
3257    $text = preg_replace('/#.*/', '', $text);
3258    return array_map('trim', explode("\n", trim($text)));
3259}
3260
3261function show_result($result, $tested, $tested_file, $extra = '', $temp_filenames = null)
3262{
3263    global $html_output, $html_file, $temp_target, $temp_urlbase, $line_length, $SHOW_ONLY_GROUPS;
3264
3265    if (!$SHOW_ONLY_GROUPS || in_array($result, $SHOW_ONLY_GROUPS)) {
3266        echo "$result $tested [$tested_file] $extra\n";
3267    } else if (!$SHOW_ONLY_GROUPS) {
3268        clear_show_test();
3269    }
3270
3271    if ($html_output) {
3272
3273        if (isset($temp_filenames['file']) && file_exists($temp_filenames['file'])) {
3274            $url = str_replace($temp_target, $temp_urlbase, $temp_filenames['file']);
3275            $tested = "<a href='$url'>$tested</a>";
3276        }
3277
3278        if (isset($temp_filenames['skip']) && file_exists($temp_filenames['skip'])) {
3279
3280            if (empty($extra)) {
3281                $extra = "skipif";
3282            }
3283
3284            $url = str_replace($temp_target, $temp_urlbase, $temp_filenames['skip']);
3285            $extra = "<a href='$url'>$extra</a>";
3286
3287        } else if (empty($extra)) {
3288            $extra = "&nbsp;";
3289        }
3290
3291        if (isset($temp_filenames['diff']) && file_exists($temp_filenames['diff'])) {
3292            $url = str_replace($temp_target, $temp_urlbase, $temp_filenames['diff']);
3293            $diff = "<a href='$url'>diff</a>";
3294        } else {
3295            $diff = "&nbsp;";
3296        }
3297
3298        if (isset($temp_filenames['mem']) && file_exists($temp_filenames['mem'])) {
3299            $url = str_replace($temp_target, $temp_urlbase, $temp_filenames['mem']);
3300            $mem = "<a href='$url'>leaks</a>";
3301        } else {
3302            $mem = "&nbsp;";
3303        }
3304
3305        fwrite(
3306            $html_file,
3307            "<tr>" .
3308                "<td>$result</td>" .
3309                "<td>$tested</td>" .
3310                "<td>$extra</td>" .
3311                "<td>$diff</td>" .
3312                "<td>$mem</td>" .
3313                "</tr>\n"
3314        );
3315    }
3316}
3317
3318function junit_init()
3319{
3320    // Check whether a junit log is wanted.
3321    global $workerID;
3322    $JUNIT = getenv('TEST_PHP_JUNIT');
3323    if (empty($JUNIT)) {
3324        $GLOBALS['JUNIT'] = false;
3325        return;
3326    }
3327    if ($workerID) {
3328        $fp = null;
3329    } else if (!$fp = fopen($JUNIT, 'w')) {
3330        error("Failed to open $JUNIT for writing.");
3331    }
3332    $GLOBALS['JUNIT'] = array(
3333        'fp' => $fp,
3334        'name' => 'PHP',
3335        'test_total' => 0,
3336        'test_pass' => 0,
3337        'test_fail' => 0,
3338        'test_error' => 0,
3339        'test_skip' => 0,
3340        'test_warn' => 0,
3341        'execution_time' => 0,
3342        'suites' => array(),
3343        'files' => array()
3344    );
3345}
3346
3347function junit_save_xml()
3348{
3349    global $JUNIT;
3350    if (!junit_enabled()) return;
3351
3352    $xml = '<' . '?' . 'xml version="1.0" encoding="UTF-8"' . '?' . '>' . PHP_EOL;
3353    $xml .= sprintf(
3354        '<testsuites name="%s" tests="%s" failures="%d" errors="%d" skip="%d" time="%s">' . PHP_EOL,
3355        $JUNIT['name'],
3356        $JUNIT['test_total'],
3357        $JUNIT['test_fail'],
3358        $JUNIT['test_error'],
3359        $JUNIT['test_skip'],
3360        $JUNIT['execution_time']
3361    );
3362    $xml .= junit_get_suite_xml();
3363    $xml .= '</testsuites>';
3364    fwrite($JUNIT['fp'], $xml);
3365}
3366
3367function junit_get_suite_xml($suite_name = '')
3368{
3369    global $JUNIT;
3370
3371    $result = "";
3372
3373    foreach ($JUNIT['suites'] as $suite_name => $suite) {
3374        $result .= sprintf(
3375            '<testsuite name="%s" tests="%s" failures="%d" errors="%d" skip="%d" time="%s">' . PHP_EOL,
3376            $suite['name'],
3377            $suite['test_total'],
3378            $suite['test_fail'],
3379            $suite['test_error'],
3380            $suite['test_skip'],
3381            $suite['execution_time']
3382        );
3383
3384        if (!empty($suite_name)) {
3385            foreach ($suite['files'] as $file) {
3386                $result .= $JUNIT['files'][$file]['xml'];
3387            }
3388        }
3389
3390        $result .= '</testsuite>' . PHP_EOL;
3391    }
3392
3393    return $result;
3394}
3395
3396function junit_enabled()
3397{
3398    global $JUNIT;
3399    return !empty($JUNIT);
3400}
3401
3402/**
3403 * @param array|string $type
3404 * @param string $file_name
3405 * @param string $test_name
3406 * @param int|string $time
3407 * @param string $message
3408 * @param string $details
3409 * @return void
3410 */
3411function junit_mark_test_as($type, $file_name, $test_name, $time = null, $message = '', $details = '')
3412{
3413    global $JUNIT;
3414    if (!junit_enabled()) return;
3415
3416    $suite = junit_get_suitename_for($file_name);
3417
3418    junit_suite_record($suite, 'test_total');
3419
3420    $time = $time ?? junit_get_timer($file_name);
3421    junit_suite_record($suite, 'execution_time', $time);
3422
3423    $escaped_details = htmlspecialchars($details, ENT_QUOTES, 'UTF-8');
3424    $escaped_details = preg_replace_callback('/[\0-\x08\x0B\x0C\x0E-\x1F]/', function ($c) {
3425        return sprintf('[[0x%02x]]', ord($c[0]));
3426    }, $escaped_details);
3427    $escaped_message = htmlspecialchars($message, ENT_QUOTES, 'UTF-8');
3428
3429    $escaped_test_name = htmlspecialchars($file_name . ' (' . $test_name . ')', ENT_QUOTES);
3430    $JUNIT['files'][$file_name]['xml'] = "<testcase name='$escaped_test_name' time='$time'>\n";
3431
3432    if (is_array($type)) {
3433        $output_type = $type[0] . 'ED';
3434        $temp = array_intersect(array('XFAIL', 'XLEAK', 'FAIL', 'WARN'), $type);
3435        $type = reset($temp);
3436    } else {
3437        $output_type = $type . 'ED';
3438    }
3439
3440    if ('PASS' == $type || 'XFAIL' == $type || 'XLEAK' == $type) {
3441        junit_suite_record($suite, 'test_pass');
3442    } elseif ('BORK' == $type) {
3443        junit_suite_record($suite, 'test_error');
3444        $JUNIT['files'][$file_name]['xml'] .= "<error type='$output_type' message='$escaped_message'/>\n";
3445    } elseif ('SKIP' == $type) {
3446        junit_suite_record($suite, 'test_skip');
3447        $JUNIT['files'][$file_name]['xml'] .= "<skipped>$escaped_message</skipped>\n";
3448    } elseif ('WARN' == $type) {
3449        junit_suite_record($suite, 'test_warn');
3450        $JUNIT['files'][$file_name]['xml'] .= "<warning>$escaped_message</warning>\n";
3451    } elseif ('FAIL' == $type) {
3452        junit_suite_record($suite, 'test_fail');
3453        $JUNIT['files'][$file_name]['xml'] .= "<failure type='$output_type' message='$escaped_message'>$escaped_details</failure>\n";
3454    } else {
3455        junit_suite_record($suite, 'test_error');
3456        $JUNIT['files'][$file_name]['xml'] .= "<error type='$output_type' message='$escaped_message'>$escaped_details</error>\n";
3457    }
3458
3459    $JUNIT['files'][$file_name]['xml'] .= "</testcase>\n";
3460
3461}
3462
3463function junit_suite_record($suite, $param, $value = 1)
3464{
3465    global $JUNIT;
3466
3467    $JUNIT[$param] += $value;
3468    $JUNIT['suites'][$suite][$param] += $value;
3469}
3470
3471function junit_get_timer($file_name)
3472{
3473    global $JUNIT;
3474    if (!junit_enabled()) return 0;
3475
3476    if (isset($JUNIT['files'][$file_name]['total'])) {
3477        return number_format($JUNIT['files'][$file_name]['total'], 4);
3478    }
3479
3480    return 0;
3481}
3482
3483function junit_start_timer($file_name)
3484{
3485    global $JUNIT;
3486    if (!junit_enabled()) return;
3487
3488    if (!isset($JUNIT['files'][$file_name]['start'])) {
3489        $JUNIT['files'][$file_name]['start'] = microtime(true);
3490
3491        $suite = junit_get_suitename_for($file_name);
3492        junit_init_suite($suite);
3493        $JUNIT['suites'][$suite]['files'][$file_name] = $file_name;
3494    }
3495}
3496
3497function junit_get_suitename_for($file_name)
3498{
3499    return junit_path_to_classname(dirname($file_name));
3500}
3501
3502function junit_path_to_classname($file_name)
3503{
3504    global $JUNIT;
3505
3506    if (!junit_enabled()) return '';
3507
3508    $ret = $JUNIT['name'];
3509    $_tmp = array();
3510
3511    // lookup whether we're in the PHP source checkout
3512    $max = 5;
3513    if (is_file($file_name)) {
3514        $dir = dirname(realpath($file_name));
3515    } else {
3516        $dir = realpath($file_name);
3517    }
3518    do {
3519        array_unshift($_tmp, basename($dir));
3520        $chk = $dir . DIRECTORY_SEPARATOR . "main" . DIRECTORY_SEPARATOR . "php_version.h";
3521        $dir = dirname($dir);
3522    } while (!file_exists($chk) && --$max > 0);
3523    if (file_exists($chk)) {
3524        if ($max) {
3525            array_shift($_tmp);
3526        }
3527        foreach ($_tmp as $p) {
3528            $ret .= "." . preg_replace(",[^a-z0-9]+,i", ".", $p);
3529        }
3530        return $ret;
3531    }
3532
3533    return $JUNIT['name'] . '.' . str_replace(array(DIRECTORY_SEPARATOR, '-'), '.', $file_name);
3534}
3535
3536function junit_init_suite($suite_name)
3537{
3538    global $JUNIT;
3539    if (!junit_enabled()) return;
3540
3541    if (!empty($JUNIT['suites'][$suite_name])) {
3542        return;
3543    }
3544
3545    $JUNIT['suites'][$suite_name] = array(
3546        'name' => $suite_name,
3547        'test_total' => 0,
3548        'test_pass' => 0,
3549        'test_fail' => 0,
3550        'test_error' => 0,
3551        'test_skip' => 0,
3552        'test_warn' => 0,
3553        'files' => array(),
3554        'execution_time' => 0,
3555    );
3556}
3557
3558function junit_finish_timer($file_name)
3559{
3560    global $JUNIT;
3561    if (!junit_enabled()) return;
3562
3563    if (!isset($JUNIT['files'][$file_name]['start'])) {
3564        error("Timer for $file_name was not started!");
3565    }
3566
3567    if (!isset($JUNIT['files'][$file_name]['total'])) {
3568        $JUNIT['files'][$file_name]['total'] = 0;
3569    }
3570
3571    $start = $JUNIT['files'][$file_name]['start'];
3572    $JUNIT['files'][$file_name]['total'] += microtime(true) - $start;
3573    unset($JUNIT['files'][$file_name]['start']);
3574}
3575
3576function junit_merge_results($junit)
3577{
3578    global $JUNIT;
3579    $JUNIT['test_total'] += $junit['test_total'];
3580    $JUNIT['test_pass']  += $junit['test_pass'];
3581    $JUNIT['test_fail']  += $junit['test_fail'];
3582    $JUNIT['test_error'] += $junit['test_error'];
3583    $JUNIT['test_skip']  += $junit['test_skip'];
3584    $JUNIT['test_warn']  += $junit['test_warn'];
3585    $JUNIT['execution_time'] += $junit['execution_time'];
3586    $JUNIT['files'] += $junit['files'];
3587    foreach ($junit['suites'] as $name => $suite) {
3588        if (!isset($JUNIT['suites'][$name])) {
3589            $JUNIT['suites'][$name] = $suite;
3590            continue;
3591        }
3592
3593        $SUITE =& $JUNIT['suites'][$name];
3594        $SUITE['test_total'] += $suite['test_total'];
3595        $SUITE['test_pass']  += $suite['test_pass'];
3596        $SUITE['test_fail']  += $suite['test_fail'];
3597        $SUITE['test_error'] += $suite['test_error'];
3598        $SUITE['test_skip']  += $suite['test_skip'];
3599        $SUITE['test_warn']  += $suite['test_warn'];
3600        $SUITE['execution_time'] += $suite['execution_time'];
3601        $SUITE['files'] += $suite['files'];
3602    }
3603}
3604
3605class RuntestsValgrind
3606{
3607    protected $version = '';
3608    protected $header = '';
3609    protected $version_3_3_0 = false;
3610    protected $version_3_8_0 = false;
3611    protected $tool = null;
3612
3613    public function getVersion()
3614    {
3615        return $this->version;
3616    }
3617
3618    public function getHeader()
3619    {
3620        return $this->header;
3621    }
3622
3623    public function __construct(array $environment, string $tool = 'memcheck')
3624    {
3625        $this->tool = $tool;
3626        $header = system_with_timeout("valgrind --tool={$this->tool} --version", $environment);
3627        if (!$header) {
3628            error("Valgrind returned no version info for {$this->tool}, cannot proceed.\n".
3629                  "Please check if Valgrind is installed and the tool is named correctly.");
3630        }
3631        $count = 0;
3632        $version = preg_replace("/valgrind-(\d+)\.(\d+)\.(\d+)([.\w_-]+)?(\s+)/", '$1.$2.$3', $header, 1, $count);
3633        if ($count != 1) {
3634            error("Valgrind returned invalid version info (\"{$header}\") for {$this->tool}, cannot proceed.");
3635        }
3636        $this->version = $version;
3637        $this->header = sprintf(
3638            "%s (%s)", trim($header), $this->tool);
3639        $this->version_3_3_0 = version_compare($version, '3.3.0', '>=');
3640        $this->version_3_8_0 = version_compare($version, '3.8.0', '>=');
3641    }
3642
3643    public function wrapCommand($cmd, $memcheck_filename, $check_all)
3644    {
3645        $vcmd = "valgrind -q --tool={$this->tool} --trace-children=yes";
3646        if ($check_all) {
3647            $vcmd .= ' --smc-check=all';
3648        }
3649
3650        /* --vex-iropt-register-updates=allregs-at-mem-access is necessary for phpdbg watchpoint tests */
3651        if ($this->version_3_8_0) {
3652            /* valgrind 3.3.0+ doesn't have --log-file-exactly option */
3653            return "$vcmd --vex-iropt-register-updates=allregs-at-mem-access --log-file=$memcheck_filename $cmd";
3654        } elseif ($this->version_3_3_0) {
3655            return "$vcmd --vex-iropt-precise-memory-exns=yes --log-file=$memcheck_filename $cmd";
3656        } else {
3657            return "$vcmd --vex-iropt-precise-memory-exns=yes --log-file-exactly=$memcheck_filename $cmd";
3658        }
3659    }
3660}
3661
3662main();
3663