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