xref: /aosp_15_r20/prebuilts/misc/common/jacoco/src/main/java/com/google/android/jacoco/reporter/ReportGenerator.java (revision 847dbab7980efcc7f5706bb9c6d844b91a680afd)
1 /*
2  * Copyright (C) 2017 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
5  * except in compliance with the License. You may obtain a copy of the License at
6  *
7  *      http://www.apache.org/licenses/LICENSE-2.0
8  *
9  * Unless required by applicable law or agreed to in writing, software distributed under the
10  * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
11  * KIND, either express or implied. See the License for the specific language governing
12  * permissions and limitations under the License.
13  */
14 package com.google.android.jacoco.reporter;
15 
16 import org.apache.commons.cli.CommandLine;
17 import org.apache.commons.cli.HelpFormatter;
18 import org.apache.commons.cli.Option;
19 import org.apache.commons.cli.Options;
20 import org.apache.commons.cli.ParseException;
21 import org.apache.commons.cli.PosixParser;
22 import org.jacoco.core.analysis.Analyzer;
23 import org.jacoco.core.analysis.CoverageBuilder;
24 import org.jacoco.core.analysis.IBundleCoverage;
25 import org.jacoco.core.data.ExecutionDataStore;
26 import org.jacoco.core.tools.ExecFileLoader;
27 import org.jacoco.report.DirectorySourceFileLocator;
28 import org.jacoco.report.FileMultiReportOutput;
29 import org.jacoco.report.IMultiReportOutput;
30 import org.jacoco.report.IReportVisitor;
31 import org.jacoco.report.MultiReportVisitor;
32 import org.jacoco.report.MultiSourceFileLocator;
33 import org.jacoco.report.html.HTMLFormatter;
34 import org.jacoco.report.xml.XMLFormatter;
35 
36 import java.io.File;
37 import java.io.FileNotFoundException;
38 import java.io.FileOutputStream;
39 import java.io.IOException;
40 import java.io.InputStream;
41 import java.util.ArrayList;
42 import java.util.List;
43 import java.util.jar.JarFile;
44 import java.util.stream.Collectors;
45 
46 public class ReportGenerator {
47     private static final String OPT_CLASSPATH = "classpath";
48     private static final String OPT_REPORT_NAME = "name";
49     private static final String OPT_EXEC_FILE = "exec-file";
50     private static final String OPT_SOURCES = "srcs";
51     private static final String OPT_SRCJARS = "srcjars";
52     private static final String OPT_REPORT_DIR = "report-dir";
53     private static final int TAB_WIDTH = 4;
54 
55     private final Config mConfig;
56 
ReportGenerator(Config config)57     private ReportGenerator(Config config) {
58         mConfig = config;
59     }
60 
execute()61     private void execute() throws IOException {
62         ExecFileLoader execFileLoader = new ExecFileLoader();
63         execFileLoader.load(mConfig.mExecFileDir);
64         IReportVisitor reportVisitor = new MultiReportVisitor(getVisitors());
65         reportVisitor.visitInfo(execFileLoader.getSessionInfoStore().getInfos(),
66                 execFileLoader.getExecutionDataStore().getContents());
67         MultiSourceFileLocator sourceFileLocator = new MultiSourceFileLocator(TAB_WIDTH);
68         mConfig.mSourceDirs.stream().filter(File::isDirectory)
69                 .map(sourceDir -> new DirectorySourceFileLocator(sourceDir, null, TAB_WIDTH))
70                 .forEach(sourceFileLocator::add);
71         mConfig.mSrcJars.stream()
72                 .map(srcJar -> new JarSourceFileLocator(srcJar, null, TAB_WIDTH))
73                 .forEach(sourceFileLocator::add);
74         reportVisitor.visitBundle(createBundle(execFileLoader.getExecutionDataStore()),
75                 sourceFileLocator);
76         reportVisitor.visitEnd();
77     }
78 
createBundle(ExecutionDataStore dataStore)79     private IBundleCoverage createBundle(ExecutionDataStore dataStore) throws IOException {
80         CoverageBuilder coverageBuilder = new CoverageBuilder();
81         Analyzer analyzer = new Analyzer(dataStore, coverageBuilder) {
82             @Override
83             public void analyzeClass(InputStream input, String location) throws IOException {
84                 String className = parseClassName(location);
85                 if (weHaveSourceFor(className, "java")
86                         || weHaveSourceFor(className, "kt")) {
87                     super.analyzeClass(input, location);
88                 }
89             }
90 
91             private String parseClassName(String location) {
92                 return location.substring(location.indexOf('@') + 1, location.lastIndexOf('.'));
93             }
94 
95             private boolean weHaveSourceFor(String asmClassName, String fileExtension) {
96                 String fileName = asmClassName.replaceFirst("\\$.*", "") + "." + fileExtension;
97                 return mConfig.mSourceDirs.stream().map(parent -> new File(parent, fileName))
98                         .anyMatch(File::exists) ||
99                         mConfig.mSrcJars.stream().anyMatch(srcJar -> srcJar.stream().anyMatch(
100                                 it -> it.getName().endsWith(fileName)));
101             }
102         };
103 
104         for (File file : mConfig.mClasspath) {
105             analyzer.analyzeAll(file);
106         }
107         return coverageBuilder.getBundle(mConfig.mReportName);
108     }
109 
getVisitors()110     private List<IReportVisitor> getVisitors() throws IOException {
111         List<IReportVisitor> visitors = new ArrayList<>();
112         visitors.add(new XMLFormatter().createVisitor(mConfig.getXmlOutputStream()));
113         visitors.add(new HTMLFormatter().createVisitor(mConfig.getHtmlReportOutput()));
114         return visitors;
115     }
116 
117     private static class Config {
118         final String mReportName;
119         final List<File> mClasspath;
120         final List<File> mSourceDirs;
121         final List<JarFile> mSrcJars;
122         final File mReportDir;
123         final File mExecFileDir;
124 
Config(String reportName, List<File> classpath, List<File> sourceDirs, List<JarFile> srcJars, File reportDir, File execFileDir)125         Config(String reportName, List<File> classpath, List<File> sourceDirs,
126                 List<JarFile> srcJars, File reportDir, File execFileDir) {
127             mReportName = reportName;
128             mClasspath = classpath;
129             mSourceDirs = sourceDirs;
130             mSrcJars = srcJars;
131             mReportDir = reportDir;
132             mExecFileDir = execFileDir;
133         }
134 
getXmlOutputStream()135         FileOutputStream getXmlOutputStream() throws FileNotFoundException {
136             File xmlFile = new File(mReportDir, "coverage.xml");
137             return new FileOutputStream(xmlFile);
138         }
139 
from(CommandLine commandLine)140         static Config from(CommandLine commandLine) {
141             List<File> classpaths = parse(commandLine.getOptionValue(OPT_CLASSPATH),
142                     "WARN: Classpath entry [%s] does not exist or is not a directory");
143             List<File> sources = parse(commandLine.getOptionValue(OPT_SOURCES),
144                     "WARN: Source entry [%s] does not exist or is not a directory");
145             List<File> srcJars = parse(commandLine.getOptionValue(OPT_SRCJARS),
146                     "WARN: srcjars entry [%s] does not exist");
147 
148             ensure(!sources.isEmpty() || !srcJars.isEmpty(),
149                     "--%s or --%s argument is required", OPT_SOURCES, OPT_SRCJARS);
150 
151             List<JarFile> srcJarFiles = srcJars.stream().filter(File::exists)
152                     .map(srcJar -> {
153                         try {
154                             return new JarFile(srcJar);
155                         } catch (IOException e) {
156                             System.out.printf("WARN: Failed to open srcjars file [%s]", srcJar);
157                             return null;
158                         }
159                     }).filter(jarFile -> jarFile != null).collect(Collectors.toList());
160 
161             File execFileDir = new File(commandLine.getOptionValue(OPT_EXEC_FILE));
162             ensure(execFileDir.exists() && execFileDir.canRead() && execFileDir.isFile(),
163                     "execFile: [%s] does not exist or could not be read.", execFileDir);
164 
165             File reportDir = new File(commandLine.getOptionValue(OPT_REPORT_DIR));
166             ensure(reportDir.exists() || reportDir.mkdirs(),
167                     "Unable to create report dir [%s]", reportDir);
168 
169             return new Config(commandLine.getOptionValue(OPT_REPORT_NAME), classpaths, sources,
170                     srcJarFiles, reportDir, execFileDir);
171         }
172 
getHtmlReportOutput()173         IMultiReportOutput getHtmlReportOutput() {
174             return new FileMultiReportOutput(mReportDir);
175         }
176     }
177 
ensure(boolean condition, String errorMessage, Object... args)178     private static void ensure(boolean condition, String errorMessage, Object... args) {
179         if (!condition) {
180             System.err.printf(errorMessage, args);
181             System.exit(0);
182         }
183     }
184 
parse(String value, String warningMessage)185     private static List<File> parse(String value, String warningMessage) {
186         List<File> files = new ArrayList<>(0);
187         if (value != null) {
188             for (String classpath : value.split(System.getProperty("path.separator"))) {
189                 File file = new File(classpath);
190                 if (file.exists()) {
191                     files.add(file);
192                 } else {
193                     System.out.println(String.format(warningMessage, classpath));
194                 }
195             }
196         }
197         return files;
198     }
199 
printHelp(ParseException e, Options options)200     private static void printHelp(ParseException e, Options options) {
201         System.out.println(e.getMessage());
202         HelpFormatter helpFormatter = new HelpFormatter();
203         helpFormatter.printHelp("java -cp ${report.jar} " + ReportGenerator.class.getName(),
204                 "Generates jacoco reports in XML and HTML format.", options, "", true);
205     }
206 
addOption(Options options, String longName, String description, boolean required)207     private static void addOption(Options options, String longName, String description,
208             boolean required) {
209         Option option = new Option(null, longName, true, description);
210         option.setArgs(1);
211         option.setRequired(required);
212         options.addOption(option);
213     }
214 
addOption(Options options, String longName, String description)215     private static void addOption(Options options, String longName, String description) {
216         addOption(options, longName, description, true);
217     }
218 
main(String[] args)219     public static void main(String[] args) {
220         Options options = new Options();
221         try {
222             // TODO: Support multiple exec-files
223             addOption(options, OPT_CLASSPATH, "Classpath used during testing");
224             addOption(options, OPT_EXEC_FILE, "File generated by jacoco during testing");
225             addOption(options, OPT_REPORT_NAME, "Name of the project tested");
226             addOption(options, OPT_REPORT_DIR, "Directory into which reports will be generated");
227             addOption(options, OPT_SOURCES, "List of source directories", false);
228             addOption(options, OPT_SRCJARS, "List of jars containing source files", false);
229             CommandLine commandLine = new PosixParser().parse(options, args);
230             new ReportGenerator(Config.from(commandLine))
231                     .execute();
232         } catch (ParseException e) {
233             printHelp(e, options);
234             System.exit(1);
235         } catch (IOException e) {
236             e.printStackTrace();
237             System.exit(1);
238         }
239     }
240 }
241