run-tests.sh 28.4 KB
Newer Older
1
2
3
<?php
/**
 * @file
4
 * This script runs Drupal tests from command line.
5
 */
6
7
8

require_once __DIR__ . '/../vendor/autoload.php';

9
use Drupal\Component\Utility\Timer;
10
use Drupal\Core\StreamWrapper\PublicStream;
11

12
13
14
const SIMPLETEST_SCRIPT_COLOR_PASS = 32; // Green.
const SIMPLETEST_SCRIPT_COLOR_FAIL = 31; // Red.
const SIMPLETEST_SCRIPT_COLOR_EXCEPTION = 33; // Brown.
15
16
17
18
19
20
21
22
23

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

if ($args['help'] || $count == 0) {
  simpletest_script_help();
  exit;
}

24
if ($args['execute-test']) {
25
26
  // Masquerade as Apache for running tests.
  simpletest_script_init("Apache");
27
  simpletest_script_run_one_test($args['test-id'], $args['execute-test']);
28
  // Sub-process script execution ends here.
29
}
30
31
else {
  // Run administrative functions as CLI.
32
  simpletest_script_init(NULL);
33
}
34

35
// Bootstrap to perform initial validation or other operations.
36
drupal_bootstrap(DRUPAL_BOOTSTRAP_CODE);
37

38
if (!\Drupal::moduleHandler()->moduleExists('simpletest')) {
39
  simpletest_script_print_error("The Testing (simpletest) module must be installed before this script can run.");
40
41
  exit;
}
42
simpletest_classloader_register();
43
44
45
46
// We have to add a Request.
$request = \Symfony\Component\HttpFoundation\Request::createFromGlobals();
$container = \Drupal::getContainer();
$container->set('request', $request);
47
48
49
50
51
52
53
54

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

  // Get the status messages and print them.
  $messages = array_pop(drupal_get_messages('status'));
55
  foreach ($messages as $text) {
56
57
58
59
60
61
    echo " - " . $text . "\n";
  }
  exit;
}

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

75
76
$test_list = simpletest_script_get_test_list();

77
78
// Try to allocate unlimited time to run the tests.
drupal_set_time_limit(0);
79
80
81
82

simpletest_script_reporter_init();

// Execute tests.
83
84
for ($i = 0; $i < $args['repeat']; $i++) {
  simpletest_script_execute_batch($test_list);
85
}
86

87
88
89
// Stop the timer.
simpletest_script_reporter_timer_stop();

90
91
92
// Display results before database is cleared.
simpletest_script_reporter_display_results();

93
94
95
96
if ($args['xml']) {
  simpletest_script_reporter_write_xml_results();
}

97
// Clean up all test results.
98
99
100
if (!$args['keep-results']) {
  simpletest_clean_results_table();
}
101

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

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

  echo <<<EOF

Run Drupal tests from the shell.

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

All arguments are long options.

  --help      Print this page.
121
122
123

  --list      Display all available test groups.

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

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
130
              localhost and you have not set \$base_url in settings.php. Tests
              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
135
  --concurrency [num]

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

138
  --all       Run all available tests.
139

140
141
142
  --module    Run all tests belonging to the specified module name.
              (e.g., 'node')

143
  --class     Run tests identified by specific class names, instead of group names.
144

145
  --file      Run tests identified by specific file names, instead of group names.
146
147
              Specify the path and the extension
              (i.e. 'core/modules/user/user.test').
148

149
150
151
152
153
  --xml       <path>

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

  --color     Output text format results with color highlighting.
154
155
156

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

157
158
159
160
161
  --keep-results

              Keeps detailed assertion results (in the database) after tests
              have completed. By default, assertion results are cleared.

162
163
164
165
  --repeat    Number of times to repeat the test.

  --die-on-fail

166
167
168
169
              Exit test execution immediately upon any failed assertion. This
              allows to access the test site by changing settings.php to use the
              test database and configuration directories. Use in combination
              with --repeat for debugging random test failures.
170

171
172
  <test1>[,<test2>[,<test3> ...]]

173
              One or more tests to be run. By default, these are interpreted
174
              as the names of test groups as shown at
175
              admin/config/development/testing.
176
177
178
              These group names typically correspond to module names like "User"
              or "Profile" or "System", but there is also a group "XML-RPC".
              If --class is specified then these are interpreted as the names of
179
180
              specific test classes whose test methods will be run. Tests must
              be separated by commas. Ignored if --all is specified.
181

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

185
sudo -u [wwwrun|www-data|etc] php ./core/scripts/{$args['script']}
186
  --url http://example.com/ --all
187
sudo -u [wwwrun|www-data|etc] php ./core/scripts/{$args['script']}
188
  --url http://example.com/ --class "Drupal\block\Tests\BlockTest"
189
190
191
192
\n
EOF;
}

193
194
195
196
197
198
199
200
201
202
203
204
205
/**
 * Parse execution argument and ensure that all are valid.
 *
 * @return The list of arguments.
 */
function simpletest_script_parse_args() {
  // Set default values.
  $args = array(
    'script' => '',
    'help' => FALSE,
    'list' => FALSE,
    'clean' => FALSE,
    'url' => '',
206
    'php' => '',
207
208
    'concurrency' => 1,
    'all' => FALSE,
209
    'module' => NULL,
210
    'class' => FALSE,
211
    'file' => FALSE,
212
213
    'color' => FALSE,
    'verbose' => FALSE,
214
    'keep-results' => FALSE,
215
    'test_names' => array(),
216
217
    'repeat' => 1,
    'die-on-fail' => FALSE,
218
    // Used internally.
219
220
    'test-id' => 0,
    'execute-test' => '',
221
    'xml' => '',
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
  );

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

  $count = 0;
  while ($arg = array_shift($_SERVER['argv'])) {
    if (preg_match('/--(\S+)/', $arg, $matches)) {
      // Argument found.
      if (array_key_exists($matches[1], $args)) {
        // Argument found in list.
        $previous_arg = $matches[1];
        if (is_bool($args[$previous_arg])) {
          $args[$matches[1]] = TRUE;
        }
        else {
          $args[$matches[1]] = array_shift($_SERVER['argv']);
        }
240
        // Clear extraneous values.
241
242
243
244
245
246
247
248
249
250
251
252
        $args['test_names'] = array();
        $count++;
      }
      else {
        // Argument not found in list.
        simpletest_script_print_error("Unknown argument '$arg'.");
        exit;
      }
    }
    else {
      // Values found without an argument should be test names.
      $args['test_names'] += explode(',', $arg);
253
      $count++;
254
    }
255
  }
256
257
258
259
260
261
262

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

263
  return array($args, $count);
264
265
}

266
267
268
/**
 * Initialize script variables and perform general setup requirements.
 */
269
function simpletest_script_init($server_software) {
270
271
272
273
  global $args, $php;

  $host = 'localhost';
  $path = '';
274
275
  $port = '80';

276
  // Determine location of php command automatically, unless a command line argument is supplied.
277
  if (!empty($args['php'])) {
278
279
    $php = $args['php'];
  }
280
  elseif ($php_env = getenv('_')) {
281
    // '_' is an environment variable set by the shell. It contains the command that was executed.
282
    $php = $php_env;
283
  }
284
  elseif ($sudo = getenv('SUDO_COMMAND')) {
285
286
    // 'SUDO_COMMAND' is an environment variable set by the sudo program.
    // Extract only the PHP interpreter, not the rest of the command.
287
    list($php, ) = explode(' ', $sudo, 2);
288
289
  }
  else {
290
    simpletest_script_print_error('Unable to automatically determine the path to the PHP interpreter. Supply the --php command line argument.');
291
    simpletest_script_help();
292
293
    exit();
  }
294

295
  // Get URL from arguments.
296
297
  if (!empty($args['url'])) {
    $parsed_url = parse_url($args['url']);
298
    $host = $parsed_url['host'] . (isset($parsed_url['port']) ? ':' . $parsed_url['port'] : '');
299
    $path = isset($parsed_url['path']) ? rtrim(rtrim($parsed_url['path']), '/') : '';
300
    $port = (isset($parsed_url['port']) ? $parsed_url['port'] : $port);
301
302
303
    if ($path == '/') {
      $path = '';
    }
304
    // If the passed URL schema is 'https' then setup the $_SERVER variables
305
    // properly so that testing will run under HTTPS.
306
307
308
    if ($parsed_url['scheme'] == 'https') {
      $_SERVER['HTTPS'] = 'on';
    }
309
310
311
312
313
  }

  $_SERVER['HTTP_HOST'] = $host;
  $_SERVER['REMOTE_ADDR'] = '127.0.0.1';
  $_SERVER['SERVER_ADDR'] = '127.0.0.1';
314
  $_SERVER['SERVER_PORT'] = $port;
315
  $_SERVER['SERVER_SOFTWARE'] = $server_software;
316
317
  $_SERVER['SERVER_NAME'] = 'localhost';
  $_SERVER['REQUEST_URI'] = $path .'/';
318
  $_SERVER['REQUEST_METHOD'] = 'GET';
319
  $_SERVER['SCRIPT_NAME'] = $path .'/index.php';
320
  $_SERVER['SCRIPT_FILENAME'] = $path .'/index.php';
321
322
323
  $_SERVER['PHP_SELF'] = $path .'/index.php';
  $_SERVER['HTTP_USER_AGENT'] = 'Drupal command line';

324
  if (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') {
325
326
327
328
329
330
    // Ensure that any and all environment variables are changed to https://.
    foreach ($_SERVER as $key => $value) {
      $_SERVER[$key] = str_replace('http://', 'https://', $_SERVER[$key]);
    }
  }

331
  chdir(realpath(__DIR__ . '/../..'));
332
  require_once dirname(__DIR__) . '/includes/bootstrap.inc';
333
334
}

335
336
337
/**
 * Get all available tests from simpletest and PHPUnit.
 *
338
339
340
341
 * @param string $module
 *   Name of a module. If set then only tests belonging to this module are
 *   returned.
 *
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
 * @return
 *   An array of tests keyed with the groups specified in each of the tests
 *   getInfo() method and then keyed by the test class. An example of the array
 *   structure is provided below.
 *
 *   @code
 *     $groups['Block'] => array(
 *       'BlockTestCase' => array(
 *         'name' => 'Block functionality',
 *         'description' => 'Add, edit and delete custom block...',
 *         'group' => 'Block',
 *       ),
 *     );
 *   @endcode
 */
357
358
359
function simpletest_script_get_all_tests($module = NULL) {
  $tests = simpletest_test_get_all($module);
  $tests['PHPUnit'] = simpletest_phpunit_get_available_tests($module);
360
361
362
  return $tests;
}

363
364
365
/**
 * Execute a batch of tests.
 */
366
function simpletest_script_execute_batch($test_classes) {
367
  global $args, $test_ids;
368

369
370
  // Multi-process execution.
  $children = array();
371
  while (!empty($test_classes) || !empty($children)) {
372
    while (count($children) < $args['concurrency']) {
373
      if (empty($test_classes)) {
374
        break;
375
      }
376

377
378
      $test_id = db_insert('simpletest_test_id')->useDefaults(array('test_id'))->execute();
      $test_ids[] = $test_id;
379

380
      $test_class = array_shift($test_classes);
381
382
      // Process phpunit tests immediately since they are fast and we don't need
      // to fork for them.
383
384
      if (is_subclass_of($test_class, 'Drupal\Tests\UnitTestCase')) {
        simpletest_script_run_phpunit($test_id, $test_class);
385
386
387
388
        continue;
      }

      // Fork a child process.
389
390
391
392
393
394
      $command = simpletest_script_command($test_id, $test_class);
      $process = proc_open($command, array(), $pipes, NULL, NULL, array('bypass_shell' => TRUE));

      if (!is_resource($process)) {
        echo "Unable to fork test process. Aborting.\n";
        exit;
395
396
      }

397
398
399
      // Register our new child.
      $children[] = array(
        'process' => $process,
400
        'test_id' => $test_id,
401
402
403
404
        'class' => $test_class,
        'pipes' => $pipes,
      );
    }
405

406
407
408
409
410
411
412
413
414
415
    // Wait for children every 200ms.
    usleep(200000);

    // Check if some children finished.
    foreach ($children as $cid => $child) {
      $status = proc_get_status($child['process']);
      if (empty($status['running'])) {
        // The child exited, unregister it.
        proc_close($child['process']);
        if ($status['exitcode']) {
416
          echo 'FATAL ' . $child['class'] . ': test runner returned a non-zero error code (' . $status['exitcode'] . ').' . "\n";
417
418
          if ($args['die-on-fail']) {
            list($db_prefix, ) = simpletest_last_test_get($child['test_id']);
419
            $public_files = PublicStream::basePath();
420
421
            $test_directory = $public_files . '/simpletest/' . substr($db_prefix, 10);
            echo 'Simpletest database and files kept and test exited immediately on fail so should be reproducible if you change settings.php to use the database prefix '. $db_prefix . ' and config directories in '. $test_directory . "\n";
422
423
424
            $args['keep-results'] = TRUE;
            // Exit repeat loop immediately.
            $args['repeat'] = -1;
425
          }
426
        }
427
        // Free-up space by removing any potentially created resources.
428
429
430
        if (!$args['keep-results']) {
          simpletest_script_cleanup($child['test_id'], $child['class'], $status['exitcode']);
        }
431
432

        // Remove this child.
433
        unset($children[$cid]);
434
435
436
437
438
      }
    }
  }
}

439
440
441
/**
 * Run a group of phpunit tests.
 */
442
443
function simpletest_script_run_phpunit($test_id, $class) {
  $results = simpletest_run_phpunit_tests($test_id, array($class));
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
  simpletest_process_phpunit_results($results);

  // Map phpunit results to a data structure we can pass to
  // _simpletest_format_summary_line.
  $summaries = array();
  foreach ($results as $result) {
    if (!isset($summaries[$result['test_class']])) {
      $summaries[$result['test_class']] = array(
        '#pass' => 0,
        '#fail' => 0,
        '#exception' => 0,
        '#debug' => 0,
      );
    }

    switch ($result['status']) {
      case 'pass':
        $summaries[$result['test_class']]['#pass']++;
        break;
      case 'fail':
        $summaries[$result['test_class']]['#fail']++;
        break;
      case 'exception':
        $summaries[$result['test_class']]['#exception']++;
        break;
      case 'debug':
470
        $summaries[$result['test_class']]['#debug']++;
471
472
473
474
475
476
477
478
479
480
481
482
483
        break;
    }
  }

  foreach ($summaries as $class => $summary) {
    $had_fails = $summary['#fail'] > 0;
    $had_exceptions = $summary['#exception'] > 0;
    $status = ($had_fails || $had_exceptions ? 'fail' : 'pass');
    $info = call_user_func(array($class, 'getInfo'));
    simpletest_script_print($info['name'] . ' ' . _simpletest_format_summary_line($summary) . "\n", simpletest_script_color_code($status));
  }
}

484
/**
485
 * Bootstrap Drupal and run a single test.
486
487
 */
function simpletest_script_run_one_test($test_id, $test_class) {
488
  global $args;
489

490
491
  try {
    // Bootstrap Drupal.
492
    drupal_bootstrap(DRUPAL_BOOTSTRAP_CODE);
493
    simpletest_classloader_register();
494
495
496
497
    // We have to add a Request.
    $request = \Symfony\Component\HttpFoundation\Request::createFromGlobals();
    $container = \Drupal::getContainer();
    $container->set('request', $request);
498

499
    $test = new $test_class($test_id);
500
    $test->dieOnFail = (bool) $args['die-on-fail'];
501
    $test->verbose = (bool) $args['verbose'];
502
503
504
505
506
507
508
    $test->run();
    $info = $test->getInfo();

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

510
511
512
    // Finished, kill this runner.
    exit(0);
  }
513
514
  // DrupalTestCase::run() catches exceptions already, so this is only reached
  // when an exception is thrown in the wrapping test runner environment.
515
516
517
518
  catch (Exception $e) {
    echo (string) $e;
    exit(1);
  }
519
520
}

521
/**
522
523
524
525
526
527
 * Return a command used to run a test in a separate process.
 *
 * @param $test_id
 *  The current test ID.
 * @param $test_class
 *  The name of the test class to run.
528
 */
529
function simpletest_script_command($test_id, $test_class) {
530
  global $args, $php;
531

532
533
534
535
  $command = escapeshellarg($php) . ' ' . escapeshellarg('./core/scripts/' . $args['script']);
  $command .= ' --url ' . escapeshellarg($args['url']);
  $command .= ' --php ' . escapeshellarg($php);
  $command .= " --test-id $test_id";
536
  foreach (array('verbose', 'keep-results', 'color', 'die-on-fail') as $arg) {
537
538
539
    if ($args[$arg]) {
      $command .= ' --' . $arg;
    }
540
  }
541
542
  // --execute-test and class name needs to come last.
  $command .= ' --execute-test ' . escapeshellarg($test_class);
543
  return $command;
544
545
}

546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
/**
 * Removes all remnants of a test runner.
 *
 * In case a (e.g., fatal) error occurs after the test site has been fully setup
 * and the error happens in many tests, the environment that executes the tests
 * can easily run out of memory or disk space. This function ensures that all
 * created resources are properly cleaned up after every executed test.
 *
 * This clean-up only exists in this script, since SimpleTest module itself does
 * not use isolated sub-processes for each test being run, so a fatal error
 * halts not only the test, but also the test runner (i.e., the parent site).
 *
 * @param int $test_id
 *   The test ID of the test run.
 * @param string $test_class
 *   The class name of the test run.
 * @param int $exitcode
 *   The exit code of the test runner.
 *
 * @see simpletest_script_run_one_test()
 */
function simpletest_script_cleanup($test_id, $test_class, $exitcode) {
  // Retrieve the last database prefix used for testing.
  list($db_prefix, ) = simpletest_last_test_get($test_id);

  // If no database prefix was found, then the test was not set up correctly.
  if (empty($db_prefix)) {
    echo "\nFATAL $test_class: Found no database prefix for test ID $test_id. (Check whether setUp() is invoked correctly.)";
    return;
  }

  // Do not output verbose cleanup messages in case of a positive exitcode.
  $output = !empty($exitcode);
  $messages = array();

  $messages[] = "- Found database prefix '$db_prefix' for test ID $test_id.";

  // Read the log file in case any fatal errors caused the test to crash.
  simpletest_log_read($test_id, $db_prefix, $test_class);

  // Check whether a test file directory was setup already.
  // @see prepareEnvironment()
588
  $public_files = PublicStream::basePath();
589
590
591
592
593
594
595
596
597
598
599
600
601
602
  $test_directory = $public_files . '/simpletest/' . substr($db_prefix, 10);
  if (is_dir($test_directory)) {
    // Output the error_log.
    if (is_file($test_directory . '/error.log')) {
      if ($errors = file_get_contents($test_directory . '/error.log')) {
        $output = TRUE;
        $messages[] = $errors;
      }
    }

    // Delete the test files directory.
    // simpletest_clean_temporary_directories() cannot be used here, since it
    // would also delete file directories of other tests that are potentially
    // running concurrently.
603
    file_unmanaged_delete_recursive($test_directory, array('Drupal\simpletest\TestBase', 'filePreDeleteCallback'));
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
    $messages[] = "- Removed test files directory.";
  }

  // Clear out all database tables from the test.
  $count = 0;
  foreach (db_find_tables($db_prefix . '%') as $table) {
    db_drop_table($table);
    $count++;
  }
  if ($count) {
    $messages[] = "- " . format_plural($count, 'Removed 1 leftover table.', 'Removed @count leftover tables.');
  }

  if ($output) {
    echo implode("\n", $messages);
    echo "\n";
  }
}

623
/**
624
 * Get list of tests based on arguments. If --all specified then
625
626
627
628
629
630
631
 * returns all available tests, otherwise reads list of tests.
 *
 * Will print error and exit if no valid tests were found.
 *
 * @return List of tests.
 */
function simpletest_script_get_test_list() {
632
  global $args;
633
634

  $test_list = array();
635
636
  if ($args['all'] || $args['module']) {
    $groups = simpletest_script_get_all_tests($args['module']);
637
638
    $all_tests = array();
    foreach ($groups as $group => $tests) {
639
      $all_tests = array_merge($all_tests, array_keys($tests));
640
    }
641
    $test_list = $all_tests;
642
643
  }
  else {
644
    if ($args['class']) {
645
      foreach ($args['test_names'] as $class_name) {
646
647
648
        $test_list[] = $class_name;
      }
    }
649
    elseif ($args['file']) {
650
      // Extract test case class names from specified files.
651
      foreach ($args['test_names'] as $file) {
652
653
654
655
656
657
658
659
660
661
662
663
664
665
        if (!file_exists($file)) {
          simpletest_script_print_error('File not found: ' . $file);
          exit;
        }
        $content = file_get_contents($file);
        // Extract a potential namespace.
        $namespace = FALSE;
        if (preg_match('@^namespace ([^ ;]+)@m', $content, $matches)) {
          $namespace = $matches[1];
        }
        // Extract all class names.
        // Abstract classes are excluded on purpose.
        preg_match_all('@^class ([^ ]+)@m', $content, $matches);
        if (!$namespace) {
666
          $test_list = array_merge($test_list, $matches[1]);
667
668
669
        }
        else {
          foreach ($matches[1] as $class_name) {
670
671
672
673
            $namespace_class = $namespace . '\\' . $class_name;
            if (is_subclass_of($namespace_class, '\Drupal\simpletest\TestBase') || is_subclass_of($namespace_class, '\Drupal\Tests\UnitTestCase')) {
              $test_list[] = $namespace_class;
            }
674
          }
675
676
677
        }
      }
    }
678
    else {
679
      $groups = simpletest_script_get_all_tests();
680
      foreach ($args['test_names'] as $group_name) {
681
        $test_list = array_merge($test_list, array_keys($groups[$group_name]));
682
683
      }
    }
684
  }
685

686
687
688
689
690
  if (empty($test_list)) {
    simpletest_script_print_error('No valid tests were specified.');
    exit;
  }
  return $test_list;
691
692
}

693
694
695
696
/**
 * Initialize the reporter.
 */
function simpletest_script_reporter_init() {
697
  global $args, $test_list, $results_map;
698
699
700
701
702
703

  $results_map = array(
    'pass' => 'Pass',
    'fail' => 'Fail',
    'exception' => 'Exception'
  );
704

705
706
707
708
  echo "\n";
  echo "Drupal test run\n";
  echo "---------------\n";
  echo "\n";
709

710
711
712
713
714
715
  // Tell the user about what tests are to be run.
  if ($args['all']) {
    echo "All tests will run.\n\n";
  }
  else {
    echo "Tests to be run:\n";
716
717
718
    foreach ($test_list as $class_name) {
      $info = call_user_func(array($class_name, 'getInfo'));
      echo " - " . $info['name'] . ' (' . $class_name . ')' . "\n";
719
720
    }
    echo "\n";
721
  }
722

723
724
  echo "Test run started:\n";
  echo " " . format_date($_SERVER['REQUEST_TIME'], 'long') . "\n";
725
  Timer::start('run-tests');
726
727
  echo "\n";

728
729
  echo "Test summary\n";
  echo "------------\n";
730
  echo "\n";
731
732
}

733
/**
734
 * Display jUnit XML test results.
735
 */
736
function simpletest_script_reporter_write_xml_results() {
737
  global $args, $test_ids, $results_map;
738

739
  $results = db_query("SELECT * FROM {simpletest} WHERE test_id IN (:test_ids) ORDER BY test_class, message_id", array(':test_ids' => $test_ids));
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806

  $test_class = '';
  $xml_files = array();

  foreach ($results as $result) {
    if (isset($results_map[$result->status])) {
      if ($result->test_class != $test_class) {
        // We've moved onto a new class, so write the last classes results to a file:
        if (isset($xml_files[$test_class])) {
          file_put_contents($args['xml'] . '/' . $test_class . '.xml', $xml_files[$test_class]['doc']->saveXML());
          unset($xml_files[$test_class]);
        }
        $test_class = $result->test_class;
        if (!isset($xml_files[$test_class])) {
          $doc = new DomDocument('1.0');
          $root = $doc->createElement('testsuite');
          $root = $doc->appendChild($root);
          $xml_files[$test_class] = array('doc' => $doc, 'suite' => $root);
        }
      }

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

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

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

        $fail = $dom_document->createElement('error');
        $fail->setAttribute('type', 'exception');
        $fail->setAttribute('message', $result->message_group);
        $full_message = $result->message . "\n\nline: " . $result->line . "\nfile: " . $result->file;
        $text = $dom_document->createTextNode($full_message);
        $fail->appendChild($text);
        $case->appendChild($fail);
      }
      // Append the test case XML to the test suite:
      $xml_files[$test_class]['suite']->appendChild($case);
    }
  }
  // The last test case hasn't been saved to a file yet, so do that now:
  if (isset($xml_files[$test_class])) {
    file_put_contents($args['xml'] . '/' . $test_class . '.xml', $xml_files[$test_class]['doc']->saveXML());
    unset($xml_files[$test_class]);
  }
}

/**
 * Stop the test timer.
 */
function simpletest_script_reporter_timer_stop() {
807
  echo "\n";
808
  $end = Timer::stop('run-tests');
809
  echo "Test run duration: " . format_interval($end['time'] / 1000);
810
  echo "\n\n";
811
812
813
814
815
816
}

/**
 * Display test results.
 */
function simpletest_script_reporter_display_results() {
817
  global $args, $test_ids, $results_map;
818

819
820
  if ($args['verbose']) {
    // Report results.
821
822
    echo "Detailed test results\n";
    echo "---------------------\n";
823

824
    $results = db_query("SELECT * FROM {simpletest} WHERE test_id IN (:test_ids) ORDER BY test_class, message_id", array(':test_ids' => $test_ids));
825
    $test_class = '';
826
    foreach ($results as $result) {
827
828
829
830
831
      if (isset($results_map[$result->status])) {
        if ($result->test_class != $test_class) {
          // Display test class every time results are for new test class.
          echo "\n\n---- $result->test_class ----\n\n\n";
          $test_class = $result->test_class;
832

833
834
835
          // Print table header.
          echo "Status    Group      Filename          Line Function                            \n";
          echo "--------------------------------------------------------------------------------\n";
836
837
838
839
840
        }

        simpletest_script_format_result($result);
      }
    }
841
842
843
  }
}

844
845
846
847
848
849
850
851
852
/**
 * Format the result so that it fits within the default 80 character
 * terminal size.
 *
 * @param $result The result object to format.
 */
function simpletest_script_format_result($result) {
  global $results_map, $color;

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

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

858
859
860
861
862
  $lines = explode("\n", wordwrap(trim(strip_tags($result->message)), 76));
  foreach ($lines as $line) {
    echo "    $line\n";
  }
}
863

864
/**
865
866
867
868
 * Print error message prefixed with "  ERROR: " and displayed in fail color
 * if color output is enabled.
 *
 * @param $message The message to print.
869
 */
870
871
function simpletest_script_print_error($message) {
  simpletest_script_print("  ERROR: $message\n", SIMPLETEST_SCRIPT_COLOR_FAIL);
872
}
873

874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
/**
 * Print a message to the console, if color is enabled then the specified
 * color code will be used.
 *
 * @param $message The message to print.
 * @param $color_code The color code to use for coloring.
 */
function simpletest_script_print($message, $color_code) {
  global $args;
  if ($args['color']) {
    echo "\033[" . $color_code . "m" . $message . "\033[0m";
  }
  else {
    echo $message;
  }
}

/**
 * Get the color code associated with the specified status.
 *
 * @param $status The status string to get code for.
 * @return Color code.
 */
function simpletest_script_color_code($status) {
  switch ($status) {
    case 'pass':
      return SIMPLETEST_SCRIPT_COLOR_PASS;
    case 'fail':
      return SIMPLETEST_SCRIPT_COLOR_FAIL;
    case 'exception':
      return SIMPLETEST_SCRIPT_COLOR_EXCEPTION;
  }
  return 0; // Default formatting.
}