comparison core/scripts/run-tests.sh @ 0:c75dbcec494b

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