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 <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 | 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>