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