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());

Reply via email to