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