skipClasses[__CLASS__] = TRUE; } /** * {@inheritdoc} */ 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') ?: []; $this->themeFiles = \Drupal::state()->get('system.theme.files') ?: []; } } /** * Create and set new configuration directories. * * @see config_get_config_directory() * * @throws \RuntimeException * Thrown when CONFIG_SYNC_DIRECTORY cannot be created or made writable. */ protected function prepareConfigDirectories() { $this->configDirectories = []; include_once DRUPAL_ROOT . '/core/includes/install.inc'; // Assign the relative path to the global variable. $path = $this->siteDirectory . '/config_' . CONFIG_SYNC_DIRECTORY; $GLOBALS['config_directories'][CONFIG_SYNC_DIRECTORY] = $path; // Ensure the directory can be created and is writeable. if (!file_prepare_directory($path, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS)) { throw new \RuntimeException("Failed to create '" . CONFIG_SYNC_DIRECTORY . "' config directory $path"); } // Provide the already resolved path for tests. $this->configDirectories[CONFIG_SYNC_DIRECTORY] = $path; } /** * {@inheritdoc} */ protected function setUp() { $this->keyValueFactory = new KeyValueMemoryFactory(); // Back up settings from TestBase::prepareEnvironment(). $settings = Settings::getAll(); // Allow for test-specific overrides. $directory = DRUPAL_ROOT . '/' . $this->siteDirectory; $settings_services_file = DRUPAL_ROOT . '/' . $this->originalSite . '/testing.services.yml'; $container_yamls = []; if (file_exists($settings_services_file)) { // Copy the testing-specific service overrides in place. $testing_services_file = $directory . '/services.yml'; copy($settings_services_file, $testing_services_file); $container_yamls[] = $testing_services_file; } $settings_testing_file = DRUPAL_ROOT . '/' . $this->originalSite . '/settings.testing.php'; if (file_exists($settings_testing_file)) { // Copy the testing-specific settings.php overrides in place. copy($settings_testing_file, $directory . '/settings.testing.php'); } if (file_exists($directory . '/settings.testing.php')) { // Add the name of the testing class to settings.php and include the // testing specific overrides $hash_salt = Settings::getHashSalt(); $test_class = get_class($this); $container_yamls_export = Variable::export($container_yamls); $php = <<kernel = new DrupalKernel('testing', $class_loader, FALSE); $request = Request::create('/'); $site_path = DrupalKernel::findSitePath($request); $this->kernel->setSitePath($site_path); if (file_exists($directory . '/settings.testing.php')) { Settings::initialize(DRUPAL_ROOT, $site_path, $class_loader); } $this->kernel->boot(); // Ensure database install tasks have been run. require_once __DIR__ . '/../../../includes/install.inc'; $connection = Database::getConnection(); $errors = db_installer_object($connection->driver())->runTasks(); if (!empty($errors)) { $this->fail('Failed to run installer database tasks: ' . implode(', ', $errors)); } // Reboot the kernel because the container might contain a connection to the // database that has been closed during the database install tasks. This // prevents any services created during the first boot from having stale // database connections, for example, \Drupal\Core\Config\DatabaseStorage. $this->kernel->shutdown(); $this->kernel->boot(); // Save the original site directory path, so that extensions in the // site-specific directory can still be discovered in the test site // environment. // @see \Drupal\Core\Extension\ExtensionDiscovery::scan() $settings['test_parent_site'] = $this->originalSite; // Restore and merge settings. // DrupalKernel::boot() initializes new Settings, and the containerBuild() // method sets additional settings. new Settings($settings + Settings::getAll()); // Create and set new configuration directories. $this->prepareConfigDirectories(); // Set the request scope. $this->container = $this->kernel->getContainer(); $this->container->get('request_stack')->push($request); // Re-inject extension file listings into state, unless the key/value // service was overridden (in which case its storage does not exist yet). if ($this->container->get('keyvalue') instanceof KeyValueMemoryFactory) { $this->container->get('state')->set('system.module.files', $this->moduleFiles); $this->container->get('state')->set('system.theme.files', $this->themeFiles); } // Create a minimal core.extension configuration object so that the list of // enabled modules can be maintained allowing // \Drupal\Core\Config\ConfigInstaller::installDefaultConfig() to work. // Write directly to active storage to avoid early instantiation of // the event dispatcher which can prevent modules from registering events. \Drupal::service('config.storage')->write('core.extension', ['module' => [], 'theme' => [], 'profile' => '']); // Collect and set a fixed module list. $class = get_class($this); $modules = []; while ($class) { if (property_exists($class, 'modules')) { // Only add the modules, if the $modules property was not inherited. $rp = new \ReflectionProperty($class, 'modules'); if ($rp->class == $class) { $modules[$class] = $class::$modules; } } $class = get_parent_class($class); } // Modules have been collected in reverse class hierarchy order; modules // defined by base classes should be sorted first. Then, merge the results // together. $modules = array_reverse($modules); $modules = call_user_func_array('array_merge_recursive', $modules); if ($modules) { $this->enableModules($modules); } // Tests based on this class are entitled to use Drupal's File and // StreamWrapper APIs. // @todo Move StreamWrapper management into DrupalKernel. // @see https://www.drupal.org/node/2028109 file_prepare_directory($this->publicFilesDirectory, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS); $this->settingsSet('file_public_path', $this->publicFilesDirectory); $this->streamWrappers = []; $this->registerStreamWrapper('public', 'Drupal\Core\StreamWrapper\PublicStream'); // The temporary stream wrapper is able to operate both with and without // configuration. $this->registerStreamWrapper('temporary', 'Drupal\Core\StreamWrapper\TemporaryStream'); // Manually configure the test mail collector implementation to prevent // tests from sending out emails and collect them in state instead. // While this should be enforced via settings.php prior to installation, // some tests expect to be able to test mail system implementations. $GLOBALS['config']['system.mail']['interface']['default'] = 'test_mail_collector'; } /** * {@inheritdoc} */ protected function tearDown() { if ($this->kernel instanceof DrupalKernel) { $this->kernel->shutdown(); } // Before tearing down the test environment, ensure that no stream wrapper // of this test leaks into the parent environment. Unlike all other global // state variables in Drupal, stream wrappers are a global state construct // of PHP core, which has to be maintained manually. // @todo Move StreamWrapper management into DrupalKernel. // @see https://www.drupal.org/node/2028109 foreach ($this->streamWrappers as $scheme => $type) { $this->unregisterStreamWrapper($scheme, $type); } parent::tearDown(); } /** * 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 called whenever * the kernel is rebuilt. * * @see \Drupal\simpletest\KernelTestBase::setUp() * @see \Drupal\simpletest\KernelTestBase::enableModules() * @see \Drupal\simpletest\KernelTestBase::disableModules() */ public function containerBuild(ContainerBuilder $container) { // Keep the container object around for tests. $this->container = $container; // Set the default language on the minimal container. $this->container->setParameter('language.default_values', $this->defaultLanguageData()); $container->register('lock', 'Drupal\Core\Lock\NullLockBackend'); $container->register('cache_factory', 'Drupal\Core\Cache\MemoryBackendFactory'); $container ->register('config.storage', 'Drupal\Core\Config\DatabaseStorage') ->addArgument(Database::getConnection()) ->addArgument('config'); if ($this->strictConfigSchema) { $container ->register('simpletest.config_schema_checker', ConfigSchemaChecker::class) ->addArgument(new Reference('config.typed')) ->addArgument($this->getConfigSchemaExclusions()) ->addTag('event_subscriber'); } $keyvalue_options = $container->getParameter('factory.keyvalue') ?: []; $keyvalue_options['default'] = 'keyvalue.memory'; $container->setParameter('factory.keyvalue', $keyvalue_options); $container->set('keyvalue.memory', $this->keyValueFactory); if (!$container->has('keyvalue')) { // TestBase::setUp puts a completely empty container in // $this->container which is somewhat the mirror of the empty // environment being set up. Unit tests need not to waste time with // getting a container set up for them. Drupal Unit Tests might just get // away with a simple container holding the absolute bare minimum. When // a kernel is overridden then there's no need to re-register the keyvalue // service but when a test is happy with the superminimal container put // together here, it still might a keyvalue storage for anything using // \Drupal::state() -- that's why a memory service was added in the first // place. $container->register('settings', 'Drupal\Core\Site\Settings') ->setFactoryClass('Drupal\Core\Site\Settings') ->setFactoryMethod('getInstance'); $container ->register('keyvalue', 'Drupal\Core\KeyValueStore\KeyValueFactory') ->addArgument(new Reference('service_container')) ->addArgument(new Parameter('factory.keyvalue')); $container->register('state', 'Drupal\Core\State\State') ->addArgument(new Reference('keyvalue')); } if ($container->hasDefinition('path_processor_alias')) { // Prevent the alias-based path processor, which requires a url_alias db // table, from being registered to the path processor manager. We do this // by removing the tags that the compiler pass looks for. This means the // url generator can safely be used within tests. $definition = $container->getDefinition('path_processor_alias'); $definition->clearTag('path_processor_inbound')->clearTag('path_processor_outbound'); } if ($container->hasDefinition('password')) { $container->getDefinition('password')->setArguments([1]); } // Register the stream wrapper manager. $container ->register('stream_wrapper_manager', 'Drupal\Core\StreamWrapper\StreamWrapperManager') ->addArgument(new Reference('module_handler')) ->addMethodCall('setContainer', [new Reference('service_container')]); $request = Request::create('/'); $container->get('request_stack')->push($request); } /** * Provides the data for setting the default language on the container. * * @return array * The data array for the default language. */ protected function defaultLanguageData() { return Language::$defaultValues; } /** * Installs default configuration for a given list of modules. * * @param array $modules * A list of modules for which to install default configuration. * * @throws \RuntimeException * Thrown when any module listed in $modules is not enabled. */ protected function installConfig(array $modules) { foreach ($modules as $module) { if (!$this->container->get('module_handler')->moduleExists($module)) { throw new \RuntimeException("'$module' module is not enabled"); } \Drupal::service('config.installer')->installDefaultConfig('module', $module); } $this->pass(format_string('Installed default config: %modules.', [ '%modules' => implode(', ', $modules), ])); } /** * Installs a specific table from a module schema definition. * * @param string $module * The name of the module that defines the table's schema. * @param string|array $tables * The name or an array of the names of the tables to install. * * @throws \RuntimeException * Thrown when $module is not enabled or when the table schema cannot be * found in the module specified. */ protected function installSchema($module, $tables) { // drupal_get_module_schema() 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 (!$this->container->get('module_handler')->moduleExists($module)) { throw new \RuntimeException("'$module' module is not enabled"); } $tables = (array) $tables; foreach ($tables as $table) { $schema = drupal_get_module_schema($module, $table); if (empty($schema)) { // BC layer to avoid some contrib tests to fail. // @todo Remove the BC layer before 8.1.x release. // @see https://www.drupal.org/node/2670360 // @see https://www.drupal.org/node/2670454 if ($module == 'system') { continue; } throw new \RuntimeException("Unknown '$table' table schema in '$module' module."); } $this->container->get('database')->schema()->createTable($table, $schema); } $this->pass(format_string('Installed %module tables: %tables.', [ '%tables' => '{' . implode('}, {', $tables) . '}', '%module' => $module, ])); } /** * Installs the storage schema for a specific entity type. * * @param string $entity_type_id * The ID of the entity type. */ protected function installEntitySchema($entity_type_id) { /** @var \Drupal\Core\Entity\EntityManagerInterface $entity_manager */ $entity_manager = $this->container->get('entity.manager'); $entity_type = $entity_manager->getDefinition($entity_type_id); $entity_manager->onEntityTypeCreate($entity_type); // For test runs, the most common storage backend is a SQL database. For // this case, ensure the tables got created. $storage = $entity_manager->getStorage($entity_type_id); if ($storage instanceof SqlEntityStorageInterface) { $tables = $storage->getTableMapping()->getTableNames(); $db_schema = $this->container->get('database')->schema(); $all_tables_exist = TRUE; foreach ($tables as $table) { if (!$db_schema->tableExists($table)) { $this->fail(new FormattableMarkup('Installed entity type table for the %entity_type entity type: %table', [ '%entity_type' => $entity_type_id, '%table' => $table, ])); $all_tables_exist = FALSE; } } if ($all_tables_exist) { $this->pass(new FormattableMarkup('Installed entity type tables for the %entity_type entity type: %tables', [ '%entity_type' => $entity_type_id, '%tables' => '{' . implode('}, {', $tables) . '}', ])); } } } /** * Enables modules for this test. * * To install test modules outside of the testing environment, add * @code * $settings['extension_discovery_scan_tests'] = TRUE; * @endcode * to your settings.php. * * @param array $modules * A list of modules to enable. Dependencies are not resolved; i.e., * multiple modules have to be specified with dependent modules first. * The new modules are only added to the active module list and loaded. */ protected function enableModules(array $modules) { // Perform an ExtensionDiscovery scan as this function may receive a // profile that is not the current profile, and we don't yet have a cached // way to receive inactive profile information. // @todo Remove as part of https://www.drupal.org/node/2186491 $listing = new ExtensionDiscovery(\Drupal::root()); $module_list = $listing->scan('module'); // In ModuleHandlerTest we pass in a profile as if it were a module. $module_list += $listing->scan('profile'); // Set the list of modules in the extension handler. $module_handler = $this->container->get('module_handler'); // Write directly to active storage to avoid early instantiation of // the event dispatcher which can prevent modules from registering events. $active_storage = \Drupal::service('config.storage'); $extensions = $active_storage->read('core.extension'); foreach ($modules as $module) { $module_handler->addModule($module, $module_list[$module]->getPath()); // Maintain the list of enabled modules in configuration. $extensions['module'][$module] = 0; } $active_storage->write('core.extension', $extensions); // Update the kernel to make their services available. $module_filenames = $module_handler->getModuleList(); $this->kernel->updateModules($module_filenames, $module_filenames); // Ensure isLoaded() is TRUE in order to make // \Drupal\Core\Theme\ThemeManagerInterface::render() work. // Note that the kernel has rebuilt the container; this $module_handler is // no longer the $module_handler instance from above. $this->container->get('module_handler')->reload(); $this->pass(format_string('Enabled modules: %modules.', [ '%modules' => implode(', ', $modules), ])); } /** * Disables modules for this test. * * @param array $modules * A list of modules to disable. Dependencies are not resolved; i.e., * multiple modules have to be specified with dependent modules first. * Code of previously active modules is still loaded. The modules are only * removed from the active module list. */ protected function disableModules(array $modules) { // Unset the list of modules in the extension handler. $module_handler = $this->container->get('module_handler'); $module_filenames = $module_handler->getModuleList(); $extension_config = $this->config('core.extension'); foreach ($modules as $module) { unset($module_filenames[$module]); $extension_config->clear('module.' . $module); } $extension_config->save(); $module_handler->setModuleList($module_filenames); $module_handler->resetImplementations(); // Update the kernel to remove their services. $this->kernel->updateModules($module_filenames, $module_filenames); // Ensure isLoaded() is TRUE in order to make // \Drupal\Core\Theme\ThemeManagerInterface::render() work. // Note that the kernel has rebuilt the container; this $module_handler is // no longer the $module_handler instance from above. $module_handler = $this->container->get('module_handler'); $module_handler->reload(); $this->pass(format_string('Disabled modules: %modules.', [ '%modules' => implode(', ', $modules), ])); } /** * Registers a stream wrapper for this test. * * @param string $scheme * The scheme to register. * @param string $class * The fully qualified class name to register. * @param int $type * The Drupal Stream Wrapper API type. Defaults to * StreamWrapperInterface::NORMAL. */ protected function registerStreamWrapper($scheme, $class, $type = StreamWrapperInterface::NORMAL) { $this->container->get('stream_wrapper_manager')->registerWrapper($scheme, $class, $type); } /** * Renders a render array. * * @param array $elements * The elements to render. * * @return string * The rendered string output (typically HTML). */ protected function render(array &$elements) { // Use the bare HTML page renderer to render our links. $renderer = $this->container->get('bare_html_page_renderer'); $response = $renderer->renderBarePage($elements, '', 'maintenance_page'); // Glean the content from the response object. $content = $response->getContent(); $this->setRawContent($content); $this->verbose('
' . Html::escape($content));
    return $content;
  }

}