diff options
| author | 2018-08-09 11:11:22 +0100 | |
|---|---|---|
| committer | 2018-08-20 15:04:08 +0100 | |
| commit | 3866641884c1661bbaae4864cbda2b61b8caaa86 (patch) | |
| tree | 8cee2b12b4bd27fe426eb43e46a71099080845eb | |
| parent | f345404c725330914b8313d2c1af17226c5b92ca (diff) | |
Support for maxTargetSdk on @UnsupportedAppUsage.
class2greylist now supports writing to multiple output greylist files
specified on the command line like so:
class2greylist --write-greylist lightgrey.txt \
--write-greylist 26:p-blacklist.txt \
--write-greylist 28:q-blacklist.txt
If no --write-greylist parameter is specified, then all annotated
interfaces will simply be written to standard out as before. This
maintains the previous behaviour, and de-couples this change from the
corresponding build changes.
Any maxTargetSdk parameters will be validated against the set of valid
values given in the --write-greylist parameters. If no such parameters
are given, the valid values are: 26 (O), 28 (P) or none.
Test: atest class2greylisttest
Bug: 110868826
Change-Id: Id001d5427b1d399f3af98444b501e8405531a66d
| -rw-r--r-- | tools/class2greylist/src/com/android/class2greylist/AnnotationVisitor.java | 85 | ||||
| -rw-r--r-- | tools/class2greylist/src/com/android/class2greylist/Class2Greylist.java | 119 | ||||
| -rw-r--r-- | tools/class2greylist/src/com/android/class2greylist/FileWritingGreylistConsumer.java | 48 | ||||
| -rw-r--r-- | tools/class2greylist/src/com/android/class2greylist/GreylistConsumer.java | 13 | ||||
| -rw-r--r-- | tools/class2greylist/src/com/android/class2greylist/Status.java | 8 | ||||
| -rw-r--r-- | tools/class2greylist/src/com/android/class2greylist/SystemOutGreylistConsumer.java | 12 | ||||
| -rw-r--r-- | tools/class2greylist/test/src/com/android/class2greylist/AnnotationVisitorTest.java (renamed from tools/class2greylist/test/src/com/android/javac/AnnotationVisitorTest.java) | 165 | ||||
| -rw-r--r-- | tools/class2greylist/test/src/com/android/class2greylist/Class2GreylistTest.java | 73 |
8 files changed, 428 insertions, 95 deletions
diff --git a/tools/class2greylist/src/com/android/class2greylist/AnnotationVisitor.java b/tools/class2greylist/src/com/android/class2greylist/AnnotationVisitor.java index 5914b26331..1838575582 100644 --- a/tools/class2greylist/src/com/android/class2greylist/AnnotationVisitor.java +++ b/tools/class2greylist/src/com/android/class2greylist/AnnotationVisitor.java @@ -17,16 +17,19 @@ package com.android.class2greylist; import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Joiner; import org.apache.bcel.Const; import org.apache.bcel.classfile.AnnotationEntry; import org.apache.bcel.classfile.DescendingVisitor; +import org.apache.bcel.classfile.ElementValue; import org.apache.bcel.classfile.ElementValuePair; import org.apache.bcel.classfile.EmptyVisitor; import org.apache.bcel.classfile.Field; import org.apache.bcel.classfile.FieldOrMethod; import org.apache.bcel.classfile.JavaClass; import org.apache.bcel.classfile.Method; +import org.apache.bcel.classfile.SimpleElementValue; import java.util.Locale; import java.util.Set; @@ -35,8 +38,8 @@ import java.util.function.Predicate; /** * Visits a JavaClass instance and pulls out all members annotated with a * specific annotation. The signatures of such members are passed to {@link - * Status#greylistEntry(String)}. Any errors result in a call to {@link - * Status#error(String)}. + * GreylistConsumer#greylistEntry(String, Integer)}. Any errors result in a + * call to {@link Status#error(String, Object...)}. * * If the annotation has a property "expectedSignature" the generated signature * will be verified against the one specified there. If it differs, an error @@ -45,10 +48,13 @@ import java.util.function.Predicate; public class AnnotationVisitor extends EmptyVisitor { private static final String EXPECTED_SIGNATURE = "expectedSignature"; + private static final String MAX_TARGET_SDK = "maxTargetSdk"; private final JavaClass mClass; private final String mAnnotationType; private final Predicate<Member> mMemberFilter; + private final Set<Integer> mValidMaxTargetSdkValues; + private final GreylistConsumer mConsumer; private final Status mStatus; private final DescendingVisitor mDescendingVisitor; @@ -66,27 +72,43 @@ public class AnnotationVisitor extends EmptyVisitor { * Indicates if this is a synthetic bridge method. */ public final boolean bridge; + /** + * Max target SDK of property this member, if it is set, else null. + * + * Note: even though the annotation itself specified a default value, + * that default value is not encoded into instances of the annotation + * in class files. So when no value is specified in source, it will + * result in null appearing in here. + */ + public final Integer maxTargetSdk; - public Member(String signature, boolean bridge) { + public Member(String signature, boolean bridge, Integer maxTargetSdk) { this.signature = signature; this.bridge = bridge; + this.maxTargetSdk = maxTargetSdk; } } - public AnnotationVisitor( - JavaClass clazz, String annotation, Set<String> publicApis, Status status) { + public AnnotationVisitor(JavaClass clazz, String annotation, Set<String> publicApis, + Set<Integer> validMaxTargetSdkValues, GreylistConsumer consumer, + Status status) { this(clazz, annotation, member -> !(member.bridge && publicApis.contains(member.signature)), + validMaxTargetSdkValues, + consumer, status); } @VisibleForTesting - public AnnotationVisitor( - JavaClass clazz, String annotation, Predicate<Member> memberFilter, Status status) { + public AnnotationVisitor(JavaClass clazz, String annotation, Predicate<Member> memberFilter, + Set<Integer> validMaxTargetSdkValues, GreylistConsumer consumer, + Status status) { mClass = clazz; mAnnotationType = annotation; mMemberFilter = memberFilter; + mValidMaxTargetSdkValues = validMaxTargetSdkValues; + mConsumer = consumer; mStatus = status; mDescendingVisitor = new DescendingVisitor(clazz, this); } @@ -127,27 +149,56 @@ public class AnnotationVisitor extends EmptyVisitor { } String signature = String.format(Locale.US, signatureFormatString, getClassDescriptor(definingClass), member.getName(), member.getSignature()); + Integer maxTargetSdk = null; for (ElementValuePair property : a.getElementValuePairs()) { switch (property.getNameString()) { case EXPECTED_SIGNATURE: - String expected = property.getValue().stringifyValue(); - // Don't enforce for bridge methods; they're generated so won't match. - if (!bridge && !signature.equals(expected)) { - error(definingClass, member, - "Expected signature does not match generated:\n" - + "Expected: %s\n" - + "Generated: %s", expected, signature); - } + verifyExpectedSignature( + property, signature, definingClass, member, bridge); + break; + case MAX_TARGET_SDK: + maxTargetSdk = verifyAndGetMaxTargetSdk( + property, definingClass, member); break; } } - if (mMemberFilter.test(new Member(signature, bridge))) { - mStatus.greylistEntry(signature); + if (mMemberFilter.test(new Member(signature, bridge, maxTargetSdk))) { + mConsumer.greylistEntry(signature, maxTargetSdk); } } } } + private void verifyExpectedSignature(ElementValuePair property, String signature, + JavaClass definingClass, FieldOrMethod member, boolean isBridge) { + String expected = property.getValue().stringifyValue(); + // Don't enforce for bridge methods; they're generated so won't match. + if (!isBridge && !signature.equals(expected)) { + error(definingClass, member, + "Expected signature does not match generated:\n" + + "Expected: %s\n" + + "Generated: %s", expected, signature); + } + } + + private Integer verifyAndGetMaxTargetSdk( + ElementValuePair property, JavaClass definingClass, FieldOrMethod member) { + if (property.getValue().getElementValueType() != ElementValue.PRIMITIVE_INT) { + error(definingClass, member, "Expected property %s to be of type int; got %d", + property.getNameString(), property.getValue().getElementValueType()); + } + int value = ((SimpleElementValue) property.getValue()).getValueInt(); + if (!mValidMaxTargetSdkValues.contains(value)) { + error(definingClass, member, + "Invalid value for %s: got %d, expected one of [%s]", + property.getNameString(), + value, + Joiner.on(",").join(mValidMaxTargetSdkValues)); + return null; + } + return value; + } + private void error(JavaClass clazz, FieldOrMethod member, String message, Object... args) { StringBuilder error = new StringBuilder(); error.append(clazz.getSourceFileName()) diff --git a/tools/class2greylist/src/com/android/class2greylist/Class2Greylist.java b/tools/class2greylist/src/com/android/class2greylist/Class2Greylist.java index abc9421e65..64a0357e13 100644 --- a/tools/class2greylist/src/com/android/class2greylist/Class2Greylist.java +++ b/tools/class2greylist/src/com/android/class2greylist/Class2Greylist.java @@ -16,6 +16,7 @@ package com.android.class2greylist; +import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.Sets; import com.google.common.io.Files; @@ -30,7 +31,11 @@ import org.apache.commons.cli.ParseException; import java.io.File; import java.io.IOException; import java.nio.charset.Charset; +import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; import java.util.Set; /** @@ -41,6 +46,11 @@ public class Class2Greylist { private static final String ANNOTATION_TYPE = "Landroid/annotation/UnsupportedAppUsage;"; + private final Status mStatus; + private final String mPublicApiListFile; + private final String[] mPerSdkOutputFiles; + private final String[] mJarFiles; + public static void main(String[] args) { Options options = new Options(); options.addOption(OptionBuilder @@ -49,6 +59,15 @@ public class Class2Greylist { .withDescription("Public API list file. Used to de-dupe bridge methods.") .create("p")); options.addOption(OptionBuilder + .withLongOpt("write-greylist") + .hasArgs() + .withDescription( + "Specify file to write greylist to. Can be specified multiple times. " + + "Format is either just a filename, or \"int:filename\". If an integer is " + + "given, members with a matching maxTargetSdk are written to the file; if " + + "no integer is given, members with no maxTargetSdk are written.") + .create("g")); + options.addOption(OptionBuilder .withLongOpt("debug") .hasArgs(0) .withDescription("Enable debug") @@ -72,7 +91,7 @@ public class Class2Greylist { if (cmd.hasOption('h')) { help(options); } - String publicApiFilename = cmd.getOptionValue('p', null); + String[] jarFiles = cmd.getArgs(); if (jarFiles.length == 0) { @@ -81,38 +100,98 @@ public class Class2Greylist { } Status status = new Status(cmd.hasOption('d')); + Class2Greylist c2gl = new Class2Greylist( + status, cmd.getOptionValue('p', null), cmd.getOptionValues('g'), jarFiles); + try { + c2gl.main(); + } catch (IOException e) { + status.error(e); + } + + if (status.ok()) { + System.exit(0); + } else { + System.exit(1); + } + + } + + @VisibleForTesting + Class2Greylist(Status status, String publicApiListFile, String[] perSdkLevelOutputFiles, + String[] jarFiles) { + mStatus = status; + mPublicApiListFile = publicApiListFile; + mPerSdkOutputFiles = perSdkLevelOutputFiles; + mJarFiles = jarFiles; + } + + private void main() throws IOException { + GreylistConsumer output; + Set<Integer> allowedSdkVersions; + if (mPerSdkOutputFiles != null) { + Map<Integer, String> outputFiles = readGreylistMap(mPerSdkOutputFiles); + output = new FileWritingGreylistConsumer(mStatus, outputFiles); + allowedSdkVersions = outputFiles.keySet(); + } else { + // TODO remove this once per-SDK greylist support integrated into the build. + // Right now, mPerSdkOutputFiles is always null as the build never passes the + // corresponding command lind flags. Once the build is updated, can remove this. + output = new SystemOutGreylistConsumer(); + allowedSdkVersions = new HashSet<>(Arrays.asList(null, 26, 28)); + } Set<String> publicApis; - if (publicApiFilename != null) { - try { - publicApis = Sets.newHashSet( - Files.readLines(new File(publicApiFilename), Charset.forName("UTF-8"))); - } catch (IOException e) { - status.error(e); - System.exit(1); - return; - } + if (mPublicApiListFile != null) { + publicApis = Sets.newHashSet( + Files.readLines(new File(mPublicApiListFile), Charset.forName("UTF-8"))); } else { publicApis = Collections.emptySet(); } - for (String jarFile : jarFiles) { - status.debug("Processing jar file %s", jarFile); + for (String jarFile : mJarFiles) { + mStatus.debug("Processing jar file %s", jarFile); try { - JarReader reader = new JarReader(status, jarFile); + JarReader reader = new JarReader(mStatus, jarFile); reader.stream().forEach(clazz -> new AnnotationVisitor(clazz, ANNOTATION_TYPE, - publicApis, status).visit()); + publicApis, allowedSdkVersions, output, mStatus).visit()); reader.close(); } catch (IOException e) { - status.error(e); + mStatus.error(e); } } - if (status.ok()) { - System.exit(0); - } else { - System.exit(1); - } + output.close(); + } + @VisibleForTesting + Map<Integer, String> readGreylistMap(String[] argValues) { + Map<Integer, String> map = new HashMap<>(); + for (String sdkFile : argValues) { + Integer maxTargetSdk = null; + String filename; + int colonPos = sdkFile.indexOf(':'); + if (colonPos != -1) { + try { + maxTargetSdk = Integer.valueOf(sdkFile.substring(0, colonPos)); + } catch (NumberFormatException nfe) { + mStatus.error("Not a valid integer: %s from argument value '%s'", + sdkFile.substring(0, colonPos), sdkFile); + } + filename = sdkFile.substring(colonPos + 1); + if (filename.length() == 0) { + mStatus.error("Not a valid file name: %s from argument value '%s'", + filename, sdkFile); + } + } else { + maxTargetSdk = null; + filename = sdkFile; + } + if (map.containsKey(maxTargetSdk)) { + mStatus.error("Multiple output files for maxTargetSdk %s", maxTargetSdk); + } else { + map.put(maxTargetSdk, filename); + } + } + return map; } private static void help(Options options) { diff --git a/tools/class2greylist/src/com/android/class2greylist/FileWritingGreylistConsumer.java b/tools/class2greylist/src/com/android/class2greylist/FileWritingGreylistConsumer.java new file mode 100644 index 0000000000..86eeeffbb5 --- /dev/null +++ b/tools/class2greylist/src/com/android/class2greylist/FileWritingGreylistConsumer.java @@ -0,0 +1,48 @@ +package com.android.class2greylist; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.PrintStream; +import java.util.HashMap; +import java.util.Map; + +public class FileWritingGreylistConsumer implements GreylistConsumer { + + private final Status mStatus; + private final Map<Integer, PrintStream> mSdkToPrintStreamMap; + + private static Map<Integer, PrintStream> openFiles( + Map<Integer, String> filenames) throws FileNotFoundException { + Map<Integer, PrintStream> streams = new HashMap<>(); + for (Map.Entry<Integer, String> entry : filenames.entrySet()) { + streams.put(entry.getKey(), + new PrintStream(new FileOutputStream(new File(entry.getValue())))); + } + return streams; + } + + public FileWritingGreylistConsumer(Status status, Map<Integer, String> sdkToFilenameMap) + throws FileNotFoundException { + mStatus = status; + mSdkToPrintStreamMap = openFiles(sdkToFilenameMap); + } + + @Override + public void greylistEntry(String signature, Integer maxTargetSdk) { + PrintStream p = mSdkToPrintStreamMap.get(maxTargetSdk); + if (p == null) { + mStatus.error("No output file for signature %s with maxTargetSdk of %d", signature, + maxTargetSdk == null ? "<absent>" : maxTargetSdk.toString()); + return; + } + p.println(signature); + } + + @Override + public void close() { + for (PrintStream p : mSdkToPrintStreamMap.values()) { + p.close(); + } + } +} diff --git a/tools/class2greylist/src/com/android/class2greylist/GreylistConsumer.java b/tools/class2greylist/src/com/android/class2greylist/GreylistConsumer.java new file mode 100644 index 0000000000..debc21d139 --- /dev/null +++ b/tools/class2greylist/src/com/android/class2greylist/GreylistConsumer.java @@ -0,0 +1,13 @@ +package com.android.class2greylist; + +public interface GreylistConsumer { + /** + * Handle a new greylist entry. + * + * @param signature Signature of the member. + * @param maxTargetSdk maxTargetSdk value from the annotation, or null if none set. + */ + void greylistEntry(String signature, Integer maxTargetSdk); + + void close(); +} diff --git a/tools/class2greylist/src/com/android/class2greylist/Status.java b/tools/class2greylist/src/com/android/class2greylist/Status.java index d7078986d9..b5ee9f138f 100644 --- a/tools/class2greylist/src/com/android/class2greylist/Status.java +++ b/tools/class2greylist/src/com/android/class2greylist/Status.java @@ -42,16 +42,12 @@ public class Status { mHasErrors = true; } - public void error(String message) { + public void error(String message, Object... args) { System.err.print(ERROR); - System.err.println(message); + System.err.println(String.format(Locale.US, message, args)); mHasErrors = true; } - public void greylistEntry(String signature) { - System.out.println(signature); - } - public boolean ok() { return !mHasErrors; } diff --git a/tools/class2greylist/src/com/android/class2greylist/SystemOutGreylistConsumer.java b/tools/class2greylist/src/com/android/class2greylist/SystemOutGreylistConsumer.java new file mode 100644 index 0000000000..8e12759f2b --- /dev/null +++ b/tools/class2greylist/src/com/android/class2greylist/SystemOutGreylistConsumer.java @@ -0,0 +1,12 @@ +package com.android.class2greylist; + +public class SystemOutGreylistConsumer implements GreylistConsumer { + @Override + public void greylistEntry(String signature, Integer maxTargetSdk) { + System.out.println(signature); + } + + @Override + public void close() { + } +} diff --git a/tools/class2greylist/test/src/com/android/javac/AnnotationVisitorTest.java b/tools/class2greylist/test/src/com/android/class2greylist/AnnotationVisitorTest.java index 20c959db12..994fe89a61 100644 --- a/tools/class2greylist/test/src/com/android/javac/AnnotationVisitorTest.java +++ b/tools/class2greylist/test/src/com/android/class2greylist/AnnotationVisitorTest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.javac; +package com.android.class2greylist; import static com.google.common.truth.Truth.assertThat; @@ -25,10 +25,12 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.withSettings; -import com.android.class2greylist.AnnotationVisitor; -import com.android.class2greylist.Status; +import static java.util.Collections.emptySet; + +import com.android.javac.Javac; import com.google.common.base.Joiner; +import com.google.common.collect.ImmutableSet; import com.google.common.collect.Sets; import org.junit.Before; @@ -48,28 +50,30 @@ public class AnnotationVisitorTest { public TestName mTestName = new TestName(); private Javac mJavac; + private GreylistConsumer mConsumer; private Status mStatus; @Before public void setup() throws IOException { System.out.println(String.format("\n============== STARTING TEST: %s ==============\n", mTestName.getMethodName())); + mConsumer = mock(GreylistConsumer.class); mStatus = mock(Status.class, withSettings().verboseLogging()); mJavac = new Javac(); mJavac.addSource("annotation.Anno", Joiner.on('\n').join( "package annotation;", - "import static java.lang.annotation.RetentionPolicy.CLASS;", - "import java.lang.annotation.Retention;", - "import java.lang.annotation.Target;", + "import static java.lang.annotation.RetentionPolicy.CLASS;", + "import java.lang.annotation.Retention;", "@Retention(CLASS)", "public @interface Anno {", " String expectedSignature() default \"\";", + " int maxTargetSdk() default Integer.MAX_VALUE;", "}")); } private void assertNoErrors() { verify(mStatus, never()).error(any(Throwable.class)); - verify(mStatus, never()).error(any(String.class)); + verify(mStatus, never()).error(any(), any()); } @Test @@ -83,12 +87,12 @@ public class AnnotationVisitorTest { "}")); assertThat(mJavac.compile()).isTrue(); - new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), ANNOTATION, x -> true, mStatus) - .visit(); + new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), ANNOTATION, x -> true, + emptySet(), mConsumer, mStatus).visit(); assertNoErrors(); ArgumentCaptor<String> greylist = ArgumentCaptor.forClass(String.class); - verify(mStatus, times(1)).greylistEntry(greylist.capture()); + verify(mConsumer, times(1)).greylistEntry(greylist.capture(), any()); assertThat(greylist.getValue()).isEqualTo("La/b/Class;->method()V"); } @@ -103,12 +107,12 @@ public class AnnotationVisitorTest { "}")); assertThat(mJavac.compile()).isTrue(); - new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), ANNOTATION, x -> true, mStatus) - .visit(); + new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), ANNOTATION, x -> true, + emptySet(), mConsumer, mStatus).visit(); assertNoErrors(); ArgumentCaptor<String> greylist = ArgumentCaptor.forClass(String.class); - verify(mStatus, times(1)).greylistEntry(greylist.capture()); + verify(mConsumer, times(1)).greylistEntry(greylist.capture(), any()); assertThat(greylist.getValue()).isEqualTo("La/b/Class;-><init>()V"); } @@ -123,12 +127,12 @@ public class AnnotationVisitorTest { "}")); assertThat(mJavac.compile()).isTrue(); - new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), ANNOTATION, x -> true, mStatus) - .visit(); + new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), ANNOTATION, x -> true, + emptySet(), mConsumer, mStatus).visit(); assertNoErrors(); ArgumentCaptor<String> greylist = ArgumentCaptor.forClass(String.class); - verify(mStatus, times(1)).greylistEntry(greylist.capture()); + verify(mConsumer, times(1)).greylistEntry(greylist.capture(), any()); assertThat(greylist.getValue()).isEqualTo("La/b/Class;->i:I"); } @@ -143,12 +147,12 @@ public class AnnotationVisitorTest { "}")); assertThat(mJavac.compile()).isTrue(); - new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), ANNOTATION, x -> true, mStatus) - .visit(); + new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), ANNOTATION, x -> true, + emptySet(), mConsumer, mStatus).visit(); assertNoErrors(); ArgumentCaptor<String> greylist = ArgumentCaptor.forClass(String.class); - verify(mStatus, times(1)).greylistEntry(greylist.capture()); + verify(mConsumer, times(1)).greylistEntry(greylist.capture(), any()); assertThat(greylist.getValue()).isEqualTo("La/b/Class;->method()V"); } @@ -163,10 +167,10 @@ public class AnnotationVisitorTest { "}")); assertThat(mJavac.compile()).isTrue(); - new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), ANNOTATION, x -> true, mStatus) - .visit(); + new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), ANNOTATION, x -> true, + emptySet(), mConsumer, mStatus).visit(); - verify(mStatus, times(1)).error(any(String.class)); + verify(mStatus, times(1)).error(any(), any()); } @Test @@ -183,11 +187,11 @@ public class AnnotationVisitorTest { assertThat(mJavac.compile()).isTrue(); new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class$Inner"), ANNOTATION, x -> true, - mStatus).visit(); + emptySet(), mConsumer, mStatus).visit(); assertNoErrors(); ArgumentCaptor<String> greylist = ArgumentCaptor.forClass(String.class); - verify(mStatus, times(1)).greylistEntry(greylist.capture()); + verify(mConsumer, times(1)).greylistEntry(greylist.capture(), any()); assertThat(greylist.getValue()).isEqualTo("La/b/Class$Inner;->method()V"); } @@ -200,11 +204,11 @@ public class AnnotationVisitorTest { "}")); assertThat(mJavac.compile()).isTrue(); - new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), ANNOTATION, x -> true, mStatus) - .visit(); + new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), ANNOTATION, x -> true, + emptySet(), mConsumer, mStatus).visit(); assertNoErrors(); - verify(mStatus, never()).greylistEntry(any(String.class)); + verify(mConsumer, never()).greylistEntry(any(String.class), any()); } @Test @@ -218,12 +222,12 @@ public class AnnotationVisitorTest { "}")); assertThat(mJavac.compile()).isTrue(); - new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), ANNOTATION, x -> true, mStatus) - .visit(); + new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), ANNOTATION, x -> true, + emptySet(), mConsumer, mStatus).visit(); assertNoErrors(); ArgumentCaptor<String> greylist = ArgumentCaptor.forClass(String.class); - verify(mStatus, times(1)).greylistEntry(greylist.capture()); + verify(mConsumer, times(1)).greylistEntry(greylist.capture(), any()); assertThat(greylist.getValue()).isEqualTo("La/b/Class;->method(Ljava/lang/String;)V"); } @@ -245,15 +249,15 @@ public class AnnotationVisitorTest { "}")); assertThat(mJavac.compile()).isTrue(); - new AnnotationVisitor(mJavac.getCompiledClass("a.b.Base"), ANNOTATION, x -> true, mStatus) - .visit(); - new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), ANNOTATION, x -> true, mStatus) - .visit(); + new AnnotationVisitor(mJavac.getCompiledClass("a.b.Base"), ANNOTATION, x -> true, + emptySet(), mConsumer, mStatus).visit(); + new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), ANNOTATION, x -> true, + emptySet(), mConsumer, mStatus).visit(); assertNoErrors(); ArgumentCaptor<String> greylist = ArgumentCaptor.forClass(String.class); // A bridge method is generated for the above, so we expect 2 greylist entries. - verify(mStatus, times(2)).greylistEntry(greylist.capture()); + verify(mConsumer, times(2)).greylistEntry(greylist.capture(), any()); assertThat(greylist.getAllValues()).containsExactly( "La/b/Class;->method(Ljava/lang/Object;)V", "La/b/Class;->method(Ljava/lang/String;)V"); @@ -277,15 +281,15 @@ public class AnnotationVisitorTest { "}")); assertThat(mJavac.compile()).isTrue(); - new AnnotationVisitor(mJavac.getCompiledClass("a.b.Base"), ANNOTATION, x -> true, mStatus) - .visit(); - new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), ANNOTATION, x -> true, mStatus) - .visit(); + new AnnotationVisitor(mJavac.getCompiledClass("a.b.Base"), ANNOTATION, x -> true, + emptySet(), mConsumer, mStatus).visit(); + new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), ANNOTATION, x -> true, + emptySet(), mConsumer, mStatus).visit(); assertNoErrors(); ArgumentCaptor<String> greylist = ArgumentCaptor.forClass(String.class); // A bridge method is generated for the above, so we expect 2 greylist entries. - verify(mStatus, times(2)).greylistEntry(greylist.capture()); + verify(mConsumer, times(2)).greylistEntry(greylist.capture(), any()); assertThat(greylist.getAllValues()).containsExactly( "La/b/Class;->method(Ljava/lang/Object;)V", "La/b/Class;->method(Ljava/lang/String;)V"); @@ -314,16 +318,19 @@ public class AnnotationVisitorTest { assertThat(mJavac.compile()).isTrue(); new AnnotationVisitor( - mJavac.getCompiledClass("a.b.Interface"), ANNOTATION, x -> true, mStatus).visit(); + mJavac.getCompiledClass("a.b.Interface"), ANNOTATION, x -> true, emptySet(), + mConsumer, mStatus).visit(); new AnnotationVisitor( - mJavac.getCompiledClass("a.b.Base"), ANNOTATION, x -> true, mStatus).visit(); + mJavac.getCompiledClass("a.b.Base"), ANNOTATION, x -> true, emptySet(), mConsumer, + mStatus).visit(); new AnnotationVisitor( - mJavac.getCompiledClass("a.b.Class"), ANNOTATION, x -> true, mStatus).visit(); + mJavac.getCompiledClass("a.b.Class"), ANNOTATION, x -> true, emptySet(), mConsumer, + mStatus).visit(); assertNoErrors(); ArgumentCaptor<String> greylist = ArgumentCaptor.forClass(String.class); // A bridge method is generated for the above, so we expect 2 greylist entries. - verify(mStatus, times(2)).greylistEntry(greylist.capture()); + verify(mConsumer, times(2)).greylistEntry(greylist.capture(), any()); assertThat(greylist.getAllValues()).containsExactly( "La/b/Class;->method(Ljava/lang/Object;)V", "La/b/Base;->method(Ljava/lang/Object;)V"); @@ -351,14 +358,14 @@ public class AnnotationVisitorTest { "La/b/Base;->method(Ljava/lang/Object;)V", "La/b/Class;->method(Ljava/lang/Object;)V"); new AnnotationVisitor(mJavac.getCompiledClass("a.b.Base"), ANNOTATION, publicApis, - mStatus).visit(); + emptySet(), mConsumer, mStatus).visit(); new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), ANNOTATION, publicApis, - mStatus).visit(); + emptySet(), mConsumer, mStatus).visit(); assertNoErrors(); ArgumentCaptor<String> greylist = ArgumentCaptor.forClass(String.class); // The bridge method generated for the above, is a public API so should be excluded - verify(mStatus, times(1)).greylistEntry(greylist.capture()); + verify(mConsumer, times(1)).greylistEntry(greylist.capture(), any()); assertThat(greylist.getValue()).isEqualTo("La/b/Class;->method(Ljava/lang/String;)V"); } @@ -375,10 +382,10 @@ public class AnnotationVisitorTest { new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), ANNOTATION, member -> !member.bridge, // exclude bridge methods - mStatus).visit(); + emptySet(), mConsumer, mStatus).visit(); assertNoErrors(); ArgumentCaptor<String> greylist = ArgumentCaptor.forClass(String.class); - verify(mStatus, times(1)).greylistEntry(greylist.capture()); + verify(mConsumer, times(1)).greylistEntry(greylist.capture(), any()); assertThat(greylist.getValue()).isEqualTo("La/b/Class;->field:I"); } @@ -393,8 +400,62 @@ public class AnnotationVisitorTest { "}")); assertThat(mJavac.compile()).isTrue(); - new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), ANNOTATION, - x -> true, mStatus).visit(); - verify(mStatus, times(1)).error(any(String.class)); + new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), ANNOTATION, x -> true, + emptySet(), mConsumer, mStatus).visit(); + verify(mStatus, times(1)).error(any(), any()); + } + + @Test + public void testMethodMaxTargetSdk() throws IOException { + mJavac.addSource("a.b.Class", Joiner.on('\n').join( + "package a.b;", + "import annotation.Anno;", + "public class Class {", + " @Anno(maxTargetSdk=1)", + " public int field;", + "}")); + assertThat(mJavac.compile()).isTrue(); + + new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), ANNOTATION, x -> true, + ImmutableSet.of(1), mConsumer, mStatus).visit(); + assertNoErrors(); + ArgumentCaptor<Integer> maxTargetSdk = ArgumentCaptor.forClass(Integer.class); + verify(mConsumer, times(1)).greylistEntry(any(), maxTargetSdk.capture()); + assertThat(maxTargetSdk.getValue()).isEqualTo(1); + } + + @Test + public void testMethodNoMaxTargetSdk() throws IOException { + mJavac.addSource("a.b.Class", Joiner.on('\n').join( + "package a.b;", + "import annotation.Anno;", + "public class Class {", + " @Anno", + " public int field;", + "}")); + assertThat(mJavac.compile()).isTrue(); + + new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), ANNOTATION, x -> true, + ImmutableSet.of(1), mConsumer, mStatus).visit(); + assertNoErrors(); + ArgumentCaptor<Integer> maxTargetSdk = ArgumentCaptor.forClass(Integer.class); + verify(mConsumer, times(1)).greylistEntry(any(), maxTargetSdk.capture()); + assertThat(maxTargetSdk.getValue()).isEqualTo(null); + } + + @Test + public void testMethodMaxTargetSdkOutOfRange() throws IOException { + mJavac.addSource("a.b.Class", Joiner.on('\n').join( + "package a.b;", + "import annotation.Anno;", + "public class Class {", + " @Anno(maxTargetSdk=2)", + " public int field;", + "}")); + assertThat(mJavac.compile()).isTrue(); + + new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), ANNOTATION, x -> true, + ImmutableSet.of(1), mConsumer, mStatus).visit(); + verify(mStatus, times(1)).error(any(), any()); } } diff --git a/tools/class2greylist/test/src/com/android/class2greylist/Class2GreylistTest.java b/tools/class2greylist/test/src/com/android/class2greylist/Class2GreylistTest.java new file mode 100644 index 0000000000..979044b909 --- /dev/null +++ b/tools/class2greylist/test/src/com/android/class2greylist/Class2GreylistTest.java @@ -0,0 +1,73 @@ +package com.android.class2greylist; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.MockitoAnnotations.initMocks; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestName; +import org.mockito.Mock; + +import java.io.IOException; +import java.util.Map; + +public class Class2GreylistTest { + + @Mock + Status mStatus; + @Rule + public TestName mTestName = new TestName(); + + @Before + public void setup() throws IOException { + System.out.println(String.format("\n============== STARTING TEST: %s ==============\n", + mTestName.getMethodName())); + initMocks(this); + } + + @Test + public void testReadGreylistMap() { + Class2Greylist c2gl = new Class2Greylist(mStatus, null, null, null); + Map<Integer, String> map = c2gl.readGreylistMap( + new String[]{"noApi", "1:apiOne", "3:apiThree"}); + verifyZeroInteractions(mStatus); + assertThat(map).containsExactly(null, "noApi", 1, "apiOne", 3, "apiThree"); + } + + @Test + public void testReadGreylistMapDuplicate() { + Class2Greylist c2gl = new Class2Greylist(mStatus, null, null, null); + Map<Integer, String> map = c2gl.readGreylistMap( + new String[]{"noApi", "1:apiOne", "1:anotherOne"}); + verify(mStatus, atLeastOnce()).error(any(), any()); + } + + @Test + public void testReadGreylistMapDuplicateNoApi() { + Class2Greylist c2gl = new Class2Greylist(mStatus, null, null, null); + Map<Integer, String> map = c2gl.readGreylistMap( + new String[]{"noApi", "anotherNoApi", "1:apiOne"}); + verify(mStatus, atLeastOnce()).error(any(), any()); + } + + @Test + public void testReadGreylistMapInvalidInt() { + Class2Greylist c2gl = new Class2Greylist(mStatus, null, null, null); + Map<Integer, String> map = c2gl.readGreylistMap(new String[]{"noApi", "a:apiOne"}); + verify(mStatus, atLeastOnce()).error(any(), any()); + } + + @Test + public void testReadGreylistMapNoFilename() { + Class2Greylist c2gl = new Class2Greylist(mStatus, null, null, null); + Map<Integer, String> map = c2gl.readGreylistMap(new String[]{"noApi", "1:"}); + verify(mStatus, atLeastOnce()).error(any(), any()); + } +} + |