diff --git a/core/includes/bootstrap.inc b/core/includes/bootstrap.inc index eac2f97..5378325 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/lib/Drupal/Core/DrupalKernel.php b/core/lib/Drupal/Core/DrupalKernel.php index 892571f..a64b88d 100644 --- a/core/lib/Drupal/Core/DrupalKernel.php +++ b/core/lib/Drupal/Core/DrupalKernel.php @@ -113,13 +113,13 @@ public function registerBundles() { /** * Implements Drupal\Core\DrupalKernelInterface::updateModules(). */ - public function updateModules($module_list) { + public function updateModules($module_list, ContainerBuilder $base_container = NULL) { $this->moduleList = $module_list; // If we haven't yet booted, we don't need to do anything: the new module // 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 f1ba201..bfbca8f 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, array_keys($this->moduleList)); + $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,8 +117,42 @@ 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 @@ -164,18 +206,29 @@ protected function enableModules(array $modules, $install = TRUE) { // Set the modules in the fixed module_list(). 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, FALSE); + 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(array_keys($this->moduleList), 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 index 6001c4f..8cac15f 100644 --- a/core/modules/simpletest/lib/Drupal/simpletest/Tests/DrupalUnitTestBaseTest.php +++ b/core/modules/simpletest/lib/Drupal/simpletest/Tests/DrupalUnitTestBaseTest.php @@ -108,6 +108,38 @@ function testEnableModulesInstall() { } /** + * 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() {