From eb4586eea49ab572c1b7607513ac5f6c3f7ceaf6 Mon Sep 17 00:00:00 2001 From: sbuisson Date: Tue, 3 Apr 2018 00:57:54 +0200 Subject: [PATCH] same as scm, but only for the tests class --- .../java/org/pitest/maven/ScmTestMojo.java | 316 ++++++++++++++++++ 1 file changed, 316 insertions(+) create mode 100644 pitest-maven/src/main/java/org/pitest/maven/ScmTestMojo.java diff --git a/pitest-maven/src/main/java/org/pitest/maven/ScmTestMojo.java b/pitest-maven/src/main/java/org/pitest/maven/ScmTestMojo.java new file mode 100644 index 000000000..b58b17ad1 --- /dev/null +++ b/pitest-maven/src/main/java/org/pitest/maven/ScmTestMojo.java @@ -0,0 +1,316 @@ +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.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.function.Function; +import java.util.function.Predicate; + +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.ScmBranch; +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.FCollection; +import java.util.Optional; +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 test files that have been + * modified or introduced locally based on the source control configured in + * maven. + */ +@Mojo(name = "scmTestMutationCoverage", + defaultPhase = LifecyclePhase.VERIFY, + requiresDependencyResolution = ResolutionScope.TEST, + threadSafe = true) +public class ScmTestMojo extends AbstractPitMojo { + + private static final int NO_LIMIT = -1; + + @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 include; + + /** + * Analyze last commit. If set to true analyzes last commited change set. + */ + @Parameter(defaultValue = "false", property = "analyseLastCommit") + private boolean analyseLastCommit; + + + @Parameter(property = "originBranch") + private String originBranch; + + @Parameter(property = "destinationBranch", defaultValue = "master") + private String destinationBranch; + + /** + * 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. + */ + private File scmRootDir; + + public ScmTestMojo(final RunPitStrategy executionStrategy, + final ScmManager manager, Predicate filter, + PluginServices plugins, boolean analyseLastCommit, Predicate nonEmptyProjectCheck) { + super(executionStrategy, filter, plugins, nonEmptyProjectCheck); + this.manager = manager; + this.analyseLastCommit = analyseLastCommit; + } + + public ScmTestMojo() { + + } + + @Override + protected Optional analyse() throws MojoExecutionException { + + this.scmRootDir = findScmRootDir(); + this.targetTests = makeConcreteList(findModifiedTestClassNames()); + + if (this.targetTests.isEmpty()) { + this.getLog().info( + "No modified test files found - nothing to mutation test, analyseLastCommit=" + this.analyseLastCommit); + return Optional.empty(); + } + + logClassNames(); + defaultTargetTestsToGroupNameIfNoValueSet(); + final ReportOptions data = new MojoToReportOptionsConverter(this, + new SurefireConfigConverter(), filter).convert(); + data.setFailWhenNoMutations(false); + + return Optional.ofNullable(this.goalStrategy.execute(detectBaseDir(), data, + plugins, new HashMap())); + + } + + private File findScmRootDir() { + MavenProject rootProject = this.project; + while (rootProject.hasParent() && rootProject.getParent().getBasedir() != null) + rootProject = rootProject.getParent(); + return rootProject.getBasedir(); + } + + private void defaultTargetTestsToGroupNameIfNoValueSet() { + if (this.getTargetTests() == null || this.getTargetTests().isEmpty()) { + this.targetTests = makeConcreteList(Collections.singletonList(this + .getProject().getGroupId() + "*")); + } + } + + private void logClassNames() { + for (final String each : this.targetTests) { + this.getLog().info("Will mutate changed tests " + each); + } + } + + private List findModifiedTestClassNames() throws MojoExecutionException { + + final File testRoot = new File(this.project.getBuild() + .getTestSourceDirectory()); + + final List modifiedPaths = FCollection.map(findModifiedPaths(), pathByScmDir()); + return FCollection.flatMap(modifiedPaths, new PathToJavaClassConverter( + testRoot.getAbsolutePath())); + + } + + private Function pathByScmDir() { + + return new Function() { + + @Override + public String apply(final String a) { + return scmRootDir.getAbsolutePath() + "/" + a; + } + + }; + } + + private Set findModifiedPaths() throws MojoExecutionException { + try { + final ScmRepository repository = this.manager + .makeScmRepository(getSCMConnection()); + final File scmRoot = scmRoot(); + this.getLog().info("Scm root dir is " + scmRoot); + + final Set statusToInclude = makeStatusSet(); + final Set modifiedPaths; + if (analyseLastCommit) { + modifiedPaths = lastCommitChanges(statusToInclude, repository, scmRoot); + } else if (originBranch != null && destinationBranch != null) { + modifiedPaths = changesBetweenBranchs(originBranch, destinationBranch, statusToInclude, repository, scmRoot); + } else { + modifiedPaths = localChanges(statusToInclude, repository, scmRoot); + } + return modifiedPaths; + } catch (final ScmException e) { + throw new MojoExecutionException("Error while querying scm", e); + } + + } + + private Set lastCommitChanges(Set statusToInclude, ScmRepository repository, File scmRoot) throws ScmException { + ChangeLogScmRequest scmRequest = new ChangeLogScmRequest(repository, new ScmFileSet(scmRoot)); + scmRequest.setLimit(1); + return pathsAffectedByChange(scmRequest, statusToInclude, 1); + } + + private Set changesBetweenBranchs(String origine, String destination, Set statusToInclude, ScmRepository repository, File scmRoot) throws ScmException { + ChangeLogScmRequest scmRequest = new ChangeLogScmRequest(repository, new ScmFileSet(scmRoot)); + scmRequest.setScmBranch(new ScmBranch(destination + ".." + origine)); + return pathsAffectedByChange(scmRequest, statusToInclude, NO_LIMIT); + } + + private Set pathsAffectedByChange(ChangeLogScmRequest scmRequest, Set statusToInclude, int limit) throws ScmException { + Set affected = new LinkedHashSet<>(); + ChangeLogScmResult changeLogScmResult = this.manager.changeLog(scmRequest); + if (changeLogScmResult.isSuccess()) { + List changeSets = limit(changeLogScmResult.getChangeLog().getChangeSets(),limit); + for (ChangeSet change : changeSets) { + List files = change.getFiles(); + for (final ChangeFile changeFile : files) { + if (statusToInclude.contains(changeFile.getAction())) { + affected.add(changeFile.getName()); + } + } + } + } + return affected; + } + + + private Set localChanges(Set statusToInclude, ScmRepository repository, File scmRoot) throws ScmException { + final StatusScmResult status = this.manager.status(repository, + new ScmFileSet(scmRoot)); + + Set affected = new LinkedHashSet<>(); + for (final ScmFile file : status.getChangedFiles()) { + if (statusToInclude.contains(file.getStatus())) { + affected.add(file.getPath()); + } + } + return affected; + } + + private List limit(List changeSets, int limit) { + if (limit < 0) { + return changeSets; + } + return changeSets.subList(0, limit); + } + + + private Set makeStatusSet() { + if ((this.include == null) || this.include.isEmpty()) { + return new HashSet<>(Arrays.asList( + ScmStatus.ADDED.getStatus(), ScmStatus.MODIFIED.getStatus())); + } + final Set s = new HashSet<>(); + FCollection.mapTo(this.include, stringToMavenScmStatus(), s); + return s; + } + + private static Function stringToMavenScmStatus() { + return new Function() { + @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 makeConcreteList(List list) { + return new ArrayList<>(list); + } + +}