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