1<?php
2
3/**
4 * @file
5 * This script runs Drupal tests from command line.
6 */
7
8use Drupal\Component\FileSystem\FileSystem;
9use Drupal\Component\Utility\Environment;
10use Drupal\Component\Utility\Html;
11use Drupal\Component\Utility\Timer;
12use Drupal\Core\Composer\Composer;
13use Drupal\Core\Database\Database;
14use Drupal\Core\File\Exception\FileException;
15use Drupal\Core\Test\EnvironmentCleaner;
16use Drupal\Core\Test\PhpUnitTestRunner;
17use Drupal\Core\Test\RunTests\TestFileParser;
18use Drupal\Core\Test\TestDatabase;
19use Drupal\Core\Test\TestRunnerKernel;
20use Drupal\Core\Test\TestDiscovery;
21use PHPUnit\Framework\TestCase;
22use PHPUnit\Runner\Version;
23use Symfony\Component\Console\Output\ConsoleOutput;
24use Symfony\Component\Filesystem\Filesystem as SymfonyFilesystem;
25use Symfony\Component\HttpFoundation\Request;
26
27// Define some colors for display.
28// A nice calming green.
29const SIMPLETEST_SCRIPT_COLOR_PASS = 32;
30// An alerting Red.
31const SIMPLETEST_SCRIPT_COLOR_FAIL = 31;
32// An annoying brown.
33const SIMPLETEST_SCRIPT_COLOR_EXCEPTION = 33;
34
35// Restricting the chunk of queries prevents memory exhaustion.
36const SIMPLETEST_SCRIPT_SQLITE_VARIABLE_LIMIT = 350;
37
38const SIMPLETEST_SCRIPT_EXIT_SUCCESS = 0;
39const SIMPLETEST_SCRIPT_EXIT_FAILURE = 1;
40const SIMPLETEST_SCRIPT_EXIT_EXCEPTION = 2;
41
42// Set defaults and get overrides.
43list($args, $count) = simpletest_script_parse_args();
44
45if ($args['help'] || $count == 0) {
46  simpletest_script_help();
47  exit(($count == 0) ? SIMPLETEST_SCRIPT_EXIT_FAILURE : SIMPLETEST_SCRIPT_EXIT_SUCCESS);
48}
49
50simpletest_script_init();
51
52if (!class_exists(TestCase::class)) {
53  echo "\nrun-tests.sh requires the PHPUnit testing framework. Please use 'composer install' to ensure that it is present.\n\n";
54  exit(SIMPLETEST_SCRIPT_EXIT_FAILURE);
55}
56
57if ($args['execute-test']) {
58  simpletest_script_setup_database();
59  simpletest_script_run_one_test($args['test-id'], $args['execute-test']);
60  // Sub-process exited already; this is just for clarity.
61  exit(SIMPLETEST_SCRIPT_EXIT_SUCCESS);
62}
63
64if ($args['list']) {
65  // Display all available tests organized by one @group annotation.
66  echo "\nAvailable test groups & classes\n";
67  echo "-------------------------------\n\n";
68  try {
69    // @todo Use \Drupal\Core\Test\TestDiscovery when we no longer need BC for
70    //   hook_simpletest_alter().
71    $groups = \Drupal::service('test_discovery')->getTestClasses($args['module']);
72  }
73  catch (Exception $e) {
74    error_log((string) $e);
75    echo (string) $e;
76    exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
77  }
78
79  // A given class can appear in multiple groups. For historical reasons, we
80  // need to present each test only once. The test is shown in the group that is
81  // printed first.
82  $printed_tests = [];
83  foreach ($groups as $group => $tests) {
84    echo $group . "\n";
85    $tests = array_diff(array_keys($tests), $printed_tests);
86    foreach ($tests as $test) {
87      echo " - $test\n";
88    }
89    $printed_tests = array_merge($printed_tests, $tests);
90  }
91  exit(SIMPLETEST_SCRIPT_EXIT_SUCCESS);
92}
93
94// List-files and list-files-json provide a way for external tools such as the
95// testbot to prioritize running changed tests.
96// @see https://www.drupal.org/node/2569585
97if ($args['list-files'] || $args['list-files-json']) {
98  // List all files which could be run as tests.
99  $test_discovery = NULL;
100  try {
101    // @todo Use \Drupal\Core\Test\TestDiscovery when we no longer need BC for
102    //   hook_simpletest_alter().
103    $test_discovery = \Drupal::service('test_discovery');
104  }
105  catch (Exception $e) {
106    error_log((string) $e);
107    echo (string) $e;
108    exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
109  }
110  // TestDiscovery::findAllClassFiles() gives us a classmap similar to a
111  // Composer 'classmap' array.
112  $test_classes = $test_discovery->findAllClassFiles();
113  // JSON output is the easiest.
114  if ($args['list-files-json']) {
115    echo json_encode($test_classes);
116    exit(SIMPLETEST_SCRIPT_EXIT_SUCCESS);
117  }
118  // Output the list of files.
119  else {
120    foreach (array_values($test_classes) as $test_class) {
121      echo $test_class . "\n";
122    }
123  }
124  exit(SIMPLETEST_SCRIPT_EXIT_SUCCESS);
125}
126
127simpletest_script_setup_database(TRUE);
128
129if ($args['clean']) {
130  // Clean up left-over tables and directories.
131  $cleaner = new EnvironmentCleaner(
132    DRUPAL_ROOT,
133    Database::getConnection(),
134    TestDatabase::getConnection(),
135    new ConsoleOutput(),
136    \Drupal::service('file_system')
137  );
138  try {
139    $cleaner->cleanEnvironment();
140  }
141  catch (Exception $e) {
142    echo (string) $e;
143    exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
144  }
145  echo "\nEnvironment cleaned.\n";
146
147  // Get the status messages and print them.
148  $messages = \Drupal::messenger()->messagesByType('status');
149  foreach ($messages as $text) {
150    echo " - " . $text . "\n";
151  }
152  exit(SIMPLETEST_SCRIPT_EXIT_SUCCESS);
153}
154
155if (!Composer::upgradePHPUnitCheck(Version::id())) {
156  simpletest_script_print_error("PHPUnit testing framework version 7 or greater is required when running on PHP 7.3 or greater. Run the command 'composer run-script drupal-phpunit-upgrade' in order to fix this.");
157  exit(SIMPLETEST_SCRIPT_EXIT_FAILURE);
158}
159
160$test_list = simpletest_script_get_test_list();
161
162// Try to allocate unlimited time to run the tests.
163Environment::setTimeLimit(0);
164simpletest_script_reporter_init();
165
166$tests_to_run = [];
167for ($i = 0; $i < $args['repeat']; $i++) {
168  $tests_to_run = array_merge($tests_to_run, $test_list);
169}
170
171// Execute tests.
172$status = simpletest_script_execute_batch($tests_to_run);
173
174// Stop the timer.
175simpletest_script_reporter_timer_stop();
176
177// Ensure all test locks are released once finished. If tests are run with a
178// concurrency of 1 the each test will clean up its own lock. Test locks are
179// not released if using a higher concurrency to ensure each test method has
180// unique fixtures.
181TestDatabase::releaseAllTestLocks();
182
183// Display results before database is cleared.
184if ($args['browser']) {
185  simpletest_script_open_browser();
186}
187else {
188  simpletest_script_reporter_display_results();
189}
190
191if ($args['xml']) {
192  simpletest_script_reporter_write_xml_results();
193}
194
195// Clean up all test results.
196if (!$args['keep-results']) {
197  try {
198    $cleaner = new EnvironmentCleaner(
199      DRUPAL_ROOT,
200      Database::getConnection(),
201      TestDatabase::getConnection(),
202      new ConsoleOutput(),
203      \Drupal::service('file_system')
204    );
205    $cleaner->cleanResultsTable();
206  }
207  catch (Exception $e) {
208    echo (string) $e;
209    exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
210  }
211}
212
213// Test complete, exit.
214exit($status);
215
216/**
217 * Print help text.
218 */
219function simpletest_script_help() {
220  global $args;
221
222  echo <<<EOF
223
224Run Drupal tests from the shell.
225
226Usage:        {$args['script']} [OPTIONS] <tests>
227Example:      {$args['script']} Profile
228
229All arguments are long options.
230
231  --help      Print this page.
232
233  --list      Display all available test groups.
234
235  --list-files
236              Display all discoverable test file paths.
237
238  --list-files-json
239              Display all discoverable test files as JSON. The array key will be
240              the test class name, and the value will be the file path of the
241              test.
242
243  --clean     Cleans up database tables or directories from previous, failed,
244              tests and then exits (no tests are run).
245
246  --url       The base URL of the root directory of this Drupal checkout; e.g.:
247                http://drupal.test/
248              Required unless the Drupal root directory maps exactly to:
249                http://localhost:80/
250              Use a https:// URL to force all tests to be run under SSL.
251
252  --sqlite    A pathname to use for the SQLite database of the test runner.
253              Required unless this script is executed with a working Drupal
254              installation that has Simpletest module installed.
255              A relative pathname is interpreted relative to the Drupal root
256              directory.
257              Note that ':memory:' cannot be used, because this script spawns
258              sub-processes. However, you may use e.g. '/tmpfs/test.sqlite'
259
260  --keep-results-table
261
262              Boolean flag to indicate to not cleanup the simpletest result
263              table. For testbots or repeated execution of a single test it can
264              be helpful to not cleanup the simpletest result table.
265
266  --dburl     A URI denoting the database driver, credentials, server hostname,
267              and database name to use in tests.
268              Required when running tests without a Drupal installation that
269              contains default database connection info in settings.php.
270              Examples:
271                mysql://username:password@localhost/databasename#table_prefix
272                sqlite://localhost/relative/path/db.sqlite
273                sqlite://localhost//absolute/path/db.sqlite
274
275  --php       The absolute path to the PHP executable. Usually not needed.
276
277  --concurrency [num]
278
279              Run tests in parallel, up to [num] tests at a time.
280
281  --all       Run all available tests.
282
283  --module    Run all tests belonging to the specified module name.
284              (e.g., 'node')
285
286  --class     Run tests identified by specific class names, instead of group names.
287              A specific test method can be added, for example,
288              'Drupal\book\Tests\BookTest::testBookExport'.
289
290  --file      Run tests identified by specific file names, instead of group names.
291              Specify the path and the extension
292              (i.e. 'core/modules/user/user.test').
293
294  --types
295
296              Runs just tests from the specified test type, for example
297              run-tests.sh
298              (i.e. --types "Simpletest,PHPUnit-Functional")
299
300  --directory Run all tests found within the specified file directory.
301
302  --xml       <path>
303
304              If provided, test results will be written as xml files to this path.
305
306  --color     Output text format results with color highlighting.
307
308  --verbose   Output detailed assertion messages in addition to summary.
309
310  --keep-results
311
312              Keeps detailed assertion results (in the database) after tests
313              have completed. By default, assertion results are cleared.
314
315  --repeat    Number of times to repeat the test.
316
317  --die-on-fail
318
319              Exit test execution immediately upon any failed assertion. This
320              allows to access the test site by changing settings.php to use the
321              test database and configuration directories. Use in combination
322              with --repeat for debugging random test failures.
323
324  --browser   Deprecated, use --verbose instead. This enforces --keep-results and
325              if you want to also view any pages rendered in the simpletest
326              browser you need to add --verbose to the command line.
327
328  --non-html  Removes escaping from output. Useful for reading results on the
329              CLI.
330
331  --suppress-deprecations
332
333              Stops tests from failing if deprecation errors are triggered. If
334              this is not set the value specified in the
335              SYMFONY_DEPRECATIONS_HELPER environment variable, or the value
336              specified in core/phpunit.xml (if it exists), or the default value
337              will be used. The default is that any unexpected silenced
338              deprecation error will fail tests.
339
340  <test1>[,<test2>[,<test3> ...]]
341
342              One or more tests to be run. By default, these are interpreted
343              as the names of test groups as shown at
344              admin/config/development/testing.
345              These group names typically correspond to module names like "User"
346              or "Profile" or "System", but there is also a group "Database".
347              If --class is specified then these are interpreted as the names of
348              specific test classes whose test methods will be run. Tests must
349              be separated by commas. Ignored if --all is specified.
350
351To run this script you will normally invoke it from the root directory of your
352Drupal installation as the webserver user (differs per configuration), or root:
353
354sudo -u [wwwrun|www-data|etc] php ./core/scripts/{$args['script']}
355  --url http://example.com/ --all
356sudo -u [wwwrun|www-data|etc] php ./core/scripts/{$args['script']}
357  --url http://example.com/ --class "Drupal\block\Tests\BlockTest"
358
359Without a preinstalled Drupal site and enabled Simpletest module, specify a
360SQLite database pathname to create and the default database connection info to
361use in tests:
362
363sudo -u [wwwrun|www-data|etc] php ./core/scripts/{$args['script']}
364  --sqlite /tmpfs/drupal/test.sqlite
365  --dburl mysql://username:password@localhost/database
366  --url http://example.com/ --all
367
368EOF;
369}
370
371/**
372 * Parse execution argument and ensure that all are valid.
373 *
374 * @return array
375 *   The list of arguments.
376 */
377function simpletest_script_parse_args() {
378  // Set default values.
379  $args = [
380    'script' => '',
381    'help' => FALSE,
382    'list' => FALSE,
383    'list-files' => FALSE,
384    'list-files-json' => FALSE,
385    'clean' => FALSE,
386    'url' => '',
387    'sqlite' => NULL,
388    'dburl' => NULL,
389    'php' => '',
390    'concurrency' => 1,
391    'all' => FALSE,
392    'module' => NULL,
393    'class' => FALSE,
394    'file' => FALSE,
395    'types' => [],
396    'directory' => NULL,
397    'color' => FALSE,
398    'verbose' => FALSE,
399    'keep-results' => FALSE,
400    'keep-results-table' => FALSE,
401    'test_names' => [],
402    'repeat' => 1,
403    'die-on-fail' => FALSE,
404    'suppress-deprecations' => FALSE,
405    'browser' => FALSE,
406    // Used internally.
407    'test-id' => 0,
408    'execute-test' => '',
409    'xml' => '',
410    'non-html' => FALSE,
411  ];
412
413  // Override with set values.
414  $args['script'] = basename(array_shift($_SERVER['argv']));
415
416  $count = 0;
417  while ($arg = array_shift($_SERVER['argv'])) {
418    if (preg_match('/--(\S+)/', $arg, $matches)) {
419      // Argument found.
420      if (array_key_exists($matches[1], $args)) {
421        // Argument found in list.
422        $previous_arg = $matches[1];
423        if (is_bool($args[$previous_arg])) {
424          $args[$matches[1]] = TRUE;
425        }
426        elseif (is_array($args[$previous_arg])) {
427          $value = array_shift($_SERVER['argv']);
428          $args[$matches[1]] = array_map('trim', explode(',', $value));
429        }
430        else {
431          $args[$matches[1]] = array_shift($_SERVER['argv']);
432        }
433        // Clear extraneous values.
434        $args['test_names'] = [];
435        $count++;
436      }
437      else {
438        // Argument not found in list.
439        simpletest_script_print_error("Unknown argument '$arg'.");
440        exit(SIMPLETEST_SCRIPT_EXIT_FAILURE);
441      }
442    }
443    else {
444      // Values found without an argument should be test names.
445      $args['test_names'] += explode(',', $arg);
446      $count++;
447    }
448  }
449
450  // Validate the concurrency argument.
451  if (!is_numeric($args['concurrency']) || $args['concurrency'] <= 0) {
452    simpletest_script_print_error("--concurrency must be a strictly positive integer.");
453    exit(SIMPLETEST_SCRIPT_EXIT_FAILURE);
454  }
455
456  if ($args['browser']) {
457    simpletest_script_print_error('The --browser option is deprecated in drupal:8.8.0 and is removed from drupal:9.0.0. Use --verbose instead. See https://www.drupal.org/node/3083549');
458    $args['keep-results'] = TRUE;
459  }
460  return [$args, $count];
461}
462
463/**
464 * Initialize script variables and perform general setup requirements.
465 */
466function simpletest_script_init() {
467  global $args, $php;
468
469  $host = 'localhost';
470  $path = '';
471  $port = '80';
472
473  // Determine location of php command automatically, unless a command line
474  // argument is supplied.
475  if (!empty($args['php'])) {
476    $php = $args['php'];
477  }
478  elseif ($php_env = getenv('_')) {
479    // '_' is an environment variable set by the shell. It contains the command
480    // that was executed.
481    $php = $php_env;
482  }
483  elseif ($sudo = getenv('SUDO_COMMAND')) {
484    // 'SUDO_COMMAND' is an environment variable set by the sudo program.
485    // Extract only the PHP interpreter, not the rest of the command.
486    list($php) = explode(' ', $sudo, 2);
487  }
488  else {
489    simpletest_script_print_error('Unable to automatically determine the path to the PHP interpreter. Supply the --php command line argument.');
490    simpletest_script_help();
491    exit(SIMPLETEST_SCRIPT_EXIT_FAILURE);
492  }
493
494  // Detect if we're in the top-level process using the private 'execute-test'
495  // argument. Determine if being run on drupal.org's testing infrastructure
496  // using the presence of 'drupalci' in the sqlite argument.
497  // @todo https://www.drupal.org/project/drupalci_testbot/issues/2860941 Use
498  //   better environment variable to detect DrupalCI.
499  if (!$args['execute-test'] && preg_match('/drupalci/', $args['sqlite'])) {
500    // Update PHPUnit if needed and possible. There is a later check once the
501    // autoloader is in place to ensure we're on the correct version. We need to
502    // do this before the autoloader is in place to ensure that it is correct.
503    $composer = ($composer = rtrim('\\' === DIRECTORY_SEPARATOR ? preg_replace('/[\r\n].*/', '', `where.exe composer.phar`) : `which composer.phar`))
504      ? $php . ' ' . escapeshellarg($composer)
505      : 'composer';
506    passthru("$composer run-script drupal-phpunit-upgrade-check");
507  }
508
509  $autoloader = require_once __DIR__ . '/../../autoload.php';
510  // The PHPUnit compatibility layer needs to be available to autoload tests.
511  $autoloader->add('Drupal\\TestTools', __DIR__ . '/../tests');
512
513  // Get URL from arguments.
514  if (!empty($args['url'])) {
515    $parsed_url = parse_url($args['url']);
516    $host = $parsed_url['host'] . (isset($parsed_url['port']) ? ':' . $parsed_url['port'] : '');
517    $path = isset($parsed_url['path']) ? rtrim(rtrim($parsed_url['path']), '/') : '';
518    $port = (isset($parsed_url['port']) ? $parsed_url['port'] : $port);
519    if ($path == '/') {
520      $path = '';
521    }
522    // If the passed URL schema is 'https' then setup the $_SERVER variables
523    // properly so that testing will run under HTTPS.
524    if ($parsed_url['scheme'] == 'https') {
525      $_SERVER['HTTPS'] = 'on';
526    }
527  }
528
529  if (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on') {
530    $base_url = 'https://';
531  }
532  else {
533    $base_url = 'http://';
534  }
535  $base_url .= $host;
536  if ($path !== '') {
537    $base_url .= $path;
538  }
539  putenv('SIMPLETEST_BASE_URL=' . $base_url);
540  $_SERVER['HTTP_HOST'] = $host;
541  $_SERVER['REMOTE_ADDR'] = '127.0.0.1';
542  $_SERVER['SERVER_ADDR'] = '127.0.0.1';
543  $_SERVER['SERVER_PORT'] = $port;
544  $_SERVER['SERVER_SOFTWARE'] = NULL;
545  $_SERVER['SERVER_NAME'] = 'localhost';
546  $_SERVER['REQUEST_URI'] = $path . '/';
547  $_SERVER['REQUEST_METHOD'] = 'GET';
548  $_SERVER['SCRIPT_NAME'] = $path . '/index.php';
549  $_SERVER['SCRIPT_FILENAME'] = $path . '/index.php';
550  $_SERVER['PHP_SELF'] = $path . '/index.php';
551  $_SERVER['HTTP_USER_AGENT'] = 'Drupal command line';
552
553  if ($args['concurrency'] > 1) {
554    $directory = FileSystem::getOsTemporaryDirectory();
555    $test_symlink = @symlink(__FILE__, $directory . '/test_symlink');
556    if (!$test_symlink) {
557      throw new \RuntimeException('In order to use a concurrency higher than 1 the test system needs to be able to create symlinks in ' . $directory);
558    }
559    unlink($directory . '/test_symlink');
560    putenv('RUN_TESTS_CONCURRENCY=' . $args['concurrency']);
561  }
562
563  if (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') {
564    // Ensure that any and all environment variables are changed to https://.
565    foreach ($_SERVER as $key => $value) {
566      $_SERVER[$key] = str_replace('http://', 'https://', $_SERVER[$key]);
567    }
568  }
569
570  chdir(realpath(__DIR__ . '/../..'));
571
572  // Prepare the kernel.
573  try {
574    $request = Request::createFromGlobals();
575    $kernel = TestRunnerKernel::createFromRequest($request, $autoloader);
576    $kernel->boot();
577    $kernel->preHandle($request);
578  }
579  catch (Exception $e) {
580    echo (string) $e;
581    exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
582  }
583}
584
585/**
586 * Sets up database connection info for running tests.
587 *
588 * If this script is executed from within a real Drupal installation, then this
589 * function essentially performs nothing (unless the --sqlite or --dburl
590 * parameters were passed).
591 *
592 * Otherwise, there are three database connections of concern:
593 * - --sqlite: The test runner connection, providing access to Simpletest
594 *   database tables for recording test IDs and assertion results.
595 * - --dburl: A database connection that is used as base connection info for all
596 *   tests; i.e., every test will spawn from this connection. In case this
597 *   connection uses e.g. SQLite, then all tests will run against SQLite. This
598 *   is exposed as $databases['default']['default'] to Drupal.
599 * - The actual database connection used within a test. This is the same as
600 *   --dburl, but uses an additional database table prefix. This is
601 *   $databases['default']['default'] within a test environment. The original
602 *   connection is retained in
603 *   $databases['simpletest_original_default']['default'] and restored after
604 *   each test.
605 *
606 * @param bool $new
607 *   Whether this process is a run-tests.sh master process. If TRUE, the SQLite
608 *   database file specified by --sqlite (if any) is set up. Otherwise, database
609 *   connections are prepared only.
610 */
611function simpletest_script_setup_database($new = FALSE) {
612  global $args;
613
614  // If there is an existing Drupal installation that contains a database
615  // connection info in settings.php, then $databases['default']['default'] will
616  // hold the default database connection already. This connection is assumed to
617  // be valid, and this connection will be used in tests, so that they run
618  // against e.g. MySQL instead of SQLite.
619  // However, in case no Drupal installation exists, this default database
620  // connection can be set and/or overridden with the --dburl parameter.
621  if (!empty($args['dburl'])) {
622    // Remove a possibly existing default connection (from settings.php).
623    Database::removeConnection('default');
624    try {
625      $databases['default']['default'] = Database::convertDbUrlToConnectionInfo($args['dburl'], DRUPAL_ROOT);
626    }
627    catch (\InvalidArgumentException $e) {
628      simpletest_script_print_error('Invalid --dburl. Reason: ' . $e->getMessage());
629      exit(SIMPLETEST_SCRIPT_EXIT_FAILURE);
630    }
631  }
632  // Otherwise, use the default database connection from settings.php.
633  else {
634    $databases['default'] = Database::getConnectionInfo('default');
635  }
636
637  // If there is no default database connection for tests, we cannot continue.
638  if (!isset($databases['default']['default'])) {
639    simpletest_script_print_error('Missing default database connection for tests. Use --dburl to specify one.');
640    exit(SIMPLETEST_SCRIPT_EXIT_FAILURE);
641  }
642  Database::addConnectionInfo('default', 'default', $databases['default']['default']);
643
644  // If no --sqlite parameter has been passed, then Simpletest module is assumed
645  // to be installed, so the test runner database connection is the default
646  // database connection.
647  if (empty($args['sqlite'])) {
648    $sqlite = FALSE;
649    $databases['test-runner']['default'] = $databases['default']['default'];
650  }
651  // Otherwise, set up a SQLite connection for the test runner.
652  else {
653    if ($args['sqlite'][0] === '/') {
654      $sqlite = $args['sqlite'];
655    }
656    else {
657      $sqlite = DRUPAL_ROOT . '/' . $args['sqlite'];
658    }
659    $databases['test-runner']['default'] = [
660      'driver' => 'sqlite',
661      'database' => $sqlite,
662      'prefix' => [
663        'default' => '',
664      ],
665    ];
666    // Create the test runner SQLite database, unless it exists already.
667    if ($new && !file_exists($sqlite)) {
668      if (!is_dir(dirname($sqlite))) {
669        mkdir(dirname($sqlite));
670      }
671      touch($sqlite);
672    }
673  }
674
675  // Add the test runner database connection.
676  Database::addConnectionInfo('test-runner', 'default', $databases['test-runner']['default']);
677
678  // Create the Simpletest schema.
679  try {
680    $connection = Database::getConnection('default', 'test-runner');
681    $schema = $connection->schema();
682  }
683  catch (\PDOException $e) {
684    simpletest_script_print_error($databases['test-runner']['default']['driver'] . ': ' . $e->getMessage());
685    exit(SIMPLETEST_SCRIPT_EXIT_FAILURE);
686  }
687  if ($new && $sqlite) {
688    foreach (TestDatabase::testingSchema() as $name => $table_spec) {
689      try {
690        $table_exists = $schema->tableExists($name);
691        if (empty($args['keep-results-table']) && $table_exists) {
692          $connection->truncate($name)->execute();
693        }
694        if (!$table_exists) {
695          $schema->createTable($name, $table_spec);
696        }
697      }
698      catch (Exception $e) {
699        echo (string) $e;
700        exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
701      }
702    }
703  }
704  // Verify that the Simpletest database schema exists by checking one table.
705  try {
706    if (!$schema->tableExists('simpletest')) {
707      simpletest_script_print_error('Missing Simpletest database schema. Either install Simpletest module or use the --sqlite parameter.');
708      exit(SIMPLETEST_SCRIPT_EXIT_FAILURE);
709    }
710  }
711  catch (Exception $e) {
712    echo (string) $e;
713    exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
714  }
715}
716
717/**
718 * Execute a batch of tests.
719 */
720function simpletest_script_execute_batch($test_classes) {
721  global $args, $test_ids;
722
723  $total_status = SIMPLETEST_SCRIPT_EXIT_SUCCESS;
724
725  // Multi-process execution.
726  $children = [];
727  while (!empty($test_classes) || !empty($children)) {
728    while (count($children) < $args['concurrency']) {
729      if (empty($test_classes)) {
730        break;
731      }
732
733      try {
734        $test_id = Database::getConnection('default', 'test-runner')
735          ->insert('simpletest_test_id')
736          ->useDefaults(['test_id'])
737          ->execute();
738      }
739      catch (Exception $e) {
740        echo (string) $e;
741        exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
742      }
743      $test_ids[] = $test_id;
744
745      $test_class = array_shift($test_classes);
746      // Fork a child process.
747      $command = simpletest_script_command($test_id, $test_class);
748      $process = proc_open($command, [], $pipes, NULL, NULL, ['bypass_shell' => TRUE]);
749
750      if (!is_resource($process)) {
751        echo "Unable to fork test process. Aborting.\n";
752        exit(SIMPLETEST_SCRIPT_EXIT_SUCCESS);
753      }
754
755      // Register our new child.
756      $children[] = [
757        'process' => $process,
758        'test_id' => $test_id,
759        'class' => $test_class,
760        'pipes' => $pipes,
761      ];
762    }
763
764    // Wait for children every 200ms.
765    usleep(200000);
766
767    // Check if some children finished.
768    foreach ($children as $cid => $child) {
769      $status = proc_get_status($child['process']);
770      if (empty($status['running'])) {
771        // The child exited, unregister it.
772        proc_close($child['process']);
773        if ($status['exitcode'] === SIMPLETEST_SCRIPT_EXIT_FAILURE) {
774          $total_status = max($status['exitcode'], $total_status);
775        }
776        elseif ($status['exitcode']) {
777          $message = 'FATAL ' . $child['class'] . ': test runner returned a non-zero error code (' . $status['exitcode'] . ').';
778          echo $message . "\n";
779          // @todo Return SIMPLETEST_SCRIPT_EXIT_EXCEPTION instead, when
780          // DrupalCI supports this.
781          // @see https://www.drupal.org/node/2780087
782          $total_status = max(SIMPLETEST_SCRIPT_EXIT_FAILURE, $total_status);
783          // Insert a fail for xml results.
784          TestDatabase::insertAssert($child['test_id'], $child['class'], FALSE, $message, 'run-tests.sh check');
785          // Ensure that an error line is displayed for the class.
786          simpletest_script_reporter_display_summary(
787            $child['class'],
788            ['#pass' => 0, '#fail' => 1, '#exception' => 0, '#debug' => 0]
789          );
790          if ($args['die-on-fail']) {
791            $db_prefix = TestDatabase::lastTestGet($child['test_id'])['last_prefix'];
792            $test_db = new TestDatabase($db_prefix);
793            $test_directory = $test_db->getTestSitePath();
794            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";
795            $args['keep-results'] = TRUE;
796            // Exit repeat loop immediately.
797            $args['repeat'] = -1;
798          }
799        }
800        // Free-up space by removing any potentially created resources.
801        if (!$args['keep-results']) {
802          simpletest_script_cleanup($child['test_id'], $child['class'], $status['exitcode']);
803        }
804
805        // Remove this child.
806        unset($children[$cid]);
807      }
808    }
809  }
810  return $total_status;
811}
812
813/**
814 * Run a PHPUnit-based test.
815 */
816function simpletest_script_run_phpunit($test_id, $class) {
817  $reflection = new \ReflectionClass($class);
818  if ($reflection->hasProperty('runLimit')) {
819    set_time_limit($reflection->getStaticPropertyValue('runLimit'));
820  }
821
822  $runner = PhpUnitTestRunner::create(\Drupal::getContainer());
823  $results = $runner->runTests($test_id, [$class], $status);
824  TestDatabase::processPhpUnitResults($results);
825
826  $summaries = $runner->summarizeResults($results);
827  foreach ($summaries as $class => $summary) {
828    simpletest_script_reporter_display_summary($class, $summary);
829  }
830  return $status;
831}
832
833/**
834 * Run a single test, bootstrapping Drupal if needed.
835 */
836function simpletest_script_run_one_test($test_id, $test_class) {
837  global $args;
838
839  try {
840    // Default to status = success. This could mean that we didn't discover any
841    // tests and that none ran.
842    $status = SIMPLETEST_SCRIPT_EXIT_SUCCESS;
843    if (strpos($test_class, '::') > 0) {
844      list($class_name, $method) = explode('::', $test_class, 2);
845      $methods = [$method];
846    }
847    else {
848      $class_name = $test_class;
849      // Use empty array to run all the test methods.
850      $methods = [];
851    }
852    $test = new $class_name($test_id);
853    if ($args['suppress-deprecations']) {
854      putenv('SYMFONY_DEPRECATIONS_HELPER=disabled');
855    }
856    if (is_subclass_of($test_class, TestCase::class)) {
857      $status = simpletest_script_run_phpunit($test_id, $test_class);
858    }
859    // If we aren't running a PHPUnit-based test, then we might have a
860    // Simpletest-based one. Ensure that: 1) The simpletest framework exists,
861    // and 2) that our test belongs to that framework.
862    elseif (class_exists('Drupal\simpletest\TestBase') && is_subclass_of($test_class, 'Drupal\simpletest\TestBase')) {
863      $test->dieOnFail = (bool) $args['die-on-fail'];
864      $test->verbose = (bool) $args['verbose'];
865      $test->run($methods);
866      simpletest_script_reporter_display_summary($test_class, $test->results);
867
868      $status = SIMPLETEST_SCRIPT_EXIT_SUCCESS;
869      // Finished, kill this runner.
870      if ($test->results['#fail'] || $test->results['#exception']) {
871        $status = SIMPLETEST_SCRIPT_EXIT_FAILURE;
872      }
873    }
874    // If the test is not a PHPUnit test, and either we don't have the
875    // Simpletest module or the \Drupal\simpletest\TestBase class available.
876    else {
877      simpletest_script_print_error(sprintf('Can not run %s. If this is a WebTestBase test the simpletest module must be installed. See https://www.drupal.org/node/3030340', $test_class));
878      $status = SIMPLETEST_SCRIPT_EXIT_FAILURE;
879    }
880
881    exit($status);
882  }
883  // DrupalTestCase::run() catches exceptions already, so this is only reached
884  // when an exception is thrown in the wrapping test runner environment.
885  catch (Exception $e) {
886    echo (string) $e;
887    exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
888  }
889}
890
891/**
892 * Return a command used to run a test in a separate process.
893 *
894 * @param int $test_id
895 *   The current test ID.
896 * @param string $test_class
897 *   The name of the test class to run.
898 *
899 * @return string
900 *   The assembled command string.
901 */
902function simpletest_script_command($test_id, $test_class) {
903  global $args, $php;
904
905  $command = escapeshellarg($php) . ' ' . escapeshellarg('./core/scripts/' . $args['script']);
906  $command .= ' --url ' . escapeshellarg($args['url']);
907  if (!empty($args['sqlite'])) {
908    $command .= ' --sqlite ' . escapeshellarg($args['sqlite']);
909  }
910  if (!empty($args['dburl'])) {
911    $command .= ' --dburl ' . escapeshellarg($args['dburl']);
912  }
913  $command .= ' --php ' . escapeshellarg($php);
914  $command .= " --test-id $test_id";
915  foreach (['verbose', 'keep-results', 'color', 'die-on-fail', 'suppress-deprecations'] as $arg) {
916    if ($args[$arg]) {
917      $command .= ' --' . $arg;
918    }
919  }
920  // --execute-test and class name needs to come last.
921  $command .= ' --execute-test ' . escapeshellarg($test_class);
922  return $command;
923}
924
925/**
926 * Removes all remnants of a test runner.
927 *
928 * In case a (e.g., fatal) error occurs after the test site has been fully setup
929 * and the error happens in many tests, the environment that executes the tests
930 * can easily run out of memory or disk space. This function ensures that all
931 * created resources are properly cleaned up after every executed test.
932 *
933 * This clean-up only exists in this script, since SimpleTest module itself does
934 * not use isolated sub-processes for each test being run, so a fatal error
935 * halts not only the test, but also the test runner (i.e., the parent site).
936 *
937 * @param int $test_id
938 *   The test ID of the test run.
939 * @param string $test_class
940 *   The class name of the test run.
941 * @param int $exitcode
942 *   The exit code of the test runner.
943 *
944 * @see simpletest_script_run_one_test()
945 */
946function simpletest_script_cleanup($test_id, $test_class, $exitcode) {
947  if (is_subclass_of($test_class, TestCase::class)) {
948    // PHPUnit test, move on.
949    return;
950  }
951  // Retrieve the last database prefix used for testing.
952  try {
953    $last_test = TestDatabase::lastTestGet($test_id);
954    $db_prefix = $last_test['last_prefix'];
955  }
956  catch (Exception $e) {
957    echo (string) $e;
958    exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
959  }
960
961  // If no database prefix was found, then the test was not set up correctly.
962  if (empty($db_prefix)) {
963    echo "\nFATAL $test_class: Found no database prefix for test ID $test_id. (Check whether setUp() is invoked correctly.)";
964    return;
965  }
966
967  // Do not output verbose cleanup messages in case of a positive exitcode.
968  $output = !empty($exitcode);
969  $messages = [];
970
971  $messages[] = "- Found database prefix '$db_prefix' for test ID $test_id.";
972
973  // Read the log file in case any fatal errors caused the test to crash.
974  try {
975    (new TestDatabase($db_prefix))->logRead($test_id, $last_test['test_class']);
976  }
977  catch (Exception $e) {
978    echo (string) $e;
979    exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
980  }
981
982  // Check whether a test site directory was setup already.
983  // @see \Drupal\simpletest\TestBase::prepareEnvironment()
984  $test_db = new TestDatabase($db_prefix);
985  $test_directory = DRUPAL_ROOT . '/' . $test_db->getTestSitePath();
986  if (is_dir($test_directory)) {
987    // Output the error_log.
988    if (is_file($test_directory . '/error.log')) {
989      if ($errors = file_get_contents($test_directory . '/error.log')) {
990        $output = TRUE;
991        $messages[] = $errors;
992      }
993    }
994    // Delete the test site directory.
995    // simpletest_clean_temporary_directories() cannot be used here, since it
996    // would also delete file directories of other tests that are potentially
997    // running concurrently.
998    try {
999      \Drupal::service('file_system')->deleteRecursive($test_directory, ['\Drupal\Tests\BrowserTestBase', 'filePreDeleteCallback']);
1000      $messages[] = "- Removed test site directory.";
1001    }
1002    catch (FileException $e) {
1003      // Ignore failed deletes.
1004    }
1005  }
1006
1007  // Clear out all database tables from the test.
1008  try {
1009    $schema = Database::getConnection('default', 'default')->schema();
1010    $count = 0;
1011    foreach ($schema->findTables($db_prefix . '%') as $table) {
1012      $schema->dropTable($table);
1013      $count++;
1014    }
1015  }
1016  catch (Exception $e) {
1017    echo (string) $e;
1018    exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
1019  }
1020
1021  if ($count) {
1022    $messages[] = "- Removed $count leftover tables.";
1023  }
1024
1025  if ($output) {
1026    echo implode("\n", $messages);
1027    echo "\n";
1028  }
1029}
1030
1031/**
1032 * Get list of tests based on arguments.
1033 *
1034 * If --all specified then return all available tests, otherwise reads list of
1035 * tests.
1036 *
1037 * @return array
1038 *   List of tests.
1039 */
1040function simpletest_script_get_test_list() {
1041  global $args;
1042
1043  // @todo Use \Drupal\Core\Test\TestDiscovery when we no longer need BC for
1044  //   hook_simpletest_alter().
1045  /** $test_discovery \Drupal\simpletest\TestDiscovery */
1046  $test_discovery = \Drupal::service('test_discovery');
1047  $types_processed = empty($args['types']);
1048  $test_list = [];
1049  if ($args['all'] || $args['module']) {
1050    try {
1051      $groups = $test_discovery->getTestClasses($args['module'], $args['types']);
1052      $types_processed = TRUE;
1053    }
1054    catch (Exception $e) {
1055      echo (string) $e;
1056      exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
1057    }
1058    $all_tests = [];
1059    foreach ($groups as $group => $tests) {
1060      $all_tests = array_merge($all_tests, array_keys($tests));
1061    }
1062    $test_list = array_unique($all_tests);
1063  }
1064  else {
1065    if ($args['class']) {
1066      $test_list = [];
1067      foreach ($args['test_names'] as $test_class) {
1068        list($class_name) = explode('::', $test_class, 2);
1069        if (class_exists($class_name)) {
1070          $test_list[] = $test_class;
1071        }
1072        else {
1073          try {
1074            $groups = $test_discovery->getTestClasses(NULL, $args['types']);
1075          }
1076          catch (Exception $e) {
1077            echo (string) $e;
1078            exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
1079          }
1080          $all_classes = [];
1081          foreach ($groups as $group) {
1082            $all_classes = array_merge($all_classes, array_keys($group));
1083          }
1084          simpletest_script_print_error('Test class not found: ' . $class_name);
1085          simpletest_script_print_alternatives($class_name, $all_classes, 6);
1086          exit(SIMPLETEST_SCRIPT_EXIT_FAILURE);
1087        }
1088      }
1089    }
1090    elseif ($args['file']) {
1091      // Extract test case class names from specified files.
1092      $parser = new TestFileParser();
1093      foreach ($args['test_names'] as $file) {
1094        if (!file_exists($file)) {
1095          simpletest_script_print_error('File not found: ' . $file);
1096          exit(SIMPLETEST_SCRIPT_EXIT_FAILURE);
1097        }
1098        $test_list = array_merge($test_list, $parser->getTestListFromFile($file));
1099      }
1100    }
1101    elseif ($args['directory']) {
1102      // Extract test case class names from specified directory.
1103      // Find all tests in the PSR-X structure; Drupal\$extension\Tests\*.php
1104      // Since we do not want to hard-code too many structural file/directory
1105      // assumptions about PSR-4 files and directories, we check for the
1106      // minimal conditions only; i.e., a '*.php' file that has '/Tests/' in
1107      // its path.
1108      // Ignore anything from third party vendors.
1109      $ignore = ['.', '..', 'vendor'];
1110      $files = [];
1111      if ($args['directory'][0] === '/') {
1112        $directory = $args['directory'];
1113      }
1114      else {
1115        $directory = DRUPAL_ROOT . "/" . $args['directory'];
1116      }
1117      foreach (\Drupal::service('file_system')->scanDirectory($directory, '/\.php$/', $ignore) as $file) {
1118        // '/Tests/' can be contained anywhere in the file's path (there can be
1119        // sub-directories below /Tests), but must be contained literally.
1120        // Case-insensitive to match all Simpletest and PHPUnit tests:
1121        // ./lib/Drupal/foo/Tests/Bar/Baz.php
1122        // ./foo/src/Tests/Bar/Baz.php
1123        // ./foo/tests/Drupal/foo/Tests/FooTest.php
1124        // ./foo/tests/src/FooTest.php
1125        // $file->filename doesn't give us a directory, so we use $file->uri
1126        // Strip the drupal root directory and trailing slash off the URI.
1127        $filename = substr($file->uri, strlen(DRUPAL_ROOT) + 1);
1128        if (stripos($filename, '/Tests/')) {
1129          $files[$filename] = $filename;
1130        }
1131      }
1132      $parser = new TestFileParser();
1133      foreach ($files as $file) {
1134        $test_list = array_merge($test_list, $parser->getTestListFromFile($file));
1135      }
1136    }
1137    else {
1138      try {
1139        $groups = $test_discovery->getTestClasses(NULL, $args['types']);
1140        $types_processed = TRUE;
1141      }
1142      catch (Exception $e) {
1143        echo (string) $e;
1144        exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
1145      }
1146      // Store all the groups so we can suggest alternatives if we need to.
1147      $all_groups = array_keys($groups);
1148      // Verify that the groups exist.
1149      if (!empty($unknown_groups = array_diff($args['test_names'], $all_groups))) {
1150        $first_group = reset($unknown_groups);
1151        simpletest_script_print_error('Test group not found: ' . $first_group);
1152        simpletest_script_print_alternatives($first_group, $all_groups);
1153        exit(SIMPLETEST_SCRIPT_EXIT_FAILURE);
1154      }
1155      // Ensure our list of tests contains only one entry for each test.
1156      foreach ($args['test_names'] as $group_name) {
1157        $test_list = array_merge($test_list, array_flip(array_keys($groups[$group_name])));
1158      }
1159      $test_list = array_flip($test_list);
1160    }
1161  }
1162
1163  // If the test list creation does not automatically limit by test type then
1164  // we need to do so here.
1165  if (!$types_processed) {
1166    $test_list = array_filter($test_list, function ($test_class) use ($args) {
1167      $test_info = TestDiscovery::getTestInfo($test_class);
1168      return in_array($test_info['type'], $args['types'], TRUE);
1169    });
1170  }
1171
1172  if (empty($test_list)) {
1173    simpletest_script_print_error('No valid tests were specified.');
1174    exit(SIMPLETEST_SCRIPT_EXIT_FAILURE);
1175  }
1176  return $test_list;
1177}
1178
1179/**
1180 * Initialize the reporter.
1181 */
1182function simpletest_script_reporter_init() {
1183  global $args, $test_list, $results_map;
1184
1185  $results_map = [
1186    'pass' => 'Pass',
1187    'fail' => 'Fail',
1188    'exception' => 'Exception',
1189  ];
1190
1191  echo "\n";
1192  echo "Drupal test run\n";
1193  echo "---------------\n";
1194  echo "\n";
1195
1196  // Tell the user about what tests are to be run.
1197  if ($args['all']) {
1198    echo "All tests will run.\n\n";
1199  }
1200  else {
1201    echo "Tests to be run:\n";
1202    foreach ($test_list as $class_name) {
1203      echo "  - $class_name\n";
1204    }
1205    echo "\n";
1206  }
1207
1208  echo "Test run started:\n";
1209  echo "  " . date('l, F j, Y - H:i', $_SERVER['REQUEST_TIME']) . "\n";
1210  Timer::start('run-tests');
1211  echo "\n";
1212
1213  echo "Test summary\n";
1214  echo "------------\n";
1215  echo "\n";
1216}
1217
1218/**
1219 * Displays the assertion result summary for a single test class.
1220 *
1221 * @param string $class
1222 *   The test class name that was run.
1223 * @param array $results
1224 *   The assertion results using #pass, #fail, #exception, #debug array keys.
1225 */
1226function simpletest_script_reporter_display_summary($class, $results) {
1227  // Output all test results vertically aligned.
1228  // Cut off the class name after 60 chars, and pad each group with 3 digits
1229  // by default (more than 999 assertions are rare).
1230  $output = vsprintf('%-60.60s %10s %9s %14s %12s', [
1231    $class,
1232    $results['#pass'] . ' passes',
1233    !$results['#fail'] ? '' : $results['#fail'] . ' fails',
1234    !$results['#exception'] ? '' : $results['#exception'] . ' exceptions',
1235    !$results['#debug'] ? '' : $results['#debug'] . ' messages',
1236  ]);
1237
1238  $status = ($results['#fail'] || $results['#exception'] ? 'fail' : 'pass');
1239  simpletest_script_print($output . "\n", simpletest_script_color_code($status));
1240}
1241
1242/**
1243 * Display jUnit XML test results.
1244 */
1245function simpletest_script_reporter_write_xml_results() {
1246  global $args, $test_ids, $results_map;
1247
1248  try {
1249    $results = simpletest_script_load_messages_by_test_id($test_ids);
1250  }
1251  catch (Exception $e) {
1252    echo (string) $e;
1253    exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
1254  }
1255
1256  $test_class = '';
1257  $xml_files = [];
1258
1259  foreach ($results as $result) {
1260    if (isset($results_map[$result->status])) {
1261      if ($result->test_class != $test_class) {
1262        // We've moved onto a new class, so write the last classes results to a
1263        // file:
1264        if (isset($xml_files[$test_class])) {
1265          file_put_contents($args['xml'] . '/' . str_replace('\\', '_', $test_class) . '.xml', $xml_files[$test_class]['doc']->saveXML());
1266          unset($xml_files[$test_class]);
1267        }
1268        $test_class = $result->test_class;
1269        if (!isset($xml_files[$test_class])) {
1270          $doc = new DomDocument('1.0');
1271          $root = $doc->createElement('testsuite');
1272          $root = $doc->appendChild($root);
1273          $xml_files[$test_class] = ['doc' => $doc, 'suite' => $root];
1274        }
1275      }
1276
1277      // For convenience:
1278      $dom_document = &$xml_files[$test_class]['doc'];
1279
1280      // Create the XML element for this test case:
1281      $case = $dom_document->createElement('testcase');
1282      $case->setAttribute('classname', $test_class);
1283      if (strpos($result->function, '->') !== FALSE) {
1284        list($class, $name) = explode('->', $result->function, 2);
1285      }
1286      else {
1287        $name = $result->function;
1288      }
1289      $case->setAttribute('name', $name);
1290
1291      // Passes get no further attention, but failures and exceptions get to add
1292      // more detail:
1293      if ($result->status == 'fail') {
1294        $fail = $dom_document->createElement('failure');
1295        $fail->setAttribute('type', 'failure');
1296        $fail->setAttribute('message', $result->message_group);
1297        $text = $dom_document->createTextNode($result->message);
1298        $fail->appendChild($text);
1299        $case->appendChild($fail);
1300      }
1301      elseif ($result->status == 'exception') {
1302        // In the case of an exception the $result->function may not be a class
1303        // method so we record the full function name:
1304        $case->setAttribute('name', $result->function);
1305
1306        $fail = $dom_document->createElement('error');
1307        $fail->setAttribute('type', 'exception');
1308        $fail->setAttribute('message', $result->message_group);
1309        $full_message = $result->message . "\n\nline: " . $result->line . "\nfile: " . $result->file;
1310        $text = $dom_document->createTextNode($full_message);
1311        $fail->appendChild($text);
1312        $case->appendChild($fail);
1313      }
1314      // Append the test case XML to the test suite:
1315      $xml_files[$test_class]['suite']->appendChild($case);
1316    }
1317  }
1318  // The last test case hasn't been saved to a file yet, so do that now:
1319  if (isset($xml_files[$test_class])) {
1320    file_put_contents($args['xml'] . '/' . str_replace('\\', '_', $test_class) . '.xml', $xml_files[$test_class]['doc']->saveXML());
1321    unset($xml_files[$test_class]);
1322  }
1323}
1324
1325/**
1326 * Stop the test timer.
1327 */
1328function simpletest_script_reporter_timer_stop() {
1329  echo "\n";
1330  $end = Timer::stop('run-tests');
1331  echo "Test run duration: " . \Drupal::service('date.formatter')->formatInterval($end['time'] / 1000);
1332  echo "\n\n";
1333}
1334
1335/**
1336 * Display test results.
1337 */
1338function simpletest_script_reporter_display_results() {
1339  global $args, $test_ids, $results_map;
1340
1341  if ($args['verbose']) {
1342    // Report results.
1343    echo "Detailed test results\n";
1344    echo "---------------------\n";
1345
1346    try {
1347      $results = simpletest_script_load_messages_by_test_id($test_ids);
1348    }
1349    catch (Exception $e) {
1350      echo (string) $e;
1351      exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
1352    }
1353    $test_class = '';
1354    foreach ($results as $result) {
1355      if (isset($results_map[$result->status])) {
1356        if ($result->test_class != $test_class) {
1357          // Display test class every time results are for new test class.
1358          echo "\n\n---- $result->test_class ----\n\n\n";
1359          $test_class = $result->test_class;
1360
1361          // Print table header.
1362          echo "Status    Group      Filename          Line Function                            \n";
1363          echo "--------------------------------------------------------------------------------\n";
1364        }
1365
1366        simpletest_script_format_result($result);
1367      }
1368    }
1369  }
1370}
1371
1372/**
1373 * Format the result so that it fits within 80 characters.
1374 *
1375 * @param object $result
1376 *   The result object to format.
1377 */
1378function simpletest_script_format_result($result) {
1379  global $args, $results_map, $color;
1380
1381  $summary = sprintf("%-9.9s %-10.10s %-17.17s %4.4s %-35.35s\n",
1382    $results_map[$result->status], $result->message_group, basename($result->file), $result->line, $result->function);
1383
1384  simpletest_script_print($summary, simpletest_script_color_code($result->status));
1385
1386  $message = trim(strip_tags($result->message));
1387  if ($args['non-html']) {
1388    $message = Html::decodeEntities($message, ENT_QUOTES, 'UTF-8');
1389  }
1390  $lines = explode("\n", wordwrap($message), 76);
1391  foreach ($lines as $line) {
1392    echo "    $line\n";
1393  }
1394}
1395
1396/**
1397 * Print error messages so the user will notice them.
1398 *
1399 * Print error message prefixed with "  ERROR: " and displayed in fail color if
1400 * color output is enabled.
1401 *
1402 * @param string $message
1403 *   The message to print.
1404 */
1405function simpletest_script_print_error($message) {
1406  simpletest_script_print("  ERROR: $message\n", SIMPLETEST_SCRIPT_COLOR_FAIL);
1407}
1408
1409/**
1410 * Print a message to the console, using a color.
1411 *
1412 * @param string $message
1413 *   The message to print.
1414 * @param int $color_code
1415 *   The color code to use for coloring.
1416 */
1417function simpletest_script_print($message, $color_code) {
1418  global $args;
1419  if ($args['color']) {
1420    echo "\033[" . $color_code . "m" . $message . "\033[0m";
1421  }
1422  else {
1423    echo $message;
1424  }
1425}
1426
1427/**
1428 * Get the color code associated with the specified status.
1429 *
1430 * @param string $status
1431 *   The status string to get code for. Special cases are: 'pass', 'fail', or
1432 *   'exception'.
1433 *
1434 * @return int
1435 *   Color code. Returns 0 for default case.
1436 */
1437function simpletest_script_color_code($status) {
1438  switch ($status) {
1439    case 'pass':
1440      return SIMPLETEST_SCRIPT_COLOR_PASS;
1441
1442    case 'fail':
1443      return SIMPLETEST_SCRIPT_COLOR_FAIL;
1444
1445    case 'exception':
1446      return SIMPLETEST_SCRIPT_COLOR_EXCEPTION;
1447  }
1448  // Default formatting.
1449  return 0;
1450}
1451
1452/**
1453 * Prints alternative test names.
1454 *
1455 * Searches the provided array of string values for close matches based on the
1456 * Levenshtein algorithm.
1457 *
1458 * @param string $string
1459 *   A string to test.
1460 * @param array $array
1461 *   A list of strings to search.
1462 * @param int $degree
1463 *   The matching strictness. Higher values return fewer matches. A value of
1464 *   4 means that the function will return strings from $array if the candidate
1465 *   string in $array would be identical to $string by changing 1/4 or fewer of
1466 *   its characters.
1467 *
1468 * @see http://php.net/manual/function.levenshtein.php
1469 */
1470function simpletest_script_print_alternatives($string, $array, $degree = 4) {
1471  $alternatives = [];
1472  foreach ($array as $item) {
1473    $lev = levenshtein($string, $item);
1474    if ($lev <= strlen($item) / $degree || FALSE !== strpos($string, $item)) {
1475      $alternatives[] = $item;
1476    }
1477  }
1478  if (!empty($alternatives)) {
1479    simpletest_script_print("  Did you mean?\n", SIMPLETEST_SCRIPT_COLOR_FAIL);
1480    foreach ($alternatives as $alternative) {
1481      simpletest_script_print("  - $alternative\n", SIMPLETEST_SCRIPT_COLOR_FAIL);
1482    }
1483  }
1484}
1485
1486/**
1487 * Loads the simpletest messages from the database.
1488 *
1489 * Messages are ordered by test class and message id.
1490 *
1491 * @param array $test_ids
1492 *   Array of test IDs of the messages to be loaded.
1493 *
1494 * @return array
1495 *   Array of simpletest messages from the database.
1496 */
1497function simpletest_script_load_messages_by_test_id($test_ids) {
1498  global $args;
1499  $results = [];
1500
1501  // Sqlite has a maximum number of variables per query. If required, the
1502  // database query is split into chunks.
1503  if (count($test_ids) > SIMPLETEST_SCRIPT_SQLITE_VARIABLE_LIMIT && !empty($args['sqlite'])) {
1504    $test_id_chunks = array_chunk($test_ids, SIMPLETEST_SCRIPT_SQLITE_VARIABLE_LIMIT);
1505  }
1506  else {
1507    $test_id_chunks = [$test_ids];
1508  }
1509
1510  foreach ($test_id_chunks as $test_id_chunk) {
1511    try {
1512      $result_chunk = Database::getConnection('default', 'test-runner')
1513        ->query("SELECT * FROM {simpletest} WHERE test_id IN ( :test_ids[] ) ORDER BY test_class, message_id", [
1514          ':test_ids[]' => $test_id_chunk,
1515        ])->fetchAll();
1516    }
1517    catch (Exception $e) {
1518      echo (string) $e;
1519      exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
1520    }
1521    if ($result_chunk) {
1522      $results = array_merge($results, $result_chunk);
1523    }
1524  }
1525
1526  return $results;
1527}
1528
1529/**
1530 * Display test results.
1531 *
1532 * @deprecated in drupal:8.8.0 and is removed from drupal:9.0.0. This function
1533 *   supports the --browser option in this script. Use the --verbose option
1534 *   instead.
1535 *
1536 * @see https://www.drupal.org/node/3083549
1537 *
1538 * @todo Remove this in https://www.drupal.org/project/drupal/issues/3075490.
1539 */
1540function simpletest_script_open_browser() {
1541  // Note: the user already has received a message about the deprecation in CLI
1542  // so we trigger an error just in case this method has been used as API.
1543  @trigger_error('The --browser option is deprecated in drupal:8.8.0 and is removed from drupal:9.0.0. Use --verbose instead. See https://www.drupal.org/node/3083549', E_USER_DEPRECATED);
1544  if (function_exists('_simpletest_run_tests_script_open_browser')) {
1545    return _simpletest_run_tests_script_open_browser();
1546  }
1547  simpletest_script_print_error('In order to use the --browser option the Simpletest module must be available. See https://www.drupal.org/node/3083549.');
1548}
1549