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>

Reply via email to