Yue Gan | af3c412 | 2016-12-05 14:36:02 +0000 | [diff] [blame] | 1 | // Copyright 2016 The Bazel Authors. All Rights Reserved. |
| 2 | // |
| 3 | // Licensed under the Apache License, Version 2.0 (the "License"); |
| 4 | // you may not use this file except in compliance with the License. |
| 5 | // 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 |
| 10 | // distributed under the License is distributed on an "AS IS" BASIS, |
| 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 12 | // See the License for the specific language governing permissions and |
| 13 | // limitations under the License. |
| 14 | |
| 15 | package com.google.testing.coverage; |
| 16 | |
elenairina | 2349149 | 2017-07-14 16:01:49 +0200 | [diff] [blame] | 17 | import static java.nio.charset.StandardCharsets.UTF_8; |
| 18 | |
| 19 | import com.google.common.annotations.VisibleForTesting; |
elenairina | 2349149 | 2017-07-14 16:01:49 +0200 | [diff] [blame] | 20 | import com.google.common.collect.ImmutableList; |
elenairina | 3c949cc | 2019-01-11 04:17:26 -0800 | [diff] [blame] | 21 | import com.google.common.collect.ImmutableList.Builder; |
elenairina | 2349149 | 2017-07-14 16:01:49 +0200 | [diff] [blame] | 22 | import com.google.common.collect.ImmutableSet; |
elenairina | 27ff758 | 2019-02-20 02:42:22 -0800 | [diff] [blame] | 23 | import com.google.common.io.ByteStreams; |
elenairina | 05418b3 | 2017-08-14 11:16:44 +0200 | [diff] [blame] | 24 | import com.google.common.io.Files; |
elenairina | 2349149 | 2017-07-14 16:01:49 +0200 | [diff] [blame] | 25 | import java.io.BufferedReader; |
Yue Gan | af3c412 | 2016-12-05 14:36:02 +0000 | [diff] [blame] | 26 | import java.io.ByteArrayInputStream; |
| 27 | import java.io.File; |
Yue Gan | af3c412 | 2016-12-05 14:36:02 +0000 | [diff] [blame] | 28 | import java.io.FileOutputStream; |
| 29 | import java.io.IOException; |
| 30 | import java.io.InputStream; |
elenairina | 2349149 | 2017-07-14 16:01:49 +0200 | [diff] [blame] | 31 | import java.io.InputStreamReader; |
Yue Gan | af3c412 | 2016-12-05 14:36:02 +0000 | [diff] [blame] | 32 | import java.io.Reader; |
iirina | c9522f9 | 2019-05-28 02:18:58 -0700 | [diff] [blame] | 33 | import java.lang.reflect.Field; |
Yue Gan | af3c412 | 2016-12-05 14:36:02 +0000 | [diff] [blame] | 34 | import java.lang.reflect.Method; |
| 35 | import java.net.URL; |
elenairina | 27ff758 | 2019-02-20 02:42:22 -0800 | [diff] [blame] | 36 | import java.net.URLClassLoader; |
Patrick Niklaus | 77b5658 | 2019-05-10 01:38:59 -0700 | [diff] [blame] | 37 | import java.net.URLDecoder; |
iirina | c9522f9 | 2019-05-28 02:18:58 -0700 | [diff] [blame] | 38 | import java.util.ArrayList; |
Yue Gan | af3c412 | 2016-12-05 14:36:02 +0000 | [diff] [blame] | 39 | import java.util.Enumeration; |
elenairina | 27ff758 | 2019-02-20 02:42:22 -0800 | [diff] [blame] | 40 | import java.util.HashMap; |
elenairina | 97966cc | 2017-07-19 15:40:11 +0200 | [diff] [blame] | 41 | import java.util.HashSet; |
Yue Gan | af3c412 | 2016-12-05 14:36:02 +0000 | [diff] [blame] | 42 | import java.util.List; |
| 43 | import java.util.Map; |
elenairina | 2349149 | 2017-07-14 16:01:49 +0200 | [diff] [blame] | 44 | import java.util.Set; |
Yue Gan | af3c412 | 2016-12-05 14:36:02 +0000 | [diff] [blame] | 45 | import java.util.TreeMap; |
| 46 | import java.util.jar.Attributes; |
elenairina | 2349149 | 2017-07-14 16:01:49 +0200 | [diff] [blame] | 47 | import java.util.jar.JarEntry; |
| 48 | import java.util.jar.JarFile; |
Yue Gan | af3c412 | 2016-12-05 14:36:02 +0000 | [diff] [blame] | 49 | import java.util.jar.Manifest; |
| 50 | import org.jacoco.agent.rt.IAgent; |
| 51 | import org.jacoco.agent.rt.RT; |
| 52 | import org.jacoco.core.analysis.Analyzer; |
| 53 | import org.jacoco.core.analysis.CoverageBuilder; |
| 54 | import org.jacoco.core.analysis.IBundleCoverage; |
| 55 | import org.jacoco.core.tools.ExecFileLoader; |
| 56 | import org.jacoco.report.IReportVisitor; |
| 57 | import org.jacoco.report.ISourceFileLocator; |
iirina | c9522f9 | 2019-05-28 02:18:58 -0700 | [diff] [blame] | 58 | import sun.misc.Unsafe; |
Yue Gan | af3c412 | 2016-12-05 14:36:02 +0000 | [diff] [blame] | 59 | |
| 60 | /** |
| 61 | * Runner class used to generate code coverage report when using Jacoco offline instrumentation. |
| 62 | * |
| 63 | * <p>The complete list of features available for Jacoco offline instrumentation: |
| 64 | * http://www.eclemma.org/jacoco/trunk/doc/offline.html |
| 65 | * |
| 66 | * <p>The structure is roughly following the canonical Jacoco example: |
| 67 | * http://www.eclemma.org/jacoco/trunk/doc/examples/java/ReportGenerator.java |
| 68 | * |
| 69 | * <p>The following environment variables are expected: |
elenairina | 2349149 | 2017-07-14 16:01:49 +0200 | [diff] [blame] | 70 | * <ul> |
| 71 | * <li>JAVA_COVERAGE_FILE - specifies final location of the generated lcov file.</li> |
| 72 | * <li>JACOCO_METADATA_JAR - specifies jar containing uninstrumented classes to be analyzed.</li> |
| 73 | * </ul> |
Yue Gan | af3c412 | 2016-12-05 14:36:02 +0000 | [diff] [blame] | 74 | */ |
| 75 | public class JacocoCoverageRunner { |
| 76 | |
elenairina | 2349149 | 2017-07-14 16:01:49 +0200 | [diff] [blame] | 77 | private final ImmutableList<File> classesJars; |
Yue Gan | af3c412 | 2016-12-05 14:36:02 +0000 | [diff] [blame] | 78 | private final InputStream executionData; |
| 79 | private final File reportFile; |
| 80 | private ExecFileLoader execFileLoader; |
elenairina | 27ff758 | 2019-02-20 02:42:22 -0800 | [diff] [blame] | 81 | private HashMap<String, byte[]> uninstrumentedClasses; |
| 82 | private ImmutableSet<String> pathsForCoverage = ImmutableSet.of(); |
Yue Gan | af3c412 | 2016-12-05 14:36:02 +0000 | [diff] [blame] | 83 | |
elenairina | 27ff758 | 2019-02-20 02:42:22 -0800 | [diff] [blame] | 84 | /** |
| 85 | * Creates a new coverage runner extracting the classes jars from a wrapper file. Uses |
| 86 | * javaRunfilesRoot to compute the absolute path of the jars inside the wrapper file. |
| 87 | */ |
| 88 | public JacocoCoverageRunner( |
iirina | c9522f9 | 2019-05-28 02:18:58 -0700 | [diff] [blame] | 89 | InputStream jacocoExec, String reportPath, File wrapperFile, String javaRunfilesRoot) |
elenairina | 27ff758 | 2019-02-20 02:42:22 -0800 | [diff] [blame] | 90 | throws IOException { |
Yue Gan | af3c412 | 2016-12-05 14:36:02 +0000 | [diff] [blame] | 91 | executionData = jacocoExec; |
| 92 | reportFile = new File(reportPath); |
elenairina | 27ff758 | 2019-02-20 02:42:22 -0800 | [diff] [blame] | 93 | this.classesJars = getFilesFromFileList(wrapperFile, javaRunfilesRoot); |
| 94 | } |
| 95 | |
iirina | c9522f9 | 2019-05-28 02:18:58 -0700 | [diff] [blame] | 96 | public JacocoCoverageRunner(InputStream jacocoExec, String reportPath, File... metadataJars) { |
elenairina | 27ff758 | 2019-02-20 02:42:22 -0800 | [diff] [blame] | 97 | executionData = jacocoExec; |
| 98 | reportFile = new File(reportPath); |
elenairina | 27ff758 | 2019-02-20 02:42:22 -0800 | [diff] [blame] | 99 | this.classesJars = ImmutableList.copyOf(metadataJars); |
| 100 | } |
| 101 | |
| 102 | public JacocoCoverageRunner( |
elenairina | 27ff758 | 2019-02-20 02:42:22 -0800 | [diff] [blame] | 103 | InputStream jacocoExec, |
| 104 | String reportPath, |
| 105 | HashMap<String, byte[]> uninstrumentedClasses, |
| 106 | ImmutableSet<String> pathsForCoverage, |
| 107 | File... metadataJars) { |
| 108 | executionData = jacocoExec; |
| 109 | reportFile = new File(reportPath); |
elenairina | 27ff758 | 2019-02-20 02:42:22 -0800 | [diff] [blame] | 110 | this.classesJars = ImmutableList.copyOf(metadataJars); |
| 111 | this.uninstrumentedClasses = uninstrumentedClasses; |
| 112 | this.pathsForCoverage = pathsForCoverage; |
Yue Gan | af3c412 | 2016-12-05 14:36:02 +0000 | [diff] [blame] | 113 | } |
| 114 | |
| 115 | public void create() throws IOException { |
| 116 | // Read the jacoco.exec file. Multiple data files could be merged at this point |
| 117 | execFileLoader = new ExecFileLoader(); |
| 118 | execFileLoader.load(executionData); |
| 119 | |
| 120 | // Run the structure analyzer on a single class folder or jar file to build up the coverage |
| 121 | // model. Typically you would create a bundle for each class folder and each jar you want in |
| 122 | // your report. If you have more than one bundle you may need to add a grouping node to the |
| 123 | // report. The lcov formatter doesn't seem to care, and we're only using one bundle anyway. |
| 124 | final IBundleCoverage bundleCoverage = analyzeStructure(); |
| 125 | |
| 126 | final Map<String, BranchCoverageDetail> branchDetails = analyzeBranch(); |
| 127 | createReport(bundleCoverage, branchDetails); |
| 128 | } |
| 129 | |
elenairina | 2349149 | 2017-07-14 16:01:49 +0200 | [diff] [blame] | 130 | @VisibleForTesting |
| 131 | void createReport( |
Yue Gan | af3c412 | 2016-12-05 14:36:02 +0000 | [diff] [blame] | 132 | final IBundleCoverage bundleCoverage, final Map<String, BranchCoverageDetail> branchDetails) |
| 133 | throws IOException { |
elenairina | 2349149 | 2017-07-14 16:01:49 +0200 | [diff] [blame] | 134 | JacocoLCOVFormatter formatter = new JacocoLCOVFormatter(createPathsSet()); |
Yue Gan | af3c412 | 2016-12-05 14:36:02 +0000 | [diff] [blame] | 135 | final IReportVisitor visitor = formatter.createVisitor(reportFile, branchDetails); |
| 136 | |
| 137 | // Initialize the report with all of the execution and session information. At this point the |
| 138 | // report doesn't know about the structure of the report being created. |
| 139 | visitor.visitInfo( |
| 140 | execFileLoader.getSessionInfoStore().getInfos(), |
| 141 | execFileLoader.getExecutionDataStore().getContents()); |
| 142 | |
| 143 | // Populate the report structure with the bundle coverage information. |
| 144 | // Call visitGroup if you need groups in your report. |
| 145 | |
| 146 | // Note the API requires a sourceFileLocator because the HTML and XML formatters display a page |
| 147 | // of code annotated with coverage information. Having the source files is not actually needed |
| 148 | // for generating the lcov report... |
| 149 | visitor.visitBundle( |
| 150 | bundleCoverage, |
| 151 | new ISourceFileLocator() { |
| 152 | |
| 153 | @Override |
| 154 | public Reader getSourceFile(String packageName, String fileName) throws IOException { |
| 155 | return null; |
| 156 | } |
| 157 | |
| 158 | @Override |
| 159 | public int getTabWidth() { |
| 160 | return 0; |
| 161 | } |
| 162 | }); |
| 163 | |
| 164 | // Signal end of structure information to allow report to write all information out |
| 165 | visitor.visitEnd(); |
| 166 | } |
| 167 | |
elenairina | 2349149 | 2017-07-14 16:01:49 +0200 | [diff] [blame] | 168 | @VisibleForTesting |
| 169 | IBundleCoverage analyzeStructure() throws IOException { |
Yue Gan | af3c412 | 2016-12-05 14:36:02 +0000 | [diff] [blame] | 170 | final CoverageBuilder coverageBuilder = new CoverageBuilder(); |
| 171 | final Analyzer analyzer = new Analyzer(execFileLoader.getExecutionDataStore(), coverageBuilder); |
elenairina | 97966cc | 2017-07-19 15:40:11 +0200 | [diff] [blame] | 172 | Set<String> alreadyInstrumentedClasses = new HashSet<>(); |
elenairina | 27ff758 | 2019-02-20 02:42:22 -0800 | [diff] [blame] | 173 | if (uninstrumentedClasses == null) { |
| 174 | for (File classesJar : classesJars) { |
iirina | c9522f9 | 2019-05-28 02:18:58 -0700 | [diff] [blame] | 175 | analyzeUninstrumentedClassesFromJar(analyzer, classesJar, alreadyInstrumentedClasses); |
elenairina | 27ff758 | 2019-02-20 02:42:22 -0800 | [diff] [blame] | 176 | } |
| 177 | } else { |
| 178 | for (Map.Entry<String, byte[]> entry : uninstrumentedClasses.entrySet()) { |
| 179 | analyzer.analyzeClass(entry.getValue(), entry.getKey()); |
elenairina | 2349149 | 2017-07-14 16:01:49 +0200 | [diff] [blame] | 180 | } |
Yue Gan | af3c412 | 2016-12-05 14:36:02 +0000 | [diff] [blame] | 181 | } |
| 182 | |
| 183 | // TODO(bazel-team): Find out where the name of the bundle can pop out in the report. |
| 184 | return coverageBuilder.getBundle("isthisevenused"); |
| 185 | } |
| 186 | |
| 187 | // Additional pass to process the branch details of the classes |
| 188 | private Map<String, BranchCoverageDetail> analyzeBranch() throws IOException { |
| 189 | final BranchDetailAnalyzer analyzer = |
| 190 | new BranchDetailAnalyzer(execFileLoader.getExecutionDataStore()); |
| 191 | |
| 192 | Map<String, BranchCoverageDetail> result = new TreeMap<>(); |
elenairina | 97966cc | 2017-07-19 15:40:11 +0200 | [diff] [blame] | 193 | Set<String> alreadyInstrumentedClasses = new HashSet<>(); |
elenairina | 27ff758 | 2019-02-20 02:42:22 -0800 | [diff] [blame] | 194 | if (uninstrumentedClasses == null) { |
| 195 | for (File classesJar : classesJars) { |
iirina | c9522f9 | 2019-05-28 02:18:58 -0700 | [diff] [blame] | 196 | analyzeUninstrumentedClassesFromJar(analyzer, classesJar, alreadyInstrumentedClasses); |
elenairina | 27ff758 | 2019-02-20 02:42:22 -0800 | [diff] [blame] | 197 | result.putAll(analyzer.getBranchDetails()); |
| 198 | } |
| 199 | } else { |
| 200 | for (Map.Entry<String, byte[]> entry : uninstrumentedClasses.entrySet()) { |
| 201 | analyzer.analyzeClass(entry.getValue(), entry.getKey()); |
elenairina | 2349149 | 2017-07-14 16:01:49 +0200 | [diff] [blame] | 202 | } |
Yue Gan | af3c412 | 2016-12-05 14:36:02 +0000 | [diff] [blame] | 203 | result.putAll(analyzer.getBranchDetails()); |
| 204 | } |
| 205 | return result; |
| 206 | } |
| 207 | |
elenairina | 2349149 | 2017-07-14 16:01:49 +0200 | [diff] [blame] | 208 | /** |
| 209 | * Analyzes all uninstrumented class files found in the given jar. |
| 210 | * |
| 211 | * <p>The uninstrumented classes are named using the .class.uninstrumented suffix. |
| 212 | */ |
elenairina | 97966cc | 2017-07-19 15:40:11 +0200 | [diff] [blame] | 213 | private void analyzeUninstrumentedClassesFromJar( |
| 214 | Analyzer analyzer, File jar, Set<String> alreadyInstrumentedClasses) throws IOException { |
elenairina | 2349149 | 2017-07-14 16:01:49 +0200 | [diff] [blame] | 215 | JarFile jarFile = new JarFile(jar); |
elenairina | 27ff758 | 2019-02-20 02:42:22 -0800 | [diff] [blame] | 216 | Enumeration<JarEntry> jarFileEntries = jarFile.entries(); |
| 217 | while (jarFileEntries.hasMoreElements()) { |
| 218 | JarEntry jarEntry = jarFileEntries.nextElement(); |
elenairina | 2349149 | 2017-07-14 16:01:49 +0200 | [diff] [blame] | 219 | String jarEntryName = jarEntry.getName(); |
elenairina | 97966cc | 2017-07-19 15:40:11 +0200 | [diff] [blame] | 220 | if (jarEntryName.endsWith(".class.uninstrumented") |
| 221 | && !alreadyInstrumentedClasses.contains(jarEntryName)) { |
elenairina | 2349149 | 2017-07-14 16:01:49 +0200 | [diff] [blame] | 222 | analyzer.analyzeAll(jarFile.getInputStream(jarEntry), jarEntryName); |
elenairina | 97966cc | 2017-07-19 15:40:11 +0200 | [diff] [blame] | 223 | alreadyInstrumentedClasses.add(jarEntryName); |
elenairina | 2349149 | 2017-07-14 16:01:49 +0200 | [diff] [blame] | 224 | } |
| 225 | } |
| 226 | } |
| 227 | |
| 228 | /** |
| 229 | * Creates a {@link Set} containing the paths of the covered Java files. |
| 230 | * |
| 231 | * <p>The paths are retrieved from a txt file that is found inside each jar containing |
| 232 | * uninstrumented classes. Each line of the txt file represents a path to be added to the set. |
| 233 | * |
| 234 | * <p>This set is needed by {@link JacocoLCOVFormatter} in order to output the correct path for |
| 235 | * each covered class. |
| 236 | */ |
| 237 | @VisibleForTesting |
| 238 | ImmutableSet<String> createPathsSet() throws IOException { |
elenairina | 27ff758 | 2019-02-20 02:42:22 -0800 | [diff] [blame] | 239 | if (!pathsForCoverage.isEmpty()) { |
| 240 | return pathsForCoverage; |
| 241 | } |
elenairina | 2349149 | 2017-07-14 16:01:49 +0200 | [diff] [blame] | 242 | ImmutableSet.Builder<String> execPathsSetBuilder = ImmutableSet.builder(); |
| 243 | for (File classJar : classesJars) { |
| 244 | addEntriesToExecPathsSet(classJar, execPathsSetBuilder); |
| 245 | } |
elenairina | 27ff758 | 2019-02-20 02:42:22 -0800 | [diff] [blame] | 246 | ImmutableSet<String> result = execPathsSetBuilder.build(); |
| 247 | return result; |
elenairina | 2349149 | 2017-07-14 16:01:49 +0200 | [diff] [blame] | 248 | } |
| 249 | |
| 250 | /** |
| 251 | * Adds to the given {@link Set} the paths found in a txt file inside the given jar. |
| 252 | * |
| 253 | * <p>If a jar contains uninstrumented classes it will also contain a txt file with the paths of |
| 254 | * each of these classes, one on each line. |
| 255 | */ |
| 256 | @VisibleForTesting |
| 257 | static void addEntriesToExecPathsSet( |
| 258 | File jar, ImmutableSet.Builder<String> execPathsSetBuilder) throws IOException { |
| 259 | JarFile jarFile = new JarFile(jar); |
elenairina | 27ff758 | 2019-02-20 02:42:22 -0800 | [diff] [blame] | 260 | Enumeration<JarEntry> jarFileEntries = jarFile.entries(); |
| 261 | while (jarFileEntries.hasMoreElements()) { |
| 262 | JarEntry jarEntry = jarFileEntries.nextElement(); |
elenairina | 2349149 | 2017-07-14 16:01:49 +0200 | [diff] [blame] | 263 | String jarEntryName = jarEntry.getName(); |
| 264 | if (jarEntryName.endsWith("-paths-for-coverage.txt")) { |
| 265 | BufferedReader bufferedReader = |
| 266 | new BufferedReader(new InputStreamReader(jarFile.getInputStream(jarEntry), UTF_8)); |
| 267 | String line; |
| 268 | while ((line = bufferedReader.readLine()) != null) { |
| 269 | execPathsSetBuilder.add(line); |
| 270 | } |
| 271 | } |
| 272 | } |
| 273 | } |
| 274 | |
iirina | c9522f9 | 2019-05-28 02:18:58 -0700 | [diff] [blame] | 275 | private static Class<?> getMainClass(boolean insideDeployJar) throws Exception { |
| 276 | Class<?> mainClass; |
| 277 | // If we're running inside a deploy jar we have to open the manifest and read the value of |
| 278 | // "Coverage-Main-Class", set by bazel. |
| 279 | // Note ClassLoader#getResource() will only return the first result, most likely a manifest |
| 280 | // from the bootclasspath. |
| 281 | if (insideDeployJar) { |
| 282 | if (JacocoCoverageRunner.class.getClassLoader() != null) { |
| 283 | Enumeration<URL> manifests = |
| 284 | JacocoCoverageRunner.class.getClassLoader().getResources("META-INF/MANIFEST.MF"); |
| 285 | while (manifests.hasMoreElements()) { |
| 286 | Manifest manifest = new Manifest(manifests.nextElement().openStream()); |
| 287 | Attributes attributes = manifest.getMainAttributes(); |
| 288 | String className = attributes.getValue("Coverage-Main-Class"); |
| 289 | if (className != null) { |
| 290 | // Some test frameworks use dummy Coverage-Main-Class in the deploy jars |
| 291 | // which should be ignored by JacocoCoverageRunner. |
| 292 | try { |
| 293 | mainClass = Class.forName(className); |
| 294 | return mainClass; |
| 295 | } catch (ClassNotFoundException e) { |
| 296 | // ignore this class and move on |
| 297 | } |
| 298 | } |
Yue Gan | af3c412 | 2016-12-05 14:36:02 +0000 | [diff] [blame] | 299 | } |
| 300 | } |
Yue Gan | af3c412 | 2016-12-05 14:36:02 +0000 | [diff] [blame] | 301 | } |
iirina | c9522f9 | 2019-05-28 02:18:58 -0700 | [diff] [blame] | 302 | // Check JACOCO_MAIN_CLASS after making sure we're not running inside a deploy jar, otherwise |
| 303 | // the deploy jar will be invoked using the wrong main class. |
| 304 | String jacocoMainClass = System.getenv("JACOCO_MAIN_CLASS"); |
| 305 | if (jacocoMainClass != null) { |
| 306 | return Class.forName(jacocoMainClass); |
| 307 | } |
| 308 | throw new IllegalStateException( |
| 309 | "JACOCO_METADATA_JAR/JACOCO_MAIN_CLASS environment variables not set, and no" |
| 310 | + " META-INF/MANIFEST.MF on the classpath has a Coverage-Main-Class attribute. " |
| 311 | + " Cannot determine the name of the main class for the code under test."); |
Yue Gan | af3c412 | 2016-12-05 14:36:02 +0000 | [diff] [blame] | 312 | } |
| 313 | |
| 314 | private static String getUniquePath(String pathTemplate, String suffix) throws IOException { |
| 315 | // If pathTemplate is null, we're likely executing from a deploy jar and the test framework |
| 316 | // did not properly set the environment for coverage reporting. This alone is not a reason for |
| 317 | // throwing an exception, we're going to run anyway and write the coverage data to a temporary, |
| 318 | // throw-away file. |
| 319 | if (pathTemplate == null) { |
| 320 | return File.createTempFile("coverage", suffix).getPath(); |
| 321 | } else { |
iirina | c9522f9 | 2019-05-28 02:18:58 -0700 | [diff] [blame] | 322 | // bazel sets the path template to a file with the .dat extension. lcov_merger matches all |
Yue Gan | af3c412 | 2016-12-05 14:36:02 +0000 | [diff] [blame] | 323 | // files having '.dat' in their name, so instead of appending we change the extension. |
| 324 | File absolutePathTemplate = new File(pathTemplate).getAbsoluteFile(); |
| 325 | String prefix = absolutePathTemplate.getName(); |
| 326 | int lastDot = prefix.lastIndexOf('.'); |
| 327 | if (lastDot != -1) { |
| 328 | prefix = prefix.substring(0, lastDot); |
| 329 | } |
| 330 | return File.createTempFile(prefix, suffix, absolutePathTemplate.getParentFile()).getPath(); |
| 331 | } |
| 332 | } |
| 333 | |
elenairina | 27ff758 | 2019-02-20 02:42:22 -0800 | [diff] [blame] | 334 | /** |
| 335 | * Returns an immutable list containing all the file paths found in mainFile. It uses the |
| 336 | * javaRunfilesRoot prefix for every found file to compute its absolute path. |
| 337 | */ |
| 338 | private static ImmutableList<File> getFilesFromFileList(File mainFile, String javaRunfilesRoot) |
| 339 | throws IOException { |
| 340 | List<String> metadataFiles = Files.readLines(mainFile, UTF_8); |
| 341 | ImmutableList.Builder<File> convertedMetadataFiles = new Builder<>(); |
| 342 | for (String metadataFile : metadataFiles) { |
| 343 | convertedMetadataFiles.add(new File(javaRunfilesRoot + "/" + metadataFile)); |
| 344 | } |
| 345 | return convertedMetadataFiles.build(); |
| 346 | } |
| 347 | |
iirina | c9522f9 | 2019-05-28 02:18:58 -0700 | [diff] [blame] | 348 | private static URL[] getUrls(ClassLoader classLoader) { |
| 349 | if (classLoader instanceof URLClassLoader) { |
| 350 | return ((URLClassLoader) classLoader).getURLs(); |
| 351 | } |
| 352 | |
| 353 | // java 9 and later |
| 354 | if (classLoader.getClass().getName().startsWith("jdk.internal.loader.ClassLoaders$")) { |
| 355 | try { |
| 356 | Field field = Unsafe.class.getDeclaredField("theUnsafe"); |
| 357 | field.setAccessible(true); |
| 358 | Unsafe unsafe = (Unsafe) field.get(null); |
| 359 | |
| 360 | // jdk.internal.loader.ClassLoaders.AppClassLoader.ucp |
| 361 | Field ucpField = classLoader.getClass().getDeclaredField("ucp"); |
| 362 | long ucpFieldOffset = unsafe.objectFieldOffset(ucpField); |
| 363 | Object ucpObject = unsafe.getObject(classLoader, ucpFieldOffset); |
| 364 | |
| 365 | // jdk.internal.loader.URLClassPath.path |
| 366 | Field pathField = ucpField.getType().getDeclaredField("path"); |
| 367 | long pathFieldOffset = unsafe.objectFieldOffset(pathField); |
| 368 | ArrayList<URL> path = (ArrayList<URL>) unsafe.getObject(ucpObject, pathFieldOffset); |
| 369 | |
| 370 | return path.toArray(new URL[path.size()]); |
| 371 | } catch (Exception e) { |
| 372 | return null; |
| 373 | } |
| 374 | } |
| 375 | return null; |
| 376 | } |
| 377 | |
Yue Gan | af3c412 | 2016-12-05 14:36:02 +0000 | [diff] [blame] | 378 | public static void main(String[] args) throws Exception { |
elenairina | 27ff758 | 2019-02-20 02:42:22 -0800 | [diff] [blame] | 379 | String metadataFile = System.getenv("JACOCO_METADATA_JAR"); |
| 380 | |
elenairina | 27ff758 | 2019-02-20 02:42:22 -0800 | [diff] [blame] | 381 | File[] metadataFiles = null; |
iirina | c9522f9 | 2019-05-28 02:18:58 -0700 | [diff] [blame] | 382 | int deployJars = 0; |
elenairina | 27ff758 | 2019-02-20 02:42:22 -0800 | [diff] [blame] | 383 | final HashMap<String, byte[]> uninstrumentedClasses = new HashMap<>(); |
| 384 | ImmutableSet.Builder<String> pathsForCoverageBuilder = new ImmutableSet.Builder<>(); |
iirina | c9522f9 | 2019-05-28 02:18:58 -0700 | [diff] [blame] | 385 | ClassLoader classLoader = ClassLoader.getSystemClassLoader(); |
| 386 | URL[] urls = getUrls(classLoader); |
| 387 | if (urls != null) { |
| 388 | metadataFiles = new File[urls.length]; |
| 389 | for (int i = 0; i < urls.length; i++) { |
| 390 | String file = URLDecoder.decode(urls[i].getFile(), "UTF-8"); |
| 391 | metadataFiles[i] = new File(file); |
| 392 | // Special case for when there is only one deploy jar on the classpath. |
| 393 | if (file.endsWith("_deploy.jar")) { |
| 394 | metadataFile = file; |
| 395 | deployJars++; |
| 396 | } |
| 397 | if (file.endsWith(".jar")) { |
| 398 | // Collect |
| 399 | // - uninstrumented class files for coverage before starting the actual test |
| 400 | // - paths considered for coverage |
| 401 | // Collecting these in the shutdown hook is too expensive (we only have a 5s budget). |
| 402 | JarFile jarFile = new JarFile(file); |
| 403 | Enumeration<JarEntry> jarFileEntries = jarFile.entries(); |
| 404 | while (jarFileEntries.hasMoreElements()) { |
| 405 | JarEntry jarEntry = jarFileEntries.nextElement(); |
| 406 | String jarEntryName = jarEntry.getName(); |
| 407 | if (jarEntryName.endsWith(".class.uninstrumented") |
| 408 | && !uninstrumentedClasses.containsKey(jarEntryName)) { |
| 409 | uninstrumentedClasses.put( |
| 410 | jarEntryName, ByteStreams.toByteArray(jarFile.getInputStream(jarEntry))); |
| 411 | } else if (jarEntryName.endsWith("-paths-for-coverage.txt")) { |
| 412 | BufferedReader bufferedReader = |
| 413 | new BufferedReader( |
| 414 | new InputStreamReader(jarFile.getInputStream(jarEntry), UTF_8)); |
| 415 | String line; |
| 416 | while ((line = bufferedReader.readLine()) != null) { |
| 417 | pathsForCoverageBuilder.add(line); |
elenairina | 27ff758 | 2019-02-20 02:42:22 -0800 | [diff] [blame] | 418 | } |
| 419 | } |
| 420 | } |
| 421 | } |
| 422 | } |
| 423 | } |
iirina | c9522f9 | 2019-05-28 02:18:58 -0700 | [diff] [blame] | 424 | |
elenairina | 27ff758 | 2019-02-20 02:42:22 -0800 | [diff] [blame] | 425 | final ImmutableSet<String> pathsForCoverage = pathsForCoverageBuilder.build(); |
| 426 | final String metadataFileFinal = metadataFile; |
| 427 | final File[] metadataFilesFinal = metadataFiles; |
elenairina | 05418b3 | 2017-08-14 11:16:44 +0200 | [diff] [blame] | 428 | final String javaRunfilesRoot = System.getenv("JACOCO_JAVA_RUNFILES_ROOT"); |
elenairina | 2349149 | 2017-07-14 16:01:49 +0200 | [diff] [blame] | 429 | |
elenairina | 1724cb2 | 2019-02-27 02:56:27 -0800 | [diff] [blame] | 430 | boolean hasOneFile = false; |
iirina | c9522f9 | 2019-05-28 02:18:58 -0700 | [diff] [blame] | 431 | if (metadataFile != null |
elenairina | 1724cb2 | 2019-02-27 02:56:27 -0800 | [diff] [blame] | 432 | && (metadataFile.endsWith("_merged_instr.jar") || metadataFile.endsWith("_deploy.jar"))) { |
iirina | c9522f9 | 2019-05-28 02:18:58 -0700 | [diff] [blame] | 433 | // bazel can set JACOCO_METADATA_JAR to either one file (a deploy jar |
elenairina | 1724cb2 | 2019-02-27 02:56:27 -0800 | [diff] [blame] | 434 | // or a merged jar) or to multiple jars. |
| 435 | hasOneFile = true; |
| 436 | } |
| 437 | final boolean hasOneFileFinal = hasOneFile; |
elenairina | 27ff758 | 2019-02-20 02:42:22 -0800 | [diff] [blame] | 438 | |
Yue Gan | af3c412 | 2016-12-05 14:36:02 +0000 | [diff] [blame] | 439 | final String coverageReportBase = System.getenv("JAVA_COVERAGE_FILE"); |
| 440 | |
| 441 | // Disable Jacoco's default output mechanism, which runs as a shutdown hook. We generate the |
| 442 | // report in our own shutdown hook below, and we want to avoid the data race (shutdown hooks are |
| 443 | // not guaranteed any particular order). Note that also by default, Jacoco appends coverage |
| 444 | // data, which can have surprising results if running tests locally or somehow encountering |
| 445 | // the previous .exec file. |
| 446 | System.setProperty("jacoco-agent.output", "none"); |
| 447 | |
| 448 | // We have no use for this sessionId property, but leaving it blank results in a DNS lookup |
| 449 | // at runtime. A minor annoyance: the documentation insists the property name is "sessionId", |
| 450 | // however on closer inspection of the source code, it turns out to be "sessionid"... |
| 451 | System.setProperty("jacoco-agent.sessionid", "default"); |
| 452 | |
| 453 | // A JVM shutdown hook has a fixed amount of time (OS-dependent) before it is terminated. |
| 454 | // For our purpose, it's more than enough to scan through the instrumented jar and match up |
| 455 | // the bytecode with the coverage data. It wouldn't be enough for scanning the entire classpath, |
| 456 | // or doing something else terribly inefficient. |
| 457 | Runtime.getRuntime() |
| 458 | .addShutdownHook( |
| 459 | new Thread() { |
| 460 | @Override |
| 461 | public void run() { |
| 462 | try { |
| 463 | // If the test spawns multiple JVMs, they will race to write to the same files. We |
| 464 | // need to generate unique paths for each execution. lcov_merger simply collects |
| 465 | // all the .dat files in the current directory anyway, so we don't need to worry |
| 466 | // about merging them. |
| 467 | String coverageReport = getUniquePath(coverageReportBase, ".dat"); |
| 468 | String coverageData = getUniquePath(coverageReportBase, ".exec"); |
| 469 | |
| 470 | // Get a handle on the Jacoco Agent and write out the coverage data. Other options |
| 471 | // included talking to the agent via TCP (useful when gathering coverage from |
| 472 | // multiple JVMs), or via JMX (the agent's MXBean is called |
| 473 | // 'org.jacoco:type=Runtime'). As we're running in the same JVM, these options |
| 474 | // seemed overkill, we can just refer to the Jacoco runtime as RT. |
| 475 | // See http://www.eclemma.org/jacoco/trunk/doc/agent.html for all the options |
| 476 | // available. |
| 477 | ByteArrayInputStream dataInputStream; |
| 478 | try { |
| 479 | IAgent agent = RT.getAgent(); |
| 480 | byte[] data = agent.getExecutionData(false); |
| 481 | try (FileOutputStream fs = new FileOutputStream(coverageData, true)) { |
| 482 | fs.write(data); |
| 483 | } |
| 484 | // We append to the output file, but run report generation only for the coverage |
| 485 | // data from this JVM. The output file may contain data from other |
| 486 | // subprocesses, etc. |
| 487 | dataInputStream = new ByteArrayInputStream(data); |
| 488 | } catch (IllegalStateException e) { |
| 489 | // In this case, we didn't execute a single instrumented file, so the agent |
| 490 | // isn't live. There's no coverage to report, but it's otherwise a successful |
| 491 | // invocation. |
| 492 | dataInputStream = new ByteArrayInputStream(new byte[0]); |
| 493 | } |
| 494 | |
elenairina | 27ff758 | 2019-02-20 02:42:22 -0800 | [diff] [blame] | 495 | if (metadataFileFinal != null || metadataFilesFinal != null) { |
| 496 | File[] metadataJars; |
| 497 | if (metadataFilesFinal != null) { |
| 498 | metadataJars = metadataFilesFinal; |
| 499 | } else { |
| 500 | metadataJars = |
elenairina | 1724cb2 | 2019-02-27 02:56:27 -0800 | [diff] [blame] | 501 | hasOneFileFinal |
elenairina | 27ff758 | 2019-02-20 02:42:22 -0800 | [diff] [blame] | 502 | ? new File[] {new File(metadataFileFinal)} |
| 503 | : getFilesFromFileList(new File(metadataFileFinal), javaRunfilesRoot) |
| 504 | .toArray(new File[0]); |
| 505 | } |
elenairina | 27ff758 | 2019-02-20 02:42:22 -0800 | [diff] [blame] | 506 | if (uninstrumentedClasses.isEmpty()) { |
iirina | c9522f9 | 2019-05-28 02:18:58 -0700 | [diff] [blame] | 507 | new JacocoCoverageRunner(dataInputStream, coverageReport, metadataJars) |
elenairina | 27ff758 | 2019-02-20 02:42:22 -0800 | [diff] [blame] | 508 | .create(); |
| 509 | } else { |
| 510 | new JacocoCoverageRunner( |
elenairina | 27ff758 | 2019-02-20 02:42:22 -0800 | [diff] [blame] | 511 | dataInputStream, |
| 512 | coverageReport, |
| 513 | uninstrumentedClasses, |
| 514 | pathsForCoverage, |
| 515 | metadataJars) |
| 516 | .create(); |
| 517 | } |
elenairina | 2a71860 | 2017-10-20 16:29:53 +0200 | [diff] [blame] | 518 | } |
Yue Gan | af3c412 | 2016-12-05 14:36:02 +0000 | [diff] [blame] | 519 | } catch (IOException e) { |
| 520 | e.printStackTrace(); |
| 521 | Runtime.getRuntime().halt(1); |
| 522 | } |
| 523 | } |
| 524 | }); |
| 525 | |
iirina | c9522f9 | 2019-05-28 02:18:58 -0700 | [diff] [blame] | 526 | // If running inside a deploy jar the classpath contains only that deploy jar. |
| 527 | // It can happen that multiple deploy jars are on the classpath. In that case we are running |
| 528 | // from a regular java binary where all the environment (e.g. JACOCO_MAIN_CLASS) is set |
| 529 | // accordingly. |
| 530 | boolean insideDeployJar = |
| 531 | (deployJars == 1) && (metadataFilesFinal == null || metadataFilesFinal.length == 1); |
| 532 | Class<?> mainClass = getMainClass(insideDeployJar); |
| 533 | Method main = mainClass.getMethod("main", String[].class); |
| 534 | main.setAccessible(true); |
Yue Gan | af3c412 | 2016-12-05 14:36:02 +0000 | [diff] [blame] | 535 | // Another option would be to run the tests in a separate JVM, let Jacoco dump out the coverage |
| 536 | // data, wait for the subprocess to finish and then generate the lcov report. The only benefit |
| 537 | // of doing this is not being constrained by the hard 5s limit of the shutdown hook. Setting up |
| 538 | // the subprocess to match all JVM flags, runtime classpath, bootclasspath, etc is doable. |
| 539 | // We'd share the same limitation if the system under test uses shutdown hooks internally, as |
| 540 | // there's no way to collect coverage data on that code. |
Yue Gan | af3c412 | 2016-12-05 14:36:02 +0000 | [diff] [blame] | 541 | main.invoke(null, new Object[] {args}); |
| 542 | } |
| 543 | } |