WebTestBase.php 129 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 Symfony\Component\HttpFoundation\Request;
22 23 24 25

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

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

35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
  /**
   * 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;

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

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

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

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

89 90 91
  /**
   * The parsed version of the page.
   *
92
   * @var \SimpleXMLElement
93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113
   */
  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.
   *
114 115
   * \Drupal\simpletest\WebTestBase itself never sets this but always obeys what
   * is set.
116 117 118 119
   */
  protected $additionalCurlOptions = array();

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    // 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');
271
    }
272 273 274 275 276

    // 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) {
277
        $settings['uid'] = $this->loggedInUser->id();
278 279 280
      }
      else {
        global $user;
281
        $settings['uid'] = $user->id();
282 283 284
      }
    }

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

291
    $node = entity_create('node', $settings);
292 293 294
    if (!empty($settings['revision'])) {
      $node->setNewRevision();
    }
295
    $node->save();
296 297 298 299 300 301 302

    return $node;
  }

  /**
   * Creates a custom content type based on default settings.
   *
303
   * @param array $values
304 305
   *   An array of settings to change from the defaults.
   *   Example: 'type' => 'foo'.
306
   *
307
   * @return \Drupal\node\Entity\NodeType
308
   *   Created content type.
309
   */
310
  protected function drupalCreateContentType(array $values = array()) {
311
    // Find a non-existent random type name.
312 313 314 315 316 317 318 319 320 321 322
    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,
323
    );
324 325
    $type = entity_create('node_type', $values);
    $status = $type->save();
326
    menu_router_rebuild();
327

328
    $this->assertEqual($status, SAVED_NEW, t('Created content type %type.', array('%type' => $type->id())));
329

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

334 335 336
    return $type;
  }

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

388
  /**
389
   * Gets a list files that can be used in tests.
390
   *
391
   * @param $type
392 393
   *   File type, possible values: 'binary', 'html', 'image', 'javascript',
   *   'php', 'sql', 'text'.
394 395
   * @param $size
   *   File size in bytes to match. Please check the tests/files folder.
396
   *
397 398
   * @return
   *   List of files that match filter.
399
   */
400
  protected function drupalGetTestFiles($type, $size = NULL) {
401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419
    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) {
420
        file_unmanaged_copy($file->uri, PublicStream::basePath());
421
      }
422

423 424 425 426
      $this->generatedTestFiles = TRUE;
    }

    $files = array();
427 428
    // Make sure type is valid.
    if (in_array($type, array('binary', 'html', 'image', 'javascript', 'php', 'sql', 'text'))) {
429
      $files = file_scan_directory('public://', '/' . $type . '\-.*/');
430 431 432 433

      // If size is set then remove any files that are not of that size.
      if ($size !== NULL) {
        foreach ($files as $file) {
434
          $stats = stat($file->uri);
435
          if ($stats['size'] != $size) {
436
            unset($files[$file->uri]);
437 438 439 440 441 442 443 444 445
          }
        }
      }
    }
    usort($files, array($this, 'drupalCompareFiles'));
    return $files;
  }

  /**
446
   * Compare two files based on size and file name.
447
   */
448
  protected function drupalCompareFiles($file1, $file2) {
449
    $compare_size = filesize($file1->uri) - filesize($file2->uri);
450 451 452
    if ($compare_size) {
      // Sort by file size.
      return $compare_size;
453 454
    }
    else {
455 456
      // The files were the same size, so sort alphabetically.
      return strnatcmp($file1->name, $file2->name);
457 458 459 460
    }
  }

  /**
461
   * Create a user with a given set of permissions.
462
   *
463 464 465
   * @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.
466
   * @param string $name
467
   *   The user name.
468
   *
469
   * @return \Drupal\user\Entity\User|false
470
   *   A fully loaded user object with pass_raw property, or FALSE if account
471 472
   *   creation fails.
   */
473
  protected function drupalCreateUser(array $permissions = array(), $name = NULL) {
474 475 476 477 478 479 480
    // Create a role with the given permission set, if any.
    $rid = FALSE;
    if ($permissions) {
      $rid = $this->drupalCreateRole($permissions);
      if (!$rid) {
        return FALSE;
      }
481 482 483 484
    }

    // Create a user assigned to that role.
    $edit = array();
485
    $edit['name']   = !empty($name) ? $name : $this->randomName();
486
    $edit['mail']   = $edit['name'] . '@example.com';
487 488
    $edit['pass']   = user_password();
    $edit['status'] = 1;
489
    if ($rid) {
490
      $edit['roles'] = array($rid);
491
    }
492

493 494
    $account = entity_create('user', $edit);
    $account->save();
495

496 497
    $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()) {
498 499 500 501 502 503 504 505 506 507 508
      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.
   *
509
   * @param array $permissions
510
   *   Array of permission names to assign to role.
511 512 513 514
   * @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.
515 516 517
   * @param integer $weight
   *   (optional) The weight for the role. Defaults NULL so that entity_create()
   *   sets the weight to maximum + 1.
518 519
   *
   * @return string
520
   *   Role ID of newly created role, or FALSE if role creation failed.
521
   */
522
  protected function drupalCreateRole(array $permissions, $rid = NULL, $name = NULL, $weight = NULL) {
523 524 525 526 527 528
    // 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)) {
529 530 531
      // In the role UI role names are trimmed and random string can start or
      // end with a space.
      $name = trim($this->randomString(8));
532 533
    }

534
    // Check the all the permissions strings are valid.
535 536 537 538
    if (!$this->checkPermissions($permissions)) {
      return FALSE;
    }

539
    // Create new role.
540 541 542 543 544 545 546 547
    $role = entity_create('user_role', array(
      'id' => $rid,
      'label' => $name,
    ));
    if (!is_null($weight)) {
      $role->set('weight', $weight);
    }
    $result = $role->save();
548 549

    $this->assertIdentical($result, SAVED_NEW, t('Created role ID @rid with name @name.', array(
550 551
      '@name' => var_export($role->label(), TRUE),
      '@rid' => var_export($role->id(), TRUE),
552 553 554 555 556
    )), t('Role'));

    if ($result === SAVED_NEW) {
      // Grant the specified permissions to the role, if any.
      if (!empty($permissions)) {
557
        user_role_grant_permissions($role->id(), $permissions);
558
        $assigned_permissions = entity_load('user_role', $role->id())->permissions;
559 560 561 562 563 564 565 566
        $missing_permissions = array_diff($permissions, $assigned_permissions);
        if (!$missing_permissions) {
          $this->pass(t('Created permissions: @perms', array('@perms' => implode(', ', $permissions))), t('Role'));
        }
        else {
          $this->fail(t('Failed to create permissions: @perms', array('@perms' => implode(', ', $missing_permissions))), t('Role'));
        }
      }
567
      return $role->id();
568 569 570 571 572 573
    }
    else {
      return FALSE;
    }
  }

574 575 576
  /**
   * Check to make sure that the array of permissions are valid.
   *
577 578 579 580
   * @param $permissions
   *   Permissions to check.
   * @param $reset
   *   Reset cached available permissions.
581
   *
582 583
   * @return
   *   TRUE or FALSE depending on whether the permissions are valid.
584
   */
585
  protected function checkPermissions(array $permissions, $reset = FALSE) {
586
    $available = &drupal_static(__FUNCTION__);
587 588

    if (!isset($available) || $reset) {
589
      $available = array_keys(module_invoke_all('permission'));
590 591 592 593 594 595 596 597 598 599 600 601
    }

    $valid = TRUE;
    foreach ($permissions as $permission) {
      if (!in_array($permission, $available)) {
        $this->fail(t('Invalid permission %permission.', array('%permission' => $permission)), t('Role'));
        $valid = FALSE;
      }
    }
    return $valid;
  }

602
  /**
603 604 605 606 607
   * 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.
   *
608
   * Please note that neither the global $user nor the passed-in user object is
609 610 611
   * 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()),
612
   * e.g. to log in the same user again, then it must be re-assigned manually.
613 614 615 616 617 618 619
   * For example:
   * @code
   *   // Create a user.
   *   $account = $this->drupalCreateUser(array());
   *   $this->drupalLogin($account);
   *   // Load real user object.
   *   $pass_raw = $account->pass_raw;
620
   *   $account = user_load($account->id());
621 622
   *   $account->pass_raw = $pass_raw;
   * @endcode
623
   *
624
   * @param \Drupal\Core\Session\AccountInterface $account
625
   *   User object representing the user to log in.
626 627
   *
   * @see drupalCreateUser()
628
   */
629
  protected function drupalLogin(AccountInterface $account) {
630
    if ($this->loggedInUser) {
631 632 633 634
      $this->drupalLogout();
    }

    $edit = array(
635
      'name' => $account->getUsername(),
636
      'pass' => $account->pass_raw
637
    );
638
    $this->drupalPostForm('user', $edit, t('Log in'));
639

640 641
    // @see WebTestBase::drupalUserIsLoggedIn()
    if (isset($this->session_id)) {
642
      $account->session_id = $this->session_id;
643
    }
644
    $pass = $this->assert($this->drupalUserIsLoggedIn($account), format_string('User %name successfully logged in.', array('%name' => $account->getUsername())), 'User login');
645
    if ($pass) {
646
      $this->loggedInUser = $account;
647
      $this->container->set('current_user', $account);
648
    }
649 650
  }

651 652 653
  /**
   * Returns whether a given user account is logged in.
   *
654
   * @param \Drupal\user\UserInterface $account
655 656 657 658 659 660 661 662 663 664
   *   The user account object to check.
   */
  protected function drupalUserIsLoggedIn($account) {
    if (!isset($account->session_id)) {
      return FALSE;
    }
    // @see _drupal_session_read()
    return (bool) db_query("SELECT sid FROM {users} u INNER JOIN {sessions} s ON u.uid = s.uid WHERE s.sid = :sid", array(':sid' => $account->session_id))->fetchField();
  }

665 666 667 668 669
  /**
   * Generate a token for the currently logged in user.
   */
  protected function drupalGetToken($value = '') {
    $private_key = drupal_get_private_key();
670
    return Crypt::hmacBase64($value, $this->session_id . $private_key);
671 672
  }

673 674 675 676
  /**
   * Logs a user out of the internal browser and confirms.
   *
   * Confirms logout by checking the login page.
677
   */
678
  protected function drupalLogout() {
679 680 681
    // 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.
682
    $this->drupalGet('user/logout', array('query' => array('destination' => 'user')));
683 684 685
    $this->assertResponse(200, 'User was logged out.');
    $pass = $this->assertField('name', 'Username field found.', 'Logout');
    $pass = $pass && $this->assertField('pass', 'Password field found.', 'Logout');
686

687
    if ($pass) {
688 689
      // @see WebTestBase::drupalUserIsLoggedIn()
      unset($this->loggedInUser->session_id);
690
      $this->loggedInUser = FALSE;
691
      $this->container->set('current_user', drupal_anonymous_user());
692
    }
693 694
  }

695 696 697 698
  /**
   * Sets up a Drupal site for running functional and integration tests.
   *
   * Generates a random database prefix and installs Drupal with the specified
699
   * installation profile in \Drupal\simpletest\WebTestBase::$profile into the
700 701
   * prefixed database. Afterwards, installs any additional modules specified by
   * the test.
702 703 704 705 706 707 708 709 710 711
   *
   * 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.
   *
   * @param ...
   *   List of modules to enable for the duration of the test. This can be
   *   either a single array or a variable number of string arguments.
   *
712 713 714
   * @see \Drupal\simpletest\WebTestBase::prepareDatabasePrefix()
   * @see \Drupal\simpletest\WebTestBase::changeDatabasePrefix()
   * @see \Drupal\simpletest\WebTestBase::prepareEnvironment()
715 716
   */
  protected function setUp() {
717
    global $user, $conf;
718

719 720 721 722 723 724 725
    // 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();

726 727 728
    // Create the database prefix for this test.
    $this->prepareDatabasePrefix();

729 730
    // Prepare the environment for running tests.
    $this->prepareEnvironment();
731 732 733
    if (!$this->setupEnvironment) {
      return FALSE;
    }
734

735 736 737 738 739 740
    // Reset all statics and variables to perform tests in a clean environment.
    $conf = array();
    drupal_static_reset();

    // Change the database prefix.
    // All static variables need to be reset before the database prefix is
741
    // changed, since \Drupal\Core\Utility\CacheArray implementations attempt to
742 743
    // write back to persistent caches when they are destructed.
    $this->changeDatabasePrefix();
744 745 746
    if (!$this->setupDatabasePrefix) {
      return FALSE;
    }
747

748 749 750 751 752
    // Set the 'simpletest_parent_profile' variable to add the parent profile's
    // search path to the child site's search paths.
    // @see drupal_system_listing()
    $conf['simpletest_parent_profile'] = $this->originalProfile;

753
    // Define information about the user 1 account.
754
    $this->root_user = new UserSession(array(
755
      'uid' => 1,
756 757 758
      'name' => 'admin',
      'mail' => 'admin@example.com',
      'pass_raw' => $this->randomName(),
759
    ));
760 761 762 763

    // Reset the static batch to remove Simpletest's batch operations.
    $batch = &batch_get();
    $batch = array();
764 765 766 767 768 769 770 771
    $variable_groups = array(
      'system.file' => array(
        'path.private' =>  $this->private_files_directory,
        'path.temporary' =>  $this->temp_files_directory,
      ),
      'locale.settings' =>  array(
        'translation.path' => $this->translation_files_directory,
      ),
772
    );
773 774 775 776
    foreach ($variable_groups as $config_base => $variables) {
      foreach ($variables as $name => $value) {
        NestedArray::setValue($GLOBALS['conf'], array_merge(array($config_base), explode('.', $name)), $value);
      }
777
    }
778
    $this->settingsSet('file_public_path', $this->public_files_directory);
779 780
    // Execute the non-interactive installer.
    require_once DRUPAL_ROOT . '/core/includes/install.core.inc';
781
    $this->settingsSet('cache', array('default' => 'cache.backend.memory'));
782 783
    $parameters = $this->installParameters();
    install_drupal($parameters);
784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799

    // Set the install_profile so that web requests to the requests to the child
    // site have the correct profile.
    $settings = array(
      'settings' => array(
        'install_profile' => (object) array(
          'value' => $this->profile,
          'required' => TRUE,
        ),
      ),
    );
    $this->writeSettings($settings);
    // Override install profile in Settings to so the correct profile is used by
    // tests.
    $this->settingsSet('install_profile', $this->profile);

800
    $this->settingsSet('cache', array());
801
    $this->rebuildContainer();
802 803 804 805

    // Restore the original Simpletest batch.
    $batch = &batch_get();
    $batch = $this->originalBatch;
806

807 808
    // Set path variables.

809
    // Set 'parent_profile' of simpletest to add the parent profile's
810 811
    // search path to the child site's search paths.
    // @see drupal_system_listing()
812
    \Drupal::config('simpletest.settings')->set('parent_profile', $this->originalProfile)->save();
813

814 815
    // Collect modules to install.
    $class = get_class($this);
816
    $modules = array();
817 818 819 820 821 822
    while ($class) {
      if (property_exists($class, 'modules')) {
        $modules = array_merge($modules, $class::$modules);
      }
      $class = get_parent_class($class);
    }
823
    if ($modules) {
824
      $modules = array_unique($modules);
825
      $success = \Drupal::moduleHandler()->install($modules, TRUE);
826
      $this->assertTrue($success, t('Enabled modules: %modules', array('%modules' => implode(', ', $modules))));
827
      $this->rebuildContainer();
828
    }
829

830 831
    // Reset/rebuild all data structures after enabling the modules.
    $this->resetAll();
832

833 834 835
    // Now make sure that the file path configurations are saved. This is done
    // after we install the modules to override default values.
    foreach ($variable_groups as $config_base => $variables) {
836
      $config = \Drupal::config($config_base);
837 838 839 840 841 842
      foreach ($variables as $name => $value) {
        $config->set($name, $value);
      }
      $config->save();
    }

843
    // Use the test mail class instead of the default mail handler class.
844
    \Drupal::config('system.mail')->set('interface.default', 'Drupal\Core\Mail\VariableLog')->save();
845

846
    drupal_set_time_limit($this->timeLimit);
847 848 849 850 851 852
    // 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');
    }
853
    $this->setup = TRUE;
854 855
  }

856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876
  /**
   * 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();
    $parameters = array(
      'interactive' => FALSE,
      'parameters' => array(
        'profile' => $this->profile,
        'langcode' => 'en',
      ),
      'forms' => array(
        'install_settings_form' => $connection_info['default'],
        'install_configure_form' => array(
          'site_name' => 'Drupal',
          'site_mail' => 'simpletest@example.com',
          'account' => array(
            'name' => $this->root_user->name,
877
            'mail' => $this->root_user->getEmail(),
878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894
            '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;
  }

895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924
  /**
   * Writes a test-specific settings.php file for the child site.
   *
   * The child site loads this after the parent site's settings.php, so settings
   * here override those.
   *
   * @param $settings An array of settings to write out, in the format expected
   *   by drupal_rewrite_settings().
   *
   * @see _drupal_load_test_overrides()
   * @see drupal_rewrite_settings()
   */
  protected function writeSettings($settings) {
    // drupal_rewrite_settings() sets the in-memory global variables in addition
    // to writing the file. We'll want to restore the original globals.
    foreach (array_keys($settings) as $variable_name) {
      $original_globals[$variable_name] = isset($GLOBALS[$variable_name]) ? $GLOBALS[$variable_name] : NULL;
    }

    include_once DRUPAL_ROOT . '/core/includes/install.inc';
    $filename = $this->public_files_directory . '/settings.php';
    file_put_contents($filename, "<?php\n");
    drupal_rewrite_settings($settings, $filename);

    // Restore the original globals.
    foreach ($original_globals as $variable_name => $value) {
      $GLOBALS[$variable_name] = $value;
    }
  }

925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962
  /**
   * Sets custom translations to the settings object and queues them to writing.
   *
   * In order for those custom translations to persist (being written in test
   * site's settings.php) make sure to also call self::writeCustomTranslations()
   *
   * @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
   */
  protected function addCustomTranslations($langcode, array $values) {
    $this->settingsSet('locale_custom_strings_' . $langcode, $values);
    foreach ($values as $key => $translations) {
      foreach ($translations as $label => $value) {
        $this->customTranslations['locale_custom_strings_' . $langcode][$key][$label] = (object) array(
          'value' => $value,
          'required' => TRUE,
        );
      }
    }
  }

  /**
   * Writes custom translations to test site's settings.php.
   */
  protected function writeCustomTranslations() {
    $this->writeSettings(array('settings' => $this->customTranslations));
    $this->customTranslations = array();
  }

963
  /**
964
   * Overrides \Drupal\simpletest\TestBase::rebuildContainer().
965 966 967 968 969 970 971 972
   */
  protected function rebuildContainer() {
    parent::rebuildContainer();
    // Make sure the url generator has a request object, otherwise calls to
    // $this->drupalGet() will fail.
    $this->prepareRequestForGenerator();
  }

973
  /**
974
   * Resets all data structures after having enabled new modules.
975
   *
976 977 978
   * 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.
979 980
   */
  protected function resetAll() {
981
    // Clear all database and static caches and rebuild data structures.
982
    drupal_flush_all_caches();
983
    $this->container = \Drupal::getContainer();
984 985 986 987 988 989

    // Reload global $conf array and permissions.
    $this->refreshVariables();
    $this->checkPermissions(array(), TRUE);
  }

990
  /**
991
   * Refreshes the in-memory set of variables.
992
   *
993 994
   * Useful after a page request is made that changes a variable in a different
   * thread.
995
   *
996
   * In other words calling a settings page with $this->drupalPostForm() with a
997 998 999 1000 1001 1002
   * changed value would update a variable to reflect that change, but in the
   * thread that made the call (thread running the test) the changed variable
   * would not be picked up.
   *
   * This method clears the variables cache and loads a fresh copy from the
   * database to ensure that the most up-to-date set of variables is loaded.
1003
   */
1004
  protected function refreshVariables() {
1005
    global $conf;
1006
    cache('bootstrap')->delete('variables');
1007
    $conf = variable_initialize();
1008 1009
    // Clear the tag cache.
    drupal_static_reset('Drupal\Core\Cache\CacheBackendInterface::tagCache');
1010
    \Drupal::service('config.factory')->reset();
1011 1012
  }

1013
  /**
1014 1015 1016 1017
   * Cleans up after testing.
   *
   * Deletes created files and temporary files directory, deletes the tables
   * created by setUp(), and resets the database prefix.
1018
   */
1019
  protected function tearDown() {
1020 1021 1022 1023
    // Destroy the testing kernel.
    if (isset($this->kernel)) {
      $this->kernel->shutdown();
    }