diff --git a/modules/simpletest/drupal_web_test_case.php b/modules/simpletest/drupal_web_test_case.php index 72cd31e..0b6d5ab 100644 --- a/modules/simpletest/drupal_web_test_case.php +++ b/modules/simpletest/drupal_web_test_case.php @@ -45,18 +45,18 @@ abstract class DrupalTestCase { protected $timeLimit = 500; /** - * Whether to cache the setUp. + * Whether to cache the installation part of the setUp() method. * * @var bool */ - public $cache = FALSE; + public $useSetupInstallationCache = FALSE; /** - * Whether to cache the setUp with modules installed. + * Whether to cache the modules installation part of the setUp() method. * * @var bool */ - public $cacheModules = FALSE; + public $useSetupModulesCache = FALSE; /** * Current results of this test case. @@ -525,7 +525,10 @@ abstract class DrupalTestCase { 'function' => $class . '->' . $method . '()', ); $completion_check_id = DrupalTestCase::insertAssert($this->testId, $class, FALSE, t('The test did not complete due to a fatal error.'), 'Completion check', $caller); + require_once DRUPAL_ROOT . '/xhprof-kit/perf.php'; + $prof = XhProfKit\XhProfKitRun::create(); $this->setUp(); + unset($prof); if ($this->setup) { try { $this->$method(); @@ -1438,54 +1441,64 @@ class DrupalWebTestCase extends DrupalTestCase { } /** - * Copies the cached tables and files for a cache entry. + * Copies the cached tables and files for a cached installation setup. * * @param string $cache_key_prefix * (optional) Additional prefix for the cache key. * - * @return - * TRUE when the cache was used, FALSE when cache was not available. + * @return bool + * TRUE when the cache was usable and loaded, FALSE when cache was not + * available. * * @see DrupalWebTestCase::setUp() */ - protected function useCache($cache_key_prefix = '') { - $cache_key = $this->getCacheKey($cache_key_prefix ); + protected function loadSetupCache($cache_key_prefix = '') { + $cache_key = $this->getSetupCacheKey($cache_key_prefix); $cache_file = $this->originalFileDirectory . '/simpletest/' . $cache_key . '/simpletest-cache-setup'; if (file_exists($cache_file)) { - $this->copyCache($cache_key, substr($this->databasePrefix, 10)); - return TRUE; + return $this->copySetupCache($cache_key, substr($this->databasePrefix, 10)); } return FALSE; } /** - * Returns the cache key used for the setUp caching. + * Returns the cache key used for the setup caching. * * @param string $cache_key_prefix * (optional) Additional prefix for the cache key. * * @return string - * The cache key to use, by default only based on profile. + * The cache key to use, by default only based on the profile used by the + * test. */ - protected function getCacheKey($cache_key_prefix = '') { - return '1cache_' . $cache_key_prefix . $this->profile; + protected function getSetupCacheKey($cache_key_prefix = '') { + // The cache key needs to start with a numeric character, so that the cached + // installation gets cleaned up properly. + $cache_key_prefix = hash('crc32b', $cache_key_prefix . $this->profile) . '_'; + return '1c_' . $cache_key_prefix . $this->profile; } /** - * Store the cache based on the cache key. + * Store the installation setup to a cache. * * @param string $cache_key_prefix * (optional) Additional prefix for the cache key. + * + * @return bool + * TRUE if the installation was stored in the cache, FALSE otherwise. */ - protected function storeCache($cache_key_prefix = '') { - $cache_key = $this->getCacheKey($cache_key_prefix); + protected function storeSetupCache($cache_key_prefix = '') { + $cache_key = $this->getSetupCacheKey($cache_key_prefix); // Use a special semaphore table that exists only in the cached tables. $cache_semaphore_table = $cache_key . 'simpletest_semaphore'; - // Everyone is allowed to pass this step. + // This function can be called concurrently when several tests try to store + // the same cache. Also a cache can be left in an unclean state in case + // that the user interrupted the copy process. Therefore failing to create + // the special cache semaphore table is expected as it might already exist. try { $semaphore_source_table = $this->databasePrefix . 'semaphore'; db_query('CREATE TABLE ' . $cache_semaphore_table . '' . ' LIKE ' . $semaphore_source_table); @@ -1493,8 +1506,10 @@ class DrupalWebTestCase extends DrupalTestCase { catch (Exception $e) { } - // Only one process will be able to insert into the cache table for each - // concurrent run. + // All concurrent tests share the same test id. Therefore it is possible to + // use the special cache semaphore table to ensure that only one process + // will store the cache. This is important as else DB tables created by one + // process could be deleted by another as the cache copying is idempotent. try { self::getDatabaseConnection() ->insert($cache_semaphore_table) @@ -1509,14 +1524,12 @@ class DrupalWebTestCase extends DrupalTestCase { return FALSE; } - // Now this step should always succeed. - try { - $this->copyCache(substr($this->databasePrefix, 10), $cache_key); - } - catch (Exception $e) { + // Try to copy the installation to the setup cache - now that we have a + // lock to do so. + if (!$this->copySetupCache(substr($this->databasePrefix, 10), $cache_key)) { // It is non-fatal if the cache cannot be copied as the next test run // will try it again. - $this->assert('debug', t('Storing cache with key @key failed', array('@key' => $cache_key)), 'storeCache'); + $this->assert('debug', t('Storing cache with key @key failed', array('@key' => $cache_key)), 'storeSetupCache'); return FALSE; } @@ -1529,39 +1542,50 @@ class DrupalWebTestCase extends DrupalTestCase { } /** - * Copy the cache from/to another table and files directory. + * Copy the setup cache from/to another table and files directory. + * + * @param string $from + * The prefix_id / cache_key from where to copy. + * @param string $to + * The prefix_id / cache_key to where to copy. * - * @param $from - * The prefixId / cacheKey from where to copy. - * @param $to - * The prefixId / cacheKey to where to copy. + * @return bool + * TRUE if the setup cache was copied to the current installation, FALSE + * otherwise. */ - protected function copyCache($from, $to) { + protected function copySetupCache($from, $to) { $from_prefix = 'simpletest' . $from; $to_prefix = 'simpletest' . $to; - $tables = db_query("SHOW TABLES LIKE :prefix", array(':prefix' => db_like($from_prefix) . '%' ))->fetchCol(); + try { + $tables = db_query("SHOW TABLES LIKE :prefix", array(':prefix' => db_like($from_prefix) . '%' ))->fetchCol(); - foreach ($tables as $from_table) { - $table = substr($from_table, strlen($from_prefix)); - $to_table = $to_prefix . $table; + foreach ($tables as $from_table) { + $table = substr($from_table, strlen($from_prefix)); + $to_table = $to_prefix . $table; - // Do not copy our internal semaphore table. - if ($table == 'simpletest_semaphore') { - continue; - } + // Do not copy our internal semaphore table. + if ($table == 'simpletest_semaphore') { + continue; + } - // Remove the table in case the copying process was interrupted. - db_query('DROP TABLE IF EXISTS ' . $to_table); - db_query('CREATE TABLE ' . $to_table . ' LIKE ' . $from_table); - db_query('ALTER TABLE ' . $to_table . ' DISABLE KEYS'); - db_query('INSERT ' . $to_table . ' SELECT * FROM ' . $from_table); - db_query('ALTER TABLE ' . $to_table . ' ENABLE KEYS'); + // Remove the table in case the copying process was interrupted. + db_query('DROP TABLE IF EXISTS ' . $to_table); + db_query('CREATE TABLE ' . $to_table . ' LIKE ' . $from_table); + db_query('ALTER TABLE ' . $to_table . ' DISABLE KEYS'); + db_query('INSERT ' . $to_table . ' SELECT * FROM ' . $from_table); + db_query('ALTER TABLE ' . $to_table . ' ENABLE KEYS'); + } + } + catch (Exception $e) { + return FALSE; } $from_dir = $this->originalFileDirectory . '/simpletest/' . $from; $to_dir = $this->originalFileDirectory . '/simpletest/' . $to; - $this->recursiveCopy($from_dir, $to_dir); + $this->recursiveDirectoryCopy($from_dir, $to_dir); + + return TRUE; } /** @@ -1572,7 +1596,7 @@ class DrupalWebTestCase extends DrupalTestCase { * @param $dest * The destination directory. */ - protected function recursiveCopy($src, $dst) { + protected function recursiveDirectoryCopy($src, $dst) { $dir = opendir($src); if (!file_exists($dst)){ @@ -1581,7 +1605,7 @@ class DrupalWebTestCase extends DrupalTestCase { while (($file = readdir($dir)) !== FALSE) { if ($file != '.' && $file != '..') { if (is_dir($src . '/' . $file)) { - $this->recursiveCopy($src . '/' . $file, $dst . '/' . $file); + $this->recursiveDirectoryCopy($src . '/' . $file, $dst . '/' . $file); } else { copy($src . '/' . $file, $dst . '/' . $file); @@ -1644,10 +1668,10 @@ class DrupalWebTestCase extends DrupalTestCase { // profile's hook_install() and other hook implementations are never invoked. $conf['install_profile'] = $this->profile; - $use_cache = FALSE; - $use_modules_cache = FALSE; + $has_installation_cache = FALSE; + $has_modules_cache = FALSE; - if ($this->cacheModules) { + if ($this->useSetupModulesCache) { $modules = func_get_args(); // Modules can be either one parameter or multiple. if (isset($modules[0]) && is_array($modules[0])) { @@ -1657,14 +1681,14 @@ class DrupalWebTestCase extends DrupalTestCase { sort($modules); $modules_cache_key_prefix = hash('crc32b', serialize($modules)) . '_'; - $use_modules_cache = $this->useCache($modules_cache_key_prefix); + $has_modules_cache = $this->loadSetupCache($modules_cache_key_prefix); } - if (!$use_modules_cache && $this->cache) { - $use_cache = $this->useCache(); + if (!$has_modules_cache && $this->useSetupInstallationCache) { + $has_installation_cache = $this->loadSetupCache(); } - if ($use_modules_cache || $use_cache) { + if ($has_modules_cache || $has_installation_cache) { // Reset path variables. variable_set('file_public_path', $this->public_files_directory); variable_set('file_private_path', $this->private_files_directory); @@ -1674,9 +1698,9 @@ class DrupalWebTestCase extends DrupalTestCase { // Load all enabled modules module_load_all(); - $this->assertTrue(TRUE, t('Using cache: @cache (@key)', array( - '@cache' => ($use_modules_cache ? 'Modules Cache' : 'Normal Cache'), - '@key' => $this->getCacheKey($use_modules_cache ? $modules_cache_key_prefix : ''), + $this->pass(t('Using cache: @cache (@key)', array( + '@cache' => $has_modules_cache ? t('Modules Cache') : t('Installation Cache'), + '@key' => $this->getSetupCacheKey($has_modules_cache ? $modules_cache_key_prefix : ''), ))); } else { @@ -1704,12 +1728,12 @@ class DrupalWebTestCase extends DrupalTestCase { // Install the modules specified by the testing profile. module_enable($profile_details['dependencies'], FALSE); - if ($this->cache) { - $this->storeCache(); + if ($this->useSetupInstallationCache) { + $this->storeSetupCache(); } } - if (!$use_modules_cache) { + if (!$has_modules_cache) { // Install modules needed for this test. This could have been passed in as // either a single array argument or a variable number of string arguments. // @todo Remove this compatibility layer in Drupal 8, and only accept @@ -1738,8 +1762,8 @@ class DrupalWebTestCase extends DrupalTestCase { // the installation process. drupal_cron_run(); - if ($this->cacheModules) { - $this->storeCache($modules_cache_key_prefix); + if ($this->useSetupModulesCache) { + $this->storeSetupCache($modules_cache_key_prefix); } } else { diff --git a/modules/simpletest/simpletest.module b/modules/simpletest/simpletest.module index b99d869..55e05a2 100644 --- a/modules/simpletest/simpletest.module +++ b/modules/simpletest/simpletest.module @@ -589,7 +589,8 @@ function simpletest_clean_temporary_directories() { $files = scandir('public://simpletest'); foreach ($files as $file) { $path = 'public://simpletest/' . $file; - if (is_dir($path) && (is_numeric($file) || strpos($file, '1cache_') === 0)) { + // Ensure that cache directories are cleaned as well. + if (is_dir($path) && (is_numeric($file) || strpos($file, '1c_') === 0)) { file_unmanaged_delete_recursive($path); $count++; } diff --git a/scripts/run-tests.sh b/scripts/run-tests.sh index 891f9ba..6e8668a 100755 --- a/scripts/run-tests.sh +++ b/scripts/run-tests.sh @@ -156,17 +156,19 @@ All arguments are long options. --verbose Output detailed assertion messages in addition to summary. - --cache Cache result of setUp per installation profile. This will create - one cache entry per profile and can be generally used. + --cache (Experimental) Cache result of setUp per installation profile. + This will create one cache entry per profile and is generally safe + to use. To clear all cache entries use --clean. --cache-modules - Cache result of setUp per installation profile and installed - modules. This will create one copy of the database tables per - module-combination and can get pretty huge. This is most useful - for local development of test cases. - This option implies --cache. To clear all cache entries use + (Experimnetal) Cache result of setUp per installation profile and + installed modules. This will create one copy of the database + tables per module-combination and therefore this option should not + be used when running all tests. This is most useful for local + development of individulat test cases. + This option implies --cache and to clear all cache entries use --clean. [,[, ...]] @@ -400,8 +402,8 @@ function simpletest_script_run_one_test($test_id, $test_class) { simpletest_classloader_register(); $test = new $test_class($test_id); - $test->cache = !empty($args['cache']); - $test->cacheModules = !empty($args['cache-modules']); + $test->useSetupInstallationCache = !empty($args['cache']); + $test->useSetupModulesCache = !empty($args['cache-modules']); $test->run(); $info = $test->getInfo();