run-tests.sh 36.6 KB
Newer Older
1
<?php
2

3
4
/**
 * @file
5
 * This script runs Drupal tests from command line.
6
 */
7

8
use Drupal\Component\Utility\Settings;
9
use Drupal\Component\Utility\Timer;
10
11
12
13
14
use Drupal\Core\Database\Database;
use Drupal\Core\DrupalKernel;
use Symfony\Component\HttpFoundation\Request;

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

16
17
18
const SIMPLETEST_SCRIPT_COLOR_PASS = 32; // Green.
const SIMPLETEST_SCRIPT_COLOR_FAIL = 31; // Red.
const SIMPLETEST_SCRIPT_COLOR_EXCEPTION = 33; // Brown.
19
20
21
22
23
24
25
26
27

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

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

28
29
30
simpletest_script_init();
simpletest_script_bootstrap();

31
if ($args['execute-test']) {
32
  simpletest_script_setup_database();
33
  simpletest_script_run_one_test($args['test-id'], $args['execute-test']);
34
  // Sub-process exited already; this is just for clarity.
35
36
  exit;
}
37
38

simpletest_script_setup_database(TRUE);
39
40

if ($args['clean']) {
41
  // Clean up left-over tables and directories.
42
43
44
45
  simpletest_clean_environment();
  echo "\nEnvironment cleaned.\n";

  // Get the status messages and print them.
46
47
  $messages = drupal_get_messages('status');
  foreach ($messages['status'] as $text) {
48
49
50
51
52
53
    echo " - " . $text . "\n";
  }
  exit;
}

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

67
68
$test_list = simpletest_script_get_test_list();

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

simpletest_script_reporter_init();

// Execute tests.
75
76
for ($i = 0; $i < $args['repeat']; $i++) {
  simpletest_script_execute_batch($test_list);
77
}
78

79
80
81
// Stop the timer.
simpletest_script_reporter_timer_stop();

82
83
84
// Display results before database is cleared.
simpletest_script_reporter_display_results();

85
86
87
88
if ($args['xml']) {
  simpletest_script_reporter_write_xml_results();
}

89
// Clean up all test results.
90
91
92
if (!$args['keep-results']) {
  simpletest_clean_results_table();
}
93

94
95
96
// Test complete, exit.
exit;

97
98
99
100
101
/**
 * Print help text.
 */
function simpletest_script_help() {
  global $args;
102
103
104
105
106

  echo <<<EOF

Run Drupal tests from the shell.

107
108
Usage:        {$args['script']} [OPTIONS] <tests>
Example:      {$args['script']} Profile
109
110
111
112

All arguments are long options.

  --help      Print this page.
113
114
115

  --list      Display all available test groups.

116
117
118
  --clean     Cleans up database tables or directories from previous, failed,
              tests and then exits (no tests are run).

119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
  --url       The base URL of the root directory of this Drupal checkout; e.g.:
                http://drupal.test/
              Required unless the Drupal root directory maps exactly to:
                http://localhost:80/
              Use a https:// URL to force all tests to be run under SSL.

  --sqlite    A pathname to use for the SQLite database of the test runner.
              Required unless this script is executed with a working Drupal
              installation that has Simpletest module installed.
              A relative pathname is interpreted relative to the Drupal root
              directory.
              Note that ':memory:' cannot be used, because this script spawns
              sub-processes. However, you may use e.g. '/tmpfs/test.sqlite'

  --dburl     A URI denoting the database driver, credentials, server hostname,
              and database name to use in tests. For example:
                mysql://username:password@localhost/databasename#table_prefix
              Only used if specified.
              Required when running tests without a Drupal installation that
              contains default database connection info in settings.php.
139

140
  --php       The absolute path to the PHP executable. Usually not needed.
141

142
143
  --concurrency [num]

144
              Run tests in parallel, up to [num] tests at a time.
145

146
  --all       Run all available tests.
147

148
149
150
  --module    Run all tests belonging to the specified module name.
              (e.g., 'node')

151
  --class     Run tests identified by specific class names, instead of group names.
152

153
  --file      Run tests identified by specific file names, instead of group names.
154
155
              Specify the path and the extension
              (i.e. 'core/modules/user/user.test').
156

157
158
159
160
161
  --xml       <path>

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

  --color     Output text format results with color highlighting.
162
163
164

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

165
166
167
168
169
  --keep-results

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

170
171
172
173
  --repeat    Number of times to repeat the test.

  --die-on-fail

174
175
176
177
              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.
178

179
180
  <test1>[,<test2>[,<test3> ...]]

181
              One or more tests to be run. By default, these are interpreted
182
              as the names of test groups as shown at
183
              admin/config/development/testing.
184
185
186
              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
187
188
              specific test classes whose test methods will be run. Tests must
              be separated by commas. Ignored if --all is specified.
189

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

193
sudo -u [wwwrun|www-data|etc] php ./core/scripts/{$args['script']}
194
  --url http://example.com/ --all
195
sudo -u [wwwrun|www-data|etc] php ./core/scripts/{$args['script']}
196
  --url http://example.com/ --class "Drupal\block\Tests\BlockTest"
197
198
199
200
201
202
203
204
205
206

Without a preinstalled Drupal site and enabled Simpletest module, specify a
SQLite database pathname to create and the default database connection info to
use in tests:

sudo -u [wwwrun|www-data|etc] php ./core/scripts/{$args['script']}
  --sqlite /tmpfs/drupal/test.sqlite
  --dburl mysql://username:password@localhost/database
  --url http://example.com/ --all

207
208
209
EOF;
}

210
211
212
213
214
215
216
217
218
219
220
221
222
/**
 * 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' => '',
223
224
    'sqlite' => NULL,
    'dburl' => NULL,
225
    'php' => '',
226
227
    'concurrency' => 1,
    'all' => FALSE,
228
    'module' => NULL,
229
    'class' => FALSE,
230
    'file' => FALSE,
231
232
    'color' => FALSE,
    'verbose' => FALSE,
233
    'keep-results' => FALSE,
234
    'test_names' => array(),
235
236
    'repeat' => 1,
    'die-on-fail' => FALSE,
237
    // Used internally.
238
239
    'test-id' => 0,
    'execute-test' => '',
240
    'xml' => '',
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
  );

  // 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']);
        }
259
        // Clear extraneous values.
260
261
262
263
264
265
266
267
268
269
270
271
        $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);
272
      $count++;
273
    }
274
  }
275
276
277
278
279
280
281

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

282
  return array($args, $count);
283
284
}

285
286
287
/**
 * Initialize script variables and perform general setup requirements.
 */
288
function simpletest_script_init() {
289
290
291
292
  global $args, $php;

  $host = 'localhost';
  $path = '';
293
294
  $port = '80';

295
  // Determine location of php command automatically, unless a command line argument is supplied.
296
  if (!empty($args['php'])) {
297
298
    $php = $args['php'];
  }
299
  elseif ($php_env = getenv('_')) {
300
    // '_' is an environment variable set by the shell. It contains the command that was executed.
301
    $php = $php_env;
302
  }
303
  elseif ($sudo = getenv('SUDO_COMMAND')) {
304
305
    // 'SUDO_COMMAND' is an environment variable set by the sudo program.
    // Extract only the PHP interpreter, not the rest of the command.
306
    list($php, ) = explode(' ', $sudo, 2);
307
308
  }
  else {
309
    simpletest_script_print_error('Unable to automatically determine the path to the PHP interpreter. Supply the --php command line argument.');
310
    simpletest_script_help();
311
312
    exit();
  }
313

314
  // Get URL from arguments.
315
316
  if (!empty($args['url'])) {
    $parsed_url = parse_url($args['url']);
317
    $host = $parsed_url['host'] . (isset($parsed_url['port']) ? ':' . $parsed_url['port'] : '');
318
    $path = isset($parsed_url['path']) ? rtrim(rtrim($parsed_url['path']), '/') : '';
319
    $port = (isset($parsed_url['port']) ? $parsed_url['port'] : $port);
320
321
322
    if ($path == '/') {
      $path = '';
    }
323
    // If the passed URL schema is 'https' then setup the $_SERVER variables
324
    // properly so that testing will run under HTTPS.
325
326
327
    if ($parsed_url['scheme'] == 'https') {
      $_SERVER['HTTPS'] = 'on';
    }
328
329
330
331
332
  }

  $_SERVER['HTTP_HOST'] = $host;
  $_SERVER['REMOTE_ADDR'] = '127.0.0.1';
  $_SERVER['SERVER_ADDR'] = '127.0.0.1';
333
  $_SERVER['SERVER_PORT'] = $port;
334
  $_SERVER['SERVER_SOFTWARE'] = NULL;
335
336
  $_SERVER['SERVER_NAME'] = 'localhost';
  $_SERVER['REQUEST_URI'] = $path .'/';
337
  $_SERVER['REQUEST_METHOD'] = 'GET';
338
  $_SERVER['SCRIPT_NAME'] = $path .'/index.php';
339
  $_SERVER['SCRIPT_FILENAME'] = $path .'/index.php';
340
341
342
  $_SERVER['PHP_SELF'] = $path .'/index.php';
  $_SERVER['HTTP_USER_AGENT'] = 'Drupal command line';

343
  if (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') {
344
345
346
347
348
349
    // Ensure that any and all environment variables are changed to https://.
    foreach ($_SERVER as $key => $value) {
      $_SERVER[$key] = str_replace('http://', 'https://', $_SERVER[$key]);
    }
  }

350
  chdir(realpath(__DIR__ . '/../..'));
351
  require_once dirname(__DIR__) . '/includes/bootstrap.inc';
352
353
}

354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
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
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
/**
 * Bootstraps a minimal Drupal environment.
 *
 * @see install_begin_request()
 */
function simpletest_script_bootstrap() {
  // Load legacy include files.
  foreach (glob(DRUPAL_ROOT . '/core/includes/*.inc') as $include) {
    require_once $include;
  }

  // Replace services with in-memory and null implementations.
  $GLOBALS['conf']['container_service_providers']['InstallerServiceProvider'] = 'Drupal\Core\Installer\InstallerServiceProvider';

  drupal_bootstrap(DRUPAL_BOOTSTRAP_CONFIGURATION);

  // Remove Drupal's error/exception handlers; they are designed for HTML
  // and there is no storage nor a (watchdog) logger here.
  restore_error_handler();
  restore_exception_handler();

  // In addition, ensure that PHP errors are not hidden away in logs.
  ini_set('display_errors', TRUE);

  // Ensure that required Settings exist.
  if (!Settings::getSingleton()->getAll()) {
    new Settings(array(
      'hash_salt' => 'run-tests',
    ));
  }

  $kernel = new DrupalKernel('testing', drupal_classloader(), FALSE);
  $kernel->boot();

  $request = Request::createFromGlobals();
  $container = $kernel->getContainer();
  $container->enterScope('request');
  $container->set('request', $request, 'request');

  $module_handler = $container->get('module_handler');
  // @todo Remove System module. Only needed because \Drupal\Core\Datetime\Date
  //   has a (needless) dependency on the 'date_format' entity, so calls to
  //   format_date()/format_interval() cause a plugin not found exception.
  $module_list['system'] = 'core/modules/system/system.module';
  $module_list['simpletest'] = 'core/modules/simpletest/simpletest.module';
  $module_handler->setModuleList($module_list);
  $module_handler->loadAll();
  $kernel->updateModules($module_list, $module_list);

  simpletest_classloader_register();
}

/**
 * Sets up database connection info for running tests.
 *
 * If this script is executed from within a real Drupal installation, then this
 * function essentially performs nothing (unless the --sqlite or --dburl
 * parameters were passed).
 *
 * Otherwise, there are three database connections of concern:
 * - --sqlite: The test runner connection, providing access to Simpletest
 *   database tables for recording test IDs and assertion results.
 * - --dburl: A database connection that is used as base connection info for all
 *   tests; i.e., every test will spawn from this connection. In case this
 *   connection uses e.g. SQLite, then all tests will run against SQLite. This
 *   is exposed as $databases['default']['default'] to Drupal.
 * - The actual database connection used within a test. This is the same as
 *   --dburl, but uses an additional database table prefix. This is
 *   $databases['default']['default'] within a test environment. The original
 *   connection is retained in
 *   $databases['simpletest_original_default']['default'] and restored after
 *   each test.
 *
 * @param bool $new
 *   Whether this process is a run-tests.sh master process. If TRUE, the SQLite
 *   database file specified by --sqlite (if any) is set up. Otherwise, database
 *   connections are prepared only.
 */
function simpletest_script_setup_database($new = FALSE) {
  global $args, $databases;

  // If there is an existing Drupal installation that contains a database
  // connection info in settings.php, then $databases['default']['default'] will
  // hold the default database connection already. This connection is assumed to
  // be valid, and this connection will be used in tests, so that they run
  // against e.g. MySQL instead of SQLite.

  // However, in case no Drupal installation exists, this default database
  // connection can be set and/or overridden with the --dburl parameter.
  if (!empty($args['dburl'])) {
    // Remove a possibly existing default connection (from settings.php).
    unset($databases['default']);
    Database::removeConnection('default');

    $info = parse_url($args['dburl']);
    if (!isset($info['scheme'], $info['host'], $info['path'])) {
      simpletest_script_print_error('Invalid --dburl. Minimum requirement: driver://host/database');
      exit(1);
    }
    $info += array(
      'user' => '',
      'pass' => '',
      'fragment' => '',
    );
    $databases['default']['default'] = array(
      'driver' => $info['scheme'],
      'username' => $info['user'],
      'password' => $info['pass'],
      'host' => $info['host'],
      'database' => ltrim($info['path'], '/'),
      'prefix' => array(
        'default' => $info['fragment'],
      ),
    );
  }
  // Otherwise, ensure that database table prefix info is an array.
  // @see https://drupal.org/node/2176621
  elseif (isset($databases['default']['default'])) {
    if (!is_array($databases['default']['default']['prefix'])) {
      $databases['default']['default']['prefix'] = array(
        'default' => $databases['default']['default']['prefix'],
      );
    }
  }

  // If there is no default database connection for tests, we cannot continue.
  if (!isset($databases['default']['default'])) {
    simpletest_script_print_error('Missing default database connection for tests. Use --dburl to specify one.');
    exit(1);
  }
  Database::addConnectionInfo('default', 'default', $databases['default']['default']);

  // If no --sqlite parameter has been passed, then Simpletest module is assumed
  // to be installed, so the test runner database connection is the default
  // database connection.
  if (empty($args['sqlite'])) {
    $sqlite = FALSE;
    $databases['test-runner']['default'] = $databases['default']['default'];
  }
  // Otherwise, set up a SQLite connection for the test runner.
  else {
    if ($args['sqlite'][0] === '/') {
      $sqlite = $args['sqlite'];
    }
    else {
      $sqlite = DRUPAL_ROOT . '/' . $args['sqlite'];
    }
    $databases['test-runner']['default'] = array(
      'driver' => 'sqlite',
      'database' => $sqlite,
      'prefix' => array(
        'default' => '',
      ),
    );
    // Create the test runner SQLite database, unless it exists already.
    if ($new && !file_exists($sqlite)) {
      if (!is_dir(dirname($sqlite))) {
        mkdir(dirname($sqlite));
      }
      touch($sqlite);
    }
  }

  // Add the test runner database connection.
  Database::addConnectionInfo('test-runner', 'default', $databases['test-runner']['default']);

  // Create the Simpletest schema.
  try {
    $schema = Database::getConnection('default', 'test-runner')->schema();
  }
  catch (\PDOException $e) {
    simpletest_script_print_error($databases['test-runner']['default']['driver'] . ': ' . $e->getMessage());
    exit(1);
  }
  if ($new && $sqlite) {
    require_once DRUPAL_ROOT . '/' . drupal_get_path('module', 'simpletest') . '/simpletest.install';
    foreach (simpletest_schema() as $name => $table_spec) {
      if ($schema->tableExists($name)) {
        $schema->dropTable($name);
      }
      $schema->createTable($name, $table_spec);
    }
  }
  // Verify that the Simpletest database schema exists by checking one table.
  if (!$schema->tableExists('simpletest')) {
    simpletest_script_print_error('Missing Simpletest database schema. Either install Simpletest module or use the --sqlite parameter.');
    exit(1);
  }
}

544
545
546
/**
 * Get all available tests from simpletest and PHPUnit.
 *
547
548
549
550
 * @param string $module
 *   Name of a module. If set then only tests belonging to this module are
 *   returned.
 *
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
 * @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
 */
566
567
568
function simpletest_script_get_all_tests($module = NULL) {
  $tests = simpletest_test_get_all($module);
  $tests['PHPUnit'] = simpletest_phpunit_get_available_tests($module);
569
570
571
  return $tests;
}

572
573
574
/**
 * Execute a batch of tests.
 */
575
function simpletest_script_execute_batch($test_classes) {
576
  global $args, $test_ids;
577

578
579
  // Multi-process execution.
  $children = array();
580
  while (!empty($test_classes) || !empty($children)) {
581
    while (count($children) < $args['concurrency']) {
582
      if (empty($test_classes)) {
583
        break;
584
      }
585

586
587
      $test_id = Database::getConnection('default', 'test-runner')
        ->insert('simpletest_test_id')->useDefaults(array('test_id'))->execute();
588
      $test_ids[] = $test_id;
589

590
      $test_class = array_shift($test_classes);
591
592
      // Process phpunit tests immediately since they are fast and we don't need
      // to fork for them.
593
594
      if (is_subclass_of($test_class, 'Drupal\Tests\UnitTestCase')) {
        simpletest_script_run_phpunit($test_id, $test_class);
595
596
597
598
        continue;
      }

      // Fork a child process.
599
600
601
602
603
604
      $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;
605
606
      }

607
608
609
      // Register our new child.
      $children[] = array(
        'process' => $process,
610
        'test_id' => $test_id,
611
612
613
614
        'class' => $test_class,
        'pipes' => $pipes,
      );
    }
615

616
617
618
619
620
621
622
623
624
625
    // 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']) {
626
          echo 'FATAL ' . $child['class'] . ': test runner returned a non-zero error code (' . $status['exitcode'] . ').' . "\n";
627
628
          if ($args['die-on-fail']) {
            list($db_prefix, ) = simpletest_last_test_get($child['test_id']);
629
            $test_directory = 'sites/simpletest/' . substr($db_prefix, 10);
630
            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";
631
632
633
            $args['keep-results'] = TRUE;
            // Exit repeat loop immediately.
            $args['repeat'] = -1;
634
          }
635
        }
636
        // Free-up space by removing any potentially created resources.
637
638
639
        if (!$args['keep-results']) {
          simpletest_script_cleanup($child['test_id'], $child['class'], $status['exitcode']);
        }
640
641

        // Remove this child.
642
        unset($children[$cid]);
643
644
645
646
647
      }
    }
  }
}

648
649
650
/**
 * Run a group of phpunit tests.
 */
651
652
function simpletest_script_run_phpunit($test_id, $class) {
  $results = simpletest_run_phpunit_tests($test_id, array($class));
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
  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':
679
        $summaries[$result['test_class']]['#debug']++;
680
681
682
683
684
        break;
    }
  }

  foreach ($summaries as $class => $summary) {
685
    simpletest_script_reporter_display_summary($class, $summary);
686
687
688
  }
}

689
/**
690
 * Bootstrap Drupal and run a single test.
691
692
 */
function simpletest_script_run_one_test($test_id, $test_class) {
693
  global $args;
694

695
696
  try {
    $test = new $test_class($test_id);
697
    $test->dieOnFail = (bool) $args['die-on-fail'];
698
    $test->verbose = (bool) $args['verbose'];
699
700
    $test->run();

701
    simpletest_script_reporter_display_summary($test_class, $test->results);
702

703
704
705
    // Finished, kill this runner.
    exit(0);
  }
706
707
  // DrupalTestCase::run() catches exceptions already, so this is only reached
  // when an exception is thrown in the wrapping test runner environment.
708
709
710
711
  catch (Exception $e) {
    echo (string) $e;
    exit(1);
  }
712
713
}

714
/**
715
716
717
718
719
720
 * 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.
721
 */
722
function simpletest_script_command($test_id, $test_class) {
723
  global $args, $php;
724

725
726
  $command = escapeshellarg($php) . ' ' . escapeshellarg('./core/scripts/' . $args['script']);
  $command .= ' --url ' . escapeshellarg($args['url']);
727
728
729
730
731
732
  if (!empty($args['sqlite'])) {
    $command .= ' --sqlite ' . escapeshellarg($args['sqlite']);
  }
  if (!empty($args['dburl'])) {
    $command .= ' --dburl ' . escapeshellarg($args['dburl']);
  }
733
734
  $command .= ' --php ' . escapeshellarg($php);
  $command .= " --test-id $test_id";
735
  foreach (array('verbose', 'keep-results', 'color', 'die-on-fail') as $arg) {
736
737
738
    if ($args[$arg]) {
      $command .= ' --' . $arg;
    }
739
  }
740
741
  // --execute-test and class name needs to come last.
  $command .= ' --execute-test ' . escapeshellarg($test_class);
742
  return $command;
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
/**
 * 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);

785
786
787
  // Check whether a test site directory was setup already.
  // @see \Drupal\simpletest\TestBase::prepareEnvironment()
  $test_directory = DRUPAL_ROOT . '/sites/simpletest/' . substr($db_prefix, 10);
788
789
790
791
792
793
794
795
  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;
      }
    }
796
    // Delete the test site directory.
797
798
799
    // simpletest_clean_temporary_directories() cannot be used here, since it
    // would also delete file directories of other tests that are potentially
    // running concurrently.
800
    file_unmanaged_delete_recursive($test_directory, array('Drupal\simpletest\TestBase', 'filePreDeleteCallback'));
801
    $messages[] = "- Removed test site directory.";
802
803
804
  }

  // Clear out all database tables from the test.
805
  $schema = Database::getConnection('default', 'default')->schema();
806
  $count = 0;
807
808
  foreach ($schema->findTables($db_prefix . '%') as $table) {
    $schema->dropTable($table);
809
810
811
    $count++;
  }
  if ($count) {
812
    $messages[] = "- Removed $count leftover tables.";
813
814
815
816
817
818
819
820
  }

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

821
/**
822
 * Get list of tests based on arguments. If --all specified then
823
824
825
826
827
828
829
 * 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() {
830
  global $args;
831
832

  $test_list = array();
833
834
  if ($args['all'] || $args['module']) {
    $groups = simpletest_script_get_all_tests($args['module']);
835
836
    $all_tests = array();
    foreach ($groups as $group => $tests) {
837
      $all_tests = array_merge($all_tests, array_keys($tests));
838
    }
839
    $test_list = $all_tests;
840
841
  }
  else {
842
    if ($args['class']) {
843
      foreach ($args['test_names'] as $class_name) {
844
845
846
        $test_list[] = $class_name;
      }
    }
847
    elseif ($args['file']) {
848
      // Extract test case class names from specified files.
849
      foreach ($args['test_names'] as $file) {
850
851
852
853
854
855
856
857
858
859
860
861
862
863
        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) {
864
          $test_list = array_merge($test_list, $matches[1]);
865
866
867
        }
        else {
          foreach ($matches[1] as $class_name) {
868
869
870
871
            $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;
            }
872
          }
873
874
875
        }
      }
    }
876
    else {
877
      $groups = simpletest_script_get_all_tests();
878
      foreach ($args['test_names'] as $group_name) {
879
        $test_list = array_merge($test_list, array_keys($groups[$group_name]));
880
881
      }
    }
882
  }
883

884
885
886
887
888
  if (empty($test_list)) {
    simpletest_script_print_error('No valid tests were specified.');
    exit;
  }
  return $test_list;
889
890
}

891
892
893
894
/**
 * Initialize the reporter.
 */
function simpletest_script_reporter_init() {
895
  global $args, $test_list, $results_map;
896
897
898
899
900
901

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

903
904
905
906
  echo "\n";
  echo "Drupal test run\n";
  echo "---------------\n";
  echo "\n";
907

908
909
910
911
912
913
  // 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";
914
    foreach ($test_list as $class_name) {
915
      echo "  - $class_name\n";
916
917
    }
    echo "\n";
918
  }
919

920
  echo "Test run started:\n";
921
  echo "  " . date('l, F j, Y - H:i', $_SERVER['REQUEST_TIME']) . "\n";
922
  Timer::start('run-tests');
923
924
  echo "\n";

925
926
  echo "Test summary\n";
  echo "------------\n";
927
  echo "\n";
928
929
}

930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
/**
 * Displays the assertion result summary for a single test class.
 *
 * @param string $class
 *   The test class name that was run.
 * @param array $results
 *   The assertion results using #pass, #fail, #exception, #debug array keys.
 */
function simpletest_script_reporter_display_summary($class, $results) {
  // Output all test results vertically aligned.
  // Cut off the class name after 60 chars, and pad each group with 3 digits
  // by default (more than 999 assertions are rare).
  $output = vsprintf('%-60.60s %10s %9s %14s %12s', array(
    $class,
                                   $results['#pass']      . ' passes',
    !$results['#fail']      ? '' : $results['#fail']      . ' fails',
    !$results['#exception'] ? '' : $results['#exception'] . ' exceptions',
    !$results['#debug']     ? '' : $results['#debug']     . ' messages',
  ));

  $status = ($results['#fail'] || $results['#exception'] ? 'fail' : 'pass');
  simpletest_script_print($output . "\n", simpletest_script_color_code($status));
}

954
/**
955
 * Display jUnit XML test results.
956
 */
957
function simpletest_script_reporter_write_xml_results() {
958
  global $args, $test_ids, $results_map;
959

960
961
962
963
  $results = Database::getConnection('default', 'test-runner')
    ->query("SELECT * FROM {simpletest} WHERE test_id IN (:test_ids) ORDER BY test_class, message_id", array(
      ':test_ids' => $test_ids,
    ));
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030

  $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() {
1031
  echo "\n";
1032
  $end = Timer::stop('run-tests');
1033
  echo "Test run duration: " . format_interval($end['time'] / 1000);
1034
  echo "\n\n";
1035
1036
1037
1038
1039
1040
}

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

1043
1044
  if ($args['verbose']) {
    // Report results.
1045
1046
    echo "Detailed test results\n";
    echo "---------------------\n";
1047

1048
1049
1050
1051
    $results = Database::getConnection('default', 'test-runner')
      ->query("SELECT * FROM {simpletest} WHERE test_id IN (:test_ids) ORDER BY test_class, message_id", array(
        ':test_ids' => $test_ids,
      ));
1052
    $test_class = '';
1053
    foreach ($results as $result) {
1054
1055
1056
1057
1058
      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;
1059

1060
1061
1062
          // Print table header.
          echo "Status    Group      Filename          Line Function                            \n";
          echo "--------------------------------------------------------------------------------\n";
1063
1064
1065
1066
1067
        }

        simpletest_script_format_result($result);
      }
    }
1068
1069
1070
  }
}

1071
1072
1073
1074
1075
1076
1077
1078
1079
/**
 * 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;

1080
1081
  $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);
1082
1083

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

1085
1086
1087
1088
1089
  $lines = explode("\n", wordwrap(trim(strip_tags($result->message)), 76));
  foreach ($lines as $line) {
    echo "    $line\n";
  }
}
1090

1091
/**
1092
1093
1094
1095
 * Print error message prefixed with "  ERROR: " and displayed in fail color
 * if color output is enabled.
 *
 * @param $message The message to print.
1096
 */
1097
1098
function simpletest_script_print_error($message) {
  simpletest_script_print("  ERROR: $message\n", SIMPLETEST_SCRIPT_COLOR_FAIL);
1099
}
1100

1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
/**
 * 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.
}