blob: e1e51531fcb8037f3aa9e10cb46d8c01705c4b6d [file] [log] [blame]
Yue Ganaf3c4122016-12-05 14:36:02 +00001// 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
15package com.google.testing.coverage;
16
elenairina23491492017-07-14 16:01:49 +020017import static java.nio.charset.StandardCharsets.UTF_8;
18
19import com.google.common.annotations.VisibleForTesting;
elenairina23491492017-07-14 16:01:49 +020020import com.google.common.collect.ImmutableList;
elenairina3c949cc2019-01-11 04:17:26 -080021import com.google.common.collect.ImmutableList.Builder;
elenairina23491492017-07-14 16:01:49 +020022import com.google.common.collect.ImmutableSet;
elenairina27ff7582019-02-20 02:42:22 -080023import com.google.common.io.ByteStreams;
elenairina05418b32017-08-14 11:16:44 +020024import com.google.common.io.Files;
elenairina23491492017-07-14 16:01:49 +020025import java.io.BufferedReader;
Yue Ganaf3c4122016-12-05 14:36:02 +000026import java.io.ByteArrayInputStream;
27import java.io.File;
Yue Ganaf3c4122016-12-05 14:36:02 +000028import java.io.FileOutputStream;
29import java.io.IOException;
30import java.io.InputStream;
elenairina23491492017-07-14 16:01:49 +020031import java.io.InputStreamReader;
Yue Ganaf3c4122016-12-05 14:36:02 +000032import java.io.Reader;
iirinac9522f92019-05-28 02:18:58 -070033import java.lang.reflect.Field;
Yue Ganaf3c4122016-12-05 14:36:02 +000034import java.lang.reflect.Method;
35import java.net.URL;
elenairina27ff7582019-02-20 02:42:22 -080036import java.net.URLClassLoader;
Patrick Niklaus77b56582019-05-10 01:38:59 -070037import java.net.URLDecoder;
iirinac9522f92019-05-28 02:18:58 -070038import java.util.ArrayList;
Yue Ganaf3c4122016-12-05 14:36:02 +000039import java.util.Enumeration;
elenairina27ff7582019-02-20 02:42:22 -080040import java.util.HashMap;
elenairina97966cc2017-07-19 15:40:11 +020041import java.util.HashSet;
Yue Ganaf3c4122016-12-05 14:36:02 +000042import java.util.List;
43import java.util.Map;
elenairina23491492017-07-14 16:01:49 +020044import java.util.Set;
Yue Ganaf3c4122016-12-05 14:36:02 +000045import java.util.TreeMap;
46import java.util.jar.Attributes;
elenairina23491492017-07-14 16:01:49 +020047import java.util.jar.JarEntry;
48import java.util.jar.JarFile;
Yue Ganaf3c4122016-12-05 14:36:02 +000049import java.util.jar.Manifest;
50import org.jacoco.agent.rt.IAgent;
51import org.jacoco.agent.rt.RT;
52import org.jacoco.core.analysis.Analyzer;
53import org.jacoco.core.analysis.CoverageBuilder;
54import org.jacoco.core.analysis.IBundleCoverage;
55import org.jacoco.core.tools.ExecFileLoader;
56import org.jacoco.report.IReportVisitor;
57import org.jacoco.report.ISourceFileLocator;
iirinac9522f92019-05-28 02:18:58 -070058import sun.misc.Unsafe;
Yue Ganaf3c4122016-12-05 14:36:02 +000059
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:
elenairina23491492017-07-14 16:01:49 +020070 * <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 Ganaf3c4122016-12-05 14:36:02 +000074 */
75public class JacocoCoverageRunner {
76
elenairina23491492017-07-14 16:01:49 +020077 private final ImmutableList<File> classesJars;
Yue Ganaf3c4122016-12-05 14:36:02 +000078 private final InputStream executionData;
79 private final File reportFile;
80 private ExecFileLoader execFileLoader;
elenairina27ff7582019-02-20 02:42:22 -080081 private HashMap<String, byte[]> uninstrumentedClasses;
82 private ImmutableSet<String> pathsForCoverage = ImmutableSet.of();
Yue Ganaf3c4122016-12-05 14:36:02 +000083
elenairina27ff7582019-02-20 02:42:22 -080084 /**
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(
iirinac9522f92019-05-28 02:18:58 -070089 InputStream jacocoExec, String reportPath, File wrapperFile, String javaRunfilesRoot)
elenairina27ff7582019-02-20 02:42:22 -080090 throws IOException {
Yue Ganaf3c4122016-12-05 14:36:02 +000091 executionData = jacocoExec;
92 reportFile = new File(reportPath);
elenairina27ff7582019-02-20 02:42:22 -080093 this.classesJars = getFilesFromFileList(wrapperFile, javaRunfilesRoot);
94 }
95
iirinac9522f92019-05-28 02:18:58 -070096 public JacocoCoverageRunner(InputStream jacocoExec, String reportPath, File... metadataJars) {
elenairina27ff7582019-02-20 02:42:22 -080097 executionData = jacocoExec;
98 reportFile = new File(reportPath);
elenairina27ff7582019-02-20 02:42:22 -080099 this.classesJars = ImmutableList.copyOf(metadataJars);
100 }
101
102 public JacocoCoverageRunner(
elenairina27ff7582019-02-20 02:42:22 -0800103 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);
elenairina27ff7582019-02-20 02:42:22 -0800110 this.classesJars = ImmutableList.copyOf(metadataJars);
111 this.uninstrumentedClasses = uninstrumentedClasses;
112 this.pathsForCoverage = pathsForCoverage;
Yue Ganaf3c4122016-12-05 14:36:02 +0000113 }
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
elenairina23491492017-07-14 16:01:49 +0200130 @VisibleForTesting
131 void createReport(
Yue Ganaf3c4122016-12-05 14:36:02 +0000132 final IBundleCoverage bundleCoverage, final Map<String, BranchCoverageDetail> branchDetails)
133 throws IOException {
elenairina23491492017-07-14 16:01:49 +0200134 JacocoLCOVFormatter formatter = new JacocoLCOVFormatter(createPathsSet());
Yue Ganaf3c4122016-12-05 14:36:02 +0000135 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
elenairina23491492017-07-14 16:01:49 +0200168 @VisibleForTesting
169 IBundleCoverage analyzeStructure() throws IOException {
Yue Ganaf3c4122016-12-05 14:36:02 +0000170 final CoverageBuilder coverageBuilder = new CoverageBuilder();
171 final Analyzer analyzer = new Analyzer(execFileLoader.getExecutionDataStore(), coverageBuilder);
elenairina97966cc2017-07-19 15:40:11 +0200172 Set<String> alreadyInstrumentedClasses = new HashSet<>();
elenairina27ff7582019-02-20 02:42:22 -0800173 if (uninstrumentedClasses == null) {
174 for (File classesJar : classesJars) {
iirinac9522f92019-05-28 02:18:58 -0700175 analyzeUninstrumentedClassesFromJar(analyzer, classesJar, alreadyInstrumentedClasses);
elenairina27ff7582019-02-20 02:42:22 -0800176 }
177 } else {
178 for (Map.Entry<String, byte[]> entry : uninstrumentedClasses.entrySet()) {
179 analyzer.analyzeClass(entry.getValue(), entry.getKey());
elenairina23491492017-07-14 16:01:49 +0200180 }
Yue Ganaf3c4122016-12-05 14:36:02 +0000181 }
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<>();
elenairina97966cc2017-07-19 15:40:11 +0200193 Set<String> alreadyInstrumentedClasses = new HashSet<>();
elenairina27ff7582019-02-20 02:42:22 -0800194 if (uninstrumentedClasses == null) {
195 for (File classesJar : classesJars) {
iirinac9522f92019-05-28 02:18:58 -0700196 analyzeUninstrumentedClassesFromJar(analyzer, classesJar, alreadyInstrumentedClasses);
elenairina27ff7582019-02-20 02:42:22 -0800197 result.putAll(analyzer.getBranchDetails());
198 }
199 } else {
200 for (Map.Entry<String, byte[]> entry : uninstrumentedClasses.entrySet()) {
201 analyzer.analyzeClass(entry.getValue(), entry.getKey());
elenairina23491492017-07-14 16:01:49 +0200202 }
Yue Ganaf3c4122016-12-05 14:36:02 +0000203 result.putAll(analyzer.getBranchDetails());
204 }
205 return result;
206 }
207
elenairina23491492017-07-14 16:01:49 +0200208 /**
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 */
elenairina97966cc2017-07-19 15:40:11 +0200213 private void analyzeUninstrumentedClassesFromJar(
214 Analyzer analyzer, File jar, Set<String> alreadyInstrumentedClasses) throws IOException {
elenairina23491492017-07-14 16:01:49 +0200215 JarFile jarFile = new JarFile(jar);
elenairina27ff7582019-02-20 02:42:22 -0800216 Enumeration<JarEntry> jarFileEntries = jarFile.entries();
217 while (jarFileEntries.hasMoreElements()) {
218 JarEntry jarEntry = jarFileEntries.nextElement();
elenairina23491492017-07-14 16:01:49 +0200219 String jarEntryName = jarEntry.getName();
elenairina97966cc2017-07-19 15:40:11 +0200220 if (jarEntryName.endsWith(".class.uninstrumented")
221 && !alreadyInstrumentedClasses.contains(jarEntryName)) {
elenairina23491492017-07-14 16:01:49 +0200222 analyzer.analyzeAll(jarFile.getInputStream(jarEntry), jarEntryName);
elenairina97966cc2017-07-19 15:40:11 +0200223 alreadyInstrumentedClasses.add(jarEntryName);
elenairina23491492017-07-14 16:01:49 +0200224 }
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 {
elenairina27ff7582019-02-20 02:42:22 -0800239 if (!pathsForCoverage.isEmpty()) {
240 return pathsForCoverage;
241 }
elenairina23491492017-07-14 16:01:49 +0200242 ImmutableSet.Builder<String> execPathsSetBuilder = ImmutableSet.builder();
243 for (File classJar : classesJars) {
244 addEntriesToExecPathsSet(classJar, execPathsSetBuilder);
245 }
elenairina27ff7582019-02-20 02:42:22 -0800246 ImmutableSet<String> result = execPathsSetBuilder.build();
247 return result;
elenairina23491492017-07-14 16:01:49 +0200248 }
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);
elenairina27ff7582019-02-20 02:42:22 -0800260 Enumeration<JarEntry> jarFileEntries = jarFile.entries();
261 while (jarFileEntries.hasMoreElements()) {
262 JarEntry jarEntry = jarFileEntries.nextElement();
elenairina23491492017-07-14 16:01:49 +0200263 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
iirinac9522f92019-05-28 02:18:58 -0700275 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 Ganaf3c4122016-12-05 14:36:02 +0000299 }
300 }
Yue Ganaf3c4122016-12-05 14:36:02 +0000301 }
iirinac9522f92019-05-28 02:18:58 -0700302 // 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 Ganaf3c4122016-12-05 14:36:02 +0000312 }
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 {
iirinac9522f92019-05-28 02:18:58 -0700322 // bazel sets the path template to a file with the .dat extension. lcov_merger matches all
Yue Ganaf3c4122016-12-05 14:36:02 +0000323 // 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
elenairina27ff7582019-02-20 02:42:22 -0800334 /**
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
iirinac9522f92019-05-28 02:18:58 -0700348 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 Ganaf3c4122016-12-05 14:36:02 +0000378 public static void main(String[] args) throws Exception {
elenairina27ff7582019-02-20 02:42:22 -0800379 String metadataFile = System.getenv("JACOCO_METADATA_JAR");
380
elenairina27ff7582019-02-20 02:42:22 -0800381 File[] metadataFiles = null;
iirinac9522f92019-05-28 02:18:58 -0700382 int deployJars = 0;
elenairina27ff7582019-02-20 02:42:22 -0800383 final HashMap<String, byte[]> uninstrumentedClasses = new HashMap<>();
384 ImmutableSet.Builder<String> pathsForCoverageBuilder = new ImmutableSet.Builder<>();
iirinac9522f92019-05-28 02:18:58 -0700385 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);
elenairina27ff7582019-02-20 02:42:22 -0800418 }
419 }
420 }
421 }
422 }
423 }
iirinac9522f92019-05-28 02:18:58 -0700424
elenairina27ff7582019-02-20 02:42:22 -0800425 final ImmutableSet<String> pathsForCoverage = pathsForCoverageBuilder.build();
426 final String metadataFileFinal = metadataFile;
427 final File[] metadataFilesFinal = metadataFiles;
elenairina05418b32017-08-14 11:16:44 +0200428 final String javaRunfilesRoot = System.getenv("JACOCO_JAVA_RUNFILES_ROOT");
elenairina23491492017-07-14 16:01:49 +0200429
elenairina1724cb22019-02-27 02:56:27 -0800430 boolean hasOneFile = false;
iirinac9522f92019-05-28 02:18:58 -0700431 if (metadataFile != null
elenairina1724cb22019-02-27 02:56:27 -0800432 && (metadataFile.endsWith("_merged_instr.jar") || metadataFile.endsWith("_deploy.jar"))) {
iirinac9522f92019-05-28 02:18:58 -0700433 // bazel can set JACOCO_METADATA_JAR to either one file (a deploy jar
elenairina1724cb22019-02-27 02:56:27 -0800434 // or a merged jar) or to multiple jars.
435 hasOneFile = true;
436 }
437 final boolean hasOneFileFinal = hasOneFile;
elenairina27ff7582019-02-20 02:42:22 -0800438
Yue Ganaf3c4122016-12-05 14:36:02 +0000439 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
elenairina27ff7582019-02-20 02:42:22 -0800495 if (metadataFileFinal != null || metadataFilesFinal != null) {
496 File[] metadataJars;
497 if (metadataFilesFinal != null) {
498 metadataJars = metadataFilesFinal;
499 } else {
500 metadataJars =
elenairina1724cb22019-02-27 02:56:27 -0800501 hasOneFileFinal
elenairina27ff7582019-02-20 02:42:22 -0800502 ? new File[] {new File(metadataFileFinal)}
503 : getFilesFromFileList(new File(metadataFileFinal), javaRunfilesRoot)
504 .toArray(new File[0]);
505 }
elenairina27ff7582019-02-20 02:42:22 -0800506 if (uninstrumentedClasses.isEmpty()) {
iirinac9522f92019-05-28 02:18:58 -0700507 new JacocoCoverageRunner(dataInputStream, coverageReport, metadataJars)
elenairina27ff7582019-02-20 02:42:22 -0800508 .create();
509 } else {
510 new JacocoCoverageRunner(
elenairina27ff7582019-02-20 02:42:22 -0800511 dataInputStream,
512 coverageReport,
513 uninstrumentedClasses,
514 pathsForCoverage,
515 metadataJars)
516 .create();
517 }
elenairina2a718602017-10-20 16:29:53 +0200518 }
Yue Ganaf3c4122016-12-05 14:36:02 +0000519 } catch (IOException e) {
520 e.printStackTrace();
521 Runtime.getRuntime().halt(1);
522 }
523 }
524 });
525
iirinac9522f92019-05-28 02:18:58 -0700526 // 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 Ganaf3c4122016-12-05 14:36:02 +0000535 // 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 Ganaf3c4122016-12-05 14:36:02 +0000541 main.invoke(null, new Object[] {args});
542 }
543}