Skip to content

Commit

Permalink
Revise stacktrace pruning
Browse files Browse the repository at this point in the history
  • Loading branch information
juliette-derancourt committed Jun 19, 2023
1 parent 4cadc8a commit 1fd2d4a
Show file tree
Hide file tree
Showing 9 changed files with 78 additions and 110 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ tasks.withType<Test>().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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
}
Expand Down Expand Up @@ -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<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");

Predicate<String> stackTraceElementFilter = ClassNamePatternFilterUtils //
.excludeMatchingClassNames(STACK_TRACE_ELEMENTS_TO_EXCLUDE);

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

}
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,6 @@ void shouldPruneStackTraceByDefault() {

List<StackTraceElement> 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:");
}
Expand All @@ -62,11 +57,6 @@ void shouldPruneStackTraceWhenEnabled() {

List<StackTraceElement> 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:");
}
Expand All @@ -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<StackTraceElement> 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<StackTraceElement> 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<StackTraceElement> 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
Expand All @@ -151,7 +139,8 @@ void shouldPruneStackTracesOfSuppressedExceptions() {

for (Throwable suppressed : throwable.getSuppressed()) {
List<StackTraceElement> stackTrace = Arrays.asList(suppressed.getStackTrace());
assertStackTraceDoesNotContain(stackTrace, "java.util.ArrayList.forEach(ArrayList.java:");
assertStackTraceDoesNotContain(stackTrace,
"jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:");
}
}

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

Expand Down Expand Up @@ -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));
}
}

Expand All @@ -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."));
}
Expand All @@ -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)"));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<TestExecutionResult> result = new AtomicReference<>();
launcher.execute(request, new TestExecutionListener() {
Expand Down
Loading

0 comments on commit 1fd2d4a

Please sign in to comment.