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; + } } }