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:52 UTC
[maven-surefire] 01/01: [SUREFIRE-2179] Support adding additional Maven artifacts to test classpath
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>