Skip to content

Commit 88f289e

Browse files
CopilotYunchuWang
andcommitted
Finalize exception serialization implementation with comprehensive tests
Co-authored-by: YunchuWang <12449837+YunchuWang@users.noreply.github.com>
1 parent 7228f17 commit 88f289e

File tree

2 files changed

+146
-5
lines changed

2 files changed

+146
-5
lines changed

client/src/main/java/com/microsoft/durabletask/FailureDetails.java

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,16 @@ public final class FailureDetails {
3434
this.isNonRetriable = isNonRetriable;
3535
}
3636

37+
/**
38+
* Creates a new failure details object from an exception.
39+
* This constructor captures comprehensive exception information including:
40+
* - Exception type
41+
* - Exception message (including multiline messages)
42+
* - Complete stack trace with inner/nested exceptions
43+
* - Suppressed exceptions
44+
*
45+
* @param exception The exception to create failure details from
46+
*/
3747
FailureDetails(Exception exception) {
3848
// Use the most specific exception in the chain for error type
3949
String errorType = exception.getClass().getName();
@@ -124,7 +134,18 @@ public boolean isCausedBy(Class<? extends Exception> exceptionClass) {
124134
}
125135
}
126136

137+
/**
138+
* Generates a comprehensive stack trace string from a throwable, including inner exceptions, suppressed exceptions,
139+
* and handling circular references. The format closely resembles Java's standard exception printing format.
140+
*
141+
* @param e The throwable to convert to a stack trace string
142+
* @return A formatted stack trace string
143+
*/
127144
static String getFullStackTrace(Throwable e) {
145+
if (e == null) {
146+
return "";
147+
}
148+
128149
StringBuilder sb = new StringBuilder();
129150

130151
// Process the exception chain recursively
@@ -166,16 +187,24 @@ private static void appendExceptionDetails(StringBuilder sb, Throwable ex, Stack
166187
Throwable[] suppressed = ex.getSuppressed();
167188
if (suppressed != null && suppressed.length > 0) {
168189
for (Throwable s : suppressed) {
169-
sb.append("\tSuppressed: ");
170-
appendExceptionDetails(sb, s, currentStackTrace);
190+
if (s != ex) { // Avoid circular references
191+
sb.append("\tSuppressed: ");
192+
appendExceptionDetails(sb, s, currentStackTrace);
193+
} else {
194+
sb.append("\tSuppressed: [CIRCULAR REFERENCE]").append(System.lineSeparator());
195+
}
171196
}
172197
}
173198

174199
// Handle cause (inner exception)
175200
Throwable cause = ex.getCause();
176-
if (cause != null && cause != ex) { // Avoid infinite recursion
177-
sb.append("Caused by: ");
178-
appendExceptionDetails(sb, cause, currentStackTrace);
201+
if (cause != null) {
202+
if (cause != ex) { // Avoid direct circular references
203+
sb.append("Caused by: ");
204+
appendExceptionDetails(sb, cause, currentStackTrace);
205+
} else {
206+
sb.append("Caused by: [CIRCULAR REFERENCE]").append(System.lineSeparator());
207+
}
179208
}
180209
}
181210

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
package com.microsoft.durabletask;
5+
6+
import org.junit.jupiter.api.Test;
7+
import static org.junit.jupiter.api.Assertions.*;
8+
9+
/**
10+
* Tests for handling complex exception serialization scenarios.
11+
*/
12+
public class ComplexExceptionTest {
13+
14+
@Test
15+
public void testDeepNestedExceptions() {
16+
// Create a chain of 5 nested exceptions
17+
Exception level5 = new IllegalArgumentException("Level 5 exception");
18+
Exception level4 = new IllegalStateException("Level 4 exception", level5);
19+
Exception level3 = new RuntimeException("Level 3 exception", level4);
20+
Exception level2 = new Exception("Level 2 exception", level3);
21+
Exception level1 = new Exception("Level 1 exception", level2);
22+
23+
FailureDetails details = new FailureDetails(level1);
24+
25+
assertEquals("java.lang.Exception", details.getErrorType());
26+
assertEquals("Level 1 exception", details.getErrorMessage());
27+
28+
String stackTrace = details.getStackTrace();
29+
assertNotNull(stackTrace);
30+
31+
// Verify all exception levels are present in the stack trace
32+
assertTrue(stackTrace.contains("Level 1 exception"));
33+
assertTrue(stackTrace.contains("Caused by: java.lang.Exception: Level 2 exception"));
34+
assertTrue(stackTrace.contains("Caused by: java.lang.RuntimeException: Level 3 exception"));
35+
assertTrue(stackTrace.contains("Caused by: java.lang.IllegalStateException: Level 4 exception"));
36+
assertTrue(stackTrace.contains("Caused by: java.lang.IllegalArgumentException: Level 5 exception"));
37+
}
38+
39+
@Test
40+
public void testExceptionWithSuppressedExceptions() {
41+
Exception mainException = new RuntimeException("Main exception");
42+
Exception suppressed1 = new IllegalArgumentException("Suppressed exception 1");
43+
Exception suppressed2 = new IllegalStateException("Suppressed exception 2");
44+
45+
mainException.addSuppressed(suppressed1);
46+
mainException.addSuppressed(suppressed2);
47+
48+
FailureDetails details = new FailureDetails(mainException);
49+
50+
assertEquals("java.lang.RuntimeException", details.getErrorType());
51+
assertEquals("Main exception", details.getErrorMessage());
52+
53+
String stackTrace = details.getStackTrace();
54+
assertNotNull(stackTrace);
55+
56+
// Verify suppressed exceptions are in the stack trace
57+
assertTrue(stackTrace.contains("Main exception"));
58+
assertTrue(stackTrace.contains("Suppressed: java.lang.IllegalArgumentException: Suppressed exception 1"));
59+
assertTrue(stackTrace.contains("Suppressed: java.lang.IllegalStateException: Suppressed exception 2"));
60+
}
61+
62+
@Test
63+
public void testNullMessageException() {
64+
NullPointerException exception = new NullPointerException(); // NPE typically has null message
65+
66+
FailureDetails details = new FailureDetails(exception);
67+
68+
assertEquals("java.lang.NullPointerException", details.getErrorType());
69+
assertEquals("", details.getErrorMessage()); // Should convert null to empty string
70+
assertNotNull(details.getStackTrace());
71+
}
72+
73+
@Test
74+
public void testCircularExceptionReference() {
75+
try {
76+
// Create an exception with a circular reference (should be handled gracefully)
77+
ExceptionWithCircularReference ex = new ExceptionWithCircularReference("Circular");
78+
ex.setCircularCause();
79+
80+
FailureDetails details = new FailureDetails(ex);
81+
82+
assertEquals(ExceptionWithCircularReference.class.getName(), details.getErrorType());
83+
assertEquals("Circular", details.getErrorMessage());
84+
assertNotNull(details.getStackTrace());
85+
86+
// No infinite loop, test passes if we get here
87+
} catch (StackOverflowError e) {
88+
fail("StackOverflowError occurred with circular exception reference");
89+
}
90+
}
91+
92+
/**
93+
* Exception class that can create a circular reference in the cause chain.
94+
*/
95+
private static class ExceptionWithCircularReference extends Exception {
96+
public ExceptionWithCircularReference(String message) {
97+
super(message);
98+
}
99+
100+
public void setCircularCause() {
101+
try {
102+
// Use reflection to set the cause field directly to this exception
103+
// to create a circular reference
104+
java.lang.reflect.Field causeField = Throwable.class.getDeclaredField("cause");
105+
causeField.setAccessible(true);
106+
causeField.set(this, this);
107+
} catch (Exception e) {
108+
// Ignore any reflection errors, this is just for testing
109+
}
110+
}
111+
}
112+
}

0 commit comments

Comments
 (0)