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

org.pitest.maven.AbstractPitMojo Maven / Gradle / Ivy

There is a newer version: 1.17.1
Show newest version
package org.pitest.maven;

import org.apache.maven.artifact.Artifact;
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.eclipse.aether.RepositorySystem;
import org.pitest.coverage.CoverageSummary;
import org.pitest.mutationtest.config.PluginServices;
import org.pitest.mutationtest.config.ReportOptions;
import org.pitest.mutationtest.engine.gregor.MethodMutatorFactory;
import org.pitest.mutationtest.statistics.MutationStatistics;
import org.pitest.mutationtest.tooling.CombinedStatistics;
import org.pitest.plugin.ToolClasspathPlugin;
import org.slf4j.bridge.SLF4JBridgeHandler;
import uk.org.lidalia.sysoutslf4j.context.SysOutOverSLF4J;

import javax.inject.Inject;
import java.io.File;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Predicate;
import java.util.logging.Logger;
import java.util.stream.Collectors;

public class AbstractPitMojo extends AbstractMojo {

  private final Predicate notEmptyProject;
  
  private final Predicate   filter;

  private final PluginServices        plugins;

  /**
   * The current build session instance.
   */
  @Parameter(defaultValue = "${session}", readonly = true)
  private MavenSession session;


  // Concrete List types declared for all fields to work around maven 2 bug
  
  /**
   * Classes to include in mutation test
   */
  @Parameter(property = "targetClasses")
  private ArrayList           targetClasses;

  /**
   * Tests to run
   */
  @Parameter(property = "targetTests")
  private ArrayList           targetTests;

  /**
   * Methods not to mutate
   */
  @Parameter(property = "excludedMethods")
  private ArrayList           excludedMethods;

  /**
   * Classes not to mutate
   */
  @Parameter(property = "excludedClasses")
  private ArrayList           excludedClasses;
  
  /**
   * Classes not to run tests from
   */
  @Parameter(property = "excludedTestClasses")
  private ArrayList           excludedTestClasses;


  /**
   * Globs to be matched against method calls. No mutations will be created on
   * the same line as a match.
   */
  @Parameter(property = "avoidCallsTo")
  private ArrayList           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;  

  /**
   * Number of threads to use
   */
  @Parameter(defaultValue = "1", property = "threads")
  private int                         threads;

  /**
   * Detect inlined code
   */
  @Parameter(defaultValue = "true", property = "detectInlinedCode")
  private boolean                     detectInlinedCode;

  /**
   * Mutation operators to apply
   */
  @Parameter(property = "mutators")
  private ArrayList           mutators;
  
  /**
   * Features to activate/deactivate
   */
  @Parameter(property = "features")
  private ArrayList           features;

  /**
   * Additional features activate/deactivate, use to
   * avoid overwriting features set in the build script when
   * specifying features from the command line
   */
  @Parameter(property = "extraFeatures")
  private ArrayList           extraFeatures;


  /**
   * 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(property = "jvmArgs")
  private ArrayList           jvmArgs;

  /**
   * Single line commandline argument
   */
  @Parameter(property = "argLine")
  private String                      argLine;

  /**
   * Formats to output during analysis phase
   */
  @Parameter(property = "outputFormats")
  private ArrayList           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 = "false", property = "timestampedReports")
  private boolean                     timestampedReports;

  /**
   * TestNG Groups/JUnit Categories to exclude
   */
  @Parameter(property = "excludedGroups")
  private ArrayList           excludedGroups;

  /**
   * TestNG Groups/JUnit Categories to include
   */
  @Parameter(property = "includedGroups")
  private ArrayList           includedGroups;

  /**
   * Test methods that should be included for challenging the mutants
   */
  @Parameter(property = "includedTestMethods")
  private ArrayList           includedTestMethods;

  /**
   * Whether to create a full mutation matrix.
   * 
   * If set to true all tests covering a mutation will be executed,
   * if set to false the test execution will stop after the first killing test.
   */
  @Parameter(property = "fullMutationMatrix", defaultValue = "false")

  private boolean                     fullMutationMatrix;
  /**
   * Maximum number of mutations to include in a single analysis unit.
   * 
   * If set to 1 will analyse very slowly, but with strong (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;

  /**
   * Test strength score threshold at which to fail build
   */
  @Parameter(defaultValue = "0", property = "testStrengthThreshold")
  private int                         testStrengthThreshold;

  /**
   * 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           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           classpathDependencyExcludes;
  
  /**
   * 
   */
  @Parameter(property = "excludedRunners")
  private ArrayList           excludedRunners;

  /**
   * When set indicates that analysis of this project should be skipped
   */
  @Parameter(property = "skipPitest", 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;

  /**
   * When set will try and set the argLine based on surefire configuration. This
   * may not give the desired result in some circumstances
   */
  @Parameter(defaultValue = "true")
  private boolean                     parseSurefireArgLine;

  /**
   * honours common skipTests flag in a maven run
   */
  @Parameter(property = "skipTests", defaultValue = "false")
  private boolean                     skipTests;

  /**
   * When set will ignore failing tests when computing coverage. Otherwise, the
   * run will fail. If parseSurefireConfig is true, will be overridden from
   * surefire configuration property testFailureIgnore
   */
  @Parameter(defaultValue = "false")
  private boolean                     skipFailingTests;

  /**
   * Use slf4j for logging
   */
  @Parameter(defaultValue = "false", property = "useSlf4j")
  private boolean                     useSlf4j;

  @Parameter(property = "pit.inputEncoding", defaultValue = "${project.build.sourceEncoding}")
  private String inputEncoding;


  @Parameter(property = "pit.outputEncoding", defaultValue = "${project.reporting.outputEncoding}")
  private String outputEncoding;

  @Parameter(property = "pit.additionalSources", defaultValue = "src/main/kotlin")
  private List additionalSources;

  @Parameter(property = "pit.additionalTestSources", defaultValue = "src/test/kotlin")
  private List additionalTestSources;

  /**
   * The base directory of a multi-module project. Defaults to the execution
   * directory
   */
  @Parameter(defaultValue = "${session.executionRootDirectory}", property = "projectBase")
  private String                       projectBase;

  /**
   * Configuration properties.
   *
   * Value pairs may be used by pitest plugins.
   *
   */
  @Parameter
  private Map         pluginConfiguration;


  /**
   * environment configuration
   *
   * Value pairs may be used by pitest plugins.
   */
  @Parameter
  private Map         environmentVariables = new HashMap<>();

  /**
   * Internal: Project to interact with.
   *
   */
  @Parameter(property = "project", readonly = true, required = true)
  private MavenProject                project;

  /**
   * Internal: Map of plugin artifacts.
   */
  @Parameter(property = "plugin.artifactMap", readonly = true, required = true)
  private Map       pluginArtifactMap;
  
  
  /**
   * Communicate the classpath using a temporary jar with a classpath
   * manifest. This allows support of very large classpaths but may cause
   * issues with certain libraries.
   */
  @Parameter(property = "useClasspathJar", defaultValue = "false")
  private boolean                     useClasspathJar;

  /**
   * Amount of debug information/noise to output. The boolean
   * verbose flag overrides this value when it is set to true.
   */
  @Parameter(property = "verbosity", defaultValue = "DEFAULT")
  // should be able to use an enum here, but test harness is broken
  private String verbosity;

  private final GoalStrategy          goalStrategy;

  private final RepositorySystem repositorySystem;

  @Inject
  public AbstractPitMojo(RepositorySystem repositorySystem) {
    this(new RunPitStrategy(), new DependencyFilter(PluginServices.makeForLoader(
        AbstractPitMojo.class.getClassLoader())), PluginServices.makeForLoader(
        AbstractPitMojo.class.getClassLoader()), new NonEmptyProjectCheck(), repositorySystem);
  }

  public AbstractPitMojo(GoalStrategy strategy, Predicate filter,
      PluginServices plugins, Predicate emptyProjectCheck, RepositorySystem repositorySystem) {
    this.goalStrategy = strategy;
    this.filter = filter;
    this.plugins = plugins;
    this.notEmptyProject = emptyProjectCheck;
    this.repositorySystem = repositorySystem;
  }

  @Override
  public final void execute() throws MojoExecutionException,
      MojoFailureException {

    switchLogging();
    augmentConfig();
    RunDecision shouldRun = shouldRun();

    if (shouldRun.shouldRun()) {
      this.getLog().info("Root dir is : " + projectBase);
      for (ToolClasspathPlugin each : this.plugins
          .findToolClasspathPlugins()) {
          this.getLog().info("Found plugin : " + each.description());
      }

      this.plugins.findClientClasspathPlugins().stream()
              .filter(p -> !(p instanceof MethodMutatorFactory))
              .forEach(p -> this.getLog().info(
                      "Found shared classpath plugin : " + p.description()));

      String operators =  this.plugins.findMutationOperators().stream()
              .map(m -> m.getName())
              .collect(Collectors.joining(","));

      this.getLog().info("Available mutators : " + operators);

      final Optional result = analyse();
      if (result.isPresent()) {
        throwErrorIfTestStrengthBelowThreshold(result.get().getMutationStatistics());
        throwErrorIfScoreBelowThreshold(result.get().getMutationStatistics());
        throwErrorIfMoreThanMaximumSurvivors(result.get().getMutationStatistics());
        throwErrorIfCoverageBelowThreshold(result.get().getCoverageSummary());
      }

    } else {
      this.getLog().info("Skipping project because:");
      for (String reason : shouldRun.getReasons()) {
        this.getLog().info("  - " + reason);
      }
    }
  }

  /**
   * Maven kotlin projects often add the kotlin sources at runtime via the build helper or kotlin p;lugins.
   * Unfortunately, pitest is often has its maven goal called directly so this
   * config isn't visible to it. We therefore add them in at runtime ourselves if present.
   */
  private void augmentConfig() {
    for (File source : emptyWithoutNulls(additionalSources)) {
      if (source.exists() && ! this.project.getCompileSourceRoots().contains(source.getAbsolutePath())) {
        this.getLog().info("Adding source root " + source);
        this.project.addCompileSourceRoot(source.getAbsolutePath());
      }
    }

    for (File source : emptyWithoutNulls(additionalTestSources)) {
      if (source.exists() && ! this.project.getTestCompileSourceRoots().contains(source.getAbsolutePath())) {
        this.getLog().info("Adding test root " + source);
        this.project.addTestCompileSourceRoot(source.getAbsolutePath());
      }
    }
  }

  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 throwErrorIfTestStrengthBelowThreshold(final MutationStatistics result)
          throws MojoFailureException {
    if ((this.testStrengthThreshold != 0)
            && (result.getTestStrength() < this.testStrengthThreshold)) {
      throw new MojoFailureException("Test strength score of "
              + result.getTestStrength() + " is below threshold of "
              + this.testStrengthThreshold);
    }
  }
  
  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 Optional analyse() throws MojoExecutionException {
    final ReportOptions data = new MojoToReportOptionsConverter(this,
        new SurefireConfigConverter(this.isParseSurefireArgLine()), this.filter).convert();

    // overwrite variable from surefire with any explicitly set
    // for pitest / add additional values
    data.getEnvironmentVariables().putAll(this.environmentVariables);

    return Optional.ofNullable(this.goalStrategy.execute(detectBaseDir(), data,
        this.plugins, data.getEnvironmentVariables()));
  }

  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();
  }

  protected Predicate getFilter() {
    return filter;
  }

  protected GoalStrategy getGoalStrategy() {
    return goalStrategy;
  }

  protected PluginServices getPlugins() {
    return plugins;
  }

  public List getTargetClasses() {
    return withoutNulls(this.targetClasses);
  }

  public void setTargetClasses(ArrayList targetClasses) {
    this.targetClasses = targetClasses;
  }

  public List getTargetTests() {
    return withoutNulls(this.targetTests);
  }

  public void setTargetTests(ArrayList targetTests) {
    this.targetTests = targetTests;
  }

  public List getExcludedMethods() {
    return withoutNulls(this.excludedMethods);
  }

  public List getExcludedClasses() {
    return withoutNulls(this.excludedClasses);
  }

  public List getAvoidCallsTo() {
    return withoutNulls(this.avoidCallsTo);
  }

  public File getReportsDirectory() {
    return this.reportsDirectory;
  }

  public int getThreads() {
    return this.threads;
  }

  public List getMutators() {
    return withoutNulls(this.mutators);
  }

  public float getTimeoutFactor() {
    return this.timeoutFactor;
  }

  public long getTimeoutConstant() {
    return this.timeoutConstant;
  }

  public ArrayList getExcludedTestClasses() {
    return withoutNulls(excludedTestClasses);
  }

  public int getMaxMutationsPerClass() {
    return this.maxMutationsPerClass;
  }

  public List getJvmArgs() {
    return withoutNulls(this.jvmArgs);
  }

  public String getArgLine() {
    return argLine;
  }

  public List getOutputFormats() {
    return withoutNulls(this.outputFormats);
  }

  public boolean isVerbose() {
    return this.verbose;
  }

  public MavenProject getProject() {
    return this.project;
  }

  public Map getPluginArtifactMap() {
    return this.pluginArtifactMap;
  }

  public boolean isFailWhenNoMutations() {
    return this.failWhenNoMutations;
  }

  public List getExcludedGroups() {
    return withoutNulls(this.excludedGroups);
  }

  public List getIncludedGroups() {
    return withoutNulls(this.includedGroups);
  }

  public List getIncludedTestMethods() {
    return withoutNulls(this.includedTestMethods);
  }

  public boolean isFullMutationMatrix() {
    return fullMutationMatrix;
  }

  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;
  }

  public Charset getSourceEncoding() {
    if (inputEncoding != null) {
      return Charset.forName(inputEncoding);
    }
    return Charset.defaultCharset();
  }

  public Charset getOutputEncoding() {
    if (outputEncoding != null) {
      return Charset.forName(outputEncoding);
    }
    return Charset.defaultCharset();
  }

  protected RunDecision shouldRun() {
    RunDecision decision = new RunDecision();

    if (this.skip) {
      decision.addReason("Execution of PIT should be skipped.");
    }

    if (this.skipTests) {
      decision.addReason("Test execution should be skipped (-DskipTests).");
    }

    if ("pom".equalsIgnoreCase(this.project.getPackaging())) {
      decision.addReason("Packaging is POM.");
    }

    if (!notEmptyProject.test(project)) {
      decision.addReason("Project has either no tests or no production code.");
    }

    return decision;
  }

  public String getMutationEngine() {
    return this.mutationEngine;
  }

  public String getJavaExecutable() {
    return this.jvm;
  }

  public List getAdditionalClasspathElements() {
    return withoutNulls(this.additionalClasspathElements);
  }

  public List getClasspathDependencyExcludes() {
    return withoutNulls(this.classpathDependencyExcludes);
  }

  public boolean isParseSurefireConfig() {
    return this.parseSurefireConfig;
  }

  public boolean isParseSurefireArgLine() {
    return this.parseSurefireArgLine;
  }

  public boolean skipFailingTests() {
    return this.skipFailingTests;
  }

  public Map getPluginProperties() {
    return pluginConfiguration;
  }

  public Map getEnvironmentVariables() {
    return environmentVariables;
  }

  public boolean useHistory() {
    return this.withHistory;
  }

  public ArrayList getExcludedRunners() {
    return withoutNulls(excludedRunners);
  }
  
  public ArrayList getFeatures() {
    ArrayList consolidated = emptyWithoutNulls(features);
    consolidated.addAll(emptyWithoutNulls(extraFeatures));
    return consolidated;
  }

  public boolean isUseClasspathJar() {
    return this.useClasspathJar;
  }

  public String getVerbosity() {
    return verbosity;
  }

  public String getProjectBase() {
    return projectBase;
  }

  public MavenSession session() {
    return session;
  }

  public RepositorySystem repositorySystem() {
    return repositorySystem;
  }

  static class RunDecision {
    private List reasons = new ArrayList<>(4);

    boolean shouldRun() {
      return reasons.isEmpty();
    }

    public void addReason(String reason) {
      reasons.add(reason);
    }

    public List getReasons() {
      return Collections.unmodifiableList(reasons);
    }
  }

  private  ArrayList emptyWithoutNulls(List originalList) {
    if (originalList == null) {
      return new ArrayList<>();
    }

    return withoutNulls(originalList);
  }

  private  ArrayList withoutNulls(List originalList) {
    if (originalList == null) {
      return null;
    }

    return originalList.stream()
        .filter(Objects::nonNull)
        .collect(Collectors.toCollection(ArrayList::new));
  }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy