All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.teamscale.tia.maven.TiaMojoBase Maven / Gradle / Ivy

There is a newer version: 34.2.0
Show newest version
package com.teamscale.tia.maven;

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.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.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 AbstractMojo { /** * 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"; /** * 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 end commit is manually specified, the plugin will try to determine the currently checked out Git commit. */ @Parameter public String endCommit; /** * You can optionally specify which code should be included in the coverage instrumentation. Each pattern is applied * to the fully qualified class names of the profiled system. Use {@code *} to match any number characters and * {@code ?} to match any single character. *

* Classes that match any of the include patterns are included, unless any exclude pattern excludes them. */ @Parameter public String[] includes; /** * You can optionally specify which code should be excluded from the coverage instrumentation. Each pattern is * applied to the fully qualified class names of the profiled system. Use {@code *} to match any number characters * and {@code ?} to match any single character. *

* Classes that match any of the exclude patterns are excluded, even if they are included by an include pattern. */ @Parameter public String[] excludes; /** * In order to instrument the system under test, a Java agent must be attached to the JVM of the system. The JVM * command line arguments to achieve this are by default written to the property {@code argLine}, which is * automatically picked up by Surefire and Failsafe and applied to the JVMs these plugins start. You can override * the name of this property if you wish to manually apply the command line arguments yourself, e.g. if your system * under test is started by some other plugin like the Spring boot starter. */ @Parameter public String propertyName; /** * Port on which the Java agent listens for commands from this plugin. 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; /** * Whether to skip the execution of this Mojo. */ @Parameter(defaultValue = "false") public boolean skip; /** * 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; /** * The running Maven session. Provided automatically by Maven. */ @Parameter(defaultValue = "${session}") public MavenSession session; private Path targetDirectory; private String resolvedEndCommit; @Override public void execute() throws MojoFailureException { if (skip) { return; } Plugin testPlugin = getTestPlugin(getTestPluginArtifact()); if (testPlugin != null) { configureTestPlugin(); for (PluginExecution execution : testPlugin.getExecutions()) { validateTestPluginConfiguration(execution); } } targetDirectory = Paths.get(projectBuildDir, "tia").toAbsolutePath(); createTargetDirectory(); resolvedEndCommit = resolveEndCommit(); setTiaProperty("reportDirectory", targetDirectory.toString()); setTiaProperty("server.url", teamscaleUrl); setTiaProperty("server.project", projectId); setTiaProperty("server.userName", username); setTiaProperty("server.userAccessToken", accessToken); setTiaProperty("endCommit", resolvedEndCommit); setTiaProperty("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()); Path agentConfigFile = createAgentConfigFiles(agentPort); Path logFilePath = targetDirectory.resolve("agent.log"); setArgLine(agentConfigFile, logFilePath); } /** * 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")) { throw new MojoFailureException( "You configured the " + getTestPluginArtifact() + " plugin to not reuse forks via the reuseForks configuration parameter." + " This is not supported when performing Test Impact analysis as it prevents properly recording testwise coverage." + " Please enable fork reuse when running Test Impact analysis."); } } private void 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-commit=" + resolvedEndCommit + "\nteamscale-partition=" + getPartition() + "\nhttp-server-port=" + agentPort + "\nlogging-config=" + loggingConfigPath + "\nout=" + agentOutputDirectory.toAbsolutePath(); if (ArrayUtils.isNotEmpty(includes)) { config += "\nincludes=" + String.join(";", includes); } if (ArrayUtils.isNotEmpty(excludes)) { config += "\nexcludes=" + String.join(";", excludes); } return config; } private Path findAgentJarFile() { Artifact agentArtifact = pluginArtifactMap.get("com.teamscale:teamscale-jacoco-agent"); return agentArtifact.getFile().toPath(); } private String resolveEndCommit() throws MojoFailureException { if (StringUtils.isNotBlank(endCommit)) { return endCommit; } Path basedir = session.getCurrentProject().getBasedir().toPath(); try { GitCommit commit = GitCommit.getGitHeadCommitDescriptor(basedir); return commit.branch + ":" + commit.timestamp; } catch (IOException e) { throw new MojoFailureException("You did not configure an in the pom.xml" + " and I could also not determine the checked out commit in " + basedir + " from Git", e); } } /** * Sets a property in the TIA namespace. It seems that, depending on Maven version and which other plugins are used, * different types of properties are respected both during the build and during tests (as e.g. failsafe tests are * often run in a separate JVM spawned by Maven). So we set our properties in every possible way to make sure the * plugin works out of the box in most situations. */ private void setTiaProperty(String name, String value) { 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); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy