Index: modules/simpletest/simpletest.css =================================================================== RCS file: /cvs/drupal/drupal/modules/simpletest/simpletest.css,v retrieving revision 1.2 diff -u -r1.2 simpletest.css --- modules/simpletest/simpletest.css 24 Jun 2008 21:51:02 -0000 1.2 +++ modules/simpletest/simpletest.css 29 Aug 2008 19:02:11 -0000 @@ -1,11 +1,9 @@ /* $Id: simpletest.css,v 1.2 2008/06/24 21:51:02 dries Exp $ */ -/* Addon for the simpletest module */ -#simpletest { -} /* Test Table */ th.simpletest_run { width: 50px; + padding-right: 0.5em; } th.simpletest_test { width: 160px; @@ -17,46 +15,55 @@ } table#simpletest-form-table tr td { - background-color: white !important; + background-color: white; } table#simpletest-form-table tr.simpletest-group td { - background-color: #EDF5FA !important; + background-color: #EDF5FA; } -div.simpletest-pass { - color: #33a333; +table#simpletest-form-table thead .form-item { + margin-bottom:0pt; + margin-top:0pt; + white-space:nowrap; } -div.simpletest-fail { - color: #a30000; +div.simpletest-image { + display: inline; } -tr.simpletest-pass.odd { - background: #b6ffb6; +div.simpletest-image, label.simpletest-group-label { + cursor: pointer; } -tr.simpletest-pass.even { - background: #9bff9b; +.simpletest-pass { + color: #316d31; } -tr.simpletest-fail.odd { - background: #ffc9c9; +.simpletest-fail { + color: #981010; } -tr.simpletest-fail.even { - background: #ffacac; +.simpletest-exception { + color: #bda400; } -tr.simpletest-exception.odd { - background: #f4ea71; +div.message > div.item-list { + font-weight: normal; } -tr.simpletest-exception.even { - background: #f5e742; +li.simpletest-important { + font-weight: bold; } -div.simpletest-image { - display: inline; +ul.simpletest-results li div.item-list ul { + padding-left: 20px; +} + +ul.simpletest-results li span.simpletest-overview { cursor: pointer; } + +ul.simpletest-results li span.simpletest-overview:hover { + text-decoration: underline; +} \ No newline at end of file Index: modules/simpletest/simpletest.test =================================================================== RCS file: /cvs/drupal/drupal/modules/simpletest/simpletest.test,v retrieving revision 1.6 diff -u -r1.6 simpletest.test --- modules/simpletest/simpletest.test 22 Aug 2008 12:35:55 -0000 1.6 +++ modules/simpletest/simpletest.test 29 Aug 2008 19:02:11 -0000 @@ -112,14 +112,13 @@ * Confirm that the stub test produced the desired results. */ function confirmStubTestResults() { - $this->assertAssertion($this->pass, 'Other', 'Pass'); - $this->assertAssertion($this->fail, 'Other', 'Fail'); - - $this->assertAssertion(t('Created permissions: @perms', array('@perms' => $this->valid_permission)), 'Role', 'Pass'); - $this->assertAssertion(t('Invalid permission %permission.', array('%permission' => $this->invalid_permission)), 'Role', 'Fail'); + $info = $this->getInfo(); + $name = $info['name']; + $this->assertAssertion($this->pass, $name); + $this->assertAssertion($this->fail, $name); - $this->test_ids[] = $test_id = $this->getTestIdFromResults(); - $this->assertTrue($test_id, t('Found test ID in results.')); + $this->assertAssertion(t('Created permissions: @perms', array('@perms' => $this->valid_permission)), 'Role'); + $this->assertAssertion(t('Invalid permission %permission.', array('%permission' => $this->invalid_permission)), 'Role'); } /** @@ -143,18 +142,17 @@ * @param string $status Assertion status. * @return Assertion result. */ - function assertAssertion($message, $type, $status) { + function assertAssertion($message, $type) { $message = trim(strip_tags($message)); $found = FALSE; - foreach ($this->results['assertions'] as $assertion) { + foreach ($this->results as $assertion) { if ($assertion['message'] == $message && - $assertion['type'] == $type && - $assertion['status'] == $status) { + $assertion['type'] == $type) { $found = TRUE; break; } } - return $this->assertTrue($found, t('Found assertion {"@message", "@type", "@status"}.', array('@message' => $message, '@type' => $type, '@status' => $status))); + return $this->assertTrue($found, t('Found assertion {"@message", "@type"}.', array('@message' => $message, '@type' => $type))); } /** @@ -164,20 +162,13 @@ $results = array(); if ($this->parse()) { - if ($fieldset = $this->getResultFieldSet()) { - // Code assumes this is the only test in group. - $results['summary'] = $this->asText($fieldset->div); - $results['name'] = $this->asText($fieldset->fieldset->legend); - - $results['assertions'] = array(); - $tbody = $fieldset->fieldset->table->tbody; - foreach ($tbody->tr as $row) { + if ($list = $this->getResultList()) { + $results = array(); + foreach ($list->li as $li) { $assertion = array(); - $assertion['message'] = $this->asText($row->td[0]); - $assertion['type'] = $this->asText($row->td[1]); - $ok_url = (url('misc/watchdog-ok.png') == 'misc/watchdog-ok.png') ? 'misc/watchdog-ok.png' : (base_path() . 'misc/watchdog-ok.png'); - $assertion['status'] = ($row->td[5]->img['src'] == $ok_url) ? 'Pass' : 'Fail'; - $results['assertions'][] = $assertion; + $assertion['message'] = $this->asText($li->span); + $assertion['type'] = str_replace('Group: ', '', $this->asText($li->div->ul->li[0])); + $results[] = $assertion; } } } @@ -190,11 +181,10 @@ * @return fieldset containing the results for group this test is in. */ function getResultFieldSet() { - $fieldsets = $this->xpath('//fieldset'); - $info = $this->getInfo(); - foreach ($fieldsets as $fieldset) { - if ($fieldset->legend == $info['group']) { - return $fieldset; + $list = $this->elements->xpath('//li'); + foreach ($list as $li) { + if (strpos($this->asText($li->span), t('SimpleTest: ')) === 0) { + return $li->div->ul->li->div->ul; } } return FALSE; Index: modules/simpletest/simpletest.install =================================================================== RCS file: /cvs/drupal/drupal/modules/simpletest/simpletest.install,v retrieving revision 1.7 diff -u -r1.7 simpletest.install --- modules/simpletest/simpletest.install 16 Aug 2008 20:57:14 -0000 1.7 +++ modules/simpletest/simpletest.install 29 Aug 2008 19:02:11 -0000 @@ -177,25 +177,25 @@ 'default' => '', 'description' => t('The message group this message belongs to. For example: warning, browser, user.'), ), - 'caller' => array( + 'function' => array( 'type' => 'varchar', 'length' => 255, 'not null' => TRUE, 'default' => '', - 'description' => t('Name of the caller function or method that created this message.'), + 'description' => t('Name of the assertion function or method that created this message.'), ), 'line' => array( 'type' => 'int', 'not null' => TRUE, 'default' => 0, - 'description' => t('Line number of the caller.'), + 'description' => t('Line number on which the function is called.'), ), 'file' => array( 'type' => 'varchar', 'length' => 255, 'not null' => TRUE, 'default' => '', - 'description' => t('Name of the file where the caller is.'), + 'description' => t('Name of the file where the function is called.'), ), ), 'primary key' => array('message_id'), Index: modules/simpletest/drupal_web_test_case.php =================================================================== RCS file: /cvs/drupal/drupal/modules/simpletest/drupal_web_test_case.php,v retrieving revision 1.34 diff -u -r1.34 drupal_web_test_case.php --- modules/simpletest/drupal_web_test_case.php 23 Aug 2008 07:42:54 -0000 1.34 +++ modules/simpletest/drupal_web_test_case.php 29 Aug 2008 19:02:11 -0000 @@ -43,56 +43,72 @@ * The message string. * @param $group * WHich group this assert belongs to. - * @param $custom_caller - * By default, the assert comes from a function which names start with - * 'test'. Instead, you can specify where this assert originates from - * by passing in an associative array as $custom_caller. Key 'file' is - * the name of the source file, 'line' is the line number and 'function' - * is the caller function itself. */ - protected function _assert($status, $message = '', $group = 'Other', $custom_caller = NULL) { + protected function _assert($status, $message = '', $group = 'Other') { global $db_prefix; + + // Convert boolean status to string status. if (is_bool($status)) { $status = $status ? 'pass' : 'fail'; } + + // Incrament summary result counter. $this->_results['#' . $status]++; - if (!isset($custom_caller)) { - $callers = debug_backtrace(); - array_shift($callers); - foreach ($callers as $function) { - if (substr($function['function'], 0, 6) != 'assert' && $function['function'] != 'pass' && $function['function'] != 'fail') { - break; - } - } - } - else { - $function = $custom_caller; - } + + // Get the function information about the call to the assertion method. + $function = $this->getAssertionCall(); + + // Switch to non-testing database to store results in. $current_db_prefix = $db_prefix; $db_prefix = $this->db_prefix_original; - db_insert('simpletest')->fields(array( + + // Creation assertion array to be displayed white tests are running. + $this->_assertions[] = $assertion = array( 'test_id' => $this->test_id, - 'test_class' => get_class($this), - 'status' => $status, - 'message' => substr($message, 0, 255), // Some messages are too long for the database. - 'message_group' => $group, - 'caller' => $function['function'], - 'line' => $function['line'], - 'file' => $function['file'], - ))->execute(); - $this->_assertions[] = array( + 'test_class' => get_class($this), 'status' => $status, - 'message' => $message, + 'message' => substr($message, 0, 255), // Some messages are too long for the database. 'group' => $group, 'function' => $function['function'], 'line' => $function['line'], 'file' => $function['file'], ); + + // Store assertion for display after the test has completed. + // Change the 'group' attribute to 'message_group' to avoid reserved word + // conflict when inserted into database. + $assertion['message_group'] = $assertion['group']; + unset($assertion['group']); + db_insert('simpletest')->fields($assertion)->execute(); + + // Return to testing prefix. $db_prefix = $current_db_prefix; return $status; } /** + * Cycle through backtrace until the first non-assertion method is found. + * Return the previous function call to the assertion method. + * + * @return + * Array representing a backtrace element. + */ + protected function getAssertionCall() { + $backtrace = debug_backtrace(); + array_shift($backtrace); // Remove call to getCaller(). + array_shift($backtrace); // Remove call to _assert(). + + foreach ($backtrace as $trace) { + if (substr($trace['function'], 0, 6) != 'assert' && + $trace['function'] != 'pass' && + $trace['function'] != 'fail') { + return $function; + } + $function = $trace; // Keep track of previous trace element. + } + } + + /** * Check to see if a value is not false (not an empty string, 0, NULL, or FALSE). * * @param $value @@ -263,11 +279,9 @@ * The message to display along with the assertion. * @param $group * The type of assertion - examples are "Browser", "PHP". - * @param $custom_caller - * The caller of the error. */ - protected function error($message = '', $group = 'Other', $custom_caller = NULL) { - return $this->_assert('exception', $message, $group, $custom_caller); + protected function error($message = '', $group = 'Other') { + return $this->_assert('exception', $message, $group); } /** @@ -732,7 +746,6 @@ // Close the CURL handler. $this->curlClose(); - restore_error_handler(); } } @@ -807,7 +820,7 @@ // them. @$htmlDom = DOMDocument::loadHTML($this->_content); if ($htmlDom) { - $this->assertTrue(TRUE, t('Valid HTML found on "@path"', array('@path' => $this->getUrl())), t('Browser')); + $this->pass(t('Valid HTML found on "@path"', array('@path' => $this->getUrl())), t('Browser')); // 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); @@ -1288,7 +1301,7 @@ * TRUE on pass, FALSE on fail. */ function assertText($text, $message = '', $group = 'Other') { - return $this->assertTextHelper($text, $message, $group = 'Other', FALSE); + return $this->assertTextHelper($text, $message, $group, FALSE); } /** Index: modules/simpletest/simpletest.module =================================================================== RCS file: /cvs/drupal/drupal/modules/simpletest/simpletest.module,v retrieving revision 1.11 diff -u -r1.11 simpletest.module --- modules/simpletest/simpletest.module 21 Aug 2008 19:36:38 -0000 1.11 +++ modules/simpletest/simpletest.module 29 Aug 2008 19:02:11 -0000 @@ -26,6 +26,14 @@ 'page arguments' => array('simpletest_test_form'), 'description' => 'Run tests against Drupal core and your active modules. These tests help assure that your site code is working as designed.', 'access arguments' => array('administer unit tests'), + 'type' => MENU_NORMAL_ITEM, + ); + $items['admin/build/testing/results'] = array( + 'title' => 'Results', + 'page callback' => 'simpletest_display_results', + 'description' => 'View the test results.', + 'access arguments' => array('administer unit tests'), + 'type' => MENU_CALLBACK, ); return $items; } @@ -50,110 +58,34 @@ 'simpletest_result_summary' => array( 'arguments' => array('form' => NULL) ), + 'simpletest_results' => array( + 'arguments' => array('results' => NULL, 'assertions' => NULL), + ), ); } /** - * Menu callback for both running tests and listing possible tests + * Menu callback -- List all tests that can be run. */ function simpletest_test_form() { global $db_prefix, $db_prefix_original; + $form = array(); + $form['run_all'] = array( + '#type' => 'checkbox', + '#default_value' => 0, + '#attributes' => array('class' => 'simpletest-master'), + ); + + // List out all tests in groups for selection. $uncategorized_tests = simpletest_get_all_tests(); $tests = simpletest_categorize_tests($uncategorized_tests); - if (isset($_SESSION['test_id'])) { - // Select all results using the active test ID used to group them. - $results = db_query("SELECT * FROM {simpletest} WHERE test_id = %d ORDER BY test_class, message_id", $_SESSION['test_id']); - - $summary = array( - '#theme' => 'simpletest_result_summary', - '#pass' => 0, - '#fail' => 0, - '#exception' => 0, - '#weight' => -10, - ); - $form['summary'] = $summary; - $form['results'] = array(); - $group_summary = array(); - $map = array( - 'pass' => theme('image', 'misc/watchdog-ok.png'), - 'fail' => theme('image', 'misc/watchdog-error.png'), - 'exception' => theme('image', 'misc/watchdog-warning.png'), - ); - $header = array(t('Message'), t('Group'), t('Filename'), t('Line'), t('Function'), array('colspan' => 2, 'data' => t('Status'))); - while ($result = db_fetch_object($results)) { - $class = $result->test_class; - $info = $uncategorized_tests[$class]->getInfo(); - $group = $info['group']; - if (!isset($group_summary[$group])) { - $group_summary[$group] = $summary; - } - $element = &$form['results'][$group][$class]; - if (!isset($element)) { - $element['summary'] = $summary; - } - $status = $result->status; - // This reporter can only handle pass, fail and exception. - if (isset($map[$status])) { - $element['#title'] = $info['name']; - $status_index = '#'. $status; - $form['summary'][$status_index]++; - $group_summary[$group][$status_index]++; - $element['summary'][$status_index]++; - $element['result_table']['#rows'][] = array( - 'data' => array( - $result->message, - $result->message_group, - basename($result->file), - $result->line, - $result->caller, - $map[$status], - ), - 'class' => "simpletest-$status", - ); - } - unset($element); - } - // Clear test results. - if (variable_get('simpletest_clear_results', TRUE)) { - db_query('DELETE FROM {simpletest} WHERE test_id = %d', $_SESSION['test_id']); - db_query('DELETE FROM {simpletest_test_id} WHERE test_id = %d', $_SESSION['test_id']); - } - unset($_SESSION['test_id']); - - $all_ok = TRUE; - foreach ($form['results'] as $group => &$elements) { - $group_ok = TRUE; - foreach ($elements as $class => &$element) { - $info = $uncategorized_tests[$class]->getInfo(); - $ok = $element['summary']['#fail'] + $element['summary']['#exception'] == 0; - $element += array( - '#type' => 'fieldset', - '#collapsible' => TRUE, - '#collapsed' => $ok, - '#description' => $info['description'], - ); - $element['result_table']['#markup'] = theme('table', $header, $element['result_table']['#rows']); - $element['summary']['#ok'] = $ok; - $group_ok = $group_ok && $ok; - } - $elements += array( - '#type' => 'fieldset', - '#title' => $group, - '#collapsible' => TRUE, - '#collapsed' => $group_ok, - 'summary' => $group_summary[$group], - ); - $elements['summary']['#ok'] = $group_ok; - $all_ok = $group_ok && $all_ok; - } - $form['summary']['#ok'] = $all_ok; - } foreach ($tests as $group_name => $test_group) { foreach ($test_group as $test) { $test_info = $test->getInfo(); $test_class = get_class($test); + $form['tests'][$group_name][$test_class] = array( '#type' => 'checkbox', '#title' => $test_info['name'], @@ -163,32 +95,16 @@ } } - $form['run'] = array( - '#type' => 'fieldset', - '#collapsible' => FALSE, - '#collapsed' => FALSE, - '#title' => t('Run tests'), - ); - $form['run']['running_options'] = array( - '#type' => 'radios', - '#default_value' => 'selected_tests', - '#options' => array( - 'all_tests' => t('Run all tests (WARNING, this may take a long time)'), - 'selected_tests' => t('Run selected tests'), - ), + // Action buttons. + $form['submit'] = array( + '#prefix' => '
', + '#suffix' => '
', ); - $form['run']['op'] = array( + $form['submit']['run'] = array( '#type' => 'submit', '#value' => t('Run tests'), ); - $form['reset'] = array( - '#type' => 'fieldset', - '#collapsible' => FALSE, - '#collapsed' => FALSE, - '#title' => t('Clean test environment'), - '#description' => t('Remove tables with the prefix "simpletest" and temporary directories that are left over from tests that crashed.') - ); - $form['reset']['op'] = array( + $form['submit']['reset'] = array( '#type' => 'submit', '#value' => t('Clean environment'), '#submit' => array('simpletest_clean_environment') @@ -202,13 +118,18 @@ * @ingroup themeable */ function theme_simpletest_test_form($form) { - drupal_add_css(drupal_get_path('module', 'simpletest') .'/simpletest.css', 'module'); - drupal_add_js(drupal_get_path('module', 'simpletest') .'/simpletest.js', 'module'); + drupal_add_css(drupal_get_path('module', 'simpletest') . '/simpletest.css', 'module'); + drupal_add_js(drupal_get_path('module', 'simpletest') . '/simpletest.js', 'module'); + + // Create header for test selection table. $header = array( - array('data' => t('Run'), 'class' => 'simpletest_run checkbox'), + array('data' => drupal_render($form['run_all']), 'class' => 'simpletest_run checkbox'), array('data' => t('Test'), 'class' => 'simpletest_test'), array('data' => t('Description'), 'class' => 'simpletest_description'), ); + unset($form['run_all']); + + // Define the images used to expand/collapse the test groups. $js = array( 'images' => array( theme('image', 'misc/menu-collapsed.png', 'Expand', 'Expand'), @@ -216,61 +137,147 @@ ), ); - // Go through each test group and create a row: + // Go through each test group and create a row. $rows = array(); foreach (element_children($form['tests']) as $key) { $element = &$form['tests'][$key]; - $test_class = strtolower(trim(preg_replace("/[^\w\d]/", "-", $key))); $row = array(); + + // Make the class name safe for output on the pace by replacing all + // non-word/decimal characters with a dash (-). + $test_class = strtolower(trim(preg_replace("/[^\w\d]/", "-", $key))); + + // Place-holder for checkboxes to select group of tests. $row[] = array('id' => $test_class, 'class' => 'simpletest-select-all'); + + // Expand/collapse image and group title. $row[] = array( - 'data' => '
'. $js['images'][0] .'
 ', + 'data' => '
' . $js['images'][0] . '
 ' . + '', 'style' => 'font-weight: bold;' ); + $row[] = isset($element['#description']) ? $element['#description'] : ' '; $rows[] = array('data' => $row, 'class' => 'simpletest-group'); - $current_js = array('testClass' => $test_class .'-test', 'testNames' => array(), 'imageDirection' => 0, 'clickActive' => FALSE); + // Add individual tests to group. + $current_js = array('testClass' => $test_class . '-test', 'testNames' => array(), 'imageDirection' => 0, 'clickActive' => FALSE); foreach (element_children($element) as $test_name) { - $current_js['testNames'][] = 'edit-'. $test_name; $test = $element[$test_name]; - foreach (array('title', 'description') as $key) { - $$key = $test['#'. $key]; - unset($test['#'. $key]); - } - $test['#name'] = $test_name; - $themed_test = drupal_render($test); $row = array(); - $row[] = $themed_test; - $row[] = theme('indentation', 1) .''; - $row[] = '
'. $description .'
'; - $rows[] = array('data' => $row, 'style' => 'display: none;', 'class' => $test_class .'-test'); + + $current_js['testNames'][] = 'edit-' . $test_name; + + // Store test title and description so that checkbox won't render them. + $title = $test['#title']; + $description = $test['#description']; + + unset($test['#title']); + unset($test['#description']); + + // Test name is used to determine what tests to run. + $test['#name'] = $test_name; + + $row[] = drupal_render($test); + $row[] = theme('indentation', 1) . ''; + $row[] = '
' . $description . '
'; + $rows[] = array('data' => $row, 'style' => 'display: none;', 'class' => $test_class . '-test'); } $js['simpletest-test-group-'. $test_class] = $current_js; } + + // Remove already rendered tests. unset($form['tests']); + + // Add js array of settings. drupal_add_js(array('simpleTest' => $js), 'setting'); - // Output test groups: - $output = ''; - if (isset($form['results'])) { - $output .= drupal_render($form['summary']); - $output .= drupal_render($form['results']); + + $output = '

' . t('Select the tests you would like to run, and click Run tests. The Clean environment + button will remove any database tables or files that might be left over from interrupted tests.') . '

'; + if (empty($rows)) { + $output .= '' . t('No tests to display.') . ''; } - if (count($rows)) { + else { $output .= theme('table', $header, $rows, array('id' => 'simpletest-form-table')); } - // Output the rest of the form, excluded test groups which have been removed: + + // Output the rest of the form, excluded tests which have been removed. $output .= drupal_render($form); return $output; } -function theme_simpletest_result_summary($form, $text = NULL) { - return '
' . _simpletest_format_summary_line($form) . '
'; +function simpletest_display_results() { + if (!isset($_SESSION['test_id'])) { + drupal_set_message(t('No results to display.')); + drupal_goto('admin/build/testing'); + } + + // Ensure that all classes are loaded before we create instances to get test information. + simpletest_get_all_tests(); + + drupal_add_css(drupal_get_path('module', 'simpletest') . '/simpletest.css', 'module'); // TODO + drupal_add_js(drupal_get_path('module', 'simpletest') . '/simpletest.js', 'module'); + + $result = db_query("SELECT * FROM {simpletest} WHERE test_id = %d ORDER BY test_class, message_id", $_SESSION['test_id']); + + $results = array(); + $assertions = array(); + $classes = array(); + while ($assertion = db_fetch_object($result)) { + $class = $assertion->test_class; + + // Get the summary results, only once per test class. + if (empty($results[$class])) { + // Instantiate class and get class information. + $test = new $class(); + $info = $test->getInfo(); + + // Init summary results counters. + $results[$class] = array('#fail' => 0, '#pass' => 0, '#exception' => 0); + + // Store class info for use when themeing. + foreach ($info as $key => $value) { + $results[$class]['#' . $key] = $value; + } + } + + // Store test class for use in re-run form. + if (!in_array($assertion->test_class, $classes)) { + $classes[] = $assertion->test_class; + } + + // Update summary counters. + $results[$class]['#' . $assertion->status]++; + + // Store assertion in nested array after converting 'message_group' to + // 'group' to avoid database field conflict with reserved word. + $assertion = (array) $assertion; + $assertion['group'] = $assertion['message_group']; + unset($assertion['message_group']); + + $assertions[$class][] = $assertion; + + // Update the state of the results for class, if any non-passes then test + // results are not "ok" and should be expanded by default. + $results[$class]['#ok'] = !($results[$class]['#fail'] + $results[$class]['#exception']); + } + +// // Clear test results. +// if (variable_get('simpletest_clear_results', TRUE)) { +// db_query('DELETE FROM {simpletest} WHERE test_id = %d', $_SESSION['test_id']); +// db_query('DELETE FROM {simpletest_test_id} WHERE test_id = %d', $_SESSION['test_id']); +// } +// unset($_SESSION['test_id']); + + return '

' . t('The assertions listed in bold text are those made by the tests themselves, rather than the implicit framework tests.') . '

' . + drupal_get_form('simpletest_filter_form') . + theme('simpletest_results', $results, $assertions) . + drupal_get_form('simpletest_rerun_form', $classes); } function _simpletest_format_summary_line($summary) { - return t('@pass, @fail, @exception', array( + return t('@pass, @fail, and @exception', array( '@pass' => format_plural(isset($summary['#pass']) ? $summary['#pass'] : 0, '1 pass', '@count passes'), '@fail' => format_plural(isset($summary['#fail']) ? $summary['#fail'] : 0, '1 fail', '@count fails'), '@exception' => format_plural(isset($summary['#exception']) ? $summary['#exception'] : 0, '1 exception', '@count exceptions'), @@ -281,17 +288,20 @@ * Run selected tests. */ function simpletest_test_form_submit($form, &$form_state) { - $output = ''; - $batch_mode = !preg_match("/^simpletest\d+$/", $_SERVER['HTTP_USER_AGENT']); - $tests_list = array(); - $run_all_tests = $form_state['values']['running_options'] == 'all_tests'; + // Ensure that all classes are loaded before we create instances to get test information and run. simpletest_get_all_tests(); + + // Get list of tests. + $tests_list = array(); foreach ($form_state['values'] as $class_name => $value) { - if (class_exists($class_name) && ($value === 1 || $run_all_tests)) { + if (class_exists($class_name) && $value === 1) { $tests_list[] = $class_name; } } - if (count($tests_list) > 0 ) { + + // If there are selected tests then run them. + if (!empty($tests_list)) { + $batch_mode = !preg_match("/^simpletest\d+$/", $_SERVER['HTTP_USER_AGENT']); simpletest_run_tests($tests_list, 'drupal', $batch_mode); } else { @@ -315,16 +325,20 @@ $test_id = db_insert('simpletest_test_id')->useDefaults(array('test_id'))->execute(); if ($batch_mode) { + $first_test = array_shift($test_list); + $first_instance = new $first_test(); + array_unshift($test_list, $first_test); + $info = $first_instance->getInfo(); $batch = array( 'title' => t('Running SimpleTests'), 'operations' => array( array('_simpletest_batch_operation', array($test_list, $test_id)), ), 'finished' => '_simpletest_batch_finished', - 'redirect' => 'admin/build/testing', - 'progress_message' => t('Processing tests.'), + 'progress_message' => '', 'css' => array(drupal_get_path('module', 'simpletest') .'/simpletest.css'), - 'init_message' => t('SimpleTest is initializing...') . ' ' . format_plural(count($test_list), "one test case will run.", "@count test cases will run."), + 'js' => array(drupal_get_path('module', 'simpletest') .'/simpletest.js'), + 'init_message' => t('Processing test @num of @max - %test.', array('%test' => $info['name'], '@num' => '1', '@max' => count($test_list))), ); batch_set($batch); } @@ -356,6 +370,7 @@ // Nth iteration: get the current values where we last stored them. $test_list = $context['sandbox']['tests']; $test_results = $context['sandbox']['test_results']; + $test_assertions = $context['sandbox']['test_assertions']; } $max = $context['sandbox']['max']; @@ -371,19 +386,27 @@ foreach ($test_results[$test_class] as $key => $value) { $test_results[$key] += $value; } - $test_results[$test_class]['#name'] = $info['name']; - $items = array(); - foreach (element_children($test_results) as $class) { - $items[] = '
' . t('@name: @summary', array('@name' => $test_results[$class]['#name'], '@summary' => _simpletest_format_summary_line($test_results[$class]))) . '
'; + $test_assertions[$test_class] = $test->_assertions; + foreach ($info as $key => $value) { + $test_results[$test_class]['#' . $key] = $value; + } + $test_results[$test_class]['#ok'] = !($test_results['#fail'] + $test_results['#exception']); + if (!empty($test_list)) { + $current_test_class = array_shift($test_list); + $instance = new $current_test_class(); + $current_info = $instance->getInfo(); + $context['message'] = t('Processing test @num of @max - %test.', array('%test' => $current_info['name'], '@num' => $max - count($test_list), '@max' => $max)); + array_unshift($test_list, $current_test_class); } - $message = t('Processed test @num of @max - %test.', array('%test' => $info['name'], '@num' => $max - $size, '@max' => $max)); - $message .= theme('item_list', $items); - $context['message'] = $message; - // TODO: Do we want a summary of all? + else { + $context['message'] = t('Finished processing all tests.'); + } + $context['message'] .= theme_simpletest_results($test_results, $test_assertions); // Save working values for the next iteration. $context['sandbox']['tests'] = $test_list; $context['sandbox']['test_results'] = $test_results; + $context['sandbox']['test_assertions'] = $test_assertions; // The test_id is the only thing we need to save for the report page. $context['results']['test_id'] = $test_id; @@ -392,13 +415,16 @@ } function _simpletest_batch_finished($success, $results, $operations) { - $_SESSION['test_id'] = $results['test_id']; + if (isset($results['test_id'])) { + $_SESSION['test_id'] = $results['test_id']; + } if ($success) { drupal_set_message(t('The tests have finished running.')); } else { drupal_set_message(t('The tests did not successfully finish.'), 'error'); } + drupal_goto('admin/build/testing/results'); } /** @@ -465,6 +491,196 @@ } /** + * Theme simpletest results. + * + * @param $results + * A nested array of simpletest results. + * @param $assertions + * An array of assertions for each test. + * @return + * A nested list of simpletest items. + */ +function theme_simpletest_results($results, $assertions) { + $items = array(); + $group_data = array(); + $totals = array( + '#fail' => 0, + '#pass' => 0, + '#exception' => 0, + ); + foreach (element_children($results) as $class) { + $totals['#fail'] += $results[$class]['#fail']; + $totals['#pass'] += $results[$class]['#pass']; + $totals['#exception'] += $results[$class]['#exception']; + $item = array(); + foreach ($results[$class] as $key => $value) { + if ($key == '#ok') { + $group_data[$results[$class]['#group']][$key] = isset($group_data[$results[$class]['#group']][$key]) ? $group_data[$results[$class]['#group']][$key] && $value : $value; + } + else { + $group_data[$results[$class]['#group']][$key] = isset($group_data[$results[$class]['#group']][$key]) ? $group_data[$results[$class]['#group']][$key] + $value : $value; + } + } + $item['class'] = 'simpletest-container ' . ($results[$class]['#fail'] ? 'simpletest-fail expanded' : ($results[$class]['#exception'] ? 'simpletest-exception expanded' : 'simpletest-pass collapsed')); + $item['data'] = '' . t('@name: @summary', array('@name' => $results[$class]['#name'], '@summary' => _simpletest_format_summary_line($results[$class]))) . ''; + $assertion_items = array(); + foreach ($assertions[$class] as $assertion) { + // The idea of importancy rests upon the fact that the testing framework + // makes many assertions in its normal process of making GET and POST + // requests, as well as many other normal procedures. We will treat + // assertions actually initiated by the test itself as far more likely to + // be important, and thus highlight it in bold text. + $important = FALSE; + if ($assertion['group'] == 'Other') { + $assertion['group'] = $results[$class]['#name']; + $important = TRUE; + } + + $css_class = simpletest_get_assertion_css_class($assertion, $important); + $assertion_items[] = array( + 'data' => t('[@status] !message', + array('@status' => simpletest_get_status_message($assertion), + '!message' => $assertion['message'])), + 'class' => 'collapsed ' . $css_class, + 'children' => array( + array( + 'data' => t('Group: @group', array('@group' => $assertion['group'])), + 'class' => 'leaf ' . $css_class, + ), + array( + 'data' => t('File: @file', array('@file' => basename($assertion['file']))), + 'class' => 'leaf ' . $css_class, + ), + array( + 'data' => t('Function: @function', array('@function' => $assertion['function'])), + 'class' => 'leaf ' . $css_class, + ), + array( + 'data' => t('Line: @line', array('@line' => $assertion['line'])), + 'class' => 'leaf ' . $css_class, + ), + ), + ); + } + $item['children'] = $assertion_items; + $items[$results[$class]['#group']]['children'][] = $item; + $items[$results[$class]['#group']]['data'] = '' . t('@name: @summary', array('@name' => $results[$class]['#group'], '@summary' => _simpletest_format_summary_line($group_data[$results[$class]['#group']]))) . ''; + $items[$results[$class]['#group']]['class'] = 'simpletest-container ' . ($group_data[$results[$class]['#group']]['#fail'] ? 'simpletest-fail expanded' : ($group_data[$results[$class]['#group']]['#exception'] ? 'simpletest-exception expanded' : 'simpletest-pass collapsed')); + } + $output = ''; + $output .= '

'; + $output .= t('Overall results: @summary', array('@summary' => _simpletest_format_summary_line($totals))); + $output .= '

'; + $output .= theme('item_list', $items, NULL, 'ul', array('class' => 'simpletest-results')); + return $output; +} + +/** + * Create a filter form used on the results page. + */ +function simpletest_filter_form() { + $form = array(); + $form['outcome'] = array( + '#type' => 'checkboxes', + '#title' => t('Filter by outcome'), + '#options' => array(), + '#attributes' => array('class' => 'container-inline'), + '#default_value' => array(), + ); + $categories = array('pass' => t('Passes'), 'fail' => t('Fails'), 'exception' => t('Exceptions')); + foreach ($categories as $outcome => $readable) { + $form['outcome'][$outcome] = array( + '#type' => 'checkbox', + '#title' => $readable, + '#attributes' => array('class' => 'simpletest-filter'), + '#default_value' => TRUE, + ); + } + return $form; +} + +/** + * Create a form containing the last run test classes so that they can bee run + * again. + */ +function simpletest_rerun_form($form_state, array $test_classes) { + $form = array(); + $form['test_classes'] = array( + '#type' => 'hidden', + '#default_value' => implode(',', $test_classes), + ); + $form['actions'] = array( + '#prefix' => '
', + '#suffix' => '
', + ); + $form['actions']['submit'] = array( + '#type' => 'submit', + '#value' => t('Re-run tests'), + ); + $form['actions']['cancel'] = array( + '#markup' => l(t('Return to testing overview page'), 'admin/build/testing'), + ); + return $form; +} + +/** + * Re-run selected tests. + */ +function simpletest_rerun_form_submit($form, &$form_state) { + // Simulate the checkboxes on the test select form by setting each of the + // test classes as a value in the form state. + foreach (explode(',', $form_state['values']['test_classes']) as $test_class) { + $form_state['values'][$test_class] = 1; + } + + // Use the existing submit handler to run the tests. + simpletest_test_form_submit($form, $form_state); +} + +/** + * Get the appropriate css class based on the assertions status and importance. + * + * @param $assertion Assertion item aray. + * @param $important Importance of assertion. + * @return CSS class. + */ +function simpletest_get_assertion_css_class($assertion, $important) { + switch ($assertion['status']) { + case 'pass': + $css_class = 'simpletest-pass'; + break; + case 'fail': + $css_class = 'simpletest-fail'; + break; + case 'exception': + $css_class = 'simpletest-exception'; + break; + } + if ($important) { + $css_class .= ' simpletest-important'; + } + return $css_class; +} + +/** + * Get the status message text + * + * @param $assertion Assertion item aray. + * @return Status message text. + */ +function simpletest_get_status_message($assertion) { + switch ($assertion['status']) { + case 'pass': + return t('PASS'); + case 'fail': + return t('FAIL'); + case 'exception': + return t('EXCEPTION'); + } + return t('UNKNOWN'); +} + +/** * Remove all temporary database tables and directories. */ function simpletest_clean_environment() { Index: modules/simpletest/simpletest.js =================================================================== RCS file: /cvs/drupal/drupal/modules/simpletest/simpletest.js,v retrieving revision 1.2 diff -u -r1.2 simpletest.js --- modules/simpletest/simpletest.js 23 Apr 2008 18:50:49 -0000 1.2 +++ modules/simpletest/simpletest.js 29 Aug 2008 19:02:11 -0000 @@ -1,12 +1,15 @@ // $Id: simpletest.js,v 1.2 2008/04/23 18:50:49 dries Exp $ +/** + * Add the cool table collapsing on the testing overview page. + */ Drupal.behaviors.simpleTestMenuCollapse = function() { // Adds expand-collapse functionality. $('div.simpletest-image').click(function() { // Toggle all of the trs. if (!Drupal.settings.simpleTest[$(this).attr('id')].clickActive) { Drupal.settings.simpleTest[$(this).attr('id')].clickActive = true; - var trs = $(this).parents('tbody').children().filter('.'+ Drupal.settings.simpleTest[$(this).attr('id')].testClass), trs_formatted = [], direction = Drupal.settings.simpleTest[$(this).attr('id')].imageDirection, self = $(this); + var trs = $(this).parents('tbody').children().filter('.' + Drupal.settings.simpleTest[$(this).attr('id')].testClass), trs_formatted = [], direction = Drupal.settings.simpleTest[$(this).attr('id')].imageDirection, self = $(this); for (var i = 0; i < trs.length; i++) { trs_formatted.push(trs[i]); } @@ -21,12 +24,21 @@ Drupal.settings.simpleTest[self.attr('id')].clickActive = false; } } - toggleTrs(trs_formatted, (direction? 'pop' : 'shift'), (direction? 'fadeOut' : 'fadeIn')); + toggleTrs(trs_formatted, (direction ? 'pop' : 'shift'), (direction ? 'fadeOut' : 'fadeIn')); Drupal.settings.simpleTest[$(this).attr('id')].imageDirection = !direction; $(this).html(Drupal.settings.simpleTest.images[(direction? 0 : 1)]); } }); + $('label.simpletest-group-label').click(function() { + $(this).siblings('div.simpletest-image').click(); + return false; + }); } + +/** + * Select/deselect all the inner checkboxes when the outer checkboxes are + * selected/deselected. + */ Drupal.behaviors.simpleTestSelectAll = function() { $('td.simpletest-select-all').each(function() { var checkboxes = Drupal.settings.simpleTest['simpletest-test-group-'+ $(this).attr('id')].testNames, totalCheckboxes = 0, @@ -35,11 +47,11 @@ for (var i = 0; i < checkboxes.length; i++) { $('#'+ checkboxes[i]).attr('checked', checked); } - self.data('simpletest-checked-tests', (checked? checkboxes.length : 0)); + self.data('simpletest-checked-tests', (checked ? checkboxes.length : 0)); }).data('simpletest-checked-tests', 0); var self = $(this); for (var i = 0; i < checkboxes.length; i++) { - if ($('#'+ checkboxes[i]).change(function() { + if ($('#' + checkboxes[i]).change(function() { if (checkbox.attr('checked') == 'checked') { checkbox.attr('checked', ''); } @@ -49,7 +61,7 @@ checkbox.attr('checked', 'checked'); } else { - checkbox.attr('checked', ''); + checkbox.removeAttr('checked'); } }).attr('checked') == 'checked') { totalCheckboxes++; @@ -60,4 +72,69 @@ } $(this).append(checkbox); }); + $('input.simpletest-master:checkbox').click(function() { + if ($(this).is(':checked')) { + $('#simpletest-form-table input:checkbox,input.simpletest-master:checkbox').attr('checked', 'checked'); + } + else { + $('#simpletest-form-table input:checkbox,input.simpletest-master:checkbox').removeAttr('checked'); + } + $('#simpletest-form-table input:checkbox:not(.simpletest-master)').change(function() { + if ($('#simpletest-form-table input:checkbox:not(:checked):not(.simpletest-master)').length) { + $('input.simpletest-master:checkbox').removeAttr('checked'); + } + else if ($('#simpletest-form-table input:checkbox:not(.simpletest-master)').length == $('#simpletest-form-table input:checkbox:checked:not(.simpletest-master)').length) { + $('input.simpletest-master:checkbox').attr('checked', 'checked'); + } + }); + }); +}; + +/** + * Filter the results based on their outcome. + */ +Drupal.behaviors.simpleTestFilterForm = function() { + var updateResults = function(type, checked) { + $('li.simpletest-' + type + ':not(.simpletest-container)')[checked ? 'show' : 'hide'](); + $('li.simpletest-container:not(:has(li.simpletest-container))').each(function() { + var children = $(this).find('li:visible:not(.simpletest-container)').is('.simpletest-pass,.simpletest-fail,.simpletest-exception'); + $(this)[children ? 'show' : 'hide'](); + }); + } + $('#edit-outcome-pass').change(function() { + updateResults('pass', $(this).is(':checked')); + }); + $('#edit-outcome-fail').change(function() { + updateResults('fail', $(this).is(':checked')); + }); + $('#edit-outcome-exception').change(function() { + updateResults('exception', $(this).is(':checked')); + }); + updateResults('pass', $('#edit-outcome-pass').is(':checked')); + updateResults('fail', $('#edit-outcome-fail').is(':checked')); + updateResults('exception', $('#edit-outcome-exception').is(':checked')); +}; + +/** + * The results should collapse when you click on them, of course. + */ +Drupal.behaviors.simpleTestResultsCollapse = function() { + $('li > span.simpletest-overview').click(function() { + $(this).siblings().children('ul').toggle().toggleClass('simpletest-hidden').parent().parent().toggleClass('collapsed').toggleClass('expanded'); + }); + $('li.collapsed > span.simpletest-overview').each(function() { + $(this).siblings().children('ul').hide().addClass('simpletest-hidden'); + }); +}; + +/** + * Batch update function - re-attaches javascript behaviors to the new elements + * now injected into the HTML. + */ +Drupal.batchUpdate.simpleTestResultsCollapse = function() { + $('li > span.simpletest-overview').click(function() { + $(this).siblings().children('ul').toggle().toggleClass('simpletest-hidden').parent().parent().toggleClass('collapsed').toggleClass('expanded'); + }).each(function() { + $(this).siblings().children('ul').hide().addClass('simpletest-hidden').parent().parent().removeClass('expanded').addClass('collapsed'); + }); }; \ No newline at end of file Index: includes/batch.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/batch.inc,v retrieving revision 1.20 diff -u -r1.20 batch.inc --- includes/batch.inc 24 Jun 2008 21:51:02 -0000 1.20 +++ includes/batch.inc 29 Aug 2008 19:02:09 -0000 @@ -22,11 +22,14 @@ // Register database update for end of processing. register_shutdown_function('_batch_shutdown'); - // Add batch-specific css. + // Add batch-specific css and javascript. foreach ($batch['sets'] as $batch_set) { foreach ($batch_set['css'] as $css) { drupal_add_css($css); } + foreach ($batch_set['js'] as $js) { + drupal_add_js($js); + } } $op = isset($_REQUEST['op']) ? $_REQUEST['op'] : ''; @@ -251,8 +254,11 @@ '@total' => $total, '@current' => floor($current), '@percentage' => $percentage, - ); - $message = strtr($progress_message, $values) . '
'; + ); + $message = strtr($progress_message, $values); + if (!empty($message)) { + $message .= '
'; + } $message .= $task_message ? $task_message : ' '; return array($percentage, $message); Index: includes/form.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/form.inc,v retrieving revision 1.281 diff -u -r1.281 form.inc --- includes/form.inc 17 Aug 2008 11:08:23 -0000 1.281 +++ includes/form.inc 29 Aug 2008 19:02:10 -0000 @@ -2421,6 +2421,7 @@ 'progress_message' => $t('Remaining @remaining of @total.'), 'error_message' => $t('An error has occurred.'), 'css' => array(), + 'js' => array(), ); $batch_set = $init + $batch_definition + $defaults; Index: misc/drupal.js =================================================================== RCS file: /cvs/drupal/drupal/misc/drupal.js,v retrieving revision 1.45 diff -u -r1.45 drupal.js --- misc/drupal.js 25 Jun 2008 07:45:03 -0000 1.45 +++ misc/drupal.js 29 Aug 2008 19:02:10 -0000 @@ -1,6 +1,6 @@ // $Id: drupal.js,v 1.45 2008/06/25 07:45:03 dries Exp $ -var Drupal = Drupal || { 'settings': {}, 'behaviors': {}, 'themes': {}, 'locale': {} }; +var Drupal = Drupal || { 'settings': {}, 'behaviors': {}, 'themes': {}, 'locale': {}, 'batchUpdate': {} }; /** * Set the variable that indicates if JavaScript behaviors should be applied Index: misc/batch.js =================================================================== RCS file: /cvs/drupal/drupal/misc/batch.js,v retrieving revision 1.4 diff -u -r1.4 batch.js --- misc/batch.js 21 Oct 2007 18:59:01 -0000 1.4 +++ misc/batch.js 29 Aug 2008 19:02:10 -0000 @@ -16,6 +16,9 @@ // Success: redirect to the summary. var updateCallback = function (progress, status, pb) { + for (behavior in Drupal.batchUpdate) { + Drupal.batchUpdate[behavior](progress, status, pb); + } if (progress == 100) { pb.stopMonitoring(); window.location = uri+'&op=finished'; Index: modules/contact/contact.test =================================================================== RCS file: /cvs/drupal/drupal/modules/contact/contact.test,v retrieving revision 1.8 diff -u -r1.8 contact.test --- modules/contact/contact.test 2 Aug 2008 05:16:47 -0000 1.8 +++ modules/contact/contact.test 29 Aug 2008 19:02:10 -0000 @@ -219,7 +219,7 @@ // Get role id (rid) for specified role. $rid = db_result(db_query("SELECT rid FROM {role} WHERE name = '%s'", array($role))); if ($rid === FALSE) { - $this->fail(t(' [permission] Role "' . $role . '" not found.')); + $this->fail(t('Role "@role" not found.', array('@role' => $role)), t('Permission')); } // Create edit array from permission. @@ -229,7 +229,7 @@ } $this->drupalPost('admin/user/permissions', $edit, t('Save permissions')); - $this->assertText(t('The changes have been saved.'), t(' [permission] Saved changes.')); + $this->assertText(t('The changes have been saved.'), t('Saved changes.'), t('Permission')); } }