annotate core/tests/Drupal/FunctionalTests/Update/UpdatePathTestBase.php @ 19:fa3358dc1485 tip

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