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