• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2010 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 com.android.cts.apicoverage;
18 
19 import static com.google.common.collect.ImmutableList.toImmutableList;
20 import static com.google.common.io.MoreFiles.getFileExtension;
21 
22 import com.android.compatibility.common.util.CddTest;
23 import com.android.compatibility.common.util.ReadElf;
24 
25 import com.google.common.collect.ImmutableList;
26 
27 import org.jf.dexlib2.DexFileFactory;
28 import org.jf.dexlib2.Opcodes;
29 import org.jf.dexlib2.iface.Annotation;
30 import org.jf.dexlib2.iface.AnnotationElement;
31 import org.jf.dexlib2.iface.ClassDef;
32 import org.jf.dexlib2.iface.DexFile;
33 import org.jf.dexlib2.iface.Method;
34 import org.jf.dexlib2.iface.value.StringEncodedValue;
35 
36 import org.xml.sax.InputSource;
37 import org.xml.sax.SAXException;
38 import org.xml.sax.XMLReader;
39 import org.xml.sax.helpers.XMLReaderFactory;
40 
41 import java.io.File;
42 import java.io.FileOutputStream;
43 import java.io.FileReader;
44 import java.io.IOException;
45 import java.io.OutputStream;
46 import java.nio.file.Files;
47 import java.nio.file.Path;
48 import java.nio.file.Paths;
49 import java.util.ArrayList;
50 import java.util.Collection;
51 import java.util.List;
52 import java.util.Locale;
53 import java.util.Set;
54 import java.util.stream.Stream;
55 import java.util.concurrent.ExecutorService;
56 import java.util.concurrent.Executors;
57 import java.util.concurrent.Future;
58 import java.util.function.Predicate;
59 
60 import javax.xml.transform.TransformerException;
61 
62 /**
63  * Tool that generates a report of what Android framework methods are being called from a given
64  * set of APKS. See the {@link #printUsage()} method for more details.
65  */
66 public class CtsApiCoverage {
67 
68     private static final int FORMAT_TXT = 0;
69 
70     private static final int FORMAT_XML = 1;
71 
72     private static final int FORMAT_HTML = 2;
73 
74     private static final String CDD_REQUIREMENT_ANNOTATION =
75             "Lcom/android/compatibility/common/util/CddTest;";
76 
77     private static final String CDD_REQUIREMENT_ELEMENT_NAME = "requirement";
78 
79     private static final String NDK_PACKAGE_NAME = "ndk";
80 
printUsage()81     private static void printUsage() {
82         System.out.println("Usage: cts-api-coverage [OPTION]... [APK]...");
83         System.out.println();
84         System.out.println("Generates a report about what Android framework methods are called ");
85         System.out.println("from the given APKs.");
86         System.out.println();
87         System.out.println("Use the Makefiles rules in CtsCoverage.mk to generate the report ");
88         System.out.println("rather than executing this directly. If you still want to run this ");
89         System.out.println("directly, then this must be used from the $ANDROID_BUILD_TOP ");
90         System.out.println("directory and dexdeps must be built via \"make dexdeps\".");
91         System.out.println();
92         System.out.println("Options:");
93         System.out.println("  -o FILE                output file or standard out if not given");
94         System.out.println("  -f [txt|xml|html]      format of output");
95         System.out.println("  -d PATH                path to dexdeps or expected to be in $PATH");
96         System.out.println("  -a PATH                path to the API XML file");
97         System.out.println(
98                 "  -n PATH                path to the NDK API XML file, which can be updated via"
99                         + " ndk-api-report with the ndk target");
100         System.out.println(
101                 "  -p PACKAGENAMEPREFIX   report coverage only for package that start with");
102         System.out.println("  -t TITLE               report title");
103         System.out.println("  -a API                 the Android API Level");
104         System.out.println("  -b BITS                64 or 32 bits, default 64");
105         System.out.println("  -j PARALLELISM         number of tasks to run in parallel, defaults"
106                         + " to number of cpus");
107         System.out.println();
108         System.exit(1);
109     }
110 
main(String[] args)111     public static void main(String[] args) throws Exception {
112         List<File> testApks = new ArrayList<File>();
113         File outputFile = null;
114         int format = FORMAT_TXT;
115         String dexDeps = "dexDeps";
116         String apiXmlPath = "";
117         String napiXmlPath = "";
118         PackageFilter packageFilter = new PackageFilter();
119         String reportTitle = "CTS API Coverage";
120         int apiLevel = Integer.MAX_VALUE;
121         String testCasesFolder = "";
122         String bits = "64";
123         int parallelism = Runtime.getRuntime().availableProcessors();
124 
125         List<File> notFoundTestApks = new ArrayList<File>();
126         int numTestApkArgs = 0;
127         for (int i = 0; i < args.length; i++) {
128             if (args[i].startsWith("-")) {
129                 if ("-o".equals(args[i])) {
130                     outputFile = new File(getExpectedArg(args, ++i));
131                 } else if ("-f".equals(args[i])) {
132                     String formatSpec = getExpectedArg(args, ++i);
133                     if ("xml".equalsIgnoreCase(formatSpec)) {
134                         format = FORMAT_XML;
135                     } else if ("txt".equalsIgnoreCase(formatSpec)) {
136                         format = FORMAT_TXT;
137                     } else if ("html".equalsIgnoreCase(formatSpec)) {
138                         format = FORMAT_HTML;
139                     } else {
140                         printUsage();
141                     }
142                 } else if ("-d".equals(args[i])) {
143                     dexDeps = getExpectedArg(args, ++i);
144                 } else if ("-a".equals(args[i])) {
145                     apiXmlPath = getExpectedArg(args, ++i);
146                 } else if ("-n".equals(args[i])) {
147                     napiXmlPath = getExpectedArg(args, ++i);
148                 } else if ("-p".equals(args[i])) {
149                     packageFilter.addPrefixToFilter(getExpectedArg(args, ++i));
150                 } else if ("-t".equals(args[i])) {
151                     reportTitle = getExpectedArg(args, ++i);
152                 } else if ("-a".equals(args[i])) {
153                     apiLevel = Integer.parseInt(getExpectedArg(args, ++i));
154                 } else if ("-b".equals(args[i])) {
155                     bits = getExpectedArg(args, ++i);
156                 } else if ("-j".equals(args[i])) {
157                     parallelism = Integer.parseInt(getExpectedArg(args, ++i));
158                 } else {
159                     printUsage();
160                 }
161             } else {
162                 Path file = Paths.get(args[i]);
163                 numTestApkArgs++;
164                 if (Files.isDirectory(file)) {
165                     List<String> extensions = ImmutableList.of("apk", "jar");
166                     try (Stream<Path> files = Files.walk(file, Integer.MAX_VALUE)) {
167                         Predicate<Path> filter =
168                                 path -> extensions.contains(getFileExtension(path).toLowerCase());
169                         List<File> matchedFiles =
170                                 files.filter(filter).map(Path::toFile).collect(toImmutableList());
171                         testApks.addAll(matchedFiles);
172                     }
173                     testCasesFolder = args[i];
174                 } else if (Files.exists(file)) {
175                     testApks.add(file.toFile());
176                 } else {
177                     notFoundTestApks.add(file.toFile());
178                 }
179             }
180         }
181 
182         if (!notFoundTestApks.isEmpty()) {
183             String msg = String.format(Locale.US, "%d/%d testApks not found: %s",
184                     notFoundTestApks.size(), numTestApkArgs, notFoundTestApks);
185             throw new IllegalArgumentException(msg);
186         }
187 
188         /*
189          * 1. Create an ApiCoverage object that is a tree of Java objects representing the API
190          *    in current.xml. The object will have no information about the coverage for each
191          *    constructor or method yet.
192          *
193          * 2. For each provided APK, scan it using dexdeps, parse the output of dexdeps, and
194          *    call methods on the ApiCoverage object to cumulatively add coverage stats.
195          *
196          * 3. Output a report based on the coverage stats in the ApiCoverage object.
197          */
198 
199         ApiCoverage apiCoverage = getEmptyApiCoverage(apiXmlPath);
200         CddCoverage cddCoverage = getEmptyCddCoverage();
201 
202         if (!napiXmlPath.equals("")) {
203             System.out.println("napiXmlPath: " + napiXmlPath);
204             ApiCoverage napiCoverage = getEmptyApiCoverage(napiXmlPath);
205             ApiPackage napiPackage = napiCoverage.getPackage(NDK_PACKAGE_NAME);
206             System.out.println(
207                     String.format(
208                             "%s, NDK Methods = %d, MemberSize = %d",
209                             napiXmlPath,
210                             napiPackage.getTotalMethods(),
211                             napiPackage.getMemberSize()));
212             apiCoverage.addPackage(napiPackage);
213         }
214 
215         // Add superclass information into api coverage.
216         apiCoverage.resolveSuperClasses();
217 
218         ExecutorService service = Executors.newFixedThreadPool(parallelism);
219         List<Future> tasks = new ArrayList<>();
220         for (File testApk : testApks) {
221             tasks.add(addApiCoverage(service, apiCoverage, testApk, dexDeps));
222             tasks.add(addCddCoverage(service, cddCoverage, testApk, apiLevel));
223         }
224         // Wait until all tasks finish.
225         for (Future task : tasks) {
226             task.get();
227         }
228         service.shutdown();
229 
230         // The below two coverage methods assume all classes and methods have been already
231         // registered, which is why we don't run them parallelly with others.
232 
233         try {
234             // Add coverage for GTest modules
235             addGTestNdkApiCoverage(apiCoverage, testCasesFolder, bits);
236         } catch (Exception e) {
237             System.out.println("warning: addGTestNdkApiCoverage failed to add to apiCoverage:");
238             e.printStackTrace();
239         }
240 
241         try {
242             // Add coverage for APK with Share Objects
243             addNdkApiCoverage(apiCoverage, testCasesFolder, bits);
244         } catch (Exception e) {
245             System.out.println("warning: addNdkApiCoverage failed to add to apiCoverage:");
246             e.printStackTrace();
247         }
248 
249         outputCoverageReport(apiCoverage, cddCoverage, testApks, outputFile,
250             format, packageFilter, reportTitle);
251     }
252 
253     /** Get the argument or print out the usage and exit. */
getExpectedArg(String[] args, int index)254     private static String getExpectedArg(String[] args, int index) {
255         if (index < args.length) {
256             return args[index];
257         } else {
258             printUsage();
259             return null;    // Never will happen because printUsage will call exit(1)
260         }
261     }
262 
263     /**
264      * Creates an object representing the API that will be used later to collect coverage
265      * statistics as we iterate over the test APKs.
266      *
267      * @param apiXmlPath to the API XML file
268      * @return an {@link ApiCoverage} object representing the API in current.xml without any
269      *     coverage statistics yet
270      */
getEmptyApiCoverage(String apiXmlPath)271     private static ApiCoverage getEmptyApiCoverage(String apiXmlPath)
272             throws SAXException, IOException {
273         XMLReader xmlReader = XMLReaderFactory.createXMLReader();
274         CurrentXmlHandler currentXmlHandler = new CurrentXmlHandler();
275         xmlReader.setContentHandler(currentXmlHandler);
276 
277         File currentXml = new File(apiXmlPath);
278         FileReader fileReader = null;
279         try {
280             fileReader = new FileReader(currentXml);
281             xmlReader.parse(new InputSource(fileReader));
282         } finally {
283             if (fileReader != null) {
284                 fileReader.close();
285             }
286         }
287 
288         return currentXmlHandler.getApi();
289     }
290 
291     /**
292      * Adds coverage information gleamed from running dexdeps on the APK to the
293      * {@link ApiCoverage} object.
294      *
295      * @param apiCoverage object to which the coverage statistics will be added to
296      * @param testApk containing the tests that will be scanned by dexdeps
297      */
addApiCoverage( ExecutorService service, ApiCoverage apiCoverage, File testApk, String dexdeps)298     private static Future addApiCoverage(
299         ExecutorService service, ApiCoverage apiCoverage, File testApk, String dexdeps) {
300         return service.submit(() -> {
301             String apkPath = testApk.getPath();
302             try {
303                 XMLReader xmlReader = XMLReaderFactory.createXMLReader();
304                 String testApkName = testApk.getName();
305                 DexDepsXmlHandler dexDepsXmlHandler = new DexDepsXmlHandler(apiCoverage, testApkName);
306                 xmlReader.setContentHandler(dexDepsXmlHandler);
307 
308                 Process process = new ProcessBuilder(dexdeps, "--format=xml", apkPath).start();
309                 xmlReader.parse(new InputSource(process.getInputStream()));
310             } catch (SAXException e) {
311                 // Catch this exception, but continue. SAXException is acceptable in cases
312                 // where the apk does not contain a classes.dex and therefore parsing won't work.
313                 System.err.println("warning: dexdeps failed for: " + apkPath);
314             } catch (IOException e) {
315                 throw new RuntimeException(e);
316             }
317         });
318     }
319 
320     /**
321      * Adds coverage information from native code symbol array to the {@link ApiCoverage} object.
322      *
323      * @param apiPackage object to which the coverage statistics will be added to
324      * @param symArr containing native code symbols
325      * @param testModules containing a list of TestModule
326      * @param moduleName test module name
327      */
addNdkSymArrToApiCoverage( ApiCoverage apiCoverage, List<TestModule> testModules)328     private static void addNdkSymArrToApiCoverage(
329             ApiCoverage apiCoverage, List<TestModule> testModules)
330             throws SAXException, IOException {
331 
332         final List<String> parameterTypes = new ArrayList<String>();
333         final ApiPackage apiPackage = apiCoverage.getPackage(NDK_PACKAGE_NAME);
334 
335         if (apiPackage != null) {
336             for (TestModule tm : testModules) {
337                 final String moduleName = tm.getModuleName();
338                 final ReadElf.Symbol[] symArr = tm.getDynSymArr();
339                 if (symArr != null) {
340                     for (ReadElf.Symbol sym : symArr) {
341                         if (sym.isGlobalUnd()) {
342                             String className = sym.getExternalLibFileName();
343                             ApiClass apiClass = apiPackage.getClass(className);
344                             if (apiClass != null) {
345                                 apiClass.markMethodCovered(
346                                         sym.name,
347                                         parameterTypes,
348                                         moduleName);
349                             } else {
350                                 System.err.println(
351                                         String.format(
352                                                 "warning: addNdkApiCoverage failed to getClass: %s",
353                                                 className));
354                             }
355                         }
356                     }
357                 } else {
358                     System.err.println(
359                             String.format(
360                                     "warning: addNdkSymbolArrToApiCoverage failed to getSymArr: %s",
361                                     moduleName));
362                 }
363             }
364         } else {
365             System.err.println(
366                     String.format(
367                             "warning: addNdkApiCoverage failed to getPackage: %s",
368                             NDK_PACKAGE_NAME));
369         }
370     }
371 
372     /**
373      * Adds coverage information gleamed from readelf on so in the APK to the {@link ApiCoverage}
374      * object.
375      *
376      * @param apiCoverage object to which the coverage statistics will be added to
377      * @param testCasesFolder containing GTest modules
378      * @param bits 64 or 32 bits of executiable
379      */
addNdkApiCoverage( ApiCoverage apiCoverage, String testCasesFolder, String bits)380     private static void addNdkApiCoverage(
381             ApiCoverage apiCoverage, String testCasesFolder, String bits)
382             throws SAXException, IOException {
383         ApkNdkApiReport apiReport = ApkNdkApiReport.parseTestcasesFolder(testCasesFolder, bits);
384         if (apiReport != null) {
385             addNdkSymArrToApiCoverage(apiCoverage, apiReport.getTestModules());
386         } else {
387             System.err.println(
388                     String.format(
389                             "warning: addNdkApiCoverage failed to get GTestApiReport from: %s @ %s"
390                                     + " bits",
391                             testCasesFolder, bits));
392         }
393     }
394 
395     /**
396      * Adds GTest coverage information gleamed from running ReadElf on the executiable to the {@link
397      * ApiCoverage} object.
398      *
399      * @param apiCoverage object to which the coverage statistics will be added to
400      * @param testCasesFolder containing GTest modules
401      * @param bits 64 or 32 bits of executiable
402      */
addGTestNdkApiCoverage( ApiCoverage apiCoverage, String testCasesFolder, String bits)403     private static void addGTestNdkApiCoverage(
404             ApiCoverage apiCoverage, String testCasesFolder, String bits)
405             throws SAXException, IOException {
406         GTestApiReport apiReport = GTestApiReport.parseTestcasesFolder(testCasesFolder, bits);
407         if (apiReport != null) {
408             addNdkSymArrToApiCoverage(apiCoverage, apiReport.getTestModules());
409         } else {
410             System.err.println(
411                     String.format(
412                             "warning: addGTestNdkApiCoverage failed to get GTestApiReport from: %s"
413                                     + " @ %s bits",
414                             testCasesFolder, bits));
415         }
416     }
417 
addCddCoverage( ExecutorService service, CddCoverage cddCoverage, File testSource, int api)418     private static Future addCddCoverage(
419         ExecutorService service, CddCoverage cddCoverage, File testSource, int api) {
420         return service.submit(() -> {
421             try {
422                 if (testSource.getName().endsWith(".apk")) {
423                     addCddApkCoverage(cddCoverage, testSource, api);
424                 } else if (testSource.getName().endsWith(".jar")) {
425                     addCddJarCoverage(cddCoverage, testSource);
426                 } else {
427                     System.err
428                         .println("Unsupported file type for CDD coverage: " + testSource.getPath());
429                 }
430             } catch (IOException e) {
431                 throw new RuntimeException(e);
432             }
433         });
434     }
435 
436     private static void addCddJarCoverage(CddCoverage cddCoverage, File testSource)
437             throws IOException {
438 
439         Collection<Class<?>> classes = JarTestFinder.getClasses(testSource);
440         for (Class<?> c : classes) {
441             for (java.lang.reflect.Method m : c.getMethods()) {
442                 if (m.isAnnotationPresent(CddTest.class)) {
443                     CddTest cddTest = m.getAnnotation(CddTest.class);
444                     CddCoverage.TestMethod testMethod =
445                             new CddCoverage.TestMethod(
446                                     testSource.getName(), c.getName(), m.getName());
447                     cddCoverage.addCoverage(cddTest.requirement(), testMethod);
448                 }
449             }
450         }
451     }
452 
453     private static void addCddApkCoverage(
454         CddCoverage cddCoverage, File testSource, int api)
455             throws IOException {
456 
457         DexFile dexFile = null;
458         try {
459             dexFile = DexFileFactory.loadDexFile(testSource, Opcodes.forApi(api));
460         } catch (IOException | DexFileFactory.DexFileNotFoundException e) {
461             System.err.println("Unable to load dex file: " + testSource.getPath());
462             return;
463         }
464 
465         String moduleName = testSource.getName();
466         for (ClassDef classDef : dexFile.getClasses()) {
467             String className = classDef.getType();
468             handleAnnotations(
469                 cddCoverage, moduleName, className, null /*methodName*/,
470                 classDef.getAnnotations());
471 
472             for (Method method : classDef.getMethods()) {
473                 String methodName = method.getName();
474                 handleAnnotations(
475                     cddCoverage, moduleName, className, methodName, method.getAnnotations());
476             }
477         }
478     }
479 
480     private static void handleAnnotations(
481             CddCoverage cddCoverage, String moduleName, String className,
482                     String methodName, Set<? extends Annotation> annotations) {
483         for (Annotation annotation : annotations) {
484             if (annotation.getType().equals(CDD_REQUIREMENT_ANNOTATION)) {
485                 for (AnnotationElement annotationElement : annotation.getElements()) {
486                     if (annotationElement.getName().equals(CDD_REQUIREMENT_ELEMENT_NAME)) {
487                         String cddRequirement =
488                                 ((StringEncodedValue) annotationElement.getValue()).getValue();
489                         CddCoverage.TestMethod testMethod =
490                                 new CddCoverage.TestMethod(
491                                         moduleName, dexToJavaName(className), methodName);
492                         cddCoverage.addCoverage(cddRequirement, testMethod);
493                     }
494                 }
495             }
496         }
497     }
498 
499     /**
500      * Given a string like Landroid/app/cts/DownloadManagerTest;
501      * return android.app.cts.DownloadManagerTest.
502      */
503     private static String dexToJavaName(String dexName) {
504         if (!dexName.startsWith("L") || !dexName.endsWith(";")) {
505             return dexName;
506         }
507         dexName = dexName.replace('/', '.');
508         if (dexName.length() > 2) {
509             dexName = dexName.substring(1, dexName.length() - 1);
510         }
511         return dexName;
512     }
513 
514     private static CddCoverage getEmptyCddCoverage() {
515         CddCoverage cddCoverage = new CddCoverage();
516         // TODO(nicksauer): Read in the valid list of requirements
517         return cddCoverage;
518     }
519 
520     private static void outputCoverageReport(ApiCoverage apiCoverage, CddCoverage cddCoverage,
521             List<File> testApks, File outputFile, int format, PackageFilter packageFilter,
522             String reportTitle)
523                 throws IOException, TransformerException, InterruptedException {
524 
525         OutputStream out = outputFile != null
526                 ? new FileOutputStream(outputFile)
527                 : System.out;
528 
529         try {
530             switch (format) {
531                 case FORMAT_TXT:
532                     TextReport.printTextReport(apiCoverage, cddCoverage, packageFilter, out);
533                     break;
534 
535                 case FORMAT_XML:
536                     XmlReport.printXmlReport(testApks, apiCoverage, cddCoverage,
537                         packageFilter, reportTitle, out);
538                     break;
539 
540                 case FORMAT_HTML:
541                     HtmlReport.printHtmlReport(testApks, apiCoverage, cddCoverage,
542                         packageFilter, reportTitle, out);
543                     break;
544             }
545         } finally {
546             out.close();
547         }
548     }
549 }
550