Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
9e04b51
Trim internal frames from AssertionFailedError
mpkorstanje Nov 17, 2025
745837f
Cover trimming boundaries
mpkorstanje Nov 18, 2025
bdbb937
Update release notes
mpkorstanje Nov 18, 2025
0e260b4
Polishing
mpkorstanje Nov 18, 2025
9b315c4
Polishing
mpkorstanje Nov 18, 2025
e78114e
Keep one frame from Assertions for clarity
mpkorstanje Nov 21, 2025
09b8bc4
Polishing
mpkorstanje Nov 21, 2025
3cb7cf3
Polishing
mpkorstanje Nov 21, 2025
32a3dbb
Polishing
mpkorstanje Nov 21, 2025
fa98895
Polishing
mpkorstanje Nov 21, 2025
792c24a
Polishing
mpkorstanje Nov 21, 2025
3df2d42
Polishing
mpkorstanje Nov 21, 2025
91e7f9b
Polishing
mpkorstanje Nov 21, 2025
ca40983
Polishing
mpkorstanje Nov 21, 2025
0bbe7c7
Polishing
mpkorstanje Nov 21, 2025
23f598a
Polishing
mpkorstanje Nov 21, 2025
e1567f0
Polishing
mpkorstanje Nov 21, 2025
3dd6eaf
Polishing
mpkorstanje Nov 21, 2025
3a91561
Merge branch 'main' into rien/prune-stacktrace-assertion-builder
mpkorstanje Nov 21, 2025
019a62a
Add precondition test
mpkorstanje Nov 21, 2025
a005178
Checkstyle
mpkorstanje Nov 21, 2025
51a1f86
Checkstyle
mpkorstanje Nov 21, 2025
a55be92
Extract retainStackTraceElements method
mpkorstanje Dec 1, 2025
f851ab5
Set retainStackTraceElements to 1 by default
mpkorstanje Dec 1, 2025
ce04f51
Prefer stacktrace elements over frames
mpkorstanje Dec 1, 2025
7537c92
Merge remote-tracking branch 'origin/main' into rien/prune-stacktrace…
mpkorstanje Dec 6, 2025
738dbaf
Merge remote-tracking branch 'origin/main' into rien/prune-stacktrace…
mpkorstanje Dec 10, 2025
a994978
Polish release notes
mpkorstanje Dec 10, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ repository on GitHub.
* https://www.junit-pioneer.org/[JUnit Pioneer]'s `DefaultLocaleExtension` and
`DefaultTimeZoneExtension` are now part of the JUnit Jupiter. Find examples in the
xref:writing-tests/built-in-extensions.adoc#DefaultLocaleAndTimeZone[User Guide].
* Trim internal stack frames from `AssertionFailedError` stack traces.
* Introduce new `trimStacktrace(Class<?>)` and `retainStackTraceElements(int)`
methods for `AssertionFailureBuilder`. These allow user defined assertions to
trim their stacktrace.

[[v6.1.0-M2-junit-vintage]]
=== JUnit Vintage
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -483,6 +483,7 @@ private static AssertionFailedError expectedArrayIsNullFailure(@Nullable Deque<I
return assertionFailure() //
.message(messageOrSupplier) //
.reason("expected array was <null>" + formatIndexes(indexes)) //
.trimStacktrace(Assertions.class) //
.build();
}

Expand All @@ -495,6 +496,7 @@ private static AssertionFailedError actualArrayIsNullFailure(@Nullable Deque<Int
return assertionFailure() //
.message(messageOrSupplier) //
.reason("actual array was <null>" + formatIndexes(indexes)) //
.trimStacktrace(Assertions.class) //
.build();
}

Expand All @@ -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();
}
}
Expand All @@ -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();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,7 @@ private static void failNotEqual(@Nullable Object expected, @Nullable Object act
.message(messageOrSupplier) //
.expected(expected) //
.actual(actual) //
.trimStacktrace(Assertions.class) //
.buildAndThrow();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ private static void failNotFalse(@Nullable Object messageOrSupplier) {
.message(messageOrSupplier) //
.expected(false) //
.actual(true) //
.trimStacktrace(Assertions.class) //
.buildAndThrow();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ private static <T> T assertInstanceOf(Class<T> 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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ private static AssertionFailedError expectedIterableIsNullFailure(Deque<Integer>
return assertionFailure() //
.message(messageOrSupplier) //
.reason("expected iterable was <null>" + formatIndexes(indexes)) //
.trimStacktrace(Assertions.class) //
.build();
}

Expand All @@ -165,6 +166,7 @@ private static AssertionFailedError actualIterableIsNullFailure(Deque<Integer> i
return assertionFailure() //
.message(messageOrSupplier) //
.reason("actual iterable was <null>" + formatIndexes(indexes)) //
.trimStacktrace(Assertions.class) //
.build();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,7 @@ void fail(String format, Object... args) {
.expected(join(newLine, expectedLines)) //
.actual(join(newLine, actualLines)) //
.includeValuesInMessage(false) //
.trimStacktrace(Assertions.class) //
.buildAndThrow();
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ private static void failNull(@Nullable Object messageOrSupplier) {
assertionFailure() //
.message(messageOrSupplier) //
.reason("expected: not <null>") //
.trimStacktrace(Assertions.class) //
.buildAndThrow();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ private static void failNotNull(@Nullable Object actual, @Nullable Object messag
.message(messageOrSupplier) //
.expected(null) //
.actual(actual) //
.trimStacktrace(Assertions.class) //
.buildAndThrow();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ private static void failNotSame(@Nullable Object expected, @Nullable Object actu
.message(messageOrSupplier) //
.expected(expected) //
.actual(actual) //
.trimStacktrace(Assertions.class) //
.buildAndThrow();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,12 +65,14 @@ private static <T extends Throwable> T assertThrows(Class<T> 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();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,13 +65,15 @@ private static <T extends Throwable> T assertThrowsExactly(Class<T> expectedType
.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();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ private static void failNotTrue(@Nullable Object messageOrSupplier) {
.message(messageOrSupplier) //
.expected(true) //
.actual(false) //
.trimStacktrace(Assertions.class) //
.buildAndThrow();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,16 @@

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;
import org.jspecify.annotations.Nullable;
import org.junit.platform.commons.util.Preconditions;
import org.junit.platform.commons.util.StringUtils;
import org.opentest4j.AssertionFailedError;

Expand All @@ -32,6 +35,8 @@
@API(status = STABLE, since = "5.9")
public class AssertionFailureBuilder {

private static final int DEFAULT_RETAIN_STACKTRACE_ELEMENTS = 1;

private @Nullable Object message;

private @Nullable Throwable cause;
Expand All @@ -46,6 +51,10 @@ public class AssertionFailureBuilder {

private boolean includeValuesInMessage = true;

private @Nullable Class<?> trimStackTraceTarget;

private int retainStackTraceElements = DEFAULT_RETAIN_STACKTRACE_ELEMENTS;

/**
* Create a new {@code AssertionFailureBuilder}.
*/
Expand Down Expand Up @@ -130,6 +139,42 @@ public AssertionFailureBuilder includeValuesInMessage(boolean includeValuesInMes
return this;
}

/**
* Set target to trim the stacktrace to.
*
* <p>Unless {@link #retainStackTraceElements(int)} is set all stacktrace
* elements before the last element from {@code target} are trimmed.
*
* @param target class to trim from the stacktrace
* @return this builder for method chaining
*/
@API(status = EXPERIMENTAL, since = "6.1")
public AssertionFailureBuilder trimStacktrace(@Nullable Class<?> target) {
this.trimStackTraceTarget = target;
return this;
}

/**
* Set depth to trim the stacktrace to. Defaults to
* {@value #DEFAULT_RETAIN_STACKTRACE_ELEMENTS}.
*
* <p>If {@link #trimStacktrace(Class)} was set, all but
* {@code retainStackTraceElements - 1} stacktrace elements before the last
* element from {@code target} are removed. If
* {@code retainStackTraceElements} is zero, all elements including those
* from {@code target} are trimmed.
*
* @param retainStackTraceElements depth of trimming, must be non-negative
* @return this builder for method chaining
*/
@API(status = EXPERIMENTAL, since = "6.1")
public AssertionFailureBuilder retainStackTraceElements(int retainStackTraceElements) {
Preconditions.condition(retainStackTraceElements >= 0,
"retainStackTraceElements must have a non-negative value");
this.retainStackTraceElements = retainStackTraceElements;
return this;
}

/**
* Build the {@link AssertionFailedError AssertionFailedError} and throw it.
*
Expand All @@ -154,9 +199,41 @@ 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 (trimStackTraceTarget == null) {
return;
}

var pruneTargetClassName = trimStackTraceTarget.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 = clamp0(lastIndexOf + 1 - retainStackTraceElements, stackTrace.length);
var trimmed = Arrays.copyOfRange(stackTrace, from, stackTrace.length);
throwable.setStackTrace(trimmed);
}
}

private static int clamp0(int value, int max) {
return Math.max(0, Math.min(value, max));
}

private static @Nullable String nullSafeGet(@Nullable Object messageOrSupplier) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,14 @@
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;

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
Expand All @@ -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) {
Expand Down
Loading