2015-08-17 17:00:26 -07:00
< ? php
/**
* @ file
* Contains \Drupal\simpletest\KernelTestBase .
*/
namespace Drupal\simpletest ;
2015-09-04 13:20:09 -07:00
use Drupal\Component\Utility\Html ;
2015-08-17 17:00:26 -07:00
use Drupal\Component\Utility\SafeMarkup ;
use Drupal\Component\Utility\Variable ;
use Drupal\Core\Database\Database ;
use Drupal\Core\DependencyInjection\ContainerBuilder ;
use Drupal\Core\DrupalKernel ;
use Drupal\Core\Entity\Sql\SqlEntityStorageInterface ;
use Drupal\Core\Extension\ExtensionDiscovery ;
use Drupal\Core\KeyValueStore\KeyValueMemoryFactory ;
use Drupal\Core\Language\Language ;
use Drupal\Core\Site\Settings ;
use Symfony\Component\DependencyInjection\Parameter ;
use Drupal\Core\StreamWrapper\StreamWrapperInterface ;
use Symfony\Component\DependencyInjection\Reference ;
use Symfony\Component\HttpFoundation\Request ;
/**
* Base class for 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 .
*
* 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 .
*
2015-09-04 13:20:09 -07:00
* @ deprecated in Drupal 8.0 . x , will be removed before Drupal 8.2 . x . Use
* \Drupal\KernelTests\KernelTestBase instead .
*
2015-08-17 17:00:26 -07:00
* @ see \Drupal\simpletest\KernelTestBase :: $modules
* @ see \Drupal\simpletest\KernelTestBase :: enableModules ()
*
* @ ingroup testing
*/
abstract class KernelTestBase extends TestBase {
use AssertContentTrait ;
/**
* Modules to enable .
*
* Test classes extending this class , and any classes in the hierarchy up to
* this class , may specify individual lists of modules to enable by setting
* this property . The values of all properties in all classes in the hierarchy
* are merged .
*
* Any modules specified in the $modules property are automatically loaded and
* set as the fixed module list .
*
* Unlike WebTestBase :: setUp (), the specified modules are loaded only , but not
* automatically installed . Modules need to be installed manually , if needed .
*
* @ see \Drupal\simpletest\KernelTestBase :: enableModules ()
* @ see \Drupal\simpletest\KernelTestBase :: setUp ()
*
* @ var array
*/
public static $modules = array ();
private $moduleFiles ;
private $themeFiles ;
/**
* The configuration directories for this test run .
*
* @ var array
*/
protected $configDirectories = array ();
/**
* A KeyValueMemoryFactory instance to use when building the container .
*
* @ var \Drupal\Core\KeyValueStore\KeyValueMemoryFactory .
*/
protected $keyValueFactory ;
/**
* Array of registered stream wrappers .
*
* @ var array
*/
protected $streamWrappers = array ();
/**
* { @ inheritdoc }
*/
function __construct ( $test_id = NULL ) {
parent :: __construct ( $test_id );
$this -> skipClasses [ __CLASS__ ] = TRUE ;
}
/**
* { @ inheritdoc }
*/
protected function beforePrepareEnvironment () {
// Copy/prime extension file lists once to avoid filesystem scans.
if ( ! isset ( $this -> moduleFiles )) {
$this -> moduleFiles = \Drupal :: state () -> get ( 'system.module.files' ) ? : array ();
$this -> themeFiles = \Drupal :: state () -> get ( 'system.theme.files' ) ? : array ();
}
}
/**
* Create and set new configuration directories .
*
* @ see config_get_config_directory ()
*
* @ throws \RuntimeException
* Thrown when CONFIG_ACTIVE_DIRECTORY or CONFIG_STAGING_DIRECTORY cannot
* be created or made writable .
*/
protected function prepareConfigDirectories () {
$this -> configDirectories = array ();
include_once DRUPAL_ROOT . '/core/includes/install.inc' ;
foreach ( array ( CONFIG_ACTIVE_DIRECTORY , CONFIG_STAGING_DIRECTORY ) as $type ) {
// Assign the relative path to the global variable.
$path = $this -> siteDirectory . '/config_' . $type ;
$GLOBALS [ 'config_directories' ][ $type ] = $path ;
// Ensure the directory can be created and is writeable.
if ( ! install_ensure_config_directory ( $type )) {
throw new \RuntimeException ( " Failed to create ' $type ' config directory $path " );
}
// Provide the already resolved path for tests.
$this -> configDirectories [ $type ] = $path ;
}
}
/**
* { @ inheritdoc }
*/
protected function setUp () {
$this -> keyValueFactory = new KeyValueMemoryFactory ();
// Back up settings from TestBase::prepareEnvironment().
$settings = Settings :: getAll ();
// Allow for test-specific overrides.
$directory = DRUPAL_ROOT . '/' . $this -> siteDirectory ;
$settings_services_file = DRUPAL_ROOT . '/' . $this -> originalSite . '/testing.services.yml' ;
$container_yamls = [];
if ( file_exists ( $settings_services_file )) {
// Copy the testing-specific service overrides in place.
$testing_services_file = $directory . '/services.yml' ;
copy ( $settings_services_file , $testing_services_file );
$container_yamls [] = $testing_services_file ;
}
$settings_testing_file = DRUPAL_ROOT . '/' . $this -> originalSite . '/settings.testing.php' ;
if ( file_exists ( $settings_testing_file )) {
// Copy the testing-specific settings.php overrides in place.
copy ( $settings_testing_file , $directory . '/settings.testing.php' );
}
if ( file_exists ( $directory . '/settings.testing.php' )) {
// Add the name of the testing class to settings.php and include the
// testing specific overrides
$hash_salt = Settings :: getHashSalt ();
$test_class = get_class ( $this );
$container_yamls_export = Variable :: export ( $container_yamls );
$php = <<< EOD
< ? php
\ $settings [ 'hash_salt' ] = '$hash_salt' ;
\ $settings [ 'container_yamls' ] = $container_yamls_export ;
\ $test_class = '$test_class' ;
include DRUPAL_ROOT . '/' . \ $site_path . '/settings.testing.php' ;
EOD ;
file_put_contents ( $directory . '/settings.php' , $php );
}
// Add this test class as a service provider.
// @todo Remove the indirection; implement ServiceProviderInterface instead.
$GLOBALS [ 'conf' ][ 'container_service_providers' ][ 'TestServiceProvider' ] = 'Drupal\simpletest\TestServiceProvider' ;
// Bootstrap a new kernel.
$class_loader = require DRUPAL_ROOT . '/autoload.php' ;
$this -> kernel = new DrupalKernel ( 'testing' , $class_loader , FALSE );
$request = Request :: create ( '/' );
$site_path = DrupalKernel :: findSitePath ( $request );
$this -> kernel -> setSitePath ( $site_path );
if ( file_exists ( $directory . '/settings.testing.php' )) {
Settings :: initialize ( DRUPAL_ROOT , $site_path , $class_loader );
}
$this -> kernel -> boot ();
// Save the original site directory path, so that extensions in the
// site-specific directory can still be discovered in the test site
// environment.
// @see \Drupal\Core\Extension\ExtensionDiscovery::scan()
$settings [ 'test_parent_site' ] = $this -> originalSite ;
// Restore and merge settings.
// DrupalKernel::boot() initializes new Settings, and the containerBuild()
// method sets additional settings.
new Settings ( $settings + Settings :: getAll ());
// Create and set new configuration directories.
$this -> prepareConfigDirectories ();
// Set the request scope.
$this -> container = $this -> kernel -> getContainer ();
$this -> container -> get ( 'request_stack' ) -> push ( $request );
// Re-inject extension file listings into state, unless the key/value
// service was overridden (in which case its storage does not exist yet).
if ( $this -> container -> get ( 'keyvalue' ) instanceof KeyValueMemoryFactory ) {
$this -> container -> get ( 'state' ) -> set ( 'system.module.files' , $this -> moduleFiles );
$this -> container -> get ( 'state' ) -> set ( 'system.theme.files' , $this -> themeFiles );
}
// Create a minimal core.extension configuration object so that the list of
// enabled modules can be maintained allowing
// \Drupal\Core\Config\ConfigInstaller::installDefaultConfig() to work.
// Write directly to active storage to avoid early instantiation of
// the event dispatcher which can prevent modules from registering events.
\Drupal :: service ( 'config.storage' ) -> write ( 'core.extension' , array ( 'module' => array (), 'theme' => array ()));
// Collect and set a fixed module list.
$class = get_class ( $this );
$modules = array ();
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 );
$modules = call_user_func_array ( 'array_merge_recursive' , $modules );
if ( $modules ) {
$this -> enableModules ( $modules );
}
// Tests based on this class are entitled to use Drupal's File and
// StreamWrapper APIs.
// @todo Move StreamWrapper management into DrupalKernel.
// @see https://www.drupal.org/node/2028109
file_prepare_directory ( $this -> publicFilesDirectory , FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS );
$this -> settingsSet ( 'file_public_path' , $this -> publicFilesDirectory );
$this -> streamWrappers = array ();
$this -> registerStreamWrapper ( 'public' , 'Drupal\Core\StreamWrapper\PublicStream' );
// The temporary stream wrapper is able to operate both with and without
// configuration.
$this -> registerStreamWrapper ( 'temporary' , 'Drupal\Core\StreamWrapper\TemporaryStream' );
}
/**
* { @ inheritdoc }
*/
protected function tearDown () {
if ( $this -> kernel instanceof DrupalKernel ) {
$this -> kernel -> shutdown ();
}
// Before tearing down the test environment, ensure that no stream wrapper
// of this test leaks into the parent environment. Unlike all other global
// state variables in Drupal, stream wrappers are a global state construct
// of PHP core, which has to be maintained manually.
// @todo Move StreamWrapper management into DrupalKernel.
// @see https://www.drupal.org/node/2028109
foreach ( $this -> streamWrappers as $scheme => $type ) {
$this -> unregisterStreamWrapper ( $scheme , $type );
}
parent :: tearDown ();
}
/**
* Sets up the base service container for this test .
*
* Extend this method in your test to register additional service overrides
* that need to persist a DrupalKernel reboot . This method is called whenever
* the kernel is rebuilt .
*
* @ see \Drupal\simpletest\KernelTestBase :: setUp ()
* @ see \Drupal\simpletest\KernelTestBase :: enableModules ()
* @ see \Drupal\simpletest\KernelTestBase :: disableModules ()
*/
public function containerBuild ( ContainerBuilder $container ) {
// Keep the container object around for tests.
$this -> container = $container ;
// Set the default language on the minimal container.
$this -> container -> setParameter ( 'language.default_values' , $this -> defaultLanguageData ());
$container -> register ( 'lock' , 'Drupal\Core\Lock\NullLockBackend' );
$container -> register ( 'cache_factory' , 'Drupal\Core\Cache\MemoryBackendFactory' );
$container
-> register ( 'config.storage' , 'Drupal\Core\Config\DatabaseStorage' )
-> addArgument ( Database :: getConnection ())
-> addArgument ( 'config' );
if ( $this -> strictConfigSchema ) {
$container
-> register ( 'simpletest.config_schema_checker' , 'Drupal\Core\Config\Testing\ConfigSchemaChecker' )
-> addArgument ( new Reference ( 'config.typed' ))
-> addTag ( 'event_subscriber' );
}
$keyvalue_options = $container -> getParameter ( 'factory.keyvalue' ) ? : array ();
$keyvalue_options [ 'default' ] = 'keyvalue.memory' ;
$container -> setParameter ( 'factory.keyvalue' , $keyvalue_options );
$container -> set ( 'keyvalue.memory' , $this -> keyValueFactory );
if ( ! $container -> has ( 'keyvalue' )) {
// TestBase::setUp puts a completely empty container in
// $this->container which is somewhat the mirror of the empty
// environment being set up. Unit tests need not to waste time with
// getting a container set up for them. Drupal Unit Tests might just get
// away with a simple container holding the absolute bare minimum. When
// a kernel is overridden then there's no need to re-register the keyvalue
// service but when a test is happy with the superminimal container put
// together here, it still might a keyvalue storage for anything using
// \Drupal::state() -- that's why a memory service was added in the first
// place.
$container -> register ( 'settings' , 'Drupal\Core\Site\Settings' )
-> setFactoryClass ( 'Drupal\Core\Site\Settings' )
-> setFactoryMethod ( 'getInstance' );
$container
-> register ( 'keyvalue' , 'Drupal\Core\KeyValueStore\KeyValueFactory' )
-> addArgument ( new Reference ( 'service_container' ))
-> addArgument ( new Parameter ( 'factory.keyvalue' ));
$container -> register ( 'state' , 'Drupal\Core\State\State' )
-> addArgument ( new Reference ( 'keyvalue' ));
}
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.
$definition = $container -> getDefinition ( 'path_processor_alias' );
$definition -> clearTag ( 'path_processor_inbound' ) -> clearTag ( 'path_processor_outbound' );
}
if ( $container -> hasDefinition ( 'password' )) {
$container -> getDefinition ( 'password' ) -> setArguments ( array ( 1 ));
}
// Register the stream wrapper manager.
$container
-> register ( 'stream_wrapper_manager' , 'Drupal\Core\StreamWrapper\StreamWrapperManager' )
-> addArgument ( new Reference ( 'module_handler' ))
-> addMethodCall ( 'setContainer' , array ( new Reference ( 'service_container' )));
$request = Request :: create ( '/' );
$container -> get ( 'request_stack' ) -> push ( $request );
}
/**
* Provides the data for setting the default language on the container .
*
* @ return array
* The data array for the default language .
*/
protected function defaultLanguageData () {
return Language :: $defaultValues ;
}
/**
* Installs default configuration for a given list of modules .
*
* @ param array $modules
* A list of modules for which to install default configuration .
*
* @ throws \RuntimeException
* Thrown when any module listed in $modules is not enabled .
*/
protected function installConfig ( array $modules ) {
foreach ( $modules as $module ) {
if ( ! $this -> container -> get ( 'module_handler' ) -> moduleExists ( $module )) {
2015-08-27 12:03:05 -07:00
throw new \RuntimeException ( " ' $module ' module is not enabled " );
2015-08-17 17:00:26 -07:00
}
\Drupal :: service ( 'config.installer' ) -> installDefaultConfig ( 'module' , $module );
}
$this -> pass ( format_string ( 'Installed default config: %modules.' , array (
'%modules' => implode ( ', ' , $modules ),
)));
}
/**
* Installs a specific table 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 \RuntimeException
* Thrown when $module is not enabled or when the table schema cannot be
* found in the module specified .
*/
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 )) {
2015-08-27 12:03:05 -07:00
throw new \RuntimeException ( " ' $module ' module is not enabled " );
2015-08-17 17:00:26 -07:00
}
$tables = ( array ) $tables ;
foreach ( $tables as $table ) {
$schema = drupal_get_module_schema ( $module , $table );
if ( empty ( $schema )) {
2015-08-27 12:03:05 -07:00
throw new \RuntimeException ( " Unknown ' $table ' table schema in ' $module ' module. " );
2015-08-17 17:00:26 -07:00
}
$this -> container -> get ( 'database' ) -> schema () -> createTable ( $table , $schema );
}
$this -> pass ( format_string ( 'Installed %module tables: %tables.' , array (
'%tables' => '{' . implode ( '}, {' , $tables ) . '}' ,
'%module' => $module ,
)));
}
/**
* 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' , array (
'%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' , array (
'%entity_type' => $entity_type_id ,
'%tables' => '{' . implode ( '}, {' , $tables ) . '}' ,
)));
}
}
}
/**
* Enables modules for this test .
*
* @ param array $modules
* A list of modules to enable . Dependencies are not resolved ; i . e . ,
* multiple modules have to be specified with dependent modules first .
* The new modules are only added to the active module list and loaded .
*/
protected function enableModules ( array $modules ) {
// 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 = \Drupal :: service ( 'config.storage' );
$extensions = $active_storage -> read ( 'core.extension' );
foreach ( $modules as $module ) {
$module_handler -> addModule ( $module , $module_list [ $module ] -> getPath ());
// Maintain the list of enabled modules in configuration.
$extensions [ 'module' ][ $module ] = 0 ;
}
$active_storage -> write ( 'core.extension' , $extensions );
// Update the kernel to make their services available.
$module_filenames = $module_handler -> getModuleList ();
$this -> kernel -> updateModules ( $module_filenames , $module_filenames );
// Ensure isLoaded() is TRUE in order to make _theme() work.
// Note that the kernel has rebuilt the container; this $module_handler is
// no longer the $module_handler instance from above.
$this -> container -> get ( 'module_handler' ) -> reload ();
$this -> pass ( format_string ( 'Enabled modules: %modules.' , array (
'%modules' => implode ( ', ' , $modules ),
)));
}
/**
* Disables modules for this test .
*
* @ param array $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 active modules is still loaded . The modules are only
* removed from the active module list .
*/
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 ) {
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 -> kernel -> updateModules ( $module_filenames , $module_filenames );
// Ensure isLoaded() is TRUE in order to make _theme() 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 ();
$this -> pass ( format_string ( 'Disabled modules: %modules.' , array (
'%modules' => implode ( ', ' , $modules ),
)));
}
/**
* Registers a stream wrapper for this test .
*
* @ param string $scheme
* The scheme to register .
* @ param string $class
* The fully qualified class name to register .
* @ param int $type
* The Drupal Stream Wrapper API type . Defaults to
* StreamWrapperInterface :: NORMAL .
*/
protected function registerStreamWrapper ( $scheme , $class , $type = StreamWrapperInterface :: NORMAL ) {
$this -> container -> get ( 'stream_wrapper_manager' ) -> registerWrapper ( $scheme , $class , $type );
}
/**
* Renders a render array .
*
* @ param array $elements
* The elements to render .
*
* @ return string
* The rendered string output ( typically HTML ) .
*/
protected function render ( array & $elements ) {
$content = $this -> container -> get ( 'renderer' ) -> renderRoot ( $elements );
drupal_process_attached ( $elements );
$this -> setRawContent ( $content );
2015-09-04 13:20:09 -07:00
$this -> verbose ( '<pre style="white-space: pre-wrap">' . Html :: escape ( $content ));
2015-08-17 17:00:26 -07:00
return $content ;
}
}