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