Skip to content

Commit 9ab4514

Browse files
authored
chore(build): New tests for the build time instrumentation (#10743)
chore(build): New tests for the build time instrumentation Co-authored-by: brice.dutheil <brice.dutheil@datadoghq.com>
1 parent a35ba74 commit 9ab4514

File tree

1 file changed

+154
-45
lines changed

1 file changed

+154
-45
lines changed

buildSrc/src/test/kotlin/datadog/gradle/plugin/instrument/BuildTimeInstrumentationPluginTest.kt

Lines changed: 154 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ import org.junit.jupiter.api.Test
77
import org.junit.jupiter.api.io.TempDir
88
import org.objectweb.asm.ClassReader
99
import org.objectweb.asm.ClassVisitor
10+
import org.objectweb.asm.ClassWriter
1011
import org.objectweb.asm.FieldVisitor
12+
import org.objectweb.asm.Opcodes
1113
import java.io.File
1214
import java.io.FileInputStream
1315

@@ -35,41 +37,6 @@ class BuildTimeInstrumentationPluginTest {
3537
]
3638
""".trimIndent()
3739

38-
private val testPlugin = """
39-
import java.io.File;
40-
import java.io.IOException;
41-
import net.bytebuddy.build.Plugin;
42-
import net.bytebuddy.description.type.TypeDescription;
43-
import net.bytebuddy.dynamic.ClassFileLocator;
44-
import net.bytebuddy.dynamic.DynamicType;
45-
46-
public class TestPlugin implements Plugin {
47-
private final File targetDir;
48-
49-
public TestPlugin(File targetDir) {
50-
this.targetDir = targetDir;
51-
}
52-
53-
@Override
54-
public boolean matches(TypeDescription target) {
55-
return "ExampleCode".equals(target.getSimpleName());
56-
}
57-
58-
@Override
59-
public DynamicType.Builder<?> apply(
60-
DynamicType.Builder<?> builder,
61-
TypeDescription typeDescription,
62-
ClassFileLocator classFileLocator) {
63-
return builder.defineField("__TEST__FIELD__", Void.class);
64-
}
65-
66-
@Override
67-
public void close() throws IOException {
68-
// no-op
69-
}
70-
}
71-
""".trimIndent()
72-
7340
private val exampleCode = """
7441
package example;
7542
public class ExampleCode {}
@@ -83,40 +50,182 @@ class BuildTimeInstrumentationPluginTest {
8350
val buildFile = File(buildDir, "build.gradle")
8451
buildFile.writeText(buildGradle)
8552

86-
val srcMainJava = File(buildDir, "src/main/java").apply { mkdirs() }
87-
File(srcMainJava, "TestPlugin.java").writeText(testPlugin)
53+
val srcMainJava = testPlugin("src/main/java", "ExampleCode")
8854

8955
val examplePackageDir = File(srcMainJava, "example").apply { mkdirs() }
9056
File(examplePackageDir, "ExampleCode.java").writeText(exampleCode)
9157

9258
// Run Gradle build with TestKit
93-
val result = GradleRunner.create().withTestKitDir(File(buildDir, ".gradle-test-kit")) // workaround in case the global test-kit cache becomes corrupted
59+
GradleRunner.create().withTestKitDir(File(buildDir, ".gradle-test-kit")) // workaround in case the global test-kit cache becomes corrupted
9460
.withDebug(true) // avoids starting daemon which can leave undeleted files post-cleanup
9561
.withProjectDir(buildDir)
9662
.withArguments("build", "--stacktrace")
9763
.withPluginClasspath()
9864
.forwardOutput()
9965
.build()
10066

101-
val classFile = File(buildDir, "build/classes/java/main/example/ExampleCode.class")
102-
assertTrue(classFile.isFile)
67+
assertInstrumented(File(buildDir, "build/classes/java/main/example/ExampleCode.class"))
68+
}
69+
70+
@Test
71+
fun `test instrument plugin processes includeClassDirectories`() {
72+
val buildFile = File(buildDir, "build.gradle")
73+
buildFile.writeText("""
74+
plugins {
75+
id 'java'
76+
id 'dd-trace-java.build-time-instrumentation'
77+
}
78+
79+
sourceCompatibility = JavaVersion.VERSION_1_8
80+
targetCompatibility = JavaVersion.VERSION_1_8
81+
82+
repositories {
83+
mavenCentral()
84+
}
85+
86+
dependencies {
87+
compileOnly group: 'net.bytebuddy', name: 'byte-buddy', version: '1.18.3'
88+
}
89+
90+
buildTimeInstrumentation {
91+
plugins = ['TestPlugin']
92+
includeClassDirectories.from(file('external-classes'))
93+
}
94+
""".trimIndent())
95+
96+
testPlugin("src/main/java", "ExternalCode")
97+
98+
// Pre-compile ExternalCode using ASM and place it in the external-classes directory
99+
val externalClassesDir = File(buildDir, "external-classes").apply { mkdirs() }
100+
precompiledClass("ExternalCode", externalClassesDir)
101+
102+
GradleRunner.create()
103+
.withTestKitDir(File(buildDir, ".gradle-test-kit"))
104+
.withDebug(true)
105+
.withProjectDir(buildDir)
106+
.withArguments("build", "--stacktrace")
107+
.withPluginClasspath()
108+
.forwardOutput()
109+
.build()
110+
111+
// ExternalCode.class should have been copied from external-classes, instrumented, and placed in the output
112+
assertInstrumented(File(buildDir, "build/classes/java/main/ExternalCode.class"))
113+
}
114+
115+
@Test
116+
fun `test rerun-tasks does not lose includeClassDirectories classes`() {
117+
val buildFile = File(buildDir, "build.gradle")
118+
buildFile.writeText("""
119+
plugins {
120+
id 'java'
121+
id 'dd-trace-java.build-time-instrumentation'
122+
}
123+
124+
sourceCompatibility = JavaVersion.VERSION_1_8
125+
targetCompatibility = JavaVersion.VERSION_1_8
126+
127+
repositories {
128+
mavenCentral()
129+
}
130+
131+
dependencies {
132+
compileOnly group: 'net.bytebuddy', name: 'byte-buddy', version: '1.18.3'
133+
}
134+
135+
buildTimeInstrumentation {
136+
plugins = ['TestPlugin']
137+
includeClassDirectories.from(file('external-classes'))
138+
}
139+
""".trimIndent())
140+
141+
val srcMainJava = testPlugin("src/main/java", "ExampleCode", "ExternalCode")
142+
val examplePackageDir = File(srcMainJava, "example").apply { mkdirs() }
143+
File(examplePackageDir, "ExampleCode.java").writeText("package example; public class ExampleCode {}")
103144

145+
val externalClassesDir = File(buildDir, "external-classes").apply { mkdirs() }
146+
precompiledClass("ExternalCode", externalClassesDir)
147+
148+
val runner = GradleRunner.create()
149+
.withTestKitDir(File(buildDir, ".gradle-test-kit"))
150+
.withDebug(true)
151+
.withProjectDir(buildDir)
152+
.withPluginClasspath()
153+
.forwardOutput()
154+
155+
// First build
156+
runner.withArguments("build", "--stacktrace").build()
157+
158+
// Second build with --rerun-tasks: compileJava wipes classesDirectory, so without
159+
// the fix InstrumentAction would only sync freshly-compiled classes and lose ExternalCode.class
160+
runner.withArguments("build", "--rerun-tasks", "--stacktrace").build()
161+
162+
assertInstrumented(File(buildDir, "build/classes/java/main/example/ExampleCode.class"))
163+
assertInstrumented(File(buildDir, "build/classes/java/main/ExternalCode.class"))
164+
}
165+
166+
private fun testPlugin(srcDir: String, vararg classNames: String): File {
167+
val dir = File(buildDir, srcDir).apply { mkdirs() }
168+
val conditions = classNames.joinToString(" || ") { "\"$it\".equals(name)" }
169+
File(dir, "TestPlugin.java").writeText("""
170+
import java.io.File;
171+
import java.io.IOException;
172+
import net.bytebuddy.build.Plugin;
173+
import net.bytebuddy.description.type.TypeDescription;
174+
import net.bytebuddy.dynamic.ClassFileLocator;
175+
import net.bytebuddy.dynamic.DynamicType;
176+
177+
public class TestPlugin implements Plugin {
178+
private final File targetDir;
179+
180+
public TestPlugin(File targetDir) {
181+
this.targetDir = targetDir;
182+
}
183+
184+
@Override
185+
public boolean matches(TypeDescription target) {
186+
String name = target.getSimpleName();
187+
return $conditions;
188+
}
189+
190+
@Override
191+
public DynamicType.Builder<?> apply(
192+
DynamicType.Builder<?> builder,
193+
TypeDescription typeDescription,
194+
ClassFileLocator classFileLocator) {
195+
return builder.defineField("__TEST__FIELD__", Void.class);
196+
}
197+
198+
@Override
199+
public void close() throws IOException {
200+
// no-op
201+
}
202+
}
203+
""".trimIndent())
204+
return dir
205+
}
206+
207+
private fun precompiledClass(className: String, targetDir: File) {
208+
val classWriter = ClassWriter(0)
209+
classWriter.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, className, null, "java/lang/Object", null)
210+
classWriter.visitEnd()
211+
File(targetDir, "$className.class").writeBytes(classWriter.toByteArray())
212+
}
213+
214+
private fun assertInstrumented(classFile: File) {
215+
assertTrue(classFile.isFile, "${classFile.name} should be present in the output directory")
104216
var foundInsertedField = false
105217
FileInputStream(classFile).use { input ->
106218
val classReader = ClassReader(input)
107219
classReader.accept(
108220
object : ClassVisitor(OpenedClassReader.ASM_API) {
109221
override fun visitField(access: Int, fieldName: String?, descriptor: String?, signature: String?, value: Any?): FieldVisitor? {
110-
if ("__TEST__FIELD__" == fieldName) {
111-
foundInsertedField = true
112-
}
222+
if ("__TEST__FIELD__" == fieldName) foundInsertedField = true
113223
return null
114224
}
115225
},
116226
OpenedClassReader.ASM_API
117227
)
118228
}
119-
120-
assertTrue(foundInsertedField)
229+
assertTrue(foundInsertedField, "${classFile.name} should have been instrumented with __TEST__FIELD__")
121230
}
122231
}

0 commit comments

Comments
 (0)