Commit 8b41aeee authored by catch's avatar catch
Browse files

Issue #2170023 by tstoeckler, sun: Use exceptions when something goes wrong in test setup.

parent a3c8c88f
......@@ -72,19 +72,21 @@ function __construct($test_id = NULL) {
}
/**
* Sets up Drupal unit test environment.
*
* @see \DrupalUnitTestBase::$modules
* @see \DrupalUnitTestBase
* Overrides TestBase::beforePrepareEnvironment().
*/
protected function setUp() {
protected function beforePrepareEnvironment() {
// Copy/prime extension file lists once to avoid filesystem scans.
if (!isset($this->moduleFiles)) {
$this->moduleFiles = \Drupal::state()->get('system.module.files') ?: array();
$this->themeFiles = \Drupal::state()->get('system.theme.files') ?: array();
$this->themeData = \Drupal::state()->get('system.theme.data') ?: array();
}
}
/**
* Sets up Drupal unit test environment.
*/
protected function setUp() {
$this->keyValueFactory = new KeyValueMemoryFactory();
parent::setUp();
......
......@@ -84,22 +84,6 @@ abstract class TestBase {
*/
protected $skipClasses = array(__CLASS__ => TRUE);
/**
* Flag to indicate whether the test has been set up.
*
* The setUp() method isolates the test from the parent Drupal site by
* creating a random prefix for the database and setting up a clean file
* storage directory. The tearDown() method then cleans up this test
* environment. We must ensure that setUp() has been run. Otherwise,
* tearDown() will act on the parent Drupal site rather than the test
* environment, destroying live data.
*/
protected $setup = FALSE;
protected $setupDatabasePrefix = FALSE;
protected $setupEnvironment = FALSE;
/**
* TRUE if verbose debugging is enabled.
*
......@@ -790,20 +774,50 @@ public function run(array $methods = array()) {
'function' => $class . '->' . $method . '()',
);
$completion_check_id = TestBase::insertAssert($this->testId, $class, FALSE, t('The test did not complete due to a fatal error.'), 'Completion check', $caller);
$this->setUp();
if ($this->setup) {
try {
$this->$method();
// Finish up.
}
catch (\Exception $e) {
$this->exceptionHandler($e);
}
try {
$this->prepareEnvironment();
}
catch (\Exception $e) {
$this->exceptionHandler($e);
// The prepareEnvironment() method isolates the test from the parent
// Drupal site by creating a random database prefix and test site
// directory. If this fails, a test would possibly operate in the
// parent site. Therefore, the entire test run for this test class
// has to be aborted.
// restoreEnvironment() cannot be called, because we do not know
// where exactly the environment setup failed.
break;
}
try {
$this->setUp();
}
catch (\Exception $e) {
$this->exceptionHandler($e);
// Abort if setUp() fails, since all test methods will fail.
// But ensure to clean up and restore the environment, since
// prepareEnvironment() succeeded.
$this->restoreEnvironment();
break;
}
try {
$this->$method();
}
catch (\Exception $e) {
$this->exceptionHandler($e);
}
try {
$this->tearDown();
}
else {
$this->fail(t("The test cannot be executed because it has not been set up properly."));
catch (\Exception $e) {
$this->exceptionHandler($e);
// If a test fails to tear down, abort the entire test class, since
// it is likely that all tests will fail in the same way and a
// failure here only results in additional test artifacts that have
// to be manually deleted.
$this->restoreEnvironment();
break;
}
$this->restoreEnvironment();
// Remove the completion check record.
TestBase::deleteAssert($completion_check_id);
}
......@@ -835,34 +849,30 @@ public function run(array $methods = array()) {
*
* @see WebTestBase::curlInitialize()
* @see drupal_valid_test_ua()
* @see WebTestBase::setUp()
*/
protected function prepareDatabasePrefix() {
private function prepareDatabasePrefix() {
$this->databasePrefix = 'simpletest' . mt_rand(1000, 1000000);
// 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.
db_update('simpletest_test_id')
$affected_rows = db_update('simpletest_test_id')
->fields(array('last_prefix' => $this->databasePrefix))
->condition('test_id', $this->testId)
->execute();
if (!$affected_rows) {
throw new \RuntimeException('Failed to set up database prefix.');
}
}
/**
* Changes the database connection to the prefixed one.
*
* @see WebTestBase::setUp()
* @see TestBase::prepareEnvironment()
*/
protected function changeDatabasePrefix() {
private function changeDatabasePrefix() {
if (empty($this->databasePrefix)) {
$this->prepareDatabasePrefix();
// If $this->prepareDatabasePrefix() failed to work, return without
// setting $this->setupDatabasePrefix to TRUE, so setUp() methods will
// know to bail out.
if (empty($this->databasePrefix)) {
return;
}
}
// Clone the current connection and replace the current prefix.
......@@ -882,9 +892,16 @@ protected function changeDatabasePrefix() {
// @todo Fix installer to use Database connection info.
global $databases;
$databases['default']['default'] = $connection_info['default'];
}
// Indicate the database prefix was set up correctly.
$this->setupDatabasePrefix = TRUE;
/**
* Act on global state information before the environment is altered for a test.
*
* Allows e.g. DrupalUnitTestBase to prime system/extension info from the
* parent site (and inject it into the test environment so as to improve
* performance).
*/
protected function beforePrepareEnvironment() {
}
/**
......@@ -892,15 +909,26 @@ protected function changeDatabasePrefix() {
*
* Backups various current environment variables and resets them, so they do
* not interfere with the Drupal site installation in which tests are executed
* and can be restored in TestBase::tearDown().
* and can be restored in TestBase::restoreEnvironment().
*
* Also sets up new resources for the testing environment, such as the public
* filesystem and configuration directories.
*
* @see TestBase::tearDown()
* This method is private as it must only be called once by TestBase::run()
* (multiple invocations for the same test would have unpredictable
* consequences) and it must not be callable or overridable by test classes.
*
* @see TestBase::beforePrepareEnvironment()
*/
protected function prepareEnvironment() {
private function prepareEnvironment() {
global $user, $conf;
// Allow (base) test classes to backup global state information.
$this->beforePrepareEnvironment();
// Create the database prefix for this test.
$this->prepareDatabasePrefix();
$language_interface = language(Language::TYPE_INTERFACE);
// When running the test runner within a test, back up the original database
......@@ -965,6 +993,9 @@ protected function prepareEnvironment() {
// Reset statics before the old container is replaced so that objects with a
// __destruct() method still have access to it.
// All static variables need to be reset before the database prefix is
// changed, since \Drupal\Core\Utility\CacheArray implementations attempt to
// write back to persistent caches when they are destructed.
// @todo: Remove once they have been converted to services.
drupal_static_reset();
......@@ -998,8 +1029,13 @@ protected function prepareEnvironment() {
$test_info['test_run_id'] = $this->databasePrefix;
$test_info['in_child_site'] = FALSE;
// Indicate the environment was set up correctly.
$this->setupEnvironment = TRUE;
// Change the database prefix.
$this->changeDatabasePrefix();
// Reset all variables to perform tests in a clean environment.
$conf = array();
drupal_set_time_limit($this->timeLimit);
}
/**
......@@ -1037,7 +1073,7 @@ protected function prepareConfigDirectories() {
* old module list.
*
* @see TestBase::prepareEnvironment()
* @see TestBase::tearDown()
* @see TestBase::restoreEnvironment()
*
* @todo Fix http://drupal.org/node/1708692 so that module enable/disable
* changes are immediately reflected in \Drupal::getContainer(). Until then,
......@@ -1060,6 +1096,12 @@ protected function rebuildContainer() {
/**
* Performs cleanup tasks after each individual test method has been run.
*/
protected function tearDown() {
}
/**
* Cleans up the test environment and restores the original environment.
*
* Deletes created files, database tables, and reverts environment changes.
*
......@@ -1069,7 +1111,7 @@ protected function rebuildContainer() {
* @see TestBase::changeDatabasePrefix()
* @see TestBase::prepareEnvironment()
*/
protected function tearDown() {
private function restoreEnvironment() {
global $user, $conf;
// Reset all static variables.
......@@ -1088,22 +1130,20 @@ protected function tearDown() {
}
}
// Ensure that TestBase::changeDatabasePrefix() has run and TestBase::$setup
// was not tricked into TRUE, since the following code would delete the
// entire parent site otherwise.
if ($this->setupDatabasePrefix) {
// Remove all prefixed tables.
$connection_info = Database::getConnectionInfo('default');
$tables = db_find_tables($connection_info['default']['prefix']['default'] . '%');
$prefix_length = strlen($connection_info['default']['prefix']['default']);
// Remove all prefixed tables.
// @todo Connection prefix info is not normalized into an array.
$original_connection_info = Database::getConnectionInfo('simpletest_original_default');
$original_prefix = is_array($original_connection_info['default']['prefix']) ? $original_connection_info['default']['prefix']['default'] : $original_connection_info['default']['prefix'];
$test_connection_info = Database::getConnectionInfo('default');
$test_prefix = is_array($test_connection_info['default']['prefix']) ? $test_connection_info['default']['prefix']['default'] : $test_connection_info['default']['prefix'];
if ($original_prefix != $test_prefix) {
$tables = Database::getConnection()->schema()->findTables($test_prefix . '%');
$prefix_length = strlen($test_prefix);
foreach ($tables as $table) {
if (db_drop_table(substr($table, $prefix_length))) {
if (Database::getConnection()->schema()->dropTable(substr($table, $prefix_length))) {
unset($tables[$table]);
}
}
if (!empty($tables)) {
$this->fail('Failed to drop all prefixed tables.');
}
}
// In case a fatal error occurred that was not in the test process read the
......@@ -1379,8 +1419,9 @@ public static function generatePermutations($parameters) {
/**
* Ensures test files are deletable within file_unmanaged_delete_recursive().
*
* Some tests chmod generated files to be read only. During tearDown() and
* other cleanup operations, these files need to get deleted too.
* Some tests chmod generated files to be read only. During
* TestBase::restoreEnvironment() and other cleanup operations, these files
* need to get deleted too.
*/
public static function filePreDeleteCallback($path) {
chmod($path, 0700);
......
......@@ -46,41 +46,74 @@ function setUp() {
}
// If the test is being run from within simpletest, set up the broken test.
else {
$this->pass(t('The test setUp() method has been run.'));
// Don't call parent::setUp(). This should trigger an error message.
if (file_get_contents($this->originalFileDirectory . '/simpletest/trigger') === 'setup') {
throw new \Exception('Broken setup');
}
$this->pass('The setUp() method has run.');
}
}
function tearDown() {
// If the test is being run from the main site, tear down normally.
if (!drupal_valid_test_ua()) {
unlink($this->originalFileDirectory . '/simpletest/trigger');
parent::tearDown();
}
// If the test is being run from within simpletest, output a message.
else {
// If the test is being run from within simpletest, output a message.
$this->pass(t('The tearDown() method has run.'));
if (file_get_contents($this->originalFileDirectory . '/simpletest/trigger') === 'teardown') {
throw new \Exception('Broken teardown');
}
$this->pass('The tearDown() method has run.');
}
}
/**
* Runs this test case from within the simpletest child site.
*/
function testBreakSetUp() {
function testMethod() {
// If the test is being run from the main site, run it again from the web
// interface within the simpletest child site.
if (!drupal_valid_test_ua()) {
// Verify that a broken setUp() method is caught.
file_put_contents($this->originalFileDirectory . '/simpletest/trigger', 'setup');
$edit['Drupal\simpletest\Tests\BrokenSetUpTest'] = TRUE;
$this->drupalPostForm('admin/config/development/testing', $edit, t('Run tests'));
$this->assertRaw('Broken setup');
$this->assertNoRaw('The setUp() method has run.');
$this->assertNoRaw('Broken test');
$this->assertNoRaw('The test method has run.');
$this->assertNoRaw('Broken teardown');
$this->assertNoRaw('The tearDown() method has run.');
// Verify that a broken tearDown() method is caught.
file_put_contents($this->originalFileDirectory . '/simpletest/trigger', 'teardown');
$edit['Drupal\simpletest\Tests\BrokenSetUpTest'] = TRUE;
$this->drupalPostForm('admin/config/development/testing', $edit, t('Run tests'));
$this->assertNoRaw('Broken setup');
$this->assertRaw('The setUp() method has run.');
$this->assertNoRaw('Broken test');
$this->assertRaw('The test method has run.');
$this->assertRaw('Broken teardown');
$this->assertNoRaw('The tearDown() method has run.');
// Verify that the broken test and its tearDown() method are skipped.
$this->assertRaw(t('The test setUp() method has been run.'));
$this->assertRaw(t('The test cannot be executed because it has not been set up properly.'));
$this->assertNoRaw(t('The test method has run.'));
$this->assertNoRaw(t('The tearDown() method has run.'));
// Verify that a broken test method is caught.
file_put_contents($this->originalFileDirectory . '/simpletest/trigger', 'test');
$edit['Drupal\simpletest\Tests\BrokenSetUpTest'] = TRUE;
$this->drupalPostForm('admin/config/development/testing', $edit, t('Run tests'));
$this->assertNoRaw('Broken setup');
$this->assertRaw('The setUp() method has run.');
$this->assertRaw('Broken test');
$this->assertNoRaw('The test method has run.');
$this->assertNoRaw('Broken teardown');
$this->assertRaw('The tearDown() method has run.');
}
// If the test is being run from within simpletest, output a message.
else {
$this->pass(t('The test method has run.'));
if (file_get_contents($this->originalFileDirectory . '/simpletest/trigger') === 'test') {
throw new \Exception('Broken test');
}
$this->pass('The test method has run.');
}
}
}
......@@ -41,32 +41,6 @@ function __construct($test_id = NULL) {
* setUp() method.
*/
protected function setUp() {
global $conf;
// Create the database prefix for this test.
$this->prepareDatabasePrefix();
// Prepare the environment for running tests.
$this->prepareEnvironment();
if (!$this->setupEnvironment) {
return FALSE;
}
// Reset all statics and variables to perform tests in a clean environment.
$conf = array();
drupal_static_reset();
$this->settingsSet('file_public_path', $this->public_files_directory);
// Change the database prefix.
// All static variables need to be reset before the database prefix is
// changed, since \Drupal\Core\Utility\CacheArray implementations attempt to
// write back to persistent caches when they are destructed.
$this->changeDatabasePrefix();
if (!$this->setupDatabasePrefix) {
return FALSE;
}
$this->setup = TRUE;
}
}
......@@ -698,27 +698,19 @@ protected function drupalLogout() {
/**
* Sets up a Drupal site for running functional and integration tests.
*
* Generates a random database prefix and installs Drupal with the specified
* installation profile in \Drupal\simpletest\WebTestBase::$profile into the
* prefixed database. Afterwards, installs any additional modules specified by
* the test.
* Installs Drupal with the installation profile specified in
* \Drupal\simpletest\WebTestBase::$profile into the prefixed database.
* Afterwards, installs any additional modules specified in the static
* \Drupal\simpletest\WebTestBase::$modules property of each class in the
* class hierarchy.
*
* After installation all caches are flushed and several configuration values
* are reset to the values of the parent site executing the test, since the
* default values may be incompatible with the environment in which tests are
* being executed.
*
* @param ...
* List of modules to enable for the duration of the test. This can be
* either a single array or a variable number of string arguments.
*
* @see \Drupal\simpletest\WebTestBase::prepareDatabasePrefix()
* @see \Drupal\simpletest\WebTestBase::changeDatabasePrefix()
* @see \Drupal\simpletest\WebTestBase::prepareEnvironment()
*/
protected function setUp() {
global $conf;
// When running tests through the Simpletest UI (vs. on the command line),
// Simpletest's batch conflicts with the installer's batch. Batch API does
// not support the concept of nested batches (in which the nested is not
......@@ -726,33 +718,6 @@ protected function setUp() {
// Backup the currently running Simpletest batch.
$this->originalBatch = batch_get();
// Create the database prefix for this test.
$this->prepareDatabasePrefix();
// Prepare the environment for running tests.
$this->prepareEnvironment();
if (!$this->setupEnvironment) {
return FALSE;
}
// Reset all statics and variables to perform tests in a clean environment.
$conf = array();
drupal_static_reset();
// Change the database prefix.
// All static variables need to be reset before the database prefix is
// changed, since \Drupal\Core\Utility\CacheArray implementations attempt to
// write back to persistent caches when they are destructed.
$this->changeDatabasePrefix();
if (!$this->setupDatabasePrefix) {
return FALSE;
}
// Set the 'simpletest_parent_profile' variable to add the parent profile's
// search path to the child site's search paths.
// @see drupal_system_listing()
$conf['simpletest_parent_profile'] = $this->originalProfile;
// Define information about the user 1 account.
$this->root_user = new UserSession(array(
'uid' => 1,
......@@ -846,14 +811,12 @@ protected function setUp() {
// Use the test mail class instead of the default mail handler class.
\Drupal::config('system.mail')->set('interface.default', 'Drupal\Core\Mail\TestMailCollector')->save();
drupal_set_time_limit($this->timeLimit);
// Temporary fix so that when running from run-tests.sh we don't get an
// empty current path which would indicate we're on the home page.
$path = current_path();
if (empty($path)) {
_current_path('run-tests');
}
$this->setup = TRUE;
}
/**
......
......@@ -15,6 +15,13 @@
*/
class InstallerTranslationTest extends InstallerTest {
/**
* Whether the installer has completed.
*
* @var bool
*/
protected $isInstalled = FALSE;
public static function getInfo() {
return array(
'name' => 'Installer translation test',
......@@ -24,41 +31,8 @@ public static function getInfo() {
}
protected function setUp() {
global $conf;
// When running tests through the SimpleTest UI (vs. on the command line),
// SimpleTest's batch conflicts with the installer's batch. Batch API does
// not support the concept of nested batches (in which the nested is not
// progressive), so we need to temporarily pretend there was no batch.
// Back up the currently running SimpleTest batch.
$this->originalBatch = batch_get();
// Add the translations directory so we can retrieve German translations.
$conf['locale.settings']['translation.path'] = drupal_get_path('module', 'simpletest') . '/files/translations';
$conf['language_default']['name'] = 'German';
$conf['language_default']['id'] = 'de';
// Create the database prefix for this test.
$this->prepareDatabasePrefix();
// Prepare the environment for running tests.
$this->prepareEnvironment();
if (!$this->setupEnvironment) {
return FALSE;
}
$this->isInstalled = FALSE;
// Reset all statics and variables to perform tests in a clean environment.
$conf = array();
drupal_static_reset();
// Change the database prefix.
// All static variables need to be reset before the database prefix is
// changed, since \Drupal\Core\Utility\CacheArray implementations attempt to
// write back to persistent caches when they are destructed.
$this->changeDatabasePrefix();
if (!$this->setupDatabasePrefix) {
return FALSE;
}
$variable_groups = array(
'system.file' => array(
'path.private' => $this->private_files_directory,
......@@ -134,14 +108,14 @@ protected function setUp() {
// Use the test mail class instead of the default mail handler class.
\Drupal::config('system.mail')->set('interface.default', 'Drupal\Core\Mail\TestMailCollector')->save();
drupal_set_time_limit($this->timeLimit);
// When running from run-tests.sh we don't get an empty current path which
// would indicate we're on the home page.
$path = current_path();
if (empty($path)) {
_current_path('run-tests');
}
$this->setup = TRUE;
$this->isInstalled = TRUE;
}
}
......@@ -15,6 +15,13 @@
*/
class InstallerTest extends WebTestBase {
/**
* Whether the installer has completed.
*
* @var bool
*/
protected $isInstalled = FALSE;
public static function getInfo() {
return array(
'name' => 'Installer tests',
......@@ -24,36 +31,8 @@ public static function getInfo() {
}
protected function setUp() {
global $conf;
// When running tests through the SimpleTest UI (vs. on the command line),
// SimpleTest's batch conflicts with the installer's batch. Batch API does
// not support the concept of nested batches (in which the nested is not
// progressive), so we need to temporarily pretend there was no batch.
// Back up the currently running SimpleTest batch.
$this->originalBatch =