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