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