Issue #2498625 by jhedstrom, larowlan: Write tests that ensure hook_update_N is properly run

8.0.x
Nathaniel Catchpole 2015-06-12 12:31:10 +01:00
parent 691697f39c
commit 5756615df3
8 changed files with 558 additions and 80 deletions

View File

@ -20,6 +20,7 @@ use Drupal\Core\Database\Database;
use Drupal\Core\DrupalKernel;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\EventSubscriber\MainContentViewSubscriber;
use Drupal\Core\Extension\MissingDependencyException;
use Drupal\Core\Render\Element;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Session\AnonymousUserSession;
@ -28,6 +29,7 @@ use Drupal\Core\Site\Settings;
use Drupal\Core\StreamWrapper\PublicStream;
use Drupal\Core\Url;
use Drupal\node\Entity\NodeType;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Request;
/**
@ -188,12 +190,20 @@ abstract class WebTestBase extends TestBase {
*/
protected $customTranslations;
/**
* The class loader to use for installation and initialization of setup.
*
* @var \Symfony\Component\Classloader\Classloader
*/
protected $classLoader;
/**
* Constructor for \Drupal\simpletest\WebTestBase.
*/
function __construct($test_id = NULL) {
parent::__construct($test_id);
$this->skipClasses[__CLASS__] = TRUE;
$this->classLoader = require DRUPAL_ROOT . '/autoload.php';
}
/**
@ -621,32 +631,57 @@ abstract class WebTestBase extends TestBase {
* being executed.
*/
protected function setUp() {
// When running tests through the Simpletest UI (vs. on the command line),
// Simpletest's batch conflicts with the installer's batch. Batch API does
// not support the concept of nested batches (in which the nested is not
// progressive), so we need to temporarily pretend there was no batch.
// Backup the currently running Simpletest batch.
$this->originalBatch = batch_get();
// Preserve original batch for later restoration.
$this->setBatch();
// Define information about the user 1 account.
$this->rootUser = new UserSession(array(
'uid' => 1,
'name' => 'admin',
'mail' => 'admin@example.com',
'pass_raw' => $this->randomMachineName(),
));
// The child site derives its session name from the database prefix when
// running web tests.
$this->generateSessionName($this->databasePrefix);
// Reset the static batch to remove Simpletest's batch operations.
$batch = &batch_get();
$batch = array();
// Initialize user 1 and session name.
$this->initUserSession();
// Get parameters for install_drupal() before removing global variables.
$parameters = $this->installParameters();
// Prepare the child site settings.
$this->prepareSettings();
// Execute the non-interactive installer.
$this->doInstall($parameters);
// Import new settings.php written by the installer.
$this->initSettings();
// Initialize the request and container post-install.
$container = $this->initKernel(\Drupal::request());
// Initialize and override certain configurations.
$this->initConfig($container);
// Collect modules to install.
$this->installModulesFromClassProperty($container);
// Restore the original batch.
$this->restoreBatch();
// Reset/rebuild everything.
$this->rebuildAll();
}
/**
* Execute the non-interactive installer.
*
* @param array $parameters
* Parameters to pass to install_drupal().
*
* @see install_drupal()
*/
protected function doInstall(array $parameters = []) {
require_once DRUPAL_ROOT . '/core/includes/install.core.inc';
install_drupal($this->classLoader, $this->installParameters());
}
/**
* Prepares site settings and services before installation.
*/
protected function prepareSettings() {
// Prepare installer settings that are not install_drupal() parameters.
// Copy and prepare an actual settings.php, so as to resemble a regular
// installation.
@ -658,32 +693,32 @@ abstract class WebTestBase extends TestBase {
// All file system paths are created by System module during installation.
// @see system_requirements()
// @see TestBase::prepareEnvironment()
$settings['settings']['file_public_path'] = (object) array(
$settings['settings']['file_public_path'] = (object) [
'value' => $this->publicFilesDirectory,
'required' => TRUE,
);
$settings['settings']['file_private_path'] = (object) array(
];
$settings['settings']['file_private_path'] = (object) [
'value' => $this->privateFilesDirectory,
'required' => TRUE,
);
];
// 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['settings']['test_parent_site'] = (object) array(
$settings['settings']['test_parent_site'] = (object) [
'value' => $this->originalSite,
'required' => TRUE,
);
];
// Add the parent profile's search path to the child site's search paths.
// @see \Drupal\Core\Extension\ExtensionDiscovery::getProfileDirectories()
$settings['conf']['simpletest.settings']['parent_profile'] = (object) array(
$settings['conf']['simpletest.settings']['parent_profile'] = (object) [
'value' => $this->originalProfile,
'required' => TRUE,
);
$settings['settings']['apcu_ensure_unique_prefix'] = (object) array(
];
$settings['settings']['apcu_ensure_unique_prefix'] = (object) [
'value' => FALSE,
'required' => TRUE,
);
];
$this->writeSettings($settings);
// Allow for test-specific overrides.
$settings_testing_file = DRUPAL_ROOT . '/' . $this->originalSite . '/settings.testing.php';
@ -714,15 +749,14 @@ abstract class WebTestBase extends TestBase {
// Since Drupal is bootstrapped already, install_begin_request() will not
// bootstrap again. Hence, we have to reload the newly written custom
// settings.php manually.
$class_loader = require DRUPAL_ROOT . '/autoload.php';
Settings::initialize(DRUPAL_ROOT, $this->siteDirectory, $class_loader);
Settings::initialize(DRUPAL_ROOT, $this->siteDirectory, $this->classLoader);
}
// Execute the non-interactive installer.
require_once DRUPAL_ROOT . '/core/includes/install.core.inc';
install_drupal($class_loader, $parameters);
// Import new settings.php written by the installer.
Settings::initialize(DRUPAL_ROOT, $this->siteDirectory, $class_loader);
/**
* Initialize settings created during install.
*/
protected function initSettings() {
Settings::initialize(DRUPAL_ROOT, $this->siteDirectory, $this->classLoader);
foreach ($GLOBALS['config_directories'] as $type => $path) {
$this->configDirectories[$type] = $path;
}
@ -733,15 +767,16 @@ abstract class WebTestBase extends TestBase {
// 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, $class_loader, '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();
chmod(DRUPAL_ROOT . '/' . $this->siteDirectory, 0777);
}
/**
* Initialize various configurations post-installation.
*
* @param \Symfony\Component\DependencyInjection\ContainerInterface $container
* The container.
*/
protected function initConfig(ContainerInterface $container) {
$config = $container->get('config.factory');
// Manually create and configure private and temporary files directories.
@ -773,34 +808,12 @@ abstract class WebTestBase extends TestBase {
->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);
try {
$success = $container->get('module_installer')->install($modules, TRUE);
$this->assertTrue($success, SafeMarkup::format('Enabled modules: %modules', array('%modules' => implode(', ', $modules))));
}
catch (\Drupal\Core\Extension\MissingDependencyException $e) {
// The exception message has all the details.
$this->fail($e->getMessage());
}
$this->rebuildContainer();
}
// Restore the original Simpletest batch.
$batch = &batch_get();
$batch = $this->originalBatch;
/**
* Reset and rebuild the environment after setup.
*/
protected function rebuildAll() {
// 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.
@ -808,7 +821,7 @@ abstract class WebTestBase extends TestBase {
// @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);
$this->kernel->prepareLegacyRequest(\Drupal::request());
// Explicitly call register() again on the container registered in \Drupal.
// @todo This should already be called through
@ -822,6 +835,9 @@ abstract class WebTestBase extends TestBase {
*
* @see install_drupal()
* @see install_state_defaults()
*
* @return array
* Array of parameters for use in install_drupal().
*/
protected function installParameters() {
$connection_info = Database::getConnectionInfo();
@ -879,6 +895,97 @@ abstract class WebTestBase extends TestBase {
return $parameters;
}
/**
* Preserve the original batch, and instantiate the test batch.
*/
protected function setBatch() {
// When running tests through the Simpletest UI (vs. on the command line),
// Simpletest's batch conflicts with the installer's batch. Batch API does
// not support the concept of nested batches (in which the nested is not
// progressive), so we need to temporarily pretend there was no batch.
// Backup the currently running Simpletest batch.
$this->originalBatch = batch_get();
// Reset the static batch to remove Simpletest's batch operations.
$batch = &batch_get();
$batch = [];
}
/**
* Restore the original batch.
*
* @see ::setBatch
*/
protected function restoreBatch() {
// Restore the original Simpletest batch.
$batch = &batch_get();
$batch = $this->originalBatch;
}
/**
* Initializes user 1 for the site to be installed.
*/
protected function initUserSession() {
// Define information about the user 1 account.
$this->rootUser = new UserSession(array(
'uid' => 1,
'name' => 'admin',
'mail' => 'admin@example.com',
'pass_raw' => $this->randomMachineName(),
));
// The child site derives its session name from the database prefix when
// running web tests.
$this->generateSessionName($this->databasePrefix);
}
/**
* Initializes the kernel after installation.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* Request object.
*
* @return \Symfony\Component\DependencyInjection\ContainerInterface
* The container.
*/
protected function initKernel(Request $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.
return $this->kernel->rebuildContainer();
}
/**
* Install modules defined by `static::$modules`.
*
* @param \Symfony\Component\DependencyInjection\ContainerInterface $container
* The container.
*/
protected function installModulesFromClassProperty(ContainerInterface $container) {
$class = get_class($this);
$modules = [];
while ($class) {
if (property_exists($class, 'modules')) {
$modules = array_merge($modules, $class::$modules);
}
$class = get_parent_class($class);
}
if ($modules) {
$modules = array_unique($modules);
try {
$success = $container->get('module_installer')->install($modules, TRUE);
$this->assertTrue($success, SafeMarkup::format('Enabled modules: %modules', ['%modules' => implode(', ', $modules)]));
}
catch (MissingDependencyException $e) {
// The exception message has all the details.
$this->fail($e->getMessage());
}
$this->rebuildContainer();
}
}
/**
* Returns all supported database driver installer objects.
*

View File

@ -0,0 +1,196 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Update\UpdatePathTestBase.
*/
namespace Drupal\system\Tests\Update;
use Drupal\Component\Utility\Crypt;
use Drupal\Core\Database\Database;
use Drupal\Core\Url;
use Drupal\simpletest\WebTestBase;
use Drupal\user\Entity\User;
use Symfony\Component\HttpFoundation\Request;
/**
* Provides a base class that loads a database as a starting point.
*/
abstract class UpdatePathTestBase extends WebTestBase {
/**
* Modules to enable after the database is loaded.
*/
protected static $modules = [];
/**
* The file path(s) to the dumped database(s) to load into the child site.
*
* @var array
*/
protected $databaseDumpFiles = [];
/**
* The install profile used in the database dump file.
*
* @var string
*/
protected $installProfile = 'standard';
/**
* Flag that indicates whether the child site has been upgraded.
*
* @var bool
*/
protected $upgradedSite = FALSE;
/**
* Array of errors triggered during the upgrade process.
*
* @var array
*/
protected $upgradeErrors = [];
/**
* Array of modules loaded when the test starts.
*
* @var array
*/
protected $loadedModules = [];
/**
* Flag to indicate whether zlib is installed or not.
*
* @var bool
*/
protected $zlibInstalled = TRUE;
/**
* Flag to indicate whether there are pending updates or not.
*
* @var bool
*/
protected $pendingUpdates = TRUE;
/**
* The update URL.
*
* @var string
*/
protected $updateUrl;
/**
* Constructs an UpdatePathTestCase object.
*
* @param $test_id
* (optional) The ID of the test. Tests with the same id are reported
* together.
*/
function __construct($test_id = NULL) {
parent::__construct($test_id);
$this->zlibInstalled = function_exists('gzopen');
// Set the update url.
$this->updateUrl = Url::fromRoute('system.db_update');
}
/**
* Overrides WebTestBase::setUp() for upgrade testing.
*
* The main difference in this method is that rather than performing the
* installation via the installer, a database is loaded. Additional work is
* then needed to set various things such as the config directories and the
* container that would normally be done via the installer.
*/
protected function setUp() {
// We are going to set a missing zlib requirement property for usage
// during the performUpgrade() and tearDown() methods. Also set that the
// tests failed.
if (!$this->zlibInstalled) {
parent::setUp();
return;
}
// These methods are called from parent::setUp().
$this->setBatch();
$this->initUserSession();
$this->prepareSettings();
// Load the database(s).
foreach ($this->databaseDumpFiles as $file) {
if (substr($file, -3) == '.gz') {
$file = "compress.zlib://$file";
}
require $file;
}
$this->initSettings();
$request = Request::createFromGlobals();
$container = $this->initKernel($request);
$this->initConfig($container);
// Add the config directories to settings.php.
drupal_install_config_directories();
// Install any additional modules.
$this->installModulesFromClassProperty($container);
// Restore the original Simpletest batch.
$this->restoreBatch();
// Rebuild and reset.
$this->rebuildAll();
// Replace User 1 with the user created here.
/** @var \Drupal\user\UserInterface $account */
$account = User::load(1);
$account->setPassword($this->rootUser->pass_raw);
$account->setEmail($this->rootUser->getEmail());
$account->setUsername($this->rootUser->getUsername());
$account->save();
}
/**
* Add settings that are missed since the installer isn't run.
*/
protected function prepareSettings() {
parent::prepareSettings();
// Remember the profile which was used.
$settings['settings']['install_profile'] = (object) [
'value' => $this->installProfile,
'required' => TRUE,
];
// Generate a hash salt.
$settings['settings']['hash_salt'] = (object) [
'value' => Crypt::randomBytesBase64(55),
'required' => TRUE,
];
// Since the installer isn't run, add the database settings here too.
$settings['databases']['default'] = (object) [
'value' => Database::getConnectionInfo(),
'required' => TRUE,
];
$this->writeSettings($settings);
}
/**
* Helper function to run pending database updates.
*/
protected function runUpdates() {
if (!$this->zlibInstalled) {
$this->fail('Missing zlib requirement for upgrade tests.');
return FALSE;
}
$this->drupalLogin($this->rootUser);
$this->drupalGet($this->updateUrl);
$this->clickLink(t('Continue'));
// Run the update hooks.
$this->clickLink(t('Apply pending updates'));
}
}

View File

@ -0,0 +1,57 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Update\UpdatePathTestBaseTest.php
*/
namespace Drupal\system\Tests\Update;
use Drupal\Component\Utility\SafeMarkup;
/**
* Tests the update path base class.
*
* @group Update
*/
class UpdatePathTestBaseTest extends UpdatePathTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['update_test_schema'];
/**
* {@inheritdoc}
*/
protected function setUp() {
$this->databaseDumpFiles = [__DIR__ . '/../../../tests/fixtures/update/drupal-8.beta11.bare.standard.php.gz'];
parent::setUp();
}
/**
* Tests that the database was properly loaded.
*/
public function testDatabaseLoaded() {
foreach (['user', 'node', 'system', 'update_test_schema'] as $module) {
$this->assertEqual(drupal_get_installed_schema_version($module), 8000, SafeMarkup::format('Module @module schema is 8000', ['@module' => $module]));
}
$this->assertEqual(\Drupal::config('system.site')->get('name'), 'Site-Install');
$this->drupalGet('<front>');
$this->assertText('Site-Install');
}
/**
* Test that updates are properly run.
*/
public function testUpdateHookN() {
// Increment the schema version.
\Drupal::state()->set('update_test_schema_version', 8001);
$this->runUpdates();
// Ensure schema has changed.
$this->assertEqual(drupal_get_installed_schema_version('update_test_schema', TRUE), 8001);
// Ensure the index was added for column a.
$this->assertTrue(db_index_exists('update_test_schema_table', 'test'), 'Version 8001 of the update_test_schema module is installed.');
}
}

View File

@ -0,0 +1,71 @@
<?php
/**
* Contains \Drupal\system\Tests\Update\UpdateSchemaTest.
*/
namespace Drupal\system\Tests\Update;
use Drupal\Core\Url;
use Drupal\simpletest\WebTestBase;
/**
* Tests that update hooks are properly run.
*
* @group Update
*/
class UpdateSchemaTest extends WebTestBase {
/**
* {@inheritdoc}
*/
public static $modules = ['update_test_schema'];
/**
* @var \Drupal\user\UserInterface
*/
protected $user;
/**
* The update URL.
*
* @var string
*/
protected $updateUrl;
/**
* {@inheritdoc}
*/
public function setUp() {
parent::setUp();
require_once \Drupal::root() . '/core/includes/update.inc';
$this->user = $this->drupalCreateUser(['administer software updates', 'access site in maintenance mode']);
$this->updateUrl = Url::fromRoute('system.db_update');
}
/**
* Tests that update hooks are properly run.
*/
public function testUpdateHooks() {
// Verify that the 8000 schema is in place.
$this->assertEqual(drupal_get_installed_schema_version('update_test_schema'), 8000);
$this->assertFalse(db_index_exists('update_test_schema_table', 'test'), 'Version 8000 of the update_test_schema module is installed.');
// Increment the schema version.
\Drupal::state()->set('update_test_schema_version', 8001);
$this->drupalLogin($this->user);
$this->drupalGet($this->updateUrl, ['external' => TRUE]);
$this->clickLink(t('Continue'));
$this->assertRaw('Schema version 8001.');
// Run the update hooks.
$this->clickLink(t('Apply pending updates'));
// Ensure schema has changed.
$this->assertEqual(drupal_get_installed_schema_version('update_test_schema', TRUE), 8001);
// Ensure the index was added for column a.
$this->assertTrue(db_index_exists('update_test_schema_table', 'test'), 'Version 8001 of the update_test_schema module is installed.');
}
}

View File

@ -0,0 +1,6 @@
name: 'Update test schema'
type: module
description: 'Support module for update testing.'
package: Testing
version: VERSION
core: 8.x

View File

@ -0,0 +1,41 @@
<?php
/**
* @file
* Update hooks and schema definition for the update_test_schema module.
*/
/**
* Implements hook_schema().
*
* The schema defined here will vary on state to allow for update hook testing.
*/
function update_test_schema_schema() {
$schema_version = \Drupal::state()->get('update_test_schema_version', 8000);
$table = [
'fields' => [
'a' => ['type' => 'int', 'not null' => TRUE],
'b' => ['type' => 'blob', 'not null' => FALSE],
],
];
switch ($schema_version) {
case 8001:
// Add the index.
$table['indexes']['test'] = ['a'];
break;
}
return ['update_test_schema_table' => $table];
}
// Update hooks are defined depending on state as well.
$schema_version = \Drupal::state()->get('update_test_schema_version', 8000);
if ($schema_version >= 8001) {
/**
* Schema version 8001.
*/
function update_test_schema_update_8001() {
// Add a column.
db_add_index('update_test_schema_table', 'test', ['a']);
}
}

View File

@ -72,23 +72,23 @@ class ToolbarCacheContextsTest extends WebTestBase {
// Test without user toolbar tab. User module is a required module so we have to
// manually remove the user toolbar tab.
$this->installModules(['toolbar_disable_user_toolbar']);
$this->installExtraModules(['toolbar_disable_user_toolbar']);
$this->assertToolbarCacheContexts(['user.permissions'], 'Expected cache contexts found without user toolbar tab.');
// Test with the toolbar and contextual enabled.
$this->installModules(['contextual']);
$this->installExtraModules(['contextual']);
$this->adminUser2 = $this->drupalCreateUser(array_merge($this->perms, ['access contextual links']));
$this->assertToolbarCacheContexts(['user.permissions'], 'Expected cache contexts found with contextual module enabled.');
\Drupal::service('module_installer')->uninstall(['contextual']);
// Test with the tour module enabled.
$this->installModules(['tour']);
$this->installExtraModules(['tour']);
$this->adminUser2 = $this->drupalCreateUser(array_merge($this->perms, ['access tour']));
$this->assertToolbarCacheContexts(['user.permissions'], 'Expected cache contexts found with tour module enabled.');
\Drupal::service('module_installer')->uninstall(['tour']);
// Test with shortcut module enabled.
$this->installModules(['shortcut']);
$this->installExtraModules(['shortcut']);
$this->adminUser2 = $this->drupalCreateUser(array_merge($this->perms, ['access shortcuts', 'administer shortcuts']));
$this->assertToolbarCacheContexts(['user'], 'Expected cache contexts found with shortcut module enabled.');
}
@ -138,7 +138,7 @@ class ToolbarCacheContextsTest extends WebTestBase {
* @param string[] $module_list
* An array of module names.
*/
protected function installModules(array $module_list) {
protected function installExtraModules(array $module_list) {
\Drupal::service('module_installer')->install($module_list);
// Installing modules updates the container and needs a router rebuild.