diff --git a/core/includes/bootstrap.inc b/core/includes/bootstrap.inc index 1eb63b6..7e33d5c 100644 --- a/core/includes/bootstrap.inc +++ b/core/includes/bootstrap.inc @@ -2,10 +2,10 @@ use Drupal\Component\Utility\NestedArray; use Drupal\Core\Database\Database; +use Drupal\Core\DependencyInjection\ContainerBuilder; use Symfony\Component\ClassLoader\UniversalClassLoader; use Symfony\Component\ClassLoader\ApcUniversalClassLoader; use Symfony\Component\DependencyInjection\Container; -use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\DependencyInjection\Exception\RuntimeException as DependencyInjectionRuntimeException; use Symfony\Component\HttpFoundation\Request; diff --git a/core/includes/schema.inc b/core/includes/schema.inc index f46739c..aab8f8a 100644 --- a/core/includes/schema.inc +++ b/core/includes/schema.inc @@ -66,9 +66,9 @@ function drupal_get_schema($table = NULL, $rebuild = FALSE) { * If TRUE, the schema will be rebuilt instead of retrieved from the cache. */ function drupal_get_complete_schema($rebuild = FALSE) { - static $schema = array(); + static $schema; - if (empty($schema) || $rebuild) { + if (!isset($schema) || $rebuild) { // Try to load the schema from cache. if (!$rebuild && $cached = cache()->get('schema')) { $schema = $cached->data; @@ -100,15 +100,15 @@ function drupal_get_complete_schema($rebuild = FALSE) { _drupal_schema_initialize($current, $module); $schema = array_merge($schema, $current); } - drupal_alter('schema', $schema); + + if ($rebuild) { + cache()->invalidateTags(array('schema' => TRUE)); + } // If the schema is empty, avoid saving it: some database engines require // the schema to perform queries, and this could lead to infinite loops. if (!empty($schema) && (drupal_get_bootstrap_phase() == DRUPAL_BOOTSTRAP_FULL)) { - cache()->set('schema', $schema); - } - if ($rebuild) { - cache()->invalidateTags(array('schema' => TRUE)); + cache()->set('schema', $schema, CacheBackendInterface::CACHE_PERMANENT, array('schema' => TRUE)); } } } @@ -284,8 +284,11 @@ function drupal_get_schema_unprocessed($module, $table = NULL) { module_load_install($module); $schema = module_invoke($module, 'schema'); - if (isset($table) && isset($schema[$table])) { - return $schema[$table]; + if (isset($table)) { + if (isset($schema[$table])) { + return $schema[$table]; + } + return array(); } elseif (!empty($schema)) { return $schema; diff --git a/core/lib/Drupal/Core/DrupalKernel.php b/core/lib/Drupal/Core/DrupalKernel.php index 072e109..25c0a1a 100644 --- a/core/lib/Drupal/Core/DrupalKernel.php +++ b/core/lib/Drupal/Core/DrupalKernel.php @@ -195,7 +195,7 @@ protected function moduleData($module) { /** * Implements Drupal\Core\DrupalKernelInterface::updateModules(). */ - public function updateModules(array $module_list, array $module_paths = array()) { + public function updateModules(array $module_list, array $module_paths = array(), ContainerBuilder $base_container = NULL) { $this->newModuleList = $module_list; foreach ($module_paths as $module => $path) { $this->moduleData[$module] = (object) array('uri' => $path); @@ -204,7 +204,7 @@ public function updateModules(array $module_list, array $module_paths = array()) // list will take effect when boot() is called. If we have already booted, // then reboot in order to refresh the bundle list and container. if ($this->booted) { - drupal_container(NULL, TRUE); + drupal_container($base_container, TRUE); $this->booted = FALSE; $this->boot(); } diff --git a/core/modules/simpletest/lib/Drupal/simpletest/DrupalUnitTestBase.php b/core/modules/simpletest/lib/Drupal/simpletest/DrupalUnitTestBase.php index a21d496..a68392f 100644 --- a/core/modules/simpletest/lib/Drupal/simpletest/DrupalUnitTestBase.php +++ b/core/modules/simpletest/lib/Drupal/simpletest/DrupalUnitTestBase.php @@ -7,7 +7,9 @@ namespace Drupal\simpletest; +use Drupal\Core\DrupalKernel; use Drupal\Core\Database\Database; +use Symfony\Component\DependencyInjection\ContainerInterface; /** * Base test case class for Drupal unit tests. @@ -63,14 +65,19 @@ private $themeData; /** + * Base service container for rebooting DrupalKernel. + * + * @var \Drupal\Core\DependencyInjection\ContainerBuilder + */ + private $baseContainer; + + /** * Sets up Drupal unit test environment. * * @see DrupalUnitTestBase::$modules * @see DrupalUnitTestBase */ protected function setUp() { - global $conf; - // Copy/prime extension file lists once to avoid filesystem scans. if (!isset($this->moduleFiles)) { $this->moduleFiles = state()->get('system.module.files') ?: array(); @@ -80,20 +87,21 @@ protected function setUp() { parent::setUp(); - // Provide a minimal, partially mocked environment for unit tests. - $conf['lock_backend'] = 'Drupal\Core\Lock\NullLockBackend'; - $conf['cache_classes'] = array('cache' => 'Drupal\Core\Cache\MemoryBackend'); - $this->container - ->register('config.storage', 'Drupal\Core\Config\FileStorage') - ->addArgument($this->configDirectories[CONFIG_ACTIVE_DIRECTORY]); - $conf['keyvalue_default'] = 'keyvalue.memory'; - $this->container - ->register('keyvalue.memory', 'Drupal\Core\KeyValueStore\KeyValueMemoryFactory'); + // Build a minimal, partially mocked environment for unit tests. + $this->setUpContainer(); state()->set('system.module.files', $this->moduleFiles); state()->set('system.theme.files', $this->themeFiles); state()->set('system.theme.data', $this->themeData); + // Back up the base container for enableModules(). + $this->baseContainer = clone $this->container; + + // Bootstrap the kernel. + $this->kernel = new DrupalKernel('testing', TRUE, drupal_classloader()); + $this->kernel->boot(); + $this->container = drupal_container(); + // Ensure that the module list is initially empty. $this->moduleList = array(); // Collect and set a fixed module list. @@ -109,18 +117,70 @@ protected function setUp() { } /** + * Sets up the base service container for this test. + * + * Extend this method in your test to register additional service overrides + * that need to persist a DrupalKernel reboot. This method is only called once + * for each test. + * + * @see DrupalUnitTestBase::setUp() + * @see DrupalUnitTestBase::enableModules() + */ + protected function setUpContainer() { + global $conf; + + $conf['lock_backend'] = 'Drupal\Core\Lock\NullLockBackend'; + $conf['cache_classes'] = array('cache' => 'Drupal\Core\Cache\MemoryBackend'); + $this->container + ->register('config.storage', 'Drupal\Core\Config\FileStorage') + ->addArgument($this->configDirectories[CONFIG_ACTIVE_DIRECTORY]); + $conf['keyvalue_default'] = 'keyvalue.memory'; + $this->container + ->register('keyvalue.memory', 'Drupal\Core\KeyValueStore\KeyValueMemoryFactory'); + } + + /** + * Overrides TestBase::tearDown(). + */ + protected function tearDown() { + // Ensure that TestBase::tearDown() gets a working container. + $this->container = $this->baseContainer; + parent::tearDown(); + } + + /** * Installs a specific table from a module schema definition. * + * Use this to install a particular table from System module. + * * @param string $module * The name of the module that defines the table's schema. * @param string $table * The name of the table to install. */ protected function installSchema($module, $table) { - require_once DRUPAL_ROOT . '/' . drupal_get_path('module', $module) . "/$module.install"; - $function = $module . '_schema'; - $schema = $function(); - Database::getConnection()->schema()->createTable($table, $schema[$table]); + // drupal_get_schema_unprocessed() is technically able to install a schema + // of a non-enabled module, but its ability to load the module's .install + // file depends on many other factors. To prevent differences in test + // behavior and non-reproducible test failures, we only allow the schema of + // explicitly loaded/enabled modules to be installed. + if (!module_exists($module)) { + throw new \RuntimeException(format_string("'@module' module is not enabled.", array( + '@module' => $module, + ))); + } + $schema = drupal_get_schema_unprocessed($module, $table); + if (empty($schema)) { + throw new \RuntimeException(format_string("Unable to retrieve '@module' module schema for '@table' table.", array( + '@module' => $module, + '@table' => $table, + ))); + } + Database::getConnection()->schema()->createTable($table, $schema); + // We need to refresh the schema cache, as any call to drupal_get_schema() + // would not know of/return the schema otherwise. + // @todo Very expensive. Refactor the Schema API to make this obsolete. + drupal_get_schema(NULL, TRUE); } /** @@ -132,7 +192,8 @@ protected function installSchema($module, $table) { * has no effect, since we are operating with a fixed module list. * * @param array $modules - * A list of modules to enable. + * A list of modules to enable. Dependencies are not resolved; i.e., + * multiple modules have to be specified with dependent modules first. * @param bool $install * (optional) Whether to install the list of modules via module_enable(). * Defaults to TRUE. If FALSE, the new modules are only added to the fixed @@ -143,18 +204,33 @@ protected function installSchema($module, $table) { */ protected function enableModules(array $modules, $install = TRUE) { // Set the modules in the fixed module_list(). + $new_enabled = array(); foreach ($modules as $module) { $this->moduleList[$module]['filename'] = drupal_get_filename('module', $module); - } - module_list(NULL, $this->moduleList); - - // Call module_enable() to enable (install) the new modules. - if ($install) { - module_enable($modules); + $new_enabled[$module] = dirname($this->moduleList[$module]['filename']); + module_list(NULL, $this->moduleList); + + // Call module_enable() to enable (install) the new module. + if ($install) { + // module_enable() reboots DrupalKernel, but that builds an entirely new + // ContainerBuilder, retrieving a fresh base container from + // drupal_container(), which means that all of the service overrides + // from DrupalUnitTestBase::setUpContainer() are lost, in turn triggering + // invalid service reference errors; e.g., in TestBase::tearDown(). + // Since DrupalKernel also replaces the container in drupal_container() + // after (re-)booting, we have to re-inject a new copy of our initial + // base container that was built in setUpContainer(). + drupal_container(clone $this->baseContainer); + module_enable(array($module), FALSE); + } } // Otherwise, only ensure that the new modules are loaded. - else { + if (!$install) { module_load_all(FALSE, TRUE); + module_implements_reset(); } + $kernel = $this->container->get('kernel'); + $kernel->updateModules($this->moduleList, $new_enabled, clone $this->baseContainer); } + } diff --git a/core/modules/simpletest/lib/Drupal/simpletest/Tests/DrupalUnitTestBaseTest.php b/core/modules/simpletest/lib/Drupal/simpletest/Tests/DrupalUnitTestBaseTest.php new file mode 100644 index 0000000..8cac15f --- /dev/null +++ b/core/modules/simpletest/lib/Drupal/simpletest/Tests/DrupalUnitTestBaseTest.php @@ -0,0 +1,180 @@ + 'DrupalUnitTestBase', + 'description' => 'Tests DrupalUnitTestBase functionality.', + 'group' => 'SimpleTest', + ); + } + + /** + * Tests expected behavior of setUp(). + */ + function testSetUp() { + $module = 'entity_test'; + $table = 'entity_test'; + + // Verify that specified $modules have been loaded. + $this->assertTrue(function_exists('entity_test_permission'), "$module.module was loaded."); + // Verify that there is a fixed module list. + $this->assertIdentical(module_list(), array($module => $module)); + $this->assertIdentical(module_implements('permission'), array($module)); + + // Verify that no modules have been installed. + $this->assertFalse(db_table_exists($table), "'$table' database table not found."); + } + + /** + * Tests expected load behavior of enableModules(). + */ + function testEnableModulesLoad() { + $module = 'field_test'; + + // Verify that the module does not exist yet. + $this->assertFalse(module_exists($module), "$module module not found."); + $list = module_list(); + $this->assertFalse(in_array($module, $list), "$module module in module_list() not found."); + $list = module_list('permission'); + $this->assertFalse(in_array($module, $list), "{$module}_permission() in module_implements() not found."); + + // Enable the module. + $this->enableModules(array($module), FALSE); + + // Verify that the module exists. + $this->assertTrue(module_exists($module), "$module module found."); + $list = module_list(); + $this->assertTrue(in_array($module, $list), "$module module in module_list() found."); + $list = module_list('permission'); + $this->assertTrue(in_array($module, $list), "{$module}_permission() in module_implements() found."); + } + + /** + * Tests expected installation behavior of enableModules(). + */ + function testEnableModulesInstall() { + $module = 'filter'; + $table = 'filter'; + + // @todo Heh. Remove after configuration system conversion. + $this->enableModules(array('system'), FALSE); + $this->installSchema('system', 'variable'); + + // Verify that the module does not exist yet. + $this->assertFalse(module_exists($module), "$module module not found."); + $list = module_list(); + $this->assertFalse(in_array($module, $list), "$module module in module_list() not found."); + $list = module_list('permission'); + $this->assertFalse(in_array($module, $list), "{$module}_permission() in module_implements() not found."); + + $this->assertFalse(db_table_exists($table), "'$table' database table not found."); + $schema = drupal_get_schema($table); + $this->assertFalse($schema, "'$table' table schema not found."); + + // Enable the module. + $this->enableModules(array($module)); + + // Verify that the module does not exist yet. + $this->assertTrue(module_exists($module), "$module module found."); + $list = module_list(); + $this->assertTrue(in_array($module, $list), "$module module in module_list() found."); + $list = module_list('permission'); + $this->assertTrue(in_array($module, $list), "{$module}_permission() in module_implements() found."); + + $this->assertTrue(db_table_exists($table), "'$table' database table found."); + $schema = drupal_get_schema($table); + $this->assertTrue($schema, "'$table' table schema found."); + } + + /** + * Tests installing of multiple modules via enableModules(). + * + * Regression test: Each passed module has to be enabled and installed on its + * own, in the same way as module_enable() enables only one module after the + * other. + */ + function testEnableModulesInstallMultiple() { + // Field retrieves entity type plugins, and EntityTypeManager calls into + // hook_entity_info_alter(). If both modules would be first enabled together + // instead of each on its own, then Node module's alter implementation + // would be called and this simply blows up. + $this->enableModules(array('field', 'node', 'comment')); + $this->pass('Comment module was installed.'); + } + + /** + * Tests installing modules via enableModules() with DepedencyInjection services. + */ + function testEnableModulesInstallContainer() { + // Install Node module. + // @todo field_sql_storage and field should technically not be necessary + // for an entity query. + $this->enableModules(array('field_sql_storage', 'field', 'node')); + // Perform an entity query against node. + $query = entity_query('node'); + $query->accessCheck(FALSE); + $query->condition('nid', 1); + $query->execute(); + $this->pass('Entity field query was executed.'); + } + + /** + * Tests expected behavior of installSchema(). + */ + function testInstallSchema() { + $module = 'entity_test'; + $table = 'entity_test'; + // Verify that we can install a table from the module schema. + $this->installSchema($module, $table); + $this->assertTrue(db_table_exists($table), "'$table' database table found."); + + // Verify that the schema is known to Schema API. + $schema = drupal_get_schema(); + $this->assertTrue($schema[$table], "'$table' table found in schema."); + $schema = drupal_get_schema($table); + $this->assertTrue($schema, "'$table' table schema found."); + + // Verify that a table from a unknown module cannot be installed. + $module = 'database_test'; + $table = 'test'; + try { + $this->installSchema($module, $table); + $this->fail('Exception for non-retrievable schema found.'); + } + catch (\Exception $e) { + $this->pass('Exception for non-retrievable schema found.'); + } + $this->assertFalse(db_table_exists($table), "'$table' database table not found."); + $schema = drupal_get_schema($table); + $this->assertFalse($schema, "'$table' table schema not found."); + + // Verify that the same table can be installed after enabling the module. + $this->enableModules(array($module), FALSE); + $this->installSchema($module, $table); + $this->assertTrue(db_table_exists($table), "'$table' database table found."); + $schema = drupal_get_schema($table); + $this->assertTrue($schema, "'$table' table schema found."); + } + +}