drupal_web_test_case.php 65.8 KB
Newer Older
1 2 3 4 5 6
<?php
// $Id$

/**
 * Test case for typical Drupal tests.
 */
7
class DrupalWebTestCase {
8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29

  /**
   * The test run ID.
   *
   * @var string
   */
  protected $testId;

  /**
   * The URL currently loaded in the internal browser.
   *
   * @var string
   */
  protected $url;

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

30 31 32 33 34 35 36
  /**
   * The headers of the page currently loaded in the internal browser.
   *
   * @var Array
   */
  protected $headers;

37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93
  /**
   * The content of the page currently loaded in the internal browser.
   *
   * @var string
   */
  protected $content;

  /**
   * The content of the page currently loaded in the internal browser (plain text version).
   *
   * @var string
   */
  protected $plainTextContent;

  /**
   * The parsed version of the page.
   *
   * @var SimpleXMLElement
   */
  protected $elements = NULL;

  /**
   * Whether a user is logged in the internal browser.
   *
   * @var bool
   */
  protected $isLoggedIn = 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.
   *
   * DrupalWebTestCase itself never sets this but always obeys what is set.
   */
  protected $additionalCurlOptions = array();

  /**
   * The original database prefix, before it was changed for testing purposes.
   *
   * @var string
   */
  protected $originalPrefix = NULL;

  /**
   * The original file directory, before it was changed for testing purposes.
   *
   * @var string
   */
  protected $originalFileDirectory = NULL;

94 95 96 97 98 99 100
  /**
   * The original user, before it was changed to a clean uid = 1 for testing purposes.
   *
   * @var object
   */
  protected $originalUser = NULL;

101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117
  /**
   * Current results of this test case.
   *
   * @var Array
   */
  public $results = array(
    '#pass' => 0,
    '#fail' => 0,
    '#exception' => 0,
  );

  /**
   * Assertions thrown in that test case.
   *
   * @var Array
   */
  protected $assertions = array();
118

119 120 121 122 123 124
  /**
   * Time limit for the test.
   */
  protected $timeLimit = 180;


125 126 127
  /**
   * Constructor for DrupalWebTestCase.
   *
128
   * @param $test_id
129 130
   *   Tests with the same id are reported together.
   */
131 132
  public function __construct($test_id = NULL) {
    $this->testId = $test_id;
133 134 135
  }

  /**
136
   * Internal helper: stores the assert.
137 138
   *
   * @param $status
139 140
   *   Can be 'pass', 'fail', 'exception'.
   *   TRUE is a synonym for 'pass', FALSE for 'fail'.
141 142 143
   * @param $message
   *   The message string.
   * @param $group
144
   *   Which group this assert belongs to.
145
   * @param $caller
146
   *   By default, the assert comes from a function whose name starts with
147
   *   'test'. Instead, you can specify where this assert originates from
148
   *   by passing in an associative array as $caller. Key 'file' is
149 150 151
   *   the name of the source file, 'line' is the line number and 'function'
   *   is the caller function itself.
   */
152
  private function assert($status, $message = '', $group = 'Other', array $caller = NULL) {
153
    global $db_prefix;
154 155

    // Convert boolean status to string status.
156 157 158
    if (is_bool($status)) {
      $status = $status ? 'pass' : 'fail';
    }
159 160

    // Increment summary result counter.
161
    $this->results['#' . $status]++;
162 163 164 165

    // Get the function information about the call to the assertion method.
    if (!$caller) {
      $caller = $this->getAssertionCall();
166
    }
167 168

    // Switch to non-testing database to store results in.
169
    $current_db_prefix = $db_prefix;
170
    $db_prefix = $this->originalPrefix;
171 172

    // Creation assertion array that can be displayed while tests are running.
173 174
    $this->assertions[] = $assertion = array(
      'test_id' => $this->testId,
175
      'test_class' => get_class($this),
176 177
      'status' => $status,
      'message' => $message,
178 179 180 181
      'message_group' => $group,
      'function' => $caller['function'],
      'line' => $caller['line'],
      'file' => $caller['file'],
182
    );
183 184 185 186 187

    // Store assertion for display after the test has completed.
    db_insert('simpletest')->fields($assertion)->execute();

    // Return to testing prefix.
188
    $db_prefix = $current_db_prefix;
189
    return $status == 'pass' ? TRUE : FALSE;
190 191
  }

192 193 194 195 196 197 198 199 200 201
  /**
   * Cycles through backtrace until the first non-assertion method is found.
   *
   * @return
   *   Array representing the true caller.
   */
  protected function getAssertionCall() {
    $backtrace = debug_backtrace();

    // The first element is the call. The second element is the caller.
202
    // We skip calls that occurred in one of the methods of DrupalWebTestCase
203 204 205 206 207 208 209 210 211 212 213
    // or in an assertion function.
    while (($caller = $backtrace[1]) &&
          ((isset($caller['class']) && $caller['class'] == 'DrupalWebTestCase') ||
            substr($caller['function'], 0, 6) == 'assert')) {
      // We remove that call.
      array_shift($backtrace);
    }

    return _drupal_get_last_caller($backtrace);
  }

214 215 216 217 218 219 220 221 222 223
  /**
   * Check to see if a value is not false (not an empty string, 0, NULL, or FALSE).
   *
   * @param $value
   *   The value on which the assertion is to be done.
   * @param $message
   *   The message to display along with the assertion.
   * @param $group
   *   The type of assertion - examples are "Browser", "PHP".
   * @return
224
   *   TRUE if the assertion succeeded, FALSE otherwise.
225 226
   */
  protected function assertTrue($value, $message = '', $group = 'Other') {
227
    return $this->assert((bool) $value, $message ? $message : t('Value is TRUE'), $group);
228 229 230 231 232 233 234 235 236 237 238 239
  }

  /**
   * Check to see if a value is false (an empty string, 0, NULL, or FALSE).
   *
   * @param $value
   *   The value on which the assertion is to be done.
   * @param $message
   *   The message to display along with the assertion.
   * @param $group
   *   The type of assertion - examples are "Browser", "PHP".
   * @return
240
   *   TRUE if the assertion succeeded, FALSE otherwise.
241 242
   */
  protected function assertFalse($value, $message = '', $group = 'Other') {
243
    return $this->assert(!$value, $message ? $message : t('Value is FALSE'), $group);
244 245 246 247 248 249 250 251 252 253 254 255
  }

  /**
   * Check to see if a value is NULL.
   *
   * @param $value
   *   The value on which the assertion is to be done.
   * @param $message
   *   The message to display along with the assertion.
   * @param $group
   *   The type of assertion - examples are "Browser", "PHP".
   * @return
256
   *   TRUE if the assertion succeeded, FALSE otherwise.
257 258
   */
  protected function assertNull($value, $message = '', $group = 'Other') {
259
    return $this->assert(!isset($value), $message ? $message : t('Value is NULL'), $group);
260 261 262 263 264 265 266 267 268 269 270 271
  }

  /**
   * Check to see if a value is not NULL.
   *
   * @param $value
   *   The value on which the assertion is to be done.
   * @param $message
   *   The message to display along with the assertion.
   * @param $group
   *   The type of assertion - examples are "Browser", "PHP".
   * @return
272
   *   TRUE if the assertion succeeded, FALSE otherwise.
273 274
   */
  protected function assertNotNull($value, $message = '', $group = 'Other') {
275
    return $this->assert(isset($value), $message ? $message : t('Value is not NULL'), $group);
276 277 278 279 280 281 282 283 284 285 286 287 288 289
  }

  /**
   * Check to see if two values are equal.
   *
   * @param $first
   *   The first value to check.
   * @param $second
   *   The second value to check.
   * @param $message
   *   The message to display along with the assertion.
   * @param $group
   *   The type of assertion - examples are "Browser", "PHP".
   * @return
290
   *   TRUE if the assertion succeeded, FALSE otherwise.
291 292
   */
  protected function assertEqual($first, $second, $message = '', $group = 'Other') {
293
    return $this->assert($first == $second, $message ? $message : t('First value is equal to second value'), $group);
294 295 296 297 298 299 300 301 302 303 304 305 306 307
  }

  /**
   * Check to see if two values are not equal.
   *
   * @param $first
   *   The first value to check.
   * @param $second
   *   The second value to check.
   * @param $message
   *   The message to display along with the assertion.
   * @param $group
   *   The type of assertion - examples are "Browser", "PHP".
   * @return
308
   *   TRUE if the assertion succeeded, FALSE otherwise.
309 310
   */
  protected function assertNotEqual($first, $second, $message = '', $group = 'Other') {
311
    return $this->assert($first != $second, $message ? $message : t('First value is not equal to second value'), $group);
312 313 314 315 316 317 318 319 320 321 322 323 324 325
  }

  /**
   * Check to see if two values are identical.
   *
   * @param $first
   *   The first value to check.
   * @param $second
   *   The second value to check.
   * @param $message
   *   The message to display along with the assertion.
   * @param $group
   *   The type of assertion - examples are "Browser", "PHP".
   * @return
326
   *   TRUE if the assertion succeeded, FALSE otherwise.
327 328
   */
  protected function assertIdentical($first, $second, $message = '', $group = 'Other') {
329
    return $this->assert($first === $second, $message ? $message : t('First value is identical to second value'), $group);
330 331 332 333 334 335 336 337 338 339 340 341 342 343
  }

  /**
   * Check to see if two values are not identical.
   *
   * @param $first
   *   The first value to check.
   * @param $second
   *   The second value to check.
   * @param $message
   *   The message to display along with the assertion.
   * @param $group
   *   The type of assertion - examples are "Browser", "PHP".
   * @return
344
   *   TRUE if the assertion succeeded, FALSE otherwise.
345 346
   */
  protected function assertNotIdentical($first, $second, $message = '', $group = 'Other') {
347
    return $this->assert($first !== $second, $message ? $message : t('First value is not identical to second value'), $group);
348 349 350 351 352 353 354 355 356 357 358 359 360
  }

  /**
   * Fire an assertion that is always positive.
   *
   * @param $message
   *   The message to display along with the assertion.
   * @param $group
   *   The type of assertion - examples are "Browser", "PHP".
   * @return
   *   TRUE.
   */
  protected function pass($message = NULL, $group = 'Other') {
361
    return $this->assert(TRUE, $message, $group);
362 363
  }

364
  /**
365
   * Fire an assertion that is always negative.
366
   *
367 368 369 370 371 372 373 374
   * @param $message
   *   The message to display along with the assertion.
   * @param $group
   *   The type of assertion - examples are "Browser", "PHP".
   * @return
   *   FALSE.
   */
  protected function fail($message = NULL, $group = 'Other') {
375
    return $this->assert(FALSE, $message, $group);
376 377 378 379 380 381 382 383 384
  }

  /**
   * Fire an error assertion.
   *
   * @param $message
   *   The message to display along with the assertion.
   * @param $group
   *   The type of assertion - examples are "Browser", "PHP".
385
   * @param $caller
386
   *   The caller of the error.
387 388
   * @return
   *   FALSE.
389
   */
390
  protected function error($message = '', $group = 'Other', array $caller = NULL) {
391
    return $this->assert('exception', $message, $group, $caller);
392 393 394 395
  }

  /**
   * Run all tests in this class.
396
   */
397
  public function run() {
398 399 400 401 402 403
    set_error_handler(array($this, 'errorHandler'));
    $methods = array();
    // Iterate through all the methods in this class.
    foreach (get_class_methods(get_class($this)) as $method) {
      // If the current method starts with "test", run it - it's a test.
      if (strtolower(substr($method, 0, 4)) == 'test') {
404
        $this->setUp();
405 406 407 408 409 410 411
        try {
          $this->$method();
          // Finish up.
        }
        catch (Exception $e) {
          $this->exceptionHandler($e);
        }
412
        $this->tearDown();
413 414
      }
    }
415 416
    // Clear out the error messages and restore error handler.
    drupal_get_messages();
417 418 419 420 421 422
    restore_error_handler();
  }

  /**
   * Handle errors.
   *
423
   * Because this is registered in set_error_handler(), it has to be public.
424
   * @see set_error_handler
425
   *
426
   */
427
  public function errorHandler($severity, $message, $file = NULL, $line = NULL) {
428
    if ($severity & error_reporting()) {
429 430 431 432 433 434 435 436 437 438 439
      $error_map = array(
        E_STRICT => 'Run-time notice',
        E_WARNING => 'Warning',
        E_NOTICE => 'Notice',
        E_CORE_ERROR => 'Core error',
        E_CORE_WARNING => 'Core warning',
        E_USER_ERROR => 'User error',
        E_USER_WARNING => 'User warning',
        E_USER_NOTICE => 'User notice',
        E_RECOVERABLE_ERROR => 'Recoverable error',
      );
440 441 442

      $backtrace = debug_backtrace();
      $this->error($message, $error_map[$severity], _drupal_get_last_caller($backtrace));
443 444
    }
    return TRUE;
445 446
  }

447 448 449 450 451
  /**
   * Handle exceptions.
   *
   * @see set_exception_handler
   */
452
  protected function exceptionHandler($exception) {
453 454 455 456 457 458 459 460 461
    $backtrace = $exception->getTrace();
    // Push on top of the backtrace the call that generated the exception.
    array_unshift($backtrace, array(
      'line' => $exception->getLine(),
      'file' => $exception->getFile(),
    ));
    $this->error($exception->getMessage(), 'Uncaught exception', _drupal_get_last_caller($backtrace));
  }

462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477
  /**
   * Get a node from the database based on its title.
   *
   * @param title
   *   A node title, usually generated by $this->randomName().
   *
   * @return
   *   A node object matching $title.
   */
  function drupalGetNodeByTitle($title) {
    $nodes = node_load_multiple(array(), array('title' => $title));
    // Load the first node returned from the database.
    $returned_node = reset($nodes);
    return $returned_node;
  }

478 479 480
  /**
   * Creates a node based on default settings.
   *
481 482
   * @param $settings
   *   An associative array of settings to change from the defaults, keys are
483
   *   node properties, for example 'body' => 'Hello, world!'.
484 485
   * @return
   *   Created node object.
486
   */
487
  protected function drupalCreateNode($settings = array()) {
488 489 490 491 492
    // Populate defaults array
    $defaults = array(
      'body'      => $this->randomName(32),
      'title'     => $this->randomName(8),
      'comment'   => 2,
493
      'changed'   => REQUEST_TIME,
494 495 496 497 498 499 500 501 502 503 504 505 506
      'format'    => FILTER_FORMAT_DEFAULT,
      'moderate'  => 0,
      'promote'   => 0,
      'revision'  => 1,
      'log'       => '',
      'status'    => 1,
      'sticky'    => 0,
      'type'      => 'page',
      'revisions' => NULL,
      'taxonomy'  => NULL,
    );
    $defaults['teaser'] = $defaults['body'];
    // If we already have a node, we use the original node's created time, and this
507 508
    if (isset($defaults['created'])) {
      $defaults['date'] = format_date($defaults['created'], 'custom', 'Y-m-d H:i:s O');
509 510 511 512 513 514 515 516 517 518 519
    }
    if (empty($settings['uid'])) {
      global $user;
      $defaults['uid'] = $user->uid;
    }
    $node = ($settings + $defaults);
    $node = (object)$node;

    node_save($node);

    // small hack to link revisions to our test user
520
    db_query('UPDATE {node_revision} SET uid = %d WHERE vid = %d', $node->uid, $node->vid);
521 522 523 524 525 526
    return $node;
  }

  /**
   * Creates a custom content type based on default settings.
   *
527
   * @param $settings
528 529
   *   An array of settings to change from the defaults.
   *   Example: 'type' => 'foo'.
530 531
   * @return
   *   Created content type.
532
   */
533
  protected function drupalCreateContentType($settings = array()) {
534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562
    // find a non-existent random type name.
    do {
      $name = strtolower($this->randomName(3, 'type_'));
    } while (node_get_types('type', $name));

    // Populate defaults array
    $defaults = array(
      'type' => $name,
      'name' => $name,
      'description' => '',
      'help' => '',
      'min_word_count' => 0,
      'title_label' => 'Title',
      'body_label' => 'Body',
      'has_title' => 1,
      'has_body' => 1,
    );
    // imposed values for a custom type
    $forced = array(
      'orig_type' => '',
      'old_type' => '',
      'module' => 'node',
      'custom' => 1,
      'modified' => 1,
      'locked' => 0,
    );
    $type = $forced + $settings + $defaults;
    $type = (object)$type;

563
    $saved_type = node_type_save($type);
564 565
    node_types_rebuild();

566
    $this->assertEqual($saved_type, SAVED_NEW, t('Created content type %type.', array('%type' => $type->type)));
567

568 569 570
    // Reset permissions so that permissions for this content type are available.
    $this->checkPermissions(array(), TRUE);

571 572 573 574 575 576
    return $type;
  }

  /**
   * Get a list files that can be used in tests.
   *
577 578 579 580 581 582
   * @param $type
   *   File type, possible values: 'binary', 'html', 'image', 'javascript', 'php', 'sql', 'text'.
   * @param $size
   *   File size in bytes to match. Please check the tests/files folder.
   * @return
   *   List of files that match filter.
583
   */
584
  protected function drupalGetTestFiles($type, $size = NULL) {
585 586 587 588 589
    $files = array();

    // Make sure type is valid.
    if (in_array($type, array('binary', 'html', 'image', 'javascript', 'php', 'sql', 'text'))) {
     // Use original file directory instead of one created during setUp().
590
      $path = $this->originalFileDirectory . '/simpletest';
591
      $files = file_scan_directory($path, '/' . $type . '\-.*/');
592 593 594 595

      // If size is set then remove any files that are not of that size.
      if ($size !== NULL) {
        foreach ($files as $file) {
596
          $stats = stat($file->filepath);
597
          if ($stats['size'] != $size) {
598
            unset($files[$file->filepath]);
599 600 601 602 603 604 605 606 607
          }
        }
      }
    }
    usort($files, array($this, 'drupalCompareFiles'));
    return $files;
  }

  /**
608
   * Compare two files based on size and file name.
609
   */
610
  protected function drupalCompareFiles($file1, $file2) {
611 612 613 614
    $compare_size = filesize($file1->filepath) - filesize($file2->filepath);
    if ($compare_size) {
      // Sort by file size.
      return $compare_size;
615 616
    }
    else {
617 618
      // The files were the same size, so sort alphabetically.
      return strnatcmp($file1->name, $file2->name);
619 620 621 622 623 624
    }
  }

  /**
   * Generates a random string.
   *
625 626 627 628 629 630
   * @param $number
   *   Number of characters in length to append to the prefix.
   * @param $prefix
   *   Prefix to use.
   * @return
   *   Randomly generated string.
631
   */
632
  public static function randomName($number = 4, $prefix = 'simpletest_') {
633 634 635 636 637 638 639 640 641 642 643 644 645 646
    $chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_';
    for ($x = 0; $x < $number; $x++) {
      $prefix .= $chars{mt_rand(0, strlen($chars) - 1)};
      if ($x == 0) {
        $chars .= '0123456789';
      }
    }
    return $prefix;
  }

  /**
   * Create a user with a given set of permissions. The permissions correspond to the
   * names given on the privileges page.
   *
647 648 649 650
   * @param $permissions
   *   Array of permission names to assign to user.
   * @return
   *   A fully loaded user object with pass_raw property, or FALSE if account
651 652
   *   creation fails.
   */
653
  protected function drupalCreateUser($permissions = NULL) {
654
    // Create a role with the given permission set.
655
    if (!($rid = $this->_drupalCreateRole($permissions))) {
656 657 658 659 660 661
      return FALSE;
    }

    // Create a user assigned to that role.
    $edit = array();
    $edit['name']   = $this->randomName();
662
    $edit['mail']   = $edit['name'] . '@example.com';
663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681
    $edit['roles']  = array($rid => $rid);
    $edit['pass']   = user_password();
    $edit['status'] = 1;

    $account = user_save('', $edit);

    $this->assertTrue(!empty($account->uid), t('User created with name %name and pass %pass', array('%name' => $edit['name'], '%pass' => $edit['pass'])), t('User login'));
    if (empty($account->uid)) {
      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.
   *
682 683 684 685
   * @param $permissions
   *   Array of permission names to assign to role.
   * @return
   *   Role ID of newly created role, or FALSE if role creation failed.
686
   */
687
  protected function _drupalCreateRole(array $permissions = NULL) {
688 689
    // Generate string version of permissions list.
    if ($permissions === NULL) {
690
      $permissions = array('access comments', 'access content', 'post comments', 'post comments without approval');
691 692
    }

693 694 695 696
    if (!$this->checkPermissions($permissions)) {
      return FALSE;
    }

697 698 699 700 701 702 703
    // Create new role.
    $role_name = $this->randomName();
    db_query("INSERT INTO {role} (name) VALUES ('%s')", $role_name);
    $role = db_fetch_object(db_query("SELECT * FROM {role} WHERE name = '%s'", $role_name));
    $this->assertTrue($role, t('Created role of name: @role_name, id: @rid', array('@role_name' => $role_name, '@rid' => (isset($role->rid) ? $role->rid : t('-n/a-')))), t('Role'));
    if ($role && !empty($role->rid)) {
      // Assign permissions to role and mark it for clean-up.
704 705 706 707 708
      foreach ($permissions as $permission_string) {
        db_query("INSERT INTO {role_permission} (rid, permission) VALUES (%d, '%s')", $role->rid, $permission_string);
      }
      $count = db_result(db_query("SELECT COUNT(*) FROM {role_permission} WHERE rid = %d", $role->rid));
      $this->assertTrue($count == count($permissions), t('Created permissions: @perms', array('@perms' => implode(', ', $permissions))), t('Role'));
709 710 711 712 713 714 715
      return $role->rid;
    }
    else {
      return FALSE;
    }
  }

716 717 718
  /**
   * Check to make sure that the array of permissions are valid.
   *
719 720 721 722 723 724
   * @param $permissions
   *   Permissions to check.
   * @param $reset
   *   Reset cached available permissions.
   * @return
   *   TRUE or FALSE depending on whether the permissions are valid.
725
   */
726
  protected function checkPermissions(array $permissions, $reset = FALSE) {
727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742
    static $available;

    if (!isset($available) || $reset) {
      $available = array_keys(module_invoke_all('perm'));
    }

    $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;
  }

743
  /**
744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763
   * 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.
   *
   * Please note that neither the global $user nor the passed in user object is
   * 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()),
   * e.g. to login the same user again, then it must be re-assigned manually.
   * For example:
   * @code
   *   // Create a user.
   *   $account = $this->drupalCreateUser(array());
   *   $this->drupalLogin($account);
   *   // Load real user object.
   *   $pass_raw = $account->pass_raw;
   *   $account = user_load($account->uid);
   *   $account->pass_raw = $pass_raw;
   * @endcode
764
   *
765 766
   * @param $user
   *   User object representing the user to login.
767 768
   *
   * @see drupalCreateUser()
769
   */
770
  protected function drupalLogin(stdClass $user) {
771
    if ($this->isLoggedIn) {
772 773 774 775 776 777 778 779 780 781 782 783 784
      $this->drupalLogout();
    }

    $edit = array(
      'name' => $user->name,
      'pass' => $user->pass_raw
    );
    $this->drupalPost('user', $edit, t('Log in'));

    $pass = $this->assertText($user->name, t('Found name: %name', array('%name' => $user->name)), t('User login'));
    $pass = $pass && $this->assertNoText(t('The username %name has been blocked.', array('%name' => $user->name)), t('No blocked message at login page'), t('User login'));
    $pass = $pass && $this->assertNoText(t('The name %name is a reserved username.', array('%name' => $user->name)), t('No reserved message at login page'), t('User login'));

785
    $this->isLoggedIn = $pass;
786 787 788 789 790
  }

  /*
   * Logs a user out of the internal browser, then check the login page to confirm logout.
   */
791
  protected function drupalLogout() {
792
    // Make a request to the logout page.
793
    $this->drupalGet('user/logout');
794 795 796 797 798 799

    // Load the user page, the idea being if you were properly logged out you should be seeing a login screen.
    $this->drupalGet('user');
    $pass = $this->assertField('name', t('Username field found.'), t('Logout'));
    $pass = $pass && $this->assertField('pass', t('Password field found.'), t('Logout'));

800
    $this->isLoggedIn = !$pass;
801 802 803
  }

  /**
804 805 806 807
   * Generates a random database prefix, runs the install scripts on the
   * prefixed database and enable the specified modules. After installation
   * many caches are flushed and the internal browser is setup so that the
   * page requests will run on the new prefix. A temporary files directory
808
   * is created with the same name as the database prefix.
809
   *
810
   * @param ...
811
   *   List of modules to enable for the duration of the test.
812
   */
813
  protected function setUp() {
814
    global $db_prefix, $user;
815 816

    // Store necessary current values before switching to prefixed database.
817
    $this->originalPrefix = $db_prefix;
818
    $clean_url_original = variable_get('clean_url', 0);
819 820

    // Generate temporary prefixed database to ensure that tests have a clean starting point.
821
    $db_prefix = Database::getConnection()->prefixTables('{simpletest' . mt_rand(1000, 1000000) . '}');
822

823
    include_once DRUPAL_ROOT . '/includes/install.inc';
824
    drupal_install_system();
825

826 827
    $this->preloadRegistry();

828
    // Add the specified modules to the list of modules in the default profile.
829
    $args = func_get_args();
830
    $modules = array_unique(array_merge(drupal_get_profile_modules('default', 'en'), $args));
831
    drupal_install_modules($modules, TRUE);
832

833
    // Because the schema is static cached, we need to flush
834
    // it between each run. If we don't, then it will contain
835 836 837
    // stale data for the previous run's database prefix and all
    // calls to it will fail.
    drupal_get_schema(NULL, TRUE);
838

839
    // Run default profile tasks.
840 841
    $task = 'profile';
    default_profile_tasks($task, '');
842 843

    // Rebuild caches.
844 845
    actions_synchronize();
    _drupal_flush_css_js();
846
    $this->refreshVariables();
847
    $this->checkPermissions(array(), TRUE);
848

849 850 851 852 853
    // Log in with a clean $user.
    $this->originalUser = $user;
    drupal_save_session(FALSE);
    $user = user_load(array('uid' => 1));

854
    // Restore necessary variables.
855 856 857
    variable_set('install_profile', 'default');
    variable_set('install_task', 'profile-finished');
    variable_set('clean_url', $clean_url_original);
858
    variable_set('site_mail', 'simpletest@example.com');
859 860

    // Use temporary files directory with the same prefix as database.
861
    $this->originalFileDirectory = file_directory_path();
862
    variable_set('file_directory_path', file_directory_path() . '/' . $db_prefix);
863
    $directory = file_directory_path();
864
    file_check_directory($directory, FILE_CREATE_DIRECTORY); // Create the files directory.
865
    set_time_limit($this->timeLimit);
866 867
  }

868
  /**
869 870 871 872
   * This method is called by DrupalWebTestCase::setUp, and preloads the
   * registry from the testing site to cut down on the time it takes to
   * setup a clean environment for the current test run.
   */
873 874 875 876 877
  protected function preloadRegistry() {
    db_query('INSERT INTO {registry} SELECT * FROM ' . $this->originalPrefix . 'registry');
    db_query('INSERT INTO {registry_file} SELECT * FROM ' . $this->originalPrefix . 'registry_file');
  }

878 879 880 881 882 883 884 885 886 887 888 889
  /**
   * Refresh the in-memory set of variables. Useful after a page request is made
   * that changes a variable in a different thread.
   *
   * In other words calling a settings page with $this->drupalPost() with a 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.
   */
890
  protected function refreshVariables() {
891 892 893 894 895
    global $conf;
    cache_clear_all('variables', 'cache');
    $conf = variable_init();
  }

896 897 898 899
  /**
   * Delete created files and temporary files directory, delete the tables created by setUp(),
   * and reset the database prefix.
   */
900
  protected function tearDown() {
901
    global $db_prefix, $user;
902 903
    if (preg_match('/simpletest\d+/', $db_prefix)) {
      // Delete temporary files directory and reset files directory path.
904
      file_unmanaged_delete_recursive(file_directory_path());
905
      variable_set('file_directory_path', $this->originalFileDirectory);
906

907
      // Remove all prefixed tables (all the tables in the schema).
908 909 910 911 912
      $schema = drupal_get_schema(NULL, TRUE);
      $ret = array();
      foreach ($schema as $name => $table) {
        db_drop_table($ret, $name);
      }
913 914

      // Return the database prefix to the original.
915
      $db_prefix = $this->originalPrefix;
916

917 918 919 920
      // Return the user to the original one.
      $user = $this->originalUser;
      drupal_save_session(TRUE);

921
      // Ensure that internal logged in variable and cURL options are reset.
922
      $this->isLoggedIn = FALSE;
923
      $this->additionalCurlOptions = array();
924

925 926
      // Reload module list and implementations to ensure that test module hooks
      // aren't called after tests.
927
      module_list(TRUE);
928
      module_implements(MODULE_IMPLEMENTS_CLEAR_CACHE);
929

930 931 932
      // Reset the Field API.
      field_cache_clear();

933 934 935
      // Rebuild caches.
      $this->refreshVariables();

936
      // Close the CURL handler.
937 938 939 940 941
      $this->curlClose();
    }
  }

  /**
942
   * Initializes the cURL connection.
943
   *
944 945 946
   * This function will add authentication headers as specified in the
   * simpletest_httpauth_username and simpletest_httpauth_pass variables. Also,
   * see the description of $curl_options among the properties.
947
   */
948
  protected function curlInitialize() {
949
    global $base_url, $db_prefix;
950 951 952 953
    if (!isset($this->curlHandle)) {
      $this->curlHandle = curl_init();
      $curl_options = $this->additionalCurlOptions + array(
        CURLOPT_COOKIEJAR => $this->cookieFile,
954 955
        CURLOPT_URL => $base_url,
        CURLOPT_FOLLOWLOCATION => TRUE,
956
        CURLOPT_MAXREDIRS => 5,
957
        CURLOPT_RETURNTRANSFER => TRUE,
958 959
        CURLOPT_SSL_VERIFYPEER => FALSE,  // Required to make the tests run on https://
        CURLOPT_SSL_VERIFYHOST => FALSE,  // Required to make the tests run on https://
960
        CURLOPT_HEADERFUNCTION => array(&$this, 'curlHeaderCallback'),
961
      );
962 963
      if (preg_match('/simpletest\d+/', $db_prefix, $matches)) {
        $curl_options[CURLOPT_USERAGENT] = $matches[0];
964 965 966
      }
      if (!isset($curl_options[CURLOPT_USERPWD]) && ($auth = variable_get('simpletest_httpauth_username', ''))) {
        if ($pass = variable_get('simpletest_httpauth_pass', '')) {
967
          $auth .= ':' . $pass;
968 969 970
        }
        $curl_options[CURLOPT_USERPWD] = $auth;
      }
971
      curl_setopt_array($this->curlHandle, $this->additionalCurlOptions + $curl_options);
972 973 974 975
    }
  }

  /**
976
   * Performs a cURL exec with the specified options after calling curlConnect().
977
   *
978 979
   * @param $curl_options
   *   Custom cURL options.
980 981
   * @return
   *   Content returned from the exec.
982 983
   */
  protected function curlExec($curl_options) {
984
    $this->curlInitialize();
985 986
    $url = empty($curl_options[CURLOPT_URL]) ? curl_getinfo($this->curlHandle, CURLINFO_EFFECTIVE_URL) : $curl_options[CURLOPT_URL];
    curl_setopt_array($this->curlHandle, $this->additionalCurlOptions + $curl_options);
987
    $this->headers = array();
988
    $this->drupalSetContent(curl_exec($this->curlHandle), curl_getinfo($this->curlHandle, CURLINFO_EFFECTIVE_URL));
989
    $this->assertTrue($this->content !== FALSE, t('!method to !url, status is !status, response is !length bytes.', array('!method' => !empty($curl_options[CURLOPT_NOBODY]) ? 'HEAD' : (empty($curl_options[CURLOPT_POSTFIELDS]) ? 'GET' : 'POST'), '!url' => $url, '!status' => curl_getinfo($this->curlHandle, CURLINFO_HTTP_CODE), '!length' => strlen($this->content))), t('Browser'));
990
    return $this->drupalGetContent();
991 992
  }

993 994 995 996 997
  /**
   * Reads headers and registers errors received from the tested site.
   *
   * @see _drupal_log_error().
   *
998 999 1000 1001
   * @param $curlHandler
   *   The cURL handler.
   * @param $header
   *   An header.
1002
   */
1003
  protected function curlHeaderCallback($curlHandler, $header) {
1004
    $this->headers[] = $header;
1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015
    // Errors are being sent via X-Drupal-Assertion-* headers,
    // generated by _drupal_log_error() in the exact form required
    // by DrupalWebTestCase::error().
    if (preg_match('/^X-Drupal-Assertion-[0-9]+: (.*)$/', $header, $matches)) {
      // Call DrupalWebTestCase::error() with the parameters from the header.
      call_user_func_array(array(&$this, 'error'), unserialize(urldecode($matches[1])));
    }
    // This is required by cURL.
    return strlen($header);
  }

1016 1017 1018 1019
  /**
   * Close the cURL handler and unset the handler.
   */
  protected function curlClose() {
1020 1021 1022
    if (isset($this->curlHandle)) {
      curl_close($this->curlHandle);
      unset($this->curlHandle);
1023 1024 1025 1026
    }
  }

  /**
1027
   * Parse content returned from curlExec using DOM and SimpleXML.
1028
   *
1029 1030
   * @return
   *   A SimpleXMLElement or FALSE on failure.
1031 1032 1033
   */
  protected function parse() {
    if (!$this->elements) {
1034
      // DOM can load HTML soup. But, HTML soup can throw warnings, suppress
1035
      // them.
1036
      @$htmlDom = DOMDocument::loadHTML($this->content);
1037
      if ($htmlDom) {
1038
        $this->pass(t('Valid HTML found on "@path"', array('@path' => $this->getUrl())), t('Browser'));
1039 1040 1041 1042 1043
        // It's much easier to work with simplexml than DOM, luckily enough
        // we can just simply import our DOM tree.
        $this->elements = simplexml_import_dom($htmlDom);
      }
    }
1044 1045 1046
    if (!$this->elements) {
      $this->fail(t('Parsed page successfully.'), t('Browser'));
    }
1047

1048 1049 1050 1051 1052 1053
    return $this->elements;
  }

  /**
   * Retrieves a Drupal path or an absolute path.
   *
1054
   * @param $path
1055
   *   Drupal path or URL to load into internal browser
1056
   * @param $options
1057
   *   Options to be forwarded to url().
1058 1059 1060
   * @param $headers
   *   An array containing additional HTTP request headers, each formatted as
   *   "name: value".
1061
   * @return
1062
   *   The retrieved HTML string, also available as $this->drupalGetContent()
1063
   */
1064
  protected function drupalGet($path, array $options = array(), array $headers = array()) {
1065
    $options['absolute'] = TRUE;
1066

1067 1068
    // We re-using a CURL connection here. If that connection still has certain
    // options set, it might change the GET into a POST. Make sure we clear out
1069
    // previous options.
1070
    $out = $this->curlExec(array(CURLOPT_HTTPGET => TRUE, CURLOPT_URL => url($path, $options), CURLOPT_NOBODY => FALSE, CURLOPT_HTTPHEADER => $headers));
1071
    $this->refreshVariables(); // Ensure that any changes to variables in the other thread are picked up.
1072 1073 1074 1075 1076

    // Replace original page output with new output from redirected page(s).
    if (($new = $this->checkForMetaRefresh())) {
      $out = $new;
    }
1077
    return $out;
1078 1079 1080
  }

  /**
1081 1082
   * Execute a POST request on a Drupal page.
   * It will be done as usual POST request with SimpleBrowser.
1083
   *
1084
   * @param $path
1085
   *   Location of the post form. Either a Drupal path or an absolute path or
1086 1087 1088 1089 1090 1091 1092 1093 1094 1095
   *   NULL to post to the current page. For multi-stage forms you can set the
   *   path to NULL and have it post to the last received page. Example:
   *
   *   // First step in form.
   *   $edit = array(...);
   *   $this->drupalPost('some_url', $edit, t('Save'));
   *
   *   // Second step in form.
   *   $edit = array(...);
   *   $this->drupalPost(NULL, $edit, t('Save'));
1096
   * @param  $edit
1097
   *   Field data in an associative array. Changes the current input fields
1098
   *   (where possible) to the values indicated. A checkbox can be set to
1099 1100 1101
   *   TRUE to be checked and FALSE to be unchecked. Note that when a form
   *   contains file upload fields, other fields cannot start with the '@'
   *   character.
1102 1103 1104 1105 1106
   *
   *   Multiple select fields can be set using name[] and setting each of the
   *   possible values. Example:
   *   $edit = array();
   *   $edit['name[]'] = array('value1', 'value2');
1107
   * @param $submit
1108
   *   Value of the submit button.
1109 1110
   * @param $options
   *   Options to be forwarded to url().
1111 1112 1113
   * @param $headers
   *   An array containing additional HTTP request headers, each formatted as
   *   "name: value".
1114
   */
1115
  protected function drupalPost($path, $edit, $submit, array $options = array(), array $headers = array()) {
1116 1117
    $submit_matches = FALSE;
    if (isset($path)) {
1118
      $html = $this->drupalGet($path, $options);
1119 1120 1121 1122
    }
    if ($this->parse()) {
      $edit_save = $edit;
      // Let's iterate over all the forms.
1123
      $forms = $this->xpath('//form');
1124
      foreach ($forms as $form) {
1125 1126 1127 1128 1129 1130
        // We try to set the fields of this form as specified in $edit.
        $edit = $edit_save;
        $post = array();
        $upload = array();
        $submit_matches = $this->handleForm($post, $edit, $upload, $submit, $form);
        $action = isset($form['action']) ? $this->getAbsoluteUrl($form['action']) : $this->getUrl();
1131

1132
        // We post only if we managed to handle every field in edit and the
1133
        // submit button matches.
1134
        if (!$edit && $submit_matches) {
1135 1136 1137 1138 1139
          if ($upload) {
            // TODO: cURL handles file uploads for us, but the implementation
            // is broken. This is a less than elegant workaround. Alternatives
            // are being explored at #253506.
            foreach ($upload as $key => $file) {
1140 1141 1142 1143
              $file = realpath($file);
              if ($file && is_file($file)) {
                $post[$key] = '@' . $file;
              }
1144 1145 1146 1147 1148 1149 1150 1151 1152 1153
            }
          }
          else {
            foreach ($post as $key => $value) {
              // Encode according to application/x-www-form-urlencoded
              // Both names and values needs to be urlencoded, according to
              // http://www.w3.org/TR/html4/interact/forms.html#h-17.13.4.1
              $post[$key] = urlencode($key) . '=' . urlencode($value);
            }
            $post = implode('&', $post);
1154
          }
1155
          $out = $this->curlExec(array(CURLOPT_URL => $action, CURLOPT_POST => TRUE, CURLOPT_POSTFIELDS => $post, CURLOPT_HTTPHEADER => $headers));
1156
          // Ensure that any changes to variables in the other thread are picked up.
1157
          $this->refreshVariables();
1158 1159 1160 1161 1162

          // Replace original page output with new output from redirected page(s).
          if (($new = $this->checkForMetaRefresh())) {
            $out = $new;
          }
1163
          return $out;
1164 1165 1166 1167 1168 1169
        }
      }
      // We have not found a form which contained all fields of $edit.
      foreach ($edit as $name => $value) {
        $this->fail(t('Failed to set field @name to @value', array('@name' => $name, '@value' => $value)));
      }
1170 1171
      $this->assertTrue($submit_matches, t('Found the @submit button', array('@submit' => $submit)));
      $this->fail(t('Found the requested form fields at @path', array('@path' => $path)));
1172 1173 1174
    }
  }

1175 1176 1177 1178 1179 1180 1181 1182
  /**
   * Check for meta refresh tag and if found call drupalGet() recursively. This
   * function looks for the http-equiv attribute to be set to "Refresh"
   * and is case-sensitive.
   *
   * @return
   *   Either the new page content or FALSE.
   */
1183
  protected function checkForMetaRefresh() {
1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196
    if ($this->drupalGetContent() != '' && $this->parse()) {
      $refresh = $this->xpath('//meta[@http-equiv="Refresh"]');
      if (!empty($refresh)) {
        // Parse the content attribute of the meta tag for the format:
        // "[delay]: URL=[page_to_redirect_to]".
        if (preg_match('/\d+;\s*URL=(?P<url>.*)/i', $refresh[0]['content'], $match)) {
          return $this->drupalGet($this->getAbsoluteUrl(decode_entities($match['url'])));
        }
      }
    }
    return FALSE;
  }

1197 1198 1199 1200 1201 1202 1203
  /**
   * Retrieves only the headers for a Drupal path or an absolute path.
   *
   * @param $path
   *   Drupal path or URL to load into internal browser
   * @param $options
   *   Options to be forwarded to url().
1204 1205 1206
   * @param $headers
   *   An array containing additional HTTP request headers, each formatted as
   *   "name: value".
1207 1208 1209
   * @return
   *   The retrieved headers, also available as $this->drupalGetContent()
   */
1210
  protected function drupalHead($path, array $options = array(), array $headers = array()) {
1211
    $options['absolute'] = TRUE;
1212
    $out = $this->curlExec(array(CURLOPT_NOBODY => TRUE, CURLOPT_URL => url($path, $options), CURLOPT_HTTPHEADER => $headers));
1213 1214 1215 1216
    $this->refreshVariables(); // Ensure that any changes to variables in the other thread are picked up.
    return $out;
  }

1217 1218
  /**
   * Handle form input related to drupalPost(). Ensure that the specified fields
1219
   * exist and attempt to create POST data in the correct manner for the particular
1220 1221
   * field type.
   *
1222
   * @param $post
1223
   *   Reference to array of post values.
1224
   * @param $edit
1225
   *   Reference to array of edit values to be checked against the form.
1226
   * @param $submit
1227
   *   Form submit button value.
1228
   * @param $form
1229
   *   Array of form elements.
1230
   * @return
1231
   *   Submit value matches a valid submit input in the form.
1232 1233 1234 1235 1236 1237 1238
   */
  protected function handleForm(&$post, &$edit, &$upload, $submit, $form) {
    // Retrieve the form elements.
    $elements = $form->xpath('.//input|.//textarea|.//select');
    $submit_matches = FALSE;
    foreach ($elements as $element) {
      // SimpleXML objects need string casting all the time.
1239
      $name = (string) $element['name'];
1240 1241 1242 1243 1244
      // This can either be the type of <input> or the name of the tag itself
      // for <select> or <textarea>.
      $type = isset($element['type']) ? (string)$element['type'] : $element->getName();
      $value = isset($element['value']) ? (string)$element['value'] : '';
      $done = FALSE;
1245
      if (isset($edit[$name])) {
1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275 1276 1277 1278 1279 1280
        switch ($type) {
          case 'text':
          case 'textarea':
          case 'password':
            $post[$name] = $edit[$name];
            unset($edit[$name]);
            break;
          case 'radio':
            if ($edit[$name] == $value) {
              $post[$name] = $edit[$name];
              unset($edit[$name]);
            }
            break;
          case 'checkbox':
            // To prevent checkbox from being checked.pass in a FALSE,
            // otherwise the checkbox will be set to its value regardless
            // of $edit.
            if ($edit[$name] === FALSE) {
              unset($edit[$name]);
              continue 2;
            }
            else {
              unset($edit[$name]);
              $post[$name] = $value;
            }
            break;
          case 'select':
            $new_value = $edit[$name];
            $index = 0;
            $key = preg_replace('/\[\]$/', '', $name);
            $options = $this->getAllOptions($element);
            foreach ($options as $option) {
              if (is_array($new_value)) {
                $option_value= (string)$option['value'];
                if (in_array($option_value, $new_value)) {
1281
                  $post[$key . '[' . $index++ . ']'] = $option_value;
1282 1283 1284 1285 1286 1287 1288 1289 1290 1291 1292 1293 1294 1295 1296 1297 1298 1299 1300 1301 1302 1303 1304 1305 1306 1307 1308 1309 1310 1311 1312 1313 1314 1315 1316 1317 1318
                  $done = TRUE;
                  unset($edit[$name]);
                }
              }
              elseif ($new_value == $option['value']) {