• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*******************************************************************************
2  * Copyright (c) 2009, 2021 Mountainminds GmbH & Co. KG and Contributors
3  * This program and the accompanying materials are made available under
4  * the terms of the Eclipse Public License 2.0 which is available at
5  * http://www.eclipse.org/legal/epl-2.0
6  *
7  * SPDX-License-Identifier: EPL-2.0
8  *
9  * Contributors:
10  *    Marc R. Hoffmann - initial API and implementation
11  *
12  *******************************************************************************/
13 package org.jacoco.core.runtime;
14 
15 import static java.lang.String.format;
16 
17 import java.io.File;
18 import java.util.Arrays;
19 import java.util.Collection;
20 import java.util.HashMap;
21 import java.util.Iterator;
22 import java.util.List;
23 import java.util.Map;
24 import java.util.Properties;
25 import java.util.regex.Pattern;
26 
27 /**
28  * Utility to create and parse options for the runtime agent. Options are
29  * represented as a string in the following format:
30  *
31  * <pre>
32  *   key1=value1,key2=value2,key3=value3
33  * </pre>
34  */
35 public final class AgentOptions {
36 
37 	/**
38 	 * Specifies the output file for execution data. Default is
39 	 * <code>jacoco.exec</code> in the working directory.
40 	 */
41 	public static final String DESTFILE = "destfile";
42 
43 	/**
44 	 * Default value for the "destfile" agent option.
45 	 */
46 	public static final String DEFAULT_DESTFILE = "jacoco.exec";
47 
48 	/**
49 	 * Specifies whether execution data should be appended to the output file.
50 	 * Default is <code>true</code>.
51 	 */
52 	public static final String APPEND = "append";
53 
54 	/**
55 	 * Wildcard expression for class names that should be included for code
56 	 * coverage. Default is <code>*</code> (all classes included).
57 	 *
58 	 * @see WildcardMatcher
59 	 */
60 	public static final String INCLUDES = "includes";
61 
62 	/**
63 	 * Wildcard expression for class names that should be excluded from code
64 	 * coverage. Default is the empty string (no exclusions).
65 	 *
66 	 * @see WildcardMatcher
67 	 */
68 	public static final String EXCLUDES = "excludes";
69 
70 	/**
71 	 * Wildcard expression for class loaders names for classes that should be
72 	 * excluded from code coverage. This means all classes loaded by a class
73 	 * loader which full qualified name matches this expression will be ignored
74 	 * for code coverage regardless of all other filtering settings. Default is
75 	 * <code>sun.reflect.DelegatingClassLoader</code>.
76 	 *
77 	 * @see WildcardMatcher
78 	 */
79 	public static final String EXCLCLASSLOADER = "exclclassloader";
80 
81 	/**
82 	 * Specifies whether also classes from the bootstrap classloader should be
83 	 * instrumented. Use this feature with caution, it needs heavy
84 	 * includes/excludes tuning. Default is <code>false</code>.
85 	 */
86 	public static final String INCLBOOTSTRAPCLASSES = "inclbootstrapclasses";
87 
88 	/**
89 	 * Specifies whether also classes without a source location should be
90 	 * instrumented. Normally such classes are generated at runtime e.g. by
91 	 * mocking frameworks and are therefore excluded by default. Default is
92 	 * <code>false</code>.
93 	 */
94 	public static final String INCLNOLOCATIONCLASSES = "inclnolocationclasses";
95 
96 	/**
97 	 * Specifies a session identifier that is written with the execution data.
98 	 * Without this parameter a random identifier is created by the agent.
99 	 */
100 	public static final String SESSIONID = "sessionid";
101 
102 	/**
103 	 * Specifies whether the agent will automatically dump coverage data on VM
104 	 * exit. Default is <code>true</code>.
105 	 */
106 	public static final String DUMPONEXIT = "dumponexit";
107 
108 	/**
109 	 * Specifies the output mode. Default is {@link OutputMode#file}.
110 	 *
111 	 * @see OutputMode#file
112 	 * @see OutputMode#tcpserver
113 	 * @see OutputMode#tcpclient
114 	 * @see OutputMode#none
115 	 */
116 	public static final String OUTPUT = "output";
117 
118 	private static final Pattern OPTION_SPLIT = Pattern
119 			.compile(",(?=[a-zA-Z0-9_\\-]+=)");
120 
121 	/**
122 	 * Possible values for {@link AgentOptions#OUTPUT}.
123 	 */
124 	public static enum OutputMode {
125 
126 		/**
127 		 * Value for the {@link AgentOptions#OUTPUT} parameter: At VM
128 		 * termination execution data is written to the file specified by
129 		 * {@link AgentOptions#DESTFILE}.
130 		 */
131 		file,
132 
133 		/**
134 		 * Value for the {@link AgentOptions#OUTPUT} parameter: The agent
135 		 * listens for incoming connections on a TCP port specified by
136 		 * {@link AgentOptions#ADDRESS} and {@link AgentOptions#PORT}.
137 		 */
138 		tcpserver,
139 
140 		/**
141 		 * Value for the {@link AgentOptions#OUTPUT} parameter: At startup the
142 		 * agent connects to a TCP port specified by the
143 		 * {@link AgentOptions#ADDRESS} and {@link AgentOptions#PORT} attribute.
144 		 */
145 		tcpclient,
146 
147 		/**
148 		 * Value for the {@link AgentOptions#OUTPUT} parameter: Do not produce
149 		 * any output.
150 		 */
151 		none
152 
153 	}
154 
155 	/**
156 	 * The IP address or DNS name the tcpserver binds to or the tcpclient
157 	 * connects to. Default is defined by {@link #DEFAULT_ADDRESS}.
158 	 */
159 	public static final String ADDRESS = "address";
160 
161 	/**
162 	 * Default value for the "address" agent option.
163 	 */
164 	public static final String DEFAULT_ADDRESS = null;
165 
166 	/**
167 	 * The port the tcpserver binds to or the tcpclient connects to. In
168 	 * tcpserver mode the port must be available, which means that if multiple
169 	 * JaCoCo agents should run on the same machine, different ports have to be
170 	 * specified. Default is defined by {@link #DEFAULT_PORT}.
171 	 */
172 	public static final String PORT = "port";
173 
174 	/**
175 	 * Default value for the "port" agent option.
176 	 */
177 	public static final int DEFAULT_PORT = 6300;
178 
179 	/**
180 	 * Specifies where the agent dumps all class files it encounters. The
181 	 * location is specified as a relative path to the working directory.
182 	 * Default is <code>null</code> (no dumps).
183 	 */
184 	public static final String CLASSDUMPDIR = "classdumpdir";
185 
186 	/**
187 	 * Specifies whether the agent should expose functionality via JMX under the
188 	 * name "org.jacoco:type=Runtime". Default is <code>false</code>.
189 	 */
190 	public static final String JMX = "jmx";
191 
192 	private static final Collection<String> VALID_OPTIONS = Arrays.asList(
193 			DESTFILE, APPEND, INCLUDES, EXCLUDES, EXCLCLASSLOADER,
194 			INCLBOOTSTRAPCLASSES, INCLNOLOCATIONCLASSES, SESSIONID, DUMPONEXIT,
195 			OUTPUT, ADDRESS, PORT, CLASSDUMPDIR, JMX);
196 
197 	private final Map<String, String> options;
198 
199 	/**
200 	 * New instance with all values set to default.
201 	 */
AgentOptions()202 	public AgentOptions() {
203 		this.options = new HashMap<String, String>();
204 	}
205 
206 	/**
207 	 * New instance parsed from the given option string.
208 	 *
209 	 * @param optionstr
210 	 *            string to parse or <code>null</code>
211 	 */
AgentOptions(final String optionstr)212 	public AgentOptions(final String optionstr) {
213 		this();
214 		if (optionstr != null && optionstr.length() > 0) {
215 			for (final String entry : OPTION_SPLIT.split(optionstr)) {
216 				final int pos = entry.indexOf('=');
217 				if (pos == -1) {
218 					throw new IllegalArgumentException(format(
219 							"Invalid agent option syntax \"%s\".", optionstr));
220 				}
221 				final String key = entry.substring(0, pos);
222 				if (!VALID_OPTIONS.contains(key)) {
223 					throw new IllegalArgumentException(
224 							format("Unknown agent option \"%s\".", key));
225 				}
226 
227 				final String value = entry.substring(pos + 1);
228 				setOption(key, value);
229 			}
230 
231 			validateAll();
232 		}
233 	}
234 
235 	/**
236 	 * New instance read from the given {@link Properties} object.
237 	 *
238 	 * @param properties
239 	 *            {@link Properties} object to read configuration options from
240 	 */
AgentOptions(final Properties properties)241 	public AgentOptions(final Properties properties) {
242 		this();
243 		for (final String key : VALID_OPTIONS) {
244 			final String value = properties.getProperty(key);
245 			if (value != null) {
246 				setOption(key, value);
247 			}
248 		}
249 	}
250 
validateAll()251 	private void validateAll() {
252 		validatePort(getPort());
253 		getOutput();
254 	}
255 
validatePort(final int port)256 	private void validatePort(final int port) {
257 		if (port < 0) {
258 			throw new IllegalArgumentException("port must be positive");
259 		}
260 	}
261 
262 	/**
263 	 * Returns the output file location.
264 	 *
265 	 * @return output file location
266 	 */
getDestfile()267 	public String getDestfile() {
268 		return getOption(DESTFILE, DEFAULT_DESTFILE);
269 	}
270 
271 	/**
272 	 * Sets the output file location.
273 	 *
274 	 * @param destfile
275 	 *            output file location
276 	 */
setDestfile(final String destfile)277 	public void setDestfile(final String destfile) {
278 		setOption(DESTFILE, destfile);
279 	}
280 
281 	/**
282 	 * Returns whether the output should be appended to an existing file.
283 	 *
284 	 * @return <code>true</code>, when the output should be appended
285 	 */
getAppend()286 	public boolean getAppend() {
287 		return getOption(APPEND, true);
288 	}
289 
290 	/**
291 	 * Sets whether the output should be appended to an existing file.
292 	 *
293 	 * @param append
294 	 *            <code>true</code>, when the output should be appended
295 	 */
setAppend(final boolean append)296 	public void setAppend(final boolean append) {
297 		setOption(APPEND, append);
298 	}
299 
300 	/**
301 	 * Returns the wildcard expression for classes to include.
302 	 *
303 	 * @return wildcard expression for classes to include
304 	 * @see WildcardMatcher
305 	 */
getIncludes()306 	public String getIncludes() {
307 		return getOption(INCLUDES, "*");
308 	}
309 
310 	/**
311 	 * Sets the wildcard expression for classes to include.
312 	 *
313 	 * @param includes
314 	 *            wildcard expression for classes to include
315 	 * @see WildcardMatcher
316 	 */
setIncludes(final String includes)317 	public void setIncludes(final String includes) {
318 		setOption(INCLUDES, includes);
319 	}
320 
321 	/**
322 	 * Returns the wildcard expression for classes to exclude.
323 	 *
324 	 * @return wildcard expression for classes to exclude
325 	 * @see WildcardMatcher
326 	 */
getExcludes()327 	public String getExcludes() {
328 		return getOption(EXCLUDES, "");
329 	}
330 
331 	/**
332 	 * Sets the wildcard expression for classes to exclude.
333 	 *
334 	 * @param excludes
335 	 *            wildcard expression for classes to exclude
336 	 * @see WildcardMatcher
337 	 */
setExcludes(final String excludes)338 	public void setExcludes(final String excludes) {
339 		setOption(EXCLUDES, excludes);
340 	}
341 
342 	/**
343 	 * Returns the wildcard expression for excluded class loaders.
344 	 *
345 	 * @return expression for excluded class loaders
346 	 * @see WildcardMatcher
347 	 */
getExclClassloader()348 	public String getExclClassloader() {
349 		return getOption(EXCLCLASSLOADER, "sun.reflect.DelegatingClassLoader");
350 	}
351 
352 	/**
353 	 * Sets the wildcard expression for excluded class loaders.
354 	 *
355 	 * @param expression
356 	 *            expression for excluded class loaders
357 	 * @see WildcardMatcher
358 	 */
setExclClassloader(final String expression)359 	public void setExclClassloader(final String expression) {
360 		setOption(EXCLCLASSLOADER, expression);
361 	}
362 
363 	/**
364 	 * Returns whether classes from the bootstrap classloader should be
365 	 * instrumented.
366 	 *
367 	 * @return <code>true</code> if classes from the bootstrap classloader
368 	 *         should be instrumented
369 	 */
getInclBootstrapClasses()370 	public boolean getInclBootstrapClasses() {
371 		return getOption(INCLBOOTSTRAPCLASSES, false);
372 	}
373 
374 	/**
375 	 * Sets whether classes from the bootstrap classloader should be
376 	 * instrumented.
377 	 *
378 	 * @param include
379 	 *            <code>true</code> if bootstrap classes should be instrumented
380 	 */
setInclBootstrapClasses(final boolean include)381 	public void setInclBootstrapClasses(final boolean include) {
382 		setOption(INCLBOOTSTRAPCLASSES, include);
383 	}
384 
385 	/**
386 	 * Returns whether classes without source location should be instrumented.
387 	 *
388 	 * @return <code>true</code> if classes without source location should be
389 	 *         instrumented
390 	 */
getInclNoLocationClasses()391 	public boolean getInclNoLocationClasses() {
392 		return getOption(INCLNOLOCATIONCLASSES, false);
393 	}
394 
395 	/**
396 	 * Sets whether classes without source location should be instrumented.
397 	 *
398 	 * @param include
399 	 *            <code>true</code> if classes without source location should be
400 	 *            instrumented
401 	 */
setInclNoLocationClasses(final boolean include)402 	public void setInclNoLocationClasses(final boolean include) {
403 		setOption(INCLNOLOCATIONCLASSES, include);
404 	}
405 
406 	/**
407 	 * Returns the session identifier.
408 	 *
409 	 * @return session identifier
410 	 */
getSessionId()411 	public String getSessionId() {
412 		return getOption(SESSIONID, null);
413 	}
414 
415 	/**
416 	 * Sets the session identifier.
417 	 *
418 	 * @param id
419 	 *            session identifier
420 	 */
setSessionId(final String id)421 	public void setSessionId(final String id) {
422 		setOption(SESSIONID, id);
423 	}
424 
425 	/**
426 	 * Returns whether coverage data should be dumped on exit.
427 	 *
428 	 * @return <code>true</code> if coverage data will be written on VM exit
429 	 */
getDumpOnExit()430 	public boolean getDumpOnExit() {
431 		return getOption(DUMPONEXIT, true);
432 	}
433 
434 	/**
435 	 * Sets whether coverage data should be dumped on exit.
436 	 *
437 	 * @param dumpOnExit
438 	 *            <code>true</code> if coverage data should be written on VM
439 	 *            exit
440 	 */
setDumpOnExit(final boolean dumpOnExit)441 	public void setDumpOnExit(final boolean dumpOnExit) {
442 		setOption(DUMPONEXIT, dumpOnExit);
443 	}
444 
445 	/**
446 	 * Returns the port on which to listen to when the output is
447 	 * <code>tcpserver</code> or the port to connect to when output is
448 	 * <code>tcpclient</code>.
449 	 *
450 	 * @return port to listen on or connect to
451 	 */
getPort()452 	public int getPort() {
453 		return getOption(PORT, DEFAULT_PORT);
454 	}
455 
456 	/**
457 	 * Sets the port on which to listen to when output is <code>tcpserver</code>
458 	 * or the port to connect to when output is <code>tcpclient</code>
459 	 *
460 	 * @param port
461 	 *            port to listen on or connect to
462 	 */
setPort(final int port)463 	public void setPort(final int port) {
464 		validatePort(port);
465 		setOption(PORT, port);
466 	}
467 
468 	/**
469 	 * Gets the hostname or IP address to listen to when output is
470 	 * <code>tcpserver</code> or connect to when output is
471 	 * <code>tcpclient</code>
472 	 *
473 	 * @return Hostname or IP address
474 	 */
getAddress()475 	public String getAddress() {
476 		return getOption(ADDRESS, DEFAULT_ADDRESS);
477 	}
478 
479 	/**
480 	 * Sets the hostname or IP address to listen to when output is
481 	 * <code>tcpserver</code> or connect to when output is
482 	 * <code>tcpclient</code>
483 	 *
484 	 * @param address
485 	 *            Hostname or IP address
486 	 */
setAddress(final String address)487 	public void setAddress(final String address) {
488 		setOption(ADDRESS, address);
489 	}
490 
491 	/**
492 	 * Returns the output mode
493 	 *
494 	 * @return current output mode
495 	 */
getOutput()496 	public OutputMode getOutput() {
497 		final String value = options.get(OUTPUT);
498 // BEGIN android-change
499 //		return value == null ? OutputMode.file : OutputMode.valueOf(value);
500 		return value == null ? OutputMode.none : OutputMode.valueOf(value);
501 // END android-change
502 	}
503 
504 	/**
505 	 * Sets the output mode
506 	 *
507 	 * @param output
508 	 *            Output mode
509 	 */
setOutput(final String output)510 	public void setOutput(final String output) {
511 		setOutput(OutputMode.valueOf(output));
512 	}
513 
514 	/**
515 	 * Sets the output mode
516 	 *
517 	 * @param output
518 	 *            Output mode
519 	 */
setOutput(final OutputMode output)520 	public void setOutput(final OutputMode output) {
521 		setOption(OUTPUT, output.name());
522 	}
523 
524 	/**
525 	 * Returns the location of the directory where class files should be dumped
526 	 * to.
527 	 *
528 	 * @return dump location or <code>null</code> (no dumps)
529 	 */
getClassDumpDir()530 	public String getClassDumpDir() {
531 		return getOption(CLASSDUMPDIR, null);
532 	}
533 
534 	/**
535 	 * Sets the directory where class files should be dumped to.
536 	 *
537 	 * @param location
538 	 *            dump location or <code>null</code> (no dumps)
539 	 */
setClassDumpDir(final String location)540 	public void setClassDumpDir(final String location) {
541 		setOption(CLASSDUMPDIR, location);
542 	}
543 
544 	/**
545 	 * Returns whether the agent exposes functionality via JMX.
546 	 *
547 	 * @return <code>true</code>, when JMX is enabled
548 	 */
getJmx()549 	public boolean getJmx() {
550 		return getOption(JMX, false);
551 	}
552 
553 	/**
554 	 * Sets whether the agent should expose functionality via JMX.
555 	 *
556 	 * @param jmx
557 	 *            <code>true</code> if JMX should be enabled
558 	 */
setJmx(final boolean jmx)559 	public void setJmx(final boolean jmx) {
560 		setOption(JMX, jmx);
561 	}
562 
setOption(final String key, final int value)563 	private void setOption(final String key, final int value) {
564 		setOption(key, Integer.toString(value));
565 	}
566 
setOption(final String key, final boolean value)567 	private void setOption(final String key, final boolean value) {
568 		setOption(key, Boolean.toString(value));
569 	}
570 
setOption(final String key, final String value)571 	private void setOption(final String key, final String value) {
572 		options.put(key, value);
573 	}
574 
getOption(final String key, final String defaultValue)575 	private String getOption(final String key, final String defaultValue) {
576 		final String value = options.get(key);
577 		return value == null ? defaultValue : value;
578 	}
579 
getOption(final String key, final boolean defaultValue)580 	private boolean getOption(final String key, final boolean defaultValue) {
581 		final String value = options.get(key);
582 		return value == null ? defaultValue : Boolean.parseBoolean(value);
583 	}
584 
getOption(final String key, final int defaultValue)585 	private int getOption(final String key, final int defaultValue) {
586 		final String value = options.get(key);
587 		return value == null ? defaultValue : Integer.parseInt(value);
588 	}
589 
590 	/**
591 	 * Generate required JVM argument based on current configuration and
592 	 * supplied agent jar location.
593 	 *
594 	 * @param agentJarFile
595 	 *            location of the JaCoCo Agent Jar
596 	 * @return Argument to pass to create new VM with coverage enabled
597 	 */
getVMArgument(final File agentJarFile)598 	public String getVMArgument(final File agentJarFile) {
599 		return format("-javaagent:%s=%s", agentJarFile, this);
600 	}
601 
602 	/**
603 	 * Generate required quoted JVM argument based on current configuration and
604 	 * supplied agent jar location.
605 	 *
606 	 * @param agentJarFile
607 	 *            location of the JaCoCo Agent Jar
608 	 * @return Quoted argument to pass to create new VM with coverage enabled
609 	 */
getQuotedVMArgument(final File agentJarFile)610 	public String getQuotedVMArgument(final File agentJarFile) {
611 		return CommandLineSupport.quote(getVMArgument(agentJarFile));
612 	}
613 
614 	/**
615 	 * Generate required quotes JVM argument based on current configuration and
616 	 * prepends it to the given argument command line. If a agent with the same
617 	 * JAR file is already specified this parameter is removed from the existing
618 	 * command line.
619 	 *
620 	 * @param arguments
621 	 *            existing command line arguments or <code>null</code>
622 	 * @param agentJarFile
623 	 *            location of the JaCoCo Agent Jar
624 	 * @return VM command line arguments prepended with configured JaCoCo agent
625 	 */
prependVMArguments(final String arguments, final File agentJarFile)626 	public String prependVMArguments(final String arguments,
627 			final File agentJarFile) {
628 		final List<String> args = CommandLineSupport.split(arguments);
629 		final String plainAgent = format("-javaagent:%s", agentJarFile);
630 		for (final Iterator<String> i = args.iterator(); i.hasNext();) {
631 			if (i.next().startsWith(plainAgent)) {
632 				i.remove();
633 			}
634 		}
635 		args.add(0, getVMArgument(agentJarFile));
636 		return CommandLineSupport.quote(args);
637 	}
638 
639 	/**
640 	 * Creates a string representation that can be passed to the agent via the
641 	 * command line. Might be the empty string, if no options are set.
642 	 */
643 	@Override
toString()644 	public String toString() {
645 		final StringBuilder sb = new StringBuilder();
646 		for (final String key : VALID_OPTIONS) {
647 			final String value = options.get(key);
648 			if (value != null) {
649 				if (sb.length() > 0) {
650 					sb.append(',');
651 				}
652 				sb.append(key).append('=').append(value);
653 			}
654 		}
655 		return sb.toString();
656 	}
657 
658 }
659