-
-
Notifications
You must be signed in to change notification settings - Fork 423
Add tests for remapClass #1974
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Add tests for remapClass #1974
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
335 changes: 335 additions & 0 deletions
335
src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/BytecodeRemappingTest.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,335 @@ | ||
| package com.github.jengelman.gradle.plugins.shadow.internal | ||
|
|
||
| import assertk.assertThat | ||
| import assertk.assertions.contains | ||
| import assertk.assertions.doesNotContain | ||
| import assertk.assertions.isEqualTo | ||
| import assertk.assertions.isTrue | ||
| import com.github.jengelman.gradle.plugins.shadow.relocation.SimpleRelocator | ||
| import com.github.jengelman.gradle.plugins.shadow.testkit.requireResourceAsPath | ||
| import com.github.jengelman.gradle.plugins.shadow.util.noOpDelegate | ||
| import java.io.File | ||
| import java.lang.classfile.Attributes | ||
| import java.lang.classfile.ClassFile | ||
| import java.lang.classfile.instruction.InvokeInstruction | ||
| import java.lang.classfile.instruction.TypeCheckInstruction | ||
| import java.nio.file.Path | ||
| import kotlin.io.path.copyTo | ||
| import kotlin.io.path.createParentDirectories | ||
| import kotlin.reflect.KClass | ||
| import org.gradle.api.file.FileCopyDetails | ||
| import org.junit.jupiter.api.Test | ||
| import org.junit.jupiter.api.io.TempDir | ||
| import org.junit.jupiter.params.ParameterizedTest | ||
| import org.junit.jupiter.params.provider.ValueSource | ||
|
|
||
| /** | ||
| * The cases reflect the cases in | ||
| * [com.github.jengelman.gradle.plugins.shadow.relocation.RelocatorsTest], but operate on the | ||
| * bytecode level to verify that the remapper correctly transforms class names in all relevant | ||
| * bytecode structures. | ||
| */ | ||
| class BytecodeRemappingTest { | ||
| @TempDir lateinit var tempDir: Path | ||
|
|
||
| // Relocator used across all relocation tests: moves the test package to a distinct target. | ||
| private val relocators = | ||
| setOf( | ||
| SimpleRelocator( | ||
| "com.github.jengelman.gradle.plugins.shadow.internal", | ||
| "com.example.relocated", | ||
| ) | ||
| ) | ||
|
|
||
| // Internal name of the relocated FixtureBase for use in assertions. | ||
| private val relocatedFixtureBase = $$"com/example/relocated/BytecodeRemappingTest$FixtureBase" | ||
|
|
||
| private val fixtureSubjectDetails | ||
| get() = FixtureSubject::class.toFileCopyDetails() | ||
|
|
||
| @Test | ||
| fun classNotModified() { | ||
| val details = fixtureSubjectDetails | ||
| // Relocator pattern does not match – original bytes must be returned as-is. | ||
| val noMatchRelocators = setOf(SimpleRelocator("org.unrelated", "org.other")) | ||
|
|
||
| val result = details.remapClass(noMatchRelocators) | ||
|
|
||
| assertThat(result).isEqualTo(details.file.readBytes()) | ||
| } | ||
|
|
||
| @Test | ||
| fun classNameIsRelocated() { | ||
| val result = fixtureSubjectDetails.remapClass(relocators) | ||
|
|
||
| val classModel = ClassFile.of().parse(result) | ||
| assertThat(classModel.thisClass().asInternalName()) | ||
| .isEqualTo($$"com/example/relocated/BytecodeRemappingTest$FixtureSubject") | ||
| } | ||
|
|
||
| @Test | ||
| fun annotationIsRelocated() { | ||
| val result = fixtureSubjectDetails.remapClass(relocators) | ||
|
|
||
| val classModel = ClassFile.of().parse(result) | ||
| val annotationsAttr = classModel.findAttribute(Attributes.runtimeVisibleAnnotations()) | ||
| assertThat(annotationsAttr.isPresent).isTrue() | ||
| val annotationDescriptors = | ||
| annotationsAttr.get().annotations().map { it.className().stringValue() } | ||
| assertThat(annotationDescriptors) | ||
| .contains($$"Lcom/example/relocated/BytecodeRemappingTest$FixtureAnnotation;") | ||
| } | ||
|
|
||
| @Test | ||
| fun baseClassNameIsRelocated() { | ||
| // Verify relocation also works on a simple class (FixtureBase has no fields/methods | ||
| // referencing the target package beyond its own class name). | ||
| val details = FixtureBase::class.toFileCopyDetails() | ||
|
|
||
| val result = details.remapClass(relocators) | ||
|
|
||
| val classModel = ClassFile.of().parse(result) | ||
| assertThat(classModel.thisClass().asInternalName()).isEqualTo(relocatedFixtureBase) | ||
| } | ||
|
|
||
| @Test | ||
| fun superclassIsRelocated() { | ||
| val result = fixtureSubjectDetails.remapClass(relocators) | ||
|
|
||
| val classModel = ClassFile.of().parse(result) | ||
| assertThat(classModel.superclass().get().asInternalName()).isEqualTo(relocatedFixtureBase) | ||
| } | ||
|
|
||
| @Test | ||
| fun fieldDescriptorIsRelocated() { | ||
| val result = fixtureSubjectDetails.remapClass(relocators) | ||
|
|
||
| val classModel = ClassFile.of().parse(result) | ||
| val fieldDescriptors = classModel.fields().map { it.fieldType().stringValue() } | ||
| assertThat(fieldDescriptors).contains("L$relocatedFixtureBase;") | ||
| } | ||
|
|
||
| @Test | ||
| fun arrayFieldDescriptorIsRelocated() { | ||
| val result = fixtureSubjectDetails.remapClass(relocators) | ||
|
|
||
| val classModel = ClassFile.of().parse(result) | ||
| val fieldDescriptors = classModel.fields().map { it.fieldType().stringValue() } | ||
| assertThat(fieldDescriptors).contains("[L$relocatedFixtureBase;") | ||
| } | ||
|
|
||
| @Test | ||
| fun array2dFieldDescriptorIsRelocated() { | ||
| val result = fixtureSubjectDetails.remapClass(relocators) | ||
|
|
||
| val classModel = ClassFile.of().parse(result) | ||
| val fieldDescriptors = classModel.fields().map { it.fieldType().stringValue() } | ||
| assertThat(fieldDescriptors).contains("[[L$relocatedFixtureBase;") | ||
| } | ||
|
|
||
| @Test | ||
| fun methodDescriptorIsRelocated() { | ||
| val result = fixtureSubjectDetails.remapClass(relocators) | ||
|
|
||
| val classModel = ClassFile.of().parse(result) | ||
| val methodDescriptors = classModel.methods().map { it.methodType().stringValue() } | ||
| assertThat(methodDescriptors).contains("(L$relocatedFixtureBase;)L$relocatedFixtureBase;") | ||
| } | ||
|
|
||
| @Test | ||
| fun methodMultipleArgsIsRelocated() { | ||
| val result = fixtureSubjectDetails.remapClass(relocators) | ||
|
|
||
| val classModel = ClassFile.of().parse(result) | ||
| val methodDescriptors = classModel.methods().map { it.methodType().stringValue() } | ||
| assertThat(methodDescriptors) | ||
| .contains("(L$relocatedFixtureBase;L$relocatedFixtureBase;)L$relocatedFixtureBase;") | ||
| } | ||
|
|
||
| @ParameterizedTest | ||
| @ValueSource(chars = ['B', 'C', 'D', 'F', 'I', 'J', 'S', 'Z']) | ||
| fun primitivePlusClassMethodIsRelocated(primitiveDescriptor: Char) { | ||
| val result = fixtureSubjectDetails.remapClass(relocators) | ||
|
|
||
| val classModel = ClassFile.of().parse(result) | ||
| val methodDescriptors = classModel.methods().map { it.methodType().stringValue() } | ||
| assertThat(methodDescriptors) | ||
| .contains("(${primitiveDescriptor}L$relocatedFixtureBase;)L$relocatedFixtureBase;") | ||
| } | ||
|
|
||
| @Test | ||
| fun stringConstantIsRelocated() { | ||
| val result = fixtureSubjectDetails.remapClass(relocators) | ||
|
|
||
| val classModel = ClassFile.of().parse(result) | ||
| // Find the constant string in the bytecode. | ||
| val stringConstants = | ||
| classModel.constantPool().mapNotNull { entry -> | ||
| if (entry is java.lang.classfile.constantpool.StringEntry) entry.stringValue() else null | ||
| } | ||
| assertThat(stringConstants) | ||
| .contains($$"com.example.relocated.BytecodeRemappingTest$FixtureBase") | ||
| } | ||
|
|
||
| @Test | ||
| fun stringConstantNotRelocatedWhenSkipEnabled() { | ||
| val skipRelocators = | ||
| setOf( | ||
| SimpleRelocator( | ||
| "com.github.jengelman.gradle.plugins.shadow.internal", | ||
| "com.example.relocated", | ||
| skipStringConstants = true, | ||
| ) | ||
| ) | ||
| val result = fixtureSubjectDetails.remapClass(skipRelocators) | ||
|
|
||
| val classModel = ClassFile.of().parse(result) | ||
| val stringConstants = | ||
| classModel.constantPool().mapNotNull { entry -> | ||
| if (entry is java.lang.classfile.constantpool.StringEntry) entry.stringValue() else null | ||
| } | ||
| assertThat(stringConstants) | ||
| .doesNotContain($$"com.example.relocated.BytecodeRemappingTest$FixtureBase") | ||
| } | ||
|
|
||
| @Test | ||
| fun multiClassDescriptorStringConstantIsRelocated() { | ||
| val result = fixtureSubjectDetails.remapClass(relocators) | ||
|
|
||
| val classModel = ClassFile.of().parse(result) | ||
| val stringConstants = | ||
| classModel.constantPool().mapNotNull { entry -> | ||
| if (entry is java.lang.classfile.constantpool.StringEntry) entry.stringValue() else null | ||
| } | ||
| // Verify that two adjacent class references in a single string constant are both relocated | ||
| // (regression test for the issue-1403 pattern). | ||
| assertThat(stringConstants) | ||
| .contains( | ||
| $$"()Lcom/example/relocated/BytecodeRemappingTest$FixtureBase;Lcom/example/relocated/BytecodeRemappingTest$FixtureBase;" | ||
| ) | ||
| } | ||
|
|
||
| @Test | ||
| fun interfaceIsRelocated() { | ||
| val result = fixtureSubjectDetails.remapClass(relocators) | ||
|
|
||
| val classModel = ClassFile.of().parse(result) | ||
| val interfaces = classModel.interfaces().map { it.asInternalName() } | ||
| assertThat(interfaces) | ||
| .contains($$"com/example/relocated/BytecodeRemappingTest$FixtureInterface") | ||
| } | ||
|
|
||
| @Test | ||
| fun signatureIsRelocated() { | ||
| val result = fixtureSubjectDetails.remapClass(relocators) | ||
|
|
||
| val classModel = ClassFile.of().parse(result) | ||
| val method = classModel.methods().first { it.methodName().stringValue() == "methodWithGeneric" } | ||
| val signatureAttr = method.findAttribute(Attributes.signature()) | ||
| assertThat(signatureAttr.isPresent).isTrue() | ||
| val sig = signatureAttr.get().signature().stringValue() | ||
| assertThat(sig).contains("L$relocatedFixtureBase;") | ||
| } | ||
|
|
||
| @Test | ||
| fun localVariableIsRelocated() { | ||
| val result = fixtureSubjectDetails.remapClass(relocators) | ||
|
|
||
| val classModel = ClassFile.of().parse(result) | ||
| val method = classModel.methods().first { it.methodName().stringValue() == "method" } | ||
| val code = method.code().get() | ||
| val lvt = code.findAttribute(Attributes.localVariableTable()) | ||
| assertThat(lvt.isPresent).isTrue() | ||
| val descriptors = lvt.get().localVariables().map { it.type().stringValue() } | ||
| assertThat(descriptors).contains("L$relocatedFixtureBase;") | ||
| } | ||
|
|
||
| @Test | ||
| fun instructionIsRelocated() { | ||
| val result = fixtureSubjectDetails.remapClass(relocators) | ||
|
|
||
| val classModel = ClassFile.of().parse(result) | ||
| val method = | ||
| classModel.methods().first { it.methodName().stringValue() == "methodWithCheckCast" } | ||
| val code = method.code().get() | ||
|
|
||
| val hasRelocatedCheckCast = | ||
| code.elementStream().anyMatch { element -> | ||
| element is TypeCheckInstruction && element.type().asInternalName() == relocatedFixtureBase | ||
| } | ||
| assertThat(hasRelocatedCheckCast).isTrue() | ||
|
|
||
| val hasRelocatedInvoke = | ||
| code.elementStream().anyMatch { element -> | ||
| element is InvokeInstruction && element.owner().asInternalName() == relocatedFixtureBase | ||
| } | ||
| assertThat(hasRelocatedInvoke).isTrue() | ||
| } | ||
|
|
||
| private fun KClass<*>.toFileCopyDetails() = | ||
| object : FileCopyDetails by noOpDelegate() { | ||
| private val _path = java.name.replace('.', '/') + ".class" | ||
| private val _file = | ||
| tempDir | ||
| .resolve(_path) | ||
| .createParentDirectories() | ||
| .also { requireResourceAsPath(_path).copyTo(it) } | ||
| .toFile() | ||
|
|
||
| override fun getPath(): String = _path | ||
|
|
||
| override fun getFile(): File = _file | ||
| } | ||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // Fixture classes – declared as nested classes so their bytecode is compiled | ||
| // into the test output directory and can be fetched via requireResourceAsPath. | ||
| // --------------------------------------------------------------------------- | ||
|
|
||
| @Retention(AnnotationRetention.RUNTIME) | ||
| @Target(AnnotationTarget.CLASS) | ||
| annotation class FixtureAnnotation | ||
|
|
||
| interface FixtureInterface | ||
|
|
||
| open class FixtureBase | ||
|
|
||
| @Suppress("unused") // Used by parsing bytecode. | ||
| @FixtureAnnotation | ||
| class FixtureSubject : FixtureBase(), FixtureInterface { | ||
| val field: FixtureBase = FixtureBase() | ||
| val arrayField: Array<FixtureBase> = emptyArray() | ||
| val array2dField: Array<Array<FixtureBase>> = emptyArray() | ||
| val stringConstant: String = | ||
| $$"com.github.jengelman.gradle.plugins.shadow.internal.BytecodeRemappingTest$FixtureBase" | ||
| val multiClassDescriptor: String = | ||
| $$"()Lcom/github/jengelman/gradle/plugins/shadow/internal/BytecodeRemappingTest$FixtureBase;Lcom/github/jengelman/gradle/plugins/shadow/internal/BytecodeRemappingTest$FixtureBase;" | ||
|
|
||
| fun method(arg: FixtureBase): FixtureBase = arg | ||
|
|
||
| fun methodMultiArgs(a: FixtureBase, b: FixtureBase): FixtureBase = a | ||
|
|
||
| fun methodWithPrimitivePlusClass(b: Byte, arg: FixtureBase): FixtureBase = arg | ||
|
|
||
| fun methodWithCharPlusClass(c: Char, arg: FixtureBase): FixtureBase = arg | ||
|
|
||
| fun methodWithDoublePlusClass(d: Double, arg: FixtureBase): FixtureBase = arg | ||
|
|
||
| fun methodWithFloatPlusClass(f: Float, arg: FixtureBase): FixtureBase = arg | ||
|
|
||
| fun methodWithIntPlusClass(i: Int, arg: FixtureBase): FixtureBase = arg | ||
|
|
||
| fun methodWithLongPlusClass(l: Long, arg: FixtureBase): FixtureBase = arg | ||
|
|
||
| fun methodWithShortPlusClass(s: Short, arg: FixtureBase): FixtureBase = arg | ||
|
|
||
| fun methodWithBooleanPlusClass(z: Boolean, arg: FixtureBase): FixtureBase = arg | ||
|
|
||
| fun methodWithCheckCast(arg: Any): FixtureBase { | ||
| (arg as FixtureBase).toString() | ||
| return arg | ||
| } | ||
|
|
||
| fun methodWithGeneric(list: List<FixtureBase>): FixtureBase = list[0] | ||
| } | ||
| } | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.