com.teamscale.tia.maven.TiaMojoBase Maven / Gradle / Ivy
package com.teamscale.tia.maven;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.execution.MavenSession;
import org.apache.maven.model.Plugin;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugins.annotations.Parameter;
import org.codehaus.plexus.util.xml.Xpp3Dom;
import org.conqat.lib.commons.filesystem.FileSystemUtils;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Collections;
import java.util.Map;
/**
* Base class for TIA Mojos. Provides all necessary functionality but can be subclassed to change the partition.
*
* For this plugin to work, you must either
*
*
* - Make Surefire and Failsafe use our JUnit 5 test engine
* - Send test start and end events to the Java agent themselves
*
*
* To use our JUnit 5 test engine, you must declare it as a dependency of the maven-surefire-plugin and/or the
* maven-failsafe-plugin. Example:
*
*
{@code
*
* org.apache.maven.plugins
* maven-surefire-plugin
* 3.0.0-M5
*
*
* com.teamscale
* teamscale-surefire-provider
* 23.2.0
*
*
*
* }
*
* To send test events yourself, you can use our TIA client library (Maven coordinates: com.teamscale:tia-client).
*
* The log file of the agent is written to {@code ${project.build.directory}/tia/agent.log}.
*/
public abstract class TiaMojoBase extends AbstractMojo {
/**
* The URL of the Teamscale instance to which the recorded coverage will be uploaded.
*/
@Parameter(required = true)
public String teamscaleUrl;
/**
* The Teamscale project to which the recorded coverage will be uploaded
*/
@Parameter(required = true)
public String projectId;
/**
* The username to use to perform the upload. Must have the "Upload external data" permission for the {@link
* #projectId}. Can also be specified via the Maven property {@code teamscale.username}.
*/
@Parameter(property = "teamscale.username", required = true)
public String username;
/**
* Teamscale access token of the {@link #username}. Can also be specified via the Maven property {@code
* teamscale.accessToken}.
*/
@Parameter(property = "teamscale.accessToken", required = true)
public String accessToken;
/**
* You can optionally use this property to override the code commit to which the coverage will be uploaded. Format:
* {@code BRANCH:UNIX_EPOCH_TIMESTAMP_IN_MILLISECONDS}
*
* If no end commit is manually specified, the plugin will try to determine the currently checked out Git commit.
*/
@Parameter
public String endCommit;
/**
* You can optionally specify which code should be included in the coverage instrumentation. Each pattern is applied
* to the fully qualified class names of the profiled system. Use {@code *} to match any number characters and
* {@code ?} to match any single character.
*
* Classes that match any of the include patterns are included, unless any exclude pattern excludes them.
*/
@Parameter
public String[] includes;
/**
* You can optionally specify which code should be excluded from the coverage instrumentation. Each pattern is
* applied to the fully qualified class names of the profiled system. Use {@code *} to match any number characters
* and {@code ?} to match any single character.
*
* Classes that match any of the exclude patterns are excluded, even if they are included by an include pattern.
*/
@Parameter
public String[] excludes;
/**
* In order to instrument the system under test, a Java agent must be attached to the JVM of the system. The JVM
* command line arguments to achieve this are by default written to the property {@code argLine}, which is
* automatically picked up by Surefire and Failsafe and applied to the JVMs these plugins start. You can override
* the name of this property if you wish to manually apply the command line arguments yourself, e.g. if your system
* under test is started by some other plugin like the Spring boot starter.
*/
@Parameter
public String propertyName;
/**
* Port on which the Java agent listens for commands from this plugin.
*/
@Parameter(defaultValue = "12888")
public String agentPort;
/**
* Optional additional arguments to send to the agent. Each argument must be of the form {@code KEY=VALUE}.
*/
@Parameter
public String[] additionalAgentOptions;
/**
* Changes the log level of the agent to DEBUG.
*/
@Parameter(defaultValue = "false")
public boolean debugLogging;
/**
* Whether to skip the execution of this Mojo.
*/
@Parameter(defaultValue = "false")
public boolean skip;
/**
* Map of resolved Maven artifacts. Provided automatically by Maven.
*/
@Parameter(property = "plugin.artifactMap", required = true, readonly = true)
public Map pluginArtifactMap;
/**
* The project build directory (usually: {@code ./target}). Provided automatically by Maven.
*/
@Parameter(defaultValue = "${project.build.directory}")
public String projectBuildDir;
/**
* The running Maven session. Provided automatically by Maven.
*/
@Parameter(defaultValue = "${session}")
public MavenSession session;
private Path targetDirectory;
private String resolvedEndCommit;
@Override
public void execute() throws MojoFailureException {
if (skip) {
return;
}
validateTestPluginConfiguration(getTestPluginArtifact());
targetDirectory = Paths.get(projectBuildDir, "tia").toAbsolutePath();
createTargetDirectory();
resolvedEndCommit = resolveEndCommit();
setTiaProperty("reportDirectory", targetDirectory.toString());
setTiaProperty("server.url", teamscaleUrl);
setTiaProperty("server.project", projectId);
setTiaProperty("server.userName", username);
setTiaProperty("server.userAccessToken", accessToken);
setTiaProperty("endCommit", resolvedEndCommit);
setTiaProperty("agentsUrls", "http://localhost:" + agentPort);
Path agentConfigFile = createAgentConfigFiles();
Path logFilePath = targetDirectory.resolve("agent.log");
setArgLine(agentConfigFile, logFilePath);
}
private void validateTestPluginConfiguration(String testPluginArtifact) throws MojoFailureException {
Map plugins = session.getCurrentProject().getModel().getBuild().getPluginsAsMap();
Plugin testPlugin = plugins.get(testPluginArtifact);
if (testPlugin == null) {
return;
}
Xpp3Dom configurationDom = (Xpp3Dom) testPlugin.getConfiguration();
if (configurationDom == null) {
return;
}
validateParallelizationParameter(testPluginArtifact, configurationDom, "threadCount");
validateParallelizationParameter(testPluginArtifact, configurationDom, "forkCount");
Xpp3Dom parameterDom = configurationDom.getChild("reuseForks");
if (parameterDom == null) {
return;
}
String value = parameterDom.getValue();
if (value != null && !value.equals("true")) {
throw new MojoFailureException(
"You configured the " + testPluginArtifact + " plugin to not reuse forks via the reuseForks configuration parameter." +
" This is not supported when performing Test Impact analysis as it prevents properly recording testwise coverage." +
" Please enable fork reuse when running Test Impact analysis.");
}
}
private void validateParallelizationParameter(String testPluginArtifact, Xpp3Dom configurationDom,
String parallelizationParameter) throws MojoFailureException {
Xpp3Dom parameterDom = configurationDom.getChild(parallelizationParameter);
if (parameterDom == null) {
return;
}
String value = parameterDom.getValue();
if (value != null && !value.equals("1")) {
throw new MojoFailureException(
"You configured parallel tests in the " + testPluginArtifact + " plugin via the " + parallelizationParameter + " configuration parameter." +
" Parallel tests are not supported when performing Test Impact analysis as they prevent recording testwise coverage." +
" Please disable parallel tests when running Test Impact analysis.");
}
}
/**
* @return the partition to upload testwise coverage to.
*/
protected abstract String getPartition();
/**
* @return the artifact name of the test plugin (e.g. Surefire, Failsafe).
*/
protected abstract String getTestPluginArtifact();
/**
* @return whether this Mojo applies to integration tests.
*
* Depending on this, different properties are used to set the argLine.
*/
protected abstract boolean isIntegrationTest();
private void createTargetDirectory() throws MojoFailureException {
try {
Files.createDirectories(targetDirectory);
} catch (IOException e) {
throw new MojoFailureException("Could not create target directory " + targetDirectory, e);
}
}
private void setArgLine(Path agentConfigFile, Path logFilePath) {
String agentLogLevel = "INFO";
if (debugLogging) {
agentLogLevel = "DEBUG";
}
ArgLine.cleanOldArgLines(session, getLog());
ArgLine.applyToMavenProject(
new ArgLine(additionalAgentOptions, agentLogLevel, findAgentJarFile(), agentConfigFile, logFilePath),
session, getLog(), propertyName, isIntegrationTest());
}
private Path createAgentConfigFiles() throws MojoFailureException {
Path loggingConfigPath = targetDirectory.resolve("logback.xml");
try (OutputStream loggingConfigOutputStream = Files.newOutputStream(loggingConfigPath)) {
FileSystemUtils.copy(readAgentLogbackConfig(), loggingConfigOutputStream);
} catch (IOException e) {
throw new MojoFailureException("Writing the logging configuration file for the TIA agent failed." +
" Make sure the path " + loggingConfigPath + " is writeable.", e);
}
Path configFilePath = targetDirectory.resolve("agent.properties");
String agentConfig = createAgentConfig(loggingConfigPath, targetDirectory.resolve("reports"));
try {
Files.write(configFilePath, Collections.singleton(agentConfig));
} catch (IOException e) {
throw new MojoFailureException("Writing the configuration file for the TIA agent failed." +
" Make sure the path " + configFilePath + " is writeable.", e);
}
getLog().info("Agent config file created at " + configFilePath);
return configFilePath;
}
private InputStream readAgentLogbackConfig() {
return TiaMojoBase.class.getResourceAsStream("logback-agent.xml");
}
private String createAgentConfig(Path loggingConfigPath, Path agentOutputDirectory) {
String config = "mode=testwise" +
"\ntia-mode=teamscale-upload" +
"\nteamscale-server-url=" + teamscaleUrl +
"\nteamscale-project=" + projectId +
"\nteamscale-user=" + username +
"\nteamscale-access-token=" + accessToken +
"\nteamscale-commit=" + resolvedEndCommit +
"\nteamscale-partition=" + getPartition() +
"\nhttp-server-port=" + agentPort +
"\nlogging-config=" + loggingConfigPath +
"\nout=" + agentOutputDirectory.toAbsolutePath();
if (ArrayUtils.isNotEmpty(includes)) {
config += "\nincludes=" + String.join(",", includes);
}
if (ArrayUtils.isNotEmpty(excludes)) {
config += "\nexcludes=" + String.join(",", excludes);
}
return config;
}
private Path findAgentJarFile() {
Artifact agentArtifact = pluginArtifactMap.get("com.teamscale:teamscale-jacoco-agent");
return agentArtifact.getFile().toPath();
}
private String resolveEndCommit() throws MojoFailureException {
if (StringUtils.isNotBlank(endCommit)) {
return endCommit;
}
Path basedir = session.getCurrentProject().getBasedir().toPath();
try {
GitCommit commit = GitCommit.getGitHeadCommitDescriptor(basedir);
return commit.branch + ":" + commit.timestamp;
} catch (IOException e) {
throw new MojoFailureException("You did not configure an in the pom.xml" +
" and I could also not determine the checked out commit in " + basedir + " from Git", e);
}
}
/**
* Sets a property in the TIA namespace. It seems that, depending on Maven version and which other plugins are used,
* different types of properties are respected both during the build and during tests (as e.g. failsafe tests are
* often run in a separate JVM spawned by Maven). So we set our properties in every possible way to make sure the
* plugin works out of the box in most situations.
*/
private void setTiaProperty(String name, String value) {
String fullyQualifiedName = "teamscale.test.impacted." + name;
getLog().debug("Setting property " + name + "=" + value);
session.getUserProperties().setProperty(fullyQualifiedName, value);
session.getSystemProperties().setProperty(fullyQualifiedName, value);
System.setProperty(fullyQualifiedName, value);
}
}