This is an automated email from the ASF dual-hosted git repository. kwin pushed a commit to branch feature/additional-classpath-via-maven-gav in repository https://gitbox.apache.org/repos/asf/maven-surefire.git
commit 238a1f9e401ecd5c75afe7e2774507515bca6d17 Author: Konrad Windszus <k...@apache.org> AuthorDate: Fri Jun 23 14:11:54 2023 +0200 [SUREFIRE-2179] Support adding additional Maven artifacts to test classpath --- .../plugin/surefire/AbstractSurefireMojo.java | 91 +++++++++++++++++++++- .../surefire/SurefireDependencyResolver.java | 43 +++++++--- .../maven/plugin/surefire/TestClassPath.java | 4 +- .../plugin/surefire/AbstractSurefireMojoTest.java | 16 ++-- .../site/apt/examples/configuring-classpath.apt.vm | 42 +++++++++- 5 files changed, 171 insertions(+), 25 deletions(-) diff --git a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/AbstractSurefireMojo.java b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/AbstractSurefireMojo.java index 8237a9f4e..6e8bd9672 100644 --- a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/AbstractSurefireMojo.java +++ b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/AbstractSurefireMojo.java @@ -26,6 +26,7 @@ import java.lang.reflect.Method; import java.math.BigDecimal; import java.nio.file.Files; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Enumeration; @@ -41,6 +42,8 @@ import java.util.Set; import java.util.SortedSet; import java.util.TreeSet; import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; +import java.util.stream.Collectors; import java.util.zip.ZipFile; import org.apache.maven.artifact.Artifact; @@ -53,6 +56,7 @@ import org.apache.maven.artifact.versioning.DefaultArtifactVersion; import org.apache.maven.artifact.versioning.InvalidVersionSpecificationException; import org.apache.maven.artifact.versioning.VersionRange; import org.apache.maven.execution.MavenSession; +import org.apache.maven.model.Dependency; import org.apache.maven.model.Plugin; import org.apache.maven.plugin.AbstractMojo; import org.apache.maven.plugin.MojoExecutionException; @@ -281,6 +285,21 @@ public abstract class AbstractSurefireMojo extends AbstractMojo implements Suref @Parameter(property = "maven.test.additionalClasspath") private String[] additionalClasspathElements; + /** + * Additional Maven dependencies to be used in the test execution classpath. + * Each element supports the parametrization like documented in <a href="https://maven.apache.org/pom.html#dependencies">POM Reference: Dependencies</a>. + * <p> + * Those dependencies are automatically collected (i.e. calculate the full dependency tree) and then all underlying artifacts resolved from the repository (including their transitive dependencies). + * Afterwards the resolved artifacts are filtered to only contain {@code compile} and {@code runtime} scoped ones and appended to the test execution classpath + * (after the ones from {@link #additionalClasspathElements}). + * <p> + * The dependency management from the project is not taken into account. + * + * @since 3.2 + */ + @Parameter(property = "maven.test.additionalClasspathDependencies") + private Dependency[] additionalClasspathDependencies; + /** * The test source directory containing test class sources. * Important <b>only</b> for TestNG HTML reports. @@ -2526,8 +2545,9 @@ public abstract class AbstractSurefireMojo extends AbstractMojo implements Suref * Generates the test classpath. * * @return the classpath elements + * @throws MojoFailureException */ - private TestClassPath generateTestClasspath() { + private TestClassPath generateTestClasspath() throws MojoFailureException { Set<Artifact> classpathArtifacts = getProject().getArtifacts(); if (getClasspathDependencyScopeExclude() != null @@ -2542,8 +2562,67 @@ public abstract class AbstractSurefireMojo extends AbstractMojo implements Suref classpathArtifacts = filterArtifacts(classpathArtifacts, dependencyFilter); } + Map<String, Artifact> dependencyConflictIdsProjectArtifacts = classpathArtifacts.stream() + .collect(Collectors.toMap(Artifact::getDependencyConflictId, Function.identity())); + Set<String> additionalClasspathElements = new HashSet<>(); + if (getAdditionalClasspathElements() != null) { + Arrays.stream(getAdditionalClasspathElements()).forEach(additionalClasspathElements::add); + } + if (getAdditionalClasspathDependencies() != null) { + Collection<Artifact> additionalArtifacts = + resolveDependencies(getAdditionalClasspathDependencies()); + // check for potential conflicts with project dependencies + for (Artifact additionalArtifact : additionalArtifacts) { + Artifact conflictingArtifact = + dependencyConflictIdsProjectArtifacts.get(additionalArtifact.getDependencyConflictId()); + if (conflictingArtifact != null + && !additionalArtifact.getVersion().equals(conflictingArtifact.getVersion())) { + getConsoleLogger() + .warning( + "Potential classpath conflict between project dependency and resolved additionalClasspathDependency: Found multiple versions of " + + additionalArtifact.getDependencyConflictId() + ": " + + additionalArtifact.getVersion() + " and " + + conflictingArtifact.getVersion()); + } + additionalClasspathElements.add(additionalArtifact.getFile().getAbsolutePath()); + } + } return new TestClassPath( - classpathArtifacts, getMainBuildPath(), getTestClassesDirectory(), getAdditionalClasspathElements()); + classpathArtifacts, getMainBuildPath(), getTestClassesDirectory(), additionalClasspathElements); + } + + protected Collection<Artifact> resolveDependencies(Dependency[] dependencies) throws MojoFailureException { + Map<String, Artifact> dependencyConflictIdsAndArtifacts = new HashMap<>(); + try { + Arrays.stream(dependencies) + .map(dependency -> { + try { + return surefireDependencyResolver.resolveDependencies( + session.getRepositorySession(), project.getRemoteProjectRepositories(), dependency); + } catch (MojoExecutionException e) { + throw new IllegalStateException(e); + } + }) + .forEach(artifacts -> { + for (Artifact a : artifacts) { + Artifact conflictingArtifact = + dependencyConflictIdsAndArtifacts.get(a.getDependencyConflictId()); + if (conflictingArtifact != null + && !a.getVersion().equals(conflictingArtifact.getVersion())) { + getConsoleLogger() + .warning( + "Potential classpath conflict among resolved additionalClasspathDependencies: Found multiple versions of " + + a.getDependencyConflictId() + ": " + a.getVersion() + " and " + + conflictingArtifact.getVersion()); + } else { + dependencyConflictIdsAndArtifacts.put(a.getDependencyConflictId(), a); + } + } + }); + } catch (IllegalStateException e) { + throw new MojoFailureException(e.getMessage(), e.getCause()); + } + return dependencyConflictIdsAndArtifacts.values(); } /** @@ -3474,6 +3553,14 @@ public abstract class AbstractSurefireMojo extends AbstractMojo implements Suref this.additionalClasspathElements = additionalClasspathElements; } + public Dependency[] getAdditionalClasspathDependencies() { + return additionalClasspathDependencies; + } + + public void setAdditionalClasspathDependencies(Dependency[] additionalClasspathDependencies) { + this.additionalClasspathDependencies = additionalClasspathDependencies; + } + public String[] getClasspathDependencyExcludes() { return classpathDependencyExcludes; } diff --git a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/SurefireDependencyResolver.java b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/SurefireDependencyResolver.java index b581a88bb..46739e347 100644 --- a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/SurefireDependencyResolver.java +++ b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/SurefireDependencyResolver.java @@ -25,6 +25,7 @@ import javax.inject.Named; import javax.inject.Singleton; import java.util.Collection; +import java.util.Collections; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.LinkedHashSet; @@ -45,6 +46,7 @@ import org.apache.maven.plugin.MojoExecutionException; import org.eclipse.aether.RepositorySystem; import org.eclipse.aether.RepositorySystemSession; import org.eclipse.aether.collection.CollectRequest; +import org.eclipse.aether.graph.DependencyFilter; import org.eclipse.aether.repository.RemoteRepository; import org.eclipse.aether.resolution.ArtifactResult; import org.eclipse.aether.resolution.DependencyRequest; @@ -136,6 +138,13 @@ class SurefireDependencyResolver { return resolveDependencies(session, repositories, RepositoryUtils.toDependency(artifact, null)); } + public Set<Artifact> resolveDependencies( + RepositorySystemSession session, List<RemoteRepository> repositories, Dependency dependency) + throws MojoExecutionException { + return resolveDependencies( + session, repositories, RepositoryUtils.toDependency(dependency, session.getArtifactTypeRegistry())); + } + private Set<Artifact> resolveDependencies( RepositorySystemSession session, List<RemoteRepository> repositories, @@ -143,26 +152,36 @@ class SurefireDependencyResolver { throws MojoExecutionException { try { - - CollectRequest collectRequest = new CollectRequest(); - collectRequest.setRoot(dependency); - collectRequest.setRepositories(repositories); - - DependencyRequest request = new DependencyRequest(); - request.setCollectRequest(collectRequest); - request.setFilter(DependencyFilterUtils.classpathFilter(JavaScopes.RUNTIME)); - - DependencyResult dependencyResult = repositorySystem.resolveDependencies(session, request); - return dependencyResult.getArtifactResults().stream() + List<ArtifactResult> results = resolveDependencies( + session, repositories, dependency, DependencyFilterUtils.classpathFilter(JavaScopes.RUNTIME)); + return results.stream() .map(ArtifactResult::getArtifact) .map(RepositoryUtils::toArtifact) - .collect(Collectors.toSet()); + .collect(Collectors.toCollection(LinkedHashSet::new)); } catch (DependencyResolutionException e) { throw new MojoExecutionException(e.getMessage(), e); } } + private List<ArtifactResult> resolveDependencies( + RepositorySystemSession session, + List<RemoteRepository> repositories, + org.eclipse.aether.graph.Dependency dependency, + DependencyFilter dependencyFilter) + throws DependencyResolutionException { + + // use a collect request without a root in order to not resolve optional dependencies + CollectRequest collectRequest = new CollectRequest(Collections.singletonList(dependency), null, repositories); + + DependencyRequest request = new DependencyRequest(); + request.setCollectRequest(collectRequest); + request.setFilter(dependencyFilter); + + DependencyResult dependencyResult = repositorySystem.resolveDependencies(session, request); + return dependencyResult.getArtifactResults(); + } + @Nonnull Set<Artifact> getProviderClasspath( RepositorySystemSession session, diff --git a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/TestClassPath.java b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/TestClassPath.java index f3379564f..d86910342 100644 --- a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/TestClassPath.java +++ b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/TestClassPath.java @@ -34,13 +34,13 @@ final class TestClassPath { private final Iterable<Artifact> artifacts; private final File classesDirectory; private final File testClassesDirectory; - private final String[] additionalClasspathElements; + private final Iterable<String> additionalClasspathElements; TestClassPath( Iterable<Artifact> artifacts, File classesDirectory, File testClassesDirectory, - String[] additionalClasspathElements) { + Iterable<String> additionalClasspathElements) { this.artifacts = artifacts; this.classesDirectory = classesDirectory; this.testClassesDirectory = testClassesDirectory; diff --git a/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/AbstractSurefireMojoTest.java b/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/AbstractSurefireMojoTest.java index 0fc3ccded..22bb2e78d 100644 --- a/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/AbstractSurefireMojoTest.java +++ b/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/AbstractSurefireMojoTest.java @@ -606,7 +606,7 @@ public class AbstractSurefireMojoTest { File classesDir = mockFile("classes"); File testClassesDir = mockFile("test-classes"); TestClassPath testClassPath = - new TestClassPath(new ArrayList<Artifact>(), classesDir, testClassesDir, new String[0]); + new TestClassPath(new ArrayList<Artifact>(), classesDir, testClassesDir, Collections.emptyList()); Artifact common = new DefaultArtifact( "org.apache.maven.surefire", @@ -711,7 +711,7 @@ public class AbstractSurefireMojoTest { File testClassesDirectory = new File(baseDir, "mock-dir"); mojo.setTestClassesDirectory(testClassesDirectory); TestClassPath testClassPath = new TestClassPath( - Collections.<Artifact>emptySet(), classesDirectory, testClassesDirectory, new String[0]); + Collections.<Artifact>emptySet(), classesDirectory, testClassesDirectory, Collections.emptyList()); ProviderInfo providerInfo = mock(ProviderInfo.class); when(providerInfo.getProviderName()).thenReturn("provider mock"); @@ -855,7 +855,7 @@ public class AbstractSurefireMojoTest { when(dependencyResolver.getProviderClasspathAsMap(any(), any(), anyString(), anyString())) .thenReturn(artifactMapByVersionlessId(createSurefireProviderResolutionResult(surefireVersion))); - when(dependencyResolver.resolveArtifacts(any(), any(), any())) + when(dependencyResolver.resolveArtifacts(any(), any(), any(Artifact.class))) .thenReturn(createExpectedJUnitPlatformLauncherResolutionResult()); final Artifact pluginDep1 = new DefaultArtifact( @@ -1063,7 +1063,7 @@ public class AbstractSurefireMojoTest { when(dependencyResolver.getProviderClasspathAsMap(any(), any(), anyString(), anyString())) .thenReturn(artifactMapByVersionlessId(createSurefireProviderResolutionResult(surefireVersion))); - when(dependencyResolver.resolveArtifacts(any(), any(), any())) + when(dependencyResolver.resolveArtifacts(any(), any(), any(Artifact.class))) .thenReturn(createExpectedJUnitPlatformLauncherResolutionResult()); mojo.setLogger(mock(Logger.class)); @@ -1154,7 +1154,7 @@ public class AbstractSurefireMojoTest { when(dependencyResolver.getProviderClasspathAsMap(any(), any(), anyString(), anyString())) .thenReturn(artifactMapByVersionlessId(createSurefireProviderResolutionResult(surefireVersion))); - when(dependencyResolver.resolveArtifacts(any(), any(), any())) + when(dependencyResolver.resolveArtifacts(any(), any(), any(Artifact.class))) .thenReturn(createExpectedJUnitPlatformLauncherResolutionResult()); mojo.setLogger(mock(Logger.class)); @@ -1268,7 +1268,7 @@ public class AbstractSurefireMojoTest { when(dependencyResolver.getProviderClasspathAsMap(any(), any(), anyString(), anyString())) .thenReturn(artifactMapByVersionlessId(createSurefireProviderResolutionResult(surefireVersion))); - when(dependencyResolver.resolveArtifacts(any(), any(), any())) + when(dependencyResolver.resolveArtifacts(any(), any(), any(Artifact.class))) .thenReturn(createExpectedJUnitPlatformLauncherResolutionResult()); mojo.setLogger(mock(Logger.class)); @@ -1541,7 +1541,7 @@ public class AbstractSurefireMojoTest { when(dependencyResolver.getProviderClasspathAsMap(any(), any(), anyString(), anyString())) .thenReturn(artifactMapByVersionlessId(createSurefireProviderResolutionResult(surefireVersion))); - when(dependencyResolver.resolveArtifacts(any(), any(), any())) + when(dependencyResolver.resolveArtifacts(any(), any(), any(Artifact.class))) .thenReturn(createExpectedJUnitPlatformLauncherResolutionResult()); mojo.setLogger(mock(Logger.class)); @@ -1724,7 +1724,7 @@ public class AbstractSurefireMojoTest { when(dependencyResolver.getProviderClasspathAsMap(any(), any(), anyString(), anyString())) .thenReturn(artifactMapByVersionlessId(createSurefireProviderResolutionResult(surefireVersion))); - when(dependencyResolver.resolveArtifacts(any(), any(), any())) + when(dependencyResolver.resolveArtifacts(any(), any(), any(Artifact.class))) .thenReturn(createExpectedJUnitPlatformLauncherResolutionResult()); mojo.setLogger(mock(Logger.class)); diff --git a/maven-surefire-plugin/src/site/apt/examples/configuring-classpath.apt.vm b/maven-surefire-plugin/src/site/apt/examples/configuring-classpath.apt.vm index 76d9b8776..c77532795 100644 --- a/maven-surefire-plugin/src/site/apt/examples/configuring-classpath.apt.vm +++ b/maven-surefire-plugin/src/site/apt/examples/configuring-classpath.apt.vm @@ -86,11 +86,51 @@ Additional Classpath Elements </project> +---+ + Since version 3.2.0 the <<<additionalClasspathDependencies>>> parameter can be used to add arbitrary dependencies to your test execution classpath (via their regular Maven coordinates). + Those are resolved from the repository like regular Maven dependencies and afterwards added as additional classpath elements to the end of the classpath, so you cannot use these to + override project dependencies or resources (except those which are filtered with <<<classpathDependencyExclude>>>). + All artifacts of scope <<<compile>>> and <<<runtime>>> scope from the dependency tree rooted in the given dependency are added. + The parametrization works like for regular {{{https://maven.apache.org/pom.html#dependencies}}Maven dependencies in a POM}. + Also exclusions can be used. + The dependency management section from the underlying POM is not used, though. + ++---+ +<project> + [...] + <build> + <plugins> + <plugin> + <groupId>${project.groupId}</groupId> + <artifactId>${project.artifactId}</artifactId> + <version>${project.version}</version> + <configuration> + <additionalClasspathDependencies> + <additionalClasspathDependency> + <groupId>myGroupId</groupId> + <artifactId>myArtifactId</artfactId> + <version>1.0.0</version> + <exclusions> + <exclusion> + <groupId>org.apache.maven</groupId> + <artifactId>maven-core</artifactId> + </exclusion> + </exclusions> + </additionalClasspathDependency> + </additionalClasspathDependencies> + </configuration> + </plugin> + </plugins> + </build> + [...] +</project> ++---+ Removing Dependency Classpath Elements Dependencies can be removed from the test classpath using the parameters <<<classpathDependencyExcludes>>> and <<<classpathDependencyScopeExclude>>>. A list of specific dependencies can be removed from the - classpath by specifying the <<<groupId:artifactId>>> to be removed. + classpath by specifying the <<<groupId:artifactId>>> to be removed. Details of the pattern matching mechanism + are outlined in the goal parameter description for <<<classpathDependencyScopeExcludes>>>. + It is important to note that this filtering is only applied to the effective project dependencies (this includes transitive project dependencies). +---+ <project>