Project

General

Profile

Paste
Download (25.1 KB) Statistics
| Branch: | Revision:

root / drupal7 / scripts / run-tests.sh @ master

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
define('SIMPLETEST_SCRIPT_EXIT_SUCCESS', 0);
12
define('SIMPLETEST_SCRIPT_EXIT_FAILURE', 1);
13
define('SIMPLETEST_SCRIPT_EXIT_EXCEPTION', 2);
14

    
15
// Set defaults and get overrides.
16
list($args, $count) = simpletest_script_parse_args();
17

    
18
if ($args['help'] || $count == 0) {
19
  simpletest_script_help();
20
  exit(($count == 0) ? SIMPLETEST_SCRIPT_EXIT_FAILURE : SIMPLETEST_SCRIPT_EXIT_SUCCESS);
21
}
22

    
23
if ($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
}
28
else {
29
  // Run administrative functions as CLI.
30
  simpletest_script_init(NULL);
31
}
32

    
33
// Bootstrap to perform initial validation or other operations.
34
drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL);
35
if (!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

    
40
if ($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();
56
foreach ($groups as $group => $tests) {
57
  $all_tests = array_merge($all_tests, array_keys($tests));
58
}
59
$test_list = array();
60

    
61
if ($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.
77
drupal_set_time_limit(0);
78

    
79
simpletest_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.
90
list($last_prefix, $last_test_class) = simpletest_last_test_get($test_id);
91
simpletest_log_read($test_id, $last_prefix, $last_test_class);
92

    
93
// Stop the timer.
94
simpletest_script_reporter_timer_stop();
95

    
96
// Display results before database is cleared.
97
simpletest_script_reporter_display_results();
98

    
99
if ($args['xml']) {
100
  simpletest_script_reporter_write_xml_results();
101
}
102

    
103
// Cleanup our test results.
104
simpletest_clean_results_table($test_id);
105

    
106
// Test complete, exit.
107
exit($status);
108

    
109
/**
110
 * Print help text.
111
 */
112
function simpletest_script_help() {
113
  global $args;
114

    
115
  echo <<<EOF
116

    
117
Run Drupal tests from the shell.
118

    
119
Usage:        {$args['script']} [OPTIONS] <tests>
120
Example:      {$args['script']} Profile
121

    
122
All 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
  <test1>[,<test2>[,<test3> ...]]
160

    
161
              One or more tests to be run. By default, these are interpreted
162
              as the names of test groups as shown at
163
              ?q=admin/config/development/testing.
164
              These group names typically correspond to module names like "User"
165
              or "Profile" or "System", but there is also a group "XML-RPC".
166
              If --class is specified then these are interpreted as the names of
167
              specific test classes whose test methods will be run. Tests must
168
              be separated by commas. Ignored if --all is specified.
169

    
170
To run this script you will normally invoke it from the root directory of your
171
Drupal installation as the webserver user (differs per configuration), or root:
172

    
173
sudo -u [wwwrun|www-data|etc] php ./scripts/{$args['script']}
174
  --url http://example.com/ --all
175
sudo -u [wwwrun|www-data|etc] php ./scripts/{$args['script']}
176
  --url http://example.com/ --class BlockTestCase
177
\n
178
EOF;
179
}
180

    
181
/**
182
 * Parse execution argument and ensure that all are valid.
183
 *
184
 * @return The list of arguments.
185
 */
186
function simpletest_script_parse_args() {
187
  // Set default values.
188
  $args = array(
189
    'script' => '',
190
    'help' => FALSE,
191
    'list' => FALSE,
192
    'clean' => FALSE,
193
    'url' => '',
194
    'php' => '',
195
    'concurrency' => 1,
196
    'all' => FALSE,
197
    'class' => FALSE,
198
    'file' => FALSE,
199
    'directory' => '',
200
    'color' => FALSE,
201
    'verbose' => FALSE,
202
    'test_names' => array(),
203
    // Used internally.
204
    'test-id' => 0,
205
    'execute-test' => '',
206
    'xml' => '',
207
  );
208

    
209
  // Override with set values.
210
  $args['script'] = basename(array_shift($_SERVER['argv']));
211

    
212
  $count = 0;
213
  while ($arg = array_shift($_SERVER['argv'])) {
214
    if (preg_match('/--(\S+)/', $arg, $matches)) {
215
      // Argument found.
216
      if (array_key_exists($matches[1], $args)) {
217
        // Argument found in list.
218
        $previous_arg = $matches[1];
219
        if (is_bool($args[$previous_arg])) {
220
          $args[$matches[1]] = TRUE;
221
        }
222
        else {
223
          $args[$matches[1]] = array_shift($_SERVER['argv']);
224
        }
225
        // Clear extraneous values.
226
        $args['test_names'] = array();
227
        $count++;
228
      }
229
      else {
230
        // Argument not found in list.
231
        simpletest_script_print_error("Unknown argument '$arg'.");
232
        exit(SIMPLETEST_SCRIPT_EXIT_FAILURE);
233
      }
234
    }
235
    else {
236
      // Values found without an argument should be test names.
237
      $args['test_names'] += explode(',', $arg);
238
      $count++;
239
    }
240
  }
241

    
242
  // Validate the concurrency argument
243
  if (!is_numeric($args['concurrency']) || $args['concurrency'] <= 0) {
244
    simpletest_script_print_error("--concurrency must be a strictly positive integer.");
245
    exit(SIMPLETEST_SCRIPT_EXIT_FAILURE);
246
  }
247

    
248
  return array($args, $count);
249
}
250

    
251
/**
252
 * Initialize script variables and perform general setup requirements.
253
 */
254
function simpletest_script_init($server_software) {
255
  global $args, $php;
256

    
257
  $host = 'localhost';
258
  $path = '';
259
  // Determine location of php command automatically, unless a command line argument is supplied.
260
  if (!empty($args['php'])) {
261
    $php = $args['php'];
262
  }
263
  elseif ($php_env = getenv('_')) {
264
    // '_' is an environment variable set by the shell. It contains the command that was executed.
265
    $php = $php_env;
266
  }
267
  elseif (defined('PHP_BINARY') && $php_env = PHP_BINARY) {
268
    // 'PHP_BINARY' specifies the PHP binary path during script execution. Available since PHP 5.4.
269
    $php = $php_env;
270
  }
271
  elseif ($sudo = getenv('SUDO_COMMAND')) {
272
    // 'SUDO_COMMAND' is an environment variable set by the sudo program.
273
    // Extract only the PHP interpreter, not the rest of the command.
274
    list($php, ) = explode(' ', $sudo, 2);
275
  }
276
  else {
277
    simpletest_script_print_error('Unable to automatically determine the path to the PHP interpreter. Supply the --php command line argument.');
278
    simpletest_script_help();
279
    exit(SIMPLETEST_SCRIPT_EXIT_FAILURE);
280
  }
281

    
282
  // Get URL from arguments.
283
  if (!empty($args['url'])) {
284
    $parsed_url = parse_url($args['url']);
285
    $host = $parsed_url['host'] . (isset($parsed_url['port']) ? ':' . $parsed_url['port'] : '');
286
    $path = isset($parsed_url['path']) ? $parsed_url['path'] : '';
287

    
288
    // If the passed URL schema is 'https' then setup the $_SERVER variables
289
    // properly so that testing will run under HTTPS.
290
    if ($parsed_url['scheme'] == 'https') {
291
      $_SERVER['HTTPS'] = 'on';
292
    }
293
  }
294

    
295
  $_SERVER['HTTP_HOST'] = $host;
296
  $_SERVER['REMOTE_ADDR'] = '127.0.0.1';
297
  $_SERVER['SERVER_ADDR'] = '127.0.0.1';
298
  $_SERVER['SERVER_SOFTWARE'] = $server_software;
299
  $_SERVER['SERVER_NAME'] = 'localhost';
300
  $_SERVER['REQUEST_URI'] = $path .'/';
301
  $_SERVER['REQUEST_METHOD'] = 'GET';
302
  $_SERVER['SCRIPT_NAME'] = $path .'/index.php';
303
  $_SERVER['PHP_SELF'] = $path .'/index.php';
304
  $_SERVER['HTTP_USER_AGENT'] = 'Drupal command line';
305

    
306
  if (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') {
307
    // Ensure that any and all environment variables are changed to https://.
308
    foreach ($_SERVER as $key => $value) {
309
      $_SERVER[$key] = str_replace('http://', 'https://', $_SERVER[$key]);
310
    }
311
  }
312

    
313
  chdir(realpath(dirname(__FILE__) . '/..'));
314
  define('DRUPAL_ROOT', getcwd());
315
  require_once DRUPAL_ROOT . '/includes/bootstrap.inc';
316
}
317

    
318
/**
319
 * Execute a batch of tests.
320
 */
321
function simpletest_script_execute_batch($test_id, $test_classes) {
322
  global $args;
323

    
324
  $total_status = SIMPLETEST_SCRIPT_EXIT_SUCCESS;
325

    
326
  // Multi-process execution.
327
  $children = array();
328
  while (!empty($test_classes) || !empty($children)) {
329
    while (count($children) < $args['concurrency']) {
330
      if (empty($test_classes)) {
331
        break;
332
      }
333

    
334
      // Fork a child process.
335
      $test_class = array_shift($test_classes);
336
      $command = simpletest_script_command($test_id, $test_class);
337
      $process = proc_open($command, array(), $pipes, NULL, NULL, array('bypass_shell' => TRUE));
338

    
339
      if (!is_resource($process)) {
340
        echo "Unable to fork test process. Aborting.\n";
341
        exit(SIMPLETEST_SCRIPT_EXIT_FAILURE);
342
      }
343

    
344
      // Register our new child.
345
      $children[] = array(
346
        'process' => $process,
347
        'class' => $test_class,
348
        'pipes' => $pipes,
349
      );
350
    }
351

    
352
    // Wait for children every 200ms.
353
    usleep(200000);
354

    
355
    // Check if some children finished.
356
    foreach ($children as $cid => $child) {
357
      $status = proc_get_status($child['process']);
358
      if (empty($status['running'])) {
359
        // The child exited, unregister it.
360
        proc_close($child['process']);
361
        if ($status['exitcode'] == SIMPLETEST_SCRIPT_EXIT_FAILURE) {
362
          if ($status['exitcode'] > $total_status) {
363
            $total_status = $status['exitcode'];
364
          }
365
        }
366
        elseif ($status['exitcode']) {
367
          $total_status = $status['exitcode'];
368
          echo 'FATAL ' . $test_class . ': test runner returned a non-zero error code (' . $status['exitcode'] . ').' . "\n";
369
        }
370

    
371
        // Remove this child.
372
        unset($children[$cid]);
373
      }
374
    }
375
  }
376
  return $total_status;
377
}
378

    
379
/**
380
 * Bootstrap Drupal and run a single test.
381
 */
382
function simpletest_script_run_one_test($test_id, $test_class) {
383
  try {
384
    // Bootstrap Drupal.
385
    drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL);
386

    
387
    simpletest_classloader_register();
388

    
389
    $test = new $test_class($test_id);
390
    $test->run();
391
    $info = $test->getInfo();
392

    
393
    $had_fails = (isset($test->results['#fail']) && $test->results['#fail'] > 0);
394
    $had_exceptions = (isset($test->results['#exception']) && $test->results['#exception'] > 0);
395
    $status = ($had_fails || $had_exceptions ? 'fail' : 'pass');
396
    simpletest_script_print($info['name'] . ' ' . _simpletest_format_summary_line($test->results) . "\n", simpletest_script_color_code($status));
397

    
398
    // Finished, kill this runner.
399
    if ($had_fails || $had_exceptions) {
400
      exit(SIMPLETEST_SCRIPT_EXIT_FAILURE);
401
    }
402
    exit(SIMPLETEST_SCRIPT_EXIT_SUCCESS);
403
  }
404
  catch (Exception $e) {
405
    echo (string) $e;
406
    exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
407
  }
408
}
409

    
410
/**
411
 * Return a command used to run a test in a separate process.
412
 *
413
 * @param $test_id
414
 *  The current test ID.
415
 * @param $test_class
416
 *  The name of the test class to run.
417
 */
418
function simpletest_script_command($test_id, $test_class) {
419
  global $args, $php;
420

    
421
  $command = escapeshellarg($php) . ' ' . escapeshellarg('./scripts/' . $args['script']) . ' --url ' . escapeshellarg($args['url']);
422
  if ($args['color']) {
423
    $command .= ' --color';
424
  }
425
  $command .= " --php " . escapeshellarg($php) . " --test-id $test_id --execute-test " . escapeshellarg($test_class);
426
  return $command;
427
}
428

    
429
/**
430
 * Get list of tests based on arguments. If --all specified then
431
 * returns all available tests, otherwise reads list of tests.
432
 *
433
 * Will print error and exit if no valid tests were found.
434
 *
435
 * @return List of tests.
436
 */
437
function simpletest_script_get_test_list() {
438
  global $args, $all_tests, $groups;
439

    
440
  $test_list = array();
441
  if ($args['all']) {
442
    $test_list = $all_tests;
443
  }
444
  else {
445
    if ($args['class']) {
446
      // Check for valid class names.
447
      $test_list = array();
448
      foreach ($args['test_names'] as $test_class) {
449
        if (class_exists($test_class)) {
450
          $test_list[] = $test_class;
451
        }
452
        else {
453
          $groups = simpletest_test_get_all();
454
          $all_classes = array();
455
          foreach ($groups as $group) {
456
            $all_classes = array_merge($all_classes, array_keys($group));
457
          }
458
          simpletest_script_print_error('Test class not found: ' . $test_class);
459
          simpletest_script_print_alternatives($test_class, $all_classes, 6);
460
          exit(SIMPLETEST_SCRIPT_EXIT_FAILURE);
461
        }
462
      }
463
    }
464
    elseif ($args['file']) {
465
      $files = array();
466
      foreach ($args['test_names'] as $file) {
467
        $files[drupal_realpath($file)] = 1;
468
      }
469

    
470
      // Check for valid class names.
471
      foreach ($all_tests as $class_name) {
472
        $refclass = new ReflectionClass($class_name);
473
        $file = $refclass->getFileName();
474
        if (isset($files[$file])) {
475
          $test_list[] = $class_name;
476
        }
477
      }
478
    }
479
    elseif ($args['directory']) {
480
      // Extract test case class names from specified directory.
481
      // Find all tests in the PSR-X structure; Drupal\$extension\Tests\*.php
482
      // Since we do not want to hard-code too many structural file/directory
483
      // assumptions about PSR-0/4 files and directories, we check for the
484
      // minimal conditions only; i.e., a '*.php' file that has '/Tests/' in
485
      // its path.
486
      // Ignore anything from third party vendors, and ignore template files used in tests.
487
      // And any api.php files.
488
      $ignore = array('nomask' => '/vendor|\.tpl\.php|\.api\.php/');
489
      $files = array();
490
      if ($args['directory'][0] === '/') {
491
        $directory = $args['directory'];
492
      }
493
      else {
494
        $directory = DRUPAL_ROOT . "/" . $args['directory'];
495
      }
496
      $file_list = file_scan_directory($directory, '/\.php|\.test$/', $ignore);
497
      foreach ($file_list as $file) {
498
        // '/Tests/' can be contained anywhere in the file's path (there can be
499
        // sub-directories below /Tests), but must be contained literally.
500
        // Case-insensitive to match all Simpletest and PHPUnit tests:
501
        //   ./lib/Drupal/foo/Tests/Bar/Baz.php
502
        //   ./foo/src/Tests/Bar/Baz.php
503
        //   ./foo/tests/Drupal/foo/Tests/FooTest.php
504
        //   ./foo/tests/src/FooTest.php
505
        // $file->filename doesn't give us a directory, so we use $file->uri
506
        // Strip the drupal root directory and trailing slash off the URI
507
        $filename = substr($file->uri, strlen(DRUPAL_ROOT)+1);
508
        if (stripos($filename, '/Tests/')) {
509
          $files[drupal_realpath($filename)] = 1;
510
        } else if (stripos($filename, '.test')){
511
          $files[drupal_realpath($filename)] = 1;
512
        }
513
      }
514

    
515
      // Check for valid class names.
516
      foreach ($all_tests as $class_name) {
517
        $refclass = new ReflectionClass($class_name);
518
        $classfile = $refclass->getFileName();
519
        if (isset($files[$classfile])) {
520
          $test_list[] = $class_name;
521
        }
522
      }
523
    }
524
    else {
525
      // Check for valid group names and get all valid classes in group.
526
      foreach ($args['test_names'] as $group_name) {
527
        if (isset($groups[$group_name])) {
528
          $test_list = array_merge($test_list, array_keys($groups[$group_name]));
529
        }
530
        else {
531
          simpletest_script_print_error('Test group not found: ' . $group_name);
532
          simpletest_script_print_alternatives($group_name, array_keys($groups));
533
          exit(SIMPLETEST_SCRIPT_EXIT_FAILURE);
534
        }
535
      }
536
    }
537
  }
538

    
539
  if (empty($test_list)) {
540
    simpletest_script_print_error('No valid tests were specified.');
541
    exit(SIMPLETEST_SCRIPT_EXIT_FAILURE);
542
  }
543
  return $test_list;
544
}
545

    
546
/**
547
 * Initialize the reporter.
548
 */
549
function simpletest_script_reporter_init() {
550
  global $args, $all_tests, $test_list, $results_map;
551

    
552
  $results_map = array(
553
    'pass' => 'Pass',
554
    'fail' => 'Fail',
555
    'exception' => 'Exception'
556
  );
557

    
558
  echo "\n";
559
  echo "Drupal test run\n";
560
  echo "---------------\n";
561
  echo "\n";
562

    
563
  // Tell the user about what tests are to be run.
564
  if ($args['all']) {
565
    echo "All tests will run.\n\n";
566
  }
567
  else {
568
    echo "Tests to be run:\n";
569
    foreach ($test_list as $class_name) {
570
      $info = call_user_func(array($class_name, 'getInfo'));
571
      echo " - " . $info['name'] . ' (' . $class_name . ')' . "\n";
572
    }
573
    echo "\n";
574
  }
575

    
576
  echo "Test run started:\n";
577
  echo " " . format_date($_SERVER['REQUEST_TIME'], 'long') . "\n";
578
  timer_start('run-tests');
579
  echo "\n";
580

    
581
  echo "Test summary\n";
582
  echo "------------\n";
583
  echo "\n";
584
}
585

    
586
/**
587
 * Display jUnit XML test results.
588
 */
589
function simpletest_script_reporter_write_xml_results() {
590
  global $args, $test_id, $results_map;
591

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

    
594
  $test_class = '';
595
  $xml_files = array();
596

    
597
  foreach ($results as $result) {
598
    if (isset($results_map[$result->status])) {
599
      if ($result->test_class != $test_class) {
600
        // We've moved onto a new class, so write the last classes results to a file:
601
        if (isset($xml_files[$test_class])) {
602
          file_put_contents($args['xml'] . '/' . $test_class . '.xml', $xml_files[$test_class]['doc']->saveXML());
603
          unset($xml_files[$test_class]);
604
        }
605
        $test_class = $result->test_class;
606
        if (!isset($xml_files[$test_class])) {
607
          $doc = new DomDocument('1.0');
608
          $root = $doc->createElement('testsuite');
609
          $root = $doc->appendChild($root);
610
          $xml_files[$test_class] = array('doc' => $doc, 'suite' => $root);
611
        }
612
      }
613

    
614
      // For convenience:
615
      $dom_document = &$xml_files[$test_class]['doc'];
616

    
617
      // Create the XML element for this test case:
618
      $case = $dom_document->createElement('testcase');
619
      $case->setAttribute('classname', $test_class);
620
      list($class, $name) = explode('->', $result->function, 2);
621
      $case->setAttribute('name', $name);
622

    
623
      // Passes get no further attention, but failures and exceptions get to add more detail:
624
      if ($result->status == 'fail') {
625
        $fail = $dom_document->createElement('failure');
626
        $fail->setAttribute('type', 'failure');
627
        $fail->setAttribute('message', $result->message_group);
628
        $text = $dom_document->createTextNode($result->message);
629
        $fail->appendChild($text);
630
        $case->appendChild($fail);
631
      }
632
      elseif ($result->status == 'exception') {
633
        // In the case of an exception the $result->function may not be a class
634
        // method so we record the full function name:
635
        $case->setAttribute('name', $result->function);
636

    
637
        $fail = $dom_document->createElement('error');
638
        $fail->setAttribute('type', 'exception');
639
        $fail->setAttribute('message', $result->message_group);
640
        $full_message = $result->message . "\n\nline: " . $result->line . "\nfile: " . $result->file;
641
        $text = $dom_document->createTextNode($full_message);
642
        $fail->appendChild($text);
643
        $case->appendChild($fail);
644
      }
645
      // Append the test case XML to the test suite:
646
      $xml_files[$test_class]['suite']->appendChild($case);
647
    }
648
  }
649
  // The last test case hasn't been saved to a file yet, so do that now:
650
  if (isset($xml_files[$test_class])) {
651
    file_put_contents($args['xml'] . '/' . $test_class . '.xml', $xml_files[$test_class]['doc']->saveXML());
652
    unset($xml_files[$test_class]);
653
  }
654
}
655

    
656
/**
657
 * Stop the test timer.
658
 */
659
function simpletest_script_reporter_timer_stop() {
660
  echo "\n";
661
  $end = timer_stop('run-tests');
662
  echo "Test run duration: " . format_interval($end['time'] / 1000);
663
  echo "\n\n";
664
}
665

    
666
/**
667
 * Display test results.
668
 */
669
function simpletest_script_reporter_display_results() {
670
  global $args, $test_id, $results_map;
671

    
672
  if ($args['verbose']) {
673
    // Report results.
674
    echo "Detailed test results\n";
675
    echo "---------------------\n";
676

    
677
    $results = db_query("SELECT * FROM {simpletest} WHERE test_id = :test_id ORDER BY test_class, message_id", array(':test_id' => $test_id));
678
    $test_class = '';
679
    foreach ($results as $result) {
680
      if (isset($results_map[$result->status])) {
681
        if ($result->test_class != $test_class) {
682
          // Display test class every time results are for new test class.
683
          echo "\n\n---- $result->test_class ----\n\n\n";
684
          $test_class = $result->test_class;
685

    
686
          // Print table header.
687
          echo "Status    Group      Filename          Line Function                            \n";
688
          echo "--------------------------------------------------------------------------------\n";
689
        }
690

    
691
        simpletest_script_format_result($result);
692
      }
693
    }
694
  }
695
}
696

    
697
/**
698
 * Format the result so that it fits within the default 80 character
699
 * terminal size.
700
 *
701
 * @param $result The result object to format.
702
 */
703
function simpletest_script_format_result($result) {
704
  global $results_map, $color;
705

    
706
  $summary = sprintf("%-9.9s %-10.10s %-17.17s %4.4s %-35.35s\n",
707
    $results_map[$result->status], $result->message_group, basename($result->file), $result->line, $result->function);
708

    
709
  simpletest_script_print($summary, simpletest_script_color_code($result->status));
710

    
711
  $lines = explode("\n", wordwrap(trim(strip_tags($result->message)), 76));
712
  foreach ($lines as $line) {
713
    echo "    $line\n";
714
  }
715
}
716

    
717
/**
718
 * Print error message prefixed with "  ERROR: " and displayed in fail color
719
 * if color output is enabled.
720
 *
721
 * @param $message The message to print.
722
 */
723
function simpletest_script_print_error($message) {
724
  simpletest_script_print("  ERROR: $message\n", SIMPLETEST_SCRIPT_COLOR_FAIL);
725
}
726

    
727
/**
728
 * Print a message to the console, if color is enabled then the specified
729
 * color code will be used.
730
 *
731
 * @param $message The message to print.
732
 * @param $color_code The color code to use for coloring.
733
 */
734
function simpletest_script_print($message, $color_code) {
735
  global $args;
736
  if (!empty($args['color'])) {
737
    echo "\033[" . $color_code . "m" . $message . "\033[0m";
738
  }
739
  else {
740
    echo $message;
741
  }
742
}
743

    
744
/**
745
 * Get the color code associated with the specified status.
746
 *
747
 * @param $status The status string to get code for.
748
 * @return Color code.
749
 */
750
function simpletest_script_color_code($status) {
751
  switch ($status) {
752
    case 'pass':
753
      return SIMPLETEST_SCRIPT_COLOR_PASS;
754
    case 'fail':
755
      return SIMPLETEST_SCRIPT_COLOR_FAIL;
756
    case 'exception':
757
      return SIMPLETEST_SCRIPT_COLOR_EXCEPTION;
758
  }
759
  return 0; // Default formatting.
760
}
761

    
762
/**
763
 * Prints alternative test names.
764
 *
765
 * Searches the provided array of string values for close matches based on the
766
 * Levenshtein algorithm.
767
 *
768
 * @see http://php.net/manual/en/function.levenshtein.php
769
 *
770
 * @param string $string
771
 *   A string to test.
772
 * @param array $array
773
 *   A list of strings to search.
774
 * @param int $degree
775
 *   The matching strictness. Higher values return fewer matches. A value of
776
 *   4 means that the function will return strings from $array if the candidate
777
 *   string in $array would be identical to $string by changing 1/4 or fewer of
778
 *   its characters.
779
 */
780
function simpletest_script_print_alternatives($string, $array, $degree = 4) {
781
  $alternatives = array();
782
  foreach ($array as $item) {
783
    $lev = levenshtein($string, $item);
784
    if ($lev <= strlen($item) / $degree || FALSE !== strpos($string, $item)) {
785
      $alternatives[] = $item;
786
    }
787
  }
788
  if (!empty($alternatives)) {
789
    simpletest_script_print("  Did you mean?\n", SIMPLETEST_SCRIPT_COLOR_FAIL);
790
    foreach ($alternatives as $alternative) {
791
      simpletest_script_print("  - $alternative\n", SIMPLETEST_SCRIPT_COLOR_FAIL);
792
    }
793
  }
794
}