diff --git a/includes/common.inc b/includes/common.inc index 81e2ca1cd68..3a00beea457 100644 --- a/includes/common.inc +++ b/includes/common.inc @@ -627,6 +627,35 @@ function drupal_error_handler($errno, $message, $filename, $line, $context) { } } +/** + * Gets the last caller (file name and line of the call, function in which the + * call originated) from a backtrace. + * + * @param $backtrace + * A standard PHP backtrace. + * @return + * An associative array with keys 'file', 'line' and 'function'. + */ +function _drupal_get_last_caller($backtrace) { + // The first trace is the call itself. + // It gives us the line and the file of the last call. + $call = $backtrace[0]; + + // The second call give us the function where the call originated. + if (isset($backtrace[1])) { + if (isset($backtrace[1]['class'])) { + $call['function'] = $backtrace[1]['class'] . $backtrace[1]['type'] . $backtrace[1]['function'] . '()'; + } + else { + $call['function'] = $backtrace[1]['function'] . '()'; + } + } + else { + $call['function'] = 'main()'; + } + return $call; +} + function _fix_gpc_magic(&$item) { if (is_array($item)) { array_walk($item, '_fix_gpc_magic'); diff --git a/modules/simpletest/drupal_web_test_case.php b/modules/simpletest/drupal_web_test_case.php index d06b5731ce9..f27cee60a94 100644 --- a/modules/simpletest/drupal_web_test_case.php +++ b/modules/simpletest/drupal_web_test_case.php @@ -43,55 +43,75 @@ class DrupalWebTestCase { * The message string. * @param $group * WHich group this assert belongs to. - * @param $custom_caller + * @param $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 + * by passing in an associative array as $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', $caller = NULL) { global $db_prefix; + + // Convert boolean status to string status. if (is_bool($status)) { $status = $status ? 'pass' : 'fail'; } + + // Increment 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. + if (!$caller) { + $caller = $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 that can be displayed while 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, - 'group' => $group, - 'function' => $function['function'], - 'line' => $function['line'], - 'file' => $function['file'], + 'message_group' => $group, + 'function' => $caller['function'], + 'line' => $caller['line'], + 'file' => $caller['file'], ); + + // Store assertion for display after the test has completed. + db_insert('simpletest')->fields($assertion)->execute(); + + // Return to testing prefix. $db_prefix = $current_db_prefix; return $status; } + /** + * 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. + // We skip calls that occured in one of the methods of DrupalWebTestCase + // or in an assertion function. + while (($caller = $backtrace[1]) && + ((isset($caller['class']) && $caller['class'] == 'DrupalWebTestCase') || + substr($caller['function'], 0, 6) == 'assert')) { + // We remove that call. + array_shift($backtrace); + } + + return _drupal_get_last_caller($backtrace); + } + /** * Check to see if a value is not false (not an empty string, 0, NULL, or FALSE). * @@ -263,11 +283,11 @@ class DrupalWebTestCase { * The message to display along with the assertion. * @param $group * The type of assertion - examples are "Browser", "PHP". - * @param $custom_caller + * @param $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', $caller = NULL) { + return $this->_assert('exception', $message, $group, $caller); } /** @@ -281,8 +301,13 @@ class DrupalWebTestCase { // If the current method starts with "test", run it - it's a test. if (strtolower(substr($method, 0, 4)) == 'test') { $this->setUp(); - $this->$method(); - // Finish up. + try { + $this->$method(); + // Finish up. + } + catch (Exception $e) { + $this->exceptionHandler($e); + } $this->tearDown(); } } @@ -308,15 +333,28 @@ class DrupalWebTestCase { E_USER_NOTICE => 'User notice', E_RECOVERABLE_ERROR => 'Recoverable error', ); - $this->error($message, $error_map[$severity], array( - 'function' => '', - 'line' => $line, - 'file' => $file, - )); + + $backtrace = debug_backtrace(); + $this->error($message, $error_map[$severity], _drupal_get_last_caller($backtrace)); } return TRUE; } + /** + * Handle exceptions. + * + * @see set_exception_handler + */ + function exceptionHandler($exception) { + $backtrace = $exception->getTrace(); + // Push on top of the backtrace the call that generated the exception. + array_unshift($backtrace, array( + 'line' => $exception->getLine(), + 'file' => $exception->getFile(), + )); + $this->error($exception->getMessage(), 'Uncaught exception', _drupal_get_last_caller($backtrace)); + } + /** * Creates a node based on default settings. * @@ -405,7 +443,7 @@ class DrupalWebTestCase { node_types_rebuild(); $this->assertEqual($saved_type, SAVED_NEW, t('Created content type %type.', array('%type' => $type->type))); - + // Reset permissions so that permissions for this content type are available. $this->checkPermissions(array(), TRUE); @@ -645,7 +683,7 @@ class DrupalWebTestCase { // Generate temporary prefixed database to ensure that tests have a clean starting point. $db_prefix = 'simpletest' . mt_rand(1000, 1000000); - + include_once './includes/install.inc'; drupal_install_system(); @@ -659,7 +697,7 @@ class DrupalWebTestCase { // stale data for the previous run's database prefix and all // calls to it will fail. drupal_get_schema(NULL, TRUE); - + // Run default profile tasks. $task = 'profile'; default_profile_tasks($task, ''); @@ -732,7 +770,6 @@ class DrupalWebTestCase { // Close the CURL handler. $this->curlClose(); - restore_error_handler(); } } @@ -807,7 +844,7 @@ class DrupalWebTestCase { // 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); @@ -1290,7 +1327,7 @@ class DrupalWebTestCase { * 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); } /** diff --git a/modules/simpletest/simpletest.install b/modules/simpletest/simpletest.install index 443f0e3753c..895e92c1d58 100644 --- a/modules/simpletest/simpletest.install +++ b/modules/simpletest/simpletest.install @@ -177,25 +177,25 @@ function simpletest_schema() { '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'), diff --git a/modules/simpletest/simpletest.module b/modules/simpletest/simpletest.module index 3decbfe7611..dec43853fbe 100644 --- a/modules/simpletest/simpletest.module +++ b/modules/simpletest/simpletest.module @@ -106,7 +106,7 @@ function simpletest_test_form() { $result->message_group, basename($result->file), $result->line, - $result->caller, + $result->function, $map[$status], ), 'class' => "simpletest-$status", diff --git a/modules/simpletest/simpletest.test b/modules/simpletest/simpletest.test index a2e212b5772..711748d7997 100644 --- a/modules/simpletest/simpletest.test +++ b/modules/simpletest/simpletest.test @@ -106,17 +106,36 @@ class SimpleTestTestCase extends DrupalWebTestCase { $this->drupalCreateUser(array($this->invalid_permission)); $this->pass(t('Test ID is @id.', array('@id' => $this->test_id))); + + // Generates a warning + $i = 1 / 0; + + // Call an assert function specific to that class. + $this->assertNothing(); + } + + /** + * Assert nothing. + */ + function assertNothing() { + $this->pass("This is nothing."); } /** * 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($this->pass, 'Other', 'Pass', 'simpletest.test', 'SimpleTestTestCase->stubTest()'); + $this->assertAssertion($this->fail, 'Other', 'Fail', 'simpletest.test', 'SimpleTestTestCase->stubTest()'); - $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'); + $this->assertAssertion(t('Created permissions: @perms', array('@perms' => $this->valid_permission)), 'Role', 'Pass', 'simpletest.test', 'SimpleTestTestCase->stubTest()'); + $this->assertAssertion(t('Invalid permission %permission.', array('%permission' => $this->invalid_permission)), 'Role', 'Fail', 'simpletest.test', 'SimpleTestTestCase->stubTest()'); + + // Check that a warning is catched by simpletest. + $this->assertAssertion('Division by zero', 'Warning', 'Fail', 'simpletest.test', 'SimpleTestTestCase->stubTest()'); + + // Check that the backtracing code works for specific assert function. + $this->assertAssertion('This is nothing.', 'Other', 'Pass', 'simpletest.test', 'SimpleTestTestCase->stubTest()'); $this->test_ids[] = $test_id = $this->getTestIdFromResults(); $this->assertTrue($test_id, t('Found test ID in results.')); @@ -141,20 +160,24 @@ class SimpleTestTestCase extends DrupalWebTestCase { * @param string $message Assertion message. * @param string $type Assertion type. * @param string $status Assertion status. + * @param string $file File where the assertion originated. + * @param string $functuion Function where the assertion originated. * @return Assertion result. */ - function assertAssertion($message, $type, $status) { + function assertAssertion($message, $type, $status, $file, $function) { $message = trim(strip_tags($message)); $found = FALSE; foreach ($this->results['assertions'] as $assertion) { if ($assertion['message'] == $message && $assertion['type'] == $type && - $assertion['status'] == $status) { + $assertion['status'] == $status && + $assertion['file'] == $file && + $assertion['function'] == $function) { $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", "@status", "@file", "@function"}.', array('@message' => $message, '@type' => $type, '@status' => $status, "@file" => $file, "@function" => $function))); } /** @@ -175,6 +198,9 @@ class SimpleTestTestCase extends DrupalWebTestCase { $assertion = array(); $assertion['message'] = $this->asText($row->td[0]); $assertion['type'] = $this->asText($row->td[1]); + $assertion['file'] = $this->asText($row->td[2]); + $assertion['line'] = $this->asText($row->td[3]); + $assertion['function'] = $this->asText($row->td[4]); $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; @@ -212,7 +238,7 @@ class SimpleTestTestCase extends DrupalWebTestCase { if (!is_object($element)) { return $this->fail('The element is not an element.'); } - return trim(strip_tags($element->asXML())); + return trim(html_entity_decode(strip_tags($element->asXML()))); } /**