package org.pitest.maven;
import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Logger;
import org.apache.maven.artifact.Artifact;
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.pitest.coverage.CoverageSummary;
import org.pitest.functional.Option;
import org.pitest.functional.predicate.Predicate;
import org.pitest.mutationtest.config.PluginServices;
import org.pitest.mutationtest.config.ReportOptions;
import org.pitest.mutationtest.statistics.MutationStatistics;
import org.pitest.mutationtest.tooling.CombinedStatistics;
import org.pitest.plugin.ClientClasspathPlugin;
import org.pitest.plugin.ToolClasspathPlugin;
import org.slf4j.bridge.SLF4JBridgeHandler;
import uk.org.lidalia.sysoutslf4j.context.SysOutOverSLF4J;
public class AbstractPitMojo extends AbstractMojo {
private final Predicate<MavenProject> notEmptyProject;
protected final Predicate<Artifact> filter;
protected final PluginServices plugins;
// Concrete List types declared for all fields to work around maven 2 bug
/**
* Classes to include in mutation test
*/
@Parameter(property = "targetClasses")
protected ArrayList<String> targetClasses;
/**
* Tests to run
*/
@Parameter(property = "targetTests")
protected ArrayList<String> targetTests;
/**
* Methods not to mutate
*/
@Parameter(property = "excludedMethods")
private ArrayList<String> excludedMethods;
/**
* Classes not to mutate or run tests from
*/
@Parameter(property = "excludedClasses")
private ArrayList<String> excludedClasses;
/**
* Globs to be matched against method calls. No mutations will be created on
* the same line as a match.
*/
@Parameter(property = "avoidCallsTo")
private ArrayList<String> avoidCallsTo;
/**
* Base directory where all reports are written to.
*/
@Parameter(defaultValue = "${project.build.directory}/pit-reports", property = "reportsDirectory")
private File reportsDirectory;
/**
* File to write history information to for incremental analysis
*/
@Parameter(property = "historyOutputFile")
private File historyOutputFile;
/**
* File to read history from for incremental analysis (can be same as output
* file)
*/
@Parameter(property = "historyInputFile")
private File historyInputFile;
/**
* Convenience flag to read and write history to a local temp file.
*
* Setting this flag is the equivalent to calling maven with -DhistoryInputFile=file -DhistoryOutputFile=file
*
* Where file is a file named [groupid][artifactid][version]_pitest_history.bin in the temp directory
*
*/
@Parameter(defaultValue = "false", property = "withHistory")
private boolean withHistory;
/**
* Maximum distance to look from test to class. Relevant when mutating static
* initializers
*
*/
@Parameter(defaultValue = "-1", property = "maxDependencyDistance")
private int maxDependencyDistance;
/**
* Number of threads to use
*/
@Parameter(defaultValue = "1", property = "threads")
private int threads;
/**
* Mutate static initializers
*/
@Parameter(defaultValue = "false", property = "mutateStaticInitializers")
private boolean mutateStaticInitializers;
/**
* Detect inlined code
*/
@Parameter(defaultValue = "true", property = "detectInlinedCode")
private boolean detectInlinedCode;
/**
* Mutation operators to apply
*/
@Parameter(property = "mutators")
private ArrayList<String> mutators;
/**
* Weighting to allow for timeouts
*/
@Parameter(defaultValue = "1.25", property = "timeoutFactor")
private float timeoutFactor;
/**
* Constant factor to allow for timeouts
*/
@Parameter(defaultValue = "3000", property = "timeoutConstant")
private long timeoutConstant;
/**
* Maximum number of mutations to allow per class
*/
@Parameter(defaultValue = "-1", property = "maxMutationsPerClass")
private int maxMutationsPerClass;
/**
* Arguments to pass to child processes
*/
@Parameter
private ArrayList<String> jvmArgs;
/**
* Formats to output during analysis phase
*/
@Parameter(property = "outputFormats")
private ArrayList<String> outputFormats;
/**
* Output verbose logging
*/
@Parameter(defaultValue = "false", property = "verbose")
private boolean verbose;
/**
* Throw error if no mutations found
*/
@Parameter(defaultValue = "true", property = "failWhenNoMutations")
private boolean failWhenNoMutations;
/**
* Create timestamped subdirectory for report
*/
@Parameter(defaultValue = "true", property = "timestampedReports")
private boolean timestampedReports;
/**
* TestNG Groups/JUnit Categories to exclude
*/
@Parameter(property = "excludedGroups")
private ArrayList<String> excludedGroups;
/**
* TestNG Groups/JUnit Categories to include
*/
@Parameter(property = "includedGroups")
private ArrayList<String> includedGroups;
/**
* Maximum number of mutations to include in a single analysis unit.
*
* If set to 1 will analyse very slowly, but with string (jvm per mutant)
* isolation.
*
*/
@Parameter(property = "mutationUnitSize")
private int mutationUnitSize;
/**
* Export line coverage data
*/
@Parameter(defaultValue = "false", property = "exportLineCoverage")
private boolean exportLineCoverage;
/**
* Mutation score threshold at which to fail build
*/
@Parameter(defaultValue = "0", property = "mutationThreshold")
private int mutationThreshold;
/**
* Maximum surviving mutants to allow
*/
@Parameter(defaultValue = "-1", property = "maxSurviving")
private int maxSurviving = -1;
/**
* Line coverage threshold at which to fail build
*/
@Parameter(defaultValue = "0", property = "coverageThreshold")
private int coverageThreshold;
/**
* Path to java executable to use when running tests. Will default to
* executable in JAVA_HOME if none set.
*/
@Parameter
private String jvm;
/**
* Engine to use when generating mutations.
*/
@Parameter(defaultValue = "gregor", property = "mutationEngine")
private String mutationEngine;
/**
* List of additional classpath entries to use when looking for tests and
* mutable code. These will be used in addition to the classpath with which
* PIT is launched.
*/
@Parameter(property = "additionalClasspathElements")
private ArrayList<String> additionalClasspathElements;
/**
* List of classpath entries, formatted as "groupId:artifactId", which should
* not be included in the classpath when running mutation tests. Modelled
* after the corresponding Surefire/Failsafe property.
*/
@Parameter(property = "classpathDependencyExcludes")
private ArrayList<String> classpathDependencyExcludes;
/**
*
*/
@Parameter(property = "excludedRunners")
private ArrayList<String> excludedRunners;
/**
* When set indicates that analysis of this project should be skipped
*/
@Parameter(defaultValue = "false")
private boolean skip;
/**
* When set will try and create settings based on surefire configuration. This
* may not give the desired result in some circumstances
*/
@Parameter(defaultValue = "true")
private boolean parseSurefireConfig;
/**
* honours common skipTests flag in a maven run
*/
@Parameter(defaultValue = "false")
private boolean skipTests;
/**
* Use slf4j for logging
*/
@Parameter(defaultValue = "false", property = "useSlf4j")
private boolean useSlf4j;
/**
* Configuration properties.
*
* Value pairs may be used by pitest plugins.
*
*/
@Parameter
private Map<String, String> pluginConfiguration;
/**
* environment configuration
*
* Value pairs may be used by pitest plugins.
*/
@Parameter
private Map<String, String> environmentVariables = new HashMap<String, String>();
/**
* <i>Internal</i>: Project to interact with.
*
*/
@Parameter(property = "project", readonly = true, required = true)
protected MavenProject project;
/**
* <i>Internal</i>: Map of plugin artifacts.
*/
@Parameter(property = "plugin.artifactMap", readonly = true, required = true)
private Map<String, Artifact> pluginArtifactMap;
protected final GoalStrategy goalStrategy;
public AbstractPitMojo() {
this(new RunPitStrategy(), new DependencyFilter(new PluginServices(
AbstractPitMojo.class.getClassLoader())), new PluginServices(
AbstractPitMojo.class.getClassLoader()), new NonEmptyProjectCheck());
}
public AbstractPitMojo(final GoalStrategy strategy, final Predicate<Artifact> filter,
final PluginServices plugins, final Predicate<MavenProject> emptyProjectCheck) {
this.goalStrategy = strategy;
this.filter = filter;
this.plugins = plugins;
this.notEmptyProject = emptyProjectCheck;
}
@Override
public final void execute() throws MojoExecutionException,
MojoFailureException {
switchLogging();
if (shouldRun()) {
for (final ToolClasspathPlugin each : this.plugins
.findToolClasspathPlugins()) {
this.getLog().info("Found plugin : " + each.description());
}
for (final ClientClasspathPlugin each : this.plugins
.findClientClasspathPlugins()) {
this.getLog().info(
"Found shared classpath plugin : " + each.description());
}
final Option<CombinedStatistics> result = analyse();
if (result.hasSome()) {
throwErrorIfScoreBelowThreshold(result.value().getMutationStatistics());
throwErrorIfMoreThanMaximumSurvivors(result.value().getMutationStatistics());
throwErrorIfCoverageBelowThreshold(result.value().getCoverageSummary());
}
} else {
this.getLog().info("Skipping project");
}
}
private void switchLogging() {
if (this.useSlf4j) {
SLF4JBridgeHandler.removeHandlersForRootLogger();
SLF4JBridgeHandler.install();
Logger.getLogger("PIT").addHandler(new SLF4JBridgeHandler());
SysOutOverSLF4J.sendSystemOutAndErrToSLF4J();
}
}
private void throwErrorIfCoverageBelowThreshold(
final CoverageSummary coverageSummary) throws MojoFailureException {
if ((this.coverageThreshold != 0)
&& (coverageSummary.getCoverage() < this.coverageThreshold)) {
throw new MojoFailureException("Line coverage of "
+ coverageSummary.getCoverage() + "("
+ coverageSummary.getNumberOfCoveredLines() + "/"
+ coverageSummary.getNumberOfLines() + ") is below threshold of "
+ this.coverageThreshold);
}
}
private void throwErrorIfScoreBelowThreshold(final MutationStatistics result)
throws MojoFailureException {
if ((this.mutationThreshold != 0)
&& (result.getPercentageDetected() < this.mutationThreshold)) {
throw new MojoFailureException("Mutation score of "
+ result.getPercentageDetected() + " is below threshold of "
+ this.mutationThreshold);
}
}
private void throwErrorIfMoreThanMaximumSurvivors(final MutationStatistics result)
throws MojoFailureException {
if ((this.maxSurviving >= 0)
&& (result.getTotalSurvivingMutations() > this.maxSurviving)) {
throw new MojoFailureException("Had "
+ result.getTotalSurvivingMutations() + " surviving mutants, but only "
+ this.maxSurviving + " survivors allowed");
}
}
protected Option<CombinedStatistics> analyse() throws MojoExecutionException {
final ReportOptions data = new MojoToReportOptionsConverter(this,
new SurefireConfigConverter(), this.filter).convert();
return Option.some(this.goalStrategy.execute(detectBaseDir(), data,
this.plugins, this.environmentVariables));
}
protected File detectBaseDir() {
// execution project doesn't seem to always be available.
// possibily a maven 2 vs maven 3 issue?
final MavenProject executionProject = this.project.getExecutionProject();
if (executionProject == null) {
return null;
}
return executionProject.getBasedir();
}
public List<String> getTargetClasses() {
return this.targetClasses;
}
public List<String> getTargetTests() {
return this.targetTests;
}
public List<String> getExcludedMethods() {
return this.excludedMethods;
}
public List<String> getExcludedClasses() {
return this.excludedClasses;
}
public List<String> getAvoidCallsTo() {
return this.avoidCallsTo;
}
public File getReportsDirectory() {
return this.reportsDirectory;
}
public int getMaxDependencyDistance() {
return this.maxDependencyDistance;
}
public int getThreads() {
return this.threads;
}
public boolean isMutateStaticInitializers() {
return this.mutateStaticInitializers;
}
public List<String> getMutators() {
return this.mutators;
}
public float getTimeoutFactor() {
return this.timeoutFactor;
}
public long getTimeoutConstant() {
return this.timeoutConstant;
}
public int getMaxMutationsPerClass() {
return this.maxMutationsPerClass;
}
public List<String> getJvmArgs() {
return this.jvmArgs;
}
public List<String> getOutputFormats() {
return this.outputFormats;
}
public boolean isVerbose() {
return this.verbose;
}
public MavenProject getProject() {
return this.project;
}
public Map<String, Artifact> getPluginArtifactMap() {
return this.pluginArtifactMap;
}
public boolean isFailWhenNoMutations() {
return this.failWhenNoMutations;
}
public List<String> getExcludedGroups() {
return this.excludedGroups;
}
public List<String> getIncludedGroups() {
return this.includedGroups;
}
public int getMutationUnitSize() {
return this.mutationUnitSize;
}
public boolean isTimestampedReports() {
return this.timestampedReports;
}
public boolean isDetectInlinedCode() {
return this.detectInlinedCode;
}
public void setTimestampedReports(final boolean timestampedReports) {
this.timestampedReports = timestampedReports;
}
public File getHistoryOutputFile() {
return this.historyOutputFile;
}
public File getHistoryInputFile() {
return this.historyInputFile;
}
public boolean isExportLineCoverage() {
return this.exportLineCoverage;
}
protected boolean shouldRun() {
return !this.skip
&& !this.skipTests
&& !this.project.getPackaging().equalsIgnoreCase("pom")
&& notEmptyProject.apply(project);
}
public String getMutationEngine() {
return this.mutationEngine;
}
public String getJavaExecutable() {
return this.jvm;
}
public void setJavaExecutable(final String javaExecutable) {
this.jvm = javaExecutable;
}
public List<String> getAdditionalClasspathElements() {
return this.additionalClasspathElements;
}
public List<String> getClasspathDependencyExcludes() {
return this.classpathDependencyExcludes;
}
public boolean isParseSurefireConfig() {
return this.parseSurefireConfig;
}
public Map<String, String> getPluginProperties() {
return pluginConfiguration;
}
public Map<String, String> getEnvironmentVariables() {
return environmentVariables;
}
public boolean useHistory() {
return this.withHistory;
}
public ArrayList<String> getExcludedRunners() {
return excludedRunners;
}
}