Code coverage for /20081101/modules/simpletest/simpletest.module

Line #Times calledCode
1
<?php
2
// $Id: simpletest.module,v 1.26 2008/11/01 21:21:35 dries Exp $
3
4
/**
5
 * Implementation of hook_help().
6
 */
723
function simpletest_help($path, $arg) {
8
  switch ($path) {
913
    case 'admin/help#simpletest':
100
      $output  = '<p>' . t('The SimpleTest module is a framework for
running automated unit tests in Drupal. It can be used to verify a working
state of Drupal before and after any code changes, or as a means for
developers to write and execute tests for their modules.') .'</p>';
110
      $output .= '<p>' . t('Visit <a href="@admin-simpletest">Administer >>
Site building >> SimpleTest</a> to display a list of available tests. For
comprehensive testing, select <em>all</em> tests, or individually select
tests for more targeted testing. Note that it might take several minutes
for all tests to complete.)', array('@admin-simpletest' =>
url('admin/build/testing'))) .'</p>';
120
      $output .= '<p>' . t('After the tests have run, a message will be
displayed next to each test group indicating whether tests within it
passed, failed, or had exceptions. A pass means that a test returned the
expected results, while fail means that it did not. An exception normally
indicates an error outside of the test, such as a PHP warning or notice. If
there were fails or exceptions, the results are expanded, and the tests
that had issues will be indicated in red or pink rows. Use these results to
refine your code and tests until all tests return a pass.') .'</p>';
130
      $output .= '<p>' . t('For more information on creating and modifying
your own tests, see the <a href="@simpletest-api">SimpleTest API
Documentation</a> in the Drupal handbook.', array('@simpletest-api' =>
'http://drupal.org/simpletest')) .'</p>';
140
      $output .= '<p>' . t('For more information, see the online handbook
entry for <a href="@simpletest">SimpleTest module</a>.',
array('@simpletest' => 'http://drupal.org/handbook/modules/simpletest'))
.'</p>';
150
      return $output;
160
  }
1713
}
18
19
/**
20
 * Implementation of hook_menu().
21
 */
2223
function simpletest_menu() {
231
  $items['admin/build/testing'] = array(
241
    'title' => 'Testing',
251
    'page callback' => 'drupal_get_form',
261
    'page arguments' => array('simpletest_test_form'),
271
    'description' => 'Run tests against Drupal core and your active
modules. These tests help assure that your site code is working as
designed.',
281
    'access arguments' => array('administer unit tests'),
29
  );
301
  return $items;
310
}
32
33
/**
34
 * Implementation of hook_perm().
35
 */
3623
function simpletest_perm() {
37
  return array(
38
    'administer unit tests' => array(
391
      'title' => t('Administer unit tests'),
401
      'description' => t('Manage and run automated testing. %warning',
array('%warning' => t('Warning: Give to trusted roles only; this permission
has security implications.'))),
411
    ),
421
  );
430
}
44
45
/**
46
 * Implemenation of hook_theme().
47
 */
4823
function simpletest_theme() {
49
  return array(
50
    'simpletest_test_table' => array(
512
      'arguments' => array('table' => NULL)
522
    ),
53
    'simpletest_result_summary' => array(
542
      'arguments' => array('form' => NULL)
552
    ),
562
  );
570
}
58
59
/**
60
 * Menu callback for both running tests and listing possible tests
61
 */
6223
function simpletest_test_form() {
636
  global $db_prefix, $db_prefix_original;
64
656
  $form = array();
66
67
  // List out all tests in groups for selection.
686
  $uncategorized_tests = simpletest_get_all_tests();
696
  $tests = simpletest_categorize_tests($uncategorized_tests);
706
  $selected_tests = array();
71
726
  if (isset($_SESSION['test_id'])) {
73
    // Select all results using the active test ID used to group them.
742
    $results = db_query("SELECT * FROM {simpletest} WHERE test_id = %d
ORDER BY test_class, message_id", $_SESSION['test_id']);
75
76
    $summary = array(
772
      '#theme' => 'simpletest_result_summary',
782
      '#pass' => 0,
792
      '#fail' => 0,
802
      '#exception' => 0,
812
      '#weight' => -10,
822
    );
832
    $form['summary'] = $summary;
842
    $form['results'] = array();
852
    $group_summary = array();
86
    $map = array(
872
      'pass' => theme('image', 'misc/watchdog-ok.png'),
882
      'fail' => theme('image', 'misc/watchdog-error.png'),
892
      'exception' => theme('image', 'misc/watchdog-warning.png'),
902
    );
912
    $header = array(t('Message'), t('Group'), t('Filename'), t('Line'),
t('Function'), array('colspan' => 2, 'data' => t('Status')));
922
    while ($result = db_fetch_object($results)) {
932
      $class = $result->test_class;
942
      $info = $uncategorized_tests[$class]->getInfo();
952
      $group = $info['group'];
962
      $selected_tests[$group][$class] = TRUE;
972
      if (!isset($group_summary[$group])) {
982
        $group_summary[$group] = $summary;
992
      }
1002
      $element = &$form['results'][$group][$class];
1012
      if (!isset($element)) {
1022
        $element['summary'] = $summary;
1032
      }
1042
      $status = $result->status;
105
      // This reporter can only handle pass, fail and exception.
1062
      if (isset($map[$status])) {
1072
        $element['#title'] = $info['name'];
1082
        $status_index = '#'. $status;
1092
        $form['summary'][$status_index]++;
1102
        $group_summary[$group][$status_index]++;
1112
        $element['summary'][$status_index]++;
1122
        $element['result_table']['#rows'][] = array(
113
          'data' => array(
1142
            $result->message,
1152
            $result->message_group,
1162
            basename($result->file),
1172
            $result->line,
1182
            $result->function,
1192
            $map[$status],
1202
          ),
1212
          'class' => "simpletest-$status",
122
        );
1232
      }
1242
      unset($element);
1252
    }
126
127
    // Clear test results.
1282
    if (variable_get('simpletest_clear_results', TRUE)) {
1292
      db_query('DELETE FROM {simpletest} WHERE test_id = %d',
$_SESSION['test_id']);
1302
      db_query('DELETE FROM {simpletest_test_id} WHERE test_id = %d',
$_SESSION['test_id']);
1312
    }
1322
    unset($_SESSION['test_id']);
133
1342
    $all_ok = TRUE;
1352
    foreach ($form['results'] as $group => &$elements) {
1362
      $group_ok = TRUE;
1372
      foreach ($elements as $class => &$element) {
1382
        $info = $uncategorized_tests[$class]->getInfo();
1392
        $ok = $element['summary']['#fail'] +
$element['summary']['#exception'] == 0;
140
        $element += array(
1412
          '#type' => 'fieldset',
1422
          '#collapsible' => TRUE,
1432
          '#collapsed' => $ok,
1442
          '#description' => $info['description'],
1450
        );
1462
        $element['result_table']['#markup'] = theme('table', $header,
$element['result_table']['#rows']);
1472
        $element['summary']['#ok'] = $ok;
1482
        $group_ok = $group_ok && $ok;
1492
      }
150
      $elements += array(
1512
        '#type' => 'fieldset',
1522
        '#title' => $group,
1532
        '#collapsible' => TRUE,
1542
        '#collapsed' => $group_ok,
1552
        'summary' => $group_summary[$group],
1560
      );
1572
      $elements['summary']['#ok'] = $group_ok;
1582
      $all_ok = $group_ok && $all_ok;
1592
    }
1602
    $form['summary']['#ok'] = $all_ok;
1612
  }
1626
  $form['tests'] = array(
1636
    '#type' => 'fieldset',
1646
    '#title' => t('Tests'),
1656
    '#description' => t('Select the tests you would like to run, and click
Run tests.'),
166
  );
1676
  $form['tests']['table'] = array(
168
    '#theme' => 'simpletest_test_table'
1696
    );
1706
  foreach ($tests as $group_name => $test_group) {
1716
    $form['tests']['table'][$group_name] = array(
1726
      '#collapsed' => TRUE,
173
    );
1746
    foreach ($test_group as $test) {
1756
      $test_info = $test->getInfo();
1766
      $test_class = get_class($test);
1776
      $is_selected = isset($selected_tests[$group_name][$test_class]);
1786
      $form['tests']['table'][$group_name][$test_class] = array(
1796
        '#type' => 'checkbox',
1806
        '#title' => $test_info['name'],
1816
        '#default_value' => $is_selected,
1826
        '#description' => $test_info['description'],
183
      );
1846
      if ($is_selected) {
1852
        $form['tests']['table'][$group_name]['#collapsed'] = FALSE;
1862
      }
1876
    }
1886
  }
189
190
  // Action buttons.
1916
  $form['tests']['op'] = array(
1926
    '#type' => 'submit',
1936
    '#value' => t('Run tests'),
194
  );
1956
  $form['reset'] = array(
1966
    '#type' => 'fieldset',
1976
    '#collapsible' => FALSE,
1986
    '#collapsed' => FALSE,
1996
    '#title' => t('Clean test environment'),
2006
    '#description' => t('Remove tables with the prefix "simpletest" and
temporary directories that are left over from tests that crashed. This is
intended for developers when creating tests.'),
201
  );
2026
  $form['reset']['op'] = array(
2036
    '#type' => 'submit',
2046
    '#value' => t('Clean environment'),
2056
    '#submit' => array('simpletest_clean_environment'),
206
  );
207
2086
  return $form;
2090
}
210
21123
function theme_simpletest_test_table($table) {
2124
  drupal_add_css(drupal_get_path('module', 'simpletest') .
'/simpletest.css');
2134
  drupal_add_js(drupal_get_path('module', 'simpletest') . '/simpletest.js',
'module');
214
215
  // Create header for test selection table.
216
  $header = array(
2174
    theme('table_select_header_cell'),
2184
    array('data' => t('Test'), 'class' => 'simpletest_test'),
2194
    array('data' => t('Description'), 'class' => 'simpletest_description'),
2204
  );
221
222
  // Define the images used to expand/collapse the test groups.
223
  $js = array(
224
    'images' => array(
2254
      theme('image', 'misc/menu-collapsed.png', 'Expand', 'Expand'),
2264
      theme('image', 'misc/menu-expanded.png', 'Collapsed', 'Collapsed'),
2274
    ),
2284
  );
229
230
  // Go through each test group and create a row.
2314
  $rows = array();
2324
  foreach (element_children($table) as $key) {
2334
    $element = &$table[$key];
2344
    $row = array();
235
236
    // Make the class name safe for output on the pace by replacing all
237
    // non-word/decimal characters with a dash (-).
2384
    $test_class = strtolower(trim(preg_replace("/[^\w\d]/", "-", $key)));
239
240
    // Select the right "expand"/"collapse" image, depending on whether the
241
    // category is expanded (at least one test selected) or not.
2424
    $collapsed = !empty($element['#collapsed']);
2434
    $image_index = $collapsed ? 0 : 1;
244
245
    // Place-holder for checkboxes to select group of tests.
2464
    $row[] = array('id' => $test_class, 'class' =>
'simpletest-select-all');
247
248
    // Expand/collapse image and group title.
2494
    $row[] = array(
2504
      'data' =>  '<div class="simpletest-image" id="simpletest-test-group-'
. $test_class . '"></div>&nbsp;' .
2514
                 '<label for="' . $test_class . '-select-all"
class="simpletest-group-label">' . $key . '</label>',
252
      'style' => 'font-weight: bold;'
2534
      );
254
2554
      $row[] = isset($element['#description']) ? $element['#description'] :
'&nbsp;';
2564
      $rows[] = array('data' => $row, 'class' => 'simpletest-group');
257
258
      // Add individual tests to group.
259
      $current_js = array(
2604
        'testClass' => $test_class . '-test',
2614
        'testNames' => array(),
2624
        'imageDirection' => $image_index,
2634
        'clickActive' => FALSE,
2644
      );
2654
      foreach (element_children($element) as $test_name) {
2664
        $test = $element[$test_name];
2674
        $row = array();
268
2694
        $current_js['testNames'][] = 'edit-' . $test_name;
270
271
        // Store test title and description so that checkbox won't render
them.
2724
        $title = $test['#title'];
2734
        $description = $test['#description'];
274
2754
        unset($test['#title']);
2764
        unset($test['#description']);
277
278
        // Test name is used to determine what tests to run.
2794
        $test['#name'] = $test_name;
280
2814
        $row[] = drupal_render($test);
2824
        $row[] = theme('indentation', 1) . '<label for="edit-' . $test_name
. '">' . $title . '</label>';
2834
        $row[] = '<div class="description">' . $description . '</div>';
2844
        $rows[] = array('data' => $row, 'class' => $test_class . '-test' .
($collapsed ? ' js-hide' : ''));
2854
      }
2864
      $js['simpletest-test-group-'. $test_class] = $current_js;
2874
      unset($table[$key]);
2884
  }
289
290
  // Add js array of settings.
2914
  drupal_add_js(array('simpleTest' => $js), 'setting');
292
2934
  if (empty($rows)) {
2940
    return '<strong>' . t('No tests to display.') . '</strong>';
2950
  }
296
  else {
2974
    return theme('table', $header, $rows, array('id' =>
'simpletest-form-table'));
298
  }
2990
}
300
30123
function theme_simpletest_result_summary($form, $text = NULL) {
3022
  return '<div class="simpletest-'. ($form['#ok'] ? 'pass' : 'fail') .'">'
. _simpletest_format_summary_line($form) . '</div>';
3030
}
304
30523
function _simpletest_format_summary_line($summary) {
3064
  return t('@pass, @fail, and @exception', array(
3074
    '@pass' => format_plural(isset($summary['#pass']) ? $summary['#pass'] :
0, '1 pass', '@count passes'),
3084
    '@fail' => format_plural(isset($summary['#fail']) ? $summary['#fail'] :
0, '1 fail', '@count fails'),
3094
    '@exception' => format_plural(isset($summary['#exception']) ?
$summary['#exception'] : 0, '1 exception', '@count exceptions'),
3104
  ));
3110
}
312
313
/**
314
 * Run selected tests.
315
 */
31623
function simpletest_test_form_submit($form, &$form_state) {
317
  // Ensure that all classes are loaded before we create instances to get
test information and run.
3182
  simpletest_get_all_tests();
319
320
  // Get list of tests.
3212
  $tests_list = array();
3222
  foreach ($form_state['values'] as $class_name => $value) {
3232
    if (class_exists($class_name) && $value === 1) {
3242
      $tests_list[] = $class_name;
3252
    }
3262
  }
3272
  if (count($tests_list) > 0 ) {
3282
    simpletest_run_tests($tests_list, 'drupal');
3290
  }
330
  else {
3310
    drupal_set_message(t('No test(s) selected.'), 'error');
332
  }
3330
}
334
335
/**
336
 * Actually runs tests.
337
 *
338
 * @param $test_list
339
 *   List of tests to run.
340
 * @param $reporter
341
 *   Which reporter to use. Allowed values are: text, xml, html and drupal,
342
 *   drupal being the default.
34323
 */
3442
function simpletest_run_tests($test_list, $reporter = 'drupal') {
3452
  global $db_prefix, $db_prefix_original;
3462
  cache_clear_all();
347
  $test_id =
db_insert('simpletest_test_id')->useDefaults(array('test_id'))->execute();
348
3492
  // Get the info for the first test being run.
350
  $first_test = array_shift($test_list);
3512
  $first_instance = new $first_test();
3522
  array_unshift($test_list, $first_test);
3532
  $info = $first_instance->getInfo();
3542
3552
  $batch = array(
3562
    'title' => t('Running SimpleTests'),
3572
    'operations' => array(
3582
      array('_simpletest_batch_operation', array($test_list, $test_id)),
3592
    ),
360
    'finished' => '_simpletest_batch_finished',
361
    'redirect' => 'admin/build/testing',
362
    'progress_message' => '',
363
    'css' => array(drupal_get_path('module', 'simpletest') .
'/simpletest.css'),
364
    'js' => array(drupal_get_path('module', 'simpletest')
.'/simpletest.js'),
365
    'init_message' => t('Processing test @num of @max - %test.',
array('%test' => $info['name'], '@num' => '1', '@max' =>
count($test_list))),
3662
  );
3670
  batch_set($batch);
368
  // Normally, the forms portion of the batch API takes care of calling
369
  // batch_process(), but in the process it saves the whole $form into the
370
  // database (which is huge for the test selection form).
371
  // By calling batch_process() directly, we skip that behavior and ensure
37223
  // that we don't exceed the size of data that can be sent to the database
373
  // (max_allowed_packet on MySQL).
3744
  batch_process();
375
}
376
3774
/**
378
 * Batch operation callback.
3794
 */
3804
function _simpletest_batch_operation($test_list_init, $test_id, &$context)
{
3814
  // Ensure that all classes are loaded before we unserialize some
instances.
3824
  simpletest_get_all_tests();
383
384
  // Get working values.
3850
  if (!isset($context['sandbox']['max'])) {
3860
    // First iteration: initialize working values.
387
    $test_list = $test_list_init;
3884
    $context['sandbox']['max'] = count($test_list);
389
    $test_results = array('#pass' => 0, '#fail' => 0, '#exception' => 0);
390
  }
3914
  else {
3924
    // Nth iteration: get the current values where we last stored them.
393135
    $test_list = $context['sandbox']['tests'];
3942
    $test_results = $context['sandbox']['test_results'];
3952
  }
396
  $max = $context['sandbox']['max'];
397
3982
  // Perform the next test.
3992
  $test_class = array_shift($test_list);
4002
  $test = new $test_class($test_id);
4012
  $test->run();
4022
  $size = count($test_list);
4032
  $info = $test->getInfo();
4042
4052
  // Gather results and compose the report.
4062
  $test_results[$test_class] = $test->_results;
4072
  foreach ($test_results[$test_class] as $key => $value) {
4082
    $test_results[$key] += $value;
4092
  }
410
  $test_results[$test_class]['#name'] = $info['name'];
411
  $items = array();
412
  foreach (element_children($test_results) as $class) {
4132
    array_unshift($items, '<div class="simpletest-' .
($test_results[$class]['#fail'] + $test_results[$class]['#exception'] ?
'fail' : 'pass') . '">' . t('@name: @summary', array('@name' =>
$test_results[$class]['#name'], '@summary' =>
_simpletest_format_summary_line($test_results[$class]))) . '</div>');
4142
  }
415
  $context['message'] = t('Processed test @num of @max - %test.',
array('%test' => $info['name'], '@num' => $max - $size, '@max' => $max));
4162
  $context['message'] .= '<div class="simpletest-' .
($test_results['#fail'] + $test_results['#exception'] ? 'fail' : 'pass') .
'">Overall results: ' . _simpletest_format_summary_line($test_results) .
'</div>';
417
  $context['message'] .= theme('item_list', $items);
418
4192
  // Save working values for the next iteration.
4202
  $context['sandbox']['tests'] = $test_list;
421
  $context['sandbox']['test_results'] = $test_results;
42223
  // The test_id is the only thing we need to save for the report page.
4232
  $context['results']['test_id'] = $test_id;
4242
4252
  // Multistep processing: report progress.
4262
  $context['finished'] = 1 - $size / $max;
427
}
4280
429
function _simpletest_batch_finished($success, $results, $operations) {
4302
  if (isset($results['test_id'])) {
431
    $_SESSION['test_id'] = $results['test_id'];
432
  }
433
  if ($success) {
434
    drupal_set_message(t('The tests have finished running.'));
435
  }
436
  else {
437
    drupal_set_message(t('The tests did not successfully finish.'),
'error');
438
  }
43923
}
44010
44110
/**
44210
 * Get a list of all of the tests.
44310
 *
44410
 * @return
44510
 *   An array of tests, with the class name as the keys and the
instantiated
44610
 *   versions of the classes as the values.
44710
 */
44810
function simpletest_get_all_tests() {
44910
  static $formatted_classes;
450
  if (!isset($formatted_classes)) {
45110
    require_once DRUPAL_ROOT . '/' . drupal_get_path('module',
'simpletest') . '/drupal_web_test_case.php';
45210
    $files = array();
45310
    foreach (array_keys(module_rebuild_cache()) as $module) {
45410
      $module_path = drupal_get_path('module', $module);
45510
      $test = $module_path . "/$module.test";
45610
      if (file_exists($test)) {
45710
        $files[] = $test;
458
      }
45910
46010
      $tests_directory = $module_path . '/tests';
46110
      if (is_dir($tests_directory)) {
46210
        foreach (file_scan_directory($tests_directory, '/\.test$/') as
$file) {
46310
          $files[] = $file->filename;
46410
        }
46510
      }
46610
    }
46710
46810
    $existing_classes = get_declared_classes();
46910
    foreach ($files as $file) {
47010
      include_once DRUPAL_ROOT . '/' . $file;
47110
    }
4720
    $classes = array_values(array_diff(get_declared_classes(),
$existing_classes));
4730
    $formatted_classes = array();
4740
    foreach ($classes as $key => $class) {
47510
      if (method_exists($class, 'getInfo')) {
4760
        $formatted_classes[$class] = new $class;
477
      }
478
    }
479
  }
480
  if (count($formatted_classes) == 0) {
481
    drupal_set_message('No test cases found.', 'error');
482
    return FALSE;
483
  }
484
  return $formatted_classes;
48523
}
4866
4876
/**
4886
 * Categorize the tests into groups.
4896
 *
4906
 * @param $tests
4916
 *   A list of tests from simpletest_get_all_tests.
4926
 * @see simpletest_get_all_tests.
4930
 */
494
function simpletest_categorize_tests($tests) {
495
  $groups = array();
496
  foreach ($tests as $test => $instance) {
497
    $info = $instance->getInfo();
49823
    $groups[$info['group']][$test] = $instance;
4990
  }
5000
  uksort($groups, 'strnatcasecmp');
5010
  return $groups;
5020
}
503
504
/**
505
 * Remove all temporary database tables and directories.
506
 */
50723
function simpletest_clean_environment() {
5080
  simpletest_clean_database();
5090
  simpletest_clean_temporary_directories();
5100
  simpletest_clean_results_table();
5110
}
5120
5130
/**
514
 * Removed prefixed talbes from the database that are left over from
crashed tests.
5150
 */
5160
function simpletest_clean_database() {
5170
  global $db_prefix;
518
  $tables =
db_find_tables(Database::getActiveConnection()->prefixTables('{simpletest}')
. '%');
5190
  $schema = drupal_get_schema_unprocessed('simpletest');
520
  $ret = array();
5210
  foreach (array_diff_key($tables, $schema) as $table) {
522
    // Strip $db_prefix and skip tables without digits following
"simpletest",
523
    // e.g. {simpletest_tets_id}.
524
    if (preg_match('/simpletest\d+.*/', $table, $matches)) {
525
      db_drop_table($ret, $matches[0]);
52623
    }
5270
  }
5280
5290
  if (count($ret) > 0) {
5300
    drupal_set_message(t('Removed @count left over tables.', array('@count'
=> count($ret))));
5310
  }
5320
  else {
5330
    drupal_set_message(t('No left over tables to remove.'));
5340
  }
5350
}
536
5370
/**
5380
 * Find all left over temporary directories and remove them.
5390
 */
540
function simpletest_clean_temporary_directories() {
5410
  $files = scandir(file_directory_path());
542
  $count = 0;
5430
  foreach ($files as $file) {
544
    $path = file_directory_path() . '/' . $file;
545
    if (is_dir($path) && preg_match('/^simpletest\d+/', $file)) {
546
      simpletest_clean_temporary_directory($path);
547
      $count++;
548
    }
549
  }
55023
551134
  if ($count > 0) {
552134
    drupal_set_message(t('Removed @count temporary directories.',
array('@count' => $count)));
553134
  }
554134
  else {
555134
    drupal_set_message(t('No temporary directories to remove.'));
5563
  }
5573
}
558
559134
/**
560
 * Remove all files from specified firectory and then remove directory.
561134
 *
562134
 * @param string $path Directory path.
563134
 */
564134
function simpletest_clean_temporary_directory($path) {
565
  $files = scandir($path);
566
  foreach ($files as $file) {
567
    if ($file != '.' && $file != '..') {
568
      $file_path = "$path/$file";
56923
      if (is_dir($file_path)) {
5700
        simpletest_clean_temporary_directory($file_path);
5710
      }
572
      else {
573
        file_unmanaged_delete($file_path);
5740
      }
5750
    }
576
  }
5770
  rmdir($path);
5780
}
5790
58023
/**
581
 * Clear the test results tables.
582
 */
583
function simpletest_clean_results_table() {
584
  if (variable_get('simpletest_clear_results', TRUE)) {
585
    $count = db_result(db_query('SELECT COUNT(test_id) FROM
{simpletest_test_id}'));
586
587
    // Clear test results.
588
    db_query('DELETE FROM {simpletest}');
589
    db_query('DELETE FROM {simpletest_test_id}');
590
591
    drupal_set_message(t('Removed @count test results.', array('@count' =>
$count)));
592
  }
593
}
594