| Drupal | PHP Cross Reference | Content Management Systems |
1 <?php 2 /** 3 * @file 4 * This script runs Drupal tests from command line. 5 */ 6 7 define('SIMPLETEST_SCRIPT_COLOR_PASS', 32); // Green. 8 define('SIMPLETEST_SCRIPT_COLOR_FAIL', 31); // Red. 9 define('SIMPLETEST_SCRIPT_COLOR_EXCEPTION', 33); // Brown. 10 11 // Set defaults and get overrides. 12 list($args, $count) = simpletest_script_parse_args(); 13 14 if ($args['help'] || $count == 0) { 15 simpletest_script_help(); 16 exit; 17 } 18 19 if ($args['execute-test']) { 20 // Masquerade as Apache for running tests. 21 simpletest_script_init("Apache"); 22 simpletest_script_run_one_test($args['test-id'], $args['execute-test']); 23 } 24 else { 25 // Run administrative functions as CLI. 26 simpletest_script_init(NULL); 27 } 28 29 // Bootstrap to perform initial validation or other operations. 30 drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL); 31 if (!module_exists('simpletest')) { 32 simpletest_script_print_error("The simpletest module must be enabled before this script can run."); 33 exit; 34 } 35 36 if ($args['clean']) { 37 // Clean up left-over times and directories. 38 simpletest_clean_environment(); 39 echo "\nEnvironment cleaned.\n"; 40 41 // Get the status messages and print them. 42 $messages = array_pop(drupal_get_messages('status')); 43 foreach ($messages as $text) { 44 echo " - " . $text . "\n"; 45 } 46 exit; 47 } 48 49 // Load SimpleTest files. 50 $groups = simpletest_test_get_all(); 51 $all_tests = array(); 52 foreach ($groups as $group => $tests) { 53 $all_tests = array_merge($all_tests, array_keys($tests)); 54 } 55 $test_list = array(); 56 57 if ($args['list']) { 58 // Display all available tests. 59 echo "\nAvailable test groups & classes\n"; 60 echo "-------------------------------\n\n"; 61 foreach ($groups as $group => $tests) { 62 echo $group . "\n"; 63 foreach ($tests as $class => $info) { 64 echo " - " . $info['name'] . ' (' . $class . ')' . "\n"; 65 } 66 } 67 exit; 68 } 69 70 $test_list = simpletest_script_get_test_list(); 71 72 // Try to allocate unlimited time to run the tests. 73 drupal_set_time_limit(0); 74 75 simpletest_script_reporter_init(); 76 77 // Setup database for test results. 78 $test_id = db_insert('simpletest_test_id')->useDefaults(array('test_id'))->execute(); 79 80 // Execute tests. 81 simpletest_script_execute_batch($test_id, simpletest_script_get_test_list()); 82 83 // Retrieve the last database prefix used for testing and the last test class 84 // that was run from. Use the information to read the lgo file in case any 85 // fatal errors caused the test to crash. 86 list($last_prefix, $last_test_class) = simpletest_last_test_get($test_id); 87 simpletest_log_read($test_id, $last_prefix, $last_test_class); 88 89 // Stop the timer. 90 simpletest_script_reporter_timer_stop(); 91 92 // Display results before database is cleared. 93 simpletest_script_reporter_display_results(); 94 95 if ($args['xml']) { 96 simpletest_script_reporter_write_xml_results(); 97 } 98 99 // Cleanup our test results. 100 simpletest_clean_results_table($test_id); 101 102 // Test complete, exit. 103 exit; 104 105 /** 106 * Print help text. 107 */ 108 function simpletest_script_help() { 109 global $args; 110 111 echo <<<EOF 112 113 Run Drupal tests from the shell. 114 115 Usage: {$args['script']} [OPTIONS] <tests> 116 Example: {$args['script']} Profile 117 118 All arguments are long options. 119 120 --help Print this page. 121 122 --list Display all available test groups. 123 124 --clean Cleans up database tables or directories from previous, failed, 125 tests and then exits (no tests are run). 126 127 --url Immediately precedes a URL to set the host and path. You will 128 need this parameter if Drupal is in a subdirectory on your 129 localhost and you have not set \$base_url in settings.php. Tests 130 can be run under SSL by including https:// in the URL. 131 132 --php The absolute path to the PHP executable. Usually not needed. 133 134 --concurrency [num] 135 136 Run tests in parallel, up to [num] tests at a time. 137 138 --all Run all available tests. 139 140 --class Run tests identified by specific class names, instead of group names. 141 142 --file Run tests identified by specific file names, instead of group names. 143 Specify the path and the extension (i.e. 'modules/user/user.test'). 144 145 --xml <path> 146 147 If provided, test results will be written as xml files to this path. 148 149 --color Output text format results with color highlighting. 150 151 --verbose Output detailed assertion messages in addition to summary. 152 153 <test1>[,<test2>[,<test3> ...]] 154 155 One or more tests to be run. By default, these are interpreted 156 as the names of test groups as shown at 157 ?q=admin/config/development/testing. 158 These group names typically correspond to module names like "User" 159 or "Profile" or "System", but there is also a group "XML-RPC". 160 If --class is specified then these are interpreted as the names of 161 specific test classes whose test methods will be run. Tests must 162 be separated by commas. Ignored if --all is specified. 163 164 To run this script you will normally invoke it from the root directory of your 165 Drupal installation as the webserver user (differs per configuration), or root: 166 167 sudo -u [wwwrun|www-data|etc] php ./scripts/{$args['script']} 168 --url http://example.com/ --all 169 sudo -u [wwwrun|www-data|etc] php ./scripts/{$args['script']} 170 --url http://example.com/ --class BlockTestCase 171 \n 172 EOF; 173 } 174 175 /** 176 * Parse execution argument and ensure that all are valid. 177 * 178 * @return The list of arguments. 179 */ 180 function simpletest_script_parse_args() { 181 // Set default values. 182 $args = array( 183 'script' => '', 184 'help' => FALSE, 185 'list' => FALSE, 186 'clean' => FALSE, 187 'url' => '', 188 'php' => '', 189 'concurrency' => 1, 190 'all' => FALSE, 191 'class' => FALSE, 192 'file' => FALSE, 193 'color' => FALSE, 194 'verbose' => FALSE, 195 'test_names' => array(), 196 // Used internally. 197 'test-id' => 0, 198 'execute-test' => '', 199 'xml' => '', 200 ); 201 202 // Override with set values. 203 $args['script'] = basename(array_shift($_SERVER['argv'])); 204 205 $count = 0; 206 while ($arg = array_shift($_SERVER['argv'])) { 207 if (preg_match('/--(\S+)/', $arg, $matches)) { 208 // Argument found. 209 if (array_key_exists($matches[1], $args)) { 210 // Argument found in list. 211 $previous_arg = $matches[1]; 212 if (is_bool($args[$previous_arg])) { 213 $args[$matches[1]] = TRUE; 214 } 215 else { 216 $args[$matches[1]] = array_shift($_SERVER['argv']); 217 } 218 // Clear extraneous values. 219 $args['test_names'] = array(); 220 $count++; 221 } 222 else { 223 // Argument not found in list. 224 simpletest_script_print_error("Unknown argument '$arg'."); 225 exit; 226 } 227 } 228 else { 229 // Values found without an argument should be test names. 230 $args['test_names'] += explode(',', $arg); 231 $count++; 232 } 233 } 234 235 // Validate the concurrency argument 236 if (!is_numeric($args['concurrency']) || $args['concurrency'] <= 0) { 237 simpletest_script_print_error("--concurrency must be a strictly positive integer."); 238 exit; 239 } 240 241 return array($args, $count); 242 } 243 244 /** 245 * Initialize script variables and perform general setup requirements. 246 */ 247 function simpletest_script_init($server_software) { 248 global $args, $php; 249 250 $host = 'localhost'; 251 $path = ''; 252 // Determine location of php command automatically, unless a command line argument is supplied. 253 if (!empty($args['php'])) { 254 $php = $args['php']; 255 } 256 elseif ($php_env = getenv('_')) { 257 // '_' is an environment variable set by the shell. It contains the command that was executed. 258 $php = $php_env; 259 } 260 elseif ($sudo = getenv('SUDO_COMMAND')) { 261 // 'SUDO_COMMAND' is an environment variable set by the sudo program. 262 // Extract only the PHP interpreter, not the rest of the command. 263 list($php, ) = explode(' ', $sudo, 2); 264 } 265 else { 266 simpletest_script_print_error('Unable to automatically determine the path to the PHP interpreter. Supply the --php command line argument.'); 267 simpletest_script_help(); 268 exit(); 269 } 270 271 // Get URL from arguments. 272 if (!empty($args['url'])) { 273 $parsed_url = parse_url($args['url']); 274 $host = $parsed_url['host'] . (isset($parsed_url['port']) ? ':' . $parsed_url['port'] : ''); 275 $path = isset($parsed_url['path']) ? $parsed_url['path'] : ''; 276 277 // If the passed URL schema is 'https' then setup the $_SERVER variables 278 // properly so that testing will run under HTTPS. 279 if ($parsed_url['scheme'] == 'https') { 280 $_SERVER['HTTPS'] = 'on'; 281 } 282 } 283 284 $_SERVER['HTTP_HOST'] = $host; 285 $_SERVER['REMOTE_ADDR'] = '127.0.0.1'; 286 $_SERVER['SERVER_ADDR'] = '127.0.0.1'; 287 $_SERVER['SERVER_SOFTWARE'] = $server_software; 288 $_SERVER['SERVER_NAME'] = 'localhost'; 289 $_SERVER['REQUEST_URI'] = $path .'/'; 290 $_SERVER['REQUEST_METHOD'] = 'GET'; 291 $_SERVER['SCRIPT_NAME'] = $path .'/index.php'; 292 $_SERVER['PHP_SELF'] = $path .'/index.php'; 293 $_SERVER['HTTP_USER_AGENT'] = 'Drupal command line'; 294 295 if (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') { 296 // Ensure that any and all environment variables are changed to https://. 297 foreach ($_SERVER as $key => $value) { 298 $_SERVER[$key] = str_replace('http://', 'https://', $_SERVER[$key]); 299 } 300 } 301 302 chdir(realpath(dirname(__FILE__) . '/..')); 303 define('DRUPAL_ROOT', getcwd()); 304 require_once DRUPAL_ROOT . '/includes/bootstrap.inc'; 305 } 306 307 /** 308 * Execute a batch of tests. 309 */ 310 function simpletest_script_execute_batch($test_id, $test_classes) { 311 global $args; 312 313 // Multi-process execution. 314 $children = array(); 315 while (!empty($test_classes) || !empty($children)) { 316 while (count($children) < $args['concurrency']) { 317 if (empty($test_classes)) { 318 break; 319 } 320 321 // Fork a child process. 322 $test_class = array_shift($test_classes); 323 $command = simpletest_script_command($test_id, $test_class); 324 $process = proc_open($command, array(), $pipes, NULL, NULL, array('bypass_shell' => TRUE)); 325 326 if (!is_resource($process)) { 327 echo "Unable to fork test process. Aborting.\n"; 328 exit; 329 } 330 331 // Register our new child. 332 $children[] = array( 333 'process' => $process, 334 'class' => $test_class, 335 'pipes' => $pipes, 336 ); 337 } 338 339 // Wait for children every 200ms. 340 usleep(200000); 341 342 // Check if some children finished. 343 foreach ($children as $cid => $child) { 344 $status = proc_get_status($child['process']); 345 if (empty($status['running'])) { 346 // The child exited, unregister it. 347 proc_close($child['process']); 348 if ($status['exitcode']) { 349 echo 'FATAL ' . $test_class . ': test runner returned a non-zero error code (' . $status['exitcode'] . ').' . "\n"; 350 } 351 unset($children[$cid]); 352 } 353 } 354 } 355 } 356 357 /** 358 * Bootstrap Drupal and run a single test. 359 */ 360 function simpletest_script_run_one_test($test_id, $test_class) { 361 try { 362 // Bootstrap Drupal. 363 drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL); 364 365 $test = new $test_class($test_id); 366 $test->run(); 367 $info = $test->getInfo(); 368 369 $had_fails = (isset($test->results['#fail']) && $test->results['#fail'] > 0); 370 $had_exceptions = (isset($test->results['#exception']) && $test->results['#exception'] > 0); 371 $status = ($had_fails || $had_exceptions ? 'fail' : 'pass'); 372 simpletest_script_print($info['name'] . ' ' . _simpletest_format_summary_line($test->results) . "\n", simpletest_script_color_code($status)); 373 374 // Finished, kill this runner. 375 exit(0); 376 } 377 catch (Exception $e) { 378 echo (string) $e; 379 exit(1); 380 } 381 } 382 383 /** 384 * Return a command used to run a test in a separate process. 385 * 386 * @param $test_id 387 * The current test ID. 388 * @param $test_class 389 * The name of the test class to run. 390 */ 391 function simpletest_script_command($test_id, $test_class) { 392 global $args, $php; 393 394 $command = escapeshellarg($php) . ' ' . escapeshellarg('./scripts/' . $args['script']) . ' --url ' . escapeshellarg($args['url']); 395 if ($args['color']) { 396 $command .= ' --color'; 397 } 398 $command .= " --php " . escapeshellarg($php) . " --test-id $test_id --execute-test $test_class"; 399 return $command; 400 } 401 402 /** 403 * Get list of tests based on arguments. If --all specified then 404 * returns all available tests, otherwise reads list of tests. 405 * 406 * Will print error and exit if no valid tests were found. 407 * 408 * @return List of tests. 409 */ 410 function simpletest_script_get_test_list() { 411 global $args, $all_tests, $groups; 412 413 $test_list = array(); 414 if ($args['all']) { 415 $test_list = $all_tests; 416 } 417 else { 418 if ($args['class']) { 419 // Check for valid class names. 420 foreach ($args['test_names'] as $class_name) { 421 if (in_array($class_name, $all_tests)) { 422 $test_list[] = $class_name; 423 } 424 } 425 } 426 elseif ($args['file']) { 427 $files = array(); 428 foreach ($args['test_names'] as $file) { 429 $files[drupal_realpath($file)] = 1; 430 } 431 432 // Check for valid class names. 433 foreach ($all_tests as $class_name) { 434 $refclass = new ReflectionClass($class_name); 435 $file = $refclass->getFileName(); 436 if (isset($files[$file])) { 437 $test_list[] = $class_name; 438 } 439 } 440 } 441 else { 442 // Check for valid group names and get all valid classes in group. 443 foreach ($args['test_names'] as $group_name) { 444 if (isset($groups[$group_name])) { 445 foreach ($groups[$group_name] as $class_name => $info) { 446 $test_list[] = $class_name; 447 } 448 } 449 } 450 } 451 } 452 453 if (empty($test_list)) { 454 simpletest_script_print_error('No valid tests were specified.'); 455 exit; 456 } 457 return $test_list; 458 } 459 460 /** 461 * Initialize the reporter. 462 */ 463 function simpletest_script_reporter_init() { 464 global $args, $all_tests, $test_list, $results_map; 465 466 $results_map = array( 467 'pass' => 'Pass', 468 'fail' => 'Fail', 469 'exception' => 'Exception' 470 ); 471 472 echo "\n"; 473 echo "Drupal test run\n"; 474 echo "---------------\n"; 475 echo "\n"; 476 477 // Tell the user about what tests are to be run. 478 if ($args['all']) { 479 echo "All tests will run.\n\n"; 480 } 481 else { 482 echo "Tests to be run:\n"; 483 foreach ($test_list as $class_name) { 484 $info = call_user_func(array($class_name, 'getInfo')); 485 echo " - " . $info['name'] . ' (' . $class_name . ')' . "\n"; 486 } 487 echo "\n"; 488 } 489 490 echo "Test run started:\n"; 491 echo " " . format_date($_SERVER['REQUEST_TIME'], 'long') . "\n"; 492 timer_start('run-tests'); 493 echo "\n"; 494 495 echo "Test summary\n"; 496 echo "------------\n"; 497 echo "\n"; 498 } 499 500 /** 501 * Display jUnit XML test results. 502 */ 503 function simpletest_script_reporter_write_xml_results() { 504 global $args, $test_id, $results_map; 505 506 $results = db_query("SELECT * FROM {simpletest} WHERE test_id = :test_id ORDER BY test_class, message_id", array(':test_id' => $test_id)); 507 508 $test_class = ''; 509 $xml_files = array(); 510 511 foreach ($results as $result) { 512 if (isset($results_map[$result->status])) { 513 if ($result->test_class != $test_class) { 514 // We've moved onto a new class, so write the last classes results to a file: 515 if (isset($xml_files[$test_class])) { 516 file_put_contents($args['xml'] . '/' . $test_class . '.xml', $xml_files[$test_class]['doc']->saveXML()); 517 unset($xml_files[$test_class]); 518 } 519 $test_class = $result->test_class; 520 if (!isset($xml_files[$test_class])) { 521 $doc = new DomDocument('1.0'); 522 $root = $doc->createElement('testsuite'); 523 $root = $doc->appendChild($root); 524 $xml_files[$test_class] = array('doc' => $doc, 'suite' => $root); 525 } 526 } 527 528 // For convenience: 529 $dom_document = &$xml_files[$test_class]['doc']; 530 531 // Create the XML element for this test case: 532 $case = $dom_document->createElement('testcase'); 533 $case->setAttribute('classname', $test_class); 534 list($class, $name) = explode('->', $result->function, 2); 535 $case->setAttribute('name', $name); 536 537 // Passes get no further attention, but failures and exceptions get to add more detail: 538 if ($result->status == 'fail') { 539 $fail = $dom_document->createElement('failure'); 540 $fail->setAttribute('type', 'failure'); 541 $fail->setAttribute('message', $result->message_group); 542 $text = $dom_document->createTextNode($result->message); 543 $fail->appendChild($text); 544 $case->appendChild($fail); 545 } 546 elseif ($result->status == 'exception') { 547 // In the case of an exception the $result->function may not be a class 548 // method so we record the full function name: 549 $case->setAttribute('name', $result->function); 550 551 $fail = $dom_document->createElement('error'); 552 $fail->setAttribute('type', 'exception'); 553 $fail->setAttribute('message', $result->message_group); 554 $full_message = $result->message . "\n\nline: " . $result->line . "\nfile: " . $result->file; 555 $text = $dom_document->createTextNode($full_message); 556 $fail->appendChild($text); 557 $case->appendChild($fail); 558 } 559 // Append the test case XML to the test suite: 560 $xml_files[$test_class]['suite']->appendChild($case); 561 } 562 } 563 // The last test case hasn't been saved to a file yet, so do that now: 564 if (isset($xml_files[$test_class])) { 565 file_put_contents($args['xml'] . '/' . $test_class . '.xml', $xml_files[$test_class]['doc']->saveXML()); 566 unset($xml_files[$test_class]); 567 } 568 } 569 570 /** 571 * Stop the test timer. 572 */ 573 function simpletest_script_reporter_timer_stop() { 574 echo "\n"; 575 $end = timer_stop('run-tests'); 576 echo "Test run duration: " . format_interval($end['time'] / 1000); 577 echo "\n\n"; 578 } 579 580 /** 581 * Display test results. 582 */ 583 function simpletest_script_reporter_display_results() { 584 global $args, $test_id, $results_map; 585 586 if ($args['verbose']) { 587 // Report results. 588 echo "Detailed test results\n"; 589 echo "---------------------\n"; 590 591 $results = db_query("SELECT * FROM {simpletest} WHERE test_id = :test_id ORDER BY test_class, message_id", array(':test_id' => $test_id)); 592 $test_class = ''; 593 foreach ($results as $result) { 594 if (isset($results_map[$result->status])) { 595 if ($result->test_class != $test_class) { 596 // Display test class every time results are for new test class. 597 echo "\n\n---- $result->test_class ----\n\n\n"; 598 $test_class = $result->test_class; 599 600 // Print table header. 601 echo "Status Group Filename Line Function \n"; 602 echo "--------------------------------------------------------------------------------\n"; 603 } 604 605 simpletest_script_format_result($result); 606 } 607 } 608 } 609 } 610 611 /** 612 * Format the result so that it fits within the default 80 character 613 * terminal size. 614 * 615 * @param $result The result object to format. 616 */ 617 function simpletest_script_format_result($result) { 618 global $results_map, $color; 619 620 $summary = sprintf("%-9.9s %-10.10s %-17.17s %4.4s %-35.35s\n", 621 $results_map[$result->status], $result->message_group, basename($result->file), $result->line, $result->function); 622 623 simpletest_script_print($summary, simpletest_script_color_code($result->status)); 624 625 $lines = explode("\n", wordwrap(trim(strip_tags($result->message)), 76)); 626 foreach ($lines as $line) { 627 echo " $line\n"; 628 } 629 } 630 631 /** 632 * Print error message prefixed with " ERROR: " and displayed in fail color 633 * if color output is enabled. 634 * 635 * @param $message The message to print. 636 */ 637 function simpletest_script_print_error($message) { 638 simpletest_script_print(" ERROR: $message\n", SIMPLETEST_SCRIPT_COLOR_FAIL); 639 } 640 641 /** 642 * Print a message to the console, if color is enabled then the specified 643 * color code will be used. 644 * 645 * @param $message The message to print. 646 * @param $color_code The color code to use for coloring. 647 */ 648 function simpletest_script_print($message, $color_code) { 649 global $args; 650 if ($args['color']) { 651 echo "\033[" . $color_code . "m" . $message . "\033[0m"; 652 } 653 else { 654 echo $message; 655 } 656 } 657 658 /** 659 * Get the color code associated with the specified status. 660 * 661 * @param $status The status string to get code for. 662 * @return Color code. 663 */ 664 function simpletest_script_color_code($status) { 665 switch ($status) { 666 case 'pass': 667 return SIMPLETEST_SCRIPT_COLOR_PASS; 668 case 'fail': 669 return SIMPLETEST_SCRIPT_COLOR_FAIL; 670 case 'exception': 671 return SIMPLETEST_SCRIPT_COLOR_EXCEPTION; 672 } 673 return 0; // Default formatting. 674 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
title