From 9e04b5116ef6685f435bb2f9d85e748f924dfbb0 Mon Sep 17 00:00:00 2001 From: "M.P. Korstanje" Date: Tue, 18 Nov 2025 00:55:18 +0100 Subject: [PATCH 01/21] 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 02/21] 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 03/21] 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 04/21] 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 05/21] 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(); } } From e78114e6d57905894b2235d0a369a73758c7a3fe Mon Sep 17 00:00:00 2001 From: "M.P. Korstanje" Date: Fri, 21 Nov 2025 16:02:10 +0100 Subject: [PATCH 06/21] Keep one frame from Assertions for clarity --- .../junit/jupiter/api/AssertArrayEquals.java | 8 ++--- .../junit/jupiter/api/AssertDoesNotThrow.java | 2 +- .../org/junit/jupiter/api/AssertEquals.java | 2 +- .../org/junit/jupiter/api/AssertFalse.java | 2 +- .../junit/jupiter/api/AssertInstanceOf.java | 2 +- .../jupiter/api/AssertIterableEquals.java | 4 +-- .../junit/jupiter/api/AssertLinesMatch.java | 2 +- .../junit/jupiter/api/AssertNotEquals.java | 2 +- .../org/junit/jupiter/api/AssertNotNull.java | 2 +- .../org/junit/jupiter/api/AssertNotSame.java | 2 +- .../org/junit/jupiter/api/AssertNull.java | 2 +- .../org/junit/jupiter/api/AssertSame.java | 2 +- .../org/junit/jupiter/api/AssertThrows.java | 4 +-- .../jupiter/api/AssertThrowsExactly.java | 4 +-- .../org/junit/jupiter/api/AssertTimeout.java | 2 +- .../api/AssertTimeoutPreemptively.java | 2 +- .../org/junit/jupiter/api/AssertTrue.java | 2 +- .../jupiter/api/AssertionFailureBuilder.java | 31 +++++++++++++------ .../org/junit/jupiter/api/AssertionUtils.java | 10 +++--- .../org/junit/jupiter/api/Assertions.kt | 6 ++-- .../api/AssertionFailureBuilderTest.java | 23 ++++++++++---- .../platform/StackTracePruningTests.java | 8 +++-- 22 files changed, 76 insertions(+), 48 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 56c707d7c489..056f99f3fdd6 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,7 +483,7 @@ private static AssertionFailedError expectedArrayIsNullFailure(@Nullable Deque" + formatIndexes(indexes)) // - .trimStacktrace(Assertions.class) // + .trimStacktrace(Assertions.class, 1) // .build(); } @@ -496,7 +496,7 @@ private static AssertionFailedError actualArrayIsNullFailure(@Nullable Deque" + formatIndexes(indexes)) // - .trimStacktrace(Assertions.class) // + .trimStacktrace(Assertions.class, 1) // .build(); } @@ -509,7 +509,7 @@ private static void assertArraysHaveSameLength(int expected, int actual, @Nullab .reason("array lengths differ" + formatIndexes(indexes)) // .expected(expected) // .actual(actual) // - .trimStacktrace(Assertions.class) // + .trimStacktrace(Assertions.class, 1) // .buildAndThrow(); } } @@ -522,7 +522,7 @@ private static void failArraysNotEqual(@Nullable Object expected, @Nullable Obje .reason("array contents differ" + formatIndexes(indexes)) // .expected(expected) // .actual(actual) // - .trimStacktrace(Assertions.class) // + .trimStacktrace(Assertions.class, 1) // .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 68882f795208..8c8cffaa9e89 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,7 +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) // + .trimStacktrace(Assertions.class, 1) // .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 d862bbdb1a6b..4c24b58a22f4 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,7 +198,7 @@ private static void failNotEqual(@Nullable Object expected, @Nullable Object act .message(messageOrSupplier) // .expected(expected) // .actual(actual) // - .trimStacktrace(Assertions.class) // + .trimStacktrace(Assertions.class, 1) // .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 4dac4ca3810b..2db7d152b704 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,7 +66,7 @@ private static void failNotFalse(@Nullable Object messageOrSupplier) { .message(messageOrSupplier) // .expected(false) // .actual(true) // - .trimStacktrace(Assertions.class) // + .trimStacktrace(Assertions.class, 1) // .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 d9a1c51eb6c7..ab9e6f713737 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,7 +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) // + .trimStacktrace(Assertions.class, 1) // .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 9523ecd19124..9c45c17d8711 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,7 +153,7 @@ private static AssertionFailedError expectedIterableIsNullFailure(Deque return assertionFailure() // .message(messageOrSupplier) // .reason("expected iterable was " + formatIndexes(indexes)) // - .trimStacktrace(Assertions.class) // + .trimStacktrace(Assertions.class, 1) // .build(); } @@ -166,7 +166,7 @@ private static AssertionFailedError actualIterableIsNullFailure(Deque i return assertionFailure() // .message(messageOrSupplier) // .reason("actual iterable was " + formatIndexes(indexes)) // - .trimStacktrace(Assertions.class) // + .trimStacktrace(Assertions.class, 1) // .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 722aaec87ee4..f783ad233aa9 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,7 +193,7 @@ void fail(String format, Object... args) { .expected(join(newLine, expectedLines)) // .actual(join(newLine, actualLines)) // .includeValuesInMessage(false) // - .trimStacktrace(Assertions.class) // + .trimStacktrace(Assertions.class, 1) // .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 52e253f101f3..3509d4ee5415 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,7 +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) // + .trimStacktrace(Assertions.class, 1) // .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 c5b25ce39bed..baa88ac35172 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,7 +52,7 @@ private static void failNull(@Nullable Object messageOrSupplier) { assertionFailure() // .message(messageOrSupplier) // .reason("expected: not ") // - .trimStacktrace(Assertions.class) // + .trimStacktrace(Assertions.class, 1) // .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 ed46b9dc1f52..8a09241be72b 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,7 +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) // + .trimStacktrace(Assertions.class, 1) // .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 d2213d1836d6..47349c72cf0a 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,7 +53,7 @@ private static void failNotNull(@Nullable Object actual, @Nullable Object messag .message(messageOrSupplier) // .expected(null) // .actual(actual) // - .trimStacktrace(Assertions.class) // + .trimStacktrace(Assertions.class, 1) // .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 9c7f1be7b2fb..b1f24e819790 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,7 +51,7 @@ private static void failNotSame(@Nullable Object expected, @Nullable Object actu .message(messageOrSupplier) // .expected(expected) // .actual(actual) // - .trimStacktrace(Assertions.class) // + .trimStacktrace(Assertions.class, 1) // .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 7c29a2af7edf..bdb8e38f34f8 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,14 +65,14 @@ private static T assertThrows(Class expectedType, Execu .actual(actualException.getClass()) // .reason("Unexpected exception type thrown") // .cause(actualException) // - .trimStacktrace(Assertions.class) // + .trimStacktrace(Assertions.class, 1) // .build(); } } throw assertionFailure() // .message(messageOrSupplier) // .reason("Expected %s to be thrown, but nothing was thrown.".formatted(getCanonicalName(expectedType))) // - .trimStacktrace(Assertions.class) // + .trimStacktrace(Assertions.class, 1) // .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 865bd1945d04..0e6fd6cb5cab 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,7 +65,7 @@ private static T assertThrowsExactly(Class expectedType .actual(actualException.getClass()) // .reason("Unexpected exception type thrown") // .cause(actualException) // - .trimStacktrace(Assertions.class) // + .trimStacktrace(Assertions.class, 1) // .build(); } } @@ -73,7 +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) // + .trimStacktrace(Assertions.class, 1) // .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 baefebfe50b4..10fc356c3d5b 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,7 +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) // + .trimStacktrace(Assertions.class, 1) // .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 72d7742a1c7a..d4ff9e964c7f 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,7 +73,7 @@ private static AssertionFailedError createAssertionFailure(Duration timeout, .message(messageSupplier) // .reason("execution timed out after " + timeout.toMillis() + " ms") // .cause(cause) // - .trimStacktrace(Assertions.class) // + .trimStacktrace(Assertions.class, 1) // .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 d56237c75b72..e4036be91680 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,7 +66,7 @@ private static void failNotTrue(@Nullable Object messageOrSupplier) { .message(messageOrSupplier) // .expected(true) // .actual(false) // - .trimStacktrace(Assertions.class) // + .trimStacktrace(Assertions.class, 1) // .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 3b71149c49c2..42f08286fd98 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 @@ -19,6 +19,7 @@ import org.apiguardian.api.API; import org.jspecify.annotations.Nullable; +import org.junit.platform.commons.util.Preconditions; import org.junit.platform.commons.util.StringUtils; import org.opentest4j.AssertionFailedError; @@ -48,7 +49,9 @@ public class AssertionFailureBuilder { private boolean includeValuesInMessage = true; - private @Nullable Class trimStackTraceTo; + private @Nullable Class trimStackTraceBefore; + + private int trimStackTraceRetain; /** * Create a new {@code AssertionFailureBuilder}. @@ -135,17 +138,21 @@ public AssertionFailureBuilder includeValuesInMessage(boolean includeValuesInMes } /** - * Set class to trim from the stacktrace. + * Set target and depth for trimming stacktrace. * - *

To improve the readability of assertion failures, stack-frames up to - * and including any frames from {@code to} are trimmed from the stacktrace. + *

Removes all but {@code retain - 1} frames before the last frame from + * {@code target}. If {@code retain} is zero, all frames before the last frame from + * {@code target} are trimmed. * - * @param to class to prune from the stacktrace. + * @param target class to trim from the stacktrace + * @param retain depth of trimming, must be non-negative * @return this builder for method chaining */ @API(status = EXPERIMENTAL, since = "6.1") - public AssertionFailureBuilder trimStacktrace(@Nullable Class to) { - this.trimStackTraceTo = to; + public AssertionFailureBuilder trimStacktrace(@Nullable Class target, int retain) { + Preconditions.condition(retain >= 0, "retain must have a non-negative value"); + this.trimStackTraceBefore = target; + this.trimStackTraceRetain = retain; return this; } @@ -183,11 +190,11 @@ public AssertionFailedError build() { } private void maybeTrimStackTrace(Throwable throwable) { - if (trimStackTraceTo == null) { + if (trimStackTraceBefore == null) { return; } - var pruneTargetClassName = trimStackTraceTo.getName(); + var pruneTargetClassName = trimStackTraceBefore.getName(); var stackTrace = throwable.getStackTrace(); int lastIndexOf = -1; @@ -200,12 +207,16 @@ private void maybeTrimStackTrace(Throwable throwable) { } if (lastIndexOf != -1) { - int from = Math.min(lastIndexOf + 1, stackTrace.length); + int from = clamp(lastIndexOf + 1 - trimStackTraceRetain, stackTrace.length); var pruned = Arrays.copyOfRange(stackTrace, from, stackTrace.length); throwable.setStackTrace(pruned); } } + private static int clamp(int value, int max) { + return Math.max(0, Math.min(value, max)); + } + private static @Nullable String nullSafeGet(@Nullable Object messageOrSupplier) { if (messageOrSupplier == null) { return null; 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 6ae0c48536a7..9be9a036e58e 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 @@ -35,7 +35,7 @@ private AssertionUtils() { @Contract(" -> fail") static void fail() { throw assertionFailure() // - .trimStacktrace(Assertions.class) // + .trimStacktrace(Assertions.class, 1) // .build(); } @@ -43,7 +43,7 @@ static void fail() { static void fail(@Nullable String message) { throw assertionFailure() // .message(message) // - .trimStacktrace(Assertions.class) // + .trimStacktrace(Assertions.class, 1) // .build(); } @@ -52,7 +52,7 @@ static void fail(@Nullable String message, @Nullable Throwable cause) { throw assertionFailure() // .message(message) // .cause(cause) // - .trimStacktrace(Assertions.class) // + .trimStacktrace(Assertions.class, 1) // .build(); } @@ -60,7 +60,7 @@ static void fail(@Nullable String message, @Nullable Throwable cause) { static void fail(@Nullable Throwable cause) { throw assertionFailure() // .cause(cause) // - .trimStacktrace(Assertions.class) // + .trimStacktrace(Assertions.class, 1) // .build(); } @@ -68,7 +68,7 @@ static void fail(@Nullable Throwable cause) { static void fail(Supplier<@Nullable String> messageSupplier) { throw assertionFailure() // .message(nullSafeGet(messageSupplier)) // - .trimStacktrace(Assertions.class) // + .trimStacktrace(Assertions.class, 1) // .build(); } 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 d6e4ea0f67d3..584a765f04d8 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,7 +363,8 @@ inline fun assertDoesNotThrow(executable: () -> R): R { throw assertionFailure() .reason("Unexpected exception thrown: ${t.javaClass.getName()}$suffix") .cause(t) - .trimStacktrace(AssertionFailureBuilder::class.java) + // we don't want to retain any frames from the AssertionFailureBuilder + .trimStacktrace(AssertionFailureBuilder::class.java, 0) .build() } } @@ -421,7 +422,8 @@ inline fun assertDoesNotThrow( .message(message()) .reason("Unexpected exception thrown: ${t.javaClass.getName()}$suffix") .cause(t) - .trimStacktrace(AssertionFailureBuilder::class.java) + // we don't want to retain any frames from the AssertionFailureBuilder + .trimStacktrace(AssertionFailureBuilder::class.java, 0) .build() } } 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 9062fbaf34f5..9048cc82d64b 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 @@ -38,7 +38,7 @@ void doesNotTrimByDefault() { @Test void trimsUpToAssertionsFacade() { - var error = AssertionsFacade.failWithTrimmedStacktrace(AssertionsFacade.class); + var error = AssertionsFacade.failWithTrimmedStacktrace(AssertionsFacade.class, 0); assertStackTraceMatch(error, """ \\Qorg.junit.jupiter.api.AssertionFailureBuilderTest.trimsUpToAssertionsFacade(AssertionFailureBuilderTest.java:\\E.+ @@ -46,9 +46,20 @@ void trimsUpToAssertionsFacade() { """); } + @Test + void trimsUpToAssertionsFacadeKeepingOne() { + var error = AssertionsFacade.failWithTrimmedStacktrace(AssertionsFacade.class, 1); + assertStackTraceMatch(error, + """ + \\Qorg.junit.jupiter.api.AssertionFailureBuilderTest$AssertionsFacade.failWithTrimmedStacktrace(AssertionFailureBuilderTest.java:\\E.+ + \\Qorg.junit.jupiter.api.AssertionFailureBuilderTest.trimsUpToAssertionsFacadeKeepingOne(AssertionFailureBuilderTest.java:\\E.+ + >>>> + """); + } + @Test void trimsUpToAssertionFailureBuilder() { - var error = AssertionsFacade.failWithTrimmedStacktrace(AssertionFailureBuilder.class); + var error = AssertionsFacade.failWithTrimmedStacktrace(AssertionFailureBuilder.class, 0); assertStackTraceMatch(error, """ \\Qorg.junit.jupiter.api.AssertionFailureBuilderTest$AssertionsFacade.failWithTrimmedStacktrace(AssertionFailureBuilderTest.java:\\E.+ @@ -59,7 +70,7 @@ void trimsUpToAssertionFailureBuilder() { @Test void ignoresClassNotInStackTrace() { - var error = AssertionsFacade.failWithTrimmedStacktrace(String.class); + var error = AssertionsFacade.failWithTrimmedStacktrace(String.class, 0); assertStackTraceMatch(error, """ \\Qorg.junit.jupiter.api.AssertionFailureBuilder.build(AssertionFailureBuilder.java:\\E.+ @@ -73,7 +84,7 @@ void ignoresClassNotInStackTrace() { 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(); + var error = service.submit(() -> AssertionsFacade.failWithTrimmedStacktrace(Thread.class, 0)).get(); assertThat(error.getStackTrace()).isEmpty(); } } @@ -90,8 +101,8 @@ static AssertionFailedError fail() { return assertionFailure().build(); } - static AssertionFailedError failWithTrimmedStacktrace(Class to) { - return assertionFailure().trimStacktrace(to).build(); + static AssertionFailedError failWithTrimmedStacktrace(Class to, int depth) { + return AssertionFailureBuilder.assertionFailure().trimStacktrace(to, depth).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 6259b7b8de45..c11d558dd43d 100644 --- a/platform-tests/src/test/java/org/junit/platform/StackTracePruningTests.java +++ b/platform-tests/src/test/java/org/junit/platform/StackTracePruningTests.java @@ -75,6 +75,7 @@ void shouldNotPruneStackTraceWhenDisabled() { List stackTrace = extractStackTrace(results); assertStackTraceMatch(stackTrace, """ + \\Qorg.junit.jupiter.api.Assertions.fail(Assertions.java:\\E.+ \\Qorg.junit.platform.StackTracePruningTests$FailingTestTestCase.fail(StackTracePruningTests.java:\\E.+ >>>> \\Qorg.junit.platform.commons.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:\\E.+ @@ -83,7 +84,7 @@ void shouldNotPruneStackTraceWhenDisabled() { } @Test - void shouldAlwaysKeepJupiterTestStackTraceElement() { + void shouldAlwaysKeepJupiterAssertionStackTraceElement() { EngineExecutionResults results = EngineTestKit.engine("junit-jupiter") // .configurationParameter("junit.platform.stacktrace.pruning.enabled", "true") // .selectors(selectMethod(FailingTestTestCase.class, "failingAssertion")) // @@ -92,12 +93,13 @@ void shouldAlwaysKeepJupiterTestStackTraceElement() { 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 //TODO: + @Test void shouldAlwaysKeepJupiterAssumptionStackTraceElement() { EngineExecutionResults results = EngineTestKit.engine("junit-jupiter") // .configurationParameter("junit.platform.stacktrace.pruning.enabled", "true") // @@ -123,6 +125,7 @@ void shouldKeepExactlyEverythingBeforeAssertionsCall() { List stackTrace = extractStackTrace(results); assertStackTraceMatch(stackTrace, """ + \\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.+ """); @@ -142,6 +145,7 @@ void shouldKeepExactlyEverythingAfterLifecycleMethodCall(Class methodClass) { assertStackTraceMatch(stackTrace, """ + \\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.+ """); From 09b8bc4d9067ef797ab2c815d05fec75d98aadc4 Mon Sep 17 00:00:00 2001 From: "M.P. Korstanje" Date: Fri, 21 Nov 2025 16:07:45 +0100 Subject: [PATCH 07/21] Polishing --- .../src/docs/asciidoc/release-notes/release-notes-6.1.0-M2.adoc | 2 +- .../test/java/org/junit/platform/StackTracePruningTests.java | 2 +- 2 files 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 7496f59a759b..a3b94e816a84 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 @@ -46,7 +46,7 @@ repository on GitHub. ==== 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. +* Introduce new `trimStacktrace(Class, int)` method for `AssertionFailureBuilder`. It allows user defined assertions to trim their stacktrace. [[release-notes-6.1.0-M2-junit-vintage]] === JUnit Vintage 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 c11d558dd43d..b5c2069049d4 100644 --- a/platform-tests/src/test/java/org/junit/platform/StackTracePruningTests.java +++ b/platform-tests/src/test/java/org/junit/platform/StackTracePruningTests.java @@ -116,7 +116,7 @@ void shouldAlwaysKeepJupiterAssumptionStackTraceElement() { } @Test - void shouldKeepExactlyEverythingBeforeAssertionsCall() { + void shouldKeepExactlyEverythingBetweenTestCallAndFirstAssertionCall() { EngineExecutionResults results = EngineTestKit.engine("junit-jupiter") // .configurationParameter("junit.platform.stacktrace.pruning.enabled", "true") // .selectors(selectMethod(FailingTestTestCase.class, "failingAssertion")) // From 3cb7cf347460cdab9a86a5453dbcd93b3c74b831 Mon Sep 17 00:00:00 2001 From: "M.P. Korstanje" Date: Fri, 21 Nov 2025 16:09:59 +0100 Subject: [PATCH 08/21] Polishing --- .../test/java/org/junit/platform/StackTracePruningTests.java | 4 ++++ 1 file changed, 4 insertions(+) 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 b5c2069049d4..4dbcd5d0c893 100644 --- a/platform-tests/src/test/java/org/junit/platform/StackTracePruningTests.java +++ b/platform-tests/src/test/java/org/junit/platform/StackTracePruningTests.java @@ -251,7 +251,11 @@ class NestedNestedTestCase { @Test void test() { } + } + } + } + } From 32a3dbb04492e324b83b20e0ad923b11961ab561 Mon Sep 17 00:00:00 2001 From: "M.P. Korstanje" Date: Fri, 21 Nov 2025 16:15:14 +0100 Subject: [PATCH 09/21] Polishing --- .../java/org/junit/jupiter/api/AssertionFailureBuilder.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 42f08286fd98..a2608cc146f9 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 @@ -141,7 +141,7 @@ public AssertionFailureBuilder includeValuesInMessage(boolean includeValuesInMes * Set target and depth for trimming stacktrace. * *

Removes all but {@code retain - 1} frames before the last frame from - * {@code target}. If {@code retain} is zero, all frames before the last frame from + * {@code target}. If {@code retain} is zero, all frames from * {@code target} are trimmed. * * @param target class to trim from the stacktrace From fa98895734fbfee6d36be177aa102e41cc0e0980 Mon Sep 17 00:00:00 2001 From: "M.P. Korstanje" Date: Fri, 21 Nov 2025 16:20:35 +0100 Subject: [PATCH 10/21] Polishing --- .../org/junit/platform/StackTracePruningTests.java | 14 +------------- 1 file changed, 1 insertion(+), 13 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 4dbcd5d0c893..a34d2417db4b 100644 --- a/platform-tests/src/test/java/org/junit/platform/StackTracePruningTests.java +++ b/platform-tests/src/test/java/org/junit/platform/StackTracePruningTests.java @@ -76,7 +76,6 @@ void shouldNotPruneStackTraceWhenDisabled() { assertStackTraceMatch(stackTrace, """ \\Qorg.junit.jupiter.api.Assertions.fail(Assertions.java:\\E.+ - \\Qorg.junit.platform.StackTracePruningTests$FailingTestTestCase.fail(StackTracePruningTests.java:\\E.+ >>>> \\Qorg.junit.platform.commons.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:\\E.+ >>>> @@ -94,7 +93,6 @@ void shouldAlwaysKeepJupiterAssertionStackTraceElement() { assertStackTraceMatch(stackTrace, """ \\Qorg.junit.jupiter.api.Assertions.fail(Assertions.java:\\E.+ - \\Qorg.junit.platform.StackTracePruningTests$FailingTestTestCase.fail(StackTracePruningTests.java:\\E.+ >>>> """); } @@ -126,7 +124,6 @@ void shouldKeepExactlyEverythingBetweenTestCallAndFirstAssertionCall() { assertStackTraceMatch(stackTrace, """ \\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.+ """); } @@ -146,7 +143,6 @@ void shouldKeepExactlyEverythingAfterLifecycleMethodCall(Class methodClass) { assertStackTraceMatch(stackTrace, """ \\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,10 +194,6 @@ static class FailingTestTestCase { @Test void failingAssertion() { - fail(); - } - - private static void fail() { Assertions.fail(); } @@ -211,7 +203,7 @@ void multipleFailingAssertions() { } private void failMultiple() { - Assertions.assertAll(FailingTestTestCase::fail, FailingTestTestCase::fail); + Assertions.assertAll(Assertions::fail, Assertions::fail); } @Test @@ -227,10 +219,6 @@ static class FailingBeforeEachTestCase { @BeforeEach void setUp() { - fail(); - } - - private static void fail() { Assertions.fail(); } From 792c24a821432ce34033bca252c63efeb0540348 Mon Sep 17 00:00:00 2001 From: "M.P. Korstanje" Date: Fri, 21 Nov 2025 16:22:51 +0100 Subject: [PATCH 11/21] Polishing --- .../org/junit/jupiter/api/AssertionFailureBuilder.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 a2608cc146f9..d7490c98d217 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 @@ -49,7 +49,7 @@ public class AssertionFailureBuilder { private boolean includeValuesInMessage = true; - private @Nullable Class trimStackTraceBefore; + private @Nullable Class trimStackTraceTarget; private int trimStackTraceRetain; @@ -151,7 +151,7 @@ public AssertionFailureBuilder includeValuesInMessage(boolean includeValuesInMes @API(status = EXPERIMENTAL, since = "6.1") public AssertionFailureBuilder trimStacktrace(@Nullable Class target, int retain) { Preconditions.condition(retain >= 0, "retain must have a non-negative value"); - this.trimStackTraceBefore = target; + this.trimStackTraceTarget = target; this.trimStackTraceRetain = retain; return this; } @@ -190,11 +190,11 @@ public AssertionFailedError build() { } private void maybeTrimStackTrace(Throwable throwable) { - if (trimStackTraceBefore == null) { + if (trimStackTraceTarget == null) { return; } - var pruneTargetClassName = trimStackTraceBefore.getName(); + var pruneTargetClassName = trimStackTraceTarget.getName(); var stackTrace = throwable.getStackTrace(); int lastIndexOf = -1; From 3df2d4227955c0db446e717f131a64f8d81b86d7 Mon Sep 17 00:00:00 2001 From: "M.P. Korstanje" Date: Fri, 21 Nov 2025 16:23:13 +0100 Subject: [PATCH 12/21] Polishing --- .../java/org/junit/jupiter/api/AssertionFailureBuilder.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 d7490c98d217..e7da8fc5539b 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 @@ -208,8 +208,8 @@ private void maybeTrimStackTrace(Throwable throwable) { if (lastIndexOf != -1) { int from = clamp(lastIndexOf + 1 - trimStackTraceRetain, stackTrace.length); - var pruned = Arrays.copyOfRange(stackTrace, from, stackTrace.length); - throwable.setStackTrace(pruned); + var trimmed = Arrays.copyOfRange(stackTrace, from, stackTrace.length); + throwable.setStackTrace(trimmed); } } From 91e7f9bd72ad1c31d62cdad8b888dbafb2f275a4 Mon Sep 17 00:00:00 2001 From: "M.P. Korstanje" Date: Fri, 21 Nov 2025 16:23:54 +0100 Subject: [PATCH 13/21] Polishing --- .../java/org/junit/jupiter/api/AssertionFailureBuilder.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 e7da8fc5539b..60df61666f3b 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 @@ -207,13 +207,13 @@ private void maybeTrimStackTrace(Throwable throwable) { } if (lastIndexOf != -1) { - int from = clamp(lastIndexOf + 1 - trimStackTraceRetain, stackTrace.length); + int from = clamp0(lastIndexOf + 1 - trimStackTraceRetain, stackTrace.length); var trimmed = Arrays.copyOfRange(stackTrace, from, stackTrace.length); throwable.setStackTrace(trimmed); } } - private static int clamp(int value, int max) { + private static int clamp0(int value, int max) { return Math.max(0, Math.min(value, max)); } From ca409835800bcc9b6c6bbd5a410a6972534891de Mon Sep 17 00:00:00 2001 From: "M.P. Korstanje" Date: Fri, 21 Nov 2025 16:26:49 +0100 Subject: [PATCH 14/21] Polishing --- .../test/java/org/junit/platform/StackTracePruningTests.java | 4 ---- 1 file changed, 4 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 a34d2417db4b..c40181995d55 100644 --- a/platform-tests/src/test/java/org/junit/platform/StackTracePruningTests.java +++ b/platform-tests/src/test/java/org/junit/platform/StackTracePruningTests.java @@ -199,10 +199,6 @@ void failingAssertion() { @Test void multipleFailingAssertions() { - failMultiple(); - } - - private void failMultiple() { Assertions.assertAll(Assertions::fail, Assertions::fail); } From 0bbe7c7f5e4fae30b8c418c04efec7468a88a11c Mon Sep 17 00:00:00 2001 From: "M.P. Korstanje" Date: Fri, 21 Nov 2025 16:27:28 +0100 Subject: [PATCH 15/21] Polishing --- .../src/test/java/org/junit/platform/StackTracePruningTests.java | 1 + 1 file changed, 1 insertion(+) 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 c40181995d55..6aba8657d0fe 100644 --- a/platform-tests/src/test/java/org/junit/platform/StackTracePruningTests.java +++ b/platform-tests/src/test/java/org/junit/platform/StackTracePruningTests.java @@ -208,6 +208,7 @@ void failingAssumption() { throw new RuntimeException(); }); } + } @SuppressWarnings("JUnitMalformedDeclaration") From 23f598a32ea0bf9b82b6360fee5c5d976ee962fd Mon Sep 17 00:00:00 2001 From: "M.P. Korstanje" Date: Fri, 21 Nov 2025 16:28:35 +0100 Subject: [PATCH 16/21] Polishing --- .../test/java/org/junit/platform/StackTracePruningTests.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 6aba8657d0fe..1dc694f641de 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.Assertions.fail(Assertions.java:\\E.+ + \\Qorg.junit.jupiter.api.AssertionUtils.fail(AssertionUtils.java:\\E.+ >>>> \\Qorg.junit.platform.commons.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:\\E.+ >>>> From e1567f03b8d69f2c60ebae222a1ed0aebec9ee91 Mon Sep 17 00:00:00 2001 From: "M.P. Korstanje" Date: Fri, 21 Nov 2025 16:29:52 +0100 Subject: [PATCH 17/21] Polishing --- .../src/test/java/org/junit/platform/StackTracePruningTests.java | 1 + 1 file changed, 1 insertion(+) 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 1dc694f641de..a0482255f232 100644 --- a/platform-tests/src/test/java/org/junit/platform/StackTracePruningTests.java +++ b/platform-tests/src/test/java/org/junit/platform/StackTracePruningTests.java @@ -121,6 +121,7 @@ void shouldKeepExactlyEverythingBetweenTestCallAndFirstAssertionCall() { .execute(); List stackTrace = extractStackTrace(results); + assertStackTraceMatch(stackTrace, """ \\Qorg.junit.jupiter.api.Assertions.fail(Assertions.java:\\E.+ From 3dd6eafb3c58f0c2fe8fd1dbc5753dd3bb625ad3 Mon Sep 17 00:00:00 2001 From: "M.P. Korstanje" Date: Fri, 21 Nov 2025 16:30:14 +0100 Subject: [PATCH 18/21] Polishing --- .../test/java/org/junit/platform/StackTracePruningTests.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 a0482255f232..a8dd91a4ce23 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.jupiter.api.Assertions.fail(Assertions.java:\\E.+ >>>> \\Qorg.junit.platform.commons.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:\\E.+ >>>> From 019a62a69b0519a0d32a716228f26d2ffd9547ee Mon Sep 17 00:00:00 2001 From: "M.P. Korstanje" Date: Fri, 21 Nov 2025 16:57:52 +0100 Subject: [PATCH 19/21] Add precondition test --- .../junit/jupiter/api/AssertionFailureBuilder.java | 2 +- .../jupiter/api/AssertionFailureBuilderTest.java | 13 +++++++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) 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 60df61666f3b..fbfa9a785bbd 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 @@ -141,7 +141,7 @@ public AssertionFailureBuilder includeValuesInMessage(boolean includeValuesInMes * Set target and depth for trimming stacktrace. * *

Removes all but {@code retain - 1} frames before the last frame from - * {@code target}. If {@code retain} is zero, all frames from + * {@code target}. If {@code retain} is zero, all frames including * {@code target} are trimmed. * * @param target class to trim from the stacktrace 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 9048cc82d64b..27783b105cc1 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 @@ -13,6 +13,7 @@ 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.*; import static org.junit.jupiter.api.Assertions.assertLinesMatch; import java.util.Arrays; @@ -20,6 +21,7 @@ import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; +import org.junit.platform.commons.PreconditionViolationException; import org.opentest4j.AssertionFailedError; class AssertionFailureBuilderTest { @@ -89,6 +91,13 @@ void canTrimToEmptyStacktrace() throws ExecutionException, InterruptedException } } + @Test + void mustRetainNonNegativeNumberOfFrames() { + var exception = assertThrows(PreconditionViolationException.class, // + () -> assertionFailure().trimStacktrace(Assertions.class, -1)); + assertThat(exception).hasMessage("retain must have a non-negative value"); + } + private static void assertStackTraceMatch(AssertionFailedError assertionFailedError, String expectedLines) { List stackStraceAsLines = Arrays.stream(assertionFailedError.getStackTrace()) // .map(StackTraceElement::toString) // @@ -101,8 +110,8 @@ static AssertionFailedError fail() { return assertionFailure().build(); } - static AssertionFailedError failWithTrimmedStacktrace(Class to, int depth) { - return AssertionFailureBuilder.assertionFailure().trimStacktrace(to, depth).build(); + static AssertionFailedError failWithTrimmedStacktrace(Class to, int retain) { + return AssertionFailureBuilder.assertionFailure().trimStacktrace(to, retain).build(); } } From a005178ebc0aa44e40b94c2c63838beb46b7ba6b Mon Sep 17 00:00:00 2001 From: "M.P. Korstanje" Date: Fri, 21 Nov 2025 17:07:46 +0100 Subject: [PATCH 20/21] Checkstyle --- .../java/org/junit/jupiter/api/AssertionFailureBuilderTest.java | 1 - 1 file changed, 1 deletion(-) 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 27783b105cc1..328c6a443671 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 @@ -13,7 +13,6 @@ 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.*; import static org.junit.jupiter.api.Assertions.assertLinesMatch; import java.util.Arrays; From 51a1f8674aeb04d768b4d727a23a76dcb6c43acf Mon Sep 17 00:00:00 2001 From: "M.P. Korstanje" Date: Fri, 21 Nov 2025 17:07:53 +0100 Subject: [PATCH 21/21] Checkstyle --- .../java/org/junit/jupiter/api/AssertionFailureBuilderTest.java | 1 + 1 file changed, 1 insertion(+) 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 328c6a443671..7d77e83f54d7 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 @@ -14,6 +14,7 @@ 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 static org.junit.jupiter.api.Assertions.assertThrows; import java.util.Arrays; import java.util.List;