Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[SUREFIRE-2179] Support adding additional Maven dependencies to the test runtime classpath #667

Merged
merged 8 commits into from
Jun 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion maven-failsafe-plugin/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
<parent>
<groupId>org.apache.maven.surefire</groupId>
<artifactId>surefire</artifactId>
<version>3.1.3-SNAPSHOT</version>
<version>3.2.0-SNAPSHOT</version>
</parent>

<groupId>org.apache.maven.plugins</groupId>
Expand Down
2 changes: 1 addition & 1 deletion maven-surefire-common/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
<parent>
<groupId>org.apache.maven.surefire</groupId>
<artifactId>surefire</artifactId>
<version>3.1.3-SNAPSHOT</version>
<version>3.2.0-SNAPSHOT</version>
</parent>

<artifactId>maven-surefire-common</artifactId>
Expand Down
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,8 @@
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.zip.ZipFile;

import org.apache.maven.artifact.Artifact;
Expand All @@ -53,6 +56,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 @@ -238,7 +242,7 @@ public abstract class AbstractSurefireMojo extends AbstractMojo implements Suref
protected File testClassesDirectory;

/**
* List of dependencies to exclude from the test classpath.
* List of dependencies to exclude from the test classpath at runtime.
* Each item is passed as pattern to {@link PatternIncludesArtifactFilter}.
* The pattern is matched against the following artifact ids:
* <ul>
Expand All @@ -255,7 +259,7 @@ public abstract class AbstractSurefireMojo extends AbstractMojo implements Suref
private String[] classpathDependencyExcludes;

/**
* A dependency scope to exclude from the test classpath. The scope should be one of the scopes defined by
* A dependency scope to exclude from the test classpath at runtime. The scope should be one of the scopes defined by
* org.apache.maven.artifact.Artifact. This includes the following:
* <br>
* <ul>
Expand All @@ -272,7 +276,7 @@ public abstract class AbstractSurefireMojo extends AbstractMojo implements Suref
private String classpathDependencyScopeExclude;

/**
* Additional elements to be appended to the classpath.
* Additional elements to be appended to the test classpath at runtime.
* Each element must be a file system path to a JAR file or a directory containing classes.
* No wildcards are allowed here.
*
Expand All @@ -281,6 +285,26 @@ public abstract class AbstractSurefireMojo extends AbstractMojo implements Suref
@Parameter(property = "maven.test.additionalClasspath")
private String[] additionalClasspathElements;

/**
* Additional Maven dependencies to be added to the test classpath at runtime.
* 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. have their full dependency tree calculated) and then all underlying artifacts are 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 classpath at runtime
* (after the ones from {@link #additionalClasspathElements}).
* <p>
* The following differences to regular project dependency resolving apply:
* <ul>
* <li>The dependency management from the project is not taken into account.</li>
* <li>Conflicts between the different items and the project dependencies are not resolved.</li>
* <li>Only external dependencies (outside the current Maven reactor) are supported.</li>
* </ul>
*
* @since 3.2
*/
@Parameter(property = "maven.test.additionalClasspathDependencies")
private List<Dependency> additionalClasspathDependencies;

/**
* The test source directory containing test class sources.
* Important <b>only</b> for TestNG HTML reports.
Expand Down Expand Up @@ -2526,8 +2550,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 +2567,66 @@ 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 (additionalClasspathDependencies != null && !additionalClasspathDependencies.isEmpty()) {
Collection<Artifact> additionalArtifacts = resolveDependencies(additionalClasspathDependencies);
// 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(List<Dependency> dependencies) throws MojoFailureException {
Map<String, Artifact> dependencyConflictIdsAndArtifacts = new HashMap<>();
try {
dependencies.stream()
.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
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
2 changes: 1 addition & 1 deletion maven-surefire-plugin/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
<parent>
<groupId>org.apache.maven.surefire</groupId>
<artifactId>surefire</artifactId>
<version>3.1.3-SNAPSHOT</version>
<version>3.2.0-SNAPSHOT</version>
</parent>

<groupId>org.apache.maven.plugins</groupId>
Expand Down