drupal_web_test_case.php 80.1 KB
Newer Older
1 2 3 4
<?php
// $Id$

/**
5 6 7
 * Base class for Drupal tests.
 *
 * Do not extend this class, use one of the subclasses in this file.
8
 */
9
abstract class DrupalTestCase {
10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
  /**
   * The test run ID.
   *
   * @var string
   */
  protected $testId;

  /**
   * 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;

31
  /**
32
   * Time limit for the test.
33
   */
34
  protected $timeLimit = 180;
35

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

54
  /**
55 56 57 58 59 60
   * This class is skipped when looking for the source of an assertion.
   *
   * When displaying which function an assert comes from, it's not too useful
   * to see "drupalWebTestCase->drupalLogin()', we would like to see the test
   * that called it. So we need to skip the classes defining these helper
   * methods.
61
   */
62
  protected $skipClasses = array(__CLASS__ => TRUE);
63

64 65 66
  /**
   * Constructor for DrupalWebTestCase.
   *
67
   * @param $test_id
68 69
   *   Tests with the same id are reported together.
   */
70 71
  public function __construct($test_id = NULL) {
    $this->testId = $test_id;
72 73 74
  }

  /**
75
   * Internal helper: stores the assert.
76 77
   *
   * @param $status
78 79
   *   Can be 'pass', 'fail', 'exception'.
   *   TRUE is a synonym for 'pass', FALSE for 'fail'.
80 81 82
   * @param $message
   *   The message string.
   * @param $group
83
   *   Which group this assert belongs to.
84
   * @param $caller
85
   *   By default, the assert comes from a function whose name starts with
86
   *   'test'. Instead, you can specify where this assert originates from
87
   *   by passing in an associative array as $caller. Key 'file' is
88 89 90
   *   the name of the source file, 'line' is the line number and 'function'
   *   is the caller function itself.
   */
91
  protected function assert($status, $message = '', $group = 'Other', array $caller = NULL) {
92
    global $db_prefix;
93 94

    // Convert boolean status to string status.
95 96 97
    if (is_bool($status)) {
      $status = $status ? 'pass' : 'fail';
    }
98 99

    // Increment summary result counter.
100
    $this->results['#' . $status]++;
101 102 103 104

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

    // Switch to non-testing database to store results in.
108
    $current_db_prefix = $db_prefix;
109
    $db_prefix = $this->originalPrefix;
110 111

    // Creation assertion array that can be displayed while tests are running.
112 113
    $this->assertions[] = $assertion = array(
      'test_id' => $this->testId,
114
      'test_class' => get_class($this),
115 116
      'status' => $status,
      'message' => $message,
117 118 119 120
      'message_group' => $group,
      'function' => $caller['function'],
      'line' => $caller['line'],
      'file' => $caller['file'],
121
    );
122 123

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

    // Return to testing prefix.
129
    $db_prefix = $current_db_prefix;
130 131 132 133 134 135 136 137
    // We do not use a ternary operator here to allow a breakpoint on
    // test failure.
    if ($status == 'pass') {
      return TRUE;
    }
    else {
      return FALSE;
    }
138 139
  }

140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172
  /**
   * Make assertions from outside the test case.
   *
   * @see DrupalTestCase::assert()
   */
  public static function assertStatic($test_id, $test_class, $status, $message = '', $group = 'Other', array $caller = NULL) {
    // Convert boolean status to string status.
    if (is_bool($status)) {
      $status = $status ? 'pass' : 'fail';
    }

    $caller += array(
      'function' => t('N/A'),
      'line' => -1,
      'file' => t('N/A'),
    );

    $assertion = array(
      'test_id' => $test_id,
      'test_class' => $test_class,
      'status' => $status,
      'message' => $message,
      'message_group' => $group,
      'function' => $caller['function'],
      'line' => $caller['line'],
      'file' => $caller['file'],
    );

    db_insert('simpletest')
      ->fields($assertion)
      ->execute();
  }

173 174 175 176 177 178 179 180 181 182
  /**
   * 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.
183
    // We skip calls that occurred in one of the methods of our base classes
184
    // or in an assertion function.
185 186 187
   while (($caller = $backtrace[1]) &&
         ((isset($caller['class']) && isset($this->skipClasses[$caller['class']])) ||
           substr($caller['function'], 0, 6) == 'assert')) {
188 189 190 191 192 193 194
      // We remove that call.
      array_shift($backtrace);
    }

    return _drupal_get_last_caller($backtrace);
  }

195 196 197 198 199 200 201 202 203 204
  /**
   * 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
205
   *   TRUE if the assertion succeeded, FALSE otherwise.
206 207
   */
  protected function assertTrue($value, $message = '', $group = 'Other') {
208
    return $this->assert((bool) $value, $message ? $message : t('Value is TRUE'), $group);
209 210 211 212 213 214 215 216 217 218 219 220
  }

  /**
   * 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
221
   *   TRUE if the assertion succeeded, FALSE otherwise.
222 223
   */
  protected function assertFalse($value, $message = '', $group = 'Other') {
224
    return $this->assert(!$value, $message ? $message : t('Value is FALSE'), $group);
225 226 227 228 229 230 231 232 233 234 235 236
  }

  /**
   * 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
237
   *   TRUE if the assertion succeeded, FALSE otherwise.
238 239
   */
  protected function assertNull($value, $message = '', $group = 'Other') {
240
    return $this->assert(!isset($value), $message ? $message : t('Value is NULL'), $group);
241 242 243 244 245 246 247 248 249 250 251 252
  }

  /**
   * 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
253
   *   TRUE if the assertion succeeded, FALSE otherwise.
254 255
   */
  protected function assertNotNull($value, $message = '', $group = 'Other') {
256
    return $this->assert(isset($value), $message ? $message : t('Value is not NULL'), $group);
257 258 259 260 261 262 263 264 265 266 267 268 269 270
  }

  /**
   * 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
271
   *   TRUE if the assertion succeeded, FALSE otherwise.
272 273
   */
  protected function assertEqual($first, $second, $message = '', $group = 'Other') {
274
    return $this->assert($first == $second, $message ? $message : t('First value is equal to second value'), $group);
275 276 277 278 279 280 281 282 283 284 285 286 287 288
  }

  /**
   * 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
289
   *   TRUE if the assertion succeeded, FALSE otherwise.
290 291
   */
  protected function assertNotEqual($first, $second, $message = '', $group = 'Other') {
292
    return $this->assert($first != $second, $message ? $message : t('First value is not equal to second value'), $group);
293 294 295 296 297 298 299 300 301 302 303 304 305 306
  }

  /**
   * 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
307
   *   TRUE if the assertion succeeded, FALSE otherwise.
308 309
   */
  protected function assertIdentical($first, $second, $message = '', $group = 'Other') {
310
    return $this->assert($first === $second, $message ? $message : t('First value is identical to second value'), $group);
311 312 313 314 315 316 317 318 319 320 321 322 323 324
  }

  /**
   * 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
325
   *   TRUE if the assertion succeeded, FALSE otherwise.
326 327
   */
  protected function assertNotIdentical($first, $second, $message = '', $group = 'Other') {
328
    return $this->assert($first !== $second, $message ? $message : t('First value is not identical to second value'), $group);
329 330 331 332 333 334 335 336 337 338 339 340 341
  }

  /**
   * 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') {
342
    return $this->assert(TRUE, $message, $group);
343 344
  }

345
  /**
346
   * Fire an assertion that is always negative.
347
   *
348 349 350 351 352 353 354 355
   * @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') {
356
    return $this->assert(FALSE, $message, $group);
357 358 359 360 361 362 363 364 365
  }

  /**
   * Fire an error assertion.
   *
   * @param $message
   *   The message to display along with the assertion.
   * @param $group
   *   The type of assertion - examples are "Browser", "PHP".
366
   * @param $caller
367
   *   The caller of the error.
368 369
   * @return
   *   FALSE.
370
   */
371
  protected function error($message = '', $group = 'Other', array $caller = NULL) {
372
    return $this->assert('exception', $message, $group, $caller);
373 374 375 376
  }

  /**
   * Run all tests in this class.
377
   */
378
  public function run() {
379 380 381
    // Initialize verbose debugging.
    simpletest_verbose(NULL, file_directory_path());

382 383
    // HTTP auth settings (<username>:<password>) for the simpletest browser
    // when sending requests to the test site.
384 385 386 387 388
    $username = variable_get('simpletest_username', NULL);
    $password = variable_get('simpletest_password', NULL);
    if ($username && $password) {
      $this->httpauth_credentials = $username . ':' . $password;
    }
389

390 391 392 393 394 395
    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') {
396
        $this->setUp();
397 398 399 400 401 402 403
        try {
          $this->$method();
          // Finish up.
        }
        catch (Exception $e) {
          $this->exceptionHandler($e);
        }
404
        $this->tearDown();
405 406
      }
    }
407 408
    // Clear out the error messages and restore error handler.
    drupal_get_messages();
409 410 411 412 413 414
    restore_error_handler();
  }

  /**
   * Handle errors.
   *
415
   * Because this is registered in set_error_handler(), it has to be public.
416
   * @see set_error_handler
417
   *
418
   */
419
  public function errorHandler($severity, $message, $file = NULL, $line = NULL) {
420
    if ($severity & error_reporting()) {
421 422 423 424 425 426 427 428 429 430 431
      $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',
      );
432 433 434

      $backtrace = debug_backtrace();
      $this->error($message, $error_map[$severity], _drupal_get_last_caller($backtrace));
435 436
    }
    return TRUE;
437 438
  }

439 440 441 442 443
  /**
   * Handle exceptions.
   *
   * @see set_exception_handler
   */
444
  protected function exceptionHandler($exception) {
445 446 447 448 449 450 451 452 453
    $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));
  }

454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519
  /**
   * Generates a random string of ASCII characters of codes 32 to 126.
   *
   * The generated string includes alpha-numeric characters and common misc
   * characters. Use this method when testing general input where the content
   * is not restricted.
   *
   * @param $length
   *   Length of random string to generate which will be appended to $db_prefix.
   * @return
   *   Randomly generated string.
   */
  public static function randomString($length = 8) {
    global $db_prefix;

    $str = '';
    for ($i = 0; $i < $length; $i++) {
      $str .= chr(mt_rand(32, 126));
    }
    return str_replace('simpletest', 's', $db_prefix) . $str;
  }

  /**
   * Generates a random string containing letters and numbers.
   *
   * The letters may be upper or lower case. This method is better for
   * restricted inputs that do not accept certain characters. For example,
   * when testing input fields that require machine readable values (ie without
   * spaces and non-standard characters) this method is best.
   *
   * @param $length
   *   Length of random string to generate which will be appended to $db_prefix.
   * @return
   *   Randomly generated string.
   */
  public static function randomName($length = 8) {
    global $db_prefix;

    $values = array_merge(range(65, 90), range(97, 122), range(48, 57));
    $max = count($values) - 1;
    $str = '';
    for ($i = 0; $i < $length; $i++) {
      $str .= chr($values[mt_rand(0, $max)]);
    }
    return str_replace('simpletest', 's', $db_prefix) . $str;
  }

}

/**
 * Test case for Drupal unit tests.
 *
 * These tests can not access the database nor files. Calling any Drupal
 * function that needs the database will throw exceptions. These include
 * watchdog(), drupal_function_exists(), module_implements(),
 * module_invoke_all() etc.
 */
class DrupalUnitTestCase extends DrupalTestCase {

  /**
   * Constructor for DrupalUnitTestCase.
   */
  function __construct($test_id = NULL) {
    parent::__construct($test_id);
    $this->skipClasses[__CLASS__] = TRUE;
  }
520

521 522 523 524 525 526 527 528 529 530 531 532 533 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 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635
  function setUp() {
    global $db_prefix, $conf;

    // Store necessary current values before switching to prefixed database.
    $this->originalPrefix = $db_prefix;
    $this->originalFileDirectory = file_directory_path();

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

    // If locale is enabled then t() will try to access the database and
    // subsequently will fail as the database is not accessible.
    $module_list = module_list();
    if (isset($module_list['locale'])) {
      $this->originalModuleList = $module_list;
      unset($module_list['locale']);
      module_list(TRUE, FALSE, $module_list);
    }
  }

  function tearDown() {
    global $db_prefix, $conf;
    if (preg_match('/simpletest\d+/', $db_prefix)) {
      $conf['file_directory_path'] = $this->originalFileDirectory;
      // Return the database prefix to the original.
      $db_prefix = $this->originalPrefix;
      // Restore modules if necessary.
      if (isset($this->originalModuleList)) {
        module_list(TRUE, FALSE, $this->originalModuleList);
      }
    }
  }
}

/**
 * Test case for typical Drupal tests.
 */
class DrupalWebTestCase extends DrupalTestCase {
  /**
   * 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;

  /**
   * 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;

  /**
   * 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.
   *
   * DrupalWebTestCase itself never sets this but always obeys what is set.
   */
  protected $additionalCurlOptions = array();

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

  /**
   * HTTP authentication credentials (<username>:<password>).
   */
  protected $httpauth_credentials = NULL;

636 637 638 639 640 641 642 643 644 645
  /**
   * The current session name, if available.
   */
  protected $session_name = NULL;

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

646 647 648 649 650 651 652 653
  /**
   * Constructor for DrupalWebTestCase.
   */
  function __construct($test_id = NULL) {
    parent::__construct($test_id);
    $this->skipClasses[__CLASS__] = TRUE;
  }

654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669
  /**
   * 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;
  }

670 671 672
  /**
   * Creates a node based on default settings.
   *
673 674
   * @param $settings
   *   An associative array of settings to change from the defaults, keys are
675
   *   node properties, for example 'title' => 'Hello, world!'.
676 677
   * @return
   *   Created node object.
678
   */
679
  protected function drupalCreateNode($settings = array()) {
680
    // Populate defaults array.
681
    $settings += array(
682
      'body'      => array(array()),
683 684
      'title'     => $this->randomName(8),
      'comment'   => 2,
685
      'changed'   => REQUEST_TIME,
686 687 688 689 690 691 692 693 694 695
      'moderate'  => 0,
      'promote'   => 0,
      'revision'  => 1,
      'log'       => '',
      'status'    => 1,
      'sticky'    => 0,
      'type'      => 'page',
      'revisions' => NULL,
      'taxonomy'  => NULL,
    );
696 697 698 699

    // 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');
700
    }
701 702 703 704 705 706 707 708 709 710 711 712 713

    // 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) {
        $settings['uid'] = $this->loggedInUser->uid;
      }
      else {
        global $user;
        $settings['uid'] = $user->uid;
      }
    }

714 715 716 717 718 719 720
    // Merge body field value and format separately.
    $body = array(
      'value' => $this->randomName(32),
      'format' => FILTER_FORMAT_DEFAULT
    );
    $settings['body'][0] += $body;

721
    $node = (object) $settings;
722 723
    node_save($node);

724
    // Small hack to link revisions to our test user.
725 726 727 728
    db_update('node_revision')
      ->fields(array('uid' => $node->uid))
      ->condition('vid', $node->vid)
      ->execute();
729 730 731 732 733 734
    return $node;
  }

  /**
   * Creates a custom content type based on default settings.
   *
735
   * @param $settings
736 737
   *   An array of settings to change from the defaults.
   *   Example: 'type' => 'foo'.
738 739
   * @return
   *   Created content type.
740
   */
741
  protected function drupalCreateContentType($settings = array()) {
742
    // Find a non-existent random type name.
743
    do {
744
      $name = strtolower($this->randomName(8));
745
    } while (node_type_get_type($name));
746

747
    // Populate defaults array.
748 749 750 751 752 753 754 755 756 757
    $defaults = array(
      'type' => $name,
      'name' => $name,
      'description' => '',
      'help' => '',
      'title_label' => 'Title',
      'body_label' => 'Body',
      'has_title' => 1,
      'has_body' => 1,
    );
758
    // Imposed values for a custom type.
759 760 761 762 763 764 765 766 767 768 769
    $forced = array(
      'orig_type' => '',
      'old_type' => '',
      'module' => 'node',
      'custom' => 1,
      'modified' => 1,
      'locked' => 0,
    );
    $type = $forced + $settings + $defaults;
    $type = (object)$type;

770
    $saved_type = node_type_save($type);
771 772
    node_types_rebuild();

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

775 776 777
    // Reset permissions so that permissions for this content type are available.
    $this->checkPermissions(array(), TRUE);

778 779 780 781 782 783
    return $type;
  }

  /**
   * Get a list files that can be used in tests.
   *
784 785 786 787 788 789
   * @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.
790
   */
791
  protected function drupalGetTestFiles($type, $size = NULL) {
792 793 794 795
    $files = array();

    // Make sure type is valid.
    if (in_array($type, array('binary', 'html', 'image', 'javascript', 'php', 'sql', 'text'))) {
796
      // Use original file directory instead of one created during setUp().
797
      $path = $this->originalFileDirectory . '/simpletest';
798
      $files = file_scan_directory($path, '/' . $type . '\-.*/');
799 800 801 802

      // If size is set then remove any files that are not of that size.
      if ($size !== NULL) {
        foreach ($files as $file) {
803
          $stats = stat($file->filepath);
804
          if ($stats['size'] != $size) {
805
            unset($files[$file->filepath]);
806 807 808 809 810 811 812 813 814
          }
        }
      }
    }
    usort($files, array($this, 'drupalCompareFiles'));
    return $files;
  }

  /**
815
   * Compare two files based on size and file name.
816
   */
817
  protected function drupalCompareFiles($file1, $file2) {
818 819 820 821
    $compare_size = filesize($file1->filepath) - filesize($file2->filepath);
    if ($compare_size) {
      // Sort by file size.
      return $compare_size;
822 823
    }
    else {
824 825
      // The files were the same size, so sort alphabetically.
      return strnatcmp($file1->name, $file2->name);
826 827 828 829 830 831 832
    }
  }

  /**
   * Create a user with a given set of permissions. The permissions correspond to the
   * names given on the privileges page.
   *
833 834 835 836
   * @param $permissions
   *   Array of permission names to assign to user.
   * @return
   *   A fully loaded user object with pass_raw property, or FALSE if account
837 838
   *   creation fails.
   */
839
  protected function drupalCreateUser($permissions = array('access comments', 'access content', 'post comments', 'post comments without approval')) {
840
    // Create a role with the given permission set.
841
    if (!($rid = $this->drupalCreateRole($permissions))) {
842 843 844 845 846 847
      return FALSE;
    }

    // Create a user assigned to that role.
    $edit = array();
    $edit['name']   = $this->randomName();
848
    $edit['mail']   = $edit['name'] . '@example.com';
849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867
    $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.
   *
868 869 870 871
   * @param $permissions
   *   Array of permission names to assign to role.
   * @return
   *   Role ID of newly created role, or FALSE if role creation failed.
872
   */
873
  protected function drupalCreateRole(array $permissions) {
874 875 876 877
    if (!$this->checkPermissions($permissions)) {
      return FALSE;
    }

878 879
    // Create new role.
    $role_name = $this->randomName();
880 881 882 883
    db_insert('role')
      ->fields(array('name' => $role_name))
      ->execute();
    $role = db_query('SELECT * FROM {role} WHERE name = :name', array(':name' => $role_name))->fetchObject();
884 885 886
    $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.
887
      $query = db_insert('role_permission')->fields(array('rid', 'permission'));
888
      foreach ($permissions as $permission_string) {
889 890 891 892
        $query->values(array(
          'rid' => $role->rid,
          'permission' => $permission_string,
        ));
893
      }
894 895
      $query->execute();
      $count = db_query('SELECT COUNT(*) FROM {role_permission} WHERE rid = :rid', array(':rid' => $role->rid))->fetchField();
896
      $this->assertTrue($count == count($permissions), t('Created permissions: @perms', array('@perms' => implode(', ', $permissions))), t('Role'));
897 898 899 900 901 902 903
      return $role->rid;
    }
    else {
      return FALSE;
    }
  }

904 905 906
  /**
   * Check to make sure that the array of permissions are valid.
   *
907 908 909 910 911 912
   * @param $permissions
   *   Permissions to check.
   * @param $reset
   *   Reset cached available permissions.
   * @return
   *   TRUE or FALSE depending on whether the permissions are valid.
913
   */
914
  protected function checkPermissions(array $permissions, $reset = FALSE) {
915
    $available = &drupal_static(__FUNCTION__);
916 917

    if (!isset($available) || $reset) {
918
      $available = array_keys(module_invoke_all('permission'));
919 920 921 922 923 924 925 926 927 928 929 930
    }

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

931
  /**
932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951
   * 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
952
   *
953 954
   * @param $user
   *   User object representing the user to login.
955 956
   *
   * @see drupalCreateUser()
957
   */
958
  protected function drupalLogin(stdClass $user) {
959
    if ($this->loggedInUser) {
960 961 962 963 964 965 966 967 968
      $this->drupalLogout();
    }

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

969 970 971
    // If a "log out" link appears on the page, it is almost certainly because
    // the login was successful.
    $pass = $this->assertLink(t('Log out'), 0, t('User %name successfully logged in.', array('%name' => $user->name)), t('User login'));
972

973 974 975
    if ($pass) {
      $this->loggedInUser = $user;
    }
976 977
  }

978 979 980 981 982 983 984 985
  /**
   * Generate a token for the currently logged in user.
   */
  protected function drupalGetToken($value = '') {
    $private_key = drupal_get_private_key();
    return md5($this->session_id . $value . $private_key);
  }

986 987 988
  /*
   * Logs a user out of the internal browser, then check the login page to confirm logout.
   */
989
  protected function drupalLogout() {
990 991 992 993
    // 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.
    $this->drupalGet('user/logout', array('query' => 'destination=user'));
994 995 996
    $pass = $this->assertField('name', t('Username field found.'), t('Logout'));
    $pass = $pass && $this->assertField('pass', t('Password field found.'), t('Logout'));

997 998 999
    if ($pass) {
      $this->loggedInUser = FALSE;
    }
1000 1001 1002
  }

  /**
1003 1004 1005 1006
   * 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
1007
   * is created with the same name as the database prefix.
1008
   *
1009
   * @param ...
1010
   *   List of modules to enable for the duration of the test.
1011
   */
1012
  protected function setUp() {
1013
    global $db_prefix, $user, $language;
1014 1015

    // Store necessary current values before switching to prefixed database.
1016 1017
    $this->originalLanguage = $language;
    $this->originalLanguageDefault = variable_get('language_default');
1018
    $this->originalPrefix = $db_prefix;
1019
    $this->originalFileDirectory = file_directory_path();
1020
    $clean_url_original = variable_get('clean_url', 0);
1021 1022

    // Generate temporary prefixed database to ensure that tests have a clean starting point.
1023 1024 1025 1026 1027 1028
    $db_prefix_new = Database::getConnection()->prefixTables('{simpletest' . mt_rand(1000, 1000000) . '}');
    db_update('simpletest_test_id')
      ->fields(array('last_prefix' => $db_prefix_new))
      ->condition('test_id', $this->testId)
      ->execute();
    $db_prefix = $db_prefix_new;
1029

1030
    include_once DRUPAL_ROOT . '/includes/install.inc';
1031
    drupal_install_system();
1032

1033 1034
    $this->preloadRegistry();

1035 1036 1037 1038
    // Include the default profile
    require_once("./profiles/default/default.profile");
    $profile_details = install_profile_info('default', 'en');

1039
    // Add the specified modules to the list of modules in the default profile.
1040
    // Install the modules specified by the default profile.
1041
    drupal_install_modules($profile_details['dependencies'], TRUE);
1042 1043 1044 1045 1046 1047 1048 1049 1050

    node_type_clear();

    // Install additional modules one at a time in order to make sure that the
    // list of modules is updated between each module's installation.
    $modules = func_get_args();
    foreach ($modules as $module) {
      drupal_install_modules(array($module), TRUE);
    }
1051

1052
    // Because the schema is static cached, we need to flush
1053
    // it between each run. If we don't, then it will contain
1054 1055 1056
    // stale data for the previous run's database prefix and all
    // calls to it will fail.
    drupal_get_schema(NULL, TRUE);
1057

1058
    // Run default profile tasks.
1059 1060
    $task = 'profile';
    default_profile_tasks($task, '');
1061 1062

    // Rebuild caches.
1063
    node_types_rebuild();
1064 1065
    actions_synchronize();
    _drupal_flush_css_js();
1066
    $this->refreshVariables();
1067
    $this->checkPermissions(array(), TRUE);
1068

1069 1070 1071
    // Log in with a clean $user.
    $this->originalUser = $user;
    drupal_save_session(FALSE);
1072
    $user = user_load(1);
1073

1074
    // Restore necessary variables.
1075 1076 1077
    variable_set('install_profile', 'default');
    variable_set('install_task', 'profile-finished');
    variable_set('clean_url', $clean_url_original);
1078
    variable_set('site_mail', 'simpletest@example.com');
1079 1080 1081
    // Set up English language.
    unset($GLOBALS['conf']['language_default']);
    $language = language_default();
1082

1083 1084 1085 1086
    // Make sure our drupal_mail_wrapper function is called instead of the
    // default mail handler.
    variable_set('smtp_library', drupal_get_path('module', 'simpletest') . '/drupal_web_test_case.php');

1087
    // Use temporary files directory with the same prefix as database.
1088
    variable_set('file_directory_path', $this->originalFileDirectory . '/simpletest/' . substr($db_prefix, 10));
1089
    $directory = file_directory_path();
1090 1091 1092
    // Create the files directory.
    file_check_directory($directory, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS);

1093 1094 1095 1096
    // Log fatal errors.
    ini_set('log_errors', 1);
    ini_set('error_log', $directory . '/error.log');

1097
    set_time_limit($this->timeLimit);
1098 1099
  }

1100
  /**
1101 1102 1103 1104
   * 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.
   */
1105 1106 1107 1108 1109
  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');
  }

1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121
  /**
   * 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.
   */
1122
  protected function refreshVariables() {
1123 1124
    global $conf;
    cache_clear_all('variables', 'cache');
1125
    $conf = variable_initialize();
1126 1127
  }

1128 1129 1130 1131
  /**
   * Delete created files and temporary files directory, delete the tables created by setUp(),
   * and reset the database prefix.
   */
1132
  protected function tearDown() {
1133
    global $db_prefix, $user, $language;
1134 1135 1136 1137 1138 1139 1140

    $emailCount = count(variable_get('simpletest_emails', array()));
    if ($emailCount) {
      $message = format_plural($emailCount, t('!count e-mail was sent during this test.'), t('!count e-mails were sent during this test.'), array('!count' => $emailCount));
      $this->pass($message, t('E-mail'));
    }

1141 1142
    if (preg_match('/simpletest\d+/', $db_prefix)) {
      // Delete temporary files directory and reset files directory path.
1143
      file_unmanaged_delete_recursive(file_directory_path());
1144
      variable_set('file_directory_path', $this->originalFileDirectory);
1145

1146
      // Remove all prefixed tables (all the tables in the schema).
1147 1148 1149 1150 1151
      $schema = drupal_get_schema(NULL, TRUE);
      $ret = array();
      foreach ($schema as $name => $table) {
        db_drop_table($ret, $name);
      }
1152 1153

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

1156 1157 1158 1159
      // Return the user to the original one.
      $user = $this->originalUser;
      drupal_save_session(TRUE);

1160
      // Ensure that internal logged in variable and cURL options are reset.
1161
      $this->loggedInUser = FALSE;
1162
      $this->additionalCurlOptions = array();
1163

1164 1165
      // Reload module list and implementations to ensure that test module hooks
      // aren't called after tests.
1166
      module_list(TRUE);
1167
      module_implements(MODULE_IMPLEMENTS_CLEAR_CACHE);
1168

1169 1170 1171
      // Reset the Field API.
      field_cache_clear();

1172 1173
      // Rebuild caches.
      $this->refreshVariables();
1174

1175
      // Reset language.
1176 1177 1178 1179
      $language = $this->originalLanguage;
      if ($this->originalLanguageDefault) {
        $GLOBALS['conf']['language_default'] = $this->originalLanguageDefault;
      }
1180

1181
      // Close the CURL handler.
1182 1183 1184 1185 1186
      $this->curlClose();
    }
  }

  /**
1187
   * Initializes the cURL connection.
1188
   *
1189 1190 1191 1192
   * If the simpletest_httpauth_credentials variable is set, this function will
   * add HTTP authentication headers. This is necessary for testing sites that
   * are protected by login credentials from public access.
   * See the description of $curl_options for other options.
1193
   */
1194
  protected function curlInitialize() {
1195
    global $base_url, $db_prefix;
1196

1197 1198 1199 1200
    if (!isset($this->curlHandle)) {
      $this->curlHandle = curl_init();
      $curl_options = $this->additionalCurlOptions + array(
        CURLOPT_COOKIEJAR => $this->cookieFile,
1201 1202
        CURLOPT_URL => $base_url,
        CURLOPT_FOLLOWLOCATION => TRUE,
1203
        CURLOPT_MAXREDIRS => 5,
1204
        CURLOPT_RETURNTRANSFER => TRUE,
1205 1206
        CURLOPT_SSL_VERIFYPEER => FALSE, // Required to make the tests run on https.
        CURLOPT_SSL_VERIFYHOST => FALSE, // Required to make the tests run on https.
1207
        CURLOPT_HEADERFUNCTION => array(&$this, 'curlHeaderCallback'),
1208
      );
1209 1210
      if (isset($this->httpauth_credentials)) {
        $curl_options[CURLOPT_USERPWD] = $this->httpauth_credentials;
1211
      }
1212
      curl_setopt_array($this->curlHandle, $this->additionalCurlOptions + $curl_options);
1213 1214 1215

      // By default, the child session name should be the same as the parent.
      $this->session_name = session_name();
1216
    }
1217 1218 1219 1220 1221
    // We set the user agent header on each request so as to use the current
    // time and a new uniqid.
    if (preg_match('/simpletest\d+/', $db_prefix, $matches)) {
      curl_setopt($this->curlHandle, CURLOPT_USERAGENT, drupal_generate_test_ua($matches[0]));
    }
1222 1223 1224
  }

  /**
1225
   * Performs a cURL exec with the specified options after calling curlConnect().
1226
   *
1227 1228
   * @param $curl_options
   *   Custom cURL options.
1229 1230
   * @return
   *   Content returned from the exec.
1231 1232
   */
  protected function curlExec($curl_options) {
1233
    $this->curlInitialize();
1234
    $url = empty($curl_options[CURLOPT_URL]) ? curl_getinfo($this->curlHandle, CURLINFO_EFFECTIVE_URL) : $curl_options[CURLOPT_URL];
1235 1236 1237 1238 1239 1240 1241 1242
    if (!empty($curl_options[CURLOPT_POST])) {
      // This is a fix for the Curl library to prevent Expect: 100-continue
      // headers in POST requests, that may cause unexpected HTTP response
      // codes from some webservers (like lighttpd that returns a 417 error
      // code). It is done by setting an empty "Expect" header field that is
      // not overwritten by Curl.
      $curl_options[CURLOPT_HTTPHEADER][] = 'Expect:';
    }
1243
    curl_setopt_array($this->curlHandle, $this->additionalCurlOptions + $curl_options);
1244 1245 1246

    // Reset headers and the session ID.
    $this->session_id = NULL;
1247
    $this->headers = array();
1248

1249
    $this->drupalSetContent(curl_exec($this->curlHandle), curl_getinfo($this->curlHandle, CURLINFO_EFFECTIVE_URL));
1250 1251 1252 1253 1254 1255 1256 1257
    $message_vars = 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' => format_size(strlen($this->content))
    );
    $message = t('!method @url returned @status (!length).', $message_vars);
    $this->assertTrue($this->content !== FALSE, $message, t('Browser'));
1258
    return $this->drupalGetContent();
1259 1260
  }

1261 1262 1263 1264 1265
  /**
   * Reads headers and registers errors received from the tested site.
   *
   * @see _drupal_log_error().
   *
1266 1267 1268 1269
   * @param $curlHandler
   *   The cURL handler.
   * @param $header
   *   An header.
1270
   */
1271
  protected function curlHeaderCallback($curlHandler, $header) {
1272
    $this->headers[] = $header;
1273

1274 1275 1276 1277 1278 1279 1280
    // 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])));
    }
1281 1282 1283 1284 1285 1286 1287 1288 1289 1290 1291

    // Save the session cookie, if set.
    if (preg_match('/^Set-Cookie: ' . preg_quote($this->session_name) . '=([a-z90-9]+)/', $header, $matches)) {
      if ($matches[1] != 'deleted') {
        $this->session_id = $matches[1];
      }
      else {
        $this->session_id = NULL;
      }
    }

1292 1293 1294 1295
    // This is required by cURL.
    return strlen($header);
  }

1296 1297 1298 1299
  /**
   * Close the cURL handler and unset the handler.
   */
  protected function curlClose() {
1300 1301 1302
    if (isset($this->curlHandle)) {
      curl_close($this->curlHandle);
      unset($this->curlHandle);
1303 1304 1305 1306
    }
  }

  /**
1307
   * Parse content returned from curlExec using DOM and SimpleXML.
1308
   *
1309 1310
   * @return
   *   A SimpleXMLElement or FALSE on failure.
1311 1312 1313
   */
  protected function parse() {
    if (!$this->elements) {
1314
      // DOM can load HTML soup. But, HTML soup can throw warnings, suppress
1315
      // them.
1316
      @$htmlDom = DOMDocument::loadHTML($this->content);
1317
      if ($htmlDom) {
1318
        $this->pass(t('Valid HTML found on "@path"', array('@path' => $this->getUrl())), t('Browser'));
1319 1320 1321 1322 1323
        // 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);
      }
    }
1324 1325 1326
    if (!$this->elements) {
      $this->fail(t('Parsed page successfully.'), t('Browser'));
    }
1327

1328 1329 1330 1331 1332 1333
    return $this->elements;
  }

  /**
   * Retrieves a Drupal path or an absolute path.
   *
1334
   * @param $path
1335
   *   Drupal path or URL to load into internal browser
1336
   * @param $options
1337
   *   Options to be forwarded to url().
1338 1339 1340
   * @param $headers
   *   An array containing additional HTTP request headers, each formatted as
   *   "name: value".
1341
   * @return
1342
   *   The retrieved HTML string, also available as $this->drupalGetContent()
1343
   */
1344
  protected function drupalGet($path, array $options = array(), array $headers = array()) {
Dries's avatar