1 /******************************************************************************* 2 * Copyright (c) 2009, 2019 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 */ AgentOptions()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 */ AgentOptions(final String optionstr)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 */ AgentOptions(final Properties properties)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 validateAll()250 private void validateAll() { 251 validatePort(getPort()); 252 getOutput(); 253 } 254 validatePort(final int port)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 */ getDestfile()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 */ setDestfile(final String destfile)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 */ getAppend()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 */ setAppend(final boolean append)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 */ getIncludes()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 */ setIncludes(final String includes)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 */ getExcludes()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 */ setExcludes(final String excludes)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 */ getExclClassloader()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 */ setExclClassloader(final String expression)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 */ getInclBootstrapClasses()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 */ setInclBootstrapClasses(final boolean include)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 */ getInclNoLocationClasses()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 */ setInclNoLocationClasses(final boolean include)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 */ getSessionId()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 */ setSessionId(final String id)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 */ getDumpOnExit()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 */ setDumpOnExit(final boolean dumpOnExit)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 */ getPort()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 */ setPort(final int port)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 */ getAddress()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 */ setAddress(final String address)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 */ getOutput()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 */ setOutput(final String output)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 */ setOutput(final OutputMode output)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 */ getClassDumpDir()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 */ setClassDumpDir(final String location)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 */ getJmx()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 */ setJmx(final boolean jmx)558 public void setJmx(final boolean jmx) { 559 setOption(JMX, jmx); 560 } 561 setOption(final String key, final int value)562 private void setOption(final String key, final int value) { 563 setOption(key, Integer.toString(value)); 564 } 565 setOption(final String key, final boolean value)566 private void setOption(final String key, final boolean value) { 567 setOption(key, Boolean.toString(value)); 568 } 569 setOption(final String key, final String value)570 private void setOption(final String key, final String value) { 571 options.put(key, value); 572 } 573 getOption(final String key, final String defaultValue)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 getOption(final String key, final boolean defaultValue)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 getOption(final String key, final int defaultValue)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 */ getVMArgument(final File agentJarFile)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 */ getQuotedVMArgument(final File agentJarFile)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 */ prependVMArguments(final String arguments, final File agentJarFile)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 toString()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