Skip to content

Commit

Permalink
[SUREFIRE-2179] Support adding additional Maven artifacts to test
Browse files Browse the repository at this point in the history
classpath
  • Loading branch information
kwin committed Jun 26, 2023
1 parent 128bd4d commit c1ac74b
Show file tree
Hide file tree
Showing 5 changed files with 173 additions and 25 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
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;
Expand All @@ -41,6 +42,9 @@
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;
Expand Down Expand Up @@ -118,6 +122,7 @@
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;
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -2526,8 +2542,9 @@ protected ClassLoaderConfiguration getClassLoaderConfiguration() {
* 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
Expand All @@ -2542,8 +2559,69 @@ private TestClassPath generateTestClasspath() {
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();
}

/**
Expand Down Expand Up @@ -3474,6 +3552,14 @@ public void setAdditionalClasspathElements(String[] additionalClasspathElements)
this.additionalClasspathElements = additionalClasspathElements;
}

public String[] getAdditionalClasspathArtifacts() {
return additionalClasspathArtifacts;
}

public void setAdditionalClasspathArtifacts(String[] additionalClasspathArtifacts) {
this.additionalClasspathArtifacts = additionalClasspathArtifacts;
}

public String[] getClasspathDependencyExcludes() {
return classpathDependencyExcludes;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import javax.inject.Singleton;

import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
Expand All @@ -45,6 +46,7 @@
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;
Expand Down Expand Up @@ -143,26 +145,58 @@ private Set<Artifact> resolveDependencies(
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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -606,7 +606,7 @@ private StartupConfiguration startupConfigurationForProvider(ProviderInfo provid
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",
Expand Down Expand Up @@ -711,7 +711,7 @@ public void shouldCreateStartupConfigWithModularPath() throws Exception {
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");
Expand Down Expand Up @@ -855,7 +855,7 @@ public void shouldSmartlyResolveJUnit5ProviderWithJUnit4() throws Exception {
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(
Expand Down Expand Up @@ -1063,7 +1063,7 @@ public void shouldSmartlyResolveJUnit5ProviderWithVintage() throws Exception {
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));
Expand Down Expand Up @@ -1154,7 +1154,7 @@ public void shouldSmartlyResolveJUnit5ProviderWithJUnit5Commons() throws Excepti
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));
Expand Down Expand Up @@ -1268,7 +1268,7 @@ public void shouldSmartlyResolveJUnit5ProviderWithJUnit5Engine() throws Exceptio
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));
Expand Down Expand Up @@ -1541,7 +1541,7 @@ public void shouldSmartlyResolveJUnit5ProviderWithJupiterEngine() throws Excepti
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));
Expand Down Expand Up @@ -1724,7 +1724,7 @@ public void shouldSmartlyResolveJUnit5ProviderWithJupiterEngineInPluginDependenc
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));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down

0 comments on commit c1ac74b

Please sign in to comment.