run-tests.sh 40.5 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\Timer;
9
use Drupal\Component\Uuid\Php;
10
use Drupal\Core\Database\Database;
11
use Drupal\Core\Form\FormState;
12
use Drupal\Core\Site\Settings;
13
use Drupal\Core\StreamWrapper\PublicStream;
14
use Drupal\Core\Test\TestRunnerKernel;
15
use Drupal\simpletest\Form\SimpletestResultsForm;
16
17
use Symfony\Component\HttpFoundation\Request;

18
$autoloader = require_once __DIR__ . '/../../autoload.php';
19

20
21
22
const SIMPLETEST_SCRIPT_COLOR_PASS = 32; // Green.
const SIMPLETEST_SCRIPT_COLOR_FAIL = 31; // Red.
const SIMPLETEST_SCRIPT_COLOR_EXCEPTION = 33; // Brown.
23
24
25

// Restricting the chunk of queries prevents memory exhaustion.
const SIMPLETEST_SCRIPT_SQLITE_VARIABLE_LIMIT = 350;
26
27
28
29
30
31

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

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

35
simpletest_script_init();
36
37
38
39

$request = Request::createFromGlobals();
$kernel = TestRunnerKernel::createFromRequest($request, $autoloader);
$kernel->prepareLegacyRequest($request);
40

41
if ($args['execute-test']) {
42
  simpletest_script_setup_database();
43
  simpletest_script_run_one_test($args['test-id'], $args['execute-test']);
44
  // Sub-process exited already; this is just for clarity.
45
  exit;
46
}
47

48
49
50
51
52
53
54
55
56
57
58
59
60
61
if ($args['list']) {
  // Display all available tests.
  echo "\nAvailable test groups & classes\n";
  echo   "-------------------------------\n\n";
  $groups = simpletest_test_get_all($args['module']);
  foreach ($groups as $group => $tests) {
    echo $group . "\n";
    foreach ($tests as $class => $info) {
      echo " - $class\n";
    }
  }
  exit;
}

62
simpletest_script_setup_database(TRUE);
63
64

if ($args['clean']) {
65
  // Clean up left-over tables and directories.
66
67
68
69
  simpletest_clean_environment();
  echo "\nEnvironment cleaned.\n";

  // Get the status messages and print them.
70
71
  $messages = drupal_get_messages('status');
  foreach ($messages['status'] as $text) {
72
73
    echo " - " . $text . "\n";
  }
74
  exit;
75
76
}

77
78
$test_list = simpletest_script_get_test_list();

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

83
$tests_to_run = array();
84
for ($i = 0; $i < $args['repeat']; $i++) {
85
  $tests_to_run = array_merge($tests_to_run, $test_list);
86
}
87

88
// Execute tests.
89
simpletest_script_execute_batch($tests_to_run);
90

91
92
93
// Stop the timer.
simpletest_script_reporter_timer_stop();

94
// Display results before database is cleared.
95
96
97
98
99
100
if ($args['browser']) {
  simpletest_script_open_browser();
}
else {
  simpletest_script_reporter_display_results();
}
101

102
103
104
105
if ($args['xml']) {
  simpletest_script_reporter_write_xml_results();
}

106
// Clean up all test results.
107
108
109
if (!$args['keep-results']) {
  simpletest_clean_results_table();
}
110

111
// Test complete, exit.
112
exit;
113

114
115
116
117
118
/**
 * Print help text.
 */
function simpletest_script_help() {
  global $args;
119
120
121
122
123

  echo <<<EOF

Run Drupal tests from the shell.

124
125
Usage:        {$args['script']} [OPTIONS] <tests>
Example:      {$args['script']} Profile
126
127
128
129

All arguments are long options.

  --help      Print this page.
130
131
132

  --list      Display all available test groups.

133
134
135
  --clean     Cleans up database tables or directories from previous, failed,
              tests and then exits (no tests are run).

136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
  --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,
151
              and database name to use in tests.
152
153
              Required when running tests without a Drupal installation that
              contains default database connection info in settings.php.
154
155
156
157
              Examples:
                mysql://username:password@localhost/databasename#table_prefix
                sqlite://localhost/relative/path/db.sqlite
                sqlite://localhost//absolute/path/db.sqlite
158

159
  --php       The absolute path to the PHP executable. Usually not needed.
160

161
162
  --concurrency [num]

163
              Run tests in parallel, up to [num] tests at a time.
164

165
  --all       Run all available tests.
166

167
168
169
  --module    Run all tests belonging to the specified module name.
              (e.g., 'node')

170
  --class     Run tests identified by specific class names, instead of group names.
171
172
              A specific test method can be added, for example,
              'Drupal\book\Tests\BookTest::testBookExport'.
173

174
  --file      Run tests identified by specific file names, instead of group names.
175
176
              Specify the path and the extension
              (i.e. 'core/modules/user/user.test').
177

178
179
180
181
182
  --xml       <path>

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

  --color     Output text format results with color highlighting.
183
184
185

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

186
187
188
189
190
  --keep-results

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

191
192
193
194
  --repeat    Number of times to repeat the test.

  --die-on-fail

195
196
197
198
              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.
199

200
201
202
203
  --browser   Opens the results in the browser. This enforces --keep-results and
              if you want to also view any pages rendered in the simpletest
              browser you need to add --verbose to the command line.

204
205
  <test1>[,<test2>[,<test3> ...]]

206
              One or more tests to be run. By default, these are interpreted
207
              as the names of test groups as shown at
208
              admin/config/development/testing.
209
              These group names typically correspond to module names like "User"
210
              or "Profile" or "System", but there is also a group "Database".
211
              If --class is specified then these are interpreted as the names of
212
213
              specific test classes whose test methods will be run. Tests must
              be separated by commas. Ignored if --all is specified.
214

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

218
sudo -u [wwwrun|www-data|etc] php ./core/scripts/{$args['script']}
219
  --url http://example.com/ --all
220
sudo -u [wwwrun|www-data|etc] php ./core/scripts/{$args['script']}
221
  --url http://example.com/ --class "Drupal\block\Tests\BlockTest"
222
223
224
225
226
227
228
229
230
231

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

232
233
234
EOF;
}

235
236
237
238
239
240
241
242
243
244
245
246
247
/**
 * 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' => '',
248
249
    'sqlite' => NULL,
    'dburl' => NULL,
250
    'php' => '',
251
252
    'concurrency' => 1,
    'all' => FALSE,
253
    'module' => NULL,
254
    'class' => FALSE,
255
    'file' => FALSE,
256
257
    'color' => FALSE,
    'verbose' => FALSE,
258
    'keep-results' => FALSE,
259
    'test_names' => array(),
260
261
    'repeat' => 1,
    'die-on-fail' => FALSE,
262
    'browser' => FALSE,
263
    // Used internally.
264
265
    'test-id' => 0,
    'execute-test' => '',
266
    'xml' => '',
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
  );

  // 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']);
        }
285
        // Clear extraneous values.
286
287
288
289
290
291
        $args['test_names'] = array();
        $count++;
      }
      else {
        // Argument not found in list.
        simpletest_script_print_error("Unknown argument '$arg'.");
292
        exit;
293
294
295
296
297
      }
    }
    else {
      // Values found without an argument should be test names.
      $args['test_names'] += explode(',', $arg);
298
      $count++;
299
    }
300
  }
301
302
303
304

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

308
309
310
  if ($args['browser']) {
    $args['keep-results'] = TRUE;
  }
311
  return array($args, $count);
312
313
}

314
315
316
/**
 * Initialize script variables and perform general setup requirements.
 */
317
function simpletest_script_init() {
318
319
320
321
  global $args, $php;

  $host = 'localhost';
  $path = '';
322
323
  $port = '80';

324
  // Determine location of php command automatically, unless a command line argument is supplied.
325
  if (!empty($args['php'])) {
326
327
    $php = $args['php'];
  }
328
  elseif ($php_env = getenv('_')) {
329
    // '_' is an environment variable set by the shell. It contains the command that was executed.
330
    $php = $php_env;
331
  }
332
  elseif ($sudo = getenv('SUDO_COMMAND')) {
333
334
    // 'SUDO_COMMAND' is an environment variable set by the sudo program.
    // Extract only the PHP interpreter, not the rest of the command.
335
    list($php, ) = explode(' ', $sudo, 2);
336
337
  }
  else {
338
    simpletest_script_print_error('Unable to automatically determine the path to the PHP interpreter. Supply the --php command line argument.');
339
    simpletest_script_help();
340
    exit();
341
  }
342

343
  // Get URL from arguments.
344
345
  if (!empty($args['url'])) {
    $parsed_url = parse_url($args['url']);
346
    $host = $parsed_url['host'] . (isset($parsed_url['port']) ? ':' . $parsed_url['port'] : '');
347
    $path = isset($parsed_url['path']) ? rtrim(rtrim($parsed_url['path']), '/') : '';
348
    $port = (isset($parsed_url['port']) ? $parsed_url['port'] : $port);
349
350
351
    if ($path == '/') {
      $path = '';
    }
352
    // If the passed URL schema is 'https' then setup the $_SERVER variables
353
    // properly so that testing will run under HTTPS.
354
355
356
    if ($parsed_url['scheme'] == 'https') {
      $_SERVER['HTTPS'] = 'on';
    }
357
358
359
360
361
  }

  $_SERVER['HTTP_HOST'] = $host;
  $_SERVER['REMOTE_ADDR'] = '127.0.0.1';
  $_SERVER['SERVER_ADDR'] = '127.0.0.1';
362
  $_SERVER['SERVER_PORT'] = $port;
363
  $_SERVER['SERVER_SOFTWARE'] = NULL;
364
365
  $_SERVER['SERVER_NAME'] = 'localhost';
  $_SERVER['REQUEST_URI'] = $path .'/';
366
  $_SERVER['REQUEST_METHOD'] = 'GET';
367
  $_SERVER['SCRIPT_NAME'] = $path .'/index.php';
368
  $_SERVER['SCRIPT_FILENAME'] = $path .'/index.php';
369
370
371
  $_SERVER['PHP_SELF'] = $path .'/index.php';
  $_SERVER['HTTP_USER_AGENT'] = 'Drupal command line';

372
  if (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') {
373
374
375
376
377
378
    // Ensure that any and all environment variables are changed to https://.
    foreach ($_SERVER as $key => $value) {
      $_SERVER[$key] = str_replace('http://', 'https://', $_SERVER[$key]);
    }
  }

379
  chdir(realpath(__DIR__ . '/../..'));
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
}

/**
 * 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) {
409
  global $args;
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425

  // 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).
    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');
426
      exit(1);
427
428
429
430
431
432
    }
    $info += array(
      'user' => '',
      'pass' => '',
      'fragment' => '',
    );
433
434
435
436
437
438
    if ($info['path'][0] === '/') {
      $info['path'] = substr($info['path'], 1);
    }
    if ($info['scheme'] === 'sqlite' && $info['path'][0] !== '/') {
      $info['path'] = DRUPAL_ROOT . '/' . $info['path'];
    }
439
440
441
442
443
    $databases['default']['default'] = array(
      'driver' => $info['scheme'],
      'username' => $info['user'],
      'password' => $info['pass'],
      'host' => $info['host'],
444
      'database' => $info['path'],
445
446
447
448
      'prefix' => array(
        'default' => $info['fragment'],
      ),
    );
449
450
451
    if (isset($info['port'])) {
      $databases['default']['default']['port'] = $info['port'];
    }
452
  }
453
454
455
  // Otherwise, use the default database connection from settings.php.
  else {
    $databases['default'] = Database::getConnectionInfo('default');
456
457
458
459
460
  }

  // 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.');
461
    exit(1);
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
  }
  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());
505
    exit(1);
506
507
508
509
510
511
512
513
514
515
516
517
518
  }
  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.');
519
    exit(1);
520
521
522
  }
}

523
524
525
/**
 * Execute a batch of tests.
 */
526
function simpletest_script_execute_batch($test_classes) {
527
  global $args, $test_ids;
528

529
530
  // Multi-process execution.
  $children = array();
531
  while (!empty($test_classes) || !empty($children)) {
532
    while (count($children) < $args['concurrency']) {
533
      if (empty($test_classes)) {
534
        break;
535
      }
536

537
538
      $test_id = Database::getConnection('default', 'test-runner')
        ->insert('simpletest_test_id')->useDefaults(array('test_id'))->execute();
539
      $test_ids[] = $test_id;
540

541
      $test_class = array_shift($test_classes);
542
      // Fork a child process.
543
544
545
546
547
      $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";
548
        exit;
549
550
      }

551
552
553
      // Register our new child.
      $children[] = array(
        'process' => $process,
554
        'test_id' => $test_id,
555
556
557
558
        'class' => $test_class,
        'pipes' => $pipes,
      );
    }
559

560
561
562
563
564
565
566
567
568
    // 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']);
569
        if ($status['exitcode']) {
570
          echo 'FATAL ' . $child['class'] . ': test runner returned a non-zero error code (' . $status['exitcode'] . ').' . "\n";
571
572
          if ($args['die-on-fail']) {
            list($db_prefix, ) = simpletest_last_test_get($child['test_id']);
573
            $test_directory = 'sites/simpletest/' . substr($db_prefix, 10);
574
            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";
575
576
577
            $args['keep-results'] = TRUE;
            // Exit repeat loop immediately.
            $args['repeat'] = -1;
578
          }
579
        }
580
        // Free-up space by removing any potentially created resources.
581
582
583
        if (!$args['keep-results']) {
          simpletest_script_cleanup($child['test_id'], $child['class'], $status['exitcode']);
        }
584
585

        // Remove this child.
586
        unset($children[$cid]);
587
588
589
590
591
      }
    }
  }
}

592
593
594
/**
 * Run a group of phpunit tests.
 */
595
function simpletest_script_run_phpunit($test_id, $class) {
596
  $results = simpletest_run_phpunit_tests($test_id, array($class));
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
  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':
623
        $summaries[$result['test_class']]['#debug']++;
624
625
626
627
628
        break;
    }
  }

  foreach ($summaries as $class => $summary) {
629
    simpletest_script_reporter_display_summary($class, $summary);
630
631
632
  }
}

633
/**
634
 * Bootstrap Drupal and run a single test.
635
636
 */
function simpletest_script_run_one_test($test_id, $test_class) {
637
  global $args;
638

639
  try {
640
641
642
643
644
645
646
647
648
649
    if (strpos($test_class, '::') > 0) {
      list($class_name, $method) = explode('::', $test_class, 2);
      $methods = [$method];
    }
    else {
      $class_name = $test_class;
      // Use empty array to run all the test methods.
      $methods = array();
    }
    $test = new $class_name($test_id);
650
651
652
653
654
655
656
657
658
    if (is_subclass_of($test_class, '\PHPUnit_Framework_TestCase')) {
      simpletest_script_run_phpunit($test_id, $test_class);
    }
    else {
      $test->dieOnFail = (bool) $args['die-on-fail'];
      $test->verbose = (bool) $args['verbose'];
      $test->run($methods);
      simpletest_script_reporter_display_summary($test_class, $test->results);
    }
659

660
    // Finished, kill this runner.
661
    exit(0);
662
  }
663
664
  // DrupalTestCase::run() catches exceptions already, so this is only reached
  // when an exception is thrown in the wrapping test runner environment.
665
666
  catch (Exception $e) {
    echo (string) $e;
667
    exit(1);
668
  }
669
670
}

671
/**
672
673
674
675
676
677
 * 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.
678
 */
679
function simpletest_script_command($test_id, $test_class) {
680
  global $args, $php;
681

682
683
  $command = escapeshellarg($php) . ' ' . escapeshellarg('./core/scripts/' . $args['script']);
  $command .= ' --url ' . escapeshellarg($args['url']);
684
685
686
687
688
689
  if (!empty($args['sqlite'])) {
    $command .= ' --sqlite ' . escapeshellarg($args['sqlite']);
  }
  if (!empty($args['dburl'])) {
    $command .= ' --dburl ' . escapeshellarg($args['dburl']);
  }
690
691
  $command .= ' --php ' . escapeshellarg($php);
  $command .= " --test-id $test_id";
692
  foreach (array('verbose', 'keep-results', 'color', 'die-on-fail') as $arg) {
693
694
695
    if ($args[$arg]) {
      $command .= ' --' . $arg;
    }
696
  }
697
698
  // --execute-test and class name needs to come last.
  $command .= ' --execute-test ' . escapeshellarg($test_class);
699
  return $command;
700
701
}

702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
/**
 * 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) {
724
725
726
727
  if (strpos($test_class, 'Drupal\\Tests\\') === 0) {
    // PHPUnit test, move on.
    return;
  }
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
  // 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);

746
747
748
  // Check whether a test site directory was setup already.
  // @see \Drupal\simpletest\TestBase::prepareEnvironment()
  $test_directory = DRUPAL_ROOT . '/sites/simpletest/' . substr($db_prefix, 10);
749
750
751
752
753
754
755
756
  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;
      }
    }
757
    // Delete the test site directory.
758
759
760
    // simpletest_clean_temporary_directories() cannot be used here, since it
    // would also delete file directories of other tests that are potentially
    // running concurrently.
761
    file_unmanaged_delete_recursive($test_directory, array('Drupal\simpletest\TestBase', 'filePreDeleteCallback'));
762
    $messages[] = "- Removed test site directory.";
763
764
765
  }

  // Clear out all database tables from the test.
766
  $schema = Database::getConnection('default', 'default')->schema();
767
  $count = 0;
768
769
  foreach ($schema->findTables($db_prefix . '%') as $table) {
    $schema->dropTable($table);
770
771
772
    $count++;
  }
  if ($count) {
773
    $messages[] = "- Removed $count leftover tables.";
774
775
776
777
778
779
780
781
  }

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

782
/**
783
 * Get list of tests based on arguments. If --all specified then
784
785
786
787
788
789
790
 * 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() {
791
  global $args;
792
793

  $test_list = array();
794
  if ($args['all'] || $args['module']) {
795
    $groups = simpletest_test_get_all($args['module']);
796
797
    $all_tests = array();
    foreach ($groups as $group => $tests) {
798
      $all_tests = array_merge($all_tests, array_keys($tests));
799
    }
800
    $test_list = $all_tests;
801
802
  }
  else {
803
    if ($args['class']) {
804
805
      $test_list = array();
      foreach ($args['test_names'] as $test_class) {
806
807
        list($class_name, ) = explode('::', $test_class, 2);
        if (class_exists($class_name)) {
808
809
810
811
812
813
814
815
          $test_list[] = $test_class;
        }
        else {
          $groups = simpletest_test_get_all();
          $all_classes = array();
          foreach ($groups as $group) {
            $all_classes = array_merge($all_classes, array_keys($group));
          }
816
817
          simpletest_script_print_error('Test class not found: ' . $class_name);
          simpletest_script_print_alternatives($class_name, $all_classes, 6);
818
          exit(1);
819
        }
820
821
      }
    }
822
    elseif ($args['file']) {
823
      // Extract test case class names from specified files.
824
      foreach ($args['test_names'] as $file) {
825
826
        if (!file_exists($file)) {
          simpletest_script_print_error('File not found: ' . $file);
827
          exit;
828
829
830
831
832
833
834
835
836
837
838
        }
        $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) {
839
          $test_list = array_merge($test_list, $matches[1]);
840
841
842
        }
        else {
          foreach ($matches[1] as $class_name) {
843
            $namespace_class = $namespace . '\\' . $class_name;
844
            if (is_subclass_of($namespace_class, '\Drupal\simpletest\TestBase') || is_subclass_of($namespace_class, '\PHPUnit_Framework_TestCase')) {
845
846
              $test_list[] = $namespace_class;
            }
847
          }
848
849
850
        }
      }
    }
851
    else {
852
      $groups = simpletest_test_get_all();
853
      foreach ($args['test_names'] as $group_name) {
854
855
856
857
858
859
        if (isset($groups[$group_name])) {
          $test_list = array_merge($test_list, array_keys($groups[$group_name]));
        }
        else {
          simpletest_script_print_error('Test group not found: ' . $group_name);
          simpletest_script_print_alternatives($group_name, array_keys($groups));
860
          exit(1);
861
        }
862
863
      }
    }
864
  }
865

866
867
  if (empty($test_list)) {
    simpletest_script_print_error('No valid tests were specified.');
868
    exit;
869
870
  }
  return $test_list;
871
872
}

873
874
875
876
/**
 * Initialize the reporter.
 */
function simpletest_script_reporter_init() {
877
  global $args, $test_list, $results_map;
878
879
880
881
882
883

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

885
886
887
888
  echo "\n";
  echo "Drupal test run\n";
  echo "---------------\n";
  echo "\n";
889

890
891
892
893
894
895
  // 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";
896
    foreach ($test_list as $class_name) {
897
      echo "  - $class_name\n";
898
899
    }
    echo "\n";
900
  }
901

902
  echo "Test run started:\n";
903
  echo "  " . date('l, F j, Y - H:i', $_SERVER['REQUEST_TIME']) . "\n";
904
  Timer::start('run-tests');
905
906
  echo "\n";

907
908
  echo "Test summary\n";
  echo "------------\n";
909
  echo "\n";
910
911
}

912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
/**
 * 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));
}

936
/**
937
 * Display jUnit XML test results.
938
 */
939
function simpletest_script_reporter_write_xml_results() {
940
  global $args, $test_ids, $results_map;
941

942
  $results = simpletest_script_load_messages_by_test_id($test_ids);
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969

  $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);
970
971
972
973
974
975
      if (strpos($result->function, '->') !== FALSE) {
        list($class, $name) = explode('->', $result->function, 2);
      }
      else {
        $name = $result->function;
      }
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
      $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() {
1015
  echo "\n";
1016
  $end = Timer::stop('run-tests');
1017
  echo "Test run duration: " . \Drupal::service('date.formatter')->formatInterval($end['time'] / 1000);
1018
  echo "\n\n";
1019
1020
1021
1022
1023
1024
}

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

1027
1028
  if ($args['verbose']) {
    // Report results.
1029
1030
    echo "Detailed test results\n";
    echo "---------------------\n";
1031

1032
    $results = simpletest_script_load_messages_by_test_id($test_ids);
1033
    $test_class = '';
1034
    foreach ($results as $result) {
1035
1036
1037
1038
1039
      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;
1040

1041
1042
1043
          // Print table header.
          echo "Status    Group      Filename          Line Function                            \n";
          echo "--------------------------------------------------------------------------------\n";
1044
1045
1046
1047
1048
        }

        simpletest_script_format_result($result);
      }
    }
1049
1050
1051
  }
}

1052
1053
1054
1055
1056
1057
1058
1059
1060
/**
 * 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;

1061
1062
  $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);
1063
1064

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

1066
1067
1068
1069
1070
  $lines = explode("\n", wordwrap(trim(strip_tags($result->message)), 76));
  foreach ($lines as $line) {
    echo "    $line\n";
  }
}
1071

1072
/**
1073
1074
1075
1076
 * Print error message prefixed with "  ERROR: " and displayed in fail color
 * if color output is enabled.
 *
 * @param $message The message to print.
1077
 */
1078
1079
function simpletest_script_print_error($message) {
  simpletest_script_print("  ERROR: $message\n", SIMPLETEST_SCRIPT_COLOR_FAIL);
1080
}
1081

1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
/**
 * 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.
}
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149

/**
 * Prints alternative test names.
 *
 * Searches the provided array of string values for close matches based on the
 * Levenshtein algorithm.
 *
 * @see http://php.net/manual/en/function.levenshtein.php
 *
 * @param string $string
 *   A string to test.
 * @param array $array
 *   A list of strings to search.
 * @param int $degree
 *   The matching strictness. Higher values return fewer matches. A value of
 *   4 means that the function will return strings from $array if the candidate
 *   string in $array would be identical to $string by changing 1/4 or fewer of
 *   its characters.
 */
function simpletest_script_print_alternatives($string, $array, $degree = 4) {
  $alternatives = array();
  foreach ($array as $item) {
    $lev = levenshtein($string, $item);
    if ($lev <= strlen($item) / $degree || FALSE !== strpos($string, $item)) {
      $alternatives[] = $item;
    }
  }
  if (!empty($alternatives)) {
    simpletest_script_print("  Did you mean?\n", SIMPLETEST_SCRIPT_COLOR_FAIL);
    foreach ($alternatives as $alternative) {
      simpletest_script_print("  - $alternative\n", SIMPLETEST_SCRIPT_COLOR_FAIL);
    }
  }
}
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176

/**
 * Loads the simpletest messages from the database.
 *
 * Messages are ordered by test class and message id.
 *
 * @param array $test_ids
 *   Array of test IDs of the messages to be loaded.
 *
 * @return array
 *   Array of simpletest messages from the database.
 */
function simpletest_script_load_messages_by_test_id($test_ids) {
  global $args;
  $results = array();

  // Sqlite has a maximum number of variables per query. If required, the
  // database query is split into chunks.
  if (count($test_ids) > SIMPLETEST_SCRIPT_SQLITE_VARIABLE_LIMIT && !empty($args['sqlite'])) {
    $test_id_chunks = array_chunk($test_ids, SIMPLETEST_SCRIPT_SQLITE_VARIABLE_LIMIT);
  }
  else {
    $test_id_chunks = array($test_ids);
  }

  foreach ($test_id_chunks as $test_id_chunk) {
    $result_chunk = Database::getConnection('default', 'test-runner')
1177
1178
      ->query("SELECT * FROM {simpletest} WHERE test_id IN ( :test_ids[] ) ORDER BY test_class, message_id", array(
        ':test_ids[]' => $test_id_chunk,
1179
1180
1181
1182
1183
1184
1185
1186
      ))->fetchAll();
    if ($result_chunk) {
      $results = array_merge($results, $result_chunk);
    }
  }

  return $results;
}
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248

/**
 * Display test results.
 */
function simpletest_script_open_browser() {
  global $test_ids;

  $connection = Database::getConnection('default', 'test-runner');
  $results = $connection->select('simpletest')
    ->fields('simpletest')
    ->condition('test_id', $test_ids, 'IN')
    ->orderBy('test_class')
    ->orderBy('message_id')
    ->execute()
    ->fetchAll();

  // Get the results form.
  $form = array();
  SimpletestResultsForm::addResultForm($form, $results);

  // Get the assets to make the details element collapsible and theme the result
  // form.
  $assets = new \Drupal\Core\Asset\AttachedAssets();
  $assets->setLibraries(['core/drupal.collapse', 'system/admin', 'simpletest/drupal.simpletest']);
  $resolver = \Drupal::service('asset.resolver');
  list($js_assets_header, $js_assets_footer) = $resolver->getJsAssets($assets, FALSE);
  $js_collection_renderer = \Drupal::service('asset.js.collection_renderer');
  $js_assets_header = $js_collection_renderer->render($js_assets_header);
  $js_assets_footer = $js_collection_renderer->render($js_assets_footer);
  $css_assets = \Drupal::service('asset.css.collection_renderer')->render($resolver->getCssAssets($assets, FALSE));

  // Make the html page to write to disk.
  $html = '<head>' . drupal_render($js_assets_header) . drupal_render($css_assets) . '</head><body>' . drupal_render($form) . drupal_render($js_assets_footer) .'</body>';

  // Ensure we have assets verbose directory - tests with no verbose output will not
  // have created one.
  $directory = PublicStream::basePath() . '/simpletest/verbose';
  file_prepare_directory($directory, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS);
  $uuid = new Php();
  $filename = $directory .'/results-'. $uuid->generate() .'.html';
  file_put_contents($filename, $html);

  // See if we can find an OS helper to open URLs in default browser.
  $browser = FALSE;
  if (shell_exec('which xdg-open')) {
    $browser = 'xdg-open';
  }
  elseif (shell_exec('which open')) {
    $browser = 'open';
  }
  elseif (substr(PHP_OS, 0, 3) == 'WIN') {
    $browser = 'start';
  }

  if ($browser) {
    shell_exec($browser . ' ' . escapeshellarg($filename));