Skip to content

Commit d46868a

Browse files
Implement ITR tests skipping for JUnit 4 (#5452)
1 parent 9a8bc0e commit d46868a

File tree

10 files changed

+302
-6
lines changed

10 files changed

+302
-6
lines changed

dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/events/TestEventsHandlerImpl.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,8 +98,10 @@ public void onTestModuleStart() {
9898

9999
@Override
100100
public void onTestModuleFinish(boolean itrTestsSkipped) {
101-
testModule.end(null, itrTestsSkipped);
102-
testModule = null;
101+
if (testModule != null) {
102+
testModule.end(null, itrTestsSkipped);
103+
testModule = null;
104+
}
103105
}
104106

105107
@Override
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package datadog.trace.instrumentation.junit4;
2+
3+
import datadog.trace.api.Config;
4+
import datadog.trace.api.civisibility.config.SkippableTest;
5+
import java.lang.reflect.Method;
6+
import java.util.Set;
7+
import org.junit.runner.Description;
8+
9+
public class ItrFilter {
10+
11+
public static final ItrFilter INSTANCE = new ItrFilter();
12+
13+
private volatile boolean testsSkipped;
14+
15+
private ItrFilter() {}
16+
17+
public boolean skip(Description description) {
18+
SkippableTest test = toSkippableTest(description);
19+
Set<SkippableTest> skippableTests = Config.get().getCiVisibilitySkippableTests();
20+
if (skippableTests.contains(test)) {
21+
testsSkipped = true;
22+
return true;
23+
} else {
24+
return false;
25+
}
26+
}
27+
28+
private SkippableTest toSkippableTest(Description description) {
29+
Method testMethod = JUnit4Utils.getTestMethod(description);
30+
String suite = description.getClassName();
31+
String name = JUnit4Utils.getTestName(description, testMethod);
32+
String parameters = JUnit4Utils.getParameters(description);
33+
return new SkippableTest(suite, name, parameters, null);
34+
}
35+
36+
public boolean testsSkipped() {
37+
return testsSkipped;
38+
}
39+
}

dd-java-agent/instrumentation/junit-4.10/src/main/java/datadog/trace/instrumentation/junit4/JUnit4Instrumentation.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,12 @@ public ElementMatcher<TypeDescription> hierarchyMatcher() {
3434

3535
@Override
3636
public String[] helperClassNames() {
37-
return new String[] {packageName + ".TracingListener", packageName + ".JUnit4Utils"};
37+
return new String[] {
38+
packageName + ".TracingListener",
39+
packageName + ".SkippedByItr",
40+
packageName + ".JUnit4Utils",
41+
packageName + ".ItrFilter",
42+
};
3843
}
3944

4045
@Override
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
package datadog.trace.instrumentation.junit4;
2+
3+
import static datadog.trace.agent.tooling.bytebuddy.matcher.HierarchyMatchers.extendsClass;
4+
import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named;
5+
import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
6+
import static net.bytebuddy.matcher.ElementMatchers.takesArguments;
7+
8+
import com.google.auto.service.AutoService;
9+
import datadog.trace.agent.tooling.Instrumenter;
10+
import datadog.trace.api.Config;
11+
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
12+
import java.util.Set;
13+
import net.bytebuddy.asm.Advice;
14+
import net.bytebuddy.description.type.TypeDescription;
15+
import net.bytebuddy.matcher.ElementMatcher;
16+
import org.junit.Ignore;
17+
import org.junit.rules.RuleChain;
18+
import org.junit.runner.Description;
19+
import org.junit.runner.notification.RunNotifier;
20+
import org.junit.runners.ParentRunner;
21+
22+
@AutoService(Instrumenter.class)
23+
public class JUnit4ItrInstrumentation extends Instrumenter.CiVisibility
24+
implements Instrumenter.ForTypeHierarchy {
25+
26+
public JUnit4ItrInstrumentation() {
27+
super("junit", "junit-4", "junit-4-itr");
28+
}
29+
30+
@Override
31+
public boolean isApplicable(Set<TargetSystem> enabledSystems) {
32+
return super.isApplicable(enabledSystems) && Config.get().isCiVisibilityItrEnabled();
33+
}
34+
35+
@Override
36+
public String hierarchyMarkerType() {
37+
return "org.junit.runners.ParentRunner";
38+
}
39+
40+
@Override
41+
public ElementMatcher<TypeDescription> hierarchyMatcher() {
42+
return extendsClass(named(hierarchyMarkerType()));
43+
}
44+
45+
@Override
46+
public String[] helperClassNames() {
47+
return new String[] {
48+
packageName + ".TracingListener",
49+
packageName + ".SkippedByItr",
50+
packageName + ".JUnit4Utils",
51+
packageName + ".ItrFilter",
52+
};
53+
}
54+
55+
@Override
56+
public void adviceTransformations(AdviceTransformation transformation) {
57+
transformation.applyAdvice(
58+
named("runChild")
59+
.and(takesArguments(2))
60+
.and(takesArgument(1, named("org.junit.runner.notification.RunNotifier"))),
61+
JUnit4ItrInstrumentation.class.getName() + "$JUnit4ItrInstrumentationAdvice");
62+
}
63+
64+
public static class JUnit4ItrInstrumentationAdvice {
65+
@SuppressFBWarnings("NP_BOOLEAN_RETURN_NULL")
66+
@Advice.OnMethodEnter(skipOn = Boolean.class)
67+
public static Boolean runChild(
68+
@Advice.This ParentRunner<?> runner,
69+
@Advice.Argument(0) Object child,
70+
@Advice.Argument(1) RunNotifier notifier) {
71+
Description description = JUnit4Utils.getDescription(runner, child);
72+
if (description == null || !description.isTest()) {
73+
// ITR only skips individual tests
74+
return null;
75+
}
76+
77+
Ignore ignoreAnnotation = description.getAnnotation(Ignore.class);
78+
if (ignoreAnnotation != null) {
79+
// class is ignored, ITR not applicable
80+
return null;
81+
}
82+
83+
if (ItrFilter.INSTANCE.skip(description)) {
84+
Description skippedDescription = JUnit4Utils.getSkippedDescription(description);
85+
notifier.fireTestIgnored(skippedDescription);
86+
return Boolean.FALSE;
87+
} else {
88+
return null;
89+
}
90+
}
91+
92+
// JUnit 4.10 and above
93+
public static void muzzleCheck(final RuleChain ruleChain) {
94+
ruleChain.apply(null, null);
95+
}
96+
}
97+
}

dd-java-agent/instrumentation/junit-4.10/src/main/java/datadog/trace/instrumentation/junit4/JUnit4SuiteEventsInstrumentation.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,12 @@ public ElementMatcher<TypeDescription> hierarchyMatcher() {
3535

3636
@Override
3737
public String[] helperClassNames() {
38-
return new String[] {packageName + ".TracingListener", packageName + ".JUnit4Utils"};
38+
return new String[] {
39+
packageName + ".TracingListener",
40+
packageName + ".SkippedByItr",
41+
packageName + ".JUnit4Utils",
42+
packageName + ".ItrFilter",
43+
};
3944
}
4045

4146
@Override

dd-java-agent/instrumentation/junit-4.10/src/main/java/datadog/trace/instrumentation/junit4/JUnit4Utils.java

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,17 @@
55
import java.lang.reflect.Field;
66
import java.lang.reflect.Method;
77
import java.util.ArrayList;
8+
import java.util.Collection;
89
import java.util.List;
10+
import java.util.regex.Matcher;
911
import java.util.regex.Pattern;
1012
import javax.annotation.Nullable;
1113
import org.junit.Test;
1214
import org.junit.experimental.categories.Category;
1315
import org.junit.runner.Description;
1416
import org.junit.runner.notification.RunListener;
1517
import org.junit.runner.notification.RunNotifier;
18+
import org.junit.runners.ParentRunner;
1619
import org.slf4j.Logger;
1720
import org.slf4j.LoggerFactory;
1821

@@ -25,6 +28,21 @@ public abstract class JUnit4Utils {
2528
// Regex for the final brackets with its content in the test name. E.g. test_name[0] --> [0]
2629
private static final Pattern testNameNormalizerRegex = Pattern.compile("\\[[^\\[]*\\]$");
2730

31+
private static final Pattern METHOD_AND_CLASS_NAME_PATTERN =
32+
Pattern.compile("([\\s\\S]*)\\((.*)\\)");
33+
34+
private static final Method DESCRIBE_CHILD = accesDescribeChildMethod();
35+
36+
private static Method accesDescribeChildMethod() {
37+
try {
38+
Method describeChild = ParentRunner.class.getDeclaredMethod("describeChild", Object.class);
39+
describeChild.setAccessible(true);
40+
return describeChild;
41+
} catch (Exception e) {
42+
return null;
43+
}
44+
}
45+
2846
public static List<RunListener> runListenersFromRunNotifier(final RunNotifier runNotifier) {
2947
try {
3048

@@ -254,4 +272,31 @@ public static List<Method> getTestMethods(final Class<?> testClass) {
254272
}
255273
return testMethods;
256274
}
275+
276+
public static Description getDescription(ParentRunner runner, Object child) {
277+
try {
278+
if (DESCRIBE_CHILD != null) {
279+
return (Description) DESCRIBE_CHILD.invoke(runner, child);
280+
}
281+
} catch (Exception e) {
282+
log.error("Could not describe child: " + child, e);
283+
}
284+
return null;
285+
}
286+
287+
public static Description getSkippedDescription(Description description) {
288+
Collection<Annotation> annotations = description.getAnnotations();
289+
Annotation[] updatedAnnotations = new Annotation[annotations.size() + 1];
290+
int idx = 0;
291+
for (Annotation annotation : annotations) {
292+
updatedAnnotations[idx++] = annotation;
293+
}
294+
updatedAnnotations[idx] = new SkippedByItr();
295+
296+
String displayName = description.getDisplayName();
297+
Matcher matcher = METHOD_AND_CLASS_NAME_PATTERN.matcher(displayName);
298+
String name = matcher.matches() ? matcher.group(1) : getTestName(description, null);
299+
300+
return Description.createTestDescription(description.getTestClass(), name, updatedAnnotations);
301+
}
257302
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package datadog.trace.instrumentation.junit4;
2+
3+
import java.lang.annotation.Annotation;
4+
import org.junit.Ignore;
5+
6+
public final class SkippedByItr implements Ignore {
7+
@Override
8+
public String value() {
9+
return "Skipped by Datadog Intelligent Test Runner";
10+
}
11+
12+
@Override
13+
public Class<? extends Annotation> annotationType() {
14+
return Ignore.class;
15+
}
16+
}

dd-java-agent/instrumentation/junit-4.10/src/main/java/datadog/trace/instrumentation/junit4/TracingListener.java

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import datadog.trace.api.civisibility.InstrumentationBridge;
44
import datadog.trace.api.civisibility.events.TestEventsHandler;
5+
import datadog.trace.util.AgentThreadFactory;
56
import java.lang.reflect.Method;
67
import java.nio.file.Path;
78
import java.nio.file.Paths;
@@ -17,13 +18,34 @@
1718

1819
public class TracingListener extends RunListener {
1920

21+
private static final String GRADLE_TEST_WORKER_ID_SYSTEM_PROP = "org.gradle.test.worker";
22+
2023
private final TestEventsHandler testEventsHandler;
2124

2225
public TracingListener() {
2326
String version = Version.id();
2427
Path currentPath = Paths.get("").toAbsolutePath();
2528
testEventsHandler =
2629
InstrumentationBridge.createTestEventsHandler("junit", "junit4", version, currentPath);
30+
31+
boolean isRunByGradle = System.getProperty(GRADLE_TEST_WORKER_ID_SYSTEM_PROP) != null;
32+
if (isRunByGradle) {
33+
applyTestRunFinishedPatch();
34+
}
35+
}
36+
37+
/**
38+
* Gradle uses its own custom JUnit 4 runner. The runner does not invoke
39+
* org.junit.runner.notification.RunListener#testRunFinished, so we apply a "patch" to invoke it
40+
* before the JVM is shutdown
41+
*/
42+
private void applyTestRunFinishedPatch() {
43+
Thread shutdownHook =
44+
AgentThreadFactory.newAgentThread(
45+
AgentThreadFactory.AgentThread.CI_GRADLE_JUNIT_SHUTDOWN_HOOK,
46+
() -> testRunFinished(null),
47+
false);
48+
Runtime.getRuntime().addShutdownHook(shutdownHook);
2749
}
2850

2951
@Override
@@ -33,7 +55,7 @@ public void testRunStarted(Description description) {
3355

3456
@Override
3557
public void testRunFinished(Result result) {
36-
testEventsHandler.onTestModuleFinish(false);
58+
testEventsHandler.onTestModuleFinish(ItrFilter.INSTANCE.testsSkipped());
3759
}
3860

3961
public void testSuiteStarted(final TestClass junitTestClass) {

0 commit comments

Comments
 (0)