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