-
Notifications
You must be signed in to change notification settings - Fork 331
Expand file tree
/
Copy pathSpockRunner.java
More file actions
286 lines (260 loc) · 10.2 KB
/
SpockRunner.java
File metadata and controls
286 lines (260 loc) · 10.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
package datadog.trace.agent.test;
import com.google.common.reflect.ClassPath;
import datadog.trace.agent.test.utils.ClasspathUtils;
import datadog.trace.bootstrap.BootstrapProxy;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import java.util.jar.JarFile;
import net.bytebuddy.agent.ByteBuddyAgent;
import net.bytebuddy.dynamic.ClassFileLocator;
import org.junit.platform.runner.JUnitPlatform;
import org.junit.runner.notification.Failure;
import org.junit.runner.notification.RunListener;
import org.junit.runner.notification.RunNotifier;
import org.spockframework.mock.IMockInvocation;
import org.spockframework.mock.TooManyInvocationsError;
/**
* Runs a spock test in an agent-friendly way.
*
* <ul>
* <li>Adds agent bootstrap classes to bootstrap classpath.
* </ul>
*/
public class SpockRunner extends JUnitPlatform {
/**
* An exact copy of {@link datadog.trace.bootstrap.Constants#BOOTSTRAP_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 = {
"datadog.slf4j",
"datadog.context",
"datadog.environment",
"datadog.json",
"datadog.yaml",
"datadog.cli",
"datadog.appsec.api",
"datadog.trace.api",
"datadog.trace.bootstrap",
"datadog.trace.context",
"datadog.trace.instrumentation.api",
"datadog.trace.logging",
"datadog.trace.util",
};
private static final String[] TEST_EXCLUDED_BOOTSTRAP_PACKAGE_PREFIXES = {
"ch.qos.logback.classic.servlet", // this draws javax.servlet deps that are not needed
};
private static final String[] TEST_BOOTSTRAP_PREFIXES;
static {
ByteBuddyAgent.install();
final String[] testBS = {
"org.slf4j", "ch.qos.logback",
};
TEST_BOOTSTRAP_PREFIXES =
Arrays.copyOf(
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_COPY.length] = testBS[i];
}
setupBootstrapClasspath();
}
private final InstrumentationClassLoader customLoader;
public SpockRunner(final Class<?> clazz)
throws NoSuchFieldException, SecurityException, IllegalArgumentException,
IllegalAccessException {
super(shadowTestClass(clazz));
assertNoBootstrapClassesInTestClass(clazz);
// access the classloader created in shadowTestClass above
final Field clazzField = JUnitPlatform.class.getDeclaredField("testClass");
try {
clazzField.setAccessible(true);
customLoader =
(InstrumentationClassLoader) ((Class<?>) clazzField.get(this)).getClassLoader();
} finally {
clazzField.setAccessible(false);
}
}
private static void assertNoBootstrapClassesInTestClass(final 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(final Class<?> testClass, final 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(final String className) {
for (int i = 0; i < TEST_BOOTSTRAP_PREFIXES.length; ++i) {
if (className.startsWith(TEST_BOOTSTRAP_PREFIXES[i])) {
return Arrays.stream(TEST_EXCLUDED_BOOTSTRAP_PACKAGE_PREFIXES)
.noneMatch(className::startsWith);
}
}
return false;
}
// Shadow the test class with bytes loaded by InstrumentationClassLoader
private static Class<?> shadowTestClass(final Class<?> clazz) {
try {
final InstrumentationClassLoader customLoader =
new InstrumentationClassLoader(
datadog.trace.agent.test.SpockRunner.class.getClassLoader(), clazz.getName());
return customLoader.shadow(clazz);
} catch (final Exception e) {
throw new IllegalStateException(e);
}
}
@Override
public void run(final RunNotifier notifier) {
final ClassLoader contextLoader = Thread.currentThread().getContextClassLoader();
final RunListener listener = new TooManyInvocationsErrorListener();
try {
Thread.currentThread().setContextClassLoader(customLoader);
notifier.addFirstListener(listener);
super.run(notifier);
} finally {
notifier.removeListener(listener);
Thread.currentThread().setContextClassLoader(contextLoader);
}
}
private static void setupBootstrapClasspath() {
// Ensure there weren't any bootstrap classes loaded prematurely.
Set<String> prematureBootstrapClasses = new TreeSet<>();
for (Class clazz : ByteBuddyAgent.getInstrumentation().getAllLoadedClasses()) {
if (isBootstrapClass(clazz.getName())
&& clazz.getClassLoader() != null
&& !clazz.getName().equals("datadog.trace.api.DisableTestTrace")
&& !clazz.getName().startsWith("org.slf4j")) {
prematureBootstrapClasses.add(clazz.getName());
}
}
if (!prematureBootstrapClasses.isEmpty()) {
throw new AssertionError(
prematureBootstrapClasses.size()
+ " classes were loaded before bootstrap classpath was initialized: "
+ prematureBootstrapClasses);
}
try {
final File bootstrapJar = createBootstrapJar();
ByteBuddyAgent.getInstrumentation()
.appendToBootstrapClassLoaderSearch(new JarFile(bootstrapJar));
// Utils cannot be referenced before this line, as its static initializers load bootstrap
// classes (for example, the bootstrap proxy).
BootstrapProxy.addBootstrapResource(bootstrapJar.toURI().toURL());
} catch (final IOException e) {
throw new RuntimeException(e);
}
}
private static File createBootstrapJar() throws IOException {
final Set<String> bootstrapClasses = new HashSet<>();
for (final ClassPath.ClassInfo info : ClasspathUtils.getTestClasspath().getAllClasses()) {
// if info starts with bootstrap prefix: add to bootstrap jar
if (isBootstrapClass(info.getName())) {
bootstrapClasses.add(info.getResourceName());
}
}
return new File(
ClasspathUtils.createJarWithClasses(
AgentTestRunner.class.getClassLoader(), bootstrapClasses.toArray(new String[0]))
.getFile());
}
/** Run test classes in a classloader which loads test classes before delegating. */
private static class InstrumentationClassLoader extends java.lang.ClassLoader {
final ClassLoader parent;
final String shadowPrefix;
public InstrumentationClassLoader(final ClassLoader parent, final String shadowPrefix) {
super(parent);
this.parent = parent;
this.shadowPrefix = shadowPrefix;
}
/** Forcefully inject the bytes of clazz into this classloader. */
public Class<?> shadow(final Class<?> clazz) throws IOException {
final Class<?> loaded = findLoadedClass(clazz.getName());
if (loaded != null && loaded.getClassLoader() == this) {
return loaded;
}
final ClassFileLocator locator = ClassFileLocator.ForClassLoader.of(clazz.getClassLoader());
final byte[] classBytes = locator.locate(clazz.getName()).resolve();
return defineClass(clazz.getName(), classBytes, 0, classBytes.length);
}
@Override
protected Class<?> loadClass(final String name, final boolean resolve)
throws ClassNotFoundException {
synchronized (super.getClassLoadingLock(name)) {
final Class c = findLoadedClass(name);
if (c != null) {
return c;
}
if (name.startsWith(shadowPrefix)) {
try {
return shadow(super.loadClass(name, resolve));
} catch (final Exception e) {
}
}
return parent.loadClass(name);
}
}
}
/**
* This class tries to fix {@link TooManyInvocationsError} exceptions when the assertion error is
* caught by a mock triggering a stack overflow while composing the failure message.
*/
@SuppressWarnings("ResultOfMethodCallIgnored")
@RunListener.ThreadSafe
private static class TooManyInvocationsErrorListener extends RunListener {
@Override
public void testFailure(final Failure failure) throws Exception {
if (failure.getException() instanceof TooManyInvocationsError) {
final TooManyInvocationsError assertion = (TooManyInvocationsError) failure.getException();
try {
// try to trigger an error (e.g. stack overflow)
assertion.getMessage();
} catch (final Throwable e) {
fixTooManyInvocationsError(assertion);
}
}
}
private void fixTooManyInvocationsError(final TooManyInvocationsError error) {
final List<IMockInvocation> accepted = error.getAcceptedInvocations();
for (final IMockInvocation invocation : accepted) {
try {
invocation.toString();
} catch (final Throwable t) {
final List<Object> arguments = invocation.getArguments();
for (int i = 0; i < arguments.size(); i++) {
final Object arg = arguments.get(i);
if (arg instanceof AssertionError) {
final AssertionError updatedAssertion =
new AssertionError(
"'"
+ arg.getClass().getName()
+ "' hidden due to '"
+ t.getClass().getName()
+ "'",
t);
invocation.getArguments().set(i, updatedAssertion);
}
}
}
}
}
}
}