Skip to content

Commit f74856b

Browse files
authored
Fix exploration tests (#7719)
update the registry for pushing the image install jdk 21 use the jsoup library and patch it for running test with instrument-the-world mode add include file to specify which packages/classes we want to include into the instrumentation instead of instrumenting everything (like build tools) do not collect static inherited fields to avoid duplicate class definition for enums.
1 parent affb61a commit f74856b

File tree

14 files changed

+190
-66
lines changed

14 files changed

+190
-66
lines changed

.gitlab/exploration-tests.yml

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
variables:
2-
EXPLORATION_TESTS_IMAGE: $REGISTRY/ci/dd-trace-java:exploration-tests
2+
EXPLORATION_TESTS_IMAGE: registry.ddbuild.io/ci/dd-trace-java:exploration-tests
33

44
build-exploration-tests-image:
55
stage: exploration-tests
@@ -18,24 +18,33 @@ build-exploration-tests-image:
1818
- ./gradlew :dd-java-agent:shadowJar --no-scan
1919
- cp workspace/dd-java-agent/build/libs/*.jar /exploration-tests/dd-java-agent.jar
2020
- cp dd-java-agent/agent-debugger/exploration-tests/run-exploration-tests.sh /exploration-tests
21-
- cp dd-java-agent/agent-debugger/exploration-tests/exclude*.txt /exploration-tests
21+
- cp dd-java-agent/agent-debugger/exploration-tests/exclude_*.txt /exploration-tests
22+
- cp dd-java-agent/agent-debugger/exploration-tests/include_*.txt /exploration-tests
2223
- cd /exploration-tests
2324
after_script:
25+
- echo "$PROJECT"
2426
- cd $CI_PROJECT_DIR
25-
- cp /exploration-tests/$PROJECT/agent.log agent.log
26-
- gzip agent.log
27-
- tar czf surefire-reports.tar.gz /exploration-tests/$PROJECT/target/surefire-reports
28-
- tar czf debugger-dumps.tar.gz /tmp/debugger
27+
- cp /exploration-tests/$PROJECT/agent.log ${PROJECT}_agent.log
28+
- gzip ${PROJECT}_agent.log
29+
- tar czf ${PROJECT}_surefire-reports.tar.gz /exploration-tests/${PROJECT}/target/surefire-reports
30+
- tar czf ${PROJECT}_debugger-dumps.tar.gz /tmp/debugger
2931
stage: exploration-tests
3032
when: manual
3133
tags: [ "runner:main"]
3234
needs: []
3335
image: $EXPLORATION_TESTS_IMAGE
3436
artifacts:
3537
paths:
36-
- agent.log.gz
37-
- surefire-reports.tar.gz
38-
- debugger-dumps.tar.gz
38+
- ${PROJECT}_agent.log.gz
39+
- ${PROJECT}_surefire-reports.tar.gz
40+
- ${PROJECT}_debugger-dumps.tar.gz
41+
42+
exploration-tests-jsoup:
43+
variables:
44+
PROJECT: jsoup
45+
<<: *common-exploration-tests
46+
script:
47+
- ./run-exploration-tests.sh "$PROJECT" "mvn verify" "include_jsoup.txt" "exclude_jsoup.txt"
3948

4049

4150
exploration-tests-jackson-core:

dd-java-agent/agent-debugger/exploration-tests/Dockerfile.exploration-tests

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ FROM debian:bookworm-slim
33
ARG JAVA_8_VERSION="8.0.372-tem"
44
ARG JAVA_11_VERSION="11.0.19-tem"
55
ARG JAVA_17_VERSION="17.0.7-tem"
6+
ARG JAVA_21_VERSION="21.0.4-tem"
67
ARG MAVEN_VERSION=3.8.4
78

89
RUN apt-get update && \
@@ -16,25 +17,36 @@ RUN bash -c "source $HOME/.sdkman/bin/sdkman-init.sh && \
1617
yes | sdk install java $JAVA_8_VERSION && \
1718
yes | sdk install java $JAVA_11_VERSION && \
1819
yes | sdk install java $JAVA_17_VERSION && \
20+
yes | sdk install java $JAVA_21_VERSION && \
1921
yes | sdk install maven $MAVEN_VERSION && \
2022
rm -rf $HOME/.sdkman/archives/* && \
2123
rm -rf $HOME/.sdkman/tmp/*"
2224
ENV JAVA_8_HOME=/root/.sdkman/candidates/java/$JAVA_8_VERSION
2325
ENV JAVA_11_HOME=/root/.sdkman/candidates/java/$JAVA_11_VERSION
2426
ENV JAVA_17_HOME=/root/.sdkman/candidates/java/$JAVA_17_VERSION
27+
ENV JAVA_21_HOME=/root/.sdkman/candidates/java/$JAVA_21_VERSION
2528

2629
RUN mkdir exploration-tests
2730
WORKDIR /exploration-tests
31+
# jsoup
32+
RUN git clone -b jsoup-1.18.1 https://github.com/jhy/jsoup.git
33+
COPY jsoup_exploration-tests.patch .
34+
# fix tests that are failing because checking time to execute
35+
RUN cd jsoup && git apply /exploration-tests/jsoup_exploration-tests.patch
36+
RUN bash -c "source $HOME/.sdkman/bin/sdkman-init.sh && cd jsoup && mvn verify -DskipTests=true"
37+
38+
2839
# Jackson
29-
RUN git clone https://github.com/FasterXML/jackson-core.git
30-
RUN bash -c "source $HOME/.sdkman/bin/sdkman-init.sh && cd jackson-core && ./mvnw dependency:resolve dependency:resolve-plugins"
31-
RUN git clone https://github.com/FasterXML/jackson-databind.git
32-
RUN bash -c "source $HOME/.sdkman/bin/sdkman-init.sh && cd jackson-databind && ./mvnw dependency:resolve dependency:resolve-plugins"
40+
#RUN git clone https://github.com/FasterXML/jackson-core.git
41+
#RUN bash -c "source $HOME/.sdkman/bin/sdkman-init.sh && cd jackson-core && ./mvnw dependency:resolve dependency:resolve-plugins"
42+
#RUN git clone https://github.com/FasterXML/jackson-databind.git
43+
#RUN bash -c "source $HOME/.sdkman/bin/sdkman-init.sh && cd jackson-databind && ./mvnw dependency:resolve dependency:resolve-plugins"
44+
3345
# Netty
34-
RUN git clone https://github.com/netty/netty.git
35-
RUN bash -c "source $HOME/.sdkman/bin/sdkman-init.sh && cd netty && ./mvnw dependency:resolve dependency:resolve-plugins"
46+
#RUN git clone https://github.com/netty/netty.git
47+
#RUN bash -c "source $HOME/.sdkman/bin/sdkman-init.sh && cd netty && ./mvnw dependency:resolve dependency:resolve-plugins"
3648

3749
# Guava
38-
RUN git clone https://github.com/google/guava.git
39-
RUN bash -c "source $HOME/.sdkman/bin/sdkman-init.sh && cd guava && mvn -B -P!standard-with-extra-repos verify -U -Dmaven.javadoc.skip=true -DskipTests=true"
50+
#RUN git clone https://github.com/google/guava.git
51+
#RUN bash -c "source $HOME/.sdkman/bin/sdkman-init.sh && cd guava && mvn -B -P!standard-with-extra-repos verify -U -Dmaven.javadoc.skip=true -DskipTests=true"
4052

dd-java-agent/agent-debugger/exploration-tests/exclude.txt

Lines changed: 0 additions & 15 deletions
This file was deleted.

dd-java-agent/agent-debugger/exploration-tests/exclude_jackson-databind.txt

Lines changed: 0 additions & 4 deletions
This file was deleted.

dd-java-agent/agent-debugger/exploration-tests/exclude_jsoup.txt

Whitespace-only changes.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
org/jsoup/*
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
diff --git a/src/test/java/org/jsoup/parser/HtmlParserTest.java b/src/test/java/org/jsoup/parser/HtmlParserTest.java
2+
index a67003a8..1201d1af 100644
3+
--- a/src/test/java/org/jsoup/parser/HtmlParserTest.java
4+
+++ b/src/test/java/org/jsoup/parser/HtmlParserTest.java
5+
@@ -1033,7 +1033,7 @@ public class HtmlParserTest {
6+
7+
// Assert
8+
assertEquals(50000, doc.body().childNodeSize());
9+
- assertTrue(System.currentTimeMillis() - start < 1000);
10+
+ //assertTrue(System.currentTimeMillis() - start < 10000);
11+
}
12+
13+
@Test
14+
diff --git a/src/test/java/org/jsoup/parser/ParserIT.java b/src/test/java/org/jsoup/parser/ParserIT.java
15+
index 54d757e7..467c10bc 100644
16+
--- a/src/test/java/org/jsoup/parser/ParserIT.java
17+
+++ b/src/test/java/org/jsoup/parser/ParserIT.java
18+
@@ -48,7 +48,7 @@ public class ParserIT {
19+
// Assert
20+
assertEquals(2, doc.body().childNodeSize());
21+
assertEquals(25000, doc.select("dd").size());
22+
- assertTrue(System.currentTimeMillis() - start < 20000); // I get ~ 1.5 seconds, but others have reported slower
23+
+ //assertTrue(System.currentTimeMillis() - start < 20000); // I get ~ 1.5 seconds, but others have reported slower
24+
// was originally much longer, or stack overflow.
25+
}
26+
}

dd-java-agent/agent-debugger/exploration-tests/run-exploration-tests.sh

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@
22
set -uo pipefail
33
NAME=$1
44
COMMAND=$2
5-
PROJECT_EXCLUDE_FILE=${3:-}
5+
PROJECT_INCLUDE_FILE=${3:-}
6+
PROJECT_EXCLUDE_FILE=${4:-}
67
echo " === running debugger java exploration tests === "
7-
export JAVA_TOOL_OPTIONS="-javaagent:`pwd`/dd-java-agent.jar -Ddatadog.slf4j.simpleLogger.log.com.datadog.debugger=debug -Ddd.trace.enabled=false -Ddd.dynamic.instrumentation.enabled=true -Ddd.dynamic.instrumentation.instrument.the.world=true -Ddd.dynamic.instrumentation.classfile.dump.enabled=true -Ddd.dynamic.instrumentation.verify.bytecode=true -Ddd.dynamic.instrumentation.exclude.files=/exploration-tests/exclude.txt,/exploration-tests/$PROJECT_EXCLUDE_FILE"
8+
export JAVA_TOOL_OPTIONS="-javaagent:`pwd`/dd-java-agent.jar -Ddatadog.slf4j.simpleLogger.log.com.datadog.debugger=debug -Ddd.trace.enabled=false -Ddd.dynamic.instrumentation.enabled=true -Ddd.dynamic.instrumentation.instrument.the.world=true -Ddd.dynamic.instrumentation.classfile.dump.enabled=true -Ddd.dynamic.instrumentation.verify.bytecode=false -Ddd.dynamic.instrumentation.include.files=/exploration-tests/$PROJECT_INCLUDE_FILE -Ddd.dynamic.instrumentation.exclude.files=/exploration-tests/$PROJECT_EXCLUDE_FILE"
89
echo "$JAVA_TOOL_OPTIONS"
910
cd $NAME
1011
echo "Building repository $NAME..."

dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/agent/DebuggerTransformer.java

Lines changed: 78 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -93,8 +93,12 @@ public class DebuggerTransformer implements ClassFileTransformer {
9393
private final InstrumentationListener listener;
9494
private final DebuggerSink debuggerSink;
9595
private final boolean instrumentTheWorld;
96-
private final Set<String> excludeClasses = new HashSet<>();
97-
private final Trie excludeTrie = new Trie();
96+
private final Set<String> excludeClasses;
97+
private final Set<String> excludeMethods;
98+
private final Trie excludeTrie;
99+
private final Set<String> includeClasses;
100+
private final Set<String> includeMethods;
101+
private final Trie includeTrie;
98102
private final Map<String, LogProbe> instrumentTheWorldProbes;
99103

100104
public interface InstrumentationListener {
@@ -115,9 +119,24 @@ public DebuggerTransformer(
115119
this.instrumentTheWorld = config.isDebuggerInstrumentTheWorld();
116120
if (this.instrumentTheWorld) {
117121
instrumentTheWorldProbes = new ConcurrentHashMap<>();
118-
readExcludeFiles(config.getDebuggerExcludeFiles());
122+
excludeTrie = new Trie();
123+
excludeClasses = new HashSet<>();
124+
excludeMethods = new HashSet<>();
125+
includeTrie = new Trie();
126+
includeClasses = new HashSet<>();
127+
includeMethods = new HashSet<>();
128+
processITWFiles(
129+
config.getDebuggerExcludeFiles(), excludeTrie, excludeClasses, excludeMethods);
130+
processITWFiles(
131+
config.getDebuggerIncludeFiles(), includeTrie, includeClasses, includeMethods);
119132
} else {
120133
instrumentTheWorldProbes = null;
134+
excludeTrie = null;
135+
excludeClasses = null;
136+
excludeMethods = null;
137+
includeTrie = null;
138+
includeClasses = null;
139+
includeMethods = null;
121140
}
122141
}
123142

@@ -137,7 +156,8 @@ public DebuggerTransformer(Config config, Configuration configuration) {
137156
new SymbolSink(config)));
138157
}
139158

140-
private void readExcludeFiles(String commaSeparatedFileNames) {
159+
private void processITWFiles(
160+
String commaSeparatedFileNames, Trie prefixes, Set<String> classes, Set<String> methods) {
141161
if (commaSeparatedFileNames == null) {
142162
return;
143163
}
@@ -156,10 +176,14 @@ private void readExcludeFiles(String commaSeparatedFileNames) {
156176
return;
157177
}
158178
if (line.endsWith("*")) {
159-
excludeTrie.insert(line.substring(0, line.length() - 1));
160-
} else {
161-
excludeClasses.add(line);
179+
prefixes.insert(line.substring(0, line.length() - 1));
180+
return;
181+
}
182+
if (line.contains("::")) {
183+
methods.add(line);
184+
return;
162185
}
186+
classes.add(line);
163187
});
164188
} catch (IOException ex) {
165189
log.warn("Error reading exclude file '{}' for Instrument-The-World: ", fileName, ex);
@@ -255,12 +279,10 @@ private byte[] transformTheWorld(
255279
// Skipping bootstrap classloader
256280
return null;
257281
}
258-
if (classFilePath.startsWith("com/datadog/debugger/")
259-
|| classFilePath.startsWith("com/timgroup/statsd/")) {
260-
// Skipping classes that are used to capture and send snapshots
282+
if (isExcludedFromTransformation(classFilePath)) {
261283
return null;
262284
}
263-
if (isExcludedFromTransformation(classFilePath)) {
285+
if (!isIncludedForTransformation(classFilePath)) {
264286
return null;
265287
}
266288
URL location = null;
@@ -277,15 +299,21 @@ private byte[] transformTheWorld(
277299
loader,
278300
location);
279301
ClassNode classNode = parseClassFile(classFilePath, classfileBuffer);
302+
if (isClassLoaderRelated(classNode)) {
303+
// Skip ClassLoader classes
304+
log.debug("Skipping ClassLoader class: {}", classFilePath);
305+
excludeClasses.add(classFilePath);
306+
return null;
307+
}
280308
List<ProbeDefinition> probes = new ArrayList<>();
281309
Set<String> methodNames = new HashSet<>();
282310
for (MethodNode methodNode : classNode.methods) {
283-
if (methodNames.add(methodNode.name)) {
311+
if (isMethodIncludedForTransformation(methodNode, classNode, methodNames)) {
284312
LogProbe probe =
285313
LogProbe.builder()
286314
.probeId(UUID.randomUUID().toString(), 0)
287315
.where(classNode.name, methodNode.name)
288-
.captureSnapshot(true)
316+
.captureSnapshot(false)
289317
.build();
290318
probes.add(probe);
291319
instrumentTheWorldProbes.put(probe.getProbeId().getEncodedId(), probe);
@@ -294,6 +322,8 @@ private byte[] transformTheWorld(
294322
boolean transformed = performInstrumentation(loader, classFilePath, probes, classNode);
295323
if (transformed) {
296324
return writeClassFile(probes, loader, classFilePath, classNode);
325+
} else {
326+
log.debug("Class not transformed: {}", classFilePath);
297327
}
298328
} catch (Throwable ex) {
299329
log.warn("Cannot transform: ", ex);
@@ -302,6 +332,26 @@ private byte[] transformTheWorld(
302332
return null;
303333
}
304334

335+
private boolean isMethodIncludedForTransformation(
336+
MethodNode methodNode, ClassNode classNode, Set<String> methodNames) {
337+
if (methodNode.name.equals("<clinit>")) {
338+
// skip static class initializer
339+
return false;
340+
}
341+
String fqnMethod = classNode.name + "::" + methodNode.name;
342+
if (excludeMethods.contains(fqnMethod)) {
343+
log.debug("Skipping method: {}", fqnMethod);
344+
return false;
345+
}
346+
return methodNames.add(methodNode.name);
347+
}
348+
349+
private boolean isClassLoaderRelated(ClassNode classNode) {
350+
return classNode.superName.equals("java/lang/ClassLoader")
351+
|| classNode.superName.equals("java/net/URLClassLoader")
352+
|| excludeClasses.contains(classNode.superName);
353+
}
354+
305355
private synchronized void writeToInstrumentationLog(String classFilePath) {
306356
try (FileWriter writer = new FileWriter("/tmp/debugger/instrumentation.log", true)) {
307357
writer.write(classFilePath);
@@ -319,6 +369,11 @@ public ProbeImplementation instrumentTheWorldResolver(String id) {
319369
}
320370

321371
private boolean isExcludedFromTransformation(String classFilePath) {
372+
if (classFilePath.startsWith("com/datadog/debugger/")
373+
|| classFilePath.startsWith("com/timgroup/statsd/")) {
374+
// Skipping classes that are used to capture and send snapshots
375+
return true;
376+
}
322377
if (excludeClasses.contains(classFilePath)) {
323378
return true;
324379
}
@@ -328,6 +383,16 @@ private boolean isExcludedFromTransformation(String classFilePath) {
328383
return false;
329384
}
330385

386+
private boolean isIncludedForTransformation(String classFilePath) {
387+
if (includeClasses.contains(classFilePath)) {
388+
return true;
389+
}
390+
if (includeTrie.hasMatchingPrefix(classFilePath)) {
391+
return true;
392+
}
393+
return false;
394+
}
395+
331396
private boolean instrumentationIsAllowed(
332397
String fullyQualifiedClassName, List<ProbeDefinition> definitions) {
333398
if (denyListHelper.isDenied(fullyQualifiedClassName)) {

dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/instrumentation/CapturedContextInstrumentor.java

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,11 @@
6464
import org.objectweb.asm.tree.TryCatchBlockNode;
6565
import org.objectweb.asm.tree.TypeInsnNode;
6666
import org.objectweb.asm.tree.VarInsnNode;
67+
import org.slf4j.Logger;
68+
import org.slf4j.LoggerFactory;
6769

6870
public class CapturedContextInstrumentor extends Instrumentor {
71+
private static final Logger log = LoggerFactory.getLogger(CapturedContextInstrumentor.class);
6972
private final boolean captureSnapshot;
7073
private final Limits limits;
7174
private final LabelNode contextInitLabel = new LabelNode();
@@ -1197,7 +1200,12 @@ private static List<FieldNode> extractStaticFields(
11971200
}
11981201
}
11991202
}
1200-
addInheritedStaticFields(classNode, classLoader, limits, results, fieldCount);
1203+
if (!Config.get().isDebuggerInstrumentTheWorld()) {
1204+
// Collects inherited static fields only if the ITW mode is not enabled
1205+
// because it can lead to LinkageError: attempted duplicate class definition
1206+
// for example, when a probe is located in method overridden in enum element
1207+
addInheritedStaticFields(classNode, classLoader, limits, results, fieldCount);
1208+
}
12011209
return results;
12021210
}
12031211

@@ -1215,17 +1223,22 @@ private static void addInheritedStaticFields(
12151223
} catch (ClassNotFoundException ex) {
12161224
break;
12171225
}
1218-
for (Field field : clazz.getDeclaredFields()) {
1219-
if (isStaticField(field) && !isFinalField(field)) {
1220-
String desc = Type.getDescriptor(field.getType());
1221-
FieldNode fieldNode =
1222-
new FieldNode(field.getModifiers(), field.getName(), desc, null, field);
1223-
results.add(fieldNode);
1224-
fieldCount++;
1225-
if (fieldCount > limits.maxFieldCount) {
1226-
return;
1226+
try {
1227+
for (Field field : clazz.getDeclaredFields()) {
1228+
if (isStaticField(field) && !isFinalField(field)) {
1229+
String desc = Type.getDescriptor(field.getType());
1230+
FieldNode fieldNode =
1231+
new FieldNode(field.getModifiers(), field.getName(), desc, null, field);
1232+
results.add(fieldNode);
1233+
log.debug("Adding static inherited field {}", fieldNode.name);
1234+
fieldCount++;
1235+
if (fieldCount > limits.maxFieldCount) {
1236+
return;
1237+
}
12271238
}
12281239
}
1240+
} catch (ClassCircularityError ex) {
1241+
break;
12291242
}
12301243
clazz = clazz.getSuperclass();
12311244
superClassName = clazz.getTypeName();

0 commit comments

Comments
 (0)