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 import org.objectweb.asm.ClassReader; 36 37 import java.io.File; 38 import java.io.FileNotFoundException; 39 import java.io.FileOutputStream; 40 import java.io.IOException; 41 import java.util.ArrayList; 42 import java.util.List; 43 44 public class ReportGenerator { 45 private static final String OPT_CLASSPATH = "classpath"; 46 private static final String OPT_REPORT_NAME = "name"; 47 private static final String OPT_EXEC_FILE = "exec-file"; 48 private static final String OPT_SOURCES = "srcs"; 49 private static final String OPT_REPORT_DIR = "report-dir"; 50 private static final int TAB_WIDTH = 4; 51 52 private final Config mConfig; 53 54 private ReportGenerator(Config config) { 55 mConfig = config; 56 } 57 58 private void execute() { 59 ExecFileLoader execFileLoader = new ExecFileLoader(); 60 try { 61 execFileLoader.load(mConfig.mExecFileDir); 62 IReportVisitor reportVisitor = new MultiReportVisitor(getVisitors()); 63 reportVisitor.visitInfo(execFileLoader.getSessionInfoStore().getInfos(), 64 execFileLoader.getExecutionDataStore().getContents()); 65 MultiSourceFileLocator sourceFileLocator = new MultiSourceFileLocator(TAB_WIDTH); 66 mConfig.mSourceDirs.stream().filter(File::isDirectory) 67 .map(sourceDir -> new DirectorySourceFileLocator(sourceDir, null, TAB_WIDTH)) 68 .forEach(sourceFileLocator::add); 69 reportVisitor.visitBundle(createBundle(execFileLoader.getExecutionDataStore()), 70 sourceFileLocator); 71 reportVisitor.visitEnd(); 72 } catch (Exception e) { 73 e.printStackTrace(); 74 } 75 } 76 77 private IBundleCoverage createBundle(ExecutionDataStore dataStore) throws IOException { 78 CoverageBuilder coverageBuilder = new CoverageBuilder(); 79 Analyzer analyzer = new Analyzer(dataStore, coverageBuilder) { 80 @Override 81 public void analyzeClass(ClassReader reader) { 82 if (weHaveSourceFor(reader.getClassName())) { 83 super.analyzeClass(reader); 84 } 85 } 86 87 private boolean weHaveSourceFor(String asmClassName) { 88 String fileName = asmClassName.replaceFirst("\\$.*", "") + ".java"; 89 return mConfig.mSourceDirs.stream().map(parent -> new File(parent, fileName)) 90 .anyMatch(File::exists); 91 } 92 }; 93 94 for (File file : mConfig.mClasspath) { 95 analyzer.analyzeAll(file); 96 } 97 return coverageBuilder.getBundle(mConfig.mReportName); 98 } 99 100 private List<IReportVisitor> getVisitors() throws Exception { 101 List<IReportVisitor> visitors = new ArrayList<>(); 102 visitors.add(new XMLFormatter().createVisitor(mConfig.getXmlOutputStream())); 103 visitors.add(new HTMLFormatter().createVisitor(mConfig.getHtmlReportOutput())); 104 return visitors; 105 } 106 107 private static class Config { 108 final String mReportName; 109 final List<File> mClasspath; 110 final List<File> mSourceDirs; 111 final File mReportDir; 112 final File mExecFileDir; 113 114 Config(String reportName, List<File> classpath, List<File> sourceDirs, 115 File reportDir, File execFileDir) { 116 mReportName = reportName; 117 mClasspath = classpath; 118 mSourceDirs = sourceDirs; 119 mReportDir = reportDir; 120 mExecFileDir = execFileDir; 121 } 122 123 FileOutputStream getXmlOutputStream() throws FileNotFoundException { 124 File xmlFile = new File(mReportDir, "coverage.xml"); 125 return new FileOutputStream(xmlFile); 126 } 127 128 static Config from(CommandLine commandLine) { 129 List<File> classpaths = parse(commandLine.getOptionValue(OPT_CLASSPATH), 130 "WARN: Classpath entry [%s] does not exist or is not a directory"); 131 List<File> sources = parse(commandLine.getOptionValue(OPT_SOURCES), 132 "WARN: Source entry [%s] does not exist or is not a directory"); 133 File execFileDir = new File(commandLine.getOptionValue(OPT_EXEC_FILE)); 134 ensure(execFileDir.exists() && execFileDir.canRead() && execFileDir.isFile(), 135 "execFile: [%s] does not exist or could not be read.", execFileDir); 136 137 File reportDir = new File(commandLine.getOptionValue(OPT_REPORT_DIR)); 138 ensure(reportDir.exists() || reportDir.mkdirs(), 139 "Unable to create report dir [%s]", reportDir); 140 141 return new Config(commandLine.getOptionValue(OPT_REPORT_NAME), classpaths, sources, 142 reportDir, execFileDir); 143 } 144 145 IMultiReportOutput getHtmlReportOutput() { 146 return new FileMultiReportOutput(mReportDir); 147 } 148 } 149 150 private static void ensure(boolean condition, String errorMessage, Object... args) { 151 if (!condition) { 152 System.err.printf(errorMessage, args); 153 System.exit(0); 154 } 155 } 156 157 private static List<File> parse(String value, String warningMessage) { 158 List<File> files = new ArrayList<>(0); 159 for (String classpath : value.split(System.getProperty("path.separator"))) { 160 File file = new File(classpath); 161 if (file.exists()) { 162 files.add(file); 163 } else { 164 System.out.println(String.format(warningMessage, classpath)); 165 } 166 } 167 return files; 168 } 169 170 private static void printHelp(ParseException e, Options options) { 171 System.out.println(e.getMessage()); 172 HelpFormatter helpFormatter = new HelpFormatter(); 173 helpFormatter.printHelp("java -cp ${report.jar} " + ReportGenerator.class.getName(), 174 "Generates jacoco reports in XML and HTML format.", options, "", true); 175 } 176 177 private static void addOption(Options options, String longName, String description) { 178 Option option = new Option(null, longName, true, description); 179 option.setArgs(1); 180 option.setRequired(true); 181 options.addOption(option); 182 } 183 184 public static void main(String[] args) { 185 Options options = new Options(); 186 try { 187 // TODO: Support multiple exec-files 188 addOption(options, OPT_CLASSPATH, "Classpath used during testing"); 189 addOption(options, OPT_EXEC_FILE, "File generated by jacoco during testing"); 190 addOption(options, OPT_REPORT_NAME, "Name of the project tested"); 191 addOption(options, OPT_REPORT_DIR, "Directory into which reports will be generated"); 192 addOption(options, OPT_SOURCES, "List of source directories"); 193 CommandLine commandLine = new PosixParser().parse(options, args); 194 new ReportGenerator(Config.from(commandLine)) 195 .execute(); 196 } catch (ParseException e) { 197 printHelp(e, options); 198 System.exit(1); 199 } 200 } 201 } 202