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