Browse Source

Render chained exceptions in console exception rendering

Wrapped or chained exceptions can contain useful information for
debugging a problem. By outputting all the chained exceptions we can
help developers diagnose problems more easily.
Mark Story 3 years ago
parent
commit
a610a170db

+ 37 - 12
src/Error/Renderer/ConsoleExceptionRenderer.php

@@ -68,23 +68,48 @@ class ConsoleExceptionRenderer
      */
     public function render()
     {
+        $exceptions = [$this->error];
+        $previous = $this->error->getPrevious();
+        while ($previous !== null) {
+            $exceptions[] = $previous;
+            $previous = $previous->getPrevious();
+        }
         $out = [];
-        $out[] = sprintf(
-            '<error>[%s] %s</error> in %s on line %s',
-            get_class($this->error),
-            $this->error->getMessage(),
-            $this->error->getFile(),
-            $this->error->getLine()
-        );
+        foreach ($exceptions as $i => $error) {
+            $out = array_merge($out, $this->renderException($error, $i));
+        }
+
+        return join("\n", $out);
+    }
+
+    /**
+     * Render an individual exception
+     *
+     * @param \Throwable $exception The exception to render.
+     * @param int $index Exception index in the chain
+     * @return array
+     */
+    protected function renderException(Throwable $exception, int $index): array
+    {
+        $out = [
+            sprintf(
+                '<error>%s[%s] %s</error> in %s on line %s',
+                $index > 0 ? 'Caused by ' : '',
+                get_class($exception),
+                $exception->getMessage(),
+                $exception->getFile(),
+                $exception->getLine()
+            ),
+        ];
 
         $debug = Configure::read('debug');
-        if ($debug && $this->error instanceof CakeException) {
-            $attributes = $this->error->getAttributes();
+        if ($debug && $exception instanceof CakeException) {
+            $attributes = $exception->getAttributes();
             if ($attributes) {
                 $out[] = '';
                 $out[] = '<info>Exception Attributes</info>';
                 $out[] = '';
-                $out[] = var_export($this->error->getAttributes(), true);
+                $out[] = var_export($exception->getAttributes(), true);
             }
         }
 
@@ -92,11 +117,11 @@ class ConsoleExceptionRenderer
             $out[] = '';
             $out[] = '<info>Stack Trace:</info>';
             $out[] = '';
-            $out[] = $this->error->getTraceAsString();
+            $out[] = $exception->getTraceAsString();
             $out[] = '';
         }
 
-        return join("\n", $out);
+        return $out;
     }
 
     /**

+ 20 - 0
tests/TestCase/Error/ExceptionTrapTest.php

@@ -29,6 +29,7 @@ use Cake\Log\Log;
 use Cake\TestSuite\TestCase;
 use Cake\Utility\Text;
 use InvalidArgumentException;
+use RuntimeException;
 use TestApp\Error\LegacyErrorLogger;
 use Throwable;
 
@@ -161,6 +162,25 @@ class ExceptionTrapTest extends TestCase
         $this->assertStringContainsString('->testHandleExceptionConsoleRenderingWithStack', $out[0]);
     }
 
+    public function testHandleExceptionConsoleRenderingWithPrevious()
+    {
+        $output = new StubConsoleOutput();
+        $trap = new ExceptionTrap([
+            'exceptionRenderer' => ConsoleExceptionRenderer::class,
+            'stderr' => $output,
+            'trace' => true,
+        ]);
+        $previous = new RuntimeException('underlying error');
+        $error = new InvalidArgumentException('nope', 0, $previous);
+
+        $trap->handleException($error);
+        $out = $output->messages();
+
+        $this->assertStringContainsString('nope', $out[0]);
+        $this->assertStringContainsString('Caused by [RuntimeException] underlying error', $out[0]);
+        $this->assertEquals(2, substr_count($out[0], 'Stack Trace'));
+    }
+
     public function testHandleExceptionConsoleWithAttributes()
     {
         $output = new StubConsoleOutput();