From 82bdbd21edd447da7a26b57f22b56d238098ec63 Mon Sep 17 00:00:00 2001 From: Henry Coles Date: Thu, 3 Oct 2024 12:39:01 +0100 Subject: [PATCH] maven support for cross module tests --- .../pitest/classpath/DefaultCodeSource.java | 3 +- .../mutationtest/config/ReportOptions.java | 5 +- .../mutationtest/tooling/EntryPoint.java | 1 + .../resources/pit-cross-module-tests/pom.xml | 50 +++++++++--- .../org/pitest/maven/AbstractPitMojo.java | 17 +++- .../maven/MojoToReportOptionsConverter.java | 81 ++++++++++++++++--- .../org/pitest/maven/BasePitMojoTest.java | 5 ++ .../MojoToReportOptionsConverterTest.java | 57 +++++++++---- 8 files changed, 179 insertions(+), 40 deletions(-) diff --git a/pitest-entry/src/main/java/org/pitest/classpath/DefaultCodeSource.java b/pitest-entry/src/main/java/org/pitest/classpath/DefaultCodeSource.java index c9fdc61b6..469681ba1 100644 --- a/pitest-entry/src/main/java/org/pitest/classpath/DefaultCodeSource.java +++ b/pitest-entry/src/main/java/org/pitest/classpath/DefaultCodeSource.java @@ -8,6 +8,7 @@ import org.pitest.functional.Streams; import java.util.Collection; +import java.util.HashSet; import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; @@ -36,7 +37,7 @@ public Stream codeTrees() { } public Set getCodeUnderTestNames() { - return this.classPath.code().stream().collect(Collectors.toSet()); + return new HashSet<>(this.classPath.code()); } public Set getTestClassNames() { diff --git a/pitest-entry/src/main/java/org/pitest/mutationtest/config/ReportOptions.java b/pitest-entry/src/main/java/org/pitest/mutationtest/config/ReportOptions.java index 723eaf20d..843fb8b9d 100644 --- a/pitest-entry/src/main/java/org/pitest/mutationtest/config/ReportOptions.java +++ b/pitest-entry/src/main/java/org/pitest/mutationtest/config/ReportOptions.java @@ -33,12 +33,12 @@ import org.pitest.util.Verbosity; import java.io.File; -import java.io.FileInputStream; import java.io.IOException; import java.io.InputStreamReader; import java.io.Reader; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; @@ -502,8 +502,7 @@ public Optional createHistoryReader() { try { if (this.historyInputLocation.exists() && (this.historyInputLocation.length() > 0)) { - return Optional.ofNullable(new InputStreamReader(new FileInputStream( - this.historyInputLocation), StandardCharsets.UTF_8)); + return Optional.of(new InputStreamReader(Files.newInputStream(this.historyInputLocation.toPath()), StandardCharsets.UTF_8)); } return Optional.empty(); } catch (final IOException ex) { diff --git a/pitest-entry/src/main/java/org/pitest/mutationtest/tooling/EntryPoint.java b/pitest-entry/src/main/java/org/pitest/mutationtest/tooling/EntryPoint.java index 973aad398..75be0707f 100644 --- a/pitest-entry/src/main/java/org/pitest/mutationtest/tooling/EntryPoint.java +++ b/pitest-entry/src/main/java/org/pitest/mutationtest/tooling/EntryPoint.java @@ -111,6 +111,7 @@ public AnalysisResult execute(File baseDir, ReportOptions data, final LaunchOptions launchOptions = new LaunchOptions(ja, settings.getJavaExecutable(), createJvmArgs(data), environmentVariables) .usingClassPathJar(data.useClasspathJar()); + final ProjectClassPaths cps = data.getMutationClassPaths(); final CodeSource code = settings.createCodeSource(cps); diff --git a/pitest-maven-verification/src/test/resources/pit-cross-module-tests/pom.xml b/pitest-maven-verification/src/test/resources/pit-cross-module-tests/pom.xml index d98c8fbd6..dba85b996 100644 --- a/pitest-maven-verification/src/test/resources/pit-cross-module-tests/pom.xml +++ b/pitest-maven-verification/src/test/resources/pit-cross-module-tests/pom.xml @@ -6,13 +6,13 @@ pom 1.0-SNAPSHOT pit-parent-module - - - junit - junit - ${junit.version} - - + + + 4.13.1 + dev-SNAPSHOT + + + @@ -33,6 +33,7 @@ pitest-maven ${pit.version} + true false HTML @@ -46,12 +47,39 @@ + - - - 4.13.1 - + + + pitest + + + + org.pitest + pitest-maven + + + pitest + test-compile + + mutationCoverage + + + + + + + + + + + junit + junit + ${junit.version} + + + cross-tests-code cross-tests-tests diff --git a/pitest-maven/src/main/java/org/pitest/maven/AbstractPitMojo.java b/pitest-maven/src/main/java/org/pitest/maven/AbstractPitMojo.java index 514c3198b..f2560cc0f 100644 --- a/pitest-maven/src/main/java/org/pitest/maven/AbstractPitMojo.java +++ b/pitest-maven/src/main/java/org/pitest/maven/AbstractPitMojo.java @@ -332,6 +332,13 @@ public class AbstractPitMojo extends AbstractMojo { @Parameter(property = "skipTests", defaultValue = "false") private boolean skipTests; + /** + * Mutate code outside current module + */ + @Parameter(property = "crossModule", defaultValue = "false") + private boolean crossModule; + + /** * When set will ignore failing tests when computing coverage. Otherwise, the * run will fail. If parseSurefireConfig is true, will be overridden from @@ -739,7 +746,7 @@ protected RunDecision shouldRun() { decision.addReason("Packaging is POM."); } - if (!notEmptyProject.test(project)) { + if (!notEmptyProject.test(project) && !crossModule) { decision.addReason("Project has either no tests or no production code."); } @@ -816,6 +823,14 @@ public RepositorySystem repositorySystem() { return repositorySystem; } + public boolean isCrossModule() { + return crossModule; + } + + public List allProjects() { + return session.getProjects(); + } + static class RunDecision { private List reasons = new ArrayList<>(4); diff --git a/pitest-maven/src/main/java/org/pitest/maven/MojoToReportOptionsConverter.java b/pitest-maven/src/main/java/org/pitest/maven/MojoToReportOptionsConverter.java index 7ea345b49..b90da4f20 100644 --- a/pitest-maven/src/main/java/org/pitest/maven/MojoToReportOptionsConverter.java +++ b/pitest-maven/src/main/java/org/pitest/maven/MojoToReportOptionsConverter.java @@ -16,6 +16,7 @@ import org.apache.maven.artifact.Artifact; import org.apache.maven.artifact.DependencyResolutionRequiredException; +import org.apache.maven.model.Build; import org.apache.maven.model.Plugin; import org.apache.maven.plugin.logging.Log; import org.apache.maven.project.MavenProject; @@ -43,18 +44,20 @@ import java.util.Enumeration; import java.util.HashSet; import java.util.List; +import java.util.Objects; import java.util.Optional; import java.util.Properties; import java.util.Set; import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Collectors; +import java.util.stream.Stream; import static org.pitest.functional.Streams.asStream; public class MojoToReportOptionsConverter { - private final AbstractPitMojo mojo; + private final AbstractPitMojo mojo; private final Predicate dependencyFilter; private final Log log; private final SurefireConfigConverter surefireConverter; @@ -85,6 +88,8 @@ public ReportOptions convert() { autoAddJUnitPlatform(classPath); removeExcludedDependencies(classPath); + addCrossModuleDirsToClasspath(classPath); + ReportOptions option = parseReportOptions(classPath); ReportOptions withSureFire = updateFromSurefire(option); @@ -100,6 +105,15 @@ public ReportOptions convert() { } + private void addCrossModuleDirsToClasspath(List classPath) { + // Add the output directories modules we depend on to the start of the classpath. + // If we resolve cross project classes from a jar, the path match + // will fail. This is only an issue when running the pitest goal directly. + if (mojo.isCrossModule()) { + classPath.addAll(0, crossModuleDependencies()); + } + } + /** * The junit 5 plugin needs junit-platform-launcher to run, but this will not be on the classpath * of the project. We want to use the same version that surefire (and therefore the SUT) uses, not @@ -176,10 +190,22 @@ private ReportOptions parseReportOptions(final List classPath) { final ReportOptions data = new ReportOptions(); if (this.mojo.getProject().getBuild() != null) { + + List codePaths = new ArrayList<>(); + codePaths.add(this.mojo.getProject().getBuild() + .getOutputDirectory()); + + if (mojo.isCrossModule()) { + codePaths.addAll(crossModuleDependencies()); + } + this.log.info("Mutating from " - + this.mojo.getProject().getBuild().getOutputDirectory()); + + String.join(",", codePaths)); + data.setCodePaths(Collections.singleton(this.mojo.getProject().getBuild() .getOutputDirectory())); + + data.setCodePaths(codePaths); } data.setUseClasspathJar(this.mojo.isUseClasspathJar()); @@ -215,9 +241,7 @@ private ReportOptions parseReportOptions(final List classPath) { data.setLoggingClasses(this.mojo.getAvoidCallsTo()); } - final List sourceRoots = new ArrayList<>(); - sourceRoots.addAll(this.mojo.getProject().getCompileSourceRoots()); - sourceRoots.addAll(this.mojo.getProject().getTestCompileSourceRoots()); + final List sourceRoots = determineSourceRoots(); data.setSourceDirs(stringsToPaths(sourceRoots)); @@ -253,6 +277,41 @@ private ReportOptions parseReportOptions(final List classPath) { return data; } + private List determineSourceRoots() { + final List sourceRoots = new ArrayList<>(); + sourceRoots.addAll(this.mojo.getProject().getCompileSourceRoots()); + sourceRoots.addAll(this.mojo.getProject().getTestCompileSourceRoots()); + if (mojo.isCrossModule()) { + List otherRoots = dependedOnProjects().stream() + .flatMap(p -> p.getCompileSourceRoots().stream()) + .collect(Collectors.toList()); + + sourceRoots.addAll(otherRoots); + } + return sourceRoots; + } + + private Collection crossModuleDependencies() { + return dependedOnProjects().stream() + .map(MavenProject::getBuild) + .map(Build::getOutputDirectory) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + } + + private List dependedOnProjects() { + // strip version from dependencies + Set inScope = this.mojo.getProject().getDependencies().stream() + .map(p -> p.getGroupId() + ":" + p.getArtifactId()) + .collect(Collectors.toSet()); + + + return this.mojo.allProjects().stream() + .filter(p -> inScope.contains(p.getGroupId() + ":" + p.getArtifactId())) + .collect(Collectors.toList()); + + } + private void configureVerbosity(ReportOptions data) { if (this.mojo.isVerbose()) { data.setVerbosity(Verbosity.VERBOSE); @@ -384,6 +443,8 @@ private Collection useConfiguredTargetTestsOrFindOccupiedPackages( } private Collection findOccupiedTestPackages() { + // use only the tests within current project, even if in + // cross module mode String outputDirName = this.mojo.getProject().getBuild() .getTestOutputDirectory(); if (outputDirName != null) { @@ -430,10 +491,12 @@ private Collection useConfiguredTargetClassesOrFindOccupiedPackages( } private Collection findOccupiedPackages() { - String outputDirName = this.mojo.getProject().getBuild() - .getOutputDirectory(); - File outputDir = new File(outputDirName); - return findOccupiedPackagesIn(outputDir); + return Stream.concat(Stream.of(mojo.getProject()), dependedOnProjects().stream()) + .distinct() + .map(p -> new File(p.getBuild().getOutputDirectory())) + .flatMap(f -> findOccupiedPackagesIn(f).stream()) + .distinct() + .collect(Collectors.toList()); } public static Collection findOccupiedPackagesIn(File dir) { diff --git a/pitest-maven/src/test/java/org/pitest/maven/BasePitMojoTest.java b/pitest-maven/src/test/java/org/pitest/maven/BasePitMojoTest.java index 21419f6de..204c74e91 100644 --- a/pitest-maven/src/test/java/org/pitest/maven/BasePitMojoTest.java +++ b/pitest-maven/src/test/java/org/pitest/maven/BasePitMojoTest.java @@ -27,6 +27,7 @@ import java.util.stream.Collectors; import org.apache.maven.artifact.Artifact; +import org.apache.maven.execution.MavenSession; import org.apache.maven.model.Build; import org.apache.maven.plugin.testing.AbstractMojoTestCase; import org.apache.maven.project.MavenProject; @@ -45,6 +46,9 @@ public abstract class BasePitMojoTest extends AbstractMojoTestCase { @Mock protected MavenProject project; + @Mock + protected MavenSession session; + @Mock protected RunPitStrategy executionStrategy; @@ -118,6 +122,7 @@ protected void configurePitMojo(final AbstractPitMojo pitMojo, final String conf setVariableValueToObject(pitMojo, "pluginArtifactMap", pluginArtifacts); setVariableValueToObject(pitMojo, "project", this.project); + setVariableValueToObject(pitMojo, "session", this.session); if (pitMojo.getAdditionalClasspathElements() == null) { ArrayList elements = new ArrayList<>(); diff --git a/pitest-maven/src/test/java/org/pitest/maven/MojoToReportOptionsConverterTest.java b/pitest-maven/src/test/java/org/pitest/maven/MojoToReportOptionsConverterTest.java index 7edcf0798..3b741cc1c 100644 --- a/pitest-maven/src/test/java/org/pitest/maven/MojoToReportOptionsConverterTest.java +++ b/pitest-maven/src/test/java/org/pitest/maven/MojoToReportOptionsConverterTest.java @@ -17,23 +17,20 @@ import org.apache.maven.artifact.Artifact; import org.apache.maven.artifact.DependencyResolutionRequiredException; import org.apache.maven.model.Build; +import org.apache.maven.model.Dependency; import org.apache.maven.model.Model; import org.apache.maven.model.Plugin; +import org.apache.maven.project.MavenProject; import org.codehaus.plexus.util.xml.Xpp3Dom; -import org.junit.rules.TemporaryFolder; import org.mockito.Mockito; import org.pitest.mutationtest.config.ConfigOption; import org.pitest.mutationtest.config.ReportOptions; import org.pitest.util.Unchecked; import java.io.File; -import java.io.IOException; import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashSet; @@ -41,6 +38,7 @@ import java.util.Set; import java.util.function.Predicate; +import static java.util.Arrays.asList; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatCode; import static org.mockito.AdditionalAnswers.returnsFirstArg; @@ -92,9 +90,9 @@ public void testCreatesPredicateFromListOfTargetClassGlobs() { } public void testUsesSourceDirectoriesFromProject() { - when(this.project.getCompileSourceRoots()).thenReturn(Arrays.asList("src")); + when(this.project.getCompileSourceRoots()).thenReturn(asList("src")); when(this.project.getTestCompileSourceRoots()).thenReturn( - Arrays.asList("tst")); + asList("tst")); final ReportOptions actual = parseConfig(""); assertThat(actual.getSourcePaths()).containsExactly(Paths.get("src"), Paths.get("tst")); } @@ -125,7 +123,7 @@ public void testParsesListOfMutationOperators() { " bar" + // " "; final ReportOptions actual = parseConfig(xml); - assertEquals(Arrays.asList("foo", "bar"), actual.getMutators()); + assertEquals(asList("foo", "bar"), actual.getMutators()); } public void testParsesListOfFeatures() { @@ -205,7 +203,7 @@ public void testParsesListOfClassesToAvoidCallTo() { " foo.bar" + // " "; final ReportOptions actual = parseConfig(xml); - assertEquals(Arrays.asList("foo", "bar", "foo.bar"), + assertEquals(asList("foo", "bar", "foo.bar"), actual.getLoggingClasses()); } @@ -244,7 +242,7 @@ public void testParsesDetectInlineCodeFlag() { public void testDefaultsToHtmlReportWhenNoOutputFormatsSpecified() { final ReportOptions actual = parseConfig(""); - assertEquals(new HashSet<>(Arrays.asList("HTML")), + assertEquals(new HashSet<>(asList("HTML")), actual.getOutputFormats()); } @@ -254,7 +252,7 @@ public void testParsesListOfOutputFormatsWhenSupplied() { " CSV" + // " "; final ReportOptions actual = parseConfig(xml); - assertEquals(new HashSet<>(Arrays.asList("HTML", "CSV")), + assertEquals(new HashSet<>(asList("HTML", "CSV")), actual.getOutputFormats()); } @@ -276,19 +274,19 @@ public void testObeysSkipFailingTestsFlagWhenPackagingTypeIsNotPOM() { public void testParsesTestGroupsToExclude() { final ReportOptions actual = parseConfig("foobar"); - assertEquals(Arrays.asList("foo", "bar"), actual.getGroupConfig() + assertEquals(asList("foo", "bar"), actual.getGroupConfig() .getExcludedGroups()); } public void testParsesTestGroupsToInclude() { final ReportOptions actual = parseConfig("foobar"); - assertEquals(Arrays.asList("foo", "bar"), actual.getGroupConfig() + assertEquals(asList("foo", "bar"), actual.getGroupConfig() .getIncludedGroups()); } public void testParsesTestMethodsToInclude() { final ReportOptions actual = parseConfig("foobar"); - assertEquals(Arrays.asList("foo", "bar"), actual + assertEquals(asList("foo", "bar"), actual .getIncludedTestMethods()); } @@ -388,7 +386,7 @@ public void testParsesExcludedClasspathElements() artifacts.add(dependency); when(this.project.getArtifacts()).thenReturn(artifacts); when(this.project.getTestClasspathElements()).thenReturn( - Arrays.asList("group" + sep + "artifact" + sep + "1.0.0" + sep + asList("group" + sep + "artifact" + sep + "1.0.0" + sep + "group-artifact-1.0.0.jar")); final ReportOptions actual = parseConfig("" @@ -470,6 +468,35 @@ public void testEvaluatesNormalPropertiesInArgLines() { assertThat(actual.getArgLine()).isEqualTo("fooValue barValue"); } + public void testAddsModulesToMutationPathWhenCrossModule() { + MavenProject dependedOn = project("com.example", "foo"); + MavenProject notDependedOn = project("com.example", "bar"); + + when(session.getProjects()).thenReturn(asList(dependedOn, notDependedOn)); + + Dependency dependency = new Dependency(); + dependency.setGroupId("com.example"); + dependency.setArtifactId("foo"); + when(project.getDependencies()).thenReturn(asList(dependency)); + + final ReportOptions actual = parseConfig("true"); + + assertThat(actual.getCodePaths()).contains("foobuild"); + assertThat(actual.getCodePaths()).doesNotContain("barbuild"); + } + + private static MavenProject project(String group, String artefact) { + MavenProject dependedOn = new MavenProject(); + dependedOn.setGroupId(group); + dependedOn.setArtifactId(artefact); + + Build build = new Build(); + build.setOutputDirectory(artefact + "build"); + dependedOn.setBuild(build); + + return dependedOn; + } + private ReportOptions parseConfig(final String xml) { try { final String pom = createPomWithConfiguration(xml);