annotate core/modules/simpletest/src/KernelTestBase.php @ 19:fa3358dc1485 tip

Add ndrum files
author Chris Cannam
date Wed, 28 Aug 2019 13:14:47 +0100
parents af1871eacc83
children
rev   line source
Chris@0 1 <?php
Chris@0 2
Chris@0 3 namespace Drupal\simpletest;
Chris@0 4
Chris@18 5 @trigger_error(__NAMESPACE__ . '\KernelTestBase is deprecated in Drupal 8.0.x, will be removed before Drupal 9.0.0. Use \Drupal\KernelTests\KernelTestBase instead.', E_USER_DEPRECATED);
Chris@18 6
Chris@0 7 use Drupal\Component\Utility\Html;
Chris@17 8 use Drupal\Component\Render\FormattableMarkup;
Chris@0 9 use Drupal\Component\Utility\Variable;
Chris@0 10 use Drupal\Core\Config\Development\ConfigSchemaChecker;
Chris@0 11 use Drupal\Core\Database\Database;
Chris@0 12 use Drupal\Core\DependencyInjection\ContainerBuilder;
Chris@0 13 use Drupal\Core\DrupalKernel;
Chris@0 14 use Drupal\Core\Entity\Sql\SqlEntityStorageInterface;
Chris@0 15 use Drupal\Core\Extension\ExtensionDiscovery;
Chris@18 16 use Drupal\Core\File\FileSystemInterface;
Chris@0 17 use Drupal\Core\KeyValueStore\KeyValueMemoryFactory;
Chris@0 18 use Drupal\Core\Language\Language;
Chris@0 19 use Drupal\Core\Site\Settings;
Chris@17 20 use Drupal\KernelTests\TestServiceProvider;
Chris@0 21 use Symfony\Component\DependencyInjection\Parameter;
Chris@0 22 use Drupal\Core\StreamWrapper\StreamWrapperInterface;
Chris@0 23 use Symfony\Component\DependencyInjection\Reference;
Chris@0 24 use Symfony\Component\HttpFoundation\Request;
Chris@0 25
Chris@0 26 /**
Chris@0 27 * Base class for functional integration tests.
Chris@0 28 *
Chris@0 29 * This base class should be useful for testing some types of integrations which
Chris@0 30 * don't require the overhead of a fully-installed Drupal instance, but which
Chris@0 31 * have many dependencies on parts of Drupal which can't or shouldn't be mocked.
Chris@0 32 *
Chris@0 33 * This base class partially boots a fixture Drupal. The state of the fixture
Chris@0 34 * Drupal is comparable to the state of a system during the early part of the
Chris@0 35 * installation process.
Chris@0 36 *
Chris@0 37 * Tests extending this base class can access services and the database, but the
Chris@0 38 * system is initially empty. This Drupal runs in a minimal mocked filesystem
Chris@0 39 * which operates within vfsStream.
Chris@0 40 *
Chris@0 41 * Modules specified in the $modules property are added to the service container
Chris@0 42 * for each test. The module/hook system is functional. Additional modules
Chris@0 43 * needed in a test should override $modules. Modules specified in this way will
Chris@0 44 * be added to those specified in superclasses.
Chris@0 45 *
Chris@0 46 * Unlike \Drupal\Tests\BrowserTestBase, the modules are not installed. They are
Chris@0 47 * loaded such that their services and hooks are available, but the install
Chris@0 48 * process has not been performed.
Chris@0 49 *
Chris@0 50 * Other modules can be made available in this way using
Chris@0 51 * KernelTestBase::enableModules().
Chris@0 52 *
Chris@0 53 * Some modules can be brought into a fully-installed state using
Chris@0 54 * KernelTestBase::installConfig(), KernelTestBase::installSchema(), and
Chris@0 55 * KernelTestBase::installEntitySchema(). Alternately, tests which need modules
Chris@0 56 * to be fully installed could inherit from \Drupal\Tests\BrowserTestBase.
Chris@0 57 *
Chris@0 58 * @see \Drupal\Tests\KernelTestBase::$modules
Chris@0 59 * @see \Drupal\Tests\KernelTestBase::enableModules()
Chris@0 60 * @see \Drupal\Tests\KernelTestBase::installConfig()
Chris@0 61 * @see \Drupal\Tests\KernelTestBase::installEntitySchema()
Chris@0 62 * @see \Drupal\Tests\KernelTestBase::installSchema()
Chris@0 63 * @see \Drupal\Tests\BrowserTestBase
Chris@0 64 *
Chris@0 65 * @deprecated in Drupal 8.0.x, will be removed before Drupal 9.0.0. Use
Chris@0 66 * \Drupal\KernelTests\KernelTestBase instead.
Chris@0 67 *
Chris@0 68 * @ingroup testing
Chris@0 69 */
Chris@0 70 abstract class KernelTestBase extends TestBase {
Chris@0 71
Chris@0 72 use AssertContentTrait;
Chris@0 73
Chris@0 74 /**
Chris@0 75 * Modules to enable.
Chris@0 76 *
Chris@0 77 * Test classes extending this class, and any classes in the hierarchy up to
Chris@0 78 * this class, may specify individual lists of modules to enable by setting
Chris@0 79 * this property. The values of all properties in all classes in the hierarchy
Chris@0 80 * are merged.
Chris@0 81 *
Chris@0 82 * Any modules specified in the $modules property are automatically loaded and
Chris@0 83 * set as the fixed module list.
Chris@0 84 *
Chris@0 85 * Unlike WebTestBase::setUp(), the specified modules are loaded only, but not
Chris@0 86 * automatically installed. Modules need to be installed manually, if needed.
Chris@0 87 *
Chris@0 88 * @see \Drupal\simpletest\KernelTestBase::enableModules()
Chris@0 89 * @see \Drupal\simpletest\KernelTestBase::setUp()
Chris@0 90 *
Chris@0 91 * @var array
Chris@0 92 */
Chris@0 93 public static $modules = [];
Chris@0 94
Chris@0 95 private $moduleFiles;
Chris@0 96 private $themeFiles;
Chris@0 97
Chris@0 98 /**
Chris@0 99 * The configuration directories for this test run.
Chris@0 100 *
Chris@0 101 * @var array
Chris@0 102 */
Chris@0 103 protected $configDirectories = [];
Chris@0 104
Chris@0 105 /**
Chris@0 106 * A KeyValueMemoryFactory instance to use when building the container.
Chris@0 107 *
Chris@17 108 * @var \Drupal\Core\KeyValueStore\KeyValueMemoryFactory
Chris@0 109 */
Chris@0 110 protected $keyValueFactory;
Chris@0 111
Chris@0 112 /**
Chris@0 113 * Array of registered stream wrappers.
Chris@0 114 *
Chris@0 115 * @var array
Chris@0 116 */
Chris@0 117 protected $streamWrappers = [];
Chris@0 118
Chris@0 119 /**
Chris@0 120 * {@inheritdoc}
Chris@0 121 */
Chris@0 122 public function __construct($test_id = NULL) {
Chris@0 123 parent::__construct($test_id);
Chris@0 124 $this->skipClasses[__CLASS__] = TRUE;
Chris@0 125 }
Chris@0 126
Chris@0 127 /**
Chris@0 128 * {@inheritdoc}
Chris@0 129 */
Chris@0 130 protected function beforePrepareEnvironment() {
Chris@0 131 // Copy/prime extension file lists once to avoid filesystem scans.
Chris@0 132 if (!isset($this->moduleFiles)) {
Chris@0 133 $this->moduleFiles = \Drupal::state()->get('system.module.files') ?: [];
Chris@0 134 $this->themeFiles = \Drupal::state()->get('system.theme.files') ?: [];
Chris@0 135 }
Chris@0 136 }
Chris@0 137
Chris@0 138 /**
Chris@0 139 * Create and set new configuration directories.
Chris@0 140 *
Chris@0 141 * @see config_get_config_directory()
Chris@0 142 *
Chris@0 143 * @throws \RuntimeException
Chris@0 144 * Thrown when CONFIG_SYNC_DIRECTORY cannot be created or made writable.
Chris@0 145 */
Chris@0 146 protected function prepareConfigDirectories() {
Chris@0 147 $this->configDirectories = [];
Chris@0 148 include_once DRUPAL_ROOT . '/core/includes/install.inc';
Chris@0 149 // Assign the relative path to the global variable.
Chris@0 150 $path = $this->siteDirectory . '/config_' . CONFIG_SYNC_DIRECTORY;
Chris@0 151 $GLOBALS['config_directories'][CONFIG_SYNC_DIRECTORY] = $path;
Chris@0 152 // Ensure the directory can be created and is writeable.
Chris@18 153 if (!\Drupal::service('file_system')->prepareDirectory($path, FileSystemInterface::CREATE_DIRECTORY | FileSystemInterface::MODIFY_PERMISSIONS)) {
Chris@0 154 throw new \RuntimeException("Failed to create '" . CONFIG_SYNC_DIRECTORY . "' config directory $path");
Chris@0 155 }
Chris@0 156 // Provide the already resolved path for tests.
Chris@0 157 $this->configDirectories[CONFIG_SYNC_DIRECTORY] = $path;
Chris@0 158 }
Chris@0 159
Chris@0 160 /**
Chris@0 161 * {@inheritdoc}
Chris@0 162 */
Chris@0 163 protected function setUp() {
Chris@0 164 $this->keyValueFactory = new KeyValueMemoryFactory();
Chris@0 165
Chris@0 166 // Back up settings from TestBase::prepareEnvironment().
Chris@0 167 $settings = Settings::getAll();
Chris@0 168
Chris@0 169 // Allow for test-specific overrides.
Chris@0 170 $directory = DRUPAL_ROOT . '/' . $this->siteDirectory;
Chris@0 171 $settings_services_file = DRUPAL_ROOT . '/' . $this->originalSite . '/testing.services.yml';
Chris@0 172 $container_yamls = [];
Chris@0 173 if (file_exists($settings_services_file)) {
Chris@0 174 // Copy the testing-specific service overrides in place.
Chris@0 175 $testing_services_file = $directory . '/services.yml';
Chris@0 176 copy($settings_services_file, $testing_services_file);
Chris@0 177 $container_yamls[] = $testing_services_file;
Chris@0 178 }
Chris@0 179 $settings_testing_file = DRUPAL_ROOT . '/' . $this->originalSite . '/settings.testing.php';
Chris@0 180 if (file_exists($settings_testing_file)) {
Chris@0 181 // Copy the testing-specific settings.php overrides in place.
Chris@0 182 copy($settings_testing_file, $directory . '/settings.testing.php');
Chris@0 183 }
Chris@0 184
Chris@0 185 if (file_exists($directory . '/settings.testing.php')) {
Chris@0 186 // Add the name of the testing class to settings.php and include the
Chris@0 187 // testing specific overrides
Chris@0 188 $hash_salt = Settings::getHashSalt();
Chris@0 189 $test_class = get_class($this);
Chris@0 190 $container_yamls_export = Variable::export($container_yamls);
Chris@0 191 $php = <<<EOD
Chris@0 192 <?php
Chris@0 193
Chris@0 194 \$settings['hash_salt'] = '$hash_salt';
Chris@0 195 \$settings['container_yamls'] = $container_yamls_export;
Chris@0 196
Chris@0 197 \$test_class = '$test_class';
Chris@0 198 include DRUPAL_ROOT . '/' . \$site_path . '/settings.testing.php';
Chris@0 199 EOD;
Chris@0 200 file_put_contents($directory . '/settings.php', $php);
Chris@0 201 }
Chris@0 202
Chris@0 203 // Add this test class as a service provider.
Chris@0 204 // @todo Remove the indirection; implement ServiceProviderInterface instead.
Chris@17 205 $GLOBALS['conf']['container_service_providers']['TestServiceProvider'] = TestServiceProvider::class;
Chris@0 206
Chris@0 207 // Bootstrap a new kernel.
Chris@0 208 $class_loader = require DRUPAL_ROOT . '/autoload.php';
Chris@0 209 $this->kernel = new DrupalKernel('testing', $class_loader, FALSE);
Chris@0 210 $request = Request::create('/');
Chris@0 211 $site_path = DrupalKernel::findSitePath($request);
Chris@0 212 $this->kernel->setSitePath($site_path);
Chris@0 213 if (file_exists($directory . '/settings.testing.php')) {
Chris@0 214 Settings::initialize(DRUPAL_ROOT, $site_path, $class_loader);
Chris@0 215 }
Chris@0 216 $this->kernel->boot();
Chris@0 217
Chris@0 218 // Ensure database install tasks have been run.
Chris@0 219 require_once __DIR__ . '/../../../includes/install.inc';
Chris@0 220 $connection = Database::getConnection();
Chris@0 221 $errors = db_installer_object($connection->driver())->runTasks();
Chris@0 222 if (!empty($errors)) {
Chris@0 223 $this->fail('Failed to run installer database tasks: ' . implode(', ', $errors));
Chris@0 224 }
Chris@0 225
Chris@0 226 // Reboot the kernel because the container might contain a connection to the
Chris@0 227 // database that has been closed during the database install tasks. This
Chris@0 228 // prevents any services created during the first boot from having stale
Chris@0 229 // database connections, for example, \Drupal\Core\Config\DatabaseStorage.
Chris@0 230 $this->kernel->shutdown();
Chris@0 231 $this->kernel->boot();
Chris@0 232
Chris@0 233 // Save the original site directory path, so that extensions in the
Chris@0 234 // site-specific directory can still be discovered in the test site
Chris@0 235 // environment.
Chris@0 236 // @see \Drupal\Core\Extension\ExtensionDiscovery::scan()
Chris@0 237 $settings['test_parent_site'] = $this->originalSite;
Chris@0 238
Chris@0 239 // Restore and merge settings.
Chris@0 240 // DrupalKernel::boot() initializes new Settings, and the containerBuild()
Chris@0 241 // method sets additional settings.
Chris@0 242 new Settings($settings + Settings::getAll());
Chris@0 243
Chris@0 244 // Create and set new configuration directories.
Chris@0 245 $this->prepareConfigDirectories();
Chris@0 246
Chris@0 247 // Set the request scope.
Chris@0 248 $this->container = $this->kernel->getContainer();
Chris@0 249 $this->container->get('request_stack')->push($request);
Chris@0 250
Chris@0 251 // Re-inject extension file listings into state, unless the key/value
Chris@0 252 // service was overridden (in which case its storage does not exist yet).
Chris@0 253 if ($this->container->get('keyvalue') instanceof KeyValueMemoryFactory) {
Chris@0 254 $this->container->get('state')->set('system.module.files', $this->moduleFiles);
Chris@0 255 $this->container->get('state')->set('system.theme.files', $this->themeFiles);
Chris@0 256 }
Chris@0 257
Chris@0 258 // Create a minimal core.extension configuration object so that the list of
Chris@0 259 // enabled modules can be maintained allowing
Chris@0 260 // \Drupal\Core\Config\ConfigInstaller::installDefaultConfig() to work.
Chris@0 261 // Write directly to active storage to avoid early instantiation of
Chris@0 262 // the event dispatcher which can prevent modules from registering events.
Chris@0 263 \Drupal::service('config.storage')->write('core.extension', ['module' => [], 'theme' => [], 'profile' => '']);
Chris@0 264
Chris@0 265 // Collect and set a fixed module list.
Chris@0 266 $class = get_class($this);
Chris@0 267 $modules = [];
Chris@0 268 while ($class) {
Chris@0 269 if (property_exists($class, 'modules')) {
Chris@0 270 // Only add the modules, if the $modules property was not inherited.
Chris@0 271 $rp = new \ReflectionProperty($class, 'modules');
Chris@0 272 if ($rp->class == $class) {
Chris@0 273 $modules[$class] = $class::$modules;
Chris@0 274 }
Chris@0 275 }
Chris@0 276 $class = get_parent_class($class);
Chris@0 277 }
Chris@0 278 // Modules have been collected in reverse class hierarchy order; modules
Chris@0 279 // defined by base classes should be sorted first. Then, merge the results
Chris@0 280 // together.
Chris@0 281 $modules = array_reverse($modules);
Chris@0 282 $modules = call_user_func_array('array_merge_recursive', $modules);
Chris@0 283 if ($modules) {
Chris@0 284 $this->enableModules($modules);
Chris@0 285 }
Chris@0 286
Chris@0 287 // Tests based on this class are entitled to use Drupal's File and
Chris@0 288 // StreamWrapper APIs.
Chris@18 289 \Drupal::service('file_system')->prepareDirectory($this->publicFilesDirectory, FileSystemInterface::CREATE_DIRECTORY | FileSystemInterface::MODIFY_PERMISSIONS);
Chris@0 290 $this->settingsSet('file_public_path', $this->publicFilesDirectory);
Chris@0 291 $this->streamWrappers = [];
Chris@0 292 $this->registerStreamWrapper('public', 'Drupal\Core\StreamWrapper\PublicStream');
Chris@0 293 // The temporary stream wrapper is able to operate both with and without
Chris@0 294 // configuration.
Chris@0 295 $this->registerStreamWrapper('temporary', 'Drupal\Core\StreamWrapper\TemporaryStream');
Chris@0 296
Chris@0 297 // Manually configure the test mail collector implementation to prevent
Chris@0 298 // tests from sending out emails and collect them in state instead.
Chris@0 299 // While this should be enforced via settings.php prior to installation,
Chris@0 300 // some tests expect to be able to test mail system implementations.
Chris@0 301 $GLOBALS['config']['system.mail']['interface']['default'] = 'test_mail_collector';
Chris@0 302 }
Chris@0 303
Chris@0 304 /**
Chris@0 305 * {@inheritdoc}
Chris@0 306 */
Chris@0 307 protected function tearDown() {
Chris@0 308 if ($this->kernel instanceof DrupalKernel) {
Chris@0 309 $this->kernel->shutdown();
Chris@0 310 }
Chris@0 311 // Before tearing down the test environment, ensure that no stream wrapper
Chris@0 312 // of this test leaks into the parent environment. Unlike all other global
Chris@0 313 // state variables in Drupal, stream wrappers are a global state construct
Chris@0 314 // of PHP core, which has to be maintained manually.
Chris@0 315 // @todo Move StreamWrapper management into DrupalKernel.
Chris@0 316 // @see https://www.drupal.org/node/2028109
Chris@0 317 foreach ($this->streamWrappers as $scheme => $type) {
Chris@0 318 $this->unregisterStreamWrapper($scheme, $type);
Chris@0 319 }
Chris@0 320 parent::tearDown();
Chris@0 321 }
Chris@0 322
Chris@0 323 /**
Chris@0 324 * Sets up the base service container for this test.
Chris@0 325 *
Chris@0 326 * Extend this method in your test to register additional service overrides
Chris@0 327 * that need to persist a DrupalKernel reboot. This method is called whenever
Chris@0 328 * the kernel is rebuilt.
Chris@0 329 *
Chris@0 330 * @see \Drupal\simpletest\KernelTestBase::setUp()
Chris@0 331 * @see \Drupal\simpletest\KernelTestBase::enableModules()
Chris@0 332 * @see \Drupal\simpletest\KernelTestBase::disableModules()
Chris@0 333 */
Chris@0 334 public function containerBuild(ContainerBuilder $container) {
Chris@0 335 // Keep the container object around for tests.
Chris@0 336 $this->container = $container;
Chris@0 337
Chris@0 338 // Set the default language on the minimal container.
Chris@0 339 $this->container->setParameter('language.default_values', $this->defaultLanguageData());
Chris@0 340
Chris@0 341 $container->register('lock', 'Drupal\Core\Lock\NullLockBackend');
Chris@0 342 $container->register('cache_factory', 'Drupal\Core\Cache\MemoryBackendFactory');
Chris@0 343
Chris@0 344 $container
Chris@0 345 ->register('config.storage', 'Drupal\Core\Config\DatabaseStorage')
Chris@0 346 ->addArgument(Database::getConnection())
Chris@0 347 ->addArgument('config');
Chris@0 348
Chris@0 349 if ($this->strictConfigSchema) {
Chris@0 350 $container
Chris@0 351 ->register('simpletest.config_schema_checker', ConfigSchemaChecker::class)
Chris@0 352 ->addArgument(new Reference('config.typed'))
Chris@0 353 ->addArgument($this->getConfigSchemaExclusions())
Chris@0 354 ->addTag('event_subscriber');
Chris@0 355 }
Chris@0 356
Chris@0 357 $keyvalue_options = $container->getParameter('factory.keyvalue') ?: [];
Chris@0 358 $keyvalue_options['default'] = 'keyvalue.memory';
Chris@0 359 $container->setParameter('factory.keyvalue', $keyvalue_options);
Chris@0 360 $container->set('keyvalue.memory', $this->keyValueFactory);
Chris@0 361 if (!$container->has('keyvalue')) {
Chris@0 362 // TestBase::setUp puts a completely empty container in
Chris@0 363 // $this->container which is somewhat the mirror of the empty
Chris@0 364 // environment being set up. Unit tests need not to waste time with
Chris@0 365 // getting a container set up for them. Drupal Unit Tests might just get
Chris@0 366 // away with a simple container holding the absolute bare minimum. When
Chris@0 367 // a kernel is overridden then there's no need to re-register the keyvalue
Chris@0 368 // service but when a test is happy with the superminimal container put
Chris@0 369 // together here, it still might a keyvalue storage for anything using
Chris@0 370 // \Drupal::state() -- that's why a memory service was added in the first
Chris@0 371 // place.
Chris@0 372 $container->register('settings', 'Drupal\Core\Site\Settings')
Chris@0 373 ->setFactoryClass('Drupal\Core\Site\Settings')
Chris@0 374 ->setFactoryMethod('getInstance');
Chris@0 375
Chris@0 376 $container
Chris@0 377 ->register('keyvalue', 'Drupal\Core\KeyValueStore\KeyValueFactory')
Chris@0 378 ->addArgument(new Reference('service_container'))
Chris@0 379 ->addArgument(new Parameter('factory.keyvalue'));
Chris@0 380
Chris@0 381 $container->register('state', 'Drupal\Core\State\State')
Chris@0 382 ->addArgument(new Reference('keyvalue'));
Chris@0 383 }
Chris@0 384
Chris@0 385 if ($container->hasDefinition('path_processor_alias')) {
Chris@0 386 // Prevent the alias-based path processor, which requires a url_alias db
Chris@0 387 // table, from being registered to the path processor manager. We do this
Chris@0 388 // by removing the tags that the compiler pass looks for. This means the
Chris@0 389 // url generator can safely be used within tests.
Chris@0 390 $definition = $container->getDefinition('path_processor_alias');
Chris@0 391 $definition->clearTag('path_processor_inbound')->clearTag('path_processor_outbound');
Chris@0 392 }
Chris@0 393
Chris@0 394 if ($container->hasDefinition('password')) {
Chris@0 395 $container->getDefinition('password')->setArguments([1]);
Chris@0 396 }
Chris@0 397
Chris@0 398 // Register the stream wrapper manager.
Chris@0 399 $container
Chris@0 400 ->register('stream_wrapper_manager', 'Drupal\Core\StreamWrapper\StreamWrapperManager')
Chris@0 401 ->addArgument(new Reference('module_handler'))
Chris@0 402 ->addMethodCall('setContainer', [new Reference('service_container')]);
Chris@0 403
Chris@0 404 $request = Request::create('/');
Chris@0 405 $container->get('request_stack')->push($request);
Chris@0 406 }
Chris@0 407
Chris@0 408 /**
Chris@0 409 * Provides the data for setting the default language on the container.
Chris@0 410 *
Chris@0 411 * @return array
Chris@0 412 * The data array for the default language.
Chris@0 413 */
Chris@0 414 protected function defaultLanguageData() {
Chris@0 415 return Language::$defaultValues;
Chris@0 416 }
Chris@0 417
Chris@0 418 /**
Chris@0 419 * Installs default configuration for a given list of modules.
Chris@0 420 *
Chris@0 421 * @param array $modules
Chris@0 422 * A list of modules for which to install default configuration.
Chris@0 423 *
Chris@0 424 * @throws \RuntimeException
Chris@0 425 * Thrown when any module listed in $modules is not enabled.
Chris@0 426 */
Chris@0 427 protected function installConfig(array $modules) {
Chris@0 428 foreach ($modules as $module) {
Chris@0 429 if (!$this->container->get('module_handler')->moduleExists($module)) {
Chris@0 430 throw new \RuntimeException("'$module' module is not enabled");
Chris@0 431 }
Chris@0 432 \Drupal::service('config.installer')->installDefaultConfig('module', $module);
Chris@0 433 }
Chris@0 434 $this->pass(format_string('Installed default config: %modules.', [
Chris@0 435 '%modules' => implode(', ', $modules),
Chris@0 436 ]));
Chris@0 437 }
Chris@0 438
Chris@0 439 /**
Chris@0 440 * Installs a specific table from a module schema definition.
Chris@0 441 *
Chris@0 442 * @param string $module
Chris@0 443 * The name of the module that defines the table's schema.
Chris@0 444 * @param string|array $tables
Chris@0 445 * The name or an array of the names of the tables to install.
Chris@0 446 *
Chris@0 447 * @throws \RuntimeException
Chris@0 448 * Thrown when $module is not enabled or when the table schema cannot be
Chris@0 449 * found in the module specified.
Chris@0 450 */
Chris@0 451 protected function installSchema($module, $tables) {
Chris@0 452 // drupal_get_module_schema() is technically able to install a schema
Chris@0 453 // of a non-enabled module, but its ability to load the module's .install
Chris@0 454 // file depends on many other factors. To prevent differences in test
Chris@0 455 // behavior and non-reproducible test failures, we only allow the schema of
Chris@0 456 // explicitly loaded/enabled modules to be installed.
Chris@0 457 if (!$this->container->get('module_handler')->moduleExists($module)) {
Chris@0 458 throw new \RuntimeException("'$module' module is not enabled");
Chris@0 459 }
Chris@0 460
Chris@0 461 $tables = (array) $tables;
Chris@0 462 foreach ($tables as $table) {
Chris@0 463 $schema = drupal_get_module_schema($module, $table);
Chris@0 464 if (empty($schema)) {
Chris@0 465 // BC layer to avoid some contrib tests to fail.
Chris@0 466 // @todo Remove the BC layer before 8.1.x release.
Chris@0 467 // @see https://www.drupal.org/node/2670360
Chris@0 468 // @see https://www.drupal.org/node/2670454
Chris@0 469 if ($module == 'system') {
Chris@0 470 continue;
Chris@0 471 }
Chris@0 472 throw new \RuntimeException("Unknown '$table' table schema in '$module' module.");
Chris@0 473 }
Chris@0 474 $this->container->get('database')->schema()->createTable($table, $schema);
Chris@0 475 }
Chris@0 476 $this->pass(format_string('Installed %module tables: %tables.', [
Chris@0 477 '%tables' => '{' . implode('}, {', $tables) . '}',
Chris@0 478 '%module' => $module,
Chris@0 479 ]));
Chris@0 480 }
Chris@0 481
Chris@0 482 /**
Chris@0 483 * Installs the storage schema for a specific entity type.
Chris@0 484 *
Chris@0 485 * @param string $entity_type_id
Chris@0 486 * The ID of the entity type.
Chris@0 487 */
Chris@0 488 protected function installEntitySchema($entity_type_id) {
Chris@0 489 /** @var \Drupal\Core\Entity\EntityManagerInterface $entity_manager */
Chris@0 490 $entity_manager = $this->container->get('entity.manager');
Chris@0 491 $entity_type = $entity_manager->getDefinition($entity_type_id);
Chris@0 492 $entity_manager->onEntityTypeCreate($entity_type);
Chris@0 493
Chris@0 494 // For test runs, the most common storage backend is a SQL database. For
Chris@0 495 // this case, ensure the tables got created.
Chris@0 496 $storage = $entity_manager->getStorage($entity_type_id);
Chris@0 497 if ($storage instanceof SqlEntityStorageInterface) {
Chris@0 498 $tables = $storage->getTableMapping()->getTableNames();
Chris@0 499 $db_schema = $this->container->get('database')->schema();
Chris@0 500 $all_tables_exist = TRUE;
Chris@0 501 foreach ($tables as $table) {
Chris@0 502 if (!$db_schema->tableExists($table)) {
Chris@17 503 $this->fail(new FormattableMarkup('Installed entity type table for the %entity_type entity type: %table', [
Chris@0 504 '%entity_type' => $entity_type_id,
Chris@0 505 '%table' => $table,
Chris@0 506 ]));
Chris@0 507 $all_tables_exist = FALSE;
Chris@0 508 }
Chris@0 509 }
Chris@0 510 if ($all_tables_exist) {
Chris@17 511 $this->pass(new FormattableMarkup('Installed entity type tables for the %entity_type entity type: %tables', [
Chris@0 512 '%entity_type' => $entity_type_id,
Chris@0 513 '%tables' => '{' . implode('}, {', $tables) . '}',
Chris@0 514 ]));
Chris@0 515 }
Chris@0 516 }
Chris@0 517 }
Chris@0 518
Chris@0 519 /**
Chris@0 520 * Enables modules for this test.
Chris@0 521 *
Chris@0 522 * To install test modules outside of the testing environment, add
Chris@0 523 * @code
Chris@0 524 * $settings['extension_discovery_scan_tests'] = TRUE;
Chris@0 525 * @endcode
Chris@0 526 * to your settings.php.
Chris@0 527 *
Chris@0 528 * @param array $modules
Chris@0 529 * A list of modules to enable. Dependencies are not resolved; i.e.,
Chris@0 530 * multiple modules have to be specified with dependent modules first.
Chris@0 531 * The new modules are only added to the active module list and loaded.
Chris@0 532 */
Chris@0 533 protected function enableModules(array $modules) {
Chris@0 534 // Perform an ExtensionDiscovery scan as this function may receive a
Chris@0 535 // profile that is not the current profile, and we don't yet have a cached
Chris@0 536 // way to receive inactive profile information.
Chris@0 537 // @todo Remove as part of https://www.drupal.org/node/2186491
Chris@0 538 $listing = new ExtensionDiscovery(\Drupal::root());
Chris@0 539 $module_list = $listing->scan('module');
Chris@0 540 // In ModuleHandlerTest we pass in a profile as if it were a module.
Chris@0 541 $module_list += $listing->scan('profile');
Chris@0 542 // Set the list of modules in the extension handler.
Chris@0 543 $module_handler = $this->container->get('module_handler');
Chris@0 544
Chris@0 545 // Write directly to active storage to avoid early instantiation of
Chris@0 546 // the event dispatcher which can prevent modules from registering events.
Chris@0 547 $active_storage = \Drupal::service('config.storage');
Chris@0 548 $extensions = $active_storage->read('core.extension');
Chris@0 549
Chris@0 550 foreach ($modules as $module) {
Chris@0 551 $module_handler->addModule($module, $module_list[$module]->getPath());
Chris@0 552 // Maintain the list of enabled modules in configuration.
Chris@0 553 $extensions['module'][$module] = 0;
Chris@0 554 }
Chris@0 555 $active_storage->write('core.extension', $extensions);
Chris@0 556
Chris@0 557 // Update the kernel to make their services available.
Chris@0 558 $module_filenames = $module_handler->getModuleList();
Chris@0 559 $this->kernel->updateModules($module_filenames, $module_filenames);
Chris@0 560
Chris@0 561 // Ensure isLoaded() is TRUE in order to make
Chris@0 562 // \Drupal\Core\Theme\ThemeManagerInterface::render() work.
Chris@0 563 // Note that the kernel has rebuilt the container; this $module_handler is
Chris@0 564 // no longer the $module_handler instance from above.
Chris@0 565 $this->container->get('module_handler')->reload();
Chris@0 566 $this->pass(format_string('Enabled modules: %modules.', [
Chris@0 567 '%modules' => implode(', ', $modules),
Chris@0 568 ]));
Chris@0 569 }
Chris@0 570
Chris@0 571 /**
Chris@0 572 * Disables modules for this test.
Chris@0 573 *
Chris@0 574 * @param array $modules
Chris@0 575 * A list of modules to disable. Dependencies are not resolved; i.e.,
Chris@0 576 * multiple modules have to be specified with dependent modules first.
Chris@0 577 * Code of previously active modules is still loaded. The modules are only
Chris@0 578 * removed from the active module list.
Chris@0 579 */
Chris@0 580 protected function disableModules(array $modules) {
Chris@0 581 // Unset the list of modules in the extension handler.
Chris@0 582 $module_handler = $this->container->get('module_handler');
Chris@0 583 $module_filenames = $module_handler->getModuleList();
Chris@0 584 $extension_config = $this->config('core.extension');
Chris@0 585 foreach ($modules as $module) {
Chris@0 586 unset($module_filenames[$module]);
Chris@0 587 $extension_config->clear('module.' . $module);
Chris@0 588 }
Chris@0 589 $extension_config->save();
Chris@0 590 $module_handler->setModuleList($module_filenames);
Chris@0 591 $module_handler->resetImplementations();
Chris@0 592 // Update the kernel to remove their services.
Chris@0 593 $this->kernel->updateModules($module_filenames, $module_filenames);
Chris@0 594
Chris@0 595 // Ensure isLoaded() is TRUE in order to make
Chris@0 596 // \Drupal\Core\Theme\ThemeManagerInterface::render() work.
Chris@0 597 // Note that the kernel has rebuilt the container; this $module_handler is
Chris@0 598 // no longer the $module_handler instance from above.
Chris@0 599 $module_handler = $this->container->get('module_handler');
Chris@0 600 $module_handler->reload();
Chris@0 601 $this->pass(format_string('Disabled modules: %modules.', [
Chris@0 602 '%modules' => implode(', ', $modules),
Chris@0 603 ]));
Chris@0 604 }
Chris@0 605
Chris@0 606 /**
Chris@0 607 * Registers a stream wrapper for this test.
Chris@0 608 *
Chris@0 609 * @param string $scheme
Chris@0 610 * The scheme to register.
Chris@0 611 * @param string $class
Chris@0 612 * The fully qualified class name to register.
Chris@0 613 * @param int $type
Chris@0 614 * The Drupal Stream Wrapper API type. Defaults to
Chris@0 615 * StreamWrapperInterface::NORMAL.
Chris@0 616 */
Chris@0 617 protected function registerStreamWrapper($scheme, $class, $type = StreamWrapperInterface::NORMAL) {
Chris@0 618 $this->container->get('stream_wrapper_manager')->registerWrapper($scheme, $class, $type);
Chris@0 619 }
Chris@0 620
Chris@0 621 /**
Chris@0 622 * Renders a render array.
Chris@0 623 *
Chris@0 624 * @param array $elements
Chris@0 625 * The elements to render.
Chris@0 626 *
Chris@0 627 * @return string
Chris@0 628 * The rendered string output (typically HTML).
Chris@0 629 */
Chris@0 630 protected function render(array &$elements) {
Chris@0 631 // Use the bare HTML page renderer to render our links.
Chris@0 632 $renderer = $this->container->get('bare_html_page_renderer');
Chris@0 633 $response = $renderer->renderBarePage($elements, '', 'maintenance_page');
Chris@0 634
Chris@0 635 // Glean the content from the response object.
Chris@0 636 $content = $response->getContent();
Chris@0 637 $this->setRawContent($content);
Chris@0 638 $this->verbose('<pre style="white-space: pre-wrap">' . Html::escape($content));
Chris@0 639 return $content;
Chris@0 640 }
Chris@0 641
Chris@0 642 }