summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--tools/class2greylist/src/com/android/class2greylist/Class2Greylist.java44
-rw-r--r--tools/class2greylist/src/com/android/class2greylist/CsvGreylistConsumer.java35
-rw-r--r--tools/class2greylist/src/com/android/class2greylist/CsvWriter.java49
-rw-r--r--tools/class2greylist/src/com/android/class2greylist/FileWritingGreylistConsumer.java3
-rw-r--r--tools/class2greylist/src/com/android/class2greylist/GreylistAnnotationHandler.java17
-rw-r--r--tools/class2greylist/src/com/android/class2greylist/GreylistConsumer.java5
-rw-r--r--tools/class2greylist/src/com/android/class2greylist/SystemOutGreylistConsumer.java5
-rw-r--r--tools/class2greylist/test/src/com/android/class2greylist/GreylistAnnotationHandlerTest.java65
8 files changed, 187 insertions, 36 deletions
diff --git a/tools/class2greylist/src/com/android/class2greylist/Class2Greylist.java b/tools/class2greylist/src/com/android/class2greylist/Class2Greylist.java
index 53157a323e..3e2f6f27ea 100644
--- a/tools/class2greylist/src/com/android/class2greylist/Class2Greylist.java
+++ b/tools/class2greylist/src/com/android/class2greylist/Class2Greylist.java
@@ -17,6 +17,7 @@
package com.android.class2greylist;
import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
@@ -41,6 +42,7 @@ import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
+import java.util.function.Predicate;
/**
* Build time tool for extracting a list of members from jar files that have the @UsedByApps
@@ -55,9 +57,10 @@ public class Class2Greylist {
private final String mPublicApiListFile;
private final String[] mPerSdkOutputFiles;
private final String mWhitelistFile;
+ private final String mCsvMetadataFile;
private final String[] mJarFiles;
private final GreylistConsumer mOutput;
- private final Set<Integer> mAllowedSdkVersions;
+ private final Predicate<Integer> mAllowedSdkVersions;
private final Set<String> mPublicApis;
@@ -95,10 +98,17 @@ public class Class2Greylist {
.hasArgs(0)
.create('m'));
options.addOption(OptionBuilder
+ .withLongOpt("write-metadata-csv")
+ .hasArgs(1)
+ .withDescription("Specify a file to write API metaadata to. This is a CSV file " +
+ "containing any annotation properties for all members. Do not use in " +
+ "conjunction with --write-greylist or --write-whitelist.")
+ .create('c'));
+ options.addOption(OptionBuilder
.withLongOpt("help")
.hasArgs(0)
.withDescription("Show this help")
- .create("h"));
+ .create('h'));
CommandLineParser parser = new GnuParser();
CommandLine cmd;
@@ -132,6 +142,7 @@ public class Class2Greylist {
cmd.getOptionValue('p', null),
cmd.getOptionValues('g'),
cmd.getOptionValue('w', null),
+ cmd.getOptionValue('c', null),
jarFiles);
c2gl.main();
} catch (IOException e) {
@@ -149,22 +160,33 @@ public class Class2Greylist {
@VisibleForTesting
Class2Greylist(Status status, String publicApiListFile, String[] perSdkLevelOutputFiles,
- String whitelistOutputFile, String[] jarFiles) throws IOException {
+ String whitelistOutputFile, String csvMetadataFile, String[] jarFiles)
+ throws IOException {
mStatus = status;
mPublicApiListFile = publicApiListFile;
mPerSdkOutputFiles = perSdkLevelOutputFiles;
mWhitelistFile = whitelistOutputFile;
+ mCsvMetadataFile = csvMetadataFile;
mJarFiles = jarFiles;
- if (mPerSdkOutputFiles != null) {
+ if (mCsvMetadataFile != null) {
+ mOutput = new CsvGreylistConsumer(mStatus, mCsvMetadataFile);
+ mAllowedSdkVersions = x -> true;
+ } else {
Map<Integer, String> outputFiles = readGreylistMap(mStatus, mPerSdkOutputFiles);
mOutput = new FileWritingGreylistConsumer(mStatus, outputFiles, mWhitelistFile);
- mAllowedSdkVersions = 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.
- mOutput = new SystemOutGreylistConsumer();
- mAllowedSdkVersions = new HashSet<>(Arrays.asList(null, 26, 28));
+ mAllowedSdkVersions = new Predicate<Integer>(){
+ @Override
+ public boolean test(Integer i) {
+ return outputFiles.keySet().contains(i);
+ }
+
+ @Override
+ public String toString() {
+ // we reply on this toString behaviour for readable error messages in
+ // GreylistAnnotationHandler
+ return Joiner.on(",").join(outputFiles.keySet());
+ }
+ };
}
if (mPublicApiListFile != null) {
diff --git a/tools/class2greylist/src/com/android/class2greylist/CsvGreylistConsumer.java b/tools/class2greylist/src/com/android/class2greylist/CsvGreylistConsumer.java
new file mode 100644
index 0000000000..7d28b317f0
--- /dev/null
+++ b/tools/class2greylist/src/com/android/class2greylist/CsvGreylistConsumer.java
@@ -0,0 +1,35 @@
+package com.android.class2greylist;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.PrintStream;
+import java.util.Map;
+
+public class CsvGreylistConsumer implements GreylistConsumer {
+
+ private final Status mStatus;
+ private final CsvWriter mCsvWriter;
+
+ public CsvGreylistConsumer(Status status, String csvMetadataFile) throws FileNotFoundException {
+ mStatus = status;
+ mCsvWriter = new CsvWriter(
+ new PrintStream(new FileOutputStream(new File(csvMetadataFile))));
+ }
+
+ @Override
+ public void greylistEntry(String signature, Integer maxTargetSdk,
+ Map<String, String> annotationProperties) {
+ annotationProperties.put("signature", signature);
+ mCsvWriter.addRow(annotationProperties);
+ }
+
+ @Override
+ public void whitelistEntry(String signature) {
+ }
+
+ @Override
+ public void close() {
+ mCsvWriter.close();
+ }
+}
diff --git a/tools/class2greylist/src/com/android/class2greylist/CsvWriter.java b/tools/class2greylist/src/com/android/class2greylist/CsvWriter.java
new file mode 100644
index 0000000000..3cfec30f23
--- /dev/null
+++ b/tools/class2greylist/src/com/android/class2greylist/CsvWriter.java
@@ -0,0 +1,49 @@
+package com.android.class2greylist;
+
+import com.google.common.base.Joiner;
+
+import java.io.PrintStream;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * Helper class for writing data to a CSV file.
+ *
+ * This class does not write anything to its output until it is closed, so it can gather a set of
+ * all columns before writing the header row.
+ */
+public class CsvWriter {
+
+ private final PrintStream mOutput;
+ private final ArrayList<Map<String, String>> mContents;
+ private final Set<String> mColumns;
+
+ public CsvWriter(PrintStream out) {
+ mOutput = out;
+ mContents = new ArrayList<>();
+ mColumns = new HashSet<>();
+ }
+
+ public void addRow(Map<String, String> values) {
+ mColumns.addAll(values.keySet());
+ mContents.add(values);
+ }
+
+ public void close() {
+ List<String> columns = new ArrayList<>(mColumns);
+ columns.sort(Comparator.naturalOrder());
+ mOutput.println(columns.stream().collect(Collectors.joining(",")));
+ for (Map<String, String> row : mContents) {
+ mOutput.println(columns.stream().map(column -> row.getOrDefault(column, "")).collect(
+ Collectors.joining(",")));
+ }
+ mOutput.close();
+ }
+
+
+}
diff --git a/tools/class2greylist/src/com/android/class2greylist/FileWritingGreylistConsumer.java b/tools/class2greylist/src/com/android/class2greylist/FileWritingGreylistConsumer.java
index bfd23102b9..b3ed1b16f9 100644
--- a/tools/class2greylist/src/com/android/class2greylist/FileWritingGreylistConsumer.java
+++ b/tools/class2greylist/src/com/android/class2greylist/FileWritingGreylistConsumer.java
@@ -44,7 +44,8 @@ public class FileWritingGreylistConsumer implements GreylistConsumer {
}
@Override
- public void greylistEntry(String signature, Integer maxTargetSdk) {
+ public void greylistEntry(
+ String signature, Integer maxTargetSdk, Map<String, String> annotationProperties) {
PrintStream p = mSdkToPrintStreamMap.get(maxTargetSdk);
if (p == null) {
mStatus.error("No output file for signature %s with maxTargetSdk of %d", signature,
diff --git a/tools/class2greylist/src/com/android/class2greylist/GreylistAnnotationHandler.java b/tools/class2greylist/src/com/android/class2greylist/GreylistAnnotationHandler.java
index 460f2c3c22..fed9f9210d 100644
--- a/tools/class2greylist/src/com/android/class2greylist/GreylistAnnotationHandler.java
+++ b/tools/class2greylist/src/com/android/class2greylist/GreylistAnnotationHandler.java
@@ -11,6 +11,8 @@ import org.apache.bcel.classfile.FieldOrMethod;
import org.apache.bcel.classfile.Method;
import org.apache.bcel.classfile.SimpleElementValue;
+import java.util.HashMap;
+import java.util.Map;
import java.util.Set;
import java.util.function.Predicate;
@@ -36,7 +38,7 @@ public class GreylistAnnotationHandler implements AnnotationHandler {
private final Status mStatus;
private final Predicate<GreylistMember> mGreylistFilter;
private final GreylistConsumer mGreylistConsumer;
- private final Set<Integer> mValidMaxTargetSdkValues;
+ private final Predicate<Integer> mValidMaxTargetSdkValues;
/**
* Represents a member of a class file (a field or method).
@@ -73,7 +75,7 @@ public class GreylistAnnotationHandler implements AnnotationHandler {
Status status,
GreylistConsumer greylistConsumer,
Set<String> publicApis,
- Set<Integer> validMaxTargetSdkValues) {
+ Predicate<Integer> validMaxTargetSdkValues) {
this(status, greylistConsumer,
member -> !(member.bridge && publicApis.contains(member.signature)),
validMaxTargetSdkValues);
@@ -84,7 +86,7 @@ public class GreylistAnnotationHandler implements AnnotationHandler {
Status status,
GreylistConsumer greylistConsumer,
Predicate<GreylistMember> greylistFilter,
- Set<Integer> validMaxTargetSdkValues) {
+ Predicate<Integer> validMaxTargetSdkValues) {
mStatus = status;
mGreylistConsumer = greylistConsumer;
mGreylistFilter = greylistFilter;
@@ -101,6 +103,7 @@ public class GreylistAnnotationHandler implements AnnotationHandler {
}
String signature = context.getMemberDescriptor();
Integer maxTargetSdk = null;
+ Map<String, String> allValues = new HashMap<String, String>();
for (ElementValuePair property : annotation.getElementValuePairs()) {
switch (property.getNameString()) {
case EXPECTED_SIGNATURE:
@@ -110,9 +113,10 @@ public class GreylistAnnotationHandler implements AnnotationHandler {
maxTargetSdk = verifyAndGetMaxTargetSdk(context, property);
break;
}
+ allValues.put(property.getNameString(), property.getValue().stringifyValue());
}
if (mGreylistFilter.test(new GreylistMember(signature, bridge, maxTargetSdk))) {
- mGreylistConsumer.greylistEntry(signature, maxTargetSdk);
+ mGreylistConsumer.greylistEntry(signature, maxTargetSdk, allValues);
}
}
@@ -131,13 +135,14 @@ public class GreylistAnnotationHandler implements AnnotationHandler {
if (property.getValue().getElementValueType() != ElementValue.PRIMITIVE_INT) {
context.reportError("Expected property %s to be of type int; got %d",
property.getNameString(), property.getValue().getElementValueType());
+ return null;
}
int value = ((SimpleElementValue) property.getValue()).getValueInt();
- if (!mValidMaxTargetSdkValues.contains(value)) {
+ if (!mValidMaxTargetSdkValues.test(value)) {
context.reportError("Invalid value for %s: got %d, expected one of [%s]",
property.getNameString(),
value,
- Joiner.on(",").join(mValidMaxTargetSdkValues));
+ mValidMaxTargetSdkValues);
return null;
}
return value;
diff --git a/tools/class2greylist/src/com/android/class2greylist/GreylistConsumer.java b/tools/class2greylist/src/com/android/class2greylist/GreylistConsumer.java
index fd855e88ed..afded37e66 100644
--- a/tools/class2greylist/src/com/android/class2greylist/GreylistConsumer.java
+++ b/tools/class2greylist/src/com/android/class2greylist/GreylistConsumer.java
@@ -1,5 +1,7 @@
package com.android.class2greylist;
+import java.util.Map;
+
public interface GreylistConsumer {
/**
* Handle a new greylist entry.
@@ -7,7 +9,8 @@ public interface GreylistConsumer {
* @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 greylistEntry(
+ String signature, Integer maxTargetSdk, Map<String, String> annotationProperties);
/**
* Handle a new whitelist entry.
diff --git a/tools/class2greylist/src/com/android/class2greylist/SystemOutGreylistConsumer.java b/tools/class2greylist/src/com/android/class2greylist/SystemOutGreylistConsumer.java
index ad5ad705b4..f86ac6ec68 100644
--- a/tools/class2greylist/src/com/android/class2greylist/SystemOutGreylistConsumer.java
+++ b/tools/class2greylist/src/com/android/class2greylist/SystemOutGreylistConsumer.java
@@ -1,8 +1,11 @@
package com.android.class2greylist;
+import java.util.Map;
+
public class SystemOutGreylistConsumer implements GreylistConsumer {
@Override
- public void greylistEntry(String signature, Integer maxTargetSdk) {
+ public void greylistEntry(
+ String signature, Integer maxTargetSdk, Map<String, String> annotationValues) {
System.out.println(signature);
}
diff --git a/tools/class2greylist/test/src/com/android/class2greylist/GreylistAnnotationHandlerTest.java b/tools/class2greylist/test/src/com/android/class2greylist/GreylistAnnotationHandlerTest.java
index 1a4bfb8283..edf2ecd84d 100644
--- a/tools/class2greylist/test/src/com/android/class2greylist/GreylistAnnotationHandlerTest.java
+++ b/tools/class2greylist/test/src/com/android/class2greylist/GreylistAnnotationHandlerTest.java
@@ -60,7 +60,7 @@ public class GreylistAnnotationHandlerTest extends AnnotationHandlerTestBase {
Predicate<GreylistAnnotationHandler.GreylistMember> greylistFilter,
Set<Integer> validMaxTargetSdkValues) {
return new GreylistAnnotationHandler(
- mStatus, mConsumer, greylistFilter, validMaxTargetSdkValues);
+ mStatus, mConsumer, greylistFilter, x -> validMaxTargetSdkValues.contains(x));
}
@Test
@@ -80,7 +80,7 @@ public class GreylistAnnotationHandlerTest extends AnnotationHandlerTestBase {
assertNoErrors();
ArgumentCaptor<String> greylist = ArgumentCaptor.forClass(String.class);
- verify(mConsumer, times(1)).greylistEntry(greylist.capture(), any());
+ verify(mConsumer, times(1)).greylistEntry(greylist.capture(), any(), any());
assertThat(greylist.getValue()).isEqualTo("La/b/Class;->method()V");
}
@@ -101,7 +101,7 @@ public class GreylistAnnotationHandlerTest extends AnnotationHandlerTestBase {
assertNoErrors();
ArgumentCaptor<String> greylist = ArgumentCaptor.forClass(String.class);
- verify(mConsumer, times(1)).greylistEntry(greylist.capture(), any());
+ verify(mConsumer, times(1)).greylistEntry(greylist.capture(), any(), any());
assertThat(greylist.getValue()).isEqualTo("La/b/Class;-><init>()V");
}
@@ -122,7 +122,7 @@ public class GreylistAnnotationHandlerTest extends AnnotationHandlerTestBase {
assertNoErrors();
ArgumentCaptor<String> greylist = ArgumentCaptor.forClass(String.class);
- verify(mConsumer, times(1)).greylistEntry(greylist.capture(), any());
+ verify(mConsumer, times(1)).greylistEntry(greylist.capture(), any(), any());
assertThat(greylist.getValue()).isEqualTo("La/b/Class;->i:I");
}
@@ -143,7 +143,7 @@ public class GreylistAnnotationHandlerTest extends AnnotationHandlerTestBase {
assertNoErrors();
ArgumentCaptor<String> greylist = ArgumentCaptor.forClass(String.class);
- verify(mConsumer, times(1)).greylistEntry(greylist.capture(), any());
+ verify(mConsumer, times(1)).greylistEntry(greylist.capture(), any(), any());
assertThat(greylist.getValue()).isEqualTo("La/b/Class;->method()V");
}
@@ -184,7 +184,7 @@ public class GreylistAnnotationHandlerTest extends AnnotationHandlerTestBase {
assertNoErrors();
ArgumentCaptor<String> greylist = ArgumentCaptor.forClass(String.class);
- verify(mConsumer, times(1)).greylistEntry(greylist.capture(), any());
+ verify(mConsumer, times(1)).greylistEntry(greylist.capture(), any(), any());
assertThat(greylist.getValue()).isEqualTo("La/b/Class$Inner;->method()V");
}
@@ -202,7 +202,7 @@ public class GreylistAnnotationHandlerTest extends AnnotationHandlerTestBase {
).visit();
assertNoErrors();
- verify(mConsumer, never()).greylistEntry(any(String.class), any());
+ verify(mConsumer, never()).greylistEntry(any(String.class), any(), any());
}
@Test
@@ -222,7 +222,7 @@ public class GreylistAnnotationHandlerTest extends AnnotationHandlerTestBase {
assertNoErrors();
ArgumentCaptor<String> greylist = ArgumentCaptor.forClass(String.class);
- verify(mConsumer, times(1)).greylistEntry(greylist.capture(), any());
+ verify(mConsumer, times(1)).greylistEntry(greylist.capture(), any(), any());
assertThat(greylist.getValue()).isEqualTo("La/b/Class;->method(Ljava/lang/String;)V");
}
@@ -252,7 +252,7 @@ public class GreylistAnnotationHandlerTest extends AnnotationHandlerTestBase {
assertNoErrors();
ArgumentCaptor<String> greylist = ArgumentCaptor.forClass(String.class);
// A bridge method is generated for the above, so we expect 2 greylist entries.
- verify(mConsumer, times(2)).greylistEntry(greylist.capture(), any());
+ verify(mConsumer, times(2)).greylistEntry(greylist.capture(), any(), any());
assertThat(greylist.getAllValues()).containsExactly(
"La/b/Class;->method(Ljava/lang/Object;)V",
"La/b/Class;->method(Ljava/lang/String;)V");
@@ -284,7 +284,7 @@ public class GreylistAnnotationHandlerTest extends AnnotationHandlerTestBase {
assertNoErrors();
ArgumentCaptor<String> greylist = ArgumentCaptor.forClass(String.class);
// A bridge method is generated for the above, so we expect 2 greylist entries.
- verify(mConsumer, times(2)).greylistEntry(greylist.capture(), any());
+ verify(mConsumer, times(2)).greylistEntry(greylist.capture(), any(), any());
assertThat(greylist.getAllValues()).containsExactly(
"La/b/Class;->method(Ljava/lang/Object;)V",
"La/b/Class;->method(Ljava/lang/String;)V");
@@ -322,7 +322,7 @@ public class GreylistAnnotationHandlerTest extends AnnotationHandlerTestBase {
assertNoErrors();
ArgumentCaptor<String> greylist = ArgumentCaptor.forClass(String.class);
// A bridge method is generated for the above, so we expect 2 greylist entries.
- verify(mConsumer, times(2)).greylistEntry(greylist.capture(), any());
+ verify(mConsumer, times(2)).greylistEntry(greylist.capture(), any(), any());
assertThat(greylist.getAllValues()).containsExactly(
"La/b/Class;->method(Ljava/lang/Object;)V",
"La/b/Base;->method(Ljava/lang/Object;)V");
@@ -355,14 +355,14 @@ public class GreylistAnnotationHandlerTest extends AnnotationHandlerTestBase {
mStatus,
mConsumer,
publicApis,
- emptySet()));
+ x -> false));
new AnnotationVisitor(mJavac.getCompiledClass("a.b.Base"), mStatus, handlerMap).visit();
new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), mStatus, handlerMap).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(mConsumer, times(1)).greylistEntry(greylist.capture(), any());
+ verify(mConsumer, times(1)).greylistEntry(greylist.capture(), any(), any());
assertThat(greylist.getValue()).isEqualTo("La/b/Class;->method(Ljava/lang/String;)V");
}
@@ -384,7 +384,7 @@ public class GreylistAnnotationHandlerTest extends AnnotationHandlerTestBase {
new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), mStatus, handlerMap).visit();
assertNoErrors();
ArgumentCaptor<String> greylist = ArgumentCaptor.forClass(String.class);
- verify(mConsumer, times(1)).greylistEntry(greylist.capture(), any());
+ verify(mConsumer, times(1)).greylistEntry(greylist.capture(), any(), any());
assertThat(greylist.getValue()).isEqualTo("La/b/Class;->field:I");
}
@@ -423,7 +423,7 @@ public class GreylistAnnotationHandlerTest extends AnnotationHandlerTestBase {
new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), mStatus, handlerMap).visit();
assertNoErrors();
ArgumentCaptor<Integer> maxTargetSdk = ArgumentCaptor.forClass(Integer.class);
- verify(mConsumer, times(1)).greylistEntry(any(), maxTargetSdk.capture());
+ verify(mConsumer, times(1)).greylistEntry(any(), maxTargetSdk.capture(), any());
assertThat(maxTargetSdk.getValue()).isEqualTo(1);
}
@@ -445,7 +445,7 @@ public class GreylistAnnotationHandlerTest extends AnnotationHandlerTestBase {
new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), mStatus, handlerMap).visit();
assertNoErrors();
ArgumentCaptor<Integer> maxTargetSdk = ArgumentCaptor.forClass(Integer.class);
- verify(mConsumer, times(1)).greylistEntry(any(), maxTargetSdk.capture());
+ verify(mConsumer, times(1)).greylistEntry(any(), maxTargetSdk.capture(), any());
assertThat(maxTargetSdk.getValue()).isEqualTo(null);
}
@@ -468,4 +468,37 @@ public class GreylistAnnotationHandlerTest extends AnnotationHandlerTestBase {
verify(mStatus, times(1)).error(any(), any());
}
+ @Test
+ public void testAnnotationPropertiesIntoMap() throws IOException {
+ mJavac.addSource("annotation.Anno2", Joiner.on('\n').join(
+ "package annotation;",
+ "import static java.lang.annotation.RetentionPolicy.CLASS;",
+ "import java.lang.annotation.Retention;",
+ "@Retention(CLASS)",
+ "public @interface Anno2 {",
+ " String expectedSignature() default \"\";",
+ " int maxTargetSdk() default Integer.MAX_VALUE;",
+ " long trackingBug() default 0;",
+ "}"));
+ mJavac.addSource("a.b.Class", Joiner.on('\n').join(
+ "package a.b;",
+ "import annotation.Anno2;",
+ "public class Class {",
+ " @Anno2(maxTargetSdk=2, trackingBug=123456789)",
+ " public int field;",
+ "}"));
+ assertThat(mJavac.compile()).isTrue();
+ new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), mStatus,
+ ImmutableMap.of("Lannotation/Anno2;", createGreylistHandler(x -> true,
+ ImmutableSet.of(2)))
+ ).visit();
+
+ assertNoErrors();
+ ArgumentCaptor<Map<String, String>> properties = ArgumentCaptor.forClass(Map.class);
+ verify(mConsumer, times(1)).greylistEntry(any(), any(), properties.capture());
+ assertThat(properties.getValue()).containsExactly(
+ "maxTargetSdk", "2",
+ "trackingBug", "123456789");
+ }
+
}