Newer
Older
<?php

Angie Byron
committed
/**
* @file

catch
committed
* Script for running tests on DrupalCI.
*
* This script is intended for use only by drupal.org's testing. In general,
* tests should be run directly with phpunit.
*
* @internal
*/
use Drupal\Component\FileSystem\FileSystem;

catch
committed
use Drupal\Component\Utility\Environment;
use Drupal\Component\Utility\Html;
use Drupal\Component\Utility\Timer;
use Drupal\Core\Composer\Composer;

Angie Byron
committed
use Drupal\Core\Database\Database;
use Drupal\Core\Test\EnvironmentCleaner;

Lee Rowlands
committed
use Drupal\Core\Test\PhpUnitTestRunner;
use Drupal\Core\Test\SimpletestTestRunResultsStorage;
use Drupal\Core\Test\RunTests\TestFileParser;

catch
committed
use Drupal\Core\Test\TestDatabase;
use Drupal\Core\Test\TestRun;
use Drupal\Core\Test\TestRunnerKernel;
use Drupal\Core\Test\TestRunResultsStorageInterface;

Lee Rowlands
committed
use Drupal\Core\Test\TestDiscovery;
use Drupal\BuildTests\Framework\BuildTestBase;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
use Drupal\KernelTests\KernelTestBase;
use Drupal\Tests\BrowserTestBase;

Alex Pott
committed
use PHPUnit\Framework\TestCase;
use PHPUnit\Runner\Version;
use Symfony\Component\Console\Output\ConsoleOutput;

Angie Byron
committed
use Symfony\Component\HttpFoundation\Request;

Lee Rowlands
committed
use Symfony\Component\Process\Process;

Angie Byron
committed
// cspell:ignore exitcode wwwrun

Alex Pott
committed
// Define some colors for display.
// A nice calming green.
const SIMPLETEST_SCRIPT_COLOR_PASS = 32;
// An alerting Red.
const SIMPLETEST_SCRIPT_COLOR_FAIL = 31;
// An annoying brown.
const SIMPLETEST_SCRIPT_COLOR_EXCEPTION = 33;

Angie Byron
committed
// Restricting the chunk of queries prevents memory exhaustion.
const SIMPLETEST_SCRIPT_SQLITE_VARIABLE_LIMIT = 350;

Dries Buytaert
committed
const SIMPLETEST_SCRIPT_EXIT_SUCCESS = 0;
const SIMPLETEST_SCRIPT_EXIT_FAILURE = 1;
const SIMPLETEST_SCRIPT_EXIT_EXCEPTION = 2;

Dries Buytaert
committed
// Set defaults and get overrides.

Lee Rowlands
committed
[$args, $count] = simpletest_script_parse_args();

Dries Buytaert
committed
if ($args['help'] || $count == 0) {
simpletest_script_help();
exit(($count == 0) ? SIMPLETEST_SCRIPT_EXIT_FAILURE : SIMPLETEST_SCRIPT_EXIT_SUCCESS);

Dries Buytaert
committed
}

Angie Byron
committed
simpletest_script_init();
if (!class_exists(TestCase::class)) {
echo "\nrun-tests.sh requires the PHPUnit testing framework. Use 'composer install' to ensure that it is present.\n\n";
exit(SIMPLETEST_SCRIPT_EXIT_FAILURE);
}

Angie Byron
committed

Angie Byron
committed
if ($args['execute-test']) {

Angie Byron
committed
simpletest_script_setup_database();
$test_run_results_storage = simpletest_script_setup_test_run_results_storage();
$test_run = TestRun::get($test_run_results_storage, $args['test-id']);
simpletest_script_run_one_test($test_run, $args['execute-test']);

Angie Byron
committed
// Sub-process exited already; this is just for clarity.
exit(SIMPLETEST_SCRIPT_EXIT_SUCCESS);

Dries Buytaert
committed
}

Angie Byron
committed
if ($args['list']) {

Alex Pott
committed
// Display all available tests organized by one @group annotation.
echo "\nAvailable test groups & classes\n";

Alex Pott
committed
echo "-------------------------------\n\n";
$test_discovery = new TestDiscovery(
\Drupal::root(),
\Drupal::service('class_loader')
);
try {
$groups = $test_discovery->getTestClasses($args['module']);
}
catch (Exception $e) {

Alex Pott
committed
error_log((string) $e);
echo (string) $e;
exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
}

Alex Pott
committed
// A given class can appear in multiple groups. For historical reasons, we
// need to present each test only once. The test is shown in the group that is
// printed first.
$printed_tests = [];
foreach ($groups as $group => $tests) {
echo $group . "\n";

Alex Pott
committed
$tests = array_diff(array_keys($tests), $printed_tests);
foreach ($tests as $test) {
echo " - $test\n";
}

Alex Pott
committed
$printed_tests = array_merge($printed_tests, $tests);
}
exit(SIMPLETEST_SCRIPT_EXIT_SUCCESS);
}

Alex Pott
committed
// List-files and list-files-json provide a way for external tools such as the
// testbot to prioritize running changed tests.
// @see https://www.drupal.org/node/2569585
if ($args['list-files'] || $args['list-files-json']) {
// List all files which could be run as tests.
$test_discovery = new TestDiscovery(
\Drupal::root(),
\Drupal::service('class_loader')
);

Alex Pott
committed
// TestDiscovery::findAllClassFiles() gives us a classmap similar to a
// Composer 'classmap' array.
$test_classes = $test_discovery->findAllClassFiles();
// JSON output is the easiest.
if ($args['list-files-json']) {
echo json_encode($test_classes);
exit(SIMPLETEST_SCRIPT_EXIT_SUCCESS);
}
// Output the list of files.
else {

Lee Rowlands
committed
foreach (array_values($test_classes) as $test_class) {

Alex Pott
committed
echo $test_class . "\n";
}
}
exit(SIMPLETEST_SCRIPT_EXIT_SUCCESS);
}

Angie Byron
committed
simpletest_script_setup_database(TRUE);

Dries Buytaert
committed
// Setup the test run results storage environment. Currently, this coincides
// with the simpletest database schema.
$test_run_results_storage = simpletest_script_setup_test_run_results_storage(TRUE);

Dries Buytaert
committed
if ($args['clean']) {

Angie Byron
committed
// Clean up left-over tables and directories.
$cleaner = new EnvironmentCleaner(
DRUPAL_ROOT,
Database::getConnection(),
$test_run_results_storage,
new ConsoleOutput(),
\Drupal::service('file_system')
);
try {
$cleaner->cleanEnvironment();
}
catch (Exception $e) {
echo (string) $e;
exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
}

Dries Buytaert
committed
echo "\nEnvironment cleaned.\n";
// Get the status messages and print them.
$messages = \Drupal::messenger()->messagesByType('status');
foreach ($messages as $text) {

Dries Buytaert
committed
echo " - " . $text . "\n";
}
exit(SIMPLETEST_SCRIPT_EXIT_SUCCESS);

Dries Buytaert
committed
}
if (!Composer::upgradePHPUnitCheck(Version::id())) {
simpletest_script_print_error("PHPUnit testing framework version 9 or greater is required when running on PHP 7.4 or greater. Run the command 'composer run-script drupal-phpunit-upgrade' in order to fix this.");
exit(SIMPLETEST_SCRIPT_EXIT_FAILURE);
}

Dries Buytaert
committed
$test_list = simpletest_script_get_test_list();
// Try to allocate unlimited time to run the tests.

catch
committed
Environment::setTimeLimit(0);

Dries Buytaert
committed
simpletest_script_reporter_init();

Lee Rowlands
committed
$tests_to_run = [];
for ($i = 0; $i < $args['repeat']; $i++) {

Dries Buytaert
committed
$tests_to_run = array_merge($tests_to_run, $test_list);

Angie Byron
committed
}

Dries Buytaert
committed

Dries Buytaert
committed
// Execute tests.
$status = simpletest_script_execute_batch($test_run_results_storage, $tests_to_run);

Dries Buytaert
committed

Dries Buytaert
committed
// Stop the timer.
simpletest_script_reporter_timer_stop();

catch
committed
// Ensure all test locks are released once finished. If tests are run with a
// concurrency of 1 the each test will clean up its own lock. Test locks are
// not released if using a higher concurrency to ensure each test has unique
// fixtures.

catch
committed
TestDatabase::releaseAllTestLocks();

Dries Buytaert
committed
// Display results before database is cleared.
simpletest_script_reporter_display_results($test_run_results_storage);

Dries Buytaert
committed

Dries Buytaert
committed
if ($args['xml']) {
simpletest_script_reporter_write_xml_results($test_run_results_storage);

Dries Buytaert
committed
}

Dries Buytaert
committed
// Clean up all test results.
if (!$args['keep-results']) {
try {
$cleaner = new EnvironmentCleaner(
DRUPAL_ROOT,
Database::getConnection(),
$test_run_results_storage,
new ConsoleOutput(),
\Drupal::service('file_system')
);
$cleaner->cleanResults();
}
catch (Exception $e) {
echo (string) $e;
exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
}
}

Dries Buytaert
committed

Angie Byron
committed
// Test complete, exit.
exit($status);

Angie Byron
committed

Dries Buytaert
committed
/**
* Print help text.
*/
function simpletest_script_help() {
global $args;
echo <<<EOF
Run Drupal tests from the shell.

Dries Buytaert
committed
Usage: {$args['script']} [OPTIONS] <tests>
Example: {$args['script']} Profile
All arguments are long options.
--help Print this page.

Dries Buytaert
committed
--list Display all available test groups.

Alex Pott
committed
--list-files
Display all discoverable test file paths.
--list-files-json
Display all discoverable test files as JSON. The array key will be
the test class name, and the value will be the file path of the
test.
--clean Cleans up database tables or directories from previous, failed,
tests and then exits (no tests are run).

Angie Byron
committed
--url The base URL of the root directory of this Drupal checkout; e.g.:
http://drupal.test/
Required unless the Drupal root directory maps exactly to:
http://localhost:80/
Use a https:// URL to force all tests to be run under SSL.
--sqlite A pathname to use for the SQLite database of the test runner.
Required unless this script is executed with a working Drupal
installation.

Angie Byron
committed
A relative pathname is interpreted relative to the Drupal root
directory.
Note that ':memory:' cannot be used, because this script spawns
sub-processes. However, you may use e.g. '/tmpfs/test.sqlite'
--keep-results-table
Boolean flag to indicate to not cleanup the simpletest result
table. For testbots or repeated execution of a single test it can
be helpful to not cleanup the simpletest result table.

Angie Byron
committed
--dburl A URI denoting the database driver, credentials, server hostname,
and database name to use in tests.

Angie Byron
committed
Required when running tests without a Drupal installation that
contains default database connection info in settings.php.

Dave Long
committed
mysql://username:password@localhost/database_name#table_prefix
sqlite://localhost/relative/path/db.sqlite
sqlite://localhost//absolute/path/db.sqlite

Dries Buytaert
committed
--php The absolute path to the PHP executable. Usually not needed.

Dries Buytaert
committed
--concurrency [num]

Angie Byron
committed
Run tests in parallel, up to [num] tests at a time.

Dries Buytaert
committed

Dries Buytaert
committed
--all Run all available tests.

Dries Buytaert
committed
--module Run all tests belonging to the specified module name.
(e.g., 'node')

Dries Buytaert
committed
--class Run tests identified by specific class names, instead of group names.
--file Run tests identified by specific file names, instead of group names.

Nate Lampton
committed
Specify the path and the extension
(i.e. 'core/modules/user/tests/src/Functional/UserCreateTest.php').
This argument must be last on the command line.
--types
Runs just tests from the specified test type, for example
run-tests.sh
(i.e. --types "PHPUnit-Unit,PHPUnit-Kernel")
--directory Run all tests found within the specified file directory.

Dries Buytaert
committed
--xml <path>
If provided, test results will be written as xml files to this path.
--color Output text format results with color highlighting.

Dries Buytaert
committed
--verbose Output detailed assertion messages in addition to summary.
--keep-results
Keeps detailed assertion results (in the database) after tests
have completed. By default, assertion results are cleared.

Angie Byron
committed
--repeat Number of times to repeat the test.
--die-on-fail
Exit test execution immediately upon any failed assertion. This
allows to access the test site by changing settings.php to use the
test database and configuration directories. Use in combination
with --repeat for debugging random test failures.

Angie Byron
committed
--non-html Removes escaping from output. Useful for reading results on the
CLI.
--suppress-deprecations
Stops tests from failing if deprecation errors are triggered. If
this is not set the value specified in the
SYMFONY_DEPRECATIONS_HELPER environment variable, or the value
specified in core/phpunit.xml (if it exists), or the default value
will be used. The default is that any unexpected silenced
deprecation error will fail tests.
--ci-parallel-node-total
The total number of instances of this job running in parallel.
--ci-parallel-node-index
The index of the job in the job set.
<test1>[,<test2>[,<test3> ...]]

Dries Buytaert
committed
One or more tests to be run. By default, these are interpreted
as the names of test groups which are derived from test class
@group annotations.
These group names typically correspond to module names like "User"
or "Profile" or "System", but there is also a group "Database".
If --class is specified then these are interpreted as the names of

Dries Buytaert
committed
specific test classes whose test methods will be run. Tests must
be separated by commas. Ignored if --all is specified.

Dries Buytaert
committed
To run this script you will normally invoke it from the root directory of your

Dries Buytaert
committed
Drupal installation as the webserver user (differs per configuration), or root:

Nate Lampton
committed
sudo -u [wwwrun|www-data|etc] php ./core/scripts/{$args['script']}

Dries Buytaert
committed
--url http://example.com/ --all

Nate Lampton
committed
sudo -u [wwwrun|www-data|etc] php ./core/scripts/{$args['script']}
--url http://example.com/ --class Drupal\Tests\block\Functional\BlockTest

Angie Byron
committed
Without a preinstalled Drupal site, specify a SQLite database pathname to create

Théodore Biadala
committed
(for the test runner) and the default database connection info (for Drupal) to
use in tests:

Angie Byron
committed
sudo -u [wwwrun|www-data|etc] php ./core/scripts/{$args['script']}
--sqlite /tmpfs/drupal/test.sqlite
--dburl mysql://username:password@localhost/database
--url http://example.com/ --all

Dries Buytaert
committed
/**
* Parse execution argument and ensure that all are valid.
*

Alex Pott
committed
* @return array
* The list of arguments.

Dries Buytaert
committed
*/
function simpletest_script_parse_args() {
// Set default values.

Lee Rowlands
committed
$args = [

Dries Buytaert
committed
'script' => '',
'help' => FALSE,
'list' => FALSE,

Alex Pott
committed
'list-files' => FALSE,
'list-files-json' => FALSE,

Dries Buytaert
committed
'clean' => FALSE,
'url' => '',

Angie Byron
committed
'sqlite' => NULL,
'dburl' => NULL,

Dries Buytaert
committed
'php' => '',

Dries Buytaert
committed
'concurrency' => 1,
'all' => FALSE,
'module' => NULL,

Dries Buytaert
committed
'class' => FALSE,
'types' => [],
'directory' => NULL,

Dries Buytaert
committed
'color' => FALSE,
'verbose' => FALSE,
'keep-results' => FALSE,
'keep-results-table' => FALSE,

Lee Rowlands
committed
'test_names' => [],

Angie Byron
committed
'repeat' => 1,
'die-on-fail' => FALSE,
'suppress-deprecations' => FALSE,

Dries Buytaert
committed
// Used internally.

Angie Byron
committed
'test-id' => 0,
'execute-test' => '',

Dries Buytaert
committed
'xml' => '',
'non-html' => FALSE,
'ci-parallel-node-index' => 1,
'ci-parallel-node-total' => 1,

Lee Rowlands
committed
];

Dries Buytaert
committed
// Override with set values.
$args['script'] = basename(array_shift($_SERVER['argv']));
$count = 0;
while ($arg = array_shift($_SERVER['argv'])) {
if (preg_match('/--(\S+)/', $arg, $matches)) {
// Argument found.
if (array_key_exists($matches[1], $args)) {
// Argument found in list.
$previous_arg = $matches[1];
if (is_bool($args[$previous_arg])) {
$args[$matches[1]] = TRUE;
}
elseif (is_array($args[$previous_arg])) {
$value = array_shift($_SERVER['argv']);
$args[$matches[1]] = array_map('trim', explode(',', $value));
}

Dries Buytaert
committed
else {
$args[$matches[1]] = array_shift($_SERVER['argv']);
}
// Clear extraneous values.

Lee Rowlands
committed
$args['test_names'] = [];

Dries Buytaert
committed
$count++;
}
else {
// Argument not found in list.
simpletest_script_print_error("Unknown argument '$arg'.");
exit(SIMPLETEST_SCRIPT_EXIT_FAILURE);

Dries Buytaert
committed
}
}
else {
// Values found without an argument should be test names.
$args['test_names'] += explode(',', $arg);
$count++;

Dries Buytaert
committed
}
}

Dries Buytaert
committed

Alex Pott
committed
// Validate the concurrency argument.

Dries Buytaert
committed
if (!is_numeric($args['concurrency']) || $args['concurrency'] <= 0) {
simpletest_script_print_error("--concurrency must be a strictly positive integer.");
exit(SIMPLETEST_SCRIPT_EXIT_FAILURE);

Dries Buytaert
committed
}

Lee Rowlands
committed
return [$args, $count];
}

Dries Buytaert
committed
/**
* Initialize script variables and perform general setup requirements.
*/

Angie Byron
committed
function simpletest_script_init() {

Dries Buytaert
committed
global $args, $php;
$host = 'localhost';
$path = '';

Angie Byron
committed
$port = '80';

Alex Pott
committed
// Determine location of php command automatically, unless a command line
// argument is supplied.
if (!empty($args['php'])) {

Dries Buytaert
committed
$php = $args['php'];
}
elseif ($php_env = getenv('_')) {

Alex Pott
committed
// '_' is an environment variable set by the shell. It contains the command
// that was executed.
$php = $php_env;

Dries Buytaert
committed
}
elseif ($sudo = getenv('SUDO_COMMAND')) {

Dries Buytaert
committed
// 'SUDO_COMMAND' is an environment variable set by the sudo program.
// Extract only the PHP interpreter, not the rest of the command.

Lee Rowlands
committed
[$php] = explode(' ', $sudo, 2);

Dries Buytaert
committed
}
else {
simpletest_script_print_error('Unable to automatically determine the path to the PHP interpreter. Supply the --php command line argument.');

Dries Buytaert
committed
simpletest_script_help();
exit(SIMPLETEST_SCRIPT_EXIT_FAILURE);

Dries Buytaert
committed
}

Dries Buytaert
committed
// Detect if we're in the top-level process using the private 'execute-test'
// argument. Determine if being run on drupal.org's testing infrastructure
// using the presence of 'drupalci' in the sqlite argument.
// @todo https://www.drupal.org/project/drupalci_testbot/issues/2860941 Use
// better environment variable to detect DrupalCI.
if (!$args['execute-test'] && preg_match('/drupalci/', $args['sqlite'] ?? '')) {
// Update PHPUnit if needed and possible. There is a later check once the
// autoloader is in place to ensure we're on the correct version. We need to
// do this before the autoloader is in place to ensure that it is correct.
$composer = ($composer = rtrim('\\' === DIRECTORY_SEPARATOR ? preg_replace('/[\r\n].*/', '', `where.exe composer.phar`) : `which composer.phar`))
? $php . ' ' . escapeshellarg($composer)
: 'composer';
passthru("$composer run-script drupal-phpunit-upgrade-check");
}
$autoloader = require_once __DIR__ . '/../../autoload.php';
// The PHPUnit compatibility layer needs to be available to autoload tests.
$autoloader->add('Drupal\\TestTools', __DIR__ . '/../tests');

Angie Byron
committed
// Get URL from arguments.

Dries Buytaert
committed
if (!empty($args['url'])) {
$parsed_url = parse_url($args['url']);

Dries Buytaert
committed
$host = $parsed_url['host'] . (isset($parsed_url['port']) ? ':' . $parsed_url['port'] : '');

Angie Byron
committed
$path = isset($parsed_url['path']) ? rtrim(rtrim($parsed_url['path']), '/') : '';
$port = $parsed_url['port'] ?? $port;
if ($path == '/') {
$path = '';
}
// If the passed URL schema is 'https' then setup the $_SERVER variables

Angie Byron
committed
// properly so that testing will run under HTTPS.
if ($parsed_url['scheme'] == 'https') {
$_SERVER['HTTPS'] = 'on';
}

Dries Buytaert
committed
}

Alex Pott
committed
if (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on') {
$base_url = 'https://';
}
else {
$base_url = 'http://';
}
$base_url .= $host;
if ($path !== '') {
$base_url .= $path;
}
putenv('SIMPLETEST_BASE_URL=' . $base_url);

Dries Buytaert
committed
$_SERVER['HTTP_HOST'] = $host;
$_SERVER['REMOTE_ADDR'] = '127.0.0.1';
$_SERVER['SERVER_ADDR'] = '127.0.0.1';

Angie Byron
committed
$_SERVER['SERVER_PORT'] = $port;

Angie Byron
committed
$_SERVER['SERVER_SOFTWARE'] = NULL;

Dries Buytaert
committed
$_SERVER['SERVER_NAME'] = 'localhost';

Alex Pott
committed
$_SERVER['REQUEST_URI'] = $path . '/';
$_SERVER['REQUEST_METHOD'] = 'GET';

Alex Pott
committed
$_SERVER['SCRIPT_NAME'] = $path . '/index.php';
$_SERVER['SCRIPT_FILENAME'] = $path . '/index.php';
$_SERVER['PHP_SELF'] = $path . '/index.php';

Dries Buytaert
committed
$_SERVER['HTTP_USER_AGENT'] = 'Drupal command line';

catch
committed
if ($args['concurrency'] > 1) {
$directory = FileSystem::getOsTemporaryDirectory();
$test_symlink = @symlink(__FILE__, $directory . '/test_symlink');
if (!$test_symlink) {
throw new \RuntimeException('In order to use a concurrency higher than 1 the test system needs to be able to create symlinks in ' . $directory);
}
unlink($directory . '/test_symlink');

catch
committed
putenv('RUN_TESTS_CONCURRENCY=' . $args['concurrency']);
}
if (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') {
// Ensure that any and all environment variables are changed to https://.
foreach ($_SERVER as $key => $value) {

catch
committed
// Some values are NULL. Non-NULL values which are falsy will not contain
// text to replace.
if ($value) {
$_SERVER[$key] = str_replace('http://', 'https://', $value);
}
}
}

Nate Lampton
committed
chdir(realpath(__DIR__ . '/../..'));
// Prepare the kernel.
try {
$request = Request::createFromGlobals();
$kernel = TestRunnerKernel::createFromRequest($request, $autoloader);
$kernel->boot();
$kernel->preHandle($request);
}
catch (Exception $e) {
echo (string) $e;
exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
}

Angie Byron
committed
}
/**
* Sets up database connection info for running tests.
*
* If this script is executed from within a real Drupal installation, then this
* function essentially performs nothing (unless the --sqlite or --dburl
* parameters were passed).
*
* Otherwise, there are three database connections of concern:

catch
committed
* - --sqlite: The test runner connection, providing access to database tables
* for recording test IDs and assertion results.

Angie Byron
committed
* - --dburl: A database connection that is used as base connection info for all
* tests; i.e., every test will spawn from this connection. In case this
* connection uses e.g. SQLite, then all tests will run against SQLite. This
* is exposed as $databases['default']['default'] to Drupal.
* - The actual database connection used within a test. This is the same as
* --dburl, but uses an additional database table prefix. This is
* $databases['default']['default'] within a test environment. The original
* connection is retained in
* $databases['simpletest_original_default']['default'] and restored after
* each test.
*
* @param bool $new
* Whether this process is a run-tests.sh master process. If TRUE, the SQLite
* database file specified by --sqlite (if any) is set up. Otherwise, database
* connections are prepared only.
*/
function simpletest_script_setup_database($new = FALSE) {

Angie Byron
committed
// If there is an existing Drupal installation that contains a database
// connection info in settings.php, then $databases['default']['default'] will
// hold the default database connection already. This connection is assumed to
// be valid, and this connection will be used in tests, so that they run
// against e.g. MySQL instead of SQLite.
// However, in case no Drupal installation exists, this default database
// connection can be set and/or overridden with the --dburl parameter.
if (!empty($args['dburl'])) {
// Remove a possibly existing default connection (from settings.php).
Database::removeConnection('default');

Alex Pott
committed
try {
$databases['default']['default'] = Database::convertDbUrlToConnectionInfo($args['dburl'], DRUPAL_ROOT, TRUE);

Alex Pott
committed
catch (\InvalidArgumentException $e) {
simpletest_script_print_error('Invalid --dburl. Reason: ' . $e->getMessage());
exit(SIMPLETEST_SCRIPT_EXIT_FAILURE);
}

Angie Byron
committed
}
// Otherwise, use the default database connection from settings.php.
else {
$databases['default'] = Database::getConnectionInfo('default');

Angie Byron
committed
}
// If there is no default database connection for tests, we cannot continue.
if (!isset($databases['default']['default'])) {
simpletest_script_print_error('Missing default database connection for tests. Use --dburl to specify one.');
exit(SIMPLETEST_SCRIPT_EXIT_FAILURE);

Angie Byron
committed
}
Database::addConnectionInfo('default', 'default', $databases['default']['default']);
}
/**
* Sets up the test runs results storage.
*/
function simpletest_script_setup_test_run_results_storage($new = FALSE) {
global $args;
$databases['default'] = Database::getConnectionInfo('default');

Angie Byron
committed
// If no --sqlite parameter has been passed, then the test runner database
// connection is the default database connection.

Angie Byron
committed
if (empty($args['sqlite'])) {
$sqlite = FALSE;
$databases['test-runner']['default'] = $databases['default']['default'];
}
// Otherwise, set up a SQLite connection for the test runner.
else {
if ($args['sqlite'][0] === '/') {
$sqlite = $args['sqlite'];
}
else {
$sqlite = DRUPAL_ROOT . '/' . $args['sqlite'];
}

Lee Rowlands
committed
$databases['test-runner']['default'] = [

Angie Byron
committed
'driver' => 'sqlite',
'database' => $sqlite,
'prefix' => '',

Lee Rowlands
committed
];

Angie Byron
committed
// Create the test runner SQLite database, unless it exists already.
if ($new && !file_exists($sqlite)) {
if (!is_dir(dirname($sqlite))) {
mkdir(dirname($sqlite));
}
touch($sqlite);
}
}
// Add the test runner database connection.
Database::addConnectionInfo('test-runner', 'default', $databases['test-runner']['default']);

catch
committed
// Create the test result schema.

Angie Byron
committed
try {
$test_run_results_storage = new SimpletestTestRunResultsStorage(Database::getConnection('default', 'test-runner'));

Angie Byron
committed
}
catch (\PDOException $e) {
simpletest_script_print_error($databases['test-runner']['default']['driver'] . ': ' . $e->getMessage());
exit(SIMPLETEST_SCRIPT_EXIT_FAILURE);

Angie Byron
committed
}
if ($new && $sqlite) {
try {
$test_run_results_storage->buildTestingResultsEnvironment(!empty($args['keep-results-table']));
}
catch (Exception $e) {
echo (string) $e;
exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);

Angie Byron
committed
}
}
// Verify that the test result database schema exists by checking one table.
try {
if (!$test_run_results_storage->validateTestingResultsEnvironment()) {
simpletest_script_print_error('Missing test result database schema. Use the --sqlite parameter.');
exit(SIMPLETEST_SCRIPT_EXIT_FAILURE);
}
}
catch (Exception $e) {
echo (string) $e;
exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);

Angie Byron
committed
}
return $test_run_results_storage;

Angie Byron
committed
}

Dries Buytaert
committed
/**
* Execute a batch of tests.
*/
function simpletest_script_execute_batch(TestRunResultsStorageInterface $test_run_results_storage, $test_classes) {

Dries Buytaert
committed
global $args, $test_ids;
$total_status = SIMPLETEST_SCRIPT_EXIT_SUCCESS;

Angie Byron
committed
// Multi-process execution.

Lee Rowlands
committed
$children = [];

Dries Buytaert
committed
while (!empty($test_classes) || !empty($children)) {

Angie Byron
committed
while (count($children) < $args['concurrency']) {

Dries Buytaert
committed
if (empty($test_classes)) {

Angie Byron
committed
break;

Dries Buytaert
committed
}

Angie Byron
committed
try {
$test_run = TestRun::createNew($test_run_results_storage);
}
catch (Exception $e) {
echo (string) $e;
exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
}
$test_ids[] = $test_run->id();

Dries Buytaert
committed

Dries Buytaert
committed
$test_class = array_shift($test_classes);

Dries Buytaert
committed
// Fork a child process.
$command = simpletest_script_command($test_run, $test_class);

Lee Rowlands
committed
try {
$process = new Process($command);
$process->start();
}
catch (\Exception $e) {
echo get_class($e) . ": " . $e->getMessage() . "\n";

Angie Byron
committed
echo "Unable to fork test process. Aborting.\n";
exit(SIMPLETEST_SCRIPT_EXIT_SUCCESS);

Dries Buytaert
committed
}

Angie Byron
committed
// Register our new child.

Lee Rowlands
committed
$children[] = [

Angie Byron
committed
'process' => $process,
'test_run' => $test_run,

Angie Byron
committed
'class' => $test_class,

Lee Rowlands
committed
];

Angie Byron
committed
}

Dries Buytaert
committed

Théodore Biadala
committed
// Wait for children every 2ms.
usleep(2000);

Angie Byron
committed
// Check if some children finished.
foreach ($children as $cid => $child) {

Lee Rowlands
committed
if ($child['process']->isTerminated()) {
// The child exited.
echo $child['process']->getOutput();
$errorOutput = $child['process']->getErrorOutput();
if ($errorOutput) {
echo 'ERROR: ' . $errorOutput;
}

Lee Rowlands
committed
if ($child['process']->getExitCode() === SIMPLETEST_SCRIPT_EXIT_FAILURE) {
$total_status = max($child['process']->getExitCode(), $total_status);
}
elseif ($child['process']->getExitCode()) {
$message = 'FATAL ' . $child['class'] . ': test runner returned a non-zero error code (' . $child['process']->getExitCode() . ').';
echo $message . "\n";
// @todo Return SIMPLETEST_SCRIPT_EXIT_EXCEPTION instead, when
// DrupalCI supports this.
// @see https://www.drupal.org/node/2780087
$total_status = max(SIMPLETEST_SCRIPT_EXIT_FAILURE, $total_status);
// Insert a fail for xml results.
$child['test_run']->insertLogEntry([
'test_class' => $child['class'],
'status' => 'fail',
'message' => $message,
'message_group' => 'run-tests.sh check',
]);

Alex Pott
committed
// Ensure that an error line is displayed for the class.
simpletest_script_reporter_display_summary(
$child['class'],
['#pass' => 0, '#fail' => 1, '#exception' => 0, '#debug' => 0]
);

Angie Byron
committed
if ($args['die-on-fail']) {
$test_db = new TestDatabase($child['test_run']->getDatabasePrefix());

catch
committed
$test_directory = $test_db->getTestSitePath();
echo 'Test database and files kept and test exited immediately on fail so should be reproducible if you change settings.php to use the database prefix ' . $child['test_run']->getDatabasePrefix() . ' and config directories in ' . $test_directory . "\n";
$args['keep-results'] = TRUE;
// Exit repeat loop immediately.
$args['repeat'] = -1;

Angie Byron
committed
}

Dries Buytaert
committed
}

Dries Buytaert
committed
// Remove this child.

Angie Byron
committed
unset($children[$cid]);

Dries Buytaert
committed
}
}
}
return $total_status;

Dries Buytaert
committed
}

Dries Buytaert
committed
/**

Alex Pott
committed
* Run a PHPUnit-based test.

Dries Buytaert
committed
*/
function simpletest_script_run_phpunit(TestRun $test_run, $class) {

Dave Long
committed
global $args;

Lee Rowlands
committed
$runner = PhpUnitTestRunner::create(\Drupal::getContainer());

Théodore Biadala
committed
$start = microtime(TRUE);

Dave Long
committed
$results = $runner->execute($test_run, $class, $status, $args['color']);

Théodore Biadala
committed
$time = microtime(TRUE) - $start;
$runner->processPhpUnitResults($test_run, $results);

Dries Buytaert
committed

Lee Rowlands
committed
$summaries = $runner->summarizeResults($results);

Dries Buytaert
committed
foreach ($summaries as $class => $summary) {

Théodore Biadala
committed
simpletest_script_reporter_display_summary($class, $summary, $time);

Dries Buytaert
committed
}
return $status;

Dries Buytaert
committed
}

Dries Buytaert
committed
/**

Alex Pott
committed
* Run a single test, bootstrapping Drupal if needed.

Dries Buytaert
committed
*/
function simpletest_script_run_one_test(TestRun $test_run, $test_class) {
global $args;

Angie Byron
committed
try {
if ($args['suppress-deprecations']) {
putenv('SYMFONY_DEPRECATIONS_HELPER=disabled');
}
$status = simpletest_script_run_phpunit($test_run, $test_class);
exit($status);

Angie Byron
committed
}

Dries Buytaert
committed
// DrupalTestCase::run() catches exceptions already, so this is only reached
// when an exception is thrown in the wrapping test runner environment.

Angie Byron
committed
catch (Exception $e) {
echo (string) $e;
exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);

Angie Byron
committed
}

Dries Buytaert
committed
/**

Angie Byron
committed
* Return a command used to run a test in a separate process.
*

Alex Pott
committed
* @param int $test_id
* The current test ID.
* @param string $test_class
* The name of the test class to run.
*

Lee Rowlands
committed
* @return list<string>
* The list of command-line elements.

Dries Buytaert
committed
*/

Lee Rowlands
committed
function simpletest_script_command(TestRun $test_run, string $test_class): array {

Dries Buytaert
committed
global $args, $php;

Dries Buytaert
committed

Lee Rowlands
committed
$command = [];
$command[] = $php;
$command[] = './core/scripts/' . $args['script'];
$command[] = '--url';
$command[] = $args['url'];

Angie Byron
committed
if (!empty($args['sqlite'])) {

Lee Rowlands
committed
$command[] = '--sqlite';
$command[] = $args['sqlite'];

Angie Byron
committed
}
if (!empty($args['dburl'])) {

Lee Rowlands
committed
$command[] = '--dburl';
$command[] = $args['dburl'];

Angie Byron
committed
}

Lee Rowlands
committed
$command[] = '--php';
$command[] = $php;
$command[] = '--test-id';
$command[] = $test_run->id();

Lee Rowlands
committed
foreach (['verbose', 'keep-results', 'color', 'die-on-fail', 'suppress-deprecations'] as $arg) {
if ($args[$arg]) {

Lee Rowlands
committed
$command[] = '--' . $arg;
}
// --execute-test and class name needs to come last.

Lee Rowlands
committed
$command[] = '--execute-test';
$command[] = $test_class;

Angie Byron
committed
return $command;

Dries Buytaert
committed
}

Dries Buytaert
committed
/**

Alex Pott
committed
* Get list of tests based on arguments.

Dries Buytaert
committed
*

Alex Pott
committed
* If --all specified then return all available tests, otherwise reads list of
* tests.

Dries Buytaert
committed
*

Alex Pott
committed
* @return array
* List of tests.

Dries Buytaert
committed
*/
function simpletest_script_get_test_list() {

Dries Buytaert
committed
global $args;

Dries Buytaert
committed
$test_discovery = new TestDiscovery(
\Drupal::root(),
\Drupal::service('class_loader')
);
$types_processed = empty($args['types']);

Lee Rowlands
committed
$test_list = [];

Lee Rowlands
committed
$slow_tests = [];

Alex Pott
committed
if ($args['all'] || $args['module'] || $args['directory']) {
try {

Alex Pott
committed
$groups = $test_discovery->getTestClasses($args['module'], $args['types'], $args['directory']);
$types_processed = TRUE;
}
catch (Exception $e) {
echo (string) $e;
exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
}

Théodore Biadala
committed
// Ensure that tests marked explicitly as @group #slow are run at the
// beginning of each job.
if (key($groups) === '#slow') {
$slow_tests = array_keys(array_shift($groups));

Lee Rowlands
committed
}
$not_slow_tests = [];

Dries Buytaert
committed
foreach ($groups as $group => $tests) {
$not_slow_tests = array_merge($not_slow_tests, array_keys($tests));

Alex Pott
committed
}
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
// Filter slow tests out of the not slow tests since they may appear in more
// than one group.
$not_slow_tests = array_diff($not_slow_tests, $slow_tests);
// If the tests are not being run in parallel, then ensure slow tests run
// all together first.
if ((int) $args['ci-parallel-node-total'] <= 1 ) {
sort_tests_by_type_and_methods($slow_tests);
sort_tests_by_type_and_methods($not_slow_tests);
$test_list = array_unique(array_merge($slow_tests, $not_slow_tests));
}
else {
// Sort all tests by the number of public methods on the test class.
// This is a proxy for the approximate time taken to run the test,
// which is used in combination with @group #slow to start the slowest tests
// first and distribute tests between test runners.
sort_tests_by_public_method_count($slow_tests);
sort_tests_by_public_method_count($not_slow_tests);
// Now set up a bin per test runner.
$bin_count = (int) $args['ci-parallel-node-total'];
// Now loop over the slow tests and add them to a bin one by one, this
// distributes the tests evenly across the bins.
$binned_slow_tests = place_tests_into_bins($slow_tests, $bin_count);
$slow_tests_for_job = $binned_slow_tests[$args['ci-parallel-node-index'] - 1];
// And the same for the rest of the tests.
$binned_other_tests = place_tests_into_bins($not_slow_tests, $bin_count);
$other_tests_for_job = $binned_other_tests[$args['ci-parallel-node-index'] - 1];
$test_list = array_unique(array_merge($slow_tests_for_job, $other_tests_for_job));

Dries Buytaert
committed
}

Dries Buytaert
committed
}
else {
if ($args['class']) {

Lee Rowlands
committed
$test_list = [];

Angie Byron
committed
foreach ($args['test_names'] as $test_class) {

Lee Rowlands
committed
[$class_name] = explode('::', $test_class, 2);

Angie Byron
committed
if (class_exists($class_name)) {

Angie Byron
committed
$test_list[] = $test_class;
}
else {
try {
$groups = $test_discovery->getTestClasses(NULL, $args['types']);
}
catch (Exception $e) {
echo (string) $e;
exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
}

Lee Rowlands
committed
$all_classes = [];

Angie Byron
committed
foreach ($groups as $group) {
$all_classes = array_merge($all_classes, array_keys($group));