Newer
Older
<?php

Angie Byron
committed
/**
* @file
* This script runs Drupal tests from command line.
*/
use Drupal\Component\Utility\Timer;
use Drupal\Component\Uuid\Php;

Angie Byron
committed
use Drupal\Core\Database\Database;
use Drupal\Core\Form\FormState;

Alex Pott
committed
use Drupal\Core\Site\Settings;
use Drupal\Core\StreamWrapper\PublicStream;
use Drupal\Core\Test\TestRunnerKernel;
use Drupal\simpletest\Form\SimpletestResultsForm;

Angie Byron
committed
use Symfony\Component\HttpFoundation\Request;
$autoloader = require_once __DIR__ . '/../../autoload.php';
const SIMPLETEST_SCRIPT_COLOR_PASS = 32; // Green.
const SIMPLETEST_SCRIPT_COLOR_FAIL = 31; // Red.
const SIMPLETEST_SCRIPT_COLOR_EXCEPTION = 33; // Brown.

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

Dries Buytaert
committed
// Set defaults and get overrides.
list($args, $count) = simpletest_script_parse_args();
if ($args['help'] || $count == 0) {
simpletest_script_help();
exit;

Dries Buytaert
committed
}

Angie Byron
committed
simpletest_script_init();
$request = Request::createFromGlobals();
$kernel = TestRunnerKernel::createFromRequest($request, $autoloader);
$kernel->prepareLegacyRequest($request);

Angie Byron
committed

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

Angie Byron
committed
simpletest_script_setup_database();

Angie Byron
committed
simpletest_script_run_one_test($args['test-id'], $args['execute-test']);

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

Dries Buytaert
committed
}

Angie Byron
committed
if ($args['list']) {
// Display all available tests.
echo "\nAvailable test groups & classes\n";
echo "-------------------------------\n\n";
$groups = simpletest_test_get_all($args['module']);
foreach ($groups as $group => $tests) {
echo $group . "\n";
foreach ($tests as $class => $info) {
echo " - $class\n";
}
}
exit;
}

Angie Byron
committed
simpletest_script_setup_database(TRUE);

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

Angie Byron
committed
// Clean up left-over tables and directories.

Dries Buytaert
committed
simpletest_clean_environment();
echo "\nEnvironment cleaned.\n";
// Get the status messages and print them.

Angie Byron
committed
$messages = drupal_get_messages('status');
foreach ($messages['status'] as $text) {

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

Dries Buytaert
committed
}

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

Dries Buytaert
committed
simpletest_script_reporter_init();

Dries Buytaert
committed
$tests_to_run = array();
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.
simpletest_script_execute_batch($tests_to_run);

Dries Buytaert
committed

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

Dries Buytaert
committed
// Display results before database is cleared.
if ($args['browser']) {
simpletest_script_open_browser();
}
else {
simpletest_script_reporter_display_results();
}

Dries Buytaert
committed

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

Dries Buytaert
committed
// Clean up all test results.
if (!$args['keep-results']) {
simpletest_clean_results_table();
}

Dries Buytaert
committed

Angie Byron
committed
// Test complete, exit.
exit;

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.
--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 that has Simpletest module installed.
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'
--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.
Examples:
mysql://username:password@localhost/databasename#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.

Angie Byron
committed
A specific test method can be added, for example,
'Drupal\book\Tests\BookTest::testBookExport'.
--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/user.test').
--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
--browser Opens the results in the browser. This enforces --keep-results and
if you want to also view any pages rendered in the simpletest
browser you need to add --verbose to the command line.
<test1>[,<test2>[,<test3> ...]]

Dries Buytaert
committed
One or more tests to be run. By default, these are interpreted

Angie Byron
committed
as the names of test groups as shown at

Dries Buytaert
committed
admin/config/development/testing.
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']}

Angie Byron
committed
--url http://example.com/ --class "Drupal\block\Tests\BlockTest"

Angie Byron
committed
Without a preinstalled Drupal site and enabled Simpletest module, specify a
SQLite database pathname to create and the default database connection info to
use in tests:
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.
*
* @return The list of arguments.
*/
function simpletest_script_parse_args() {
// Set default values.
$args = array(
'script' => '',
'help' => FALSE,
'list' => FALSE,
'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,
'directory' => NULL,

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

Dries Buytaert
committed
'test_names' => array(),

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

Dries Buytaert
committed
// Used internally.

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

Dries Buytaert
committed
'xml' => '',

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;
}
else {
$args[$matches[1]] = array_shift($_SERVER['argv']);
}
// Clear extraneous values.

Dries Buytaert
committed
$args['test_names'] = array();
$count++;
}
else {
// Argument not found in list.
simpletest_script_print_error("Unknown argument '$arg'.");
exit;

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
// Validate the concurrency argument
if (!is_numeric($args['concurrency']) || $args['concurrency'] <= 0) {
simpletest_script_print_error("--concurrency must be a strictly positive integer.");
exit;

Dries Buytaert
committed
}
if ($args['browser']) {
$args['keep-results'] = TRUE;
}

Dries Buytaert
committed
return array($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';
// 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('_')) {

Dries Buytaert
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.
list($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();

Dries Buytaert
committed
}

Dries Buytaert
committed

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']), '/') : '';

Angie Byron
committed
$port = (isset($parsed_url['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';
$_SERVER['REQUEST_URI'] = $path .'/';
$_SERVER['REQUEST_METHOD'] = 'GET';

Dries Buytaert
committed
$_SERVER['SCRIPT_NAME'] = $path .'/index.php';
$_SERVER['SCRIPT_FILENAME'] = $path .'/index.php';

Dries Buytaert
committed
$_SERVER['PHP_SELF'] = $path .'/index.php';
$_SERVER['HTTP_USER_AGENT'] = 'Drupal command line';
if (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') {
// Ensure that any and all environment variables are changed to https://.
foreach ($_SERVER as $key => $value) {
$_SERVER[$key] = str_replace('http://', 'https://', $_SERVER[$key]);
}
}

Nate Lampton
committed
chdir(realpath(__DIR__ . '/../..'));

Angie Byron
committed
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
}
/**
* 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:
* - --sqlite: The test runner connection, providing access to Simpletest
* database tables for recording test IDs and assertion results.
* - --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);

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

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(1);

Angie Byron
committed
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
}
Database::addConnectionInfo('default', 'default', $databases['default']['default']);
// If no --sqlite parameter has been passed, then Simpletest module is assumed
// to be installed, so the test runner database connection is the default
// database connection.
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'];
}
$databases['test-runner']['default'] = array(
'driver' => 'sqlite',
'database' => $sqlite,
'prefix' => array(
'default' => '',
),
);
// 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']);
// Create the Simpletest schema.
try {
$schema = Database::getConnection('default', 'test-runner')->schema();
}
catch (\PDOException $e) {
simpletest_script_print_error($databases['test-runner']['default']['driver'] . ': ' . $e->getMessage());
exit(1);

Angie Byron
committed
}
if ($new && $sqlite) {
require_once DRUPAL_ROOT . '/' . drupal_get_path('module', 'simpletest') . '/simpletest.install';
foreach (simpletest_schema() as $name => $table_spec) {
if ($schema->tableExists($name)) {
$schema->dropTable($name);
}
$schema->createTable($name, $table_spec);
}
}
// Verify that the Simpletest database schema exists by checking one table.
if (!$schema->tableExists('simpletest')) {
simpletest_script_print_error('Missing Simpletest database schema. Either install Simpletest module or use the --sqlite parameter.');
exit(1);

Angie Byron
committed
}
}

Dries Buytaert
committed
/**
* Execute a batch of tests.
*/

Dries Buytaert
committed
function simpletest_script_execute_batch($test_classes) {

Dries Buytaert
committed
global $args, $test_ids;

Angie Byron
committed
// Multi-process execution.
$children = array();

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

Angie Byron
committed
$test_id = Database::getConnection('default', 'test-runner')
->insert('simpletest_test_id')->useDefaults(array('test_id'))->execute();

Dries Buytaert
committed
$test_ids[] = $test_id;

Dries Buytaert
committed

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

Dries Buytaert
committed
// Fork a child process.

Angie Byron
committed
$command = simpletest_script_command($test_id, $test_class);
$process = proc_open($command, array(), $pipes, NULL, NULL, array('bypass_shell' => TRUE));
if (!is_resource($process)) {
echo "Unable to fork test process. Aborting.\n";
exit;

Dries Buytaert
committed
}

Angie Byron
committed
// Register our new child.
$children[] = array(
'process' => $process,

Dries Buytaert
committed
'test_id' => $test_id,

Angie Byron
committed
'class' => $test_class,
'pipes' => $pipes,
);
}

Dries Buytaert
committed

Angie Byron
committed
// Wait for children every 200ms.
usleep(200000);
// Check if some children finished.
foreach ($children as $cid => $child) {
$status = proc_get_status($child['process']);
if (empty($status['running'])) {
// The child exited, unregister it.
proc_close($child['process']);
if ($status['exitcode']) {

Dries Buytaert
committed
echo 'FATAL ' . $child['class'] . ': test runner returned a non-zero error code (' . $status['exitcode'] . ').' . "\n";

Angie Byron
committed
if ($args['die-on-fail']) {
list($db_prefix, ) = simpletest_last_test_get($child['test_id']);
$test_directory = 'sites/simpletest/' . substr($db_prefix, 10);

Angie Byron
committed
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";
$args['keep-results'] = TRUE;
// Exit repeat loop immediately.
$args['repeat'] = -1;

Angie Byron
committed
}

Dries Buytaert
committed
}

Dries Buytaert
committed
// Free-up space by removing any potentially created resources.
if (!$args['keep-results']) {
simpletest_script_cleanup($child['test_id'], $child['class'], $status['exitcode']);
}

Dries Buytaert
committed
// Remove this child.

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

Dries Buytaert
committed
}
}
}
}

Dries Buytaert
committed
/**
* Run a group of phpunit tests.
*/

Dries Buytaert
committed
function simpletest_script_run_phpunit($test_id, $class) {
$results = simpletest_run_phpunit_tests($test_id, array($class));

Dries Buytaert
committed
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
simpletest_process_phpunit_results($results);
// Map phpunit results to a data structure we can pass to
// _simpletest_format_summary_line.
$summaries = array();
foreach ($results as $result) {
if (!isset($summaries[$result['test_class']])) {
$summaries[$result['test_class']] = array(
'#pass' => 0,
'#fail' => 0,
'#exception' => 0,
'#debug' => 0,
);
}
switch ($result['status']) {
case 'pass':
$summaries[$result['test_class']]['#pass']++;
break;
case 'fail':
$summaries[$result['test_class']]['#fail']++;
break;
case 'exception':
$summaries[$result['test_class']]['#exception']++;
break;
case 'debug':

Dries Buytaert
committed
$summaries[$result['test_class']]['#debug']++;

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

Angie Byron
committed
simpletest_script_reporter_display_summary($class, $summary);

Dries Buytaert
committed
}
}

Dries Buytaert
committed
/**

Angie Byron
committed
* Bootstrap Drupal and run a single test.

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

Angie Byron
committed
try {

Angie Byron
committed
if (strpos($test_class, '::') > 0) {
list($class_name, $method) = explode('::', $test_class, 2);
$methods = [$method];
}
else {
$class_name = $test_class;
// Use empty array to run all the test methods.
$methods = array();
}
$test = new $class_name($test_id);
if (is_subclass_of($test_class, '\PHPUnit_Framework_TestCase')) {
simpletest_script_run_phpunit($test_id, $test_class);
}
else {
$test->dieOnFail = (bool) $args['die-on-fail'];
$test->verbose = (bool) $args['verbose'];
$test->run($methods);
simpletest_script_reporter_display_summary($test_class, $test->results);
}

Angie Byron
committed
// Finished, kill this runner.
exit(0);

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(1);

Angie Byron
committed
}

Dries Buytaert
committed
/**

Angie Byron
committed
* Return a command used to run a test in a separate process.
*
* @param $test_id
* The current test ID.
* @param $test_class
* The name of the test class to run.

Dries Buytaert
committed
*/

Angie Byron
committed
function simpletest_script_command($test_id, $test_class) {

Dries Buytaert
committed
global $args, $php;

Dries Buytaert
committed
$command = escapeshellarg($php) . ' ' . escapeshellarg('./core/scripts/' . $args['script']);
$command .= ' --url ' . escapeshellarg($args['url']);

Angie Byron
committed
if (!empty($args['sqlite'])) {
$command .= ' --sqlite ' . escapeshellarg($args['sqlite']);
}
if (!empty($args['dburl'])) {
$command .= ' --dburl ' . escapeshellarg($args['dburl']);
}
$command .= ' --php ' . escapeshellarg($php);
$command .= " --test-id $test_id";

Angie Byron
committed
foreach (array('verbose', 'keep-results', 'color', 'die-on-fail') as $arg) {
if ($args[$arg]) {
$command .= ' --' . $arg;
}
// --execute-test and class name needs to come last.
$command .= ' --execute-test ' . escapeshellarg($test_class);

Angie Byron
committed
return $command;

Dries Buytaert
committed
}

Dries Buytaert
committed
/**
* Removes all remnants of a test runner.
*
* In case a (e.g., fatal) error occurs after the test site has been fully setup
* and the error happens in many tests, the environment that executes the tests
* can easily run out of memory or disk space. This function ensures that all
* created resources are properly cleaned up after every executed test.
*
* This clean-up only exists in this script, since SimpleTest module itself does
* not use isolated sub-processes for each test being run, so a fatal error
* halts not only the test, but also the test runner (i.e., the parent site).
*
* @param int $test_id
* The test ID of the test run.
* @param string $test_class
* The class name of the test run.
* @param int $exitcode
* The exit code of the test runner.
*
* @see simpletest_script_run_one_test()
*/
function simpletest_script_cleanup($test_id, $test_class, $exitcode) {
if (is_subclass_of($test_class, '\PHPUnit_Framework_TestCase')) {
// PHPUnit test, move on.
return;
}

Dries Buytaert
committed
// Retrieve the last database prefix used for testing.
list($db_prefix, ) = simpletest_last_test_get($test_id);
// If no database prefix was found, then the test was not set up correctly.
if (empty($db_prefix)) {
echo "\nFATAL $test_class: Found no database prefix for test ID $test_id. (Check whether setUp() is invoked correctly.)";
return;
}
// Do not output verbose cleanup messages in case of a positive exitcode.
$output = !empty($exitcode);
$messages = array();
$messages[] = "- Found database prefix '$db_prefix' for test ID $test_id.";
// Read the log file in case any fatal errors caused the test to crash.
simpletest_log_read($test_id, $db_prefix, $test_class);
// Check whether a test site directory was setup already.
// @see \Drupal\simpletest\TestBase::prepareEnvironment()
$test_directory = DRUPAL_ROOT . '/sites/simpletest/' . substr($db_prefix, 10);

Dries Buytaert
committed
if (is_dir($test_directory)) {
// Output the error_log.
if (is_file($test_directory . '/error.log')) {
if ($errors = file_get_contents($test_directory . '/error.log')) {
$output = TRUE;
$messages[] = $errors;
}
}
// Delete the test site directory.

Dries Buytaert
committed
// simpletest_clean_temporary_directories() cannot be used here, since it
// would also delete file directories of other tests that are potentially
// running concurrently.
file_unmanaged_delete_recursive($test_directory, array('Drupal\simpletest\TestBase', 'filePreDeleteCallback'));

Angie Byron
committed
$messages[] = "- Removed test site directory.";

Dries Buytaert
committed
}
// Clear out all database tables from the test.

Angie Byron
committed
$schema = Database::getConnection('default', 'default')->schema();

Dries Buytaert
committed
$count = 0;

Angie Byron
committed
foreach ($schema->findTables($db_prefix . '%') as $table) {
$schema->dropTable($table);

Dries Buytaert
committed
$count++;
}
if ($count) {

Angie Byron
committed
$messages[] = "- Removed $count leftover tables.";

Dries Buytaert
committed
}
if ($output) {
echo implode("\n", $messages);
echo "\n";
}
}

Dries Buytaert
committed
/**
* Get list of tests based on arguments. If --all specified then

Dries Buytaert
committed
* returns all available tests, otherwise reads list of tests.
*
* Will print error and exit if no valid tests were found.
*
* @return List of tests.
*/
function simpletest_script_get_test_list() {

Dries Buytaert
committed
global $args;

Dries Buytaert
committed
$test_list = array();
if ($args['all'] || $args['module']) {
$groups = simpletest_test_get_all($args['module']);

Dries Buytaert
committed
$all_tests = array();
foreach ($groups as $group => $tests) {

Dries Buytaert
committed
$all_tests = array_merge($all_tests, array_keys($tests));

Dries Buytaert
committed
}

Angie Byron
committed
$test_list = $all_tests;

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

Angie Byron
committed
$test_list = array();
foreach ($args['test_names'] as $test_class) {

Angie Byron
committed
list($class_name, ) = explode('::', $test_class, 2);
if (class_exists($class_name)) {

Angie Byron
committed
$test_list[] = $test_class;
}
else {
$groups = simpletest_test_get_all();
$all_classes = array();
foreach ($groups as $group) {
$all_classes = array_merge($all_classes, array_keys($group));
}

Angie Byron
committed
simpletest_script_print_error('Test class not found: ' . $class_name);
simpletest_script_print_alternatives($class_name, $all_classes, 6);
exit(1);

Angie Byron
committed
}

Dries Buytaert
committed
}
}

Angie Byron
committed
elseif ($args['file']) {

Dries Buytaert
committed
// Extract test case class names from specified files.
foreach ($args['test_names'] as $file) {

Dries Buytaert
committed
if (!file_exists($file)) {
simpletest_script_print_error('File not found: ' . $file);
exit;

Dries Buytaert
committed
}
$content = file_get_contents($file);
// Extract a potential namespace.
$namespace = FALSE;
if (preg_match('@^namespace ([^ ;]+)@m', $content, $matches)) {
$namespace = $matches[1];
}
// Extract all class names.
// Abstract classes are excluded on purpose.
preg_match_all('@^class ([^ ]+)@m', $content, $matches);
if (!$namespace) {

Dries Buytaert
committed
$test_list = array_merge($test_list, $matches[1]);

Dries Buytaert
committed
}
else {
foreach ($matches[1] as $class_name) {

Alex Pott
committed
$namespace_class = $namespace . '\\' . $class_name;
if (is_subclass_of($namespace_class, '\Drupal\simpletest\TestBase') || is_subclass_of($namespace_class, '\PHPUnit_Framework_TestCase')) {
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
$test_list[] = $namespace_class;
}
}
}
}
}
elseif ($args['directory']) {
// Extract test case class names from specified directory.
// Find all tests in the PSR-X structure; Drupal\$extension\Tests\*.php
// Since we do not want to hard-code too many structural file/directory
// assumptions about PSR-0/4 files and directories, we check for the
// minimal conditions only; i.e., a '*.php' file that has '/Tests/' in
// its path.
// Ignore anything from third party vendors.
$ignore = array('.', '..', 'vendor');
$files = [];
if ($args['directory'][0] === '/') {
$directory = $args['directory'];
}
else {
$directory = DRUPAL_ROOT . "/" . $args['directory'];
}
foreach (file_scan_directory($directory, '/\.php$/', $ignore) as $file) {
// '/Tests/' can be contained anywhere in the file's path (there can be
// sub-directories below /Tests), but must be contained literally.
// Case-insensitive to match all Simpletest and PHPUnit tests:
// ./lib/Drupal/foo/Tests/Bar/Baz.php
// ./foo/src/Tests/Bar/Baz.php
// ./foo/tests/Drupal/foo/Tests/FooTest.php
// ./foo/tests/src/FooTest.php
// $file->filename doesn't give us a directory, so we use $file->uri
// Strip the drupal root directory and trailing slash off the URI
$filename = substr($file->uri, strlen(DRUPAL_ROOT)+1);
if (stripos($filename, '/Tests/')) {
$files[$filename] = $filename;
}
}
foreach ($files as $file) {
$content = file_get_contents($file);
// Extract a potential namespace.
$namespace = FALSE;
if (preg_match('@^namespace ([^ ;]+)@m', $content, $matches)) {
$namespace = $matches[1];
}
// Extract all class names.
// Abstract classes are excluded on purpose.
preg_match_all('@^class ([^ ]+)@m', $content, $matches);
if (!$namespace) {
$test_list = array_merge($test_list, $matches[1]);
}
else {
foreach ($matches[1] as $class_name) {
$namespace_class = $namespace . '\\' . $class_name;
if (is_subclass_of($namespace_class, '\Drupal\simpletest\TestBase') || is_subclass_of($namespace_class, '\PHPUnit_Framework_TestCase')) {

Alex Pott
committed
$test_list[] = $namespace_class;
}

Dries Buytaert
committed
}

Dries Buytaert
committed
else {
$groups = simpletest_test_get_all();

Dries Buytaert
committed
foreach ($args['test_names'] as $group_name) {

Angie Byron
committed
if (isset($groups[$group_name])) {
$test_list = array_merge($test_list, array_keys($groups[$group_name]));
}
else {
simpletest_script_print_error('Test group not found: ' . $group_name);
simpletest_script_print_alternatives($group_name, array_keys($groups));
exit(1);

Angie Byron
committed
}

Dries Buytaert
committed
}
}

Dries Buytaert
committed

Dries Buytaert
committed
if (empty($test_list)) {
simpletest_script_print_error('No valid tests were specified.');
exit;

Dries Buytaert
committed
}
return $test_list;

Dries Buytaert
committed
/**
* Initialize the reporter.
*/
function simpletest_script_reporter_init() {

Dries Buytaert
committed
global $args, $test_list, $results_map;

Dries Buytaert
committed
$results_map = array(
'pass' => 'Pass',
'fail' => 'Fail',
'exception' => 'Exception'
);

Dries Buytaert
committed
echo "\n";
echo "Drupal test run\n";
echo "---------------\n";
echo "\n";

Dries Buytaert
committed

Dries Buytaert
committed
// Tell the user about what tests are to be run.
if ($args['all']) {
echo "All tests will run.\n\n";
}
else {
echo "Tests to be run:\n";

Dries Buytaert
committed
foreach ($test_list as $class_name) {

Angie Byron
committed
echo " - $class_name\n";

Dries Buytaert
committed
}
echo "\n";

Dries Buytaert
committed

Dries Buytaert
committed
echo "Test run started:\n";

Angie Byron
committed
echo " " . date('l, F j, Y - H:i', $_SERVER['REQUEST_TIME']) . "\n";
Timer::start('run-tests');

Dries Buytaert
committed
echo "\n";

Dries Buytaert
committed
echo "Test summary\n";
echo "------------\n";

Dries Buytaert
committed
echo "\n";

Angie Byron
committed
/**
* Displays the assertion result summary for a single test class.
*
* @param string $class
* The test class name that was run.
* @param array $results
* The assertion results using #pass, #fail, #exception, #debug array keys.
*/
function simpletest_script_reporter_display_summary($class, $results) {
// Output all test results vertically aligned.
// Cut off the class name after 60 chars, and pad each group with 3 digits
// by default (more than 999 assertions are rare).
$output = vsprintf('%-60.60s %10s %9s %14s %12s', array(
$class,
$results['#pass'] . ' passes',
!$results['#fail'] ? '' : $results['#fail'] . ' fails',
!$results['#exception'] ? '' : $results['#exception'] . ' exceptions',
!$results['#debug'] ? '' : $results['#debug'] . ' messages',
));
$status = ($results['#fail'] || $results['#exception'] ? 'fail' : 'pass');
simpletest_script_print($output . "\n", simpletest_script_color_code($status));
}

Dries Buytaert
committed
/**

Dries Buytaert
committed
* Display jUnit XML test results.

Dries Buytaert
committed
*/

Dries Buytaert
committed
function simpletest_script_reporter_write_xml_results() {

Dries Buytaert
committed
global $args, $test_ids, $results_map;

Dries Buytaert
committed

Angie Byron
committed
$results = simpletest_script_load_messages_by_test_id($test_ids);

Dries Buytaert
committed
$test_class = '';
$xml_files = array();
foreach ($results as $result) {
if (isset($results_map[$result->status])) {
if ($result->test_class != $test_class) {
// We've moved onto a new class, so write the last classes results to a file:
if (isset($xml_files[$test_class])) {
file_put_contents($args['xml'] . '/' . str_replace('\\', '_', $test_class) . '.xml', $xml_files[$test_class]['doc']->saveXML());

Dries Buytaert
committed
unset($xml_files[$test_class]);
}
$test_class = $result->test_class;