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