Last active
December 7, 2023 15:35
-
-
Save wvanderdeijl/4c5f67edff96f3d5f34f to your computer and use it in GitHub Desktop.
JUnit Rule to write Jacoco execution data file after each test for application under test running on a (remote) container
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package org.adfemg.datacontrol.view.uitest; | |
import java.io.IOException; | |
import java.net.ConnectException; | |
import java.nio.file.Path; | |
import java.nio.file.Paths; | |
import java.util.logging.Logger; | |
import org.jacoco.core.tools.ExecDumpClient; | |
import org.jacoco.core.tools.ExecFileLoader; | |
import org.junit.rules.TestRule; | |
import org.junit.runner.Description; | |
import org.junit.runners.model.Statement; | |
/** | |
* JUnit rule to collect jacoco code coverage information during execution of a unit test method or test class. | |
* Can be used with a @Rule annotation to fire before/after each test method in your test or with a @RuleClass | |
* annotation to collect statistics over the entire test run of the test class.<p> | |
* As this rule has quite a few configuration properties, it includes a Builder to conveniently configure your | |
* rule class. See the documentation on these builder methods to see the exact behavior of this rule. | |
* <p> | |
* Example: | |
* {@code @ClassRule | |
* public static final JacocoRule jacoco = JacocoRule.builder().withOutputDir(Paths.get("coverage")).build();} | |
*/ | |
public class JacocoRule implements TestRule { | |
private final boolean reset; | |
private final boolean append; | |
private final String host; | |
private final int port; | |
private final int retryCount; | |
private final long retryDelay; | |
private final Path outputdir; | |
private final JacocoReporter reporter; | |
private Description description; // Description of currently executing test-class or test-method | |
private static final Logger log = Logger.getLogger(JacocoRule.class.getName()); | |
/** | |
* Private constructor. See {@link #builder()}and {@link Builder}for a builder to create and configure a JacocoRule. | |
* @param host host with the remote jacoco agent to connect to | |
* @param port port of the remote jacoco agent to connect to | |
* @param retryCount number of retries for a failed jacoco remote agent connection | |
* @param retryDelay number of milliseconds to wait between retries | |
* @param reset should the execution counters be reset at the beginning of the test? | |
* @param append should execution data be added if the file already exists, or should we overwrite | |
* @param outputdir directory where the execution data should be written | |
* @param reporter optional reporter that transforms the execution data to a readable report after the test | |
*/ | |
private JacocoRule(final String host, final int port, final int retryCount, final long retryDelay, | |
final boolean reset, final boolean append, final Path outputdir, final JacocoReporter reporter) { | |
this.host = host; | |
this.port = port; | |
this.retryCount = retryCount; | |
this.retryDelay = retryDelay; | |
this.reset = reset; | |
this.append = append; | |
this.outputdir = outputdir; | |
this.reporter = reporter; | |
} | |
/** | |
* Static method to get a {@link Builder} to configure and create a new JacocoRule. | |
* @return Builder | |
*/ | |
public static Builder builder() { | |
return new Builder(); | |
} | |
private void before() throws IOException { | |
if (reset) { | |
reset(); | |
} | |
} | |
private void after() throws IOException { | |
// construct a jacoco client to send request to dump statistics | |
ExecDumpClient client = new ExecDumpClient(); | |
ExecFileLoader dump = dump(client); | |
Path file = getDumpFile(); | |
log.fine("Dumping code coverage execution data for " + description.getDisplayName() + " to " + file); | |
dump.save(file.toFile(), append); | |
if (reporter != null) { | |
// if a reporter is known, also create a report | |
String repname = description.getClassName(); | |
if (description.getMethodName() != null) { | |
repname += "-" + description.getMethodName(); | |
} | |
final String title = description.getDisplayName(); | |
reporter.report(dump, repname, title); | |
} | |
} | |
private void reset() throws IOException { | |
ExecDumpClient client = new ExecDumpClient(); | |
client.setDump(false); | |
client.setReset(true); | |
log.fine("Resetting code coverage execution statistics for " + description.getDisplayName()); | |
dump(client); | |
} | |
private ExecFileLoader dump(ExecDumpClient client) throws IOException { | |
client.setRetryCount(retryCount); | |
client.setRetryDelay(retryDelay); | |
try { | |
return client.dump(host, port); | |
} catch (ConnectException e) { | |
throw new IOException("make sure application-under-test is running and you started its jvm with -javaagent:/somewhere/jacocoagent.jar=output=tcpserver", | |
e); | |
} | |
} | |
private Path getDumpFile() throws IOException { | |
return outputdir.resolve(getDecriptionFileName() + ".exec").normalize().toAbsolutePath(); | |
} | |
private String getDecriptionFileName() { | |
StringBuilder filename = new StringBuilder(description.getClassName()); | |
if (description.getMethodName() != null) { | |
filename.append("-").append(description.getMethodName()); | |
} | |
return filename.toString(); | |
} | |
/** | |
* Modifies the given method-running Statement to potentiale reset execution statistics before executing the test, | |
* dumping execution statistics after the test, and optionally create a report after the test. | |
* @param base the Statement to be modified | |
* @param description a Description of the test implemented in given Statement | |
* @return a new Statement, a wrapper around base, that handles any pre- and post-actions required for collecting | |
* and report jacoco statistics. | |
*/ | |
@Override | |
public Statement apply(final Statement base, final Description description) { | |
return new Statement() { | |
@Override | |
public void evaluate() throws Throwable { | |
JacocoRule.this.description = description; | |
before(); | |
try { | |
base.evaluate(); | |
} finally { | |
after(); | |
} | |
} | |
}; | |
} | |
/** | |
* Builder to configure and create a new JacocoRule. Each configuration method returns this same builder after | |
* changing its internal state. This can be used to chain multiple method invocations in a single statement. The | |
* final method being called should be {@link #build} which actually returns the configured JacococRule. | |
* <p> | |
* See each method for an explanation of the separate configurable items. | |
* <p> | |
* Example | |
* {@code JacocoRule.builder().withHost("test.example.com").withOutputDir(Paths.get("coverage")).build();} | |
*/ | |
public static class Builder { | |
private static final String DFLT_HOST = "localhost"; | |
private static final int DFLT_PORT = 6300; | |
private static final int DFLT_RETRY_COUNT = 0; | |
private static final int DFLT_RETRY_DELAY = 1000; | |
private static final Path DFLT_OUTPUT_DIR = Paths.get(""); | |
private boolean reset = true; | |
private boolean append = false; | |
private String host = DFLT_HOST; | |
private int port = DFLT_PORT; | |
private int retryCount = DFLT_RETRY_COUNT; | |
private long retryDelay = DFLT_RETRY_DELAY; | |
private Path outputdir = DFLT_OUTPUT_DIR; | |
private JacocoReporter reporter = null; | |
/** | |
* Private constructor. | |
* @see JacocoRule#builder | |
*/ | |
private Builder() { | |
} | |
/** | |
* Do not reset the jacoco statistics at the beginning of the test instead of the default behavior which | |
* is to reset at the beginning so you get a clean report of the coverage that ran just during your test. | |
* @return Builder so you can continue invoking configuration methods | |
*/ | |
public Builder withoutReset() { | |
this.reset = false; | |
return this; | |
} | |
/** | |
* Do not overwrite the existing execution file (which is the default), but append to the file if it already | |
* exists. | |
* @return Builder so you can continue invoking configuration methods | |
*/ | |
public Builder withAppend() { | |
this.append = true; | |
return this; | |
} | |
/** | |
* Do not connect to the default host ({@value #DFLT_HOST}) but to the given host. | |
* @param host host name (or ip address) to connect to that runs the jacaoco agent | |
* @return Builder so you can continue invoking configuration methods | |
* @see #withPort | |
*/ | |
public Builder withHost(final String host) { | |
this.host = host; | |
return this; | |
} | |
/** | |
* Do not connect to the default port ( {@value #DFLT_PORT}) but to the given port. | |
* @param port port number to connect to for the host that runs the jacaoco agent | |
* @return Builder so you can continue invoking configuration methods | |
* @see #withHost | |
*/ | |
public Builder withPort(final int port) { | |
this.port = port; | |
return this; | |
} | |
/** | |
* Specify if, and how many times, we should retry connecting to the jacoco agent. Default is no retries. | |
* @param retryCount number of retries, where 0 means no retries | |
* @return Builder so you can continue invoking configuration methods | |
* @see #withRetryDelay | |
*/ | |
public Builder withRetryCount(final int retryCount) { | |
this.retryCount = retryCount; | |
return this; | |
} | |
/** | |
* Specify how many milliseconds we should wait between try attempts to connect to the jacoco agent. Default is | |
* {@value #DFLT_RETRY_DELAY} millisecs. | |
* @param retryDelayMSecs number of msecs to wait between retry attempts | |
* @return Builder so you can continue invoking configuration methods | |
* @see #withRetryCount | |
*/ | |
public Builder withRetryDelay(final long retryDelayMSecs) { | |
this.retryDelay = retryDelayMSecs; | |
return this; | |
} | |
/** | |
* Specify the directory to write the execution data files. Default is the current working directory. | |
* @param dir directory to write the execution data files | |
* @return Builder so you can continue invoking configuration methods | |
*/ | |
public Builder withOutputDir(final Path dir) { | |
this.outputdir = dir; | |
return this; | |
} | |
/** | |
* Specify the optional {@link JacocoReporter} to write a human readable report after dumping the execution | |
* data file. Default is to not use a reporter and only write the execution data file for later analysis. | |
* @param reporter a JacocoReporter or {@code null} if no reporter is needed | |
* @return Builder so you can continue invoking configuration methods | |
*/ | |
public Builder withReporter(final JacocoReporter reporter) { | |
this.reporter = reporter; | |
return this; | |
} | |
/** | |
* Construct a JacocoRule with all the configuration given to this builder. | |
* @return a fully (and immutable) JacocoRule instance. | |
*/ | |
public JacocoRule build() { | |
return new JacocoRule(host, port, retryCount, retryDelay, reset, append, outputdir, reporter); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment