Skip to content

Commit c25d437

Browse files
Update AgentTestRunner to use JUnit5
1 parent 023e525 commit c25d437

File tree

52 files changed

+406
-438
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

52 files changed

+406
-438
lines changed

dd-java-agent/agent-ci-visibility/build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ dependencies {
5353
testImplementation libs.scala
5454
testImplementation libs.kotlin
5555

56-
testFixturesApi project(':dd-java-agent:testing')
56+
testFixturesApi project(':dd-java-agent:instrumentation-testing')
5757
testFixturesApi project(':utils:test-utils')
5858

5959
testFixturesApi group: 'org.skyscreamer', name: 'jsonassert', version: '1.5.1'
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
apply from: "$rootDir/gradle/java.gradle"
2+
3+
dependencies {
4+
api libs.bytebuddy
5+
api libs.bytebuddyagent
6+
api libs.slf4j
7+
api libs.bundles.spock
8+
api libs.bundles.test.logging
9+
api libs.guava
10+
11+
api project(':dd-java-agent:testing')
12+
13+
implementation project(':dd-java-agent:agent-debugger')
14+
15+
implementation 'org.junit.platform:junit-platform-runner:1.9.0'
16+
}

dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/AgentTestRunner.groovy renamed to dd-java-agent/instrumentation-testing/src/main/groovy/datadog/trace/agent/test/AgentTestRunner.groovy

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ import net.bytebuddy.dynamic.DynamicType
6767
import net.bytebuddy.utility.JavaModule
6868
import okhttp3.HttpUrl
6969
import okhttp3.OkHttpClient
70-
import org.junit.runner.RunWith
70+
import org.junit.jupiter.api.extension.ExtendWith
7171
import org.slf4j.LoggerFactory
7272
import org.spockframework.mock.MockUtil
7373
import org.spockframework.mock.runtime.MockInvocation
@@ -107,7 +107,7 @@ import static datadog.trace.util.AgentThreadFactory.AgentThread.TASK_SCHEDULER
107107
*/
108108
// CodeNarc incorrectly thinks ".class" is unnecessary in @RunWith
109109
@SuppressWarnings('UnnecessaryDotClass')
110-
@RunWith(SpockRunner.class)
110+
@ExtendWith(SpockExtension.class)
111111
abstract class AgentTestRunner extends DDSpecification implements AgentBuilder.Listener {
112112
private static final long TIMEOUT_MILLIS = TimeUnit.SECONDS.toMillis(20)
113113

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
package datadog.trace.agent.test;
2+
3+
import static com.google.common.base.StandardSystemProperty.JAVA_CLASS_PATH;
4+
import static com.google.common.base.StandardSystemProperty.PATH_SEPARATOR;
5+
6+
import com.google.common.base.Splitter;
7+
import com.google.common.collect.ImmutableList;
8+
import com.google.common.reflect.ClassPath;
9+
import datadog.trace.agent.test.utils.ClasspathUtils;
10+
import datadog.trace.bootstrap.BootstrapProxy;
11+
import de.thetaphi.forbiddenapis.SuppressForbidden;
12+
import java.io.File;
13+
import java.io.IOException;
14+
import java.lang.reflect.Field;
15+
import java.lang.reflect.Method;
16+
import java.net.MalformedURLException;
17+
import java.net.URL;
18+
import java.net.URLClassLoader;
19+
import java.util.Arrays;
20+
import java.util.HashSet;
21+
import java.util.Set;
22+
import java.util.TreeSet;
23+
import java.util.jar.JarFile;
24+
import net.bytebuddy.agent.ByteBuddyAgent;
25+
import org.junit.platform.launcher.LauncherSession;
26+
import org.junit.platform.launcher.LauncherSessionListener;
27+
28+
public class BootstrapClasspathSetup implements LauncherSessionListener {
29+
30+
@Override
31+
public void launcherSessionOpened(LauncherSession session) {
32+
// do nothing
33+
}
34+
35+
/**
36+
* An exact copy of {@link datadog.trace.bootstrap.Constants#BOOTSTRAP_PACKAGE_PREFIXES}.
37+
*
38+
* <p>This list is needed to initialize the bootstrap classpath because Utils' static initializer
39+
* references bootstrap classes (e.g. DatadogClassLoader).
40+
*/
41+
public static final String[] BOOTSTRAP_PACKAGE_PREFIXES_COPY = {
42+
"datadog.slf4j",
43+
"datadog.context",
44+
"datadog.environment",
45+
"datadog.json",
46+
"datadog.yaml",
47+
"datadog.appsec.api",
48+
"datadog.trace.api",
49+
"datadog.trace.bootstrap",
50+
"datadog.trace.context",
51+
"datadog.trace.instrumentation.api",
52+
"datadog.trace.logging",
53+
"datadog.trace.util",
54+
};
55+
56+
private static final String[] TEST_EXCLUDED_BOOTSTRAP_PACKAGE_PREFIXES = {
57+
"ch.qos.logback.classic.servlet", // this draws javax.servlet deps that are not needed
58+
};
59+
60+
private static final String[] TEST_BOOTSTRAP_PREFIXES;
61+
62+
public static final ClassPath TEST_CLASSPATH;
63+
64+
static {
65+
ByteBuddyAgent.install();
66+
final String[] testBS = {
67+
"org.slf4j", "ch.qos.logback",
68+
};
69+
70+
TEST_BOOTSTRAP_PREFIXES =
71+
Arrays.copyOf(
72+
BOOTSTRAP_PACKAGE_PREFIXES_COPY,
73+
BOOTSTRAP_PACKAGE_PREFIXES_COPY.length + testBS.length);
74+
for (int i = 0; i < testBS.length; ++i) {
75+
TEST_BOOTSTRAP_PREFIXES[i + BOOTSTRAP_PACKAGE_PREFIXES_COPY.length] = testBS[i];
76+
}
77+
78+
TEST_CLASSPATH = computeTestClasspath();
79+
80+
setupBootstrapClasspath();
81+
}
82+
83+
private static ClassPath computeTestClasspath() {
84+
ClassLoader testClassLoader = AgentTestRunner.class.getClassLoader();
85+
if (!(testClassLoader instanceof URLClassLoader)) {
86+
// java9's system loader does not extend URLClassLoader
87+
// which breaks Guava ClassPath lookup
88+
testClassLoader = buildJavaClassPathClassLoader();
89+
}
90+
try {
91+
return ClassPath.from(testClassLoader);
92+
} catch (final IOException e) {
93+
throw new RuntimeException(e);
94+
}
95+
}
96+
97+
/**
98+
* Parse JVM classpath and return ClassLoader containing all classpath entries. Inspired by Guava.
99+
*/
100+
@SuppressForbidden
101+
private static ClassLoader buildJavaClassPathClassLoader() {
102+
final ImmutableList.Builder<URL> urls = ImmutableList.builder();
103+
for (final String entry : Splitter.on(PATH_SEPARATOR.value()).split(JAVA_CLASS_PATH.value())) {
104+
try {
105+
try {
106+
urls.add(new File(entry).toURI().toURL());
107+
} catch (final SecurityException e) { // File.toURI checks to see if the file is a directory
108+
urls.add(new URL("file", null, new File(entry).getAbsolutePath()));
109+
}
110+
} catch (final MalformedURLException e) {
111+
System.err.println(
112+
String.format(
113+
"Error injecting bootstrap jar: Malformed classpath entry: %s. %s", entry, e));
114+
}
115+
}
116+
return new URLClassLoader(urls.build().toArray(new URL[0]), null);
117+
}
118+
119+
public static void assertNoBootstrapClassesInTestClass(final Class<?> testClass) {
120+
for (final Field field : testClass.getDeclaredFields()) {
121+
assertNotBootstrapClass(testClass, field.getType());
122+
}
123+
for (final Method method : testClass.getDeclaredMethods()) {
124+
assertNotBootstrapClass(testClass, method.getReturnType());
125+
for (final Class<?> paramType : method.getParameterTypes()) {
126+
assertNotBootstrapClass(testClass, paramType);
127+
}
128+
}
129+
}
130+
131+
private static void assertNotBootstrapClass(final Class<?> testClass, final Class<?> clazz) {
132+
if (!clazz.isPrimitive() && isBootstrapClass(clazz.getName())) {
133+
throw new IllegalStateException(
134+
testClass.getName()
135+
+ ": Bootstrap classes are not allowed in test class field or method signatures. Offending class: "
136+
+ clazz.getName());
137+
}
138+
}
139+
140+
private static boolean isBootstrapClass(final String name) {
141+
for (String prefix : TEST_BOOTSTRAP_PREFIXES) {
142+
if (name.startsWith(prefix)) {
143+
for (String excluded : TEST_EXCLUDED_BOOTSTRAP_PACKAGE_PREFIXES) {
144+
if (name.startsWith(excluded)) {
145+
return false;
146+
}
147+
}
148+
return true;
149+
}
150+
}
151+
return false;
152+
}
153+
154+
private static void setupBootstrapClasspath() {
155+
// Ensure there weren't any bootstrap classes loaded prematurely.
156+
Set<String> prematureBootstrapClasses = new TreeSet<>();
157+
for (Class clazz : ByteBuddyAgent.getInstrumentation().getAllLoadedClasses()) {
158+
if (isBootstrapClass(clazz.getName())
159+
&& clazz.getClassLoader() != null
160+
&& !clazz.getName().equals("datadog.trace.api.DisableTestTrace")
161+
&& !clazz.getName().startsWith("org.slf4j")) {
162+
prematureBootstrapClasses.add(clazz.getName());
163+
}
164+
}
165+
if (!prematureBootstrapClasses.isEmpty()) {
166+
throw new AssertionError(
167+
prematureBootstrapClasses.size()
168+
+ " classes were loaded before bootstrap classpath was initialized: "
169+
+ prematureBootstrapClasses);
170+
}
171+
try {
172+
final File bootstrapJar = createBootstrapJar();
173+
ByteBuddyAgent.getInstrumentation()
174+
.appendToBootstrapClassLoaderSearch(new JarFile(bootstrapJar));
175+
// Utils cannot be referenced before this line, as its static initializers load bootstrap
176+
// classes (for example, the bootstrap proxy).
177+
BootstrapProxy.addBootstrapResource(bootstrapJar.toURI().toURL());
178+
} catch (final IOException e) {
179+
throw new RuntimeException(e);
180+
}
181+
}
182+
183+
private static File createBootstrapJar() throws IOException {
184+
Set<String> bootstrapClasses = new HashSet<>();
185+
for (ClassPath.ClassInfo info : TEST_CLASSPATH.getAllClasses()) {
186+
if (isBootstrapClass(info.getName())) {
187+
bootstrapClasses.add(info.getResourceName());
188+
}
189+
}
190+
URL jar =
191+
ClasspathUtils.createJarWithClasses(
192+
SpockExtension.class.getClassLoader(), bootstrapClasses.toArray(new String[0]));
193+
return new File(jar.getFile());
194+
}
195+
}
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
package datadog.trace.agent.test;
2+
3+
import java.io.IOException;
4+
import java.util.List;
5+
import net.bytebuddy.dynamic.ClassFileLocator;
6+
import org.junit.jupiter.api.extension.AfterAllCallback;
7+
import org.junit.jupiter.api.extension.AfterEachCallback;
8+
import org.junit.jupiter.api.extension.BeforeAllCallback;
9+
import org.junit.jupiter.api.extension.BeforeEachCallback;
10+
import org.junit.jupiter.api.extension.ExtensionContext;
11+
import org.junit.jupiter.api.extension.TestExecutionExceptionHandler;
12+
import org.spockframework.mock.IMockInvocation;
13+
import org.spockframework.mock.TooManyInvocationsError;
14+
15+
public final class SpockExtension
16+
implements BeforeAllCallback,
17+
AfterAllCallback,
18+
BeforeEachCallback,
19+
AfterEachCallback,
20+
TestExecutionExceptionHandler {
21+
22+
private static final ExtensionContext.Namespace NAMESPACE =
23+
ExtensionContext.Namespace.create("dd", "spock", "loader");
24+
25+
@Override
26+
public void beforeAll(ExtensionContext ctx) {
27+
Class<?> testClass = ctx.getRequiredTestClass();
28+
BootstrapClasspathSetup.assertNoBootstrapClassesInTestClass(testClass);
29+
30+
InstrumentationClassLoader custom =
31+
new InstrumentationClassLoader(testClass.getClassLoader(), testClass.getName());
32+
ctx.getStore(NAMESPACE).put("loader", custom);
33+
}
34+
35+
@Override
36+
public void afterAll(ExtensionContext ctx) {
37+
// nothing to clean‑up – garbage collector will take care of loader
38+
}
39+
40+
@Override
41+
public void beforeEach(ExtensionContext ctx) {
42+
ClassLoader custom = ctx.getStore(NAMESPACE).get("loader", ClassLoader.class);
43+
ctx.getStore(NAMESPACE).put("prevTCCL", Thread.currentThread().getContextClassLoader());
44+
Thread.currentThread().setContextClassLoader(custom);
45+
}
46+
47+
@Override
48+
public void afterEach(ExtensionContext ctx) {
49+
ClassLoader prev = ctx.getStore(NAMESPACE).remove("prevTCCL", ClassLoader.class);
50+
if (prev != null) {
51+
Thread.currentThread().setContextClassLoader(prev);
52+
}
53+
}
54+
55+
@Override
56+
public void handleTestExecutionException(ExtensionContext ctx, Throwable ex) throws Throwable {
57+
if (ex instanceof TooManyInvocationsError) {
58+
fixTooManyInvocationsError((TooManyInvocationsError) ex);
59+
throw ex; // re‑throw so JUnit still marks the test as failed.
60+
}
61+
throw ex;
62+
}
63+
64+
static void fixTooManyInvocationsError(final TooManyInvocationsError error) {
65+
final List<IMockInvocation> accepted = error.getAcceptedInvocations();
66+
for (final IMockInvocation invocation : accepted) {
67+
try {
68+
invocation.toString();
69+
} catch (final Throwable t) {
70+
final List<Object> args = invocation.getArguments();
71+
for (int i = 0; i < args.size(); i++) {
72+
final Object arg = args.get(i);
73+
if (arg instanceof AssertionError) {
74+
args.set(
75+
i,
76+
new AssertionError(
77+
"'"
78+
+ arg.getClass().getName()
79+
+ "' hidden due to '"
80+
+ t.getClass().getName()
81+
+ "'",
82+
t));
83+
}
84+
}
85+
}
86+
}
87+
}
88+
89+
/**
90+
* Class‑loader that <em>shadows</em> the test class, so that any re‑definitions via ByteBuddy
91+
* operate on a loader‑private copy instead of the one used by the build toolʼs class‑path
92+
* scanning. Delegation order = child‑first for the testʼs own package, parent‑first otherwise.
93+
*/
94+
private static class InstrumentationClassLoader extends ClassLoader {
95+
private final ClassLoader parent;
96+
private final String shadowPrefix;
97+
98+
InstrumentationClassLoader(ClassLoader parent, String shadowPrefix) {
99+
super(parent);
100+
this.parent = parent;
101+
this.shadowPrefix = shadowPrefix;
102+
}
103+
104+
/** Inject the bytes of {@code clazz} into <b>this</b> loader, producing a shadow copy. */
105+
Class<?> shadow(Class<?> clazz) throws IOException {
106+
Class<?> loaded = findLoadedClass(clazz.getName());
107+
if (loaded != null && loaded.getClassLoader() == this) {
108+
return loaded;
109+
}
110+
byte[] classBytes =
111+
ClassFileLocator.ForClassLoader.of(clazz.getClassLoader())
112+
.locate(clazz.getName())
113+
.resolve();
114+
return defineClass(clazz.getName(), classBytes, 0, classBytes.length);
115+
}
116+
117+
@Override
118+
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
119+
synchronized (getClassLoadingLock(name)) {
120+
Class<?> c = findLoadedClass(name);
121+
if (c != null) {
122+
return c;
123+
}
124+
if (name.startsWith(shadowPrefix)) {
125+
try {
126+
Class<?> shadowed = shadow(parent.loadClass(name));
127+
if (resolve) {
128+
resolveClass(shadowed);
129+
}
130+
return shadowed;
131+
} catch (Exception ignored) {
132+
// fall‑back to parent below
133+
}
134+
}
135+
return super.loadClass(name, resolve);
136+
}
137+
}
138+
}
139+
}

dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/base/HttpClientTest.groovy renamed to dd-java-agent/instrumentation-testing/src/main/groovy/datadog/trace/agent/test/base/HttpClientTest.groovy

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ abstract class HttpClientTest extends VersionedNamingTestBase {
106106

107107
@Override
108108
protected void configurePreAgent() {
109-
super.configurePreAgent()
109+
Object.configurePreAgent()
110110
// we inject this config because it's statically assigned and we cannot inject this at test level without forking
111111
// not starting with "/" made full url (http://..) matching but not the path portion (because starting with /)
112112
// this settings should not affect test results

dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/base/WithHttpServer.groovy renamed to dd-java-agent/instrumentation-testing/src/main/groovy/datadog/trace/agent/test/base/WithHttpServer.groovy

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ abstract class WithHttpServer<SERVER> extends VersionedNamingTestBase {
8383
&& throwable.message.startsWith("Illegal access: this web application instance has been stopped already. Could not load")) {
8484
println "Ignoring class load error at shutdown"
8585
} else {
86-
super.onError(typeName, classLoader, module, loaded, throwable)
86+
Object.onError(typeName, classLoader, module, loaded, throwable)
8787
}
8888
}
8989

0 commit comments

Comments
 (0)