Commit 682c5504 authored by webchick's avatar webchick

Issue #1808220 by sun: Remove run-tests.sh dependency on existing/installed parent site.

parent e1db0da4
......@@ -13,6 +13,14 @@
* Tests _contextual_links_to_id() & _contextual_id_to_links().
*/
class ContextualUnitTest extends DrupalUnitTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('contextual');
public static function getInfo() {
return array(
'name' => 'Conversion to and from "contextual id"s (for placeholders)',
......
......@@ -371,13 +371,24 @@ public static function deleteAssert($message_id) {
* The database connection to use for inserting assertions.
*/
public static function getDatabaseConnection() {
// Check whether there is a test runner connection.
// @see run-tests.sh
// @todo Convert Simpletest UI runner to create + use this connection, too.
try {
$connection = Database::getConnection('default', 'simpletest_original_default');
$connection = Database::getConnection('default', 'test-runner');
}
catch (ConnectionNotDefinedException $e) {
// If the test was not set up, the simpletest_original_default
// connection does not exist.
$connection = Database::getConnection('default', 'default');
// Check whether there is a backup of the original default connection.
// @see TestBase::prepareEnvironment()
try {
$connection = Database::getConnection('default', 'simpletest_original_default');
}
catch (ConnectionNotDefinedException $e) {
// If TestBase::prepareEnvironment() or TestBase::restoreEnvironment()
// failed, the test-specific database connection does not exist
// yet/anymore, so fall back to the default of the (UI) test runner.
$connection = Database::getConnection('default', 'default');
}
}
return $connection;
}
......@@ -918,7 +929,7 @@ private function prepareDatabasePrefix() {
// As soon as the database prefix is set, the test might start to execute.
// All assertions as well as the SimpleTest batch operations are associated
// with the testId, so the database prefix has to be associated with it.
$affected_rows = db_update('simpletest_test_id')
$affected_rows = self::getDatabaseConnection()->update('simpletest_test_id')
->fields(array('last_prefix' => $this->databasePrefix))
->condition('test_id', $this->testId)
->execute();
......@@ -936,6 +947,13 @@ private function changeDatabasePrefix() {
if (empty($this->databasePrefix)) {
$this->prepareDatabasePrefix();
}
// If the backup already exists, something went terribly wrong.
// This case is possible, because database connection info is a static
// global state construct on the Database class, which at least persists
// for all test methods executed in one PHP process.
if (Database::getConnectionInfo('simpletest_original_default')) {
throw new \RuntimeException("Bad Database connection state: 'simpletest_original_default' connection key already exists. Broken test?");
}
// Clone the current connection and replace the current prefix.
$connection_info = Database::getConnectionInfo('default');
......@@ -992,6 +1010,9 @@ private function prepareEnvironment() {
$this->originalSite = conf_path();
$this->originalSettings = settings()->getAll();
$this->originalConfig = $GLOBALS['config'];
// @todo Remove all remnants of $GLOBALS['conf'].
// @see https://drupal.org/node/2183323
$this->originalConf = isset($GLOBALS['conf']) ? $GLOBALS['conf'] : NULL;
// Backup statics and globals.
$this->originalContainer = clone \Drupal::getContainer();
......@@ -1075,6 +1096,8 @@ private function prepareEnvironment() {
// Unset globals.
unset($GLOBALS['config_directories']);
unset($GLOBALS['config']);
unset($GLOBALS['conf']);
unset($GLOBALS['theme_key']);
unset($GLOBALS['theme']);
......@@ -1085,9 +1108,6 @@ private function prepareEnvironment() {
// Change the database prefix.
$this->changeDatabasePrefix();
// Remove all configuration overrides.
$GLOBALS['config'] = array();
// After preparing the environment and changing the database prefix, we are
// in a valid test environment.
drupal_valid_test_ua($this->databasePrefix);
......@@ -1214,6 +1234,7 @@ private function restoreEnvironment() {
// Restore original in-memory configuration.
$GLOBALS['config'] = $this->originalConfig;
$GLOBALS['conf'] = $this->originalConf;
new Settings($this->originalSettings);
// Restore original statics and globals.
......
......@@ -233,7 +233,7 @@ function confirmStubTestResults() {
$this->assertAssertion("Debug: 'Foo'", 'Debug', 'Fail', 'SimpleTestTest.php', 'Drupal\simpletest\Tests\SimpleTestTest->stubTest()');
$this->assertEqual('6 passes, 5 fails, 2 exceptions, 1 debug message', $this->childTestResults['summary'], 'Stub test summary is correct');
$this->assertEqual('6 passes, 5 fails, 2 exceptions, 1 debug message', $this->childTestResults['summary']);
$this->test_ids[] = $test_id = $this->getTestIdFromResults();
$this->assertTrue($test_id, 'Found test ID in results.');
......
......@@ -361,8 +361,16 @@ function _simpletest_batch_finished($success, $results, $operations, $elapsed) {
* that ran.
*/
function simpletest_last_test_get($test_id) {
$last_prefix = db_query_range('SELECT last_prefix FROM {simpletest_test_id} WHERE test_id = :test_id', 0, 1, array(':test_id' => $test_id))->fetchField();
$last_test_class = db_query_range('SELECT test_class FROM {simpletest} WHERE test_id = :test_id ORDER BY message_id DESC', 0, 1, array(':test_id' => $test_id))->fetchField();
$last_prefix = TestBase::getDatabaseConnection()
->queryRange('SELECT last_prefix FROM {simpletest_test_id} WHERE test_id = :test_id', 0, 1, array(
':test_id' => $test_id,
))
->fetchField();
$last_test_class = TestBase::getDatabaseConnection()
->queryRange('SELECT test_class FROM {simpletest} WHERE test_id = :test_id ORDER BY message_id DESC', 0, 1, array(
':test_id' => $test_id,
))
->fetchField();
return array($last_prefix, $last_test_class);
}
......@@ -696,22 +704,23 @@ function simpletest_clean_temporary_directories() {
*/
function simpletest_clean_results_table($test_id = NULL) {
if (\Drupal::config('simpletest.settings')->get('clear_results')) {
$connection = TestBase::getDatabaseConnection();
if ($test_id) {
$count = db_query('SELECT COUNT(test_id) FROM {simpletest_test_id} WHERE test_id = :test_id', array(':test_id' => $test_id))->fetchField();
$count = $connection->query('SELECT COUNT(test_id) FROM {simpletest_test_id} WHERE test_id = :test_id', array(':test_id' => $test_id))->fetchField();
db_delete('simpletest')
$connection->delete('simpletest')
->condition('test_id', $test_id)
->execute();
db_delete('simpletest_test_id')
$connection->delete('simpletest_test_id')
->condition('test_id', $test_id)
->execute();
}
else {
$count = db_query('SELECT COUNT(test_id) FROM {simpletest_test_id}')->fetchField();
$count = $connection->query('SELECT COUNT(test_id) FROM {simpletest_test_id}')->fetchField();
// Clear test results.
db_delete('simpletest')->execute();
db_delete('simpletest_test_id')->execute();
$connection->delete('simpletest')->execute();
$connection->delete('simpletest_test_id')->execute();
}
return $count;
......
<?php
/**
* @file
* This script runs Drupal tests from command line.
*/
require_once __DIR__ . '/../vendor/autoload.php';
use Drupal\Component\Utility\Settings;
use Drupal\Component\Utility\Timer;
use Drupal\Core\Database\Database;
use Drupal\Core\DrupalKernel;
use Symfony\Component\HttpFoundation\Request;
require_once __DIR__ . '/../vendor/autoload.php';
const SIMPLETEST_SCRIPT_COLOR_PASS = 32; // Green.
const SIMPLETEST_SCRIPT_COLOR_FAIL = 31; // Red.
......@@ -20,38 +25,26 @@
exit;
}
simpletest_script_init();
simpletest_script_bootstrap();
if ($args['execute-test']) {
// Masquerade as Apache for running tests.
simpletest_script_init("Apache");
simpletest_script_setup_database();
simpletest_script_run_one_test($args['test-id'], $args['execute-test']);
// Sub-process script execution ends here.
}
else {
// Run administrative functions as CLI.
simpletest_script_init(NULL);
}
// Bootstrap to perform initial validation or other operations.
drupal_bootstrap(DRUPAL_BOOTSTRAP_CODE);
if (!\Drupal::moduleHandler()->moduleExists('simpletest')) {
simpletest_script_print_error("The Testing (simpletest) module must be installed before this script can run.");
// Sub-process exited already; this is just for clarity.
exit;
}
simpletest_classloader_register();
// We have to add a Request.
$request = \Symfony\Component\HttpFoundation\Request::createFromGlobals();
$container = \Drupal::getContainer();
$container->set('request', $request);
simpletest_script_setup_database(TRUE);
if ($args['clean']) {
// Clean up left-over times and directories.
// Clean up left-over tables and directories.
simpletest_clean_environment();
echo "\nEnvironment cleaned.\n";
// Get the status messages and print them.
$messages = array_pop(drupal_get_messages('status'));
foreach ($messages as $text) {
$messages = drupal_get_messages('status');
foreach ($messages['status'] as $text) {
echo " - " . $text . "\n";
}
exit;
......@@ -123,10 +116,26 @@ function simpletest_script_help() {
--clean Cleans up database tables or directories from previous, failed,
tests and then exits (no tests are run).
--url Immediately precedes a URL to set the host and path. You will
need this parameter if Drupal is in a subdirectory on your
localhost and you have not set \$base_url in settings.php. Tests
can be run under SSL by including https:// in the URL.
--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. For example:
mysql://username:password@localhost/databasename#table_prefix
Only used if specified.
Required when running tests without a Drupal installation that
contains default database connection info in settings.php.
--php The absolute path to the PHP executable. Usually not needed.
......@@ -185,7 +194,16 @@ function simpletest_script_help() {
--url http://example.com/ --all
sudo -u [wwwrun|www-data|etc] php ./core/scripts/{$args['script']}
--url http://example.com/ --class "Drupal\block\Tests\BlockTest"
\n
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
EOF;
}
......@@ -202,6 +220,8 @@ function simpletest_script_parse_args() {
'list' => FALSE,
'clean' => FALSE,
'url' => '',
'sqlite' => NULL,
'dburl' => NULL,
'php' => '',
'concurrency' => 1,
'all' => FALSE,
......@@ -265,7 +285,7 @@ function simpletest_script_parse_args() {
/**
* Initialize script variables and perform general setup requirements.
*/
function simpletest_script_init($server_software) {
function simpletest_script_init() {
global $args, $php;
$host = 'localhost';
......@@ -311,7 +331,7 @@ function simpletest_script_init($server_software) {
$_SERVER['REMOTE_ADDR'] = '127.0.0.1';
$_SERVER['SERVER_ADDR'] = '127.0.0.1';
$_SERVER['SERVER_PORT'] = $port;
$_SERVER['SERVER_SOFTWARE'] = $server_software;
$_SERVER['SERVER_SOFTWARE'] = NULL;
$_SERVER['SERVER_NAME'] = 'localhost';
$_SERVER['REQUEST_URI'] = $path .'/';
$_SERVER['REQUEST_METHOD'] = 'GET';
......@@ -331,6 +351,196 @@ function simpletest_script_init($server_software) {
require_once dirname(__DIR__) . '/includes/bootstrap.inc';
}
/**
* Bootstraps a minimal Drupal environment.
*
* @see install_begin_request()
*/
function simpletest_script_bootstrap() {
// Load legacy include files.
foreach (glob(DRUPAL_ROOT . '/core/includes/*.inc') as $include) {
require_once $include;
}
// Replace services with in-memory and null implementations.
$GLOBALS['conf']['container_service_providers']['InstallerServiceProvider'] = 'Drupal\Core\Installer\InstallerServiceProvider';
drupal_bootstrap(DRUPAL_BOOTSTRAP_CONFIGURATION);
// Remove Drupal's error/exception handlers; they are designed for HTML
// and there is no storage nor a (watchdog) logger here.
restore_error_handler();
restore_exception_handler();
// In addition, ensure that PHP errors are not hidden away in logs.
ini_set('display_errors', TRUE);
// Ensure that required Settings exist.
if (!Settings::getSingleton()->getAll()) {
new Settings(array(
'hash_salt' => 'run-tests',
));
}
$kernel = new DrupalKernel('testing', drupal_classloader(), FALSE);
$kernel->boot();
$request = Request::createFromGlobals();
$container = $kernel->getContainer();
$container->enterScope('request');
$container->set('request', $request, 'request');
$module_handler = $container->get('module_handler');
// @todo Remove System module. Only needed because \Drupal\Core\Datetime\Date
// has a (needless) dependency on the 'date_format' entity, so calls to
// format_date()/format_interval() cause a plugin not found exception.
$module_list['system'] = 'core/modules/system/system.module';
$module_list['simpletest'] = 'core/modules/simpletest/simpletest.module';
$module_handler->setModuleList($module_list);
$module_handler->loadAll();
$kernel->updateModules($module_list, $module_list);
simpletest_classloader_register();
}
/**
* 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) {
global $args, $databases;
// 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).
unset($databases['default']);
Database::removeConnection('default');
$info = parse_url($args['dburl']);
if (!isset($info['scheme'], $info['host'], $info['path'])) {
simpletest_script_print_error('Invalid --dburl. Minimum requirement: driver://host/database');
exit(1);
}
$info += array(
'user' => '',
'pass' => '',
'fragment' => '',
);
$databases['default']['default'] = array(
'driver' => $info['scheme'],
'username' => $info['user'],
'password' => $info['pass'],
'host' => $info['host'],
'database' => ltrim($info['path'], '/'),
'prefix' => array(
'default' => $info['fragment'],
),
);
}
// Otherwise, ensure that database table prefix info is an array.
// @see https://drupal.org/node/2176621
elseif (isset($databases['default']['default'])) {
if (!is_array($databases['default']['default']['prefix'])) {
$databases['default']['default']['prefix'] = array(
'default' => $databases['default']['default']['prefix'],
);
}
}
// 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);
}
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);
}
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);
}
}
/**
* Get all available tests from simpletest and PHPUnit.
*
......@@ -373,7 +583,8 @@ function simpletest_script_execute_batch($test_classes) {
break;
}
$test_id = db_insert('simpletest_test_id')->useDefaults(array('test_id'))->execute();
$test_id = Database::getConnection('default', 'test-runner')
->insert('simpletest_test_id')->useDefaults(array('test_id'))->execute();
$test_ids[] = $test_id;
$test_class = array_shift($test_classes);
......@@ -471,11 +682,7 @@ function simpletest_script_run_phpunit($test_id, $class) {
}
foreach ($summaries as $class => $summary) {
$had_fails = $summary['#fail'] > 0;
$had_exceptions = $summary['#exception'] > 0;
$status = ($had_fails || $had_exceptions ? 'fail' : 'pass');
$info = call_user_func(array($class, 'getInfo'));
simpletest_script_print($info['name'] . ' ' . _simpletest_format_summary_line($summary) . "\n", simpletest_script_color_code($status));
simpletest_script_reporter_display_summary($class, $summary);
}
}
......@@ -486,24 +693,12 @@ function simpletest_script_run_one_test($test_id, $test_class) {
global $args;
try {
// Bootstrap Drupal.
drupal_bootstrap(DRUPAL_BOOTSTRAP_CODE);
simpletest_classloader_register();
// We have to add a Request.
$request = \Symfony\Component\HttpFoundation\Request::createFromGlobals();
$container = \Drupal::getContainer();
$container->set('request', $request);
$test = new $test_class($test_id);
$test->dieOnFail = (bool) $args['die-on-fail'];
$test->verbose = (bool) $args['verbose'];
$test->run();
$info = $test->getInfo();
$had_fails = (isset($test->results['#fail']) && $test->results['#fail'] > 0);
$had_exceptions = (isset($test->results['#exception']) && $test->results['#exception'] > 0);
$status = ($had_fails || $had_exceptions ? 'fail' : 'pass');
simpletest_script_print($info['name'] . ' ' . _simpletest_format_summary_line($test->results) . "\n", simpletest_script_color_code($status));
simpletest_script_reporter_display_summary($test_class, $test->results);
// Finished, kill this runner.
exit(0);
......@@ -529,6 +724,12 @@ function simpletest_script_command($test_id, $test_class) {
$command = escapeshellarg($php) . ' ' . escapeshellarg('./core/scripts/' . $args['script']);
$command .= ' --url ' . escapeshellarg($args['url']);
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";
foreach (array('verbose', 'keep-results', 'color', 'die-on-fail') as $arg) {
......@@ -597,17 +798,18 @@ function simpletest_script_cleanup($test_id, $test_class, $exitcode) {
// would also delete file directories of other tests that are potentially
// running concurrently.
file_unmanaged_delete_recursive($test_directory, array('Drupal\simpletest\TestBase', 'filePreDeleteCallback'));
$messages[] = "- Removed test files directory.";
$messages[] = "- Removed test site directory.";
}
// Clear out all database tables from the test.
$schema = Database::getConnection('default', 'default')->schema();
$count = 0;
foreach (db_find_tables($db_prefix . '%') as $table) {
db_drop_table($table);
foreach ($schema->findTables($db_prefix . '%') as $table) {
$schema->dropTable($table);
$count++;
}
if ($count) {
$messages[] = "- " . format_plural($count, 'Removed 1 leftover table.', 'Removed @count leftover tables.');
$messages[] = "- Removed $count leftover tables.";
}
if ($output) {
......@@ -710,14 +912,13 @@ function simpletest_script_reporter_init() {
else {
echo "Tests to be run:\n";
foreach ($test_list as $class_name) {
$info = call_user_func(array($class_name, 'getInfo'));
echo " - " . $info['name'] . ' (' . $class_name . ')' . "\n";
echo " - $class_name\n";
}
echo "\n";
}
echo "Test run started:\n";
echo " " . format_date($_SERVER['REQUEST_TIME'], 'long') . "\n";
echo " " . date('l, F j, Y - H:i', $_SERVER['REQUEST_TIME']) . "\n";
Timer::start('run-tests');
echo "\n";
......@@ -726,13 +927,40 @@ function simpletest_script_reporter_init() {
echo "\n";
}
/**
* 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));
}