Skip to content

Commit 0f0e219

Browse files
authored
3.x: Change how the cause of CompositeException is generated (#6748)
* 3.x: Change how the cause of CompositeException is generated * Fix reoccurrence formatting. * Fix a mistake in the unit test
1 parent 4c97d20 commit 0f0e219

File tree

2 files changed

+143
-165
lines changed

2 files changed

+143
-165
lines changed

Diff for: src/main/java/io/reactivex/rxjava3/exceptions/CompositeException.java

+70-71
Original file line numberDiff line numberDiff line change
@@ -106,40 +106,69 @@ public String getMessage() {
106106
@NonNull
107107
public synchronized Throwable getCause() { // NOPMD
108108
if (cause == null) {
109-
// we lazily generate this causal chain if this is called
110-
CompositeExceptionCausalChain localCause = new CompositeExceptionCausalChain();
111-
Set<Throwable> seenCauses = new HashSet<Throwable>();
112-
113-
Throwable chain = localCause;
114-
for (Throwable e : exceptions) {
115-
if (seenCauses.contains(e)) {
116-
// already seen this outer Throwable so skip
117-
continue;
118-
}
119-
seenCauses.add(e);
120-
121-
List<Throwable> listOfCauses = getListOfCauses(e);
122-
// check if any of them have been seen before
123-
for (Throwable child : listOfCauses) {
124-
if (seenCauses.contains(child)) {
125-
// already seen this outer Throwable so skip
126-
e = new RuntimeException("Duplicate found in causal chain so cropping to prevent loop ...");
127-
continue;
109+
String separator = System.getProperty("line.separator");
110+
if (exceptions.size() > 1) {
111+
Map<Throwable, Boolean> seenCauses = new IdentityHashMap<Throwable, Boolean>();
112+
113+
StringBuilder aggregateMessage = new StringBuilder();
114+
aggregateMessage.append("Multiple exceptions (").append(exceptions.size()).append(")").append(separator);
115+
116+
for (Throwable inner : exceptions) {
117+
int depth = 0;
118+
while (inner != null) {
119+
for (int i = 0; i < depth; i++) {
120+
aggregateMessage.append(" ");
121+
}
122+
aggregateMessage.append("|-- ");
123+
aggregateMessage.append(inner.getClass().getCanonicalName()).append(": ");
124+
String innerMessage = inner.getMessage();
125+
if (innerMessage != null && innerMessage.contains(separator)) {
126+
aggregateMessage.append(separator);
127+
for (String line : innerMessage.split(separator)) {
128+
for (int i = 0; i < depth + 2; i++) {
129+
aggregateMessage.append(" ");
130+
}
131+
aggregateMessage.append(line).append(separator);
132+
}
133+
} else {
134+
aggregateMessage.append(innerMessage);
135+
aggregateMessage.append(separator);
136+
}
137+
138+
for (int i = 0; i < depth + 2; i++) {
139+
aggregateMessage.append(" ");
140+
}
141+
StackTraceElement[] st = inner.getStackTrace();
142+
if (st.length > 0) {
143+
aggregateMessage.append("at ").append(st[0]).append(separator);
144+
}
145+
146+
if (!seenCauses.containsKey(inner)) {
147+
seenCauses.put(inner, true);
148+
149+
inner = inner.getCause();
150+
depth++;
151+
} else {
152+
inner = inner.getCause();
153+
if (inner != null) {
154+
for (int i = 0; i < depth + 2; i++) {
155+
aggregateMessage.append(" ");
156+
}
157+
aggregateMessage.append("|-- ");
158+
aggregateMessage.append("(cause not expanded again) ");
159+
aggregateMessage.append(inner.getClass().getCanonicalName()).append(": ");
160+
aggregateMessage.append(inner.getMessage());
161+
aggregateMessage.append(separator);
162+
}
163+
break;
164+
}
128165
}
129-
seenCauses.add(child);
130166
}
131167

132-
// we now have 'e' as the last in the chain
133-
try {
134-
chain.initCause(e);
135-
} catch (Throwable t) { // NOPMD
136-
// ignore
137-
// the JavaDocs say that some Throwables (depending on how they're made) will never
138-
// let me call initCause without blowing up even if it returns null
139-
}
140-
chain = getRootCause(chain);
168+
cause = new ExceptionOverview(aggregateMessage.toString().trim());
169+
} else {
170+
cause = exceptions.get(0);
141171
}
142-
cause = localCause;
143172
}
144173
return cause;
145174
}
@@ -236,31 +265,21 @@ void println(Object o) {
236265
}
237266
}
238267

239-
static final class CompositeExceptionCausalChain extends RuntimeException {
268+
/**
269+
* Contains a formatted message with a simplified representation of the exception graph
270+
* contained within the CompositeException.
271+
*/
272+
static final class ExceptionOverview extends RuntimeException {
273+
240274
private static final long serialVersionUID = 3875212506787802066L;
241-
/* package-private */static final String MESSAGE = "Chain of Causes for CompositeException In Order Received =>";
242275

243-
@Override
244-
public String getMessage() {
245-
return MESSAGE;
276+
ExceptionOverview(String message) {
277+
super(message);
246278
}
247-
}
248279

249-
private List<Throwable> getListOfCauses(Throwable ex) {
250-
List<Throwable> list = new ArrayList<Throwable>();
251-
Throwable root = ex.getCause();
252-
if (root == null || root == ex) {
253-
return list;
254-
} else {
255-
while (true) {
256-
list.add(root);
257-
Throwable cause = root.getCause();
258-
if (cause == null || cause == root) {
259-
return list;
260-
} else {
261-
root = cause;
262-
}
263-
}
280+
@Override
281+
public synchronized Throwable fillInStackTrace() {
282+
return this;
264283
}
265284
}
266285

@@ -271,24 +290,4 @@ private List<Throwable> getListOfCauses(Throwable ex) {
271290
public int size() {
272291
return exceptions.size();
273292
}
274-
275-
/**
276-
* Returns the root cause of {@code e}. If {@code e.getCause()} returns {@code null} or {@code e}, just return {@code e} itself.
277-
*
278-
* @param e the {@link Throwable} {@code e}.
279-
* @return The root cause of {@code e}. If {@code e.getCause()} returns {@code null} or {@code e}, just return {@code e} itself.
280-
*/
281-
/*private */Throwable getRootCause(Throwable e) {
282-
Throwable root = e.getCause();
283-
if (root == null || e == root) {
284-
return e;
285-
}
286-
while (true) {
287-
Throwable cause = root.getCause();
288-
if (cause == null || cause == root) {
289-
return root;
290-
}
291-
root = cause;
292-
}
293-
}
294293
}

Diff for: src/test/java/io/reactivex/rxjava3/exceptions/CompositeExceptionTest.java

+73-94
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@
2323
import org.junit.Test;
2424

2525
import io.reactivex.rxjava3.core.RxJavaTest;
26-
import io.reactivex.rxjava3.exceptions.CompositeException.CompositeExceptionCausalChain;
2726

2827
public class CompositeExceptionTest extends RxJavaTest {
2928

@@ -251,41 +250,6 @@ public void messageVarargs() {
251250
assertEquals("3 exceptions occurred. ", compositeException.getMessage());
252251
}
253252

254-
@Test
255-
public void complexCauses() {
256-
Throwable e1 = new Throwable("1");
257-
Throwable e2 = new Throwable("2");
258-
e1.initCause(e2);
259-
260-
Throwable e3 = new Throwable("3");
261-
Throwable e4 = new Throwable("4");
262-
e3.initCause(e4);
263-
264-
Throwable e5 = new Throwable("5");
265-
Throwable e6 = new Throwable("6");
266-
e5.initCause(e6);
267-
268-
CompositeException compositeException = new CompositeException(e1, e3, e5);
269-
assertTrue(compositeException.getCause() instanceof CompositeExceptionCausalChain);
270-
271-
List<Throwable> causeChain = new ArrayList<Throwable>();
272-
Throwable cause = compositeException.getCause().getCause();
273-
while (cause != null) {
274-
causeChain.add(cause);
275-
cause = cause.getCause();
276-
}
277-
// The original relations
278-
//
279-
// e1 -> e2
280-
// e3 -> e4
281-
// e5 -> e6
282-
//
283-
// will be set to
284-
//
285-
// e1 -> e2 -> e3 -> e4 -> e5 -> e6
286-
assertEquals(Arrays.asList(e1, e2, e3, e4, e5, e6), causeChain);
287-
}
288-
289253
@Test
290254
public void constructorWithNull() {
291255
assertTrue(new CompositeException((Throwable[])null).getExceptions().get(0) instanceof NullPointerException);
@@ -306,81 +270,96 @@ public void printStackTrace() {
306270
}
307271

308272
@Test
309-
public void cyclicRootCause() {
310-
RuntimeException te = new RuntimeException() {
311-
312-
private static final long serialVersionUID = -8492568224555229753L;
313-
Throwable cause;
314-
315-
@Override
316-
public Throwable initCause(Throwable c) {
317-
return this;
318-
}
319-
320-
@Override
321-
public Throwable getCause() {
322-
return cause;
323-
}
324-
};
325-
326-
assertSame(te, new CompositeException(te).getCause().getCause());
327-
328-
assertSame(te, new CompositeException(new RuntimeException(te)).getCause().getCause().getCause());
273+
public void badException() {
274+
Throwable e = new BadException();
275+
assertSame(e, new CompositeException(e).getCause().getCause());
276+
assertSame(e, new CompositeException(new RuntimeException(e)).getCause().getCause().getCause());
329277
}
330278

331279
@Test
332-
public void nullRootCause() {
333-
RuntimeException te = new RuntimeException() {
280+
public void exceptionOverview() {
281+
CompositeException composite = new CompositeException(
282+
new TestException("ex1"),
283+
new TestException("ex2"),
284+
new TestException("ex3", new TestException("ex4"))
285+
);
286+
287+
String overview = composite.getCause().getMessage();
288+
289+
assertTrue(overview, overview.contains("Multiple exceptions (3)"));
290+
assertTrue(overview, overview.contains("io.reactivex.rxjava3.exceptions.TestException: ex1"));
291+
assertTrue(overview, overview.contains("io.reactivex.rxjava3.exceptions.TestException: ex2"));
292+
assertTrue(overview, overview.contains("io.reactivex.rxjava3.exceptions.TestException: ex3"));
293+
assertTrue(overview, overview.contains("io.reactivex.rxjava3.exceptions.TestException: ex4"));
294+
assertTrue(overview, overview.contains("at io.reactivex.rxjava3.exceptions.CompositeExceptionTest.exceptionOverview"));
295+
}
334296

335-
private static final long serialVersionUID = -8492568224555229753L;
297+
@Test
298+
public void causeWithExceptionWithoutStacktrace() {
299+
CompositeException composite = new CompositeException(
300+
new TestException("ex1"),
301+
new CompositeException.ExceptionOverview("example")
302+
);
336303

337-
@Override
338-
public Throwable getCause() {
339-
return null;
340-
}
341-
};
304+
String overview = composite.getCause().getMessage();
342305

343-
assertSame(te, new CompositeException(te).getCause().getCause());
306+
assertTrue(overview, overview.contains("Multiple exceptions (2)"));
307+
assertTrue(overview, overview.contains("io.reactivex.rxjava3.exceptions.TestException: ex1"));
308+
assertTrue(overview, overview.contains("io.reactivex.rxjava3.exceptions.CompositeException.ExceptionOverview: example"));
344309

345-
assertSame(te, new CompositeException(new RuntimeException(te)).getCause().getCause().getCause());
310+
assertEquals(overview, 2, overview.split("at\\s").length);
346311
}
347312

348313
@Test
349-
public void badException() {
350-
Throwable e = new BadException();
351-
assertSame(e, new CompositeException(e).getCause().getCause());
352-
assertSame(e, new CompositeException(new RuntimeException(e)).getCause().getCause().getCause());
314+
public void reoccurringException() {
315+
TestException ex0 = new TestException("ex0");
316+
TestException ex1 = new TestException("ex1", ex0);
317+
CompositeException composite = new CompositeException(
318+
ex1,
319+
new TestException("ex2", ex1)
320+
);
321+
322+
String overview = composite.getCause().getMessage();
323+
System.err.println(overview);
324+
325+
assertTrue(overview, overview.contains("Multiple exceptions (2)"));
326+
assertTrue(overview, overview.contains("io.reactivex.rxjava3.exceptions.TestException: ex0"));
327+
assertTrue(overview, overview.contains("io.reactivex.rxjava3.exceptions.TestException: ex1"));
328+
assertTrue(overview, overview.contains("io.reactivex.rxjava3.exceptions.TestException: ex2"));
329+
assertTrue(overview, overview.contains("(cause not expanded again) io.reactivex.rxjava3.exceptions.TestException: ex0"));
330+
assertEquals(overview, 5, overview.split("at\\s").length);
353331
}
354332

355333
@Test
356-
public void rootCauseEval() {
357-
final TestException ex0 = new TestException();
358-
Throwable throwable = new Throwable() {
359-
360-
private static final long serialVersionUID = 3597694032723032281L;
361-
362-
@Override
363-
public synchronized Throwable getCause() {
364-
return ex0;
365-
}
366-
};
367-
CompositeException ex = new CompositeException(throwable);
368-
assertSame(ex0, ex.getRootCause(ex));
334+
public void nestedMultilineMessage() {
335+
TestException ex1 = new TestException("ex1");
336+
TestException ex2 = new TestException("ex2");
337+
CompositeException composite1 = new CompositeException(
338+
ex1,
339+
ex2
340+
);
341+
TestException ex3 = new TestException("ex3");
342+
TestException ex4 = new TestException("ex4", composite1);
343+
344+
CompositeException composite2 = new CompositeException(
345+
ex3,
346+
ex4
347+
);
348+
349+
String overview = composite2.getCause().getMessage();
350+
System.err.println(overview);
351+
352+
assertTrue(overview, overview.contains(" Multiple exceptions (2)"));
353+
assertTrue(overview, overview.contains(" |-- io.reactivex.rxjava3.exceptions.TestException: ex1"));
354+
assertTrue(overview, overview.contains(" |-- io.reactivex.rxjava3.exceptions.TestException: ex2"));
369355
}
370356

371357
@Test
372-
public void rootCauseSelf() {
373-
Throwable throwable = new Throwable() {
358+
public void singleExceptionIsTheCause() {
359+
TestException ex = new TestException("ex1");
360+
CompositeException composite = new CompositeException(ex);
374361

375-
private static final long serialVersionUID = -4398003222998914415L;
376-
377-
@Override
378-
public synchronized Throwable getCause() {
379-
return this;
380-
}
381-
};
382-
CompositeException tmp = new CompositeException(new TestException());
383-
assertSame(throwable, tmp.getRootCause(throwable));
362+
assertSame(composite.getCause(), ex);
384363
}
385364
}
386365

0 commit comments

Comments
 (0)