diff --git a/junit-platform-commons/src/main/java/org/junit/platform/commons/util/ExceptionUtils.java b/junit-platform-commons/src/main/java/org/junit/platform/commons/util/ExceptionUtils.java index a236b89fc90e..45af4204233f 100644 --- a/junit-platform-commons/src/main/java/org/junit/platform/commons/util/ExceptionUtils.java +++ b/junit-platform-commons/src/main/java/org/junit/platform/commons/util/ExceptionUtils.java @@ -101,16 +101,19 @@ public static String readStackTrace(Throwable throwable) { /** * Prune the stack trace of the supplied {@link Throwable}. * - *

Prune all {@linkplain StackTraceElement stack trace elements} up one - * of the supplied {@code classNames} are pruned. All subsequent elements - * in the stack trace will be retained. + *

Prune all {@linkplain StackTraceElement stack trace elements} up to one + * of the supplied {@code classNames}. All subsequent elements in the stack + * trace will be retained. * *

If the {@code classNames} do not match any of the stacktrace elements * then the {@code org.junit}, {@code jdk.internal.reflect}, and * {@code sun.reflect} packages are pruned. * - *

Additionally, all elements prior to and including the first JUnit Platform - * Launcher call will be removed. + *

Additionally: + *

* * @param throwable the {@code Throwable} whose stack trace should be pruned; * never {@code null} @@ -126,6 +129,7 @@ public static void pruneStackTrace(Throwable throwable, List classNames) List stackTrace = Arrays.asList(throwable.getStackTrace()); List prunedStackTrace = new ArrayList<>(); + List junitStartStackTrace = new ArrayList<>(0); Collections.reverse(stackTrace); @@ -133,7 +137,7 @@ public static void pruneStackTrace(Throwable throwable, List classNames) StackTraceElement element = stackTrace.get(i); String className = element.getClassName(); - if (classNames.contains(className)) { + if (classNames.contains(className) && !includesJunitStart(stackTrace, i + 1)) { // We found the test // everything before that is not informative. prunedStackTrace.clear(); @@ -142,7 +146,9 @@ public static void pruneStackTrace(Throwable throwable, List classNames) break; } else if (className.startsWith(JUNIT_START_PACKAGE_PREFIX)) { + junitStartStackTrace.addAll(prunedStackTrace); prunedStackTrace.clear(); + junitStartStackTrace.add(element); } else if (className.startsWith(JUNIT_PLATFORM_LAUNCHER_PACKAGE_PREFIX)) { prunedStackTrace.clear(); @@ -152,10 +158,22 @@ else if (STACK_TRACE_ELEMENT_FILTER.test(className)) { } } + if (!junitStartStackTrace.isEmpty()) { + junitStartStackTrace.addAll(prunedStackTrace); + prunedStackTrace = junitStartStackTrace; + } + Collections.reverse(prunedStackTrace); throwable.setStackTrace(prunedStackTrace.toArray(new StackTraceElement[0])); } + private static boolean includesJunitStart(List stackTrace, int fromIndex) { + return stackTrace.stream() // + .skip(fromIndex) // + .map(StackTraceElement::getClassName) // + .anyMatch(className -> className.startsWith(JUNIT_START_PACKAGE_PREFIX)); + } + /** * Find all causes and suppressed exceptions in the stack trace of the * supplied {@link Throwable}. diff --git a/platform-tests/src/test/java/org/junit/platform/commons/util/ExceptionUtilsTests.java b/platform-tests/src/test/java/org/junit/platform/commons/util/ExceptionUtilsTests.java index ef82870c3a58..76d9163fd730 100644 --- a/platform-tests/src/test/java/org/junit/platform/commons/util/ExceptionUtilsTests.java +++ b/platform-tests/src/test/java/org/junit/platform/commons/util/ExceptionUtilsTests.java @@ -19,6 +19,7 @@ import static org.junit.platform.commons.util.ExceptionUtils.throwAsUncheckedException; import java.io.IOException; +import java.util.Arrays; import java.util.List; import org.junit.jupiter.api.Test; @@ -104,6 +105,30 @@ void pruneStackTraceOfEverythingPriorToFirstLauncherCall() { .noneMatch(element -> element.toString().contains("org.example.Class.method(file:123)")); } + @Test + void pruneStackTraceRetainsStackFramesFromJUnitStart() { + // Non-test class frames from org.junit are filtered. + var testClassName = "com.example.project.HelloTest"; + var testFileName = "HelloTest.java"; + + var exception = new JUnitException("expected"); + var stackTrace = exception.getStackTrace(); + var extendedStacktrace = Arrays.copyOfRange(stackTrace, 0, stackTrace.length + 2); + extendedStacktrace[0] = new StackTraceElement(testClassName, "stringLength", testFileName, 10); + extendedStacktrace[stackTrace.length] = new StackTraceElement("org.junit.start.JUnit", "run", "JUnit.java", 3); + extendedStacktrace[stackTrace.length + 1] = new StackTraceElement(testClassName, "main", testFileName, 5); + exception.setStackTrace(extendedStacktrace); + + pruneStackTrace(exception, List.of(testClassName)); + + assertThat(exception.getStackTrace()) // + .extracting(StackTraceElement::toString) // + .containsExactly( // + "com.example.project.HelloTest.stringLength(HelloTest.java:10)", // + "org.junit.start.JUnit.run(JUnit.java:3)", // + "com.example.project.HelloTest.main(HelloTest.java:5)"); + } + @Test void findSuppressedExceptionsAndCausesOfThrowable() { Throwable t1 = new Throwable("#1");