Skip to content

Commit de84102

Browse files
authored
Prune exceptions when JUnit.start is used (#5158)
Prune exceptions when JUnit.start is used When using `JUnit.start` the `pruneStackTrace` algorithm immediately sees the `TestClass.main` frame and assumes that this is the test method because the test class name matches. ``` org.opentest4j.AssertionFailedError: expected: <11> but was: <12> at [email protected]/org.junit.jupiter.api.AssertionFailureBuilder.build(AssertionFailureBuilder.java:158) at [email protected]/org.junit.jupiter.api.AssertionFailureBuilder.buildAndThrow(AssertionFailureBuilder.java:139) at [email protected]/org.junit.jupiter.api.AssertEquals.failNotEqual(AssertEquals.java:201) at [email protected]/org.junit.jupiter.api.AssertEquals.assertEquals(AssertEquals.java:152) at [email protected]/org.junit.jupiter.api.AssertEquals.assertEquals(AssertEquals.java:147) at [email protected]/org.junit.jupiter.api.Assertions.assertEquals(Assertions.java:558) at com.examp.project/com.example.project.HelloTest.stringLength(HelloTest.java:14) at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ... at [email protected]/org.junit.platform.launcher.core.SessionPerRequestLauncher.execute(SessionPerRequestLauncher.java:81) at [email protected]/org.junit.start.JUnit.run(JUnit.java:63) at [email protected]/org.junit.start.JUnit.run(JUnit.java:37) at com.examp.project/com.example.project.HelloTest.main(HelloTest.java:9) ``` By checking if `org.junit.start` is involved further down the stack we exclude this scenario. And can limit the stacktrace to a more readable. ``` org.opentest4j.AssertionFailedError: expected: <11> but was: <12> at [email protected]/org.junit.jupiter.api.Assertions.assertEquals(Assertions.java:558) at com.examp.project/com.example.project.HelloTest.stringLength(HelloTest.java:14) at [email protected]/org.junit.start.JUnit.run(JUnit.java:37) at com.examp.project/com.example.project.HelloTest.main(HelloTest.java:9) ```
1 parent 94dc7be commit de84102

File tree

2 files changed

+49
-6
lines changed

2 files changed

+49
-6
lines changed

junit-platform-commons/src/main/java/org/junit/platform/commons/util/ExceptionUtils.java

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -101,16 +101,19 @@ public static String readStackTrace(Throwable throwable) {
101101
/**
102102
* Prune the stack trace of the supplied {@link Throwable}.
103103
*
104-
* <p>Prune all {@linkplain StackTraceElement stack trace elements} up one
105-
* of the supplied {@code classNames} are pruned. All subsequent elements
106-
* in the stack trace will be retained.
104+
* <p>Prune all {@linkplain StackTraceElement stack trace elements} up to one
105+
* of the supplied {@code classNames}. All subsequent elements in the stack
106+
* trace will be retained.
107107
*
108108
* <p>If the {@code classNames} do not match any of the stacktrace elements
109109
* then the {@code org.junit}, {@code jdk.internal.reflect}, and
110110
* {@code sun.reflect} packages are pruned.
111111
*
112-
* <p>Additionally, all elements prior to and including the first JUnit Platform
113-
* Launcher call will be removed.
112+
* <p>Additionally:
113+
* <ul>
114+
* <li>all elements prior to and including the first JUnit Platform Launcher call will be removed.
115+
* <li>all elements prior to and including {@code org.junit.start} are kept.
116+
* </ul>
114117
*
115118
* @param throwable the {@code Throwable} whose stack trace should be pruned;
116119
* never {@code null}
@@ -126,14 +129,15 @@ public static void pruneStackTrace(Throwable throwable, List<String> classNames)
126129

127130
List<StackTraceElement> stackTrace = Arrays.asList(throwable.getStackTrace());
128131
List<StackTraceElement> prunedStackTrace = new ArrayList<>();
132+
List<StackTraceElement> junitStartStackTrace = new ArrayList<>(0);
129133

130134
Collections.reverse(stackTrace);
131135

132136
for (int i = 0; i < stackTrace.size(); i++) {
133137
StackTraceElement element = stackTrace.get(i);
134138
String className = element.getClassName();
135139

136-
if (classNames.contains(className)) {
140+
if (classNames.contains(className) && !includesJunitStart(stackTrace, i + 1)) {
137141
// We found the test
138142
// everything before that is not informative.
139143
prunedStackTrace.clear();
@@ -142,7 +146,9 @@ public static void pruneStackTrace(Throwable throwable, List<String> classNames)
142146
break;
143147
}
144148
else if (className.startsWith(JUNIT_START_PACKAGE_PREFIX)) {
149+
junitStartStackTrace.addAll(prunedStackTrace);
145150
prunedStackTrace.clear();
151+
junitStartStackTrace.add(element);
146152
}
147153
else if (className.startsWith(JUNIT_PLATFORM_LAUNCHER_PACKAGE_PREFIX)) {
148154
prunedStackTrace.clear();
@@ -152,10 +158,22 @@ else if (STACK_TRACE_ELEMENT_FILTER.test(className)) {
152158
}
153159
}
154160

161+
if (!junitStartStackTrace.isEmpty()) {
162+
junitStartStackTrace.addAll(prunedStackTrace);
163+
prunedStackTrace = junitStartStackTrace;
164+
}
165+
155166
Collections.reverse(prunedStackTrace);
156167
throwable.setStackTrace(prunedStackTrace.toArray(new StackTraceElement[0]));
157168
}
158169

170+
private static boolean includesJunitStart(List<StackTraceElement> stackTrace, int fromIndex) {
171+
return stackTrace.stream() //
172+
.skip(fromIndex) //
173+
.map(StackTraceElement::getClassName) //
174+
.anyMatch(className -> className.startsWith(JUNIT_START_PACKAGE_PREFIX));
175+
}
176+
159177
/**
160178
* Find all causes and suppressed exceptions in the stack trace of the
161179
* supplied {@link Throwable}.

platform-tests/src/test/java/org/junit/platform/commons/util/ExceptionUtilsTests.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import static org.junit.platform.commons.util.ExceptionUtils.throwAsUncheckedException;
2020

2121
import java.io.IOException;
22+
import java.util.Arrays;
2223
import java.util.List;
2324

2425
import org.junit.jupiter.api.Test;
@@ -104,6 +105,30 @@ void pruneStackTraceOfEverythingPriorToFirstLauncherCall() {
104105
.noneMatch(element -> element.toString().contains("org.example.Class.method(file:123)"));
105106
}
106107

108+
@Test
109+
void pruneStackTraceRetainsStackFramesFromJUnitStart() {
110+
// Non-test class frames from org.junit are filtered.
111+
var testClassName = "com.example.project.HelloTest";
112+
var testFileName = "HelloTest.java";
113+
114+
var exception = new JUnitException("expected");
115+
var stackTrace = exception.getStackTrace();
116+
var extendedStacktrace = Arrays.copyOfRange(stackTrace, 0, stackTrace.length + 2);
117+
extendedStacktrace[0] = new StackTraceElement(testClassName, "stringLength", testFileName, 10);
118+
extendedStacktrace[stackTrace.length] = new StackTraceElement("org.junit.start.JUnit", "run", "JUnit.java", 3);
119+
extendedStacktrace[stackTrace.length + 1] = new StackTraceElement(testClassName, "main", testFileName, 5);
120+
exception.setStackTrace(extendedStacktrace);
121+
122+
pruneStackTrace(exception, List.of(testClassName));
123+
124+
assertThat(exception.getStackTrace()) //
125+
.extracting(StackTraceElement::toString) //
126+
.containsExactly( //
127+
"com.example.project.HelloTest.stringLength(HelloTest.java:10)", //
128+
"org.junit.start.JUnit.run(JUnit.java:3)", //
129+
"com.example.project.HelloTest.main(HelloTest.java:5)");
130+
}
131+
107132
@Test
108133
void findSuppressedExceptionsAndCausesOfThrowable() {
109134
Throwable t1 = new Throwable("#1");

0 commit comments

Comments
 (0)