diff --git a/system-tests/cucumber-maven-tia/src/test/java/com/teamscale/tia/TiaMavenCucumberSystemTest.java b/system-tests/cucumber-maven-tia/src/test/java/com/teamscale/tia/TiaMavenCucumberSystemTest.java index 17952e61c..825869ed6 100644 --- a/system-tests/cucumber-maven-tia/src/test/java/com/teamscale/tia/TiaMavenCucumberSystemTest.java +++ b/system-tests/cucumber-maven-tia/src/test/java/com/teamscale/tia/TiaMavenCucumberSystemTest.java @@ -14,7 +14,7 @@ import static org.junit.jupiter.api.Assertions.assertAll; /** - * Runs several Maven projects' Surefire tests that have the agent attached and one of our JUnit run listeners enabled. + * Runs several Maven projects' Surefire tests that have the agent attached, and one of our JUnit run listeners enabled. * Checks that this produces a correct coverage report. */ public class TiaMavenCucumberSystemTest { diff --git a/teamscale-maven-plugin/pom.xml b/teamscale-maven-plugin/pom.xml index 9594d72b7..b77fe86ad 100644 --- a/teamscale-maven-plugin/pom.xml +++ b/teamscale-maven-plugin/pom.xml @@ -21,7 +21,7 @@ The Apache License, Version 2.0 - http://www.apache.org/licenses/LICENSE-2.0.txt + https://www.apache.org/licenses/LICENSE-2.0.txt @@ -59,6 +59,8 @@ This prevents frequent updates of this file in the repository. --> 34.0.0 + 2.1.0 + true @@ -68,11 +70,23 @@ 5.11.4 test + + org.jetbrains.kotlin + kotlin-stdlib + ${kotlin.version} + + + org.jetbrains.kotlin + kotlin-test-junit + ${kotlin.version} + test + com.teamscale teamscale-jacoco-agent ${teamscale.agent.version} + org.apache.maven maven-plugin-api @@ -91,6 +105,7 @@ 3.9.9 provided + org.eclipse.jgit org.eclipse.jgit @@ -107,7 +122,6 @@ ${teamscale.agent.version} runtime - @@ -124,11 +138,58 @@ + + org.jetbrains.kotlin + kotlin-maven-plugin + ${kotlin.version} + + + compile + + compile + + process-sources + + + src/main/kotlin + + + + + test-compile + + test-compile + + test-compile + + + src/test/kotlin + + + + + org.apache.maven.plugins maven-plugin-plugin - 3.15.1 + 3.6.4 + + + kotlin + + + + + com.github.gantsign.maven.plugin-tools + kotlin-maven-plugin-tools + 1.1.0 + + + + default-descriptor + process-classes + generate-helpmojo 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: - *

    - *
  1. Validate Jacoco Maven plugin configuration
  2. - *
  3. Locate and upload all reports in one session
  4. - *
- * - * @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