diff --git a/scripts/run-tests.sh b/scripts/run-tests.sh index db7600b2073b2c6e34454e27f1665139d5ec6d44..22615c1a966a3d0c81dcff69443c35ef633452e8 100755 --- a/scripts/run-tests.sh +++ b/scripts/run-tests.sh @@ -1,28 +1,103 @@ <?php // $Id$ - /** * @file * This script runs Drupal tests from command line. - * You can provide groups or class names of the tests you wish to run. - * For example: php scripts/run-functional-tests.sh Profile - * If no arguments are provided, the help text will print. */ -$test_names = array(); -$host = 'localhost'; -$path = ''; -$script = basename(array_shift($_SERVER['argv'])); -// XXX: is there a way to get the interpreter path dynamically? -$php = "/usr/bin/php"; +define('SIMPLETEST_SCRIPT_COLOR_PASS', 32); // Green. +define('SIMPLETEST_SCRIPT_COLOR_FAIL', 31); // Red. +define('SIMPLETEST_SCRIPT_COLOR_EXCEPTION', 33); // Brown. + +// Set defaults and get overrides. +list($args, $count) = simpletest_script_parse_args(); + +simpletest_script_init(); + +if ($args['help'] || $count == 0) { + simpletest_script_help(); + exit; +} + +if ($args['execute-batch']) { + simpletest_script_execute_batch(); +} + +// Bootstrap to perform initial validation or other opperations. +drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL); +if (!module_exists('simpletest')) { + simpletest_script_print_error("The simpletest module must be enabled before this script can run."); + exit; +} + +if ($args['clean']) { + // Clean up left-over times and directories. + simpletest_clean_environment(); + echo "\nEnvironment cleaned.\n"; + + // Get the status messages and print them. + $messages = array_pop(drupal_get_messages('status')); + foreach($messages as $text) { + echo " - " . $text . "\n"; + } + exit; +} + +// Load SimpleTest files. +$all_tests = simpletest_get_all_tests(); +$groups = simpletest_categorize_tests($all_tests); +$test_list = array(); + +if ($args['list']) { + // Display all availabe tests. + echo "\nAvailable test groups & classes\n"; + echo "-------------------------------\n\n"; + foreach ($groups as $group => $tests) { + echo $group . "\n"; + foreach ($tests as $class_name => $instance) { + $info = $instance->getInfo(); + echo " - " . $info['name'] . ' (' . $class_name . ')' . "\n"; + } + } + exit; +} + +$test_list = simpletest_script_get_test_list(); + +// If not in 'safe mode', increase the maximum execution time. +if (!ini_get('safe_mode')) { + set_time_limit(0); +} + +simpletest_script_reporter_init(); + +// Setup database for test results. +db_query('INSERT INTO {simpletest_test_id} VALUES (default)'); +$test_id = db_last_insert_id('simpletest_test_id', 'test_id'); + +// Execute tests. +simpletest_script_command($args['concurrency'], $test_id, implode(",", $test_list)); + +// Display results before database is cleared. +simpletest_script_reporter_display_results(); + +// Cleanup our test results. +db_query("DELETE FROM {simpletest} WHERE test_id = %d", $test_id); + + + +/** + * Print help text. + */ +function simpletest_script_help() { + global $args; -if (in_array('--help', $_SERVER['argv']) || empty($_SERVER['argv'])) { echo <<<EOF Run Drupal tests from the shell. -Usage: {$script} [OPTIONS] <tests> -Example: {$script} Profile +Usage: {$args['script']} [OPTIONS] <tests> +Example: {$args['script']} Profile All arguments are long options. @@ -46,6 +121,10 @@ --class Run tests identified by specific class names, instead of group names. + --color Output the rusults with color highlighting. + + --verbose Output detailed assertion messages in addition to summary. + <test1>[,<test2>[,<test3> ...]] One or more tests to be run. By default, these are interpreted @@ -57,124 +136,159 @@ be separated by commas. Ignored if --all is specified. To run this script you will normally invoke it from the root directory of your -Drupal installation as +Drupal installation as the webserver user, or root, with -php ./scripts/{$script} +php ./scripts/{$args['script']} \n EOF; - exit; } -$list = FALSE; -$clean = FALSE; -$all = FALSE; -$concurrency = 1; -$class_names = FALSE; -$test_names = array(); -$execute_batch = FALSE; -$test_id = NULL; - -while ($param = array_shift($_SERVER['argv'])) { - switch ($param) { - case '--list': - $list = TRUE; - break; - case '--url': - $url = array_shift($_SERVER['argv']); - $parsed = parse_url($url); - $host = $parsed['host']; - $path = $parsed['path']; - break; - case '--all': - $all = TRUE; - break; - case '--class': - $class_names = TRUE; - break; - case '--clean': - $clean = TRUE; - break; - case '--concurrency': - $concurrency = array_shift($_SERVER['argv']); - break; - case '--test-id': - $test_id = array_shift($_SERVER['argv']); - break; - case '--execute-batch': - $execute_batch = TRUE; - break; - default: - $test_names += explode(',', $param); - break; +/** + * Parse execution argument and ensure that all are valid. + * + * @return The list of arguments. + */ +function simpletest_script_parse_args() { + // Set default values. + $args = array( + 'script' => '', + 'help' => FALSE, + 'list' => FALSE, + 'clean' => FALSE, + 'url' => '', + 'concurrency' => 1, + 'all' => FALSE, + 'class' => FALSE, + 'color' => FALSE, + 'verbose' => FALSE, + 'test_names' => array(), + // Used internally. + 'test-id' => NULL, + 'execute-batch' => FALSE + ); + + // Override with set values. + $args['script'] = basename(array_shift($_SERVER['argv'])); + + $count = 0; + while ($arg = array_shift($_SERVER['argv'])) { + if (preg_match('/--(\S+)/', $arg, $matches)) { + // Argument found. + if (array_key_exists($matches[1], $args)) { + // Argument found in list. + $previous_arg = $matches[1]; + if (is_bool($args[$previous_arg])) { + $args[$matches[1]] = TRUE; + } + else { + $args[$matches[1]] = array_shift($_SERVER['argv']); + } + // Clear an extrenious values. + $args['test_names'] = array(); + $count++; + } + else { + // Argument not found in list. + simpletest_script_print_error("Unknown argument '$arg'."); + exit; + } + } + else { + // Values found without an argument should be test names. + $args['test_names'] += explode(',', $arg); + } } + return array($args, $count); } -$_SERVER['HTTP_HOST'] = $host; -$_SERVER['REMOTE_ADDR'] = '127.0.0.1'; -$_SERVER['SERVER_ADDR'] = '127.0.0.1'; -$_SERVER['SERVER_SOFTWARE'] = 'Apache'; -$_SERVER['SERVER_NAME'] = 'localhost'; -$_SERVER['REQUEST_URI'] = $path .'/'; -$_SERVER['SCRIPT_NAME'] = $path .'/index.php'; -$_SERVER['PHP_SELF'] = $path .'/index.php'; -$_SERVER['HTTP_USER_AGENT'] = 'Drupal command line'; - -chdir(realpath(dirname(__FILE__) . '/..')); -require_once './includes/bootstrap.inc'; - -if ($execute_batch) { - if (is_null($test_id)) { - echo "ERROR: --execute-batch should not be called interactively.\n"; +/** + * Initialize script variables and perform general setup requirements. + */ +function simpletest_script_init() { + global $args, $php; + + $host = 'localhost'; + $path = ''; + $php = "/usr/bin/php"; // TODO Get dynamically if possible. + + // Get url from arguments. + if (!empty($args['url'])) { + $parsed_url = parse_url($args['url']); + $host = $parsed_url['host']; + $path = $parsed_url['path']; + } + + $_SERVER['HTTP_HOST'] = $host; + $_SERVER['REMOTE_ADDR'] = '127.0.0.1'; + $_SERVER['SERVER_ADDR'] = '127.0.0.1'; + $_SERVER['SERVER_SOFTWARE'] = 'Apache'; + $_SERVER['SERVER_NAME'] = 'localhost'; + $_SERVER['REQUEST_URI'] = $path .'/'; + $_SERVER['SCRIPT_NAME'] = $path .'/index.php'; + $_SERVER['PHP_SELF'] = $path .'/index.php'; + $_SERVER['HTTP_USER_AGENT'] = 'Drupal command line'; + + chdir(realpath(dirname(__FILE__) . '/..')); + require_once './includes/bootstrap.inc'; +} + +/** + * Execute a batch of tests. + */ +function simpletest_script_execute_batch() { + global $args; + + if (is_null($args['test-id'])) { + simpletest_script_print_error("--execute-batch should not be called interactively."); exit; } - if ($concurrency == 1 || !function_exists('pcntl_fork')) { - // Fallback to mono-threaded execution - if (count($test_names) > 1) { - foreach($test_names as $test_class) { - // Note: we still need to execute each test in its separate Drupal environment - passthru($php . " ./scripts/run-tests.sh --url $url --concurrency 1 --test-id $test_id --execute-batch $test_class"); + if ($args['concurrency'] == 1 || !function_exists('pcntl_fork')) { + // Fallback to mono-threaded execution. + if (count($args['test_names']) > 1) { + foreach ($args['test_names'] as $test_class) { + // Execute each test in its separate Drupal environment. + simpletest_script_command(1, $args['test-id'], $test_class); } exit; } else { - // Execute an individual test - $test_class = array_shift($test_names); + // Execute an individual test. + $test_class = array_shift($args['test_names']); drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL); - simpletest_run_one_test($test_id, $test_class); + simpletest_script_run_one_test($args['test-id'], $test_class); exit; } } else { - // Multi-threaded execution + // Multi-threaded execution. $children = array(); - while (!empty($test_names) || !empty($children)) { - // Fork children - // Note: we can safely fork here, because Drupal is not bootstrapped yet - while(count($children) < $concurrency) { - if (empty($test_names)) break; + while (!empty($args['test_names']) || !empty($children)) { + // Fork children safely since Drupal is not bootstrapped yet. + while (count($children) < $concurrency) { + if (empty($args['test_names'])) break; $child = array(); - $child['test_class'] = $test_class = array_shift($test_names); + $child['test_class'] = $test_class = array_shift($args['test_names']); $child['pid'] = pcntl_fork(); if (!$child['pid']) { - // This is the child process, bootstrap and execute the test + // This is the child process, bootstrap and execute the test. drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL); - simpletest_run_one_test($test_id, $test_class); + simpletest_script_run_one_test($test_id, $test_class); exit; } else { - // Register our new child + // Register our new child. $children[] = $child; } } - // Wait for children every 200ms + // Wait for children every 200ms. usleep(200000); - // Check if some children finished - foreach($children as $cid => $child) { + // Check if some children finished. + foreach ($children as $cid => $child) { if (pcntl_waitpid($child['pid'], $status, WUNTRACED | WNOHANG)) { - // This particular child exited + // This particular child exited. unset($children[$cid]); } } @@ -183,153 +297,208 @@ } } -drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL); - -if (!module_exists('simpletest')) { - echo("ERROR: The simpletest module must be enabled before this script can run.\n"); - exit; -} +/** + * Run a single test (assume a Drupal bootstrapped environnement). + */ +function simpletest_script_run_one_test($test_id, $test_class) { + simpletest_get_all_tests(); + $test = new $test_class($test_id); + $test->run(); + $info = $test->getInfo(); -if ($clean) { - // Clean up left-over times and directories. - simpletest_clean_environment(); - // Get the status messages and print them. - $messages = array_pop(drupal_get_messages('status')); - foreach($messages as $text) { - echo(" - " . $text . "\n"); - } - exit; + $status = ((isset($test->_results['#fail']) && $test->_results['#fail'] > 0) + || (isset($test->_results['#exception']) && $test->_results['#exception'] > 0) ? 'fail' : 'pass'); + simpletest_script_print($info['name'] . ' ' . _simpletest_format_summary_line($test->_results) . "\n", simpletest_script_color_code($status)); } -// Run tests as user #1. -$GLOBALS['user'] = user_load(1); - -// Load simpletest files -$all_tests = simpletest_get_all_tests(); -$groups = simpletest_categorize_tests($all_tests); -$test_list = array(); +/** + * Execute a command to run batch of tests in separate process. + */ +function simpletest_script_command($concurrency, $test_id, $tests) { + global $args, $php; -if ($list) { - // Display all availabe tests. - echo("\nAvailable test groups\n---------------------\n\n"); - foreach ($groups as $group => $tests) { - echo($group . "\n"); - foreach ($tests as $class_name => $instance) { - $info = $instance->getInfo(); - echo " - " . $info['name'] . ' (' . $class_name . ')' . "\n"; - } + $command = "$php ./scripts/{$args['script']} --url {$args['url']}"; + if ($args['color']) { + $command .= ' --color'; } - exit; + $command .= " --concurrency $concurrency --test-id $test_id --execute-batch $tests"; + passthru($command); } -if ($all) { - $test_list = array_keys($all_tests); -} -else { - if ($class_names) { - // Use only valid class names - foreach ($test_names as $class_name) { - if (isset($all_tests[$class_name])) { - $test_list[] = $class_name; - } - } +/** + * Get list of tests based on arguments. If --all specfied then + * returns all available tests, otherwise reads list of tests. + * + * Will print error and exit if no valid tests were found. + * + * @return List of tests. + */ +function simpletest_script_get_test_list() { + global $args, $all_tests, $groups; + + $test_list = array(); + if ($args['all']) { + $test_list = array_keys($all_tests); } else { - // Resolve group names - foreach ($test_names as $group_name) { - if (isset($groups[$group_name])) { - foreach($groups[$group_name] as $class_name => $instance) { + if ($args['class_names']) { + // Check for valid class names. + foreach ($args['test_names'] as $class_name) { + if (isset($all_tests[$class_name])) { $test_list[] = $class_name; } } } + else { + // Check for valid group names and get all valid classes in group. + foreach ($args['test_names'] as $group_name) { + if (isset($groups[$group_name])) { + foreach($groups[$group_name] as $class_name => $instance) { + $test_list[] = $class_name; + } + } + } + } } -} -if (empty($test_list) && !$all) { - echo("ERROR: No valid tests were specified.\n"); - exit; + if (empty($test_list)) { + simpletest_script_print_error('No valid tests were specified.'); + exit; + } + return $test_list; } -// If not in 'safe mode', increase the maximum execution time: -if (!ini_get('safe_mode')) { - set_time_limit(0); -} +/** + * Initialize the reporter. + */ +function simpletest_script_reporter_init() { + global $args, $all_tests, $test_list; -echo "\n"; -echo "Drupal test run\n"; -echo "---------------\n"; -echo "\n"; + echo "\n"; + echo "Drupal test run\n"; + echo "---------------\n"; + echo "\n"; -// Tell the user about what tests are to be run. -if ($all) { - echo "All tests will run.\n\n"; -} -else { - echo "Tests to be run:\n"; - foreach ($test_list as $class_name) { - $info = $all_tests[$class_name]->getInfo(); - echo " - " . $info['name'] . ' (' . $class_name . ')' . "\n"; + // Tell the user about what tests are to be run. + if ($args['all']) { + echo "All tests will run.\n\n"; + } + else { + echo "Tests to be run:\n"; + foreach ($test_list as $class_name) { + $info = $all_tests[$class_name]->getInfo(); + echo " - " . $info['name'] . ' (' . $class_name . ')' . "\n"; + } + echo "\n"; } + + echo "Test run started: " . format_date(time(), 'long') . "\n"; + echo "\n"; + + echo "Test summary:\n"; + echo "-------------\n"; echo "\n"; } -echo "Test run started: " . format_date(time(), 'long') . "\n"; -echo "\n"; +/** + * Display test results. + */ +function simpletest_script_reporter_display_results() { + global $args, $test_id, $results_map; -db_query('INSERT INTO {simpletest_test_id} VALUES (default)'); -$test_id = db_last_insert_id('simpletest_test_id', 'test_id'); + echo "\n"; + echo "Test run ended: " . format_date(time(), 'long') . "\n"; + echo "\n"; -echo "Test summary:\n"; -echo "-------------\n"; -echo "\n"; - -// Now execute tests -passthru($php . " ./scripts/run-tests.sh --url $url --test-id $test_id --concurrency $concurrency --execute-batch " . implode(",", $test_list)); - -echo "\n"; -echo "Test run ended: " . format_date(time(), 'long') . "\n"; -echo "\n"; - -// Report results -echo "Detailed test results:\n"; -echo "----------------------\n"; -echo "\n"; - -$results_map = array( - 'pass' => 'Pass', - 'fail' => 'Fail', - 'exception' => 'Exception' -); - -$results = db_query("SELECT * FROM {simpletest} WHERE test_id = %d ORDER BY test_class, message_id", $test_id); -while($result = db_fetch_object($results)) { - if (isset($results_map[$result->status])) { - $data = array( - '[' . $results_map[$result->status] . ']', - $result->message, - $result->message_group, - basename($result->file), - $result->line, - $result->caller, + if ($args['verbose']) { + // Report results. + echo "Detailed test results:\n"; + echo "----------------------\n"; + echo "\n"; + + $results_map = array( + 'pass' => 'Pass', + 'fail' => 'Fail', + 'exception' => 'Exception' ); - echo implode("\t", $data) . "\n"; + + $results = db_query("SELECT * FROM {simpletest} WHERE test_id = %d ORDER BY test_class, message_id", $test_id); + $test_class = ''; + while($result = db_fetch_object($results)) { + if (isset($results_map[$result->status])) { + if ($result->test_class != $test_class) { + // Display test class every time results are for new test class. + echo "\n\n---- $result->test_class ----\n\n\n"; + $test_class = $result->test_class; + } + + simpletest_script_format_result($result); + } + } } } -// Cleanup our test results -db_query("DELETE FROM {simpletest} WHERE test_id = %d", $test_id); +/** + * Format the result so that it fits within the default 80 character + * terminal size. + * + * @param $result The result object to format. + */ +function simpletest_script_format_result($result) { + global $results_map, $color; + + $summary = sprintf("%-10.10s %-10.10s %-30.30s %-5.5s %-20.20s\n", + $results_map[$result->status], $result->message_group, basename($result->file), $result->line, $result->caller); + + simpletest_script_print($summary, simpletest_script_color_code($result->status)); -// Support function: + $lines = explode("\n", wordwrap(trim(strip_tags($result->message)), 76)); + foreach ($lines as $line) { + echo " $line\n"; + } +} /** - * Run a single test (assume a Drupal bootstrapped environnement). + * Print error message prefixed with " ERROR: " and displayed in fail color + * if color output is enabled. + * + * @param $message The message to print. */ -function simpletest_run_one_test($test_id, $test_class) { - simpletest_get_all_tests(); - $test = new $test_class($test_id); - $test->run(); - $info = $test->getInfo(); - echo $info['name'] . ' ' . _simpletest_format_summary_line($test->_results) . "\n"; +function simpletest_script_print_error($message) { + simpletest_script_print(" ERROR: $message\n", SIMPLETEST_SCRIPT_COLOR_FAIL); } +/** + * Print a message to the console, if color is enabled then the specified + * color code will be used. + * + * @param $message The message to print. + * @param $color_code The color code to use for coloring. + */ +function simpletest_script_print($message, $color_code) { + global $args; + if ($args['color']) { + echo "\033[" . $color_code . "m" . $message . "\033[0m"; + } + else { + echo $message; + } +} + +/** + * Get the color code associated with the specified status. + * + * @param $status The status string to get code for. + * @return Color code. + */ +function simpletest_script_color_code($status) { + switch ($status) { + case 'pass': + return SIMPLETEST_SCRIPT_COLOR_PASS; + case 'fail': + return SIMPLETEST_SCRIPT_COLOR_FAIL; + case 'exception': + return SIMPLETEST_SCRIPT_COLOR_EXCEPTION; + } + return 0; // Default formatting. +}