Added Entity and Entity Reference Revisions which got dropped somewhere along the...
[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' 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 = \Drupal::service('test_discovery')->getTestClasses($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::messenger()->messagesByType('status');
129   foreach ($messages 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.0 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. If
314               this is not set the value specified in the
315               SYMFONY_DEPRECATIONS_HELPER environment variable, or the value
316               specified in core/phpunit.xml (if it exists), or the default value
317               will be used. The default is that any unexpected silenced
318               deprecation error will fail tests.
319
320   <test1>[,<test2>[,<test3> ...]]
321
322               One or more tests to be run. By default, these are interpreted
323               as the names of test groups as shown at
324               admin/config/development/testing.
325               These group names typically correspond to module names like "User"
326               or "Profile" or "System", but there is also a group "Database".
327               If --class is specified then these are interpreted as the names of
328               specific test classes whose test methods will be run. Tests must
329               be separated by commas. Ignored if --all is specified.
330
331 To run this script you will normally invoke it from the root directory of your
332 Drupal installation as the webserver user (differs per configuration), or root:
333
334 sudo -u [wwwrun|www-data|etc] php ./core/scripts/{$args['script']}
335   --url http://example.com/ --all
336 sudo -u [wwwrun|www-data|etc] php ./core/scripts/{$args['script']}
337   --url http://example.com/ --class "Drupal\block\Tests\BlockTest"
338
339 Without a preinstalled Drupal site and enabled Simpletest module, specify a
340 SQLite database pathname to create and the default database connection info to
341 use in tests:
342
343 sudo -u [wwwrun|www-data|etc] php ./core/scripts/{$args['script']}
344   --sqlite /tmpfs/drupal/test.sqlite
345   --dburl mysql://username:password@localhost/database
346   --url http://example.com/ --all
347
348 EOF;
349 }
350
351 /**
352  * Parse execution argument and ensure that all are valid.
353  *
354  * @return array
355  *   The list of arguments.
356  */
357 function simpletest_script_parse_args() {
358   // Set default values.
359   $args = [
360     'script' => '',
361     'help' => FALSE,
362     'list' => FALSE,
363     'list-files' => FALSE,
364     'list-files-json' => FALSE,
365     'clean' => FALSE,
366     'url' => '',
367     'sqlite' => NULL,
368     'dburl' => NULL,
369     'php' => '',
370     'concurrency' => 1,
371     'all' => FALSE,
372     'module' => NULL,
373     'class' => FALSE,
374     'file' => FALSE,
375     'types' => [],
376     'directory' => NULL,
377     'color' => FALSE,
378     'verbose' => FALSE,
379     'keep-results' => FALSE,
380     'keep-results-table' => FALSE,
381     'test_names' => [],
382     'repeat' => 1,
383     'die-on-fail' => FALSE,
384     'suppress-deprecations' => FALSE,
385     'browser' => FALSE,
386     // Used internally.
387     'test-id' => 0,
388     'execute-test' => '',
389     'xml' => '',
390     'non-html' => FALSE,
391   ];
392
393   // Override with set values.
394   $args['script'] = basename(array_shift($_SERVER['argv']));
395
396   $count = 0;
397   while ($arg = array_shift($_SERVER['argv'])) {
398     if (preg_match('/--(\S+)/', $arg, $matches)) {
399       // Argument found.
400       if (array_key_exists($matches[1], $args)) {
401         // Argument found in list.
402         $previous_arg = $matches[1];
403         if (is_bool($args[$previous_arg])) {
404           $args[$matches[1]] = TRUE;
405         }
406         elseif (is_array($args[$previous_arg])) {
407           $value = array_shift($_SERVER['argv']);
408           $args[$matches[1]] = array_map('trim', explode(',', $value));
409         }
410         else {
411           $args[$matches[1]] = array_shift($_SERVER['argv']);
412         }
413         // Clear extraneous values.
414         $args['test_names'] = [];
415         $count++;
416       }
417       else {
418         // Argument not found in list.
419         simpletest_script_print_error("Unknown argument '$arg'.");
420         exit(SIMPLETEST_SCRIPT_EXIT_FAILURE);
421       }
422     }
423     else {
424       // Values found without an argument should be test names.
425       $args['test_names'] += explode(',', $arg);
426       $count++;
427     }
428   }
429
430   // Validate the concurrency argument.
431   if (!is_numeric($args['concurrency']) || $args['concurrency'] <= 0) {
432     simpletest_script_print_error("--concurrency must be a strictly positive integer.");
433     exit(SIMPLETEST_SCRIPT_EXIT_FAILURE);
434   }
435
436   if ($args['browser']) {
437     $args['keep-results'] = TRUE;
438   }
439   return [$args, $count];
440 }
441
442 /**
443  * Initialize script variables and perform general setup requirements.
444  */
445 function simpletest_script_init() {
446   global $args, $php;
447
448   $host = 'localhost';
449   $path = '';
450   $port = '80';
451
452   // Determine location of php command automatically, unless a command line
453   // argument is supplied.
454   if (!empty($args['php'])) {
455     $php = $args['php'];
456   }
457   elseif ($php_env = getenv('_')) {
458     // '_' is an environment variable set by the shell. It contains the command
459     // that was executed.
460     $php = $php_env;
461   }
462   elseif ($sudo = getenv('SUDO_COMMAND')) {
463     // 'SUDO_COMMAND' is an environment variable set by the sudo program.
464     // Extract only the PHP interpreter, not the rest of the command.
465     list($php) = explode(' ', $sudo, 2);
466   }
467   else {
468     simpletest_script_print_error('Unable to automatically determine the path to the PHP interpreter. Supply the --php command line argument.');
469     simpletest_script_help();
470     exit(SIMPLETEST_SCRIPT_EXIT_FAILURE);
471   }
472
473   // Detect if we're in the top-level process using the private 'execute-test'
474   // argument. Determine if being run on drupal.org's testing infrastructure
475   // using the presence of 'drupaltestbot' in the database url.
476   // @todo https://www.drupal.org/project/drupalci_testbot/issues/2860941 Use
477   //   better environment variable to detect DrupalCI.
478   // @todo https://www.drupal.org/project/drupal/issues/2942473 Remove when
479   //   dropping PHPUnit 4 and PHP 5 support.
480   if (!$args['execute-test'] && preg_match('/drupalci/', $args['sqlite'])) {
481     // Update PHPUnit if needed and possible. There is a later check once the
482     // autoloader is in place to ensure we're on the correct version. We need to
483     // do this before the autoloader is in place to ensure that it is correct.
484     $composer = ($composer = rtrim('\\' === DIRECTORY_SEPARATOR ? preg_replace('/[\r\n].*/', '', `where.exe composer.phar`) : `which composer.phar`))
485       ? $php . ' ' . escapeshellarg($composer)
486       : 'composer';
487     passthru("$composer run-script drupal-phpunit-upgrade-check");
488   }
489
490   $autoloader = require_once __DIR__ . '/../../autoload.php';
491
492   // Get URL from arguments.
493   if (!empty($args['url'])) {
494     $parsed_url = parse_url($args['url']);
495     $host = $parsed_url['host'] . (isset($parsed_url['port']) ? ':' . $parsed_url['port'] : '');
496     $path = isset($parsed_url['path']) ? rtrim(rtrim($parsed_url['path']), '/') : '';
497     $port = (isset($parsed_url['port']) ? $parsed_url['port'] : $port);
498     if ($path == '/') {
499       $path = '';
500     }
501     // If the passed URL schema is 'https' then setup the $_SERVER variables
502     // properly so that testing will run under HTTPS.
503     if ($parsed_url['scheme'] == 'https') {
504       $_SERVER['HTTPS'] = 'on';
505     }
506   }
507
508   if (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on') {
509     $base_url = 'https://';
510   }
511   else {
512     $base_url = 'http://';
513   }
514   $base_url .= $host;
515   if ($path !== '') {
516     $base_url .= $path;
517   }
518   putenv('SIMPLETEST_BASE_URL=' . $base_url);
519   $_SERVER['HTTP_HOST'] = $host;
520   $_SERVER['REMOTE_ADDR'] = '127.0.0.1';
521   $_SERVER['SERVER_ADDR'] = '127.0.0.1';
522   $_SERVER['SERVER_PORT'] = $port;
523   $_SERVER['SERVER_SOFTWARE'] = NULL;
524   $_SERVER['SERVER_NAME'] = 'localhost';
525   $_SERVER['REQUEST_URI'] = $path . '/';
526   $_SERVER['REQUEST_METHOD'] = 'GET';
527   $_SERVER['SCRIPT_NAME'] = $path . '/index.php';
528   $_SERVER['SCRIPT_FILENAME'] = $path . '/index.php';
529   $_SERVER['PHP_SELF'] = $path . '/index.php';
530   $_SERVER['HTTP_USER_AGENT'] = 'Drupal command line';
531
532   if ($args['concurrency'] > 1) {
533     $directory = FileSystem::getOsTemporaryDirectory();
534     $test_symlink = @symlink(__FILE__, $directory . '/test_symlink');
535     if (!$test_symlink) {
536       throw new \RuntimeException('In order to use a concurrency higher than 1 the test system needs to be able to create symlinks in ' . $directory);
537     }
538     unlink($directory . '/test_symlink');
539     putenv('RUN_TESTS_CONCURRENCY=' . $args['concurrency']);
540   }
541
542   if (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') {
543     // Ensure that any and all environment variables are changed to https://.
544     foreach ($_SERVER as $key => $value) {
545       $_SERVER[$key] = str_replace('http://', 'https://', $_SERVER[$key]);
546     }
547   }
548
549   chdir(realpath(__DIR__ . '/../..'));
550
551   // Prepare the kernel.
552   try {
553     $request = Request::createFromGlobals();
554     $kernel = TestRunnerKernel::createFromRequest($request, $autoloader);
555     $kernel->prepareLegacyRequest($request);
556   }
557   catch (Exception $e) {
558     echo (string) $e;
559     exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
560   }
561 }
562
563 /**
564  * Sets up database connection info for running tests.
565  *
566  * If this script is executed from within a real Drupal installation, then this
567  * function essentially performs nothing (unless the --sqlite or --dburl
568  * parameters were passed).
569  *
570  * Otherwise, there are three database connections of concern:
571  * - --sqlite: The test runner connection, providing access to Simpletest
572  *   database tables for recording test IDs and assertion results.
573  * - --dburl: A database connection that is used as base connection info for all
574  *   tests; i.e., every test will spawn from this connection. In case this
575  *   connection uses e.g. SQLite, then all tests will run against SQLite. This
576  *   is exposed as $databases['default']['default'] to Drupal.
577  * - The actual database connection used within a test. This is the same as
578  *   --dburl, but uses an additional database table prefix. This is
579  *   $databases['default']['default'] within a test environment. The original
580  *   connection is retained in
581  *   $databases['simpletest_original_default']['default'] and restored after
582  *   each test.
583  *
584  * @param bool $new
585  *   Whether this process is a run-tests.sh master process. If TRUE, the SQLite
586  *   database file specified by --sqlite (if any) is set up. Otherwise, database
587  *   connections are prepared only.
588  */
589 function simpletest_script_setup_database($new = FALSE) {
590   global $args;
591
592   // If there is an existing Drupal installation that contains a database
593   // connection info in settings.php, then $databases['default']['default'] will
594   // hold the default database connection already. This connection is assumed to
595   // be valid, and this connection will be used in tests, so that they run
596   // against e.g. MySQL instead of SQLite.
597   // However, in case no Drupal installation exists, this default database
598   // connection can be set and/or overridden with the --dburl parameter.
599   if (!empty($args['dburl'])) {
600     // Remove a possibly existing default connection (from settings.php).
601     Database::removeConnection('default');
602     try {
603       $databases['default']['default'] = Database::convertDbUrlToConnectionInfo($args['dburl'], DRUPAL_ROOT);
604     }
605     catch (\InvalidArgumentException $e) {
606       simpletest_script_print_error('Invalid --dburl. Reason: ' . $e->getMessage());
607       exit(SIMPLETEST_SCRIPT_EXIT_FAILURE);
608     }
609   }
610   // Otherwise, use the default database connection from settings.php.
611   else {
612     $databases['default'] = Database::getConnectionInfo('default');
613   }
614
615   // If there is no default database connection for tests, we cannot continue.
616   if (!isset($databases['default']['default'])) {
617     simpletest_script_print_error('Missing default database connection for tests. Use --dburl to specify one.');
618     exit(SIMPLETEST_SCRIPT_EXIT_FAILURE);
619   }
620   Database::addConnectionInfo('default', 'default', $databases['default']['default']);
621
622   // If no --sqlite parameter has been passed, then Simpletest module is assumed
623   // to be installed, so the test runner database connection is the default
624   // database connection.
625   if (empty($args['sqlite'])) {
626     $sqlite = FALSE;
627     $databases['test-runner']['default'] = $databases['default']['default'];
628   }
629   // Otherwise, set up a SQLite connection for the test runner.
630   else {
631     if ($args['sqlite'][0] === '/') {
632       $sqlite = $args['sqlite'];
633     }
634     else {
635       $sqlite = DRUPAL_ROOT . '/' . $args['sqlite'];
636     }
637     $databases['test-runner']['default'] = [
638       'driver' => 'sqlite',
639       'database' => $sqlite,
640       'prefix' => [
641         'default' => '',
642       ],
643     ];
644     // Create the test runner SQLite database, unless it exists already.
645     if ($new && !file_exists($sqlite)) {
646       if (!is_dir(dirname($sqlite))) {
647         mkdir(dirname($sqlite));
648       }
649       touch($sqlite);
650     }
651   }
652
653   // Add the test runner database connection.
654   Database::addConnectionInfo('test-runner', 'default', $databases['test-runner']['default']);
655
656   // Create the Simpletest schema.
657   try {
658     $connection = Database::getConnection('default', 'test-runner');
659     $schema = $connection->schema();
660   }
661   catch (\PDOException $e) {
662     simpletest_script_print_error($databases['test-runner']['default']['driver'] . ': ' . $e->getMessage());
663     exit(SIMPLETEST_SCRIPT_EXIT_FAILURE);
664   }
665   if ($new && $sqlite) {
666     require_once DRUPAL_ROOT . '/' . drupal_get_path('module', 'simpletest') . '/simpletest.install';
667     foreach (simpletest_schema() as $name => $table_spec) {
668       try {
669         $table_exists = $schema->tableExists($name);
670         if (empty($args['keep-results-table']) && $table_exists) {
671           $connection->truncate($name)->execute();
672         }
673         if (!$table_exists) {
674           $schema->createTable($name, $table_spec);
675         }
676       }
677       catch (Exception $e) {
678         echo (string) $e;
679         exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
680       }
681     }
682   }
683   // Verify that the Simpletest database schema exists by checking one table.
684   try {
685     if (!$schema->tableExists('simpletest')) {
686       simpletest_script_print_error('Missing Simpletest database schema. Either install Simpletest module or use the --sqlite parameter.');
687       exit(SIMPLETEST_SCRIPT_EXIT_FAILURE);
688     }
689   }
690   catch (Exception $e) {
691     echo (string) $e;
692     exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
693   }
694 }
695
696 /**
697  * Execute a batch of tests.
698  */
699 function simpletest_script_execute_batch($test_classes) {
700   global $args, $test_ids;
701
702   $total_status = SIMPLETEST_SCRIPT_EXIT_SUCCESS;
703
704   // Multi-process execution.
705   $children = [];
706   while (!empty($test_classes) || !empty($children)) {
707     while (count($children) < $args['concurrency']) {
708       if (empty($test_classes)) {
709         break;
710       }
711
712       try {
713         $test_id = Database::getConnection('default', 'test-runner')
714           ->insert('simpletest_test_id')
715           ->useDefaults(['test_id'])
716           ->execute();
717       }
718       catch (Exception $e) {
719         echo (string) $e;
720         exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
721       }
722       $test_ids[] = $test_id;
723
724       $test_class = array_shift($test_classes);
725       // Fork a child process.
726       $command = simpletest_script_command($test_id, $test_class);
727       $process = proc_open($command, [], $pipes, NULL, NULL, ['bypass_shell' => TRUE]);
728
729       if (!is_resource($process)) {
730         echo "Unable to fork test process. Aborting.\n";
731         exit(SIMPLETEST_SCRIPT_EXIT_SUCCESS);
732       }
733
734       // Register our new child.
735       $children[] = [
736         'process' => $process,
737         'test_id' => $test_id,
738         'class' => $test_class,
739         'pipes' => $pipes,
740       ];
741     }
742
743     // Wait for children every 200ms.
744     usleep(200000);
745
746     // Check if some children finished.
747     foreach ($children as $cid => $child) {
748       $status = proc_get_status($child['process']);
749       if (empty($status['running'])) {
750         // The child exited, unregister it.
751         proc_close($child['process']);
752         if ($status['exitcode'] === SIMPLETEST_SCRIPT_EXIT_FAILURE) {
753           $total_status = max($status['exitcode'], $total_status);
754         }
755         elseif ($status['exitcode']) {
756           $message = 'FATAL ' . $child['class'] . ': test runner returned a non-zero error code (' . $status['exitcode'] . ').';
757           echo $message . "\n";
758           // @todo Return SIMPLETEST_SCRIPT_EXIT_EXCEPTION instead, when
759           // DrupalCI supports this.
760           // @see https://www.drupal.org/node/2780087
761           $total_status = max(SIMPLETEST_SCRIPT_EXIT_FAILURE, $total_status);
762           // Insert a fail for xml results.
763           TestBase::insertAssert($child['test_id'], $child['class'], FALSE, $message, 'run-tests.sh check');
764           // Ensure that an error line is displayed for the class.
765           simpletest_script_reporter_display_summary(
766             $child['class'],
767             ['#pass' => 0, '#fail' => 1, '#exception' => 0, '#debug' => 0]
768           );
769           if ($args['die-on-fail']) {
770             list($db_prefix) = simpletest_last_test_get($child['test_id']);
771             $test_db = new TestDatabase($db_prefix);
772             $test_directory = $test_db->getTestSitePath();
773             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";
774             $args['keep-results'] = TRUE;
775             // Exit repeat loop immediately.
776             $args['repeat'] = -1;
777           }
778         }
779         // Free-up space by removing any potentially created resources.
780         if (!$args['keep-results']) {
781           simpletest_script_cleanup($child['test_id'], $child['class'], $status['exitcode']);
782         }
783
784         // Remove this child.
785         unset($children[$cid]);
786       }
787     }
788   }
789   return $total_status;
790 }
791
792 /**
793  * Run a PHPUnit-based test.
794  */
795 function simpletest_script_run_phpunit($test_id, $class) {
796   $reflection = new \ReflectionClass($class);
797   if ($reflection->hasProperty('runLimit')) {
798     set_time_limit($reflection->getStaticPropertyValue('runLimit'));
799   }
800
801   $results = simpletest_run_phpunit_tests($test_id, [$class], $status);
802   simpletest_process_phpunit_results($results);
803
804   // Map phpunit results to a data structure we can pass to
805   // _simpletest_format_summary_line.
806   $summaries = simpletest_summarize_phpunit_result($results);
807   foreach ($summaries as $class => $summary) {
808     simpletest_script_reporter_display_summary($class, $summary);
809   }
810   return $status;
811 }
812
813 /**
814  * Run a single test, bootstrapping Drupal if needed.
815  */
816 function simpletest_script_run_one_test($test_id, $test_class) {
817   global $args;
818
819   try {
820     if (strpos($test_class, '::') > 0) {
821       list($class_name, $method) = explode('::', $test_class, 2);
822       $methods = [$method];
823     }
824     else {
825       $class_name = $test_class;
826       // Use empty array to run all the test methods.
827       $methods = [];
828     }
829     $test = new $class_name($test_id);
830     if ($args['suppress-deprecations']) {
831       putenv('SYMFONY_DEPRECATIONS_HELPER=disabled');
832     }
833     if (is_subclass_of($test_class, TestCase::class)) {
834       $status = simpletest_script_run_phpunit($test_id, $test_class);
835     }
836     else {
837       $test->dieOnFail = (bool) $args['die-on-fail'];
838       $test->verbose = (bool) $args['verbose'];
839       $test->run($methods);
840       simpletest_script_reporter_display_summary($test_class, $test->results);
841
842       $status = SIMPLETEST_SCRIPT_EXIT_SUCCESS;
843       // Finished, kill this runner.
844       if ($test->results['#fail'] || $test->results['#exception']) {
845         $status = SIMPLETEST_SCRIPT_EXIT_FAILURE;
846       }
847     }
848
849     exit($status);
850   }
851   // DrupalTestCase::run() catches exceptions already, so this is only reached
852   // when an exception is thrown in the wrapping test runner environment.
853   catch (Exception $e) {
854     echo (string) $e;
855     exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
856   }
857 }
858
859 /**
860  * Return a command used to run a test in a separate process.
861  *
862  * @param int $test_id
863  *   The current test ID.
864  * @param string $test_class
865  *   The name of the test class to run.
866  *
867  * @return string
868  *   The assembled command string.
869  */
870 function simpletest_script_command($test_id, $test_class) {
871   global $args, $php;
872
873   $command = escapeshellarg($php) . ' ' . escapeshellarg('./core/scripts/' . $args['script']);
874   $command .= ' --url ' . escapeshellarg($args['url']);
875   if (!empty($args['sqlite'])) {
876     $command .= ' --sqlite ' . escapeshellarg($args['sqlite']);
877   }
878   if (!empty($args['dburl'])) {
879     $command .= ' --dburl ' . escapeshellarg($args['dburl']);
880   }
881   $command .= ' --php ' . escapeshellarg($php);
882   $command .= " --test-id $test_id";
883   foreach (['verbose', 'keep-results', 'color', 'die-on-fail', 'suppress-deprecations'] as $arg) {
884     if ($args[$arg]) {
885       $command .= ' --' . $arg;
886     }
887   }
888   // --execute-test and class name needs to come last.
889   $command .= ' --execute-test ' . escapeshellarg($test_class);
890   return $command;
891 }
892
893 /**
894  * Removes all remnants of a test runner.
895  *
896  * In case a (e.g., fatal) error occurs after the test site has been fully setup
897  * and the error happens in many tests, the environment that executes the tests
898  * can easily run out of memory or disk space. This function ensures that all
899  * created resources are properly cleaned up after every executed test.
900  *
901  * This clean-up only exists in this script, since SimpleTest module itself does
902  * not use isolated sub-processes for each test being run, so a fatal error
903  * halts not only the test, but also the test runner (i.e., the parent site).
904  *
905  * @param int $test_id
906  *   The test ID of the test run.
907  * @param string $test_class
908  *   The class name of the test run.
909  * @param int $exitcode
910  *   The exit code of the test runner.
911  *
912  * @see simpletest_script_run_one_test()
913  */
914 function simpletest_script_cleanup($test_id, $test_class, $exitcode) {
915   if (is_subclass_of($test_class, TestCase::class)) {
916     // PHPUnit test, move on.
917     return;
918   }
919   // Retrieve the last database prefix used for testing.
920   try {
921     list($db_prefix) = simpletest_last_test_get($test_id);
922   }
923   catch (Exception $e) {
924     echo (string) $e;
925     exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
926   }
927
928   // If no database prefix was found, then the test was not set up correctly.
929   if (empty($db_prefix)) {
930     echo "\nFATAL $test_class: Found no database prefix for test ID $test_id. (Check whether setUp() is invoked correctly.)";
931     return;
932   }
933
934   // Do not output verbose cleanup messages in case of a positive exitcode.
935   $output = !empty($exitcode);
936   $messages = [];
937
938   $messages[] = "- Found database prefix '$db_prefix' for test ID $test_id.";
939
940   // Read the log file in case any fatal errors caused the test to crash.
941   try {
942     simpletest_log_read($test_id, $db_prefix, $test_class);
943   }
944   catch (Exception $e) {
945     echo (string) $e;
946     exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
947   }
948
949   // Check whether a test site directory was setup already.
950   // @see \Drupal\simpletest\TestBase::prepareEnvironment()
951   $test_db = new TestDatabase($db_prefix);
952   $test_directory = DRUPAL_ROOT . '/' . $test_db->getTestSitePath();
953   if (is_dir($test_directory)) {
954     // Output the error_log.
955     if (is_file($test_directory . '/error.log')) {
956       if ($errors = file_get_contents($test_directory . '/error.log')) {
957         $output = TRUE;
958         $messages[] = $errors;
959       }
960     }
961     // Delete the test site directory.
962     // simpletest_clean_temporary_directories() cannot be used here, since it
963     // would also delete file directories of other tests that are potentially
964     // running concurrently.
965     file_unmanaged_delete_recursive($test_directory, ['Drupal\simpletest\TestBase', 'filePreDeleteCallback']);
966     $messages[] = "- Removed test site directory.";
967   }
968
969   // Clear out all database tables from the test.
970   try {
971     $schema = Database::getConnection('default', 'default')->schema();
972     $count = 0;
973     foreach ($schema->findTables($db_prefix . '%') as $table) {
974       $schema->dropTable($table);
975       $count++;
976     }
977   }
978   catch (Exception $e) {
979     echo (string) $e;
980     exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
981   }
982
983   if ($count) {
984     $messages[] = "- Removed $count leftover tables.";
985   }
986
987   if ($output) {
988     echo implode("\n", $messages);
989     echo "\n";
990   }
991 }
992
993 /**
994  * Get list of tests based on arguments.
995  *
996  * If --all specified then return all available tests, otherwise reads list of
997  * tests.
998  *
999  * @return array
1000  *   List of tests.
1001  */
1002 function simpletest_script_get_test_list() {
1003   global $args;
1004
1005   /** $test_discovery \Drupal\simpletest\TestDiscovery */
1006   $test_discovery = \Drupal::service('test_discovery');
1007   $types_processed = empty($args['types']);
1008   $test_list = [];
1009   if ($args['all'] || $args['module']) {
1010     try {
1011       $groups = $test_discovery->getTestClasses($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 = $test_discovery->getTestClasses(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 = $test_discovery->getTestClasses(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 }