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 764cc7f
Show file tree
Hide file tree
Showing 5 changed files with 172 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 All @@ -53,6 +57,7 @@
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;
Expand Down Expand Up @@ -281,6 +286,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.
Expand Down Expand Up @@ -2526,8 +2546,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 +2563,67 @@ 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 (getAdditionalClasspathDependencies() != null) {
Collection<Artifact> additionalArtifacts =
resolveDependencies(Arrays.stream(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(Stream<Dependency> dependencies) throws MojoFailureException {
Map<String, Artifact> dependencyConflictIdsAndArtifacts = new HashMap<>();
try {
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();
}

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

public Dependency[] getAdditionalClasspathDependencies() {
return additionalClasspathDependencies;
}

public void setAdditionalClasspathDependencies(Dependency[] additionalClasspathDependencies) {
this.additionalClasspathDependencies = additionalClasspathDependencies;
}

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 @@ -136,33 +138,50 @@ public Set<Artifact> resolveArtifacts(
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,
org.eclipse.aether.graph.Dependency dependency)
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,
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,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>
Expand Down

0 comments on commit 764cc7f

Please sign in to comment.