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