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 org.xml.sax.InputSource;
20 import org.xml.sax.SAXException;
21 import org.xml.sax.XMLReader;
22 import org.xml.sax.helpers.XMLReaderFactory;
23 
24 import java.io.File;
25 import java.io.FileOutputStream;
26 import java.io.FileReader;
27 import java.io.IOException;
28 import java.io.OutputStream;
29 import java.util.ArrayList;
30 import java.util.List;
31 
32 import javax.xml.transform.TransformerException;
33 
34 /**
35  * Tool that generates a report of what Android framework methods are being called from a given
36  * set of APKS. See the {@link #printUsage()} method for more details.
37  */
38 public class CtsApiCoverage {
39 
40     private static final int FORMAT_TXT = 0;
41 
42     private static final int FORMAT_XML = 1;
43 
44     private static final int FORMAT_HTML = 2;
45 
printUsage()46     private static void printUsage() {
47         System.out.println("Usage: cts-api-coverage [OPTION]... [APK]...");
48         System.out.println();
49         System.out.println("Generates a report about what Android framework methods are called ");
50         System.out.println("from the given APKs.");
51         System.out.println();
52         System.out.println("Use the Makefiles rules in CtsTestCoverage.mk to generate the report ");
53         System.out.println("rather than executing this directly. If you still want to run this ");
54         System.out.println("directly, then this must be used from the $ANDROID_BUILD_TOP ");
55         System.out.println("directory and dexdeps must be built via \"make dexdeps\".");
56         System.out.println();
57         System.out.println("Options:");
58         System.out.println("  -o FILE                output file or standard out if not given");
59         System.out.println("  -f [txt|xml|html]      format of output");
60         System.out.println("  -d PATH                path to dexdeps or expected to be in $PATH");
61         System.out.println("  -a PATH                path to the API XML file");
62         System.out.println("  -p PACKAGENAMEPREFIX   report coverage only for package that start with");
63         System.out.println("  -t TITLE               report title");
64         System.out.println();
65         System.exit(1);
66     }
67 
main(String[] args)68     public static void main(String[] args) throws Exception {
69         List<File> testApks = new ArrayList<File>();
70         File outputFile = null;
71         int format = FORMAT_TXT;
72         String dexDeps = "dexDeps";
73         String apiXmlPath = "";
74         PackageFilter packageFilter = new PackageFilter();
75         String reportTitle = "CTS API Coverage";
76 
77         for (int i = 0; i < args.length; i++) {
78             if (args[i].startsWith("-")) {
79                 if ("-o".equals(args[i])) {
80                     outputFile = new File(getExpectedArg(args, ++i));
81                 } else if ("-f".equals(args[i])) {
82                     String formatSpec = getExpectedArg(args, ++i);
83                     if ("xml".equalsIgnoreCase(formatSpec)) {
84                         format = FORMAT_XML;
85                     } else if ("txt".equalsIgnoreCase(formatSpec)) {
86                         format = FORMAT_TXT;
87                     } else if ("html".equalsIgnoreCase(formatSpec)) {
88                         format = FORMAT_HTML;
89                     } else {
90                         printUsage();
91                     }
92                 } else if ("-d".equals(args[i])) {
93                     dexDeps = getExpectedArg(args, ++i);
94                 } else if ("-a".equals(args[i])) {
95                     apiXmlPath = getExpectedArg(args, ++i);
96                 } else if ("-p".equals(args[i])) {
97                     packageFilter.addPrefixToFilter(getExpectedArg(args, ++i));
98                 } else if ("-t".equals(args[i])) {
99                     reportTitle = getExpectedArg(args, ++i);
100                 } else {
101                     printUsage();
102                 }
103             } else {
104                 testApks.add(new File(args[i]));
105             }
106         }
107 
108         /*
109          * 1. Create an ApiCoverage object that is a tree of Java objects representing the API
110          *    in current.xml. The object will have no information about the coverage for each
111          *    constructor or method yet.
112          *
113          * 2. For each provided APK, scan it using dexdeps, parse the output of dexdeps, and
114          *    call methods on the ApiCoverage object to cumulatively add coverage stats.
115          *
116          * 3. Output a report based on the coverage stats in the ApiCoverage object.
117          */
118 
119         ApiCoverage apiCoverage = getEmptyApiCoverage(apiXmlPath);
120         // Add superclass information into api coverage.
121         apiCoverage.resolveSuperClasses();
122         for (File testApk : testApks) {
123             addApiCoverage(apiCoverage, testApk, dexDeps);
124         }
125         outputCoverageReport(apiCoverage, testApks, outputFile, format, packageFilter, reportTitle);
126     }
127 
128     /** Get the argument or print out the usage and exit. */
getExpectedArg(String[] args, int index)129     private static String getExpectedArg(String[] args, int index) {
130         if (index < args.length) {
131             return args[index];
132         } else {
133             printUsage();
134             return null;    // Never will happen because printUsage will call exit(1)
135         }
136     }
137 
138     /**
139      * Creates an object representing the API that will be used later to collect coverage
140      * statistics as we iterate over the test APKs.
141      *
142      * @param apiXmlPath to the API XML file
143      * @return an {@link ApiCoverage} object representing the API in current.xml without any
144      *     coverage statistics yet
145      */
getEmptyApiCoverage(String apiXmlPath)146     private static ApiCoverage getEmptyApiCoverage(String apiXmlPath)
147             throws SAXException, IOException {
148         XMLReader xmlReader = XMLReaderFactory.createXMLReader();
149         CurrentXmlHandler currentXmlHandler = new CurrentXmlHandler();
150         xmlReader.setContentHandler(currentXmlHandler);
151 
152         File currentXml = new File(apiXmlPath);
153         FileReader fileReader = null;
154         try {
155             fileReader = new FileReader(currentXml);
156             xmlReader.parse(new InputSource(fileReader));
157         } finally {
158             if (fileReader != null) {
159                 fileReader.close();
160             }
161         }
162 
163         return currentXmlHandler.getApi();
164     }
165 
166     /**
167      * Adds coverage information gleamed from running dexdeps on the APK to the
168      * {@link ApiCoverage} object.
169      *
170      * @param apiCoverage object to which the coverage statistics will be added to
171      * @param testApk containing the tests that will be scanned by dexdeps
172      */
addApiCoverage(ApiCoverage apiCoverage, File testApk, String dexdeps)173     private static void addApiCoverage(ApiCoverage apiCoverage, File testApk, String dexdeps)
174             throws SAXException, IOException {
175         XMLReader xmlReader = XMLReaderFactory.createXMLReader();
176         DexDepsXmlHandler dexDepsXmlHandler = new DexDepsXmlHandler(apiCoverage);
177         xmlReader.setContentHandler(dexDepsXmlHandler);
178 
179         String apkPath = testApk.getPath();
180         Process process = new ProcessBuilder(dexdeps, "--format=xml", apkPath).start();
181         try {
182             xmlReader.parse(new InputSource(process.getInputStream()));
183         } catch (SAXException e) {
184           // Catch this exception, but continue. SAXException is acceptable in cases
185           // where the apk does not contain a classes.dex and therefore parsing won't work.
186           System.err.println("warning: dexdeps failed for: " + apkPath);
187         }
188     }
189 
outputCoverageReport(ApiCoverage apiCoverage, List<File> testApks, File outputFile, int format, PackageFilter packageFilter, String reportTitle)190     private static void outputCoverageReport(ApiCoverage apiCoverage, List<File> testApks,
191             File outputFile, int format, PackageFilter packageFilter, String reportTitle)
192                 throws IOException, TransformerException, InterruptedException {
193 
194         OutputStream out = outputFile != null
195                 ? new FileOutputStream(outputFile)
196                 : System.out;
197 
198         try {
199             switch (format) {
200                 case FORMAT_TXT:
201                     TextReport.printTextReport(apiCoverage, packageFilter, out);
202                     break;
203 
204                 case FORMAT_XML:
205                     XmlReport.printXmlReport(testApks, apiCoverage, packageFilter, reportTitle, out);
206                     break;
207 
208                 case FORMAT_HTML:
209                     HtmlReport.printHtmlReport(testApks, apiCoverage, packageFilter, reportTitle, out);
210                     break;
211             }
212         } finally {
213             out.close();
214         }
215     }
216 }
217