From 4f66a53b4fef8b13e530deb38f2e7603a0db1c5a Mon Sep 17 00:00:00 2001 From: Nathaniel Catchpole <catch@35733.no-reply.drupal.org> Date: Thu, 9 Jan 2014 11:50:54 +0000 Subject: [PATCH] Issue #2157691 by damiankloip: Move some helper methods in errors.inc to an Error utility class. --- core/includes/bootstrap.inc | 16 +- core/includes/errors.inc | 131 +------------ core/includes/session.inc | 4 +- core/includes/update.inc | 6 +- .../Core/Controller/ExceptionController.php | 46 +---- core/lib/Drupal/Core/Utility/Error.php | 178 ++++++++++++++++++ .../lib/Drupal/migrate/MigrateExecutable.php | 3 +- .../lib/Drupal/simpletest/TestBase.php | 18 +- .../Tests/System/ShutdownFunctionsTest.php | 6 +- .../modules/system_test/system_test.module | 2 +- .../Drupal/Tests/Core/Utility/ErrorTest.php | 162 ++++++++++++++++ 11 files changed, 377 insertions(+), 195 deletions(-) create mode 100644 core/lib/Drupal/Core/Utility/Error.php create mode 100644 core/tests/Drupal/Tests/Core/Utility/ErrorTest.php diff --git a/core/includes/bootstrap.inc b/core/includes/bootstrap.inc index 1568f451a026..d995e322a36d 100644 --- a/core/includes/bootstrap.inc +++ b/core/includes/bootstrap.inc @@ -11,6 +11,7 @@ use Drupal\Core\Database\Database; use Drupal\Core\DependencyInjection\ContainerBuilder; use Drupal\Core\Utility\Title; +use Drupal\Core\Utility\Error; use Symfony\Component\ClassLoader\ApcClassLoader; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\DependencyInjection\Container; @@ -1456,7 +1457,7 @@ function request_uri($omit_query_string = FALSE) { * A link to associate with the message. * * @see watchdog() - * @see _drupal_decode_exception() + * @see \Drupal\Core\Utility\Error::decodeException() */ function watchdog_exception($type, Exception $exception, $message = NULL, $variables = array(), $severity = WATCHDOG_ERROR, $link = NULL) { @@ -1464,7 +1465,7 @@ function watchdog_exception($type, Exception $exception, $message = NULL, $varia if (empty($message)) { // The exception message is run through // \Drupal\Component\Utility\String::checkPlain() by - // _drupal_decode_exception(). + // \Drupal\Core\Utility\Error:decodeException(). $message = '%type: !message in %function (line %line of %file).'; } // $variables must be an array so that we can add the exception information. @@ -1472,8 +1473,7 @@ function watchdog_exception($type, Exception $exception, $message = NULL, $varia $variables = array(); } - require_once __DIR__ . '/errors.inc'; - $variables += _drupal_decode_exception($exception); + $variables += Error::decodeException($exception); watchdog($type, $message, $variables, $severity, $link); } @@ -1934,15 +1934,15 @@ function _drupal_exception_handler($exception) { try { // Log the message to the watchdog and return an error page to the user. - _drupal_log_error(_drupal_decode_exception($exception), TRUE); + _drupal_log_error(Error::decodeException($exception), TRUE); } catch (Exception $exception2) { // Another uncaught exception was thrown while handling the first one. // If we are displaying errors, then do so with no possibility of a further uncaught exception being thrown. if (error_displayable()) { print '<h1>Additional uncaught exception thrown while handling exception.</h1>'; - print '<h2>Original</h2><p>' . _drupal_render_exception_safe($exception) . '</p>'; - print '<h2>Additional</h2><p>' . _drupal_render_exception_safe($exception2) . '</p><hr />'; + print '<h2>Original</h2><p>' . Error::renderExceptionSafe($exception) . '</p>'; + print '<h2>Additional</h2><p>' . Error::renderExceptionSafe($exception2) . '</p><hr />'; } } } @@ -2977,7 +2977,7 @@ function _drupal_shutdown_function() { require_once __DIR__ . '/errors.inc'; if (error_displayable()) { print '<h1>Uncaught exception thrown in shutdown function.</h1>'; - print '<p>' . _drupal_render_exception_safe($exception) . '</p><hr />'; + print '<p>' . Error::renderExceptionSafe($exception) . '</p><hr />'; } error_log($exception); } diff --git a/core/includes/errors.inc b/core/includes/errors.inc index 03594fea5a2e..4f5c923cc522 100644 --- a/core/includes/errors.inc +++ b/core/includes/errors.inc @@ -5,6 +5,7 @@ * Functions for error handling. */ +use Drupal\Core\Utility\Error; use Drupal\Component\Utility\String; use Symfony\Component\HttpFoundation\Response; @@ -58,7 +59,7 @@ function _drupal_error_handler_real($error_level, $message, $filename, $line, $c $types = drupal_error_levels(); list($severity_msg, $severity_level) = $types[$error_level]; $backtrace = debug_backtrace(); - $caller = _drupal_get_last_caller($backtrace); + $caller = Error::getLastCaller($backtrace); if (!function_exists('filter_xss_admin')) { require_once __DIR__ . '/common.inc'; @@ -79,69 +80,6 @@ function _drupal_error_handler_real($error_level, $message, $filename, $line, $c } } -/** - * Decodes an exception and retrieves the correct caller. - * - * @param $exception - * The exception object that was thrown. - * - * @return - * An error in the format expected by _drupal_log_error(). - */ -function _drupal_decode_exception($exception) { - $message = $exception->getMessage(); - - $backtrace = $exception->getTrace(); - // Add the line throwing the exception to the backtrace. - array_unshift($backtrace, array('line' => $exception->getLine(), 'file' => $exception->getFile())); - - // For PDOException errors, we try to return the initial caller, - // skipping internal functions of the database layer. - if ($exception instanceof PDOException) { - // The first element in the stack is the call, the second element gives us the caller. - // We skip calls that occurred in one of the classes of the database layer - // or in one of its global functions. - $db_functions = array('db_query', 'db_query_range'); - while (!empty($backtrace[1]) && ($caller = $backtrace[1]) && - ((isset($caller['class']) && (strpos($caller['class'], 'Query') !== FALSE || strpos($caller['class'], 'Database') !== FALSE || strpos($caller['class'], 'PDO') !== FALSE)) || - in_array($caller['function'], $db_functions))) { - // We remove that call. - array_shift($backtrace); - } - if (isset($exception->query_string, $exception->args)) { - $message .= ": " . $exception->query_string . "; " . print_r($exception->args, TRUE); - } - } - $caller = _drupal_get_last_caller($backtrace); - - return array( - '%type' => get_class($exception), - // The standard PHP exception handler considers that the exception message - // is plain-text. We mimick this behavior here. - '!message' => String::checkPlain($message), - '%function' => $caller['function'], - '%file' => $caller['file'], - '%line' => $caller['line'], - 'severity_level' => WATCHDOG_ERROR, - 'backtrace' => $backtrace, - ); -} - -/** - * Renders an exception error message without further exceptions. - * - * @param $exception - * The exception object that was thrown. - * - * @return - * An error message. - */ -function _drupal_render_exception_safe($exception) { - $decode = _drupal_decode_exception($exception); - unset($decode['backtrace']); - return String::checkPlain(strtr('%type: !message in %function (line %line of %file).', $decode)); -} - /** * Determines whether an error should be displayed. * @@ -312,43 +250,6 @@ function _drupal_get_error_level() { } } -/** - * Gets the last caller from a backtrace. - * - * @param $backtrace - * A standard PHP backtrace. Passed by reference. - * - * @return - * An associative array with keys 'file', 'line' and 'function'. - */ -function _drupal_get_last_caller(&$backtrace) { - // Errors that occur inside PHP internal functions do not generate - // information about file and line. Ignore black listed functions. - $blacklist = array('debug', '_drupal_error_handler', '_drupal_exception_handler'); - while (($backtrace && !isset($backtrace[0]['line'])) || - (isset($backtrace[1]['function']) && in_array($backtrace[1]['function'], $blacklist))) { - array_shift($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; -} - /** * Formats a backtrace into a plain-text string. * @@ -359,31 +260,9 @@ function _drupal_get_last_caller(&$backtrace) { * * @return string * A plain-text line-wrapped string ready to be put inside <pre>. + * + * @deprecated Use \Drupal\Core\Utility\Error::formatBacktrace() instead. */ function format_backtrace(array $backtrace) { - $return = ''; - foreach ($backtrace as $trace) { - $call = array('function' => '', 'args' => array()); - if (isset($trace['class'])) { - $call['function'] = $trace['class'] . $trace['type'] . $trace['function']; - } - elseif (isset($trace['function'])) { - $call['function'] = $trace['function']; - } - else { - $call['function'] = 'main'; - } - if (isset($trace['args'])) { - foreach ($trace['args'] as $arg) { - if (is_scalar($arg)) { - $call['args'][] = is_string($arg) ? '\'' . filter_xss($arg) . '\'' : $arg; - } - else { - $call['args'][] = ucfirst(gettype($arg)); - } - } - } - $return .= $call['function'] . '(' . implode(', ', $call['args']) . ")\n"; - } - return $return; + return Error::formatBacktrace($backtrace); } diff --git a/core/includes/session.inc b/core/includes/session.inc index 5ffbb8ded8df..fa469729cf8e 100644 --- a/core/includes/session.inc +++ b/core/includes/session.inc @@ -17,8 +17,8 @@ */ use Drupal\Component\Utility\Crypt; - use Drupal\Core\Session\UserSession; +use Drupal\Core\Utility\Error; /** * Session handler assigned by session_set_save_handler(). @@ -228,7 +228,7 @@ function _drupal_session_write($sid, $value) { // uncaught exception being thrown. if (error_displayable()) { print '<h1>Uncaught exception thrown in session handler.</h1>'; - print '<p>' . _drupal_render_exception_safe($exception) . '</p><hr />'; + print '<p>' . Error::renderExceptionSafe($exception) . '</p><hr />'; } return FALSE; } diff --git a/core/includes/update.inc b/core/includes/update.inc index b17624f7f921..24f65832fde3 100644 --- a/core/includes/update.inc +++ b/core/includes/update.inc @@ -14,6 +14,7 @@ use Drupal\Core\Config\FileStorage; use Drupal\Core\Config\ConfigException; use Drupal\Core\DrupalKernel; +use Drupal\Core\Utility\Error; use Drupal\Component\Uuid\Uuid; use Drupal\Component\Utility\NestedArray; use Symfony\Component\HttpFoundation\Request; @@ -815,12 +816,11 @@ function update_do_one($module, $number, $dependency_map, &$context) { catch (Exception $e) { watchdog_exception('update', $e); - require_once __DIR__ . '/errors.inc'; - $variables = _drupal_decode_exception($e); + $variables = Error::decodeException($e); unset($variables['backtrace']); // The exception message is run through // \Drupal\Component\Utility\String::checkPlain() by - // _drupal_decode_exception(). + // \Drupal\Core\Utility\Error::decodeException(). $ret['#abort'] = array('success' => FALSE, 'query' => t('%type: !message in %function (line %line of %file).', $variables)); } } diff --git a/core/lib/Drupal/Core/Controller/ExceptionController.php b/core/lib/Drupal/Core/Controller/ExceptionController.php index af66226f321e..06a89a87ad43 100644 --- a/core/lib/Drupal/Core/Controller/ExceptionController.php +++ b/core/lib/Drupal/Core/Controller/ExceptionController.php @@ -18,6 +18,7 @@ use Drupal\Component\Utility\String; use Symfony\Component\Debug\Exception\FlattenException; use Drupal\Core\ContentNegotiation; +use Drupal\Core\Utility\Error; /** * This controller handles HTTP errors generated by the routing system. @@ -413,7 +414,8 @@ protected function decodeException(FlattenException $exception) { array_shift($backtrace); } } - $caller = $this->getLastCaller($backtrace); + + $caller = Error::getLastCaller($backtrace); return array( '%type' => $exception->getClass(), @@ -427,46 +429,4 @@ protected function decodeException(FlattenException $exception) { ); } - /** - * Gets the last caller from a backtrace. - * - * The last caller is not necessarily the first item in the backtrace. Rather, - * it is the first item in the backtrace that is a PHP userspace function, - * and not one of our debug functions. - * - * @param $backtrace - * A standard PHP backtrace. - * - * @return array - * An associative array with keys 'file', 'line' and 'function'. - */ - protected function getLastCaller($backtrace) { - // Ignore black listed error handling functions. - $blacklist = array('debug', '_drupal_error_handler', '_drupal_exception_handler'); - - // Errors that occur inside PHP internal functions do not generate - // information about file and line. - while (($backtrace && !isset($backtrace[0]['line'])) || - (isset($backtrace[1]['function']) && in_array($backtrace[1]['function'], $blacklist))) { - array_shift($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; - } } diff --git a/core/lib/Drupal/Core/Utility/Error.php b/core/lib/Drupal/Core/Utility/Error.php new file mode 100644 index 000000000000..6cdc7e3f1881 --- /dev/null +++ b/core/lib/Drupal/Core/Utility/Error.php @@ -0,0 +1,178 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\Utility\Error. + */ + +namespace Drupal\Core\Utility; + +use Drupal\Component\Utility\String; +use Drupal\Component\Utility\Xss; + +/** + * Drupal error utility class. + */ +class Error { + + /** + * The error severity level. + * + * @var int + */ + const ERROR = 3; + + /** + * An array of blacklisted functions. + * + * @var array + */ + protected static $blacklistFunctions = array('debug', '_drupal_error_handler', '_drupal_exception_handler'); + + /** + * Decodes an exception and retrieves the correct caller. + * + * @param \Exception $exception + * The exception object that was thrown. + * + * @return array + * An error in the format expected by _drupal_log_error(). + */ + public static function decodeException(\Exception $exception) { + $message = $exception->getMessage(); + + $backtrace = $exception->getTrace(); + // Add the line throwing the exception to the backtrace. + array_unshift($backtrace, array('line' => $exception->getLine(), 'file' => $exception->getFile())); + + // For PDOException errors, we try to return the initial caller, + // skipping internal functions of the database layer. + if ($exception instanceof \PDOException) { + // The first element in the stack is the call, the second element gives us + // the caller. We skip calls that occurred in one of the classes of the + // database layer or in one of its global functions. + $db_functions = array('db_query', 'db_query_range'); + while (!empty($backtrace[1]) && ($caller = $backtrace[1]) && + ((isset($caller['class']) && (strpos($caller['class'], 'Query') !== FALSE || strpos($caller['class'], 'Database') !== FALSE || strpos($caller['class'], 'PDO') !== FALSE)) || + in_array($caller['function'], $db_functions))) { + // We remove that call. + array_shift($backtrace); + } + if (isset($exception->query_string, $exception->args)) { + $message .= ": " . $exception->query_string . "; " . print_r($exception->args, TRUE); + } + } + + $caller = static::getLastCaller($backtrace); + + return array( + '%type' => get_class($exception), + // The standard PHP exception handler considers that the exception message + // is plain-text. We mimic this behavior here. + '!message' => String::checkPlain($message), + '%function' => $caller['function'], + '%file' => $caller['file'], + '%line' => $caller['line'], + 'severity_level' => static::ERROR, + 'backtrace' => $backtrace, + ); + } + + /** + * Renders an exception error message without further exceptions. + * + * @param \Exception $exception + * The exception object that was thrown. + * + * @return string + * An error message. + */ + public static function renderExceptionSafe(\Exception $exception) { + $decode = static::decodeException($exception); + unset($decode['backtrace']); + + return String::format('%type: !message in %function (line %line of %file).', $decode); + } + + /** + * Gets the last caller from a backtrace. + * + * @param array $backtrace + * A standard PHP backtrace. Passed by reference. + * + * @return array + * An associative array with keys 'file', 'line' and 'function'. + */ + public static function getLastCaller(array &$backtrace) { + // Errors that occur inside PHP internal functions do not generate + // information about file and line. Ignore black listed functions. + while (($backtrace && !isset($backtrace[0]['line'])) || + (isset($backtrace[1]['function']) && in_array($backtrace[1]['function'], static::$blacklistFunctions))) { + array_shift($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 gives 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; + } + + /** + * Formats a backtrace into a plain-text string. + * + * The calls show values for scalar arguments and type names for complex ones. + * + * @param array $backtrace + * A standard PHP backtrace. + * + * @return string + * A plain-text line-wrapped string ready to be put inside <pre>. + */ + public static function formatBacktrace(array $backtrace) { + $return = ''; + + foreach ($backtrace as $trace) { + $call = array('function' => '', 'args' => array()); + + if (isset($trace['class'])) { + $call['function'] = $trace['class'] . $trace['type'] . $trace['function']; + } + elseif (isset($trace['function'])) { + $call['function'] = $trace['function']; + } + else { + $call['function'] = 'main'; + } + + if (isset($trace['args'])) { + foreach ($trace['args'] as $arg) { + if (is_scalar($arg)) { + $call['args'][] = is_string($arg) ? '\'' . Xss::filter($arg) . '\'' : $arg; + } + else { + $call['args'][] = ucfirst(gettype($arg)); + } + } + } + + $return .= $call['function'] . '(' . implode(', ', $call['args']) . ")\n"; + } + + return $return; + } + +} diff --git a/core/modules/migrate/lib/Drupal/migrate/MigrateExecutable.php b/core/modules/migrate/lib/Drupal/migrate/MigrateExecutable.php index 2b4f181422de..7d78cd419fad 100644 --- a/core/modules/migrate/lib/Drupal/migrate/MigrateExecutable.php +++ b/core/modules/migrate/lib/Drupal/migrate/MigrateExecutable.php @@ -7,6 +7,7 @@ namespace Drupal\migrate; +use Drupal\Core\Utility\Error; use Drupal\migrate\Entity\MigrationInterface; use Drupal\migrate\Plugin\MigrateIdMapInterface; @@ -539,7 +540,7 @@ protected function timeExceeded() { * in contexts where this doesn't make sense. */ public function handleException($exception, $save = TRUE) { - $result = _drupal_decode_exception($exception); + $result = Error::decodeException($exception); $message = $result['!message'] . ' (' . $result['%file'] . ':' . $result['%line'] . ')'; if ($save) { $this->saveMessage($message); diff --git a/core/modules/simpletest/lib/Drupal/simpletest/TestBase.php b/core/modules/simpletest/lib/Drupal/simpletest/TestBase.php index 9e7b6b2a26d4..560afb1ec159 100644 --- a/core/modules/simpletest/lib/Drupal/simpletest/TestBase.php +++ b/core/modules/simpletest/lib/Drupal/simpletest/TestBase.php @@ -18,6 +18,7 @@ use Drupal\Core\DrupalKernel; use Drupal\Core\Language\Language; use Drupal\Core\StreamWrapper\PublicStream; +use Drupal\Core\Utility\Error; use Symfony\Component\HttpFoundation\Request; /** @@ -397,7 +398,7 @@ protected function getAssertionCall() { array_shift($backtrace); } - return _drupal_get_last_caller($backtrace); + return Error::getLastCaller($backtrace); } /** @@ -1177,10 +1178,10 @@ public function errorHandler($severity, $message, $file = NULL, $line = NULL) { if ($severity !== E_USER_NOTICE) { $verbose_backtrace = $backtrace; array_shift($verbose_backtrace); - $message .= '<pre class="backtrace">' . format_backtrace($verbose_backtrace) . '</pre>'; + $message .= '<pre class="backtrace">' . Error::formatBacktrace($verbose_backtrace) . '</pre>'; } - $this->error($message, $error_map[$severity], _drupal_get_last_caller($backtrace)); + $this->error($message, $error_map[$severity], Error::getLastCaller($backtrace)); } return TRUE; } @@ -1199,14 +1200,15 @@ protected function exceptionHandler($exception) { 'line' => $exception->getLine(), 'file' => $exception->getFile(), )); - // The exception message is run through check_plain() - // by _drupal_decode_exception(). - $decoded_exception = _drupal_decode_exception($exception); + // The exception message is run through + // \Drupal\Component\Utility\checkPlain() by + // \Drupal\Core\Utility\decodeException(). + $decoded_exception = Error::decodeException($exception); unset($decoded_exception['backtrace']); $message = format_string('%type: !message in %function (line %line of %file). <pre class="backtrace">!backtrace</pre>', $decoded_exception + array( - '!backtrace' => format_backtrace($verbose_backtrace), + '!backtrace' => Error::formatBacktrace($verbose_backtrace), )); - $this->error($message, 'Uncaught exception', _drupal_get_last_caller($backtrace)); + $this->error($message, 'Uncaught exception', Error::getLastCaller($backtrace)); } /** diff --git a/core/modules/system/lib/Drupal/system/Tests/System/ShutdownFunctionsTest.php b/core/modules/system/lib/Drupal/system/Tests/System/ShutdownFunctionsTest.php index d3a160e5ad70..96b2f8d5741d 100644 --- a/core/modules/system/lib/Drupal/system/Tests/System/ShutdownFunctionsTest.php +++ b/core/modules/system/lib/Drupal/system/Tests/System/ShutdownFunctionsTest.php @@ -39,8 +39,8 @@ function testShutdownFunctions() { $this->assertText(t('First shutdown function, arg1 : @arg1, arg2: @arg2', array('@arg1' => $arg1, '@arg2' => $arg2))); $this->assertText(t('Second shutdown function, arg1 : @arg1, arg2: @arg2', array('@arg1' => $arg1, '@arg2' => $arg2))); - // Make sure exceptions displayed through _drupal_render_exception_safe() - // are correctly escaped. - $this->assertRaw('Drupal is &lt;blink&gt;awesome&lt;/blink&gt;.'); + // Make sure exceptions displayed through + // \Drupal\Core\Utility\Error::renderExceptionSafe() are correctly escaped. + $this->assertRaw('Drupal is <blink>awesome</blink>.'); } } diff --git a/core/modules/system/tests/modules/system_test/system_test.module b/core/modules/system/tests/modules/system_test/system_test.module index aa93e6d672ae..040d9f31573f 100644 --- a/core/modules/system/tests/modules/system_test/system_test.module +++ b/core/modules/system/tests/modules/system_test/system_test.module @@ -157,7 +157,7 @@ function _system_test_second_shutdown_function($arg1, $arg2) { // Throw an exception with an HTML tag. Since this is called in a shutdown // function, it will not bubble up to the default exception handler but will // be caught in _drupal_shutdown_function() and be displayed through - // _drupal_render_exception_safe(). + // \Drupal\Core\Utility\Error::renderExceptionSafe(). throw new Exception('Drupal is <blink>awesome</blink>.'); } diff --git a/core/tests/Drupal/Tests/Core/Utility/ErrorTest.php b/core/tests/Drupal/Tests/Core/Utility/ErrorTest.php new file mode 100644 index 000000000000..bb3fa1fbe8f5 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Utility/ErrorTest.php @@ -0,0 +1,162 @@ +<?php + +/** + * @file + * Contains \Drupal\Tests\Core\Utility\ErrorTest. + */ + +namespace Drupal\Tests\Core\Utility; + +use Drupal\Tests\UnitTestCase; +use Drupal\Core\Utility\Error; + +/** + * Tests the Error class. + * + * @group Drupal + * + * @see \Drupal\Core\Utility\Error + */ +class ErrorTest extends UnitTestCase { + + public static function getInfo() { + return array( + 'name' => 'Error', + 'description' => 'Tests the Error utility class.', + 'group' => 'Common', + ); + } + + /** + * Tests the getLastCaller() method. + * + * @param array $backtrace + * The test backtrace array. + * @param array $expected + * The expected return array. + * + * @dataProvider providerTestGetLastCaller + * + */ + public function testGetLastCaller($backtrace, $expected) { + $this->assertSame($expected, Error::getLastCaller($backtrace)); + } + + /** + * Data provider for testGetLastCaller. + * + * @return array + * An array of parameter data. + */ + public function providerTestGetLastCaller() { + $data = array(); + + // Test with just one item. This should default to the function being + // main(). + $single_item = array($this->createBacktraceItem()); + $data[] = array($single_item, $this->createBacktraceItem('main()')); + + // Add a second item, without a class. + $two_items = $single_item; + $two_items[] = $this->createBacktraceItem('test_function_two'); + $data[] = array($two_items, $this->createBacktraceItem('test_function_two()')); + + // Add a second item, with a class. + $two_items = $single_item; + $two_items[] = $this->createBacktraceItem('test_function_two', 'TestClass'); + $data[] = array($two_items, $this->createBacktraceItem('TestClass->test_function_two()')); + + // Add blacklist functions to backtrace. They should get removed. + foreach (array('debug', '_drupal_error_handler', '_drupal_exception_handler') as $function) { + $two_items = $single_item; + // Push to the start of the backtrace. + array_unshift($two_items, $this->createBacktraceItem($function)); + $data[] = array($single_item, $this->createBacktraceItem('main()')); + } + + return $data; + } + + /** + * Tests the formatBacktrace() method. + * + * @param array $backtrace + * The test backtrace array. + * @param array $expected + * The expected return array. + * + * @dataProvider providerTestFormatBacktrace + */ + public function testFormatBacktrace($backtrace, $expected) { + $this->assertSame($expected, Error::formatBacktrace($backtrace)); + } + + /** + * Data provider for testFormatBacktrace. + * + * @return array + */ + public function providerTestFormatBacktrace() { + $data = array(); + + // Test with no function, main should be in the backtrace. + $data[] = array(array($this->createBacktraceItem(NULL, NULL)), "main()\n"); + + $base = array($this->createBacktraceItem()); + $data[] = array($base, "test_function()\n"); + + // Add a second item. + $second_item = $base; + $second_item[] = $this->createBacktraceItem('test_function_2'); + + $data[] = array($second_item, "test_function()\ntest_function_2()\n"); + + // Add a second item, with a class. + $second_item_class = $base; + $second_item_class[] = $this->createBacktraceItem('test_function_2', 'TestClass'); + + $data[] = array($second_item_class, "test_function()\nTestClass->test_function_2()\n"); + + // Add a second item, with a class. + $second_item_args = $base; + $second_item_args[] = $this->createBacktraceItem('test_function_2', NULL, array('string', 10, new \stdClass())); + + $data[] = array($second_item_args, "test_function()\ntest_function_2('string', 10, Object)\n"); + + return $data; + } + + /** + * Creates a mock backtrace item. + * + * @param string|NULL $function + * (optional) The function name to use in the backtrace item. + * @param string $class + * (optional) The class to use in the backtrace item. + * @param array $args + * (optional) An array of function arguments to add to the backtrace item. + * + * @return array + * A backtrace array item. + */ + protected function createBacktraceItem($function = 'test_function', $class = NULL, array $args = array()) { + $backtrace = array( + 'file' => 'test_file', + 'line' => 10, + 'function' => $function, + 'args' => array(), + ); + + if (isset($class)) { + $backtrace['class'] = $class; + $backtrace['type'] = '->'; + } + + if (!empty($args)) { + $backtrace['args'] = $args; + } + + return $backtrace; + } + +} -- GitLab