@@ -7,7 +7,9 @@ import org.junit.jupiter.api.Test
77import org.junit.jupiter.api.io.TempDir
88import org.objectweb.asm.ClassReader
99import org.objectweb.asm.ClassVisitor
10+ import org.objectweb.asm.ClassWriter
1011import org.objectweb.asm.FieldVisitor
12+ import org.objectweb.asm.Opcodes
1113import java.io.File
1214import 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