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>
+   * &#064;RunWith(JUnit4.class)
+   * public class DesugarRuleTest {
+   *
+   *   &#064;Rule
+   *   public final DesugarRule desugarRule =
+   *       DesugarRule.builder(this, MethodHandles.lookup())
+   *           .addRuntimeInputs("path/to/my_jar.jar")
+   *           .build();
+   *
+   *   &#064;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>
+   * &#064;RunWith(JUnit4.class)
+   * public class DesugarRuleTest {
+   *
+   *   &#064;Rule
+   *   public final DesugarRule desugarRule =
+   *       DesugarRule.builder(this, MethodHandles.lookup())
+   *           .addRuntimeInputs("path/to/my_jar.jar")
+   *           .build();
+   *
+   *   &#064;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;
+    }
   }
 }