diff --git a/.appveyor.yml b/.appveyor.yml index 39d84466c00a..afd5ee88795c 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -31,9 +31,18 @@ environment: matrix: - THREAD_SAFE: 0 OPCACHE: 0 + PARALLEL: + - THREAD_SAFE: 0 + OPCACHE: 0 + PARALLEL: -j2 + - THREAD_SAFE: 1 + OPCACHE: 1 + INTRINSICS: AVX + PARALLEL: - THREAD_SAFE: 1 OPCACHE: 1 INTRINSICS: AVX + PARALLEL: -j2 services: # the setup scripts have to be touched, once some other db version is used diff --git a/.travis.yml b/.travis.yml index 64f4d5a3c933..e645543ca7b6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -44,8 +44,10 @@ env: - PDO_MYSQL_TEST_HOST=127.0.0.1 - REPORT_EXIT_STATUS=1 matrix: - - ENABLE_MAINTAINER_ZTS=0 ENABLE_DEBUG=0 - - ENABLE_MAINTAINER_ZTS=1 ENABLE_DEBUG=1 + - ENABLE_MAINTAINER_ZTS=0 ENABLE_DEBUG=0 PARALLEL_TESTS=0 + - ENABLE_MAINTAINER_ZTS=0 ENABLE_DEBUG=0 PARALLEL_TESTS=2 + - ENABLE_MAINTAINER_ZTS=1 ENABLE_DEBUG=1 PARALLEL_TESTS=0 + - ENABLE_MAINTAINER_ZTS=1 ENABLE_DEBUG=1 PARALLEL_TESTS=2 before_script: - ccache --version @@ -64,7 +66,7 @@ before_script: # Run PHPs run-tests.php script: - - ./sapi/cli/php run-tests.php -p `pwd`/sapi/cli/php $(if [ $ENABLE_DEBUG == 0 ]; then echo "-d opcache.enable_cli=1 -d zend_extension=`pwd`/modules/opcache.so"; fi) -g "FAIL,XFAIL,BORK,WARN,LEAK,SKIP" --offline --show-diff --show-slow 1000 --set-timeout 120 + - ./sapi/cli/php run-tests.php $(if [ $PARALLEL_TESTS -gt 0 ]; then echo "-j$PARALLEL_TESTS"; fi) -p `pwd`/sapi/cli/php $(if [ $ENABLE_DEBUG == 0 ]; then echo "-d opcache.enable_cli=1 -d zend_extension=`pwd`/modules/opcache.so"; fi) -g "FAIL,XFAIL,BORK,WARN,LEAK,SKIP" --offline --show-diff --show-slow 1000 --set-timeout 120 after_success: - ccache --show-stats diff --git a/Zend/tests/!CAN_BE_PARALLELISED b/Zend/tests/!CAN_BE_PARALLELISED new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/Zend/tests/traits/!CAN_BE_PARALLELISED b/Zend/tests/traits/!CAN_BE_PARALLELISED new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/appveyor/test_task.bat b/appveyor/test_task.bat index 49a0b2dcfeaa..80a76c4a70e1 100644 --- a/appveyor/test_task.bat +++ b/appveyor/test_task.bat @@ -91,7 +91,7 @@ mkdir c:\tests_tmp set TEST_PHP_JUNIT=c:\junit.out.xml cd "%APPVEYOR_BUILD_FOLDER%" -nmake test TESTS="%OPCACHE_OPTS% -q --offline --show-diff --show-slow 1000 --set-timeout 120 -g FAIL,XFAIL,BORK,WARN,LEAK,SKIP --temp-source c:\tests_tmp --temp-target c:\tests_tmp" +nmake test TESTS="%OPCACHE_OPTS% %PARALLEL% -q --offline --show-diff --show-slow 1000 --set-timeout 120 -g FAIL,XFAIL,BORK,WARN,LEAK,SKIP --temp-source c:\tests_tmp --temp-target c:\tests_tmp" set EXIT_CODE=%errorlevel% diff --git a/ext/date/tests/!CAN_BE_PARALLELISED b/ext/date/tests/!CAN_BE_PARALLELISED new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/ext/dom/tests/!CAN_BE_PARALLELISED b/ext/dom/tests/!CAN_BE_PARALLELISED new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/ext/pcre/tests/!CAN_BE_PARALLELISED b/ext/pcre/tests/!CAN_BE_PARALLELISED new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/ext/reflection/tests/!CAN_BE_PARALLELISED b/ext/reflection/tests/!CAN_BE_PARALLELISED new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/ext/standard/tests/math/!CAN_BE_PARALLELISED b/ext/standard/tests/math/!CAN_BE_PARALLELISED new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/ext/standard/tests/serialize/!CAN_BE_PARALLELISED b/ext/standard/tests/serialize/!CAN_BE_PARALLELISED new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/ext/standard/tests/strings/!CAN_BE_PARALLELISED b/ext/standard/tests/strings/!CAN_BE_PARALLELISED new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/run-tests.php b/run-tests.php index e7f908c2e88e..269a5e4e635d 100644 --- a/run-tests.php +++ b/run-tests.php @@ -19,6 +19,7 @@ | Marcus Boerger | | Derick Rethans | | Sander Roobol | + | Andrea Faulds | | (based on version by: Stig Bakken ) | | (based on the PHP 3 test framework by Rasmus Lerdorf) | +----------------------------------------------------------------------+ @@ -26,22 +27,57 @@ /* $Id$ */ -define('INIT_DIR', getcwd()); +if (!function_exists("hrtime")) { + function hrtime(bool $as_num = false) + { + $t = microtime(true); + + if ($as_num) { + return $t*1000000000; + } -// change into the PHP source directory. -if (getenv('TEST_PHP_SRCDIR')) { - @chdir(getenv('TEST_PHP_SRCDIR')); + $s = floor($t); + return array(0 => $s, 1 => ($t - $s)*1000000000); + } } -define('TEST_PHP_SRCDIR', getcwd()); +run_tests(); + +function run_tests() { + global + $argc, $argv, + $php, $php_cgi, $phpdbg, $user_tests, $ini_overwrites, $pass_options, + $exts_to_test, $valgrind, $no_file_cache, $sum_results, + $just_save_results, $PHP_FAILED_TESTS, $output_file, $test_files, + $ignored_by_ext, $exts_skipped, $DETAILED, $test_results, + $failed_tests_file, $result_tests_file, $test_idx, $cfg, $log_format, + $test_cnt, $temp_source, $temp_target, $environment, $no_clean, + $SHOW_ONLY_GROUPS, $slow_min_ms, $exts_tested, $end_time, $start_time, + $html_output, $html_file, $temp_urlbase, $workers, $workerID; + + $workerID = 0; + if (getenv("TEST_PHP_WORKER")) { + $workerID = intval(getenv("TEST_PHP_WORKER"), 10); + run_worker(); + die; + } + + define('INIT_DIR', getcwd()); + + // change into the PHP source directory. + if (getenv('TEST_PHP_SRCDIR')) { + @chdir(getenv('TEST_PHP_SRCDIR')); + } + define('TEST_PHP_SRCDIR', getcwd()); -/* Sanity check to ensure that pcre extension needed by this script is available. - * In the event it is not, print a nice error message indicating that this script will - * not run without it. - */ -if (!extension_loaded('pcre')) { - echo << $s, 1 => ($t - $s)*1000000000); + $environment['TEST_PHPDBG_EXECUTABLE'] = $phpdbg; } -} -function verify_config() -{ - global $php; - - if (empty($php) || !file_exists($php)) { - error('environment variable TEST_PHP_EXECUTABLE must be set to specify PHP executable!'); + if (getenv('TEST_PHP_LOG_FORMAT')) { + $log_format = strtoupper(getenv('TEST_PHP_LOG_FORMAT')); + } else { + $log_format = 'LEODS'; } - if (function_exists('is_executable') && !is_executable($php)) { - error("invalid PHP executable specified by TEST_PHP_EXECUTABLE = $php"); + // Check whether a detailed log is wanted. + if (getenv('TEST_PHP_DETAILED')) { + $DETAILED = getenv('TEST_PHP_DETAILED'); + } else { + $DETAILED = 0; } -} - -if (getenv('TEST_PHP_LOG_FORMAT')) { - $log_format = strtoupper(getenv('TEST_PHP_LOG_FORMAT')); -} else { - $log_format = 'LEODS'; -} - -// Check whether a detailed log is wanted. -if (getenv('TEST_PHP_DETAILED')) { - $DETAILED = getenv('TEST_PHP_DETAILED'); -} else { - $DETAILED = 0; -} -junit_init(); + junit_init(); -if (getenv('SHOW_ONLY_GROUPS')) { - $SHOW_ONLY_GROUPS = explode(",", getenv('SHOW_ONLY_GROUPS')); -} else { - $SHOW_ONLY_GROUPS = array(); -} + if (getenv('SHOW_ONLY_GROUPS')) { + $SHOW_ONLY_GROUPS = explode(",", getenv('SHOW_ONLY_GROUPS')); + } else { + $SHOW_ONLY_GROUPS = array(); + } -// Check whether user test dirs are requested. -if (getenv('TEST_PHP_USER')) { - $user_tests = explode (',', getenv('TEST_PHP_USER')); -} else { - $user_tests = array(); -} + // Check whether user test dirs are requested. + if (getenv('TEST_PHP_USER')) { + $user_tests = explode (',', getenv('TEST_PHP_USER')); + } else { + $user_tests = array(); + } -$exts_to_test = array(); -$ini_overwrites = array( + $exts_to_test = array(); + $ini_overwrites = array( 'output_handler=', 'open_basedir=', 'disable_functions=', @@ -267,447 +276,250 @@ function verify_config() 'zend.assertions=1', ); -$no_file_cache = '-d opcache.file_cache= -d opcache.file_cache_only=0'; + $no_file_cache = '-d opcache.file_cache= -d opcache.file_cache_only=0'; -function write_information() -{ - global $php, $php_cgi, $phpdbg, $php_info, $user_tests, $ini_overwrites, $pass_options, $exts_to_test, $valgrind, $no_file_cache; + define('PHP_QA_EMAIL', 'qa-reports@lists.php.net'); + define('QA_SUBMISSION_PAGE', 'http://qa.php.net/buildtest-process.php'); + define('QA_REPORTS_PAGE', 'http://qa.php.net/reports'); + define('TRAVIS_CI' , (bool) getenv('TRAVIS')); - // Get info from php - $info_file = __DIR__ . '/run-test-info.php'; - @unlink($info_file); - $php_info = ''; - save_text($info_file, $php_info); - $info_params = array(); - settings2array($ini_overwrites, $info_params); - settings2params($info_params); - $php_info = `$php $pass_options $info_params $no_file_cache "$info_file"`; - define('TESTED_PHP_VERSION', `$php -n -r "echo PHP_VERSION;"`); + // Determine the tests to be run. - if ($php_cgi && $php != $php_cgi) { - $php_info_cgi = `$php_cgi $pass_options $info_params $no_file_cache -q "$info_file"`; - $php_info_sep = "\n---------------------------------------------------------------------"; - $php_cgi_info = "$php_info_sep\nPHP : $php_cgi $php_info_cgi$php_info_sep"; - } else { - $php_cgi_info = ''; - } + $test_files = array(); + $redir_tests = array(); + $test_results = array(); + $PHP_FAILED_TESTS = array('BORKED' => array(), 'FAILED' => array(), 'WARNED' => array(), 'LEAKED' => array(), 'XFAILED' => array(), 'SLOW' => array()); - if ($phpdbg) { - $phpdbg_info = `$phpdbg $pass_options $info_params $no_file_cache -qrr "$info_file"`; - $php_info_sep = "\n---------------------------------------------------------------------"; - $phpdbg_info = "$php_info_sep\nPHP : $phpdbg $phpdbg_info$php_info_sep"; - } else { - $phpdbg_info = ''; - } + // If parameters given assume they represent selected tests to run. + $result_tests_file= false; + $failed_tests_file= false; + $pass_option_n = false; + $pass_options = ''; - if (function_exists('opcache_invalidate')) { - opcache_invalidate($info_file, true); - } - @unlink($info_file); + $output_file = INIT_DIR . '/php_test_results_' . date('Ymd_Hi') . '.txt'; - // load list of enabled extensions - save_text($info_file, ''); - $exts_to_test = explode(',',`$php $pass_options $info_params $no_file_cache "$info_file"`); - // check for extensions that need special handling and regenerate - $info_params_ex = array( - 'session' => array('session.auto_start=0'), - 'tidy' => array('tidy.clean_output=0'), - 'zlib' => array('zlib.output_compression=Off'), - 'xdebug' => array('xdebug.default_enable=0'), - 'mbstring' => array('mbstring.func_overload=0'), - ); + $just_save_results = false; + $valgrind = null; + $html_output = false; + $html_file = null; + $temp_source = null; + $temp_target = null; + $temp_urlbase = null; + $conf_passed = null; + $no_clean = false; + $slow_min_ms = INF; + $workers = null; - foreach($info_params_ex as $ext => $ini_overwrites_ex) { - if (in_array($ext, $exts_to_test)) { - $ini_overwrites = array_merge($ini_overwrites, $ini_overwrites_ex); - } - } + $cfgtypes = array('show', 'keep'); + $cfgfiles = array('skip', 'php', 'clean', 'out', 'diff', 'exp', 'mem'); + $cfg = array(); - if (function_exists('opcache_invalidate')) { - opcache_invalidate($info_file, true); - } - @unlink($info_file); + foreach($cfgtypes as $type) { + $cfg[$type] = array(); - // Write test context information. - echo " -===================================================================== -PHP : $php $php_info $php_cgi_info $phpdbg_info -CWD : ".TEST_PHP_SRCDIR." -Extra dirs : "; - foreach ($user_tests as $test_dir) { - echo "{$test_dir}\n "; + foreach($cfgfiles as $file) { + $cfg[$type][$file] = false; + } } - echo " -VALGRIND : " . ($valgrind ? $valgrind->getHeader() : 'Not used') . " -===================================================================== -"; -} -define('PHP_QA_EMAIL', 'qa-reports@lists.php.net'); -define('QA_SUBMISSION_PAGE', 'http://qa.php.net/buildtest-process.php'); -define('QA_REPORTS_PAGE', 'http://qa.php.net/reports'); -define('TRAVIS_CI' , (bool) getenv('TRAVIS')); - -function save_or_mail_results() -{ - global $sum_results, $just_save_results, $failed_test_summary, - $PHP_FAILED_TESTS, $php, $output_file; + if (getenv('TEST_PHP_ARGS')) { - /* We got failed Tests, offer the user to send an e-mail to QA team, unless NO_INTERACTION is set */ - if (!getenv('NO_INTERACTION') && !TRAVIS_CI) { - $fp = fopen("php://stdin", "r+"); - if ($sum_results['FAILED'] || $sum_results['BORKED'] || $sum_results['WARNED'] || $sum_results['LEAKED']) { - echo "\nYou may have found a problem in PHP."; + if (!isset($argc, $argv) || !$argc) { + $argv = array(__FILE__); } - echo "\nThis report can be automatically sent to the PHP QA team at\n"; - echo QA_REPORTS_PAGE . " and http://news.php.net/php.qa.reports\n"; - echo "This gives us a better understanding of PHP's behavior.\n"; - echo "If you don't want to send the report immediately you can choose\n"; - echo "option \"s\" to save it. You can then email it to ". PHP_QA_EMAIL . " later.\n"; - echo "Do you want to send this report now? [Yns]: "; - flush(); - $user_input = fgets($fp, 10); - $just_save_results = (strtolower($user_input[0]) == 's'); + $argv = array_merge($argv, explode(' ', getenv('TEST_PHP_ARGS'))); + $argc = count($argv); } - if ($just_save_results || !getenv('NO_INTERACTION') || TRAVIS_CI) { - if ($just_save_results || TRAVIS_CI || strlen(trim($user_input)) == 0 || strtolower($user_input[0]) == 'y') { - /* - * Collect information about the host system for our report - * Fetch phpinfo() output so that we can see the PHP environment - * Make an archive of all the failed tests - * Send an email - */ - if ($just_save_results) { - $user_input = 's'; - } + if (isset($argc) && $argc > 1) { - /* Ask the user to provide an email address, so that QA team can contact the user */ - if (TRAVIS_CI) { - $user_email = 'travis at php dot net'; - } elseif (!strncasecmp($user_input, 'y', 1) || strlen(trim($user_input)) == 0) { - 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): "; - flush(); - $user_email = trim(fgets($fp, 1024)); - $user_email = str_replace("@", " at ", str_replace(".", " dot ", $user_email)); - } + for ($i=1; $i<$argc; $i++) { + $is_switch = false; + $switch = substr($argv[$i],1,1); + $repeat = substr($argv[$i],0,1) == '-'; - $failed_tests_data = ''; - $sep = "\n" . str_repeat('=', 80) . "\n"; - $failed_tests_data .= $failed_test_summary . "\n"; - $failed_tests_data .= get_summary(true, false) . "\n"; + while ($repeat) { - if ($sum_results['FAILED']) { - foreach ($PHP_FAILED_TESTS['FAILED'] as $test_info) { - $failed_tests_data .= $sep . $test_info['name'] . $test_info['info']; - $failed_tests_data .= $sep . file_get_contents(realpath($test_info['output']), FILE_BINARY); - $failed_tests_data .= $sep . file_get_contents(realpath($test_info['diff']), FILE_BINARY); - $failed_tests_data .= $sep . "\n\n"; + if (!$is_switch) { + $switch = substr($argv[$i],1,1); } - $status = "failed"; - } else { - $status = "success"; - } - $failed_tests_data .= "\n" . $sep . 'BUILD ENVIRONMENT' . $sep; - $failed_tests_data .= "OS:\n" . PHP_OS . " - " . php_uname() . "\n\n"; - $ldd = $autoconf = $sys_libtool = $libtool = $compiler = 'N/A'; + $is_switch = true; - if (substr(PHP_OS, 0, 3) != "WIN") { - /* If PHP_AUTOCONF is set, use it; otherwise, use 'autoconf'. */ - if (getenv('PHP_AUTOCONF')) { - $autoconf = shell_exec(getenv('PHP_AUTOCONF') . ' --version'); - } else { - $autoconf = shell_exec('autoconf --version'); + if ($repeat) { + foreach($cfgtypes as $type) { + if (strpos($switch, '--' . $type) === 0) { + foreach($cfgfiles as $file) { + if ($switch == '--' . $type . '-' . $file) { + $cfg[$type][$file] = true; + $is_switch = false; + break; + } + } + } + } } - /* Always use the generated libtool - Mac OSX uses 'glibtool' */ - $libtool = shell_exec(INIT_DIR . '/libtool --version'); - - /* Use shtool to find out if there is glibtool present (MacOSX) */ - $sys_libtool_path = shell_exec(__DIR__ . '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/build/shtool path glibtool libtool'); - - if ($sys_libtool_path) { - $sys_libtool = shell_exec(str_replace("\n", "", $sys_libtool_path) . ' --version'); + if (!$is_switch) { + $is_switch = true; + break; } - /* Try the most common flags for 'version' */ - $flags = array('-v', '-V', '--version'); - $cc_status = 0; + $repeat = false; - foreach($flags AS $flag) { - system(getenv('CC') . " $flag >/dev/null 2>&1", $cc_status); - if ($cc_status == 0) { - $compiler = shell_exec(getenv('CC') . " $flag 2>&1"); + switch($switch) { + case 'j': + $workers = substr($argv[$i], 2); + if (!preg_match('/^\d+$/', $workers) || $workers == 0) { + error("'$workers' is not a valid number of workers, try e.g. -j16 for 16 workers"); + } + $workers = intval($workers, 10); + $environment['SKIP_IO_CAPTURE_TESTS'] = 1; break; - } - } - - $ldd = shell_exec("ldd $php 2>/dev/null"); - } - - $failed_tests_data .= "Autoconf:\n$autoconf\n"; - $failed_tests_data .= "Bundled Libtool:\n$libtool\n"; - $failed_tests_data .= "System Libtool:\n$sys_libtool\n"; - $failed_tests_data .= "Compiler:\n$compiler\n"; - $failed_tests_data .= "Bison:\n". shell_exec('bison --version 2>/dev/null') . "\n"; - $failed_tests_data .= "Libraries:\n$ldd\n"; - $failed_tests_data .= "\n"; - - if (isset($user_email)) { - $failed_tests_data .= "User's E-mail: " . $user_email . "\n\n"; - } - - $failed_tests_data .= $sep . "PHPINFO" . $sep; - $failed_tests_data .= shell_exec($php . ' -ddisplay_errors=stderr -dhtml_errors=0 -i 2> /dev/null'); - - if (($just_save_results || !mail_qa_team($failed_tests_data, $status)) && !TRAVIS_CI) { - file_put_contents($output_file, $failed_tests_data); - - if (!$just_save_results) { - echo "\nThe test script was unable to automatically send the report to PHP's QA Team\n"; - } - - echo "Please send " . $output_file . " to " . PHP_QA_EMAIL . " manually, thank you.\n"; - } elseif (!getenv('NO_INTERACTION') && !TRAVIS_CI) { - fwrite($fp, "\nThank you for helping to make PHP better.\n"); - fclose($fp); - } - } - } -} - -// Determine the tests to be run. - -$test_files = array(); -$redir_tests = array(); -$test_results = array(); -$PHP_FAILED_TESTS = array('BORKED' => array(), 'FAILED' => array(), 'WARNED' => array(), 'LEAKED' => array(), 'XFAILED' => array(), 'SLOW' => array()); - -// If parameters given assume they represent selected tests to run. -$result_tests_file= false; -$failed_tests_file= false; -$pass_option_n = false; -$pass_options = ''; - -$output_file = INIT_DIR . '/php_test_results_' . date('Ymd_Hi') . '.txt'; - -$just_save_results = false; -$valgrind = null; -$html_output = false; -$html_file = null; -$temp_source = null; -$temp_target = null; -$temp_urlbase = null; -$conf_passed = null; -$no_clean = false; -$slow_min_ms = INF; - -$cfgtypes = array('show', 'keep'); -$cfgfiles = array('skip', 'php', 'clean', 'out', 'diff', 'exp', 'mem'); -$cfg = array(); - -foreach($cfgtypes as $type) { - $cfg[$type] = array(); - - foreach($cfgfiles as $file) { - $cfg[$type][$file] = false; - } -} - -if (getenv('TEST_PHP_ARGS')) { - - if (!isset($argc, $argv) || !$argc) { - $argv = array(__FILE__); - } - - $argv = array_merge($argv, explode(' ', getenv('TEST_PHP_ARGS'))); - $argc = count($argv); -} - -if (isset($argc) && $argc > 1) { - - for ($i=1; $i<$argc; $i++) { - $is_switch = false; - $switch = substr($argv[$i],1,1); - $repeat = substr($argv[$i],0,1) == '-'; - - while ($repeat) { - - if (!$is_switch) { - $switch = substr($argv[$i],1,1); - } - - $is_switch = true; - - if ($repeat) { - foreach($cfgtypes as $type) { - if (strpos($switch, '--' . $type) === 0) { - foreach($cfgfiles as $file) { - if ($switch == '--' . $type . '-' . $file) { - $cfg[$type][$file] = true; - $is_switch = false; - break; + case 'r': + case 'l': + $test_list = file($argv[++$i]); + if ($test_list) { + foreach($test_list as $test) { + $matches = array(); + if (preg_match('/^#.*\[(.*)\]\:\s+(.*)$/', $test, $matches)) { + $redir_tests[] = array($matches[1], $matches[2]); + } else if (strlen($test)) { + $test_files[] = trim($test); + } } } - } - } - } - - if (!$is_switch) { - $is_switch = true; - break; - } - - $repeat = false; - - switch($switch) { - case 'r': - case 'l': - $test_list = file($argv[++$i]); - if ($test_list) { - foreach($test_list as $test) { - $matches = array(); - if (preg_match('/^#.*\[(.*)\]\:\s+(.*)$/', $test, $matches)) { - $redir_tests[] = array($matches[1], $matches[2]); - } else if (strlen($test)) { - $test_files[] = trim($test); - } + if ($switch != 'l') { + break; } - } - if ($switch != 'l') { - break; - } - $i--; + $i--; // break left intentionally - case 'w': - $failed_tests_file = fopen($argv[++$i], 'w+t'); - break; - case 'a': - $failed_tests_file = fopen($argv[++$i], 'a+t'); - break; - case 'W': - $result_tests_file = fopen($argv[++$i], 'w+t'); - break; - case 'c': - $conf_passed = $argv[++$i]; - break; - case 'd': - $ini_overwrites[] = $argv[++$i]; - break; - case 'g': - $SHOW_ONLY_GROUPS = explode(",", $argv[++$i]); - break; - //case 'h' - case '--keep-all': - foreach($cfgfiles as $file) { - $cfg['keep'][$file] = true; - } - break; - //case 'l' - case 'm': - $valgrind = new RuntestsValgrind($environment); - break; - case 'n': - if (!$pass_option_n) { - $pass_options .= ' -n'; - } - $pass_option_n = true; - break; - case 'e': - $pass_options .= ' -e'; - break; - case '--no-clean': - $no_clean = true; - break; - case 'p': - $php = $argv[++$i]; - putenv("TEST_PHP_EXECUTABLE=$php"); - $environment['TEST_PHP_EXECUTABLE'] = $php; - break; - case 'P': - if(constant('PHP_BINARY')) { - $php = PHP_BINARY; - } else { + case 'w': + $failed_tests_file = fopen($argv[++$i], 'w+t'); break; - } - putenv("TEST_PHP_EXECUTABLE=$php"); - $environment['TEST_PHP_EXECUTABLE'] = $php; - break; - case 'q': - putenv('NO_INTERACTION=1'); - break; - //case 'r' - case 's': - $output_file = $argv[++$i]; - $just_save_results = true; - break; - case '--set-timeout': - $environment['TEST_TIMEOUT'] = $argv[++$i]; - break; - case '--show-all': - foreach($cfgfiles as $file) { - $cfg['show'][$file] = true; - } - break; - case '--show-slow': - $slow_min_ms = $argv[++$i]; - break; - case '--temp-source': - $temp_source = $argv[++$i]; - break; - case '--temp-target': - $temp_target = $argv[++$i]; - if ($temp_urlbase) { - $temp_urlbase = $temp_target; - } - break; - case '--temp-urlbase': - $temp_urlbase = $argv[++$i]; - break; - case 'v': - case '--verbose': - $DETAILED = true; - break; - case 'x': - $environment['SKIP_SLOW_TESTS'] = 1; - break; - case '--offline': - $environment['SKIP_ONLINE_TESTS'] = 1; - break; - //case 'w' - case '-': - // repeat check with full switch - $switch = $argv[$i]; - if ($switch != '-') { - $repeat = true; - } - break; - case '--html': - $html_file = fopen($argv[++$i], 'wt'); - $html_output = is_resource($html_file); - break; - case '--version': - echo '$Id$' . "\n"; - exit(1); - - default: - echo "Illegal switch '$switch' specified!\n"; - case 'h': - case '-help': - case '--help': - echo << Run simultaneous testing processes in parallel for + quicker testing on systems with multiple logical processors. + Note that this is experimental feature. + -l Read the testfiles to be executed from . After the test has finished all failed tests are written to the same . If the list is empty and no further test is specified then @@ -777,144 +589,415 @@ function save_or_mail_results() get written independent of the log format, however 'diff' only exists when a test fails. - --show-slow [n] - Show all tests that took longer than [n] milliseconds to run. + --show-slow [n] + Show all tests that took longer than [n] milliseconds to run. + + --no-clean Do not execute clean section if any. + +HELP; + exit(1); + } + } + + if (!$is_switch) { + $testfile = realpath($argv[$i]); + + if (!$testfile && strpos($argv[$i], '*') !== false && function_exists('glob')) { + + if (substr($argv[$i], -5) == '.phpt') { + $pattern_match = glob($argv[$i]); + } else if (preg_match("/\*$/", $argv[$i])) { + $pattern_match = glob($argv[$i] . '.phpt'); + } else { + die('Cannot find test file "' . $argv[$i] . '".' . PHP_EOL); + } + + if (is_array($pattern_match)) { + $test_files = array_merge($test_files, $pattern_match); + } + + } else if (is_dir($testfile)) { + find_files($testfile); + } else if (substr($testfile, -5) == '.phpt') { + $test_files[] = $testfile; + } else { + die('Cannot find test file "' . $argv[$i] . '".' . PHP_EOL); + } + } + } + + if (strlen($conf_passed)) { + if (substr(PHP_OS, 0, 3) == "WIN") { + $pass_options .= " -c " . escapeshellarg($conf_passed); + } else { + $pass_options .= " -c '$conf_passed'"; + } + } + + $test_files = array_unique($test_files); + $test_files = array_merge($test_files, $redir_tests); + + // Run selected tests. + $test_cnt = count($test_files); + + if ($test_cnt) { + putenv('NO_INTERACTION=1'); + verify_config(); + write_information(); + usort($test_files, "test_sort"); + $start_time = time(); + + if (!$html_output) { + echo "Running selected tests.\n"; + } else { + show_start($start_time); + } + + $test_idx = 0; + run_all_tests($test_files, $environment); + $end_time = time(); + + if ($html_output) { + show_end($end_time); + } + + if ($failed_tests_file) { + fclose($failed_tests_file); + } + + if ($result_tests_file) { + fclose($result_tests_file); + } + + compute_summary(); + if ($html_output) { + fwrite($html_file, "
\n" . get_summary(false, true)); + } + echo "====================================================================="; + echo get_summary(false, false); + + if ($html_output) { + fclose($html_file); + } + + if ($output_file != '' && $just_save_results) { + save_or_mail_results(); + } + + junit_save_xml(); + + if (getenv('REPORT_EXIT_STATUS') !== '0' && + getenv('REPORT_EXIT_STATUS') !== 'no' && + ($sum_results['FAILED'] || $sum_results['BORKED'] || $sum_results['LEAKED'])) { + exit(1); + } + + exit(0); + } + } + + verify_config(); + write_information(); + + // Compile a list of all test files (*.phpt). + $test_files = array(); + $exts_tested = count($exts_to_test); + $exts_skipped = 0; + $ignored_by_ext = 0; + sort($exts_to_test); + $test_dirs = array(); + $optionals = array('tests', 'ext', 'Zend', 'sapi'); + + foreach($optionals as $dir) { + if (is_dir($dir)) { + $test_dirs[] = $dir; + } + } + + // Convert extension names to lowercase + foreach ($exts_to_test as $key => $val) { + $exts_to_test[$key] = strtolower($val); + } + + foreach ($test_dirs as $dir) { + find_files(TEST_PHP_SRCDIR."/{$dir}", $dir == 'ext'); + } + + foreach ($user_tests as $dir) { + find_files($dir, $dir == 'ext'); + } + + + $test_files = array_unique($test_files); + usort($test_files, "test_sort"); + + $start_time = time(); + show_start($start_time); + + $test_cnt = count($test_files); + $test_idx = 0; + run_all_tests($test_files, $environment); + $end_time = time(); + + if ($failed_tests_file) { + fclose($failed_tests_file); + } + + if ($result_tests_file) { + fclose($result_tests_file); + } + + // Summarize results + + if (0 == count($test_results)) { + echo "No tests were run.\n"; + return; + } + + compute_summary(); + + show_end($end_time); + show_summary(); + + if ($html_output) { + fclose($html_file); + } + + save_or_mail_results(); + + junit_save_xml(); + + if (getenv('REPORT_EXIT_STATUS') !== '0' && + getenv('REPORT_EXIT_STATUS') !== 'no' && + ($sum_results['FAILED'] || $sum_results['BORKED'])) { + exit(1); + } + exit(0); +} + +function verify_config() +{ + global $php; + + if (empty($php) || !file_exists($php)) { + error('environment variable TEST_PHP_EXECUTABLE must be set to specify PHP executable!'); + } + + if (function_exists('is_executable') && !is_executable($php)) { + error("invalid PHP executable specified by TEST_PHP_EXECUTABLE = $php"); + } +} + +function write_information() +{ + global $php, $php_cgi, $phpdbg, $php_info, $user_tests, $ini_overwrites, $pass_options, $exts_to_test, $valgrind, $no_file_cache; + + // Get info from php + $info_file = __DIR__ . '/run-test-info.php'; + @unlink($info_file); + $php_info = ''; + save_text($info_file, $php_info); + $info_params = array(); + settings2array($ini_overwrites, $info_params); + settings2params($info_params); + $php_info = `$php $pass_options $info_params $no_file_cache "$info_file"`; + define('TESTED_PHP_VERSION', `$php -n -r "echo PHP_VERSION;"`); + + if ($php_cgi && $php != $php_cgi) { + $php_info_cgi = `$php_cgi $pass_options $info_params $no_file_cache -q "$info_file"`; + $php_info_sep = "\n---------------------------------------------------------------------"; + $php_cgi_info = "$php_info_sep\nPHP : $php_cgi $php_info_cgi$php_info_sep"; + } else { + $php_cgi_info = ''; + } + + if ($phpdbg) { + $phpdbg_info = `$phpdbg $pass_options $info_params $no_file_cache -qrr "$info_file"`; + $php_info_sep = "\n---------------------------------------------------------------------"; + $phpdbg_info = "$php_info_sep\nPHP : $phpdbg $phpdbg_info$php_info_sep"; + } else { + $phpdbg_info = ''; + } + + if (function_exists('opcache_invalidate')) { + opcache_invalidate($info_file, true); + } + @unlink($info_file); + + // load list of enabled extensions + save_text($info_file, ''); + $exts_to_test = explode(',',`$php $pass_options $info_params $no_file_cache "$info_file"`); + // check for extensions that need special handling and regenerate + $info_params_ex = array( + 'session' => array('session.auto_start=0'), + 'tidy' => array('tidy.clean_output=0'), + 'zlib' => array('zlib.output_compression=Off'), + 'xdebug' => array('xdebug.default_enable=0'), + 'mbstring' => array('mbstring.func_overload=0'), + ); + + foreach($info_params_ex as $ext => $ini_overwrites_ex) { + if (in_array($ext, $exts_to_test)) { + $ini_overwrites = array_merge($ini_overwrites, $ini_overwrites_ex); + } + } + + if (function_exists('opcache_invalidate')) { + opcache_invalidate($info_file, true); + } + @unlink($info_file); - --no-clean Do not execute clean section if any. + // Write test context information. + echo " +===================================================================== +PHP : $php $php_info $php_cgi_info $phpdbg_info +CWD : ".TEST_PHP_SRCDIR." +Extra dirs : "; + foreach ($user_tests as $test_dir) { + echo "{$test_dir}\n "; + } + echo " +VALGRIND : " . ($valgrind ? $valgrind->getHeader() : 'Not used') . " +===================================================================== +"; +} -HELP; - exit(1); - } +function save_or_mail_results() +{ + global $sum_results, $just_save_results, $failed_test_summary, + $PHP_FAILED_TESTS, $php, $output_file; + + /* We got failed Tests, offer the user to send an e-mail to QA team, unless NO_INTERACTION is set */ + if (!getenv('NO_INTERACTION') && !TRAVIS_CI) { + $fp = fopen("php://stdin", "r+"); + if ($sum_results['FAILED'] || $sum_results['BORKED'] || $sum_results['WARNED'] || $sum_results['LEAKED']) { + echo "\nYou may have found a problem in PHP."; } + echo "\nThis report can be automatically sent to the PHP QA team at\n"; + echo QA_REPORTS_PAGE . " and http://news.php.net/php.qa.reports\n"; + echo "This gives us a better understanding of PHP's behavior.\n"; + echo "If you don't want to send the report immediately you can choose\n"; + echo "option \"s\" to save it. You can then email it to ". PHP_QA_EMAIL . " later.\n"; + echo "Do you want to send this report now? [Yns]: "; + flush(); - if (!$is_switch) { - $testfile = realpath($argv[$i]); + $user_input = fgets($fp, 10); + $just_save_results = (strtolower($user_input[0]) == 's'); + } - if (!$testfile && strpos($argv[$i], '*') !== false && function_exists('glob')) { + if ($just_save_results || !getenv('NO_INTERACTION') || TRAVIS_CI) { + if ($just_save_results || TRAVIS_CI || strlen(trim($user_input)) == 0 || strtolower($user_input[0]) == 'y') { + /* + * Collect information about the host system for our report + * Fetch phpinfo() output so that we can see the PHP environment + * Make an archive of all the failed tests + * Send an email + */ + if ($just_save_results) { + $user_input = 's'; + } - if (substr($argv[$i], -5) == '.phpt') { - $pattern_match = glob($argv[$i]); - } else if (preg_match("/\*$/", $argv[$i])) { - $pattern_match = glob($argv[$i] . '.phpt'); - } else { - die('Cannot find test file "' . $argv[$i] . '".' . PHP_EOL); - } + /* Ask the user to provide an email address, so that QA team can contact the user */ + if (TRAVIS_CI) { + $user_email = 'travis at php dot net'; + } elseif (!strncasecmp($user_input, 'y', 1) || strlen(trim($user_input)) == 0) { + 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): "; + flush(); + $user_email = trim(fgets($fp, 1024)); + $user_email = str_replace("@", " at ", str_replace(".", " dot ", $user_email)); + } - if (is_array($pattern_match)) { - $test_files = array_merge($test_files, $pattern_match); - } + $failed_tests_data = ''; + $sep = "\n" . str_repeat('=', 80) . "\n"; + $failed_tests_data .= $failed_test_summary . "\n"; + $failed_tests_data .= get_summary(true, false) . "\n"; - } else if (is_dir($testfile)) { - find_files($testfile); - } else if (substr($testfile, -5) == '.phpt') { - $test_files[] = $testfile; + if ($sum_results['FAILED']) { + foreach ($PHP_FAILED_TESTS['FAILED'] as $test_info) { + $failed_tests_data .= $sep . $test_info['name'] . $test_info['info']; + $failed_tests_data .= $sep . file_get_contents(realpath($test_info['output']), FILE_BINARY); + $failed_tests_data .= $sep . file_get_contents(realpath($test_info['diff']), FILE_BINARY); + $failed_tests_data .= $sep . "\n\n"; + } + $status = "failed"; } else { - die('Cannot find test file "' . $argv[$i] . '".' . PHP_EOL); + $status = "success"; } - } - } - if (strlen($conf_passed)) { - if (substr(PHP_OS, 0, 3) == "WIN") { - $pass_options .= " -c " . escapeshellarg($conf_passed); - } else { - $pass_options .= " -c '$conf_passed'"; - } - } + $failed_tests_data .= "\n" . $sep . 'BUILD ENVIRONMENT' . $sep; + $failed_tests_data .= "OS:\n" . PHP_OS . " - " . php_uname() . "\n\n"; + $ldd = $autoconf = $sys_libtool = $libtool = $compiler = 'N/A'; - $test_files = array_unique($test_files); - $test_files = array_merge($test_files, $redir_tests); + if (substr(PHP_OS, 0, 3) != "WIN") { + /* If PHP_AUTOCONF is set, use it; otherwise, use 'autoconf'. */ + if (getenv('PHP_AUTOCONF')) { + $autoconf = shell_exec(getenv('PHP_AUTOCONF') . ' --version'); + } else { + $autoconf = shell_exec('autoconf --version'); + } - // Run selected tests. - $test_cnt = count($test_files); + /* Always use the generated libtool - Mac OSX uses 'glibtool' */ + $libtool = shell_exec(INIT_DIR . '/libtool --version'); - if ($test_cnt) { - putenv('NO_INTERACTION=1'); - verify_config(); - write_information(); - usort($test_files, "test_sort"); - $start_time = time(); + /* Use shtool to find out if there is glibtool present (MacOSX) */ + $sys_libtool_path = shell_exec(__DIR__ . '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/build/shtool path glibtool libtool'); - if (!$html_output) { - echo "Running selected tests.\n"; - } else { - show_start($start_time); - } + if ($sys_libtool_path) { + $sys_libtool = shell_exec(str_replace("\n", "", $sys_libtool_path) . ' --version'); + } - $test_idx = 0; - run_all_tests($test_files, $environment); - $end_time = time(); + /* Try the most common flags for 'version' */ + $flags = array('-v', '-V', '--version'); + $cc_status = 0; - if ($html_output) { - show_end($end_time); - } + foreach($flags AS $flag) { + system(getenv('CC') . " $flag >/dev/null 2>&1", $cc_status); + if ($cc_status == 0) { + $compiler = shell_exec(getenv('CC') . " $flag 2>&1"); + break; + } + } - if ($failed_tests_file) { - fclose($failed_tests_file); - } + $ldd = shell_exec("ldd $php 2>/dev/null"); + } - if ($result_tests_file) { - fclose($result_tests_file); - } + $failed_tests_data .= "Autoconf:\n$autoconf\n"; + $failed_tests_data .= "Bundled Libtool:\n$libtool\n"; + $failed_tests_data .= "System Libtool:\n$sys_libtool\n"; + $failed_tests_data .= "Compiler:\n$compiler\n"; + $failed_tests_data .= "Bison:\n". shell_exec('bison --version 2>/dev/null') . "\n"; + $failed_tests_data .= "Libraries:\n$ldd\n"; + $failed_tests_data .= "\n"; - compute_summary(); - if ($html_output) { - fwrite($html_file, "
\n" . get_summary(false, true)); - } - echo "====================================================================="; - echo get_summary(false, false); + if (isset($user_email)) { + $failed_tests_data .= "User's E-mail: " . $user_email . "\n\n"; + } - if ($html_output) { - fclose($html_file); - } + $failed_tests_data .= $sep . "PHPINFO" . $sep; + $failed_tests_data .= shell_exec($php . ' -ddisplay_errors=stderr -dhtml_errors=0 -i 2> /dev/null'); - if ($output_file != '' && $just_save_results) { - save_or_mail_results(); - } + if (($just_save_results || !mail_qa_team($failed_tests_data, $status)) && !TRAVIS_CI) { + file_put_contents($output_file, $failed_tests_data); - junit_save_xml(); + if (!$just_save_results) { + echo "\nThe test script was unable to automatically send the report to PHP's QA Team\n"; + } - if (getenv('REPORT_EXIT_STATUS') !== '0' && - getenv('REPORT_EXIT_STATUS') !== 'no' && - ($sum_results['FAILED'] || $sum_results['BORKED'] || $sum_results['LEAKED'])) { - exit(1); + echo "Please send " . $output_file . " to " . PHP_QA_EMAIL . " manually, thank you.\n"; + } elseif (!getenv('NO_INTERACTION') && !TRAVIS_CI) { + fwrite($fp, "\nThank you for helping to make PHP better.\n"); + fclose($fp); + } } - - exit(0); - } -} - -verify_config(); -write_information(); - -// Compile a list of all test files (*.phpt). -$test_files = array(); -$exts_tested = count($exts_to_test); -$exts_skipped = 0; -$ignored_by_ext = 0; -sort($exts_to_test); -$test_dirs = array(); -$optionals = array('tests', 'ext', 'Zend', 'sapi'); - -foreach($optionals as $dir) { - if (is_dir($dir)) { - $test_dirs[] = $dir; } } -// Convert extension names to lowercase -foreach ($exts_to_test as $key => $val) { - $exts_to_test[$key] = strtolower($val); -} - -foreach ($test_dirs as $dir) { - find_files(TEST_PHP_SRCDIR."/{$dir}", $dir == 'ext'); -} - -foreach ($user_tests as $dir) { - find_files($dir, $dir == 'ext'); -} - function find_files($dir, $is_ext_dir = false, $ignore = false) { global $test_files, $exts_to_test, $ignored_by_ext, $exts_skipped; @@ -974,52 +1057,6 @@ function test_sort($a, $b) return $tb - $ta; } } - -$test_files = array_unique($test_files); -usort($test_files, "test_sort"); - -$start_time = time(); -show_start($start_time); - -$test_cnt = count($test_files); -$test_idx = 0; -run_all_tests($test_files, $environment); -$end_time = time(); - -if ($failed_tests_file) { - fclose($failed_tests_file); -} - -if ($result_tests_file) { - fclose($result_tests_file); -} - -// Summarize results - -if (0 == count($test_results)) { - echo "No tests were run.\n"; - return; -} - -compute_summary(); - -show_end($end_time); -show_summary(); - -if ($html_output) { - fclose($html_file); -} - -save_or_mail_results(); - -junit_save_xml(); -if (getenv('REPORT_EXIT_STATUS') !== '0' && - getenv('REPORT_EXIT_STATUS') !== 'no' && - ($sum_results['FAILED'] || $sum_results['LEAKED'])) { - exit(1); -} -exit(0); - // // Send Email to QA Team // @@ -1196,10 +1233,14 @@ function system_with_timeout($commandline, $env = null, $stdin = null, $captureS function run_all_tests($test_files, $env, $redir_tested = null) { - global $test_results, $failed_tests_file, $result_tests_file, $php, $test_idx; + global $test_results, $failed_tests_file, $result_tests_file, $php, $test_idx, $PHP_FAILED_TESTS, $workers, $workerID, $workerSock; - foreach($test_files as $name) { + if ($workers !== null && !$workerID) { + run_all_tests_parallel($test_files, $env, $redir_tested); + return; + } + foreach($test_files as $name) { if (is_array($name)) { $index = "# $name[1]: $name[0]"; @@ -1212,10 +1253,30 @@ function run_all_tests($test_files, $env, $redir_tested = null) $index = $name; } $test_idx++; + + if ($workerID) { + $PHP_FAILED_TESTS = ['BORKED' => [], 'FAILED' => [], 'WARNED' => [], 'LEAKED' => [], 'XFAILED' => [], 'SLOW' => []]; + ob_start(); + } + $result = run_test($php, $name, $env); + $resultText = ob_get_clean(); if (!is_array($name) && $result != 'REDIR') { + if ($workerID) { + send_message($workerSock, [ + "type" => "test_result", + "name" => $name, + "index" => $index, + "result" => $result, + "text" => $resultText, + "PHP_FAILED_TESTS" => $PHP_FAILED_TESTS + ]); + continue; + } + $test_results[$index] = $result; + if ($failed_tests_file && ($result == 'XFAILED' || $result == 'FAILED' || $result == 'WARNED' || $result == 'LEAKED')) { fwrite($failed_tests_file, "$index\n"); } @@ -1226,6 +1287,372 @@ function run_all_tests($test_files, $env, $redir_tested = null) } } +function run_all_tests_parallel($test_files, $env, $redir_tested) { + global $workers, $test_idx, $test_cnt, $test_results, $failed_tests_file, $result_tests_file, $PHP_FAILED_TESTS; + + // The PHP binary running run-tests.php, and run-tests.php itself + // This PHP executable is *not* necessarily the same as the tested version + $thisPHP = PHP_BINARY; + $thisScript = __FILE__; + + $workerProcs = []; + $workerSocks = []; + + echo "====⚡️===========================================================⚡️====\n"; + echo "====⚡️==== WELCOME TO THE FUTURE: run-tests PARALLEL EDITION ====⚡️====\n"; + echo "====⚡️===========================================================⚡️====\n"; + + // Because some of the PHP test suite has not been written with + // parallel execution in mind, it is not safe to just run any two tests + // concurrently. + // Therefore, we divide the test set into directories and test multiple + // directories at once, but not multiple tests within them. + + $testDirsToGo = []; + + foreach ($test_files as $file) { + $dirSeparator = strrpos($file, DIRECTORY_SEPARATOR); + if ($dirSeparator !== FALSE) { + $testDirsToGo[substr($file, 0, $dirSeparator)][] = $file; + } else { + $testDirsToGo[""][] = $file; + } + } + + // We assume most test directories should be executed in serial, but for + // big directories, this would waste time if they can actually be parallel. + // Therefore, if a directory has a special '!CAN_BE_PARALLELISED' file, we + // will divide it up into smaller “directories” automatically. + foreach ($testDirsToGo as $dir => &$tests) { + if (count($tests) < 64 || !is_string($dir)) { + continue; + } + if (file_exists($dir . DIRECTORY_SEPARATOR . '!CAN_BE_PARALLELISED')) { + foreach (array_chunk($tests, 64) as $testsChunk) { + $testDirsToGo[] = $testsChunk; + } + unset($testDirsToGo[$dir]); + } + } + unset($tests); + + // Sort test dirs so the biggest ones are handled first, so we spend less + // time waiting on workers tasked with very large dirs. + // This is an ascending sort because items are popped off the end. + // Thank you Rasmus for this idea :) + uasort($testDirsToGo, function ($a, $b) { + return count($a) - count($b); + }); + + $testDirsInProgress = 0; + + echo "Isolated ", count($testDirsToGo), " directories to be tested in parallel.\n"; + + $shamedDirs = array_reverse(array_filter($testDirsToGo, function ($files) { + return count($files) > 100; + }), true); + + if ($shamedDirs) { + $shameList = ""; + foreach ($shamedDirs as $dir => $shame) { + $shameList .= "\n$dir: " . count($shame) . " files"; + } + + echo << ['pipe', 'r'], + 1 => ['pipe', 'w'], + 2 => ['pipe', 'w'] + ], + $pipes, + NULL, + $_ENV + [ + "TEST_PHP_WORKER" => $i, + "TEST_PHP_URI" => $sockUri, + ], + [ + "suppress_errors" => TRUE + ] + ); + if ($proc === FALSE) { + kill_children($workerProcs); + error("Failed to spawn worker $i"); + } + $workerProcs[$i] = $proc; + + $workerSock = stream_socket_accept($listenSock, 5); + if ($workerSock === FALSE) { + kill_children($workerProcs); + error("Failed to accept connection from worker $i"); + } + + $greeting = base64_encode(serialize([ + "type" => "hello", + "workerID" => $i, + "GLOBALS" => $GLOBALS, + "constants" => [ + "INIT_DIR" => INIT_DIR, + "TEST_PHP_SRCDIR" => TEST_PHP_SRCDIR, + "PHP_QA_EMAIL" => PHP_QA_EMAIL, + "QA_SUBMISSION_PAGE" => QA_SUBMISSION_PAGE, + "QA_REPORTS_PAGE" => QA_REPORTS_PAGE, + "TRAVIS_CI" => TRAVIS_CI + ] + ])) . "\n"; + + stream_set_timeout($workerSock, 5); + if (fwrite($workerSock, $greeting) === FALSE) { + kill_children($workerProcs); + error("Failed to send greeting to worker $i."); + } + + $rawReply = fgets($workerSock); + if ($rawReply === FALSE) { + kill_children($workerProcs); + error("Failed to read greeting reply from worker $i."); + } + + $reply = unserialize(base64_decode($rawReply)); + if (!$reply || $reply["type"] !== "hello_reply" || $reply["workerID"] !== $i) { + kill_children($workerProcs); + error("Greeting reply from worker $i unexpected or could not be decoded: '$rawReply'"); + } + + stream_set_timeout($workerSock, 0); + stream_set_blocking($workerSock, FALSE); + + $workerSocks[$i] = $workerSock; + + echo "$i "; + } + echo "… done!\n"; + echo "====⚡️===========================================================⚡️====\n"; + echo "\n"; + + $rawMessageBuffers = []; + +escape: + while ($testDirsToGo || ($testDirsInProgress > 0)) { + $toRead = array_values($workerSocks); + $toWrite = NULL; + $toExcept = NULL; + if (stream_select($toRead, $toWrite, $toExcept, 10, 0)) { + foreach ($toRead as $workerSock) { + $i = array_search($workerSock, $workerSocks); + if ($i === FALSE) { + kill_children($workerProcs); + error("Could not find worker stdout in array of worker stdouts, THIS SHOULD NOT HAPPEN."); + } + while (FALSE !== ($rawMessage = fgets($workerSock))) { + // work around fgets truncating things + if (($rawMessageBuffers[$i] ?? '') !== '') { + $rawMessage = $rawMessageBuffers[$i] . $rawMessage; + $rawMessageBuffers[$i] = ''; + } + if ($rawMessage[-1] !== "\n") { + $rawMessageBuffers[$i] = $rawMessage; + continue; + } + + $message = unserialize(base64_decode($rawMessage)); + if (!$message) { + kill_children($workerProcs); + $stuff = fread($workerSock, 65536); + error("Could not decode message from worker $i: '$rawMessage$stuff'"); + } + + switch ($message["type"]) { + case "dir_finished": + $testDirsInProgress--; + // intentional fall-through + case "ready": + if ($testDir = array_pop($testDirsToGo)) { + $testDirsInProgress++; + send_message($workerSocks[$i], [ + "type" => "run_tests", + "test_files" => $testDir, + "env" => $env, + "redir_tested" => $redir_tested + ]); + } else { + proc_terminate($workerProcs[$i]); + unset($workerProcs[$i]); + unset($workerSocks[$i]); + goto escape; + } + break; + case "test_result": + [$name, $index, $result, $resultText] = [$message["name"], $message["index"], $message["result"], $message["text"]]; + foreach ($message["PHP_FAILED_TESTS"] as $category => $tests) { + $PHP_FAILED_TESTS[$category] = array_merge($PHP_FAILED_TESTS[$category], $tests); + } + $test_idx++; + clear_show_test(); + echo $resultText; + show_test($test_idx, "⚡️[" . count($workerProcs) . "/$workers concurrent test workers running]⚡️"); + + if (!is_array($name) && $result != 'REDIR') { + $test_results[$index] = $result; + + if ($failed_tests_file && ($result == 'XFAILED' || $result == 'FAILED' || $result == 'WARNED' || $result == 'LEAKED')) { + fwrite($failed_tests_file, "$index\n"); + } + if ($result_tests_file) { + fwrite($result_tests_file, "$result\t$index\n"); + } + } + break; + case "error": + kill_children($workerProcs); + error("Worker $i reported error: $message[msg]"); + break; + case "php_error": + kill_children($workerProcs); + $error_consts = [ + 'E_ERROR', + 'E_WARNING', + 'E_PARSE', + 'E_NOTICE', + 'E_CORE_ERROR', + 'E_CORE_WARNING', + 'E_COMPILE_ERROR', + 'E_COMPILE_WARNING', + 'E_USER_ERROR', + 'E_USER_WARNING', + 'E_USER_NOTICE', + 'E_STRICT', + 'E_RECOVERABLE_ERROR', + 'E_USER_DEPRECATED' + ]; + $error_consts = array_combine(array_map('constant', $error_consts), $error_consts); + error("Worker $i reported unexpected {$error_consts[$message['errno']]}: $message[errstr] in $message[errfile] on line $message[errline]"); + default: + kill_children($workerProcs); + error("Unrecognised message type '$message[type]' from worker $i"); + } + } + } + } + } + + clear_show_test(); + + kill_children($workerProcs); + + if ($testDirsInProgress < 0) { + error("$testDirsInProgress test directories “in progress”, which is less than zero. THIS SHOULD NOT HAPPEN."); + } +} + +function send_message($stream, array $message) { + $blocking = stream_get_meta_data($stream)["blocked"]; + stream_set_blocking($stream, true); + fwrite($stream, base64_encode(serialize($message)) . "\n"); + stream_set_blocking($stream, $blocking); +} + +function kill_children(array $children) { + foreach ($children as $child) { + if ($child) { + proc_terminate($child); + } + } +} + +function run_worker() { + global $workerID, $workerSock; + + $sockUri = getenv("TEST_PHP_URI"); + + @unlink(__DIR__ . "/../worker$workerID.log"); + ini_set("error_log", __DIR__ . "/../worker$workerID.log"); + set_error_handler(function ($errno, $errstr, $errfile, $errline) use (&$workerSock) { + if (error_reporting() & $errno) { + send_message($workerSock, compact('errno', 'errstr', 'errfile', 'errline') + [ + 'type' => 'php_error' + ]); + } + + return true; + }); + + $workerSock = stream_socket_client($sockUri, $_, $_, 5) or error("Couldn't connect to $sockUri"); + + $greeting = fgets($workerSock); + $greeting = unserialize(base64_decode($greeting)) or die("Could not decode greeting\n"); + if ($greeting["type"] !== "hello" || $greeting["workerID"] !== $workerID) { + error("Unexpected greeting of type $greeting[type] and for worker $greeting[workerID]"); + } + + foreach ($greeting["GLOBALS"] as $var => $value) { + if ($var !== "workerID" && $var !== "workerSock" && $var !== "GLOBALS") { + $GLOBALS[$var] = $value; + } + } + foreach ($greeting["constants"] as $const => $value) { + define($const, $value); + } + + send_message($workerSock, [ + "type" => "hello_reply", + "workerID" => $workerID + ]); + + send_message($workerSock, [ + "type" => "ready" + ]); + + while (($command = fgets($workerSock))) { + $command = unserialize(base64_decode($command)); + + switch ($command["type"]) { + case "run_tests": + run_all_tests($command["test_files"], $command["env"], $command["redir_tested"]); + send_message($workerSock, [ + "type" => "dir_finished" + ]); + break; + default: + send_message($workerSock, [ + "type" => "error", + "msg" => "Unrecognised message type: $command[type]" + ]); + die; + } + } + + die; +} + // // Show file or result block // @@ -1257,6 +1684,7 @@ function run_test($php, $file, $env) global $SHOW_ONLY_GROUPS; global $no_file_cache; global $slow_min_ms; + global $workerID; $temp_filenames = null; $org_file = $file; @@ -1464,7 +1892,7 @@ function run_test($php, $file, $env) } } - if (!$SHOW_ONLY_GROUPS) { + if (!$SHOW_ONLY_GROUPS && !$workerID) { show_test($test_idx, $shortname); } @@ -2639,7 +3067,7 @@ function show_summary() function show_redirect_start($tests, $tested, $tested_file) { - global $html_output, $html_file, $line_length, $SHOW_ONLY_GROUPS; + global $html_output, $html_file, $line_length, $SHOW_ONLY_GROUPS, $workerID; if ($html_output) { fwrite($html_file, "---> $tests ($tested [$tested_file]) begin\n"); @@ -2647,15 +3075,14 @@ function show_redirect_start($tests, $tested, $tested_file) if (!$SHOW_ONLY_GROUPS || in_array('REDIRECT', $SHOW_ONLY_GROUPS)) { echo "REDIRECT $tests ($tested [$tested_file]) begin\n"; - } else { - // Write over the last line to avoid random trailing chars on next echo - echo str_repeat(" ", $line_length), "\r"; + } else if (!$workerID) { + clear_show_test(); } } function show_redirect_ends($tests, $tested, $tested_file) { - global $html_output, $html_file, $line_length, $SHOW_ONLY_GROUPS; + global $html_output, $html_file, $line_length, $SHOW_ONLY_GROUPS, $workerID; if ($html_output) { fwrite($html_file, "---> $tests ($tested [$tested_file]) done\n"); @@ -2663,9 +3090,8 @@ function show_redirect_ends($tests, $tested, $tested_file) if (!$SHOW_ONLY_GROUPS || in_array('REDIRECT', $SHOW_ONLY_GROUPS)) { echo "REDIRECT $tests ($tested [$tested_file]) done\n"; - } else { - // Write over the last line to avoid random trailing chars on next echo - echo str_repeat(" ", $line_length), "\r"; + } else if (!$workerID) { + clear_show_test(); } } @@ -2680,15 +3106,21 @@ function show_test($test_idx, $shortname) flush(); } +function clear_show_test() { + global $line_length; + + // Write over the last line to avoid random trailing chars on next echo + echo str_repeat(" ", $line_length), "\r"; +} + function show_result($result, $tested, $tested_file, $extra = '', $temp_filenames = null) { - global $html_output, $html_file, $temp_target, $temp_urlbase, $line_length, $SHOW_ONLY_GROUPS; + global $html_output, $html_file, $temp_target, $temp_urlbase, $line_length, $SHOW_ONLY_GROUPS, $workerID; if (!$SHOW_ONLY_GROUPS || in_array($result, $SHOW_ONLY_GROUPS)) { echo "$result $tested [$tested_file] $extra\n"; - } else if (!$SHOW_ONLY_GROUPS) { - // Write over the last line to avoid random trailing chars on next echo - echo str_repeat(" ", $line_length), "\r"; + } else if (!$SHOW_ONLY_GROUPS && !$workerID) { + clear_show_test(); } if ($html_output) { diff --git a/tests/classes/!CAN_BE_PARALLELISED b/tests/classes/!CAN_BE_PARALLELISED new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/tests/lang/!CAN_BE_PARALLELISED b/tests/lang/!CAN_BE_PARALLELISED new file mode 100644 index 000000000000..e69de29bb2d1