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..54f31a808833 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,8 @@ public final class ExceptionUtils { private static final String JUNIT_PLATFORM_LAUNCHER_PACKAGE_PREFIX = "org.junit.platform.launcher."; + private static final String STACK_TRACE_ELEMENTS_TO_EXCLUDE = "org.junit.*,jdk.internal.reflect.*,sun.reflect.*"; + private ExceptionUtils() { /* no-op */ } @@ -103,29 +105,38 @@ public static String readStackTrace(Throwable throwable) { * * @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"); + + Predicate stackTraceElementFilter = ClassNamePatternFilterUtils // + .excludeMatchingClassNames(STACK_TRACE_ELEMENTS_TO_EXCLUDE); 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)) { prunedStackTrace.clear(); } else if (stackTraceElementFilter.test(className)) { prunedStackTrace.add(element); } + else if (testClassNames.contains(className)) { + // Include all elements called by the test + prunedStackTrace.addAll(stackTrace.subList(i, stackTrace.size())); + break; + } } Collections.reverse(prunedStackTrace); 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..d2c04eb139db 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,58 @@ package org.junit.platform.launcher.core; -import java.util.Arrays; import java.util.List; -import java.util.function.Predicate; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.Stream; -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 Stream.of(testDescriptor.getSource(), testDescriptor.getParent().flatMap(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; + } + }) // + .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..cc849fa215ed 100644 --- a/platform-tests/src/test/java/org/junit/platform/StackTracePruningTests.java +++ b/platform-tests/src/test/java/org/junit/platform/StackTracePruningTests.java @@ -44,11 +44,6 @@ void shouldPruneStackTraceByDefault() { 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:"); } @@ -62,11 +57,6 @@ void shouldPruneStackTraceWhenEnabled() { 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:"); } @@ -82,62 +72,60 @@ void shouldNotPruneStackTraceWhenDisabled() { 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")) // .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(StackTracePruningTestCase.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(StackTracePruningTestCase.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$StackTracePruningTestCase.failingAssertion(StackTracePruningTests.java:\\E.+ + >>>> + """); } @Test @@ -151,7 +139,8 @@ void shouldPruneStackTracesOfSuppressedExceptions() { 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:"); } } 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..d779409862a2 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.Collections; 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,15 +67,16 @@ void readStackTraceForLocalJUnitException() { } } - @Test - void pruneStackTraceOfCallsFromSpecificPackage() { + @ParameterizedTest + @ValueSource(strings = { "org.junit.", "jdk.internal.reflect.", "sun.reflect." }) + void pruneStackTraceOfCallsFromSpecificPackage(String shouldBePruned) { try { throw new JUnitException("expected"); } catch (JUnitException e) { - pruneStackTrace(e, element -> !element.startsWith("org.junit.")); + pruneStackTrace(e, Collections.emptyList()); assertThat(e.getStackTrace()) // - .noneMatch(element -> element.toString().contains("org.junit.")); + .noneMatch(element -> element.toString().contains(shouldBePruned)); } } @@ -82,7 +86,7 @@ void pruneStackTraceOfAllLauncherCalls() { throw new JUnitException("expected"); } catch (JUnitException e) { - pruneStackTrace(e, element -> true); + pruneStackTrace(e, Collections.emptyList()); assertThat(e.getStackTrace()) // .noneMatch(element -> element.toString().contains("org.junit.platform.launcher.")); } @@ -98,7 +102,7 @@ void pruneStackTraceOfEverythingPriorToFirstLauncherCall() { stackTrace[stackTrace.length - 1] = new StackTraceElement("org.example.Class", "method", "file", 123); e.setStackTrace(stackTrace); - pruneStackTrace(e, element -> true); + pruneStackTrace(e, Collections.emptyList()); assertThat(e.getStackTrace()) // .noneMatch(element -> element.toString().contains("org.example.Class.method(file:123)")); } 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 {