WebTestBase.php 109 KB
Newer Older
1 2 3
<?php

/**
4
 * @file
5
 * Contains \Drupal\simpletest\WebTestBase.
6
 */
7

8
namespace Drupal\simpletest;
9

10
use Drupal\block\Entity\Block;
11
use Drupal\Component\FileCache\FileCacheFactory;
12
use Drupal\Component\Serialization\Json;
13
use Drupal\Component\Serialization\Yaml;
14
use Drupal\Component\Utility\Html;
15
use Drupal\Component\Utility\NestedArray;
16
use Drupal\Component\Utility\UrlHelper;
17
use Drupal\Core\Cache\Cache;
18
use Drupal\Component\Utility\SafeMarkup;
19
use Drupal\Core\Database\Database;
20
use Drupal\Core\DrupalKernel;
21
use Drupal\Core\Entity\EntityInterface;
22
use Drupal\Core\EventSubscriber\AjaxResponseSubscriber;
23
use Drupal\Core\EventSubscriber\MainContentViewSubscriber;
24
use Drupal\Core\Extension\MissingDependencyException;
25
use Drupal\Core\Render\Element;
26
use Drupal\Core\Session\AccountInterface;
27
use Drupal\Core\Session\AnonymousUserSession;
28
use Drupal\Core\Session\UserSession;
29
use Drupal\Core\Site\Settings;
30
use Drupal\Core\StreamWrapper\PublicStream;
31
use Drupal\Core\Url;
32
use Drupal\node\Entity\NodeType;
33
use Symfony\Component\DependencyInjection\ContainerInterface;
34
use Symfony\Component\HttpFoundation\Request;
35
use Zend\Diactoros\Uri;
36 37 38

/**
 * Test case for typical Drupal tests.
39 40
 *
 * @ingroup testing
41
 */
42 43
abstract class WebTestBase extends TestBase {

44 45
  use AssertContentTrait;

46 47 48 49 50 51
  use UserCreationTrait {
    createUser as drupalCreateUser;
    createRole as drupalCreateRole;
    createAdminRole as drupalCreateAdminRole;
  }

52 53 54 55 56
  /**
   * The profile to install as a basis for testing.
   *
   * @var string
   */
57
  protected $profile = 'testing';
58

59 60 61 62 63 64 65 66 67 68 69 70 71 72
  /**
   * The URL currently loaded in the internal browser.
   *
   * @var string
   */
  protected $url;

  /**
   * The handle of the current cURL connection.
   *
   * @var resource
   */
  protected $curlHandle;

73 74 75 76 77 78 79
  /**
   * Whether or not to assert the presence of the X-Drupal-Ajax-Token.
   *
   * @var bool
   */
  protected $assertAjaxHeader = TRUE;

80 81 82 83 84 85 86
  /**
   * The headers of the page currently loaded in the internal browser.
   *
   * @var Array
   */
  protected $headers;

87 88 89 90 91
  /**
   * The cookies of the page currently loaded in the internal browser.
   *
   * @var array
   */
92
  protected $cookies = array();
93

94 95 96 97
  /**
   * Indicates that headers should be dumped if verbose output is enabled.
   *
   * Headers are dumped to verbose by drupalGet(), drupalHead(), and
98
   * drupalPostForm().
99 100 101 102 103
   *
   * @var bool
   */
  protected $dumpHeaders = FALSE;

104 105 106
  /**
   * The current user logged in using the internal browser.
   *
107
   * @var \Drupal\Core\Session\AccountInterface|bool
108 109 110
   */
  protected $loggedInUser = FALSE;

111 112 113 114 115 116 117 118
  /**
   * The "#1" admin user.
   *
   * @var \Drupal\Core\Session\AccountInterface
   */
  protected $rootUser;


119 120 121 122 123 124 125 126 127 128 129
  /**
   * The current cookie file used by cURL.
   *
   * We do not reuse the cookies in further runs, so we do not need a file
   * but we still need cookie handling, so we set the jar to NULL.
   */
  protected $cookieFile = NULL;

  /**
   * Additional cURL options.
   *
130 131
   * \Drupal\simpletest\WebTestBase itself never sets this but always obeys what
   * is set.
132 133 134
   */
  protected $additionalCurlOptions = array();

135 136 137 138 139 140 141
  /**
   * The original batch, before it was changed for testing purposes.
   *
   * @var array
   */
  protected $originalBatch;

142
  /**
143
   * The original user, before it was changed to a clean uid = 1 for testing.
144 145 146 147 148
   *
   * @var object
   */
  protected $originalUser = NULL;

149
  /**
150
   * The original shutdown handlers array, before it was cleaned for testing.
151 152 153 154 155
   *
   * @var array
   */
  protected $originalShutdownCallbacks = array();

156 157 158
  /**
   * The current session ID, if available.
   */
159
  protected $sessionId = NULL;
160

161 162 163 164 165
  /**
   * Whether the files were copied to the test files directory.
   */
  protected $generatedTestFiles = FALSE;

166 167 168 169 170
  /**
   * The maximum number of redirects to follow when handling responses.
   */
  protected $maximumRedirects = 5;

171 172 173
  /**
   * The number of redirects followed during the handling of a request.
   */
174
  protected $redirectCount;
175

176 177 178 179 180 181 182 183 184 185 186 187 188 189 190

  /**
   * The number of meta refresh redirects to follow, or NULL if unlimited.
   *
   * @var null|int
   */
  protected $maximumMetaRefreshCount = NULL;

  /**
   * The number of meta refresh redirects followed during ::drupalGet().
   *
   * @var int
   */
  protected $metaRefreshCount = 0;

191 192
  /**
   * The kernel used in this test.
193 194
   *
   * @var \Drupal\Core\DrupalKernel
195 196 197
   */
  protected $kernel;

198 199 200 201 202
  /**
   * The config directories used in this test.
   */
  protected $configDirectories = array();

203 204 205 206 207 208 209
  /**
   * Cookies to set on curl requests.
   *
   * @var array
   */
  protected $curlCookies = array();

210 211 212 213 214 215 216
  /**
   * An array of custom translations suitable for drupal_rewrite_settings().
   *
   * @var array
   */
  protected $customTranslations;

217 218 219 220 221 222 223
  /**
   * The class loader to use for installation and initialization of setup.
   *
   * @var \Symfony\Component\Classloader\Classloader
   */
  protected $classLoader;

224
  /**
225
   * Constructor for \Drupal\simpletest\WebTestBase.
226 227 228 229
   */
  function __construct($test_id = NULL) {
    parent::__construct($test_id);
    $this->skipClasses[__CLASS__] = TRUE;
230
    $this->classLoader = require DRUPAL_ROOT . '/autoload.php';
231 232
  }

233 234 235
  /**
   * Get a node from the database based on its title.
   *
236
   * @param string|\Drupal\Component\Render\MarkupInterface $title
237
   *   A node title, usually generated by $this->randomMachineName().
238
   * @param $reset
239
   *   (optional) Whether to reset the entity cache.
240
   *
241
   * @return \Drupal\node\NodeInterface
242
   *   A node entity matching $title.
243
   */
244
  function drupalGetNodeByTitle($title, $reset = FALSE) {
245
    if ($reset) {
246
      \Drupal::entityManager()->getStorage('node')->resetCache();
247
    }
248
    // Cast MarkupInterface objects to string.
249
    $title = (string) $title;
250
    $nodes = entity_load_multiple_by_properties('node', array('title' => $title));
251 252 253 254 255
    // Load the first node returned from the database.
    $returned_node = reset($nodes);
    return $returned_node;
  }

256 257 258
  /**
   * Creates a node based on default settings.
   *
259 260 261 262 263 264 265 266 267 268 269 270 271
   * @param array $settings
   *   (optional) An associative array of settings for the node, as used in
   *   entity_create(). Override the defaults by specifying the key and value
   *   in the array, for example:
   *   @code
   *     $this->drupalCreateNode(array(
   *       'title' => t('Hello, world!'),
   *       'type' => 'article',
   *     ));
   *   @endcode
   *   The following defaults are provided:
   *   - body: Random string using the default filter format:
   *     @code
272
   *       $settings['body'][0] = array(
273
   *         'value' => $this->randomMachineName(32),
274 275 276 277 278
   *         'format' => filter_default_format(),
   *       );
   *     @endcode
   *   - title: Random string.
   *   - type: 'page'.
279
   *   - uid: The currently logged in user, or anonymous.
280
   *
281
   * @return \Drupal\node\NodeInterface
282 283 284
   *   The created node entity.
   */
  protected function drupalCreateNode(array $settings = array()) {
285
    // Populate defaults array.
286
    $settings += array(
287 288 289 290
      'body'      => array(array(
        'value' => $this->randomMachineName(32),
        'format' => filter_default_format(),
      )),
291
      'title'     => $this->randomMachineName(8),
292
      'type'      => 'page',
293
      'uid'       => \Drupal::currentUser()->id(),
294
    );
295 296
    $node = entity_create('node', $settings);
    $node->save();
297 298 299 300 301 302 303

    return $node;
  }

  /**
   * Creates a custom content type based on default settings.
   *
304
   * @param array $values
305 306
   *   An array of settings to change from the defaults.
   *   Example: 'type' => 'foo'.
307
   *
308
   * @return \Drupal\node\Entity\NodeType
309
   *   Created content type.
310
   */
311
  protected function drupalCreateContentType(array $values = array()) {
312
    // Find a non-existent random type name.
313 314
    if (!isset($values['type'])) {
      do {
315
        $id = strtolower($this->randomMachineName(8));
316
      } while (NodeType::load($id));
317 318 319 320 321 322 323
    }
    else {
      $id = $values['type'];
    }
    $values += array(
      'type' => $id,
      'name' => $id,
324
    );
325 326
    $type = entity_create('node_type', $values);
    $status = $type->save();
327
    node_add_body_field($type);
328
    \Drupal::service('router.builder')->rebuild();
329

330
    $this->assertEqual($status, SAVED_NEW, SafeMarkup::format('Created content type %type.', array('%type' => $type->id())));
331

332 333 334
    return $type;
  }

335 336 337 338 339
  /**
   * Builds the renderable view of an entity.
   *
   * Entities postpone the composition of their renderable arrays to #pre_render
   * functions in order to maximize cache efficacy. This means that the full
340 341
   * renderable array for an entity is constructed in drupal_render(). Some
   * tests require the complete renderable array for an entity outside of the
342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359
   * drupal_render process in order to verify the presence of specific values.
   * This method isolates the steps in the render process that produce an
   * entity's renderable array.
   *
   * @param \Drupal\Core\Entity\EntityInterface $entity
   *   The entity to prepare a renderable array for.
   * @param string $view_mode
   *   (optional) The view mode that should be used to build the entity.
   * @param null $langcode
   *   (optional) For which language the entity should be prepared, defaults to
   *   the current content language.
   * @param bool $reset
   *   (optional) Whether to clear the cache for this entity.
   * @return array
   *
   * @see drupal_render()
   */
  protected function drupalBuildEntityView(EntityInterface $entity, $view_mode = 'full', $langcode = NULL, $reset = FALSE) {
360 361 362 363
    $ensure_fully_built = function(&$elements) use (&$ensure_fully_built) {
      // If the default values for this element have not been loaded yet, populate
      // them.
      if (isset($elements['#type']) && empty($elements['#defaults_loaded'])) {
364
        $elements += \Drupal::service('element_info')->getInfo($elements['#type']);
365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382
      }

      // Make any final changes to the element before it is rendered. This means
      // that the $element or the children can be altered or corrected before the
      // element is rendered into the final text.
      if (isset($elements['#pre_render'])) {
        foreach ($elements['#pre_render'] as $callable) {
          $elements = call_user_func($callable, $elements);
        }
      }

      // And recurse.
      $children = Element::children($elements, TRUE);
      foreach ($children as $key) {
        $ensure_fully_built($elements[$key]);
      }
    };

383 384 385 386
    $render_controller = $this->container->get('entity.manager')->getViewBuilder($entity->getEntityTypeId());
    if ($reset) {
      $render_controller->resetCache(array($entity->id()));
    }
387 388
    $build = $render_controller->view($entity, $view_mode, $langcode);
    $ensure_fully_built($build);
389

390
    return $build;
391 392
  }

393 394 395 396 397
  /**
   * Creates a block instance based on default settings.
   *
   * @param string $plugin_id
   *   The plugin ID of the block type for this block instance.
398 399
   * @param array $settings
   *   (optional) An associative array of settings for the block entity.
400
   *   Override the defaults by specifying the key and value in the array, for
401 402 403
   *   example:
   *   @code
   *     $this->drupalPlaceBlock('system_powered_by_block', array(
404
   *       'label' => t('Hello, world!'),
405 406 407
   *     ));
   *   @endcode
   *   The following defaults are provided:
408
   *   - label: Random string.
409
   *   - ID: Random string.
410
   *   - region: 'sidebar_first'.
411
   *   - theme: The default theme.
412
   *   - visibility: Empty array.
413
   *
414
   * @return \Drupal\block\Entity\Block
415
   *   The block entity.
416 417 418 419
   *
   * @todo
   *   Add support for creating custom block instances.
   */
420 421
  protected function drupalPlaceBlock($plugin_id, array $settings = array()) {
    $settings += array(
422
      'plugin' => $plugin_id,
423
      'region' => 'sidebar_first',
424
      'id' => strtolower($this->randomMachineName(8)),
425
      'theme' => $this->config('system.theme')->get('default'),
426
      'label' => $this->randomMachineName(8),
427
      'visibility' => array(),
428
      'weight' => 0,
429
    );
430 431
    $values = [];
    foreach (array('region', 'id', 'theme', 'plugin', 'weight', 'visibility') as $key) {
432
      $values[$key] = $settings[$key];
433
      // Remove extra values that do not belong in the settings array.
434 435
      unset($settings[$key]);
    }
436 437
    foreach ($values['visibility'] as $id => $visibility) {
      $values['visibility'][$id]['id'] = $id;
438
    }
439
    $values['settings'] = $settings;
440 441 442
    $block = entity_create('block', $values);
    $block->save();
    return $block;
443 444
  }

445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479
  /**
   * Checks to see whether a block appears on the page.
   *
   * @param \Drupal\block\Entity\Block $block
   *   The block entity to find on the page.
   */
  protected function assertBlockAppears(Block $block) {
    $result = $this->findBlockInstance($block);
    $this->assertTrue(!empty($result), format_string('Ensure the block @id appears on the page', array('@id' => $block->id())));
  }

  /**
   * Checks to see whether a block does not appears on the page.
   *
   * @param \Drupal\block\Entity\Block $block
   *   The block entity to find on the page.
   */
  protected function assertNoBlockAppears(Block $block) {
    $result = $this->findBlockInstance($block);
    $this->assertFalse(!empty($result), format_string('Ensure the block @id does not appear on the page', array('@id' => $block->id())));
  }

  /**
   * Find a block instance on the page.
   *
   * @param \Drupal\block\Entity\Block $block
   *   The block entity to find on the page.
   *
   * @return array
   *   The result from the xpath query.
   */
  protected function findBlockInstance(Block $block) {
    return $this->xpath('//div[@id = :id]', array(':id' => 'block-' . $block->id()));
  }

480
  /**
481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501
   * Gets a list of files that can be used in tests.
   *
   * The first time this method is called, it will call
   * simpletest_generate_file() to generate binary and ASCII text files in the
   * public:// directory. It will also copy all files in
   * core/modules/simpletest/files to public://. These contain image, SQL, PHP,
   * JavaScript, and HTML files.
   *
   * All filenames are prefixed with their type and have appropriate extensions:
   * - text-*.txt
   * - binary-*.txt
   * - html-*.html and html-*.txt
   * - image-*.png, image-*.jpg, and image-*.gif
   * - javascript-*.txt and javascript-*.script
   * - php-*.txt and php-*.php
   * - sql-*.txt and sql-*.sql
   *
   * Any subsequent calls will not generate any new files, or copy the files
   * over again. However, if a test class adds a new file to public:// that
   * is prefixed with one of the above types, it will get returned as well, even
   * on subsequent calls.
502
   *
503
   * @param $type
504 505
   *   File type, possible values: 'binary', 'html', 'image', 'javascript',
   *   'php', 'sql', 'text'.
506
   * @param $size
507 508
   *   (optional) File size in bytes to match. Defaults to NULL, which will not
   *   filter the returned list by size.
509
   *
510
   * @return
511
   *   List of files in public:// that match the filter(s).
512
   */
513
  protected function drupalGetTestFiles($type, $size = NULL) {
514 515 516 517 518 519 520 521
    if (empty($this->generatedTestFiles)) {
      // Generate binary test files.
      $lines = array(64, 1024);
      $count = 0;
      foreach ($lines as $line) {
        simpletest_generate_file('binary-' . $count++, 64, $line, 'binary');
      }

522
      // Generate ASCII text test files.
523 524 525
      $lines = array(16, 256, 1024, 2048, 20480);
      $count = 0;
      foreach ($lines as $line) {
526
        simpletest_generate_file('text-' . $count++, 64, $line, 'text');
527 528 529 530 531 532
      }

      // Copy other test files from simpletest.
      $original = drupal_get_path('module', 'simpletest') . '/files';
      $files = file_scan_directory($original, '/(html|image|javascript|php|sql)-.*/');
      foreach ($files as $file) {
533
        file_unmanaged_copy($file->uri, PublicStream::basePath());
534
      }
535

536 537 538 539
      $this->generatedTestFiles = TRUE;
    }

    $files = array();
540 541
    // Make sure type is valid.
    if (in_array($type, array('binary', 'html', 'image', 'javascript', 'php', 'sql', 'text'))) {
542
      $files = file_scan_directory('public://', '/' . $type . '\-.*/');
543 544 545 546

      // If size is set then remove any files that are not of that size.
      if ($size !== NULL) {
        foreach ($files as $file) {
547
          $stats = stat($file->uri);
548
          if ($stats['size'] != $size) {
549
            unset($files[$file->uri]);
550 551 552 553 554 555 556 557 558
          }
        }
      }
    }
    usort($files, array($this, 'drupalCompareFiles'));
    return $files;
  }

  /**
559
   * Compare two files based on size and file name.
560
   */
561
  protected function drupalCompareFiles($file1, $file2) {
562
    $compare_size = filesize($file1->uri) - filesize($file2->uri);
563 564 565
    if ($compare_size) {
      // Sort by file size.
      return $compare_size;
566 567
    }
    else {
568 569
      // The files were the same size, so sort alphabetically.
      return strnatcmp($file1->name, $file2->name);
570 571 572 573
    }
  }

  /**
574 575 576 577 578
   * Log in a user with the internal browser.
   *
   * If a user is already logged in, then the current user is logged out before
   * logging in the specified user.
   *
579
   * Please note that neither the current user nor the passed-in user object is
580 581 582
   * 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()),
583
   * e.g. to log in the same user again, then it must be re-assigned manually.
584 585 586 587 588 589 590
   * For example:
   * @code
   *   // Create a user.
   *   $account = $this->drupalCreateUser(array());
   *   $this->drupalLogin($account);
   *   // Load real user object.
   *   $pass_raw = $account->pass_raw;
591
   *   $account = User::load($account->id());
592 593
   *   $account->pass_raw = $pass_raw;
   * @endcode
594
   *
595
   * @param \Drupal\Core\Session\AccountInterface $account
596
   *   User object representing the user to log in.
597 598
   *
   * @see drupalCreateUser()
599
   */
600
  protected function drupalLogin(AccountInterface $account) {
601
    if ($this->loggedInUser) {
602 603 604 605
      $this->drupalLogout();
    }

    $edit = array(
606
      'name' => $account->getUsername(),
607
      'pass' => $account->pass_raw
608
    );
609
    $this->drupalPostForm('user/login', $edit, t('Log in'));
610

611
    // @see WebTestBase::drupalUserIsLoggedIn()
612 613
    if (isset($this->sessionId)) {
      $account->session_id = $this->sessionId;
614
    }
615
    $pass = $this->assert($this->drupalUserIsLoggedIn($account), format_string('User %name successfully logged in.', array('%name' => $account->getUsername())), 'User login');
616
    if ($pass) {
617
      $this->loggedInUser = $account;
618
      $this->container->get('current_user')->setAccount($account);
619
    }
620 621
  }

622 623 624
  /**
   * Returns whether a given user account is logged in.
   *
625
   * @param \Drupal\user\UserInterface $account
626 627 628
   *   The user account object to check.
   */
  protected function drupalUserIsLoggedIn($account) {
629 630 631 632 633
    $logged_in = FALSE;

    if (isset($account->session_id)) {
      $session_handler = $this->container->get('session_handler.storage');
      $logged_in = (bool) $session_handler->read($account->session_id);
634
    }
635

636
    return $logged_in;
637 638
  }

639 640 641 642
  /**
   * Logs a user out of the internal browser and confirms.
   *
   * Confirms logout by checking the login page.
643
   */
644
  protected function drupalLogout() {
645 646 647
    // 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.
648
    $this->drupalGet('user/logout', array('query' => array('destination' => 'user/login')));
649 650 651
    $this->assertResponse(200, 'User was logged out.');
    $pass = $this->assertField('name', 'Username field found.', 'Logout');
    $pass = $pass && $this->assertField('pass', 'Password field found.', 'Logout');
652

653
    if ($pass) {
654 655
      // @see WebTestBase::drupalUserIsLoggedIn()
      unset($this->loggedInUser->session_id);
656
      $this->loggedInUser = FALSE;
657
      $this->container->get('current_user')->setAccount(new AnonymousUserSession());
658
    }
659 660
  }

661 662 663
  /**
   * Sets up a Drupal site for running functional and integration tests.
   *
664 665
   * Installs Drupal with the installation profile specified in
   * \Drupal\simpletest\WebTestBase::$profile into the prefixed database.
666
   *
667 668 669
   * Afterwards, installs any additional modules specified in the static
   * \Drupal\simpletest\WebTestBase::$modules property of each class in the
   * class hierarchy.
670 671 672 673 674 675 676
   *
   * After installation all caches are flushed and several configuration values
   * are reset to the values of the parent site executing the test, since the
   * default values may be incompatible with the environment in which tests are
   * being executed.
   */
  protected function setUp() {
677 678
    // Preserve original batch for later restoration.
    $this->setBatch();
679

680 681
    // Initialize user 1 and session name.
    $this->initUserSession();
682

683 684 685
    // Get parameters for install_drupal() before removing global variables.
    $parameters = $this->installParameters();

686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727
    // 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() {
728 729 730 731
    // 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.
732 733
    $directory = DRUPAL_ROOT . '/' . $this->siteDirectory;
    copy(DRUPAL_ROOT . '/sites/default/default.settings.php', $directory . '/settings.php');
734 735 736 737

    // All file system paths are created by System module during installation.
    // @see system_requirements()
    // @see TestBase::prepareEnvironment()
738
    $settings['settings']['file_public_path'] = (object) [
739
      'value' => $this->publicFilesDirectory,
740
      'required' => TRUE,
741 742
    ];
    $settings['settings']['file_private_path'] = (object) [
743
      'value' => $this->privateFilesDirectory,
744
      'required' => TRUE,
745
    ];
746 747 748
    // Save the original site directory path, so that extensions in the
    // site-specific directory can still be discovered in the test site
    // environment.
749
    // @see \Drupal\Core\Extension\ExtensionDiscovery::scan()
750
    $settings['settings']['test_parent_site'] = (object) [
751 752
      'value' => $this->originalSite,
      'required' => TRUE,
753
    ];
754
    // Add the parent profile's search path to the child site's search paths.
755
    // @see \Drupal\Core\Extension\ExtensionDiscovery::getProfileDirectories()
756
    $settings['conf']['simpletest.settings']['parent_profile'] = (object) [
757 758
      'value' => $this->originalProfile,
      'required' => TRUE,
759 760
    ];
    $settings['settings']['apcu_ensure_unique_prefix'] = (object) [
761 762
      'value' => FALSE,
      'required' => TRUE,
763
    ];
764
    $this->writeSettings($settings);
765 766 767 768 769 770 771
    // Allow for test-specific overrides.
    $settings_testing_file = DRUPAL_ROOT . '/' . $this->originalSite . '/settings.testing.php';
    if (file_exists($settings_testing_file)) {
      // Copy the testing-specific settings.php overrides in place.
      copy($settings_testing_file, $directory . '/settings.testing.php');
      // Add the name of the testing class to settings.php and include the
      // testing specific overrides
772
      file_put_contents($directory . '/settings.php', "\n\$test_class = '" . get_class($this) ."';\n" . 'include DRUPAL_ROOT . \'/\' . $site_path . \'/settings.testing.php\';' ."\n", FILE_APPEND);
773 774
    }
    $settings_services_file = DRUPAL_ROOT . '/' . $this->originalSite . '/testing.services.yml';
775 776 777
    if (!file_exists($settings_services_file)) {
      // Otherwise, use the default services as a starting point for overrides.
      $settings_services_file = DRUPAL_ROOT . '/sites/default/default.services.yml';
778
    }
779 780
    // Copy the testing-specific service overrides in place.
    copy($settings_services_file, $directory . '/services.yml');
781 782 783
    if ($this->strictConfigSchema) {
      // Add a listener to validate configuration schema on save.
      $yaml = new \Symfony\Component\Yaml\Yaml();
784 785
      $content = file_get_contents($directory . '/services.yml');
      $services = $yaml->parse($content);
786 787
      $services['services']['simpletest.config_schema_checker'] = [
        'class' => 'Drupal\Core\Config\Testing\ConfigSchemaChecker',
788
        'arguments' => ['@config.typed', $this->getConfigSchemaExclusions()],
789 790 791 792
        'tags' => [['name' => 'event_subscriber']]
      ];
      file_put_contents($directory . '/services.yml', $yaml->dump($services));
    }
793
    // Since Drupal is bootstrapped already, install_begin_request() will not
794 795
    // bootstrap again. Hence, we have to reload the newly written custom
    // settings.php manually.
796 797
    Settings::initialize(DRUPAL_ROOT, $this->siteDirectory, $this->classLoader);
  }
798

799 800 801 802 803
  /**
   * Initialize settings created during install.
   */
  protected function initSettings() {
    Settings::initialize(DRUPAL_ROOT, $this->siteDirectory, $this->classLoader);
804 805 806 807 808 809 810 811 812 813
    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.
814 815
    chmod(DRUPAL_ROOT . '/' . $this->siteDirectory, 0777);
  }
816

817 818 819 820 821 822 823
  /**
   * Initialize various configurations post-installation.
   *
   * @param \Symfony\Component\DependencyInjection\ContainerInterface $container
   *   The container.
   */
  protected function initConfig(ContainerInterface $container) {
824
    $config = $container->get('config.factory');
825

826 827 828 829
    // 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.
830 831
    file_prepare_directory($this->privateFilesDirectory, FILE_CREATE_DIRECTORY);
    file_prepare_directory($this->tempFilesDirectory, FILE_CREATE_DIRECTORY);
832
    $config->getEditable('system.file')
833
      ->set('path.temporary', $this->tempFilesDirectory)
834 835 836
      ->save();

    // Manually configure the test mail collector implementation to prevent
837
    // tests from sending out emails and collect them in state instead.
838 839
    // While this should be enforced via settings.php prior to installation,
    // some tests expect to be able to test mail system implementations.
840
    $config->getEditable('system.mail')
841
      ->set('interface.default', 'test_mail_collector')
842 843
      ->save();

844 845 846
    // 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.
847
    // @see https://www.drupal.org/node/2259167
848
    $config->getEditable('system.logging')
849 850
      ->set('error_level', 'verbose')
      ->save();
851
    $config->getEditable('system.performance')
852 853
      ->set('css.preprocess', FALSE)
      ->set('js.preprocess', FALSE)
854
      ->save();
855 856 857 858 859 860 861 862 863

    // Set an explicit time zone to not rely on the system one, which may vary
    // from setup to setup. The Australia/Sydney time zone is chosen so all
    // tests are run using an edge case scenario (UTC+10 and DST). This choice
    // is made to prevent time zone related regressions and reduce the
    // fragility of the testing system in general.
    $config->getEditable('system.date')
      ->set('timezone.default', 'Australia/Sydney')
      ->save();
864
  }
865

866 867 868 869
  /**
   * Reset and rebuild the environment after setup.
   */
  protected function rebuildAll() {
870 871 872
    // 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.
873
    // @see \Drupal\Core\DrupalKernel::bootCode()
874 875
    // @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.
876
    $this->resetAll();
877
    $this->kernel->prepareLegacyRequest(\Drupal::request());
878

879 880 881 882 883
    // Explicitly call register() again on the container registered in \Drupal.
    // @todo This should already be called through
    //   DrupalKernel::prepareLegacyRequest() -> DrupalKernel::boot() but that
    //   appears to be calling a different container.
    $this->container->get('stream_wrapper_manager')->register();
884 885
  }

886 887 888 889 890
  /**
   * Returns the parameters that will be used when Simpletest installs Drupal.
   *
   * @see install_drupal()
   * @see install_state_defaults()
891 892 893
   *
   * @return array
   *   Array of parameters for use in install_drupal().
894 895 896
   */
  protected function installParameters() {
    $connection_info = Database::getConnectionInfo();
897
    $driver = $connection_info['default']['driver'];
898
    $connection_info['default']['prefix'] = $connection_info['default']['prefix']['default'];
899 900 901 902
    unset($connection_info['default']['driver']);
    unset($connection_info['default']['namespace']);
    unset($connection_info['default']['pdo']);
    unset($connection_info['default']['init_commands']);
903 904 905 906 907 908 909
    // Remove database connection info that is not used by SQLite.
    if ($driver == 'sqlite') {
      unset($connection_info['default']['username']);
      unset($connection_info['default']['password']);
      unset($connection_info['default']['host']);
      unset($connection_info['default']['port']);
    }
910 911 912 913 914 915 916
    $parameters = array(
      'interactive' => FALSE,
      'parameters' => array(
        'profile' => $this->profile,
        'langcode' => 'en',
      ),
      'forms' => array(
917 918 919 920
        'install_settings_form' => array(
          'driver' => $driver,
          $driver => $connection_info['default'],
        ),
921 922 923 924
        'install_configure_form' => array(
          'site_name' => 'Drupal',
          'site_mail' => 'simpletest@example.com',
          'account' => array(
925 926
            'name' => $this->rootUser->name,
            'mail' => $this->rootUser->getEmail(),
927
            'pass' => array(
928 929
              'pass1' => $this->rootUser->pass_raw,
              'pass2' => $this->rootUser->pass_raw,
930 931
            ),
          ),
932 933 934
          // \Drupal\Core\Render\Element\Checkboxes::valueCallback() requires
          // NULL instead of FALSE values for programmatic form submissions to
          // disable a checkbox.
935 936 937 938 939 940 941
          'update_status_module' => array(
            1 => NULL,
            2 => NULL,
          ),
        ),
      ),
    );
942 943 944

    // If we only have one db driver available, we cannot set the driver.
    include_once DRUPAL_ROOT . '/core/includes/install.inc';
945
    if (count($this->getDatabaseTypes()) == 1) {
946 947
      unset($parameters['forms']['install_settings_form']['driver']);
    }
948 949 950
    return $parameters;
  }

951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041
  /**
   * 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();
    }
  }

1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052
  /**
   * Returns all supported database driver installer objects.
   *
   * This wraps drupal_get_database_types() for use without a current container.
   *
   * @return \Drupal\Core\Database\Install\Tasks[]
   *   An array of available database driver installer objects.
   */
  protected function getDatabaseTypes() {
    \Drupal::setContainer($this->originalContainer);
    $database_types = drupal_get_database_types();
1053
    \Drupal::unsetContainer();
1054 1055 1056
    return $database_types;
  }

1057
  /**
1058
   * Rewrites the settings.php file of the test site.
1059
   *
1060 1061 1062
   * @param array $settings
   *   An array of settings to write out, in the format expected by
   *   drupal_rewrite_settings().
1063