run-tests.sh 48.1 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\Html;
9
use Drupal\Component\Utility\Timer;
10
use Drupal\Component\Uuid\Php;
11
use Drupal\Core\Database\Database;
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
use Drupal\simpletest\TestBase;
17
18
use Symfony\Component\HttpFoundation\Request;

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

21
22
23
24
25
26
27
// Define some colors for display.
// A nice calming green.
const SIMPLETEST_SCRIPT_COLOR_PASS = 32;
// An alerting Red.
const SIMPLETEST_SCRIPT_COLOR_FAIL = 31;
// An annoying brown.
const SIMPLETEST_SCRIPT_COLOR_EXCEPTION = 33;
28
29
30

// Restricting the chunk of queries prevents memory exhaustion.
const SIMPLETEST_SCRIPT_SQLITE_VARIABLE_LIMIT = 350;
31

32
33
34
35
const SIMPLETEST_SCRIPT_EXIT_SUCCESS = 0;
const SIMPLETEST_SCRIPT_EXIT_FAILURE = 1;
const SIMPLETEST_SCRIPT_EXIT_EXCEPTION = 2;

36
37
38
39
40
// Set defaults and get overrides.
list($args, $count) = simpletest_script_parse_args();

if ($args['help'] || $count == 0) {
  simpletest_script_help();
41
  exit(($count == 0) ? SIMPLETEST_SCRIPT_EXIT_FAILURE : SIMPLETEST_SCRIPT_EXIT_SUCCESS);
42
43
}

44
simpletest_script_init();
45

46
47
48
49
50
51
52
53
54
try {
  $request = Request::createFromGlobals();
  $kernel = TestRunnerKernel::createFromRequest($request, $autoloader);
  $kernel->prepareLegacyRequest($request);
}
catch (Exception $e) {
  echo (string) $e;
  exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
}
55

56
if ($args['execute-test']) {
57
  simpletest_script_setup_database();
58
  simpletest_script_run_one_test($args['test-id'], $args['execute-test']);
59
  // Sub-process exited already; this is just for clarity.
60
  exit(SIMPLETEST_SCRIPT_EXIT_SUCCESS);
61
}
62

63
64
65
if ($args['list']) {
  // Display all available tests.
  echo "\nAvailable test groups & classes\n";
66
  echo "-------------------------------\n\n";
67
68
69
70
71
72
73
  try {
    $groups = simpletest_test_get_all($args['module']);
  }
  catch (Exception $e) {
    echo (string) $e;
    exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
  }
74
75
76
77
78
79
  foreach ($groups as $group => $tests) {
    echo $group . "\n";
    foreach ($tests as $class => $info) {
      echo " - $class\n";
    }
  }
80
  exit(SIMPLETEST_SCRIPT_EXIT_SUCCESS);
81
82
}

83
simpletest_script_setup_database(TRUE);
84
85

if ($args['clean']) {
86
  // Clean up left-over tables and directories.
87
88
89
90
91
92
93
  try {
    simpletest_clean_environment();
  }
  catch (Exception $e) {
    echo (string) $e;
    exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
  }
94
95
96
  echo "\nEnvironment cleaned.\n";

  // Get the status messages and print them.
97
98
  $messages = drupal_get_messages('status');
  foreach ($messages['status'] as $text) {
99
100
    echo " - " . $text . "\n";
  }
101
  exit(SIMPLETEST_SCRIPT_EXIT_SUCCESS);
102
103
}

104
105
$test_list = simpletest_script_get_test_list();

106
107
// Try to allocate unlimited time to run the tests.
drupal_set_time_limit(0);
108
109
simpletest_script_reporter_init();

110
$tests_to_run = array();
111
for ($i = 0; $i < $args['repeat']; $i++) {
112
  $tests_to_run = array_merge($tests_to_run, $test_list);
113
}
114

115
// Execute tests.
116
$status = simpletest_script_execute_batch($tests_to_run);
117

118
119
120
// Stop the timer.
simpletest_script_reporter_timer_stop();

121
// Display results before database is cleared.
122
123
124
125
126
127
if ($args['browser']) {
  simpletest_script_open_browser();
}
else {
  simpletest_script_reporter_display_results();
}
128

129
130
131
132
if ($args['xml']) {
  simpletest_script_reporter_write_xml_results();
}

133
// Clean up all test results.
134
if (!$args['keep-results']) {
135
136
137
138
139
140
141
  try {
    simpletest_clean_results_table();
  }
  catch (Exception $e) {
    echo (string) $e;
    exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
  }
142
}
143

144
// Test complete, exit.
145
exit($status);
146

147
148
149
150
151
/**
 * Print help text.
 */
function simpletest_script_help() {
  global $args;
152
153
154
155
156

  echo <<<EOF

Run Drupal tests from the shell.

157
158
Usage:        {$args['script']} [OPTIONS] <tests>
Example:      {$args['script']} Profile
159
160
161
162

All arguments are long options.

  --help      Print this page.
163
164
165

  --list      Display all available test groups.

166
167
168
  --clean     Cleans up database tables or directories from previous, failed,
              tests and then exits (no tests are run).

169
170
171
172
173
174
175
176
177
178
179
180
181
182
  --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'

183
184
185
186
187
188
  --keep-results-table

              Boolean flag to indicate to not cleanup the simpletest result
              table. For testbots or repeated execution of a single test it can
              be helpful to not cleanup the simpletest result table.

189
  --dburl     A URI denoting the database driver, credentials, server hostname,
190
              and database name to use in tests.
191
192
              Required when running tests without a Drupal installation that
              contains default database connection info in settings.php.
193
194
195
196
              Examples:
                mysql://username:password@localhost/databasename#table_prefix
                sqlite://localhost/relative/path/db.sqlite
                sqlite://localhost//absolute/path/db.sqlite
197

198
  --php       The absolute path to the PHP executable. Usually not needed.
199

200
201
  --concurrency [num]

202
              Run tests in parallel, up to [num] tests at a time.
203

204
  --all       Run all available tests.
205

206
207
208
  --module    Run all tests belonging to the specified module name.
              (e.g., 'node')

209
  --class     Run tests identified by specific class names, instead of group names.
210
211
              A specific test method can be added, for example,
              'Drupal\book\Tests\BookTest::testBookExport'.
212

213
  --file      Run tests identified by specific file names, instead of group names.
214
215
              Specify the path and the extension
              (i.e. 'core/modules/user/user.test').
216

217
218
219
220
221
222
  --types

              Runs just tests from the specified test type, for example
              run-tests.sh
              (i.e. --types "Simpletest,PHPUnit-Functional")

223
224
  --directory Run all tests found within the specified file directory.

225
226
227
228
229
  --xml       <path>

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

  --color     Output text format results with color highlighting.
230
231
232

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

233
234
235
236
237
  --keep-results

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

238
239
240
241
  --repeat    Number of times to repeat the test.

  --die-on-fail

242
243
244
245
              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.
246

247
248
249
250
  --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.

251
252
253
  --non-html  Removes escaping from output. Useful for reading results on the
              CLI.

254
255
  <test1>[,<test2>[,<test3> ...]]

256
              One or more tests to be run. By default, these are interpreted
257
              as the names of test groups as shown at
258
              admin/config/development/testing.
259
              These group names typically correspond to module names like "User"
260
              or "Profile" or "System", but there is also a group "Database".
261
              If --class is specified then these are interpreted as the names of
262
263
              specific test classes whose test methods will be run. Tests must
              be separated by commas. Ignored if --all is specified.
264

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

268
sudo -u [wwwrun|www-data|etc] php ./core/scripts/{$args['script']}
269
  --url http://example.com/ --all
270
sudo -u [wwwrun|www-data|etc] php ./core/scripts/{$args['script']}
271
  --url http://example.com/ --class "Drupal\block\Tests\BlockTest"
272
273
274
275
276
277
278
279
280
281

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

282
283
284
EOF;
}

285
286
287
/**
 * Parse execution argument and ensure that all are valid.
 *
288
289
 * @return array
 *   The list of arguments.
290
291
292
293
294
295
296
297
298
 */
function simpletest_script_parse_args() {
  // Set default values.
  $args = array(
    'script' => '',
    'help' => FALSE,
    'list' => FALSE,
    'clean' => FALSE,
    'url' => '',
299
300
    'sqlite' => NULL,
    'dburl' => NULL,
301
    'php' => '',
302
303
    'concurrency' => 1,
    'all' => FALSE,
304
    'module' => NULL,
305
    'class' => FALSE,
306
    'file' => FALSE,
307
    'types' => [],
308
    'directory' => NULL,
309
310
    'color' => FALSE,
    'verbose' => FALSE,
311
    'keep-results' => FALSE,
312
    'keep-results-table' => FALSE,
313
    'test_names' => array(),
314
315
    'repeat' => 1,
    'die-on-fail' => FALSE,
316
    'browser' => FALSE,
317
    // Used internally.
318
319
    'test-id' => 0,
    'execute-test' => '',
320
    'xml' => '',
321
    'non-html' => FALSE,
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
  );

  // 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;
        }
337
338
339
340
        elseif (is_array($args[$previous_arg])) {
          $value = array_shift($_SERVER['argv']);
          $args[$matches[1]] = array_map('trim', explode(',', $value));
        }
341
342
343
        else {
          $args[$matches[1]] = array_shift($_SERVER['argv']);
        }
344
        // Clear extraneous values.
345
346
347
348
349
350
        $args['test_names'] = array();
        $count++;
      }
      else {
        // Argument not found in list.
        simpletest_script_print_error("Unknown argument '$arg'.");
351
        exit(SIMPLETEST_SCRIPT_EXIT_FAILURE);
352
353
354
355
356
      }
    }
    else {
      // Values found without an argument should be test names.
      $args['test_names'] += explode(',', $arg);
357
      $count++;
358
    }
359
  }
360

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

367
368
369
  if ($args['browser']) {
    $args['keep-results'] = TRUE;
  }
370
  return array($args, $count);
371
372
}

373
374
375
/**
 * Initialize script variables and perform general setup requirements.
 */
376
function simpletest_script_init() {
377
378
379
380
  global $args, $php;

  $host = 'localhost';
  $path = '';
381
382
  $port = '80';

383
384
  // Determine location of php command automatically, unless a command line
  // argument is supplied.
385
  if (!empty($args['php'])) {
386
387
    $php = $args['php'];
  }
388
  elseif ($php_env = getenv('_')) {
389
390
    // '_' is an environment variable set by the shell. It contains the command
    // that was executed.
391
    $php = $php_env;
392
  }
393
  elseif ($sudo = getenv('SUDO_COMMAND')) {
394
395
    // 'SUDO_COMMAND' is an environment variable set by the sudo program.
    // Extract only the PHP interpreter, not the rest of the command.
396
    list($php) = explode(' ', $sudo, 2);
397
398
  }
  else {
399
    simpletest_script_print_error('Unable to automatically determine the path to the PHP interpreter. Supply the --php command line argument.');
400
    simpletest_script_help();
401
    exit(SIMPLETEST_SCRIPT_EXIT_FAILURE);
402
  }
403

404
  // Get URL from arguments.
405
406
  if (!empty($args['url'])) {
    $parsed_url = parse_url($args['url']);
407
    $host = $parsed_url['host'] . (isset($parsed_url['port']) ? ':' . $parsed_url['port'] : '');
408
    $path = isset($parsed_url['path']) ? rtrim(rtrim($parsed_url['path']), '/') : '';
409
    $port = (isset($parsed_url['port']) ? $parsed_url['port'] : $port);
410
411
412
    if ($path == '/') {
      $path = '';
    }
413
    // If the passed URL schema is 'https' then setup the $_SERVER variables
414
    // properly so that testing will run under HTTPS.
415
416
417
    if ($parsed_url['scheme'] == 'https') {
      $_SERVER['HTTPS'] = 'on';
    }
418
419
  }

420
421
422
423
424
425
426
427
428
429
430
  if (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on') {
    $base_url = 'https://';
  }
  else {
    $base_url = 'http://';
  }
  $base_url .= $host;
  if ($path !== '') {
    $base_url .= $path;
  }
  putenv('SIMPLETEST_BASE_URL=' . $base_url);
431
432
433
  $_SERVER['HTTP_HOST'] = $host;
  $_SERVER['REMOTE_ADDR'] = '127.0.0.1';
  $_SERVER['SERVER_ADDR'] = '127.0.0.1';
434
  $_SERVER['SERVER_PORT'] = $port;
435
  $_SERVER['SERVER_SOFTWARE'] = NULL;
436
  $_SERVER['SERVER_NAME'] = 'localhost';
437
  $_SERVER['REQUEST_URI'] = $path . '/';
438
  $_SERVER['REQUEST_METHOD'] = 'GET';
439
440
441
  $_SERVER['SCRIPT_NAME'] = $path . '/index.php';
  $_SERVER['SCRIPT_FILENAME'] = $path . '/index.php';
  $_SERVER['PHP_SELF'] = $path . '/index.php';
442
443
  $_SERVER['HTTP_USER_AGENT'] = 'Drupal command line';

444
  if (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') {
445
446
447
448
449
450
    // Ensure that any and all environment variables are changed to https://.
    foreach ($_SERVER as $key => $value) {
      $_SERVER[$key] = str_replace('http://', 'https://', $_SERVER[$key]);
    }
  }

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

/**
 * 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) {
481
  global $args;
482
483
484
485
486
487
488
489
490
491
492

  // 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');
493
494
    try {
      $databases['default']['default'] = Database::convertDbUrlToConnectionInfo($args['dburl'], DRUPAL_ROOT);
495
    }
496
497
    catch (\InvalidArgumentException $e) {
      simpletest_script_print_error('Invalid --dburl. Reason: ' . $e->getMessage());
498
      exit(SIMPLETEST_SCRIPT_EXIT_FAILURE);
499
    }
500
  }
501
502
503
  // Otherwise, use the default database connection from settings.php.
  else {
    $databases['default'] = Database::getConnectionInfo('default');
504
505
506
507
508
  }

  // 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.');
509
    exit(SIMPLETEST_SCRIPT_EXIT_FAILURE);
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
544
545
546
547
548
  }
  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 {
549
550
    $connection = Database::getConnection('default', 'test-runner');
    $schema = $connection->schema();
551
552
553
  }
  catch (\PDOException $e) {
    simpletest_script_print_error($databases['test-runner']['default']['driver'] . ': ' . $e->getMessage());
554
    exit(SIMPLETEST_SCRIPT_EXIT_FAILURE);
555
556
557
558
  }
  if ($new && $sqlite) {
    require_once DRUPAL_ROOT . '/' . drupal_get_path('module', 'simpletest') . '/simpletest.install';
    foreach (simpletest_schema() as $name => $table_spec) {
559
      try {
560
561
562
563
564
565
        $table_exists = $schema->tableExists($name);
        if (empty($args['keep-results-table']) && $table_exists) {
          $connection->truncate($name)->execute();
        }
        if (!$table_exists) {
          $schema->createTable($name, $table_spec);
566
567
568
569
570
        }
      }
      catch (Exception $e) {
        echo (string) $e;
        exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
571
572
573
574
      }
    }
  }
  // Verify that the Simpletest database schema exists by checking one table.
575
576
577
578
579
580
581
582
583
  try {
    if (!$schema->tableExists('simpletest')) {
      simpletest_script_print_error('Missing Simpletest database schema. Either install Simpletest module or use the --sqlite parameter.');
      exit(SIMPLETEST_SCRIPT_EXIT_FAILURE);
    }
  }
  catch (Exception $e) {
    echo (string) $e;
    exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
584
585
586
  }
}

587
588
589
/**
 * Execute a batch of tests.
 */
590
function simpletest_script_execute_batch($test_classes) {
591
  global $args, $test_ids;
592

593
594
  $total_status = SIMPLETEST_SCRIPT_EXIT_SUCCESS;

595
596
  // Multi-process execution.
  $children = array();
597
  while (!empty($test_classes) || !empty($children)) {
598
    while (count($children) < $args['concurrency']) {
599
      if (empty($test_classes)) {
600
        break;
601
      }
602

603
604
605
606
607
608
609
610
611
612
      try {
        $test_id = Database::getConnection('default', 'test-runner')
          ->insert('simpletest_test_id')
          ->useDefaults(array('test_id'))
          ->execute();
      }
      catch (Exception $e) {
        echo (string) $e;
        exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
      }
613
      $test_ids[] = $test_id;
614

615
      $test_class = array_shift($test_classes);
616
      // Fork a child process.
617
618
619
620
621
      $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";
622
        exit(SIMPLETEST_SCRIPT_EXIT_SUCCESS);
623
624
      }

625
626
627
      // Register our new child.
      $children[] = array(
        'process' => $process,
628
        'test_id' => $test_id,
629
630
631
632
        'class' => $test_class,
        'pipes' => $pipes,
      );
    }
633

634
635
636
637
638
639
640
641
642
    // 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']);
643
644
645
646
        if ($status['exitcode'] === SIMPLETEST_SCRIPT_EXIT_FAILURE) {
          $total_status = max($status['exitcode'], $total_status);
        }
        elseif ($status['exitcode']) {
647
648
649
650
          $message = 'FATAL ' . $child['class'] . ': test runner returned a non-zero error code (' . $status['exitcode'] . ').';
          echo $message . "\n";
          // Insert a fail for xml results.
          TestBase::insertAssert($child['test_id'], $child['class'], FALSE, $message, 'run-tests.sh check');
651
652
653
654
655
          // Ensure that an error line is displayed for the class.
          simpletest_script_reporter_display_summary(
            $child['class'],
            ['#pass' => 0, '#fail' => 1, '#exception' => 0, '#debug' => 0]
          );
656
          if ($args['die-on-fail']) {
657
            list($db_prefix) = simpletest_last_test_get($child['test_id']);
658
            $test_directory = 'sites/simpletest/' . substr($db_prefix, 10);
659
            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";
660
661
662
            $args['keep-results'] = TRUE;
            // Exit repeat loop immediately.
            $args['repeat'] = -1;
663
          }
664
        }
665
        // Free-up space by removing any potentially created resources.
666
667
668
        if (!$args['keep-results']) {
          simpletest_script_cleanup($child['test_id'], $child['class'], $status['exitcode']);
        }
669
670

        // Remove this child.
671
        unset($children[$cid]);
672
673
674
      }
    }
  }
675
  return $total_status;
676
677
}

678
/**
679
 * Run a PHPUnit-based test.
680
 */
681
function simpletest_script_run_phpunit($test_id, $class) {
682
683
684
685
686
  $reflection = new \ReflectionClass($class);
  if ($reflection->hasProperty('runLimit')) {
    set_time_limit($reflection->getStaticPropertyValue('runLimit'));
  }

687
  $results = simpletest_run_phpunit_tests($test_id, array($class), $status);
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
  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;
707

708
709
710
      case 'fail':
        $summaries[$result['test_class']]['#fail']++;
        break;
711

712
713
714
      case 'exception':
        $summaries[$result['test_class']]['#exception']++;
        break;
715

716
      case 'debug':
717
        $summaries[$result['test_class']]['#debug']++;
718
719
720
721
722
        break;
    }
  }

  foreach ($summaries as $class => $summary) {
723
    simpletest_script_reporter_display_summary($class, $summary);
724
  }
725
  return $status;
726
727
}

728
/**
729
 * Run a single test, bootstrapping Drupal if needed.
730
731
 */
function simpletest_script_run_one_test($test_id, $test_class) {
732
  global $args;
733

734
  try {
735
736
737
738
739
740
741
742
743
744
    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);
745
    if (is_subclass_of($test_class, '\PHPUnit_Framework_TestCase')) {
746
      $status = simpletest_script_run_phpunit($test_id, $test_class);
747
748
749
750
751
752
    }
    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);
753
754
755
756
757
758

      $status = SIMPLETEST_SCRIPT_EXIT_SUCCESS;
      // Finished, kill this runner.
      if ($test->results['#fail'] || $test->results['#exception']) {
        $status = SIMPLETEST_SCRIPT_EXIT_FAILURE;
      }
759
    }
760

761
    exit($status);
762
  }
763
764
  // DrupalTestCase::run() catches exceptions already, so this is only reached
  // when an exception is thrown in the wrapping test runner environment.
765
766
  catch (Exception $e) {
    echo (string) $e;
767
    exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
768
  }
769
770
}

771
/**
772
773
 * Return a command used to run a test in a separate process.
 *
774
775
776
777
778
779
780
 * @param int $test_id
 *   The current test ID.
 * @param string $test_class
 *   The name of the test class to run.
 *
 * @return string
 *   The assembled command string.
781
 */
782
function simpletest_script_command($test_id, $test_class) {
783
  global $args, $php;
784

785
786
  $command = escapeshellarg($php) . ' ' . escapeshellarg('./core/scripts/' . $args['script']);
  $command .= ' --url ' . escapeshellarg($args['url']);
787
788
789
790
791
792
  if (!empty($args['sqlite'])) {
    $command .= ' --sqlite ' . escapeshellarg($args['sqlite']);
  }
  if (!empty($args['dburl'])) {
    $command .= ' --dburl ' . escapeshellarg($args['dburl']);
  }
793
794
  $command .= ' --php ' . escapeshellarg($php);
  $command .= " --test-id $test_id";
795
  foreach (array('verbose', 'keep-results', 'color', 'die-on-fail') as $arg) {
796
797
798
    if ($args[$arg]) {
      $command .= ' --' . $arg;
    }
799
  }
800
801
  // --execute-test and class name needs to come last.
  $command .= ' --execute-test ' . escapeshellarg($test_class);
802
  return $command;
803
804
}

805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
/**
 * 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) {
827
  if (is_subclass_of($test_class, '\PHPUnit_Framework_TestCase')) {
828
829
830
    // PHPUnit test, move on.
    return;
  }
831
  // Retrieve the last database prefix used for testing.
832
  try {
833
    list($db_prefix) = simpletest_last_test_get($test_id);
834
835
836
837
838
  }
  catch (Exception $e) {
    echo (string) $e;
    exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
  }
839
840
841
842
843
844
845
846
847
848
849
850
851
852

  // 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.
853
854
855
856
857
858
859
  try {
    simpletest_log_read($test_id, $db_prefix, $test_class);
  }
  catch (Exception $e) {
    echo (string) $e;
    exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
  }
860

861
862
863
  // Check whether a test site directory was setup already.
  // @see \Drupal\simpletest\TestBase::prepareEnvironment()
  $test_directory = DRUPAL_ROOT . '/sites/simpletest/' . substr($db_prefix, 10);
864
865
866
867
868
869
870
871
  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;
      }
    }
872
    // Delete the test site directory.
873
874
875
    // simpletest_clean_temporary_directories() cannot be used here, since it
    // would also delete file directories of other tests that are potentially
    // running concurrently.
876
    file_unmanaged_delete_recursive($test_directory, array('Drupal\simpletest\TestBase', 'filePreDeleteCallback'));
877
    $messages[] = "- Removed test site directory.";
878
879
880
  }

  // Clear out all database tables from the test.
881
882
883
884
885
886
887
  try {
    $schema = Database::getConnection('default', 'default')->schema();
    $count = 0;
    foreach ($schema->findTables($db_prefix . '%') as $table) {
      $schema->dropTable($table);
      $count++;
    }
888
  }
889
890
891
892
893
  catch (Exception $e) {
    echo (string) $e;
    exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
  }

894
  if ($count) {
895
    $messages[] = "- Removed $count leftover tables.";
896
897
898
899
900
901
902
903
  }

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

904
/**
905
 * Get list of tests based on arguments.
906
 *
907
908
 * If --all specified then return all available tests, otherwise reads list of
 * tests.
909
 *
910
911
 * @return array
 *   List of tests.
912
913
 */
function simpletest_script_get_test_list() {
914
  global $args;
915
916

  $test_list = array();
917
  if ($args['all'] || $args['module']) {
918
    try {
919
      $groups = simpletest_test_get_all($args['module'], $args['types']);
920
921
922
923
924
    }
    catch (Exception $e) {
      echo (string) $e;
      exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
    }
925
926
    $all_tests = array();
    foreach ($groups as $group => $tests) {
927
      $all_tests = array_merge($all_tests, array_keys($tests));
928
    }
929
    $test_list = $all_tests;
930
931
  }
  else {
932
    if ($args['class']) {
933
934
      $test_list = array();
      foreach ($args['test_names'] as $test_class) {
935
        list($class_name) = explode('::', $test_class, 2);
936
        if (class_exists($class_name)) {
937
938
939
          $test_list[] = $test_class;
        }
        else {
940
          try {
941
            $groups = simpletest_test_get_all(NULL, $args['types']);
942
943
944
945
946
          }
          catch (Exception $e) {
            echo (string) $e;
            exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
          }
947
948
949
950
          $all_classes = array();
          foreach ($groups as $group) {
            $all_classes = array_merge($all_classes, array_keys($group));
          }
951
952
          simpletest_script_print_error('Test class not found: ' . $class_name);
          simpletest_script_print_alternatives($class_name, $all_classes, 6);
953
          exit(SIMPLETEST_SCRIPT_EXIT_FAILURE);
954
        }
955
956
      }
    }
957
    elseif ($args['file']) {
958
      // Extract test case class names from specified files.
959
      foreach ($args['test_names'] as $file) {
960
961
        if (!file_exists($file)) {
          simpletest_script_print_error('File not found: ' . $file);
962
          exit(SIMPLETEST_SCRIPT_EXIT_FAILURE);
963
964
965
966
967
968
969
970
971
972
973
        }
        $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) {
974
          $test_list = array_merge($test_list, $matches[1]);
975
976
977
        }
        else {
          foreach ($matches[1] as $class_name) {
978
            $namespace_class = $namespace . '\\' . $class_name;
979
            if (is_subclass_of($namespace_class, '\Drupal\simpletest\TestBase') || is_subclass_of($namespace_class, '\PHPUnit_Framework_TestCase')) {
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
              $test_list[] = $namespace_class;
            }
          }
        }
      }
    }
    elseif ($args['directory']) {
      // Extract test case class names from specified directory.
      // Find all tests in the PSR-X structure; Drupal\$extension\Tests\*.php
      // Since we do not want to hard-code too many structural file/directory
      // assumptions about PSR-0/4 files and directories, we check for the
      // minimal conditions only; i.e., a '*.php' file that has '/Tests/' in
      // its path.
      // Ignore anything from third party vendors.
      $ignore = array('.', '..', 'vendor');
      $files = [];
      if ($args['directory'][0] === '/') {
        $directory = $args['directory'];
      }
      else {
        $directory = DRUPAL_ROOT . "/" . $args['directory'];
      }
      foreach (file_scan_directory($directory, '/\.php$/', $ignore) as $file) {
        // '/Tests/' can be contained anywhere in the file's path (there can be
        // sub-directories below /Tests), but must be contained literally.
        // Case-insensitive to match all Simpletest and PHPUnit tests:
1006
1007
1008
1009
        // ./lib/Drupal/foo/Tests/Bar/Baz.php
        // ./foo/src/Tests/Bar/Baz.php
        // ./foo/tests/Drupal/foo/Tests/FooTest.php
        // ./foo/tests/src/FooTest.php
1010
        // $file->filename doesn't give us a directory, so we use $file->uri
1011
1012
        // Strip the drupal root directory and trailing slash off the URI.
        $filename = substr($file->uri, strlen(DRUPAL_ROOT) + 1);
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
        if (stripos($filename, '/Tests/')) {
          $files[$filename] = $filename;
        }
      }
      foreach ($files as $file) {
        $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) {
          $test_list = array_merge($test_list, $matches[1]);
        }
        else {
          foreach ($matches[1] as $class_name) {
            $namespace_class = $namespace . '\\' . $class_name;
            if (is_subclass_of($namespace_class, '\Drupal\simpletest\TestBase') || is_subclass_of($namespace_class, '\PHPUnit_Framework_TestCase')) {
1034
1035
              $test_list[] = $namespace_class;
            }
1036
          }
1037
1038
1039
        }
      }
    }
1040
    else {
1041
      try {
1042
        $groups = simpletest_test_get_all(NULL, $args['types']);
1043
1044
1045
1046
1047
      }
      catch (Exception $e) {
        echo (string) $e;
        exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
      }
1048
      foreach ($args['test_names'] as $group_name) {
1049
1050
1051
1052
1053
1054
        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));
1055
          exit(SIMPLETEST_SCRIPT_EXIT_FAILURE);
1056
        }
1057
1058
      }
    }
1059
  }
1060

1061
1062
  if (empty($test_list)) {
    simpletest_script_print_error('No valid tests were specified.');
1063
    exit(SIMPLETEST_SCRIPT_EXIT_FAILURE);
1064
1065
  }
  return $test_list;
1066
1067
}

1068
1069
1070
1071
/**
 * Initialize the reporter.
 */
function simpletest_script_reporter_init() {
1072
  global $args, $test_list, $results_map;
1073
1074
1075
1076

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

1080
1081
1082
1083
  echo "\n";
  echo "Drupal test run\n";
  echo "---------------\n";
  echo "\n";
1084

1085
1086
1087
1088
1089
1090
  // 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";
1091
    foreach ($test_list as $class_name) {
1092
      echo "  - $class_name\n";
1093
1094
    }
    echo "\n";
1095
  }
1096

1097
  echo "Test run started:\n";
1098
  echo "  " . date('l, F j, Y - H:i', $_SERVER['REQUEST_TIME']) . "\n";
1099
  Timer::start('run-tests');
1100
1101