Skip to content
Snippets Groups Projects
Commit a4883506 authored by Dries Buytaert's avatar Dries Buytaert
Browse files

- Patch #254166 by boombatower: improve and clean-up the test script. Added...

- Patch #254166 by boombatower: improve and clean-up the test script.  Added color coding and allow people to control the verbosity.
parent f5645d67
No related branches found
No related tags found
2 merge requests!7452Issue #1797438. HTML5 validation is preventing form submit and not fully...,!789Issue #3210310: Adjust Database API to remove deprecated Drupal 9 code in Drupal 10
<?php <?php
// $Id$ // $Id$
/** /**
* @file * @file
* This script runs Drupal tests from command line. * 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(); define('SIMPLETEST_SCRIPT_COLOR_PASS', 32); // Green.
$host = 'localhost'; define('SIMPLETEST_SCRIPT_COLOR_FAIL', 31); // Red.
$path = ''; define('SIMPLETEST_SCRIPT_COLOR_EXCEPTION', 33); // Brown.
$script = basename(array_shift($_SERVER['argv']));
// XXX: is there a way to get the interpreter path dynamically? // Set defaults and get overrides.
$php = "/usr/bin/php"; 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 echo <<<EOF
Run Drupal tests from the shell. Run Drupal tests from the shell.
Usage: {$script} [OPTIONS] <tests> Usage: {$args['script']} [OPTIONS] <tests>
Example: {$script} Profile Example: {$args['script']} Profile
All arguments are long options. All arguments are long options.
...@@ -46,6 +121,10 @@ ...@@ -46,6 +121,10 @@
--class Run tests identified by specific class names, instead of group names. --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> ...]] <test1>[,<test2>[,<test3> ...]]
One or more tests to be run. By default, these are interpreted One or more tests to be run. By default, these are interpreted
...@@ -57,124 +136,159 @@ ...@@ -57,124 +136,159 @@
be separated by commas. Ignored if --all is specified. be separated by commas. Ignored if --all is specified.
To run this script you will normally invoke it from the root directory of your 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 \n
EOF; EOF;
exit;
} }
$list = FALSE; /**
$clean = FALSE; * Parse execution argument and ensure that all are valid.
$all = FALSE; *
$concurrency = 1; * @return The list of arguments.
$class_names = FALSE; */
$test_names = array(); function simpletest_script_parse_args() {
$execute_batch = FALSE; // Set default values.
$test_id = NULL; $args = array(
'script' => '',
while ($param = array_shift($_SERVER['argv'])) { 'help' => FALSE,
switch ($param) { 'list' => FALSE,
case '--list': 'clean' => FALSE,
$list = TRUE; 'url' => '',
break; 'concurrency' => 1,
case '--url': 'all' => FALSE,
$url = array_shift($_SERVER['argv']); 'class' => FALSE,
$parsed = parse_url($url); 'color' => FALSE,
$host = $parsed['host']; 'verbose' => FALSE,
$path = $parsed['path']; 'test_names' => array(),
break; // Used internally.
case '--all': 'test-id' => NULL,
$all = TRUE; 'execute-batch' => FALSE
break; );
case '--class':
$class_names = TRUE; // Override with set values.
break; $args['script'] = basename(array_shift($_SERVER['argv']));
case '--clean':
$clean = TRUE; $count = 0;
break; while ($arg = array_shift($_SERVER['argv'])) {
case '--concurrency': if (preg_match('/--(\S+)/', $arg, $matches)) {
$concurrency = array_shift($_SERVER['argv']); // Argument found.
break; if (array_key_exists($matches[1], $args)) {
case '--test-id': // Argument found in list.
$test_id = array_shift($_SERVER['argv']); $previous_arg = $matches[1];
break; if (is_bool($args[$previous_arg])) {
case '--execute-batch': $args[$matches[1]] = TRUE;
$execute_batch = TRUE; }
break; else {
default: $args[$matches[1]] = array_shift($_SERVER['argv']);
$test_names += explode(',', $param); }
break; // 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'; * Initialize script variables and perform general setup requirements.
$_SERVER['SERVER_ADDR'] = '127.0.0.1'; */
$_SERVER['SERVER_SOFTWARE'] = 'Apache'; function simpletest_script_init() {
$_SERVER['SERVER_NAME'] = 'localhost'; global $args, $php;
$_SERVER['REQUEST_URI'] = $path .'/';
$_SERVER['SCRIPT_NAME'] = $path .'/index.php'; $host = 'localhost';
$_SERVER['PHP_SELF'] = $path .'/index.php'; $path = '';
$_SERVER['HTTP_USER_AGENT'] = 'Drupal command line'; $php = "/usr/bin/php"; // TODO Get dynamically if possible.
chdir(realpath(dirname(__FILE__) . '/..')); // Get url from arguments.
require_once './includes/bootstrap.inc'; if (!empty($args['url'])) {
$parsed_url = parse_url($args['url']);
if ($execute_batch) { $host = $parsed_url['host'];
if (is_null($test_id)) { $path = $parsed_url['path'];
echo "ERROR: --execute-batch should not be called interactively.\n"; }
$_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; exit;
} }
if ($concurrency == 1 || !function_exists('pcntl_fork')) { if ($args['concurrency'] == 1 || !function_exists('pcntl_fork')) {
// Fallback to mono-threaded execution // Fallback to mono-threaded execution.
if (count($test_names) > 1) { if (count($args['test_names']) > 1) {
foreach($test_names as $test_class) { foreach ($args['test_names'] as $test_class) {
// Note: we still need to execute each test in its separate Drupal environment // 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"); simpletest_script_command(1, $args['test-id'], $test_class);
} }
exit; exit;
} }
else { else {
// Execute an individual test // Execute an individual test.
$test_class = array_shift($test_names); $test_class = array_shift($args['test_names']);
drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL); drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL);
simpletest_run_one_test($test_id, $test_class); simpletest_script_run_one_test($args['test-id'], $test_class);
exit; exit;
} }
} }
else { else {
// Multi-threaded execution // Multi-threaded execution.
$children = array(); $children = array();
while (!empty($test_names) || !empty($children)) { while (!empty($args['test_names']) || !empty($children)) {
// Fork children // Fork children safely since Drupal is not bootstrapped yet.
// Note: we can safely fork here, because Drupal is not bootstrapped yet while (count($children) < $concurrency) {
while(count($children) < $concurrency) { if (empty($args['test_names'])) break;
if (empty($test_names)) break;
$child = array(); $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(); $child['pid'] = pcntl_fork();
if (!$child['pid']) { 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); drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL);
simpletest_run_one_test($test_id, $test_class); simpletest_script_run_one_test($test_id, $test_class);
exit; exit;
} }
else { else {
// Register our new child // Register our new child.
$children[] = $child; $children[] = $child;
} }
} }
// Wait for children every 200ms // Wait for children every 200ms.
usleep(200000); usleep(200000);
// Check if some children finished // Check if some children finished.
foreach($children as $cid => $child) { foreach ($children as $cid => $child) {
if (pcntl_waitpid($child['pid'], $status, WUNTRACED | WNOHANG)) { if (pcntl_waitpid($child['pid'], $status, WUNTRACED | WNOHANG)) {
// This particular child exited // This particular child exited.
unset($children[$cid]); unset($children[$cid]);
} }
} }
...@@ -183,153 +297,208 @@ ...@@ -183,153 +297,208 @@
} }
} }
drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL); /**
* Run a single test (assume a Drupal bootstrapped environnement).
if (!module_exists('simpletest')) { */
echo("ERROR: The simpletest module must be enabled before this script can run.\n"); function simpletest_script_run_one_test($test_id, $test_class) {
exit; simpletest_get_all_tests();
} $test = new $test_class($test_id);
$test->run();
$info = $test->getInfo();
if ($clean) { $status = ((isset($test->_results['#fail']) && $test->_results['#fail'] > 0)
// Clean up left-over times and directories. || (isset($test->_results['#exception']) && $test->_results['#exception'] > 0) ? 'fail' : 'pass');
simpletest_clean_environment(); simpletest_script_print($info['name'] . ' ' . _simpletest_format_summary_line($test->_results) . "\n", simpletest_script_color_code($status));
// Get the status messages and print them.
$messages = array_pop(drupal_get_messages('status'));
foreach($messages as $text) {
echo(" - " . $text . "\n");
}
exit;
} }
// Run tests as user #1. /**
$GLOBALS['user'] = user_load(1); * Execute a command to run batch of tests in separate process.
*/
// Load simpletest files function simpletest_script_command($concurrency, $test_id, $tests) {
$all_tests = simpletest_get_all_tests(); global $args, $php;
$groups = simpletest_categorize_tests($all_tests);
$test_list = array();
if ($list) { $command = "$php ./scripts/{$args['script']} --url {$args['url']}";
// Display all availabe tests. if ($args['color']) {
echo("\nAvailable test groups\n---------------------\n\n"); $command .= ' --color';
foreach ($groups as $group => $tests) {
echo($group . "\n");
foreach ($tests as $class_name => $instance) {
$info = $instance->getInfo();
echo " - " . $info['name'] . ' (' . $class_name . ')' . "\n";
}
} }
exit; $command .= " --concurrency $concurrency --test-id $test_id --execute-batch $tests";
passthru($command);
} }
if ($all) { /**
$test_list = array_keys($all_tests); * Get list of tests based on arguments. If --all specfied then
} * returns all available tests, otherwise reads list of tests.
else { *
if ($class_names) { * Will print error and exit if no valid tests were found.
// Use only valid class names *
foreach ($test_names as $class_name) { * @return List of tests.
if (isset($all_tests[$class_name])) { */
$test_list[] = $class_name; function simpletest_script_get_test_list() {
} global $args, $all_tests, $groups;
}
$test_list = array();
if ($args['all']) {
$test_list = array_keys($all_tests);
} }
else { else {
// Resolve group names if ($args['class_names']) {
foreach ($test_names as $group_name) { // Check for valid class names.
if (isset($groups[$group_name])) { foreach ($args['test_names'] as $class_name) {
foreach($groups[$group_name] as $class_name => $instance) { if (isset($all_tests[$class_name])) {
$test_list[] = $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) { if (empty($test_list)) {
echo("ERROR: No valid tests were specified.\n"); simpletest_script_print_error('No valid tests were specified.');
exit; exit;
}
return $test_list;
} }
// If not in 'safe mode', increase the maximum execution time: /**
if (!ini_get('safe_mode')) { * Initialize the reporter.
set_time_limit(0); */
} function simpletest_script_reporter_init() {
global $args, $all_tests, $test_list;
echo "\n"; echo "\n";
echo "Drupal test run\n"; echo "Drupal test run\n";
echo "---------------\n"; echo "---------------\n";
echo "\n"; echo "\n";
// Tell the user about what tests are to be run. // Tell the user about what tests are to be run.
if ($all) { if ($args['all']) {
echo "All tests will run.\n\n"; echo "All tests will run.\n\n";
} }
else { else {
echo "Tests to be run:\n"; echo "Tests to be run:\n";
foreach ($test_list as $class_name) { foreach ($test_list as $class_name) {
$info = $all_tests[$class_name]->getInfo(); $info = $all_tests[$class_name]->getInfo();
echo " - " . $info['name'] . ' (' . $class_name . ')' . "\n"; 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 "\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)'); echo "\n";
$test_id = db_last_insert_id('simpletest_test_id', 'test_id'); echo "Test run ended: " . format_date(time(), 'long') . "\n";
echo "\n";
echo "Test summary:\n"; if ($args['verbose']) {
echo "-------------\n"; // Report results.
echo "\n"; echo "Detailed test results:\n";
echo "----------------------\n";
// Now execute tests echo "\n";
passthru($php . " ./scripts/run-tests.sh --url $url --test-id $test_id --concurrency $concurrency --execute-batch " . implode(",", $test_list));
$results_map = array(
echo "\n"; 'pass' => 'Pass',
echo "Test run ended: " . format_date(time(), 'long') . "\n"; 'fail' => 'Fail',
echo "\n"; 'exception' => 'Exception'
// 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,
); );
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) { function simpletest_script_print_error($message) {
simpletest_get_all_tests(); simpletest_script_print(" ERROR: $message\n", SIMPLETEST_SCRIPT_COLOR_FAIL);
$test = new $test_class($test_id);
$test->run();
$info = $test->getInfo();
echo $info['name'] . ' ' . _simpletest_format_summary_line($test->_results) . "\n";
} }
/**
* 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.
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment