Newer
Older
<?php
/**
* @file
* This script runs Drupal tests from command line.
*/

Dries Buytaert
committed
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();
if ($args['help'] || $count == 0) {
simpletest_script_help();
exit;
}
if ($args['execute-batch']) {

Dries Buytaert
committed
// Masquerade as Apache for running tests.
simpletest_script_init("Apache");

Dries Buytaert
committed
simpletest_script_execute_batch();
}

Dries Buytaert
committed
else {
// Run administrative functions as CLI.

Dries Buytaert
committed
simpletest_script_init(NULL);

Dries Buytaert
committed
}

Dries Buytaert
committed
// Bootstrap to perform initial validation or other operations.

Dries Buytaert
committed
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'));

Dries Buytaert
committed
foreach ($messages as $text) {

Dries Buytaert
committed
echo " - " . $text . "\n";
}
exit;
}
// Load SimpleTest files.
$groups = simpletest_test_get_all();
$all_tests = array();
foreach ($groups as $group => $tests) {
$all_tests = array_merge($all_tests, array_keys($tests));
}

Dries Buytaert
committed
$test_list = array();
if ($args['list']) {
// Display all available tests.

Dries Buytaert
committed
echo "\nAvailable test groups & classes\n";
echo "-------------------------------\n\n";
foreach ($groups as $group => $tests) {
echo $group . "\n";
foreach ($tests as $class => $info) {
echo " - " . $info['name'] . ' (' . $class . ')' . "\n";

Dries Buytaert
committed
}
}
exit;
}
$test_list = simpletest_script_get_test_list();
// Try to allocate unlimited time to run the tests.
drupal_set_time_limit(0);

Dries Buytaert
committed
simpletest_script_reporter_init();
// Setup database for test results.

Dries Buytaert
committed
$test_id = db_insert('simpletest_test_id')->useDefaults(array('test_id'))->execute();

Dries Buytaert
committed
// Execute tests.
simpletest_script_command($args['concurrency'], $test_id, implode(",", $test_list));

Dries Buytaert
committed
// Retrieve the last database prefix used for testing and the last test class
// that was run from. Use the information to read the lgo file in case any
// fatal errors caused the test to crash.
list($last_prefix, $last_test_class) = simpletest_last_test_get($test_id);
simpletest_log_read($test_id, $last_prefix, $last_test_class);

Dries Buytaert
committed
// Stop the timer.
simpletest_script_reporter_timer_stop();

Dries Buytaert
committed
// Display results before database is cleared.
simpletest_script_reporter_display_results();

Dries Buytaert
committed
if ($args['xml']) {
simpletest_script_reporter_write_xml_results();
}

Dries Buytaert
committed
// Cleanup our test results.

Angie Byron
committed
simpletest_clean_results_table($test_id);

Dries Buytaert
committed
/**
* Print help text.
*/
function simpletest_script_help() {
global $args;
echo <<<EOF
Run Drupal tests from the shell.

Dries Buytaert
committed
Usage: {$args['script']} [OPTIONS] <tests>
Example: {$args['script']} Profile
All arguments are long options.
--help Print this page.

Dries Buytaert
committed
--list Display all available test groups.
--clean Cleans up database tables or directories from previous, failed,
tests and then exits (no tests are run).

Dries Buytaert
committed
--url Immediately preceeds a URL to set the host and path. You will
need this parameter if Drupal is in a subdirectory on your
localhost and you have not set \$base_url in settings.php. Tests
can be run under SSL by including https:// in the URL.

Dries Buytaert
committed
--php The absolute path to the PHP executable. Usually not needed.

Dries Buytaert
committed
--concurrency [num]
Run tests in parallel, up to [num] tests at a time. This requires
the Process Control Extension (PCNTL) to be compiled in PHP, not
supported under Windows.

Dries Buytaert
committed

Dries Buytaert
committed
--all Run all available tests.

Dries Buytaert
committed
--class Run tests identified by specific class names, instead of group names.
--file Run tests identified by specific file names, instead of group names.
Specify the path and the extension (i.e. 'modules/user/user.test').

Dries Buytaert
committed
--xml <path>
If provided, test results will be written as xml files to this path.
--color Output text format results with color highlighting.

Dries Buytaert
committed
--verbose Output detailed assertion messages in addition to summary.
<test1>[,<test2>[,<test3> ...]]

Dries Buytaert
committed
One or more tests to be run. By default, these are interpreted

Angie Byron
committed
as the names of test groups as shown at
?q=admin/config/development/testing.
These group names typically correspond to module names like "User"
or "Profile" or "System", but there is also a group "XML-RPC".
If --class is specified then these are interpreted as the names of

Dries Buytaert
committed
specific test classes whose test methods will be run. Tests must
be separated by commas. Ignored if --all is specified.

Dries Buytaert
committed
To run this script you will normally invoke it from the root directory of your

Dries Buytaert
committed
Drupal installation as the webserver user (differs per configuration), or root:

Dries Buytaert
committed
sudo -u [wwwrun|www-data|etc] php ./scripts/{$args['script']}
--url http://example.com/ --all
sudo -u [wwwrun|www-data|etc] php ./scripts/{$args['script']}

Dries Buytaert
committed
--url http://example.com/ --class BlockTestCase
\n
EOF;
}

Dries Buytaert
committed
/**
* 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' => '',

Dries Buytaert
committed
'php' => '',

Dries Buytaert
committed
'concurrency' => 1,
'all' => FALSE,
'class' => FALSE,

Dries Buytaert
committed
'color' => FALSE,
'verbose' => FALSE,
'test_names' => array(),
// Used internally.
'test-id' => NULL,

Dries Buytaert
committed
'execute-batch' => FALSE,
'xml' => '',

Dries Buytaert
committed
);
// 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 extraneous values.

Dries Buytaert
committed
$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);
$count++;

Dries Buytaert
committed
}
}

Dries Buytaert
committed
// Validate the concurrency argument
if (!is_numeric($args['concurrency']) || $args['concurrency'] <= 0) {
simpletest_script_print_error("--concurrency must be a strictly positive integer.");
exit;
}

Angie Byron
committed
elseif ($args['concurrency'] > 1 && !function_exists('pcntl_fork')) {
simpletest_script_print_error("Parallel test execution requires the Process Control extension to be compiled in PHP. See http://php.net/manual/en/intro.pcntl.php for more information.");

Dries Buytaert
committed
exit;
}

Dries Buytaert
committed
return array($args, $count);
}

Dries Buytaert
committed
/**
* Initialize script variables and perform general setup requirements.
*/

Dries Buytaert
committed
function simpletest_script_init($server_software) {

Dries Buytaert
committed
global $args, $php;
$host = 'localhost';
$path = '';
// Determine location of php command automatically, unless a command line argument is supplied.
if (!empty($args['php'])) {

Dries Buytaert
committed
$php = $args['php'];
}
elseif (!empty($_ENV['_'])) {

Dries Buytaert
committed
// '_' is an environment variable set by the shell. It contains the command that was executed.
$php = $_ENV['_'];
}
elseif (!empty($_ENV['SUDO_COMMAND'])) {

Dries Buytaert
committed
// 'SUDO_COMMAND' is an environment variable set by the sudo program.
// Extract only the PHP interpreter, not the rest of the command.
list($php, ) = explode(' ', $_ENV['SUDO_COMMAND'], 2);
}
else {
simpletest_script_print_error('Unable to automatically determine the path to the PHP interpreter. Supply the --php command line argument.');

Dries Buytaert
committed
simpletest_script_help();

Dries Buytaert
committed
exit();
}

Dries Buytaert
committed
// Get url from arguments.
if (!empty($args['url'])) {
$parsed_url = parse_url($args['url']);

Dries Buytaert
committed
$host = $parsed_url['host'] . (isset($parsed_url['port']) ? ':' . $parsed_url['port'] : '');

Dries Buytaert
committed
$path = isset($parsed_url['path']) ? $parsed_url['path'] : '';
// If the passed URL schema is 'https' then setup the $_SERVER variables
// properly so that testing will run under https.
if ($parsed_url['scheme'] == 'https') {
$_SERVER['HTTPS'] = 'on';
}

Dries Buytaert
committed
}
$_SERVER['HTTP_HOST'] = $host;
$_SERVER['REMOTE_ADDR'] = '127.0.0.1';
$_SERVER['SERVER_ADDR'] = '127.0.0.1';

Dries Buytaert
committed
$_SERVER['SERVER_SOFTWARE'] = $server_software;

Dries Buytaert
committed
$_SERVER['SERVER_NAME'] = 'localhost';
$_SERVER['REQUEST_URI'] = $path .'/';
$_SERVER['REQUEST_METHOD'] = 'GET';

Dries Buytaert
committed
$_SERVER['SCRIPT_NAME'] = $path .'/index.php';
$_SERVER['PHP_SELF'] = $path .'/index.php';
$_SERVER['HTTP_USER_AGENT'] = 'Drupal command line';
if (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') {
// Ensure that any and all environment variables are changed to https://.
foreach ($_SERVER as $key => $value) {
$_SERVER[$key] = str_replace('http://', 'https://', $_SERVER[$key]);
}
}

Dries Buytaert
committed
chdir(realpath(dirname(__FILE__) . '/..'));

Angie Byron
committed
define('DRUPAL_ROOT', getcwd());
require_once DRUPAL_ROOT . '/includes/bootstrap.inc';

Dries Buytaert
committed
}
/**
* Execute a batch of tests.
*/
function simpletest_script_execute_batch() {
global $args;
if (!isset($args['test-id'])) {

Dries Buytaert
committed
simpletest_script_print_error("--execute-batch should not be called interactively.");

Dries Buytaert
committed
exit;
}

Dries Buytaert
committed
if ($args['concurrency'] == 1) {

Dries Buytaert
committed
// 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);

Dries Buytaert
committed
}
exit;
}
else {

Dries Buytaert
committed
// Execute an individual test.
$test_class = array_shift($args['test_names']);

Dries Buytaert
committed
drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL);

Dries Buytaert
committed
simpletest_script_run_one_test($args['test-id'], $test_class);

Dries Buytaert
committed
exit;
}
}
else {

Dries Buytaert
committed
// Multi-threaded execution.

Dries Buytaert
committed
$children = array();

Dries Buytaert
committed
while (!empty($args['test_names']) || !empty($children)) {
// Fork children safely since Drupal is not bootstrapped yet.

Dries Buytaert
committed
while (count($children) < $args['concurrency']) {

Dries Buytaert
committed
if (empty($args['test_names'])) break;

Dries Buytaert
committed
$child = array();

Dries Buytaert
committed
$child['test_class'] = $test_class = array_shift($args['test_names']);

Dries Buytaert
committed
$child['pid'] = pcntl_fork();
if (!$child['pid']) {

Dries Buytaert
committed
// This is the child process, bootstrap and execute the test.

Dries Buytaert
committed
drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL);

Dries Buytaert
committed
simpletest_script_run_one_test($args['test-id'], $test_class);

Dries Buytaert
committed
exit;
}
else {

Dries Buytaert
committed
// Register our new child.

Dries Buytaert
committed
$children[] = $child;
}
}

Dries Buytaert
committed
// Wait for children every 200ms.

Dries Buytaert
committed
usleep(200000);

Dries Buytaert
committed
// Check if some children finished.
foreach ($children as $cid => $child) {

Dries Buytaert
committed
if (pcntl_waitpid($child['pid'], $status, WUNTRACED | WNOHANG)) {

Dries Buytaert
committed
// This particular child exited.

Dries Buytaert
committed
unset($children[$cid]);
}
}
}
exit;
}
}

Dries Buytaert
committed
/**
* Run a single test (assume a Drupal bootstrapped environment).

Dries Buytaert
committed
*/
function simpletest_script_run_one_test($test_id, $test_class) {
$test = new $test_class($test_id);
$test->run();
$info = $test->getInfo();
$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));

Dries Buytaert
committed
/**
* Execute a command to run batch of tests in separate process.
*/
function simpletest_script_command($concurrency, $test_id, $tests) {
global $args, $php;

Dries Buytaert
committed

Dries Buytaert
committed
$command = "$php ./scripts/{$args['script']} --url {$args['url']}";
if ($args['color']) {
$command .= ' --color';

Dries Buytaert
committed
$command .= " --php " . escapeshellarg($php) . " --concurrency $concurrency --test-id $test_id --execute-batch $tests";

Dries Buytaert
committed
passthru($command);

Dries Buytaert
committed
}

Dries Buytaert
committed
/**
* Get list of tests based on arguments. If --all specified then

Dries Buytaert
committed
* 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']) {

Angie Byron
committed
$test_list = $all_tests;

Dries Buytaert
committed
}
else {
if ($args['class']) {

Dries Buytaert
committed
// Check for valid class names.
foreach ($args['test_names'] as $class_name) {

Angie Byron
committed
if (in_array($class_name, $all_tests)) {

Dries Buytaert
committed
$test_list[] = $class_name;
}
}
}

Angie Byron
committed
elseif ($args['file']) {
$files = array();
foreach ($args['test_names'] as $file) {
$files[drupal_realpath($file)] = 1;
// Check for valid class names.
foreach ($all_tests as $class_name) {
$refclass = new ReflectionClass($class_name);
$file = $refclass->getFileName();
if (isset($files[$file])) {
$test_list[] = $class_name;
}
}
}

Dries Buytaert
committed
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])) {

Dries Buytaert
committed
foreach ($groups[$group_name] as $class_name => $info) {

Dries Buytaert
committed
$test_list[] = $class_name;
}
}
}
}

Dries Buytaert
committed

Dries Buytaert
committed
if (empty($test_list)) {
simpletest_script_print_error('No valid tests were specified.');
exit;
}
return $test_list;

Dries Buytaert
committed
/**
* Initialize the reporter.
*/
function simpletest_script_reporter_init() {

Dries Buytaert
committed
global $args, $all_tests, $test_list, $results_map;
$results_map = array(
'pass' => 'Pass',
'fail' => 'Fail',
'exception' => 'Exception'
);

Dries Buytaert
committed
echo "\n";
echo "Drupal test run\n";
echo "---------------\n";
echo "\n";

Dries Buytaert
committed

Dries Buytaert
committed
// 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) {

Angie Byron
committed
$info = call_user_func(array($class_name, 'getInfo'));

Dries Buytaert
committed
echo " - " . $info['name'] . ' (' . $class_name . ')' . "\n";
}
echo "\n";

Dries Buytaert
committed

Dries Buytaert
committed
echo "Test run started: " . format_date($_SERVER['REQUEST_TIME'], 'long') . "\n";
timer_start('run-tests');

Dries Buytaert
committed
echo "\n";
echo "Test summary:\n";
echo "-------------\n";

Dries Buytaert
committed
echo "\n";

Dries Buytaert
committed
/**

Dries Buytaert
committed
* Display jUnit XML test results.

Dries Buytaert
committed
*/

Dries Buytaert
committed
function simpletest_script_reporter_write_xml_results() {

Dries Buytaert
committed
global $args, $test_id, $results_map;

Dries Buytaert
committed

Dries Buytaert
committed
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
$results = db_query("SELECT * FROM {simpletest} WHERE test_id = :test_id ORDER BY test_class, message_id", array(':test_id' => $test_id));
$test_class = '';
$xml_files = array();
foreach ($results as $result) {
if (isset($results_map[$result->status])) {
if ($result->test_class != $test_class) {
// We've moved onto a new class, so write the last classes results to a file:
if (isset($xml_files[$test_class])) {
file_put_contents($args['xml'] . '/' . $test_class . '.xml', $xml_files[$test_class]['doc']->saveXML());
unset($xml_files[$test_class]);
}
$test_class = $result->test_class;
if (!isset($xml_files[$test_class])) {
$doc = new DomDocument('1.0');
$root = $doc->createElement('testsuite');
$root = $doc->appendChild($root);
$xml_files[$test_class] = array('doc' => $doc, 'suite' => $root);
}
}
// For convenience:
$dom_document = &$xml_files[$test_class]['doc'];
// Create the XML element for this test case:
$case = $dom_document->createElement('testcase');
$case->setAttribute('classname', $test_class);
list($class, $name) = explode('->', $result->function, 2);
$case->setAttribute('name', $name);
// Passes get no further attention, but failures and exceptions get to add more detail:
if ($result->status == 'fail') {
$fail = $dom_document->createElement('failure');
$fail->setAttribute('type', 'failure');
$fail->setAttribute('message', $result->message_group);
$text = $dom_document->createTextNode($result->message);
$fail->appendChild($text);
$case->appendChild($fail);
}
elseif ($result->status == 'exception') {
// In the case of an exception the $result->function may not be a class
// method so we record the full function name:
$case->setAttribute('name', $result->function);
$fail = $dom_document->createElement('error');
$fail->setAttribute('type', 'exception');
$fail->setAttribute('message', $result->message_group);
$full_message = $result->message . "\n\nline: " . $result->line . "\nfile: " . $result->file;
$text = $dom_document->createTextNode($full_message);
$fail->appendChild($text);
$case->appendChild($fail);
}
// Append the test case XML to the test suite:
$xml_files[$test_class]['suite']->appendChild($case);
}
}
// The last test case hasn't been saved to a file yet, so do that now:
if (isset($xml_files[$test_class])) {
file_put_contents($args['xml'] . '/' . $test_class . '.xml', $xml_files[$test_class]['doc']->saveXML());
unset($xml_files[$test_class]);
}
}
/**
* Stop the test timer.
*/
function simpletest_script_reporter_timer_stop() {

Dries Buytaert
committed
echo "\n";
$end = timer_stop('run-tests');
echo "Test run duration: " . format_interval($end['time'] / 1000);

Dries Buytaert
committed
echo "\n";

Dries Buytaert
committed
}
/**
* Display test results.
*/
function simpletest_script_reporter_display_results() {
global $args, $test_id, $results_map;

Dries Buytaert
committed

Dries Buytaert
committed
if ($args['verbose']) {
// Report results.
echo "Detailed test results:\n";
echo "----------------------\n";
echo "\n";

Dries Buytaert
committed
$results = db_query("SELECT * FROM {simpletest} WHERE test_id = :test_id ORDER BY test_class, message_id", array(':test_id' => $test_id));

Dries Buytaert
committed
$test_class = '';

Dries Buytaert
committed
foreach ($results as $result) {

Dries Buytaert
committed
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);
}
}

Dries Buytaert
committed
/**
* 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));

Dries Buytaert
committed

Dries Buytaert
committed
$lines = explode("\n", wordwrap(trim(strip_tags($result->message)), 76));
foreach ($lines as $line) {
echo " $line\n";
}
}

Dries Buytaert
committed

Dries Buytaert
committed
/**

Dries Buytaert
committed
* Print error message prefixed with " ERROR: " and displayed in fail color
* if color output is enabled.
*
* @param $message The message to print.

Dries Buytaert
committed
*/

Dries Buytaert
committed
function simpletest_script_print_error($message) {
simpletest_script_print(" ERROR: $message\n", SIMPLETEST_SCRIPT_COLOR_FAIL);

Dries Buytaert
committed
}

Dries Buytaert
committed

Dries Buytaert
committed
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
/**
* 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.
}