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 6 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 @@ -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 used in the test execution classpath.
kwin marked this conversation as resolved.
Show resolved Hide resolved
* 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 execution classpath
* (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
Original file line number Diff line number Diff line change
Expand Up @@ -86,11 +86,54 @@ 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 project 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}}.
{{{https://maven.apache.org/pom.html#exclusions}Exlusions}} are supported as well.
Neither the dependency management section from the underlying POM is used nor are the conflicts among the different dependency trees
(from the project dependencies or from the additional dependencies) automatically resolved.
Conflicts lead to warnings, though, which help you clean up the classpath manually.
Only external dependencies (outside the current Maven reactor) are supported.

+---+
<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
2 changes: 1 addition & 1 deletion maven-surefire-report-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