aboutsummaryrefslogtreecommitdiff
path: root/org.jacoco.core/src/org/jacoco/core/runtime/AgentOptions.java
blob: 60f03c0a3e019d1552ffd59b06a24dd39fa7a5e7 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
/*******************************************************************************
 * Copyright (c) 2009, 2019 Mountainminds GmbH & Co. KG and Contributors
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *    Marc R. Hoffmann - initial API and implementation
 *    
 *******************************************************************************/
package org.jacoco.core.runtime;

import static java.lang.String.format;

import java.io.File;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.regex.Pattern;

/**
 * Utility to create and parse options for the runtime agent. Options are
 * represented as a string in the following format:
 * 
 * <pre>
 *   key1=value1,key2=value2,key3=value3
 * </pre>
 */
public final class AgentOptions {

	/**
	 * Specifies the output file for execution data. Default is
	 * <code>jacoco.exec</code> in the working directory.
	 */
	public static final String DESTFILE = "destfile";

	/**
	 * Default value for the "destfile" agent option.
	 */
	public static final String DEFAULT_DESTFILE = "jacoco.exec";

	/**
	 * Specifies whether execution data should be appended to the output file.
	 * Default is <code>true</code>.
	 */
	public static final String APPEND = "append";

	/**
	 * Wildcard expression for class names that should be included for code
	 * coverage. Default is <code>*</code> (all classes included).
	 * 
	 * @see WildcardMatcher
	 */
	public static final String INCLUDES = "includes";

	/**
	 * Wildcard expression for class names that should be excluded from code
	 * coverage. Default is the empty string (no exclusions).
	 * 
	 * @see WildcardMatcher
	 */
	public static final String EXCLUDES = "excludes";

	/**
	 * Wildcard expression for class loaders names for classes that should be
	 * excluded from code coverage. This means all classes loaded by a class
	 * loader which full qualified name matches this expression will be ignored
	 * for code coverage regardless of all other filtering settings. Default is
	 * <code>sun.reflect.DelegatingClassLoader</code>.
	 * 
	 * @see WildcardMatcher
	 */
	public static final String EXCLCLASSLOADER = "exclclassloader";

	/**
	 * Specifies whether also classes from the bootstrap classloader should be
	 * instrumented. Use this feature with caution, it needs heavy
	 * includes/excludes tuning. Default is <code>false</code>.
	 */
	public static final String INCLBOOTSTRAPCLASSES = "inclbootstrapclasses";

	/**
	 * Specifies whether also classes without a source location should be
	 * instrumented. Normally such classes are generated at runtime e.g. by
	 * mocking frameworks and are therefore excluded by default. Default is
	 * <code>false</code>.
	 */
	public static final String INCLNOLOCATIONCLASSES = "inclnolocationclasses";

	/**
	 * Specifies a session identifier that is written with the execution data.
	 * Without this parameter a random identifier is created by the agent.
	 */
	public static final String SESSIONID = "sessionid";

	/**
	 * Specifies whether the agent will automatically dump coverage data on VM
	 * exit. Default is <code>true</code>.
	 */
	public static final String DUMPONEXIT = "dumponexit";

	/**
	 * Specifies the output mode. Default is {@link OutputMode#file}.
	 * 
	 * @see OutputMode#file
	 * @see OutputMode#tcpserver
	 * @see OutputMode#tcpclient
	 * @see OutputMode#none
	 */
	public static final String OUTPUT = "output";

	private static final Pattern OPTION_SPLIT = Pattern
			.compile(",(?=[a-zA-Z0-9_\\-]+=)");

	/**
	 * Possible values for {@link AgentOptions#OUTPUT}.
	 */
	public static enum OutputMode {

		/**
		 * Value for the {@link AgentOptions#OUTPUT} parameter: At VM
		 * termination execution data is written to the file specified by
		 * {@link AgentOptions#DESTFILE}.
		 */
		file,

		/**
		 * Value for the {@link AgentOptions#OUTPUT} parameter: The agent
		 * listens for incoming connections on a TCP port specified by
		 * {@link AgentOptions#ADDRESS} and {@link AgentOptions#PORT}.
		 */
		tcpserver,

		/**
		 * Value for the {@link AgentOptions#OUTPUT} parameter: At startup the
		 * agent connects to a TCP port specified by the
		 * {@link AgentOptions#ADDRESS} and {@link AgentOptions#PORT} attribute.
		 */
		tcpclient,

		/**
		 * Value for the {@link AgentOptions#OUTPUT} parameter: Do not produce
		 * any output.
		 */
		none

	}

	/**
	 * The IP address or DNS name the tcpserver binds to or the tcpclient
	 * connects to. Default is defined by {@link #DEFAULT_ADDRESS}.
	 */
	public static final String ADDRESS = "address";

	/**
	 * Default value for the "address" agent option.
	 */
	public static final String DEFAULT_ADDRESS = null;

	/**
	 * The port the tcpserver binds to or the tcpclient connects to. In
	 * tcpserver mode the port must be available, which means that if multiple
	 * JaCoCo agents should run on the same machine, different ports have to be
	 * specified. Default is defined by {@link #DEFAULT_PORT}.
	 */
	public static final String PORT = "port";

	/**
	 * Default value for the "port" agent option.
	 */
	public static final int DEFAULT_PORT = 6300;

	/**
	 * Specifies where the agent dumps all class files it encounters. The
	 * location is specified as a relative path to the working directory.
	 * Default is <code>null</code> (no dumps).
	 */
	public static final String CLASSDUMPDIR = "classdumpdir";

	/**
	 * Specifies whether the agent should expose functionality via JMX under the
	 * name "org.jacoco:type=Runtime". Default is <code>false</code>.
	 */
	public static final String JMX = "jmx";

	private static final Collection<String> VALID_OPTIONS = Arrays.asList(
			DESTFILE, APPEND, INCLUDES, EXCLUDES, EXCLCLASSLOADER,
			INCLBOOTSTRAPCLASSES, INCLNOLOCATIONCLASSES, SESSIONID, DUMPONEXIT,
			OUTPUT, ADDRESS, PORT, CLASSDUMPDIR, JMX);

	private final Map<String, String> options;

	/**
	 * New instance with all values set to default.
	 */
	public AgentOptions() {
		this.options = new HashMap<String, String>();
	}

	/**
	 * New instance parsed from the given option string.
	 * 
	 * @param optionstr
	 *            string to parse or <code>null</code>
	 */
	public AgentOptions(final String optionstr) {
		this();
		if (optionstr != null && optionstr.length() > 0) {
			for (final String entry : OPTION_SPLIT.split(optionstr)) {
				final int pos = entry.indexOf('=');
				if (pos == -1) {
					throw new IllegalArgumentException(format(
							"Invalid agent option syntax \"%s\".", optionstr));
				}
				final String key = entry.substring(0, pos);
				if (!VALID_OPTIONS.contains(key)) {
					throw new IllegalArgumentException(format(
							"Unknown agent option \"%s\".", key));
				}

				final String value = entry.substring(pos + 1);
				setOption(key, value);
			}

			validateAll();
		}
	}

	/**
	 * New instance read from the given {@link Properties} object.
	 * 
	 * @param properties
	 *            {@link Properties} object to read configuration options from
	 */
	public AgentOptions(final Properties properties) {
		this();
		for (final String key : VALID_OPTIONS) {
			final String value = properties.getProperty(key);
			if (value != null) {
				setOption(key, value);
			}
		}
	}

	private void validateAll() {
		validatePort(getPort());
		getOutput();
	}

	private void validatePort(final int port) {
		if (port < 0) {
			throw new IllegalArgumentException("port must be positive");
		}
	}

	/**
	 * Returns the output file location.
	 * 
	 * @return output file location
	 */
	public String getDestfile() {
		return getOption(DESTFILE, DEFAULT_DESTFILE);
	}

	/**
	 * Sets the output file location.
	 * 
	 * @param destfile
	 *            output file location
	 */
	public void setDestfile(final String destfile) {
		setOption(DESTFILE, destfile);
	}

	/**
	 * Returns whether the output should be appended to an existing file.
	 * 
	 * @return <code>true</code>, when the output should be appended
	 */
	public boolean getAppend() {
		return getOption(APPEND, true);
	}

	/**
	 * Sets whether the output should be appended to an existing file.
	 * 
	 * @param append
	 *            <code>true</code>, when the output should be appended
	 */
	public void setAppend(final boolean append) {
		setOption(APPEND, append);
	}

	/**
	 * Returns the wildcard expression for classes to include.
	 * 
	 * @return wildcard expression for classes to include
	 * @see WildcardMatcher
	 */
	public String getIncludes() {
		return getOption(INCLUDES, "*");
	}

	/**
	 * Sets the wildcard expression for classes to include.
	 * 
	 * @param includes
	 *            wildcard expression for classes to include
	 * @see WildcardMatcher
	 */
	public void setIncludes(final String includes) {
		setOption(INCLUDES, includes);
	}

	/**
	 * Returns the wildcard expression for classes to exclude.
	 * 
	 * @return wildcard expression for classes to exclude
	 * @see WildcardMatcher
	 */
	public String getExcludes() {
		return getOption(EXCLUDES, "");
	}

	/**
	 * Sets the wildcard expression for classes to exclude.
	 * 
	 * @param excludes
	 *            wildcard expression for classes to exclude
	 * @see WildcardMatcher
	 */
	public void setExcludes(final String excludes) {
		setOption(EXCLUDES, excludes);
	}

	/**
	 * Returns the wildcard expression for excluded class loaders.
	 * 
	 * @return expression for excluded class loaders
	 * @see WildcardMatcher
	 */
	public String getExclClassloader() {
		return getOption(EXCLCLASSLOADER, "sun.reflect.DelegatingClassLoader");
	}

	/**
	 * Sets the wildcard expression for excluded class loaders.
	 * 
	 * @param expression
	 *            expression for excluded class loaders
	 * @see WildcardMatcher
	 */
	public void setExclClassloader(final String expression) {
		setOption(EXCLCLASSLOADER, expression);
	}

	/**
	 * Returns whether classes from the bootstrap classloader should be
	 * instrumented.
	 * 
	 * @return <code>true</code> if classes from the bootstrap classloader
	 *         should be instrumented
	 */
	public boolean getInclBootstrapClasses() {
		return getOption(INCLBOOTSTRAPCLASSES, false);
	}

	/**
	 * Sets whether classes from the bootstrap classloader should be
	 * instrumented.
	 * 
	 * @param include
	 *            <code>true</code> if bootstrap classes should be instrumented
	 */
	public void setInclBootstrapClasses(final boolean include) {
		setOption(INCLBOOTSTRAPCLASSES, include);
	}

	/**
	 * Returns whether classes without source location should be instrumented.
	 * 
	 * @return <code>true</code> if classes without source location should be
	 *         instrumented
	 */
	public boolean getInclNoLocationClasses() {
		return getOption(INCLNOLOCATIONCLASSES, false);
	}

	/**
	 * Sets whether classes without source location should be instrumented.
	 * 
	 * @param include
	 *            <code>true</code> if classes without source location should be
	 *            instrumented
	 */
	public void setInclNoLocationClasses(final boolean include) {
		setOption(INCLNOLOCATIONCLASSES, include);
	}

	/**
	 * Returns the session identifier.
	 * 
	 * @return session identifier
	 */
	public String getSessionId() {
		return getOption(SESSIONID, null);
	}

	/**
	 * Sets the session identifier.
	 * 
	 * @param id
	 *            session identifier
	 */
	public void setSessionId(final String id) {
		setOption(SESSIONID, id);
	}

	/**
	 * Returns whether coverage data should be dumped on exit.
	 * 
	 * @return <code>true</code> if coverage data will be written on VM exit
	 */
	public boolean getDumpOnExit() {
		return getOption(DUMPONEXIT, true);
	}

	/**
	 * Sets whether coverage data should be dumped on exit.
	 * 
	 * @param dumpOnExit
	 *            <code>true</code> if coverage data should be written on VM
	 *            exit
	 */
	public void setDumpOnExit(final boolean dumpOnExit) {
		setOption(DUMPONEXIT, dumpOnExit);
	}

	/**
	 * Returns the port on which to listen to when the output is
	 * <code>tcpserver</code> or the port to connect to when output is
	 * <code>tcpclient</code>.
	 * 
	 * @return port to listen on or connect to
	 */
	public int getPort() {
		return getOption(PORT, DEFAULT_PORT);
	}

	/**
	 * Sets the port on which to listen to when output is <code>tcpserver</code>
	 * or the port to connect to when output is <code>tcpclient</code>
	 * 
	 * @param port
	 *            port to listen on or connect to
	 */
	public void setPort(final int port) {
		validatePort(port);
		setOption(PORT, port);
	}

	/**
	 * Gets the hostname or IP address to listen to when output is
	 * <code>tcpserver</code> or connect to when output is
	 * <code>tcpclient</code>
	 * 
	 * @return Hostname or IP address
	 */
	public String getAddress() {
		return getOption(ADDRESS, DEFAULT_ADDRESS);
	}

	/**
	 * Sets the hostname or IP address to listen to when output is
	 * <code>tcpserver</code> or connect to when output is
	 * <code>tcpclient</code>
	 * 
	 * @param address
	 *            Hostname or IP address
	 */
	public void setAddress(final String address) {
		setOption(ADDRESS, address);
	}

	/**
	 * Returns the output mode
	 * 
	 * @return current output mode
	 */
	public OutputMode getOutput() {
		final String value = options.get(OUTPUT);
// BEGIN android-change
//		return value == null ? OutputMode.file : OutputMode.valueOf(value);
		return value == null ? OutputMode.none : OutputMode.valueOf(value);
// END android-change
	}

	/**
	 * Sets the output mode
	 * 
	 * @param output
	 *            Output mode
	 */
	public void setOutput(final String output) {
		setOutput(OutputMode.valueOf(output));
	}

	/**
	 * Sets the output mode
	 * 
	 * @param output
	 *            Output mode
	 */
	public void setOutput(final OutputMode output) {
		setOption(OUTPUT, output.name());
	}

	/**
	 * Returns the location of the directory where class files should be dumped
	 * to.
	 * 
	 * @return dump location or <code>null</code> (no dumps)
	 */
	public String getClassDumpDir() {
		return getOption(CLASSDUMPDIR, null);
	}

	/**
	 * Sets the directory where class files should be dumped to.
	 * 
	 * @param location
	 *            dump location or <code>null</code> (no dumps)
	 */
	public void setClassDumpDir(final String location) {
		setOption(CLASSDUMPDIR, location);
	}

	/**
	 * Returns whether the agent exposes functionality via JMX.
	 * 
	 * @return <code>true</code>, when JMX is enabled
	 */
	public boolean getJmx() {
		return getOption(JMX, false);
	}

	/**
	 * Sets whether the agent should expose functionality via JMX.
	 * 
	 * @param jmx
	 *            <code>true</code> if JMX should be enabled
	 */
	public void setJmx(final boolean jmx) {
		setOption(JMX, jmx);
	}

	private void setOption(final String key, final int value) {
		setOption(key, Integer.toString(value));
	}

	private void setOption(final String key, final boolean value) {
		setOption(key, Boolean.toString(value));
	}

	private void setOption(final String key, final String value) {
		options.put(key, value);
	}

	private String getOption(final String key, final String defaultValue) {
		final String value = options.get(key);
		return value == null ? defaultValue : value;
	}

	private boolean getOption(final String key, final boolean defaultValue) {
		final String value = options.get(key);
		return value == null ? defaultValue : Boolean.parseBoolean(value);
	}

	private int getOption(final String key, final int defaultValue) {
		final String value = options.get(key);
		return value == null ? defaultValue : Integer.parseInt(value);
	}

	/**
	 * Generate required JVM argument based on current configuration and
	 * supplied agent jar location.
	 * 
	 * @param agentJarFile
	 *            location of the JaCoCo Agent Jar
	 * @return Argument to pass to create new VM with coverage enabled
	 */
	public String getVMArgument(final File agentJarFile) {
		return format("-javaagent:%s=%s", agentJarFile, this);
	}

	/**
	 * Generate required quoted JVM argument based on current configuration and
	 * supplied agent jar location.
	 * 
	 * @param agentJarFile
	 *            location of the JaCoCo Agent Jar
	 * @return Quoted argument to pass to create new VM with coverage enabled
	 */
	public String getQuotedVMArgument(final File agentJarFile) {
		return CommandLineSupport.quote(getVMArgument(agentJarFile));
	}

	/**
	 * Generate required quotes JVM argument based on current configuration and
	 * prepends it to the given argument command line. If a agent with the same
	 * JAR file is already specified this parameter is removed from the existing
	 * command line.
	 * 
	 * @param arguments
	 *            existing command line arguments or <code>null</code>
	 * @param agentJarFile
	 *            location of the JaCoCo Agent Jar
	 * @return VM command line arguments prepended with configured JaCoCo agent
	 */
	public String prependVMArguments(final String arguments,
			final File agentJarFile) {
		final List<String> args = CommandLineSupport.split(arguments);
		final String plainAgent = format("-javaagent:%s", agentJarFile);
		for (final Iterator<String> i = args.iterator(); i.hasNext();) {
			if (i.next().startsWith(plainAgent)) {
				i.remove();
			}
		}
		args.add(0, getVMArgument(agentJarFile));
		return CommandLineSupport.quote(args);
	}

	/**
	 * Creates a string representation that can be passed to the agent via the
	 * command line. Might be the empty string, if no options are set.
	 */
	@Override
	public String toString() {
		final StringBuilder sb = new StringBuilder();
		for (final String key : VALID_OPTIONS) {
			final String value = options.get(key);
			if (value != null) {
				if (sb.length() > 0) {
					sb.append(',');
				}
				sb.append(key).append('=').append(value);
			}
		}
		return sb.toString();
	}

}