diff --git a/includes/common.inc b/includes/common.inc index 41399f8e2d6..af16de978ca 100644 --- a/includes/common.inc +++ b/includes/common.inc @@ -3443,6 +3443,14 @@ function _drupal_bootstrap_full() { set_error_handler('_drupal_error_handler'); set_exception_handler('_drupal_exception_handler'); + if (isset($_SERVER['HTTP_USER_AGENT']) && strpos($_SERVER['HTTP_USER_AGENT'], 'simpletest') !== FALSE) { + // Valid SimpleTest user-agent, log fatal errors to test specific file + // directory. The user-agent is validated in DRUPAL_BOOTSTRAP_DATABASE + // phase so as long as it is a SimpleTest user-agent it is valid. + ini_set('log_errors', 1); + ini_set('error_log', file_directory_path() . '/error.log'); + } + // Emit the correct charset HTTP header. drupal_set_header('Content-Type', 'text/html; charset=utf-8'); // Detect string handling method diff --git a/modules/simpletest/drupal_web_test_case.php b/modules/simpletest/drupal_web_test_case.php index a6afcde3212..6df78fbf255 100644 --- a/modules/simpletest/drupal_web_test_case.php +++ b/modules/simpletest/drupal_web_test_case.php @@ -138,20 +138,27 @@ abstract class DrupalTestCase { } /** - * Make assertions from outside the test case. + * 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. * * @see DrupalTestCase::assert() */ - public static function assertStatic($test_id, $test_class, $status, $message = '', $group = 'Other', array $caller = NULL) { + public static function insertAssert($test_id, $test_class, $status, $message = '', $group = 'Other', array $caller = array()) { // Convert boolean status to string status. if (is_bool($status)) { $status = $status ? 'pass' : 'fail'; } $caller += array( - 'function' => t('N/A'), - 'line' => -1, - 'file' => t('N/A'), + 'function' => t('Unknown'), + 'line' => 0, + 'file' => t('Unknown'), ); $assertion = array( @@ -1033,13 +1040,22 @@ class DrupalWebTestCase extends DrupalTestCase { ->execute(); $db_prefix = $db_prefix_new; + // Create test directory ahead of installation so fatal errors and debug + // information can be logged during installation process. + $directory = $this->originalFileDirectory . '/simpletest/' . substr($db_prefix, 10); + file_check_directory($directory, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS); + + // Log fatal errors. + ini_set('log_errors', 1); + ini_set('error_log', $directory . '/error.log'); + include_once DRUPAL_ROOT . '/includes/install.inc'; drupal_install_system(); $this->preloadRegistry(); // Include the default profile - require_once("./profiles/default/default.profile"); + require_once('./profiles/default/default.profile'); $profile_details = install_profile_info('default', 'en'); // Add the specified modules to the list of modules in the default profile. @@ -1090,15 +1106,9 @@ class DrupalWebTestCase extends DrupalTestCase { // default mail handler. variable_set('smtp_library', drupal_get_path('module', 'simpletest') . '/drupal_web_test_case.php'); - // Use temporary files directory with the same prefix as database. - variable_set('file_directory_path', $this->originalFileDirectory . '/simpletest/' . substr($db_prefix, 10)); - $directory = file_directory_path(); - // Create the files directory. - file_check_directory($directory, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS); - - // Log fatal errors. - ini_set('log_errors', 1); - ini_set('error_log', $directory . '/error.log'); + // Use temporary files directory with the same prefix as database. The + // directory will have been created already. + variable_set('file_directory_path', $directory); set_time_limit($this->timeLimit); } @@ -1138,6 +1148,13 @@ class DrupalWebTestCase extends DrupalTestCase { protected function tearDown() { global $db_prefix, $user, $language; + // In case a fatal error occured that was not in the test process read the + // log to pick up any fatal errors. + $db_prefix_temp = $db_prefix; + $db_prefix = $this->originalPrefix; + simpletest_log_read($this->testId, $db_prefix, get_class($this), TRUE); + $db_prefix = $db_prefix_temp; + $emailCount = count(variable_get('simpletest_emails', array())); if ($emailCount) { $message = format_plural($emailCount, t('!count e-mail was sent during this test.'), t('!count e-mails were sent during this test.'), array('!count' => $emailCount)); diff --git a/modules/simpletest/simpletest.module b/modules/simpletest/simpletest.module index c7bc1516a22..a181b444ae6 100644 --- a/modules/simpletest/simpletest.module +++ b/modules/simpletest/simpletest.module @@ -203,7 +203,14 @@ function _simpletest_batch_finished($success, $results, $operations, $elapsed) { } else { // Use the test_id passed as a parameter to _simpletest_batch_operation(). - simpletest_log_read($operations[0][1][1]); + $test_id = $operations[0][1][1]; + + // Retrieve the last database prefix used for testing and the last test + // class that was run from. Use the information to read the lgo file + // in case any fatal errors caused the test to crash. + list($last_prefix, $last_test_class) = simpletest_last_test_get($test_id); + simpletest_log_read($test_id, $last_prefix, $last_test_class); + drupal_set_message(t('The test run did not successfully finish.'), 'error'); drupal_set_message(t('Please use the Clean environment button to clean-up temporary files and tables.'), 'warning'); @@ -211,6 +218,21 @@ function _simpletest_batch_finished($success, $results, $operations, $elapsed) { module_invoke_all('test_group_finished'); } +/* + * Get information about the last test that ran given a test ID. + * + * @param $test_id + * The test ID to get the last test from. + * @return + * Array containing the last database prefix used and the last test class + * that ran. + */ +function simpletest_last_test_get($test_id) { + $last_prefix = db_result(db_query_range('SELECT last_prefix FROM {simpletest_test_id} WHERE test_id = :test_id', array(':test_id' => $test_id), 0, 1)); + $last_test_class = db_result(db_query_range('SELECT test_class FROM {simpletest} WHERE test_id = :test_id ORDER BY message_id DESC', array(':test_id' => $test_id), 0, 1)); + return array($last_prefix, $last_test_class); +} + /** * Read the error log and report any errors as assertion failures. * @@ -218,28 +240,39 @@ function _simpletest_batch_finished($success, $results, $operations, $elapsed) { * will have been recorded by the error handler. * * @param $test_id - * The test ID to read log file for. + * The test ID to which the log relates. + * @param $prefix + * The database prefix to which the log relates. + * @param $test_class + * The test class to which the log relates. + * @param $during_test + * Indicates that the current file directory path is a temporary file + * file directory used during testing. + * @return + * Found any entries in log. */ -function simpletest_log_read($test_id) { - $last_prefix = db_query('SELECT last_prefix FROM {simpletest_test_id} WHERE test_id = :test_id', array(':test_id' => $test_id))->fetchField(); - $last_prefix = substr($last_prefix, 10); - - $test_class = db_query('SELECT test_class FROM {simpletest} WHERE test_id = :test_id ORDER BY message_id', array(':test_id' => $test_id))->fetchField(); - $log = file_directory_path() . "/simpletest/$last_prefix/error.log"; +function simpletest_log_read($test_id, $prefix, $test_class, $during_test = FALSE) { + $log = file_directory_path() . ($during_test ? '' : '/simpletest/' . substr($prefix, 10)) . '/error.log'; + $found = FALSE; if (file_exists($log)) { foreach (file($log) as $line) { - if (preg_match('/PHP Fatal error: (.*?) in (.*) on line (\d+)/', $line, $match)) { + if (preg_match('/\[.*?\] (.*?): (.*?) in (.*) on line (\d+)/', $line, $match)) { + // Parse PHP fatal errors for example: PHP Fatal error: Call to + // undefined function break_me() in /path/to/file.php on line 17 $caller = array( - 'line' => $match[3], - 'file' => $match[2], + 'line' => $match[4], + 'file' => $match[3], ); - DrupalTestCase::assertStatic($test_id, $test_class, FALSE, $match[1], 'Fatal error', $caller); + DrupalTestCase::insertAssert($test_id, $test_class, FALSE, $match[2], $match[1], $caller); } else { - DrupalTestCase::assertStatic($test_id, $test_class, FALSE, $line, 'Fatal error'); + // Unkown format, place the entire message in the log. + DrupalTestCase::insertAssert($test_id, $test_class, FALSE, $line, 'Fatal error'); } + $found = TRUE; } } + return $found; } /** diff --git a/scripts/run-tests.sh b/scripts/run-tests.sh index 9e42d1dc4c8..37d3c4399fc 100755 --- a/scripts/run-tests.sh +++ b/scripts/run-tests.sh @@ -83,6 +83,12 @@ $test_id = db_insert('simpletest_test_id')->useDefaults(array('test_id'))->execu // Execute tests. simpletest_script_command($args['concurrency'], $test_id, implode(",", $test_list)); +// Retrieve the last database prefix used for testing and the last test class +// that was run from. Use the information to read the lgo file in case any +// fatal errors caused the test to crash. +list($last_prefix, $last_test_class) = simpletest_last_test_get($test_id); +simpletest_log_read($test_id, $last_prefix, $last_test_class); + // Display results before database is cleared. simpletest_script_reporter_display_results(); @@ -466,8 +472,6 @@ function simpletest_script_reporter_init() { function simpletest_script_reporter_display_results() { global $args, $test_id, $results_map; - simpletest_log_read($test_id); - echo "\n"; $end = timer_stop('run-tests'); echo "Test run duration: " . format_interval($end['time'] / 1000);