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 Symfony\Component\HttpFoundation\Request;
22 $autoloader = require_once __DIR__ . '/../../autoload.php';
24 // Define some colors for display.
25 // A nice calming green.
26 const SIMPLETEST_SCRIPT_COLOR_PASS = 32;
28 const SIMPLETEST_SCRIPT_COLOR_FAIL = 31;
30 const SIMPLETEST_SCRIPT_COLOR_EXCEPTION = 33;
32 // Restricting the chunk of queries prevents memory exhaustion.
33 const SIMPLETEST_SCRIPT_SQLITE_VARIABLE_LIMIT = 350;
35 const SIMPLETEST_SCRIPT_EXIT_SUCCESS = 0;
36 const SIMPLETEST_SCRIPT_EXIT_FAILURE = 1;
37 const SIMPLETEST_SCRIPT_EXIT_EXCEPTION = 2;
39 if (!class_exists('\PHPUnit_Framework_TestCase')) {
40 echo "\nrun-tests.sh requires the PHPUnit testing framework. Please use 'composer install --dev' to ensure that it is present.\n\n";
41 exit(SIMPLETEST_SCRIPT_EXIT_FAILURE);
44 // Set defaults and get overrides.
45 list($args, $count) = simpletest_script_parse_args();
47 if ($args['help'] || $count == 0) {
48 simpletest_script_help();
49 exit(($count == 0) ? SIMPLETEST_SCRIPT_EXIT_FAILURE : SIMPLETEST_SCRIPT_EXIT_SUCCESS);
52 simpletest_script_init();
55 $request = Request::createFromGlobals();
56 $kernel = TestRunnerKernel::createFromRequest($request, $autoloader);
57 $kernel->prepareLegacyRequest($request);
59 catch (Exception $e) {
61 exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
64 if ($args['execute-test']) {
65 simpletest_script_setup_database();
66 simpletest_script_run_one_test($args['test-id'], $args['execute-test']);
67 // Sub-process exited already; this is just for clarity.
68 exit(SIMPLETEST_SCRIPT_EXIT_SUCCESS);
72 // Display all available tests.
73 echo "\nAvailable test groups & classes\n";
74 echo "-------------------------------\n\n";
76 $groups = simpletest_test_get_all($args['module']);
78 catch (Exception $e) {
79 error_log((string) $e);
81 exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
83 foreach ($groups as $group => $tests) {
85 foreach ($tests as $class => $info) {
89 exit(SIMPLETEST_SCRIPT_EXIT_SUCCESS);
92 // List-files and list-files-json provide a way for external tools such as the
93 // testbot to prioritize running changed tests.
94 // @see https://www.drupal.org/node/2569585
95 if ($args['list-files'] || $args['list-files-json']) {
96 // List all files which could be run as tests.
97 $test_discovery = NULL;
99 $test_discovery = \Drupal::service('test_discovery');
100 } catch (Exception $e) {
101 error_log((string) $e);
103 exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
105 // TestDiscovery::findAllClassFiles() gives us a classmap similar to a
106 // Composer 'classmap' array.
107 $test_classes = $test_discovery->findAllClassFiles();
108 // JSON output is the easiest.
109 if ($args['list-files-json']) {
110 echo json_encode($test_classes);
111 exit(SIMPLETEST_SCRIPT_EXIT_SUCCESS);
113 // Output the list of files.
115 foreach(array_values($test_classes) as $test_class) {
116 echo $test_class . "\n";
119 exit(SIMPLETEST_SCRIPT_EXIT_SUCCESS);
122 simpletest_script_setup_database(TRUE);
124 if ($args['clean']) {
125 // Clean up left-over tables and directories.
127 simpletest_clean_environment();
129 catch (Exception $e) {
131 exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
133 echo "\nEnvironment cleaned.\n";
135 // Get the status messages and print them.
136 $messages = drupal_get_messages('status');
137 foreach ($messages['status'] as $text) {
138 echo " - " . $text . "\n";
140 exit(SIMPLETEST_SCRIPT_EXIT_SUCCESS);
143 $test_list = simpletest_script_get_test_list();
145 // Try to allocate unlimited time to run the tests.
146 drupal_set_time_limit(0);
147 simpletest_script_reporter_init();
149 $tests_to_run = array();
150 for ($i = 0; $i < $args['repeat']; $i++) {
151 $tests_to_run = array_merge($tests_to_run, $test_list);
155 $status = simpletest_script_execute_batch($tests_to_run);
158 simpletest_script_reporter_timer_stop();
160 // Ensure all test locks are released once finished. If tests are run with a
161 // concurrency of 1 the each test will clean up its own lock. Test locks are
162 // not released if using a higher concurrency to ensure each test method has
164 TestDatabase::releaseAllTestLocks();
166 // Display results before database is cleared.
167 if ($args['browser']) {
168 simpletest_script_open_browser();
171 simpletest_script_reporter_display_results();
175 simpletest_script_reporter_write_xml_results();
178 // Clean up all test results.
179 if (!$args['keep-results']) {
181 simpletest_clean_results_table();
183 catch (Exception $e) {
185 exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
189 // Test complete, exit.
195 function simpletest_script_help() {
200 Run Drupal tests from the shell.
202 Usage: {$args['script']} [OPTIONS] <tests>
203 Example: {$args['script']} Profile
205 All arguments are long options.
207 --help Print this page.
209 --list Display all available test groups.
212 Display all discoverable test file paths.
215 Display all discoverable test files as JSON. The array key will be
216 the test class name, and the value will be the file path of the
219 --clean Cleans up database tables or directories from previous, failed,
220 tests and then exits (no tests are run).
222 --url The base URL of the root directory of this Drupal checkout; e.g.:
224 Required unless the Drupal root directory maps exactly to:
226 Use a https:// URL to force all tests to be run under SSL.
228 --sqlite A pathname to use for the SQLite database of the test runner.
229 Required unless this script is executed with a working Drupal
230 installation that has Simpletest module installed.
231 A relative pathname is interpreted relative to the Drupal root
233 Note that ':memory:' cannot be used, because this script spawns
234 sub-processes. However, you may use e.g. '/tmpfs/test.sqlite'
238 Boolean flag to indicate to not cleanup the simpletest result
239 table. For testbots or repeated execution of a single test it can
240 be helpful to not cleanup the simpletest result table.
242 --dburl A URI denoting the database driver, credentials, server hostname,
243 and database name to use in tests.
244 Required when running tests without a Drupal installation that
245 contains default database connection info in settings.php.
247 mysql://username:password@localhost/databasename#table_prefix
248 sqlite://localhost/relative/path/db.sqlite
249 sqlite://localhost//absolute/path/db.sqlite
251 --php The absolute path to the PHP executable. Usually not needed.
255 Run tests in parallel, up to [num] tests at a time.
257 --all Run all available tests.
259 --module Run all tests belonging to the specified module name.
262 --class Run tests identified by specific class names, instead of group names.
263 A specific test method can be added, for example,
264 'Drupal\book\Tests\BookTest::testBookExport'.
266 --file Run tests identified by specific file names, instead of group names.
267 Specify the path and the extension
268 (i.e. 'core/modules/user/user.test').
272 Runs just tests from the specified test type, for example
274 (i.e. --types "Simpletest,PHPUnit-Functional")
276 --directory Run all tests found within the specified file directory.
280 If provided, test results will be written as xml files to this path.
282 --color Output text format results with color highlighting.
284 --verbose Output detailed assertion messages in addition to summary.
288 Keeps detailed assertion results (in the database) after tests
289 have completed. By default, assertion results are cleared.
291 --repeat Number of times to repeat the test.
295 Exit test execution immediately upon any failed assertion. This
296 allows to access the test site by changing settings.php to use the
297 test database and configuration directories. Use in combination
298 with --repeat for debugging random test failures.
300 --browser Opens the results in the browser. This enforces --keep-results and
301 if you want to also view any pages rendered in the simpletest
302 browser you need to add --verbose to the command line.
304 --non-html Removes escaping from output. Useful for reading results on the
307 <test1>[,<test2>[,<test3> ...]]
309 One or more tests to be run. By default, these are interpreted
310 as the names of test groups as shown at
311 admin/config/development/testing.
312 These group names typically correspond to module names like "User"
313 or "Profile" or "System", but there is also a group "Database".
314 If --class is specified then these are interpreted as the names of
315 specific test classes whose test methods will be run. Tests must
316 be separated by commas. Ignored if --all is specified.
318 To run this script you will normally invoke it from the root directory of your
319 Drupal installation as the webserver user (differs per configuration), or root:
321 sudo -u [wwwrun|www-data|etc] php ./core/scripts/{$args['script']}
322 --url http://example.com/ --all
323 sudo -u [wwwrun|www-data|etc] php ./core/scripts/{$args['script']}
324 --url http://example.com/ --class "Drupal\block\Tests\BlockTest"
326 Without a preinstalled Drupal site and enabled Simpletest module, specify a
327 SQLite database pathname to create and the default database connection info to
330 sudo -u [wwwrun|www-data|etc] php ./core/scripts/{$args['script']}
331 --sqlite /tmpfs/drupal/test.sqlite
332 --dburl mysql://username:password@localhost/database
333 --url http://example.com/ --all
339 * Parse execution argument and ensure that all are valid.
342 * The list of arguments.
344 function simpletest_script_parse_args() {
345 // Set default values.
350 'list-files' => FALSE,
351 'list-files-json' => FALSE,
366 'keep-results' => FALSE,
367 'keep-results-table' => FALSE,
368 'test_names' => array(),
370 'die-on-fail' => FALSE,
374 'execute-test' => '',
379 // Override with set values.
380 $args['script'] = basename(array_shift($_SERVER['argv']));
383 while ($arg = array_shift($_SERVER['argv'])) {
384 if (preg_match('/--(\S+)/', $arg, $matches)) {
386 if (array_key_exists($matches[1], $args)) {
387 // Argument found in list.
388 $previous_arg = $matches[1];
389 if (is_bool($args[$previous_arg])) {
390 $args[$matches[1]] = TRUE;
392 elseif (is_array($args[$previous_arg])) {
393 $value = array_shift($_SERVER['argv']);
394 $args[$matches[1]] = array_map('trim', explode(',', $value));
397 $args[$matches[1]] = array_shift($_SERVER['argv']);
399 // Clear extraneous values.
400 $args['test_names'] = array();
404 // Argument not found in list.
405 simpletest_script_print_error("Unknown argument '$arg'.");
406 exit(SIMPLETEST_SCRIPT_EXIT_FAILURE);
410 // Values found without an argument should be test names.
411 $args['test_names'] += explode(',', $arg);
416 // Validate the concurrency argument.
417 if (!is_numeric($args['concurrency']) || $args['concurrency'] <= 0) {
418 simpletest_script_print_error("--concurrency must be a strictly positive integer.");
419 exit(SIMPLETEST_SCRIPT_EXIT_FAILURE);
422 if ($args['browser']) {
423 $args['keep-results'] = TRUE;
425 return array($args, $count);
429 * Initialize script variables and perform general setup requirements.
431 function simpletest_script_init() {
438 // Determine location of php command automatically, unless a command line
439 // argument is supplied.
440 if (!empty($args['php'])) {
443 elseif ($php_env = getenv('_')) {
444 // '_' is an environment variable set by the shell. It contains the command
445 // that was executed.
448 elseif ($sudo = getenv('SUDO_COMMAND')) {
449 // 'SUDO_COMMAND' is an environment variable set by the sudo program.
450 // Extract only the PHP interpreter, not the rest of the command.
451 list($php) = explode(' ', $sudo, 2);
454 simpletest_script_print_error('Unable to automatically determine the path to the PHP interpreter. Supply the --php command line argument.');
455 simpletest_script_help();
456 exit(SIMPLETEST_SCRIPT_EXIT_FAILURE);
459 // Get URL from arguments.
460 if (!empty($args['url'])) {
461 $parsed_url = parse_url($args['url']);
462 $host = $parsed_url['host'] . (isset($parsed_url['port']) ? ':' . $parsed_url['port'] : '');
463 $path = isset($parsed_url['path']) ? rtrim(rtrim($parsed_url['path']), '/') : '';
464 $port = (isset($parsed_url['port']) ? $parsed_url['port'] : $port);
468 // If the passed URL schema is 'https' then setup the $_SERVER variables
469 // properly so that testing will run under HTTPS.
470 if ($parsed_url['scheme'] == 'https') {
471 $_SERVER['HTTPS'] = 'on';
475 if (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on') {
476 $base_url = 'https://';
479 $base_url = 'http://';
485 putenv('SIMPLETEST_BASE_URL=' . $base_url);
486 $_SERVER['HTTP_HOST'] = $host;
487 $_SERVER['REMOTE_ADDR'] = '127.0.0.1';
488 $_SERVER['SERVER_ADDR'] = '127.0.0.1';
489 $_SERVER['SERVER_PORT'] = $port;
490 $_SERVER['SERVER_SOFTWARE'] = NULL;
491 $_SERVER['SERVER_NAME'] = 'localhost';
492 $_SERVER['REQUEST_URI'] = $path . '/';
493 $_SERVER['REQUEST_METHOD'] = 'GET';
494 $_SERVER['SCRIPT_NAME'] = $path . '/index.php';
495 $_SERVER['SCRIPT_FILENAME'] = $path . '/index.php';
496 $_SERVER['PHP_SELF'] = $path . '/index.php';
497 $_SERVER['HTTP_USER_AGENT'] = 'Drupal command line';
499 if ($args['concurrency'] > 1) {
500 $directory = FileSystem::getOsTemporaryDirectory();
501 $test_symlink = @symlink(__FILE__, $directory . '/test_symlink');
502 if (!$test_symlink) {
503 throw new \RuntimeException('In order to use a concurrency higher than 1 the test system needs to be able to create symlinks in ' . $directory);
505 unlink($directory . '/test_symlink');
506 putenv('RUN_TESTS_CONCURRENCY=' . $args['concurrency']);
509 if (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') {
510 // Ensure that any and all environment variables are changed to https://.
511 foreach ($_SERVER as $key => $value) {
512 $_SERVER[$key] = str_replace('http://', 'https://', $_SERVER[$key]);
516 chdir(realpath(__DIR__ . '/../..'));
520 * Sets up database connection info for running tests.
522 * If this script is executed from within a real Drupal installation, then this
523 * function essentially performs nothing (unless the --sqlite or --dburl
524 * parameters were passed).
526 * Otherwise, there are three database connections of concern:
527 * - --sqlite: The test runner connection, providing access to Simpletest
528 * database tables for recording test IDs and assertion results.
529 * - --dburl: A database connection that is used as base connection info for all
530 * tests; i.e., every test will spawn from this connection. In case this
531 * connection uses e.g. SQLite, then all tests will run against SQLite. This
532 * is exposed as $databases['default']['default'] to Drupal.
533 * - The actual database connection used within a test. This is the same as
534 * --dburl, but uses an additional database table prefix. This is
535 * $databases['default']['default'] within a test environment. The original
536 * connection is retained in
537 * $databases['simpletest_original_default']['default'] and restored after
541 * Whether this process is a run-tests.sh master process. If TRUE, the SQLite
542 * database file specified by --sqlite (if any) is set up. Otherwise, database
543 * connections are prepared only.
545 function simpletest_script_setup_database($new = FALSE) {
548 // If there is an existing Drupal installation that contains a database
549 // connection info in settings.php, then $databases['default']['default'] will
550 // hold the default database connection already. This connection is assumed to
551 // be valid, and this connection will be used in tests, so that they run
552 // against e.g. MySQL instead of SQLite.
553 // However, in case no Drupal installation exists, this default database
554 // connection can be set and/or overridden with the --dburl parameter.
555 if (!empty($args['dburl'])) {
556 // Remove a possibly existing default connection (from settings.php).
557 Database::removeConnection('default');
559 $databases['default']['default'] = Database::convertDbUrlToConnectionInfo($args['dburl'], DRUPAL_ROOT);
561 catch (\InvalidArgumentException $e) {
562 simpletest_script_print_error('Invalid --dburl. Reason: ' . $e->getMessage());
563 exit(SIMPLETEST_SCRIPT_EXIT_FAILURE);
566 // Otherwise, use the default database connection from settings.php.
568 $databases['default'] = Database::getConnectionInfo('default');
571 // If there is no default database connection for tests, we cannot continue.
572 if (!isset($databases['default']['default'])) {
573 simpletest_script_print_error('Missing default database connection for tests. Use --dburl to specify one.');
574 exit(SIMPLETEST_SCRIPT_EXIT_FAILURE);
576 Database::addConnectionInfo('default', 'default', $databases['default']['default']);
578 // If no --sqlite parameter has been passed, then Simpletest module is assumed
579 // to be installed, so the test runner database connection is the default
580 // database connection.
581 if (empty($args['sqlite'])) {
583 $databases['test-runner']['default'] = $databases['default']['default'];
585 // Otherwise, set up a SQLite connection for the test runner.
587 if ($args['sqlite'][0] === '/') {
588 $sqlite = $args['sqlite'];
591 $sqlite = DRUPAL_ROOT . '/' . $args['sqlite'];
593 $databases['test-runner']['default'] = array(
594 'driver' => 'sqlite',
595 'database' => $sqlite,
600 // Create the test runner SQLite database, unless it exists already.
601 if ($new && !file_exists($sqlite)) {
602 if (!is_dir(dirname($sqlite))) {
603 mkdir(dirname($sqlite));
609 // Add the test runner database connection.
610 Database::addConnectionInfo('test-runner', 'default', $databases['test-runner']['default']);
612 // Create the Simpletest schema.
614 $connection = Database::getConnection('default', 'test-runner');
615 $schema = $connection->schema();
617 catch (\PDOException $e) {
618 simpletest_script_print_error($databases['test-runner']['default']['driver'] . ': ' . $e->getMessage());
619 exit(SIMPLETEST_SCRIPT_EXIT_FAILURE);
621 if ($new && $sqlite) {
622 require_once DRUPAL_ROOT . '/' . drupal_get_path('module', 'simpletest') . '/simpletest.install';
623 foreach (simpletest_schema() as $name => $table_spec) {
625 $table_exists = $schema->tableExists($name);
626 if (empty($args['keep-results-table']) && $table_exists) {
627 $connection->truncate($name)->execute();
629 if (!$table_exists) {
630 $schema->createTable($name, $table_spec);
633 catch (Exception $e) {
635 exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
639 // Verify that the Simpletest database schema exists by checking one table.
641 if (!$schema->tableExists('simpletest')) {
642 simpletest_script_print_error('Missing Simpletest database schema. Either install Simpletest module or use the --sqlite parameter.');
643 exit(SIMPLETEST_SCRIPT_EXIT_FAILURE);
646 catch (Exception $e) {
648 exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
653 * Execute a batch of tests.
655 function simpletest_script_execute_batch($test_classes) {
656 global $args, $test_ids;
658 $total_status = SIMPLETEST_SCRIPT_EXIT_SUCCESS;
660 // Multi-process execution.
662 while (!empty($test_classes) || !empty($children)) {
663 while (count($children) < $args['concurrency']) {
664 if (empty($test_classes)) {
669 $test_id = Database::getConnection('default', 'test-runner')
670 ->insert('simpletest_test_id')
671 ->useDefaults(array('test_id'))
674 catch (Exception $e) {
676 exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
678 $test_ids[] = $test_id;
680 $test_class = array_shift($test_classes);
681 // Fork a child process.
682 $command = simpletest_script_command($test_id, $test_class);
683 $process = proc_open($command, array(), $pipes, NULL, NULL, array('bypass_shell' => TRUE));
685 if (!is_resource($process)) {
686 echo "Unable to fork test process. Aborting.\n";
687 exit(SIMPLETEST_SCRIPT_EXIT_SUCCESS);
690 // Register our new child.
692 'process' => $process,
693 'test_id' => $test_id,
694 'class' => $test_class,
699 // Wait for children every 200ms.
702 // Check if some children finished.
703 foreach ($children as $cid => $child) {
704 $status = proc_get_status($child['process']);
705 if (empty($status['running'])) {
706 // The child exited, unregister it.
707 proc_close($child['process']);
708 if ($status['exitcode'] === SIMPLETEST_SCRIPT_EXIT_FAILURE) {
709 $total_status = max($status['exitcode'], $total_status);
711 elseif ($status['exitcode']) {
712 $message = 'FATAL ' . $child['class'] . ': test runner returned a non-zero error code (' . $status['exitcode'] . ').';
713 echo $message . "\n";
714 // @todo Return SIMPLETEST_SCRIPT_EXIT_EXCEPTION instead, when
715 // DrupalCI supports this.
716 // @see https://www.drupal.org/node/2780087
717 $total_status = max(SIMPLETEST_SCRIPT_EXIT_FAILURE, $total_status);
718 // Insert a fail for xml results.
719 TestBase::insertAssert($child['test_id'], $child['class'], FALSE, $message, 'run-tests.sh check');
720 // Ensure that an error line is displayed for the class.
721 simpletest_script_reporter_display_summary(
723 ['#pass' => 0, '#fail' => 1, '#exception' => 0, '#debug' => 0]
725 if ($args['die-on-fail']) {
726 list($db_prefix) = simpletest_last_test_get($child['test_id']);
727 $test_db = new TestDatabase($db_prefix);
728 $test_directory = $test_db->getTestSitePath();
729 echo 'Simpletest database and files kept and test exited immediately on fail so should be reproducible if you change settings.php to use the database prefix ' . $db_prefix . ' and config directories in ' . $test_directory . "\n";
730 $args['keep-results'] = TRUE;
731 // Exit repeat loop immediately.
732 $args['repeat'] = -1;
735 // Free-up space by removing any potentially created resources.
736 if (!$args['keep-results']) {
737 simpletest_script_cleanup($child['test_id'], $child['class'], $status['exitcode']);
740 // Remove this child.
741 unset($children[$cid]);
745 return $total_status;
749 * Run a PHPUnit-based test.
751 function simpletest_script_run_phpunit($test_id, $class) {
752 $reflection = new \ReflectionClass($class);
753 if ($reflection->hasProperty('runLimit')) {
754 set_time_limit($reflection->getStaticPropertyValue('runLimit'));
757 $results = simpletest_run_phpunit_tests($test_id, array($class), $status);
758 simpletest_process_phpunit_results($results);
760 // Map phpunit results to a data structure we can pass to
761 // _simpletest_format_summary_line.
762 $summaries = simpletest_summarize_phpunit_result($results);
763 foreach ($summaries as $class => $summary) {
764 simpletest_script_reporter_display_summary($class, $summary);
770 * Run a single test, bootstrapping Drupal if needed.
772 function simpletest_script_run_one_test($test_id, $test_class) {
776 if (strpos($test_class, '::') > 0) {
777 list($class_name, $method) = explode('::', $test_class, 2);
778 $methods = [$method];
781 $class_name = $test_class;
782 // Use empty array to run all the test methods.
785 $test = new $class_name($test_id);
786 if (is_subclass_of($test_class, '\PHPUnit_Framework_TestCase')) {
787 $status = simpletest_script_run_phpunit($test_id, $test_class);
790 $test->dieOnFail = (bool) $args['die-on-fail'];
791 $test->verbose = (bool) $args['verbose'];
792 $test->run($methods);
793 simpletest_script_reporter_display_summary($test_class, $test->results);
795 $status = SIMPLETEST_SCRIPT_EXIT_SUCCESS;
796 // Finished, kill this runner.
797 if ($test->results['#fail'] || $test->results['#exception']) {
798 $status = SIMPLETEST_SCRIPT_EXIT_FAILURE;
804 // DrupalTestCase::run() catches exceptions already, so this is only reached
805 // when an exception is thrown in the wrapping test runner environment.
806 catch (Exception $e) {
808 exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
813 * Return a command used to run a test in a separate process.
815 * @param int $test_id
816 * The current test ID.
817 * @param string $test_class
818 * The name of the test class to run.
821 * The assembled command string.
823 function simpletest_script_command($test_id, $test_class) {
826 $command = escapeshellarg($php) . ' ' . escapeshellarg('./core/scripts/' . $args['script']);
827 $command .= ' --url ' . escapeshellarg($args['url']);
828 if (!empty($args['sqlite'])) {
829 $command .= ' --sqlite ' . escapeshellarg($args['sqlite']);
831 if (!empty($args['dburl'])) {
832 $command .= ' --dburl ' . escapeshellarg($args['dburl']);
834 $command .= ' --php ' . escapeshellarg($php);
835 $command .= " --test-id $test_id";
836 foreach (array('verbose', 'keep-results', 'color', 'die-on-fail') as $arg) {
838 $command .= ' --' . $arg;
841 // --execute-test and class name needs to come last.
842 $command .= ' --execute-test ' . escapeshellarg($test_class);
847 * Removes all remnants of a test runner.
849 * In case a (e.g., fatal) error occurs after the test site has been fully setup
850 * and the error happens in many tests, the environment that executes the tests
851 * can easily run out of memory or disk space. This function ensures that all
852 * created resources are properly cleaned up after every executed test.
854 * This clean-up only exists in this script, since SimpleTest module itself does
855 * not use isolated sub-processes for each test being run, so a fatal error
856 * halts not only the test, but also the test runner (i.e., the parent site).
858 * @param int $test_id
859 * The test ID of the test run.
860 * @param string $test_class
861 * The class name of the test run.
862 * @param int $exitcode
863 * The exit code of the test runner.
865 * @see simpletest_script_run_one_test()
867 function simpletest_script_cleanup($test_id, $test_class, $exitcode) {
868 if (is_subclass_of($test_class, '\PHPUnit_Framework_TestCase')) {
869 // PHPUnit test, move on.
872 // Retrieve the last database prefix used for testing.
874 list($db_prefix) = simpletest_last_test_get($test_id);
876 catch (Exception $e) {
878 exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
881 // If no database prefix was found, then the test was not set up correctly.
882 if (empty($db_prefix)) {
883 echo "\nFATAL $test_class: Found no database prefix for test ID $test_id. (Check whether setUp() is invoked correctly.)";
887 // Do not output verbose cleanup messages in case of a positive exitcode.
888 $output = !empty($exitcode);
891 $messages[] = "- Found database prefix '$db_prefix' for test ID $test_id.";
893 // Read the log file in case any fatal errors caused the test to crash.
895 simpletest_log_read($test_id, $db_prefix, $test_class);
897 catch (Exception $e) {
899 exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
902 // Check whether a test site directory was setup already.
903 // @see \Drupal\simpletest\TestBase::prepareEnvironment()
904 $test_db = new TestDatabase($db_prefix);
905 $test_directory = DRUPAL_ROOT . '/' . $test_db->getTestSitePath();
906 if (is_dir($test_directory)) {
907 // Output the error_log.
908 if (is_file($test_directory . '/error.log')) {
909 if ($errors = file_get_contents($test_directory . '/error.log')) {
911 $messages[] = $errors;
914 // Delete the test site directory.
915 // simpletest_clean_temporary_directories() cannot be used here, since it
916 // would also delete file directories of other tests that are potentially
917 // running concurrently.
918 file_unmanaged_delete_recursive($test_directory, array('Drupal\simpletest\TestBase', 'filePreDeleteCallback'));
919 $messages[] = "- Removed test site directory.";
922 // Clear out all database tables from the test.
924 $schema = Database::getConnection('default', 'default')->schema();
926 foreach ($schema->findTables($db_prefix . '%') as $table) {
927 $schema->dropTable($table);
931 catch (Exception $e) {
933 exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
937 $messages[] = "- Removed $count leftover tables.";
941 echo implode("\n", $messages);
947 * Get list of tests based on arguments.
949 * If --all specified then return all available tests, otherwise reads list of
955 function simpletest_script_get_test_list() {
958 $types_processed = empty($args['types']);
959 $test_list = array();
960 if ($args['all'] || $args['module']) {
962 $groups = simpletest_test_get_all($args['module'], $args['types']);
963 $types_processed = TRUE;
965 catch (Exception $e) {
967 exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
969 $all_tests = array();
970 foreach ($groups as $group => $tests) {
971 $all_tests = array_merge($all_tests, array_keys($tests));
973 $test_list = $all_tests;
976 if ($args['class']) {
977 $test_list = array();
978 foreach ($args['test_names'] as $test_class) {
979 list($class_name) = explode('::', $test_class, 2);
980 if (class_exists($class_name)) {
981 $test_list[] = $test_class;
985 $groups = simpletest_test_get_all(NULL, $args['types']);
987 catch (Exception $e) {
989 exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
991 $all_classes = array();
992 foreach ($groups as $group) {
993 $all_classes = array_merge($all_classes, array_keys($group));
995 simpletest_script_print_error('Test class not found: ' . $class_name);
996 simpletest_script_print_alternatives($class_name, $all_classes, 6);
997 exit(SIMPLETEST_SCRIPT_EXIT_FAILURE);
1001 elseif ($args['file']) {
1002 // Extract test case class names from specified files.
1003 foreach ($args['test_names'] as $file) {
1004 if (!file_exists($file)) {
1005 simpletest_script_print_error('File not found: ' . $file);
1006 exit(SIMPLETEST_SCRIPT_EXIT_FAILURE);
1008 $content = file_get_contents($file);
1009 // Extract a potential namespace.
1011 if (preg_match('@^namespace ([^ ;]+)@m', $content, $matches)) {
1012 $namespace = $matches[1];
1014 // Extract all class names.
1015 // Abstract classes are excluded on purpose.
1016 preg_match_all('@^class ([^ ]+)@m', $content, $matches);
1018 $test_list = array_merge($test_list, $matches[1]);
1021 foreach ($matches[1] as $class_name) {
1022 $namespace_class = $namespace . '\\' . $class_name;
1023 if (is_subclass_of($namespace_class, '\Drupal\simpletest\TestBase') || is_subclass_of($namespace_class, '\PHPUnit_Framework_TestCase')) {
1024 $test_list[] = $namespace_class;
1030 elseif ($args['directory']) {
1031 // Extract test case class names from specified directory.
1032 // Find all tests in the PSR-X structure; Drupal\$extension\Tests\*.php
1033 // Since we do not want to hard-code too many structural file/directory
1034 // assumptions about PSR-0/4 files and directories, we check for the
1035 // minimal conditions only; i.e., a '*.php' file that has '/Tests/' in
1037 // Ignore anything from third party vendors.
1038 $ignore = array('.', '..', 'vendor');
1040 if ($args['directory'][0] === '/') {
1041 $directory = $args['directory'];
1044 $directory = DRUPAL_ROOT . "/" . $args['directory'];
1046 foreach (file_scan_directory($directory, '/\.php$/', $ignore) as $file) {
1047 // '/Tests/' can be contained anywhere in the file's path (there can be
1048 // sub-directories below /Tests), but must be contained literally.
1049 // Case-insensitive to match all Simpletest and PHPUnit tests:
1050 // ./lib/Drupal/foo/Tests/Bar/Baz.php
1051 // ./foo/src/Tests/Bar/Baz.php
1052 // ./foo/tests/Drupal/foo/Tests/FooTest.php
1053 // ./foo/tests/src/FooTest.php
1054 // $file->filename doesn't give us a directory, so we use $file->uri
1055 // Strip the drupal root directory and trailing slash off the URI.
1056 $filename = substr($file->uri, strlen(DRUPAL_ROOT) + 1);
1057 if (stripos($filename, '/Tests/')) {
1058 $files[$filename] = $filename;
1061 foreach ($files as $file) {
1062 $content = file_get_contents($file);
1063 // Extract a potential namespace.
1065 if (preg_match('@^\s*namespace ([^ ;]+)@m', $content, $matches)) {
1066 $namespace = $matches[1];
1068 // Extract all class names.
1069 // Abstract classes are excluded on purpose.
1070 preg_match_all('@^\s*class ([^ ]+)@m', $content, $matches);
1072 $test_list = array_merge($test_list, $matches[1]);
1075 foreach ($matches[1] as $class_name) {
1076 $namespace_class = $namespace . '\\' . $class_name;
1077 if (is_subclass_of($namespace_class, '\Drupal\simpletest\TestBase') || is_subclass_of($namespace_class, '\PHPUnit_Framework_TestCase')) {
1078 $test_list[] = $namespace_class;
1086 $groups = simpletest_test_get_all(NULL, $args['types']);
1087 $types_processed = TRUE;
1089 catch (Exception $e) {
1091 exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
1093 foreach ($args['test_names'] as $group_name) {
1094 if (isset($groups[$group_name])) {
1095 $test_list = array_merge($test_list, array_keys($groups[$group_name]));
1098 simpletest_script_print_error('Test group not found: ' . $group_name);
1099 simpletest_script_print_alternatives($group_name, array_keys($groups));
1100 exit(SIMPLETEST_SCRIPT_EXIT_FAILURE);
1106 // If the test list creation does not automatically limit by test type then
1107 // we need to do so here.
1108 if (!$types_processed) {
1109 $test_list = array_filter($test_list, function ($test_class) use ($args) {
1110 $test_info = TestDiscovery::getTestInfo($test_class);
1111 return in_array($test_info['type'], $args['types'], TRUE);
1115 if (empty($test_list)) {
1116 simpletest_script_print_error('No valid tests were specified.');
1117 exit(SIMPLETEST_SCRIPT_EXIT_FAILURE);
1123 * Initialize the reporter.
1125 function simpletest_script_reporter_init() {
1126 global $args, $test_list, $results_map;
1128 $results_map = array(
1131 'exception' => 'Exception',
1135 echo "Drupal test run\n";
1136 echo "---------------\n";
1139 // Tell the user about what tests are to be run.
1141 echo "All tests will run.\n\n";
1144 echo "Tests to be run:\n";
1145 foreach ($test_list as $class_name) {
1146 echo " - $class_name\n";
1151 echo "Test run started:\n";
1152 echo " " . date('l, F j, Y - H:i', $_SERVER['REQUEST_TIME']) . "\n";
1153 Timer::start('run-tests');
1156 echo "Test summary\n";
1157 echo "------------\n";
1162 * Displays the assertion result summary for a single test class.
1164 * @param string $class
1165 * The test class name that was run.
1166 * @param array $results
1167 * The assertion results using #pass, #fail, #exception, #debug array keys.
1169 function simpletest_script_reporter_display_summary($class, $results) {
1170 // Output all test results vertically aligned.
1171 // Cut off the class name after 60 chars, and pad each group with 3 digits
1172 // by default (more than 999 assertions are rare).
1173 $output = vsprintf('%-60.60s %10s %9s %14s %12s', array(
1175 $results['#pass'] . ' passes',
1176 !$results['#fail'] ? '' : $results['#fail'] . ' fails',
1177 !$results['#exception'] ? '' : $results['#exception'] . ' exceptions',
1178 !$results['#debug'] ? '' : $results['#debug'] . ' messages',
1181 $status = ($results['#fail'] || $results['#exception'] ? 'fail' : 'pass');
1182 simpletest_script_print($output . "\n", simpletest_script_color_code($status));
1186 * Display jUnit XML test results.
1188 function simpletest_script_reporter_write_xml_results() {
1189 global $args, $test_ids, $results_map;
1192 $results = simpletest_script_load_messages_by_test_id($test_ids);
1194 catch (Exception $e) {
1196 exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
1200 $xml_files = array();
1202 foreach ($results as $result) {
1203 if (isset($results_map[$result->status])) {
1204 if ($result->test_class != $test_class) {
1205 // We've moved onto a new class, so write the last classes results to a
1207 if (isset($xml_files[$test_class])) {
1208 file_put_contents($args['xml'] . '/' . str_replace('\\', '_', $test_class) . '.xml', $xml_files[$test_class]['doc']->saveXML());
1209 unset($xml_files[$test_class]);
1211 $test_class = $result->test_class;
1212 if (!isset($xml_files[$test_class])) {
1213 $doc = new DomDocument('1.0');
1214 $root = $doc->createElement('testsuite');
1215 $root = $doc->appendChild($root);
1216 $xml_files[$test_class] = array('doc' => $doc, 'suite' => $root);
1221 $dom_document = &$xml_files[$test_class]['doc'];
1223 // Create the XML element for this test case:
1224 $case = $dom_document->createElement('testcase');
1225 $case->setAttribute('classname', $test_class);
1226 if (strpos($result->function, '->') !== FALSE) {
1227 list($class, $name) = explode('->', $result->function, 2);
1230 $name = $result->function;
1232 $case->setAttribute('name', $name);
1234 // Passes get no further attention, but failures and exceptions get to add
1236 if ($result->status == 'fail') {
1237 $fail = $dom_document->createElement('failure');
1238 $fail->setAttribute('type', 'failure');
1239 $fail->setAttribute('message', $result->message_group);
1240 $text = $dom_document->createTextNode($result->message);
1241 $fail->appendChild($text);
1242 $case->appendChild($fail);
1244 elseif ($result->status == 'exception') {
1245 // In the case of an exception the $result->function may not be a class
1246 // method so we record the full function name:
1247 $case->setAttribute('name', $result->function);
1249 $fail = $dom_document->createElement('error');
1250 $fail->setAttribute('type', 'exception');
1251 $fail->setAttribute('message', $result->message_group);
1252 $full_message = $result->message . "\n\nline: " . $result->line . "\nfile: " . $result->file;
1253 $text = $dom_document->createTextNode($full_message);
1254 $fail->appendChild($text);
1255 $case->appendChild($fail);
1257 // Append the test case XML to the test suite:
1258 $xml_files[$test_class]['suite']->appendChild($case);
1261 // The last test case hasn't been saved to a file yet, so do that now:
1262 if (isset($xml_files[$test_class])) {
1263 file_put_contents($args['xml'] . '/' . str_replace('\\', '_', $test_class) . '.xml', $xml_files[$test_class]['doc']->saveXML());
1264 unset($xml_files[$test_class]);
1269 * Stop the test timer.
1271 function simpletest_script_reporter_timer_stop() {
1273 $end = Timer::stop('run-tests');
1274 echo "Test run duration: " . \Drupal::service('date.formatter')->formatInterval($end['time'] / 1000);
1279 * Display test results.
1281 function simpletest_script_reporter_display_results() {
1282 global $args, $test_ids, $results_map;
1284 if ($args['verbose']) {
1286 echo "Detailed test results\n";
1287 echo "---------------------\n";
1290 $results = simpletest_script_load_messages_by_test_id($test_ids);
1292 catch (Exception $e) {
1294 exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
1297 foreach ($results as $result) {
1298 if (isset($results_map[$result->status])) {
1299 if ($result->test_class != $test_class) {
1300 // Display test class every time results are for new test class.
1301 echo "\n\n---- $result->test_class ----\n\n\n";
1302 $test_class = $result->test_class;
1304 // Print table header.
1305 echo "Status Group Filename Line Function \n";
1306 echo "--------------------------------------------------------------------------------\n";
1309 simpletest_script_format_result($result);
1316 * Format the result so that it fits within 80 characters.
1318 * @param object $result
1319 * The result object to format.
1321 function simpletest_script_format_result($result) {
1322 global $args, $results_map, $color;
1324 $summary = sprintf("%-9.9s %-10.10s %-17.17s %4.4s %-35.35s\n",
1325 $results_map[$result->status], $result->message_group, basename($result->file), $result->line, $result->function);
1327 simpletest_script_print($summary, simpletest_script_color_code($result->status));
1329 $message = trim(strip_tags($result->message));
1330 if ($args['non-html']) {
1331 $message = Html::decodeEntities($message, ENT_QUOTES, 'UTF-8');
1333 $lines = explode("\n", wordwrap($message), 76);
1334 foreach ($lines as $line) {
1340 * Print error messages so the user will notice them.
1342 * Print error message prefixed with " ERROR: " and displayed in fail color if
1343 * color output is enabled.
1345 * @param string $message
1346 * The message to print.
1348 function simpletest_script_print_error($message) {
1349 simpletest_script_print(" ERROR: $message\n", SIMPLETEST_SCRIPT_COLOR_FAIL);
1353 * Print a message to the console, using a color.
1355 * @param string $message
1356 * The message to print.
1357 * @param int $color_code
1358 * The color code to use for coloring.
1360 function simpletest_script_print($message, $color_code) {
1362 if ($args['color']) {
1363 echo "\033[" . $color_code . "m" . $message . "\033[0m";
1371 * Get the color code associated with the specified status.
1373 * @param string $status
1374 * The status string to get code for. Special cases are: 'pass', 'fail', or
1378 * Color code. Returns 0 for default case.
1380 function simpletest_script_color_code($status) {
1383 return SIMPLETEST_SCRIPT_COLOR_PASS;
1386 return SIMPLETEST_SCRIPT_COLOR_FAIL;
1389 return SIMPLETEST_SCRIPT_COLOR_EXCEPTION;
1391 // Default formatting.
1396 * Prints alternative test names.
1398 * Searches the provided array of string values for close matches based on the
1399 * Levenshtein algorithm.
1401 * @param string $string
1403 * @param array $array
1404 * A list of strings to search.
1405 * @param int $degree
1406 * The matching strictness. Higher values return fewer matches. A value of
1407 * 4 means that the function will return strings from $array if the candidate
1408 * string in $array would be identical to $string by changing 1/4 or fewer of
1411 * @see http://php.net/manual/en/function.levenshtein.php
1413 function simpletest_script_print_alternatives($string, $array, $degree = 4) {
1414 $alternatives = array();
1415 foreach ($array as $item) {
1416 $lev = levenshtein($string, $item);
1417 if ($lev <= strlen($item) / $degree || FALSE !== strpos($string, $item)) {
1418 $alternatives[] = $item;
1421 if (!empty($alternatives)) {
1422 simpletest_script_print(" Did you mean?\n", SIMPLETEST_SCRIPT_COLOR_FAIL);
1423 foreach ($alternatives as $alternative) {
1424 simpletest_script_print(" - $alternative\n", SIMPLETEST_SCRIPT_COLOR_FAIL);
1430 * Loads the simpletest messages from the database.
1432 * Messages are ordered by test class and message id.
1434 * @param array $test_ids
1435 * Array of test IDs of the messages to be loaded.
1438 * Array of simpletest messages from the database.
1440 function simpletest_script_load_messages_by_test_id($test_ids) {
1444 // Sqlite has a maximum number of variables per query. If required, the
1445 // database query is split into chunks.
1446 if (count($test_ids) > SIMPLETEST_SCRIPT_SQLITE_VARIABLE_LIMIT && !empty($args['sqlite'])) {
1447 $test_id_chunks = array_chunk($test_ids, SIMPLETEST_SCRIPT_SQLITE_VARIABLE_LIMIT);
1450 $test_id_chunks = array($test_ids);
1453 foreach ($test_id_chunks as $test_id_chunk) {
1455 $result_chunk = Database::getConnection('default', 'test-runner')
1456 ->query("SELECT * FROM {simpletest} WHERE test_id IN ( :test_ids[] ) ORDER BY test_class, message_id", array(
1457 ':test_ids[]' => $test_id_chunk,
1460 catch (Exception $e) {
1462 exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
1464 if ($result_chunk) {
1465 $results = array_merge($results, $result_chunk);
1473 * Display test results.
1475 function simpletest_script_open_browser() {
1479 $connection = Database::getConnection('default', 'test-runner');
1480 $results = $connection->select('simpletest')
1481 ->fields('simpletest')
1482 ->condition('test_id', $test_ids, 'IN')
1483 ->orderBy('test_class')
1484 ->orderBy('message_id')
1488 catch (Exception $e) {
1490 exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
1493 // Get the results form.
1495 SimpletestResultsForm::addResultForm($form, $results);
1497 // Get the assets to make the details element collapsible and theme the result
1499 $assets = new \Drupal\Core\Asset\AttachedAssets();
1500 $assets->setLibraries([
1501 'core/drupal.collapse',
1503 'simpletest/drupal.simpletest',
1505 $resolver = \Drupal::service('asset.resolver');
1506 list($js_assets_header, $js_assets_footer) = $resolver->getJsAssets($assets, FALSE);
1507 $js_collection_renderer = \Drupal::service('asset.js.collection_renderer');
1508 $js_assets_header = $js_collection_renderer->render($js_assets_header);
1509 $js_assets_footer = $js_collection_renderer->render($js_assets_footer);
1510 $css_assets = \Drupal::service('asset.css.collection_renderer')->render($resolver->getCssAssets($assets, FALSE));
1512 // Make the html page to write to disk.
1513 $render_service = \Drupal::service('renderer');
1514 $html = '<head>' . $render_service->renderPlain($js_assets_header) . $render_service->renderPlain($css_assets) . '</head><body>' . $render_service->renderPlain($form) . $render_service->renderPlain($js_assets_footer) . '</body>';
1516 // Ensure we have assets verbose directory - tests with no verbose output will
1517 // not have created one.
1518 $directory = PublicStream::basePath() . '/simpletest/verbose';
1519 file_prepare_directory($directory, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS);
1521 $uuid = $php->generate();
1522 $filename = $directory . '/results-' . $uuid . '.html';
1523 $base_url = getenv('SIMPLETEST_BASE_URL');
1524 if (empty($base_url)) {
1525 simpletest_script_print_error("--browser needs argument --url.");
1527 $url = $base_url . '/' . PublicStream::basePath() . '/simpletest/verbose/results-' . $uuid . '.html';
1528 file_put_contents($filename, $html);
1530 // See if we can find an OS helper to open URLs in default browser.
1532 if (shell_exec('which xdg-open')) {
1533 $browser = 'xdg-open';
1535 elseif (shell_exec('which open')) {
1538 elseif (substr(PHP_OS, 0, 3) == 'WIN') {
1543 shell_exec($browser . ' ' . escapeshellarg($url));
1546 // Can't find assets valid browser.
1547 print 'Open file://' . realpath($filename) . ' in your browser to see the verbose output.';