From a75d16fd2d8c6175347ae3e5adc0df5fc9e59182 Mon Sep 17 00:00:00 2001 From: "M.P. Korstanje" Date: Mon, 17 Nov 2025 22:51:56 +0100 Subject: [PATCH] Prune org.junit.start from exceptions 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 org.junit.jupiter.api@6.1.0-SNAPSHOT/org.junit.jupiter.api.AssertionFailureBuilder.build(AssertionFailureBuilder.java:158) at org.junit.jupiter.api@6.1.0-SNAPSHOT/org.junit.jupiter.api.AssertionFailureBuilder.buildAndThrow(AssertionFailureBuilder.java:139) at org.junit.jupiter.api@6.1.0-SNAPSHOT/org.junit.jupiter.api.AssertEquals.failNotEqual(AssertEquals.java:201) at org.junit.jupiter.api@6.1.0-SNAPSHOT/org.junit.jupiter.api.AssertEquals.assertEquals(AssertEquals.java:152) at org.junit.jupiter.api@6.1.0-SNAPSHOT/org.junit.jupiter.api.AssertEquals.assertEquals(AssertEquals.java:147) at org.junit.jupiter.api@6.1.0-SNAPSHOT/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 org.junit.platform.launcher@6.1.0-SNAPSHOT/org.junit.platform.launcher.core.SessionPerRequestLauncher.execute(SessionPerRequestLauncher.java:81) at org.junit.start@6.1.0-SNAPSHOT/org.junit.start.JUnit.run(JUnit.java:63) at org.junit.start@6.1.0-SNAPSHOT/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. --- .../platform/commons/util/ExceptionUtils.java | 9 ++++++++- .../commons/util/ExceptionUtilsTests.java | 20 +++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) 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..777332e106b1 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 @@ -133,7 +133,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(); @@ -156,6 +156,13 @@ else if (STACK_TRACE_ELEMENT_FILTER.test(className)) { 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..8bbdee1a4dee 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,25 @@ void pruneStackTraceOfEverythingPriorToFirstLauncherCall() { .noneMatch(element -> element.toString().contains("org.example.Class.method(file:123)")); } + @Test + void pruneStackTraceOfJUnitStart() { + var testClassName = ExceptionUtilsTests.class.getCanonicalName(); + + var exception = new JUnitException("expected"); + var stackTrace = exception.getStackTrace(); + + var extendedStacktrace = Arrays.copyOfRange(stackTrace, 0, stackTrace.length + 2); + extendedStacktrace[stackTrace.length] = new StackTraceElement("org.junit.start.JUnit", "run", "JUnit.class", 3); + extendedStacktrace[stackTrace.length + 1] = new StackTraceElement(testClassName, "main", + "ExceptionUtilsTest.class", 5); + exception.setStackTrace(extendedStacktrace); + + pruneStackTrace(exception, List.of(testClassName)); + + assertThat(exception.getStackTrace()) // + .noneMatch(element -> element.toString().contains(testClassName + ".main(file:5)")); + } + @Test void findSuppressedExceptionsAndCausesOfThrowable() { Throwable t1 = new Throwable("#1");