drupal_web_test_case.php 121 KB
Newer Older
1
2
<?php

3
use Drupal\Database\Database;
Crell's avatar
Crell committed
4
use Drupal\Database\DatabaseConnectionNotDefinedException;
5

6
7
8
9
10
11
12
13
14
15
16
17
/**
 * Global variable that holds information about the tests being run.
 *
 * An array, with the following keys:
 *  - 'test_run_id': the ID of the test being run, in the form 'simpletest_%"
 *  - 'in_child_site': TRUE if the current request is a cURL request from
 *     the parent site.
 *
 * @var array
 */
global $drupal_test_info;

18
/**
19
20
21
 * Base class for Drupal tests.
 *
 * Do not extend this class, use one of the subclasses in this file.
22
 */
23
abstract class DrupalTestCase {
24
25
26
27
28
29
30
31
  /**
   * The test run ID.
   *
   * @var string
   */
  protected $testId;

  /**
32
   * The database prefix of this test run.
33
34
35
   *
   * @var string
   */
36
  protected $databasePrefix = NULL;
37
38
39
40
41
42
43
44

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

45
  /**
46
   * Time limit for the test.
47
   */
48
  protected $timeLimit = 500;
49

50
51
52
53
54
55
56
57
58
  /**
   * Current results of this test case.
   *
   * @var Array
   */
  public $results = array(
    '#pass' => 0,
    '#fail' => 0,
    '#exception' => 0,
59
    '#debug' => 0,
60
61
62
63
64
65
66
67
  );

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

69
  /**
70
71
72
73
74
75
   * 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.
76
   */
77
  protected $skipClasses = array(__CLASS__ => TRUE);
78

79
80
81
82
83
84
85
86
87
88
89
90
  /**
   * Flag to indicate whether the test has been set up.
   *
   * The setUp() method isolates the test from the parent Drupal site by
   * creating a random prefix for the database and setting up a clean file
   * storage directory. The tearDown() method then cleans up this test
   * environment. We must ensure that setUp() has been run. Otherwise,
   * tearDown() will act on the parent Drupal site rather than the test
   * environment, destroying live data.
   */
  protected $setup = FALSE;

91
  /**
92
   * Constructor for DrupalTestCase.
93
   *
94
   * @param $test_id
95
96
   *   Tests with the same id are reported together.
   */
97
98
  public function __construct($test_id = NULL) {
    $this->testId = $test_id;
99
100
  }

101
102
103
104
105
106
107
108
109
110
  /**
   * Checks the matching requirements for DrupalTestCase.
   *
   * @return
   *   Array of errors containing a list of unmet requirements.
   */
  protected function checkRequirements() {
    return array();
  }

111
  /**
112
   * Internal helper: stores the assert.
113
114
   *
   * @param $status
115
116
   *   Can be 'pass', 'fail', 'exception'.
   *   TRUE is a synonym for 'pass', FALSE for 'fail'.
117
118
119
   * @param $message
   *   The message string.
   * @param $group
120
   *   Which group this assert belongs to.
121
   * @param $caller
122
   *   By default, the assert comes from a function whose name starts with
123
   *   'test'. Instead, you can specify where this assert originates from
124
   *   by passing in an associative array as $caller. Key 'file' is
125
126
127
   *   the name of the source file, 'line' is the line number and 'function'
   *   is the caller function itself.
   */
128
  protected function assert($status, $message = '', $group = 'Other', array $caller = NULL) {
129
    // Convert boolean status to string status.
130
131
132
    if (is_bool($status)) {
      $status = $status ? 'pass' : 'fail';
    }
133
134

    // Increment summary result counter.
135
    $this->results['#' . $status]++;
136
137
138
139

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

    // Creation assertion array that can be displayed while tests are running.
143
144
    $this->assertions[] = $assertion = array(
      'test_id' => $this->testId,
145
      'test_class' => get_class($this),
146
147
      'status' => $status,
      'message' => $message,
148
149
150
151
      'message_group' => $group,
      'function' => $caller['function'],
      'line' => $caller['line'],
      'file' => $caller['file'],
152
    );
153
154

    // Store assertion for display after the test has completed.
155
156
157
158
159
160
161
162
163
    try {
      $connection = Database::getConnection('default', 'simpletest_original_default');
    }
    catch (DatabaseConnectionNotDefinedException $e) {
      // If the test was not set up, the simpletest_original_default
      // connection does not exist.
      $connection = Database::getConnection('default', 'default');
    }
    $connection
164
      ->insert('simpletest')
165
166
      ->fields($assertion)
      ->execute();
167

168
169
170
171
172
173
174
175
    // We do not use a ternary operator here to allow a breakpoint on
    // test failure.
    if ($status == 'pass') {
      return TRUE;
    }
    else {
      return FALSE;
    }
176
177
  }

178
  /**
179
180
181
182
183
184
185
186
   * Store an assertion from outside the testing context.
   *
   * This is useful for inserting assertions that can only be recorded after
   * the test case has been destroyed, such as PHP fatal errors. The caller
   * information is not automatically gathered since the caller is most likely
   * inserting the assertion on behalf of other code. In all other respects
   * the method behaves just like DrupalTestCase::assert() in terms of storing
   * the assertion.
187
   *
188
189
190
   * @return
   *   Message ID of the stored assertion.
   *
191
   * @see DrupalTestCase::assert()
192
   * @see DrupalTestCase::deleteAssert()
193
   */
194
  public static function insertAssert($test_id, $test_class, $status, $message = '', $group = 'Other', array $caller = array()) {
195
196
197
198
199
200
    // Convert boolean status to string status.
    if (is_bool($status)) {
      $status = $status ? 'pass' : 'fail';
    }

    $caller += array(
201
202
203
      'function' => t('Unknown'),
      'line' => 0,
      'file' => t('Unknown'),
204
205
206
207
208
209
210
211
212
213
214
215
216
    );

    $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'],
    );

217
    return db_insert('simpletest')
218
219
220
221
      ->fields($assertion)
      ->execute();
  }

222
223
  /**
   * Delete an assertion record by message ID.
224
   *
225
226
227
228
   * @param $message_id
   *   Message ID of the assertion to delete.
   * @return
   *   TRUE if the assertion was deleted, FALSE otherwise.
229
   *
230
231
232
233
234
235
236
237
   * @see DrupalTestCase::insertAssert()
   */
  public static function deleteAssert($message_id) {
    return (bool) db_delete('simpletest')
      ->condition('message_id', $message_id)
      ->execute();
  }

238
239
240
241
242
243
244
245
246
247
  /**
   * 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.
248
    // We skip calls that occurred in one of the methods of our base classes
249
    // or in an assertion function.
250
251
252
   while (($caller = $backtrace[1]) &&
         ((isset($caller['class']) && isset($this->skipClasses[$caller['class']])) ||
           substr($caller['function'], 0, 6) == 'assert')) {
253
254
255
256
257
258
259
      // We remove that call.
      array_shift($backtrace);
    }

    return _drupal_get_last_caller($backtrace);
  }

260
261
262
263
264
265
266
267
268
269
  /**
   * 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
270
   *   TRUE if the assertion succeeded, FALSE otherwise.
271
272
   */
  protected function assertTrue($value, $message = '', $group = 'Other') {
273
    return $this->assert((bool) $value, $message ? $message : t('Value @value is TRUE.', array('@value' => var_export($value, TRUE))), $group);
274
275
276
277
278
279
280
281
282
283
284
285
  }

  /**
   * 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
286
   *   TRUE if the assertion succeeded, FALSE otherwise.
287
288
   */
  protected function assertFalse($value, $message = '', $group = 'Other') {
289
    return $this->assert(!$value, $message ? $message : t('Value @value is FALSE.', array('@value' => var_export($value, TRUE))), $group);
290
291
292
293
294
295
296
297
298
299
300
301
  }

  /**
   * 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
302
   *   TRUE if the assertion succeeded, FALSE otherwise.
303
304
   */
  protected function assertNull($value, $message = '', $group = 'Other') {
305
    return $this->assert(!isset($value), $message ? $message : t('Value @value is NULL.', array('@value' => var_export($value, TRUE))), $group);
306
307
308
309
310
311
312
313
314
315
316
317
  }

  /**
   * 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
318
   *   TRUE if the assertion succeeded, FALSE otherwise.
319
320
   */
  protected function assertNotNull($value, $message = '', $group = 'Other') {
321
    return $this->assert(isset($value), $message ? $message : t('Value @value is not NULL.', array('@value' => var_export($value, TRUE))), $group);
322
323
324
325
326
327
328
329
330
331
332
333
334
335
  }

  /**
   * 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
336
   *   TRUE if the assertion succeeded, FALSE otherwise.
337
338
   */
  protected function assertEqual($first, $second, $message = '', $group = 'Other') {
339
    return $this->assert($first == $second, $message ? $message : t('Value @first is equal to value @second.', array('@first' => var_export($first, TRUE), '@second' => var_export($second, TRUE))), $group);
340
341
342
343
344
345
346
347
348
349
350
351
352
353
  }

  /**
   * 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
354
   *   TRUE if the assertion succeeded, FALSE otherwise.
355
356
   */
  protected function assertNotEqual($first, $second, $message = '', $group = 'Other') {
357
    return $this->assert($first != $second, $message ? $message : t('Value @first is not equal to value @second.', array('@first' => var_export($first, TRUE), '@second' => var_export($second, TRUE))), $group);
358
359
360
361
362
363
364
365
366
367
368
369
370
371
  }

  /**
   * 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
372
   *   TRUE if the assertion succeeded, FALSE otherwise.
373
374
   */
  protected function assertIdentical($first, $second, $message = '', $group = 'Other') {
375
    return $this->assert($first === $second, $message ? $message : t('Value @first is identical to value @second.', array('@first' => var_export($first, TRUE), '@second' => var_export($second, TRUE))), $group);
376
377
378
379
380
381
382
383
384
385
386
387
388
389
  }

  /**
   * 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
390
   *   TRUE if the assertion succeeded, FALSE otherwise.
391
392
   */
  protected function assertNotIdentical($first, $second, $message = '', $group = 'Other') {
393
    return $this->assert($first !== $second, $message ? $message : t('Value @first is not identical to value @second.', array('@first' => var_export($first, TRUE), '@second' => var_export($second, TRUE))), $group);
394
395
396
397
398
399
400
401
402
403
404
405
406
  }

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

410
  /**
411
   * Fire an assertion that is always negative.
412
   *
413
414
415
416
417
418
419
420
   * @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') {
421
    return $this->assert(FALSE, $message, $group);
422
423
424
425
426
427
428
429
430
  }

  /**
   * Fire an error assertion.
   *
   * @param $message
   *   The message to display along with the assertion.
   * @param $group
   *   The type of assertion - examples are "Browser", "PHP".
431
   * @param $caller
432
   *   The caller of the error.
433
434
   * @return
   *   FALSE.
435
   */
436
  protected function error($message = '', $group = 'Other', array $caller = NULL) {
437
438
439
440
441
442
    if ($group == 'User notice') {
      // Since 'User notice' is set by trigger_error() which is used for debug
      // set the message to a status of 'debug'.
      return $this->assert('debug', $message, 'Debug', $caller);
    }

443
    return $this->assert('exception', $message, $group, $caller);
444
445
  }

446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
  /**
   * Logs verbose message in a text file.
   *
   * The a link to the vebose message will be placed in the test results via
   * as a passing assertion with the text '[verbose message]'.
   *
   * @param $message
   *   The verbose message to be stored.
   *
   * @see simpletest_verbose()
   */
  protected function verbose($message) {
    if ($id = simpletest_verbose($message)) {
      $url = file_create_url($this->originalFileDirectory . '/simpletest/verbose/' . get_class($this) . '-' . $id . '.html');
      $this->error(l(t('Verbose message'), $url, array('attributes' => array('target' => '_blank'))), 'User notice');
    }
  }

464
465
  /**
   * Run all tests in this class.
466
467
468
469
470
471
472
473
474
   *
   * Regardless of whether $methods are passed or not, only method names
   * starting with "test" are executed.
   *
   * @param $methods
   *   (optional) A list of method names in the test case class to run; e.g.,
   *   array('testFoo', 'testBar'). By default, all methods of the class are
   *   taken into account, but it can be useful to only run a few selected test
   *   methods during debugging.
475
   */
476
  public function run(array $methods = array()) {
477
    // Initialize verbose debugging.
478
    simpletest_verbose(NULL, variable_get('file_public_path', conf_path() . '/files'), get_class($this));
479

480
481
    // HTTP auth settings (<username>:<password>) for the simpletest browser
    // when sending requests to the test site.
482
483
484
    $this->httpauth_method = variable_get('simpletest_httpauth_method', CURLAUTH_BASIC);
    $username = variable_get('simpletest_httpauth_username', NULL);
    $password = variable_get('simpletest_httpauth_password', NULL);
485
486
487
    if ($username && $password) {
      $this->httpauth_credentials = $username . ':' . $password;
    }
488

489
    set_error_handler(array($this, 'errorHandler'));
490
    $class = get_class($this);
491
492
493
494
495
496
    // Iterate through all the methods in this class, unless a specific list of
    // methods to run was passed.
    $class_methods = get_class_methods($class);
    if ($methods) {
      $class_methods = array_intersect($class_methods, $methods);
    }
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
    $missing_requirements = $this->checkRequirements();
    if (!empty($missing_requirements)) {
      $missing_requirements_object = new ReflectionObject($this);
      $caller = array(
        'file' => $missing_requirements_object->getFileName(),
      );
      foreach ($missing_requirements as $missing_requirement) {
        DrupalTestCase::insertAssert($this->testId, $class, FALSE, $missing_requirement, 'Requirements check.', $caller);
      }
    }
    else {
      foreach ($class_methods as $method) {
        // If the current method starts with "test", run it - it's a test.
        if (strtolower(substr($method, 0, 4)) == 'test') {
          // Insert a fail record. This will be deleted on completion to ensure
          // that testing completed.
          $method_info = new ReflectionMethod($class, $method);
          $caller = array(
            'file' => $method_info->getFileName(),
            'line' => $method_info->getStartLine(),
            'function' => $class . '->' . $method . '()',
          );
          $completion_check_id = DrupalTestCase::insertAssert($this->testId, $class, FALSE, t('The test did not complete due to a fatal error.'), 'Completion check', $caller);
          $this->setUp();
          if ($this->setup) {
            try {
              $this->$method();
              // Finish up.
            }
            catch (Exception $e) {
              $this->exceptionHandler($e);
            }
            $this->tearDown();
530
          }
531
532
          else {
            $this->fail(t("The test cannot be executed because it has not been set up properly."));
533
          }
534
535
          // Remove the completion check record.
          DrupalTestCase::deleteAssert($completion_check_id);
536
        }
537
538
      }
    }
539
540
    // Clear out the error messages and restore error handler.
    drupal_get_messages();
541
542
543
544
    restore_error_handler();
  }

  /**
545
   * Handle errors during test runs.
546
   *
547
   * Because this is registered in set_error_handler(), it has to be public.
548
549
   * @see set_error_handler
   */
550
  public function errorHandler($severity, $message, $file = NULL, $line = NULL) {
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
    if ($severity & error_reporting()) {
      $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',
      );

      $backtrace = debug_backtrace();
      $this->error($message, $error_map[$severity], _drupal_get_last_caller($backtrace));
    }
567
    return TRUE;
568
569
  }

570
571
572
573
574
  /**
   * Handle exceptions.
   *
   * @see set_exception_handler
   */
575
  protected function exceptionHandler($exception) {
576
577
578
579
580
581
    $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(),
    ));
582
    require_once DRUPAL_ROOT . '/core/includes/errors.inc';
583
584
    // The exception message is run through check_plain() by _drupal_decode_exception().
    $this->error(t('%type: !message in %function (line %line of %file).', _drupal_decode_exception($exception)), 'Uncaught exception', _drupal_get_last_caller($backtrace));
585
586
  }

587
588
589
590
591
592
593
594
  /**
   * 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
595
   *   Length of random string to generate.
596
597
598
599
600
601
602
603
   * @return
   *   Randomly generated string.
   */
  public static function randomString($length = 8) {
    $str = '';
    for ($i = 0; $i < $length; $i++) {
      $str .= chr(mt_rand(32, 126));
    }
604
    return $str;
605
606
607
608
609
  }

  /**
   * Generates a random string containing letters and numbers.
   *
610
611
612
613
614
   * The string will always start with a letter. 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 (i.e. without spaces and non-standard
   * characters) this method is best.
615
616
   *
   * @param $length
617
   *   Length of random string to generate.
618
619
620
621
622
623
   * @return
   *   Randomly generated string.
   */
  public static function randomName($length = 8) {
    $values = array_merge(range(65, 90), range(97, 122), range(48, 57));
    $max = count($values) - 1;
624
625
    $str = chr(mt_rand(97, 122));
    for ($i = 1; $i < $length; $i++) {
626
627
      $str .= chr($values[mt_rand(0, $max)]);
    }
628
    return $str;
629
  }
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678

  /**
   * Converts a list of possible parameters into a stack of permutations.
   *
   * Takes a list of parameters containing possible values, and converts all of
   * them into a list of items containing every possible permutation.
   *
   * Example:
   * @code
   * $parameters = array(
   *   'one' => array(0, 1),
   *   'two' => array(2, 3),
   * );
   * $permutations = $this->permute($parameters);
   * // Result:
   * $permutations == array(
   *   array('one' => 0, 'two' => 2),
   *   array('one' => 1, 'two' => 2),
   *   array('one' => 0, 'two' => 3),
   *   array('one' => 1, 'two' => 3),
   * )
   * @endcode
   *
   * @param $parameters
   *   An associative array of parameters, keyed by parameter name, and whose
   *   values are arrays of parameter values.
   *
   * @return
   *   A list of permutations, which is an array of arrays. Each inner array
   *   contains the full list of parameters that have been passed, but with a
   *   single value only.
   */
  public static function generatePermutations($parameters) {
    $all_permutations = array(array());
    foreach ($parameters as $parameter => $values) {
      $new_permutations = array();
      // Iterate over all values of the parameter.
      foreach ($values as $value) {
        // Iterate over all existing permutations.
        foreach ($all_permutations as $permutation) {
          // Add the new parameter value to existing permutations.
          $new_permutations[] = $permutation + array($parameter => $value);
        }
      }
      // Replace the old permutations with the new permutations.
      $all_permutations = $new_permutations;
    }
    return $all_permutations;
  }
679
680
681
682
683
684
685
}

/**
 * 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
686
 * watchdog(), module_implements(), module_invoke_all() etc.
687
688
689
690
691
692
693
694
695
696
 */
class DrupalUnitTestCase extends DrupalTestCase {

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

698
699
700
701
702
703
704
705
  /**
   * Sets up unit test environment.
   *
   * Unlike DrupalWebTestCase::setUp(), DrupalUnitTestCase::setUp() does not
   * install modules because tests are performed without accessing the database.
   * Any required files must be explicitly included by the child class setUp()
   * method.
   */
706
  protected function setUp() {
707
    global $conf;
708

709
    // Store necessary current values before switching to the test environment.
710
    $this->originalFileDirectory = variable_get('file_public_path', conf_path() . '/files');
711

712
713
714
    // Reset all statics so that test is performed with a clean environment.
    drupal_static_reset();

715
    // Generate temporary prefixed database to ensure that tests have a clean starting point.
716
    $this->databasePrefix = Database::getConnection()->prefixTables('{simpletest' . mt_rand(1000, 1000000) . '}');
717
718
719
720
721

    // Create test directory.
    $public_files_directory = $this->originalFileDirectory . '/simpletest/' . substr($this->databasePrefix, 10);
    file_prepare_directory($public_files_directory, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS);
    $conf['file_public_path'] = $public_files_directory;
722
723
724
725
726
727
728
729
730
731

    // Clone the current connection and replace the current prefix.
    $connection_info = Database::getConnectionInfo('default');
    Database::renameConnection('default', 'simpletest_original_default');
    foreach ($connection_info as $target => $value) {
      $connection_info[$target]['prefix'] = array(
        'default' => $value['prefix']['default'] . $this->databasePrefix,
      );
    }
    Database::addConnectionInfo('default', 'default', $connection_info['default']);
732

733
    // Set user agent to be consistent with web test case.
734
    $_SERVER['HTTP_USER_AGENT'] = $this->databasePrefix;
735

736
737
738
739
740
741
    // 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']);
742
      module_list(TRUE, FALSE, FALSE, $module_list);
743
    }
744
    $this->setup = TRUE;
745
746
  }

747
  protected function tearDown() {
748
749
750
751
752
753
754
755
756
757
    global $conf;

    // Get back to the original connection.
    Database::removeConnection('default');
    Database::renameConnection('simpletest_original_default', 'default');

    $conf['file_public_path'] = $this->originalFileDirectory;
    // Restore modules if necessary.
    if (isset($this->originalModuleList)) {
      module_list(TRUE, FALSE, FALSE, $this->originalModuleList);
758
759
760
761
762
763
764
765
    }
  }
}

/**
 * Test case for typical Drupal tests.
 */
class DrupalWebTestCase extends DrupalTestCase {
766
767
768
769
770
771
772
  /**
   * The profile to install as a basis for testing.
   *
   * @var string
   */
  protected $profile = 'standard';

773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
  /**
   * 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;

808
809
810
811
812
813
814
  /**
   * The value of the Drupal.settings JavaScript variable for the page currently loaded in the internal browser.
   *
   * @var Array
   */
  protected $drupalSettings;

815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
  /**
   * 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;

851
852
853
854
855
856
857
  /**
   * The original shutdown handlers array, before it was cleaned for testing purposes.
   *
   * @var array
   */
  protected $originalShutdownCallbacks = array();

858
859
860
861
862
  /**
   * HTTP authentication method
   */
  protected $httpauth_method = CURLAUTH_BASIC;

863
864
865
866
867
  /**
   * HTTP authentication credentials (<username>:<password>).
   */
  protected $httpauth_credentials = NULL;

868
869
870
871
872
873
874
875
876
877
  /**
   * The current session name, if available.
   */
  protected $session_name = NULL;

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

878
879
880
881
882
  /**
   * Whether the files were copied to the test files directory.
   */
  protected $generatedTestFiles = FALSE;

883
884
885
886
887
  /**
   * The number of redirects followed during the handling of a request.
   */
  protected $redirect_count;

888
889
890
891
892
893
894
895
  /**
   * Constructor for DrupalWebTestCase.
   */
  function __construct($test_id = NULL) {
    parent::__construct($test_id);
    $this->skipClasses[__CLASS__] = TRUE;
  }

896
897
898
  /**
   * Get a node from the database based on its title.
   *
899
   * @param $title
900
   *   A node title, usually generated by $this->randomName().
901
902
   * @param $reset
   *   (optional) Whether to reset the internal node_load() cache.
903
904
905
906
   *
   * @return
   *   A node object matching $title.
   */
907
908
  function drupalGetNodeByTitle($title, $reset = FALSE) {
    $nodes = node_load_multiple(array(), array('title' => $title), $reset);
909
910
911
912
913
    // Load the first node returned from the database.
    $returned_node = reset($nodes);
    return $returned_node;
  }

914
915
916
  /**
   * Creates a node based on default settings.
   *
917
918
   * @param $settings
   *   An associative array of settings to change from the defaults, keys are
919
   *   node properties, for example 'title' => 'Hello, world!'.
920
921
   * @return
   *   Created node object.
922
   */
923
  protected function drupalCreateNode($settings = array()) {
924
    // Populate defaults array.
925
    $settings += array(
926
      'body'      => array(LANGUAGE_NONE => array(array())),
927
      'title'     => $this->randomName(8),
928
      'comment'   => 2,
929
      'changed'   => REQUEST_TIME,
930
931
932
933
934
935
936
937
      'moderate'  => 0,
      'promote'   => 0,
      'revision'  => 1,
      'log'       => '',
      'status'    => 1,
      'sticky'    => 0,
      'type'      => 'page',
      'revisions' => NULL,
938
      'language'  => LANGUAGE_NONE,
939
    );
940
941
942
943

    // 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');
944
    }
945
946
947
948
949
950
951
952
953
954
955
956
957

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

958
959
960
    // Merge body field value and format separately.
    $body = array(
      'value' => $this->randomName(32),
961
      'format' => filter_default_format(),
962
    );
963
    $settings['body'][$settings['language']][0] += $body;
964

965
    $node = (object) $settings;
966
967
    node_save($node);

968
    // Small hack to link revisions to our test user.
969
970
971
972
    db_update('node_revision')
      ->fields(array('uid' => $node->uid))
      ->condition('vid', $node->vid)
      ->execute();
973
974
975
976
977
978
    return $node;
  }

  /**
   * Creates a custom content type based on default settings.
   *
979
   * @param $settings
980
981
   *   An array of settings to change from the defaults.
   *   Example: 'type' => 'foo'.
982
983
   * @return
   *   Created content type.
984
   */
985
  protected function drupalCreateContentType($settings = array()) {
986
    // Find a non-existent random type name.
987
    do {
988
      $name = strtolower($this->randomName(8));
989
    } while (node_type_get_type($name));
990

991
    // Populate defaults array.
992
993
994
    $defaults = array(
      'type' => $name,
      'name' => $name,
995
      'base' => 'node_content',
996
997
998
999
1000
1001
1002
      'description' => '',
      'help' => '',
      'title_label' => 'Title',
      'body_label' => 'Body',
      'has_title' => 1,
      'has_body' => 1,
    );
1003
    // Imposed values for a custom type.
1004
1005
1006
1007
1008
1009
1010
1011
1012
    $forced = array(
      'orig_type' => '',
      'old_type' => '',
      'module' => 'node',
      'custom' => 1,
      'modified' => 1,
      'locked' => 0,
    );
    $type = $forced + $settings + $defaults;
1013
    $type = (object) $type;
1014

1015
    $saved_type = node_type_save($type);
1016
    node_types_rebuild();
1017
    menu_rebuild();
1018
    node_add_body_field($type);
1019

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

1022
1023
1024
    // Reset permissions so that permissions for this content type are available.
    $this->checkPermissions(array(), TRUE);

1025
1026
1027
1028
1029
1030
    return $type;
  }

  /**
   * Get a list files that can be used in tests.
   *
1031
1032
1033
1034
1035
1036
   * @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.
1037
   */
1038
  protected function drupalGetTestFiles($type, $size = NULL) {
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
    if (empty($this->generatedTestFiles)) {
      // Generate binary test files.
      $lines = array(64, 1024);
      $count = 0;
      foreach ($lines as $line) {
        simpletest_generate_file('binary-' . $count++, 64, $line, 'binary');
      }

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

      // Copy other test files from simpletest.
      $original = drupal_get_path('module', 'simpletest') . '/files';
      $files = file_scan_directory($original, '/(html|image|javascript|php|sql)-.*/');
      foreach ($files as $file) {
1058
        file_unmanaged_copy($file->uri, variable_get('file_public_path', conf_path() . '/files'));
1059
      }
1060

1061
1062
1063
1064
      $this->generatedTestFiles = TRUE;
    }

    $files = array();
1065
1066
    // Make sure type is valid.
    if (in_array($type, array('binary', 'html', 'image', 'javascript', 'php', 'sql', 'text'))) {
1067
      $files = file_scan_directory('public://', '/' . $type . '\-.*/');
1068
1069
1070
1071

      // If size is set then remove any files that are not of that size.
      if ($size !== NULL) {
        foreach ($files as $file) {
1072
          $stats = stat($file->uri);
1073
          if ($stats['size'] != $size) {
1074
            unset($files[$file->uri]);
1075
1076
1077
1078
1079
1080
1081
1082
1083
          }
        }
      }
    }
    usort($files, array($this, 'drupalCompareFiles'));
    return $files;
  }

  /**
1084
   * Compare two files based on size and file name.
1085
   */
1086
  protected function drupalCompareFiles($file1, $file2) {
1087
    $compare_size = filesize($file1->uri) - filesize($file2->uri);
1088
1089
1090
    if ($compare_size) {
      // Sort by file size.
      return $compare_size;
1091
1092
    }
    else {
1093
1094
      // The files were the same size, so sort alphabetically.
      return strnatcmp($file1->name, $file2->name);
1095
1096
1097
1098
    }
  }

  /**
1099
   * Create a user with a given set of permissions.
1100
   *
1101
1102
1103
1104
1105
   * @param array $permissions
   *   Array of permission names to assign to user. Note that the user always
   *   has the default permissions derived from the "authenticated users" role.
   *
   * @return object|false
1106
   *   A fully loaded user object with pass_raw property, or FALSE if account
1107
1108
   *   creation fails.
   */
1109
1110
1111
1112
1113
1114
1115
1116
  protected function drupalCreateUser(array $permissions = array()) {
    // Create a role with the given permission set, if any.
    $rid = FALSE;
    if ($permissions) {
      $rid = $this->drupalCreateRole($permissions);
      if (!$rid) {
        return FALSE;
      }
1117
1118
1119
1120
1121
    }

    // Create a user assigned to that role.
    $edit = array();
    $edit['name']   = $this->randomName();
1122
    $edit['mail']   = $edit['name'] . '@example.com';
1123
1124
    $edit['pass']   = user_password();
    $edit['status'] = 1;
1125
1126
1127
    if ($rid) {
      $edit['roles'] = array($rid => $rid);
    }
1128

1129
    $account = user_save(drupal_anonymous_user(), $edit);
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143

    $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.
   *
1144
1145
   * @param $permissions
   *   Array of permission names to assign to role.
1146
1147
   * @param $name
   *   (optional) String for the name of the role.  Defaults to a random string.
1148
1149
   * @return
   *   Role ID of newly created role, or FALSE if role creation failed.
1150
   */
1151
1152
1153
1154
1155
1156
  protected function drupalCreateRole(array $permissions, $name = NULL) {
    // Generate random name if it was not passed.
    if (!$name) {
      $name = $this->randomName();
    }

1157
    // Check the all the permissions strings are valid.
1158
1159
1160
1161
    if (!$this->checkPermissions($permissions)) {
      return FALSE;
    }

1162
    // Create new role.
1163
1164
1165
    $role = new stdClass();
    $role->name = $name;
    user_role_save($role);
1166
    user_role_grant_permissions($role->rid, $permissions);
1167

1168
    $this->assertTrue(isset($role->rid), t('Created role of name: @name, id: @rid', array('@name' => $name, '@rid' => (isset($role->rid) ? $role->rid : t('-n/a-')))), t('Role'));
1169
    if ($role && !empty($role->rid)) {
1170
      $count = db_query('SELECT COUNT(*) FROM {role_permission} WHERE rid = :rid', array(':rid' => $role->rid))->fetchField();
1171
      $this->assertTrue($count == count($permissions), t('Created permissions: @perms', array('@perms' => implode(', ', $permissions))), t('Role'));
1172
1173
1174
1175
1176
1177
1178
      return $role->rid;
    }
    else {
      return FALSE;
    }
  }

1179
1180
1181
  /**
   * Check to make sure that the array of permissions are valid.
   *
1182
1183
1184
1185
1186
1187
   * @param $permissions
   *   Permissions to check.
   * @param $reset
   *   Reset cached available permissions.
   * @return
   *   TRUE or FALSE depending on whether the permissions are valid.
1188
   */
1189
  protected function checkPermissions(array $permissions, $reset = FALSE) {
1190
    $available = &drupal_static(__FUNCTION__);
1191
1192

    if (!isset($available) || $reset) {
1193
      $available = array_keys(module_invoke_all('permission'));
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
    }

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

1206
  /**
1207
1208
1209
1210
1211
   * 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.
   *
1212
   * Please note that neither the global $user nor the passed-in user object is
1213
1214
1215
   * 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()),
1216
   * e.g. to log in the same user again, then it must be re-assigned manually.
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
   * 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
1227
   *
1228
   * @param $user
1229
   *   User object representing the user to log in.
1230
1231
   *
   * @see drupalCreateUser()
1232
   */
1233
  protected function drupalLogin(stdClass $user) {
1234
    if ($this->loggedInUser) {
1235
1236
1237
1238
1239
1240
1241
1242
1243
      $this->drupalLogout();
    }

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

1244
1245
1246
    // 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'));
1247

1248
1249
1250
    if ($pass) {
      $this->loggedInUser = $user;
    }
1251
1252
  }

1253
1254
1255
1256
1257
  /**
   * Generate a token for the currently logged in user.
   */
  protected function drupalGetToken($value = '') {
    $private_key = drupal_get_private_key();
1258
    return drupal_hmac_base64($value, $this->session_id . $private_key);
1259
1260
  }

1261
1262
1263
  /*
   * Logs a user out of the internal browser, then check the login page to confirm logout.
   */
1264
  protected function drupalLogout() {
1265
1266
1267
    // 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.
1268
1269
    $this->drupalGet('user/logout');
    $this->drupalGet('user');
1270
1271
1272
    $pass = $this->assertField('name', t('Username field found.'), t('Logout'));
    $pass = $pass && $this->assertField('pass', t('Password field found.'), t('Logout'));

1273
1274
1275
    if ($pass) {
      $this->loggedInUser = FALSE