Java 11 Desugar Testing Infra Support Add-Ons
- Previous: https://github.com/bazelbuild/bazel/commit/69e0894fe44f43f60d8be55207be55e267dd4b9b
- Support the injection of org.objectweb.asm.tree.ClassNode
for structured assertions and analysis.
- Support iterative desugaring for testing Idempotency.
(feeding the desugared jar as the input as re-desugaring)
PiperOrigin-RevId: 284681352
diff --git a/src/test/java/com/google/devtools/build/android/desugar/DesugarRule.java b/src/test/java/com/google/devtools/build/android/desugar/DesugarRule.java
index 4f8a664..1df8ae5 100644
--- a/src/test/java/com/google/devtools/build/android/desugar/DesugarRule.java
+++ b/src/test/java/com/google/devtools/build/android/desugar/DesugarRule.java
@@ -26,6 +26,7 @@
import com.google.devtools.build.runtime.RunfilesPaths;
import com.google.errorprone.annotations.FormatMethod;
import java.io.IOException;
+import java.io.InputStream;
import java.lang.annotation.Annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
@@ -51,11 +52,16 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
import org.junit.rules.TemporaryFolder;
import org.junit.rules.TestRule;
import org.junit.runner.Description;
import org.junit.runner.RunWith;
import org.junit.runners.model.Statement;
+import org.objectweb.asm.ClassReader;
+import org.objectweb.asm.Opcodes;
+import org.objectweb.asm.tree.ClassNode;
/** A JUnit4 Rule that desugars an input jar file and load the transformed jar to JVM. */
public class DesugarRule implements TestRule {
@@ -72,11 +78,11 @@
* @Rule
* public final DesugarRule desugarRule =
* DesugarRule.builder(this, MethodHandles.lookup())
- * .addRuntimeInputs("MyJar.jar")
+ * .addRuntimeInputs("path/to/my_jar.jar")
* .build();
*
* @LoadClass("my.package.ClassToDesugar")
- * private Class<?> classToDesugarClass;f
+ * private Class<?> classToDesugarClass;
*
* // ... Test methods ...
* }
@@ -98,6 +104,81 @@
int round() default 1;
}
+ /**
+ * Identifies injectable {@link ZipEntry} fields with a zip entry path. The desugar rule resolves
+ * the requested zip entry at runtime and assign it to the annotated field. An injectable {@link
+ * ZipEntry} field may have any access modifier (private, package-private, protected, public).
+ * Sample usage:
+ *
+ * <pre><code>
+ * @RunWith(JUnit4.class)
+ * public class DesugarRuleTest {
+ *
+ * @Rule
+ * public final DesugarRule desugarRule =
+ * DesugarRule.builder(this, MethodHandles.lookup())
+ * .addRuntimeInputs("path/to/my_jar.jar")
+ * .build();
+ *
+ * @LoadZipEntry("my/package/ClassToDesugar.class")
+ * private ZipEntry classToDesugarClassFile;
+ *
+ * // ... Test methods ...
+ * }
+ * </code></pre>
+ */
+ @UsedReflectively
+ @Documented
+ @Target(ElementType.FIELD)
+ @Retention(RetentionPolicy.RUNTIME)
+ public @interface LoadZipEntry {
+
+ /** The requested zip entry path name within a zip file. */
+ String value();
+
+ /** The round during which its associated jar is being used. */
+ int round() default 1;
+ }
+
+ /**
+ * Identifies injectable {@link ClassNode} fields with a qualified class name. The desugar rule
+ * resolves the requested class at runtime, parse it into a {@link ClassNode} and assign parsed
+ * class node to the annotated field. An injectable {@link ClassNode} field may have any access
+ * modifier (private, package-private, protected, public). Sample usage:
+ *
+ * <pre><code>
+ * @RunWith(JUnit4.class)
+ * public class DesugarRuleTest {
+ *
+ * @Rule
+ * public final DesugarRule desugarRule =
+ * DesugarRule.builder(this, MethodHandles.lookup())
+ * .addRuntimeInputs("path/to/my_jar.jar")
+ * .build();
+ *
+ * @LoadClassNode("my.package.ClassToDesugar")
+ * private ClassNode classToDesugarClassFile;
+ *
+ * // ... Test methods ...
+ * }
+ * </code></pre>
+ */
+ @UsedReflectively
+ @Documented
+ @Target(ElementType.FIELD)
+ @Retention(RetentionPolicy.RUNTIME)
+ public @interface LoadClassNode {
+
+ /**
+ * The fully-qualified class name of the class to load. The format agrees with {@link
+ * Class#getName}.
+ */
+ String value();
+
+ /** The round during which its associated jar is being used. */
+ int round() default 1;
+ }
+
private static final Path ANDROID_RUNTIME_JAR_PATH =
RunfilesPaths.resolve(
"third_party/java/android/android_sdk_linux/platforms/experimental/android_blaze.jar");
@@ -171,11 +252,17 @@
private final Object testInstance;
private final MethodHandles.Lookup testInstanceLookup;
+
+ /** The maximum number of desugar operations, used for testing idempotency. */
private final int maxNumOfTransformations;
+
+ private final ImmutableList<Field> injectableClassLiterals;
+ private final ImmutableList<Field> injectableAstClassNodes;
+ private final ImmutableList<Field> injectableZipEntries;
+
private final ImmutableList<Path> inputs;
private final ImmutableList<Path> classPathEntries;
private final ImmutableList<Path> bootClassPathEntries;
- private final ImmutableList<Field> fieldForDynamicClassLoading;
private final ImmutableListMultimap<String, String> extraCustomCommandOptions;
private final List<JarTransformationRecord> jarTransformationRecords;
@@ -194,22 +281,26 @@
}
private DesugarRule(
- DesugarRuleBuilder desugarRuleBuilder,
+ Object testInstance,
Lookup testInstanceLookup,
int maxNumOfTransformations,
+ ImmutableList<Field> injectableClassLiterals,
+ ImmutableList<Field> injectableAstClassNodes,
+ ImmutableList<Field> injectableZipEntries,
ImmutableList<Path> inputJars,
ImmutableList<Path> classPathEntries,
- ImmutableList<Path> bootClassPathEntries) {
- this.testInstance = desugarRuleBuilder.testInstance;
+ ImmutableList<Path> bootClassPathEntries,
+ ImmutableListMultimap<String, String> customCommandOptions) {
+ this.testInstance = testInstance;
this.testInstanceLookup = testInstanceLookup;
this.maxNumOfTransformations = maxNumOfTransformations;
+ this.injectableClassLiterals = injectableClassLiterals;
+ this.injectableAstClassNodes = injectableAstClassNodes;
+ this.injectableZipEntries = injectableZipEntries;
this.inputs = inputJars;
this.classPathEntries = classPathEntries;
this.bootClassPathEntries = bootClassPathEntries;
- this.extraCustomCommandOptions =
- ImmutableListMultimap.copyOf(desugarRuleBuilder.customCommandOptions);
- this.fieldForDynamicClassLoading =
- findAllFieldsWithAnnotation(testInstance.getClass(), LoadClass.class);
+ this.extraCustomCommandOptions = customCommandOptions;
this.jarTransformationRecords = new ArrayList<>(maxNumOfTransformations);
}
@@ -226,7 +317,7 @@
transInputs,
temporaryFolder,
tempDirs,
- /* defaultOutputRootPrefix= */ DEFAULT_OUTPUT_ROOT_PREFIX + "_" + i);
+ /* outputRootPrefix= */ DEFAULT_OUTPUT_ROOT_PREFIX + "_" + i);
JarTransformationRecord transformationRecord =
JarTransformationRecord.create(
transInputs,
@@ -240,24 +331,101 @@
transInputs = transOutputs;
}
- for (Field field : fieldForDynamicClassLoading) {
- LoadClass loadClassAnnotation = field.getDeclaredAnnotation(LoadClass.class);
- String qualifiedClassName = loadClassAnnotation.value();
- int round = loadClassAnnotation.round();
- ClassLoader outputJarClassLoader =
- round == 0
- ? getInputClassLoader()
- : jarTransformationRecords.get(round - 1).getOutputClassLoader();
- Class<?> classLiteral = outputJarClassLoader.loadClass(qualifiedClassName);
+ for (Field field : injectableClassLiterals) {
+ Class<?> classLiteral =
+ getClassLiteral(
+ field.getDeclaredAnnotation(LoadClass.class),
+ getInputClassLoader(),
+ jarTransformationRecords);
MethodHandle fieldSetter = testInstanceLookup.unreflectSetter(field);
fieldSetter.invoke(testInstance, classLiteral);
}
+
+ for (Field field : injectableAstClassNodes) {
+ ClassNode classNode =
+ getAstClassNode(
+ field.getDeclaredAnnotation(LoadClassNode.class),
+ inputs,
+ jarTransformationRecords);
+ MethodHandle fieldSetter = testInstanceLookup.unreflectSetter(field);
+ fieldSetter.invoke(testInstance, classNode);
+ }
+
+ for (Field field : injectableZipEntries) {
+ ZipEntry zipEntry = getZipEntry(field.getDeclaredAnnotation(LoadZipEntry.class));
+ MethodHandle fieldSetter = testInstanceLookup.unreflectSetter(field);
+ fieldSetter.invoke(testInstance, zipEntry);
+ }
base.evaluate();
}
},
description);
}
+ private static Class<?> getClassLiteral(
+ LoadClass classLiteralRequestInfo,
+ ClassLoader initialInputClassLoader,
+ List<JarTransformationRecord> jarTransformationRecords)
+ throws Throwable {
+ String qualifiedClassName = classLiteralRequestInfo.value();
+ int round = classLiteralRequestInfo.round();
+ ClassLoader outputJarClassLoader =
+ round == 0
+ ? initialInputClassLoader
+ : jarTransformationRecords.get(round - 1).getOutputClassLoader();
+ return outputJarClassLoader.loadClass(qualifiedClassName);
+ }
+
+ private static ClassNode getAstClassNode(
+ LoadClassNode astNodeRequestInfo,
+ ImmutableList<Path> initialInputs,
+ List<JarTransformationRecord> jarTransformationRecords)
+ throws IOException, ClassNotFoundException {
+ String qualifiedClassName = astNodeRequestInfo.value();
+ String classFileName = qualifiedClassName.replace('.', '/') + ".class";
+ int round = astNodeRequestInfo.round();
+ ImmutableList<Path> jars =
+ round == 0 ? initialInputs : jarTransformationRecords.get(round - 1).outputJars();
+ ClassNode classNode = findClassNode(classFileName, jars);
+ if (classNode == null) {
+ throw new ClassNotFoundException(qualifiedClassName);
+ }
+ return classNode;
+ }
+
+ private ZipEntry getZipEntry(LoadZipEntry zipEntryRequestInfo) throws IOException {
+ String zipEntryPathName = zipEntryRequestInfo.value();
+ int round = zipEntryRequestInfo.round();
+ ImmutableList<Path> jars =
+ round == 0 ? inputs : jarTransformationRecords.get(round - 1).outputJars();
+ for (Path jar : jars) {
+ ZipFile zipFile = new ZipFile(jar.toFile());
+ ZipEntry zipEntry = zipFile.getEntry(zipEntryPathName);
+ if (zipEntry != null) {
+ return zipEntry;
+ }
+ }
+ throw new IllegalStateException(
+ String.format("Expected zip entry of (%s) present.", zipEntryPathName));
+ }
+
+ private static ClassNode findClassNode(String zipEntryPathName, ImmutableList<Path> jars)
+ throws IOException {
+ for (Path jar : jars) {
+ ZipFile zipFile = new ZipFile(jar.toFile());
+ ZipEntry zipEntry = zipFile.getEntry(zipEntryPathName);
+ if (zipEntry != null) {
+ try (InputStream inputStream = zipFile.getInputStream(zipEntry)) {
+ ClassReader cr = new ClassReader(inputStream);
+ ClassNode classNode = new ClassNode(Opcodes.ASM7);
+ cr.accept(classNode, 0);
+ return classNode;
+ }
+ }
+ }
+ return null;
+ }
+
private ClassLoader getInputClassLoader() throws MalformedURLException {
List<URL> urls = new ArrayList<>();
for (Path path : Iterables.concat(inputs, classPathEntries, bootClassPathEntries)) {
@@ -270,11 +438,11 @@
ImmutableList<Path> inputs,
TemporaryFolder temporaryFolder,
Map<String, Path> tempDirs,
- String defaultOutputRootPrefix)
+ String outputRootPrefix)
throws IOException {
ImmutableList.Builder<Path> outputRuntimePathsBuilder = ImmutableList.builder();
for (Path path : inputs) {
- String targetDirKey = Paths.get(defaultOutputRootPrefix) + "/" + path.getParent();
+ String targetDirKey = Paths.get(outputRootPrefix) + "/" + path.getParent();
final Path outputDirPath;
if (tempDirs.containsKey(targetDirKey)) {
outputDirPath = tempDirs.get(targetDirKey);
@@ -307,7 +475,9 @@
private final Object testInstance;
private final MethodHandles.Lookup testInstanceLookup;
- private final ImmutableList<Field> classLiteralFieldToBeLoaded;
+ private final ImmutableList<Field> injectableClassLiterals;
+ private final ImmutableList<Field> injectableAstClassNodes;
+ private final ImmutableList<Field> injectableZipEntries;
private int maxNumOfTransformations;
private final List<Path> inputs = new ArrayList<>();
private final List<Path> classPathEntries = new ArrayList<>();
@@ -330,7 +500,9 @@
"Expected a test instance whose class is annotated with @RunWith. %s", testClass);
}
- classLiteralFieldToBeLoaded = findAllFieldsWithAnnotation(testClass, LoadClass.class);
+ injectableClassLiterals = findAllFieldsWithAnnotation(testClass, LoadClass.class);
+ injectableAstClassNodes = findAllFieldsWithAnnotation(testClass, LoadClassNode.class);
+ injectableZipEntries = findAllFieldsWithAnnotation(testClass, LoadZipEntry.class);
}
public DesugarRuleBuilder enableIterativeTransformation(int maxNumOfTransformations) {
@@ -384,7 +556,9 @@
public DesugarRule build() {
checkJVMOptions();
- checkClassLiteralFieldToBeLoaded();
+ checkInjectableClassLiterals();
+ checkInjectableAstNodes();
+ checkInjectableZipEntries();
if (bootClassPathEntries.isEmpty()
&& !customCommandOptions.containsKey("allow_empty_bootclasspath")) {
@@ -400,16 +574,20 @@
addClasspathEntries(JACOCO_RUNTIME_PATH);
return new DesugarRule(
- this,
+ testInstance,
testInstanceLookup,
maxNumOfTransformations,
+ injectableClassLiterals,
+ injectableAstClassNodes,
+ injectableZipEntries,
ImmutableList.copyOf(inputs),
ImmutableList.copyOf(classPathEntries),
- ImmutableList.copyOf(bootClassPathEntries));
+ ImmutableList.copyOf(bootClassPathEntries),
+ ImmutableListMultimap.copyOf(customCommandOptions));
}
- private void checkClassLiteralFieldToBeLoaded() {
- for (Field field : classLiteralFieldToBeLoaded) {
+ private void checkInjectableClassLiterals() {
+ for (Field field : injectableClassLiterals) {
if (Modifier.isStatic(field.getModifiers())) {
errorMessenger.addError("Expected to be non-static for field (%s)", field);
}
@@ -428,6 +606,48 @@
}
}
}
+
+ private void checkInjectableAstNodes() {
+ for (Field field : injectableAstClassNodes) {
+ if (Modifier.isStatic(field.getModifiers())) {
+ errorMessenger.addError("Expected to be non-static for field (%s)", field);
+ }
+
+ if (field.getType() != ClassNode.class) {
+ errorMessenger.addError(
+ "Expected a field with Type: (%s) but gets (%s)", ClassNode.class.getName(), field);
+ }
+
+ LoadClassNode astClassNodeInfo = field.getDeclaredAnnotation(LoadClassNode.class);
+ if (astClassNodeInfo.round() < 0 || astClassNodeInfo.round() > maxNumOfTransformations) {
+ errorMessenger.addError(
+ "Expected the round of desugar transformation within [0, %d], where 0 indicates no"
+ + " transformation is used.",
+ maxNumOfTransformations);
+ }
+ }
+ }
+
+ private void checkInjectableZipEntries() {
+ for (Field field : injectableZipEntries) {
+ if (Modifier.isStatic(field.getModifiers())) {
+ errorMessenger.addError("Expected to be non-static for field (%s)", field);
+ }
+
+ if (field.getType() != ZipEntry.class) {
+ errorMessenger.addError(
+ "Expected a field with Type: (%s) but gets (%s)", ZipEntry.class.getName(), field);
+ }
+
+ LoadZipEntry zipEntryInfo = field.getDeclaredAnnotation(LoadZipEntry.class);
+ if (zipEntryInfo.round() < 0 || zipEntryInfo.round() > maxNumOfTransformations) {
+ errorMessenger.addError(
+ "Expected the round of desugar transformation within [0, %d], where 0 indicates no"
+ + " transformation is used.",
+ maxNumOfTransformations);
+ }
+ }
+ }
}
/** A messenger that manages desugar configuration errors. */
diff --git a/src/test/java/com/google/devtools/build/android/desugar/DesugarRuleTest.java b/src/test/java/com/google/devtools/build/android/desugar/DesugarRuleTest.java
index f6ad262..22d1cd7 100644
--- a/src/test/java/com/google/devtools/build/android/desugar/DesugarRuleTest.java
+++ b/src/test/java/com/google/devtools/build/android/desugar/DesugarRuleTest.java
@@ -16,17 +16,22 @@
package com.google.devtools.build.android.desugar;
+import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth8.assertThat;
import static org.junit.Assert.assertThrows;
import com.google.devtools.build.android.desugar.DesugarRule.LoadClass;
+import com.google.devtools.build.android.desugar.DesugarRule.LoadClassNode;
+import com.google.devtools.build.android.desugar.DesugarRule.LoadZipEntry;
import java.lang.invoke.MethodHandles;
import java.lang.reflect.Method;
import java.util.Arrays;
+import java.util.zip.ZipEntry;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
+import org.objectweb.asm.tree.ClassNode;
/** The test for {@link DesugarRule}. */
@RunWith(JUnit4.class)
@@ -56,6 +61,21 @@
"com.google.devtools.build.android.desugar.DesugarRuleTestTarget$InterfaceSubjectToDesugar$$CC")
private Class<?> interfaceSubjectToDesugarCompanionClass;
+ @LoadZipEntry(
+ value =
+ "com/google/devtools/build/android/desugar/DesugarRuleTestTarget$InterfaceSubjectToDesugar$$CC.class",
+ round = 1)
+ private ZipEntry interfaceSubjectToDesugarZipEntryRound1;
+
+ @LoadZipEntry(
+ value =
+ "com/google/devtools/build/android/desugar/DesugarRuleTestTarget$InterfaceSubjectToDesugar$$CC.class",
+ round = 2)
+ private ZipEntry interfaceSubjectToDesugarZipEntryRound2;
+
+ @LoadClassNode("com.google.devtools.build.android.desugar.DesugarRuleTestTarget")
+ private ClassNode desugarRuleTestTargetClassNode;
+
@Test
public void staticMethodsAreMovedFromOriginatingClass() {
assertThrows(
@@ -77,4 +97,17 @@
.map(Method::getName))
.contains("staticMethod$$STATIC$$");
}
+
+ @Test
+ public void nestMembers() {
+ assertThat(desugarRuleTestTargetClassNode.nestMembers)
+ .containsExactly(
+ "com/google/devtools/build/android/desugar/DesugarRuleTestTarget$InterfaceSubjectToDesugar");
+ }
+
+ @Test
+ public void idempotencyOperation() {
+ assertThat(interfaceSubjectToDesugarZipEntryRound1.getCrc())
+ .isEqualTo(interfaceSubjectToDesugarZipEntryRound2.getCrc());
+ }
}
diff --git a/src/test/java/com/google/devtools/build/android/desugar/DesugarRuleTestTarget.java b/src/test/java/com/google/devtools/build/android/desugar/DesugarRuleTestTarget.java
index e82b8e3..0986f80 100644
--- a/src/test/java/com/google/devtools/build/android/desugar/DesugarRuleTestTarget.java
+++ b/src/test/java/com/google/devtools/build/android/desugar/DesugarRuleTestTarget.java
@@ -26,5 +26,9 @@
class DesugarRuleTestTarget {
interface InterfaceSubjectToDesugar {
static void staticMethod() {}
+
+ default int defaultMethod(int x, int y) {
+ return x + y;
+ }
}
}