3 namespace Drupal\simpletest;
5 use Drupal\Component\Utility\Html;
6 use Drupal\Component\Utility\SafeMarkup;
7 use Drupal\Component\Utility\Variable;
8 use Drupal\Core\Config\Development\ConfigSchemaChecker;
9 use Drupal\Core\Database\Database;
10 use Drupal\Core\DependencyInjection\ContainerBuilder;
11 use Drupal\Core\DrupalKernel;
12 use Drupal\Core\Entity\Sql\SqlEntityStorageInterface;
13 use Drupal\Core\Extension\ExtensionDiscovery;
14 use Drupal\Core\KeyValueStore\KeyValueMemoryFactory;
15 use Drupal\Core\Language\Language;
16 use Drupal\Core\Site\Settings;
17 use Symfony\Component\DependencyInjection\Parameter;
18 use Drupal\Core\StreamWrapper\StreamWrapperInterface;
19 use Symfony\Component\DependencyInjection\Reference;
20 use Symfony\Component\HttpFoundation\Request;
23 * Base class for integration tests.
25 * Tests extending this base class can access files and the database, but the
26 * entire environment is initially empty. Drupal runs in a minimal mocked
27 * environment, comparable to the one in the early installer.
29 * The module/hook system is functional and operates on a fixed module list.
30 * Additional modules needed in a test may be loaded and added to the fixed
33 * @deprecated in Drupal 8.0.x, will be removed before Drupal 9.0.0. Use
34 * \Drupal\KernelTests\KernelTestBase instead.
36 * @see \Drupal\simpletest\KernelTestBase::$modules
37 * @see \Drupal\simpletest\KernelTestBase::enableModules()
41 abstract class KernelTestBase extends TestBase {
43 use AssertContentTrait;
48 * Test classes extending this class, and any classes in the hierarchy up to
49 * this class, may specify individual lists of modules to enable by setting
50 * this property. The values of all properties in all classes in the hierarchy
53 * Any modules specified in the $modules property are automatically loaded and
54 * set as the fixed module list.
56 * Unlike WebTestBase::setUp(), the specified modules are loaded only, but not
57 * automatically installed. Modules need to be installed manually, if needed.
59 * @see \Drupal\simpletest\KernelTestBase::enableModules()
60 * @see \Drupal\simpletest\KernelTestBase::setUp()
64 public static $modules = [];
70 * The configuration directories for this test run.
74 protected $configDirectories = [];
77 * A KeyValueMemoryFactory instance to use when building the container.
79 * @var \Drupal\Core\KeyValueStore\KeyValueMemoryFactory.
81 protected $keyValueFactory;
84 * Array of registered stream wrappers.
88 protected $streamWrappers = [];
93 public function __construct($test_id = NULL) {
94 parent::__construct($test_id);
95 $this->skipClasses[__CLASS__] = TRUE;
101 protected function beforePrepareEnvironment() {
102 // Copy/prime extension file lists once to avoid filesystem scans.
103 if (!isset($this->moduleFiles)) {
104 $this->moduleFiles = \Drupal::state()->get('system.module.files') ?: [];
105 $this->themeFiles = \Drupal::state()->get('system.theme.files') ?: [];
110 * Create and set new configuration directories.
112 * @see config_get_config_directory()
114 * @throws \RuntimeException
115 * Thrown when CONFIG_SYNC_DIRECTORY cannot be created or made writable.
117 protected function prepareConfigDirectories() {
118 $this->configDirectories = [];
119 include_once DRUPAL_ROOT . '/core/includes/install.inc';
120 // Assign the relative path to the global variable.
121 $path = $this->siteDirectory . '/config_' . CONFIG_SYNC_DIRECTORY;
122 $GLOBALS['config_directories'][CONFIG_SYNC_DIRECTORY] = $path;
123 // Ensure the directory can be created and is writeable.
124 if (!install_ensure_config_directory(CONFIG_SYNC_DIRECTORY)) {
125 throw new \RuntimeException("Failed to create '" . CONFIG_SYNC_DIRECTORY . "' config directory $path");
127 // Provide the already resolved path for tests.
128 $this->configDirectories[CONFIG_SYNC_DIRECTORY] = $path;
134 protected function setUp() {
135 $this->keyValueFactory = new KeyValueMemoryFactory();
137 // Back up settings from TestBase::prepareEnvironment().
138 $settings = Settings::getAll();
140 // Allow for test-specific overrides.
141 $directory = DRUPAL_ROOT . '/' . $this->siteDirectory;
142 $settings_services_file = DRUPAL_ROOT . '/' . $this->originalSite . '/testing.services.yml';
143 $container_yamls = [];
144 if (file_exists($settings_services_file)) {
145 // Copy the testing-specific service overrides in place.
146 $testing_services_file = $directory . '/services.yml';
147 copy($settings_services_file, $testing_services_file);
148 $container_yamls[] = $testing_services_file;
150 $settings_testing_file = DRUPAL_ROOT . '/' . $this->originalSite . '/settings.testing.php';
151 if (file_exists($settings_testing_file)) {
152 // Copy the testing-specific settings.php overrides in place.
153 copy($settings_testing_file, $directory . '/settings.testing.php');
156 if (file_exists($directory . '/settings.testing.php')) {
157 // Add the name of the testing class to settings.php and include the
158 // testing specific overrides
159 $hash_salt = Settings::getHashSalt();
160 $test_class = get_class($this);
161 $container_yamls_export = Variable::export($container_yamls);
165 \$settings['hash_salt'] = '$hash_salt';
166 \$settings['container_yamls'] = $container_yamls_export;
168 \$test_class = '$test_class';
169 include DRUPAL_ROOT . '/' . \$site_path . '/settings.testing.php';
171 file_put_contents($directory . '/settings.php', $php);
174 // Add this test class as a service provider.
175 // @todo Remove the indirection; implement ServiceProviderInterface instead.
176 $GLOBALS['conf']['container_service_providers']['TestServiceProvider'] = 'Drupal\simpletest\TestServiceProvider';
178 // Bootstrap a new kernel.
179 $class_loader = require DRUPAL_ROOT . '/autoload.php';
180 $this->kernel = new DrupalKernel('testing', $class_loader, FALSE);
181 $request = Request::create('/');
182 $site_path = DrupalKernel::findSitePath($request);
183 $this->kernel->setSitePath($site_path);
184 if (file_exists($directory . '/settings.testing.php')) {
185 Settings::initialize(DRUPAL_ROOT, $site_path, $class_loader);
187 $this->kernel->boot();
189 // Ensure database install tasks have been run.
190 require_once __DIR__ . '/../../../includes/install.inc';
191 $connection = Database::getConnection();
192 $errors = db_installer_object($connection->driver())->runTasks();
193 if (!empty($errors)) {
194 $this->fail('Failed to run installer database tasks: ' . implode(', ', $errors));
197 // Reboot the kernel because the container might contain a connection to the
198 // database that has been closed during the database install tasks. This
199 // prevents any services created during the first boot from having stale
200 // database connections, for example, \Drupal\Core\Config\DatabaseStorage.
201 $this->kernel->shutdown();
202 $this->kernel->boot();
205 // Save the original site directory path, so that extensions in the
206 // site-specific directory can still be discovered in the test site
208 // @see \Drupal\Core\Extension\ExtensionDiscovery::scan()
209 $settings['test_parent_site'] = $this->originalSite;
211 // Restore and merge settings.
212 // DrupalKernel::boot() initializes new Settings, and the containerBuild()
213 // method sets additional settings.
214 new Settings($settings + Settings::getAll());
216 // Create and set new configuration directories.
217 $this->prepareConfigDirectories();
219 // Set the request scope.
220 $this->container = $this->kernel->getContainer();
221 $this->container->get('request_stack')->push($request);
223 // Re-inject extension file listings into state, unless the key/value
224 // service was overridden (in which case its storage does not exist yet).
225 if ($this->container->get('keyvalue') instanceof KeyValueMemoryFactory) {
226 $this->container->get('state')->set('system.module.files', $this->moduleFiles);
227 $this->container->get('state')->set('system.theme.files', $this->themeFiles);
230 // Create a minimal core.extension configuration object so that the list of
231 // enabled modules can be maintained allowing
232 // \Drupal\Core\Config\ConfigInstaller::installDefaultConfig() to work.
233 // Write directly to active storage to avoid early instantiation of
234 // the event dispatcher which can prevent modules from registering events.
235 \Drupal::service('config.storage')->write('core.extension', ['module' => [], 'theme' => [], 'profile' => '']);
237 // Collect and set a fixed module list.
238 $class = get_class($this);
241 if (property_exists($class, 'modules')) {
242 // Only add the modules, if the $modules property was not inherited.
243 $rp = new \ReflectionProperty($class, 'modules');
244 if ($rp->class == $class) {
245 $modules[$class] = $class::$modules;
248 $class = get_parent_class($class);
250 // Modules have been collected in reverse class hierarchy order; modules
251 // defined by base classes should be sorted first. Then, merge the results
253 $modules = array_reverse($modules);
254 $modules = call_user_func_array('array_merge_recursive', $modules);
256 $this->enableModules($modules);
259 // Tests based on this class are entitled to use Drupal's File and
260 // StreamWrapper APIs.
261 // @todo Move StreamWrapper management into DrupalKernel.
262 // @see https://www.drupal.org/node/2028109
263 file_prepare_directory($this->publicFilesDirectory, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS);
264 $this->settingsSet('file_public_path', $this->publicFilesDirectory);
265 $this->streamWrappers = [];
266 $this->registerStreamWrapper('public', 'Drupal\Core\StreamWrapper\PublicStream');
267 // The temporary stream wrapper is able to operate both with and without
269 $this->registerStreamWrapper('temporary', 'Drupal\Core\StreamWrapper\TemporaryStream');
271 // Manually configure the test mail collector implementation to prevent
272 // tests from sending out emails and collect them in state instead.
273 // While this should be enforced via settings.php prior to installation,
274 // some tests expect to be able to test mail system implementations.
275 $GLOBALS['config']['system.mail']['interface']['default'] = 'test_mail_collector';
281 protected function tearDown() {
282 if ($this->kernel instanceof DrupalKernel) {
283 $this->kernel->shutdown();
285 // Before tearing down the test environment, ensure that no stream wrapper
286 // of this test leaks into the parent environment. Unlike all other global
287 // state variables in Drupal, stream wrappers are a global state construct
288 // of PHP core, which has to be maintained manually.
289 // @todo Move StreamWrapper management into DrupalKernel.
290 // @see https://www.drupal.org/node/2028109
291 foreach ($this->streamWrappers as $scheme => $type) {
292 $this->unregisterStreamWrapper($scheme, $type);
298 * Sets up the base service container for this test.
300 * Extend this method in your test to register additional service overrides
301 * that need to persist a DrupalKernel reboot. This method is called whenever
302 * the kernel is rebuilt.
304 * @see \Drupal\simpletest\KernelTestBase::setUp()
305 * @see \Drupal\simpletest\KernelTestBase::enableModules()
306 * @see \Drupal\simpletest\KernelTestBase::disableModules()
308 public function containerBuild(ContainerBuilder $container) {
309 // Keep the container object around for tests.
310 $this->container = $container;
312 // Set the default language on the minimal container.
313 $this->container->setParameter('language.default_values', $this->defaultLanguageData());
315 $container->register('lock', 'Drupal\Core\Lock\NullLockBackend');
316 $container->register('cache_factory', 'Drupal\Core\Cache\MemoryBackendFactory');
319 ->register('config.storage', 'Drupal\Core\Config\DatabaseStorage')
320 ->addArgument(Database::getConnection())
321 ->addArgument('config');
323 if ($this->strictConfigSchema) {
325 ->register('simpletest.config_schema_checker', ConfigSchemaChecker::class)
326 ->addArgument(new Reference('config.typed'))
327 ->addArgument($this->getConfigSchemaExclusions())
328 ->addTag('event_subscriber');
331 $keyvalue_options = $container->getParameter('factory.keyvalue') ?: [];
332 $keyvalue_options['default'] = 'keyvalue.memory';
333 $container->setParameter('factory.keyvalue', $keyvalue_options);
334 $container->set('keyvalue.memory', $this->keyValueFactory);
335 if (!$container->has('keyvalue')) {
336 // TestBase::setUp puts a completely empty container in
337 // $this->container which is somewhat the mirror of the empty
338 // environment being set up. Unit tests need not to waste time with
339 // getting a container set up for them. Drupal Unit Tests might just get
340 // away with a simple container holding the absolute bare minimum. When
341 // a kernel is overridden then there's no need to re-register the keyvalue
342 // service but when a test is happy with the superminimal container put
343 // together here, it still might a keyvalue storage for anything using
344 // \Drupal::state() -- that's why a memory service was added in the first
346 $container->register('settings', 'Drupal\Core\Site\Settings')
347 ->setFactoryClass('Drupal\Core\Site\Settings')
348 ->setFactoryMethod('getInstance');
351 ->register('keyvalue', 'Drupal\Core\KeyValueStore\KeyValueFactory')
352 ->addArgument(new Reference('service_container'))
353 ->addArgument(new Parameter('factory.keyvalue'));
355 $container->register('state', 'Drupal\Core\State\State')
356 ->addArgument(new Reference('keyvalue'));
359 if ($container->hasDefinition('path_processor_alias')) {
360 // Prevent the alias-based path processor, which requires a url_alias db
361 // table, from being registered to the path processor manager. We do this
362 // by removing the tags that the compiler pass looks for. This means the
363 // url generator can safely be used within tests.
364 $definition = $container->getDefinition('path_processor_alias');
365 $definition->clearTag('path_processor_inbound')->clearTag('path_processor_outbound');
368 if ($container->hasDefinition('password')) {
369 $container->getDefinition('password')->setArguments([1]);
372 // Register the stream wrapper manager.
374 ->register('stream_wrapper_manager', 'Drupal\Core\StreamWrapper\StreamWrapperManager')
375 ->addArgument(new Reference('module_handler'))
376 ->addMethodCall('setContainer', [new Reference('service_container')]);
378 $request = Request::create('/');
379 $container->get('request_stack')->push($request);
383 * Provides the data for setting the default language on the container.
386 * The data array for the default language.
388 protected function defaultLanguageData() {
389 return Language::$defaultValues;
393 * Installs default configuration for a given list of modules.
395 * @param array $modules
396 * A list of modules for which to install default configuration.
398 * @throws \RuntimeException
399 * Thrown when any module listed in $modules is not enabled.
401 protected function installConfig(array $modules) {
402 foreach ($modules as $module) {
403 if (!$this->container->get('module_handler')->moduleExists($module)) {
404 throw new \RuntimeException("'$module' module is not enabled");
406 \Drupal::service('config.installer')->installDefaultConfig('module', $module);
408 $this->pass(format_string('Installed default config: %modules.', [
409 '%modules' => implode(', ', $modules),
414 * Installs a specific table from a module schema definition.
416 * @param string $module
417 * The name of the module that defines the table's schema.
418 * @param string|array $tables
419 * The name or an array of the names of the tables to install.
421 * @throws \RuntimeException
422 * Thrown when $module is not enabled or when the table schema cannot be
423 * found in the module specified.
425 protected function installSchema($module, $tables) {
426 // drupal_get_module_schema() is technically able to install a schema
427 // of a non-enabled module, but its ability to load the module's .install
428 // file depends on many other factors. To prevent differences in test
429 // behavior and non-reproducible test failures, we only allow the schema of
430 // explicitly loaded/enabled modules to be installed.
431 if (!$this->container->get('module_handler')->moduleExists($module)) {
432 throw new \RuntimeException("'$module' module is not enabled");
435 $tables = (array) $tables;
436 foreach ($tables as $table) {
437 $schema = drupal_get_module_schema($module, $table);
438 if (empty($schema)) {
439 // BC layer to avoid some contrib tests to fail.
440 // @todo Remove the BC layer before 8.1.x release.
441 // @see https://www.drupal.org/node/2670360
442 // @see https://www.drupal.org/node/2670454
443 if ($module == 'system') {
446 throw new \RuntimeException("Unknown '$table' table schema in '$module' module.");
448 $this->container->get('database')->schema()->createTable($table, $schema);
450 $this->pass(format_string('Installed %module tables: %tables.', [
451 '%tables' => '{' . implode('}, {', $tables) . '}',
452 '%module' => $module,
459 * Installs the storage schema for a specific entity type.
461 * @param string $entity_type_id
462 * The ID of the entity type.
464 protected function installEntitySchema($entity_type_id) {
465 /** @var \Drupal\Core\Entity\EntityManagerInterface $entity_manager */
466 $entity_manager = $this->container->get('entity.manager');
467 $entity_type = $entity_manager->getDefinition($entity_type_id);
468 $entity_manager->onEntityTypeCreate($entity_type);
470 // For test runs, the most common storage backend is a SQL database. For
471 // this case, ensure the tables got created.
472 $storage = $entity_manager->getStorage($entity_type_id);
473 if ($storage instanceof SqlEntityStorageInterface) {
474 $tables = $storage->getTableMapping()->getTableNames();
475 $db_schema = $this->container->get('database')->schema();
476 $all_tables_exist = TRUE;
477 foreach ($tables as $table) {
478 if (!$db_schema->tableExists($table)) {
479 $this->fail(SafeMarkup::format('Installed entity type table for the %entity_type entity type: %table', [
480 '%entity_type' => $entity_type_id,
483 $all_tables_exist = FALSE;
486 if ($all_tables_exist) {
487 $this->pass(SafeMarkup::format('Installed entity type tables for the %entity_type entity type: %tables', [
488 '%entity_type' => $entity_type_id,
489 '%tables' => '{' . implode('}, {', $tables) . '}',
496 * Enables modules for this test.
498 * To install test modules outside of the testing environment, add
500 * $settings['extension_discovery_scan_tests'] = TRUE;
502 * to your settings.php.
504 * @param array $modules
505 * A list of modules to enable. Dependencies are not resolved; i.e.,
506 * multiple modules have to be specified with dependent modules first.
507 * The new modules are only added to the active module list and loaded.
509 protected function enableModules(array $modules) {
510 // Perform an ExtensionDiscovery scan as this function may receive a
511 // profile that is not the current profile, and we don't yet have a cached
512 // way to receive inactive profile information.
513 // @todo Remove as part of https://www.drupal.org/node/2186491
514 $listing = new ExtensionDiscovery(\Drupal::root());
515 $module_list = $listing->scan('module');
516 // In ModuleHandlerTest we pass in a profile as if it were a module.
517 $module_list += $listing->scan('profile');
518 // Set the list of modules in the extension handler.
519 $module_handler = $this->container->get('module_handler');
521 // Write directly to active storage to avoid early instantiation of
522 // the event dispatcher which can prevent modules from registering events.
523 $active_storage = \Drupal::service('config.storage');
524 $extensions = $active_storage->read('core.extension');
526 foreach ($modules as $module) {
527 $module_handler->addModule($module, $module_list[$module]->getPath());
528 // Maintain the list of enabled modules in configuration.
529 $extensions['module'][$module] = 0;
531 $active_storage->write('core.extension', $extensions);
533 // Update the kernel to make their services available.
534 $module_filenames = $module_handler->getModuleList();
535 $this->kernel->updateModules($module_filenames, $module_filenames);
537 // Ensure isLoaded() is TRUE in order to make
538 // \Drupal\Core\Theme\ThemeManagerInterface::render() work.
539 // Note that the kernel has rebuilt the container; this $module_handler is
540 // no longer the $module_handler instance from above.
541 $this->container->get('module_handler')->reload();
542 $this->pass(format_string('Enabled modules: %modules.', [
543 '%modules' => implode(', ', $modules),
548 * Disables modules for this test.
550 * @param array $modules
551 * A list of modules to disable. Dependencies are not resolved; i.e.,
552 * multiple modules have to be specified with dependent modules first.
553 * Code of previously active modules is still loaded. The modules are only
554 * removed from the active module list.
556 protected function disableModules(array $modules) {
557 // Unset the list of modules in the extension handler.
558 $module_handler = $this->container->get('module_handler');
559 $module_filenames = $module_handler->getModuleList();
560 $extension_config = $this->config('core.extension');
561 foreach ($modules as $module) {
562 unset($module_filenames[$module]);
563 $extension_config->clear('module.' . $module);
565 $extension_config->save();
566 $module_handler->setModuleList($module_filenames);
567 $module_handler->resetImplementations();
568 // Update the kernel to remove their services.
569 $this->kernel->updateModules($module_filenames, $module_filenames);
571 // Ensure isLoaded() is TRUE in order to make
572 // \Drupal\Core\Theme\ThemeManagerInterface::render() work.
573 // Note that the kernel has rebuilt the container; this $module_handler is
574 // no longer the $module_handler instance from above.
575 $module_handler = $this->container->get('module_handler');
576 $module_handler->reload();
577 $this->pass(format_string('Disabled modules: %modules.', [
578 '%modules' => implode(', ', $modules),
583 * Registers a stream wrapper for this test.
585 * @param string $scheme
586 * The scheme to register.
587 * @param string $class
588 * The fully qualified class name to register.
590 * The Drupal Stream Wrapper API type. Defaults to
591 * StreamWrapperInterface::NORMAL.
593 protected function registerStreamWrapper($scheme, $class, $type = StreamWrapperInterface::NORMAL) {
594 $this->container->get('stream_wrapper_manager')->registerWrapper($scheme, $class, $type);
598 * Renders a render array.
600 * @param array $elements
601 * The elements to render.
604 * The rendered string output (typically HTML).
606 protected function render(array &$elements) {
607 // Use the bare HTML page renderer to render our links.
608 $renderer = $this->container->get('bare_html_page_renderer');
609 $response = $renderer->renderBarePage($elements, '', 'maintenance_page');
611 // Glean the content from the response object.
612 $content = $response->getContent();
613 $this->setRawContent($content);
614 $this->verbose('<pre style="white-space: pre-wrap">' . Html::escape($content));