From 5b0715d78b4e26e86166ef4793a1ebf738e1d05c Mon Sep 17 00:00:00 2001 From: catch <catch@35733.no-reply.drupal.org> Date: Thu, 31 May 2012 20:55:32 +0900 Subject: [PATCH] Issue #1563620 by sun, Berdir: Fixed All unit tests blow up with a fatal error. --- core/includes/module.inc | 9 +- .../lib/Drupal/simpletest/TestBase.php | 175 ++++++++++++++++++ .../lib/Drupal/simpletest/UnitTestBase.php | 67 +++---- .../lib/Drupal/simpletest/WebTestBase.php | 154 +-------------- 4 files changed, 208 insertions(+), 197 deletions(-) diff --git a/core/includes/module.inc b/core/includes/module.inc index 6b4604a7b21d..928abc93642e 100644 --- a/core/includes/module.inc +++ b/core/includes/module.inc @@ -79,16 +79,19 @@ function module_list($refresh = FALSE, $bootstrap_refresh = FALSE, $sort = FALSE // Use the advanced drupal_static() pattern, since this is called very often. static $drupal_static_fast; if (!isset($drupal_static_fast)) { - $drupal_static_fast['list'] = &drupal_static(__FUNCTION__ . ':list', array()); + $drupal_static_fast['list'] = &drupal_static(__FUNCTION__ . ':list'); $drupal_static_fast['sorted_list'] = &drupal_static(__FUNCTION__ . ':sorted_list'); } $list = &$drupal_static_fast['list']; $sorted_list = &$drupal_static_fast['sorted_list']; - if (empty($list) || $refresh || $fixed_list) { + if (!isset($list) || $refresh || isset($fixed_list)) { $list = array(); $sorted_list = NULL; - if ($fixed_list) { + // The fixed list may be a completely empty array, thus check for isset(). + // Calling code may use this to empty out the module list entirely. For + // example, unit tests need to ensure that no modules are invoked. + if (isset($fixed_list)) { foreach ($fixed_list as $name => $module) { drupal_get_filename('module', $name, $module['filename']); $list[$name] = $name; diff --git a/core/modules/simpletest/lib/Drupal/simpletest/TestBase.php b/core/modules/simpletest/lib/Drupal/simpletest/TestBase.php index 552b4e35c4cb..aa5f233790a0 100644 --- a/core/modules/simpletest/lib/Drupal/simpletest/TestBase.php +++ b/core/modules/simpletest/lib/Drupal/simpletest/TestBase.php @@ -539,6 +539,181 @@ public function run(array $methods = array()) { restore_error_handler(); } + /** + * Generates a database prefix for running tests. + * + * The database prefix is used by prepareEnvironment() to setup a public files + * directory for the test to be run, which also contains the PHP error log, + * which is written to in case of a fatal error. Since that directory is based + * on the database prefix, all tests (even unit tests) need to have one, in + * order to access and read the error log. + * + * @see TestBase::prepareEnvironment() + * + * The generated database table prefix is used for the Drupal installation + * being performed for the test. It is also used as user agent HTTP header + * value by the cURL-based browser of DrupalWebTestCase, which is sent to the + * Drupal installation of the test. During early Drupal bootstrap, the user + * agent HTTP header is parsed, and if it matches, all database queries use + * the database table prefix that has been generated here. + * + * @see WebTestBase::curlInitialize() + * @see drupal_valid_test_ua() + * @see WebTestBase::setUp() + */ + protected 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') + ->fields(array('last_prefix' => $this->databasePrefix)) + ->condition('test_id', $this->testId) + ->execute(); + } + + /** + * Changes the database connection to the prefixed one. + * + * @see WebTestBase::setUp() + */ + protected function changeDatabasePrefix() { + if (empty($this->databasePrefix)) { + $this->prepareDatabasePrefix(); + } + + // Clone the current connection and replace the current prefix. + $connection_info = Database::getConnectionInfo('default'); + Database::renameConnection('default', 'simpletest_original_default'); + foreach ($connection_info as $target => $value) { + $connection_info[$target]['prefix'] = array( + 'default' => $value['prefix']['default'] . $this->databasePrefix, + ); + } + Database::addConnectionInfo('default', 'default', $connection_info['default']); + } + + /** + * Prepares the current environment for running the test. + * + * 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(). + * + * Also sets up new resources for the testing environment, such as the public + * filesystem and configuration directories. + * + * @see TestBase::tearDown() + */ + protected function prepareEnvironment() { + global $user, $language_interface, $conf; + + // Backup current in-memory configuration. + $this->originalConf = $conf; + + // Backup statics and globals. + $this->originalContainer = clone drupal_container(); + $this->originalLanguage = $language_interface; + $this->originalConfigDirectory = $GLOBALS['config_directory_name']; + + // Save further contextual information. + $this->originalFileDirectory = variable_get('file_public_path', conf_path() . '/files'); + $this->originalProfile = drupal_get_profile(); + $this->originalUser = $user; + + // Save and clean the shutdown callbacks array because it is static cached + // and will be changed by the test run. Otherwise it will contain callbacks + // from both environments and the testing environment will try to call the + // handlers defined by the original one. + $callbacks = &drupal_register_shutdown_function(); + $this->originalShutdownCallbacks = $callbacks; + $callbacks = array(); + + // Create test directory ahead of installation so fatal errors and debug + // information can be logged during installation process. + // Use temporary files directory with the same prefix as the database. + $this->public_files_directory = $this->originalFileDirectory . '/simpletest/' . substr($this->databasePrefix, 10); + $this->private_files_directory = $this->public_files_directory . '/private'; + $this->temp_files_directory = $this->private_files_directory . '/temp'; + + // Create the directories + file_prepare_directory($this->public_files_directory, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS); + file_prepare_directory($this->private_files_directory, FILE_CREATE_DIRECTORY); + file_prepare_directory($this->temp_files_directory, FILE_CREATE_DIRECTORY); + $this->generatedTestFiles = FALSE; + + // Create and set a new configuration directory and signature key. + // The child site automatically adjusts the global $config_directory_name to + // a test-prefix-specific directory within the public files directory. + // @see config_get_config_directory() + $GLOBALS['config_directory_name'] = 'simpletest/' . substr($this->databasePrefix, 10) . '/config'; + $this->configFileDirectory = $this->originalFileDirectory . '/' . $GLOBALS['config_directory_name']; + file_prepare_directory($this->configFileDirectory, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS); + + // Log fatal errors. + ini_set('log_errors', 1); + ini_set('error_log', $this->public_files_directory . '/error.log'); + + // Set the test information for use in other parts of Drupal. + $test_info = &$GLOBALS['drupal_test_info']; + $test_info['test_run_id'] = $this->databasePrefix; + $test_info['in_child_site'] = FALSE; + } + + /** + * Deletes created files, database tables, and reverts all environment changes. + * + * This method needs to be invoked for both unit and integration tests. + * + * @see TestBase::prepareDatabasePrefix() + * @see TestBase::changeDatabasePrefix() + * @see TestBase::prepareEnvironment() + */ + protected function tearDown() { + global $user, $language_interface, $conf; + + // In case a fatal error occurred that was not in the test process read the + // log to pick up any fatal errors. + simpletest_log_read($this->testId, $this->databasePrefix, get_class($this), TRUE); + + $emailCount = count(variable_get('drupal_test_email_collector', array())); + if ($emailCount) { + $message = format_plural($emailCount, '1 e-mail was sent during this test.', '@count e-mails were sent during this test.'); + $this->pass($message, t('E-mail')); + } + + // Delete temporary files directory. + file_unmanaged_delete_recursive($this->originalFileDirectory . '/simpletest/' . substr($this->databasePrefix, 10)); + + // Restore original database connection. + Database::removeConnection('default'); + Database::renameConnection('simpletest_original_default', 'default'); + + // Reset all static variables. + drupal_static_reset(); + + // Restore has_run state. + $has_run = &drupal_static('module_load_all'); + $has_run = TRUE; + + // Restore original in-memory configuration. + $conf = $this->originalConf; + + // Restore original statics and globals. + drupal_container($this->originalContainer); + $language_interface = $this->originalLanguage; + $GLOBALS['config_directory_name'] = $this->originalConfigDirectory; + + // Restore original shutdown callbacks. + $callbacks = &drupal_register_shutdown_function(); + $callbacks = $this->originalShutdownCallbacks; + + // Restore original user session. + $user = $this->originalUser; + drupal_save_session(TRUE); + } + /** * Handle errors during test runs. * diff --git a/core/modules/simpletest/lib/Drupal/simpletest/UnitTestBase.php b/core/modules/simpletest/lib/Drupal/simpletest/UnitTestBase.php index 5b5850be2282..b5314cecb6d7 100644 --- a/core/modules/simpletest/lib/Drupal/simpletest/UnitTestBase.php +++ b/core/modules/simpletest/lib/Drupal/simpletest/UnitTestBase.php @@ -38,55 +38,40 @@ function __construct($test_id = NULL) { protected function setUp() { global $conf; - // Store necessary current values before switching to the test environment. - $this->originalFileDirectory = variable_get('file_public_path', conf_path() . '/files'); + // Create the database prefix for this test. + $this->prepareDatabasePrefix(); - // Reset all statics so that test is performed with a clean environment. + // Prepare the environment for running tests. + $this->prepareEnvironment(); + $this->originalThemeRegistry = theme_get_registry(FALSE); + + // Reset all statics and variables to perform tests in a clean environment. + $conf = array(); drupal_static_reset(); - // Generate temporary prefixed database to ensure that tests have a clean starting point. - $this->databasePrefix = Database::getConnection()->prefixTables('{simpletest' . mt_rand(1000, 1000000) . '}'); + // Empty out module list. + module_list(TRUE, FALSE, FALSE, array()); + // Prevent module_load_all() from attempting to refresh it. + $has_run = &drupal_static('module_load_all'); + $has_run = TRUE; + + // Re-implant theme registry. + // Required for l() and other functions to work correctly and not trigger + // database lookups. + $theme_get_registry = &drupal_static('theme_get_registry'); + $theme_get_registry[FALSE] = $this->originalThemeRegistry; - // Create test directory. - $public_files_directory = $this->originalFileDirectory . '/simpletest/' . substr($this->databasePrefix, 10); - file_prepare_directory($public_files_directory, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS); - $conf['file_public_path'] = $public_files_directory; + $conf['file_public_path'] = $this->public_files_directory; - // Clone the current connection and replace the current prefix. - $connection_info = Database::getConnectionInfo('default'); - Database::renameConnection('default', 'simpletest_original_default'); - foreach ($connection_info as $target => $value) { - $connection_info[$target]['prefix'] = array( - 'default' => $value['prefix']['default'] . $this->databasePrefix, - ); - } - Database::addConnectionInfo('default', 'default', $connection_info['default']); + // 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(); - // Set user agent to be consistent with web test case. + // Set user agent to be consistent with WebTestBase. $_SERVER['HTTP_USER_AGENT'] = $this->databasePrefix; - // If locale is enabled then t() will try to access the database and - // subsequently will fail as the database is not accessible. - $module_list = module_list(); - if (isset($module_list['locale'])) { - $this->originalModuleList = $module_list; - unset($module_list['locale']); - module_list(TRUE, FALSE, FALSE, $module_list); - } $this->setup = TRUE; } - - protected function tearDown() { - global $conf; - - // Get back to the original connection. - Database::removeConnection('default'); - Database::renameConnection('simpletest_original_default', 'default'); - - $conf['file_public_path'] = $this->originalFileDirectory; - // Restore modules if necessary. - if (isset($this->originalModuleList)) { - module_list(TRUE, FALSE, FALSE, $this->originalModuleList); - } - } } diff --git a/core/modules/simpletest/lib/Drupal/simpletest/WebTestBase.php b/core/modules/simpletest/lib/Drupal/simpletest/WebTestBase.php index 33b26e32bd01..e6779f79cf0f 100644 --- a/core/modules/simpletest/lib/Drupal/simpletest/WebTestBase.php +++ b/core/modules/simpletest/lib/Drupal/simpletest/WebTestBase.php @@ -534,117 +534,6 @@ protected function drupalLogout() { } } - /** - * Generates a database prefix for running tests. - * - * The generated database table prefix is used for the Drupal installation - * being performed for the test. It is also used as user agent HTTP header - * value by the cURL-based browser of Drupal\simpletest\WebTestBase, which is sent - * to the Drupal installation of the test. During early Drupal bootstrap, the - * user agent HTTP header is parsed, and if it matches, all database queries - * use the database table prefix that has been generated here. - * - * @see Drupal\simpletest\WebTestBase::curlInitialize() - * @see drupal_valid_test_ua() - * @see Drupal\simpletest\WebTestBase::setUp() - */ - protected 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') - ->fields(array('last_prefix' => $this->databasePrefix)) - ->condition('test_id', $this->testId) - ->execute(); - } - - /** - * Changes the database connection to the prefixed one. - * - * @see Drupal\simpletest\WebTestBase::setUp() - */ - protected function changeDatabasePrefix() { - if (empty($this->databasePrefix)) { - $this->prepareDatabasePrefix(); - } - - // Clone the current connection and replace the current prefix. - $connection_info = Database::getConnectionInfo('default'); - Database::renameConnection('default', 'simpletest_original_default'); - foreach ($connection_info as $target => $value) { - $connection_info[$target]['prefix'] = array( - 'default' => $value['prefix']['default'] . $this->databasePrefix, - ); - } - Database::addConnectionInfo('default', 'default', $connection_info['default']); - } - - /** - * Prepares the current environment for running the test. - * - * 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 tearDown(). - * - * Also sets up new resources for the testing environment, such as the public - * filesystem and configuration directories. - * - * @see Drupal\simpletest\WebTestBase::setUp() - * @see Drupal\simpletest\WebTestBase::tearDown() - */ - protected function prepareEnvironment() { - global $user, $language_interface, $conf; - - // Store necessary current values before switching to prefixed database. - $this->originalContainer = clone drupal_container(); - $this->originalLanguage = $language_interface; - $this->originalLanguageDefault = variable_get('language_default'); - $this->originalConfigDirectory = $GLOBALS['config_directory_name']; - $this->originalFileDirectory = variable_get('file_public_path', conf_path() . '/files'); - $this->originalProfile = drupal_get_profile(); - $this->originalUser = $user; - - // Save and clean the shutdown callbacks array because it is static cached - // and will be changed by the test run. Otherwise it will contain callbacks - // from both environments and the testing environment will try to call the - // handlers defined by the original one. - $callbacks = &drupal_register_shutdown_function(); - $this->originalShutdownCallbacks = $callbacks; - $callbacks = array(); - - // Create test directory ahead of installation so fatal errors and debug - // information can be logged during installation process. - // Use temporary files directory with the same prefix as the database. - $this->public_files_directory = $this->originalFileDirectory . '/simpletest/' . substr($this->databasePrefix, 10); - $this->private_files_directory = $this->public_files_directory . '/private'; - $this->temp_files_directory = $this->private_files_directory . '/temp'; - - // Create the directories - file_prepare_directory($this->public_files_directory, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS); - file_prepare_directory($this->private_files_directory, FILE_CREATE_DIRECTORY); - file_prepare_directory($this->temp_files_directory, FILE_CREATE_DIRECTORY); - $this->generatedTestFiles = FALSE; - - // Create and set a new configuration directory and signature key. - // The child site automatically adjusts the global $config_directory_name to - // a test-prefix-specific directory within the public files directory. - // @see config_get_config_directory() - $GLOBALS['config_directory_name'] = 'simpletest/' . substr($this->databasePrefix, 10) . '/config'; - $this->configFileDirectory = $this->originalFileDirectory . '/' . $GLOBALS['config_directory_name']; - file_prepare_directory($this->configFileDirectory, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS); - - // Log fatal errors. - ini_set('log_errors', 1); - ini_set('error_log', $this->public_files_directory . '/error.log'); - - // Set the test information for use in other parts of Drupal. - $test_info = &$GLOBALS['drupal_test_info']; - $test_info['test_run_id'] = $this->databasePrefix; - $test_info['in_child_site'] = FALSE; - } - /** * Sets up a Drupal site for running functional and integration tests. * @@ -841,21 +730,6 @@ protected function refreshVariables() { * and reset the database prefix. */ protected function tearDown() { - global $user, $language_interface; - - // In case a fatal error occurred that was not in the test process read the - // log to pick up any fatal errors. - simpletest_log_read($this->testId, $this->databasePrefix, get_class($this), TRUE); - - $emailCount = count(variable_get('drupal_test_email_collector', array())); - if ($emailCount) { - $message = format_plural($emailCount, '1 e-mail was sent during this test.', '@count e-mails were sent during this test.'); - $this->pass($message, t('E-mail')); - } - - // Delete temporary files directory. - file_unmanaged_delete_recursive($this->originalFileDirectory . '/simpletest/' . substr($this->databasePrefix, 10)); - // Remove all prefixed tables. $connection_info = Database::getConnectionInfo('default'); $tables = db_find_tables($connection_info['default']['prefix']['default'] . '%'); @@ -872,21 +746,7 @@ protected function tearDown() { $this->fail('Failed to drop all prefixed tables.'); } - // Get back to the original connection. - Database::removeConnection('default'); - Database::renameConnection('simpletest_original_default', 'default'); - - // Restore the original dependency injection container. - drupal_container($this->originalContainer); - - // Restore original shutdown callbacks array to prevent original - // environment of calling handlers from test run. - $callbacks = &drupal_register_shutdown_function(); - $callbacks = $this->originalShutdownCallbacks; - - // Return the user to the original one. - $user = $this->originalUser; - drupal_save_session(TRUE); + parent::tearDown(); // Ensure that internal logged in variable and cURL options are reset. $this->loggedInUser = FALSE; @@ -903,18 +763,6 @@ protected function tearDown() { // Rebuild caches. $this->refreshVariables(); - // Reset public files directory. - $GLOBALS['conf']['file_public_path'] = $this->originalFileDirectory; - - // Reset configuration globals. - $GLOBALS['config_directory_name'] = $this->originalConfigDirectory; - - // Reset language. - $language_interface = $this->originalLanguage; - if ($this->originalLanguageDefault) { - $GLOBALS['conf']['language_default'] = $this->originalLanguageDefault; - } - // Close the CURL handler. $this->curlClose(); } -- GitLab