You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maven.apache.org by kw...@apache.org on 2023/06/26 12:25:51 UTC

[maven-surefire] branch feature/additional-classpath-via-maven-gav updated (7fdd4efbd -> c1ac74bf2)

This is an automated email from the ASF dual-hosted git repository.

kwin pushed a change to branch feature/additional-classpath-via-maven-gav
in repository https://gitbox.apache.org/repos/asf/maven-surefire.git


    omit 7fdd4efbd [SUREFIRE-2179] Support adding additional Maven  artifacts to test classpath
     new c1ac74bf2 [SUREFIRE-2179] Support adding additional Maven  artifacts to test classpath

This update added new revisions after undoing existing revisions.
That is to say, some revisions that were in the old version of the
branch are not in the new version.  This situation occurs
when a user --force pushes a change and generates a repository
containing something like this:

 * -- * -- B -- O -- O -- O   (7fdd4efbd)
            \
             N -- N -- N   refs/heads/feature/additional-classpath-via-maven-gav (c1ac74bf2)

You should already have received notification emails for all of the O
revisions, and so the following emails describe only the N revisions
from the common base, B.

Any revisions marked "omit" are not gone; other references still
refer to them.  Any revisions marked "discard" are gone forever.

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 .../plugin/surefire/AbstractSurefireMojo.java      | 92 ++++++++++++----------
 .../surefire/SurefireDependencyResolver.java       | 32 ++++++--
 .../plugin/surefire/AbstractSurefireMojoTest.java  | 12 +--
 3 files changed, 83 insertions(+), 53 deletions(-)


[maven-surefire] 01/01: [SUREFIRE-2179] Support adding additional Maven artifacts to test classpath

Posted by kw...@apache.org.
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 c1ac74bf22e33cacd14026cc2d785c1cdf625ad7
Author: Konrad Windszus <kw...@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      | 90 +++++++++++++++++++++-
 .../surefire/SurefireDependencyResolver.java       | 58 +++++++++++---
 .../maven/plugin/surefire/TestClassPath.java       |  4 +-
 .../plugin/surefire/AbstractSurefireMojoTest.java  | 16 ++--
 .../site/apt/examples/configuring-classpath.apt.vm | 30 +++++++-
 5 files changed, 173 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..edf8d9f97 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,9 @@ 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.stream.Stream;
 import java.util.zip.ZipFile;
 
 import org.apache.maven.artifact.Artifact;
@@ -118,6 +122,7 @@ import org.codehaus.plexus.languages.java.jpms.ResolvePathResult;
 import org.codehaus.plexus.languages.java.jpms.ResolvePathsRequest;
 import org.codehaus.plexus.languages.java.jpms.ResolvePathsResult;
 import org.codehaus.plexus.logging.Logger;
+import org.eclipse.aether.resolution.DependencyResolutionException;
 
 import static java.lang.Integer.parseInt;
 import static java.util.Arrays.asList;
@@ -281,6 +286,17 @@ public abstract class AbstractSurefireMojo extends AbstractMojo implements Suref
     @Parameter(property = "maven.test.additionalClasspath")
     private String[] additionalClasspathElements;
 
+    /**
+     * Maven coordinates in the format {@code <groupId>:<artifactId>[:<extension>[:<classifier>]]:<version>} of additional artifacts.
+     * Those artifacts are automatically resolved from the repository (including their transitive dependencies).
+     * Afterwards they are appended including their transitive dependencies to the classpath
+     * (after the ones from {@link #additionalClasspathElements}).
+     *
+     * @since 3.2
+     */
+    @Parameter(property = "maven.test.additionalClasspathArtifacts")
+    private String[] additionalClasspathArtifacts;
+
     /**
      * The test source directory containing test class sources.
      * Important <b>only</b> for TestNG HTML reports.
@@ -2526,8 +2542,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 +2559,69 @@ 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 (getAdditionalClasspathArtifacts() != null) {
+            Collection<Artifact> additionalArtifacts =
+                    resolveArtifacts(Arrays.stream(getAdditionalClasspathArtifacts()));
+            // 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 additionalClasspathArtifact: 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> resolveArtifacts(Stream<String> coordinatesStream) throws MojoFailureException {
+        Map<String, Artifact> dependencyConflictIdsAndArtifacts = new HashMap<>();
+        try {
+            coordinatesStream
+                    .map(coordinates -> {
+                        try {
+                            return surefireDependencyResolver.resolveArtifacts(
+                                    session.getRepositorySession(),
+                                    project.getRemoteProjectRepositories(),
+                                    coordinates);
+                        } catch (DependencyResolutionException 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 additionalClasspathArtifacts: 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 +3552,14 @@ public abstract class AbstractSurefireMojo extends AbstractMojo implements Suref
         this.additionalClasspathElements = additionalClasspathElements;
     }
 
+    public String[] getAdditionalClasspathArtifacts() {
+        return additionalClasspathArtifacts;
+    }
+
+    public void setAdditionalClasspathArtifacts(String[] additionalClasspathArtifacts) {
+        this.additionalClasspathArtifacts = additionalClasspathArtifacts;
+    }
+
     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..b394f0936 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;
@@ -143,26 +145,58 @@ 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();
+    }
+
+    /**
+     * Resolves the artifact and its transitive runtime dependencies
+     * @param coordinates
+     * @return a collection of file paths (pointing to the local repository)
+     * @throws IllegalStateException in case resolving fails
+     * @throws DependencyResolutionException
+     */
+    public Collection<Artifact> resolveArtifacts(
+            RepositorySystemSession session, List<RemoteRepository> repositories, String coordinates)
+            throws IllegalStateException, DependencyResolutionException {
+        org.eclipse.aether.artifact.Artifact resolverArtifact =
+                new org.eclipse.aether.artifact.DefaultArtifact(coordinates);
+        DependencyFilter filter = DependencyFilterUtils.classpathFilter(JavaScopes.RUNTIME);
+        List<ArtifactResult> results;
+        results = resolveDependencies(
+                session, repositories, new org.eclipse.aether.graph.Dependency(resolverArtifact, null), filter);
+        return results.stream()
+                .map(ArtifactResult::getArtifact)
+                .map(RepositoryUtils::toArtifact)
+                .collect(Collectors.toSet());
+    }
+
     @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..f4e56fce1 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,39 @@ Additional Classpath Elements
 </project>
 +---+
 
+  Since version 3.2.0 the <<<additionalClasspathArtifacts>>> parameter can be used to add arbitrary artifacts 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>>>).
+  Note that even transitive dependencies (both <<<compile>>> + <<<runtime>>> scope) are added implicitly.
+
++---+
+<project>
+  [...]
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>${project.groupId}</groupId>
+        <artifactId>${project.artifactId}</artifactId>
+        <version>${project.version}</version>
+        <configuration>
+          <additionalClasspathArtifacts>
+            <additionalClasspathArtifact>myGroupId:myArtifactId:1.0.0</additionalClasspathArtifact>
+            <additionalClasspathArtifact>myGroupId:myOtherArtifactId:1.2.0</additionalClasspathArtifact>
+          </additionalClasspathArtifacts>
+        </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>