This is an automated email from the ASF dual-hosted git repository.
garydgregory pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-lang.git
The following commit(s) were added to refs/heads/master by this push:
new 0f76857f5 Clarify ExceptionUtils common-frame removal and strengthen
regression coverage
0f76857f5 is described below
commit 0f76857f50615046721a883b5c2062a4e0030859
Author: Gary Gregory <[email protected]>
AuthorDate: Sun Oct 25 09:02:36 2026 -0400
Clarify ExceptionUtils common-frame removal and strengthen regression
coverage
Document shared-suffix matching, list mutation, and the distinction from
JDK frame comparison. Add coverage for an immediate mismatch and verify
that wrapper frames remain unchanged.
---
src/changes/changes.xml | 1 +
.../commons/lang3/exception/ExceptionUtils.java | 17 +++++++++--
.../lang3/exception/ExceptionUtilsTest.java | 33 +++++++++++++++-------
3 files changed, 38 insertions(+), 13 deletions(-)
diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index 1a31fe8be..84a4f428a 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -73,6 +73,7 @@ The <action> type attribute can be add,update,fix,remove.
<action type="fix" dev="ggregory" due-to="alhuda, Gary
Gregory">Keep StrSubstitutor from treating a value's trailing $ as an escape
(#1834).</action>
<action type="fix" dev="ggregory" due-to="alhuda, Gary
Gregory">Fix FastDateParser normalization of out-of-range ISO day-of-week
values (#1839).</action>
<action type="fix" dev="ggregory" due-to="Aminark, Gary
Gregory">[javadoc] Correct StringUtils example methods and overloads
(#1836).</action>
+ <action type="fix" dev="ggregory" due-to="alhuda, Gary
Gregory">Stop ExceptionUtils.removeCommonFrames at the first differing frame
(#1831).</action>
<!-- ADD -->
<!-- UPDATE -->
<action type="update" dev="ggregory" due-to="Gary Gregory">Bump
org.apache.commons:commons-parent from 105 to 106.</action>
diff --git
a/src/main/java/org/apache/commons/lang3/exception/ExceptionUtils.java
b/src/main/java/org/apache/commons/lang3/exception/ExceptionUtils.java
index 2c5bed6a2..fcdfdffaa 100644
--- a/src/main/java/org/apache/commons/lang3/exception/ExceptionUtils.java
+++ b/src/main/java/org/apache/commons/lang3/exception/ExceptionUtils.java
@@ -909,10 +909,21 @@ public static void printRootCauseStackTrace(final
Throwable throwable, final Pri
}
/**
- * Removes common frames from the cause trace given the two stack traces.
+ * Removes the longest shared suffix from the cause stack trace.
*
- * @param causeFrames stack trace of a cause throwable.
- * @param wrapperFrames stack trace of a wrapper throwable.
+ * <p>
+ * Compares frame strings from the ends of the two lists, stopping at the
first difference or when either list is exhausted. Frames at lower indexes
+ * than the first differing frame are retained, even if they match. The
cause list is modified in place. The wrapper list is only read; changes to the
+ * cause list may also be visible through the wrapper list if the lists
share backing storage.
+ * </p>
+ *
+ * <p>
+ * This follows the shared-suffix rule used by {@link
Throwable#printStackTrace()}, but compares formatted frame strings rather than
+ * {@link StackTraceElement} objects.
+ * </p>
+ *
+ * @param causeFrames stack trace of a cause throwable, from which the
shared suffix is removed.
+ * @param wrapperFrames stack trace of a wrapper throwable, used for
comparison.
* @throws NullPointerException Thrown if either argument is null.
* @since 2.0
*/
diff --git
a/src/test/java/org/apache/commons/lang3/exception/ExceptionUtilsTest.java
b/src/test/java/org/apache/commons/lang3/exception/ExceptionUtilsTest.java
index a52f34d0d..12d9dbf26 100644
--- a/src/test/java/org/apache/commons/lang3/exception/ExceptionUtilsTest.java
+++ b/src/test/java/org/apache/commons/lang3/exception/ExceptionUtilsTest.java
@@ -152,6 +152,10 @@ public static class TestThrowable extends Throwable {
private static final long serialVersionUID = 1L;
}
+ private static RuntimeException newRuntimeException(final String message,
final Throwable cause) {
+ return new RuntimeException(message, cause);
+ }
+
private static int redeclareCheckedException() {
return throwsCheckedException();
}
@@ -919,21 +923,21 @@ void testRemoveCommonFramesStopsAtFirstDifference() {
"\tat a.B.main(B.java:9)"));
final List<String> wrapperFrames = Arrays.asList("\tat
a.Factory.create(Factory.java:1)", "\tat a.B.two(B.java:5)",
"\tat a.B.main(B.java:9)");
+ final List<String> expectedWrapperFrames = new
ArrayList<>(wrapperFrames);
ExceptionUtils.removeCommonFrames(causeFrames, wrapperFrames);
assertEquals(Arrays.asList("\tat a.Factory.create(Factory.java:1)",
"\tat a.B.one(B.java:2)"), causeFrames);
+ assertEquals(expectedWrapperFrames, wrapperFrames);
}
@Test
- void testRootCauseStackTraceKeepsSharedFactoryFrame() {
- final RuntimeException cause = newRuntimeException("cause", null);
- final RuntimeException wrapper = newRuntimeException("wrapper", cause);
- final List<String> frames =
ExceptionUtils.getRootCauseStackTraceList(wrapper);
- assertEquals(cause.toString(), frames.get(0));
- assertTrue(frames.get(1).contains("newRuntimeException"),
frames.get(1));
- }
-
- private static RuntimeException newRuntimeException(final String message,
final Throwable cause) {
- return new RuntimeException(message, cause);
+ void testRemoveCommonFramesStopsAtImmediateDifference() {
+ final List<String> causeFrames = new ArrayList<>(Arrays.asList("\tat
a.Factory.create(Factory.java:1)", "\tat a.B.one(B.java:2)"));
+ final List<String> wrapperFrames = new ArrayList<>(Arrays.asList("\tat
a.Factory.create(Factory.java:1)", "\tat a.B.two(B.java:5)"));
+ final List<String> expectedCauseFrames = new ArrayList<>(causeFrames);
+ final List<String> expectedWrapperFrames = new
ArrayList<>(wrapperFrames);
+ ExceptionUtils.removeCommonFrames(causeFrames, wrapperFrames);
+ assertEquals(expectedCauseFrames, causeFrames);
+ assertEquals(expectedWrapperFrames, wrapperFrames);
}
@Test
@@ -959,6 +963,15 @@ void testRethrow() {
assertNotSame(expected, assertThrows(expected.getClass(), () ->
ExceptionUtils.rethrow(new InterruptedException())));
}
+ @Test
+ void testRootCauseStackTraceKeepsSharedFactoryFrame() {
+ final RuntimeException cause = newRuntimeException("cause", null);
+ final RuntimeException wrapper = newRuntimeException("wrapper", cause);
+ final List<String> frames =
ExceptionUtils.getRootCauseStackTraceList(wrapper);
+ assertEquals(cause.toString(), frames.get(0));
+ assertTrue(frames.get(1).contains("newRuntimeException"),
frames.get(1));
+ }
+
@Test
void testStream_jdkNoCause() {
assertEquals(1, ExceptionUtils.stream(jdkNoCause).count());