5 * This script runs Drupal tests from command line.
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;
23 $autoloader = require_once __DIR__ . '/../../autoload.php';
25 // Define some colors for display.
26 // A nice calming green.
27 const SIMPLETEST_SCRIPT_COLOR_PASS = 32;
29 const SIMPLETEST_SCRIPT_COLOR_FAIL = 31;
31 const SIMPLETEST_SCRIPT_COLOR_EXCEPTION = 33;
33 // Restricting the chunk of queries prevents memory exhaustion.
34 const SIMPLETEST_SCRIPT_SQLITE_VARIABLE_LIMIT = 350;
36 const SIMPLETEST_SCRIPT_EXIT_SUCCESS = 0;
37 const SIMPLETEST_SCRIPT_EXIT_FAILURE = 1;
38 const SIMPLETEST_SCRIPT_EXIT_EXCEPTION = 2;
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);
45 // Set defaults and get overrides.
46 list($args, $count) = simpletest_script_parse_args();
48 if ($args['help'] || $count == 0) {
49 simpletest_script_help();
50 exit(($count == 0) ? SIMPLETEST_SCRIPT_EXIT_FAILURE : SIMPLETEST_SCRIPT_EXIT_SUCCESS);
53 simpletest_script_init();
56 $request = Request::createFromGlobals();
57 $kernel = TestRunnerKernel::createFromRequest($request, $autoloader);
58 $kernel->prepareLegacyRequest($request);
60 catch (Exception $e) {
62 exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
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);
73 // Display all available tests.
74 echo "\nAvailable test groups & classes\n";
75 echo "-------------------------------\n\n";
77 $groups = simpletest_test_get_all($args['module']);
79 catch (Exception $e) {
80 error_log((string) $e);
82 exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
84 foreach ($groups as $group => $tests) {
86 foreach ($tests as $class => $info) {
90 exit(SIMPLETEST_SCRIPT_EXIT_SUCCESS);
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;
100 $test_discovery = \Drupal::service('test_discovery');
101 } catch (Exception $e) {
102 error_log((string) $e);
104 exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
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);
114 // Output the list of files.
116 foreach(array_values($test_classes) as $test_class) {
117 echo $test_class . "\n";
120 exit(SIMPLETEST_SCRIPT_EXIT_SUCCESS);
123 simpletest_script_setup_database(TRUE);
125 if ($args['clean']) {
126 // Clean up left-over tables and directories.
128 simpletest_clean_environment();
130 catch (Exception $e) {
132 exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
134 echo "\nEnvironment cleaned.\n";
136 // Get the status messages and print them.
137 $messages = drupal_get_messages('status');
138 foreach ($messages['status'] as $text) {
139 echo " - " . $text . "\n";
141 exit(SIMPLETEST_SCRIPT_EXIT_SUCCESS);
144 $test_list = simpletest_script_get_test_list();
146 // Try to allocate unlimited time to run the tests.
147 drupal_set_time_limit(0);
148 simpletest_script_reporter_init();
150 $tests_to_run = array();
151 for ($i = 0; $i < $args['repeat']; $i++) {
152 $tests_to_run = array_merge($tests_to_run, $test_list);
156 $status = simpletest_script_execute_batch($tests_to_run);
159 simpletest_script_reporter_timer_stop();
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
165 TestDatabase::releaseAllTestLocks();
167 // Display results before database is cleared.
168 if ($args['browser']) {
169 simpletest_script_open_browser();
172 simpletest_script_reporter_display_results();
176 simpletest_script_reporter_write_xml_results();
179 // Clean up all test results.
180 if (!$args['keep-results']) {
182 simpletest_clean_results_table();
184 catch (Exception $e) {
186 exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
190 // Test complete, exit.
196 function simpletest_script_help() {
201 Run Drupal tests from the shell.
203 Usage: {$args['script']} [OPTIONS] <tests>
204 Example: {$args['script']} Profile
206 All arguments are long options.
208 --help Print this page.
210 --list Display all available test groups.
213 Display all discoverable test file paths.
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
220 --clean Cleans up database tables or directories from previous, failed,
221 tests and then exits (no tests are run).
223 --url The base URL of the root directory of this Drupal checkout; e.g.:
225 Required unless the Drupal root directory maps exactly to:
227 Use a https:// URL to force all tests to be run under SSL.
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
234 Note that ':memory:' cannot be used, because this script spawns
235 sub-processes. However, you may use e.g. '/tmpfs/test.sqlite'
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.
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.
248 mysql://username:password@localhost/databasename#table_prefix
249 sqlite://localhost/relative/path/db.sqlite
250 sqlite://localhost//absolute/path/db.sqlite
252 --php The absolute path to the PHP executable. Usually not needed.
256 Run tests in parallel, up to [num] tests at a time.
258 --all Run all available tests.
260 --module Run all tests belonging to the specified module name.
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'.
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').
273 Runs just tests from the specified test type, for example
275 (i.e. --types "Simpletest,PHPUnit-Functional")
277 --directory Run all tests found within the specified file directory.
281 If provided, test results will be written as xml files to this path.
283 --color Output text format results with color highlighting.
285 --verbose Output detailed assertion messages in addition to summary.
289 Keeps detailed assertion results (in the database) after tests
290 have completed. By default, assertion results are cleared.
292 --repeat Number of times to repeat the test.
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.
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.
305 --non-html Removes escaping from output. Useful for reading results on the
308 --suppress-deprecations
310 Stops tests from failing if deprecation errors are triggered.
312 <test1>[,<test2>[,<test3> ...]]
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.
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:
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"
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
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
344 * Parse execution argument and ensure that all are valid.
347 * The list of arguments.
349 function simpletest_script_parse_args() {
350 // Set default values.
355 'list-files' => FALSE,
356 'list-files-json' => FALSE,
371 'keep-results' => FALSE,
372 'keep-results-table' => FALSE,
373 'test_names' => array(),
375 'die-on-fail' => FALSE,
376 'suppress-deprecations' => FALSE,
380 'execute-test' => '',
385 // Override with set values.
386 $args['script'] = basename(array_shift($_SERVER['argv']));
389 while ($arg = array_shift($_SERVER['argv'])) {
390 if (preg_match('/--(\S+)/', $arg, $matches)) {
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;
398 elseif (is_array($args[$previous_arg])) {
399 $value = array_shift($_SERVER['argv']);
400 $args[$matches[1]] = array_map('trim', explode(',', $value));
403 $args[$matches[1]] = array_shift($_SERVER['argv']);
405 // Clear extraneous values.
406 $args['test_names'] = array();
410 // Argument not found in list.
411 simpletest_script_print_error("Unknown argument '$arg'.");
412 exit(SIMPLETEST_SCRIPT_EXIT_FAILURE);
416 // Values found without an argument should be test names.
417 $args['test_names'] += explode(',', $arg);
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);
428 if ($args['browser']) {
429 $args['keep-results'] = TRUE;
431 return array($args, $count);
435 * Initialize script variables and perform general setup requirements.
437 function simpletest_script_init() {
444 // Determine location of php command automatically, unless a command line
445 // argument is supplied.
446 if (!empty($args['php'])) {
449 elseif ($php_env = getenv('_')) {
450 // '_' is an environment variable set by the shell. It contains the command
451 // that was executed.
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);
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);
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);
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';
481 if (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on') {
482 $base_url = 'https://';
485 $base_url = 'http://';
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';
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);
511 unlink($directory . '/test_symlink');
512 putenv('RUN_TESTS_CONCURRENCY=' . $args['concurrency']);
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]);
522 chdir(realpath(__DIR__ . '/../..'));
526 * Sets up database connection info for running tests.
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).
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
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.
551 function simpletest_script_setup_database($new = FALSE) {
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');
565 $databases['default']['default'] = Database::convertDbUrlToConnectionInfo($args['dburl'], DRUPAL_ROOT);
567 catch (\InvalidArgumentException $e) {
568 simpletest_script_print_error('Invalid --dburl. Reason: ' . $e->getMessage());
569 exit(SIMPLETEST_SCRIPT_EXIT_FAILURE);
572 // Otherwise, use the default database connection from settings.php.
574 $databases['default'] = Database::getConnectionInfo('default');
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);
582 Database::addConnectionInfo('default', 'default', $databases['default']['default']);
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'])) {
589 $databases['test-runner']['default'] = $databases['default']['default'];
591 // Otherwise, set up a SQLite connection for the test runner.
593 if ($args['sqlite'][0] === '/') {
594 $sqlite = $args['sqlite'];
597 $sqlite = DRUPAL_ROOT . '/' . $args['sqlite'];
599 $databases['test-runner']['default'] = array(
600 'driver' => 'sqlite',
601 'database' => $sqlite,
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));
615 // Add the test runner database connection.
616 Database::addConnectionInfo('test-runner', 'default', $databases['test-runner']['default']);
618 // Create the Simpletest schema.
620 $connection = Database::getConnection('default', 'test-runner');
621 $schema = $connection->schema();
623 catch (\PDOException $e) {
624 simpletest_script_print_error($databases['test-runner']['default']['driver'] . ': ' . $e->getMessage());
625 exit(SIMPLETEST_SCRIPT_EXIT_FAILURE);
627 if ($new && $sqlite) {
628 require_once DRUPAL_ROOT . '/' . drupal_get_path('module', 'simpletest') . '/simpletest.install';
629 foreach (simpletest_schema() as $name => $table_spec) {
631 $table_exists = $schema->tableExists($name);
632 if (empty($args['keep-results-table']) && $table_exists) {
633 $connection->truncate($name)->execute();
635 if (!$table_exists) {
636 $schema->createTable($name, $table_spec);
639 catch (Exception $e) {
641 exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
645 // Verify that the Simpletest database schema exists by checking one table.
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);
652 catch (Exception $e) {
654 exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
659 * Execute a batch of tests.
661 function simpletest_script_execute_batch($test_classes) {
662 global $args, $test_ids;
664 $total_status = SIMPLETEST_SCRIPT_EXIT_SUCCESS;
666 // Multi-process execution.
668 while (!empty($test_classes) || !empty($children)) {
669 while (count($children) < $args['concurrency']) {
670 if (empty($test_classes)) {
675 $test_id = Database::getConnection('default', 'test-runner')
676 ->insert('simpletest_test_id')
677 ->useDefaults(array('test_id'))
680 catch (Exception $e) {
682 exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
684 $test_ids[] = $test_id;
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));
691 if (!is_resource($process)) {
692 echo "Unable to fork test process. Aborting.\n";
693 exit(SIMPLETEST_SCRIPT_EXIT_SUCCESS);
696 // Register our new child.
698 'process' => $process,
699 'test_id' => $test_id,
700 'class' => $test_class,
705 // Wait for children every 200ms.
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);
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(
729 ['#pass' => 0, '#fail' => 1, '#exception' => 0, '#debug' => 0]
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;
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']);
746 // Remove this child.
747 unset($children[$cid]);
751 return $total_status;
755 * Run a PHPUnit-based test.
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'));
763 $results = simpletest_run_phpunit_tests($test_id, array($class), $status);
764 simpletest_process_phpunit_results($results);
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);
776 * Run a single test, bootstrapping Drupal if needed.
778 function simpletest_script_run_one_test($test_id, $test_class) {
782 if (strpos($test_class, '::') > 0) {
783 list($class_name, $method) = explode('::', $test_class, 2);
784 $methods = [$method];
787 $class_name = $test_class;
788 // Use empty array to run all the test methods.
791 $test = new $class_name($test_id);
792 if ($args['suppress-deprecations']) {
793 putenv('SYMFONY_DEPRECATIONS_HELPER=disabled');
796 putenv('SYMFONY_DEPRECATIONS_HELPER=strict');
798 if (is_subclass_of($test_class, TestCase::class)) {
799 $status = simpletest_script_run_phpunit($test_id, $test_class);
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);
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;
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) {
820 exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
825 * Return a command used to run a test in a separate process.
827 * @param int $test_id
828 * The current test ID.
829 * @param string $test_class
830 * The name of the test class to run.
833 * The assembled command string.
835 function simpletest_script_command($test_id, $test_class) {
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']);
843 if (!empty($args['dburl'])) {
844 $command .= ' --dburl ' . escapeshellarg($args['dburl']);
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) {
850 $command .= ' --' . $arg;
853 // --execute-test and class name needs to come last.
854 $command .= ' --execute-test ' . escapeshellarg($test_class);
859 * Removes all remnants of a test runner.
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.
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).
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.
877 * @see simpletest_script_run_one_test()
879 function simpletest_script_cleanup($test_id, $test_class, $exitcode) {
880 if (is_subclass_of($test_class, TestCase::class)) {
881 // PHPUnit test, move on.
884 // Retrieve the last database prefix used for testing.
886 list($db_prefix) = simpletest_last_test_get($test_id);
888 catch (Exception $e) {
890 exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
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.)";
899 // Do not output verbose cleanup messages in case of a positive exitcode.
900 $output = !empty($exitcode);
903 $messages[] = "- Found database prefix '$db_prefix' for test ID $test_id.";
905 // Read the log file in case any fatal errors caused the test to crash.
907 simpletest_log_read($test_id, $db_prefix, $test_class);
909 catch (Exception $e) {
911 exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
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')) {
923 $messages[] = $errors;
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.";
934 // Clear out all database tables from the test.
936 $schema = Database::getConnection('default', 'default')->schema();
938 foreach ($schema->findTables($db_prefix . '%') as $table) {
939 $schema->dropTable($table);
943 catch (Exception $e) {
945 exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
949 $messages[] = "- Removed $count leftover tables.";
953 echo implode("\n", $messages);
959 * Get list of tests based on arguments.
961 * If --all specified then return all available tests, otherwise reads list of
967 function simpletest_script_get_test_list() {
970 $types_processed = empty($args['types']);
971 $test_list = array();
972 if ($args['all'] || $args['module']) {
974 $groups = simpletest_test_get_all($args['module'], $args['types']);
975 $types_processed = TRUE;
977 catch (Exception $e) {
979 exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
981 $all_tests = array();
982 foreach ($groups as $group => $tests) {
983 $all_tests = array_merge($all_tests, array_keys($tests));
985 $test_list = $all_tests;
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;
997 $groups = simpletest_test_get_all(NULL, $args['types']);
999 catch (Exception $e) {
1001 exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
1003 $all_classes = array();
1004 foreach ($groups as $group) {
1005 $all_classes = array_merge($all_classes, array_keys($group));
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);
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);
1020 $content = file_get_contents($file);
1021 // Extract a potential namespace.
1023 if (preg_match('@^namespace ([^ ;]+)@m', $content, $matches)) {
1024 $namespace = $matches[1];
1026 // Extract all class names.
1027 // Abstract classes are excluded on purpose.
1028 preg_match_all('@^class ([^ ]+)@m', $content, $matches);
1030 $test_list = array_merge($test_list, $matches[1]);
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;
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
1049 // Ignore anything from third party vendors.
1050 $ignore = array('.', '..', 'vendor');
1052 if ($args['directory'][0] === '/') {
1053 $directory = $args['directory'];
1056 $directory = DRUPAL_ROOT . "/" . $args['directory'];
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;
1073 foreach ($files as $file) {
1074 $content = file_get_contents($file);
1075 // Extract a potential namespace.
1077 if (preg_match('@^\s*namespace ([^ ;]+)@m', $content, $matches)) {
1078 $namespace = $matches[1];
1080 // Extract all class names.
1081 // Abstract classes are excluded on purpose.
1082 preg_match_all('@^\s*class ([^ ]+)@m', $content, $matches);
1084 $test_list = array_merge($test_list, $matches[1]);
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;
1098 $groups = simpletest_test_get_all(NULL, $args['types']);
1099 $types_processed = TRUE;
1101 catch (Exception $e) {
1103 exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
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]));
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);
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);
1127 if (empty($test_list)) {
1128 simpletest_script_print_error('No valid tests were specified.');
1129 exit(SIMPLETEST_SCRIPT_EXIT_FAILURE);
1135 * Initialize the reporter.
1137 function simpletest_script_reporter_init() {
1138 global $args, $test_list, $results_map;
1140 $results_map = array(
1143 'exception' => 'Exception',
1147 echo "Drupal test run\n";
1148 echo "---------------\n";
1151 // Tell the user about what tests are to be run.
1153 echo "All tests will run.\n\n";
1156 echo "Tests to be run:\n";
1157 foreach ($test_list as $class_name) {
1158 echo " - $class_name\n";
1163 echo "Test run started:\n";
1164 echo " " . date('l, F j, Y - H:i', $_SERVER['REQUEST_TIME']) . "\n";
1165 Timer::start('run-tests');
1168 echo "Test summary\n";
1169 echo "------------\n";
1174 * Displays the assertion result summary for a single test class.
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.
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(
1187 $results['#pass'] . ' passes',
1188 !$results['#fail'] ? '' : $results['#fail'] . ' fails',
1189 !$results['#exception'] ? '' : $results['#exception'] . ' exceptions',
1190 !$results['#debug'] ? '' : $results['#debug'] . ' messages',
1193 $status = ($results['#fail'] || $results['#exception'] ? 'fail' : 'pass');
1194 simpletest_script_print($output . "\n", simpletest_script_color_code($status));
1198 * Display jUnit XML test results.
1200 function simpletest_script_reporter_write_xml_results() {
1201 global $args, $test_ids, $results_map;
1204 $results = simpletest_script_load_messages_by_test_id($test_ids);
1206 catch (Exception $e) {
1208 exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
1212 $xml_files = array();
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
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]);
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);
1233 $dom_document = &$xml_files[$test_class]['doc'];
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);
1242 $name = $result->function;
1244 $case->setAttribute('name', $name);
1246 // Passes get no further attention, but failures and exceptions get to add
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);
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);
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);
1269 // Append the test case XML to the test suite:
1270 $xml_files[$test_class]['suite']->appendChild($case);
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]);
1281 * Stop the test timer.
1283 function simpletest_script_reporter_timer_stop() {
1285 $end = Timer::stop('run-tests');
1286 echo "Test run duration: " . \Drupal::service('date.formatter')->formatInterval($end['time'] / 1000);
1291 * Display test results.
1293 function simpletest_script_reporter_display_results() {
1294 global $args, $test_ids, $results_map;
1296 if ($args['verbose']) {
1298 echo "Detailed test results\n";
1299 echo "---------------------\n";
1302 $results = simpletest_script_load_messages_by_test_id($test_ids);
1304 catch (Exception $e) {
1306 exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
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;
1316 // Print table header.
1317 echo "Status Group Filename Line Function \n";
1318 echo "--------------------------------------------------------------------------------\n";
1321 simpletest_script_format_result($result);
1328 * Format the result so that it fits within 80 characters.
1330 * @param object $result
1331 * The result object to format.
1333 function simpletest_script_format_result($result) {
1334 global $args, $results_map, $color;
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);
1339 simpletest_script_print($summary, simpletest_script_color_code($result->status));
1341 $message = trim(strip_tags($result->message));
1342 if ($args['non-html']) {
1343 $message = Html::decodeEntities($message, ENT_QUOTES, 'UTF-8');
1345 $lines = explode("\n", wordwrap($message), 76);
1346 foreach ($lines as $line) {
1352 * Print error messages so the user will notice them.
1354 * Print error message prefixed with " ERROR: " and displayed in fail color if
1355 * color output is enabled.
1357 * @param string $message
1358 * The message to print.
1360 function simpletest_script_print_error($message) {
1361 simpletest_script_print(" ERROR: $message\n", SIMPLETEST_SCRIPT_COLOR_FAIL);
1365 * Print a message to the console, using a color.
1367 * @param string $message
1368 * The message to print.
1369 * @param int $color_code
1370 * The color code to use for coloring.
1372 function simpletest_script_print($message, $color_code) {
1374 if ($args['color']) {
1375 echo "\033[" . $color_code . "m" . $message . "\033[0m";
1383 * Get the color code associated with the specified status.
1385 * @param string $status
1386 * The status string to get code for. Special cases are: 'pass', 'fail', or
1390 * Color code. Returns 0 for default case.
1392 function simpletest_script_color_code($status) {
1395 return SIMPLETEST_SCRIPT_COLOR_PASS;
1398 return SIMPLETEST_SCRIPT_COLOR_FAIL;
1401 return SIMPLETEST_SCRIPT_COLOR_EXCEPTION;
1403 // Default formatting.
1408 * Prints alternative test names.
1410 * Searches the provided array of string values for close matches based on the
1411 * Levenshtein algorithm.
1413 * @param string $string
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
1423 * @see http://php.net/manual/en/function.levenshtein.php
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;
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);
1442 * Loads the simpletest messages from the database.
1444 * Messages are ordered by test class and message id.
1446 * @param array $test_ids
1447 * Array of test IDs of the messages to be loaded.
1450 * Array of simpletest messages from the database.
1452 function simpletest_script_load_messages_by_test_id($test_ids) {
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);
1462 $test_id_chunks = array($test_ids);
1465 foreach ($test_id_chunks as $test_id_chunk) {
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,
1472 catch (Exception $e) {
1474 exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
1476 if ($result_chunk) {
1477 $results = array_merge($results, $result_chunk);
1485 * Display test results.
1487 function simpletest_script_open_browser() {
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')
1500 catch (Exception $e) {
1502 exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
1505 // Get the results form.
1507 SimpletestResultsForm::addResultForm($form, $results);
1509 // Get the assets to make the details element collapsible and theme the result
1511 $assets = new \Drupal\Core\Asset\AttachedAssets();
1512 $assets->setLibraries([
1513 'core/drupal.collapse',
1515 'simpletest/drupal.simpletest',
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));
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>';
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);
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.");
1539 $url = $base_url . '/' . PublicStream::basePath() . '/simpletest/verbose/results-' . $uuid . '.html';
1540 file_put_contents($filename, $html);
1542 // See if we can find an OS helper to open URLs in default browser.
1544 if (shell_exec('which xdg-open')) {
1545 $browser = 'xdg-open';
1547 elseif (shell_exec('which open')) {
1550 elseif (substr(PHP_OS, 0, 3) == 'WIN') {
1555 shell_exec($browser . ' ' . escapeshellarg($url));
1558 // Can't find assets valid browser.
1559 print 'Open file://' . realpath($filename) . ' in your browser to see the verbose output.';