From 04ca2daf4b90a3f938108ad52c6ffdb63af25108 Mon Sep 17 00:00:00 2001 From: Andrea Faulds Date: Sat, 7 Oct 2017 19:25:48 +0100 Subject: [PATCH 01/23] Move top-level run-tests.php code into a function --- run-tests.php | 1290 +++++++++++++++++++++++++------------------------ 1 file changed, 653 insertions(+), 637 deletions(-) diff --git a/run-tests.php b/run-tests.php index e7f908c2e88e..467916cdb063 100644 --- a/run-tests.php +++ b/run-tests.php @@ -26,22 +26,50 @@ /* $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(); -/* 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. - */ +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; + + 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()); -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,7 +268,504 @@ 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'; + + 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')); + + // 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; + } + } + } + } + } + + 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; + } + $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 { + 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 << 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 + all tests are executed (same as: -r -w ). + + -r Read the testfiles to be executed from . + + -w Write a list of all failed tests to . + + -a Same as -w but append rather then truncating . + + -W Write a list of all tests and their result status to . + + -c Look for php.ini in directory or use as ini. + + -n Pass -n option to the php binary (Do not use a php.ini). + + -d foo=bar Pass -d option to the php binary (Define INI entry foo + with value 'bar'). + + -g Comma separated list of groups to show during test run + (possible values: PASS, FAIL, XFAIL, SKIP, BORK, WARN, LEAK, REDIRECT). + + -m Test for memory leaks with Valgrind. + + -p Specify PHP executable to run. + + -P Use PHP_BINARY as PHP executable to run. + + -q Quiet, no user interaction (same as environment NO_INTERACTION). + + -s Write output to . + + -x Sets 'SKIP_SLOW_TESTS' environmental variable. + + --offline Sets 'SKIP_ONLINE_TESTS' environmental variable. + + --verbose + -v Verbose mode. + + --help + -h This Help. + + --html Generate HTML output. + + --temp-source --temp-target [--temp-urlbase ] + Write temporary files to by replacing from the + filenames to generate with . If --html is being used and + given then the generated links are relative and prefixed + with the given url. In general you want to make the path + to your source files and some pach in your web page + hierarchy with pointing to . + + --keep-[all|php|skip|clean] + Do not delete 'all' files, 'php' test file, 'skip' or 'clean' + file. + + --set-timeout [n] + Set timeout for individual tests, where [n] is the number of + seconds. The default value is 60 seconds, or 300 seconds when + testing for memory leaks. + + --show-[all|php|skip|clean|exp|diff|out] + Show 'all' files, 'php' test file, 'skip' or 'clean' file. You + can also use this to show the output 'out', the expected result + 'exp' or the difference between them 'diff'. The result types + 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. + + --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() { @@ -349,11 +847,6 @@ function write_information() "; } -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, @@ -484,437 +977,6 @@ function save_or_mail_results() } } -// 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; - } - } - } - } - } - - 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; - } - $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 { - 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 << 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 - all tests are executed (same as: -r -w ). - - -r Read the testfiles to be executed from . - - -w Write a list of all failed tests to . - - -a Same as -w but append rather then truncating . - - -W Write a list of all tests and their result status to . - - -c Look for php.ini in directory or use as ini. - - -n Pass -n option to the php binary (Do not use a php.ini). - - -d foo=bar Pass -d option to the php binary (Define INI entry foo - with value 'bar'). - - -g Comma separated list of groups to show during test run - (possible values: PASS, FAIL, XFAIL, SKIP, BORK, WARN, LEAK, REDIRECT). - - -m Test for memory leaks with Valgrind. - - -p Specify PHP executable to run. - - -P Use PHP_BINARY as PHP executable to run. - - -q Quiet, no user interaction (same as environment NO_INTERACTION). - - -s Write output to . - - -x Sets 'SKIP_SLOW_TESTS' environmental variable. - - --offline Sets 'SKIP_ONLINE_TESTS' environmental variable. - - --verbose - -v Verbose mode. - - --help - -h This Help. - - --html Generate HTML output. - - --temp-source --temp-target [--temp-urlbase ] - Write temporary files to by replacing from the - filenames to generate with . If --html is being used and - given then the generated links are relative and prefixed - with the given url. In general you want to make the path - to your source files and some pach in your web page - hierarchy with pointing to . - - --keep-[all|php|skip|clean] - Do not delete 'all' files, 'php' test file, 'skip' or 'clean' - file. - - --set-timeout [n] - Set timeout for individual tests, where [n] is the number of - seconds. The default value is 60 seconds, or 300 seconds when - testing for memory leaks. - - --show-[all|php|skip|clean|exp|diff|out] - Show 'all' files, 'php' test file, 'skip' or 'clean' file. You - can also use this to show the output 'out', the expected result - 'exp' or the difference between them 'diff'. The result types - 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. - - --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'); -} - function find_files($dir, $is_ext_dir = false, $ignore = false) { global $test_files, $exts_to_test, $ignored_by_ext, $exts_skipped; @@ -974,52 +1036,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 // From aec3dee3ed08812c32e97488cac901e9e9f264c9 Mon Sep 17 00:00:00 2001 From: Andrea Faulds Date: Sun, 8 Oct 2017 04:04:46 +0100 Subject: [PATCH 02/23] Add experimental parallel test execution support --- run-tests.php | 336 +++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 320 insertions(+), 16 deletions(-) diff --git a/run-tests.php b/run-tests.php index 467916cdb063..2ba831cee3f4 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) | +----------------------------------------------------------------------+ @@ -52,7 +53,14 @@ function run_tests() { $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; + $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()); @@ -300,6 +308,7 @@ function run_tests() { $conf_passed = null; $no_clean = false; $slow_min_ms = INF; + $workers = null; $cfgtypes = array('show', 'keep'); $cfgfiles = array('skip', 'php', 'clean', 'out', 'diff', 'exp', 'mem'); @@ -360,6 +369,13 @@ function run_tests() { $repeat = false; 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); + break; case 'r': case 'l': $test_list = file($argv[++$i]); @@ -499,6 +515,10 @@ function run_tests() { php run-tests.php [options] [files] [directories] Options: + -j 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 @@ -1212,10 +1232,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; - 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]"; @@ -1228,10 +1252,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(STDOUT, [ + "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"); } @@ -1242,6 +1286,261 @@ 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 = []; + $workerStdins = []; + $workerStdouts = []; + $workerStderrs = []; + + 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. + + $testDirs = []; + + foreach ($test_files as $file) { + $dirSeparator = strrpos($file, DIRECTORY_SEPARATOR); + if ($dirSeparator !== FALSE) { + $testDirs[substr($file, 0, $dirSeparator)][] = $file; + } else { + $testDirs[""][] = $file; + } + } + + $testDirsToGo = array_values($testDirs); + // 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 :) + usort($testDirsToGo, 'count'); + $testsInProgress = 0; + + echo "Isolated ", count($testDirsToGo), " directories to be tested in parallel.\n"; + + echo "Spawning workers… "; + for ($i = 1; $i <= $workers; $i++) { + $proc = proc_open( + $thisPHP . ' ' . escapeshellarg($thisScript), + [ + 0 => ['pipe', 'r'], + 1 => ['pipe', 'w'], + 2 => ['pipe', 'w'] + ], + $pipes, + NULL, + $_ENV + [ + "TEST_PHP_WORKER" => $i + ], + [ + "suppress_errors" => TRUE + ] + ); + if ($proc === FALSE) { + kill_children($workerProcs); + error("Failed to spawn worker $i"); + } + $workerProcs[$i] = $proc; + + $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($pipes[0], 5); + if (fwrite($pipes[0], $greeting) === FALSE) { + kill_children($workerProcs); + error("Failed to send greeting to worker $i."); + } + + stream_set_timeout($pipes[1], 5); + $rawReply = fgets($pipes[1]); + 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'"); + } + + $workerStdins[$i] = $pipes[0]; + $workerStdouts[$i] = $pipes[1]; + $workerStderrs[$i] = $pipes[2]; + + echo "$i "; + } + echo "… done!\n"; + echo "====⚡️===========================================================⚡️====\n"; + echo "\n"; + + while ($testDirsToGo || $testsInProgress) { + $toRead = array_values($workerStdouts); + $toWrite = NULL; + $toExcept = NULL; + if (stream_select($toRead, $toWrite, $toExcept, 0, 50 * 1000)) { + foreach ($toRead as $workerStdout) { + $i = array_search($workerStdout, $workerStdouts); + stream_set_timeout($workerStdout, 0); + stream_set_blocking($workerStdout, FALSE); + while (FALSE !== ($rawMessage = fgets($workerStdout))) { + $message = unserialize(base64_decode($rawMessage)); + if (!$message) { + kill_children($workerProcs); + $stuff = fread($workerStdout, 65536); + error("Could not decode message from worker $i: '$rawMessage$stuff'"); + } + + switch ($message["type"]) { + case "ready": + if ($testDir = array_pop($testDirsToGo)) { + $testsInProgress += count($testDir); + send_message($workerStdins[$i], [ + "type" => "run_tests", + "test_files" => $testDir, + "env" => $env, + "redir_tested" => $redir_tested + ]); + } else { + proc_terminate($workerProcs[$i]); + unset($workerProcs[$i]); + unset($workerStdins[$i]); + unset($workerStdouts[$i]); + unset($workerStderrs[$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++; + $testsInProgress--; + 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; + default: + kill_children($workerProcs); + error("Unrecognised message type '$message[type]' from worker $i"); + } + } + } + } +escape: + } + + clear_show_test(); + + if ($test_idx !== $test_cnt) { + error("Somehow, " . ($test_cnt - $test_idx) . " tests never got executed. This is probably a bug in parallel test execution."); + } + kill_children($workerProcs); +} + +function send_message($stream, array $message) { + fwrite($stream, base64_encode(serialize($message)) . "\n"); +} + +function kill_children(array $children) { + foreach ($children as $child) { + if ($child) { + proc_terminate($child); + } + } +} + +function run_worker() { + global $workerID; + + @unlink(__DIR__ . "/../worker$workerID.log"); + ini_set("error_log", __DIR__ . "/../worker$workerID.log"); + + $greeting = fgets(STDIN); + $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 !== "GLOBALS") { + $GLOBALS[$var] = $value; + } + } + foreach ($greeting["constants"] as $const => $value) { + define($const, $value); + } + + send_message(STDOUT, [ + "type" => "hello_reply", + "workerID" => $workerID + ]); + + send_message(STDOUT, [ + "type" => "ready" + ]); + + while (($command = fgets(STDIN))) { + $command = unserialize(base64_decode($command)); + + switch ($command["type"]) { + case "run_tests": + run_all_tests($command["test_files"], $command["env"], $command["redir_tested"]); + send_message(STDOUT, [ + "type" => "ready" + ]); + break; + default: + send_message(STDOUT, [ + "type" => "error", + "msg" => "Unrecognised message type: $command[type]" + ]); + die; + } + } + + die; +} + // // Show file or result block // @@ -1273,6 +1572,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; @@ -1480,7 +1780,7 @@ function run_test($php, $file, $env) } } - if (!$SHOW_ONLY_GROUPS) { + if (!$SHOW_ONLY_GROUPS && !$workerID) { show_test($test_idx, $shortname); } @@ -2655,7 +2955,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"); @@ -2663,15 +2963,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"); @@ -2679,9 +2978,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(); } } @@ -2696,15 +2994,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) { From 5b7c0b7114f329f870a28490e5ceb4e1c83d16c6 Mon Sep 17 00:00:00 2001 From: Andrea Faulds Date: Sun, 8 Oct 2017 15:01:55 +0100 Subject: [PATCH 03/23] Fix count() sorting --- run-tests.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/run-tests.php b/run-tests.php index 2ba831cee3f4..fe1db269679d 100644 --- a/run-tests.php +++ b/run-tests.php @@ -1325,7 +1325,9 @@ function run_all_tests_parallel($test_files, $env, $redir_tested) { // 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 :) - usort($testDirsToGo, 'count'); + usort($testDirsToGo, function ($a, $b) { + return count($a) - count($b); + }); $testsInProgress = 0; echo "Isolated ", count($testDirsToGo), " directories to be tested in parallel.\n"; From 30179495fe0f15dc546c742efdd36091bc2b1269 Mon Sep 17 00:00:00 2001 From: Andrea Faulds Date: Sun, 8 Oct 2017 19:09:32 +0100 Subject: [PATCH 04/23] Add special error handler for run-tests workers --- run-tests.php | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/run-tests.php b/run-tests.php index fe1db269679d..79d9963b7c3e 100644 --- a/run-tests.php +++ b/run-tests.php @@ -1461,6 +1461,26 @@ function run_all_tests_parallel($test_files, $env, $redir_tested) { 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"); @@ -1496,6 +1516,15 @@ function run_worker() { @unlink(__DIR__ . "/../worker$workerID.log"); ini_set("error_log", __DIR__ . "/../worker$workerID.log"); + set_error_handler(function ($errno, $errstr, $errfile, $errline) { + if (error_reporting() & $errno) { + send_message(STDOUT, compact('errno', 'errstr', 'errfile', 'errline') + [ + 'type' => 'php_error' + ]); + } + + return true; + }); $greeting = fgets(STDIN); $greeting = unserialize(base64_decode($greeting)) or die("Could not decode greeting\n"); From a93e8153041e81e1bf8ac0284e65022fbd249e83 Mon Sep 17 00:00:00 2001 From: Andrea Faulds Date: Sun, 8 Oct 2017 20:36:18 +0100 Subject: [PATCH 05/23] Add experimental two- and four-worker parallel testing to Travis matrix --- .travis.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 64f4d5a3c933..f2446187658b 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=1 ENABLE_DEBUG=1 PARALLEL_TESTS=0 + - ENABLE_MAINTAINER_ZTS=1 ENABLE_DEBUG=1 PARALLEL_TESTS=2 + - ENABLE_MAINTAINER_ZTS=1 ENABLE_DEBUG=1 PARALLEL_TESTS=4 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 From 67ad56423b07669418c1c4857f979ce33041705c Mon Sep 17 00:00:00 2001 From: Andrea Faulds Date: Sun, 8 Oct 2017 21:11:25 +0100 Subject: [PATCH 06/23] Additional sanity checks --- run-tests.php | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/run-tests.php b/run-tests.php index 79d9963b7c3e..c5be494ee9a8 100644 --- a/run-tests.php +++ b/run-tests.php @@ -1399,13 +1399,18 @@ function run_all_tests_parallel($test_files, $env, $redir_tested) { echo "====⚡️===========================================================⚡️====\n"; echo "\n"; - while ($testDirsToGo || $testsInProgress) { +escape: + while ($testDirsToGo || ($testsInProgress > 0)) { $toRead = array_values($workerStdouts); $toWrite = NULL; $toExcept = NULL; if (stream_select($toRead, $toWrite, $toExcept, 0, 50 * 1000)) { foreach ($toRead as $workerStdout) { $i = array_search($workerStdout, $workerStdouts); + if ($i === FALSE) { + kill_children($workerProcs); + error("Could not find worker stdout in array of worker stdouts, THIS SHOULD NOT HAPPEN."); + } stream_set_timeout($workerStdout, 0); stream_set_blocking($workerStdout, FALSE); while (FALSE !== ($rawMessage = fgets($workerStdout))) { @@ -1488,11 +1493,15 @@ function run_all_tests_parallel($test_files, $env, $redir_tested) { } } } -escape: } clear_show_test(); + if ($testsInProgress < 0) { + kill_children($workerProcs); + error("$testsInProgress tests in progress, THIS SHOULD NOT HAPPEN."); + } + if ($test_idx !== $test_cnt) { error("Somehow, " . ($test_cnt - $test_idx) . " tests never got executed. This is probably a bug in parallel test execution."); } From ea5f8570a45605210b55258cd76e280e9d4072ec Mon Sep 17 00:00:00 2001 From: Andrea Faulds Date: Sun, 8 Oct 2017 22:06:14 +0100 Subject: [PATCH 07/23] Track directories, not test files --- run-tests.php | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/run-tests.php b/run-tests.php index c5be494ee9a8..c2d250cde9d3 100644 --- a/run-tests.php +++ b/run-tests.php @@ -1328,7 +1328,7 @@ function run_all_tests_parallel($test_files, $env, $redir_tested) { usort($testDirsToGo, function ($a, $b) { return count($a) - count($b); }); - $testsInProgress = 0; + $testDirsInProgress = 0; echo "Isolated ", count($testDirsToGo), " directories to be tested in parallel.\n"; @@ -1400,7 +1400,7 @@ function run_all_tests_parallel($test_files, $env, $redir_tested) { echo "\n"; escape: - while ($testDirsToGo || ($testsInProgress > 0)) { + while ($testDirsToGo || ($testDirsInProgress > 0)) { $toRead = array_values($workerStdouts); $toWrite = NULL; $toExcept = NULL; @@ -1422,9 +1422,12 @@ function run_all_tests_parallel($test_files, $env, $redir_tested) { } switch ($message["type"]) { + case "dir_finished": + $testDirsInProgress--; + // intentional fall-through case "ready": if ($testDir = array_pop($testDirsToGo)) { - $testsInProgress += count($testDir); + $testDirsInProgress++; send_message($workerStdins[$i], [ "type" => "run_tests", "test_files" => $testDir, @@ -1446,7 +1449,6 @@ function run_all_tests_parallel($test_files, $env, $redir_tested) { $PHP_FAILED_TESTS[$category] = array_merge($PHP_FAILED_TESTS[$category], $tests); } $test_idx++; - $testsInProgress--; clear_show_test(); echo $resultText; show_test($test_idx, "⚡️[" . count($workerProcs) . "/$workers concurrent test workers running]⚡️"); @@ -1497,15 +1499,15 @@ function run_all_tests_parallel($test_files, $env, $redir_tested) { clear_show_test(); - if ($testsInProgress < 0) { - kill_children($workerProcs); - error("$testsInProgress tests in progress, THIS SHOULD NOT HAPPEN."); + kill_children($workerProcs); + + if ($testDirsInProgress < 0) { + error("$testDirsInProgress test directories “in progress”, which is less than zero. THIS SHOULD NOT HAPPEN."); } if ($test_idx !== $test_cnt) { error("Somehow, " . ($test_cnt - $test_idx) . " tests never got executed. This is probably a bug in parallel test execution."); } - kill_children($workerProcs); } function send_message($stream, array $message) { @@ -1566,7 +1568,7 @@ function run_worker() { case "run_tests": run_all_tests($command["test_files"], $command["env"], $command["redir_tested"]); send_message(STDOUT, [ - "type" => "ready" + "type" => "dir_finished" ]); break; default: From d94f3991106b32baf41226cb1cfb8ad1df4cc898 Mon Sep 17 00:00:00 2001 From: Andrea Faulds Date: Sun, 8 Oct 2017 23:11:55 +0100 Subject: [PATCH 08/23] Don't check $test_idx == $test_cnt, REDIR means they don't match --- run-tests.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/run-tests.php b/run-tests.php index c2d250cde9d3..277eea93d728 100644 --- a/run-tests.php +++ b/run-tests.php @@ -1504,10 +1504,6 @@ function run_all_tests_parallel($test_files, $env, $redir_tested) { if ($testDirsInProgress < 0) { error("$testDirsInProgress test directories “in progress”, which is less than zero. THIS SHOULD NOT HAPPEN."); } - - if ($test_idx !== $test_cnt) { - error("Somehow, " . ($test_cnt - $test_idx) . " tests never got executed. This is probably a bug in parallel test execution."); - } } function send_message($stream, array $message) { From 24b68345041fd9747a0e4e50bb0f8f5879e52202 Mon Sep 17 00:00:00 2001 From: Andrea Faulds Date: Sun, 8 Oct 2017 23:35:00 +0100 Subject: [PATCH 09/23] Don't repeatedly mark pipes non-blocking, once is enough --- run-tests.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/run-tests.php b/run-tests.php index 277eea93d728..31c268fa417c 100644 --- a/run-tests.php +++ b/run-tests.php @@ -1389,6 +1389,9 @@ function run_all_tests_parallel($test_files, $env, $redir_tested) { error("Greeting reply from worker $i unexpected or could not be decoded: '$rawReply'"); } + stream_set_timeout($pipes[1], 0); + stream_set_blocking($pipes[1], FALSE); + $workerStdins[$i] = $pipes[0]; $workerStdouts[$i] = $pipes[1]; $workerStderrs[$i] = $pipes[2]; @@ -1411,8 +1414,6 @@ function run_all_tests_parallel($test_files, $env, $redir_tested) { kill_children($workerProcs); error("Could not find worker stdout in array of worker stdouts, THIS SHOULD NOT HAPPEN."); } - stream_set_timeout($workerStdout, 0); - stream_set_blocking($workerStdout, FALSE); while (FALSE !== ($rawMessage = fgets($workerStdout))) { $message = unserialize(base64_decode($rawMessage)); if (!$message) { From f31683e13c1aa4b4c98cabed65c79de6e91d7772 Mon Sep 17 00:00:00 2001 From: Andrea Faulds Date: Mon, 9 Oct 2017 00:04:01 +0100 Subject: [PATCH 10/23] Name and shame big test directories --- run-tests.php | 32 +++++++++++++++++++++++++++----- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/run-tests.php b/run-tests.php index 31c268fa417c..fa5c706a36f0 100644 --- a/run-tests.php +++ b/run-tests.php @@ -1309,29 +1309,51 @@ function run_all_tests_parallel($test_files, $env, $redir_tested) { // Therefore, we divide the test set into directories and test multiple // directories at once, but not multiple tests within them. - $testDirs = []; + $testDirsToGo = []; foreach ($test_files as $file) { $dirSeparator = strrpos($file, DIRECTORY_SEPARATOR); if ($dirSeparator !== FALSE) { - $testDirs[substr($file, 0, $dirSeparator)][] = $file; + $testDirsToGo[substr($file, 0, $dirSeparator)][] = $file; } else { - $testDirs[""][] = $file; + $testDirsToGo[""][] = $file; } } - $testDirsToGo = array_values($testDirs); // 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 :) - usort($testDirsToGo, function ($a, $b) { + 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 .= "$dir: " . count($shame) . " files\n"; + } + + echo << Date: Mon, 9 Oct 2017 00:44:46 +0100 Subject: [PATCH 11/23] Add per-directory flag to enable parallelisation --- run-tests.php | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/run-tests.php b/run-tests.php index fa5c706a36f0..381b554a7ddf 100644 --- a/run-tests.php +++ b/run-tests.php @@ -1320,6 +1320,23 @@ function run_all_tests_parallel($test_files, $env, $redir_tested) { } } + // 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. @@ -1339,15 +1356,15 @@ function run_all_tests_parallel($test_files, $env, $redir_tested) { if ($shamedDirs) { $shameList = ""; foreach ($shamedDirs as $dir => $shame) { - $shameList .= "$dir: " . count($shame) . " files\n"; + $shameList .= "\n$dir: " . count($shame) . " files"; } echo << Date: Mon, 9 Oct 2017 01:21:43 +0100 Subject: [PATCH 12/23] Flag low-hanging-fruit dirs with !CAN_BE_PARALLELISED --- Zend/tests/!CAN_BE_PARALLELISED | 0 Zend/tests/traits/!CAN_BE_PARALLELISED | 0 ext/date/tests/!CAN_BE_PARALLELISED | 0 ext/dom/tests/!CAN_BE_PARALLELISED | 0 ext/pcre/tests/!CAN_BE_PARALLELISED | 0 ext/reflection/tests/!CAN_BE_PARALLELISED | 0 ext/standard/tests/math/!CAN_BE_PARALLELISED | 0 ext/standard/tests/serialize/!CAN_BE_PARALLELISED | 0 ext/standard/tests/strings/!CAN_BE_PARALLELISED | 0 tests/classes/!CAN_BE_PARALLELISED | 0 tests/lang/!CAN_BE_PARALLELISED | 0 11 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 Zend/tests/!CAN_BE_PARALLELISED create mode 100644 Zend/tests/traits/!CAN_BE_PARALLELISED create mode 100644 ext/date/tests/!CAN_BE_PARALLELISED create mode 100644 ext/dom/tests/!CAN_BE_PARALLELISED create mode 100644 ext/pcre/tests/!CAN_BE_PARALLELISED create mode 100644 ext/reflection/tests/!CAN_BE_PARALLELISED create mode 100644 ext/standard/tests/math/!CAN_BE_PARALLELISED create mode 100644 ext/standard/tests/serialize/!CAN_BE_PARALLELISED create mode 100644 ext/standard/tests/strings/!CAN_BE_PARALLELISED create mode 100644 tests/classes/!CAN_BE_PARALLELISED create mode 100644 tests/lang/!CAN_BE_PARALLELISED 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/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/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 From 03ce2a31a3bc91cb36ca456c23324c70f4c7e22c Mon Sep 17 00:00:00 2001 From: Andrea Faulds Date: Wed, 11 Oct 2017 00:35:44 +0100 Subject: [PATCH 13/23] Use 10s select() timeout --- run-tests.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/run-tests.php b/run-tests.php index 381b554a7ddf..5a9f06d07201 100644 --- a/run-tests.php +++ b/run-tests.php @@ -1446,7 +1446,7 @@ function run_all_tests_parallel($test_files, $env, $redir_tested) { $toRead = array_values($workerStdouts); $toWrite = NULL; $toExcept = NULL; - if (stream_select($toRead, $toWrite, $toExcept, 0, 50 * 1000)) { + if (stream_select($toRead, $toWrite, $toExcept, 10, 0)) { foreach ($toRead as $workerStdout) { $i = array_search($workerStdout, $workerStdouts); if ($i === FALSE) { From f6794e5ea9e28dee50a50293a5d5bea462c145e8 Mon Sep 17 00:00:00 2001 From: Andrea Faulds Date: Wed, 11 Oct 2017 20:18:47 +0100 Subject: [PATCH 14/23] Use sockets on Windows --- run-tests.php | 109 ++++++++++++++++++++++++++++++++++---------------- 1 file changed, 75 insertions(+), 34 deletions(-) diff --git a/run-tests.php b/run-tests.php index 5a9f06d07201..bae29ca87a33 100644 --- a/run-tests.php +++ b/run-tests.php @@ -1232,7 +1232,7 @@ 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, $PHP_FAILED_TESTS, $workers, $workerID; + global $test_results, $failed_tests_file, $result_tests_file, $php, $test_idx, $PHP_FAILED_TESTS, $workers, $workerID, $workerOutput; if ($workers !== null && !$workerID) { run_all_tests_parallel($test_files, $env, $redir_tested); @@ -1263,7 +1263,7 @@ function run_all_tests($test_files, $env, $redir_tested = null) if (!is_array($name) && $result != 'REDIR') { if ($workerID) { - send_message(STDOUT, [ + send_message($workerOutput, [ "type" => "test_result", "name" => $name, "index" => $index, @@ -1295,9 +1295,8 @@ function run_all_tests_parallel($test_files, $env, $redir_tested) { $thisScript = __FILE__; $workerProcs = []; - $workerStdins = []; - $workerStdouts = []; - $workerStderrs = []; + $workerInputs = []; + $workerOutputs = []; echo "====⚡️===========================================================⚡️====\n"; echo "====⚡️==== WELCOME TO THE FUTURE: run-tests PARALLEL EDITION ====⚡️====\n"; @@ -1372,6 +1371,16 @@ function run_all_tests_parallel($test_files, $env, $redir_tested) { } echo "Spawning workers… "; + + // Windows pipes can't be non-blocking, so use sockets there instead + $useSockets = (PHP_OS === "WINNT"); + + if ($useSockets) { + // IPv6 because nobody uses it, so less chance of collisions, right? ;) + $port = (ord('<') << 8) + ord('?'); + $listenSock = stream_socket_server("tcp://[::1]:$port") or error("Couldn't create socket on [::1]:$port."); + } + for ($i = 1; $i <= $workers; $i++) { $proc = proc_open( $thisPHP . ' ' . escapeshellarg($thisScript), @@ -1384,7 +1393,9 @@ function run_all_tests_parallel($test_files, $env, $redir_tested) { NULL, $_ENV + [ "TEST_PHP_WORKER" => $i - ], + ] + (!$useSockets ? [] : [ + "TEST_PHP_PORT" => $port + ]), [ "suppress_errors" => TRUE ] @@ -1395,6 +1406,19 @@ function run_all_tests_parallel($test_files, $env, $redir_tested) { } $workerProcs[$i] = $proc; + if ($useSockets) { + $workerSock = stream_socket_accept($listenSock, 5); + if ($workerSock === FALSE) { + kill_children($workerProcs); + error("Failed to accept connection from worker $i"); + } + $workerInput = $workerSock; + $workerOutput = $workerSock; + } else { + $workerInput = $pipes[0]; + $workerOutput = $pipes[1]; + } + $greeting = base64_encode(serialize([ "type" => "hello", "workerID" => $i, @@ -1409,14 +1433,14 @@ function run_all_tests_parallel($test_files, $env, $redir_tested) { ] ])) . "\n"; - stream_set_timeout($pipes[0], 5); - if (fwrite($pipes[0], $greeting) === FALSE) { + stream_set_timeout($workerInput, 5); + if (fwrite($workerInput, $greeting) === FALSE) { kill_children($workerProcs); error("Failed to send greeting to worker $i."); } - stream_set_timeout($pipes[1], 5); - $rawReply = fgets($pipes[1]); + stream_set_timeout($workerOutput, 5); + $rawReply = fgets($workerOutput); if ($rawReply === FALSE) { kill_children($workerProcs); error("Failed to read greeting reply from worker $i."); @@ -1428,12 +1452,11 @@ function run_all_tests_parallel($test_files, $env, $redir_tested) { error("Greeting reply from worker $i unexpected or could not be decoded: '$rawReply'"); } - stream_set_timeout($pipes[1], 0); - stream_set_blocking($pipes[1], FALSE); + stream_set_timeout($workerOutput, 0); + stream_set_blocking($workerOutput, FALSE); - $workerStdins[$i] = $pipes[0]; - $workerStdouts[$i] = $pipes[1]; - $workerStderrs[$i] = $pipes[2]; + $workerInputs[$i] = $workerInput; + $workerOutputs[$i] = $workerOutput; echo "$i "; } @@ -1443,21 +1466,21 @@ function run_all_tests_parallel($test_files, $env, $redir_tested) { escape: while ($testDirsToGo || ($testDirsInProgress > 0)) { - $toRead = array_values($workerStdouts); + $toRead = array_values($workerOutputs); $toWrite = NULL; $toExcept = NULL; if (stream_select($toRead, $toWrite, $toExcept, 10, 0)) { - foreach ($toRead as $workerStdout) { - $i = array_search($workerStdout, $workerStdouts); + foreach ($toRead as $workerOutput) { + $i = array_search($workerOutput, $workerOutputs); 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($workerStdout))) { + while (FALSE !== ($rawMessage = fgets($workerOutput))) { $message = unserialize(base64_decode($rawMessage)); if (!$message) { kill_children($workerProcs); - $stuff = fread($workerStdout, 65536); + $stuff = fread($workerOutput, 65536); error("Could not decode message from worker $i: '$rawMessage$stuff'"); } @@ -1468,7 +1491,7 @@ function run_all_tests_parallel($test_files, $env, $redir_tested) { case "ready": if ($testDir = array_pop($testDirsToGo)) { $testDirsInProgress++; - send_message($workerStdins[$i], [ + send_message($workerInputs[$i], [ "type" => "run_tests", "test_files" => $testDir, "env" => $env, @@ -1477,9 +1500,8 @@ function run_all_tests_parallel($test_files, $env, $redir_tested) { } else { proc_terminate($workerProcs[$i]); unset($workerProcs[$i]); - unset($workerStdins[$i]); - unset($workerStdouts[$i]); - unset($workerStderrs[$i]); + unset($workerInputs[$i]); + unset($workerOutputs[$i]); goto escape; } break; @@ -1547,7 +1569,10 @@ function run_all_tests_parallel($test_files, $env, $redir_tested) { } 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) { @@ -1559,13 +1584,20 @@ function kill_children(array $children) { } function run_worker() { - global $workerID; + global $workerID, $workerInput, $workerOutput; + + if (getenv("TEST_PHP_PORT")) { + $useSockets = true; + $port = (int)getenv("TEST_PHP_PORT"); + } else { + $useSockets = false; + } @unlink(__DIR__ . "/../worker$workerID.log"); ini_set("error_log", __DIR__ . "/../worker$workerID.log"); - set_error_handler(function ($errno, $errstr, $errfile, $errline) { + set_error_handler(function ($errno, $errstr, $errfile, $errline) use (&$workerOutput) { if (error_reporting() & $errno) { - send_message(STDOUT, compact('errno', 'errstr', 'errfile', 'errline') + [ + send_message($workerOutput, compact('errno', 'errstr', 'errfile', 'errline') + [ 'type' => 'php_error' ]); } @@ -1573,14 +1605,23 @@ function run_worker() { return true; }); - $greeting = fgets(STDIN); + if ($useSockets) { + $sock = stream_socket_client("tcp://[::1]:$port", $_, $_, 5) or error("Couldn't connect to [::1]:$port"); + $workerInput = $sock; + $workerOutput = $sock; + } else { + $workerInput = STDIN; + $workerOutput = STDOUT; + } + + $greeting = fgets($workerInput); $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 !== "GLOBALS") { + if ($var !== "workerID" && $var !== "workerInput" && $var !== "workerOutput" && $var !== "GLOBALS") { $GLOBALS[$var] = $value; } } @@ -1588,27 +1629,27 @@ function run_worker() { define($const, $value); } - send_message(STDOUT, [ + send_message($workerOutput, [ "type" => "hello_reply", "workerID" => $workerID ]); - send_message(STDOUT, [ + send_message($workerOutput, [ "type" => "ready" ]); - while (($command = fgets(STDIN))) { + while (($command = fgets($workerInput))) { $command = unserialize(base64_decode($command)); switch ($command["type"]) { case "run_tests": run_all_tests($command["test_files"], $command["env"], $command["redir_tested"]); - send_message(STDOUT, [ + send_message($workerOutput, [ "type" => "dir_finished" ]); break; default: - send_message(STDOUT, [ + send_message($workerOutput, [ "type" => "error", "msg" => "Unrecognised message type: $command[type]" ]); From 1fa336bc33a96e9607bbacb68fb1aea969ddf59e Mon Sep 17 00:00:00 2001 From: Andrea Faulds Date: Tue, 17 Oct 2017 20:54:28 +0100 Subject: [PATCH 15/23] Use dynamically-allocated port --- run-tests.php | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/run-tests.php b/run-tests.php index bae29ca87a33..a4c71fb1b7bb 100644 --- a/run-tests.php +++ b/run-tests.php @@ -1376,9 +1376,16 @@ function run_all_tests_parallel($test_files, $env, $redir_tested) { $useSockets = (PHP_OS === "WINNT"); if ($useSockets) { - // IPv6 because nobody uses it, so less chance of collisions, right? ;) - $port = (ord('<') << 8) + ord('?'); - $listenSock = stream_socket_server("tcp://[::1]:$port") or error("Couldn't create socket on [::1]:$port."); + $listenSock = stream_socket_server("tcp://127.0.0.1:0") or error("Couldn't create socket on localhost."); + $sockName = stream_socket_get_name($listenSock, false); + // PHP is terrible and returns IPv6 addresses not enclosed by [] + $portPos = strrpos($sockName, ":"); + $sockHost = substr($sockName, 0, $portPos); + if (FALSE !== strpos($sockHost, ":")) { + $sockHost = "[$sockHost]"; + } + $sockPort = substr($sockName, $portPos + 1); + $sockUri = "tcp://$sockHost:$sockPort"; } for ($i = 1; $i <= $workers; $i++) { @@ -1394,7 +1401,7 @@ function run_all_tests_parallel($test_files, $env, $redir_tested) { $_ENV + [ "TEST_PHP_WORKER" => $i ] + (!$useSockets ? [] : [ - "TEST_PHP_PORT" => $port + "TEST_PHP_URI" => $sockUri ]), [ "suppress_errors" => TRUE @@ -1586,9 +1593,9 @@ function kill_children(array $children) { function run_worker() { global $workerID, $workerInput, $workerOutput; - if (getenv("TEST_PHP_PORT")) { + if (getenv("TEST_PHP_URI")) { $useSockets = true; - $port = (int)getenv("TEST_PHP_PORT"); + $sockUri = getenv("TEST_PHP_URI"); } else { $useSockets = false; } @@ -1606,7 +1613,7 @@ function run_worker() { }); if ($useSockets) { - $sock = stream_socket_client("tcp://[::1]:$port", $_, $_, 5) or error("Couldn't connect to [::1]:$port"); + $sock = stream_socket_client($sockUri, $_, $_, 5) or error("Couldn't connect to $sockUri"); $workerInput = $sock; $workerOutput = $sock; } else { From d06e51383b21d5764333993c4584e50e84be1a78 Mon Sep 17 00:00:00 2001 From: Andrea Faulds Date: Tue, 17 Oct 2017 21:16:45 +0100 Subject: [PATCH 16/23] Use sockets for comms on all platforms --- run-tests.php | 119 +++++++++++++++++++------------------------------- 1 file changed, 46 insertions(+), 73 deletions(-) diff --git a/run-tests.php b/run-tests.php index a4c71fb1b7bb..ee33e39dd85a 100644 --- a/run-tests.php +++ b/run-tests.php @@ -1232,7 +1232,7 @@ 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, $PHP_FAILED_TESTS, $workers, $workerID, $workerOutput; + global $test_results, $failed_tests_file, $result_tests_file, $php, $test_idx, $PHP_FAILED_TESTS, $workers, $workerID, $workerSock; if ($workers !== null && !$workerID) { run_all_tests_parallel($test_files, $env, $redir_tested); @@ -1263,7 +1263,7 @@ function run_all_tests($test_files, $env, $redir_tested = null) if (!is_array($name) && $result != 'REDIR') { if ($workerID) { - send_message($workerOutput, [ + send_message($workerSock, [ "type" => "test_result", "name" => $name, "index" => $index, @@ -1295,8 +1295,7 @@ function run_all_tests_parallel($test_files, $env, $redir_tested) { $thisScript = __FILE__; $workerProcs = []; - $workerInputs = []; - $workerOutputs = []; + $workerSocks = []; echo "====⚡️===========================================================⚡️====\n"; echo "====⚡️==== WELCOME TO THE FUTURE: run-tests PARALLEL EDITION ====⚡️====\n"; @@ -1372,21 +1371,18 @@ function run_all_tests_parallel($test_files, $env, $redir_tested) { echo "Spawning workers… "; - // Windows pipes can't be non-blocking, so use sockets there instead - $useSockets = (PHP_OS === "WINNT"); - - if ($useSockets) { - $listenSock = stream_socket_server("tcp://127.0.0.1:0") or error("Couldn't create socket on localhost."); - $sockName = stream_socket_get_name($listenSock, false); - // PHP is terrible and returns IPv6 addresses not enclosed by [] - $portPos = strrpos($sockName, ":"); - $sockHost = substr($sockName, 0, $portPos); - if (FALSE !== strpos($sockHost, ":")) { - $sockHost = "[$sockHost]"; - } - $sockPort = substr($sockName, $portPos + 1); - $sockUri = "tcp://$sockHost:$sockPort"; + // We use sockets rather than STDIN/STDOUT for comms because on Windows, + // those can't be non-blocking for some reason. + $listenSock = stream_socket_server("tcp://127.0.0.1:0") or error("Couldn't create socket on localhost."); + $sockName = stream_socket_get_name($listenSock, false); + // PHP is terrible and returns IPv6 addresses not enclosed by [] + $portPos = strrpos($sockName, ":"); + $sockHost = substr($sockName, 0, $portPos); + if (FALSE !== strpos($sockHost, ":")) { + $sockHost = "[$sockHost]"; } + $sockPort = substr($sockName, $portPos + 1); + $sockUri = "tcp://$sockHost:$sockPort"; for ($i = 1; $i <= $workers; $i++) { $proc = proc_open( @@ -1399,10 +1395,9 @@ function run_all_tests_parallel($test_files, $env, $redir_tested) { $pipes, NULL, $_ENV + [ - "TEST_PHP_WORKER" => $i - ] + (!$useSockets ? [] : [ - "TEST_PHP_URI" => $sockUri - ]), + "TEST_PHP_WORKER" => $i, + "TEST_PHP_URI" => $sockUri, + ], [ "suppress_errors" => TRUE ] @@ -1413,17 +1408,10 @@ function run_all_tests_parallel($test_files, $env, $redir_tested) { } $workerProcs[$i] = $proc; - if ($useSockets) { - $workerSock = stream_socket_accept($listenSock, 5); - if ($workerSock === FALSE) { - kill_children($workerProcs); - error("Failed to accept connection from worker $i"); - } - $workerInput = $workerSock; - $workerOutput = $workerSock; - } else { - $workerInput = $pipes[0]; - $workerOutput = $pipes[1]; + $workerSock = stream_socket_accept($listenSock, 5); + if ($workerSock === FALSE) { + kill_children($workerProcs); + error("Failed to accept connection from worker $i"); } $greeting = base64_encode(serialize([ @@ -1440,14 +1428,13 @@ function run_all_tests_parallel($test_files, $env, $redir_tested) { ] ])) . "\n"; - stream_set_timeout($workerInput, 5); - if (fwrite($workerInput, $greeting) === FALSE) { + stream_set_timeout($workerSock, 5); + if (fwrite($workerSock, $greeting) === FALSE) { kill_children($workerProcs); error("Failed to send greeting to worker $i."); } - stream_set_timeout($workerOutput, 5); - $rawReply = fgets($workerOutput); + $rawReply = fgets($workerSock); if ($rawReply === FALSE) { kill_children($workerProcs); error("Failed to read greeting reply from worker $i."); @@ -1459,11 +1446,10 @@ function run_all_tests_parallel($test_files, $env, $redir_tested) { error("Greeting reply from worker $i unexpected or could not be decoded: '$rawReply'"); } - stream_set_timeout($workerOutput, 0); - stream_set_blocking($workerOutput, FALSE); + stream_set_timeout($workerSock, 0); + stream_set_blocking($workerSock, FALSE); - $workerInputs[$i] = $workerInput; - $workerOutputs[$i] = $workerOutput; + $workerSocks[$i] = $workerSock; echo "$i "; } @@ -1473,21 +1459,21 @@ function run_all_tests_parallel($test_files, $env, $redir_tested) { escape: while ($testDirsToGo || ($testDirsInProgress > 0)) { - $toRead = array_values($workerOutputs); + $toRead = array_values($workerSocks); $toWrite = NULL; $toExcept = NULL; if (stream_select($toRead, $toWrite, $toExcept, 10, 0)) { - foreach ($toRead as $workerOutput) { - $i = array_search($workerOutput, $workerOutputs); + 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($workerOutput))) { + while (FALSE !== ($rawMessage = fgets($workerSock))) { $message = unserialize(base64_decode($rawMessage)); if (!$message) { kill_children($workerProcs); - $stuff = fread($workerOutput, 65536); + $stuff = fread($workerSock, 65536); error("Could not decode message from worker $i: '$rawMessage$stuff'"); } @@ -1498,7 +1484,7 @@ function run_all_tests_parallel($test_files, $env, $redir_tested) { case "ready": if ($testDir = array_pop($testDirsToGo)) { $testDirsInProgress++; - send_message($workerInputs[$i], [ + send_message($workerSocks[$i], [ "type" => "run_tests", "test_files" => $testDir, "env" => $env, @@ -1507,8 +1493,7 @@ function run_all_tests_parallel($test_files, $env, $redir_tested) { } else { proc_terminate($workerProcs[$i]); unset($workerProcs[$i]); - unset($workerInputs[$i]); - unset($workerOutputs[$i]); + unset($workerSocks[$i]); goto escape; } break; @@ -1591,20 +1576,15 @@ function kill_children(array $children) { } function run_worker() { - global $workerID, $workerInput, $workerOutput; + global $workerID, $workerSock; - if (getenv("TEST_PHP_URI")) { - $useSockets = true; - $sockUri = getenv("TEST_PHP_URI"); - } else { - $useSockets = false; - } + $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 (&$workerOutput) { + set_error_handler(function ($errno, $errstr, $errfile, $errline) use (&$workerSock) { if (error_reporting() & $errno) { - send_message($workerOutput, compact('errno', 'errstr', 'errfile', 'errline') + [ + send_message($workerSock, compact('errno', 'errstr', 'errfile', 'errline') + [ 'type' => 'php_error' ]); } @@ -1612,23 +1592,16 @@ function run_worker() { return true; }); - if ($useSockets) { - $sock = stream_socket_client($sockUri, $_, $_, 5) or error("Couldn't connect to $sockUri"); - $workerInput = $sock; - $workerOutput = $sock; - } else { - $workerInput = STDIN; - $workerOutput = STDOUT; - } + $workerSock = stream_socket_client($sockUri, $_, $_, 5) or error("Couldn't connect to $sockUri"); - $greeting = fgets($workerInput); + $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 !== "workerInput" && $var !== "workerOutput" && $var !== "GLOBALS") { + if ($var !== "workerID" && $var !== "workerSock" && $var !== "GLOBALS") { $GLOBALS[$var] = $value; } } @@ -1636,27 +1609,27 @@ function run_worker() { define($const, $value); } - send_message($workerOutput, [ + send_message($workerSock, [ "type" => "hello_reply", "workerID" => $workerID ]); - send_message($workerOutput, [ + send_message($workerSock, [ "type" => "ready" ]); - while (($command = fgets($workerInput))) { + 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($workerOutput, [ + send_message($workerSock, [ "type" => "dir_finished" ]); break; default: - send_message($workerOutput, [ + send_message($workerSock, [ "type" => "error", "msg" => "Unrecognised message type: $command[type]" ]); From c0e67d6aea5b698c0fcdd0de6f6fea16815d860f Mon Sep 17 00:00:00 2001 From: Andrea Faulds Date: Thu, 19 Oct 2017 22:38:31 +0100 Subject: [PATCH 17/23] Add experimental two- and four-worker parallel testing to AppVeyor matrix --- .appveyor.yml | 9 +++++++++ appveyor/test_task.bat | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/.appveyor.yml b/.appveyor.yml index 39d84466c00a..e2c8b5028fc4 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -31,9 +31,18 @@ environment: matrix: - THREAD_SAFE: 0 OPCACHE: 0 + PARALLEL: - THREAD_SAFE: 1 OPCACHE: 1 INTRINSICS: AVX + PARALLEL: + - THREAD_SAFE: 1 + OPCACHE: 1 + INTRINSICS: AVX + PARALLEL: -j2 + - THREAD_SAFE: 1 + OPCACHE: 1 + PARALLEL: -j4 services: # the setup scripts have to be touched, once some other db version is used 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% From dea5ecfbbb8aca24791609c62ac96d747691b4d2 Mon Sep 17 00:00:00 2001 From: Andrea Faulds Date: Thu, 19 Oct 2017 22:46:08 +0100 Subject: [PATCH 18/23] Swap -j4 Travis tests for non-ZTS non-debug -j2 --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index f2446187658b..e645543ca7b6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -45,9 +45,9 @@ env: - REPORT_EXIT_STATUS=1 matrix: - 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 - - ENABLE_MAINTAINER_ZTS=1 ENABLE_DEBUG=1 PARALLEL_TESTS=4 before_script: - ccache --version From cabbfaabe675a885d2c9876ea4e83aef0ba3d139 Mon Sep 17 00:00:00 2001 From: Andrea Faulds Date: Fri, 20 Oct 2017 00:37:14 +0100 Subject: [PATCH 19/23] Swap -j4 AppVeyor tests for non-ZTS non-debug -j2 --- .appveyor.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.appveyor.yml b/.appveyor.yml index e2c8b5028fc4..afd5ee88795c 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -32,6 +32,9 @@ environment: - THREAD_SAFE: 0 OPCACHE: 0 PARALLEL: + - THREAD_SAFE: 0 + OPCACHE: 0 + PARALLEL: -j2 - THREAD_SAFE: 1 OPCACHE: 1 INTRINSICS: AVX @@ -40,9 +43,6 @@ environment: OPCACHE: 1 INTRINSICS: AVX PARALLEL: -j2 - - THREAD_SAFE: 1 - OPCACHE: 1 - PARALLEL: -j4 services: # the setup scripts have to be touched, once some other db version is used From 69b648a605ab2f862b7d57cb5bf12b12cb8f78f7 Mon Sep 17 00:00:00 2001 From: Andrea Faulds Date: Mon, 18 Dec 2017 22:33:54 +0000 Subject: [PATCH 20/23] Skip I/O capture tests in parallel mode --- run-tests.php | 1 + 1 file changed, 1 insertion(+) diff --git a/run-tests.php b/run-tests.php index ee33e39dd85a..343c27e60029 100644 --- a/run-tests.php +++ b/run-tests.php @@ -375,6 +375,7 @@ function run_tests() { 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; case 'r': case 'l': From b3289c161f48b835de78e9cc1aa3a4cc12b397ba Mon Sep 17 00:00:00 2001 From: Andrea Faulds Date: Thu, 5 Jul 2018 23:58:14 +0200 Subject: [PATCH 21/23] Don't choke on huge messages(?) --- run-tests.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/run-tests.php b/run-tests.php index 343c27e60029..fb2679d6b26e 100644 --- a/run-tests.php +++ b/run-tests.php @@ -1470,7 +1470,8 @@ function run_all_tests_parallel($test_files, $env, $redir_tested) { kill_children($workerProcs); error("Could not find worker stdout in array of worker stdouts, THIS SHOULD NOT HAPPEN."); } - while (FALSE !== ($rawMessage = fgets($workerSock))) { + // Some tests have *very* long output and fgets() seems to truncate to 8KiB unless we say we want more + while (FALSE !== ($rawMessage = fgets($workerSock, 64 * 1024))) { $message = unserialize(base64_decode($rawMessage)); if (!$message) { kill_children($workerProcs); From a15c6663d700c0bc902801306abcbdb7287d03d5 Mon Sep 17 00:00:00 2001 From: Andrea Faulds Date: Fri, 6 Jul 2018 00:30:08 +0200 Subject: [PATCH 22/23] Don't choke on huge messages, approach 2 --- run-tests.php | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/run-tests.php b/run-tests.php index fb2679d6b26e..269a5e4e635d 100644 --- a/run-tests.php +++ b/run-tests.php @@ -1458,6 +1458,8 @@ function run_all_tests_parallel($test_files, $env, $redir_tested) { echo "====⚡️===========================================================⚡️====\n"; echo "\n"; + $rawMessageBuffers = []; + escape: while ($testDirsToGo || ($testDirsInProgress > 0)) { $toRead = array_values($workerSocks); @@ -1470,8 +1472,17 @@ function run_all_tests_parallel($test_files, $env, $redir_tested) { kill_children($workerProcs); error("Could not find worker stdout in array of worker stdouts, THIS SHOULD NOT HAPPEN."); } - // Some tests have *very* long output and fgets() seems to truncate to 8KiB unless we say we want more - while (FALSE !== ($rawMessage = fgets($workerSock, 64 * 1024))) { + 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); From ca174904f5a5ca861d338bf1f93b28f16ac09e51 Mon Sep 17 00:00:00 2001 From: Andrea Faulds Date: Thu, 12 Jul 2018 21:03:38 +0200 Subject: [PATCH 23/23] blank commit to force rebuild