This repository has been archived on 2025-01-19. You can view files and clone it, but cannot push or open issues or pull requests.
drupalcampbristol/web/core/tests/Drupal/KernelTests/KernelTestBase.php
2017-07-03 16:47:07 +01:00

1102 lines
38 KiB
PHP

<?php
namespace Drupal\KernelTests;
use Drupal\Component\FileCache\ApcuFileCacheBackend;
use Drupal\Component\FileCache\FileCache;
use Drupal\Component\FileCache\FileCacheFactory;
use Drupal\Component\Utility\Html;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\Core\Config\Development\ConfigSchemaChecker;
use Drupal\Core\Database\Database;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Core\DependencyInjection\ServiceProviderInterface;
use Drupal\Core\DrupalKernel;
use Drupal\Core\Entity\Sql\SqlEntityStorageInterface;
use Drupal\Core\Extension\ExtensionDiscovery;
use Drupal\Core\Language\Language;
use Drupal\Core\Site\Settings;
use Drupal\Core\Test\TestDatabase;
use Drupal\simpletest\AssertContentTrait;
use Drupal\simpletest\AssertHelperTrait;
use Drupal\Tests\ConfigTestTrait;
use Drupal\Tests\RandomGeneratorTrait;
use Drupal\simpletest\TestServiceProvider;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\HttpFoundation\Request;
use org\bovigo\vfs\vfsStream;
use org\bovigo\vfs\visitor\vfsStreamPrintVisitor;
/**
* Base class for functional integration tests.
*
* Tests extending this base class can access files and the database, but the
* entire environment is initially empty. Drupal runs in a minimal mocked
* environment, comparable to the one in the early installer.
*
* Unlike \Drupal\Tests\UnitTestCase, modules specified in the $modules
* property are automatically added to the service container for each test.
* The module/hook system is functional and operates on a fixed module list.
* Additional modules needed in a test may be loaded and added to the fixed
* module list.
*
* Unlike \Drupal\simpletest\WebTestBase, the modules are only loaded, but not
* installed. Modules have to be installed manually, if needed.
*
* @see \Drupal\Tests\KernelTestBase::$modules
* @see \Drupal\Tests\KernelTestBase::enableModules()
*
* @todo Extend ::setRequirementsFromAnnotation() and ::checkRequirements() to
* account for '@requires module'.
*/
abstract class KernelTestBase extends \PHPUnit_Framework_TestCase implements ServiceProviderInterface {
use AssertLegacyTrait;
use AssertContentTrait;
use AssertHelperTrait;
use RandomGeneratorTrait;
use ConfigTestTrait;
/**
* {@inheritdoc}
*
* Back up and restore any global variables that may be changed by tests.
*
* @see self::runTestInSeparateProcess
*/
protected $backupGlobals = TRUE;
/**
* {@inheritdoc}
*
* Kernel tests are run in separate processes because they allow autoloading
* of code from extensions. Running the test in a separate process isolates
* this behavior from other tests. Subclasses should not override this
* property.
*/
protected $runTestInSeparateProcess = TRUE;
/**
* {@inheritdoc}
*
* Back up and restore static class properties that may be changed by tests.
*
* @see self::runTestInSeparateProcess
*/
protected $backupStaticAttributes = TRUE;
/**
* {@inheritdoc}
*
* Contains a few static class properties for performance.
*/
protected $backupStaticAttributesBlacklist = [
// Ignore static discovery/parser caches to speed up tests.
'Drupal\Component\Discovery\YamlDiscovery' => ['parsedFiles'],
'Drupal\Core\DependencyInjection\YamlFileLoader' => ['yaml'],
'Drupal\Core\Extension\ExtensionDiscovery' => ['files'],
'Drupal\Core\Extension\InfoParser' => ['parsedInfos'],
// Drupal::$container cannot be serialized.
'Drupal' => ['container'],
// Settings cannot be serialized.
'Drupal\Core\Site\Settings' => ['instance'],
];
/**
* {@inheritdoc}
*
* Do not forward any global state from the parent process to the processes
* that run the actual tests.
*
* @see self::runTestInSeparateProcess
*/
protected $preserveGlobalState = FALSE;
/**
* @var \Composer\Autoload\Classloader
*/
protected $classLoader;
/**
* @var string
*/
protected $siteDirectory;
/**
* @var string
*/
protected $databasePrefix;
/**
* @var \Drupal\Core\DependencyInjection\ContainerBuilder
*/
protected $container;
/**
* Modules to enable.
*
* The test runner will merge the $modules lists from this class, the class
* it extends, and so on up the class hierarchy. It is not necessary to
* include modules in your list that a parent class has already declared.
*
* @see \Drupal\Tests\KernelTestBase::enableModules()
* @see \Drupal\Tests\KernelTestBase::bootKernel()
*
* @var array
*/
protected static $modules = [];
/**
* The virtual filesystem root directory.
*
* @var \org\bovigo\vfs\vfsStreamDirectory
*/
protected $vfsRoot;
/**
* @var int
*/
protected $expectedLogSeverity;
/**
* @var string
*/
protected $expectedLogMessage;
/**
* @todo Move into Config test base class.
* @var \Drupal\Core\Config\ConfigImporter
*/
protected $configImporter;
/**
* The app root.
*
* @var string
*/
protected $root;
/**
* Set to TRUE to strict check all configuration saved.
*
* @see \Drupal\Core\Config\Development\ConfigSchemaChecker
*
* @var bool
*/
protected $strictConfigSchema = TRUE;
/**
* An array of config object names that are excluded from schema checking.
*
* @var string[]
*/
protected static $configSchemaCheckerExclusions = [
// Following are used to test lack of or partial schema. Where partial
// schema is provided, that is explicitly tested in specific tests.
'config_schema_test.noschema',
'config_schema_test.someschema',
'config_schema_test.schema_data_types',
'config_schema_test.no_schema_data_types',
// Used to test application of schema to filtering of configuration.
'config_test.dynamic.system',
];
/**
* {@inheritdoc}
*/
public static function setUpBeforeClass() {
parent::setUpBeforeClass();
// Change the current dir to DRUPAL_ROOT.
chdir(static::getDrupalRoot());
}
/**
* Returns the drupal root directory.
*
* @return string
*/
protected static function getDrupalRoot() {
return dirname(dirname(substr(__DIR__, 0, -strlen(__NAMESPACE__))));
}
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->root = static::getDrupalRoot();
$this->initFileCache();
$this->bootEnvironment();
$this->bootKernel();
}
/**
* Bootstraps a basic test environment.
*
* Should not be called by tests. Only visible for DrupalKernel integration
* tests.
*
* @see \Drupal\system\Tests\DrupalKernel\DrupalKernelTest
* @internal
*/
protected function bootEnvironment() {
$this->streamWrappers = [];
\Drupal::unsetContainer();
$this->classLoader = require $this->root . '/autoload.php';
require_once $this->root . '/core/includes/bootstrap.inc';
// Set up virtual filesystem.
Database::addConnectionInfo('default', 'test-runner', $this->getDatabaseConnectionInfo()['default']);
$test_db = new TestDatabase();
$this->siteDirectory = $test_db->getTestSitePath();
// Ensure that all code that relies on drupal_valid_test_ua() can still be
// safely executed. This primarily affects the (test) site directory
// resolution (used by e.g. LocalStream and PhpStorage).
$this->databasePrefix = $test_db->getDatabasePrefix();
drupal_valid_test_ua($this->databasePrefix);
$settings = [
'hash_salt' => get_class($this),
'file_public_path' => $this->siteDirectory . '/files',
// Disable Twig template caching/dumping.
'twig_cache' => FALSE,
// @see \Drupal\KernelTests\KernelTestBase::register()
];
new Settings($settings);
$this->setUpFilesystem();
foreach (Database::getAllConnectionInfo() as $key => $targets) {
Database::removeConnection($key);
}
Database::addConnectionInfo('default', 'default', $this->getDatabaseConnectionInfo()['default']);
}
/**
* Sets up the filesystem, so things like the file directory.
*/
protected function setUpFilesystem() {
$test_db = new TestDatabase($this->databasePrefix);
$test_site_path = $test_db->getTestSitePath();
$this->vfsRoot = vfsStream::setup('root');
$this->vfsRoot->addChild(vfsStream::newDirectory($test_site_path));
$this->siteDirectory = vfsStream::url('root/' . $test_site_path);
mkdir($this->siteDirectory . '/files', 0775);
mkdir($this->siteDirectory . '/files/config/' . CONFIG_SYNC_DIRECTORY, 0775, TRUE);
$settings = Settings::getInstance() ? Settings::getAll() : [];
$settings['file_public_path'] = $this->siteDirectory . '/files';
new Settings($settings);
$GLOBALS['config_directories'] = [
CONFIG_SYNC_DIRECTORY => $this->siteDirectory . '/files/config/sync',
];
}
/**
* @return string
*/
public function getDatabasePrefix() {
return $this->databasePrefix;
}
/**
* Bootstraps a kernel for a test.
*/
private function bootKernel() {
$this->setSetting('container_yamls', []);
// Allow for test-specific overrides.
$settings_services_file = $this->root . '/sites/default' . '/testing.services.yml';
if (file_exists($settings_services_file)) {
// Copy the testing-specific service overrides in place.
$testing_services_file = $this->root . '/' . $this->siteDirectory . '/services.yml';
copy($settings_services_file, $testing_services_file);
$this->setSetting('container_yamls', [$testing_services_file]);
}
// Allow for global test environment overrides.
if (file_exists($test_env = $this->root . '/sites/default/testing.services.yml')) {
$GLOBALS['conf']['container_yamls']['testing'] = $test_env;
}
// Add this test class as a service provider.
$GLOBALS['conf']['container_service_providers']['test'] = $this;
$modules = self::getModulesToEnable(get_class($this));
// Prepare a precompiled container for all tests of this class.
// Substantially improves performance, since ContainerBuilder::compile()
// is very expensive. Encourages testing best practices (small tests).
// Normally a setUpBeforeClass() operation, but object scope is required to
// inject $this test class instance as a service provider (see above).
$rc = new \ReflectionClass(get_class($this));
$test_method_count = count(array_filter($rc->getMethods(), function ($method) {
// PHPUnit's @test annotations are intentionally ignored/not supported.
return strpos($method->getName(), 'test') === 0;
}));
// Bootstrap the kernel. Do not use createFromRequest() to retain Settings.
$kernel = new DrupalKernel('testing', $this->classLoader, FALSE);
$kernel->setSitePath($this->siteDirectory);
// Boot a new one-time container from scratch. Ensure to set the module list
// upfront to avoid a subsequent rebuild.
if ($modules && $extensions = $this->getExtensionsForModules($modules)) {
$kernel->updateModules($extensions, $extensions);
}
// DrupalKernel::boot() is not sufficient as it does not invoke preHandle(),
// which is required to initialize legacy global variables.
$request = Request::create('/');
$kernel->prepareLegacyRequest($request);
// register() is only called if a new container was built/compiled.
$this->container = $kernel->getContainer();
// Ensure database tasks have been run.
require_once __DIR__ . '/../../../includes/install.inc';
$connection = Database::getConnection();
$errors = db_installer_object($connection->driver())->runTasks();
if (!empty($errors)) {
$this->fail('Failed to run installer database tasks: ' . implode(', ', $errors));
}
if ($modules) {
$this->container->get('module_handler')->loadAll();
}
$this->container->get('request_stack')->push($request);
// Setup the destion to the be frontpage by default.
\Drupal::destination()->set('/');
// Write the core.extension configuration.
// Required for ConfigInstaller::installDefaultConfig() to work.
$this->container->get('config.storage')->write('core.extension', [
'module' => array_fill_keys($modules, 0),
'theme' => [],
'profile' => '',
]);
$settings = Settings::getAll();
$settings['php_storage']['default'] = [
'class' => '\Drupal\Component\PhpStorage\FileStorage',
];
new Settings($settings);
// Manually configure the test mail collector implementation to prevent
// tests from sending out emails and collect them in state instead.
// While this should be enforced via settings.php prior to installation,
// some tests expect to be able to test mail system implementations.
$GLOBALS['config']['system.mail']['interface']['default'] = 'test_mail_collector';
// Manually configure the default file scheme so that modules that use file
// functions don't have to install system and its configuration.
// @see file_default_scheme()
$GLOBALS['config']['system.file']['default_scheme'] = 'public';
}
/**
* Configuration accessor for tests. Returns non-overridden configuration.
*
* @param string $name
* The configuration name.
*
* @return \Drupal\Core\Config\Config
* The configuration object with original configuration data.
*/
protected function config($name) {
return $this->container->get('config.factory')->getEditable($name);
}
/**
* Returns the Database connection info to be used for this test.
*
* This method only exists for tests of the Database component itself, because
* they require multiple database connections. Each SQLite :memory: connection
* creates a new/separate database in memory. A shared-memory SQLite file URI
* triggers PHP open_basedir/allow_url_fopen/allow_url_include restrictions.
* Due to that, Database tests are running against a SQLite database that is
* located in an actual file in the system's temporary directory.
*
* Other tests should not override this method.
*
* @return array
* A Database connection info array.
*
* @internal
*/
protected function getDatabaseConnectionInfo() {
// If the test is run with argument dburl then use it.
$db_url = getenv('SIMPLETEST_DB');
if (empty($db_url)) {
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.');
}
else {
$database = Database::convertDbUrlToConnectionInfo($db_url, $this->root);
Database::addConnectionInfo('default', 'default', $database);
}
// Clone the current connection and replace the current prefix.
$connection_info = Database::getConnectionInfo('default');
if (!empty($connection_info)) {
Database::renameConnection('default', 'simpletest_original_default');
foreach ($connection_info as $target => $value) {
// Replace the full table prefix definition to ensure that no table
// prefixes of the test runner leak into the test.
$connection_info[$target]['prefix'] = [
'default' => $value['prefix']['default'] . $this->databasePrefix,
];
}
}
return $connection_info;
}
/**
* Initializes the FileCache component.
*
* We can not use the Settings object in a component, that's why we have to do
* it here instead of \Drupal\Component\FileCache\FileCacheFactory.
*/
protected function initFileCache() {
$configuration = Settings::get('file_cache');
// Provide a default configuration, if not set.
if (!isset($configuration['default'])) {
// @todo Use extension_loaded('apcu') for non-testbot
// https://www.drupal.org/node/2447753.
if (function_exists('apcu_fetch')) {
$configuration['default']['cache_backend_class'] = ApcuFileCacheBackend::class;
}
}
FileCacheFactory::setConfiguration($configuration);
FileCacheFactory::setPrefix(Settings::getApcuPrefix('file_cache', $this->root));
}
/**
* Returns Extension objects for $modules to enable.
*
* @param string[] $modules
* The list of modules to enable.
*
* @return \Drupal\Core\Extension\Extension[]
* Extension objects for $modules, keyed by module name.
*
* @throws \PHPUnit_Framework_Exception
* If a module is not available.
*
* @see \Drupal\Tests\KernelTestBase::enableModules()
* @see \Drupal\Core\Extension\ModuleHandler::add()
*/
private function getExtensionsForModules(array $modules) {
$extensions = [];
$discovery = new ExtensionDiscovery($this->root);
$discovery->setProfileDirectories([]);
$list = $discovery->scan('module');
foreach ($modules as $name) {
if (!isset($list[$name])) {
throw new \PHPUnit_Framework_Exception("Unavailable module: '$name'. If this module needs to be downloaded separately, annotate the test class with '@requires module $name'.");
}
$extensions[$name] = $list[$name];
}
return $extensions;
}
/**
* Registers test-specific services.
*
* Extend this method in your test to register additional services. This
* method is called whenever the kernel is rebuilt.
*
* @param \Drupal\Core\DependencyInjection\ContainerBuilder $container
* The service container to enhance.
*
* @see \Drupal\Tests\KernelTestBase::bootKernel()
*/
public function register(ContainerBuilder $container) {
// Keep the container object around for tests.
$this->container = $container;
$container
->register('flood', 'Drupal\Core\Flood\MemoryBackend')
->addArgument(new Reference('request_stack'));
$container
->register('lock', 'Drupal\Core\Lock\NullLockBackend');
$container
->register('cache_factory', 'Drupal\Core\Cache\MemoryBackendFactory');
$container
->register('keyvalue.memory', 'Drupal\Core\KeyValueStore\KeyValueMemoryFactory')
// Must persist container rebuilds, or all data would vanish otherwise.
->addTag('persist');
$container
->setAlias('keyvalue', 'keyvalue.memory');
// Set the default language on the minimal container.
$container->setParameter('language.default_values', Language::$defaultValues);
if ($this->strictConfigSchema) {
$container
->register('simpletest.config_schema_checker', ConfigSchemaChecker::class)
->addArgument(new Reference('config.typed'))
->addArgument($this->getConfigSchemaExclusions())
->addTag('event_subscriber');
}
if ($container->hasDefinition('path_processor_alias')) {
// Prevent the alias-based path processor, which requires a url_alias db
// table, from being registered to the path processor manager. We do this
// by removing the tags that the compiler pass looks for. This means the
// url generator can safely be used within tests.
$container->getDefinition('path_processor_alias')
->clearTag('path_processor_inbound')
->clearTag('path_processor_outbound');
}
if ($container->hasDefinition('password')) {
$container->getDefinition('password')
->setArguments([1]);
}
TestServiceProvider::addRouteProvider($container);
}
/**
* Gets the config schema exclusions for this test.
*
* @return string[]
* An array of config object names that are excluded from schema checking.
*/
protected function getConfigSchemaExclusions() {
$class = get_class($this);
$exceptions = [];
while ($class) {
if (property_exists($class, 'configSchemaCheckerExclusions')) {
$exceptions = array_merge($exceptions, $class::$configSchemaCheckerExclusions);
}
$class = get_parent_class($class);
}
// Filter out any duplicates.
return array_unique($exceptions);
}
/**
* {@inheritdoc}
*/
protected function assertPostConditions() {
// Execute registered Drupal shutdown functions prior to tearing down.
// @see _drupal_shutdown_function()
$callbacks = &drupal_register_shutdown_function();
while ($callback = array_shift($callbacks)) {
call_user_func_array($callback['callback'], $callback['arguments']);
}
// Shut down the kernel (if bootKernel() was called).
// @see \Drupal\KernelTests\Core\DrupalKernel\DrupalKernelTest
if ($this->container) {
$this->container->get('kernel')->shutdown();
}
// Fail in case any (new) shutdown functions exist.
$this->assertCount(0, drupal_register_shutdown_function(), 'Unexpected Drupal shutdown callbacks exist after running shutdown functions.');
parent::assertPostConditions();
}
/**
* {@inheritdoc}
*/
protected function tearDown() {
// Destroy the testing kernel.
if (isset($this->kernel)) {
$this->kernel->shutdown();
}
// Remove all prefixed tables.
$original_connection_info = Database::getConnectionInfo('simpletest_original_default');
$original_prefix = $original_connection_info['default']['prefix']['default'];
$test_connection_info = Database::getConnectionInfo('default');
$test_prefix = $test_connection_info['default']['prefix']['default'];
if ($original_prefix != $test_prefix) {
$tables = Database::getConnection()->schema()->findTables('%');
foreach ($tables as $table) {
if (Database::getConnection()->schema()->dropTable($table)) {
unset($tables[$table]);
}
}
}
// Free up memory: Own properties.
$this->classLoader = NULL;
$this->vfsRoot = NULL;
$this->configImporter = NULL;
// Free up memory: Custom test class properties.
// Note: Private properties cannot be cleaned up.
$rc = new \ReflectionClass(__CLASS__);
$blacklist = [];
foreach ($rc->getProperties() as $property) {
$blacklist[$property->name] = $property->getDeclaringClass()->name;
}
$rc = new \ReflectionClass($this);
foreach ($rc->getProperties(\ReflectionProperty::IS_PUBLIC | \ReflectionProperty::IS_PROTECTED) as $property) {
if (!$property->isStatic() && !isset($blacklist[$property->name])) {
$this->{$property->name} = NULL;
}
}
// Clean FileCache cache.
FileCache::reset();
// Clean up statics, container, and settings.
if (function_exists('drupal_static_reset')) {
drupal_static_reset();
}
\Drupal::unsetContainer();
$this->container = NULL;
new Settings([]);
parent::tearDown();
}
/**
* @after
*
* Additional tear down method to close the connection at the end.
*/
public function tearDownCloseDatabaseConnection() {
// Destroy the database connection, which for example removes the memory
// from sqlite in memory.
foreach (Database::getAllConnectionInfo() as $key => $targets) {
Database::removeConnection($key);
}
}
/**
* Installs default configuration for a given list of modules.
*
* @param string|string[] $modules
* A list of modules for which to install default configuration.
*
* @throws \LogicException
* If any module in $modules is not enabled.
*/
protected function installConfig($modules) {
foreach ((array) $modules as $module) {
if (!$this->container->get('module_handler')->moduleExists($module)) {
throw new \LogicException("$module module is not enabled.");
}
$this->container->get('config.installer')->installDefaultConfig('module', $module);
}
}
/**
* Installs database tables from a module schema definition.
*
* @param string $module
* The name of the module that defines the table's schema.
* @param string|array $tables
* The name or an array of the names of the tables to install.
*
* @throws \LogicException
* If $module is not enabled or the table schema cannot be found.
*/
protected function installSchema($module, $tables) {
// drupal_get_module_schema() is technically able to install a schema
// of a non-enabled module, but its ability to load the module's .install
// file depends on many other factors. To prevent differences in test
// behavior and non-reproducible test failures, we only allow the schema of
// explicitly loaded/enabled modules to be installed.
if (!$this->container->get('module_handler')->moduleExists($module)) {
throw new \LogicException("$module module is not enabled.");
}
$tables = (array) $tables;
foreach ($tables as $table) {
$schema = drupal_get_module_schema($module, $table);
if (empty($schema)) {
// BC layer to avoid some contrib tests to fail.
// @todo Remove the BC layer before 8.1.x release.
// @see https://www.drupal.org/node/2670360
// @see https://www.drupal.org/node/2670454
if ($module == 'system') {
continue;
}
throw new \LogicException("$module module does not define a schema for table '$table'.");
}
$this->container->get('database')->schema()->createTable($table, $schema);
}
}
/**
* Installs the storage schema for a specific entity type.
*
* @param string $entity_type_id
* The ID of the entity type.
*/
protected function installEntitySchema($entity_type_id) {
/** @var \Drupal\Core\Entity\EntityManagerInterface $entity_manager */
$entity_manager = $this->container->get('entity.manager');
$entity_type = $entity_manager->getDefinition($entity_type_id);
$entity_manager->onEntityTypeCreate($entity_type);
// For test runs, the most common storage backend is a SQL database. For
// this case, ensure the tables got created.
$storage = $entity_manager->getStorage($entity_type_id);
if ($storage instanceof SqlEntityStorageInterface) {
$tables = $storage->getTableMapping()->getTableNames();
$db_schema = $this->container->get('database')->schema();
$all_tables_exist = TRUE;
foreach ($tables as $table) {
if (!$db_schema->tableExists($table)) {
$this->fail(SafeMarkup::format('Installed entity type table for the %entity_type entity type: %table', [
'%entity_type' => $entity_type_id,
'%table' => $table,
]));
$all_tables_exist = FALSE;
}
}
if ($all_tables_exist) {
$this->pass(SafeMarkup::format('Installed entity type tables for the %entity_type entity type: %tables', [
'%entity_type' => $entity_type_id,
'%tables' => '{' . implode('}, {', $tables) . '}',
]));
}
}
}
/**
* Enables modules for this test.
*
* To install test modules outside of the testing environment, add
* @code
* $settings['extension_discovery_scan_tests'] = TRUE;
* @endcode
* to your settings.php.
*
* @param string[] $modules
* A list of modules to enable. Dependencies are not resolved; i.e.,
* multiple modules have to be specified individually. The modules are only
* added to the active module list and loaded; i.e., their database schema
* is not installed. hook_install() is not invoked. A custom module weight
* is not applied.
*
* @throws \LogicException
* If any module in $modules is already enabled.
* @throws \RuntimeException
* If a module is not enabled after enabling it.
*/
protected function enableModules(array $modules) {
$trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
if ($trace[1]['function'] === 'setUp') {
trigger_error('KernelTestBase::enableModules() should not be called from setUp(). Use the $modules property instead.', E_USER_DEPRECATED);
}
unset($trace);
// Perform an ExtensionDiscovery scan as this function may receive a
// profile that is not the current profile, and we don't yet have a cached
// way to receive inactive profile information.
// @todo Remove as part of https://www.drupal.org/node/2186491
$listing = new ExtensionDiscovery(\Drupal::root());
$module_list = $listing->scan('module');
// In ModuleHandlerTest we pass in a profile as if it were a module.
$module_list += $listing->scan('profile');
// Set the list of modules in the extension handler.
$module_handler = $this->container->get('module_handler');
// Write directly to active storage to avoid early instantiation of
// the event dispatcher which can prevent modules from registering events.
$active_storage = $this->container->get('config.storage');
$extension_config = $active_storage->read('core.extension');
foreach ($modules as $module) {
if ($module_handler->moduleExists($module)) {
throw new \LogicException("$module module is already enabled.");
}
$module_handler->addModule($module, $module_list[$module]->getPath());
// Maintain the list of enabled modules in configuration.
$extension_config['module'][$module] = 0;
}
$active_storage->write('core.extension', $extension_config);
// Update the kernel to make their services available.
$extensions = $module_handler->getModuleList();
$this->container->get('kernel')->updateModules($extensions, $extensions);
// Ensure isLoaded() is TRUE in order to make
// \Drupal\Core\Theme\ThemeManagerInterface::render() work.
// Note that the kernel has rebuilt the container; this $module_handler is
// no longer the $module_handler instance from above.
$module_handler = $this->container->get('module_handler');
$module_handler->reload();
foreach ($modules as $module) {
if (!$module_handler->moduleExists($module)) {
throw new \RuntimeException("$module module is not enabled after enabling it.");
}
}
}
/**
* Disables modules for this test.
*
* @param string[] $modules
* A list of modules to disable. Dependencies are not resolved; i.e.,
* multiple modules have to be specified with dependent modules first.
* Code of previously enabled modules is still loaded. The modules are only
* removed from the active module list.
*
* @throws \LogicException
* If any module in $modules is already disabled.
* @throws \RuntimeException
* If a module is not disabled after disabling it.
*/
protected function disableModules(array $modules) {
// Unset the list of modules in the extension handler.
$module_handler = $this->container->get('module_handler');
$module_filenames = $module_handler->getModuleList();
$extension_config = $this->config('core.extension');
foreach ($modules as $module) {
if (!$module_handler->moduleExists($module)) {
throw new \LogicException("$module module cannot be disabled because it is not enabled.");
}
unset($module_filenames[$module]);
$extension_config->clear('module.' . $module);
}
$extension_config->save();
$module_handler->setModuleList($module_filenames);
$module_handler->resetImplementations();
// Update the kernel to remove their services.
$this->container->get('kernel')->updateModules($module_filenames, $module_filenames);
// Ensure isLoaded() is TRUE in order to make
// \Drupal\Core\Theme\ThemeManagerInterface::render() work.
// Note that the kernel has rebuilt the container; this $module_handler is
// no longer the $module_handler instance from above.
$module_handler = $this->container->get('module_handler');
$module_handler->reload();
foreach ($modules as $module) {
if ($module_handler->moduleExists($module)) {
throw new \RuntimeException("$module module is not disabled after disabling it.");
}
}
}
/**
* Renders a render array.
*
* @param array $elements
* The elements to render.
*
* @return string
* The rendered string output (typically HTML).
*/
protected function render(array &$elements) {
// \Drupal\Core\Render\BareHtmlPageRenderer::renderBarePage calls out to
// system_page_attachments() directly.
if (!\Drupal::moduleHandler()->moduleExists('system')) {
throw new \Exception(__METHOD__ . ' requires system module to be installed.');
}
// Use the bare HTML page renderer to render our links.
$renderer = $this->container->get('bare_html_page_renderer');
$response = $renderer->renderBarePage($elements, '', 'maintenance_page');
// Glean the content from the response object.
$content = $response->getContent();
$this->setRawContent($content);
$this->verbose('<pre style="white-space: pre-wrap">' . Html::escape($content));
return $content;
}
/**
* Sets an in-memory Settings variable.
*
* @param string $name
* The name of the setting to set.
* @param bool|string|int|array|null $value
* The value to set. Note that array values are replaced entirely; use
* \Drupal\Core\Site\Settings::get() to perform custom merges.
*/
protected function setSetting($name, $value) {
$settings = Settings::getInstance() ? Settings::getAll() : [];
$settings[$name] = $value;
new Settings($settings);
}
/**
* Stops test execution.
*/
protected function stop() {
$this->getTestResultObject()->stop();
}
/**
* Dumps the current state of the virtual filesystem to STDOUT.
*/
protected function vfsDump() {
vfsStream::inspect(new vfsStreamPrintVisitor());
}
/**
* Returns the modules to enable for this test.
*
* @param string $class
* The fully-qualified class name of this test.
*
* @return array
*/
private static function getModulesToEnable($class) {
$modules = [];
while ($class) {
if (property_exists($class, 'modules')) {
// Only add the modules, if the $modules property was not inherited.
$rp = new \ReflectionProperty($class, 'modules');
if ($rp->class == $class) {
$modules[$class] = $class::$modules;
}
}
$class = get_parent_class($class);
}
// Modules have been collected in reverse class hierarchy order; modules
// defined by base classes should be sorted first. Then, merge the results
// together.
$modules = array_reverse($modules);
return call_user_func_array('array_merge_recursive', $modules);
}
/**
* {@inheritdoc}
*/
protected function prepareTemplate(\Text_Template $template) {
$bootstrap_globals = '';
// Fix missing bootstrap.php when $preserveGlobalState is FALSE.
// @see https://github.com/sebastianbergmann/phpunit/pull/797
$bootstrap_globals .= '$__PHPUNIT_BOOTSTRAP = ' . var_export($GLOBALS['__PHPUNIT_BOOTSTRAP'], TRUE) . ";\n";
// Avoid repetitive test namespace discoveries to improve performance.
// @see /core/tests/bootstrap.php
$bootstrap_globals .= '$namespaces = ' . var_export($GLOBALS['namespaces'], TRUE) . ";\n";
$template->setVar([
'constants' => '',
'included_files' => '',
'globals' => $bootstrap_globals,
]);
}
/**
* Returns whether the current test method is running in a separate process.
*
* Note that KernelTestBase will run in a separate process by default.
*
* @return bool
*
* @see \Drupal\KernelTests\KernelTestBase::$runTestInSeparateProcess
* @see https://github.com/sebastianbergmann/phpunit/pull/1350
*
* @deprecated in Drupal 8.4.x, for removal before the Drupal 9.0.0 release.
* KernelTestBase tests are always run in isolated processes.
*/
protected function isTestInIsolation() {
@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);
return function_exists('__phpunit_run_isolated_test');
}
/**
* BC: Automatically resolve former KernelTestBase class properties.
*
* Test authors should follow the provided instructions and adjust their tests
* accordingly.
*
* @deprecated in Drupal 8.0.x, will be removed before Drupal 8.2.0.
*/
public function __get($name) {
if (in_array($name, [
'public_files_directory',
'private_files_directory',
'temp_files_directory',
'translation_files_directory',
])) {
// @comment it in again.
trigger_error(sprintf("KernelTestBase::\$%s no longer exists. Use the regular API method to retrieve it instead (e.g., Settings).", $name), E_USER_DEPRECATED);
switch ($name) {
case 'public_files_directory':
return Settings::get('file_public_path', \Drupal::service('site.path') . '/files');
case 'private_files_directory':
return Settings::get('file_private_path');
case 'temp_files_directory':
return file_directory_temp();
case 'translation_files_directory':
return Settings::get('file_public_path', \Drupal::service('site.path') . '/translations');
}
}
if ($name === 'configDirectories') {
trigger_error(sprintf("KernelTestBase::\$%s no longer exists. Use config_get_config_directory() directly instead.", $name), E_USER_DEPRECATED);
return [
CONFIG_SYNC_DIRECTORY => config_get_config_directory(CONFIG_SYNC_DIRECTORY),
];
}
$denied = [
// @see \Drupal\simpletest\TestBase
'testId',
'timeLimit',
'results',
'assertions',
'skipClasses',
'verbose',
'verboseId',
'verboseClassName',
'verboseDirectory',
'verboseDirectoryUrl',
'dieOnFail',
'kernel',
// @see \Drupal\simpletest\TestBase::prepareEnvironment()
'generatedTestFiles',
// Properties from the old KernelTestBase class that has been removed.
'keyValueFactory',
];
if (in_array($name, $denied) || strpos($name, 'original') === 0) {
throw new \RuntimeException(sprintf('TestBase::$%s property no longer exists', $name));
}
}
/**
* Prevents serializing any properties.
*
* Kernel tests are run in a separate process. To do this PHPUnit creates a
* script to run the test. If it fails, the test result object will contain a
* stack trace which includes the test object. It will attempt to serialize
* it. Returning an empty array prevents it from serializing anything it
* should not.
*
* @return array
* An empty array.
*
* @see vendor/phpunit/phpunit/src/Util/PHP/Template/TestCaseMethod.tpl.dist
*/
public function __sleep() {
return [];
}
/**
* {@inheritdoc}
*/
public static function assertEquals($expected, $actual, $message = '', $delta = 0.0, $maxDepth = 10, $canonicalize = FALSE, $ignoreCase = FALSE) {
// Cast objects implementing MarkupInterface to string instead of
// relying on PHP casting them to string depending on what they are being
// comparing with.
$expected = static::castSafeStrings($expected);
$actual = static::castSafeStrings($actual);
parent::assertEquals($expected, $actual, $message, $delta, $maxDepth, $canonicalize, $ignoreCase);
}
}