Skip to content

Commit 7228f17

Browse files
CopilotYunchuWang
andcommitted
Add integration tests for exception serialization and improve stack trace formatting
Co-authored-by: YunchuWang <12449837+YunchuWang@users.noreply.github.com>
1 parent 1c5a2b7 commit 7228f17

File tree

2 files changed

+150
-2
lines changed

2 files changed

+150
-2
lines changed

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

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -148,8 +148,18 @@ private static void appendExceptionDetails(StringBuilder sb, Throwable ex, Stack
148148

149149
// Add the stack trace elements
150150
StackTraceElement[] currentStackTrace = ex.getStackTrace();
151-
for (StackTraceElement element : currentStackTrace) {
152-
sb.append("\tat ").append(element.toString()).append(System.lineSeparator());
151+
int framesInCommon = 0;
152+
if (parentStackTrace != null) {
153+
framesInCommon = countCommonFrames(currentStackTrace, parentStackTrace);
154+
}
155+
156+
int framesToPrint = currentStackTrace.length - framesInCommon;
157+
for (int i = 0; i < framesToPrint; i++) {
158+
sb.append("\tat ").append(currentStackTrace[i].toString()).append(System.lineSeparator());
159+
}
160+
161+
if (framesInCommon > 0) {
162+
sb.append("\t... ").append(framesInCommon).append(" more").append(System.lineSeparator());
153163
}
154164

155165
// Handle any suppressed exceptions
@@ -168,6 +178,22 @@ private static void appendExceptionDetails(StringBuilder sb, Throwable ex, Stack
168178
appendExceptionDetails(sb, cause, currentStackTrace);
169179
}
170180
}
181+
182+
/**
183+
* Count frames in common between two stack traces, starting from the end.
184+
* This helps produce more concise stack traces for chained exceptions.
185+
*/
186+
private static int countCommonFrames(StackTraceElement[] trace1, StackTraceElement[] trace2) {
187+
int m = trace1.length - 1;
188+
int n = trace2.length - 1;
189+
int count = 0;
190+
while (m >= 0 && n >= 0 && trace1[m].equals(trace2[n])) {
191+
m--;
192+
n--;
193+
count++;
194+
}
195+
return count;
196+
}
171197

172198
TaskFailureDetails toProto() {
173199
return TaskFailureDetails.newBuilder()
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
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.Tag;
7+
import org.junit.jupiter.api.Test;
8+
import org.junit.jupiter.api.extension.ExtendWith;
9+
10+
import java.util.concurrent.TimeoutException;
11+
12+
import static org.junit.jupiter.api.Assertions.*;
13+
14+
/**
15+
* Integration tests for validating the serialization of exceptions in various scenarios.
16+
*/
17+
@Tag("integration")
18+
@ExtendWith(TestRetryExtension.class)
19+
public class ExceptionSerializationIntegrationTest extends IntegrationTestBase {
20+
21+
@RetryingTest
22+
void testMultilineExceptionMessage() throws TimeoutException {
23+
final String orchestratorName = "MultilineExceptionOrchestrator";
24+
final String multilineErrorMessage = "Line 1\nLine 2\nLine 3";
25+
26+
DurableTaskGrpcWorker worker = this.createWorkerBuilder()
27+
.addOrchestrator(orchestratorName, ctx -> {
28+
throw new RuntimeException(multilineErrorMessage);
29+
})
30+
.buildAndStart();
31+
32+
DurableTaskClient client = new DurableTaskGrpcClientBuilder().build();
33+
try (worker; client) {
34+
String instanceId = client.scheduleNewOrchestrationInstance(orchestratorName, 0);
35+
OrchestrationMetadata instance = client.waitForInstanceCompletion(instanceId, defaultTimeout, true);
36+
assertNotNull(instance);
37+
assertEquals(OrchestrationRuntimeStatus.FAILED, instance.getRuntimeStatus());
38+
39+
FailureDetails details = instance.getFailureDetails();
40+
assertNotNull(details);
41+
assertEquals("java.lang.RuntimeException", details.getErrorType());
42+
assertEquals(multilineErrorMessage, details.getErrorMessage());
43+
assertNotNull(details.getStackTrace());
44+
}
45+
}
46+
47+
@RetryingTest
48+
void testNestedExceptions() throws TimeoutException {
49+
final String orchestratorName = "NestedExceptionOrchestrator";
50+
final String innerMessage = "Inner exception";
51+
final String outerMessage = "Outer exception";
52+
53+
DurableTaskGrpcWorker worker = this.createWorkerBuilder()
54+
.addOrchestrator(orchestratorName, ctx -> {
55+
Exception innerException = new IllegalArgumentException(innerMessage);
56+
throw new RuntimeException(outerMessage, innerException);
57+
})
58+
.buildAndStart();
59+
60+
DurableTaskClient client = new DurableTaskGrpcClientBuilder().build();
61+
try (worker; client) {
62+
String instanceId = client.scheduleNewOrchestrationInstance(orchestratorName, 0);
63+
OrchestrationMetadata instance = client.waitForInstanceCompletion(instanceId, defaultTimeout, true);
64+
assertNotNull(instance);
65+
assertEquals(OrchestrationRuntimeStatus.FAILED, instance.getRuntimeStatus());
66+
67+
FailureDetails details = instance.getFailureDetails();
68+
assertNotNull(details);
69+
assertEquals("java.lang.RuntimeException", details.getErrorType());
70+
assertEquals(outerMessage, details.getErrorMessage());
71+
assertNotNull(details.getStackTrace());
72+
73+
// Verify both exceptions are in the stack trace
74+
String stackTrace = details.getStackTrace();
75+
assertTrue(stackTrace.contains(outerMessage), "Stack trace should contain outer exception message");
76+
assertTrue(stackTrace.contains(innerMessage), "Stack trace should contain inner exception message");
77+
assertTrue(stackTrace.contains("Caused by: java.lang.IllegalArgumentException"),
78+
"Stack trace should include 'Caused by' section for inner exception");
79+
}
80+
}
81+
82+
@RetryingTest
83+
void testCustomExceptionWithNonStandardToString() throws TimeoutException {
84+
final String orchestratorName = "CustomExceptionOrchestrator";
85+
final String customMessage = "Custom exception message";
86+
87+
DurableTaskGrpcWorker worker = this.createWorkerBuilder()
88+
.addOrchestrator(orchestratorName, ctx -> {
89+
throw new CustomException(customMessage);
90+
})
91+
.buildAndStart();
92+
93+
DurableTaskClient client = new DurableTaskGrpcClientBuilder().build();
94+
try (worker; client) {
95+
String instanceId = client.scheduleNewOrchestrationInstance(orchestratorName, 0);
96+
OrchestrationMetadata instance = client.waitForInstanceCompletion(instanceId, defaultTimeout, true);
97+
assertNotNull(instance);
98+
assertEquals(OrchestrationRuntimeStatus.FAILED, instance.getRuntimeStatus());
99+
100+
FailureDetails details = instance.getFailureDetails();
101+
assertNotNull(details);
102+
String expectedType = CustomException.class.getName();
103+
assertEquals(expectedType, details.getErrorType());
104+
assertEquals(customMessage, details.getErrorMessage());
105+
assertNotNull(details.getStackTrace());
106+
}
107+
}
108+
109+
/**
110+
* Custom exception class with a non-standard toString implementation.
111+
*/
112+
private static class CustomException extends RuntimeException {
113+
public CustomException(String message) {
114+
super(message);
115+
}
116+
117+
@Override
118+
public String toString() {
119+
return "CUSTOM_EXCEPTION_FORMAT: " + getMessage();
120+
}
121+
}
122+
}

0 commit comments

Comments
 (0)