Skip to content

Commit 2f1d607

Browse files
authored
Add tests for remapClass (#1974)
* Configure compileKotlin and compileJava * Add RelocatorRemapperTest * Cleanups * Add more cases
1 parent 683b5d1 commit 2f1d607

File tree

3 files changed

+345
-6
lines changed

3 files changed

+345
-6
lines changed

build.gradle.kts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,18 +30,12 @@ dokka { dokkaPublications.html { outputDirectory = rootDir.resolve("docs/api") }
3030
kotlin {
3131
explicitApi()
3232
@OptIn(ExperimentalAbiValidation::class) abiValidation { enabled = true }
33-
val jdkRelease = "17"
3433
compilerOptions {
3534
allWarningsAsErrors = true
3635
// https://docs.gradle.org/current/userguide/compatibility.html#kotlin
3736
apiVersion = KotlinVersion.KOTLIN_2_2
3837
languageVersion = apiVersion
39-
jvmTarget = JvmTarget.fromTarget(jdkRelease)
4038
jvmDefault = JvmDefaultMode.NO_COMPATIBILITY
41-
freeCompilerArgs.add("-Xjdk-release=$jdkRelease")
42-
}
43-
target.compilations.configureEach {
44-
compileJavaTaskProvider { options.release = jdkRelease.toInt() }
4539
}
4640
}
4741

@@ -222,6 +216,15 @@ kotlin.target.compilations {
222216
}
223217
}
224218

219+
tasks.compileKotlin {
220+
compilerOptions {
221+
jvmTarget = JvmTarget.fromTarget(libs.versions.jdkRelease.get())
222+
freeCompilerArgs.add("-Xjdk-release=${libs.versions.jdkRelease.get()}")
223+
}
224+
}
225+
226+
tasks.compileJava { options.release = libs.versions.jdkRelease.get().toInt() }
227+
225228
tasks.pluginUnderTestMetadata { pluginClasspath.from(testPluginClasspath) }
226229

227230
tasks.check { dependsOn(tasks.withType<Test>()) }

gradle/libs.versions.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ minGradle = "9.0.0"
33
kotlin = "2.3.20"
44
moshi = "1.15.2"
55
pluginPublish = "2.1.0"
6+
jdkRelease = "17"
67

78
[libraries]
89
apache-ant = "org.apache.ant:ant:1.10.15"
Lines changed: 335 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,335 @@
1+
package com.github.jengelman.gradle.plugins.shadow.internal
2+
3+
import assertk.assertThat
4+
import assertk.assertions.contains
5+
import assertk.assertions.doesNotContain
6+
import assertk.assertions.isEqualTo
7+
import assertk.assertions.isTrue
8+
import com.github.jengelman.gradle.plugins.shadow.relocation.SimpleRelocator
9+
import com.github.jengelman.gradle.plugins.shadow.testkit.requireResourceAsPath
10+
import com.github.jengelman.gradle.plugins.shadow.util.noOpDelegate
11+
import java.io.File
12+
import java.lang.classfile.Attributes
13+
import java.lang.classfile.ClassFile
14+
import java.lang.classfile.instruction.InvokeInstruction
15+
import java.lang.classfile.instruction.TypeCheckInstruction
16+
import java.nio.file.Path
17+
import kotlin.io.path.copyTo
18+
import kotlin.io.path.createParentDirectories
19+
import kotlin.reflect.KClass
20+
import org.gradle.api.file.FileCopyDetails
21+
import org.junit.jupiter.api.Test
22+
import org.junit.jupiter.api.io.TempDir
23+
import org.junit.jupiter.params.ParameterizedTest
24+
import org.junit.jupiter.params.provider.ValueSource
25+
26+
/**
27+
* The cases reflect the cases in
28+
* [com.github.jengelman.gradle.plugins.shadow.relocation.RelocatorsTest], but operate on the
29+
* bytecode level to verify that the remapper correctly transforms class names in all relevant
30+
* bytecode structures.
31+
*/
32+
class BytecodeRemappingTest {
33+
@TempDir lateinit var tempDir: Path
34+
35+
// Relocator used across all relocation tests: moves the test package to a distinct target.
36+
private val relocators =
37+
setOf(
38+
SimpleRelocator(
39+
"com.github.jengelman.gradle.plugins.shadow.internal",
40+
"com.example.relocated",
41+
)
42+
)
43+
44+
// Internal name of the relocated FixtureBase for use in assertions.
45+
private val relocatedFixtureBase = $$"com/example/relocated/BytecodeRemappingTest$FixtureBase"
46+
47+
private val fixtureSubjectDetails
48+
get() = FixtureSubject::class.toFileCopyDetails()
49+
50+
@Test
51+
fun classNotModified() {
52+
val details = fixtureSubjectDetails
53+
// Relocator pattern does not match – original bytes must be returned as-is.
54+
val noMatchRelocators = setOf(SimpleRelocator("org.unrelated", "org.other"))
55+
56+
val result = details.remapClass(noMatchRelocators)
57+
58+
assertThat(result).isEqualTo(details.file.readBytes())
59+
}
60+
61+
@Test
62+
fun classNameIsRelocated() {
63+
val result = fixtureSubjectDetails.remapClass(relocators)
64+
65+
val classModel = ClassFile.of().parse(result)
66+
assertThat(classModel.thisClass().asInternalName())
67+
.isEqualTo($$"com/example/relocated/BytecodeRemappingTest$FixtureSubject")
68+
}
69+
70+
@Test
71+
fun annotationIsRelocated() {
72+
val result = fixtureSubjectDetails.remapClass(relocators)
73+
74+
val classModel = ClassFile.of().parse(result)
75+
val annotationsAttr = classModel.findAttribute(Attributes.runtimeVisibleAnnotations())
76+
assertThat(annotationsAttr.isPresent).isTrue()
77+
val annotationDescriptors =
78+
annotationsAttr.get().annotations().map { it.className().stringValue() }
79+
assertThat(annotationDescriptors)
80+
.contains($$"Lcom/example/relocated/BytecodeRemappingTest$FixtureAnnotation;")
81+
}
82+
83+
@Test
84+
fun baseClassNameIsRelocated() {
85+
// Verify relocation also works on a simple class (FixtureBase has no fields/methods
86+
// referencing the target package beyond its own class name).
87+
val details = FixtureBase::class.toFileCopyDetails()
88+
89+
val result = details.remapClass(relocators)
90+
91+
val classModel = ClassFile.of().parse(result)
92+
assertThat(classModel.thisClass().asInternalName()).isEqualTo(relocatedFixtureBase)
93+
}
94+
95+
@Test
96+
fun superclassIsRelocated() {
97+
val result = fixtureSubjectDetails.remapClass(relocators)
98+
99+
val classModel = ClassFile.of().parse(result)
100+
assertThat(classModel.superclass().get().asInternalName()).isEqualTo(relocatedFixtureBase)
101+
}
102+
103+
@Test
104+
fun fieldDescriptorIsRelocated() {
105+
val result = fixtureSubjectDetails.remapClass(relocators)
106+
107+
val classModel = ClassFile.of().parse(result)
108+
val fieldDescriptors = classModel.fields().map { it.fieldType().stringValue() }
109+
assertThat(fieldDescriptors).contains("L$relocatedFixtureBase;")
110+
}
111+
112+
@Test
113+
fun arrayFieldDescriptorIsRelocated() {
114+
val result = fixtureSubjectDetails.remapClass(relocators)
115+
116+
val classModel = ClassFile.of().parse(result)
117+
val fieldDescriptors = classModel.fields().map { it.fieldType().stringValue() }
118+
assertThat(fieldDescriptors).contains("[L$relocatedFixtureBase;")
119+
}
120+
121+
@Test
122+
fun array2dFieldDescriptorIsRelocated() {
123+
val result = fixtureSubjectDetails.remapClass(relocators)
124+
125+
val classModel = ClassFile.of().parse(result)
126+
val fieldDescriptors = classModel.fields().map { it.fieldType().stringValue() }
127+
assertThat(fieldDescriptors).contains("[[L$relocatedFixtureBase;")
128+
}
129+
130+
@Test
131+
fun methodDescriptorIsRelocated() {
132+
val result = fixtureSubjectDetails.remapClass(relocators)
133+
134+
val classModel = ClassFile.of().parse(result)
135+
val methodDescriptors = classModel.methods().map { it.methodType().stringValue() }
136+
assertThat(methodDescriptors).contains("(L$relocatedFixtureBase;)L$relocatedFixtureBase;")
137+
}
138+
139+
@Test
140+
fun methodMultipleArgsIsRelocated() {
141+
val result = fixtureSubjectDetails.remapClass(relocators)
142+
143+
val classModel = ClassFile.of().parse(result)
144+
val methodDescriptors = classModel.methods().map { it.methodType().stringValue() }
145+
assertThat(methodDescriptors)
146+
.contains("(L$relocatedFixtureBase;L$relocatedFixtureBase;)L$relocatedFixtureBase;")
147+
}
148+
149+
@ParameterizedTest
150+
@ValueSource(chars = ['B', 'C', 'D', 'F', 'I', 'J', 'S', 'Z'])
151+
fun primitivePlusClassMethodIsRelocated(primitiveDescriptor: Char) {
152+
val result = fixtureSubjectDetails.remapClass(relocators)
153+
154+
val classModel = ClassFile.of().parse(result)
155+
val methodDescriptors = classModel.methods().map { it.methodType().stringValue() }
156+
assertThat(methodDescriptors)
157+
.contains("(${primitiveDescriptor}L$relocatedFixtureBase;)L$relocatedFixtureBase;")
158+
}
159+
160+
@Test
161+
fun stringConstantIsRelocated() {
162+
val result = fixtureSubjectDetails.remapClass(relocators)
163+
164+
val classModel = ClassFile.of().parse(result)
165+
// Find the constant string in the bytecode.
166+
val stringConstants =
167+
classModel.constantPool().mapNotNull { entry ->
168+
if (entry is java.lang.classfile.constantpool.StringEntry) entry.stringValue() else null
169+
}
170+
assertThat(stringConstants)
171+
.contains($$"com.example.relocated.BytecodeRemappingTest$FixtureBase")
172+
}
173+
174+
@Test
175+
fun stringConstantNotRelocatedWhenSkipEnabled() {
176+
val skipRelocators =
177+
setOf(
178+
SimpleRelocator(
179+
"com.github.jengelman.gradle.plugins.shadow.internal",
180+
"com.example.relocated",
181+
skipStringConstants = true,
182+
)
183+
)
184+
val result = fixtureSubjectDetails.remapClass(skipRelocators)
185+
186+
val classModel = ClassFile.of().parse(result)
187+
val stringConstants =
188+
classModel.constantPool().mapNotNull { entry ->
189+
if (entry is java.lang.classfile.constantpool.StringEntry) entry.stringValue() else null
190+
}
191+
assertThat(stringConstants)
192+
.doesNotContain($$"com.example.relocated.BytecodeRemappingTest$FixtureBase")
193+
}
194+
195+
@Test
196+
fun multiClassDescriptorStringConstantIsRelocated() {
197+
val result = fixtureSubjectDetails.remapClass(relocators)
198+
199+
val classModel = ClassFile.of().parse(result)
200+
val stringConstants =
201+
classModel.constantPool().mapNotNull { entry ->
202+
if (entry is java.lang.classfile.constantpool.StringEntry) entry.stringValue() else null
203+
}
204+
// Verify that two adjacent class references in a single string constant are both relocated
205+
// (regression test for the issue-1403 pattern).
206+
assertThat(stringConstants)
207+
.contains(
208+
$$"()Lcom/example/relocated/BytecodeRemappingTest$FixtureBase;Lcom/example/relocated/BytecodeRemappingTest$FixtureBase;"
209+
)
210+
}
211+
212+
@Test
213+
fun interfaceIsRelocated() {
214+
val result = fixtureSubjectDetails.remapClass(relocators)
215+
216+
val classModel = ClassFile.of().parse(result)
217+
val interfaces = classModel.interfaces().map { it.asInternalName() }
218+
assertThat(interfaces)
219+
.contains($$"com/example/relocated/BytecodeRemappingTest$FixtureInterface")
220+
}
221+
222+
@Test
223+
fun signatureIsRelocated() {
224+
val result = fixtureSubjectDetails.remapClass(relocators)
225+
226+
val classModel = ClassFile.of().parse(result)
227+
val method = classModel.methods().first { it.methodName().stringValue() == "methodWithGeneric" }
228+
val signatureAttr = method.findAttribute(Attributes.signature())
229+
assertThat(signatureAttr.isPresent).isTrue()
230+
val sig = signatureAttr.get().signature().stringValue()
231+
assertThat(sig).contains("L$relocatedFixtureBase;")
232+
}
233+
234+
@Test
235+
fun localVariableIsRelocated() {
236+
val result = fixtureSubjectDetails.remapClass(relocators)
237+
238+
val classModel = ClassFile.of().parse(result)
239+
val method = classModel.methods().first { it.methodName().stringValue() == "method" }
240+
val code = method.code().get()
241+
val lvt = code.findAttribute(Attributes.localVariableTable())
242+
assertThat(lvt.isPresent).isTrue()
243+
val descriptors = lvt.get().localVariables().map { it.type().stringValue() }
244+
assertThat(descriptors).contains("L$relocatedFixtureBase;")
245+
}
246+
247+
@Test
248+
fun instructionIsRelocated() {
249+
val result = fixtureSubjectDetails.remapClass(relocators)
250+
251+
val classModel = ClassFile.of().parse(result)
252+
val method =
253+
classModel.methods().first { it.methodName().stringValue() == "methodWithCheckCast" }
254+
val code = method.code().get()
255+
256+
val hasRelocatedCheckCast =
257+
code.elementStream().anyMatch { element ->
258+
element is TypeCheckInstruction && element.type().asInternalName() == relocatedFixtureBase
259+
}
260+
assertThat(hasRelocatedCheckCast).isTrue()
261+
262+
val hasRelocatedInvoke =
263+
code.elementStream().anyMatch { element ->
264+
element is InvokeInstruction && element.owner().asInternalName() == relocatedFixtureBase
265+
}
266+
assertThat(hasRelocatedInvoke).isTrue()
267+
}
268+
269+
private fun KClass<*>.toFileCopyDetails() =
270+
object : FileCopyDetails by noOpDelegate() {
271+
private val _path = java.name.replace('.', '/') + ".class"
272+
private val _file =
273+
tempDir
274+
.resolve(_path)
275+
.createParentDirectories()
276+
.also { requireResourceAsPath(_path).copyTo(it) }
277+
.toFile()
278+
279+
override fun getPath(): String = _path
280+
281+
override fun getFile(): File = _file
282+
}
283+
284+
// ---------------------------------------------------------------------------
285+
// Fixture classes – declared as nested classes so their bytecode is compiled
286+
// into the test output directory and can be fetched via requireResourceAsPath.
287+
// ---------------------------------------------------------------------------
288+
289+
@Retention(AnnotationRetention.RUNTIME)
290+
@Target(AnnotationTarget.CLASS)
291+
annotation class FixtureAnnotation
292+
293+
interface FixtureInterface
294+
295+
open class FixtureBase
296+
297+
@Suppress("unused") // Used by parsing bytecode.
298+
@FixtureAnnotation
299+
class FixtureSubject : FixtureBase(), FixtureInterface {
300+
val field: FixtureBase = FixtureBase()
301+
val arrayField: Array<FixtureBase> = emptyArray()
302+
val array2dField: Array<Array<FixtureBase>> = emptyArray()
303+
val stringConstant: String =
304+
$$"com.github.jengelman.gradle.plugins.shadow.internal.BytecodeRemappingTest$FixtureBase"
305+
val multiClassDescriptor: String =
306+
$$"()Lcom/github/jengelman/gradle/plugins/shadow/internal/BytecodeRemappingTest$FixtureBase;Lcom/github/jengelman/gradle/plugins/shadow/internal/BytecodeRemappingTest$FixtureBase;"
307+
308+
fun method(arg: FixtureBase): FixtureBase = arg
309+
310+
fun methodMultiArgs(a: FixtureBase, b: FixtureBase): FixtureBase = a
311+
312+
fun methodWithPrimitivePlusClass(b: Byte, arg: FixtureBase): FixtureBase = arg
313+
314+
fun methodWithCharPlusClass(c: Char, arg: FixtureBase): FixtureBase = arg
315+
316+
fun methodWithDoublePlusClass(d: Double, arg: FixtureBase): FixtureBase = arg
317+
318+
fun methodWithFloatPlusClass(f: Float, arg: FixtureBase): FixtureBase = arg
319+
320+
fun methodWithIntPlusClass(i: Int, arg: FixtureBase): FixtureBase = arg
321+
322+
fun methodWithLongPlusClass(l: Long, arg: FixtureBase): FixtureBase = arg
323+
324+
fun methodWithShortPlusClass(s: Short, arg: FixtureBase): FixtureBase = arg
325+
326+
fun methodWithBooleanPlusClass(z: Boolean, arg: FixtureBase): FixtureBase = arg
327+
328+
fun methodWithCheckCast(arg: Any): FixtureBase {
329+
(arg as FixtureBase).toString()
330+
return arg
331+
}
332+
333+
fun methodWithGeneric(list: List<FixtureBase>): FixtureBase = list[0]
334+
}
335+
}

0 commit comments

Comments
 (0)