From 9e04b5116ef6685f435bb2f9d85e748f924dfbb0 Mon Sep 17 00:00:00 2001 From: "M.P. Korstanje" Date: Tue, 18 Nov 2025 00:55:18 +0100 Subject: [PATCH 1/5] Trim internal frames from AssertionFailedError When using `JUnit.start` and creating a failing test, users will be confronted with a large stacktrace with mostly irrelevant information. Even after #5158 is merged, the stacktrace will contain several internal frames: ``` 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) ``` By pruning these internal frames, the stacktrace can be reduced to a much more readable: ``` org.opentest4j.AssertionFailedError: expected: <11> but was: <12> at com.examp.project/com.example.project.HelloTest.stringLength(HelloTest.java:14) ``` Comparable behaviour can be found in AssertJ[1] and IDEA which folds internal frames in the console using `<6 internal line>`. The pruning functionality is intentionally added to the `AssertionFailureBuilder` rather than the `ExceptionUtils` to enable other users of the builder to also prune the internal frames from their own assertions. 1. https://github.com/assertj/assertj/blob/79bdebf1817692e5e0ff5ee3ab097dcd104d47ae/assertj-core/src/main/java/org/assertj/core/util/Throwables.java#L117-L148 --- .../junit/jupiter/api/AssertArrayEquals.java | 4 ++ .../junit/jupiter/api/AssertDoesNotThrow.java | 1 + .../org/junit/jupiter/api/AssertEquals.java | 1 + .../org/junit/jupiter/api/AssertFalse.java | 1 + .../junit/jupiter/api/AssertInstanceOf.java | 1 + .../jupiter/api/AssertIterableEquals.java | 2 + .../junit/jupiter/api/AssertLinesMatch.java | 1 + .../junit/jupiter/api/AssertNotEquals.java | 1 + .../org/junit/jupiter/api/AssertNotNull.java | 1 + .../org/junit/jupiter/api/AssertNotSame.java | 1 + .../org/junit/jupiter/api/AssertNull.java | 1 + .../org/junit/jupiter/api/AssertSame.java | 1 + .../org/junit/jupiter/api/AssertThrows.java | 2 + .../jupiter/api/AssertThrowsExactly.java | 2 + .../org/junit/jupiter/api/AssertTimeout.java | 1 + .../api/AssertTimeoutPreemptively.java | 1 + .../org/junit/jupiter/api/AssertTrue.java | 1 + .../jupiter/api/AssertionFailureBuilder.java | 49 ++++++++++++++++++- .../org/junit/jupiter/api/AssertionUtils.java | 27 +++++++--- .../org/junit/jupiter/api/Assertions.kt | 2 + .../platform/StackTracePruningTests.java | 35 +++++++------ 21 files changed, 115 insertions(+), 21 deletions(-) diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AssertArrayEquals.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AssertArrayEquals.java index 35bc69437fae..56c707d7c489 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AssertArrayEquals.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AssertArrayEquals.java @@ -483,6 +483,7 @@ private static AssertionFailedError expectedArrayIsNullFailure(@Nullable Deque" + formatIndexes(indexes)) // + .trimStacktrace(Assertions.class) // .build(); } @@ -495,6 +496,7 @@ private static AssertionFailedError actualArrayIsNullFailure(@Nullable Deque" + formatIndexes(indexes)) // + .trimStacktrace(Assertions.class) // .build(); } @@ -507,6 +509,7 @@ private static void assertArraysHaveSameLength(int expected, int actual, @Nullab .reason("array lengths differ" + formatIndexes(indexes)) // .expected(expected) // .actual(actual) // + .trimStacktrace(Assertions.class) // .buildAndThrow(); } } @@ -519,6 +522,7 @@ private static void failArraysNotEqual(@Nullable Object expected, @Nullable Obje .reason("array contents differ" + formatIndexes(indexes)) // .expected(expected) // .actual(actual) // + .trimStacktrace(Assertions.class) // .buildAndThrow(); } diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AssertDoesNotThrow.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AssertDoesNotThrow.java index 82e15c85a41a..68882f795208 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AssertDoesNotThrow.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AssertDoesNotThrow.java @@ -87,6 +87,7 @@ public static AssertionFailedError createAssertionFailedError(@Nullable Object m .message(messageOrSupplier) // .reason("Unexpected exception thrown: " + t.getClass().getName() + buildSuffix(t.getMessage())) // .cause(t) // + .trimStacktrace(Assertions.class) // .build(); } diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AssertEquals.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AssertEquals.java index 2e38bdb6f1c6..d862bbdb1a6b 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AssertEquals.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AssertEquals.java @@ -198,6 +198,7 @@ private static void failNotEqual(@Nullable Object expected, @Nullable Object act .message(messageOrSupplier) // .expected(expected) // .actual(actual) // + .trimStacktrace(Assertions.class) // .buildAndThrow(); } } diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AssertFalse.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AssertFalse.java index d0e308d3be26..4dac4ca3810b 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AssertFalse.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AssertFalse.java @@ -66,6 +66,7 @@ private static void failNotFalse(@Nullable Object messageOrSupplier) { .message(messageOrSupplier) // .expected(false) // .actual(true) // + .trimStacktrace(Assertions.class) // .buildAndThrow(); } diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AssertInstanceOf.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AssertInstanceOf.java index 92ae88a5d9e6..d9a1c51eb6c7 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AssertInstanceOf.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AssertInstanceOf.java @@ -56,6 +56,7 @@ private static T assertInstanceOf(Class expectedType, @Nullable Object ac .expected(expectedType) // .actual(actualValue == null ? null : actualValue.getClass()) // .cause(actualValue instanceof Throwable t ? t : null) // + .trimStacktrace(Assertions.class) // .buildAndThrow(); } return expectedType.cast(actualValue); diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AssertIterableEquals.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AssertIterableEquals.java index b9e70a58844d..9523ecd19124 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AssertIterableEquals.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AssertIterableEquals.java @@ -153,6 +153,7 @@ private static AssertionFailedError expectedIterableIsNullFailure(Deque return assertionFailure() // .message(messageOrSupplier) // .reason("expected iterable was " + formatIndexes(indexes)) // + .trimStacktrace(Assertions.class) // .build(); } @@ -165,6 +166,7 @@ private static AssertionFailedError actualIterableIsNullFailure(Deque i return assertionFailure() // .message(messageOrSupplier) // .reason("actual iterable was " + formatIndexes(indexes)) // + .trimStacktrace(Assertions.class) // .build(); } diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AssertLinesMatch.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AssertLinesMatch.java index 990d12b94f29..722aaec87ee4 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AssertLinesMatch.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AssertLinesMatch.java @@ -193,6 +193,7 @@ void fail(String format, Object... args) { .expected(join(newLine, expectedLines)) // .actual(join(newLine, actualLines)) // .includeValuesInMessage(false) // + .trimStacktrace(Assertions.class) // .buildAndThrow(); } } diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AssertNotEquals.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AssertNotEquals.java index e78a32eb4aa8..52e253f101f3 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AssertNotEquals.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AssertNotEquals.java @@ -279,6 +279,7 @@ private static void failEqual(@Nullable Object actual, @Nullable Object messageO assertionFailure() // .message(messageOrSupplier) // .reason("expected: not equal but was: <" + actual + ">") // + .trimStacktrace(Assertions.class) // .buildAndThrow(); } diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AssertNotNull.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AssertNotNull.java index a03079f49341..c5b25ce39bed 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AssertNotNull.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AssertNotNull.java @@ -52,6 +52,7 @@ private static void failNull(@Nullable Object messageOrSupplier) { assertionFailure() // .message(messageOrSupplier) // .reason("expected: not ") // + .trimStacktrace(Assertions.class) // .buildAndThrow(); } } diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AssertNotSame.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AssertNotSame.java index d46c8f711fe1..ed46b9dc1f52 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AssertNotSame.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AssertNotSame.java @@ -49,6 +49,7 @@ private static void failSame(@Nullable Object actual, @Nullable Object messageOr assertionFailure() // .message(messageOrSupplier) // .reason("expected: not same but was: <" + actual + ">") // + .trimStacktrace(Assertions.class) // .buildAndThrow(); } diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AssertNull.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AssertNull.java index 137fb1e83aee..d2213d1836d6 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AssertNull.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AssertNull.java @@ -53,6 +53,7 @@ private static void failNotNull(@Nullable Object actual, @Nullable Object messag .message(messageOrSupplier) // .expected(null) // .actual(actual) // + .trimStacktrace(Assertions.class) // .buildAndThrow(); } diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AssertSame.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AssertSame.java index 2cf1476f3742..9c7f1be7b2fb 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AssertSame.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AssertSame.java @@ -51,6 +51,7 @@ private static void failNotSame(@Nullable Object expected, @Nullable Object actu .message(messageOrSupplier) // .expected(expected) // .actual(actual) // + .trimStacktrace(Assertions.class) // .buildAndThrow(); } diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AssertThrows.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AssertThrows.java index d198486087a4..7c29a2af7edf 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AssertThrows.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AssertThrows.java @@ -65,12 +65,14 @@ private static T assertThrows(Class expectedType, Execu .actual(actualException.getClass()) // .reason("Unexpected exception type thrown") // .cause(actualException) // + .trimStacktrace(Assertions.class) // .build(); } } throw assertionFailure() // .message(messageOrSupplier) // .reason("Expected %s to be thrown, but nothing was thrown.".formatted(getCanonicalName(expectedType))) // + .trimStacktrace(Assertions.class) // .build(); } diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AssertThrowsExactly.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AssertThrowsExactly.java index 59e72305be9e..865bd1945d04 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AssertThrowsExactly.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AssertThrowsExactly.java @@ -65,6 +65,7 @@ private static T assertThrowsExactly(Class expectedType .actual(actualException.getClass()) // .reason("Unexpected exception type thrown") // .cause(actualException) // + .trimStacktrace(Assertions.class) // .build(); } } @@ -72,6 +73,7 @@ private static T assertThrowsExactly(Class expectedType throw assertionFailure() // .message(messageOrSupplier) // .reason("Expected %s to be thrown, but nothing was thrown.".formatted(getCanonicalName(expectedType))) // + .trimStacktrace(Assertions.class) // .build(); } diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AssertTimeout.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AssertTimeout.java index d60b065be1bb..baefebfe50b4 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AssertTimeout.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AssertTimeout.java @@ -82,6 +82,7 @@ static void assertTimeout(Duration timeout, Executable executable, Supplier<@Nul .message(messageOrSupplier) // .reason("execution exceeded timeout of " + timeoutInMillis + " ms by " + (timeElapsed - timeoutInMillis) + " ms") // + .trimStacktrace(Assertions.class) // .buildAndThrow(); } return result; diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AssertTimeoutPreemptively.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AssertTimeoutPreemptively.java index f6c6afd42a12..72d7742a1c7a 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AssertTimeoutPreemptively.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AssertTimeoutPreemptively.java @@ -73,6 +73,7 @@ private static AssertionFailedError createAssertionFailure(Duration timeout, .message(messageSupplier) // .reason("execution timed out after " + timeout.toMillis() + " ms") // .cause(cause) // + .trimStacktrace(Assertions.class) // .build(); } diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AssertTrue.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AssertTrue.java index cb92e2278419..d56237c75b72 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AssertTrue.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AssertTrue.java @@ -66,6 +66,7 @@ private static void failNotTrue(@Nullable Object messageOrSupplier) { .message(messageOrSupplier) // .expected(true) // .actual(false) // + .trimStacktrace(Assertions.class) // .buildAndThrow(); } diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AssertionFailureBuilder.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AssertionFailureBuilder.java index a8fd354ce2a4..3b71149c49c2 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AssertionFailureBuilder.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AssertionFailureBuilder.java @@ -10,9 +10,11 @@ package org.junit.jupiter.api; +import static org.apiguardian.api.API.Status.EXPERIMENTAL; import static org.apiguardian.api.API.Status.STABLE; import static org.junit.jupiter.api.AssertionUtils.getCanonicalName; +import java.util.Arrays; import java.util.function.Supplier; import org.apiguardian.api.API; @@ -46,6 +48,8 @@ public class AssertionFailureBuilder { private boolean includeValuesInMessage = true; + private @Nullable Class trimStackTraceTo; + /** * Create a new {@code AssertionFailureBuilder}. */ @@ -130,6 +134,21 @@ public AssertionFailureBuilder includeValuesInMessage(boolean includeValuesInMes return this; } + /** + * Set class to trim from the stacktrace. + * + *

To improve the readability of assertion failures, stack-frames up to + * and including any frames from {@code to} are trimmed from the stacktrace. + * + * @param to class to prune from the stacktrace. + * @return this builder for method chaining + */ + @API(status = EXPERIMENTAL, since = "6.1") + public AssertionFailureBuilder trimStacktrace(@Nullable Class to) { + this.trimStackTraceTo = to; + return this; + } + /** * Build the {@link AssertionFailedError AssertionFailedError} and throw it. * @@ -154,9 +173,37 @@ public AssertionFailedError build() { if (reason != null) { message = buildPrefix(message) + reason; } - return mismatch // + + var assertionFailedError = mismatch // ? new AssertionFailedError(message, expected, actual, cause) // : new AssertionFailedError(message, cause); + + maybeTrimStackTrace(assertionFailedError); + return assertionFailedError; + } + + private void maybeTrimStackTrace(Throwable throwable) { + if (trimStackTraceTo == null) { + return; + } + + var pruneTargetClassName = trimStackTraceTo.getName(); + var stackTrace = throwable.getStackTrace(); + + int lastIndexOf = -1; + for (int i = 0; i < stackTrace.length; i++) { + var element = stackTrace[i]; + var className = element.getClassName(); + if (className.equals(pruneTargetClassName)) { + lastIndexOf = i; + } + } + + if (lastIndexOf != -1) { + int from = Math.min(lastIndexOf + 1, stackTrace.length); + var pruned = Arrays.copyOfRange(stackTrace, from, stackTrace.length); + throwable.setStackTrace(pruned); + } } private static @Nullable String nullSafeGet(@Nullable Object messageOrSupplier) { diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AssertionUtils.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AssertionUtils.java index f5dae75d401f..6ae0c48536a7 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AssertionUtils.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AssertionUtils.java @@ -11,6 +11,7 @@ package org.junit.jupiter.api; import static java.util.stream.Collectors.joining; +import static org.junit.jupiter.api.AssertionFailureBuilder.assertionFailure; import java.util.Deque; import java.util.function.Supplier; @@ -18,7 +19,6 @@ import org.jspecify.annotations.Nullable; import org.junit.platform.commons.annotation.Contract; import org.junit.platform.commons.util.UnrecoverableExceptions; -import org.opentest4j.AssertionFailedError; /** * {@code AssertionUtils} is a collection of utility methods that are common to @@ -34,27 +34,42 @@ private AssertionUtils() { @Contract(" -> fail") static void fail() { - throw new AssertionFailedError(); + throw assertionFailure() // + .trimStacktrace(Assertions.class) // + .build(); } @Contract("_ -> fail") static void fail(@Nullable String message) { - throw new AssertionFailedError(message); + throw assertionFailure() // + .message(message) // + .trimStacktrace(Assertions.class) // + .build(); } @Contract("_, _ -> fail") static void fail(@Nullable String message, @Nullable Throwable cause) { - throw new AssertionFailedError(message, cause); + throw assertionFailure() // + .message(message) // + .cause(cause) // + .trimStacktrace(Assertions.class) // + .build(); } @Contract("_ -> fail") static void fail(@Nullable Throwable cause) { - throw new AssertionFailedError(null, cause); + throw assertionFailure() // + .cause(cause) // + .trimStacktrace(Assertions.class) // + .build(); } @Contract("_ -> fail") static void fail(Supplier<@Nullable String> messageSupplier) { - throw new AssertionFailedError(nullSafeGet(messageSupplier)); + throw assertionFailure() // + .message(nullSafeGet(messageSupplier)) // + .trimStacktrace(Assertions.class) // + .build(); } static @Nullable String nullSafeGet(@Nullable Supplier<@Nullable String> messageSupplier) { diff --git a/junit-jupiter-api/src/main/kotlin/org/junit/jupiter/api/Assertions.kt b/junit-jupiter-api/src/main/kotlin/org/junit/jupiter/api/Assertions.kt index 932c281ac982..d6e4ea0f67d3 100644 --- a/junit-jupiter-api/src/main/kotlin/org/junit/jupiter/api/Assertions.kt +++ b/junit-jupiter-api/src/main/kotlin/org/junit/jupiter/api/Assertions.kt @@ -363,6 +363,7 @@ inline fun assertDoesNotThrow(executable: () -> R): R { throw assertionFailure() .reason("Unexpected exception thrown: ${t.javaClass.getName()}$suffix") .cause(t) + .trimStacktrace(AssertionFailureBuilder::class.java) .build() } } @@ -420,6 +421,7 @@ inline fun assertDoesNotThrow( .message(message()) .reason("Unexpected exception thrown: ${t.javaClass.getName()}$suffix") .cause(t) + .trimStacktrace(AssertionFailureBuilder::class.java) .build() } } diff --git a/platform-tests/src/test/java/org/junit/platform/StackTracePruningTests.java b/platform-tests/src/test/java/org/junit/platform/StackTracePruningTests.java index 83e56373d509..0e53f5b0f529 100644 --- a/platform-tests/src/test/java/org/junit/platform/StackTracePruningTests.java +++ b/platform-tests/src/test/java/org/junit/platform/StackTracePruningTests.java @@ -75,7 +75,7 @@ void shouldNotPruneStackTraceWhenDisabled() { List stackTrace = extractStackTrace(results); assertStackTraceMatch(stackTrace, """ - \\Qorg.junit.jupiter.api.AssertionUtils.fail(AssertionUtils.java:\\E.+ + \\Qorg.junit.platform.StackTracePruningTests$FailingTestTestCase.fail(StackTracePruningTests.java:\\E.+ >>>> \\Qorg.junit.platform.commons.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:\\E.+ >>>> @@ -83,7 +83,7 @@ void shouldNotPruneStackTraceWhenDisabled() { } @Test - void shouldAlwaysKeepJupiterAssertionStackTraceElement() { + void shouldAlwaysKeepJupiterTestStackTraceElement() { EngineExecutionResults results = EngineTestKit.engine("junit-jupiter") // .configurationParameter("junit.platform.stacktrace.pruning.enabled", "true") // .selectors(selectMethod(FailingTestTestCase.class, "failingAssertion")) // @@ -92,13 +92,12 @@ void shouldAlwaysKeepJupiterAssertionStackTraceElement() { List stackTrace = extractStackTrace(results); assertStackTraceMatch(stackTrace, """ - >>>> - \\Qorg.junit.jupiter.api.Assertions.fail(Assertions.java:\\E.+ + \\Qorg.junit.platform.StackTracePruningTests$FailingTestTestCase.fail(StackTracePruningTests.java:\\E.+ >>>> """); } - @Test + @Test //TODO: void shouldAlwaysKeepJupiterAssumptionStackTraceElement() { EngineExecutionResults results = EngineTestKit.engine("junit-jupiter") // .configurationParameter("junit.platform.stacktrace.pruning.enabled", "true") // @@ -115,18 +114,16 @@ void shouldAlwaysKeepJupiterAssumptionStackTraceElement() { } @Test - void shouldKeepExactlyEverythingAfterTestCall() { + void shouldKeepExactlyEverythingBeforeAssertionsCall() { EngineExecutionResults results = EngineTestKit.engine("junit-jupiter") // .configurationParameter("junit.platform.stacktrace.pruning.enabled", "true") // .selectors(selectMethod(FailingTestTestCase.class, "failingAssertion")) // .execute(); List stackTrace = extractStackTrace(results); - assertStackTraceMatch(stackTrace, """ - \\Qorg.junit.jupiter.api.AssertionUtils.fail(AssertionUtils.java:\\E.+ - \\Qorg.junit.jupiter.api.Assertions.fail(Assertions.java:\\E.+ + \\Qorg.junit.platform.StackTracePruningTests$FailingTestTestCase.fail(StackTracePruningTests.java:\\E.+ \\Qorg.junit.platform.StackTracePruningTests$FailingTestTestCase.failingAssertion(StackTracePruningTests.java:\\E.+ """); } @@ -145,8 +142,7 @@ void shouldKeepExactlyEverythingAfterLifecycleMethodCall(Class methodClass) { assertStackTraceMatch(stackTrace, """ - \\Qorg.junit.jupiter.api.AssertionUtils.fail(AssertionUtils.java:\\E.+ - \\Qorg.junit.jupiter.api.Assertions.fail(Assertions.java:\\E.+ + \\Qorg.junit.platform.StackTracePruningTests$FailingBeforeEachTestCase.fail(StackTracePruningTests.java:\\E.+ \\Qorg.junit.platform.StackTracePruningTests$FailingBeforeEachTestCase.setUp(StackTracePruningTests.java:\\E.+ """); } @@ -198,12 +194,16 @@ static class FailingTestTestCase { @Test void failingAssertion() { - Assertions.fail(); + fail(); } @Test void multipleFailingAssertions() { - Assertions.assertAll(Assertions::fail, Assertions::fail); + failMultiple(); + } + + private void failMultiple() { + Assertions.assertAll(FailingTestTestCase::fail, FailingTestTestCase::fail); } @Test @@ -213,6 +213,9 @@ void failingAssumption() { }); } + private static void fail() { + Assertions.fail(); + } } @SuppressWarnings("JUnitMalformedDeclaration") @@ -220,7 +223,7 @@ static class FailingBeforeEachTestCase { @BeforeEach void setUp() { - Assertions.fail(); + fail(); } @Test @@ -245,6 +248,10 @@ void test() { } + private static void fail() { + Assertions.fail(); + } + } } From 745837fe00fb6b42f6f2a3a1456d7e6d2c7b645a Mon Sep 17 00:00:00 2001 From: "M.P. Korstanje" Date: Tue, 18 Nov 2025 12:48:28 +0100 Subject: [PATCH 2/5] Cover trimming boundaries --- .../api/AssertionFailureBuilderTest.java | 98 +++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 jupiter-tests/src/test/java/org/junit/jupiter/api/AssertionFailureBuilderTest.java diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/api/AssertionFailureBuilderTest.java b/jupiter-tests/src/test/java/org/junit/jupiter/api/AssertionFailureBuilderTest.java new file mode 100644 index 000000000000..ed6a6af731d7 --- /dev/null +++ b/jupiter-tests/src/test/java/org/junit/jupiter/api/AssertionFailureBuilderTest.java @@ -0,0 +1,98 @@ +/* + * Copyright 2015-2025 the original author or authors. + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License v2.0 which + * accompanies this distribution and is available at + * + * https://www.eclipse.org/legal/epl-v20.html + */ + +package org.junit.jupiter.api; + +import static java.util.concurrent.Executors.newSingleThreadExecutor; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.AssertionFailureBuilder.assertionFailure; +import static org.junit.jupiter.api.Assertions.assertLinesMatch; + +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; + +import org.opentest4j.AssertionFailedError; + +class AssertionFailureBuilderTest { + + @Test + void doesNotTrimByDefault() { + var error = AssertionsFacade.fail(); + assertStackTraceMatch(error, + """ + \\Qorg.junit.jupiter.api.AssertionFailureBuilder.build(AssertionFailureBuilder.java:\\E.+ + \\Qorg.junit.jupiter.api.AssertionFailureBuilderTest$AssertionsFacade.fail(AssertionFailureBuilderTest.java:\\E.+ + \\Qorg.junit.jupiter.api.AssertionFailureBuilderTest.doesNotTrimByDefault(AssertionFailureBuilderTest.java:\\E.+ + >>>> + """); + } + + @Test + void trimsUpToAssertionsFacade() { + var error = AssertionsFacade.failWithTrimmedStacktrace(AssertionsFacade.class); + assertStackTraceMatch(error, + """ + \\Qorg.junit.jupiter.api.AssertionFailureBuilderTest.trimsUpToAssertionsFacade(AssertionFailureBuilderTest.java:\\E.+ + >>>> + """); + } + + @Test + void trimsUpToAssertionFailureBuilder() { + var error = AssertionsFacade.failWithTrimmedStacktrace(AssertionFailureBuilder.class); + assertStackTraceMatch(error, + """ + \\Qorg.junit.jupiter.api.AssertionFailureBuilderTest$AssertionsFacade.failWithTrimmedStacktrace(AssertionFailureBuilderTest.java:\\E.+ + \\Qorg.junit.jupiter.api.AssertionFailureBuilderTest.trimsUpToAssertionFailureBuilder(AssertionFailureBuilderTest.java:\\E.+ + >>>> + """); + } + + @Test + void ignoresClassNotInStackTrace() { + var error = AssertionsFacade.failWithTrimmedStacktrace(String.class); + assertStackTraceMatch(error, + """ + \\Qorg.junit.jupiter.api.AssertionFailureBuilder.build(AssertionFailureBuilder.java:\\E.+ + \\Qorg.junit.jupiter.api.AssertionFailureBuilderTest$AssertionsFacade.failWithTrimmedStacktrace(AssertionFailureBuilderTest.java:\\E.+ + \\Qorg.junit.jupiter.api.AssertionFailureBuilderTest.ignoresClassNotInStackTrace(AssertionFailureBuilderTest.java:\\E.+ + >>>> + """); + } + + @Test + void canTrimToEmptyStacktrace() throws ExecutionException, InterruptedException { + try (ExecutorService service = newSingleThreadExecutor()) { + // Ensure that the stacktrace starts at Thread. + var error = service.submit(() -> AssertionsFacade.failWithTrimmedStacktrace(Thread.class)).get(); + assertThat(error.getStackTrace()).isEmpty(); + } + } + + private static void assertStackTraceMatch(AssertionFailedError assertionFailedError, String expectedLines) { + List stackStraceAsLines = Arrays.stream(assertionFailedError.getStackTrace()) // + .map(StackTraceElement::toString) // + .toList(); + assertLinesMatch(expectedLines.lines().toList(), stackStraceAsLines); + } + + static class AssertionsFacade { + static AssertionFailedError fail() { + return assertionFailure().build(); + } + + static AssertionFailedError failWithTrimmedStacktrace(Class testCaseClass) { + return assertionFailure().trimStacktrace(testCaseClass).build(); + } + + } +} From bdbb937a770b2491ae6d686282e023153993ccdf Mon Sep 17 00:00:00 2001 From: "M.P. Korstanje" Date: Tue, 18 Nov 2025 17:12:43 +0100 Subject: [PATCH 3/5] Update release notes --- .../docs/asciidoc/release-notes/release-notes-6.1.0-M2.adoc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/documentation/src/docs/asciidoc/release-notes/release-notes-6.1.0-M2.adoc b/documentation/src/docs/asciidoc/release-notes/release-notes-6.1.0-M2.adoc index 4f40bc523b81..7496f59a759b 100644 --- a/documentation/src/docs/asciidoc/release-notes/release-notes-6.1.0-M2.adoc +++ b/documentation/src/docs/asciidoc/release-notes/release-notes-6.1.0-M2.adoc @@ -45,8 +45,8 @@ repository on GitHub. [[release-notes-6.1.0-M2-junit-jupiter-new-features-and-improvements]] ==== New Features and Improvements -* ❓ - +* Trim internal stack frames from `AssertionFailedError` stack traces. +* Introduce new `trimStacktrace(Class)` method for `AssertionFailureBuilder`. It allows user defined assertions to trim their stacktrace. [[release-notes-6.1.0-M2-junit-vintage]] === JUnit Vintage From 0e260b4585d40d9fe46ca216b2b25663c23eb3c8 Mon Sep 17 00:00:00 2001 From: "M.P. Korstanje" Date: Tue, 18 Nov 2025 17:17:38 +0100 Subject: [PATCH 4/5] Polishing --- .../platform/StackTracePruningTests.java | 20 ++++++++----------- 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/platform-tests/src/test/java/org/junit/platform/StackTracePruningTests.java b/platform-tests/src/test/java/org/junit/platform/StackTracePruningTests.java index 0e53f5b0f529..6259b7b8de45 100644 --- a/platform-tests/src/test/java/org/junit/platform/StackTracePruningTests.java +++ b/platform-tests/src/test/java/org/junit/platform/StackTracePruningTests.java @@ -197,6 +197,10 @@ void failingAssertion() { fail(); } + private static void fail() { + Assertions.fail(); + } + @Test void multipleFailingAssertions() { failMultiple(); @@ -212,10 +216,6 @@ void failingAssumption() { throw new RuntimeException(); }); } - - private static void fail() { - Assertions.fail(); - } } @SuppressWarnings("JUnitMalformedDeclaration") @@ -226,6 +226,10 @@ void setUp() { fail(); } + private static void fail() { + Assertions.fail(); + } + @Test void test() { } @@ -243,15 +247,7 @@ class NestedNestedTestCase { @Test void test() { } - } - - } - - private static void fail() { - Assertions.fail(); } - } - } From 9b315c4fbd57818ea9147e44b478133900a2a99b Mon Sep 17 00:00:00 2001 From: "M.P. Korstanje" Date: Tue, 18 Nov 2025 17:18:37 +0100 Subject: [PATCH 5/5] Polishing --- .../org/junit/jupiter/api/AssertionFailureBuilderTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/api/AssertionFailureBuilderTest.java b/jupiter-tests/src/test/java/org/junit/jupiter/api/AssertionFailureBuilderTest.java index ed6a6af731d7..9062fbaf34f5 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/api/AssertionFailureBuilderTest.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/api/AssertionFailureBuilderTest.java @@ -90,8 +90,8 @@ static AssertionFailedError fail() { return assertionFailure().build(); } - static AssertionFailedError failWithTrimmedStacktrace(Class testCaseClass) { - return assertionFailure().trimStacktrace(testCaseClass).build(); + static AssertionFailedError failWithTrimmedStacktrace(Class to) { + return assertionFailure().trimStacktrace(to).build(); } }