Project

General

Profile

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

root / drupal7 / scripts / run-tests.sh @ 6d8023f2

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 ($sudo = getenv('SUDO_COMMAND')) {
268
    // 'SUDO_COMMAND' is an environment variable set by the sudo program.
269
    // Extract only the PHP interpreter, not the rest of the command.
270
    list($php, ) = explode(' ', $sudo, 2);
271
  }
272
  else {
273
    simpletest_script_print_error('Unable to automatically determine the path to the PHP interpreter. Supply the --php command line argument.');
274
    simpletest_script_help();
275
    exit(SIMPLETEST_SCRIPT_EXIT_FAILURE);
276
  }
277

    
278
  // Get URL from arguments.
279
  if (!empty($args['url'])) {
280
    $parsed_url = parse_url($args['url']);
281
    $host = $parsed_url['host'] . (isset($parsed_url['port']) ? ':' . $parsed_url['port'] : '');
282
    $path = isset($parsed_url['path']) ? $parsed_url['path'] : '';
283

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

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

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

    
309
  chdir(realpath(dirname(__FILE__) . '/..'));
310
  define('DRUPAL_ROOT', getcwd());
311
  require_once DRUPAL_ROOT . '/includes/bootstrap.inc';
312
}
313

    
314
/**
315
 * Execute a batch of tests.
316
 */
317
function simpletest_script_execute_batch($test_id, $test_classes) {
318
  global $args;
319

    
320
  $total_status = SIMPLETEST_SCRIPT_EXIT_SUCCESS;
321

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

    
330
      // Fork a child process.
331
      $test_class = array_shift($test_classes);
332
      $command = simpletest_script_command($test_id, $test_class);
333
      $process = proc_open($command, array(), $pipes, NULL, NULL, array('bypass_shell' => TRUE));
334

    
335
      if (!is_resource($process)) {
336
        echo "Unable to fork test process. Aborting.\n";
337
        exit(SIMPLETEST_SCRIPT_EXIT_FAILURE);
338
      }
339

    
340
      // Register our new child.
341
      $children[] = array(
342
        'process' => $process,
343
        'class' => $test_class,
344
        'pipes' => $pipes,
345
      );
346
    }
347

    
348
    // Wait for children every 200ms.
349
    usleep(200000);
350

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

    
367
        // Remove this child.
368
        unset($children[$cid]);
369
      }
370
    }
371
  }
372
  return $total_status;
373
}
374

    
375
/**
376
 * Bootstrap Drupal and run a single test.
377
 */
378
function simpletest_script_run_one_test($test_id, $test_class) {
379
  try {
380
    // Bootstrap Drupal.
381
    drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL);
382

    
383
    simpletest_classloader_register();
384

    
385
    $test = new $test_class($test_id);
386
    $test->run();
387
    $info = $test->getInfo();
388

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

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

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

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

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

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

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

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

    
535
  if (empty($test_list)) {
536
    simpletest_script_print_error('No valid tests were specified.');
537
    exit(SIMPLETEST_SCRIPT_EXIT_FAILURE);
538
  }
539
  return $test_list;
540
}
541

    
542
/**
543
 * Initialize the reporter.
544
 */
545
function simpletest_script_reporter_init() {
546
  global $args, $all_tests, $test_list, $results_map;
547

    
548
  $results_map = array(
549
    'pass' => 'Pass',
550
    'fail' => 'Fail',
551
    'exception' => 'Exception'
552
  );
553

    
554
  echo "\n";
555
  echo "Drupal test run\n";
556
  echo "---------------\n";
557
  echo "\n";
558

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

    
572
  echo "Test run started:\n";
573
  echo " " . format_date($_SERVER['REQUEST_TIME'], 'long') . "\n";
574
  timer_start('run-tests');
575
  echo "\n";
576

    
577
  echo "Test summary\n";
578
  echo "------------\n";
579
  echo "\n";
580
}
581

    
582
/**
583
 * Display jUnit XML test results.
584
 */
585
function simpletest_script_reporter_write_xml_results() {
586
  global $args, $test_id, $results_map;
587

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

    
590
  $test_class = '';
591
  $xml_files = array();
592

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

    
610
      // For convenience:
611
      $dom_document = &$xml_files[$test_class]['doc'];
612

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

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

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

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

    
662
/**
663
 * Display test results.
664
 */
665
function simpletest_script_reporter_display_results() {
666
  global $args, $test_id, $results_map;
667

    
668
  if ($args['verbose']) {
669
    // Report results.
670
    echo "Detailed test results\n";
671
    echo "---------------------\n";
672

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

    
682
          // Print table header.
683
          echo "Status    Group      Filename          Line Function                            \n";
684
          echo "--------------------------------------------------------------------------------\n";
685
        }
686

    
687
        simpletest_script_format_result($result);
688
      }
689
    }
690
  }
691
}
692

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

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

    
705
  simpletest_script_print($summary, simpletest_script_color_code($result->status));
706

    
707
  $lines = explode("\n", wordwrap(trim(strip_tags($result->message)), 76));
708
  foreach ($lines as $line) {
709
    echo "    $line\n";
710
  }
711
}
712

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

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

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

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