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

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

8
namespace Drupal\simpletest;
9

10
use Drupal\Component\Utility\Crypt;
11
use Drupal\Component\Utility\NestedArray;
12
use Drupal\Component\Utility\String;
13
use Drupal\Core\DrupalKernel;
14 15
use Drupal\Core\Database\Database;
use Drupal\Core\Database\ConnectionNotDefinedException;
16
use Drupal\Core\Language\Language;
17 18
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Session\UserSession;
19
use Drupal\Core\StreamWrapper\PublicStream;
20
use Drupal\Core\Datetime\DrupalDateTime;
21
use Drupal\block\Entity\Block;
22
use Symfony\Component\HttpFoundation\Request;
23 24 25 26

/**
 * Test case for typical Drupal tests.
 */
27 28
abstract class WebTestBase extends TestBase {

29 30 31 32 33
  /**
   * The profile to install as a basis for testing.
   *
   * @var string
   */
34
  protected $profile = 'testing';
35

36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
  /**
   * The URL currently loaded in the internal browser.
   *
   * @var string
   */
  protected $url;

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

  /**
   * The headers of the page currently loaded in the internal browser.
   *
   * @var Array
   */
  protected $headers;

57 58 59 60
  /**
   * Indicates that headers should be dumped if verbose output is enabled.
   *
   * Headers are dumped to verbose by drupalGet(), drupalHead(), and
61
   * drupalPostForm().
62 63 64 65 66
   *
   * @var bool
   */
  protected $dumpHeaders = FALSE;

67 68 69 70 71 72 73 74
  /**
   * The content of the page currently loaded in the internal browser.
   *
   * @var string
   */
  protected $content;

  /**
75
   * The plain-text content of the currently-loaded page.
76 77 78 79 80
   *
   * @var string
   */
  protected $plainTextContent;

81
  /**
82
   * The value of drupalSettings for the currently-loaded page.
83
   *
84
   * drupalSettings refers to the drupalSettings JavaScript variable.
85 86 87 88 89
   *
   * @var Array
   */
  protected $drupalSettings;

90 91 92
  /**
   * The parsed version of the page.
   *
93
   * @var \SimpleXMLElement
94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114
   */
  protected $elements = NULL;

  /**
   * The current user logged in using the internal browser.
   *
   * @var bool
   */
  protected $loggedInUser = FALSE;

  /**
   * 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.
   *
115 116
   * \Drupal\simpletest\WebTestBase itself never sets this but always obeys what
   * is set.
117 118 119 120
   */
  protected $additionalCurlOptions = array();

  /**
121
   * The original user, before it was changed to a clean uid = 1 for testing.
122 123 124 125 126
   *
   * @var object
   */
  protected $originalUser = NULL;

127
  /**
128
   * The original shutdown handlers array, before it was cleaned for testing.
129 130 131 132 133
   *
   * @var array
   */
  protected $originalShutdownCallbacks = array();

134
  /**
135
   * HTTP authentication method.
136 137 138
   */
  protected $httpauth_method = CURLAUTH_BASIC;

139 140 141 142 143
  /**
   * HTTP authentication credentials (<username>:<password>).
   */
  protected $httpauth_credentials = NULL;

144 145 146 147 148 149 150 151 152 153
  /**
   * The current session name, if available.
   */
  protected $session_name = NULL;

  /**
   * The current session ID, if available.
   */
  protected $session_id = NULL;

154 155 156 157 158
  /**
   * Whether the files were copied to the test files directory.
   */
  protected $generatedTestFiles = FALSE;

159 160 161 162 163
  /**
   * The maximum number of redirects to follow when handling responses.
   */
  protected $maximumRedirects = 5;

164 165 166 167 168
  /**
   * The number of redirects followed during the handling of a request.
   */
  protected $redirect_count;

169 170 171 172 173
  /**
   * The kernel used in this test.
   */
  protected $kernel;

174 175 176 177 178 179 180
  /**
   * Cookies to set on curl requests.
   *
   * @var array
   */
  protected $curlCookies = array();

181 182 183 184 185 186 187
  /**
   * An array of custom translations suitable for drupal_rewrite_settings().
   *
   * @var array
   */
  protected $customTranslations;

188
  /**
189
   * Constructor for \Drupal\simpletest\WebTestBase.
190 191 192 193 194 195
   */
  function __construct($test_id = NULL) {
    parent::__construct($test_id);
    $this->skipClasses[__CLASS__] = TRUE;
  }

196 197 198
  /**
   * Get a node from the database based on its title.
   *
199
   * @param $title
200
   *   A node title, usually generated by $this->randomName().
201
   * @param $reset
202
   *   (optional) Whether to reset the entity cache.
203
   *
204
   * @return \Drupal\node\NodeInterface
205
   *   A node entity matching $title.
206
   */
207
  function drupalGetNodeByTitle($title, $reset = FALSE) {
208
    if ($reset) {
209
      \Drupal::entityManager()->getStorageController('node')->resetCache();
210 211
    }
    $nodes = entity_load_multiple_by_properties('node', array('title' => $title));
212 213 214 215 216
    // Load the first node returned from the database.
    $returned_node = reset($nodes);
    return $returned_node;
  }

217 218 219
  /**
   * Creates a node based on default settings.
   *
220 221 222 223 224 225 226 227 228 229 230 231 232
   * @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
233
   *       $settings['body'][0] = array(
234 235 236 237 238
   *         'value' => $this->randomName(32),
   *         'format' => filter_default_format(),
   *       );
   *     @endcode
   *   - title: Random string.
239
   *   - comment: COMMENT_OPEN.
240 241 242 243 244 245
   *   - changed: REQUEST_TIME.
   *   - promote: NODE_NOT_PROMOTED.
   *   - log: Empty string.
   *   - status: NODE_PUBLISHED.
   *   - sticky: NODE_NOT_STICKY.
   *   - type: 'page'.
246
   *   - langcode: Language::LANGCODE_NOT_SPECIFIED.
247 248 249 250
   *   - uid: The currently logged in user, or the user running test.
   *   - revision: 1. (Backwards-compatible binary flag indicating whether a
   *     new revision should be created; use 1 to specify a new revision.)
   *
251
   * @return \Drupal\node\NodeInterface
252 253 254
   *   The created node entity.
   */
  protected function drupalCreateNode(array $settings = array()) {
255
    // Populate defaults array.
256
    $settings += array(
257
      'body'      => array(array()),
258
      'title'     => $this->randomName(8),
259
      'changed'   => REQUEST_TIME,
260
      'promote'   => NODE_NOT_PROMOTED,
261 262
      'revision'  => 1,
      'log'       => '',
263 264
      'status'    => NODE_PUBLISHED,
      'sticky'    => NODE_NOT_STICKY,
265
      'type'      => 'page',
266
      'langcode'  => Language::LANGCODE_NOT_SPECIFIED,
267
    );
268 269 270 271

    // Use the original node's created time for existing nodes.
    if (isset($settings['created']) && !isset($settings['date'])) {
      $settings['date'] = format_date($settings['created'], 'custom', 'Y-m-d H:i:s O');
272
    }
273 274 275 276 277

    // If the node's user uid is not specified manually, use the currently
    // logged in user if available, or else the user running the test.
    if (!isset($settings['uid'])) {
      if ($this->loggedInUser) {
278
        $settings['uid'] = $this->loggedInUser->id();
279 280
      }
      else {
281
        $user = \Drupal::currentUser() ?: $GLOBALS['user'];
282
        $settings['uid'] = $user->id();
283 284 285
      }
    }

286
    // Merge body field value and format separately.
287
    $settings['body'][0] += array(
288
      'value' => $this->randomName(32),
289
      'format' => filter_default_format(),
290 291
    );

292
    $node = entity_create('node', $settings);
293 294 295
    if (!empty($settings['revision'])) {
      $node->setNewRevision();
    }
296
    $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 315 316 317 318 319 320 321 322 323
    if (!isset($values['type'])) {
      do {
        $id = strtolower($this->randomName(8));
      } while (node_type_load($id));
    }
    else {
      $id = $values['type'];
    }
    $values += array(
      'type' => $id,
      'name' => $id,
324
    );
325 326
    $type = entity_create('node_type', $values);
    $status = $type->save();
327
    \Drupal::service('router.builder')->rebuild();
328

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

331 332
    // Reset permissions so that permissions for this content type are
    // available.
333 334
    $this->checkPermissions(array(), TRUE);

335 336 337
    return $type;
  }

338 339 340 341 342 343 344 345
  /**
   * Creates a block instance based on default settings.
   *
   * Note: Until this can be done programmatically, the active user account
   * must have permission to administer blocks.
   *
   * @param string $plugin_id
   *   The plugin ID of the block type for this block instance.
346 347
   * @param array $settings
   *   (optional) An associative array of settings for the block entity.
348
   *   Override the defaults by specifying the key and value in the array, for
349 350 351
   *   example:
   *   @code
   *     $this->drupalPlaceBlock('system_powered_by_block', array(
352
   *       'label' => t('Hello, world!'),
353 354 355
   *     ));
   *   @endcode
   *   The following defaults are provided:
356
   *   - label: Random string.
357
   *   - id: Random string.
358
   *   - region: 'sidebar_first'.
359
   *   - theme: The default theme.
360
   *   - visibility: Empty array.
361
   *
362
   * @return \Drupal\block\Entity\Block
363
   *   The block entity.
364 365 366 367
   *
   * @todo
   *   Add support for creating custom block instances.
   */
368 369
  protected function drupalPlaceBlock($plugin_id, array $settings = array()) {
    $settings += array(
370
      'plugin' => $plugin_id,
371
      'region' => 'sidebar_first',
372
      'id' => strtolower($this->randomName(8)),
373
      'theme' => \Drupal::config('system.theme')->get('default'),
374 375
      'label' => $this->randomName(8),
      'visibility' => array(),
376
      'weight' => 0,
377
    );
378
    foreach (array('region', 'id', 'theme', 'plugin', 'visibility', 'weight') as $key) {
379
      $values[$key] = $settings[$key];
380
      // Remove extra values that do not belong in the settings array.
381 382 383
      unset($settings[$key]);
    }
    $values['settings'] = $settings;
384 385 386
    $block = entity_create('block', $values);
    $block->save();
    return $block;
387 388
  }

389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423
  /**
   * 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()));
  }

424
  /**
425
   * Gets a list files that can be used in tests.
426
   *
427
   * @param $type
428 429
   *   File type, possible values: 'binary', 'html', 'image', 'javascript',
   *   'php', 'sql', 'text'.
430 431
   * @param $size
   *   File size in bytes to match. Please check the tests/files folder.
432
   *
433 434
   * @return
   *   List of files that match filter.
435
   */
436
  protected function drupalGetTestFiles($type, $size = NULL) {
437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455
    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');
      }

      // Generate text test files.
      $lines = array(16, 256, 1024, 2048, 20480);
      $count = 0;
      foreach ($lines as $line) {
        simpletest_generate_file('text-' . $count++, 64, $line);
      }

      // 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) {
456
        file_unmanaged_copy($file->uri, PublicStream::basePath());
457
      }
458

459 460 461 462
      $this->generatedTestFiles = TRUE;
    }

    $files = array();
463 464
    // Make sure type is valid.
    if (in_array($type, array('binary', 'html', 'image', 'javascript', 'php', 'sql', 'text'))) {
465
      $files = file_scan_directory('public://', '/' . $type . '\-.*/');
466 467 468 469

      // If size is set then remove any files that are not of that size.
      if ($size !== NULL) {
        foreach ($files as $file) {
470
          $stats = stat($file->uri);
471
          if ($stats['size'] != $size) {
472
            unset($files[$file->uri]);
473 474 475 476 477 478 479 480 481
          }
        }
      }
    }
    usort($files, array($this, 'drupalCompareFiles'));
    return $files;
  }

  /**
482
   * Compare two files based on size and file name.
483
   */
484
  protected function drupalCompareFiles($file1, $file2) {
485
    $compare_size = filesize($file1->uri) - filesize($file2->uri);
486 487 488
    if ($compare_size) {
      // Sort by file size.
      return $compare_size;
489 490
    }
    else {
491 492
      // The files were the same size, so sort alphabetically.
      return strnatcmp($file1->name, $file2->name);
493 494 495 496
    }
  }

  /**
497
   * Create a user with a given set of permissions.
498
   *
499 500 501
   * @param array $permissions
   *   Array of permission names to assign to user. Note that the user always
   *   has the default permissions derived from the "authenticated users" role.
502
   * @param string $name
503
   *   The user name.
504
   *
505
   * @return \Drupal\user\Entity\User|false
506
   *   A fully loaded user object with pass_raw property, or FALSE if account
507 508
   *   creation fails.
   */
509
  protected function drupalCreateUser(array $permissions = array(), $name = NULL) {
510 511 512 513 514 515 516
    // Create a role with the given permission set, if any.
    $rid = FALSE;
    if ($permissions) {
      $rid = $this->drupalCreateRole($permissions);
      if (!$rid) {
        return FALSE;
      }
517 518 519 520
    }

    // Create a user assigned to that role.
    $edit = array();
521
    $edit['name']   = !empty($name) ? $name : $this->randomName();
522
    $edit['mail']   = $edit['name'] . '@example.com';
523 524
    $edit['pass']   = user_password();
    $edit['status'] = 1;
525
    if ($rid) {
526
      $edit['roles'] = array($rid);
527
    }
528

529 530
    $account = entity_create('user', $edit);
    $account->save();
531

532 533
    $this->assertTrue($account->id(), String::format('User created with name %name and pass %pass', array('%name' => $edit['name'], '%pass' => $edit['pass'])), 'User login');
    if (!$account->id()) {
534 535 536 537 538 539 540 541 542 543 544
      return FALSE;
    }

    // Add the raw password so that we can log in as this user.
    $account->pass_raw = $edit['pass'];
    return $account;
  }

  /**
   * Internal helper function; Create a role with specified permissions.
   *
545
   * @param array $permissions
546
   *   Array of permission names to assign to role.
547 548 549 550
   * @param string $rid
   *   (optional) The role ID (machine name). Defaults to a random name.
   * @param string $name
   *   (optional) The label for the role. Defaults to a random string.
551 552 553
   * @param integer $weight
   *   (optional) The weight for the role. Defaults NULL so that entity_create()
   *   sets the weight to maximum + 1.
554 555
   *
   * @return string
556
   *   Role ID of newly created role, or FALSE if role creation failed.
557
   */
558
  protected function drupalCreateRole(array $permissions, $rid = NULL, $name = NULL, $weight = NULL) {
559 560 561 562 563 564
    // Generate a random, lowercase machine name if none was passed.
    if (!isset($rid)) {
      $rid = strtolower($this->randomName(8));
    }
    // Generate a random label.
    if (!isset($name)) {
565 566 567
      // In the role UI role names are trimmed and random string can start or
      // end with a space.
      $name = trim($this->randomString(8));
568 569
    }

570
    // Check the all the permissions strings are valid.
571 572 573 574
    if (!$this->checkPermissions($permissions)) {
      return FALSE;
    }

575
    // Create new role.
576 577 578 579 580 581 582 583
    $role = entity_create('user_role', array(
      'id' => $rid,
      'label' => $name,
    ));
    if (!is_null($weight)) {
      $role->set('weight', $weight);
    }
    $result = $role->save();
584

585
    $this->assertIdentical($result, SAVED_NEW, String::format('Created role ID @rid with name @name.', array(
586 587
      '@name' => var_export($role->label(), TRUE),
      '@rid' => var_export($role->id(), TRUE),
588
    )), 'Role');
589 590 591 592

    if ($result === SAVED_NEW) {
      // Grant the specified permissions to the role, if any.
      if (!empty($permissions)) {
593
        user_role_grant_permissions($role->id(), $permissions);
594
        $assigned_permissions = entity_load('user_role', $role->id())->permissions;
595 596
        $missing_permissions = array_diff($permissions, $assigned_permissions);
        if (!$missing_permissions) {
597
          $this->pass(String::format('Created permissions: @perms', array('@perms' => implode(', ', $permissions))), 'Role');
598 599
        }
        else {
600
          $this->fail(String::format('Failed to create permissions: @perms', array('@perms' => implode(', ', $missing_permissions))), 'Role');
601 602
        }
      }
603
      return $role->id();
604 605 606 607 608 609
    }
    else {
      return FALSE;
    }
  }

610 611 612
  /**
   * Check to make sure that the array of permissions are valid.
   *
613 614 615 616
   * @param $permissions
   *   Permissions to check.
   * @param $reset
   *   Reset cached available permissions.
617
   *
618 619
   * @return
   *   TRUE or FALSE depending on whether the permissions are valid.
620
   */
621
  protected function checkPermissions(array $permissions, $reset = FALSE) {
622
    $available = &drupal_static(__FUNCTION__);
623 624

    if (!isset($available) || $reset) {
625
      $available = array_keys(\Drupal::moduleHandler()->invokeAll('permission'));
626 627 628 629 630
    }

    $valid = TRUE;
    foreach ($permissions as $permission) {
      if (!in_array($permission, $available)) {
631
        $this->fail(String::format('Invalid permission %permission.', array('%permission' => $permission)), 'Role');
632 633 634 635 636 637
        $valid = FALSE;
      }
    }
    return $valid;
  }

638
  /**
639 640 641 642 643
   * 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.
   *
644
   * Please note that neither the global $user nor the passed-in user object is
645 646 647
   * 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()),
648
   * e.g. to log in the same user again, then it must be re-assigned manually.
649 650 651 652 653 654 655
   * For example:
   * @code
   *   // Create a user.
   *   $account = $this->drupalCreateUser(array());
   *   $this->drupalLogin($account);
   *   // Load real user object.
   *   $pass_raw = $account->pass_raw;
656
   *   $account = user_load($account->id());
657 658
   *   $account->pass_raw = $pass_raw;
   * @endcode
659
   *
660
   * @param \Drupal\Core\Session\AccountInterface $account
661
   *   User object representing the user to log in.
662 663
   *
   * @see drupalCreateUser()
664
   */
665
  protected function drupalLogin(AccountInterface $account) {
666
    if ($this->loggedInUser) {
667 668 669 670
      $this->drupalLogout();
    }

    $edit = array(
671
      'name' => $account->getUsername(),
672
      'pass' => $account->pass_raw
673
    );
674
    $this->drupalPostForm('user', $edit, t('Log in'));
675

676 677
    // @see WebTestBase::drupalUserIsLoggedIn()
    if (isset($this->session_id)) {
678
      $account->session_id = $this->session_id;
679
    }
680
    $pass = $this->assert($this->drupalUserIsLoggedIn($account), format_string('User %name successfully logged in.', array('%name' => $account->getUsername())), 'User login');
681
    if ($pass) {
682
      $this->loggedInUser = $account;
683
      $this->container->set('current_user', $account);
684 685 686
      // @todo Temporary workaround for not being able to use synchronized
      //   services in non dumped container.
      $this->container->get('access_subscriber')->setCurrentUser($account);
687
    }
688 689
  }

690 691 692
  /**
   * Returns whether a given user account is logged in.
   *
693
   * @param \Drupal\user\UserInterface $account
694 695 696 697 698 699
   *   The user account object to check.
   */
  protected function drupalUserIsLoggedIn($account) {
    if (!isset($account->session_id)) {
      return FALSE;
    }
700 701 702
    // @see _drupal_session_read(). The session ID is hashed before being stored
    // in the database.
    return (bool) db_query("SELECT sid FROM {users} u INNER JOIN {sessions} s ON u.uid = s.uid WHERE s.sid = :sid", array(':sid' => Crypt::hashBase64($account->session_id)))->fetchField();
703 704
  }

705 706 707 708 709
  /**
   * Generate a token for the currently logged in user.
   */
  protected function drupalGetToken($value = '') {
    $private_key = drupal_get_private_key();
710
    return Crypt::hmacBase64($value, $this->session_id . $private_key);
711 712
  }

713 714 715 716
  /**
   * Logs a user out of the internal browser and confirms.
   *
   * Confirms logout by checking the login page.
717
   */
718
  protected function drupalLogout() {
719 720 721
    // 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.
722
    $this->drupalGet('user/logout', array('query' => array('destination' => 'user')));
723 724 725
    $this->assertResponse(200, 'User was logged out.');
    $pass = $this->assertField('name', 'Username field found.', 'Logout');
    $pass = $pass && $this->assertField('pass', 'Password field found.', 'Logout');
726

727
    if ($pass) {
728 729
      // @see WebTestBase::drupalUserIsLoggedIn()
      unset($this->loggedInUser->session_id);
730
      $this->loggedInUser = FALSE;
731
      $this->container->set('current_user', drupal_anonymous_user());
732
    }
733 734
  }

735 736 737
  /**
   * Sets up a Drupal site for running functional and integration tests.
   *
738 739 740 741 742 743
   * Installs Drupal with the installation profile specified in
   * \Drupal\simpletest\WebTestBase::$profile into the prefixed database.

   * Afterwards, installs any additional modules specified in the static
   * \Drupal\simpletest\WebTestBase::$modules property of each class in the
   * class hierarchy.
744 745 746 747 748 749 750
   *
   * 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() {
751 752 753 754 755 756 757
    // 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();

758
    // Define information about the user 1 account.
759
    $this->root_user = new UserSession(array(
760
      'uid' => 1,
761 762 763
      'name' => 'admin',
      'mail' => 'admin@example.com',
      'pass_raw' => $this->randomName(),
764
    ));
765 766 767 768

    // Reset the static batch to remove Simpletest's batch operations.
    $batch = &batch_get();
    $batch = array();
769

770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785
    // Get parameters for install_drupal() before removing global variables.
    $parameters = $this->installParameters();

    // Prepare installer settings that are not install_drupal() parameters.
    // Copy and prepare an actual settings.php, so as to resemble a regular
    // installation.
    // Not using File API; a potential error must trigger a PHP warning.
    copy(DRUPAL_ROOT . '/sites/default/default.settings.php', DRUPAL_ROOT . '/' . $this->siteDirectory . '/settings.php');

    // All file system paths are created by System module during installation.
    // @see system_requirements()
    // @see TestBase::prepareEnvironment()
    $settings['settings']['file_public_path'] = (object) array(
      'value' => $this->public_files_directory,
      'required' => TRUE,
    );
786 787 788 789 790 791 792 793
    // 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\SystemListing::scan()
    $settings['settings']['test_parent_site'] = (object) array(
      'value' => $this->originalSite,
      'required' => TRUE,
    );
794
    // Add the parent profile's search path to the child site's search paths.
795
    // @see \Drupal\Core\Extension\ExtensionDiscovery::getProfileDirectories()
796 797 798 799 800 801 802 803 804 805
    $settings['conf']['simpletest.settings']['parent_profile'] = (object) array(
      'value' => $this->originalProfile,
      'required' => TRUE,
    );
    $this->writeSettings($settings);

    // Since Drupal is bootstrapped already, install_begin_request() will not
    // bootstrap into DRUPAL_BOOTSTRAP_CONFIGURATION (again). Hence, we have to
    // reload the newly written custom settings.php manually.
    drupal_settings_initialize();
806

807 808
    // Execute the non-interactive installer.
    require_once DRUPAL_ROOT . '/core/includes/install.core.inc';
809
    install_drupal($parameters);
810

811 812 813 814 815 816 817 818 819 820 821 822 823
    // Import new settings.php written by the installer.
    drupal_settings_initialize();
    foreach ($GLOBALS['config_directories'] as $type => $path) {
      $this->configDirectories[$type] = $path;
    }

    // After writing settings.php, the installer removes write permissions
    // from the site directory. To allow drupal_generate_test_ua() to write
    // a file containing the private key for drupal_valid_test_ua(), the site
    // directory has to be writable.
    // TestBase::restoreEnvironment() will delete the entire site directory.
    // Not using File API; a potential error must trigger a PHP warning.
    chmod(DRUPAL_ROOT . '/' . $this->siteDirectory, 0777);
824

825
    $this->rebuildContainer();
826

827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842
    // Manually create and configure private and temporary files directories.
    // While these could be preset/enforced in settings.php like the public
    // files directory above, some tests expect them to be configurable in the
    // UI. If declared in settings.php, they would no longer be configurable.
    file_prepare_directory($this->private_files_directory, FILE_CREATE_DIRECTORY);
    file_prepare_directory($this->temp_files_directory, FILE_CREATE_DIRECTORY);
    \Drupal::config('system.file')
      ->set('path.private', $this->private_files_directory)
      ->set('path.temporary', $this->temp_files_directory)
      ->save();

    // Manually configure the test mail collector implementation to prevent
    // tests from sending out e-mails and collect them in state instead.
    // While this should be enforced via settings.php prior to installation,
    // some tests expect to be able to test mail system implementations.
    \Drupal::config('system.mail')
843
      ->set('interface.default', 'test_mail_collector')
844 845
      ->save();

846 847 848
    // Restore the original Simpletest batch.
    $batch = &batch_get();
    $batch = $this->originalBatch;
849

850 851
    // Collect modules to install.
    $class = get_class($this);
852
    $modules = array();
853 854 855 856 857 858
    while ($class) {
      if (property_exists($class, 'modules')) {
        $modules = array_merge($modules, $class::$modules);
      }
      $class = get_parent_class($class);
    }
859
    if ($modules) {
860
      $modules = array_unique($modules);
861
      $success = \Drupal::moduleHandler()->install($modules, TRUE);
862
      $this->assertTrue($success, String::format('Enabled modules: %modules', array('%modules' => implode(', ', $modules))));
863
      $this->rebuildContainer();
864
    }
865

866 867 868 869 870 871 872 873 874 875
    // Like DRUPAL_BOOTSTRAP_CONFIGURATION above, any further bootstrap phases
    // are not re-executed by the installer, as Drupal is bootstrapped already.
    // Reset/rebuild all data structures after enabling the modules, primarily
    // to synchronize all data structures and caches between the test runner and
    // the child site.
    // Affects e.g. file_get_stream_wrappers().
    // @see _drupal_bootstrap_code()
    // @see _drupal_bootstrap_full()
    // @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

878 879 880 881 882 883
    // Temporary fix so that when running from run-tests.sh we don't get an
    // empty current path which would indicate we're on the home page.
    $path = current_path();
    if (empty($path)) {
      _current_path('run-tests');
    }
884 885
  }

886 887 888 889 890 891 892 893
  /**
   * Returns the parameters that will be used when Simpletest installs Drupal.
   *
   * @see install_drupal()
   * @see install_state_defaults()
   */
  protected function installParameters() {
    $connection_info = Database::getConnectionInfo();
894 895 896 897 898
    $driver = $connection_info['default']['driver'];
    unset($connection_info['default']['driver']);
    unset($connection_info['default']['namespace']);
    unset($connection_info['default']['pdo']);
    unset($connection_info['default']['init_commands']);
899 900 901 902 903 904 905
    $parameters = array(
      'interactive' => FALSE,
      'parameters' => array(
        'profile' => $this->profile,
        'langcode' => 'en',
      ),
      'forms' => array(
906 907 908 909
        'install_settings_form' => array(
          'driver' => $driver,
          $driver => $connection_info['default'],
        ),
910 911 912 913 914
        'install_configure_form' => array(
          'site_name' => 'Drupal',
          'site_mail' => 'simpletest@example.com',
          'account' => array(
            'name' => $this->root_user->name,
915
            'mail' => $this->root_user->getEmail(),
916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932
            'pass' => array(
              'pass1' => $this->root_user->pass_raw,
              'pass2' => $this->root_user->pass_raw,
            ),
          ),
          // form_type_checkboxes_value() requires NULL instead of FALSE values
          // for programmatic form submissions to disable a checkbox.
          'update_status_module' => array(
            1 => NULL,
            2 => NULL,
          ),
        ),
      ),
    );
    return $parameters;
  }

933
  /**
934
   * Rewrites the settings.php file of the test site.
935
   *
936 937 938
   * @param array $settings
   *   An array of settings to write out, in the format expected by
   *   drupal_rewrite_settings().
939 940 941
   *
   * @see drupal_rewrite_settings()
   */
942
  protected function writeSettings(array $settings) {
943
    include_once DRUPAL_ROOT . '/core/includes/install.inc';
944 945 946 947 948
    $filename = $this->siteDirectory . '/settings.php';
    // system_requirements() removes write permissions from settings.php
    // whenever it is invoked.
    // Not using File API; a potential error must trigger a PHP warning.
    chmod($filename, 0666);
949 950 951
    drupal_rewrite_settings($settings, $filename);
  }

952
  /**
953
   * Queues custom translations to be written to settings.php.
954
   *
955 956
   * Use WebTestBase::writeCustomTranslations() to apply and write the queued
   * translations.
957 958 959 960 961 962 963 964 965 966 967 968
   *
   * @param string $langcode
   *   The langcode to add translations for.
   * @param array $values
   *   Array of values containing the untranslated string and its translation.
   *   For example:
   *   @code
   *   array(
   *     '' => array('Sunday' => 'domingo'),
   *     'Long month name' => array('March' => 'marzo'),
   *   );
   *   @endcode
969 970
   *   Pass an empty array to remove all existing custom translations for the
   *   given $langcode.
971 972
   */
  protected function addCustomTranslations($langcode, array $values) {
973 974 975 976 977 978 979 980 981 982 983 984
    // If $values is empty, then the test expects all custom translations to be
    // cleared.
    if (empty($values)) {
      $this->customTranslations[$langcode] = array();
    }
    // Otherwise, $values are expected to be merged into previously passed
    // values, while retaining keys that are not explicitly set.
    else {
      foreach ($values as $context => $translations) {
        foreach ($translations as $original => $translation) {
          $this->customTranslations[$langcode][$context][$original] = $translation;
        }
985 986 987 988 989
      }
    }
  }

  /**
990 991 992 993
   * Writes custom translations to the test site's settings.php.
   *
   * Use TestBase::addCustomTranslations() to queue custom translations before
   * calling this method.
994 995
   */
  protected function writeCustomTranslations() {
996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011
    $settings = array();
    foreach ($this->customTranslations as $langcode => $values) {
      $settings_key = 'locale_custom_strings_' . $langcode;

      // Update in-memory settings directly.
      $this->settingsSet($settings_key, $values);

      $settings['settings'][$settings_key] = (object) array(
        'value' => $values,
        'required' => TRUE,
      );
    }
    // Only rewrite settings if there are any translation changes to write.
    if (!empty($settings)) {
      $this->writeSettings($settings);
    }
1012 1013
  }

1014
  /**
1015
   * Overrides \Drupal\simpletest\TestBase::rebuildContainer().
1016
   */
1017 1018
  protected function rebuildContainer($environment = 'prod') {
    parent::rebuildContainer($environment);
1019 1020 1021 1022 1023
    // Make sure the url generator has a request object, otherwise calls to
    // $this->drupalGet() will fail.
    $this->prepareRequestForGenerator();
  }

1024
  /**
1025
   * Resets all data structures after having enabled new modules.
1026
   *
1027 1028 1029
   * This method is called by \Drupal\simpletest\WebTestBase::setUp() after
   * enabling the requested modules. It must be called again when additional
   * modules are enabled later.
1030 1031
   */
  protected function resetAll() {
1032
    // Clear all database and static caches and rebuild data structures.