annotate core/tests/Drupal/FunctionalTests/Update/UpdatePathTestBase.php @ 17:129ea1e6d783

Update, including to Drupal core 8.6.10
author Chris Cannam
date Thu, 28 Feb 2019 13:21:36 +0000
parents c2387f117808
children af1871eacc83
rev   line source
Chris@0 1 <?php
Chris@0 2
Chris@0 3 namespace Drupal\FunctionalTests\Update;
Chris@0 4
Chris@0 5 use Drupal\Component\Utility\Crypt;
Chris@0 6 use Drupal\Core\Test\TestRunnerKernel;
Chris@0 7 use Drupal\Tests\BrowserTestBase;
Chris@0 8 use Drupal\Tests\SchemaCheckTestTrait;
Chris@0 9 use Drupal\Core\Database\Database;
Chris@0 10 use Drupal\Core\DependencyInjection\ContainerBuilder;
Chris@0 11 use Drupal\Core\Language\Language;
Chris@0 12 use Drupal\Core\Url;
Chris@0 13 use Drupal\user\Entity\User;
Chris@0 14 use Symfony\Component\DependencyInjection\Reference;
Chris@0 15 use Symfony\Component\HttpFoundation\Request;
Chris@0 16
Chris@0 17 /**
Chris@0 18 * Provides a base class for writing an update test.
Chris@0 19 *
Chris@0 20 * To write an update test:
Chris@0 21 * - Write the hook_update_N() implementations that you are testing.
Chris@0 22 * - Create one or more database dump files, which will set the database to the
Chris@0 23 * "before updates" state. Normally, these will add some configuration data to
Chris@0 24 * the database, set up some tables/fields, etc.
Chris@0 25 * - Create a class that extends this class.
Chris@17 26 * - Ensure that the test is in the legacy group. Tests can have multiple
Chris@17 27 * groups.
Chris@0 28 * - In your setUp() method, point the $this->databaseDumpFiles variable to the
Chris@0 29 * database dump files, and then call parent::setUp() to run the base setUp()
Chris@0 30 * method in this class.
Chris@0 31 * - In your test method, call $this->runUpdates() to run the necessary updates,
Chris@0 32 * and then use test assertions to verify that the result is what you expect.
Chris@0 33 * - In order to test both with a "bare" database dump as well as with a
Chris@0 34 * database dump filled with content, extend your update path test class with
Chris@0 35 * a new test class that overrides the bare database dump. Refer to
Chris@0 36 * UpdatePathTestBaseFilledTest for an example.
Chris@0 37 *
Chris@0 38 * @ingroup update_api
Chris@0 39 *
Chris@0 40 * @see hook_update_N()
Chris@0 41 */
Chris@0 42 abstract class UpdatePathTestBase extends BrowserTestBase {
Chris@0 43
Chris@0 44 use SchemaCheckTestTrait;
Chris@0 45
Chris@0 46 /**
Chris@0 47 * Modules to enable after the database is loaded.
Chris@0 48 */
Chris@0 49 protected static $modules = [];
Chris@0 50
Chris@0 51 /**
Chris@0 52 * The file path(s) to the dumped database(s) to load into the child site.
Chris@0 53 *
Chris@0 54 * The file system/tests/fixtures/update/drupal-8.bare.standard.php.gz is
Chris@0 55 * normally included first -- this sets up the base database from a bare
Chris@0 56 * standard Drupal installation.
Chris@0 57 *
Chris@0 58 * The file system/tests/fixtures/update/drupal-8.filled.standard.php.gz
Chris@0 59 * can also be used in case we want to test with a database filled with
Chris@0 60 * content, and with all core modules enabled.
Chris@0 61 *
Chris@0 62 * @var array
Chris@0 63 */
Chris@0 64 protected $databaseDumpFiles = [];
Chris@0 65
Chris@0 66 /**
Chris@0 67 * The install profile used in the database dump file.
Chris@0 68 *
Chris@0 69 * @var string
Chris@0 70 */
Chris@0 71 protected $installProfile = 'standard';
Chris@0 72
Chris@0 73 /**
Chris@0 74 * Flag that indicates whether the child site has been updated.
Chris@0 75 *
Chris@0 76 * @var bool
Chris@0 77 */
Chris@0 78 protected $upgradedSite = FALSE;
Chris@0 79
Chris@0 80 /**
Chris@0 81 * Array of errors triggered during the update process.
Chris@0 82 *
Chris@0 83 * @var array
Chris@0 84 */
Chris@0 85 protected $upgradeErrors = [];
Chris@0 86
Chris@0 87 /**
Chris@0 88 * Array of modules loaded when the test starts.
Chris@0 89 *
Chris@0 90 * @var array
Chris@0 91 */
Chris@0 92 protected $loadedModules = [];
Chris@0 93
Chris@0 94 /**
Chris@0 95 * Flag to indicate whether zlib is installed or not.
Chris@0 96 *
Chris@0 97 * @var bool
Chris@0 98 */
Chris@0 99 protected $zlibInstalled = TRUE;
Chris@0 100
Chris@0 101 /**
Chris@0 102 * Flag to indicate whether there are pending updates or not.
Chris@0 103 *
Chris@0 104 * @var bool
Chris@0 105 */
Chris@0 106 protected $pendingUpdates = TRUE;
Chris@0 107
Chris@0 108 /**
Chris@0 109 * The update URL.
Chris@0 110 *
Chris@0 111 * @var string
Chris@0 112 */
Chris@0 113 protected $updateUrl;
Chris@0 114
Chris@0 115 /**
Chris@0 116 * Disable strict config schema checking.
Chris@0 117 *
Chris@0 118 * The schema is verified at the end of running the update.
Chris@0 119 *
Chris@0 120 * @var bool
Chris@0 121 */
Chris@0 122 protected $strictConfigSchema = FALSE;
Chris@0 123
Chris@0 124 /**
Chris@0 125 * Fail the test if there are failed updates.
Chris@0 126 *
Chris@0 127 * @var bool
Chris@0 128 */
Chris@0 129 protected $checkFailedUpdates = TRUE;
Chris@0 130
Chris@0 131 /**
Chris@0 132 * Constructs an UpdatePathTestCase object.
Chris@0 133 *
Chris@0 134 * @param $test_id
Chris@0 135 * (optional) The ID of the test. Tests with the same id are reported
Chris@0 136 * together.
Chris@0 137 */
Chris@0 138 public function __construct($test_id = NULL) {
Chris@0 139 parent::__construct($test_id);
Chris@0 140 $this->zlibInstalled = function_exists('gzopen');
Chris@0 141 }
Chris@0 142
Chris@0 143 /**
Chris@0 144 * Overrides WebTestBase::setUp() for update testing.
Chris@0 145 *
Chris@0 146 * The main difference in this method is that rather than performing the
Chris@0 147 * installation via the installer, a database is loaded. Additional work is
Chris@0 148 * then needed to set various things such as the config directories and the
Chris@0 149 * container that would normally be done via the installer.
Chris@0 150 */
Chris@0 151 protected function setUp() {
Chris@0 152 $request = Request::createFromGlobals();
Chris@0 153
Chris@0 154 // Boot up Drupal into a state where calling the database API is possible.
Chris@0 155 // This is used to initialize the database system, so we can load the dump
Chris@0 156 // files.
Chris@0 157 $autoloader = require $this->root . '/autoload.php';
Chris@0 158 $kernel = TestRunnerKernel::createFromRequest($request, $autoloader);
Chris@0 159 $kernel->loadLegacyIncludes();
Chris@0 160
Chris@17 161 // Set the update url. This must be set here rather than in
Chris@17 162 // self::__construct() or the old URL generator will leak additional test
Chris@17 163 // sites.
Chris@17 164 $this->updateUrl = Url::fromRoute('system.db_update');
Chris@17 165
Chris@17 166 $this->setupBaseUrl();
Chris@17 167
Chris@17 168 // Install Drupal test site.
Chris@17 169 $this->prepareEnvironment();
Chris@0 170 $this->runDbTasks();
Chris@0 171 // Allow classes to set database dump files.
Chris@0 172 $this->setDatabaseDumpFiles();
Chris@0 173
Chris@0 174 // We are going to set a missing zlib requirement property for usage
Chris@0 175 // during the performUpgrade() and tearDown() methods. Also set that the
Chris@0 176 // tests failed.
Chris@0 177 if (!$this->zlibInstalled) {
Chris@0 178 parent::setUp();
Chris@0 179 return;
Chris@0 180 }
Chris@0 181 $this->installDrupal();
Chris@0 182
Chris@0 183 // Add the config directories to settings.php.
Chris@0 184 drupal_install_config_directories();
Chris@0 185
Chris@0 186 // Set the container. parent::rebuildAll() would normally do this, but this
Chris@0 187 // not safe to do here, because the database has not been updated yet.
Chris@0 188 $this->container = \Drupal::getContainer();
Chris@0 189
Chris@0 190 $this->replaceUser1();
Chris@0 191
Chris@17 192 require_once $this->root . '/core/includes/update.inc';
Chris@0 193
Chris@0 194 // Setup Mink.
Chris@17 195 $this->initMink();
Chris@0 196
Chris@0 197 // Set up the browser test output file.
Chris@0 198 $this->initBrowserOutputFile();
Chris@0 199 }
Chris@0 200
Chris@0 201 /**
Chris@0 202 * {@inheritdoc}
Chris@0 203 */
Chris@0 204 public function installDrupal() {
Chris@0 205 $this->initUserSession();
Chris@0 206 $this->prepareSettings();
Chris@0 207 $this->doInstall();
Chris@0 208 $this->initSettings();
Chris@0 209
Chris@0 210 $request = Request::createFromGlobals();
Chris@0 211 $container = $this->initKernel($request);
Chris@0 212 $this->initConfig($container);
Chris@0 213 }
Chris@0 214
Chris@0 215 /**
Chris@0 216 * {@inheritdoc}
Chris@0 217 */
Chris@0 218 protected function doInstall() {
Chris@0 219 $this->runDbTasks();
Chris@0 220 // Allow classes to set database dump files.
Chris@0 221 $this->setDatabaseDumpFiles();
Chris@0 222
Chris@0 223 // Load the database(s).
Chris@0 224 foreach ($this->databaseDumpFiles as $file) {
Chris@0 225 if (substr($file, -3) == '.gz') {
Chris@0 226 $file = "compress.zlib://$file";
Chris@0 227 }
Chris@0 228 require $file;
Chris@0 229 }
Chris@0 230 }
Chris@0 231
Chris@0 232 /**
Chris@0 233 * {@inheritdoc}
Chris@0 234 */
Chris@17 235 protected function initFrontPage() {
Chris@17 236 // Do nothing as Drupal is not installed yet.
Chris@0 237 }
Chris@0 238
Chris@0 239 /**
Chris@0 240 * Set database dump files to be used.
Chris@0 241 */
Chris@0 242 abstract protected function setDatabaseDumpFiles();
Chris@0 243
Chris@0 244 /**
Chris@0 245 * Add settings that are missed since the installer isn't run.
Chris@0 246 */
Chris@0 247 protected function prepareSettings() {
Chris@0 248 parent::prepareSettings();
Chris@0 249
Chris@0 250 // Remember the profile which was used.
Chris@0 251 $settings['settings']['install_profile'] = (object) [
Chris@0 252 'value' => $this->installProfile,
Chris@0 253 'required' => TRUE,
Chris@0 254 ];
Chris@0 255 // Generate a hash salt.
Chris@0 256 $settings['settings']['hash_salt'] = (object) [
Chris@0 257 'value' => Crypt::randomBytesBase64(55),
Chris@0 258 'required' => TRUE,
Chris@0 259 ];
Chris@0 260
Chris@0 261 // Since the installer isn't run, add the database settings here too.
Chris@0 262 $settings['databases']['default'] = (object) [
Chris@0 263 'value' => Database::getConnectionInfo(),
Chris@0 264 'required' => TRUE,
Chris@0 265 ];
Chris@0 266
Chris@0 267 $this->writeSettings($settings);
Chris@0 268 }
Chris@0 269
Chris@0 270 /**
Chris@0 271 * Helper function to run pending database updates.
Chris@0 272 */
Chris@0 273 protected function runUpdates() {
Chris@0 274 if (!$this->zlibInstalled) {
Chris@0 275 $this->fail('Missing zlib requirement for update tests.');
Chris@0 276 return FALSE;
Chris@0 277 }
Chris@0 278 // The site might be broken at the time so logging in using the UI might
Chris@0 279 // not work, so we use the API itself.
Chris@0 280 drupal_rewrite_settings([
Chris@0 281 'settings' => [
Chris@0 282 'update_free_access' => (object) [
Chris@0 283 'value' => TRUE,
Chris@0 284 'required' => TRUE,
Chris@0 285 ],
Chris@0 286 ],
Chris@0 287 ]);
Chris@0 288
Chris@0 289 $this->drupalGet($this->updateUrl);
Chris@0 290 $this->clickLink(t('Continue'));
Chris@0 291
Chris@0 292 $this->doSelectionTest();
Chris@0 293 // Run the update hooks.
Chris@0 294 $this->clickLink(t('Apply pending updates'));
Chris@0 295 $this->checkForMetaRefresh();
Chris@0 296
Chris@0 297 // Ensure there are no failed updates.
Chris@0 298 if ($this->checkFailedUpdates) {
Chris@17 299 $failure = $this->cssSelect('.failure');
Chris@17 300 if ($failure) {
Chris@17 301 $this->fail('The update failed with the following message: "' . reset($failure)->getText() . '"');
Chris@17 302 }
Chris@0 303
Chris@0 304 // Ensure that there are no pending updates.
Chris@0 305 foreach (['update', 'post_update'] as $update_type) {
Chris@0 306 switch ($update_type) {
Chris@0 307 case 'update':
Chris@0 308 $all_updates = update_get_update_list();
Chris@0 309 break;
Chris@0 310 case 'post_update':
Chris@0 311 $all_updates = \Drupal::service('update.post_update_registry')->getPendingUpdateInformation();
Chris@0 312 break;
Chris@0 313 }
Chris@0 314 foreach ($all_updates as $module => $updates) {
Chris@0 315 if (!empty($updates['pending'])) {
Chris@0 316 foreach (array_keys($updates['pending']) as $update_name) {
Chris@0 317 $this->fail("The $update_name() update function from the $module module did not run.");
Chris@0 318 }
Chris@0 319 }
Chris@0 320 }
Chris@0 321 }
Chris@0 322 // Reset the static cache of drupal_get_installed_schema_version() so that
Chris@0 323 // more complex update path testing works.
Chris@0 324 drupal_static_reset('drupal_get_installed_schema_version');
Chris@0 325
Chris@0 326 // The config schema can be incorrect while the update functions are being
Chris@0 327 // executed. But once the update has been completed, it needs to be valid
Chris@0 328 // again. Assert the schema of all configuration objects now.
Chris@0 329 $names = $this->container->get('config.storage')->listAll();
Chris@0 330 /** @var \Drupal\Core\Config\TypedConfigManagerInterface $typed_config */
Chris@0 331 $typed_config = $this->container->get('config.typed');
Chris@0 332 $typed_config->clearCachedDefinitions();
Chris@0 333 foreach ($names as $name) {
Chris@0 334 $config = $this->config($name);
Chris@0 335 $this->assertConfigSchema($typed_config, $name, $config->get());
Chris@0 336 }
Chris@0 337
Chris@0 338 // Ensure that the update hooks updated all entity schema.
Chris@0 339 $needs_updates = \Drupal::entityDefinitionUpdateManager()->needsUpdates();
Chris@0 340 if ($needs_updates) {
Chris@16 341 foreach (\Drupal::entityDefinitionUpdateManager()->getChangeSummary() as $entity_type_id => $summary) {
Chris@16 342 $entity_type_label = \Drupal::entityTypeManager()->getDefinition($entity_type_id)->getLabel();
Chris@0 343 foreach ($summary as $message) {
Chris@16 344 $this->fail("$entity_type_label: $message");
Chris@0 345 }
Chris@0 346 }
Chris@14 347 // The above calls to `fail()` should prevent this from ever being
Chris@14 348 // called, but it is here in case something goes really wrong.
Chris@14 349 $this->assertFalse($needs_updates, 'After all updates ran, entity schema is up to date.');
Chris@0 350 }
Chris@0 351 }
Chris@0 352 }
Chris@0 353
Chris@0 354 /**
Chris@0 355 * Runs the install database tasks for the driver used by the test runner.
Chris@0 356 */
Chris@0 357 protected function runDbTasks() {
Chris@0 358 // Create a minimal container so that t() works.
Chris@0 359 // @see install_begin_request()
Chris@0 360 $container = new ContainerBuilder();
Chris@0 361 $container->setParameter('language.default_values', Language::$defaultValues);
Chris@0 362 $container
Chris@0 363 ->register('language.default', 'Drupal\Core\Language\LanguageDefault')
Chris@0 364 ->addArgument('%language.default_values%');
Chris@0 365 $container
Chris@0 366 ->register('string_translation', 'Drupal\Core\StringTranslation\TranslationManager')
Chris@0 367 ->addArgument(new Reference('language.default'));
Chris@0 368 \Drupal::setContainer($container);
Chris@0 369
Chris@0 370 require_once __DIR__ . '/../../../../includes/install.inc';
Chris@0 371 $connection = Database::getConnection();
Chris@0 372 $errors = db_installer_object($connection->driver())->runTasks();
Chris@0 373 if (!empty($errors)) {
Chris@0 374 $this->fail('Failed to run installer database tasks: ' . implode(', ', $errors));
Chris@0 375 }
Chris@0 376 }
Chris@0 377
Chris@0 378 /**
Chris@0 379 * Replace User 1 with the user created here.
Chris@0 380 */
Chris@0 381 protected function replaceUser1() {
Chris@0 382 /** @var \Drupal\user\UserInterface $account */
Chris@0 383 // @todo: Saving the account before the update is problematic.
Chris@0 384 // https://www.drupal.org/node/2560237
Chris@0 385 $account = User::load(1);
Chris@0 386 $account->setPassword($this->rootUser->pass_raw);
Chris@0 387 $account->setEmail($this->rootUser->getEmail());
Chris@0 388 $account->setUsername($this->rootUser->getUsername());
Chris@0 389 $account->save();
Chris@0 390 }
Chris@0 391
Chris@0 392 /**
Chris@0 393 * Tests the selection page.
Chris@0 394 */
Chris@0 395 protected function doSelectionTest() {
Chris@0 396 // No-op. Tests wishing to do test the selection page or the general
Chris@0 397 // update.php environment before running update.php can override this method
Chris@0 398 // and implement their required tests.
Chris@0 399 }
Chris@0 400
Chris@0 401 }