package org.pitest.maven;
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugins.annotations.Component;
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.apache.maven.scm.ChangeFile;
import org.apache.maven.scm.ChangeSet;
import org.apache.maven.scm.ScmException;
import org.apache.maven.scm.ScmFile;
import org.apache.maven.scm.ScmFileSet;
import org.apache.maven.scm.ScmFileStatus;
import org.apache.maven.scm.command.changelog.ChangeLogScmRequest;
import org.apache.maven.scm.command.changelog.ChangeLogScmResult;
import org.apache.maven.scm.command.status.StatusScmResult;
import org.apache.maven.scm.manager.ScmManager;
import org.apache.maven.scm.repository.ScmRepository;
import org.codehaus.plexus.util.StringUtils;
import org.pitest.functional.F;
import org.pitest.functional.FCollection;
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.tooling.CombinedStatistics;
/**
* Goal which runs a coverage mutation report only for files that have been
* modified or introduced locally based on the source control configured in
* maven.
*/
@Mojo(name = "scmMutationCoverage", defaultPhase = LifecyclePhase.VERIFY, requiresDependencyResolution = ResolutionScope.TEST)
public class ScmMojo extends AbstractPitMojo {
@Component
private ScmManager manager;
/**
* List of scm status to include. Names match those defined by the maven scm
* plugin.
*
* Common values include ADDED,MODIFIED (the defaults) & UNKNOWN.
*/
@Parameter(property = "include")
private HashSet<String> include;
/**
* Analyze last commit. If set to true analyzes last commited change set.
*/
@Parameter(defaultValue = "false", property = "analyseLastCommit")
private boolean analyseLastCommit;
/**
* Connection type to use when querying scm for changed files. Can either be
* "connection" or "developerConnection".
*/
@Parameter(property = "connectionType", defaultValue = "connection")
private String connectionType;
/**
* Project basedir
*/
@Parameter(property = "basedir", required = true)
private File basedir;
/**
* Base of scm root. For a multi module project this is probably the parent
* project.
*/
@Parameter(property = "project.parent.basedir")
private File scmRootDir;
public ScmMojo(final RunPitStrategy executionStrategy,
final ScmManager manager, Predicate<Artifact> filter,
PluginServices plugins, boolean analyseLastCommit, Predicate<MavenProject> nonEmptyProjectCheck) {
super(executionStrategy, filter, plugins, nonEmptyProjectCheck);
this.manager = manager;
this.analyseLastCommit = analyseLastCommit;
}
public ScmMojo() {
}
@Override
protected Option<CombinedStatistics> analyse() throws MojoExecutionException {
this.targetClasses = makeConcreteList(findModifiedClassNames());
if (this.targetClasses.isEmpty()) {
this.getLog().info(
"No modified files found - nothing to mutation test, analyseLastCommit=" + this.analyseLastCommit);
return Option.none();
}
logClassNames();
defaultTargetTestsToGroupNameIfNoValueSet();
final ReportOptions data = new MojoToReportOptionsConverter(this,
new SurefireConfigConverter(), filter).convert();
data.setFailWhenNoMutations(false);
return Option.some(this.goalStrategy.execute(detectBaseDir(), data,
plugins, new HashMap<String, String>()));
}
private void defaultTargetTestsToGroupNameIfNoValueSet() {
if (this.getTargetTests() == null) {
this.targetTests = makeConcreteList(Collections.singletonList(this
.getProject().getGroupId() + "*"));
}
}
private void logClassNames() {
for (final String each : this.targetClasses) {
this.getLog().info("Will mutate changed class " + each);
}
}
private List<String> findModifiedClassNames() throws MojoExecutionException {
final File sourceRoot = new File(this.project.getBuild()
.getSourceDirectory());
final List<String> modifiedPaths = findModifiedPaths();
return FCollection.flatMap(modifiedPaths, new PathToJavaClassConverter(
sourceRoot.getAbsolutePath()));
}
private List<String> findModifiedPaths() throws MojoExecutionException {
try {
final Set<ScmFileStatus> statusToInclude = makeStatusSet();
final List<String> modifiedPaths = new ArrayList<String>();
final ScmRepository repository = this.manager
.makeScmRepository(getSCMConnection());
final File scmRoot = scmRoot();
this.getLog().info("Scm root dir is " + scmRoot);
if (analyseLastCommit) {
lastCommitChanges(statusToInclude, modifiedPaths, repository, scmRoot);
} else {
localChanges(statusToInclude, modifiedPaths, repository, scmRoot);
}
return modifiedPaths;
} catch (final ScmException e) {
throw new MojoExecutionException("Error while querying scm", e);
}
}
private void lastCommitChanges(Set<ScmFileStatus> statusToInclude, List<String> modifiedPaths, ScmRepository repository, File scmRoot) throws ScmException {
ChangeLogScmRequest scmRequest = new ChangeLogScmRequest(repository, new ScmFileSet(scmRoot));
scmRequest.setLimit(1);
ChangeLogScmResult changeLogScmResult = this.manager.changeLog(scmRequest);
if (changeLogScmResult.isSuccess()) {
List<ChangeSet> changeSets = changeLogScmResult.getChangeLog().getChangeSets();
if (!changeSets.isEmpty()) {
List<ChangeFile> files = changeSets.get(0).getFiles();
for (final ChangeFile changeFile : files) {
if (statusToInclude.contains(changeFile.getAction())) {
modifiedPaths.add(changeFile.getName());
}
}
}
}
}
private void localChanges(Set<ScmFileStatus> statusToInclude, List<String> modifiedPaths, ScmRepository repository, File scmRoot) throws ScmException {
final StatusScmResult status = this.manager.status(repository,
new ScmFileSet(scmRoot));
for (final ScmFile file : status.getChangedFiles()) {
if (statusToInclude.contains(file.getStatus())) {
modifiedPaths.add(file.getPath());
}
}
}
private Set<ScmFileStatus> makeStatusSet() {
if ((this.include == null) || this.include.isEmpty()) {
return new HashSet<ScmFileStatus>(Arrays.asList(
ScmStatus.ADDED.getStatus(), ScmStatus.MODIFIED.getStatus()));
}
final Set<ScmFileStatus> s = new HashSet<ScmFileStatus>();
FCollection.mapTo(this.include, stringToMavenScmStatus(), s);
return s;
}
private static F<String, ScmFileStatus> stringToMavenScmStatus() {
return new F<String, ScmFileStatus>() {
@Override
public ScmFileStatus apply(final String a) {
return ScmStatus.valueOf(a.toUpperCase()).getStatus();
}
};
}
private File scmRoot() {
if (this.scmRootDir != null) {
return this.scmRootDir;
}
return this.basedir;
}
private String getSCMConnection() throws MojoExecutionException {
if (this.project.getScm() == null) {
throw new MojoExecutionException("No SCM Connection configured.");
}
final String scmConnection = this.project.getScm().getConnection();
if ("connection".equalsIgnoreCase(this.connectionType)
&& StringUtils.isNotEmpty(scmConnection)) {
return scmConnection;
}
final String scmDeveloper = this.project.getScm().getDeveloperConnection();
if ("developerconnection".equalsIgnoreCase(this.connectionType)
&& StringUtils.isNotEmpty(scmDeveloper)) {
return scmDeveloper;
}
throw new MojoExecutionException("SCM Connection is not set.");
}
public void setConnectionType(final String connectionType) {
this.connectionType = connectionType;
}
public void setScmRootDir(final File scmRootDir) {
this.scmRootDir = scmRootDir;
}
/**
* A bug in maven 2 requires that all list fields declare a concrete list type
*/
private static ArrayList<String> makeConcreteList(List<String> list) {
return new ArrayList<String>(list);
}
}