Repository: junit-team/junit-framework (Java)

Issue:
Revise stack trace pruning
## Context

Stack trace pruning was introduced in #3247.
We want this feature to be disabled for tests inside this repository (otherwise all stack traces would be almost empty), and it is when the tests are launched via gradle (due to [this setting](https://github.com/junit-team/junit5/blob/69e74953eacb174842a65a2d7bf67c4d633de6e9/gradle/plugins/common/src/main/kotlin/junitbuild.testing-conventions.gradle.kts#L44)). But when launched in a different way, this setting have no impact and the feature is therefore enabled when it shouldn’t.

## Solution

A potential solution could be to not prune stack traces of tests specifically located inside the `org.junit` package, even when the feature is enabled.

## Related

- https://github.com/junit-team/junit5/pull/3277#discussion_r1181233970

## Deliverables

- [x] Collect `MethodSource` and `ClassSource` instances from the current test descriptor and its parents, find the bottom-most stackframe with one of these classes, and include all stackframe upward from there
- [x] Don't prune everything from `java.*` and `jdk.*`, just the reflection packages from both but only below the test method
- [x] Remove package pattern configuration parameter introduced in M1



Fix (patch):
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/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<String> 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.
 	 *
 	 * <p>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<String> stackTraceElementFilter) {
+	public static void pruneStackTrace(Throwable throwable, List<String> 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<StackTraceElement> stackTrace = Arrays.asList(throwable.getStackTrace());
 		List<StackTraceElement> 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<? extends TestDescriptor> getChildren();
 
+	/**
+	 * Get the immutable set of all <em>ancestors</em> of this descriptor.
+	 *
+	 * <p>An <em>ancestors</em> 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<? extends TestDescriptor> getAncestors() {
+		if (!getParent().isPresent()) {
+			return Collections.emptySet();
+		}
+		TestDescriptor parent = getParent().get();
+		Set<TestDescriptor> ancestors = new LinkedHashSet<>();
+		ancestors.add(parent);
+		ancestors.addAll(parent.getAncestors());
+		return Collections.unmodifiableSet(ancestors);
+	}
+
 	/**
 	 * Get the immutable set of all <em>descendants</em> 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.
-	 *
-	 * <h4>Pattern Matching Syntax</h4>
-	 *
-	 * <p>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 (<em>FQCN</em>) 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.
-	 *
-	 * <h4>Examples</h4>
-	 *
-	 * <ul>
-	 * <li>{@code *}: remove all elements.
-	 * <li>{@code org.junit.*}: remove every element with the {@code org.junit}
-	 * base package and any of its subpackages.
-	 * <li>{@code *.MyClass}: remove every element whose simple class name is
-	 * exactly {@code MyClass}.
-	 * <li>{@code *System*, *Dev*}: exclude every element whose FQCN contains
-	 * {@code System} or {@code Dev}.
-	 * <li>{@code org.example.MyClass, org.example.TheirClass}: remove
-	 * elements whose FQCN is exactly {@code org.example.MyClass} or
-	 * {@code org.example.TheirClass}.
-	 * </ul>
-	 *
-	 * @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<String> ALWAYS_INCLUDED_STACK_TRACE_ELEMENTS = Arrays.asList( //
-		"org.junit.jupiter.api.Assertions", //
-		"org.junit.jupiter.api.Assumptions" //
-	);
-
-	private final Predicate<String> 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<String> 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<String> 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());
 	}
 
 }