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 &amp;lt;blink&amp;gt;awesome&amp;lt;/blink&amp;gt;.');
+    // Make sure exceptions displayed through
+    // \Drupal\Core\Utility\Error::renderExceptionSafe() are correctly escaped.
+    $this->assertRaw('Drupal is &lt;blink&gt;awesome&lt;/blink&gt;.');
   }
 }
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