BrowserTestBase.php in Zircon Profile 8
Same filename and directory in other branches
Contains \Drupal\simpletest\BrowserTestBase.
Namespace
Drupal\simpletestFile
core/modules/simpletest/src/BrowserTestBase.phpView source
<?php
/**
* @file
* Contains \Drupal\simpletest\BrowserTestBase.
*/
namespace Drupal\simpletest;
use Behat\Mink\Driver\GoutteDriver;
use Behat\Mink\Element\Element;
use Behat\Mink\Exception\Exception;
use Behat\Mink\Mink;
use Behat\Mink\Session;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\Component\Utility\UrlHelper;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Database\ConnectionNotDefinedException;
use Drupal\Core\Database\Database;
use Drupal\Core\DrupalKernel;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Session\AnonymousUserSession;
use Drupal\Core\Session\UserSession;
use Drupal\Core\Site\Settings;
use Drupal\Core\StreamWrapper\StreamWrapperInterface;
use Drupal\Core\Test\TestRunnerKernel;
use Drupal\Core\Url;
use Drupal\user\UserInterface;
use Symfony\Component\HttpFoundation\Request;
/**
* Provides a test case for functional Drupal tests.
*
* Note that this class does not yet have feature parity with WebTestBase, so
* WebTestBase should be used where possible. In particular, this class does
* not yet have the following features:
* - verbose output - see https://www.drupal.org/node/2469721
* - ajax form emulation - see https://www.drupal.org/node/2469713
*
* Tests extending BrowserTestBase must exist in the
* Drupal\Tests\yourmodule\Functional namespace and live in the
* modules/yourmodule/Tests/Functional directory.
*
* All BrowserTestBase tests must have two annotations to ensure process
* isolation:
* - @runTestsInSeparateProcesses
* - @preserveGlobalState disabled
*
* @ingroup testing
*
* @see \Drupal\simpletest\WebTestBase
*/
abstract class BrowserTestBase extends \PHPUnit_Framework_TestCase {
use RandomGeneratorTrait;
use SessionTestTrait;
/**
* Class loader.
*
* @var object
*/
protected $classLoader;
/**
* The site directory of this test run.
*
* @var string
*/
protected $siteDirectory;
/**
* The database prefix of this test run.
*
* @var string
*/
protected $databasePrefix;
/**
* The site directory of the original parent site.
*
* @var string
*/
protected $originalSiteDirectory;
/**
* Time limit in seconds for the test.
*
* @var int
*/
protected $timeLimit = 500;
/**
* The public file directory for the test environment.
*
* This is set in BrowserTestBase::prepareEnvironment().
*
* @var string
*/
protected $publicFilesDirectory;
/**
* The private file directory for the test environment.
*
* This is set in BrowserTestBase::prepareEnvironment().
*
* @var string
*/
protected $privateFilesDirectory;
/**
* The temp file directory for the test environment.
*
* This is set in BrowserTestBase::prepareEnvironment().
*
* @var string
*/
protected $tempFilesDirectory;
/**
* The translation file directory for the test environment.
*
* This is set in BrowserTestBase::prepareEnvironment().
*
* @var string
*/
protected $translationFilesDirectory;
/**
* The DrupalKernel instance used in the test.
*
* @var \Drupal\Core\DrupalKernel
*/
protected $kernel;
/**
* The dependency injection container used in the test.
*
* @var \Symfony\Component\DependencyInjection\ContainerInterface
*/
protected $container;
/**
* The config importer that can be used in a test.
*
* @var \Drupal\Core\Config\ConfigImporter
*/
protected $configImporter;
/**
* The profile to install as a basis for testing.
*
* @var string
*/
protected $profile = 'testing';
/**
* The current user logged in using the Mink controlled browser.
*
* @var \Drupal\user\UserInterface
*/
protected $loggedInUser = FALSE;
/**
* The root user.
*
* @var \Drupal\Core\Session\UserSession
*/
protected $rootUser;
/**
* The config directories used in this test.
*
* @var array
*/
protected $configDirectories = array();
/**
* An array of custom translations suitable for drupal_rewrite_settings().
*
* @var array
*/
protected $customTranslations;
/*
* Mink class for the default driver to use.
*
* Shoud be a fully qualified class name that implements
* Behat\Mink\Driver\DriverInterface.
*
* Value can be overridden using the environment variable MINK_DRIVER_CLASS.
*
* @var string.
*/
protected $minkDefaultDriverClass = '\\Behat\\Mink\\Driver\\GoutteDriver';
/*
* Mink default driver params.
*
* If it's an array its contents are used as constructor params when default
* Mink driver class is instantiated.
*
* Can be overridden using the environment variable MINK_DRIVER_ARGS. In this
* case that variable should be a JSON array, for example:
* '["firefox", null, "http://localhost:4444/wd/hub"]'.
*
*
* @var array
*/
protected $minkDefaultDriverArgs;
/**
* Mink session manager.
*
* @var \Behat\Mink\Mink
*/
protected $mink;
/**
* Initializes Mink sessions.
*/
protected function initMink() {
$driver = $this
->getDefaultDriverInstance();
$session = new Session($driver);
$this->mink = new Mink();
$this->mink
->registerSession('default', $session);
$this->mink
->setDefaultSessionName('default');
$this
->registerSessions();
return $session;
}
/**
* Gets an instance of the default Mink driver.
*
* @return Behat\Mink\Driver\DriverInterface
* Instance of default Mink driver.
*
* @throws \InvalidArgumentException
* When provided default Mink driver class can't be instantiated.
*/
protected function getDefaultDriverInstance() {
// Get default driver params from environment if availables.
if ($arg_json = getenv('MINK_DRIVER_ARGS')) {
$this->minkDefaultDriverArgs = json_decode($arg_json);
}
// Get and check default driver class from environment if availables.
if ($minkDriverClass = getenv('MINK_DRIVER_CLASS')) {
if (class_exists($minkDriverClass)) {
$this->minkDefaultDriverClass = $minkDriverClass;
}
else {
throw new \InvalidArgumentException("Can't instantiate provided {$minkDriverClass} class by environment as default driver class.");
}
}
if (is_array($this->minkDefaultDriverArgs)) {
// Use ReflectionClass to instantiate class with received params.
$reflector = new ReflectionClass($this->minkDefaultDriverClass);
$driver = $reflector
->newInstanceArgs($this->minkDefaultDriverArgs);
}
else {
$driver = new $this->minkDefaultDriverClass();
}
return $driver;
}
/**
* Registers additional Mink sessions.
*
* Tests wishing to use a different driver or change the default driver should
* override this method.
*
* @code
* // Register a new session that uses the MinkPonyDriver.
* $pony = new MinkPonyDriver();
* $session = new Session($pony);
* $this->mink->registerSession('pony', $session);
* @endcode
*/
protected function registerSessions() {
}
/**
* {@inheritdoc}
*/
protected function setUp() {
global $base_url;
parent::setUp();
// Get and set the domain of the environment we are running our test
// coverage against.
$base_url = getenv('SIMPLETEST_BASE_URL');
if (!$base_url) {
$this
->markTestSkipped('You must provide a SIMPLETEST_BASE_URL environment variable to run some PHPUnit based functional tests.');
}
// Setup $_SERVER variable.
$parsed_url = parse_url($base_url);
$host = $parsed_url['host'] . (isset($parsed_url['port']) ? ':' . $parsed_url['port'] : '');
$path = isset($parsed_url['path']) ? rtrim(rtrim($parsed_url['path']), '/') : '';
$port = isset($parsed_url['port']) ? $parsed_url['port'] : 80;
// If the passed URL schema is 'https' then setup the $_SERVER variables
// properly so that testing will run under HTTPS.
if ($parsed_url['scheme'] === 'https') {
$_SERVER['HTTPS'] = 'on';
}
$_SERVER['HTTP_HOST'] = $host;
$_SERVER['REMOTE_ADDR'] = '127.0.0.1';
$_SERVER['SERVER_ADDR'] = '127.0.0.1';
$_SERVER['SERVER_PORT'] = $port;
$_SERVER['SERVER_SOFTWARE'] = NULL;
$_SERVER['SERVER_NAME'] = 'localhost';
$_SERVER['REQUEST_URI'] = $path . '/';
$_SERVER['REQUEST_METHOD'] = 'GET';
$_SERVER['SCRIPT_NAME'] = $path . '/index.php';
$_SERVER['SCRIPT_FILENAME'] = $path . '/index.php';
$_SERVER['PHP_SELF'] = $path . '/index.php';
$_SERVER['HTTP_USER_AGENT'] = 'Drupal command line';
// Install Drupal test site.
$this
->prepareEnvironment();
$this
->installDrupal();
// Setup Mink.
$session = $this
->initMink();
// In order to debug web tests you need to either set a cookie, have the
// Xdebug session in the URL or set an environment variable in case of CLI
// requests. If the developer listens to connection when running tests, by
// default the cookie is not forwarded to the client side, so you cannot
// debug the code running on the test site. In order to make debuggers work
// this bit of information is forwarded. Make sure that the debugger listens
// to at least three external connections.
$request = \Drupal::request();
$cookie_params = $request->cookies;
if ($cookie_params
->has('XDEBUG_SESSION')) {
$session
->setCookie('XDEBUG_SESSION', $cookie_params
->get('XDEBUG_SESSION'));
}
// For CLI requests, the information is stored in $_SERVER.
$server = $request->server;
if ($server
->has('XDEBUG_CONFIG')) {
// $_SERVER['XDEBUG_CONFIG'] has the form "key1=value1 key2=value2 ...".
$pairs = explode(' ', $server
->get('XDEBUG_CONFIG'));
foreach ($pairs as $pair) {
list($key, $value) = explode('=', $pair);
// Account for key-value pairs being separated by multiple spaces.
if (trim($key) == 'idekey') {
$session
->setCookie('XDEBUG_SESSION', trim($value));
}
}
}
}
/**
* Ensures test files are deletable within file_unmanaged_delete_recursive().
*
* Some tests chmod generated files to be read only. During
* BrowserTestBase::cleanupEnvironment() and other cleanup operations,
* these files need to get deleted too.
*
* @param string $path
* The file path.
*/
public static function filePreDeleteCallback($path) {
$success = @chmod($path, 0700);
if (!$success) {
trigger_error("Can not make {$path} writable whilst cleaning up test directory. The webserver and phpunit are probably not being run by the same user.");
}
}
/**
* Clean up the Simpletest environment.
*/
protected function cleanupEnvironment() {
// 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]);
}
}
}
// Delete test site directory.
file_unmanaged_delete_recursive($this->siteDirectory, array(
$this,
'filePreDeleteCallback',
));
}
/**
* {@inheritdoc}
*/
protected function tearDown() {
parent::tearDown();
// Destroy the testing kernel.
if (isset($this->kernel)) {
$this
->cleanupEnvironment();
$this->kernel
->shutdown();
}
// Ensure that internal logged in variable is reset.
$this->loggedInUser = FALSE;
if ($this->mink) {
$this->mink
->stopSessions();
}
}
/**
* Returns Mink session.
*
* @param string $name
* (optional) Name of the session. Defaults to the active session.
*
* @return \Behat\Mink\Session
* The active Mink session object.
*/
public function getSession($name = NULL) {
return $this->mink
->getSession($name);
}
/**
* Returns WebAssert object.
*
* @param string $name
* (optional) Name of the session. Defaults to the active session.
*
* @return \Drupal\simpletest\WebAssert
* A new web-assert option for asserting the presence of elements with.
*/
public function assertSession($name = NULL) {
return new WebAssert($this
->getSession($name));
}
/**
* Prepare for a request to testing site.
*
* The testing site is protected via a SIMPLETEST_USER_AGENT cookie that is
* checked by drupal_valid_test_ua().
*
* @see drupal_valid_test_ua()
*/
protected function prepareRequest() {
$session = $this
->getSession();
$session
->setCookie('SIMPLETEST_USER_AGENT', drupal_generate_test_ua($this->databasePrefix));
}
/**
* Retrieves a Drupal path or an absolute path.
*
* @param string $path
* Drupal path or URL to load into Mink controlled browser.
* @param array $options
* (optional) Options to be forwarded to the url generator.
*
* @return string
* The retrieved HTML string, also available as $this->getRawContent()
*/
protected function drupalGet($path, array $options = array()) {
$options['absolute'] = TRUE;
// The URL generator service is not necessarily available yet; e.g., in
// interactive installer tests.
if ($this->container
->has('url_generator')) {
if (UrlHelper::isExternal($path)) {
$url = Url::fromUri($path, $options)
->toString();
}
else {
// This is needed for language prefixing.
$options['path_processing'] = TRUE;
$url = Url::fromUri('base:/' . $path, $options)
->toString();
}
}
else {
$url = $this
->getAbsoluteUrl($path);
}
$session = $this
->getSession();
$this
->prepareRequest();
$session
->visit($url);
$out = $session
->getPage()
->getContent();
// Ensure that any changes to variables in the other thread are picked up.
$this
->refreshVariables();
return $out;
}
/**
* Takes a path and returns an absolute path.
*
* @param string $path
* A path from the Mink controlled browser content.
*
* @return string
* The $path with $base_url prepended, if necessary.
*/
protected function getAbsoluteUrl($path) {
global $base_url, $base_path;
$parts = parse_url($path);
if (empty($parts['host'])) {
// Ensure that we have a string (and no xpath object).
$path = (string) $path;
// Strip $base_path, if existent.
$length = strlen($base_path);
if (substr($path, 0, $length) === $base_path) {
$path = substr($path, $length);
}
// Ensure that we have an absolute path.
if (empty($path) || $path[0] !== '/') {
$path = '/' . $path;
}
// Finally, prepend the $base_url.
$path = $base_url . $path;
}
return $path;
}
/**
* Creates a user with a given set of permissions.
*
* @param array $permissions
* (optional) Array of permission names to assign to user. Note that the
* user always has the default permissions derived from the
* "authenticated users" role.
* @param string $name
* (optional) The user name.
*
* @return \Drupal\user\Entity\User|false
* A fully loaded user object with passRaw property, or FALSE if account
* creation fails.
*/
protected function drupalCreateUser(array $permissions = array(), $name = NULL) {
// Create a role with the given permission set, if any.
$rid = FALSE;
if ($permissions) {
$rid = $this
->drupalCreateRole($permissions);
if (!$rid) {
return FALSE;
}
}
// Create a user assigned to that role.
$edit = array();
$edit['name'] = !empty($name) ? $name : $this
->randomMachineName();
$edit['mail'] = $edit['name'] . '@example.com';
$edit['pass'] = user_password();
$edit['status'] = 1;
if ($rid) {
$edit['roles'] = array(
$rid,
);
}
$account = entity_create('user', $edit);
$account
->save();
$this
->assertNotNull($account
->id(), SafeMarkup::format('User created with name %name and pass %pass', array(
'%name' => $edit['name'],
'%pass' => $edit['pass'],
)));
if (!$account
->id()) {
return FALSE;
}
// Add the raw password so that we can log in as this user.
$account->passRaw = $edit['pass'];
return $account;
}
/**
* Creates a role with specified permissions.
*
* @param array $permissions
* Array of permission names to assign to role.
* @param string $rid
* (optional) The role ID (machine name). Defaults to a random name.
* @param string $name
* (optional) The label for the role. Defaults to a random string.
* @param int $weight
* (optional) The weight for the role. Defaults NULL so that entity_create()
* sets the weight to maximum + 1.
*
* @return string
* Role ID of newly created role, or FALSE if role creation failed.
*/
protected function drupalCreateRole(array $permissions, $rid = NULL, $name = NULL, $weight = NULL) {
// Generate a random, lowercase machine name if none was passed.
if (!isset($rid)) {
$rid = strtolower($this
->randomMachineName(8));
}
// Generate a random label.
if (!isset($name)) {
// In the role UI role names are trimmed and random string can start or
// end with a space.
$name = trim($this
->randomString(8));
}
// Check the all the permissions strings are valid.
if (!$this
->checkPermissions($permissions)) {
return FALSE;
}
// Create new role.
/* @var \Drupal\user\RoleInterface $role */
$role = entity_create('user_role', array(
'id' => $rid,
'label' => $name,
));
if (!is_null($weight)) {
$role
->set('weight', $weight);
}
$result = $role
->save();
$this
->assertSame($result, SAVED_NEW, SafeMarkup::format('Created role ID @rid with name @name.', array(
'@name' => var_export($role
->label(), TRUE),
'@rid' => var_export($role
->id(), TRUE),
)));
if ($result === SAVED_NEW) {
// Grant the specified permissions to the role, if any.
if (!empty($permissions)) {
user_role_grant_permissions($role
->id(), $permissions);
$assigned_permissions = entity_load('user_role', $role
->id())
->getPermissions();
$missing_permissions = array_diff($permissions, $assigned_permissions);
if ($missing_permissions) {
$this
->fail(SafeMarkup::format('Failed to create permissions: @perms', array(
'@perms' => implode(', ', $missing_permissions),
)));
}
}
return $role
->id();
}
return FALSE;
}
/**
* Checks whether a given list of permission names is valid.
*
* @param array $permissions
* The permission names to check.
*
* @return bool
* TRUE if the permissions are valid, FALSE otherwise.
*/
protected function checkPermissions(array $permissions) {
$available = array_keys(\Drupal::service('user.permissions')
->getPermissions());
$valid = TRUE;
foreach ($permissions as $permission) {
if (!in_array($permission, $available)) {
$this
->fail(SafeMarkup::format('Invalid permission %permission.', array(
'%permission' => $permission,
)));
$valid = FALSE;
}
}
return $valid;
}
/**
* Logs in a user using the Mink controlled browser.
*
* If a user is already logged in, then the current user is logged out before
* logging in the specified user.
*
* Please note that neither the current user nor the passed-in user object is
* populated with data of the logged in user. If you need full access to the
* user object after logging in, it must be updated manually. If you also need
* access to the plain-text password of the user (set by drupalCreateUser()),
* e.g. to log in the same user again, then it must be re-assigned manually.
* For example:
* @code
* // Create a user.
* $account = $this->drupalCreateUser(array());
* $this->drupalLogin($account);
* // Load real user object.
* $pass_raw = $account->passRaw;
* $account = User::load($account->id());
* $account->passRaw = $pass_raw;
* @endcode
*
* @param \Drupal\Core\Session\AccountInterface $account
* User object representing the user to log in.
*
* @see drupalCreateUser()
*/
protected function drupalLogin(AccountInterface $account) {
if ($this->loggedInUser) {
$this
->drupalLogout();
}
$this
->drupalGet('user');
$this
->assertSession()
->statusCodeEquals(200);
$this
->submitForm(array(
'name' => $account
->getUsername(),
'pass' => $account->passRaw,
), t('Log in'));
// @see BrowserTestBase::drupalUserIsLoggedIn()
$account->sessionId = $this
->getSession()
->getCookie($this
->getSessionName());
$this
->assertTrue($this
->drupalUserIsLoggedIn($account), SafeMarkup::format('User %name successfully logged in.', array(
'name' => $account
->getUsername(),
)));
$this->loggedInUser = $account;
$this->container
->get('current_user')
->setAccount($account);
}
/**
* Logs a user out of the Mink controlled browser and confirms.
*
* Confirms logout by checking the login page.
*/
protected function drupalLogout() {
// Make a request to the logout page, and redirect to the user page, the
// idea being if you were properly logged out you should be seeing a login
// screen.
$assert_session = $this
->assertSession();
$this
->drupalGet('user/logout', array(
'query' => array(
'destination' => 'user',
),
));
$assert_session
->statusCodeEquals(200);
$assert_session
->fieldExists('name');
$assert_session
->fieldExists('pass');
// @see BrowserTestBase::drupalUserIsLoggedIn()
unset($this->loggedInUser->sessionId);
$this->loggedInUser = FALSE;
$this->container
->get('current_user')
->setAccount(new AnonymousUserSession());
}
/**
* Fills and submits a form.
*
* @param array $edit
* Field data in an associative array. Changes the current input fields
* (where possible) to the values indicated.
*
* A checkbox can be set to TRUE to be checked and should be set to FALSE to
* be unchecked.
* @param string $submit
* Value of the submit button whose click is to be emulated. For example,
* t('Save'). The processing of the request depends on this value. For
* example, a form may have one button with the value t('Save') and another
* button with the value t('Delete'), and execute different code depending
* on which one is clicked.
* @param string $form_html_id
* (optional) HTML ID of the form to be submitted. On some pages
* there are many identical forms, so just using the value of the submit
* button is not enough. For example: 'trigger-node-presave-assign-form'.
* Note that this is not the Drupal $form_id, but rather the HTML ID of the
* form, which is typically the same thing but with hyphens replacing the
* underscores.
*/
protected function submitForm(array $edit, $submit, $form_html_id = NULL) {
$assert_session = $this
->assertSession();
// Get the form.
if (isset($form_html_id)) {
$form = $assert_session
->elementExists('xpath', "//form[@id='{$form_html_id}']");
$submit_button = $assert_session
->buttonExists($submit, $form);
}
else {
$submit_button = $assert_session
->buttonExists($submit);
$form = $assert_session
->elementExists('xpath', './ancestor::form', $submit_button);
}
// Edit the form values.
foreach ($edit as $name => $value) {
$field = $assert_session
->fieldExists($name, $form);
$field
->setValue($value);
}
// Submit form.
$this
->prepareRequest();
$submit_button
->press();
// Ensure that any changes to variables in the other thread are picked up.
$this
->refreshVariables();
}
/**
* Helper function to get the options of select field.
*
* @param \Behat\Mink\Element\NodeElement|string $select
* Name, ID, or Label of select field to assert.
* @param \Behat\Mink\Element\Element $container
* (optional) Container element to check against. Defaults to current page.
*
* @return array
* Associative array of option keys and values.
*/
protected function getOptions($select, Element $container = NULL) {
if (is_string($select)) {
$select = $this
->assertSession()
->selectExists($select, $container);
}
$options = [];
/* @var \Behat\Mink\Element\NodeElement $option */
foreach ($select
->findAll('xpath', '//option') as $option) {
$label = $option
->getText();
$value = $option
->getAttribute('value') ?: $label;
$options[$value] = $label;
}
return $options;
}
/**
* Override to use Mink exceptions.
*
* @return mixed
* Either a test result or NULL.
*
* @throws \PHPUnit_Framework_AssertionFailedError
* When exception was thrown inside the test.
*/
protected function runTest() {
try {
return parent::runTest();
} catch (Exception $e) {
throw new \PHPUnit_Framework_AssertionFailedError($e
->getMessage());
}
}
/**
* Installs Drupal into the Simpletest site.
*/
public function installDrupal() {
// Define information about the user 1 account.
$this->rootUser = new UserSession(array(
'uid' => 1,
'name' => 'admin',
'mail' => 'admin@example.com',
'passRaw' => $this
->randomMachineName(),
));
// The child site derives its session name from the database prefix when
// running web tests.
$this
->generateSessionName($this->databasePrefix);
// Get parameters for install_drupal() before removing global variables.
$parameters = $this
->installParameters();
// Prepare installer settings that are not install_drupal() parameters.
// Copy and prepare an actual settings.php, so as to resemble a regular
// installation.
// Not using File API; a potential error must trigger a PHP warning.
$directory = DRUPAL_ROOT . '/' . $this->siteDirectory;
copy(DRUPAL_ROOT . '/sites/default/default.settings.php', $directory . '/settings.php');
// All file system paths are created by System module during installation.
// @see system_requirements()
// @see TestBase::prepareEnvironment()
$settings['settings']['file_public_path'] = (object) array(
'value' => $this->publicFilesDirectory,
'required' => TRUE,
);
$this
->writeSettings($settings);
// Allow for test-specific overrides.
$settings_testing_file = DRUPAL_ROOT . '/' . $this->originalSiteDirectory . '/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');
// Add the name of the testing class to settings.php and include the
// testing specific overrides.
file_put_contents($directory . '/settings.php', "\n\$test_class = '" . get_class($this) . "';\n" . 'include DRUPAL_ROOT . \'/\' . $site_path . \'/settings.testing.php\';' . "\n", FILE_APPEND);
}
$settings_services_file = DRUPAL_ROOT . '/' . $this->originalSiteDirectory . '/testing.services.yml';
if (file_exists($settings_services_file)) {
// Copy the testing-specific service overrides in place.
copy($settings_services_file, $directory . '/services.yml');
}
// Since Drupal is bootstrapped already, install_begin_request() will not
// bootstrap into DRUPAL_BOOTSTRAP_CONFIGURATION (again). Hence, we have to
// reload the newly written custom settings.php manually.
Settings::initialize(DRUPAL_ROOT, $directory, $this->classLoader);
// Execute the non-interactive installer.
require_once DRUPAL_ROOT . '/core/includes/install.core.inc';
install_drupal($parameters);
// Import new settings.php written by the installer.
Settings::initialize(DRUPAL_ROOT, $directory, $this->classLoader);
foreach ($GLOBALS['config_directories'] as $type => $path) {
$this->configDirectories[$type] = $path;
}
// After writing settings.php, the installer removes write permissions from
// the site directory. To allow drupal_generate_test_ua() to write a file
// containing the private key for drupal_valid_test_ua(), the site directory
// has to be writable.
// TestBase::restoreEnvironment() will delete the entire site directory. Not
// using File API; a potential error must trigger a PHP warning.
chmod($directory, 0777);
$request = \Drupal::request();
$this->kernel = DrupalKernel::createFromRequest($request, $this->classLoader, 'prod', TRUE);
$this->kernel
->prepareLegacyRequest($request);
// Force the container to be built from scratch instead of loaded from the
// disk. This forces us to not accidentally load the parent site.
$container = $this->kernel
->rebuildContainer();
$config = $container
->get('config.factory');
// Manually create and configure private and temporary files directories.
// While these could be preset/enforced in settings.php like the public
// files directory above, some tests expect them to be configurable in the
// UI. If declared in settings.php, they would no longer be configurable.
file_prepare_directory($this->privateFilesDirectory, FILE_CREATE_DIRECTORY);
file_prepare_directory($this->tempFilesDirectory, FILE_CREATE_DIRECTORY);
$config
->getEditable('system.file')
->set('path.private', $this->privateFilesDirectory)
->set('path.temporary', $this->tempFilesDirectory)
->save();
// 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.
$config
->getEditable('system.mail')
->set('interface.default', 'test_mail_collector')
->save();
// By default, verbosely display all errors and disable all production
// environment optimizations for all tests to avoid needless overhead and
// ensure a sane default experience for test authors.
// @see https://www.drupal.org/node/2259167
$config
->getEditable('system.logging')
->set('error_level', 'verbose')
->save();
$config
->getEditable('system.performance')
->set('css.preprocess', FALSE)
->set('js.preprocess', FALSE)
->save();
// Collect modules to install.
$class = get_class($this);
$modules = array();
while ($class) {
if (property_exists($class, 'modules')) {
$modules = array_merge($modules, $class::$modules);
}
$class = get_parent_class($class);
}
if ($modules) {
$modules = array_unique($modules);
$success = $container
->get('module_installer')
->install($modules, TRUE);
$this
->assertTrue($success, SafeMarkup::format('Enabled modules: %modules', array(
'%modules' => implode(', ', $modules),
)));
$this
->rebuildContainer();
}
// Reset/rebuild all data structures after enabling the modules, primarily
// to synchronize all data structures and caches between the test runner and
// the child site.
// Affects e.g. StreamWrapperManagerInterface::getWrappers().
// @see \Drupal\Core\DrupalKernel::bootCode()
// @todo Test-specific setUp() methods may set up further fixtures; find a
// way to execute this after setUp() is done, or to eliminate it entirely.
$this
->resetAll();
$this->kernel
->prepareLegacyRequest($request);
}
/**
* Returns the parameters that will be used when Simpletest installs Drupal.
*
* @see install_drupal()
* @see install_state_defaults()
*/
protected function installParameters() {
$connection_info = Database::getConnectionInfo();
$driver = $connection_info['default']['driver'];
$connection_info['default']['prefix'] = $connection_info['default']['prefix']['default'];
unset($connection_info['default']['driver']);
unset($connection_info['default']['namespace']);
unset($connection_info['default']['pdo']);
unset($connection_info['default']['init_commands']);
$parameters = array(
'interactive' => FALSE,
'parameters' => array(
'profile' => $this->profile,
'langcode' => 'en',
),
'forms' => array(
'install_settings_form' => array(
'driver' => $driver,
$driver => $connection_info['default'],
),
'install_configure_form' => array(
'site_name' => 'Drupal',
'site_mail' => 'simpletest@example.com',
'account' => array(
'name' => $this->rootUser->name,
'mail' => $this->rootUser
->getEmail(),
'pass' => array(
'pass1' => $this->rootUser->passRaw,
'pass2' => $this->rootUser->passRaw,
),
),
// form_type_checkboxes_value() requires NULL instead of FALSE values
// for programmatic form submissions to disable a checkbox.
'update_status_module' => array(
1 => NULL,
2 => NULL,
),
),
),
);
return $parameters;
}
/**
* Generates a database prefix for running tests.
*
* The database prefix is used by prepareEnvironment() to setup a public files
* directory for the test to be run, which also contains the PHP error log,
* which is written to in case of a fatal error. Since that directory is based
* on the database prefix, all tests (even unit tests) need to have one, in
* order to access and read the error log.
*
* The generated database table prefix is used for the Drupal installation
* being performed for the test. It is also used by the cookie value of
* SIMPLETEST_USER_AGENT by the Mink controlled browser. During early Drupal
* bootstrap, the cookie is parsed, and if it matches, all database queries
* use the database table prefix that has been generated here.
*
* @see drupal_valid_test_ua()
* @see BrowserTestBase::prepareEnvironment()
*/
private function prepareDatabasePrefix() {
// Ensure that the generated test site directory does not exist already,
// which may happen with a large amount of concurrent threads and
// long-running tests.
do {
$suffix = mt_rand(100000, 999999);
$this->siteDirectory = 'sites/simpletest/' . $suffix;
$this->databasePrefix = 'simpletest' . $suffix;
} while (is_dir(DRUPAL_ROOT . '/' . $this->siteDirectory));
}
/**
* Changes the database connection to the prefixed one.
*
* @see BrowserTestBase::prepareEnvironment()
*/
private function changeDatabasePrefix() {
if (empty($this->databasePrefix)) {
$this
->prepareDatabasePrefix();
}
// If the test is run with argument dburl then use it.
$db_url = getenv('SIMPLETEST_DB');
if (!empty($db_url)) {
$database = Database::convertDbUrlToConnectionInfo($db_url, DRUPAL_ROOT);
Database::addConnectionInfo('default', 'default', $database);
}
// Clone the current connection and replace the current prefix.
$connection_info = Database::getConnectionInfo('default');
if (is_null($connection_info)) {
throw new \InvalidArgumentException('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.');
}
else {
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'] = array(
'default' => $value['prefix']['default'] . $this->databasePrefix,
);
}
Database::addConnectionInfo('default', 'default', $connection_info['default']);
}
}
/**
* Prepares the current environment for running the test.
*
* Also sets up new resources for the testing environment, such as the public
* filesystem and configuration directories.
*
* This method is private as it must only be called once by
* BrowserTestBase::setUp() (multiple invocations for the same test would have
* unpredictable consequences) and it must not be callable or overridable by
* test classes.
*/
protected function prepareEnvironment() {
// Bootstrap Drupal so we can use Drupal's built in functions.
$this->classLoader = (require __DIR__ . '/../../../../autoload.php');
$request = Request::createFromGlobals();
$kernel = TestRunnerKernel::createFromRequest($request, $this->classLoader);
// TestRunnerKernel expects the working directory to be DRUPAL_ROOT.
chdir(DRUPAL_ROOT);
$kernel
->prepareLegacyRequest($request);
$this
->prepareDatabasePrefix();
$this->originalSiteDirectory = $kernel
->findSitePath($request);
// Create test directory ahead of installation so fatal errors and debug
// information can be logged during installation process.
file_prepare_directory($this->siteDirectory, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS);
// Prepare filesystem directory paths.
$this->publicFilesDirectory = $this->siteDirectory . '/files';
$this->privateFilesDirectory = $this->siteDirectory . '/private';
$this->tempFilesDirectory = $this->siteDirectory . '/temp';
$this->translationFilesDirectory = $this->siteDirectory . '/translations';
// Ensure the configImporter is refreshed for each test.
$this->configImporter = NULL;
// Unregister all custom stream wrappers of the parent site.
$wrappers = \Drupal::service('stream_wrapper_manager')
->getWrappers(StreamWrapperInterface::ALL);
foreach ($wrappers as $scheme => $info) {
stream_wrapper_unregister($scheme);
}
// Reset statics.
drupal_static_reset();
// Ensure there is no service container.
$this->container = NULL;
\Drupal::unsetContainer();
// Unset globals.
unset($GLOBALS['config_directories']);
unset($GLOBALS['config']);
unset($GLOBALS['conf']);
// Log fatal errors.
ini_set('log_errors', 1);
ini_set('error_log', DRUPAL_ROOT . '/' . $this->siteDirectory . '/error.log');
// Change the database prefix.
$this
->changeDatabasePrefix();
// After preparing the environment and changing the database prefix, we are
// in a valid test environment.
drupal_valid_test_ua($this->databasePrefix);
// Reset settings.
new Settings(array(
// For performance, simply use the database prefix as hash salt.
'hash_salt' => $this->databasePrefix,
));
drupal_set_time_limit($this->timeLimit);
}
/**
* Returns the database connection to the site running Simpletest.
*
* @return \Drupal\Core\Database\Connection
* The database connection to use for inserting assertions.
*/
public static function getDatabaseConnection() {
// Check whether there is a test runner connection.
// @see run-tests.sh
try {
$connection = Database::getConnection('default', 'test-runner');
} catch (ConnectionNotDefinedException $e) {
// Check whether there is a backup of the original default connection.
// @see BrowserTestBase::prepareEnvironment()
try {
$connection = Database::getConnection('default', 'simpletest_original_default');
} catch (ConnectionNotDefinedException $e) {
// If BrowserTestBase::prepareEnvironment() or
// BrowserTestBase::restoreEnvironment() failed, the test-specific
// database connection does not exist yet/anymore, so fall back to the
// default of the (UI) test runner.
$connection = Database::getConnection('default', 'default');
}
}
return $connection;
}
/**
* Rewrites the settings.php file of the test site.
*
* @param array $settings
* An array of settings to write out, in the format expected by
* drupal_rewrite_settings().
*
* @see drupal_rewrite_settings()
*/
protected function writeSettings(array $settings) {
include_once DRUPAL_ROOT . '/core/includes/install.inc';
$filename = $this->siteDirectory . '/settings.php';
// system_requirements() removes write permissions from settings.php
// whenever it is invoked.
// Not using File API; a potential error must trigger a PHP warning.
chmod($filename, 0666);
drupal_rewrite_settings($settings, $filename);
}
/**
* Rebuilds \Drupal::getContainer().
*
* Use this to build a new kernel and service container. For example, when the
* list of enabled modules is changed via the Mink controlled browser, in
* which case the test process still contains an old kernel and service
* container with an old module list.
*
* @see BrowserTestBase::prepareEnvironment()
* @see BrowserTestBase::restoreEnvironment()
*
* @todo Fix https://www.drupal.org/node/2021959 so that module enable/disable
* changes are immediately reflected in \Drupal::getContainer(). Until then,
* tests can invoke this workaround when requiring services from newly
* enabled modules to be immediately available in the same request.
*/
protected function rebuildContainer() {
// Rebuild the kernel and bring it back to a fully bootstrapped state.
$this->container = $this->kernel
->rebuildContainer();
// Make sure the url generator has a request object, otherwise calls to
// $this->drupalGet() will fail.
$this
->prepareRequestForGenerator();
}
/**
* Creates a mock request and sets it on the generator.
*
* This is used to manipulate how the generator generates paths during tests.
* It also ensures that calls to $this->drupalGet() will work when running
* from run-tests.sh because the url generator no longer looks at the global
* variables that are set there but relies on getting this information from a
* request object.
*
* @param bool $clean_urls
* Whether to mock the request using clean urls.
* @param array $override_server_vars
* An array of server variables to override.
*
* @return Request
* The mocked request object.
*/
protected function prepareRequestForGenerator($clean_urls = TRUE, $override_server_vars = array()) {
$request = Request::createFromGlobals();
$server = $request->server
->all();
if (basename($server['SCRIPT_FILENAME']) != basename($server['SCRIPT_NAME'])) {
// We need this for when the test is executed by run-tests.sh.
// @todo Remove this once run-tests.sh has been converted to use a Request
// object.
$cwd = getcwd();
$server['SCRIPT_FILENAME'] = $cwd . '/' . basename($server['SCRIPT_NAME']);
$base_path = rtrim($server['REQUEST_URI'], '/');
}
else {
$base_path = $request
->getBasePath();
}
if ($clean_urls) {
$request_path = $base_path ? $base_path . '/user' : 'user';
}
else {
$request_path = $base_path ? $base_path . '/index.php/user' : '/index.php/user';
}
$server = array_merge($server, $override_server_vars);
$request = Request::create($request_path, 'GET', array(), array(), array(), $server);
// Ensure the request time is REQUEST_TIME to ensure that API calls
// in the test use the right timestamp.
$request->server
->set('REQUEST_TIME', REQUEST_TIME);
$this->container
->get('request_stack')
->push($request);
// The request context is normally set by the router_listener from within
// its KernelEvents::REQUEST listener. In the Simpletest parent site this
// event is not fired, therefore it is necessary to updated the request
// context manually here.
$this->container
->get('router.request_context')
->fromRequest($request);
return $request;
}
/**
* Resets all data structures after having enabled new modules.
*
* This method is called by \Drupal\simpletest\BrowserTestBase::setUp() after
* enabling the requested modules. It must be called again when additional
* modules are enabled later.
*/
protected function resetAll() {
// Clear all database and static caches and rebuild data structures.
drupal_flush_all_caches();
$this->container = \Drupal::getContainer();
// Reset static variables and reload permissions.
$this
->refreshVariables();
}
/**
* Refreshes in-memory configuration and state information.
*
* Useful after a page request is made that changes configuration or state in
* a different thread.
*
* In other words calling a settings page with $this->submitForm() with a
* changed value would update configuration to reflect that change, but in the
* thread that made the call (thread running the test) the changed values
* would not be picked up.
*
* This method clears the cache and loads a fresh copy.
*/
protected function refreshVariables() {
// Clear the tag cache.
// @todo Replace drupal_static() usage within classes and provide a
// proper interface for invoking reset() on a cache backend:
// https://www.drupal.org/node/2311945.
drupal_static_reset('Drupal\\Core\\Cache\\CacheBackendInterface::tagCache');
drupal_static_reset('Drupal\\Core\\Cache\\DatabaseBackend::deletedTags');
drupal_static_reset('Drupal\\Core\\Cache\\DatabaseBackend::invalidatedTags');
foreach (Cache::getBins() as $backend) {
if (is_callable(array(
$backend,
'reset',
))) {
$backend
->reset();
}
}
$this->container
->get('config.factory')
->reset();
$this->container
->get('state')
->resetCache();
}
/**
* Returns whether a given user account is logged in.
*
* @param \Drupal\user\UserInterface $account
* The user account object to check.
*
* @return bool
* Return TRUE if the user is logged in, FALSE otherwise.
*/
protected function drupalUserIsLoggedIn(UserInterface $account) {
$logged_in = FALSE;
if (isset($account->sessionId)) {
$session_handler = $this->container
->get('session_handler.storage');
$logged_in = (bool) $session_handler
->read($account->sessionId);
}
return $logged_in;
}
}
Classes
Name | Description |
---|---|
BrowserTestBase | Provides a test case for functional Drupal tests. |