Merge pull request #527 from DataDog/ark/fix-agent-test-runner

Fix AgentTestRunner bootstrapping
This commit is contained in:
Andrew Kent 2018-10-11 20:50:30 -07:00 committed by GitHub
commit 64aed18f76
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 150 additions and 62 deletions

View File

@ -6,7 +6,11 @@ import java.lang.reflect.Method;
import java.net.URL;
public class Utils {
/* packages which will be loaded on the bootstrap classloader*/
/**
* packages which will be loaded on the bootstrap classloader
*
* <p>Updates should be mirrored in TestUtils#BOOTSTRAP_PACKAGE_PREFIXES_COPY
*/
public static final String[] BOOTSTRAP_PACKAGE_PREFIXES = {
"io.opentracing",
"datadog.slf4j",
@ -14,6 +18,7 @@ public class Utils {
"datadog.trace.api",
"datadog.trace.context"
};
public static final String[] AGENT_PACKAGE_PREFIXES = {
"datadog.trace.common",
"datadog.trace.agent",

View File

@ -1,19 +1,10 @@
package datadog.trace.agent.test;
import static com.google.common.base.StandardSystemProperty.JAVA_CLASS_PATH;
import static com.google.common.base.StandardSystemProperty.PATH_SEPARATOR;
import static datadog.trace.agent.tooling.Utils.BOOTSTRAP_PACKAGE_PREFIXES;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableList;
import com.google.common.reflect.ClassPath;
import datadog.trace.agent.tooling.Utils;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Field;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
@ -32,6 +23,20 @@ import org.spockframework.runtime.Sputnik;
* </ul>
*/
public class SpockRunner extends Sputnik {
/**
* An exact copy of Utils#BOOSTRAP_PACKAGE_PREFIXES.
*
* <p>This list is needed to initialize the bootstrap classpath because Utils' static initializer
* references bootstrap classes (e.g. DatadogClassLoader).
*/
public static final String[] BOOTSTRAP_PACKAGE_PREFIXES_COPY = {
"io.opentracing",
"datadog.slf4j",
"datadog.trace.bootstrap",
"datadog.trace.api",
"datadog.trace.context"
};
private static final String[] TEST_BOOTSTRAP_PREFIXES;
static {
@ -47,9 +52,10 @@ public class SpockRunner extends Sputnik {
};
TEST_BOOTSTRAP_PREFIXES =
Arrays.copyOf(
BOOTSTRAP_PACKAGE_PREFIXES, BOOTSTRAP_PACKAGE_PREFIXES.length + testBS.length);
BOOTSTRAP_PACKAGE_PREFIXES_COPY,
BOOTSTRAP_PACKAGE_PREFIXES_COPY.length + testBS.length);
for (int i = 0; i < testBS.length; ++i) {
TEST_BOOTSTRAP_PREFIXES[i + BOOTSTRAP_PACKAGE_PREFIXES.length] = testBS[i];
TEST_BOOTSTRAP_PREFIXES[i + BOOTSTRAP_PACKAGE_PREFIXES_COPY.length] = testBS[i];
}
setupBootstrapClasspath();
@ -61,6 +67,7 @@ public class SpockRunner extends Sputnik {
throws InitializationError, NoSuchFieldException, SecurityException, IllegalArgumentException,
IllegalAccessException {
super(shadowTestClass(clazz));
assertNoBootstrapClassesInTestClass(clazz);
// access the classloader created in shadowTestClass above
Field clazzField = Sputnik.class.getDeclaredField("clazz");
try {
@ -72,6 +79,37 @@ public class SpockRunner extends Sputnik {
}
}
private static void assertNoBootstrapClassesInTestClass(Class<?> testClass) {
for (final Field field : testClass.getDeclaredFields()) {
assertNotBootstrapClass(testClass, field.getType());
}
for (final Method method : testClass.getDeclaredMethods()) {
assertNotBootstrapClass(testClass, method.getReturnType());
for (final Class paramType : method.getParameterTypes()) {
assertNotBootstrapClass(testClass, paramType);
}
}
}
private static void assertNotBootstrapClass(Class<?> testClass, Class<?> clazz) {
if ((!clazz.isPrimitive()) && isBootstrapClass(clazz.getName())) {
throw new IllegalStateException(
testClass.getName()
+ ": Bootstrap classes are not allowed in test class field or method signatures. Offending class: "
+ clazz.getName());
}
}
private static boolean isBootstrapClass(String className) {
for (int i = 0; i < TEST_BOOTSTRAP_PREFIXES.length; ++i) {
if (className.startsWith(TEST_BOOTSTRAP_PREFIXES[i])) {
return true;
}
}
return false;
}
// Shadow the test class with bytes loaded by InstrumentationClassLoader
private static Class<?> shadowTestClass(final Class<?> clazz) {
try {
@ -100,56 +138,26 @@ public class SpockRunner extends Sputnik {
final File bootstrapJar = createBootstrapJar();
ByteBuddyAgent.getInstrumentation()
.appendToBootstrapClassLoaderSearch(new JarFile(bootstrapJar));
Utils.getBootstrapProxy().addURL(bootstrapJar.toURI().toURL());
// Utils cannot be referenced before this line, as its static initializers load bootstrap
// classes (for example, the bootstrap proxy).
datadog.trace.agent.tooling.Utils.getBootstrapProxy().addURL(bootstrapJar.toURI().toURL());
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private static File createBootstrapJar() throws IOException {
ClassLoader loader = AgentTestRunner.class.getClassLoader();
if (!(loader instanceof URLClassLoader)) {
// java9's system loader does not extend URLClassLoader
// which breaks Guava ClassPath lookup
loader = buildJavaClassPathClassLoader();
}
final ClassPath testCP = ClassPath.from(loader);
Set<String> bootstrapClasses = new HashSet<String>();
for (ClassPath.ClassInfo info : testCP.getAllClasses()) {
for (ClassPath.ClassInfo info : TestUtils.getTestClasspath().getAllClasses()) {
// if info starts with bootstrap prefix: add to bootstrap jar
for (int i = 0; i < TEST_BOOTSTRAP_PREFIXES.length; ++i) {
if (info.getName().startsWith(TEST_BOOTSTRAP_PREFIXES[i])) {
bootstrapClasses.add(info.getResourceName());
break;
}
if (isBootstrapClass(info.getName())) {
bootstrapClasses.add(info.getResourceName());
}
}
return new File(
TestUtils.createJarWithClasses(loader, bootstrapClasses.toArray(new String[0])).getFile());
}
/**
* Parse JVM classpath and return ClassLoader containing all classpath entries. Inspired by Guava.
*
* <p>TODO: use we cannot use Guava version when we can update Guava to version that has this
* logic, i.e. when we drop Java7 support.
*/
private static ClassLoader buildJavaClassPathClassLoader() {
ImmutableList.Builder<URL> urls = ImmutableList.builder();
for (String entry : Splitter.on(PATH_SEPARATOR.value()).split(JAVA_CLASS_PATH.value())) {
try {
try {
urls.add(new File(entry).toURI().toURL());
} catch (SecurityException e) { // File.toURI checks to see if the file is a directory
urls.add(new URL("file", null, new File(entry).getAbsolutePath()));
}
} catch (MalformedURLException e) {
System.err.println(
String.format(
"Error injecting bootstrap jar: Malformed classpath entry: %s. %s", entry, e));
}
}
return new URLClassLoader(urls.build().toArray(new URL[0]), null);
TestUtils.createJarWithClasses(
AgentTestRunner.class.getClassLoader(), bootstrapClasses.toArray(new String[0]))
.getFile());
}
/** Run test classes in a classloader which loads test classes before delegating. */

View File

@ -1,7 +1,12 @@
package datadog.trace.agent.test;
import static com.google.common.base.StandardSystemProperty.JAVA_CLASS_PATH;
import static com.google.common.base.StandardSystemProperty.PATH_SEPARATOR;
import static io.opentracing.log.Fields.ERROR_OBJECT;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableList;
import com.google.common.reflect.ClassPath;
import datadog.trace.agent.tooling.Utils;
import datadog.trace.context.TraceScope;
import io.opentracing.Scope;
@ -16,8 +21,10 @@ import java.io.IOException;
import java.io.InputStream;
import java.lang.ref.WeakReference;
import java.lang.reflect.Field;
import java.net.MalformedURLException;
import java.net.ServerSocket;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.Collections;
import java.util.UUID;
import java.util.concurrent.Callable;
@ -26,6 +33,7 @@ import java.util.jar.JarOutputStream;
import java.util.jar.Manifest;
public class TestUtils {
private static final ClassPath testClasspath = computeTestClasspath();
public static void registerOrReplaceGlobalTracer(final Tracer tracer) {
try {
@ -223,4 +231,43 @@ public class TestUtils {
System.runFinalization();
}
}
public static ClassPath getTestClasspath() {
return testClasspath;
}
private static ClassPath computeTestClasspath() {
ClassLoader testClassLoader = AgentTestRunner.class.getClassLoader();
if (!(testClassLoader instanceof URLClassLoader)) {
// java9's system loader does not extend URLClassLoader
// which breaks Guava ClassPath lookup
testClassLoader = buildJavaClassPathClassLoader();
}
try {
return ClassPath.from(testClassLoader);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* Parse JVM classpath and return ClassLoader containing all classpath entries. Inspired by Guava.
*/
private static ClassLoader buildJavaClassPathClassLoader() {
ImmutableList.Builder<URL> urls = ImmutableList.builder();
for (String entry : Splitter.on(PATH_SEPARATOR.value()).split(JAVA_CLASS_PATH.value())) {
try {
try {
urls.add(new File(entry).toURI().toURL());
} catch (SecurityException e) { // File.toURI checks to see if the file is a directory
urls.add(new URL("file", null, new File(entry).getAbsolutePath()));
}
} catch (MalformedURLException e) {
System.err.println(
String.format(
"Error injecting bootstrap jar: Malformed classpath entry: %s. %s", entry, e));
}
}
return new URLClassLoader(urls.build().toArray(new URL[0]), null);
}
}

View File

@ -1,6 +1,11 @@
import com.google.common.reflect.ClassPath
import datadog.trace.agent.test.AgentTestRunner
import datadog.trace.agent.test.SpockRunner
import datadog.trace.agent.test.TestUtils
import datadog.trace.agent.tooling.Utils
import io.opentracing.Span
import io.opentracing.Tracer
import spock.lang.Shared
import java.lang.reflect.Field
@ -8,10 +13,9 @@ class AgentTestRunnerTest extends AgentTestRunner {
private static final ClassLoader BOOTSTRAP_CLASSLOADER = null
private static final ClassLoader OT_LOADER
private static final boolean AGENT_INSTALLED_IN_CLINIT
// having opentracing class in test field should not cause problems
private static final Tracer A_TRACER = null
// having dd tracer api class in test field should not cause problems
private static final datadog.trace.api.Tracer DD_API_TRACER = null
@Shared
private Class sharedSpanClass
static {
// when test class initializes, opentracing should be set up, but not the agent.
@ -19,16 +23,39 @@ class AgentTestRunnerTest extends AgentTestRunner {
AGENT_INSTALLED_IN_CLINIT = getAgentTransformer() != null
}
def "classpath setup"() {
def setupSpec() {
sharedSpanClass = Span
}
def "spock runner bootstrap prefixes correct for test setup"() {
expect:
A_TRACER == null
DD_API_TRACER == null
OT_LOADER == BOOTSTRAP_CLASSLOADER
SpockRunner.BOOTSTRAP_PACKAGE_PREFIXES_COPY == Utils.BOOTSTRAP_PACKAGE_PREFIXES
}
def "classpath setup"() {
setup:
final List<String> bootstrapClassesIncorrectlyLoaded = []
for (ClassPath.ClassInfo info : TestUtils.getTestClasspath().getAllClasses()) {
for (int i = 0; i < Utils.BOOTSTRAP_PACKAGE_PREFIXES.length; ++i) {
if (info.getName().startsWith(Utils.BOOTSTRAP_PACKAGE_PREFIXES[i])) {
Class<?> bootstrapClass = Class.forName(info.getName())
if (bootstrapClass.getClassLoader() != BOOTSTRAP_CLASSLOADER) {
bootstrapClassesIncorrectlyLoaded.add(bootstrapClass)
}
break
}
}
}
expect:
// shared OT classes should cause no trouble
sharedSpanClass.getClassLoader() == BOOTSTRAP_CLASSLOADER
Tracer.getClassLoader() == BOOTSTRAP_CLASSLOADER
!AGENT_INSTALLED_IN_CLINIT
getTestTracer() == TestUtils.getUnderlyingGlobalTracer()
getAgentTransformer() != null
datadog.trace.api.Trace.getClassLoader() == BOOTSTRAP_CLASSLOADER
TestUtils.getUnderlyingGlobalTracer() == datadog.trace.api.GlobalTracer.get()
bootstrapClassesIncorrectlyLoaded == []
}
def "logging works"() {

View File

@ -5,7 +5,8 @@ minimumInstructionCoverage = 0.6
excludedClassesConverage += [
'datadog.trace.agent.test.asserts.*Assert',
'datadog.trace.agent.test.AgentTestRunner.ErrorCountingListener',
'datadog.trace.agent.test.OkHttpUtils'
'datadog.trace.agent.test.OkHttpUtils',
'datadog.trace.agent.test.TestUtils'
]
dependencies {