Projet

Général

Profil

Paste
Télécharger (21,7 ko) Statistiques
| Branche: | Révision:

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

1
<?php
2
/**
3
 * @file
4
 * This script runs Drupal tests from command line.
5
 */
6

    
7
define('SIMPLETEST_SCRIPT_COLOR_PASS', 32); // Green.
8
define('SIMPLETEST_SCRIPT_COLOR_FAIL', 31); // Red.
9
define('SIMPLETEST_SCRIPT_COLOR_EXCEPTION', 33); // Brown.
10

    
11
// Set defaults and get overrides.
12
list($args, $count) = simpletest_script_parse_args();
13

    
14
if ($args['help'] || $count == 0) {
15
  simpletest_script_help();
16
  exit;
17
}
18

    
19
if ($args['execute-test']) {
20
  // Masquerade as Apache for running tests.
21
  simpletest_script_init("Apache");
22
  simpletest_script_run_one_test($args['test-id'], $args['execute-test']);
23
}
24
else {
25
  // Run administrative functions as CLI.
26
  simpletest_script_init(NULL);
27
}
28

    
29
// Bootstrap to perform initial validation or other operations.
30
drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL);
31
if (!module_exists('simpletest')) {
32
  simpletest_script_print_error("The simpletest module must be enabled before this script can run.");
33
  exit;
34
}
35

    
36
if ($args['clean']) {
37
  // Clean up left-over times and directories.
38
  simpletest_clean_environment();
39
  echo "\nEnvironment cleaned.\n";
40

    
41
  // Get the status messages and print them.
42
  $messages = array_pop(drupal_get_messages('status'));
43
  foreach ($messages as $text) {
44
    echo " - " . $text . "\n";
45
  }
46
  exit;
47
}
48

    
49
// Load SimpleTest files.
50
$groups = simpletest_test_get_all();
51
$all_tests = array();
52
foreach ($groups as $group => $tests) {
53
  $all_tests = array_merge($all_tests, array_keys($tests));
54
}
55
$test_list = array();
56

    
57
if ($args['list']) {
58
  // Display all available tests.
59
  echo "\nAvailable test groups & classes\n";
60
  echo   "-------------------------------\n\n";
61
  foreach ($groups as $group => $tests) {
62
    echo $group . "\n";
63
    foreach ($tests as $class => $info) {
64
      echo " - " . $info['name'] . ' (' . $class . ')' . "\n";
65
    }
66
  }
67
  exit;
68
}
69

    
70
$test_list = simpletest_script_get_test_list();
71

    
72
// Try to allocate unlimited time to run the tests.
73
drupal_set_time_limit(0);
74

    
75
simpletest_script_reporter_init();
76

    
77
// Setup database for test results.
78
$test_id = db_insert('simpletest_test_id')->useDefaults(array('test_id'))->execute();
79

    
80
// Execute tests.
81
simpletest_script_execute_batch($test_id, simpletest_script_get_test_list());
82

    
83
// Retrieve the last database prefix used for testing and the last test class
84
// that was run from. Use the information to read the lgo file in case any
85
// fatal errors caused the test to crash.
86
list($last_prefix, $last_test_class) = simpletest_last_test_get($test_id);
87
simpletest_log_read($test_id, $last_prefix, $last_test_class);
88

    
89
// Stop the timer.
90
simpletest_script_reporter_timer_stop();
91

    
92
// Display results before database is cleared.
93
simpletest_script_reporter_display_results();
94

    
95
if ($args['xml']) {
96
  simpletest_script_reporter_write_xml_results();
97
}
98

    
99
// Cleanup our test results.
100
simpletest_clean_results_table($test_id);
101

    
102
// Test complete, exit.
103
exit;
104

    
105
/**
106
 * Print help text.
107
 */
108
function simpletest_script_help() {
109
  global $args;
110

    
111
  echo <<<EOF
112

    
113
Run Drupal tests from the shell.
114

    
115
Usage:        {$args['script']} [OPTIONS] <tests>
116
Example:      {$args['script']} Profile
117

    
118
All arguments are long options.
119

    
120
  --help      Print this page.
121

    
122
  --list      Display all available test groups.
123

    
124
  --clean     Cleans up database tables or directories from previous, failed,
125
              tests and then exits (no tests are run).
126

    
127
  --url       Immediately precedes a URL to set the host and path. You will
128
              need this parameter if Drupal is in a subdirectory on your
129
              localhost and you have not set \$base_url in settings.php. Tests
130
              can be run under SSL by including https:// in the URL.
131

    
132
  --php       The absolute path to the PHP executable. Usually not needed.
133

    
134
  --concurrency [num]
135

    
136
              Run tests in parallel, up to [num] tests at a time.
137

    
138
  --all       Run all available tests.
139

    
140
  --class     Run tests identified by specific class names, instead of group names.
141

    
142
  --file      Run tests identified by specific file names, instead of group names.
143
              Specify the path and the extension (i.e. 'modules/user/user.test').
144

    
145
  --xml       <path>
146

    
147
              If provided, test results will be written as xml files to this path.
148

    
149
  --color     Output text format results with color highlighting.
150

    
151
  --verbose   Output detailed assertion messages in addition to summary.
152

    
153
  <test1>[,<test2>[,<test3> ...]]
154

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

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

    
167
sudo -u [wwwrun|www-data|etc] php ./scripts/{$args['script']}
168
  --url http://example.com/ --all
169
sudo -u [wwwrun|www-data|etc] php ./scripts/{$args['script']}
170
  --url http://example.com/ --class BlockTestCase
171
\n
172
EOF;
173
}
174

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

    
202
  // Override with set values.
203
  $args['script'] = basename(array_shift($_SERVER['argv']));
204

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

    
235
  // Validate the concurrency argument
236
  if (!is_numeric($args['concurrency']) || $args['concurrency'] <= 0) {
237
    simpletest_script_print_error("--concurrency must be a strictly positive integer.");
238
    exit;
239
  }
240

    
241
  return array($args, $count);
242
}
243

    
244
/**
245
 * Initialize script variables and perform general setup requirements.
246
 */
247
function simpletest_script_init($server_software) {
248
  global $args, $php;
249

    
250
  $host = 'localhost';
251
  $path = '';
252
  // Determine location of php command automatically, unless a command line argument is supplied.
253
  if (!empty($args['php'])) {
254
    $php = $args['php'];
255
  }
256
  elseif ($php_env = getenv('_')) {
257
    // '_' is an environment variable set by the shell. It contains the command that was executed.
258
    $php = $php_env;
259
  }
260
  elseif ($sudo = getenv('SUDO_COMMAND')) {
261
    // 'SUDO_COMMAND' is an environment variable set by the sudo program.
262
    // Extract only the PHP interpreter, not the rest of the command.
263
    list($php, ) = explode(' ', $sudo, 2);
264
  }
265
  else {
266
    simpletest_script_print_error('Unable to automatically determine the path to the PHP interpreter. Supply the --php command line argument.');
267
    simpletest_script_help();
268
    exit();
269
  }
270

    
271
  // Get URL from arguments.
272
  if (!empty($args['url'])) {
273
    $parsed_url = parse_url($args['url']);
274
    $host = $parsed_url['host'] . (isset($parsed_url['port']) ? ':' . $parsed_url['port'] : '');
275
    $path = isset($parsed_url['path']) ? $parsed_url['path'] : '';
276

    
277
    // If the passed URL schema is 'https' then setup the $_SERVER variables
278
    // properly so that testing will run under HTTPS.
279
    if ($parsed_url['scheme'] == 'https') {
280
      $_SERVER['HTTPS'] = 'on';
281
    }
282
  }
283

    
284
  $_SERVER['HTTP_HOST'] = $host;
285
  $_SERVER['REMOTE_ADDR'] = '127.0.0.1';
286
  $_SERVER['SERVER_ADDR'] = '127.0.0.1';
287
  $_SERVER['SERVER_SOFTWARE'] = $server_software;
288
  $_SERVER['SERVER_NAME'] = 'localhost';
289
  $_SERVER['REQUEST_URI'] = $path .'/';
290
  $_SERVER['REQUEST_METHOD'] = 'GET';
291
  $_SERVER['SCRIPT_NAME'] = $path .'/index.php';
292
  $_SERVER['PHP_SELF'] = $path .'/index.php';
293
  $_SERVER['HTTP_USER_AGENT'] = 'Drupal command line';
294

    
295
  if (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') {
296
    // Ensure that any and all environment variables are changed to https://.
297
    foreach ($_SERVER as $key => $value) {
298
      $_SERVER[$key] = str_replace('http://', 'https://', $_SERVER[$key]);
299
    }
300
  }
301

    
302
  chdir(realpath(dirname(__FILE__) . '/..'));
303
  define('DRUPAL_ROOT', getcwd());
304
  require_once DRUPAL_ROOT . '/includes/bootstrap.inc';
305
}
306

    
307
/**
308
 * Execute a batch of tests.
309
 */
310
function simpletest_script_execute_batch($test_id, $test_classes) {
311
  global $args;
312

    
313
  // Multi-process execution.
314
  $children = array();
315
  while (!empty($test_classes) || !empty($children)) {
316
    while (count($children) < $args['concurrency']) {
317
      if (empty($test_classes)) {
318
        break;
319
      }
320

    
321
      // Fork a child process.
322
      $test_class = array_shift($test_classes);
323
      $command = simpletest_script_command($test_id, $test_class);
324
      $process = proc_open($command, array(), $pipes, NULL, NULL, array('bypass_shell' => TRUE));
325

    
326
      if (!is_resource($process)) {
327
        echo "Unable to fork test process. Aborting.\n";
328
        exit;
329
      }
330

    
331
      // Register our new child.
332
      $children[] = array(
333
        'process' => $process,
334
        'class' => $test_class,
335
        'pipes' => $pipes,
336
      );
337
    }
338

    
339
    // Wait for children every 200ms.
340
    usleep(200000);
341

    
342
    // Check if some children finished.
343
    foreach ($children as $cid => $child) {
344
      $status = proc_get_status($child['process']);
345
      if (empty($status['running'])) {
346
        // The child exited, unregister it.
347
        proc_close($child['process']);
348
        if ($status['exitcode']) {
349
          echo 'FATAL ' . $test_class . ': test runner returned a non-zero error code (' . $status['exitcode'] . ').' . "\n";
350
        }
351
        unset($children[$cid]);
352
      }
353
    }
354
  }
355
}
356

    
357
/**
358
 * Bootstrap Drupal and run a single test.
359
 */
360
function simpletest_script_run_one_test($test_id, $test_class) {
361
  try {
362
    // Bootstrap Drupal.
363
    drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL);
364

    
365
    simpletest_classloader_register();
366

    
367
    $test = new $test_class($test_id);
368
    $test->run();
369
    $info = $test->getInfo();
370

    
371
    $had_fails = (isset($test->results['#fail']) && $test->results['#fail'] > 0);
372
    $had_exceptions = (isset($test->results['#exception']) && $test->results['#exception'] > 0);
373
    $status = ($had_fails || $had_exceptions ? 'fail' : 'pass');
374
    simpletest_script_print($info['name'] . ' ' . _simpletest_format_summary_line($test->results) . "\n", simpletest_script_color_code($status));
375

    
376
    // Finished, kill this runner.
377
    exit(0);
378
  }
379
  catch (Exception $e) {
380
    echo (string) $e;
381
    exit(1);
382
  }
383
}
384

    
385
/**
386
 * Return a command used to run a test in a separate process.
387
 *
388
 * @param $test_id
389
 *  The current test ID.
390
 * @param $test_class
391
 *  The name of the test class to run.
392
 */
393
function simpletest_script_command($test_id, $test_class) {
394
  global $args, $php;
395

    
396
  $command = escapeshellarg($php) . ' ' . escapeshellarg('./scripts/' . $args['script']) . ' --url ' . escapeshellarg($args['url']);
397
  if ($args['color']) {
398
    $command .= ' --color';
399
  }
400
  $command .= " --php " . escapeshellarg($php) . " --test-id $test_id --execute-test " . escapeshellarg($test_class);
401
  return $command;
402
}
403

    
404
/**
405
 * Get list of tests based on arguments. If --all specified then
406
 * returns all available tests, otherwise reads list of tests.
407
 *
408
 * Will print error and exit if no valid tests were found.
409
 *
410
 * @return List of tests.
411
 */
412
function simpletest_script_get_test_list() {
413
  global $args, $all_tests, $groups;
414

    
415
  $test_list = array();
416
  if ($args['all']) {
417
    $test_list = $all_tests;
418
  }
419
  else {
420
    if ($args['class']) {
421
      // Check for valid class names.
422
      $test_list = array();
423
      foreach ($args['test_names'] as $test_class) {
424
        if (class_exists($test_class)) {
425
          $test_list[] = $test_class;
426
        }
427
        else {
428
          $groups = simpletest_test_get_all();
429
          $all_classes = array();
430
          foreach ($groups as $group) {
431
            $all_classes = array_merge($all_classes, array_keys($group));
432
          }
433
          simpletest_script_print_error('Test class not found: ' . $test_class);
434
          simpletest_script_print_alternatives($test_class, $all_classes, 6);
435
          exit(1);
436
        }
437
      }
438
    }
439
    elseif ($args['file']) {
440
      $files = array();
441
      foreach ($args['test_names'] as $file) {
442
        $files[drupal_realpath($file)] = 1;
443
      }
444

    
445
      // Check for valid class names.
446
      foreach ($all_tests as $class_name) {
447
        $refclass = new ReflectionClass($class_name);
448
        $file = $refclass->getFileName();
449
        if (isset($files[$file])) {
450
          $test_list[] = $class_name;
451
        }
452
      }
453
    }
454
    else {
455
      // Check for valid group names and get all valid classes in group.
456
      foreach ($args['test_names'] as $group_name) {
457
        if (isset($groups[$group_name])) {
458
          $test_list = array_merge($test_list, array_keys($groups[$group_name]));
459
        }
460
        else {
461
          simpletest_script_print_error('Test group not found: ' . $group_name);
462
          simpletest_script_print_alternatives($group_name, array_keys($groups));
463
          exit(1);
464
        }
465
      }
466
    }
467
  }
468

    
469
  if (empty($test_list)) {
470
    simpletest_script_print_error('No valid tests were specified.');
471
    exit;
472
  }
473
  return $test_list;
474
}
475

    
476
/**
477
 * Initialize the reporter.
478
 */
479
function simpletest_script_reporter_init() {
480
  global $args, $all_tests, $test_list, $results_map;
481

    
482
  $results_map = array(
483
    'pass' => 'Pass',
484
    'fail' => 'Fail',
485
    'exception' => 'Exception'
486
  );
487

    
488
  echo "\n";
489
  echo "Drupal test run\n";
490
  echo "---------------\n";
491
  echo "\n";
492

    
493
  // Tell the user about what tests are to be run.
494
  if ($args['all']) {
495
    echo "All tests will run.\n\n";
496
  }
497
  else {
498
    echo "Tests to be run:\n";
499
    foreach ($test_list as $class_name) {
500
      $info = call_user_func(array($class_name, 'getInfo'));
501
      echo " - " . $info['name'] . ' (' . $class_name . ')' . "\n";
502
    }
503
    echo "\n";
504
  }
505

    
506
  echo "Test run started:\n";
507
  echo " " . format_date($_SERVER['REQUEST_TIME'], 'long') . "\n";
508
  timer_start('run-tests');
509
  echo "\n";
510

    
511
  echo "Test summary\n";
512
  echo "------------\n";
513
  echo "\n";
514
}
515

    
516
/**
517
 * Display jUnit XML test results.
518
 */
519
function simpletest_script_reporter_write_xml_results() {
520
  global $args, $test_id, $results_map;
521

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

    
524
  $test_class = '';
525
  $xml_files = array();
526

    
527
  foreach ($results as $result) {
528
    if (isset($results_map[$result->status])) {
529
      if ($result->test_class != $test_class) {
530
        // We've moved onto a new class, so write the last classes results to a file:
531
        if (isset($xml_files[$test_class])) {
532
          file_put_contents($args['xml'] . '/' . $test_class . '.xml', $xml_files[$test_class]['doc']->saveXML());
533
          unset($xml_files[$test_class]);
534
        }
535
        $test_class = $result->test_class;
536
        if (!isset($xml_files[$test_class])) {
537
          $doc = new DomDocument('1.0');
538
          $root = $doc->createElement('testsuite');
539
          $root = $doc->appendChild($root);
540
          $xml_files[$test_class] = array('doc' => $doc, 'suite' => $root);
541
        }
542
      }
543

    
544
      // For convenience:
545
      $dom_document = &$xml_files[$test_class]['doc'];
546

    
547
      // Create the XML element for this test case:
548
      $case = $dom_document->createElement('testcase');
549
      $case->setAttribute('classname', $test_class);
550
      list($class, $name) = explode('->', $result->function, 2);
551
      $case->setAttribute('name', $name);
552

    
553
      // Passes get no further attention, but failures and exceptions get to add more detail:
554
      if ($result->status == 'fail') {
555
        $fail = $dom_document->createElement('failure');
556
        $fail->setAttribute('type', 'failure');
557
        $fail->setAttribute('message', $result->message_group);
558
        $text = $dom_document->createTextNode($result->message);
559
        $fail->appendChild($text);
560
        $case->appendChild($fail);
561
      }
562
      elseif ($result->status == 'exception') {
563
        // In the case of an exception the $result->function may not be a class
564
        // method so we record the full function name:
565
        $case->setAttribute('name', $result->function);
566

    
567
        $fail = $dom_document->createElement('error');
568
        $fail->setAttribute('type', 'exception');
569
        $fail->setAttribute('message', $result->message_group);
570
        $full_message = $result->message . "\n\nline: " . $result->line . "\nfile: " . $result->file;
571
        $text = $dom_document->createTextNode($full_message);
572
        $fail->appendChild($text);
573
        $case->appendChild($fail);
574
      }
575
      // Append the test case XML to the test suite:
576
      $xml_files[$test_class]['suite']->appendChild($case);
577
    }
578
  }
579
  // The last test case hasn't been saved to a file yet, so do that now:
580
  if (isset($xml_files[$test_class])) {
581
    file_put_contents($args['xml'] . '/' . $test_class . '.xml', $xml_files[$test_class]['doc']->saveXML());
582
    unset($xml_files[$test_class]);
583
  }
584
}
585

    
586
/**
587
 * Stop the test timer.
588
 */
589
function simpletest_script_reporter_timer_stop() {
590
  echo "\n";
591
  $end = timer_stop('run-tests');
592
  echo "Test run duration: " . format_interval($end['time'] / 1000);
593
  echo "\n\n";
594
}
595

    
596
/**
597
 * Display test results.
598
 */
599
function simpletest_script_reporter_display_results() {
600
  global $args, $test_id, $results_map;
601

    
602
  if ($args['verbose']) {
603
    // Report results.
604
    echo "Detailed test results\n";
605
    echo "---------------------\n";
606

    
607
    $results = db_query("SELECT * FROM {simpletest} WHERE test_id = :test_id ORDER BY test_class, message_id", array(':test_id' => $test_id));
608
    $test_class = '';
609
    foreach ($results as $result) {
610
      if (isset($results_map[$result->status])) {
611
        if ($result->test_class != $test_class) {
612
          // Display test class every time results are for new test class.
613
          echo "\n\n---- $result->test_class ----\n\n\n";
614
          $test_class = $result->test_class;
615

    
616
          // Print table header.
617
          echo "Status    Group      Filename          Line Function                            \n";
618
          echo "--------------------------------------------------------------------------------\n";
619
        }
620

    
621
        simpletest_script_format_result($result);
622
      }
623
    }
624
  }
625
}
626

    
627
/**
628
 * Format the result so that it fits within the default 80 character
629
 * terminal size.
630
 *
631
 * @param $result The result object to format.
632
 */
633
function simpletest_script_format_result($result) {
634
  global $results_map, $color;
635

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

    
639
  simpletest_script_print($summary, simpletest_script_color_code($result->status));
640

    
641
  $lines = explode("\n", wordwrap(trim(strip_tags($result->message)), 76));
642
  foreach ($lines as $line) {
643
    echo "    $line\n";
644
  }
645
}
646

    
647
/**
648
 * Print error message prefixed with "  ERROR: " and displayed in fail color
649
 * if color output is enabled.
650
 *
651
 * @param $message The message to print.
652
 */
653
function simpletest_script_print_error($message) {
654
  simpletest_script_print("  ERROR: $message\n", SIMPLETEST_SCRIPT_COLOR_FAIL);
655
}
656

    
657
/**
658
 * Print a message to the console, if color is enabled then the specified
659
 * color code will be used.
660
 *
661
 * @param $message The message to print.
662
 * @param $color_code The color code to use for coloring.
663
 */
664
function simpletest_script_print($message, $color_code) {
665
  global $args;
666
  if ($args['color']) {
667
    echo "\033[" . $color_code . "m" . $message . "\033[0m";
668
  }
669
  else {
670
    echo $message;
671
  }
672
}
673

    
674
/**
675
 * Get the color code associated with the specified status.
676
 *
677
 * @param $status The status string to get code for.
678
 * @return Color code.
679
 */
680
function simpletest_script_color_code($status) {
681
  switch ($status) {
682
    case 'pass':
683
      return SIMPLETEST_SCRIPT_COLOR_PASS;
684
    case 'fail':
685
      return SIMPLETEST_SCRIPT_COLOR_FAIL;
686
    case 'exception':
687
      return SIMPLETEST_SCRIPT_COLOR_EXCEPTION;
688
  }
689
  return 0; // Default formatting.
690
}
691

    
692
/**
693
 * Prints alternative test names.
694
 *
695
 * Searches the provided array of string values for close matches based on the
696
 * Levenshtein algorithm.
697
 *
698
 * @see http://php.net/manual/en/function.levenshtein.php
699
 *
700
 * @param string $string
701
 *   A string to test.
702
 * @param array $array
703
 *   A list of strings to search.
704
 * @param int $degree
705
 *   The matching strictness. Higher values return fewer matches. A value of
706
 *   4 means that the function will return strings from $array if the candidate
707
 *   string in $array would be identical to $string by changing 1/4 or fewer of
708
 *   its characters.
709
 */
710
function simpletest_script_print_alternatives($string, $array, $degree = 4) {
711
  $alternatives = array();
712
  foreach ($array as $item) {
713
    $lev = levenshtein($string, $item);
714
    if ($lev <= strlen($item) / $degree || FALSE !== strpos($string, $item)) {
715
      $alternatives[] = $item;
716
    }
717
  }
718
  if (!empty($alternatives)) {
719
    simpletest_script_print("  Did you mean?\n", SIMPLETEST_SCRIPT_COLOR_FAIL);
720
    foreach ($alternatives as $alternative) {
721
      simpletest_script_print("  - $alternative\n", SIMPLETEST_SCRIPT_COLOR_FAIL);
722
    }
723
  }
724
}