Chris@0: ['parsedFiles'], Chris@0: 'Drupal\Core\DependencyInjection\YamlFileLoader' => ['yaml'], Chris@0: 'Drupal\Core\Extension\ExtensionDiscovery' => ['files'], Chris@0: 'Drupal\Core\Extension\InfoParser' => ['parsedInfos'], Chris@0: // Drupal::$container cannot be serialized. Chris@0: 'Drupal' => ['container'], Chris@0: // Settings cannot be serialized. Chris@0: 'Drupal\Core\Site\Settings' => ['instance'], Chris@0: ]; Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: * Chris@0: * Do not forward any global state from the parent process to the processes Chris@0: * that run the actual tests. Chris@0: * Chris@0: * @see self::runTestInSeparateProcess Chris@0: */ Chris@0: protected $preserveGlobalState = FALSE; Chris@0: Chris@0: /** Chris@0: * @var \Composer\Autoload\Classloader Chris@0: */ Chris@0: protected $classLoader; Chris@0: Chris@0: /** Chris@0: * @var string Chris@0: */ Chris@0: protected $siteDirectory; Chris@0: Chris@0: /** Chris@0: * @var string Chris@0: */ Chris@0: protected $databasePrefix; Chris@0: Chris@0: /** Chris@0: * @var \Drupal\Core\DependencyInjection\ContainerBuilder Chris@0: */ Chris@0: protected $container; Chris@0: Chris@0: /** Chris@0: * Modules to enable. Chris@0: * Chris@0: * The test runner will merge the $modules lists from this class, the class Chris@0: * it extends, and so on up the class hierarchy. It is not necessary to Chris@0: * include modules in your list that a parent class has already declared. Chris@0: * Chris@0: * @see \Drupal\Tests\KernelTestBase::enableModules() Chris@0: * @see \Drupal\Tests\KernelTestBase::bootKernel() Chris@0: * Chris@0: * @var array Chris@0: */ Chris@0: protected static $modules = []; Chris@0: Chris@0: /** Chris@0: * The virtual filesystem root directory. Chris@0: * Chris@0: * @var \org\bovigo\vfs\vfsStreamDirectory Chris@0: */ Chris@0: protected $vfsRoot; Chris@0: Chris@0: /** Chris@0: * @todo Move into Config test base class. Chris@0: * @var \Drupal\Core\Config\ConfigImporter Chris@0: */ Chris@0: protected $configImporter; Chris@0: Chris@0: /** Chris@0: * The app root. Chris@0: * Chris@0: * @var string Chris@0: */ Chris@0: protected $root; Chris@0: Chris@0: /** Chris@0: * Set to TRUE to strict check all configuration saved. Chris@0: * Chris@0: * @see \Drupal\Core\Config\Development\ConfigSchemaChecker Chris@0: * Chris@0: * @var bool Chris@0: */ Chris@0: protected $strictConfigSchema = TRUE; Chris@0: Chris@0: /** Chris@0: * An array of config object names that are excluded from schema checking. Chris@0: * Chris@0: * @var string[] Chris@0: */ Chris@0: protected static $configSchemaCheckerExclusions = [ Chris@0: // Following are used to test lack of or partial schema. Where partial Chris@0: // schema is provided, that is explicitly tested in specific tests. Chris@0: 'config_schema_test.noschema', Chris@0: 'config_schema_test.someschema', Chris@0: 'config_schema_test.schema_data_types', Chris@0: 'config_schema_test.no_schema_data_types', Chris@0: // Used to test application of schema to filtering of configuration. Chris@0: 'config_test.dynamic.system', Chris@0: ]; Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public static function setUpBeforeClass() { Chris@0: parent::setUpBeforeClass(); Chris@0: Chris@0: // Change the current dir to DRUPAL_ROOT. Chris@0: chdir(static::getDrupalRoot()); Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: protected function setUp() { Chris@0: parent::setUp(); Chris@0: Chris@0: $this->root = static::getDrupalRoot(); Chris@0: $this->initFileCache(); Chris@0: $this->bootEnvironment(); Chris@0: $this->bootKernel(); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Bootstraps a basic test environment. Chris@0: * Chris@0: * Should not be called by tests. Only visible for DrupalKernel integration Chris@0: * tests. Chris@0: * Chris@14: * @see \Drupal\KernelTests\Core\DrupalKernel\DrupalKernelTest Chris@0: * @internal Chris@0: */ Chris@0: protected function bootEnvironment() { Chris@0: $this->streamWrappers = []; Chris@0: \Drupal::unsetContainer(); Chris@0: Chris@0: $this->classLoader = require $this->root . '/autoload.php'; Chris@0: Chris@0: require_once $this->root . '/core/includes/bootstrap.inc'; Chris@0: Chris@0: // Set up virtual filesystem. Chris@0: Database::addConnectionInfo('default', 'test-runner', $this->getDatabaseConnectionInfo()['default']); Chris@0: $test_db = new TestDatabase(); Chris@0: $this->siteDirectory = $test_db->getTestSitePath(); Chris@0: Chris@0: // Ensure that all code that relies on drupal_valid_test_ua() can still be Chris@0: // safely executed. This primarily affects the (test) site directory Chris@0: // resolution (used by e.g. LocalStream and PhpStorage). Chris@0: $this->databasePrefix = $test_db->getDatabasePrefix(); Chris@0: drupal_valid_test_ua($this->databasePrefix); Chris@0: Chris@0: $settings = [ Chris@0: 'hash_salt' => get_class($this), Chris@0: 'file_public_path' => $this->siteDirectory . '/files', Chris@0: // Disable Twig template caching/dumping. Chris@0: 'twig_cache' => FALSE, Chris@0: // @see \Drupal\KernelTests\KernelTestBase::register() Chris@0: ]; Chris@0: new Settings($settings); Chris@0: Chris@0: $this->setUpFilesystem(); Chris@0: Chris@0: foreach (Database::getAllConnectionInfo() as $key => $targets) { Chris@0: Database::removeConnection($key); Chris@0: } Chris@0: Database::addConnectionInfo('default', 'default', $this->getDatabaseConnectionInfo()['default']); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Sets up the filesystem, so things like the file directory. Chris@0: */ Chris@0: protected function setUpFilesystem() { Chris@0: $test_db = new TestDatabase($this->databasePrefix); Chris@0: $test_site_path = $test_db->getTestSitePath(); Chris@0: Chris@0: $this->vfsRoot = vfsStream::setup('root'); Chris@0: $this->vfsRoot->addChild(vfsStream::newDirectory($test_site_path)); Chris@0: $this->siteDirectory = vfsStream::url('root/' . $test_site_path); Chris@0: Chris@0: mkdir($this->siteDirectory . '/files', 0775); Chris@0: mkdir($this->siteDirectory . '/files/config/' . CONFIG_SYNC_DIRECTORY, 0775, TRUE); Chris@0: Chris@0: $settings = Settings::getInstance() ? Settings::getAll() : []; Chris@0: $settings['file_public_path'] = $this->siteDirectory . '/files'; Chris@0: new Settings($settings); Chris@0: Chris@0: $GLOBALS['config_directories'] = [ Chris@0: CONFIG_SYNC_DIRECTORY => $this->siteDirectory . '/files/config/sync', Chris@0: ]; Chris@0: } Chris@0: Chris@0: /** Chris@0: * @return string Chris@0: */ Chris@0: public function getDatabasePrefix() { Chris@0: return $this->databasePrefix; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Bootstraps a kernel for a test. Chris@0: */ Chris@0: private function bootKernel() { Chris@0: $this->setSetting('container_yamls', []); Chris@0: // Allow for test-specific overrides. Chris@12: $settings_services_file = $this->root . '/sites/default/testing.services.yml'; Chris@0: if (file_exists($settings_services_file)) { Chris@0: // Copy the testing-specific service overrides in place. Chris@12: $testing_services_file = $this->siteDirectory . '/services.yml'; Chris@0: copy($settings_services_file, $testing_services_file); Chris@0: $this->setSetting('container_yamls', [$testing_services_file]); Chris@0: } Chris@0: Chris@0: // Allow for global test environment overrides. Chris@0: if (file_exists($test_env = $this->root . '/sites/default/testing.services.yml')) { Chris@0: $GLOBALS['conf']['container_yamls']['testing'] = $test_env; Chris@0: } Chris@0: // Add this test class as a service provider. Chris@0: $GLOBALS['conf']['container_service_providers']['test'] = $this; Chris@0: Chris@0: $modules = self::getModulesToEnable(get_class($this)); Chris@0: Chris@0: // Bootstrap the kernel. Do not use createFromRequest() to retain Settings. Chris@0: $kernel = new DrupalKernel('testing', $this->classLoader, FALSE); Chris@0: $kernel->setSitePath($this->siteDirectory); Chris@0: // Boot a new one-time container from scratch. Ensure to set the module list Chris@0: // upfront to avoid a subsequent rebuild. Chris@0: if ($modules && $extensions = $this->getExtensionsForModules($modules)) { Chris@0: $kernel->updateModules($extensions, $extensions); Chris@0: } Chris@0: // DrupalKernel::boot() is not sufficient as it does not invoke preHandle(), Chris@0: // which is required to initialize legacy global variables. Chris@0: $request = Request::create('/'); Chris@0: $kernel->prepareLegacyRequest($request); Chris@0: Chris@0: // register() is only called if a new container was built/compiled. Chris@0: $this->container = $kernel->getContainer(); Chris@0: Chris@0: // Ensure database 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: if ($modules) { Chris@0: $this->container->get('module_handler')->loadAll(); Chris@0: } Chris@0: Chris@0: $this->container->get('request_stack')->push($request); Chris@0: Chris@0: // Setup the destion to the be frontpage by default. Chris@0: \Drupal::destination()->set('/'); Chris@0: Chris@0: // Write the core.extension configuration. Chris@0: // Required for ConfigInstaller::installDefaultConfig() to work. Chris@0: $this->container->get('config.storage')->write('core.extension', [ Chris@0: 'module' => array_fill_keys($modules, 0), Chris@0: 'theme' => [], Chris@0: 'profile' => '', Chris@0: ]); Chris@0: Chris@0: $settings = Settings::getAll(); Chris@0: $settings['php_storage']['default'] = [ Chris@0: 'class' => '\Drupal\Component\PhpStorage\FileStorage', Chris@0: ]; Chris@0: new Settings($settings); 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: // Manually configure the default file scheme so that modules that use file Chris@0: // functions don't have to install system and its configuration. Chris@0: // @see file_default_scheme() Chris@0: $GLOBALS['config']['system.file']['default_scheme'] = 'public'; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Configuration accessor for tests. Returns non-overridden configuration. Chris@0: * Chris@0: * @param string $name Chris@0: * The configuration name. Chris@0: * Chris@0: * @return \Drupal\Core\Config\Config Chris@0: * The configuration object with original configuration data. Chris@0: */ Chris@0: protected function config($name) { Chris@0: return $this->container->get('config.factory')->getEditable($name); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Returns the Database connection info to be used for this test. Chris@0: * Chris@0: * This method only exists for tests of the Database component itself, because Chris@0: * they require multiple database connections. Each SQLite :memory: connection Chris@0: * creates a new/separate database in memory. A shared-memory SQLite file URI Chris@0: * triggers PHP open_basedir/allow_url_fopen/allow_url_include restrictions. Chris@0: * Due to that, Database tests are running against a SQLite database that is Chris@0: * located in an actual file in the system's temporary directory. Chris@0: * Chris@0: * Other tests should not override this method. Chris@0: * Chris@0: * @return array Chris@0: * A Database connection info array. Chris@0: * Chris@0: * @internal Chris@0: */ Chris@0: protected function getDatabaseConnectionInfo() { Chris@0: // If the test is run with argument dburl then use it. Chris@0: $db_url = getenv('SIMPLETEST_DB'); Chris@0: if (empty($db_url)) { Chris@0: throw new \Exception('There is no database connection so no tests can be run. You must provide a SIMPLETEST_DB environment variable to run PHPUnit based functional tests outside of run-tests.sh. See https://www.drupal.org/node/2116263#skipped-tests for more information.'); Chris@0: } Chris@0: else { Chris@0: $database = Database::convertDbUrlToConnectionInfo($db_url, $this->root); Chris@0: Database::addConnectionInfo('default', 'default', $database); Chris@0: } Chris@0: Chris@0: // Clone the current connection and replace the current prefix. Chris@0: $connection_info = Database::getConnectionInfo('default'); Chris@0: if (!empty($connection_info)) { Chris@0: Database::renameConnection('default', 'simpletest_original_default'); Chris@0: foreach ($connection_info as $target => $value) { Chris@0: // Replace the full table prefix definition to ensure that no table Chris@0: // prefixes of the test runner leak into the test. Chris@0: $connection_info[$target]['prefix'] = [ Chris@17: 'default' => $this->databasePrefix, Chris@0: ]; Chris@0: } Chris@0: } Chris@0: return $connection_info; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Initializes the FileCache component. Chris@0: * Chris@0: * We can not use the Settings object in a component, that's why we have to do Chris@0: * it here instead of \Drupal\Component\FileCache\FileCacheFactory. Chris@0: */ Chris@0: protected function initFileCache() { Chris@0: $configuration = Settings::get('file_cache'); Chris@0: Chris@0: // Provide a default configuration, if not set. Chris@0: if (!isset($configuration['default'])) { Chris@0: // @todo Use extension_loaded('apcu') for non-testbot Chris@0: // https://www.drupal.org/node/2447753. Chris@0: if (function_exists('apcu_fetch')) { Chris@0: $configuration['default']['cache_backend_class'] = ApcuFileCacheBackend::class; Chris@0: } Chris@0: } Chris@0: FileCacheFactory::setConfiguration($configuration); Chris@0: FileCacheFactory::setPrefix(Settings::getApcuPrefix('file_cache', $this->root)); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Returns Extension objects for $modules to enable. Chris@0: * Chris@0: * @param string[] $modules Chris@0: * The list of modules to enable. Chris@0: * Chris@0: * @return \Drupal\Core\Extension\Extension[] Chris@0: * Extension objects for $modules, keyed by module name. Chris@0: * Chris@0: * @throws \PHPUnit_Framework_Exception Chris@0: * If a module is not available. Chris@0: * Chris@0: * @see \Drupal\Tests\KernelTestBase::enableModules() Chris@0: * @see \Drupal\Core\Extension\ModuleHandler::add() Chris@0: */ Chris@0: private function getExtensionsForModules(array $modules) { Chris@0: $extensions = []; Chris@0: $discovery = new ExtensionDiscovery($this->root); Chris@0: $discovery->setProfileDirectories([]); Chris@0: $list = $discovery->scan('module'); Chris@0: foreach ($modules as $name) { Chris@0: if (!isset($list[$name])) { Chris@0: throw new \PHPUnit_Framework_Exception("Unavailable module: '$name'. If this module needs to be downloaded separately, annotate the test class with '@requires module $name'."); Chris@0: } Chris@0: $extensions[$name] = $list[$name]; Chris@0: } Chris@0: return $extensions; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Registers test-specific services. Chris@0: * Chris@0: * Extend this method in your test to register additional services. This Chris@0: * method is called whenever the kernel is rebuilt. Chris@0: * Chris@0: * @param \Drupal\Core\DependencyInjection\ContainerBuilder $container Chris@0: * The service container to enhance. Chris@0: * Chris@0: * @see \Drupal\Tests\KernelTestBase::bootKernel() Chris@0: */ Chris@0: public function register(ContainerBuilder $container) { Chris@0: // Keep the container object around for tests. Chris@0: $this->container = $container; Chris@0: Chris@0: $container Chris@0: ->register('flood', 'Drupal\Core\Flood\MemoryBackend') Chris@0: ->addArgument(new Reference('request_stack')); Chris@0: $container Chris@0: ->register('lock', 'Drupal\Core\Lock\NullLockBackend'); Chris@0: $container Chris@0: ->register('cache_factory', 'Drupal\Core\Cache\MemoryBackendFactory'); Chris@0: $container Chris@0: ->register('keyvalue.memory', 'Drupal\Core\KeyValueStore\KeyValueMemoryFactory') Chris@0: // Must persist container rebuilds, or all data would vanish otherwise. Chris@0: ->addTag('persist'); Chris@0: $container Chris@0: ->setAlias('keyvalue', 'keyvalue.memory'); Chris@0: Chris@0: // Set the default language on the minimal container. Chris@0: $container->setParameter('language.default_values', Language::$defaultValues); 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: 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: $container->getDefinition('path_processor_alias') Chris@0: ->clearTag('path_processor_inbound') Chris@0: ->clearTag('path_processor_outbound'); Chris@0: } Chris@0: Chris@0: if ($container->hasDefinition('password')) { Chris@0: $container->getDefinition('password') Chris@0: ->setArguments([1]); Chris@0: } Chris@0: TestServiceProvider::addRouteProvider($container); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Gets the config schema exclusions for this test. Chris@0: * Chris@0: * @return string[] Chris@0: * An array of config object names that are excluded from schema checking. Chris@0: */ Chris@0: protected function getConfigSchemaExclusions() { Chris@0: $class = get_class($this); Chris@0: $exceptions = []; Chris@0: while ($class) { Chris@0: if (property_exists($class, 'configSchemaCheckerExclusions')) { Chris@0: $exceptions = array_merge($exceptions, $class::$configSchemaCheckerExclusions); Chris@0: } Chris@0: $class = get_parent_class($class); Chris@0: } Chris@0: // Filter out any duplicates. Chris@0: return array_unique($exceptions); Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: protected function assertPostConditions() { Chris@0: // Execute registered Drupal shutdown functions prior to tearing down. Chris@0: // @see _drupal_shutdown_function() Chris@0: $callbacks = &drupal_register_shutdown_function(); Chris@0: while ($callback = array_shift($callbacks)) { Chris@0: call_user_func_array($callback['callback'], $callback['arguments']); Chris@0: } Chris@0: Chris@0: // Shut down the kernel (if bootKernel() was called). Chris@0: // @see \Drupal\KernelTests\Core\DrupalKernel\DrupalKernelTest Chris@0: if ($this->container) { Chris@0: $this->container->get('kernel')->shutdown(); Chris@0: } Chris@0: Chris@0: // Fail in case any (new) shutdown functions exist. Chris@0: $this->assertCount(0, drupal_register_shutdown_function(), 'Unexpected Drupal shutdown callbacks exist after running shutdown functions.'); Chris@0: Chris@0: parent::assertPostConditions(); Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: protected function tearDown() { Chris@0: // Destroy the testing kernel. Chris@0: if (isset($this->kernel)) { Chris@0: $this->kernel->shutdown(); Chris@0: } Chris@0: Chris@0: // Remove all prefixed tables. Chris@0: $original_connection_info = Database::getConnectionInfo('simpletest_original_default'); Chris@0: $original_prefix = $original_connection_info['default']['prefix']['default']; Chris@0: $test_connection_info = Database::getConnectionInfo('default'); Chris@0: $test_prefix = $test_connection_info['default']['prefix']['default']; Chris@0: if ($original_prefix != $test_prefix) { Chris@0: $tables = Database::getConnection()->schema()->findTables('%'); Chris@0: foreach ($tables as $table) { Chris@0: if (Database::getConnection()->schema()->dropTable($table)) { Chris@0: unset($tables[$table]); Chris@0: } Chris@0: } Chris@0: } Chris@0: Chris@0: // Free up memory: Own properties. Chris@0: $this->classLoader = NULL; Chris@0: $this->vfsRoot = NULL; Chris@0: $this->configImporter = NULL; Chris@0: Chris@0: // Free up memory: Custom test class properties. Chris@0: // Note: Private properties cannot be cleaned up. Chris@0: $rc = new \ReflectionClass(__CLASS__); Chris@0: $blacklist = []; Chris@0: foreach ($rc->getProperties() as $property) { Chris@0: $blacklist[$property->name] = $property->getDeclaringClass()->name; Chris@0: } Chris@0: $rc = new \ReflectionClass($this); Chris@0: foreach ($rc->getProperties(\ReflectionProperty::IS_PUBLIC | \ReflectionProperty::IS_PROTECTED) as $property) { Chris@0: if (!$property->isStatic() && !isset($blacklist[$property->name])) { Chris@0: $this->{$property->name} = NULL; Chris@0: } Chris@0: } Chris@0: Chris@0: // Clean FileCache cache. Chris@0: FileCache::reset(); Chris@0: Chris@0: // Clean up statics, container, and settings. Chris@0: if (function_exists('drupal_static_reset')) { Chris@0: drupal_static_reset(); Chris@0: } Chris@0: \Drupal::unsetContainer(); Chris@0: $this->container = NULL; Chris@0: new Settings([]); Chris@0: Chris@0: parent::tearDown(); Chris@0: } Chris@0: Chris@0: /** Chris@0: * @after Chris@0: * Chris@0: * Additional tear down method to close the connection at the end. Chris@0: */ Chris@0: public function tearDownCloseDatabaseConnection() { Chris@0: // Destroy the database connection, which for example removes the memory Chris@0: // from sqlite in memory. Chris@0: foreach (Database::getAllConnectionInfo() as $key => $targets) { Chris@0: Database::removeConnection($key); Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * Installs default configuration for a given list of modules. Chris@0: * Chris@0: * @param string|string[] $modules Chris@0: * A module or list of modules for which to install default configuration. Chris@0: * Chris@0: * @throws \LogicException Chris@0: * If any module in $modules is not enabled. Chris@0: */ Chris@0: protected function installConfig($modules) { Chris@0: foreach ((array) $modules as $module) { Chris@0: if (!$this->container->get('module_handler')->moduleExists($module)) { Chris@0: throw new \LogicException("$module module is not enabled."); Chris@0: } Chris@0: $this->container->get('config.installer')->installDefaultConfig('module', $module); Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * Installs database tables 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 \LogicException Chris@0: * If $module is not enabled or the table schema cannot be found. 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 \LogicException("$module module is not enabled."); 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: if ($module == 'system') { Chris@18: @trigger_error('Special handling of system module schemas in \Drupal\KernelTests\KernelTestBase::installSchema has been deprecated in Drupal 8.7.x, remove any calls to this method that use invalid schema names. See https://www.drupal.org/project/drupal/issues/2794347.', E_USER_DEPRECATED); Chris@0: continue; Chris@0: } Chris@0: throw new \LogicException("$module module does not define a schema for table '$table'."); Chris@0: } Chris@0: $this->container->get('database')->schema()->createTable($table, $schema); 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@18: $entity_type_manager = \Drupal::entityTypeManager(); Chris@18: $entity_type = $entity_type_manager->getDefinition($entity_type_id); Chris@18: \Drupal::service('entity_type.listener')->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@18: $storage = $entity_type_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: * This method does not install modules fully. Services and hooks for the Chris@0: * module are available, but the install process is not performed. 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 string[] $modules Chris@0: * A list of modules to enable. Dependencies are not resolved; i.e., Chris@0: * multiple modules have to be specified individually. The modules are only Chris@0: * added to the active module list and loaded; i.e., their database schema Chris@0: * is not installed. hook_install() is not invoked. A custom module weight Chris@0: * is not applied. Chris@0: * Chris@0: * @throws \LogicException Chris@0: * If any module in $modules is already enabled. Chris@0: * @throws \RuntimeException Chris@0: * If a module is not enabled after enabling it. Chris@0: */ Chris@0: protected function enableModules(array $modules) { Chris@0: $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); Chris@0: if ($trace[1]['function'] === 'setUp') { Chris@0: trigger_error('KernelTestBase::enableModules() should not be called from setUp(). Use the $modules property instead.', E_USER_DEPRECATED); Chris@0: } Chris@0: unset($trace); Chris@0: 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@17: $listing = new ExtensionDiscovery($this->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: 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 = $this->container->get('config.storage'); Chris@0: $extension_config = $active_storage->read('core.extension'); Chris@0: Chris@0: foreach ($modules as $module) { Chris@0: if ($module_handler->moduleExists($module)) { Chris@12: continue; Chris@0: } Chris@0: $module_handler->addModule($module, $module_list[$module]->getPath()); Chris@0: // Maintain the list of enabled modules in configuration. Chris@0: $extension_config['module'][$module] = 0; Chris@0: } Chris@0: $active_storage->write('core.extension', $extension_config); Chris@0: Chris@0: // Update the kernel to make their services available. Chris@0: $extensions = $module_handler->getModuleList(); Chris@0: $this->container->get('kernel')->updateModules($extensions, $extensions); 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: foreach ($modules as $module) { Chris@0: if (!$module_handler->moduleExists($module)) { Chris@0: throw new \RuntimeException("$module module is not enabled after enabling it."); Chris@0: } Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * Disables modules for this test. Chris@0: * Chris@0: * @param string[] $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 enabled modules is still loaded. The modules are only Chris@0: * removed from the active module list. Chris@0: * Chris@0: * @throws \LogicException Chris@0: * If any module in $modules is already disabled. Chris@0: * @throws \RuntimeException Chris@0: * If a module is not disabled after disabling it. 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: if (!$module_handler->moduleExists($module)) { Chris@0: throw new \LogicException("$module module cannot be disabled because it is not enabled."); Chris@0: } 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->container->get('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: foreach ($modules as $module) { Chris@0: if ($module_handler->moduleExists($module)) { Chris@0: throw new \RuntimeException("$module module is not disabled after disabling it."); Chris@0: } Chris@0: } 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: // \Drupal\Core\Render\BareHtmlPageRenderer::renderBarePage calls out to Chris@0: // system_page_attachments() directly. Chris@0: if (!\Drupal::moduleHandler()->moduleExists('system')) { Chris@0: throw new \Exception(__METHOD__ . ' requires system module to be installed.'); Chris@0: } Chris@0: 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: /** Chris@0: * Sets an in-memory Settings variable. Chris@0: * Chris@0: * @param string $name Chris@0: * The name of the setting to set. Chris@0: * @param bool|string|int|array|null $value Chris@0: * The value to set. Note that array values are replaced entirely; use Chris@0: * \Drupal\Core\Site\Settings::get() to perform custom merges. Chris@0: */ Chris@0: protected function setSetting($name, $value) { Chris@17: if ($name === 'install_profile') { Chris@17: @trigger_error('Use \Drupal\KernelTests\KernelTestBase::setInstallProfile() to set the install profile in kernel tests. See https://www.drupal.org/node/2538996', E_USER_DEPRECATED); Chris@17: $this->setInstallProfile($value); Chris@17: } Chris@0: $settings = Settings::getInstance() ? Settings::getAll() : []; Chris@0: $settings[$name] = $value; Chris@0: new Settings($settings); Chris@0: } Chris@0: Chris@0: /** Chris@17: * Sets the install profile and rebuilds the container to update it. Chris@17: * Chris@17: * @param string $profile Chris@17: * The install profile to set. Chris@17: */ Chris@17: protected function setInstallProfile($profile) { Chris@17: $this->container->get('config.factory') Chris@17: ->getEditable('core.extension') Chris@17: ->set('profile', $profile) Chris@17: ->save(); Chris@17: Chris@17: // The installation profile is provided by a container parameter. Saving Chris@17: // the configuration doesn't automatically trigger invalidation Chris@17: $this->container->get('kernel')->rebuildContainer(); Chris@17: } Chris@17: Chris@17: /** Chris@0: * Stops test execution. Chris@0: */ Chris@0: protected function stop() { Chris@0: $this->getTestResultObject()->stop(); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Dumps the current state of the virtual filesystem to STDOUT. Chris@0: */ Chris@0: protected function vfsDump() { Chris@0: vfsStream::inspect(new vfsStreamPrintVisitor()); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Returns the modules to enable for this test. Chris@0: * Chris@0: * @param string $class Chris@0: * The fully-qualified class name of this test. Chris@0: * Chris@0: * @return array Chris@0: */ Chris@0: private static function getModulesToEnable($class) { 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: return call_user_func_array('array_merge_recursive', $modules); Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: protected function prepareTemplate(\Text_Template $template) { Chris@0: $bootstrap_globals = ''; Chris@0: Chris@0: // Fix missing bootstrap.php when $preserveGlobalState is FALSE. Chris@0: // @see https://github.com/sebastianbergmann/phpunit/pull/797 Chris@0: $bootstrap_globals .= '$__PHPUNIT_BOOTSTRAP = ' . var_export($GLOBALS['__PHPUNIT_BOOTSTRAP'], TRUE) . ";\n"; Chris@0: Chris@0: // Avoid repetitive test namespace discoveries to improve performance. Chris@0: // @see /core/tests/bootstrap.php Chris@0: $bootstrap_globals .= '$namespaces = ' . var_export($GLOBALS['namespaces'], TRUE) . ";\n"; Chris@0: Chris@0: $template->setVar([ Chris@0: 'constants' => '', Chris@0: 'included_files' => '', Chris@0: 'globals' => $bootstrap_globals, Chris@0: ]); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Returns whether the current test method is running in a separate process. Chris@0: * Chris@0: * Note that KernelTestBase will run in a separate process by default. Chris@0: * Chris@0: * @return bool Chris@0: * Chris@0: * @see \Drupal\KernelTests\KernelTestBase::$runTestInSeparateProcess Chris@0: * @see https://github.com/sebastianbergmann/phpunit/pull/1350 Chris@0: * Chris@0: * @deprecated in Drupal 8.4.x, for removal before the Drupal 9.0.0 release. Chris@0: * KernelTestBase tests are always run in isolated processes. Chris@0: */ Chris@0: protected function isTestInIsolation() { Chris@0: @trigger_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated in Drupal 8.4.x, for removal before the Drupal 9.0.0 release. KernelTestBase tests are always run in isolated processes.', E_USER_DEPRECATED); Chris@0: return function_exists('__phpunit_run_isolated_test'); Chris@0: } Chris@0: Chris@0: /** Chris@0: * BC: Automatically resolve former KernelTestBase class properties. Chris@0: * Chris@0: * Test authors should follow the provided instructions and adjust their tests Chris@0: * accordingly. Chris@0: * Chris@16: * @deprecated in Drupal 8.0.0, will be removed before Drupal 9.0.0. Chris@0: */ Chris@0: public function __get($name) { Chris@0: if (in_array($name, [ Chris@0: 'public_files_directory', Chris@0: 'private_files_directory', Chris@0: 'temp_files_directory', Chris@0: 'translation_files_directory', Chris@0: ])) { Chris@0: // @comment it in again. Chris@0: trigger_error(sprintf("KernelTestBase::\$%s no longer exists. Use the regular API method to retrieve it instead (e.g., Settings).", $name), E_USER_DEPRECATED); Chris@0: switch ($name) { Chris@0: case 'public_files_directory': Chris@0: return Settings::get('file_public_path', \Drupal::service('site.path') . '/files'); Chris@0: Chris@0: case 'private_files_directory': Chris@0: return Settings::get('file_private_path'); Chris@0: Chris@0: case 'temp_files_directory': Chris@0: return file_directory_temp(); Chris@0: Chris@0: case 'translation_files_directory': Chris@0: return Settings::get('file_public_path', \Drupal::service('site.path') . '/translations'); Chris@0: } Chris@0: } Chris@0: Chris@0: if ($name === 'configDirectories') { Chris@0: trigger_error(sprintf("KernelTestBase::\$%s no longer exists. Use config_get_config_directory() directly instead.", $name), E_USER_DEPRECATED); Chris@0: return [ Chris@0: CONFIG_SYNC_DIRECTORY => config_get_config_directory(CONFIG_SYNC_DIRECTORY), Chris@0: ]; Chris@0: } Chris@0: Chris@0: $denied = [ Chris@0: // @see \Drupal\simpletest\TestBase Chris@0: 'testId', Chris@0: 'timeLimit', Chris@0: 'results', Chris@0: 'assertions', Chris@0: 'skipClasses', Chris@0: 'verbose', Chris@0: 'verboseId', Chris@0: 'verboseClassName', Chris@0: 'verboseDirectory', Chris@0: 'verboseDirectoryUrl', Chris@0: 'dieOnFail', Chris@0: 'kernel', Chris@0: // @see \Drupal\simpletest\TestBase::prepareEnvironment() Chris@0: 'generatedTestFiles', Chris@0: // Properties from the old KernelTestBase class that has been removed. Chris@0: 'keyValueFactory', Chris@0: ]; Chris@0: if (in_array($name, $denied) || strpos($name, 'original') === 0) { Chris@0: throw new \RuntimeException(sprintf('TestBase::$%s property no longer exists', $name)); Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * Prevents serializing any properties. Chris@0: * Chris@0: * Kernel tests are run in a separate process. To do this PHPUnit creates a Chris@0: * script to run the test. If it fails, the test result object will contain a Chris@0: * stack trace which includes the test object. It will attempt to serialize Chris@0: * it. Returning an empty array prevents it from serializing anything it Chris@0: * should not. Chris@0: * Chris@0: * @return array Chris@0: * An empty array. Chris@0: * Chris@0: * @see vendor/phpunit/phpunit/src/Util/PHP/Template/TestCaseMethod.tpl.dist Chris@0: */ Chris@0: public function __sleep() { Chris@0: return []; Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public static function assertEquals($expected, $actual, $message = '', $delta = 0.0, $maxDepth = 10, $canonicalize = FALSE, $ignoreCase = FALSE) { Chris@0: // Cast objects implementing MarkupInterface to string instead of Chris@0: // relying on PHP casting them to string depending on what they are being Chris@0: // comparing with. Chris@0: $expected = static::castSafeStrings($expected); Chris@0: $actual = static::castSafeStrings($actual); Chris@0: parent::assertEquals($expected, $actual, $message, $delta, $maxDepth, $canonicalize, $ignoreCase); Chris@0: } Chris@0: Chris@0: }