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..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 @@ -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, 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/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..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,6 +483,7 @@ private static AssertionFailedError expectedArrayIsNullFailure(@Nullable Deque" + formatIndexes(indexes)) // + .trimStacktrace(Assertions.class, 1) // .build(); } @@ -495,6 +496,7 @@ private static AssertionFailedError actualArrayIsNullFailure(@Nullable Deque" + formatIndexes(indexes)) // + .trimStacktrace(Assertions.class, 1) // .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, 1) // .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, 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 82e15c85a41a..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,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, 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 2e38bdb6f1c6..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,6 +198,7 @@ private static void failNotEqual(@Nullable Object expected, @Nullable Object act .message(messageOrSupplier) // .expected(expected) // .actual(actual) // + .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 d0e308d3be26..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,6 +66,7 @@ private static void failNotFalse(@Nullable Object messageOrSupplier) { .message(messageOrSupplier) // .expected(false) // .actual(true) // + .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 92ae88a5d9e6..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,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, 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 b9e70a58844d..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,6 +153,7 @@ private static AssertionFailedError expectedIterableIsNullFailure(Deque return assertionFailure() // .message(messageOrSupplier) // .reason("expected iterable was " + formatIndexes(indexes)) // + .trimStacktrace(Assertions.class, 1) // .build(); } @@ -165,6 +166,7 @@ private static AssertionFailedError actualIterableIsNullFailure(Deque i return assertionFailure() // .message(messageOrSupplier) // .reason("actual iterable was " + formatIndexes(indexes)) // + .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 990d12b94f29..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,6 +193,7 @@ void fail(String format, Object... args) { .expected(join(newLine, expectedLines)) // .actual(join(newLine, actualLines)) // .includeValuesInMessage(false) // + .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 e78a32eb4aa8..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,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, 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 a03079f49341..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,6 +52,7 @@ private static void failNull(@Nullable Object messageOrSupplier) { assertionFailure() // .message(messageOrSupplier) // .reason("expected: not ") // + .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 d46c8f711fe1..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,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, 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 137fb1e83aee..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,6 +53,7 @@ private static void failNotNull(@Nullable Object actual, @Nullable Object messag .message(messageOrSupplier) // .expected(null) // .actual(actual) // + .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 2cf1476f3742..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,6 +51,7 @@ private static void failNotSame(@Nullable Object expected, @Nullable Object actu .message(messageOrSupplier) // .expected(expected) // .actual(actual) // + .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 d198486087a4..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,12 +65,14 @@ private static T assertThrows(Class expectedType, Execu .actual(actualException.getClass()) // .reason("Unexpected exception type thrown") // .cause(actualException) // + .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, 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 59e72305be9e..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,6 +65,7 @@ private static T assertThrowsExactly(Class expectedType .actual(actualException.getClass()) // .reason("Unexpected exception type thrown") // .cause(actualException) // + .trimStacktrace(Assertions.class, 1) // .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, 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 d60b065be1bb..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,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, 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 f6c6afd42a12..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,6 +73,7 @@ private static AssertionFailedError createAssertionFailure(Duration timeout, .message(messageSupplier) // .reason("execution timed out after " + timeout.toMillis() + " ms") // .cause(cause) // + .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 cb92e2278419..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,6 +66,7 @@ private static void failNotTrue(@Nullable Object messageOrSupplier) { .message(messageOrSupplier) // .expected(true) // .actual(false) // + .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 a8fd354ce2a4..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 @@ -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; @@ -46,6 +49,10 @@ public class AssertionFailureBuilder { private boolean includeValuesInMessage = true; + private @Nullable Class trimStackTraceTarget; + + private int trimStackTraceRetain; + /** * Create a new {@code AssertionFailureBuilder}. */ @@ -130,6 +137,25 @@ public AssertionFailureBuilder includeValuesInMessage(boolean includeValuesInMes return this; } + /** + * 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 including + * {@code target} are trimmed. + * + * @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 target, int retain) { + Preconditions.condition(retain >= 0, "retain must have a non-negative value"); + this.trimStackTraceTarget = target; + this.trimStackTraceRetain = retain; + return this; + } + /** * Build the {@link AssertionFailedError AssertionFailedError} and throw it. * @@ -154,9 +180,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 - trimStackTraceRetain, 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) { 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..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 @@ -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, 1) // + .build(); } @Contract("_ -> fail") static void fail(@Nullable String message) { - throw new AssertionFailedError(message); + throw assertionFailure() // + .message(message) // + .trimStacktrace(Assertions.class, 1) // + .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, 1) // + .build(); } @Contract("_ -> fail") static void fail(@Nullable Throwable cause) { - throw new AssertionFailedError(null, cause); + throw assertionFailure() // + .cause(cause) // + .trimStacktrace(Assertions.class, 1) // + .build(); } @Contract("_ -> fail") static void fail(Supplier<@Nullable String> messageSupplier) { - throw new AssertionFailedError(nullSafeGet(messageSupplier)); + throw assertionFailure() // + .message(nullSafeGet(messageSupplier)) // + .trimStacktrace(Assertions.class, 1) // + .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..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,6 +363,8 @@ inline fun assertDoesNotThrow(executable: () -> R): R { throw assertionFailure() .reason("Unexpected exception thrown: ${t.javaClass.getName()}$suffix") .cause(t) + // we don't want to retain any frames from the AssertionFailureBuilder + .trimStacktrace(AssertionFailureBuilder::class.java, 0) .build() } } @@ -420,6 +422,8 @@ inline fun assertDoesNotThrow( .message(message()) .reason("Unexpected exception thrown: ${t.javaClass.getName()}$suffix") .cause(t) + // 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 new file mode 100644 index 000000000000..7d77e83f54d7 --- /dev/null +++ b/jupiter-tests/src/test/java/org/junit/jupiter/api/AssertionFailureBuilderTest.java @@ -0,0 +1,118 @@ +/* + * 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 static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; + +import org.junit.platform.commons.PreconditionViolationException; +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, 0); + assertStackTraceMatch(error, + """ + \\Qorg.junit.jupiter.api.AssertionFailureBuilderTest.trimsUpToAssertionsFacade(AssertionFailureBuilderTest.java:\\E.+ + >>>> + """); + } + + @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, 0); + 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, 0); + 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, 0)).get(); + assertThat(error.getStackTrace()).isEmpty(); + } + } + + @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) // + .toList(); + assertLinesMatch(expectedLines.lines().toList(), stackStraceAsLines); + } + + static class AssertionsFacade { + static AssertionFailedError fail() { + return assertionFailure().build(); + } + + static AssertionFailedError failWithTrimmedStacktrace(Class to, int retain) { + return AssertionFailureBuilder.assertionFailure().trimStacktrace(to, retain).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..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.+ >>>> @@ -92,7 +92,6 @@ void shouldAlwaysKeepJupiterAssertionStackTraceElement() { List stackTrace = extractStackTrace(results); assertStackTraceMatch(stackTrace, """ - >>>> \\Qorg.junit.jupiter.api.Assertions.fail(Assertions.java:\\E.+ >>>> """); @@ -115,7 +114,7 @@ void shouldAlwaysKeepJupiterAssumptionStackTraceElement() { } @Test - void shouldKeepExactlyEverythingAfterTestCall() { + void shouldKeepExactlyEverythingBetweenTestCallAndFirstAssertionCall() { EngineExecutionResults results = EngineTestKit.engine("junit-jupiter") // .configurationParameter("junit.platform.stacktrace.pruning.enabled", "true") // .selectors(selectMethod(FailingTestTestCase.class, "failingAssertion")) // @@ -125,7 +124,6 @@ void shouldKeepExactlyEverythingAfterTestCall() { 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.failingAssertion(StackTracePruningTests.java:\\E.+ """); @@ -145,7 +143,6 @@ 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.setUp(StackTracePruningTests.java:\\E.+ """);