diff --git a/teamscale-maven-plugin/src/main/java/.gitkeep b/teamscale-maven-plugin/src/main/java/.gitkeep
new file mode 100644
index 000000000..e69de29bb
diff --git a/teamscale-maven-plugin/src/main/java/com/teamscale/maven/GitCommitUtils.java b/teamscale-maven-plugin/src/main/java/com/teamscale/maven/GitCommitUtils.java
deleted file mode 100644
index 092ba3a49..000000000
--- a/teamscale-maven-plugin/src/main/java/com/teamscale/maven/GitCommitUtils.java
+++ /dev/null
@@ -1,45 +0,0 @@
-package com.teamscale.maven;
-
-import org.eclipse.jgit.api.Git;
-import org.eclipse.jgit.lib.Repository;
-
-import java.io.IOException;
-import java.nio.file.Files;
-import java.nio.file.Path;
-
-/**
- * Utils for working with a Git repository.
- */
-public class GitCommitUtils {
-
- /**
- * Determines the current HEAD commit in the Git repository located in the or above the given search directory.
- *
- * @throws IOException if reading from the Git repository fails or the current directory is not a Git repository.
- */
- public static String getGitHeadRevision(Path searchDirectory) throws IOException {
- Path gitDirectory = findGitBaseDirectory(searchDirectory);
- if (gitDirectory == null) {
- throw new IOException("Could not find git directory in " + searchDirectory);
- }
- Repository repository;
- try (Git git = Git.open(gitDirectory.toFile())) {
- repository = git.getRepository();
- return repository.getRefDatabase().findRef("HEAD").getObjectId().getName();
- }
- }
-
- /**
- * Traverses the directory tree upwards until it finds a .git directory. Returns null if no .git directory is
- * found.
- */
- private static Path findGitBaseDirectory(Path searchDirectory) {
- while (searchDirectory != null) {
- if (Files.exists(searchDirectory.resolve(".git"))) {
- return searchDirectory;
- }
- searchDirectory = searchDirectory.getParent();
- }
- return null;
- }
-}
diff --git a/teamscale-maven-plugin/src/main/java/com/teamscale/maven/TeamscaleMojoBase.java b/teamscale-maven-plugin/src/main/java/com/teamscale/maven/TeamscaleMojoBase.java
deleted file mode 100644
index 1996d1a3b..000000000
--- a/teamscale-maven-plugin/src/main/java/com/teamscale/maven/TeamscaleMojoBase.java
+++ /dev/null
@@ -1,151 +0,0 @@
-package com.teamscale.maven;
-
-import org.apache.commons.lang3.StringUtils;
-import org.apache.maven.execution.MavenSession;
-import org.apache.maven.model.Plugin;
-import org.apache.maven.model.PluginExecution;
-import org.apache.maven.plugin.AbstractMojo;
-import org.apache.maven.plugin.MojoExecutionException;
-import org.apache.maven.plugin.MojoFailureException;
-import org.apache.maven.plugins.annotations.Parameter;
-import org.apache.maven.project.MavenProject;
-import org.codehaus.plexus.util.xml.Xpp3Dom;
-
-import java.io.IOException;
-import java.nio.file.Path;
-
-/**
- * A base class for all Teamscale related maven Mojos. Offers basic attributes and functionality related to Teamscale
- * and Maven.
- */
-public abstract class TeamscaleMojoBase extends AbstractMojo {
-
- /**
- * The URL of the Teamscale instance to which the recorded coverage will be uploaded.
- */
- @Parameter()
- public String teamscaleUrl;
-
- /**
- * The Teamscale project to which the recorded coverage will be uploaded
- */
- @Parameter()
- 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")
- 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")
- 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 commit and revision is manually specified, the plugin will try to determine the currently checked-out Git
- * commit. You should specify either commit or revision, not both. If both are specified, a warning is logged and
- * the revision takes precedence.
- */
- @Parameter(property = "teamscale.commit")
- public String commit;
-
- /**
- * You can optionally use this property to override the revision to which the coverage will be uploaded. If no
- * commit and revision is manually specified, the plugin will try to determine the current git revision. You should
- * specify either commit or revision, not both. If both are specified, a warning is logged and the revision takes
- * precedence.
- */
- @Parameter(property = "teamscale.revision")
- public String revision;
-
- /**
- * The repository id in your Teamscale project which Teamscale should use to look up the revision, if given. Null or
- * empty will lead to a lookup in all repositories in the Teamscale project.
- */
- @Parameter(property = "teamscale.repository")
- public String repository;
-
- /**
- * Whether to skip the execution of this Mojo.
- */
- @Parameter(defaultValue = "false")
- public boolean skip;
-
- /**
- * The running Maven session. Provided automatically by Maven.
- */
- @Parameter(defaultValue = "${session}")
- public MavenSession session;
-
- /**
- * The resolved commit, either provided by the user or determined via the GitCommit class
- */
- protected String resolvedCommit;
-
- /**
- * The resolved revision, either provided by the user or determined via the GitCommit class
- */
- protected String resolvedRevision;
-
- @Override
- public void execute() throws MojoExecutionException, MojoFailureException {
- if (StringUtils.isNotEmpty(revision) && StringUtils.isNotBlank(commit)) {
- getLog().warn("Both revision and commit are set but only one of them is needed. " +
- "Teamscale will prefer the revision. If that's not intended, please do not set the revision manually.");
- }
- }
-
- /**
- * Sets the resolvedRevision or resolvedCommit. If not provided, try to determine the
- * revision via the GitCommit class.
- *
- * @see GitCommitUtils
- */
- protected void resolveCommitOrRevision() throws MojoFailureException {
- if (StringUtils.isNotBlank(revision)) {
- resolvedRevision = revision;
- return;
- }
- if (StringUtils.isNotBlank(commit)) {
- resolvedCommit = commit;
- return;
- }
- Path basedir = session.getCurrentProject().getBasedir().toPath();
- try {
- resolvedRevision = GitCommitUtils.getGitHeadRevision(basedir);
- } catch (IOException e) {
- throw new MojoFailureException("There is no or configured in the pom.xml" +
- " and it was not possible to determine the current revision in " + basedir + " from Git", e);
- }
- }
-
- /**
- * Retrieves the configuration of a goal execution for the given plugin
- *
- * @param pluginArtifact The id of the plugin
- * @param pluginGoal The name of the goal
- * @return The configuration DOM if present, otherwise null
- */
- protected Xpp3Dom getExecutionConfigurationDom(MavenProject project, String pluginArtifact, String pluginGoal) {
- Plugin plugin = project.getPlugin(pluginArtifact);
- if (plugin == null) {
- return null;
- }
-
- for (PluginExecution pluginExecution : plugin.getExecutions()) {
- if (pluginExecution.getGoals().contains(pluginGoal)) {
- return (Xpp3Dom) pluginExecution.getConfiguration();
- }
- }
-
- return null;
- }
-}
diff --git a/teamscale-maven-plugin/src/main/java/com/teamscale/maven/tia/ArgLine.java b/teamscale-maven-plugin/src/main/java/com/teamscale/maven/tia/ArgLine.java
deleted file mode 100644
index 5092e97d7..000000000
--- a/teamscale-maven-plugin/src/main/java/com/teamscale/maven/tia/ArgLine.java
+++ /dev/null
@@ -1,143 +0,0 @@
-package com.teamscale.maven.tia;
-
-import org.apache.commons.lang3.ArrayUtils;
-import org.apache.commons.lang3.StringUtils;
-import org.apache.maven.execution.MavenSession;
-import org.apache.maven.plugin.logging.Log;
-import org.apache.maven.project.MavenProject;
-
-import java.nio.file.Path;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-import java.util.stream.Collectors;
-
-/**
- * Composes a new argLine based on the current one and input about the desired agent configuration.
- */
-public class ArgLine {
-
- private final String[] additionalAgentOptions;
- private final String agentLogLevel;
- private final Path agentJarFile;
- private final Path agentConfigFile;
- private final Path logFilePath;
-
- public ArgLine(String[] additionalAgentOptions, String agentLogLevel, Path agentJarFile,
- Path agentConfigFile, Path logFilePath) {
- this.additionalAgentOptions = additionalAgentOptions;
- this.agentLogLevel = agentLogLevel;
- this.agentJarFile = agentJarFile;
- this.agentConfigFile = agentConfigFile;
- this.logFilePath = logFilePath;
- }
-
- /** Applies the given {@link ArgLine} to the given {@link MavenSession}. */
- public static void applyToMavenProject(ArgLine argLine, MavenSession session, Log log,
- String userDefinedPropertyName, boolean isIntegrationTest) {
- MavenProject mavenProject = session.getCurrentProject();
- ArgLineProperty effectiveProperty = ArgLine.getEffectiveProperty(userDefinedPropertyName, mavenProject,
- isIntegrationTest);
-
- String oldArgLine = effectiveProperty.getValue(session);
- String newArgLine = argLine.prependTo(oldArgLine);
-
- effectiveProperty.setValue(session, newArgLine);
- log.info(effectiveProperty.propertyName + " set to " + newArgLine);
- }
-
- /**
- * Removes any occurrences of our agent from all {@link ArgLineProperty#STANDARD_PROPERTIES}.
- */
- public static void cleanOldArgLines(MavenSession session, Log log) {
- for (ArgLineProperty property : ArgLineProperty.STANDARD_PROPERTIES) {
- String oldArgLine = property.getValue(session);
- if (StringUtils.isBlank(oldArgLine)) {
- continue;
- }
-
- String newArgLine = removePreviousTiaAgent(oldArgLine);
- if (!oldArgLine.equals(newArgLine)) {
- log.info("Removed agent from property " + property.propertyName);
- property.setValue(session, newArgLine);
- }
- }
- }
-
- /**
- * Takes the given old argLine, removes any previous invocation of our agent and prepends a new one based on the
- * constructor parameters of this class. Preserves all other options in the old argLine.
- */
- /*package*/ String prependTo(String oldArgLine) {
- String jvmOptions = createJvmOptions();
- if (StringUtils.isBlank(oldArgLine)) {
- return jvmOptions;
- }
-
- return jvmOptions + " " + oldArgLine;
- }
-
- private String createJvmOptions() {
- List jvmOptions = new ArrayList<>();
- jvmOptions.add("-Dteamscale.markstart");
- jvmOptions.add(createJavaAgentArgument());
- jvmOptions.add("-DTEAMSCALE_AGENT_LOG_FILE=" + logFilePath);
- jvmOptions.add("-DTEAMSCALE_AGENT_LOG_LEVEL=" + agentLogLevel);
- jvmOptions.add("-Dteamscale.markend");
- return jvmOptions.stream().map(ArgLine::quoteCommandLineOptionIfNecessary)
- .collect(Collectors.joining(" "));
- }
-
- private static String quoteCommandLineOptionIfNecessary(String option) {
- if (StringUtils.containsWhitespace(option)) {
- return "'" + option + "'";
- } else {
- return option;
- }
- }
-
- private String createJavaAgentArgument() {
- List agentOptions = new ArrayList<>();
- agentOptions.add("config-file=" + agentConfigFile.toAbsolutePath());
- agentOptions.addAll(Arrays.asList(ArrayUtils.nullToEmpty(additionalAgentOptions)));
- return "-javaagent:" + agentJarFile.toAbsolutePath() + "=" + String.join(",", agentOptions);
- }
-
- /**
- * Determines the property in which to set the argLine. By default, this is the property used by the testing
- * framework of the current project's packaging. The user may override this by providing their own property name.
- */
- private static ArgLineProperty getEffectiveProperty(String userDefinedPropertyName, MavenProject mavenProject,
- boolean isIntegrationTest) {
- if (StringUtils.isNotBlank(userDefinedPropertyName)) {
- return ArgLineProperty.projectProperty(userDefinedPropertyName);
- }
-
- if (isIntegrationTest && hasSpringBootPluginEnabled(mavenProject)) {
- return ArgLineProperty.SPRING_BOOT_ARG_LINE;
- }
-
- if ("eclipse-test-plugin".equals(mavenProject.getPackaging())) {
- return ArgLineProperty.TYCHO_ARG_LINE;
- }
- return ArgLineProperty.SUREFIRE_ARG_LINE;
- }
-
- private static boolean hasSpringBootPluginEnabled(MavenProject mavenProject) {
- return mavenProject.getBuildPlugins().stream()
- .anyMatch(plugin -> plugin.getArtifactId().equals("spring-boot-maven-plugin"));
- }
-
- /**
- * Removes any previous invocation of our agent from the given argLine. This is necessary in case we want to
- * instrument unit and integration tests but with different arguments.
- */
- /*package*/
- static String removePreviousTiaAgent(String argLine) {
- if (argLine == null) {
- return "";
- }
- return argLine.replaceAll("-Dteamscale.markstart.*teamscale.markend", "");
- }
-
-}
diff --git a/teamscale-maven-plugin/src/main/java/com/teamscale/maven/tia/ArgLineProperty.java b/teamscale-maven-plugin/src/main/java/com/teamscale/maven/tia/ArgLineProperty.java
deleted file mode 100644
index 4a582cdec..000000000
--- a/teamscale-maven-plugin/src/main/java/com/teamscale/maven/tia/ArgLineProperty.java
+++ /dev/null
@@ -1,67 +0,0 @@
-package com.teamscale.maven.tia;
-
-import org.apache.maven.execution.MavenSession;
-import org.apache.maven.project.MavenProject;
-
-import java.util.Properties;
-import java.util.function.Function;
-
-/** Accessor for different types of properties, e.g. project or user properties, in a {@link MavenSession}. */
-public class ArgLineProperty {
-
- /**
- * Name of the property used in the maven-osgi-test-plugin.
- */
- public static final ArgLineProperty TYCHO_ARG_LINE = projectProperty("tycho.testArgLine");
-
- /**
- * Name of the property used in the maven-surefire-plugin.
- */
- public static final ArgLineProperty SUREFIRE_ARG_LINE = projectProperty("argLine");
-
- /**
- * Name of the property used in the spring-boot-maven-plugin start goal.
- */
- public static final ArgLineProperty SPRING_BOOT_ARG_LINE = userProperty("spring-boot.run.jvmArguments");
-
- /** The standard properties that this plugin might modify. */
- public static final ArgLineProperty[] STANDARD_PROPERTIES = new ArgLineProperty[]{TYCHO_ARG_LINE, SUREFIRE_ARG_LINE, SPRING_BOOT_ARG_LINE};
-
- private static Properties getProjectProperties(MavenSession session) {
- return session.getCurrentProject().getProperties();
- }
-
- private static Properties getUserProperties(MavenSession session) {
- return session.getUserProperties();
- }
-
- /** Creates a project property ({@link MavenProject#getProperties()}). */
- public static ArgLineProperty projectProperty(String name) {
- return new ArgLineProperty(name, ArgLineProperty::getProjectProperties);
- }
-
- /** Creates a user property ({@link MavenSession#getUserProperties()}). */
- public static ArgLineProperty userProperty(String name) {
- return new ArgLineProperty(name, ArgLineProperty::getUserProperties);
- }
-
- /** The name of the property. */
- public final String propertyName;
- private final Function propertiesAccess;
-
- private ArgLineProperty(String propertyName,
- Function propertiesAccess) {
- this.propertyName = propertyName;
- this.propertiesAccess = propertiesAccess;
- }
-
- /** Returns the value of this property in the given Maven session */
- public String getValue(MavenSession session) {
- return propertiesAccess.apply(session).getProperty(propertyName);
- }
-
- /** Sets the value of this property in the given Maven session */
- public void setValue(MavenSession session, String value) {
- propertiesAccess.apply(session).setProperty(propertyName, value);
- }
-}
diff --git a/teamscale-maven-plugin/src/main/java/com/teamscale/maven/tia/TiaCoverageConvertMojo.java b/teamscale-maven-plugin/src/main/java/com/teamscale/maven/tia/TiaCoverageConvertMojo.java
deleted file mode 100644
index 81aa2c5a5..000000000
--- a/teamscale-maven-plugin/src/main/java/com/teamscale/maven/tia/TiaCoverageConvertMojo.java
+++ /dev/null
@@ -1,156 +0,0 @@
-package com.teamscale.maven.tia;
-
-import com.google.common.base.Strings;
-import com.teamscale.jacoco.agent.options.AgentOptionParseException;
-import com.teamscale.jacoco.agent.options.ClasspathUtils;
-import com.teamscale.jacoco.agent.options.FilePatternResolver;
-import org.apache.maven.execution.MavenSession;
-import org.apache.maven.plugin.AbstractMojo;
-import org.apache.maven.plugin.MojoFailureException;
-import org.apache.maven.plugins.annotations.LifecyclePhase;
-import org.apache.maven.plugins.annotations.Mojo;
-import org.apache.maven.plugins.annotations.Parameter;
-import org.apache.maven.plugins.annotations.ResolutionScope;
-import org.apache.maven.project.MavenProject;
-import shadow.com.teamscale.client.TestDetails;
-import shadow.com.teamscale.report.EDuplicateClassFileBehavior;
-import shadow.com.teamscale.report.ReportUtils;
-import shadow.com.teamscale.report.testwise.ETestArtifactFormat;
-import shadow.com.teamscale.report.testwise.TestwiseCoverageReportWriter;
-import shadow.com.teamscale.report.testwise.jacoco.JaCoCoTestwiseReportGenerator;
-import shadow.com.teamscale.report.testwise.model.TestExecution;
-import shadow.com.teamscale.report.testwise.model.factory.TestInfoFactory;
-import shadow.com.teamscale.report.util.ClasspathWildcardIncludeFilter;
-import shadow.com.teamscale.report.util.CommandLineLogger;
-import shadow.com.teamscale.report.util.ILogger;
-
-import java.io.File;
-import java.io.IOException;
-import java.nio.file.Files;
-import java.nio.file.Paths;
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * Batch converts all created .exec file reports into a testwise coverage
- * report.
- */
-@Mojo(name = "testwise-coverage-converter", defaultPhase = LifecyclePhase.VERIFY, requiresDependencyResolution = ResolutionScope.RUNTIME)
-public class TiaCoverageConvertMojo extends AbstractMojo {
- /**
- * Wildcard include patterns to apply during JaCoCo's traversal of class files.
- */
- @Parameter(defaultValue = "**")
- public String[] includes;
- /**
- * Wildcard exclude patterns to apply during JaCoCo's traversal of class files.
- */
- @Parameter()
- public String[] excludes;
-
- /**
- * After how many tests the testwise coverage should be split into multiple
- * reports (Default is 5000).
- */
- @Parameter(defaultValue = "5000")
- public int splitAfter;
-
- /**
- * The project build directory (usually: {@code ./target}). Provided
- * automatically by Maven.
- */
- @Parameter(defaultValue = "${project.build.directory}")
- public String projectBuildDir;
-
- /**
- * The output directory of the testwise coverage reports.
- */
- @Parameter()
- public String outputFolder;
-
- /**
- * The running Maven session. Provided automatically by Maven.
- */
- @Parameter(defaultValue = "${session}")
- public MavenSession session;
- private final ILogger logger = new CommandLineLogger();
-
- @Override
- public void execute() throws MojoFailureException {
-
- List reportFileDirectories = new ArrayList<>();
- reportFileDirectories.add(Paths.get(projectBuildDir, "tia").toAbsolutePath().resolve("reports").toFile());
- List classFileDirectories;
- if (Strings.isNullOrEmpty(outputFolder)) {
- outputFolder = Paths.get(projectBuildDir, "tia", "reports").toString();
- }
- try {
- Files.createDirectories(Paths.get(outputFolder));
- classFileDirectories = getClassDirectoriesOrZips(projectBuildDir);
- findSubprojectReportAndClassDirectories(reportFileDirectories, classFileDirectories);
- } catch (IOException | AgentOptionParseException e) {
- logger.error("Could not create testwise report generator. Aborting.", e);
- throw new MojoFailureException(e);
- }
- logger.info("Generating the testwise coverage report");
- JaCoCoTestwiseReportGenerator generator = createJaCoCoTestwiseReportGenerator(classFileDirectories);
- TestInfoFactory testInfoFactory = createTestInfoFactory(reportFileDirectories);
- List jacocoExecutionDataList = ReportUtils.listFiles(ETestArtifactFormat.JACOCO, reportFileDirectories);
- String reportFilePath = Paths.get(outputFolder, "testwise-coverage.json").toString();
-
- try (TestwiseCoverageReportWriter coverageWriter = new TestwiseCoverageReportWriter(testInfoFactory,
- new File(reportFilePath), splitAfter)) {
- for (File executionDataFile : jacocoExecutionDataList) {
- logger.info("Writing execution data for file: " + executionDataFile.getName());
- generator.convertAndConsume(executionDataFile, coverageWriter);
- }
- } catch (IOException e) {
- throw new RuntimeException(e);
- }
- }
-
- private void findSubprojectReportAndClassDirectories(List reportFiles,
- List classFiles) throws AgentOptionParseException {
-
- for (MavenProject subProject : session.getTopLevelProject().getCollectedProjects()) {
- String subprojectBuildDirectory = subProject.getBuild().getDirectory();
- reportFiles.add(Paths.get(subprojectBuildDirectory, "tia").toAbsolutePath().resolve("reports")
- .toFile());
- classFiles.addAll(getClassDirectoriesOrZips(subprojectBuildDirectory));
- }
- }
-
- private TestInfoFactory createTestInfoFactory(List reportFiles) throws MojoFailureException {
- try {
- List testDetails = ReportUtils.readObjects(ETestArtifactFormat.TEST_LIST, TestDetails[].class,
- reportFiles);
- List testExecutions = ReportUtils.readObjects(ETestArtifactFormat.TEST_EXECUTION,
- TestExecution[].class, reportFiles);
- logger.info("Writing report with " + testDetails.size() + " Details/" + testExecutions.size() + " Results");
- return new TestInfoFactory(testDetails, testExecutions);
- } catch (IOException e) {
- logger.error("Could not read test details from reports. Aborting.", e);
- throw new MojoFailureException(e);
- }
- }
-
- private JaCoCoTestwiseReportGenerator createJaCoCoTestwiseReportGenerator(List classFiles) {
- String includes = null;
- if (this.includes != null) {
- includes = String.join(":", this.includes);
- }
- String excludes = null;
- if (this.excludes != null) {
- excludes = String.join(":", this.excludes);
- }
- return new JaCoCoTestwiseReportGenerator(classFiles,
- new ClasspathWildcardIncludeFilter(includes, excludes), EDuplicateClassFileBehavior.WARN, logger);
- }
-
- private List getClassDirectoriesOrZips(String projectBuildDir) throws AgentOptionParseException {
- List classDirectoriesOrZips = new ArrayList<>();
- classDirectoriesOrZips.add(projectBuildDir);
- return ClasspathUtils.resolveClasspathTextFiles("classes", new FilePatternResolver(new CommandLineLogger()),
- classDirectoriesOrZips);
- }
-}
diff --git a/teamscale-maven-plugin/src/main/java/com/teamscale/maven/tia/TiaIntegrationTestMojo.java b/teamscale-maven-plugin/src/main/java/com/teamscale/maven/tia/TiaIntegrationTestMojo.java
deleted file mode 100644
index f280c0456..000000000
--- a/teamscale-maven-plugin/src/main/java/com/teamscale/maven/tia/TiaIntegrationTestMojo.java
+++ /dev/null
@@ -1,40 +0,0 @@
-package com.teamscale.maven.tia;
-
-import org.apache.maven.plugins.annotations.LifecyclePhase;
-import org.apache.maven.plugins.annotations.Mojo;
-import org.apache.maven.plugins.annotations.Parameter;
-import org.apache.maven.plugins.annotations.ResolutionScope;
-
-/**
- * Instruments the Failsafe integration tests and uploads testwise coverage to Teamscale.
- */
-@Mojo(name = "prepare-tia-integration-test", defaultPhase = LifecyclePhase.PACKAGE,
- requiresDependencyResolution = ResolutionScope.RUNTIME, threadSafe = true)
-public class TiaIntegrationTestMojo extends TiaMojoBase {
-
- /**
- * The partition to which to upload integration test coverage.
- */
- @Parameter(defaultValue = "Integration Tests")
- public String partition;
-
- @Override
- protected String getPartition() {
- return partition;
- }
-
- @Override
- protected boolean isIntegrationTest() {
- return true;
- }
-
- @Override
- protected String getTestPluginArtifact() {
- return "org.apache.maven.plugins:maven-failsafe-plugin";
- }
-
- @Override
- protected String getTestPluginPropertyPrefix() {
- return "failsafe";
- }
-}
diff --git a/teamscale-maven-plugin/src/main/java/com/teamscale/maven/tia/TiaMojoBase.java b/teamscale-maven-plugin/src/main/java/com/teamscale/maven/tia/TiaMojoBase.java
deleted file mode 100644
index 23be7bbf0..000000000
--- a/teamscale-maven-plugin/src/main/java/com/teamscale/maven/tia/TiaMojoBase.java
+++ /dev/null
@@ -1,447 +0,0 @@
-package com.teamscale.maven.tia;
-
-import com.teamscale.maven.TeamscaleMojoBase;
-import org.apache.commons.lang3.ArrayUtils;
-import org.apache.commons.lang3.StringUtils;
-import org.apache.logging.log4j.util.Strings;
-import org.apache.maven.artifact.Artifact;
-import org.apache.maven.model.Plugin;
-import org.apache.maven.model.PluginExecution;
-import org.apache.maven.plugin.MojoExecutionException;
-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 org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.net.ServerSocket;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.util.Collections;
-import java.util.Map;
-import java.util.Properties;
-
-/**
- * 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 impacted-test-engine, you must declare it as a test dependency. Example:
- *
- *
{@code
- *
- *
- * com.teamscale
- * impacted-test-engine
- * 30.0.0
- * test
- *
- *
- * }
- *
- * 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 TeamscaleMojoBase {
-
- /**
- * Name of the surefire/failsafe option to pass in
- * included
- * engines
- */
- private static final String INCLUDE_JUNIT5_ENGINES_OPTION = "includeJUnit5Engines";
-
- /**
- * Name of the surefire/failsafe option to pass in
- * excluded
- * engines
- */
- private static final String EXCLUDE_JUNIT5_ENGINES_OPTION = "excludeJUnit5Engines";
-
- /**
- * Impacted tests are calculated from "baselineCommit" to "commit". This sets the baseline.
- */
- @Parameter
- public String baselineCommit;
-
- /**
- * Impacted tests are calculated from "baselineCommit" to "commit".
- * The baselineRevision sets the baselineCommit with the help of a VCS revision (e.g. git SHA1) instead of a branch and timestamp
- */
- @Parameter
- public String baselineRevision;
-
- /**
- * 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. The default value 0 will tell the agent to
- * automatically search for an open port.
- */
- @Parameter(defaultValue = "0")
- 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;
-
- /**
- * Executes all tests, not only impacted ones if set. Defaults to false.
- */
- @Parameter(defaultValue = "false")
- public boolean runAllTests;
-
- /**
- * Executes only impacted tests, not all ones if set. Defaults to true.
- */
- @Parameter(defaultValue = "true")
- public boolean runImpacted;
-
- /**
- * Mode of producing testwise coverage.
- */
- @Parameter(defaultValue = "teamscale-upload")
- public String tiaMode;
-
- /**
- * 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;
-
- private Path targetDirectory;
-
- @Override
- public void execute() throws MojoFailureException, MojoExecutionException {
- super.execute();
-
- if (StringUtils.isNotEmpty(baselineCommit) && StringUtils.isNotEmpty(baselineRevision)) {
- getLog().warn("Both baselineRevision and baselineCommit are set but only one of them is needed. " +
- "The revision will be preferred in this case. If that's not intended, please do not set the baselineRevision manually.");
- }
-
- if (skip) {
- return;
- }
-
- Plugin testPlugin = getTestPlugin(getTestPluginArtifact());
- if (testPlugin != null) {
- configureTestPlugin();
- for (PluginExecution execution : testPlugin.getExecutions()) {
- validateTestPluginConfiguration(execution);
- }
- }
-
- targetDirectory = Paths.get(projectBuildDir, "tia").toAbsolutePath();
- createTargetDirectory();
-
- resolveCommitOrRevision();
-
- setTiaProperties();
-
- Path agentConfigFile = createAgentConfigFiles(agentPort);
- Path logFilePath = targetDirectory.resolve("agent.log");
- setArgLine(agentConfigFile, logFilePath);
- }
-
- private void setTiaProperties() {
- setTiaProperty("reportDirectory", targetDirectory.toString());
- setTiaProperty("server.url", teamscaleUrl);
- setTiaProperty("server.project", projectId);
- setTiaProperty("server.userName", username);
- setTiaProperty("server.userAccessToken", accessToken);
-
- if (StringUtils.isNotEmpty(resolvedRevision)) {
- setTiaProperty("endRevision", resolvedRevision);
- } else {
- setTiaProperty("endCommit", resolvedCommit);
- }
-
- if (StringUtils.isNotEmpty(baselineRevision)) {
- setTiaProperty("baselineRevision", baselineRevision);
- } else {
- setTiaProperty("baseline", baselineCommit);
- }
-
- setTiaProperty("repository", repository);
- setTiaProperty("partition", getPartition());
- if (agentPort.equals("0")) {
- agentPort = findAvailablePort();
- }
-
- setTiaProperty("agentsUrls", "http://localhost:" + agentPort);
- setTiaProperty("runImpacted", Boolean.valueOf(runImpacted).toString());
- setTiaProperty("runAllTests", Boolean.valueOf(runAllTests).toString());
- }
-
- /**
- * Automatically find an available port.
- */
- private String findAvailablePort() {
- try (ServerSocket socket = new ServerSocket(0)) {
- int port = socket.getLocalPort();
- getLog().info("Automatically set server port to " + port);
- return String.valueOf(port);
- } catch (IOException e) {
- getLog().error("Port blocked, trying again.", e);
- return findAvailablePort();
- }
- }
-
- /**
- * Sets the teamscale-test-impacted engine as only includedEngine and passes all previous engine configuration to
- * the impacted test engine instead.
- */
- private void configureTestPlugin() {
- enforcePropertyValue(INCLUDE_JUNIT5_ENGINES_OPTION, "includedEngines", "teamscale-test-impacted");
- enforcePropertyValue(EXCLUDE_JUNIT5_ENGINES_OPTION, "excludedEngines", "");
- }
-
- private void enforcePropertyValue(String engineOption, String impactedEngineSuffix,
- String newValue) {
- overrideProperty(engineOption, impactedEngineSuffix, newValue, session.getCurrentProject().getProperties());
- overrideProperty(engineOption, impactedEngineSuffix, newValue, session.getUserProperties());
- }
-
- private void overrideProperty(String engineOption, String impactedEngineSuffix, String newValue,
- Properties properties) {
- Object originalValue = properties.put(getPropertyName(engineOption), newValue);
- if (originalValue instanceof String && !Strings.isBlank((String) originalValue) && !newValue.equals(
- originalValue)) {
- setTiaProperty(impactedEngineSuffix, (String) originalValue);
- }
- }
-
- private void validateTestPluginConfiguration(PluginExecution execution) throws MojoFailureException {
- Xpp3Dom configurationDom = (Xpp3Dom) execution.getConfiguration();
- if (configurationDom == null) {
- return;
- }
-
- validateEngineNotConfigured(configurationDom, INCLUDE_JUNIT5_ENGINES_OPTION);
- validateEngineNotConfigured(configurationDom, EXCLUDE_JUNIT5_ENGINES_OPTION);
-
- validateParallelizationParameter(configurationDom, "threadCount");
- validateParallelizationParameter(configurationDom, "forkCount");
-
- Xpp3Dom parameterDom = configurationDom.getChild("reuseForks");
- if (parameterDom == null) {
- return;
- }
- String value = parameterDom.getValue();
- if (value != null && !value.equals("true")) {
- getLog().warn(
- "You configured surefire to not reuse forks." +
- " This has been shown to lead to performance decreases in combination with the Teamscale Maven Plugin." +
- " If you notice performance problems, please have a look at our troubleshooting section for possible solutions: https://docs.teamscale.com/howto/providing-testwise-coverage/#troubleshooting.");
- }
- }
-
- private void validateEngineNotConfigured(Xpp3Dom configurationDom,
- String xmlConfigurationName) throws MojoFailureException {
- Xpp3Dom engines = configurationDom.getChild(xmlConfigurationName);
- if (engines != null) {
- throw new MojoFailureException(
- "You configured JUnit 5 engines in the " + getTestPluginArtifact() + " plugin via the " + xmlConfigurationName + " configuration parameter." +
- " This is currently not supported when performing Test Impact analysis." +
- " Please add the " + xmlConfigurationName + " via the " + getPropertyName(
- xmlConfigurationName) + " property.");
- }
- }
-
- @NotNull
- private String getPropertyName(String xmlConfigurationName) {
- return getTestPluginPropertyPrefix() + "." + xmlConfigurationName;
- }
-
- @Nullable
- private Plugin getTestPlugin(String testPluginArtifact) {
- Map plugins = session.getCurrentProject().getModel().getBuild().getPluginsAsMap();
- return plugins.get(testPluginArtifact);
- }
-
- private void validateParallelizationParameter(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 " + getTestPluginArtifact() + " 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 The prefix of the properties that are used to pass parameters to the plugin. */
- protected abstract String getTestPluginPropertyPrefix();
-
- /**
- * @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(String agentPort) 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-at-port-" + agentPort + ".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=" + tiaMode +
- "\nteamscale-server-url=" + teamscaleUrl +
- "\nteamscale-project=" + projectId +
- "\nteamscale-user=" + username +
- "\nteamscale-access-token=" + accessToken +
- "\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);
- }
- if (StringUtils.isNotBlank(repository)) {
- config += "\nteamscale-repository=" + repository;
- }
-
- if (StringUtils.isNotEmpty(resolvedRevision)) {
- config += "\nteamscale-revision=" + resolvedRevision;
- } else {
- config += "\nteamscale-commit=" + resolvedCommit;
- }
- return config;
- }
-
- private Path findAgentJarFile() {
- Artifact agentArtifact = pluginArtifactMap.get("com.teamscale:teamscale-jacoco-agent");
- return agentArtifact.getFile().toPath();
- }
-
- /**
- * 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) {
- if (value != null) {
- 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);
- }
- }
-}
diff --git a/teamscale-maven-plugin/src/main/java/com/teamscale/maven/tia/TiaUnitTestMojo.java b/teamscale-maven-plugin/src/main/java/com/teamscale/maven/tia/TiaUnitTestMojo.java
deleted file mode 100644
index e43bf160f..000000000
--- a/teamscale-maven-plugin/src/main/java/com/teamscale/maven/tia/TiaUnitTestMojo.java
+++ /dev/null
@@ -1,40 +0,0 @@
-package com.teamscale.maven.tia;
-
-import org.apache.maven.plugins.annotations.LifecyclePhase;
-import org.apache.maven.plugins.annotations.Mojo;
-import org.apache.maven.plugins.annotations.Parameter;
-import org.apache.maven.plugins.annotations.ResolutionScope;
-
-/**
- * Instruments the Surefire unit tests and uploads testwise coverage to Teamscale.
- */
-@Mojo(name = "prepare-tia-unit-test", defaultPhase = LifecyclePhase.INITIALIZE, requiresDependencyResolution = ResolutionScope.RUNTIME,
- threadSafe = true)
-public class TiaUnitTestMojo extends TiaMojoBase {
-
- /**
- * The partition to which to upload unit test coverage.
- */
- @Parameter(defaultValue = "Unit Tests")
- public String partition;
-
- @Override
- protected String getPartition() {
- return partition;
- }
-
- @Override
- protected boolean isIntegrationTest() {
- return false;
- }
-
- @Override
- protected String getTestPluginArtifact() {
- return "org.apache.maven.plugins:maven-surefire-plugin";
- }
-
- @Override
- protected String getTestPluginPropertyPrefix() {
- return "surefire";
- }
-}
diff --git a/teamscale-maven-plugin/src/main/java/com/teamscale/maven/upload/CoverageUploadMojo.java b/teamscale-maven-plugin/src/main/java/com/teamscale/maven/upload/CoverageUploadMojo.java
deleted file mode 100644
index 915e411a5..000000000
--- a/teamscale-maven-plugin/src/main/java/com/teamscale/maven/upload/CoverageUploadMojo.java
+++ /dev/null
@@ -1,270 +0,0 @@
-package com.teamscale.maven.upload;
-
-import com.google.common.base.Strings;
-import com.teamscale.maven.TeamscaleMojoBase;
-import org.apache.maven.plugin.MojoExecutionException;
-import org.apache.maven.plugin.MojoFailureException;
-import org.apache.maven.plugins.annotations.LifecyclePhase;
-import org.apache.maven.plugins.annotations.Mojo;
-import org.apache.maven.plugins.annotations.Parameter;
-import org.apache.maven.plugins.annotations.ResolutionScope;
-import org.apache.maven.project.MavenProject;
-import org.codehaus.plexus.util.xml.Xpp3Dom;
-import shadow.com.teamscale.client.CommitDescriptor;
-import shadow.com.teamscale.client.EReportFormat;
-import shadow.com.teamscale.client.TeamscaleClient;
-
-import java.io.File;
-import java.io.IOException;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-import java.util.Optional;
-import java.util.stream.Collectors;
-
-/**
- * Run this goal after the Jacoco report generation to upload them to a
- * configured Teamscale instance. The configuration can be specified in the root
- * Maven project. Offers the following functionality:
- *
- * - Validate Jacoco Maven plugin configuration
- * - Locate and upload all reports in one session
- *
- *
- * @see Jacoco
- * Plugin
- */
-@Mojo(name = "upload-coverage", defaultPhase = LifecyclePhase.VERIFY, requiresDependencyResolution = ResolutionScope.RUNTIME,
- threadSafe = true)
-public class CoverageUploadMojo extends TeamscaleMojoBase {
-
- private static final String JACOCO_PLUGIN_NAME = "org.jacoco:jacoco-maven-plugin";
-
- private static final String COVERAGE_UPLOAD_MESSAGE = "Coverage upload via Teamscale Maven plugin";
-
- /**
- * The Teamscale partition name to which unit test reports will be uploaded.
- */
- @Parameter(property = "teamscale.unitTestPartition", defaultValue = "Unit Tests")
- public String unitTestPartition;
-
- /**
- * The Teamscale partition name to which integration test reports will be
- * uploaded.
- */
- @Parameter(property = "teamscale.integrationTestPartition", defaultValue = "Integration Tests")
- public String integrationTestPartition;
-
- /**
- * The Teamscale partition name to which aggregated test reports will be
- * uploaded.
- */
- @Parameter(property = "teamscale.aggregatedTestPartition", defaultValue = "Aggregated Tests")
- public String aggregatedTestPartition;
-
- /**
- * The output directory of the testwise coverage reports. Should only be set if
- * testwise coverage is uploaded.
- */
- @Parameter()
- public String testwiseCoverageOutputFolder;
-
- /**
- * The Teamscale partition name to which testwise coverage reports will be
- * uploaded.
- */
- @Parameter(property = "teamscale.testwisePartition", defaultValue = "Testwise Coverage")
- public String testwisePartition;
-
- /**
- * Paths to all reports generated by subprojects
- *
- * @see report
- */
- private final List reportGoalOutputFiles = new ArrayList<>();
-
- /**
- * Paths to all integration reports generated by subprojects
- *
- * @see report-integration
- */
- private final List reportIntegrationGoalOutputFiles = new ArrayList<>();
-
- /**
- * The project build directory (usually: {@code ./target}). Provided
- * automatically by Maven.
- */
- @Parameter(defaultValue = "${project.build.directory}")
- public String projectBuildDir;
-
- /**
- * Paths to all aggregated reports generated by subprojects
- *
- * @see report-aggregate
- */
- private final List reportAggregateGoalOutputFiles = new ArrayList<>();
-
- /**
- * The Teamscale client that is used to upload reports to a Teamscale instance.
- */
- private TeamscaleClient teamscaleClient;
-
- @Override
- public void execute() throws MojoFailureException, MojoExecutionException {
- super.execute();
- if (skip) {
- getLog().debug("Skipping since skip is set to true");
- return;
- }
- if (!session.getCurrentProject().equals(session.getTopLevelProject())) {
- getLog().debug("Skipping since upload should only happen in top project");
- return;
- }
- teamscaleClient = new TeamscaleClient(teamscaleUrl, username, accessToken, projectId);
- getLog().debug("Resolving end commit");
- resolveCommitOrRevision();
- getLog().debug("Parsing Jacoco plugin configurations");
- parseJacocoConfiguration();
- try {
- getLog().debug("Uploading coverage reports");
- uploadCoverageReports();
- } catch (IOException e) {
- throw new MojoFailureException(
- "Uploading coverage reports failed. No upload to Teamscale was performed. You can try again or upload the XML coverage reports manually, see https://docs.teamscale.com/reference/ui/project/project/#manual-report-upload",
- e);
- }
- }
-
- /**
- * Check that Jacoco is set up correctly and read any custom settings that may
- * have been set
- *
- * @throws MojoFailureException
- * If Jacoco is not set up correctly
- */
- private void parseJacocoConfiguration() throws MojoFailureException {
- collectReportOutputDirectory(session.getTopLevelProject(), "report", "jacoco", reportGoalOutputFiles);
- collectReportOutputDirectory(session.getTopLevelProject(), "report-integration", "jacoco-it",
- reportIntegrationGoalOutputFiles);
- collectReportOutputDirectory(session.getTopLevelProject(), "report-aggregate", "jacoco-aggregate",
- reportAggregateGoalOutputFiles);
- getLog().debug(
- String.format("Found %d sub-modules", session.getTopLevelProject().getCollectedProjects().size()));
- for (MavenProject subProject : session.getTopLevelProject().getCollectedProjects()) {
- collectReportOutputDirectory(subProject, "report", "jacoco", reportGoalOutputFiles);
- collectReportOutputDirectory(subProject, "report-integration", "jacoco-it",
- reportIntegrationGoalOutputFiles);
- collectReportOutputDirectory(subProject, "report-aggregate", "jacoco-aggregate",
- reportAggregateGoalOutputFiles);
- }
- }
-
- /**
- * Collect the file locations in which JaCoCo is configured to save the XML
- * coverage reports
- *
- * @param project
- * The project
- * @param reportGoal
- * The JaCoCo report goal
- * @param jacocoDirectory
- * The name of the directory, matching the JaCoCo goal
- * @see Goals
- */
- private void collectReportOutputDirectory(MavenProject project, String reportGoal, String jacocoDirectory,
- List reportOutputFiles) throws MojoFailureException {
- Path defaultOutputDirectory = Paths.get(project.getReporting().getOutputDirectory());
- // If a Dom is null it means the execution goal uses default parameters which work correctly
- Xpp3Dom reportConfigurationDom = getJacocoGoalExecutionConfiguration(project, reportGoal);
- String errorMessage = "Skipping upload for %s as %s is not configured to produce XML reports for goal %s. See https://www.jacoco.org/jacoco/trunk/doc/report-mojo.html#formats";
- if (!validateReportFormat(reportConfigurationDom)) {
- throw new MojoFailureException(
- String.format(errorMessage, project.getName(), JACOCO_PLUGIN_NAME, jacocoDirectory));
- }
- Path resolvedCoverageFile = getCustomOutputDirectory(reportConfigurationDom).orElse(defaultOutputDirectory)
- .resolve(jacocoDirectory).resolve("jacoco.xml");
- getLog().debug(String.format("Adding possible report location: %s", resolvedCoverageFile));
- reportOutputFiles.add(resolvedCoverageFile);
- }
-
- private void uploadCoverageReports() throws IOException {
- Path reportPath;
- if (Strings.isNullOrEmpty(testwiseCoverageOutputFolder)) {
- reportPath = Paths.get(projectBuildDir, "tia", "reports");
- } else {
- reportPath = Paths.get(testwiseCoverageOutputFolder);
- }
- File[] files = reportPath.toFile().listFiles(File::isFile);
- if (files != null) {
- List testwiseCoverageFiles = Arrays.asList(files);
- getLog().debug("Uploading testwise coverage to partition " + testwisePartition);
- uploadCoverage(testwiseCoverageFiles.stream().map(File::toPath).collect(Collectors.toList()),
- testwisePartition, EReportFormat.TESTWISE_COVERAGE);
-
- }
- uploadCoverage(reportGoalOutputFiles, unitTestPartition, EReportFormat.JACOCO);
- uploadCoverage(reportIntegrationGoalOutputFiles, integrationTestPartition, EReportFormat.JACOCO);
- uploadCoverage(reportAggregateGoalOutputFiles, aggregatedTestPartition, EReportFormat.JACOCO);
- }
-
- private void uploadCoverage(List reportOutputFiles, String partition, EReportFormat format)
- throws IOException {
- List reports = new ArrayList<>();
- getLog().debug(String.format("Scanning through %d locations for %s...", reportOutputFiles.size(), partition));
- for (Path reportPath : reportOutputFiles) {
- File report = reportPath.toFile();
- if (!report.exists()) {
- getLog().debug(String.format("Cannot find %s, skipping...", report.getAbsolutePath()));
- continue;
- }
- if (!report.canRead()) {
- getLog().warn(String.format("Cannot read %s, skipping!", report.getAbsolutePath()));
- continue;
- }
- reports.add(report);
- }
- if (!reports.isEmpty()) {
- getLog().info(
- String.format("Uploading %d report for project %s to %s", reports.size(), projectId,
- partition));
- teamscaleClient.uploadReports(format, reports, CommitDescriptor.parse(resolvedCommit), revision, repository,
- partition, COVERAGE_UPLOAD_MESSAGE);
- } else {
- getLog().info(String.format("Found no valid reports for %s", partition));
- }
- }
-
- /**
- * Validates that a configuration Dom is set up to generate XML reports
- *
- * @param configurationDom
- * The configuration Dom of a goal execution
- */
- private boolean validateReportFormat(Xpp3Dom configurationDom) {
- if (configurationDom == null || configurationDom.getChild("formats") == null) {
- return true;
- }
- for (Xpp3Dom format : configurationDom.getChild("formats").getChildren()) {
- if (format.getValue().equals("XML")) {
- return true;
- }
- }
- return false;
- }
-
- private Optional getCustomOutputDirectory(Xpp3Dom configurationDom) {
- if (configurationDom != null && configurationDom.getChild("outputDirectory") != null) {
- return Optional.of(Paths.get(configurationDom.getChild("outputDirectory").getValue()));
- }
- return Optional.empty();
- }
-
- private Xpp3Dom getJacocoGoalExecutionConfiguration(MavenProject project, String pluginGoal) {
- return super.getExecutionConfigurationDom(project, JACOCO_PLUGIN_NAME, pluginGoal);
- }
-}
diff --git a/teamscale-maven-plugin/src/main/kotlin/com/teamscale/maven/GitCommitUtils.kt b/teamscale-maven-plugin/src/main/kotlin/com/teamscale/maven/GitCommitUtils.kt
new file mode 100644
index 000000000..64217ead7
--- /dev/null
+++ b/teamscale-maven-plugin/src/main/kotlin/com/teamscale/maven/GitCommitUtils.kt
@@ -0,0 +1,45 @@
+package com.teamscale.maven
+
+import org.eclipse.jgit.api.Git
+import org.eclipse.jgit.lib.Repository
+import java.io.IOException
+import java.nio.file.Files
+import java.nio.file.Path
+import kotlin.io.path.exists
+
+object GitCommitUtils {
+ private const val HEAD_REF = "HEAD"
+
+ /**
+ * Determines the current HEAD commit in the Git repository located in the or above the given search directory.
+ *
+ * @throws IOException if reading from the Git repository fails or the current directory is not a Git repository.
+ */
+ @JvmStatic
+ @Throws(IOException::class)
+ fun getGitHeadRevision(searchDirectory: Path): String {
+ val gitDirectory = findGitDirectory(searchDirectory)
+ ?: throw IOException("Could not find git directory in $searchDirectory")
+ return getHeadCommitFromGitDirectory(gitDirectory)
+ }
+
+ /**
+ * Retrieves the HEAD commit hash from the given Git directory.
+ *
+ * @param gitDirectory the base directory of the Git repository.
+ * @return the hash of the HEAD commit.
+ */
+ private fun getHeadCommitFromGitDirectory(gitDirectory: Path): String {
+ Git.open(gitDirectory.toFile()).use { git ->
+ return git.repository.refDatabase.findRef(HEAD_REF).objectId.name
+ }
+ }
+
+ /**
+ * Traverses the directory tree upwards until it finds a .git directory.
+ * Returns null if no .git directory is found.
+ */
+ private fun findGitDirectory(searchDirectory: Path?) =
+ generateSequence(searchDirectory) { it.parent }
+ .firstOrNull { it.resolve(".git").exists() }
+}
diff --git a/teamscale-maven-plugin/src/main/kotlin/com/teamscale/maven/TeamscaleMojoBase.kt b/teamscale-maven-plugin/src/main/kotlin/com/teamscale/maven/TeamscaleMojoBase.kt
new file mode 100644
index 000000000..a670d2204
--- /dev/null
+++ b/teamscale-maven-plugin/src/main/kotlin/com/teamscale/maven/TeamscaleMojoBase.kt
@@ -0,0 +1,148 @@
+package com.teamscale.maven
+
+import org.apache.maven.execution.MavenSession
+import org.apache.maven.plugin.AbstractMojo
+import org.apache.maven.plugin.MojoExecutionException
+import org.apache.maven.plugin.MojoFailureException
+import org.apache.maven.plugins.annotations.Parameter
+import org.apache.maven.project.MavenProject
+import org.codehaus.plexus.util.xml.Xpp3Dom
+import java.io.IOException
+
+/**
+ * A base class for all Teamscale-related Maven Mojos. Offers basic attributes and functionality related to Teamscale
+ * and Maven.
+ */
+abstract class TeamscaleMojoBase : AbstractMojo() {
+
+ /**
+ * The URL of the Teamscale instance to which the recorded coverage will be uploaded.
+ */
+ @Parameter
+ var teamscaleUrl: String? = null
+
+ /**
+ * The Teamscale project to which the recorded coverage will be uploaded
+ */
+ @Parameter
+ var projectId: String? = null
+
+ /**
+ * The username to use to perform the upload. Must have the "Upload external data" permission for the [projectId].
+ * Can also be specified via the Maven property `teamscale.username`.
+ */
+ @Parameter(property = "teamscale.username")
+ lateinit var username: String
+
+ /**
+ * Teamscale access token of the [username]. Can also be specified via the Maven property `teamscale.accessToken`.
+ */
+ @Parameter(property = "teamscale.accessToken")
+ lateinit var accessToken: String
+
+ /**
+ * You can optionally use this property to override the code commit to which the coverage will be uploaded. Format:
+ * `BRANCH:UNIX_EPOCH_TIMESTAMP_IN_MILLISECONDS`
+ *
+ * If no commit and revision is manually specified, the plugin will try to determine the currently checked-out Git
+ * commit. You should specify either commit or revision, not both. If both are specified, a warning is logged and
+ * the revision takes precedence.
+ */
+ @Parameter(property = "teamscale.commit")
+ lateinit var commit: String
+
+ /**
+ * You can optionally use this property to override the revision to which the coverage will be uploaded. If no
+ * commit and revision is manually specified, the plugin will try to determine the current git revision. You should
+ * specify either commit or revision, not both. If both are specified, a warning is logged and the revision takes
+ * precedence.
+ */
+ @Parameter(property = "teamscale.revision")
+ lateinit var revision: String
+
+ /**
+ * The repository id in your Teamscale project which Teamscale should use to look up the revision, if given. Null or
+ * empty will lead to a lookup in all repositories in the Teamscale project.
+ */
+ @Parameter(property = "teamscale.repository")
+ lateinit var repository: String
+
+ /**
+ * Whether to skip the execution of this Mojo.
+ */
+ @Parameter(defaultValue = "false")
+ var skip: Boolean = false
+
+ /**
+ * The running Maven session. Provided automatically by Maven.
+ */
+ @Parameter(defaultValue = "\${session}")
+ lateinit var session: MavenSession
+
+ /**
+ * The resolved commit, either provided by the user or determined via the GitCommit class
+ */
+ @JvmField
+ protected var resolvedCommit: String? = null
+
+ /**
+ * The resolved revision, either provided by the user or determined via the GitCommit class
+ */
+ @JvmField
+ protected var resolvedRevision: String? = null
+
+ @Throws(MojoExecutionException::class, MojoFailureException::class)
+ override fun execute() {
+ if (revision.isNotBlank() && commit.isNotBlank()) {
+ log.warn(
+ "Both revision and commit are set but only one of them is needed. " +
+ "Teamscale will prefer the revision. If that's not intended, please do not set the revision manually."
+ )
+ }
+ }
+
+ /**
+ * Sets the `resolvedRevision` or `resolvedCommit`. If not provided, try to determine the
+ * revision via the GitCommit class.
+ *
+ * @see GitCommitUtils
+ */
+ @Throws(MojoFailureException::class)
+ protected fun resolveCommitOrRevision() {
+ when {
+ revision.isNotBlank() -> {
+ resolvedRevision = revision
+ }
+ commit.isNotBlank() -> {
+ resolvedCommit = commit
+ }
+ else -> {
+ val basedir = session.currentProject.basedir.toPath()
+ try {
+ resolvedRevision = GitCommitUtils.getGitHeadRevision(basedir)
+ } catch (e: IOException) {
+ throw MojoFailureException(
+ "There is no or configured in the pom.xml" +
+ " and it was not possible to determine the current revision in $basedir from Git", e
+ )
+ }
+ }
+ }
+ }
+
+ /**
+ * Retrieves the configuration of a goal execution for the given plugin
+ *
+ * @receiver The maven project
+ * @param pluginArtifact The id of the plugin
+ * @param pluginGoal The name of the goal
+ * @return The configuration DOM if present, otherwise `null`
+ */
+ protected fun MavenProject.getExecutionConfigurationDom(
+ pluginArtifact: String,
+ pluginGoal: String
+ ) = getPlugin(pluginArtifact)
+ ?.executions
+ ?.firstOrNull { it.goals.contains(pluginGoal) }
+ ?.configuration as? Xpp3Dom
+}
diff --git a/teamscale-maven-plugin/src/main/kotlin/com/teamscale/maven/tia/ArgLine.kt b/teamscale-maven-plugin/src/main/kotlin/com/teamscale/maven/tia/ArgLine.kt
new file mode 100644
index 000000000..fe6ed86ae
--- /dev/null
+++ b/teamscale-maven-plugin/src/main/kotlin/com/teamscale/maven/tia/ArgLine.kt
@@ -0,0 +1,127 @@
+package com.teamscale.maven.tia
+
+import org.apache.commons.lang3.ArrayUtils
+import org.apache.commons.lang3.StringUtils
+import org.apache.maven.execution.MavenSession
+import org.apache.maven.model.Plugin
+import org.apache.maven.plugin.logging.Log
+import org.apache.maven.project.MavenProject
+import java.nio.file.Path
+import java.util.*
+import java.util.stream.Collectors
+
+/**
+ * Composes a new argLine based on the current one and input about the desired agent configuration.
+ */
+class ArgLine(
+ private val additionalAgentOptions: Array?,
+ private val agentLogLevel: String,
+ private val agentJarFile: Path,
+ private val agentConfigFile: Path,
+ private val logFilePath: Path
+) {
+ /**
+ * Takes the given old argLine, removes any previous invocation of our agent and prepends a new one based on the
+ * constructor parameters of this class. Preserves all other options in the old argLine.
+ */
+ fun prependTo(oldArgLine: String?): String {
+ val jvmOptions = createJvmOptions()
+ if (oldArgLine.isNullOrBlank()) {
+ return jvmOptions
+ }
+
+ return "$jvmOptions $oldArgLine"
+ }
+
+ private fun createJvmOptions() =
+ listOf(
+ "-Dteamscale.markstart",
+ createJavaAgentArgument(),
+ "-DTEAMSCALE_AGENT_LOG_FILE=$logFilePath",
+ "-DTEAMSCALE_AGENT_LOG_LEVEL=$agentLogLevel",
+ "-Dteamscale.markend"
+ ).joinToString(" ") { quoteCommandLineOptionIfNecessary(it) }
+
+ private fun createJavaAgentArgument(): String {
+ val agentOptions = mutableListOf().apply {
+ add("config-file=" + agentConfigFile.toAbsolutePath())
+ addAll(listOf(*ArrayUtils.nullToEmpty(additionalAgentOptions)))
+ }
+ return "-javaagent:${agentJarFile.toAbsolutePath()}=${agentOptions.joinToString(",")}"
+ }
+
+ companion object {
+ /** Applies the given [ArgLine] to the given [MavenSession]. */
+ fun applyToMavenProject(
+ argLine: ArgLine,
+ session: MavenSession,
+ log: Log,
+ userDefinedPropertyName: String,
+ isIntegrationTest: Boolean
+ ) {
+ val mavenProject = session.currentProject
+ val effectiveProperty = getEffectiveProperty(
+ userDefinedPropertyName, mavenProject, isIntegrationTest
+ )
+
+ val oldArgLine = effectiveProperty.getValue(session)
+ val newArgLine = argLine.prependTo(oldArgLine)
+
+ effectiveProperty.setValue(session, newArgLine)
+ log.info("${effectiveProperty.propertyName} set to $newArgLine")
+ }
+
+ /**
+ * Removes any occurrences of our agent from all [ArgLineProperty.STANDARD_PROPERTIES].
+ */
+ fun cleanOldArgLines(session: MavenSession, log: Log) {
+ ArgLineProperty.STANDARD_PROPERTIES.forEach { property ->
+ val oldArgLine = property.getValue(session)
+ if (oldArgLine.isBlank()) return@forEach
+
+ val newArgLine = removePreviousTiaAgent(oldArgLine)
+ if (oldArgLine != newArgLine) {
+ log.info("Removed agent from property ${property.propertyName}")
+ property.setValue(session, newArgLine)
+ }
+ }
+ }
+
+ private fun quoteCommandLineOptionIfNecessary(option: String) =
+ if (StringUtils.containsWhitespace(option)) "'$option'" else option
+
+ /**
+ * Determines the property in which to set the argLine. By default, this is the property used by the testing
+ * framework of the current project's packaging. The user may override this by providing their own property name.
+ */
+ private fun getEffectiveProperty(
+ userDefinedPropertyName: String,
+ mavenProject: MavenProject,
+ isIntegrationTest: Boolean
+ ): ArgLineProperty {
+ if (userDefinedPropertyName.isNotBlank()) {
+ return ArgLineProperty.projectProperty(userDefinedPropertyName)
+ }
+
+ if (isIntegrationTest && hasSpringBootPluginEnabled(mavenProject)) {
+ return ArgLineProperty.SPRING_BOOT_ARG_LINE
+ }
+
+ if ("eclipse-test-plugin" == mavenProject.packaging) {
+ return ArgLineProperty.TYCHO_ARG_LINE
+ }
+ return ArgLineProperty.SUREFIRE_ARG_LINE
+ }
+
+ private fun hasSpringBootPluginEnabled(mavenProject: MavenProject) =
+ mavenProject.buildPlugins.any { it.artifactId == "spring-boot-maven-plugin" }
+
+ /**
+ * Removes any previous invocation of our agent from the given argLine. This is necessary in case we want to
+ * instrument unit and integration tests but with different arguments.
+ */
+ @JvmStatic
+ fun removePreviousTiaAgent(argLine: String?) =
+ argLine?.replace("-Dteamscale.markstart.*teamscale.markend".toRegex(), "") ?: ""
+ }
+}
diff --git a/teamscale-maven-plugin/src/main/kotlin/com/teamscale/maven/tia/ArgLineProperty.kt b/teamscale-maven-plugin/src/main/kotlin/com/teamscale/maven/tia/ArgLineProperty.kt
new file mode 100644
index 000000000..723167901
--- /dev/null
+++ b/teamscale-maven-plugin/src/main/kotlin/com/teamscale/maven/tia/ArgLineProperty.kt
@@ -0,0 +1,57 @@
+package com.teamscale.maven.tia
+
+import org.apache.maven.execution.MavenSession
+import org.apache.maven.project.MavenProject
+import java.util.*
+import java.util.function.Function
+
+/** Accessor for different types of properties, e.g. project or user properties, in a [MavenSession]. */
+class ArgLineProperty private constructor(
+ /** The name of the property. */
+ val propertyName: String,
+ private val propertiesAccess: (MavenSession) -> Properties
+) {
+ /** Returns the value of this property in the given Maven session */
+ fun getValue(session: MavenSession): String =
+ propertiesAccess(session).getProperty(propertyName)
+
+ /** Sets the value of this property in the given Maven session */
+ fun setValue(session: MavenSession, value: String?) {
+ propertiesAccess(session).setProperty(propertyName, value)
+ }
+
+ companion object {
+ /**
+ * Name of the property used in the maven-osgi-test-plugin.
+ */
+ val TYCHO_ARG_LINE: ArgLineProperty = projectProperty("tycho.testArgLine")
+
+ /**
+ * Name of the property used in the maven-surefire-plugin.
+ */
+ val SUREFIRE_ARG_LINE: ArgLineProperty = projectProperty("argLine")
+
+ /**
+ * Name of the property used in the spring-boot-maven-plugin start goal.
+ */
+ val SPRING_BOOT_ARG_LINE: ArgLineProperty = userProperty("spring-boot.run.jvmArguments")
+
+ /** The standard properties that this plugin might modify. */
+ val STANDARD_PROPERTIES: Array =
+ arrayOf(TYCHO_ARG_LINE, SUREFIRE_ARG_LINE, SPRING_BOOT_ARG_LINE)
+
+ private fun getProjectProperties(session: MavenSession) =
+ session.currentProject.properties
+
+ private fun getUserProperties(session: MavenSession) =
+ session.userProperties
+
+ /** Creates a project property ([MavenProject.getProperties]). */
+ fun projectProperty(name: String) =
+ ArgLineProperty(name) { getProjectProperties(it) }
+
+ /** Creates a user property ([MavenSession.getUserProperties]). */
+ fun userProperty(name: String) =
+ ArgLineProperty(name) { getUserProperties(it) }
+ }
+}
diff --git a/teamscale-maven-plugin/src/main/kotlin/com/teamscale/maven/tia/TiaCoverageConvertMojo.kt b/teamscale-maven-plugin/src/main/kotlin/com/teamscale/maven/tia/TiaCoverageConvertMojo.kt
new file mode 100644
index 000000000..0ebf42d8e
--- /dev/null
+++ b/teamscale-maven-plugin/src/main/kotlin/com/teamscale/maven/tia/TiaCoverageConvertMojo.kt
@@ -0,0 +1,167 @@
+package com.teamscale.maven.tia
+
+import com.teamscale.jacoco.agent.options.AgentOptionParseException
+import com.teamscale.jacoco.agent.options.ClasspathUtils
+import com.teamscale.jacoco.agent.options.FilePatternResolver
+import org.apache.maven.execution.MavenSession
+import org.apache.maven.plugin.AbstractMojo
+import org.apache.maven.plugin.MojoFailureException
+import org.apache.maven.plugins.annotations.LifecyclePhase
+import org.apache.maven.plugins.annotations.Mojo
+import org.apache.maven.plugins.annotations.Parameter
+import org.apache.maven.plugins.annotations.ResolutionScope
+import shadow.com.teamscale.client.TestDetails
+import shadow.com.teamscale.report.EDuplicateClassFileBehavior
+import shadow.com.teamscale.report.ReportUtils
+import shadow.com.teamscale.report.testwise.ETestArtifactFormat
+import shadow.com.teamscale.report.testwise.TestwiseCoverageReportWriter
+import shadow.com.teamscale.report.testwise.jacoco.JaCoCoTestwiseReportGenerator
+import shadow.com.teamscale.report.testwise.model.TestExecution
+import shadow.com.teamscale.report.testwise.model.factory.TestInfoFactory
+import shadow.com.teamscale.report.util.ClasspathWildcardIncludeFilter
+import shadow.com.teamscale.report.util.CommandLineLogger
+import java.io.File
+import java.io.IOException
+import java.nio.file.Files
+import java.nio.file.Paths
+
+/**
+ * Batch converts all created .exec file reports into a testwise coverage
+ * report.
+ */
+@Mojo(
+ name = "testwise-coverage-converter",
+ defaultPhase = LifecyclePhase.VERIFY,
+ requiresDependencyResolution = ResolutionScope.RUNTIME
+)
+class TiaCoverageConvertMojo : AbstractMojo() {
+ /**
+ * Wildcard include patterns to apply during JaCoCo's traversal of class files.
+ */
+ @Parameter(defaultValue = "**")
+ lateinit var includes: Array
+
+ /**
+ * Wildcard exclude patterns to apply during JaCoCo's traversal of class files.
+ */
+ @Parameter
+ lateinit var excludes: Array
+
+ /**
+ * After how many tests the testwise coverage should be split into multiple
+ * reports (Default is 5000).
+ */
+ @Parameter(defaultValue = "5000")
+ var splitAfter: Int = 5000
+
+ /**
+ * The project build directory (usually: `./target`). Provided
+ * automatically by Maven.
+ */
+ @Parameter(defaultValue = "\${project.build.directory}")
+ lateinit var projectBuildDir: String
+
+ /**
+ * The output directory of the testwise coverage reports.
+ */
+ @Parameter
+ lateinit var outputFolder: String
+
+ /**
+ * The running Maven session. Provided automatically by Maven.
+ */
+ @Parameter(defaultValue = "\${session}")
+ lateinit var session: MavenSession
+ private val logger = CommandLineLogger()
+
+ @Throws(MojoFailureException::class)
+ override fun execute() {
+ val reportFileDirectories = mutableListOf()
+ reportFileDirectories.add(Paths.get(projectBuildDir, "tia").toAbsolutePath().resolve("reports").toFile())
+ if (outputFolder.isBlank()) {
+ outputFolder = Paths.get(projectBuildDir, "tia", "reports").toString()
+ }
+ val classFileDirectories: MutableList
+ try {
+ Files.createDirectories(Paths.get(outputFolder))
+ classFileDirectories = getClassDirectoriesOrZips(projectBuildDir)
+ findSubprojectReportAndClassDirectories(reportFileDirectories, classFileDirectories)
+ } catch (e: IOException) {
+ logger.error("Could not create testwise report generator. Aborting.", e)
+ throw MojoFailureException(e)
+ } catch (e: AgentOptionParseException) {
+ logger.error("Could not create testwise report generator. Aborting.", e)
+ throw MojoFailureException(e)
+ }
+ logger.info("Generating the testwise coverage report")
+ val generator = createJaCoCoTestwiseReportGenerator(classFileDirectories)
+ val testInfoFactory = createTestInfoFactory(reportFileDirectories)
+ val jacocoExecutionDataList = ReportUtils.listFiles(ETestArtifactFormat.JACOCO, reportFileDirectories)
+ val reportFilePath = Paths.get(outputFolder, "testwise-coverage.json").toString()
+
+ try {
+ TestwiseCoverageReportWriter(
+ testInfoFactory, File(reportFilePath), splitAfter
+ ).use { coverageWriter ->
+ jacocoExecutionDataList.forEach { executionDataFile ->
+ logger.info("Writing execution data for file: ${executionDataFile.name}")
+ generator.convertAndConsume(executionDataFile, coverageWriter)
+ }
+ }
+ } catch (e: IOException) {
+ throw RuntimeException(e)
+ }
+ }
+
+ @Throws(AgentOptionParseException::class)
+ private fun findSubprojectReportAndClassDirectories(
+ reportFiles: MutableList,
+ classFiles: MutableList
+ ) {
+ session.topLevelProject.collectedProjects.forEach { subProject ->
+ val subprojectBuildDirectory = subProject.build.directory
+ reportFiles.add(
+ Paths.get(subprojectBuildDirectory, "tia").toAbsolutePath().resolve("reports").toFile()
+ )
+ classFiles.addAll(getClassDirectoriesOrZips(subprojectBuildDirectory))
+ }
+ }
+
+ @Throws(MojoFailureException::class)
+ private fun createTestInfoFactory(reportFiles: List): TestInfoFactory {
+ try {
+ val testDetails = ReportUtils.readObjects(
+ ETestArtifactFormat.TEST_LIST,
+ Array::class.java,
+ reportFiles
+ )
+ val testExecutions = ReportUtils.readObjects(
+ ETestArtifactFormat.TEST_EXECUTION,
+ Array::class.java,
+ reportFiles
+ )
+ logger.info("Writing report with ${testDetails.size} Details/${testExecutions.size} Results")
+ return TestInfoFactory(testDetails, testExecutions)
+ } catch (e: IOException) {
+ logger.error("Could not read test details from reports. Aborting.", e)
+ throw MojoFailureException(e)
+ }
+ }
+
+ private fun createJaCoCoTestwiseReportGenerator(classFiles: List) =
+ JaCoCoTestwiseReportGenerator(
+ classFiles,
+ ClasspathWildcardIncludeFilter(
+ includes.joinToString(":"),
+ excludes.joinToString(":")
+ ), EDuplicateClassFileBehavior.WARN, logger
+ )
+
+ @Throws(AgentOptionParseException::class)
+ private fun getClassDirectoriesOrZips(projectBuildDir: String) =
+ ClasspathUtils.resolveClasspathTextFiles(
+ "classes",
+ FilePatternResolver(CommandLineLogger()),
+ listOf(projectBuildDir)
+ )
+}
diff --git a/teamscale-maven-plugin/src/main/kotlin/com/teamscale/maven/tia/TiaIntegrationTestMojo.kt b/teamscale-maven-plugin/src/main/kotlin/com/teamscale/maven/tia/TiaIntegrationTestMojo.kt
new file mode 100644
index 000000000..694f4b2ba
--- /dev/null
+++ b/teamscale-maven-plugin/src/main/kotlin/com/teamscale/maven/tia/TiaIntegrationTestMojo.kt
@@ -0,0 +1,27 @@
+package com.teamscale.maven.tia
+
+import org.apache.maven.plugins.annotations.LifecyclePhase
+import org.apache.maven.plugins.annotations.Mojo
+import org.apache.maven.plugins.annotations.Parameter
+import org.apache.maven.plugins.annotations.ResolutionScope
+
+/**
+ * Instruments the Failsafe integration tests and uploads testwise coverage to Teamscale.
+ */
+@Mojo(
+ name = "prepare-tia-integration-test",
+ defaultPhase = LifecyclePhase.PACKAGE,
+ requiresDependencyResolution = ResolutionScope.RUNTIME,
+ threadSafe = true
+)
+class TiaIntegrationTestMojo : TiaMojoBase() {
+ /**
+ * The partition to which to upload integration test coverage.
+ */
+ @Parameter(defaultValue = "Integration Tests")
+ override lateinit var partition: String
+
+ override val isIntegrationTest = true
+ override val testPluginArtifact = "org.apache.maven.plugins:maven-failsafe-plugin"
+ override val testPluginPropertyPrefix = "failsafe"
+}
diff --git a/teamscale-maven-plugin/src/main/kotlin/com/teamscale/maven/tia/TiaMojoBase.kt b/teamscale-maven-plugin/src/main/kotlin/com/teamscale/maven/tia/TiaMojoBase.kt
new file mode 100644
index 000000000..2b6dd7e27
--- /dev/null
+++ b/teamscale-maven-plugin/src/main/kotlin/com/teamscale/maven/tia/TiaMojoBase.kt
@@ -0,0 +1,445 @@
+package com.teamscale.maven.tia
+
+import com.teamscale.maven.TeamscaleMojoBase
+import org.apache.commons.lang3.StringUtils
+import org.apache.maven.artifact.Artifact
+import org.apache.maven.model.PluginExecution
+import org.apache.maven.plugin.MojoExecutionException
+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.net.ServerSocket
+import java.nio.file.Files
+import java.nio.file.Path
+import java.nio.file.Paths
+import java.util.*
+import kotlin.io.path.createDirectories
+
+/**
+ * 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 impacted-test-engine, you must declare it as a test dependency. Example:
+ *
+ * ```
+ *
+ *
+ * com.teamscale
+ * impacted-test-engine
+ * 30.0.0
+ * test
+ *
+ *
+ * ```
+ *
+ * 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 `${project.build.directory}/tia/agent.log`.
+ */
+abstract class TiaMojoBase : TeamscaleMojoBase() {
+ /**
+ * Impacted tests are calculated from [baselineCommit] to [commit]. This sets the baseline.
+ */
+ @Parameter
+ var baselineCommit: String? = null
+
+ /**
+ * Impacted tests are calculated from [baselineCommit] to [commit].
+ * The [baselineRevision] sets the [baselineCommit] with the help of a VCS revision (e.g. git SHA1) instead of a branch and timestamp
+ */
+ @Parameter
+ var baselineRevision: String? = null
+
+ /**
+ * 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 `*` to match any number characters and
+ * `?` to match any single character.
+ *
+ * Classes that match any of the include patterns are included, unless any exclude pattern excludes them.
+ */
+ @Parameter
+ var includes = emptyArray()
+
+ /**
+ * 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 `*` to match any number characters
+ * and `?` 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
+ var excludes = emptyArray()
+
+ /**
+ * 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 `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
+ var propertyName: String? = null
+
+ /**
+ * Port on which the Java agent listens for commands from this plugin. The default value 0 will tell the agent to
+ * automatically search for an open port.
+ */
+ @Parameter(defaultValue = "0")
+ lateinit var agentPort: String
+
+ /**
+ * Optional additional arguments to send to the agent. Each argument must be of the form `KEY=VALUE`.
+ */
+ @Parameter
+ var additionalAgentOptions = emptyArray()
+
+ /**
+ * Changes the log level of the agent to DEBUG.
+ */
+ @Parameter(defaultValue = "false")
+ var debugLogging: Boolean = false
+
+ /**
+ * Executes all tests, not only impacted ones if set. Defaults to false.
+ */
+ @Parameter(defaultValue = "false")
+ var runAllTests: Boolean = false
+
+ /**
+ * Executes only impacted tests, not all ones if set. Defaults to true.
+ */
+ @Parameter(defaultValue = "true")
+ var runImpacted: Boolean = true
+
+ /**
+ * Mode of producing testwise coverage.
+ */
+ @Parameter(defaultValue = "teamscale-upload")
+ lateinit var tiaMode: String
+
+ /**
+ * Map of resolved Maven artifacts. Provided automatically by Maven.
+ */
+ @Parameter(property = "plugin.artifactMap", required = true, readonly = true)
+ lateinit var pluginArtifactMap: Map
+
+ /**
+ * The project build directory (usually: `./target`). Provided automatically by Maven.
+ */
+ @Parameter(defaultValue = "\${project.build.directory}")
+ lateinit var projectBuildDir: String
+
+ private lateinit var targetDirectory: Path
+
+ @Throws(MojoFailureException::class, MojoExecutionException::class)
+ override fun execute() {
+ super.execute()
+
+ if (!baselineCommit.isNullOrBlank() && !baselineRevision.isNullOrBlank()) {
+ log.warn(
+ "Both baselineRevision and baselineCommit are set but only one of them is needed. " +
+ "The revision will be preferred in this case. If that's not intended, please do not set the baselineRevision manually."
+ )
+ }
+
+ if (skip) return
+
+ getTestPlugin(testPluginArtifact)?.let { testPlugin ->
+ configureTestPlugin()
+ testPlugin.executions.forEach { execution ->
+ validateTestPluginConfiguration(execution)
+ }
+ }
+
+ targetDirectory = Paths.get(projectBuildDir, "tia").toAbsolutePath()
+ createTargetDirectory()
+
+ resolveCommitOrRevision()
+ setTiaProperties()
+
+ val agentConfigFile = createAgentConfigFiles(agentPort)
+ setArgLine(agentConfigFile, targetDirectory.resolve("agent.log"))
+ }
+
+ private fun setTiaProperties() {
+ setTiaProperty("reportDirectory", targetDirectory.toString())
+ setTiaProperty("server.url", teamscaleUrl)
+ setTiaProperty("server.project", projectId)
+ setTiaProperty("server.userName", username)
+ setTiaProperty("server.userAccessToken", accessToken)
+
+ if (StringUtils.isNotEmpty(resolvedRevision)) {
+ setTiaProperty("endRevision", resolvedRevision)
+ } else {
+ setTiaProperty("endCommit", resolvedCommit)
+ }
+
+ if (StringUtils.isNotEmpty(baselineRevision)) {
+ setTiaProperty("baselineRevision", baselineRevision)
+ } else {
+ setTiaProperty("baseline", baselineCommit)
+ }
+
+ setTiaProperty("repository", repository)
+ setTiaProperty("partition", partition)
+ if (agentPort == "0") {
+ agentPort = findAvailablePort()
+ }
+
+ setTiaProperty("agentsUrls", "http://localhost:$agentPort")
+ setTiaProperty("runImpacted", runImpacted.toString())
+ setTiaProperty("runAllTests", runAllTests.toString())
+ }
+
+ /**
+ * Automatically find an available port.
+ */
+ private fun findAvailablePort(): String {
+ try {
+ ServerSocket(0).use { socket ->
+ val port = socket.localPort
+ log.info("Automatically set server port to $port")
+ return port.toString()
+ }
+ } catch (e: IOException) {
+ log.error("Port blocked, trying again.", e)
+ return findAvailablePort()
+ }
+ }
+
+ /**
+ * Sets the teamscale-test-impacted engine as only includedEngine and passes all previous engine configuration to
+ * the impacted test engine instead.
+ */
+ private fun configureTestPlugin() {
+ enforcePropertyValue(INCLUDE_JUNIT5_ENGINES_OPTION, "includedEngines", "teamscale-test-impacted")
+ enforcePropertyValue(EXCLUDE_JUNIT5_ENGINES_OPTION, "excludedEngines", "")
+ }
+
+ private fun enforcePropertyValue(
+ engineOption: String,
+ impactedEngineSuffix: String,
+ newValue: String
+ ) {
+ overrideProperty(engineOption, impactedEngineSuffix, newValue, session.currentProject.properties)
+ overrideProperty(engineOption, impactedEngineSuffix, newValue, session.userProperties)
+ }
+
+ private fun overrideProperty(
+ engineOption: String,
+ impactedEngineSuffix: String,
+ newValue: String,
+ properties: Properties
+ ) {
+ (properties.put(getPropertyName(engineOption), newValue) as? String)?.let { originalValue ->
+ if (originalValue.isNotBlank() && (newValue != originalValue)) {
+ setTiaProperty(impactedEngineSuffix, originalValue)
+ }
+ }
+ }
+
+ @Throws(MojoFailureException::class)
+ private fun validateTestPluginConfiguration(execution: PluginExecution) {
+ val configurationDom = execution.configuration as Xpp3Dom
+
+ validateEngineNotConfigured(configurationDom, INCLUDE_JUNIT5_ENGINES_OPTION)
+ validateEngineNotConfigured(configurationDom, EXCLUDE_JUNIT5_ENGINES_OPTION)
+
+ validateParallelizationParameter(configurationDom, "threadCount")
+ validateParallelizationParameter(configurationDom, "forkCount")
+
+ val parameterDom = configurationDom.getChild("reuseForks") ?: return
+ val value = parameterDom.value
+ if (value != null && value != "true") {
+ log.warn(
+ "You configured surefire to not reuse forks." +
+ " This has been shown to lead to performance decreases in combination with the Teamscale Maven Plugin." +
+ " If you notice performance problems, please have a look at our troubleshooting section for possible solutions: https://docs.teamscale.com/howto/providing-testwise-coverage/#troubleshooting."
+ )
+ }
+ }
+
+ @Throws(MojoFailureException::class)
+ private fun validateEngineNotConfigured(
+ configurationDom: Xpp3Dom,
+ xmlConfigurationName: String
+ ) {
+ configurationDom.getChild(xmlConfigurationName)?.let {
+ throw MojoFailureException(
+ "You configured JUnit 5 engines in the $testPluginArtifact plugin via the $xmlConfigurationName configuration parameter. This is currently not supported when performing Test Impact analysis. Please add the $xmlConfigurationName via the ${
+ getPropertyName(xmlConfigurationName)
+ } property."
+ )
+ }
+ }
+
+ private fun getPropertyName(xmlConfigurationName: String) =
+ "$testPluginPropertyPrefix.$xmlConfigurationName"
+
+ private fun getTestPlugin(testPluginArtifact: String) =
+ session.currentProject.model.build.pluginsAsMap[testPluginArtifact]
+
+ @Throws(MojoFailureException::class)
+ private fun validateParallelizationParameter(
+ configurationDom: Xpp3Dom,
+ parallelizationParameter: String
+ ) {
+ configurationDom.getChild(parallelizationParameter)?.value?.let { value ->
+ if (value == "1") return@let
+ throw 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 val partition: String
+
+ /**
+ * @return the artifact name of the test plugin (e.g. Surefire, Failsafe).
+ */
+ protected abstract val testPluginArtifact: String
+
+ /** @return The prefix of the properties that are used to pass parameters to the plugin.
+ */
+ protected abstract val testPluginPropertyPrefix: String
+
+ /**
+ * @return whether this Mojo applies to integration tests.
+ *
+ *
+ * Depending on this, different properties are used to set the argLine.
+ */
+ protected abstract val isIntegrationTest: Boolean
+
+ @Throws(MojoFailureException::class)
+ private fun createTargetDirectory() {
+ try {
+ targetDirectory.createDirectories()
+ } catch (e: IOException) {
+ throw MojoFailureException("Could not create target directory $targetDirectory", e)
+ }
+ }
+
+ private fun setArgLine(agentConfigFile: Path, logFilePath: Path) {
+ var agentLogLevel = "INFO"
+ if (debugLogging) {
+ agentLogLevel = "DEBUG"
+ }
+
+ ArgLine.cleanOldArgLines(session, log)
+ findAgentJarFile()?.let { agentJarFile ->
+ ArgLine.applyToMavenProject(
+ ArgLine(additionalAgentOptions, agentLogLevel, agentJarFile, agentConfigFile, logFilePath),
+ session, log, propertyName ?: "None", isIntegrationTest
+ )
+ }
+ }
+
+ @Throws(MojoFailureException::class)
+ private fun createAgentConfigFiles(agentPort: String): Path {
+ val loggingConfigPath = targetDirectory.resolve("logback.xml")
+ try {
+ Files.newOutputStream(loggingConfigPath).use { loggingConfigOutputStream ->
+ FileSystemUtils.copy(readAgentLogbackConfig(), loggingConfigOutputStream)
+ }
+ } catch (e: IOException) {
+ throw MojoFailureException(
+ "Writing the logging configuration file for the TIA agent failed. Make sure the path $loggingConfigPath is writeable.", e
+ )
+ }
+
+ val configFilePath = targetDirectory.resolve("agent-at-port-$agentPort.properties")
+ val agentConfig = createAgentConfig(loggingConfigPath, targetDirectory.resolve("reports"))
+ try {
+ Files.write(configFilePath, setOf(agentConfig))
+ } catch (e: IOException) {
+ throw MojoFailureException(
+ "Writing the configuration file for the TIA agent failed. Make sure the path $configFilePath is writeable.", e
+ )
+ }
+
+ log.info("Agent config file created at $configFilePath")
+ return configFilePath
+ }
+
+ private fun readAgentLogbackConfig() =
+ TiaMojoBase::class.java.getResourceAsStream("logback-agent.xml")
+
+ private fun createAgentConfig(loggingConfigPath: Path, agentOutputDirectory: Path): String {
+ var config = """
+ mode=testwise
+ tia-mode=$tiaMode
+ teamscale-server-url=$teamscaleUrl
+ teamscale-project=$projectId
+ teamscale-user=$username
+ teamscale-access-token=$accessToken
+ teamscale-partition=${partition}
+ http-server-port=$agentPort
+ logging-config=$loggingConfigPath
+ out=${agentOutputDirectory.toAbsolutePath()}
+ """.trimIndent()
+ if (includes.isNotEmpty()) {
+ config += """
+
+ includes=${includes.joinToString(";")}
+ """.trimIndent()
+ }
+ if (excludes.isNotEmpty()) {
+ config += """
+
+ excludes=${excludes.joinToString(";")}
+ """.trimIndent()
+ }
+ if (repository.isNotBlank()) {
+ config += "\nteamscale-repository=$repository"
+ }
+
+ config += if (!resolvedRevision.isNullOrBlank()) {
+ "\nteamscale-revision=$resolvedRevision"
+ } else {
+ "\nteamscale-commit=$resolvedCommit"
+ }
+ return config
+ }
+
+ private fun findAgentJarFile() =
+ pluginArtifactMap["com.teamscale:teamscale-jacoco-agent"]?.file?.toPath()
+
+ /**
+ * 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 fun setTiaProperty(name: String, value: String?) {
+ if (value == null) return
+ val fullyQualifiedName = "teamscale.test.impacted.$name"
+ log.debug("Setting property $name=$value")
+ session.userProperties.setProperty(fullyQualifiedName, value)
+ session.systemProperties.setProperty(fullyQualifiedName, value)
+ System.setProperty(fullyQualifiedName, value)
+ }
+
+ companion object {
+ /**
+ * Name of the surefire/failsafe option to pass in
+ * [included engines](https://maven.apache.org/surefire/maven-surefire-plugin/test-mojo.html#includeJUnit5Engines)
+ */
+ private const val INCLUDE_JUNIT5_ENGINES_OPTION = "includeJUnit5Engines"
+
+ /**
+ * Name of the surefire/failsafe option to pass in
+ * [excluded engines](https://maven.apache.org/surefire/maven-surefire-plugin/test-mojo.html#excludejunit5engines)
+ */
+ private const val EXCLUDE_JUNIT5_ENGINES_OPTION = "excludeJUnit5Engines"
+ }
+}
diff --git a/teamscale-maven-plugin/src/main/kotlin/com/teamscale/maven/tia/TiaUnitTestMojo.kt b/teamscale-maven-plugin/src/main/kotlin/com/teamscale/maven/tia/TiaUnitTestMojo.kt
new file mode 100644
index 000000000..67c4f7eb8
--- /dev/null
+++ b/teamscale-maven-plugin/src/main/kotlin/com/teamscale/maven/tia/TiaUnitTestMojo.kt
@@ -0,0 +1,27 @@
+package com.teamscale.maven.tia
+
+import org.apache.maven.plugins.annotations.LifecyclePhase
+import org.apache.maven.plugins.annotations.Mojo
+import org.apache.maven.plugins.annotations.Parameter
+import org.apache.maven.plugins.annotations.ResolutionScope
+
+/**
+ * Instruments the Surefire unit tests and uploads testwise coverage to Teamscale.
+ */
+@Mojo(
+ name = "prepare-tia-unit-test",
+ defaultPhase = LifecyclePhase.INITIALIZE,
+ requiresDependencyResolution = ResolutionScope.RUNTIME,
+ threadSafe = true
+)
+class TiaUnitTestMojo : TiaMojoBase() {
+ /**
+ * The partition to which to upload unit test coverage.
+ */
+ @Parameter(defaultValue = "Unit Tests")
+ override lateinit var partition: String
+
+ override val isIntegrationTest = false
+ override val testPluginArtifact = "org.apache.maven.plugins:maven-surefire-plugin"
+ override val testPluginPropertyPrefix = "surefire"
+}
diff --git a/teamscale-maven-plugin/src/main/kotlin/com/teamscale/maven/upload/CoverageUploadMojo.kt b/teamscale-maven-plugin/src/main/kotlin/com/teamscale/maven/upload/CoverageUploadMojo.kt
new file mode 100644
index 000000000..1ae3290bf
--- /dev/null
+++ b/teamscale-maven-plugin/src/main/kotlin/com/teamscale/maven/upload/CoverageUploadMojo.kt
@@ -0,0 +1,281 @@
+package com.teamscale.maven.upload
+
+import com.teamscale.maven.TeamscaleMojoBase
+import org.apache.maven.plugin.MojoExecutionException
+import org.apache.maven.plugin.MojoFailureException
+import org.apache.maven.plugins.annotations.LifecyclePhase
+import org.apache.maven.plugins.annotations.Mojo
+import org.apache.maven.plugins.annotations.Parameter
+import org.apache.maven.plugins.annotations.ResolutionScope
+import org.apache.maven.project.MavenProject
+import org.codehaus.plexus.util.xml.Xpp3Dom
+import shadow.com.teamscale.client.CommitDescriptor
+import shadow.com.teamscale.client.EReportFormat
+import shadow.com.teamscale.client.TeamscaleClient
+import java.io.File
+import java.io.IOException
+import java.nio.file.Path
+import java.nio.file.Paths
+import java.util.*
+
+/**
+ * Run this goal after the Jacoco report generation to upload them to a
+ * configured Teamscale instance. The configuration can be specified in the root
+ * Maven project. Offers the following functionality:
+ *
+ * 1. Validate Jacoco Maven plugin configuration
+ * 1. Locate and upload all reports in one session
+ *
+ *
+ * @see [Jacoco
+ * Plugin](https://www.jacoco.org/jacoco/trunk/doc/maven.html)
+ */
+@Mojo(
+ name = "upload-coverage",
+ defaultPhase = LifecyclePhase.VERIFY,
+ requiresDependencyResolution = ResolutionScope.RUNTIME,
+ threadSafe = true
+)
+class CoverageUploadMojo : TeamscaleMojoBase() {
+ /**
+ * The Teamscale partition name to which unit test reports will be uploaded.
+ */
+ @Parameter(property = "teamscale.unitTestPartition", defaultValue = "Unit Tests")
+ lateinit var unitTestPartition: String
+
+ /**
+ * The Teamscale partition name to which integration test reports will be
+ * uploaded.
+ */
+ @Parameter(property = "teamscale.integrationTestPartition", defaultValue = "Integration Tests")
+ lateinit var integrationTestPartition: String
+
+ /**
+ * The Teamscale partition name to which aggregated test reports will be
+ * uploaded.
+ */
+ @Parameter(property = "teamscale.aggregatedTestPartition", defaultValue = "Aggregated Tests")
+ lateinit var aggregatedTestPartition: String
+
+ /**
+ * The output directory of the testwise coverage reports. Should only be set if
+ * testwise coverage is uploaded.
+ */
+ @Parameter
+ var testwiseCoverageOutputFolder: String? = null
+
+ /**
+ * The Teamscale partition name to which testwise coverage reports will be
+ * uploaded.
+ */
+ @Parameter(property = "teamscale.testwisePartition", defaultValue = "Testwise Coverage")
+ lateinit var testwisePartition: String
+
+ /**
+ * Paths to all reports generated by subprojects
+ *
+ * @see [report](https://www.jacoco.org/jacoco/trunk/doc/report-mojo.html)
+ */
+ private val reportGoalOutputFiles = mutableListOf()
+
+ /**
+ * Paths to all integration reports generated by subprojects
+ *
+ * @see [report-integration](https://www.jacoco.org/jacoco/trunk/doc/report-integration-mojo.html)
+ */
+ private val reportIntegrationGoalOutputFiles = mutableListOf()
+
+ /**
+ * The project build directory (usually: `./target`). Provided
+ * automatically by Maven.
+ */
+ @Parameter(defaultValue = "\${project.build.directory}")
+ lateinit var projectBuildDir: String
+
+ /**
+ * Paths to all aggregated reports generated by subprojects
+ *
+ * @see [report-aggregate](https://www.jacoco.org/jacoco/trunk/doc/report-aggregate-mojo.html)
+ */
+ private val reportAggregateGoalOutputFiles = mutableListOf()
+
+ /**
+ * The Teamscale client that is used to upload reports to a Teamscale instance.
+ */
+ private lateinit var teamscaleClient: TeamscaleClient
+
+ @Throws(MojoFailureException::class, MojoExecutionException::class)
+ override fun execute() {
+ super.execute()
+ if (skip) {
+ log.debug("Skipping since skip is set to true")
+ return
+ }
+ if (session.currentProject != session.topLevelProject) {
+ log.debug("Skipping since upload should only happen in top project")
+ return
+ }
+ teamscaleClient = TeamscaleClient(teamscaleUrl, username, accessToken, projectId)
+ log.debug("Resolving end commit")
+ resolveCommitOrRevision()
+ log.debug("Parsing Jacoco plugin configurations")
+ parseJacocoConfiguration()
+ try {
+ log.debug("Uploading coverage reports")
+ uploadCoverageReports()
+ } catch (e: IOException) {
+ throw MojoFailureException(
+ "Uploading coverage reports failed. No upload to Teamscale was performed. You can try again or upload the XML coverage reports manually, see https://docs.teamscale.com/reference/ui/project/project/#manual-report-upload",
+ e
+ )
+ }
+ }
+
+ /**
+ * Check that Jacoco is set up correctly and read any custom settings that may
+ * have been set
+ *
+ * @throws MojoFailureException
+ * If Jacoco is not set up correctly
+ */
+ @Throws(MojoFailureException::class)
+ private fun parseJacocoConfiguration() {
+ val project = session.topLevelProject ?: return
+ collectReportOutputDirectory(project, "report", "jacoco", reportGoalOutputFiles)
+ collectReportOutputDirectory(
+ project,
+ "report-integration",
+ "jacoco-it",
+ reportIntegrationGoalOutputFiles
+ )
+ collectReportOutputDirectory(
+ project,
+ "report-aggregate",
+ "jacoco-aggregate",
+ reportAggregateGoalOutputFiles
+ )
+ log.debug("Found ${project.collectedProjects.size} sub-modules")
+ project.collectedProjects.forEach { subProject ->
+ collectReportOutputDirectory(subProject, "report", "jacoco", reportGoalOutputFiles)
+ collectReportOutputDirectory(
+ subProject,
+ "report-integration",
+ "jacoco-it",
+ reportIntegrationGoalOutputFiles
+ )
+ collectReportOutputDirectory(
+ subProject,
+ "report-aggregate",
+ "jacoco-aggregate",
+ reportAggregateGoalOutputFiles
+ )
+ }
+ }
+
+ /**
+ * Collect the file locations in which JaCoCo is configured to save the XML
+ * coverage reports
+ *
+ * @param project
+ * The project
+ * @param reportGoal
+ * The JaCoCo report goal
+ * @param jacocoDirectory
+ * The name of the directory, matching the JaCoCo goal
+ * @see [Goals](https://www.eclemma.org/jacoco/trunk/doc/maven.html)
+ */
+ @Throws(MojoFailureException::class)
+ private fun collectReportOutputDirectory(
+ project: MavenProject,
+ reportGoal: String,
+ jacocoDirectory: String,
+ reportOutputFiles: MutableList
+ ) {
+ val defaultOutputDirectory = Paths.get(project.model.reporting.outputDirectory)
+ // If a Dom is null it means the execution goal uses default parameters which work correctly
+ val reportConfigurationDom = getJacocoGoalExecutionConfiguration(project, reportGoal)
+ if (!validateReportFormat(reportConfigurationDom)) {
+ val errorMessage = "Skipping upload for ${project.name} as $JACOCO_PLUGIN_NAME is not configured to produce XML reports for goal ${jacocoDirectory}. See https://www.jacoco.org/jacoco/trunk/doc/report-mojo.html#formats"
+ throw MojoFailureException(errorMessage)
+ }
+ val resolvedCoverageFile = getCustomOutputDirectory(reportConfigurationDom)
+ ?: defaultOutputDirectory.resolve(jacocoDirectory).resolve("jacoco.xml")
+ log.debug("Adding possible report location: $resolvedCoverageFile")
+ reportOutputFiles.add(resolvedCoverageFile)
+ }
+
+ @Throws(IOException::class)
+ private fun uploadCoverageReports() {
+ val reportPath = if (testwiseCoverageOutputFolder.isNullOrBlank()) {
+ Paths.get(projectBuildDir, "tia", "reports")
+ } else {
+ Paths.get(testwiseCoverageOutputFolder)
+ }
+ reportPath.toFile().listFiles { obj -> obj.isFile }?.let { files ->
+ val testwiseCoverageFiles = listOf(*files)
+ log.debug("Uploading testwise coverage to partition $testwisePartition")
+ uploadCoverage(testwiseCoverageFiles.map { it.toPath() }, testwisePartition, EReportFormat.TESTWISE_COVERAGE)
+ }
+ uploadCoverage(reportGoalOutputFiles, unitTestPartition, EReportFormat.JACOCO)
+ uploadCoverage(reportIntegrationGoalOutputFiles, integrationTestPartition, EReportFormat.JACOCO)
+ uploadCoverage(reportAggregateGoalOutputFiles, aggregatedTestPartition, EReportFormat.JACOCO)
+ }
+
+ @Throws(IOException::class)
+ private fun uploadCoverage(reportOutputFiles: List, partition: String, format: EReportFormat) {
+ val reports = mutableListOf()
+ log.debug("Scanning through ${reportOutputFiles.size} locations for ${partition}...")
+ reportOutputFiles.forEach { reportPath ->
+ val report = reportPath.toFile()
+ if (!report.exists()) {
+ log.debug("Cannot find ${report.absolutePath}, skipping...")
+ return@forEach
+ }
+ if (!report.canRead()) {
+ log.warn("Cannot read ${report.absolutePath}, skipping!")
+ return@forEach
+ }
+ reports.add(report)
+ }
+ if (reports.isNotEmpty()) {
+ log.info("Uploading ${reports.size} report for project $projectId to $partition")
+ teamscaleClient.uploadReports(
+ format, reports, CommitDescriptor.parse(resolvedCommit), revision, repository, partition, COVERAGE_UPLOAD_MESSAGE
+ )
+ } else {
+ log.info("Found no valid reports for $partition")
+ }
+ }
+
+ /**
+ * Validates that a configuration Dom is set up to generate XML reports
+ *
+ * @param configurationDom
+ * The configuration Dom of a goal execution
+ */
+ private fun validateReportFormat(configurationDom: Xpp3Dom?): Boolean {
+ if (configurationDom?.getChild("formats") == null) {
+ return true
+ }
+ for (format in configurationDom.getChild("formats").children) {
+ if (format.value == "XML") {
+ return true
+ }
+ }
+ return false
+ }
+
+ private fun getCustomOutputDirectory(configurationDom: Xpp3Dom?) =
+ configurationDom?.getChild("outputDirectory")?.let {
+ Paths.get(it.value)
+ }
+
+ private fun getJacocoGoalExecutionConfiguration(project: MavenProject, pluginGoal: String) =
+ project.getExecutionConfigurationDom(JACOCO_PLUGIN_NAME, pluginGoal)
+
+ companion object {
+ private const val JACOCO_PLUGIN_NAME = "org.jacoco:jacoco-maven-plugin"
+
+ private const val COVERAGE_UPLOAD_MESSAGE = "Coverage upload via Teamscale Maven plugin"
+ }
+}
diff --git a/teamscale-maven-plugin/src/test/java/com/teamscale/maven/tia/ArgLineTest.java b/teamscale-maven-plugin/src/test/java/com/teamscale/maven/tia/ArgLineTest.java
deleted file mode 100644
index fbb54dbc3..000000000
--- a/teamscale-maven-plugin/src/test/java/com/teamscale/maven/tia/ArgLineTest.java
+++ /dev/null
@@ -1,40 +0,0 @@
-package com.teamscale.maven.tia;
-
-import org.junit.jupiter.api.Test;
-
-import java.nio.file.Paths;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-class ArgLineTest {
-
- @Test
- public void isIdempotent() {
- ArgLine argLine = new ArgLine(null, "info", Paths.get("agent.jar"), Paths.get("agent.properties"),
- Paths.get("agent.log"));
- String firstArgLine = argLine.prependTo("");
- String secondArgLine = argLine.prependTo(ArgLine.removePreviousTiaAgent(firstArgLine));
-
- assertEquals(firstArgLine, secondArgLine);
- }
-
- @Test
- public void testNullOriginalArgLine() {
- ArgLine argLine = new ArgLine(null, "info", Paths.get("agent.jar"), Paths.get("agent.properties"),
- Paths.get("agent.log"));
- String newArgLine = argLine.prependTo(null);
-
- assertTrue(newArgLine.startsWith("-Dteamscale.markstart"));
- assertTrue(newArgLine.endsWith("-Dteamscale.markend"));
- }
-
- @Test
- public void preservesUnrelatedAgents() {
- String argLine = "-javaagent:someother.jar";
- String newArgLine = ArgLine.removePreviousTiaAgent(argLine);
-
- assertEquals(argLine, newArgLine);
- }
-
-}
\ No newline at end of file
diff --git a/teamscale-maven-plugin/src/test/kotlin/com/teamscale/maven/tia/ArgLineTest.kt b/teamscale-maven-plugin/src/test/kotlin/com/teamscale/maven/tia/ArgLineTest.kt
new file mode 100644
index 000000000..f57f3db95
--- /dev/null
+++ b/teamscale-maven-plugin/src/test/kotlin/com/teamscale/maven/tia/ArgLineTest.kt
@@ -0,0 +1,41 @@
+package com.teamscale.maven.tia
+
+import com.teamscale.maven.tia.ArgLine.Companion.removePreviousTiaAgent
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import java.nio.file.Paths
+
+internal class ArgLineTest {
+ @Test
+ fun isIdempotent() {
+ val argLine = ArgLine(
+ null, "info", Paths.get("agent.jar"), Paths.get("agent.properties"),
+ Paths.get("agent.log")
+ )
+ val firstArgLine = argLine.prependTo("")
+ val secondArgLine = argLine.prependTo(removePreviousTiaAgent(firstArgLine))
+
+ assertEquals(firstArgLine, secondArgLine)
+ }
+
+ @Test
+ fun testNullOriginalArgLine() {
+ val argLine = ArgLine(
+ null, "info", Paths.get("agent.jar"), Paths.get("agent.properties"),
+ Paths.get("agent.log")
+ )
+ val newArgLine = argLine.prependTo(null)
+
+ assertTrue(newArgLine.startsWith("-Dteamscale.markstart"))
+ assertTrue(newArgLine.endsWith("-Dteamscale.markend"))
+ }
+
+ @Test
+ fun preservesUnrelatedAgents() {
+ val argLine = "-javaagent:someother.jar"
+ val newArgLine = removePreviousTiaAgent(argLine)
+
+ assertEquals(argLine, newArgLine)
+ }
+}
\ No newline at end of file