1 /*
2  * Copyright (C) 2023 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package libcore.tools.generator.noncts;
18 
19 import com.android.json.stream.JsonWriter;
20 
21 import com.beust.jcommander.JCommander;
22 import com.beust.jcommander.Parameter;
23 import com.beust.jcommander.converters.FileConverter;
24 
25 import org.objectweb.asm.ClassReader;
26 import org.objectweb.asm.Type;
27 import org.objectweb.asm.tree.AnnotationNode;
28 import org.objectweb.asm.tree.ClassNode;
29 import org.objectweb.asm.tree.MethodNode;
30 
31 import java.io.File;
32 import java.io.IOException;
33 import java.io.InputStream;
34 import java.io.OutputStream;
35 import java.io.PrintStream;
36 import java.io.PrintWriter;
37 import java.io.UncheckedIOException;
38 import java.io.Writer;
39 import java.util.ArrayList;
40 import java.util.Comparator;
41 import java.util.List;
42 import java.util.Map;
43 import java.util.Set;
44 import java.util.TreeMap;
45 import java.util.TreeSet;
46 import java.util.zip.ZipEntry;
47 import java.util.zip.ZipFile;
48 
49 import vogar.expect.Expectation;
50 import vogar.expect.ExpectationStore;
51 import vogar.expect.ModeId;
52 import vogar.expect.util.Log;
53 import vogar.expect.util.LogOutput;
54 
55 public class Main {
56 
57 
58     private static class MainArgs {
59 
60         @Parameter(names = "-h", help = true, description = "Shows this help message")
61         public boolean help = false;
62 
63         @Parameter(converter = FileConverter.class, description = "list of jar files")
64         public List<File> inputFiles = new ArrayList<>();
65     }
66 
main(String[] argv)67     public static void main(String[] argv) {
68         MainArgs mainArgs = new MainArgs();
69 
70         JCommander jCommander = JCommander.newBuilder()
71                 .addObject(mainArgs)
72                 .build();
73         jCommander.parse(argv);
74 
75         if (mainArgs.help) {
76             jCommander.usage();
77             return;
78         }
79 
80         // See "libcore-non-cts-tests-txt" soong module where System.out is piped into
81         // skippedCtsTest.txt
82         PrintStream out = System.out;
83         out.println("/* Do not modify directly.");
84         out.println(" * Generated by tools/non-cts-json-generator/update_skippedCtsTest.sh");
85         out.println(" * which dumps all @NonCts tests.");
86         out.println(" */");
87 
88 
89         NonCtsAnnotationReader reader = new NonCtsAnnotationReader(mainArgs.inputFiles).parse();
90         ExpectationStore baseStore = readBaseExpectationStore();
91         ExpectationWriter writer = ExpectationWriter.from(baseStore, reader);
92         try {
93             writer.write(out);
94         } catch (IOException e) {
95             throw new UncheckedIOException(e);
96         }
97     }
98 
readBaseExpectationStore()99     private static ExpectationStore readBaseExpectationStore() {
100         Log.setOutput(new LogOutput() {
101             @Override
102             public void verbose(String s) {
103                 System.err.println(s);
104             }
105 
106             @Override
107             public void warn(String message) {
108                 System.err.println(message);
109             }
110 
111             @Override
112             public void warn(String message, List<String> list) {
113                 System.err.printf(message + '\n', list.toArray());
114             }
115 
116             @Override
117             public void nativeOutput(String outputLine) {
118                 System.err.println(outputLine);
119             }
120 
121             @Override
122             public void info(String s) {}
123 
124             @Override
125             public void info(String message, Throwable throwable) {}
126         });
127         try {
128             return ExpectationStore.parseResources(Main.class,
129                     Set.of("/skippedCtsTest_manual_base.txt"), ModeId.HOST);
130         } catch (IOException e) {
131             throw new UncheckedIOException(e);
132         }
133     }
134 
135     /**
136      * Read {@link libcore.test.annotation.NonCts} annotations from the .class files in the give
137      * jar files.
138      */
139     private static class NonCtsAnnotationReader {
140         private final List<NonCtsEntry> mEntries = new ArrayList<>();
141 
142         private final List<File> mJarFiles;
NonCtsAnnotationReader(List<File> jarFiles)143         public NonCtsAnnotationReader(List<File> jarFiles) {
144             mJarFiles = jarFiles;
145         }
146 
getEntries()147         public List<NonCtsEntry> getEntries() {
148             return mEntries;
149         }
150 
parse()151         public NonCtsAnnotationReader parse() {
152             for (File jarFile : mJarFiles) {
153                 try (ZipFile zipFile = new ZipFile(jarFile)) {
154                     var zipEntries = zipFile.entries();
155                     while (zipEntries.hasMoreElements()) {
156                         ZipEntry zipEntry = zipEntries.nextElement();
157                         if (!zipEntry.getName().endsWith(".class")) {
158                             continue;
159                         }
160 
161                         ClassNode classNode = parseClass(zipFile, zipEntry);
162                         storeIfNonCts(classNode);
163                         String className = Type.getObjectType(classNode.name).getClassName();
164                         for (MethodNode methodNode : classNode.methods) {
165                             storeIfNonCts(className, methodNode);
166                         }
167                     }
168                 } catch (IOException e) {
169                     throw new UncheckedIOException(e);
170                 }
171             }
172 
173             return this;
174         }
parseClass(ZipFile zipFile, ZipEntry zipEntry)175         private static ClassNode parseClass(ZipFile zipFile, ZipEntry zipEntry) throws IOException {
176             try (InputStream in = zipFile.getInputStream(zipEntry)) {
177                 ClassReader classReader = new ClassReader(in);
178                 ClassNode node = new ClassNode();
179                 classReader.accept(node, 0);
180                 return node;
181             }
182         }
183 
storeIfNonCts(String className, MethodNode node)184         private void storeIfNonCts(String className, MethodNode node) {
185             if (node.visibleAnnotations == null) {
186                 return;
187             }
188 
189             String methodName = className + "#" + node.name;
190             storeIfNonCts(node.visibleAnnotations, methodName);
191         }
192 
storeIfNonCts(ClassNode node)193         private void storeIfNonCts(ClassNode node) {
194             if (node.visibleAnnotations == null) {
195                 return;
196             }
197 
198             String className = Type.getObjectType(node.name).getClassName();
199             storeIfNonCts(node.visibleAnnotations, className);
200         }
201 
storeIfNonCts(List<AnnotationNode> visibleAnnotations, String name)202         private void storeIfNonCts(List<AnnotationNode> visibleAnnotations, String name) {
203             visibleAnnotations.stream()
204                     .filter(a -> "Llibcore/test/annotation/NonCts;".equals(a.desc))
205                     .map(a -> {
206                         Long bug = -1L;
207                         String desc = null;
208                         for (int i = 0; i < a.values.size() - 1; i = i + 2) {
209                             if (!(a.values.get(i) instanceof String key)) {
210                                 continue;
211                             }
212                             switch(key) {
213                                 case "bug" -> bug = (Long) a.values.get(i + 1);
214                                 case "reason" -> desc = (String) a.values.get(i + 1);
215                             }
216                         }
217                         return new NonCtsEntry(name, bug, desc);
218                     })
219                     .findFirst()
220                     .ifPresent(mEntries::add);
221         }
222     }
223 
224 
225     /**
226      * Write the expectation into the format read by {@link vogar.expect.ExpectationStore}.
227      */
228     public static class ExpectationWriter {
229         private final TreeMap<NonCtsCluster.Key, NonCtsCluster> clusterMap;
230 
ExpectationWriter(TreeMap<NonCtsCluster.Key, NonCtsCluster> clusterMap)231         private ExpectationWriter(TreeMap<NonCtsCluster.Key, NonCtsCluster> clusterMap) {
232             this.clusterMap = clusterMap;
233         }
234 
from(ExpectationStore baseStore, NonCtsAnnotationReader annotationReader)235         public static ExpectationWriter from(ExpectationStore baseStore,
236                 NonCtsAnnotationReader annotationReader) {
237             TreeMap<NonCtsCluster.Key, NonCtsCluster> clusterMap = new TreeMap<>();
238             for (NonCtsEntry entry : annotationReader.getEntries()) {
239                 NonCtsCluster.Key key = new NonCtsCluster.Key(entry);
240                 clusterMap.compute(key, (k, cluster) -> {
241                     if (cluster == null) {
242                         cluster = new NonCtsCluster(k);
243                     }
244                     cluster.add(entry.name());
245                     return cluster;
246                 });
247             }
248             for (var entry : baseStore.getAllOutComes().entrySet()) {
249                 NonCtsCluster.Key key = new NonCtsCluster.Key(entry.getValue());
250                 String testName = entry.getKey();
251                 clusterMap.compute(key, (k, cluster) -> {
252                     if (cluster == null) {
253                         cluster = new NonCtsCluster(k);
254                     }
255                     cluster.add(testName);
256                     return cluster;
257                 });
258             }
259 
260             return new ExpectationWriter(clusterMap);
261         }
262 
write(OutputStream out)263         public void write(OutputStream out) throws IOException {
264             Writer writer = new PrintWriter(out);
265             JsonWriter jsonWriter = new JsonWriter(writer);
266             jsonWriter.setIndent("  ");
267             jsonWriter.beginArray();
268 
269             for (NonCtsCluster cluster : clusterMap.values()) {
270                 jsonWriter.beginObject();
271                 if (cluster.key.bug != -1) {
272                     jsonWriter.name("bug");
273                     jsonWriter.value(cluster.key.bug);
274                 }
275                 String description = cluster.key.description;
276                 if (description == null) {
277                     description = "";
278                 }
279                 jsonWriter.name("description");
280                 jsonWriter.value(description);
281 
282                 jsonWriter.name("names");
283                 jsonWriter.beginArray();
284                 for (String testName : cluster.testNames) {
285                     jsonWriter.value(testName);
286                 }
287 
288                 jsonWriter.endArray();
289                 jsonWriter.endObject();
290             }
291 
292             jsonWriter.endArray();
293 
294             jsonWriter.flush();
295         }
296     }
297 
298     /**
299      * This data class is very similar {@link vogar.expect.Expectation}, but contains only the name
300      * and attributes supported by {@link libcore.test.annotation.NonCts}.
301      */
NonCtsEntry(String name, long bug, String description)302     public record NonCtsEntry(String name, long bug, String description) {}
303 
304     public static final class NonCtsCluster {
305         public final Key key;
306 
307         public final TreeSet<String> testNames = new TreeSet<>();
308 
309         public record Key(long bug, String description) implements Comparable<Key> {
310 
311             private static final Comparator<Key> COMPARATOR = Comparator.comparing(Key::bug)
312                     .thenComparing(Key::description);
Key(NonCtsEntry entry)313             public Key(NonCtsEntry entry) {
314                 this(entry.bug, entry.description);
315             }
Key(Expectation e)316             public Key(Expectation e) {
317                 this(e.getBug(), e.getDescription());
318             }
319 
320             @Override
compareTo(Key key2)321             public int compareTo(Key key2) {
322                 return COMPARATOR.compare(this, key2);
323             }
324         }
325 
NonCtsCluster(Key key)326         public NonCtsCluster(Key key) {
327             this.key = key;
328         }
329 
add(String testName)330         public NonCtsCluster add(String testName) {
331             testNames.add(testName);
332             return this;
333         }
334     }
335 }
336 
337