Skip to content

Commit

Permalink
Added test suite level visibility for JUnit 3.8 test cases (#6320)
Browse files Browse the repository at this point in the history
  • Loading branch information
nikita-tkachenko-datadog committed Dec 7, 2023
1 parent 4d0b113 commit 3fe1b2d
Show file tree
Hide file tree
Showing 7 changed files with 322 additions and 109 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package datadog.trace.instrumentation.junit4;

import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named;
import static net.bytebuddy.matcher.ElementMatchers.takesArgument;

import com.google.auto.service.AutoService;
import datadog.trace.agent.tooling.Instrumenter;
import java.util.List;
import junit.framework.TestCase;
import net.bytebuddy.asm.Advice;
import org.junit.rules.RuleChain;
import org.junit.runner.Runner;
import org.junit.runner.notification.RunListener;
import org.junit.runner.notification.RunNotifier;

/** Supports suite started/finished events for {@link TestCase} subclasses. */
@AutoService(Instrumenter.class)
public class JUnit38SuiteEventsInstrumentation extends Instrumenter.CiVisibility
implements Instrumenter.ForSingleType {

public JUnit38SuiteEventsInstrumentation() {
super("ci-visibility", "junit-4", "junit-38");
}

@Override
public String instrumentedType() {
return "org.junit.internal.runners.JUnit38ClassRunner";
}

@Override
public String[] helperClassNames() {
return new String[] {
packageName + ".TestEventsHandlerHolder",
packageName + ".SkippedByItr",
packageName + ".JUnit4Utils",
packageName + ".TracingListener",
packageName + ".JUnit4TracingListener",
};
}

@Override
public void adviceTransformations(AdviceTransformation transformation) {
transformation.applyAdvice(
named("run").and(takesArgument(0, named("org.junit.runner.notification.RunNotifier"))),
JUnit38SuiteEventsInstrumentation.class.getName() + "$JUnit38SuiteEventsAdvice");
}

public static class JUnit38SuiteEventsAdvice {
@Advice.OnMethodEnter(suppress = Throwable.class)
public static void fireSuiteStartedEvent(
@Advice.Argument(0) final RunNotifier runNotifier, @Advice.This final Runner runner) {
final List<RunListener> runListeners = JUnit4Utils.runListenersFromRunNotifier(runNotifier);
if (runListeners == null) {
return;
}

for (final RunListener listener : runListeners) {
TracingListener tracingListener = JUnit4Utils.toTracingListener(listener);
if (tracingListener != null) {
tracingListener.testSuiteStarted(runner.getDescription());
}
}
}

@Advice.OnMethodExit(suppress = Throwable.class)
public static void fireSuiteFinishedEvent(
@Advice.Argument(0) final RunNotifier runNotifier, @Advice.This final Runner runner) {
final List<RunListener> runListeners = JUnit4Utils.runListenersFromRunNotifier(runNotifier);
if (runListeners == null) {
return;
}

for (final RunListener listener : runListeners) {
TracingListener tracingListener = JUnit4Utils.toTracingListener(listener);
if (tracingListener != null) {
tracingListener.testSuiteFinished(runner.getDescription());
}
}
}

// JUnit 4.10 and above
public static void muzzleCheck(final RuleChain ruleChain) {
ruleChain.apply(null, null);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,30 +1,27 @@
package datadog.trace.instrumentation.junit4;

import datadog.trace.api.civisibility.config.SkippableTest;
import datadog.trace.util.MethodHandles;
import datadog.trace.util.Strings;
import java.lang.annotation.Annotation;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.annotation.Nullable;
import junit.framework.TestCase;
import org.junit.Test;
import org.junit.experimental.categories.Category;
import org.junit.runner.Description;
import org.junit.runner.notification.RunListener;
import org.junit.runner.notification.RunNotifier;
import org.junit.runners.ParentRunner;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public abstract class JUnit4Utils {

private static final Logger log = LoggerFactory.getLogger(JUnit4Utils.class);
private static final String SYNCHRONIZED_LISTENER =
"org.junit.runner.notification.SynchronizedRunListener";

Expand All @@ -34,91 +31,37 @@ public abstract class JUnit4Utils {
private static final Pattern METHOD_AND_CLASS_NAME_PATTERN =
Pattern.compile("([\\s\\S]*)\\((.*)\\)");

private static final MethodHandle PARENT_RUNNER_DESCRIBE_CHILD;
private static final MethodHandle RUN_NOTIFIER_LISTENERS;
private static final MethodHandle INNER_SYNCHRONIZED_LISTENER;
private static final MethodHandle DESCRIPTION_UNIQUE_ID;

static {
MethodHandles.Lookup lookup = MethodHandles.lookup();
PARENT_RUNNER_DESCRIBE_CHILD = accessDescribeChildMethodInParentRunner(lookup);
RUN_NOTIFIER_LISTENERS = accessListenersFieldInRunNotifier(lookup);
INNER_SYNCHRONIZED_LISTENER = accessListenerFieldInSynchronizedListener(lookup);
DESCRIPTION_UNIQUE_ID = accessUniqueIdInDescription(lookup);
}

private static MethodHandle accessDescribeChildMethodInParentRunner(MethodHandles.Lookup lookup) {
try {
Method describeChild = ParentRunner.class.getDeclaredMethod("describeChild", Object.class);
describeChild.setAccessible(true);
return lookup.unreflect(describeChild);
} catch (Exception e) {
return null;
}
}

private static MethodHandle accessListenersFieldInRunNotifier(MethodHandles.Lookup lookup) {
try {
Field listeners;
try {
// Since JUnit 4.12, the field is called "listeners"
listeners = RunNotifier.class.getDeclaredField("listeners");
} catch (final NoSuchFieldException e) {
// Before JUnit 4.12, the field is called "fListeners"
listeners = RunNotifier.class.getDeclaredField("fListeners");
}

listeners.setAccessible(true);
return lookup.unreflectGetter(listeners);
} catch (final Throwable e) {
log.debug("Could not get runListeners for JUnit4Advice", e);
return null;
private static final MethodHandles METHOD_HANDLES =
new MethodHandles(ParentRunner.class.getClassLoader());
private static final MethodHandle PARENT_RUNNER_DESCRIBE_CHILD =
METHOD_HANDLES.method(ParentRunner.class, "describeChild", Object.class);
private static final MethodHandle RUN_NOTIFIER_LISTENERS = accessListenersFieldInRunNotifier();
private static final MethodHandle INNER_SYNCHRONIZED_LISTENER =
accessListenerFieldInSynchronizedListener();
private static final MethodHandle DESCRIPTION_UNIQUE_ID =
METHOD_HANDLES.privateFieldGetter(Description.class, "fUniqueId");

private static MethodHandle accessListenersFieldInRunNotifier() {
MethodHandle listeners = METHOD_HANDLES.privateFieldGetter(RunNotifier.class, "listeners");
if (listeners != null) {
return listeners;
}
// Before JUnit 4.12, the field is called "fListeners"
return METHOD_HANDLES.privateFieldGetter(RunNotifier.class, "fListeners");
}

private static MethodHandle accessListenerFieldInSynchronizedListener(
MethodHandles.Lookup lookup) {
ClassLoader classLoader = RunListener.class.getClassLoader();
MethodHandle handle = accessListenerFieldInSynchronizedListener(lookup, classLoader);
private static MethodHandle accessListenerFieldInSynchronizedListener() {
MethodHandle handle = METHOD_HANDLES.privateFieldGetter(SYNCHRONIZED_LISTENER, "listener");
if (handle != null) {
return handle;
} else {
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
return accessListenerFieldInSynchronizedListener(lookup, contextClassLoader);
}
}

private static MethodHandle accessListenerFieldInSynchronizedListener(
MethodHandles.Lookup lookup, ClassLoader classLoader) {
try {
Class<?> synchronizedListenerClass = classLoader.loadClass(SYNCHRONIZED_LISTENER);
final Field innerListenerField = synchronizedListenerClass.getDeclaredField("listener");
innerListenerField.setAccessible(true);
return lookup.unreflectGetter(innerListenerField);
} catch (Exception e) {
return null;
}
}

private static MethodHandle accessUniqueIdInDescription(MethodHandles.Lookup lookup) {
try {
final Field uniqueIdField = Description.class.getDeclaredField("fUniqueId");
uniqueIdField.setAccessible(true);
return lookup.unreflectGetter(uniqueIdField);
} catch (Throwable throwable) {
return null;
}
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
return new MethodHandles(contextClassLoader)
.privateFieldGetter(SYNCHRONIZED_LISTENER, "listeners");
}

public static List<RunListener> runListenersFromRunNotifier(final RunNotifier runNotifier) {
try {
if (RUN_NOTIFIER_LISTENERS != null) {
return (List<RunListener>) RUN_NOTIFIER_LISTENERS.invoke(runNotifier);
}
} catch (final Throwable e) {
log.debug("Could not get runListeners for JUnit4Advice", e);
}
return null;
return METHOD_HANDLES.invoke(RUN_NOTIFIER_LISTENERS, runNotifier);
}

public static TracingListener toTracingListener(final RunListener listener) {
Expand All @@ -128,15 +71,9 @@ public static TracingListener toTracingListener(final RunListener listener) {

// Since JUnit 4.12, the RunListener are wrapped by a SynchronizedRunListener object.
if (SYNCHRONIZED_LISTENER.equals(listener.getClass().getName())) {
try {
if (INNER_SYNCHRONIZED_LISTENER != null) {
Object innerListener = INNER_SYNCHRONIZED_LISTENER.invoke(listener);
if (innerListener instanceof TracingListener) {
return (TracingListener) innerListener;
}
}
} catch (final Throwable e) {
log.debug("Could not get inner listener from SynchronizedRunListener", e);
RunListener innerListener = METHOD_HANDLES.invoke(INNER_SYNCHRONIZED_LISTENER, listener);
if (innerListener instanceof TracingListener) {
return (TracingListener) innerListener;
}
}
return null;
Expand Down Expand Up @@ -301,18 +238,11 @@ public static boolean isSuiteContainingChildren(final Description description) {
return true;
}
}
return false;
return TestCase.class.isAssignableFrom(testClass);
}

public static Object getUniqueId(final Description description) {
try {
if (DESCRIPTION_UNIQUE_ID != null) {
return DESCRIPTION_UNIQUE_ID.invoke(description);
}
} catch (Throwable e) {
log.error("Could not get unique ID from descriptions: " + description, e);
}
return null;
return METHOD_HANDLES.invoke(DESCRIPTION_UNIQUE_ID, description);
}

public static String getSuiteName(final Class<?> testClass, final Description description) {
Expand All @@ -332,14 +262,7 @@ public static List<Method> getTestMethods(final Class<?> testClass) {
}

public static Description getDescription(ParentRunner<?> runner, Object child) {
try {
if (PARENT_RUNNER_DESCRIBE_CHILD != null) {
return (Description) PARENT_RUNNER_DESCRIBE_CHILD.invokeWithArguments(runner, child);
}
} catch (Throwable e) {
log.error("Could not describe child: " + child, e);
}
return null;
return METHOD_HANDLES.invoke(PARENT_RUNNER_DESCRIBE_CHILD, runner, child);
}

public static Description getSkippedDescription(Description description) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import org.example.TestSkipped
import org.example.TestSkippedClass
import org.example.TestSucceed
import org.example.TestSucceedAndSkipped
import org.example.TestSucceedLegacy
import org.example.TestSucceedSuite
import org.example.TestSucceedUnskippable
import org.example.TestSucceedUnskippableSuite
Expand Down Expand Up @@ -66,6 +67,7 @@ class JUnit4Test extends CiVisibilityInstrumentationTest {
"test-itr-unskippable" | [TestSucceedUnskippable] | 2 | [new SkippableTest("org.example.TestSucceedUnskippable", "test_succeed", null, null)]
"test-itr-unskippable-suite" | [TestSucceedUnskippableSuite] | 2 | [new SkippableTest("org.example.TestSucceedUnskippableSuite", "test_succeed", null, null)]
"test-itr-unskippable-not-skipped" | [TestSucceedUnskippable] | 2 | []
"test-legacy" | [TestSucceedLegacy] | 2 | []
}

private void runTests(Collection<Class<?>> tests) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package org.example;

import junit.framework.TestCase;

public class TestSucceedLegacy extends TestCase {

public void test_succeed() {
assertTrue(true);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[ ]
Loading

0 comments on commit 3fe1b2d

Please sign in to comment.