From 309a696f4f7a2cda191521b43b0f2482832a6123 Mon Sep 17 00:00:00 2001 From: Juliette de Rancourt Date: Wed, 28 Jun 2023 20:41:23 +0200 Subject: [PATCH] Revise stacktrace pruning (#3364) Resolves #3299. --- .../release-notes-5.10.0-RC1.adoc | 5 +- .../asciidoc/user-guide/running-tests.adoc | 19 ++- .../junitbuild.testing-conventions.gradle.kts | 1 - .../platform/commons/util/ExceptionUtils.java | 33 +++-- .../junit/platform/engine/TestDescriptor.java | 20 +++ .../platform/launcher/LauncherConstants.java | 44 ------- .../core/EngineExecutionOrchestrator.java | 6 +- ...ckTracePruningEngineExecutionListener.java | 46 ++++--- .../platform/StackTracePruningTests.java | 117 ++++++++++++------ .../commons/util/ExceptionUtilsTests.java | 55 ++++---- .../AbstractTestDescriptorTests.java | 32 ++++- .../launcher/core/LauncherFactoryTests.java | 4 +- ...egacyXmlReportGeneratingListenerTests.java | 4 +- 13 files changed, 224 insertions(+), 162 deletions(-) diff --git a/documentation/src/docs/asciidoc/release-notes/release-notes-5.10.0-RC1.adoc b/documentation/src/docs/asciidoc/release-notes/release-notes-5.10.0-RC1.adoc index c3a9bdaea88f..4524496a0b36 100644 --- a/documentation/src/docs/asciidoc/release-notes/release-notes-5.10.0-RC1.adoc +++ b/documentation/src/docs/asciidoc/release-notes/release-notes-5.10.0-RC1.adoc @@ -33,7 +33,10 @@ JUnit repository on GitHub. * New `selectMethod()` and `selectNestedMethod()` variants in `DiscoverySelectors` that accept a `Class...` argument of parameter types as a type-safe alternative to providing the names of parameter types as a comma-delimited string. - +* Stack trace pruning has been revised and now only removes calls from the `org.junit`, + `jdk.internal.reflect`, and `sun.reflect` packages. Please refer to the + <<../user-guide/index.adoc#stacktrace-pruning, User Guide>> for details. +* New `getAncestors()` method in `TestDescriptor`. [[release-notes-5.10.0-RC1-junit-jupiter]] === JUnit Jupiter diff --git a/documentation/src/docs/asciidoc/user-guide/running-tests.adoc b/documentation/src/docs/asciidoc/user-guide/running-tests.adoc index 8461b9bc7c1f..35915254fd95 100644 --- a/documentation/src/docs/asciidoc/user-guide/running-tests.adoc +++ b/documentation/src/docs/asciidoc/user-guide/running-tests.adoc @@ -1138,18 +1138,13 @@ Since version 1.10, the JUnit Platform provides built-in support for pruning sta produced by failing tests. This feature can be enabled or disabled via the `junit.platform.stacktrace.pruning.enabled` _configuration parameter_. -By default, all calls from the `org.junit`, `java`, and `jdk` packages are removed from the -stack trace. You can also configure the JUnit Platform to exclude different or additional -calls. To do this, provide a pattern for the `junit.platform.stacktrace.pruning.pattern` -_configuration parameter_ to specify which fully qualified class names should be excluded -from the stack traces. - -In addition, and independently of the provided pattern, all elements prior to and -including the first call from the JUnit Platform Launcher will be removed. - -NOTE: Since they provide necessary insights to understand a test failure, calls to -`{Assertions}` or `{Assumptions}` will never be excluded from stack traces even though -they are part of the `org.junit` package. +If enabled, all calls from the `org.junit`, `jdk.internal.reflect`, and `sun.reflect` +packages are removed from the stack trace, unless they are subsequent to the test itself +or any of its ancestors. For that reason, calls to `{Assertions}` or `{Assumptions}` will +never be excluded. + +In addition, all elements prior to and including the first call from the JUnit Platform +Launcher will be removed. [[stacktrace-pruning-pattern]] ==== Pattern Matching Syntax diff --git a/gradle/plugins/common/src/main/kotlin/junitbuild.testing-conventions.gradle.kts b/gradle/plugins/common/src/main/kotlin/junitbuild.testing-conventions.gradle.kts index 122db6b5b5af..8a1cdd89955f 100644 --- a/gradle/plugins/common/src/main/kotlin/junitbuild.testing-conventions.gradle.kts +++ b/gradle/plugins/common/src/main/kotlin/junitbuild.testing-conventions.gradle.kts @@ -41,7 +41,6 @@ tasks.withType().configureEach { server.set(uri("https://ge.junit.org")) } systemProperty("java.util.logging.manager", "org.apache.logging.log4j.jul.LogManager") - systemProperty("junit.platform.stacktrace.pruning.enabled", false) // Required until ASM officially supports the JDK 14 systemProperty("net.bytebuddy.experimental", true) if (buildParameters.testing.enableJFR) { diff --git a/junit-platform-commons/src/main/java/org/junit/platform/commons/util/ExceptionUtils.java b/junit-platform-commons/src/main/java/org/junit/platform/commons/util/ExceptionUtils.java index 64f59d12d5fb..7b108da8070f 100644 --- a/junit-platform-commons/src/main/java/org/junit/platform/commons/util/ExceptionUtils.java +++ b/junit-platform-commons/src/main/java/org/junit/platform/commons/util/ExceptionUtils.java @@ -42,6 +42,9 @@ public final class ExceptionUtils { private static final String JUNIT_PLATFORM_LAUNCHER_PACKAGE_PREFIX = "org.junit.platform.launcher."; + private static final Predicate STACK_TRACE_ELEMENT_FILTER = ClassNamePatternFilterUtils // + .excludeMatchingClassNames("org.junit.*,jdk.internal.reflect.*,sun.reflect.*"); + private ExceptionUtils() { /* no-op */ } @@ -92,38 +95,44 @@ public static String readStackTrace(Throwable throwable) { } /** - * Prune the stack trace of the supplied {@link Throwable} by filtering its - * elements using the supplied {@link Predicate}, except for - * {@code org.junit.jupiter.api.Assertions} and - * {@code org.junit.jupiter.api.Assumptions} that will always remain - * present. + * Prune the stack trace of the supplied {@link Throwable} by removing + * elements from the {@code org.junit}, {@code jdk.internal.reflect} and + * {@code sun.reflect} packages. If an element matching one of the supplied + * class names is encountered, all following elements will be kept regardless. * *

Additionally, all elements prior to and including the first * JUnit Launcher call will be removed. * * @param throwable the {@code Throwable} whose stack trace should be * pruned; never {@code null} - * @param stackTraceElementFilter the {@code Predicate} used to filter - * elements of the stack trace; never {@code null} + * @param testClassNames the test class names that should stop the pruning + * if encountered; never {@code null} * * @since 5.10 */ @API(status = INTERNAL, since = "5.10") - public static void pruneStackTrace(Throwable throwable, Predicate stackTraceElementFilter) { + public static void pruneStackTrace(Throwable throwable, List testClassNames) { Preconditions.notNull(throwable, "Throwable must not be null"); - Preconditions.notNull(stackTraceElementFilter, "Predicate must not be null"); + Preconditions.notNull(testClassNames, "List of test class names must not be null"); List stackTrace = Arrays.asList(throwable.getStackTrace()); List prunedStackTrace = new ArrayList<>(); Collections.reverse(stackTrace); - for (StackTraceElement element : stackTrace) { + for (int i = 0; i < stackTrace.size(); i++) { + StackTraceElement element = stackTrace.get(i); String className = element.getClassName(); - if (className.startsWith(JUNIT_PLATFORM_LAUNCHER_PACKAGE_PREFIX)) { + + if (testClassNames.contains(className)) { + // Include all elements called by the test + prunedStackTrace.addAll(stackTrace.subList(i, stackTrace.size())); + break; + } + else if (className.startsWith(JUNIT_PLATFORM_LAUNCHER_PACKAGE_PREFIX)) { prunedStackTrace.clear(); } - else if (stackTraceElementFilter.test(className)) { + else if (STACK_TRACE_ELEMENT_FILTER.test(className)) { prunedStackTrace.add(element); } } diff --git a/junit-platform-engine/src/main/java/org/junit/platform/engine/TestDescriptor.java b/junit-platform-engine/src/main/java/org/junit/platform/engine/TestDescriptor.java index b16a8d4319e2..7dfcecc644ca 100644 --- a/junit-platform-engine/src/main/java/org/junit/platform/engine/TestDescriptor.java +++ b/junit-platform-engine/src/main/java/org/junit/platform/engine/TestDescriptor.java @@ -106,6 +106,26 @@ default String getLegacyReportingName() { */ Set getChildren(); + /** + * Get the immutable set of all ancestors of this descriptor. + * + *

An ancestors is the parent of this descriptor or the parent + * of one of its parents, recursively. + * + * @see #getParent() + */ + @API(status = STABLE, since = "5.10") + default Set getAncestors() { + if (!getParent().isPresent()) { + return Collections.emptySet(); + } + TestDescriptor parent = getParent().get(); + Set ancestors = new LinkedHashSet<>(); + ancestors.add(parent); + ancestors.addAll(parent.getAncestors()); + return Collections.unmodifiableSet(ancestors); + } + /** * Get the immutable set of all descendants of this descriptor. * diff --git a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/LauncherConstants.java b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/LauncherConstants.java index bf16677f933c..af05c4ae86ee 100644 --- a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/LauncherConstants.java +++ b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/LauncherConstants.java @@ -174,50 +174,6 @@ public class LauncherConstants { @API(status = EXPERIMENTAL, since = "1.10") public static final String STACKTRACE_PRUNING_ENABLED_PROPERTY_NAME = "junit.platform.stacktrace.pruning.enabled"; - /** - * Property name used to provide patterns to remove elements from stack traces. - * - *

Pattern Matching Syntax

- * - *

If the property value consists solely of an asterisk ({@code *}), all - * elements will be removed. Otherwise, the property value will be treated - * as a comma-separated list of patterns where each individual pattern will - * be matched against the fully qualified class name (FQCN) of the - * stack trace element. Any dot ({@code .}) in a pattern will match against - * a dot ({@code .}) or a dollar sign ({@code $}) in a FQCN. Any asterisk - * ({@code *}) will match against one or more characters in a FQCN. All - * other characters in a pattern will be matched one-to-one against a FQCN. - * - *

Examples

- * - *
    - *
  • {@code *}: remove all elements. - *
  • {@code org.junit.*}: remove every element with the {@code org.junit} - * base package and any of its subpackages. - *
  • {@code *.MyClass}: remove every element whose simple class name is - * exactly {@code MyClass}. - *
  • {@code *System*, *Dev*}: exclude every element whose FQCN contains - * {@code System} or {@code Dev}. - *
  • {@code org.example.MyClass, org.example.TheirClass}: remove - * elements whose FQCN is exactly {@code org.example.MyClass} or - * {@code org.example.TheirClass}. - *
- * - * @see #STACKTRACE_PRUNING_DEFAULT_PATTERN - */ - @API(status = EXPERIMENTAL, since = "1.10") - public static final String STACKTRACE_PRUNING_PATTERN_PROPERTY_NAME = "junit.platform.stacktrace.pruning.pattern"; - - /** - * Default pattern for stack trace pruning which matches the - * {@code org.junit}, {@code java}, and {@code jdk} base packages as well - * as any of their subpackages. - * - * @see #STACKTRACE_PRUNING_PATTERN_PROPERTY_NAME - */ - @API(status = EXPERIMENTAL, since = "1.10") - public static final String STACKTRACE_PRUNING_DEFAULT_PATTERN = "org.junit.*,java.*,jdk.*"; - private LauncherConstants() { /* no-op */ } diff --git a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/EngineExecutionOrchestrator.java b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/EngineExecutionOrchestrator.java index ab1a8e94b503..ed764199a3f6 100644 --- a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/EngineExecutionOrchestrator.java +++ b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/EngineExecutionOrchestrator.java @@ -12,9 +12,7 @@ import static org.apiguardian.api.API.Status.INTERNAL; import static org.junit.platform.launcher.LauncherConstants.DRY_RUN_PROPERTY_NAME; -import static org.junit.platform.launcher.LauncherConstants.STACKTRACE_PRUNING_DEFAULT_PATTERN; import static org.junit.platform.launcher.LauncherConstants.STACKTRACE_PRUNING_ENABLED_PROPERTY_NAME; -import static org.junit.platform.launcher.LauncherConstants.STACKTRACE_PRUNING_PATTERN_PROPERTY_NAME; import static org.junit.platform.launcher.core.ListenerRegistry.forEngineExecutionListeners; import java.util.Optional; @@ -179,9 +177,7 @@ private static EngineExecutionListener selectExecutionListener(EngineExecutionLi boolean stackTracePruningEnabled = configurationParameters.getBoolean(STACKTRACE_PRUNING_ENABLED_PROPERTY_NAME) // .orElse(true); if (stackTracePruningEnabled) { - String pruningPattern = configurationParameters.get(STACKTRACE_PRUNING_PATTERN_PROPERTY_NAME) // - .orElse(STACKTRACE_PRUNING_DEFAULT_PATTERN); - return new StackTracePruningEngineExecutionListener(engineExecutionListener, pruningPattern); + return new StackTracePruningEngineExecutionListener(engineExecutionListener); } return engineExecutionListener; } diff --git a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/StackTracePruningEngineExecutionListener.java b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/StackTracePruningEngineExecutionListener.java index 574ce4827d47..c183fdb683b8 100644 --- a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/StackTracePruningEngineExecutionListener.java +++ b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/StackTracePruningEngineExecutionListener.java @@ -10,49 +10,61 @@ package org.junit.platform.launcher.core; -import java.util.Arrays; import java.util.List; -import java.util.function.Predicate; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Collectors; -import org.junit.platform.commons.util.ClassNamePatternFilterUtils; import org.junit.platform.commons.util.ExceptionUtils; import org.junit.platform.engine.EngineExecutionListener; import org.junit.platform.engine.TestDescriptor; import org.junit.platform.engine.TestExecutionResult; +import org.junit.platform.engine.support.descriptor.ClassSource; +import org.junit.platform.engine.support.descriptor.MethodSource; /** * Prunes the stack trace in case of a failed event. * * @since 1.10 - * @see org.junit.platform.commons.util.ExceptionUtils#pruneStackTrace(Throwable, Predicate) + * @see org.junit.platform.commons.util.ExceptionUtils#pruneStackTrace(Throwable, List) */ class StackTracePruningEngineExecutionListener extends DelegatingEngineExecutionListener { - private static final List ALWAYS_INCLUDED_STACK_TRACE_ELEMENTS = Arrays.asList( // - "org.junit.jupiter.api.Assertions", // - "org.junit.jupiter.api.Assumptions" // - ); - - private final Predicate stackTraceElementFilter; - - StackTracePruningEngineExecutionListener(EngineExecutionListener delegate, String pruningPattern) { + StackTracePruningEngineExecutionListener(EngineExecutionListener delegate) { super(delegate); - this.stackTraceElementFilter = ClassNamePatternFilterUtils.excludeMatchingClassNames(pruningPattern) // - .or(ALWAYS_INCLUDED_STACK_TRACE_ELEMENTS::contains); } @Override public void executionFinished(TestDescriptor testDescriptor, TestExecutionResult testExecutionResult) { + List testClassNames = getTestClassNames(testDescriptor); if (testExecutionResult.getThrowable().isPresent()) { Throwable throwable = testExecutionResult.getThrowable().get(); - ExceptionUtils.findNestedThrowables(throwable).forEach(this::pruneStackTrace); + ExceptionUtils.findNestedThrowables(throwable).forEach( + t -> ExceptionUtils.pruneStackTrace(t, testClassNames)); } super.executionFinished(testDescriptor, testExecutionResult); } - private void pruneStackTrace(Throwable throwable) { - ExceptionUtils.pruneStackTrace(throwable, stackTraceElementFilter); + private static List getTestClassNames(TestDescriptor testDescriptor) { + return testDescriptor.getAncestors() // + .stream() // + .map(TestDescriptor::getSource) // + .filter(Optional::isPresent) // + .map(Optional::get) // + .map(source -> { + if (source instanceof ClassSource) { + return ((ClassSource) source).getClassName(); + } + else if (source instanceof MethodSource) { + return ((MethodSource) source).getClassName(); + } + else { + return null; + } + }) // + .filter(Objects::nonNull) // + .collect(Collectors.toList()); } } 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 397dacc3172d..d8a612bef2bc 100644 --- a/platform-tests/src/test/java/org/junit/platform/StackTracePruningTests.java +++ b/platform-tests/src/test/java/org/junit/platform/StackTracePruningTests.java @@ -20,7 +20,11 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import org.junit.platform.engine.TestExecutionResult; import org.junit.platform.testkit.engine.EngineExecutionResults; import org.junit.platform.testkit.engine.EngineTestKit; @@ -39,16 +43,11 @@ class StackTracePruningTests { @Test void shouldPruneStackTraceByDefault() { EngineExecutionResults results = EngineTestKit.engine("junit-jupiter") // - .selectors(selectMethod(StackTracePruningTestCase.class, "failingAssertion")) // + .selectors(selectMethod(FailingTestTestCase.class, "failingAssertion")) // .execute(); List stackTrace = extractStackTrace(results); - assertStackTraceMatch(stackTrace, """ - \\Qorg.junit.jupiter.api.Assertions.fail(Assertions.java:\\E.+ - """); - - assertStackTraceDoesNotContain(stackTrace, "java.util.ArrayList.forEach(ArrayList.java:"); assertStackTraceDoesNotContain(stackTrace, "jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:"); } @@ -57,16 +56,11 @@ void shouldPruneStackTraceByDefault() { void shouldPruneStackTraceWhenEnabled() { EngineExecutionResults results = EngineTestKit.engine("junit-jupiter") // .configurationParameter("junit.platform.stacktrace.pruning.enabled", "true") // - .selectors(selectMethod(StackTracePruningTestCase.class, "failingAssertion")) // + .selectors(selectMethod(FailingTestTestCase.class, "failingAssertion")) // .execute(); List stackTrace = extractStackTrace(results); - assertStackTraceMatch(stackTrace, """ - \\Qorg.junit.jupiter.api.Assertions.fail(Assertions.java:\\E.+ - """); - - assertStackTraceDoesNotContain(stackTrace, "java.util.ArrayList.forEach(ArrayList.java:"); assertStackTraceDoesNotContain(stackTrace, "jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:"); } @@ -75,83 +69,103 @@ void shouldPruneStackTraceWhenEnabled() { void shouldNotPruneStackTraceWhenDisabled() { EngineExecutionResults results = EngineTestKit.engine("junit-jupiter") // .configurationParameter("junit.platform.stacktrace.pruning.enabled", "false") // - .selectors(selectMethod(StackTracePruningTestCase.class, "failingAssertion")) // + .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.+ >>>> - \\Qjava.base/java.util.ArrayList.forEach(ArrayList.java:\\E.+ + \\Qorg.junit.platform.commons.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:\\E.+ >>>> """); } @Test - void shouldPruneStackTraceAccordingToPattern() { + void shouldAlwaysKeepJupiterAssertionStackTraceElement() { EngineExecutionResults results = EngineTestKit.engine("junit-jupiter") // .configurationParameter("junit.platform.stacktrace.pruning.enabled", "true") // - .configurationParameter("junit.platform.stacktrace.pruning.pattern", "jdk.*") // - .selectors(selectMethod(StackTracePruningTestCase.class, "failingAssertion")) // + .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.+ >>>> - \\Qjava.base/java.util.ArrayList.forEach(ArrayList.java:\\E.+ + \\Qorg.junit.jupiter.api.Assertions.fail(Assertions.java:\\E.+ >>>> """); - - assertStackTraceDoesNotContain(stackTrace, "jdk."); } @Test - void shouldAlwaysKeepJupiterAssertionStackTraceElement() { + void shouldAlwaysKeepJupiterAssumptionStackTraceElement() { EngineExecutionResults results = EngineTestKit.engine("junit-jupiter") // .configurationParameter("junit.platform.stacktrace.pruning.enabled", "true") // - .configurationParameter("junit.platform.stacktrace.pruning.pattern", "*") // - .selectors(selectMethod(StackTracePruningTestCase.class, "failingAssertion")) // + .selectors(selectMethod(FailingTestTestCase.class, "failingAssumption")) // .execute(); List stackTrace = extractStackTrace(results); assertStackTraceMatch(stackTrace, """ - \\Qorg.junit.jupiter.api.Assertions.fail(Assertions.java:\\E.+ + >>>> + \\Qorg.junit.jupiter.api.Assumptions.assumeTrue(Assumptions.java:\\E.+ + >>>> """); } @Test - void shouldAlwaysKeepJupiterAssumptionStackTraceElement() { + void shouldKeepEverythingAfterTestCall() { EngineExecutionResults results = EngineTestKit.engine("junit-jupiter") // .configurationParameter("junit.platform.stacktrace.pruning.enabled", "true") // - .configurationParameter("junit.platform.stacktrace.pruning.pattern", "*") // - .selectors(selectMethod(StackTracePruningTestCase.class, "failingAssumption")) // + .selectors(selectMethod(FailingTestTestCase.class, "failingAssertion")) // .execute(); List stackTrace = extractStackTrace(results); - assertStackTraceMatch(stackTrace, """ - \\Qorg.junit.jupiter.api.Assumptions.assumeTrue(Assumptions.java:\\E.+ - """); + 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.+ + >>>> + """); + } + + @ParameterizedTest + @ValueSource(strings = { "org.junit.platform.StackTracePruningTests$FailingBeforeEachTestCase", + "org.junit.platform.StackTracePruningTests$FailingBeforeEachTestCase$NestedTestCase", + "org.junit.platform.StackTracePruningTests$FailingBeforeEachTestCase$NestedTestCase$NestedNestedTestCase" }) + void shouldKeepEverythingAfterLifecycleMethodCall(Class methodClass) { + EngineExecutionResults results = EngineTestKit.engine("junit-jupiter") // + .configurationParameter("junit.platform.stacktrace.pruning.enabled", "true") // + .selectors(selectMethod(methodClass, "test")) // + .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$FailingBeforeEachTestCase.setUp(StackTracePruningTests.java:\\E.+ + >>>> + """); } @Test void shouldPruneStackTracesOfSuppressedExceptions() { EngineExecutionResults results = EngineTestKit.engine("junit-jupiter") // .configurationParameter("junit.platform.stacktrace.pruning.enabled", "true") // - .selectors(selectMethod(StackTracePruningTestCase.class, "multipleFailingAssertions")) // + .selectors(selectMethod(FailingTestTestCase.class, "multipleFailingAssertions")) // .execute(); Throwable throwable = getThrowable(results); for (Throwable suppressed : throwable.getSuppressed()) { List stackTrace = Arrays.asList(suppressed.getStackTrace()); - assertStackTraceDoesNotContain(stackTrace, "java.util.ArrayList.forEach(ArrayList.java:"); + assertStackTraceDoesNotContain(stackTrace, + "jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:"); } } @@ -181,7 +195,7 @@ private static void assertStackTraceDoesNotContain(List stack // ------------------------------------------------------------------- - static class StackTracePruningTestCase { + static class FailingTestTestCase { @Test void failingAssertion() { @@ -202,4 +216,35 @@ void failingAssumption() { } + static class FailingBeforeEachTestCase { + + @BeforeEach + void setUp() { + Assertions.fail(); + } + + @Test + void test() { + } + + @Nested + class NestedTestCase { + + @Test + void test() { + } + + @Nested + class NestedNestedTestCase { + + @Test + void test() { + } + + } + + } + + } + } diff --git a/platform-tests/src/test/java/org/junit/platform/commons/util/ExceptionUtilsTests.java b/platform-tests/src/test/java/org/junit/platform/commons/util/ExceptionUtilsTests.java index 1c96ca1b7e9b..edd11a17ded4 100644 --- a/platform-tests/src/test/java/org/junit/platform/commons/util/ExceptionUtilsTests.java +++ b/platform-tests/src/test/java/org/junit/platform/commons/util/ExceptionUtilsTests.java @@ -18,8 +18,11 @@ import static org.junit.platform.commons.util.ExceptionUtils.throwAsUncheckedException; import java.io.IOException; +import java.util.List; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import org.junit.platform.commons.JUnitException; import org.junit.platform.commons.PreconditionViolationException; @@ -64,44 +67,38 @@ void readStackTraceForLocalJUnitException() { } } - @Test - void pruneStackTraceOfCallsFromSpecificPackage() { - try { - throw new JUnitException("expected"); - } - catch (JUnitException e) { - pruneStackTrace(e, element -> !element.startsWith("org.junit.")); - assertThat(e.getStackTrace()) // - .noneMatch(element -> element.toString().contains("org.junit.")); - } + @ParameterizedTest + @ValueSource(strings = { "org.junit.", "jdk.internal.reflect.", "sun.reflect." }) + void pruneStackTraceOfCallsFromSpecificPackage(String shouldBePruned) { + JUnitException exception = new JUnitException("expected"); + + pruneStackTrace(exception, List.of()); + + assertThat(exception.getStackTrace()) // + .noneMatch(element -> element.toString().contains(shouldBePruned)); } @Test void pruneStackTraceOfAllLauncherCalls() { - try { - throw new JUnitException("expected"); - } - catch (JUnitException e) { - pruneStackTrace(e, element -> true); - assertThat(e.getStackTrace()) // - .noneMatch(element -> element.toString().contains("org.junit.platform.launcher.")); - } + JUnitException exception = new JUnitException("expected"); + + pruneStackTrace(exception, List.of()); + + assertThat(exception.getStackTrace()) // + .noneMatch(element -> element.toString().contains("org.junit.platform.launcher.")); } @Test void pruneStackTraceOfEverythingPriorToFirstLauncherCall() { - try { - throw new JUnitException("expected"); - } - catch (JUnitException e) { - StackTraceElement[] stackTrace = e.getStackTrace(); - stackTrace[stackTrace.length - 1] = new StackTraceElement("org.example.Class", "method", "file", 123); - e.setStackTrace(stackTrace); + JUnitException exception = new JUnitException("expected"); + StackTraceElement[] stackTrace = exception.getStackTrace(); + stackTrace[stackTrace.length - 1] = new StackTraceElement("org.example.Class", "method", "file", 123); + exception.setStackTrace(stackTrace); - pruneStackTrace(e, element -> true); - assertThat(e.getStackTrace()) // - .noneMatch(element -> element.toString().contains("org.example.Class.method(file:123)")); - } + pruneStackTrace(exception, List.of()); + + assertThat(exception.getStackTrace()) // + .noneMatch(element -> element.toString().contains("org.example.Class.method(file:123)")); } @Test diff --git a/platform-tests/src/test/java/org/junit/platform/engine/support/descriptor/AbstractTestDescriptorTests.java b/platform-tests/src/test/java/org/junit/platform/engine/support/descriptor/AbstractTestDescriptorTests.java index abbc0f1e9f69..97d5614470d7 100644 --- a/platform-tests/src/test/java/org/junit/platform/engine/support/descriptor/AbstractTestDescriptorTests.java +++ b/platform-tests/src/test/java/org/junit/platform/engine/support/descriptor/AbstractTestDescriptorTests.java @@ -10,6 +10,7 @@ package org.junit.platform.engine.support.descriptor; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertSame; @@ -34,15 +35,18 @@ class AbstractTestDescriptorTests { private EngineDescriptor engineDescriptor; + private GroupDescriptor group1; + private GroupDescriptor group11; + private LeafDescriptor leaf111; @BeforeEach void initTree() { engineDescriptor = new EngineDescriptor(UniqueId.forEngine("testEngine"), "testEngine"); - var group1 = new GroupDescriptor(UniqueId.root("group", "group1")); + group1 = new GroupDescriptor(UniqueId.root("group", "group1")); engineDescriptor.addChild(group1); var group2 = new GroupDescriptor(UniqueId.root("group", "group2")); engineDescriptor.addChild(group2); - var group11 = new GroupDescriptor(UniqueId.root("group", "group1-1")); + group11 = new GroupDescriptor(UniqueId.root("group", "group1-1")); group1.addChild(group11); group1.addChild(new LeafDescriptor(UniqueId.root("leaf", "leaf1-1"))); @@ -50,7 +54,8 @@ void initTree() { group2.addChild(new LeafDescriptor(UniqueId.root("leaf", "leaf2-1"))); - group11.addChild(new LeafDescriptor(UniqueId.root("leaf", "leaf11-1"))); + leaf111 = new LeafDescriptor(UniqueId.root("leaf", "leaf11-1")); + group11.addChild(leaf111); } @Test @@ -133,6 +138,27 @@ void pruneGroup() { assertFalse(visited.contains(UniqueId.root("group", "group1"))); } + @Test + void getAncestors() { + assertThat(getAncestorsUniqueIds(engineDescriptor)).isEmpty(); + + assertThat(getAncestorsUniqueIds(group1)).containsExactly( // + UniqueId.forEngine("testEngine")); + + assertThat(getAncestorsUniqueIds(group11)).containsExactly( // + UniqueId.root("group", "group1"), // + UniqueId.forEngine("testEngine")); + + assertThat(getAncestorsUniqueIds(leaf111)).containsExactly( // + UniqueId.root("group", "group1-1"), // + UniqueId.root("group", "group1"), // + UniqueId.forEngine("testEngine")); + } + + private List getAncestorsUniqueIds(TestDescriptor descriptor) { + return descriptor.getAncestors().stream().map(TestDescriptor::getUniqueId).toList(); + } + } class GroupDescriptor extends AbstractTestDescriptor { diff --git a/platform-tests/src/test/java/org/junit/platform/launcher/core/LauncherFactoryTests.java b/platform-tests/src/test/java/org/junit/platform/launcher/core/LauncherFactoryTests.java index e769149edfff..d1432692d6d1 100644 --- a/platform-tests/src/test/java/org/junit/platform/launcher/core/LauncherFactoryTests.java +++ b/platform-tests/src/test/java/org/junit/platform/launcher/core/LauncherFactoryTests.java @@ -38,6 +38,7 @@ import org.junit.platform.fakes.TestEngineSpy; import org.junit.platform.launcher.InterceptedTestEngine; import org.junit.platform.launcher.InterceptorInjectedLauncherSessionListener; +import org.junit.platform.launcher.LauncherConstants; import org.junit.platform.launcher.LauncherDiscoveryRequest; import org.junit.platform.launcher.LauncherSessionListener; import org.junit.platform.launcher.TagFilter; @@ -298,7 +299,8 @@ public void execute(ExecutionRequest request) { .addTestEngines(engine) // .build(); var launcher = LauncherFactory.create(config); - var request = request().build(); + var request = request().configurationParameter(LauncherConstants.STACKTRACE_PRUNING_ENABLED_PROPERTY_NAME, + "false").build(); AtomicReference result = new AtomicReference<>(); launcher.execute(request, new TestExecutionListener() { diff --git a/platform-tests/src/test/java/org/junit/platform/reporting/legacy/xml/LegacyXmlReportGeneratingListenerTests.java b/platform-tests/src/test/java/org/junit/platform/reporting/legacy/xml/LegacyXmlReportGeneratingListenerTests.java index 1ae505f8ba54..4e4c14cabece 100644 --- a/platform-tests/src/test/java/org/junit/platform/reporting/legacy/xml/LegacyXmlReportGeneratingListenerTests.java +++ b/platform-tests/src/test/java/org/junit/platform/reporting/legacy/xml/LegacyXmlReportGeneratingListenerTests.java @@ -51,6 +51,7 @@ import org.junit.platform.engine.support.hierarchical.DemoHierarchicalTestDescriptor; import org.junit.platform.engine.support.hierarchical.DemoHierarchicalTestEngine; import org.junit.platform.fakes.TestDescriptorStub; +import org.junit.platform.launcher.LauncherConstants; import org.junit.platform.launcher.TestIdentifier; import org.junit.platform.launcher.TestPlan; import org.opentest4j.AssertionFailedError; @@ -429,7 +430,8 @@ private void executeTests(TestEngine engine, Clock clock) { var reportListener = new LegacyXmlReportGeneratingListener(tempDirectory.toString(), out, clock); var launcher = createLauncher(engine); launcher.registerTestExecutionListeners(reportListener); - launcher.execute(request().selectors(selectUniqueId(UniqueId.forEngine(engine.getId()))).build()); + launcher.execute(request().configurationParameter(LauncherConstants.STACKTRACE_PRUNING_ENABLED_PROPERTY_NAME, + "false").selectors(selectUniqueId(UniqueId.forEngine(engine.getId()))).build()); } private Match readValidXmlFile(Path xmlFile) throws Exception {