blob: 4bc76e44ec714557746af5e1c19c81a7f054e315 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.maven.graph;
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.apache.maven.MavenExecutionException;
import org.apache.maven.execution.BuildResumptionDataRepository;
import org.apache.maven.execution.MavenExecutionRequest;
import org.apache.maven.execution.MavenSession;
import org.apache.maven.execution.ProjectActivation;
import org.apache.maven.execution.ProjectDependencyGraph;
import org.apache.maven.model.Dependency;
import org.apache.maven.model.Parent;
import org.apache.maven.model.building.Result;
import org.apache.maven.model.locator.DefaultModelLocator;
import org.apache.maven.model.locator.ModelLocator;
import org.apache.maven.project.MavenProject;
import org.apache.maven.project.ProjectBuilder;
import org.apache.maven.project.ProjectBuildingException;
import org.apache.maven.project.ProjectBuildingRequest;
import org.apache.maven.project.ProjectBuildingResult;
import org.apache.maven.project.collector.DefaultProjectsSelector;
import org.apache.maven.project.collector.MultiModuleCollectionStrategy;
import org.apache.maven.project.collector.PomlessCollectionStrategy;
import org.apache.maven.project.collector.ProjectsSelector;
import org.apache.maven.project.collector.RequestPomCollectionStrategy;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import static java.util.Arrays.asList;
import static java.util.Collections.emptyList;
import static java.util.Collections.singletonList;
import static java.util.function.Function.identity;
import static java.util.stream.Collectors.toList;
import static org.apache.maven.execution.MavenExecutionRequest.REACTOR_MAKE_DOWNSTREAM;
import static org.apache.maven.execution.MavenExecutionRequest.REACTOR_MAKE_UPSTREAM;
import static org.apache.maven.graph.DefaultGraphBuilderTest.ScenarioBuilder.scenario;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyList;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
class DefaultGraphBuilderTest {
/*
The multi-module structure in this project is displayed as follows:
module-parent
└─── module-independent (without parent declaration)
module-a
module-b (depends on module-a)
module-c
└─── module-c-1
module-c-2 (depends on module-b)
module-d (packaging is bom)
*/
private static final String GROUP_ID = "unittest";
private static final String PARENT_MODULE = "module-parent";
private static final String INDEPENDENT_MODULE = "module-independent";
private static final String MODULE_A = "module-a";
private static final String MODULE_B = "module-b";
private static final String MODULE_C = "module-c";
private static final String MODULE_D = "module-d";
private static final String MODULE_C_1 = "module-c-1";
private static final String MODULE_C_2 = "module-c-2";
private DefaultGraphBuilder graphBuilder;
private final ProjectBuilder projectBuilder = mock(ProjectBuilder.class);
private final MavenSession session = mock(MavenSession.class);
private final MavenExecutionRequest mavenExecutionRequest = mock(MavenExecutionRequest.class);
private final ProjectsSelector projectsSelector = new DefaultProjectsSelector(projectBuilder);
// Not using mocks for these strategies - a mock would just copy the actual implementation.
private final ModelLocator modelLocator = new DefaultModelLocator();
private final PomlessCollectionStrategy pomlessCollectionStrategy = new PomlessCollectionStrategy(projectBuilder);
private final MultiModuleCollectionStrategy multiModuleCollectionStrategy =
new MultiModuleCollectionStrategy(modelLocator, projectsSelector);
private final RequestPomCollectionStrategy requestPomCollectionStrategy =
new RequestPomCollectionStrategy(projectsSelector);
private Map<String, MavenProject> artifactIdProjectMap;
public static Stream<Arguments> parameters() {
return Stream.of(
scenario("Full reactor in order")
.expectResult(
PARENT_MODULE,
MODULE_C,
MODULE_C_1,
MODULE_A,
MODULE_B,
MODULE_C_2,
INDEPENDENT_MODULE),
scenario("Selected project").activeRequiredProjects(MODULE_B).expectResult(MODULE_B),
scenario("Selected aggregator project (including child modules)")
.activeRequiredProjects(MODULE_C)
.expectResult(MODULE_C, MODULE_C_1, MODULE_C_2),
scenario("Selected aggregator project with non-recursive")
.activeRequiredProjects(MODULE_C)
.nonRecursive()
.expectResult(MODULE_C),
scenario("Selected optional project")
.activeOptionalProjects(MODULE_B)
.expectResult(MODULE_B),
scenario("Selected missing optional project")
.activeOptionalProjects("non-existing-module")
.expectResult(
PARENT_MODULE,
MODULE_C,
MODULE_C_1,
MODULE_A,
MODULE_B,
MODULE_C_2,
INDEPENDENT_MODULE),
scenario("Selected missing optional and required project")
.activeOptionalProjects("non-existing-module")
.activeRequiredProjects(MODULE_B)
.expectResult(MODULE_B),
scenario("Excluded project")
.inactiveRequiredProjects(MODULE_B)
.expectResult(PARENT_MODULE, MODULE_C, MODULE_C_1, MODULE_A, MODULE_C_2, INDEPENDENT_MODULE),
scenario("Excluded optional project")
.inactiveOptionalProjects(MODULE_B)
.expectResult(PARENT_MODULE, MODULE_C, MODULE_C_1, MODULE_A, MODULE_C_2, INDEPENDENT_MODULE),
scenario("Excluded missing optional project")
.inactiveOptionalProjects("non-existing-module")
.expectResult(
PARENT_MODULE,
MODULE_C,
MODULE_C_1,
MODULE_A,
MODULE_B,
MODULE_C_2,
INDEPENDENT_MODULE),
scenario("Excluded missing optional and required project")
.inactiveOptionalProjects("non-existing-module")
.inactiveRequiredProjects(MODULE_B)
.expectResult(PARENT_MODULE, MODULE_C, MODULE_C_1, MODULE_A, MODULE_C_2, INDEPENDENT_MODULE),
scenario("Excluded aggregator project with non-recursive")
.inactiveRequiredProjects(MODULE_C)
.nonRecursive()
.expectResult(PARENT_MODULE, MODULE_C_1, MODULE_A, MODULE_B, MODULE_C_2, INDEPENDENT_MODULE),
scenario("Selected and excluded same project")
.activeRequiredProjects(MODULE_A)
.inactiveRequiredProjects(MODULE_A)
.expectResult(MavenExecutionException.class, "empty reactor"),
scenario("Excluded aggregator, but selected child")
.activeRequiredProjects(MODULE_C_1)
.inactiveRequiredProjects(MODULE_C)
.expectResult(MavenExecutionException.class, "empty reactor"),
scenario("Project selected with different selector resolves to same project")
.activeRequiredProjects(GROUP_ID + ":" + MODULE_A)
.inactiveRequiredProjects(MODULE_A)
.expectResult(MavenExecutionException.class, "empty reactor"),
scenario("Selected and excluded same project, but also selected another project")
.activeRequiredProjects(MODULE_A, MODULE_B)
.inactiveRequiredProjects(MODULE_A)
.expectResult(MODULE_B),
scenario("Selected missing project as required and as optional")
.activeRequiredProjects("non-existing-module")
.activeOptionalProjects("non-existing-module")
.expectResult(MavenExecutionException.class, "not find the selected project"),
scenario("Resuming from project")
.resumeFrom(MODULE_B)
.expectResult(MODULE_B, MODULE_C_2, INDEPENDENT_MODULE),
scenario("Selected project with also make dependencies")
.activeRequiredProjects(MODULE_C_2)
.makeBehavior(REACTOR_MAKE_UPSTREAM)
.expectResult(PARENT_MODULE, MODULE_C, MODULE_A, MODULE_B, MODULE_C_2),
scenario("Selected project with also make dependents")
.activeRequiredProjects(MODULE_B)
.makeBehavior(REACTOR_MAKE_DOWNSTREAM)
.expectResult(MODULE_B, MODULE_C_2),
scenario("Resuming from project with also make dependencies")
.makeBehavior(REACTOR_MAKE_UPSTREAM)
.resumeFrom(MODULE_C_2)
.expectResult(PARENT_MODULE, MODULE_C, MODULE_A, MODULE_B, MODULE_C_2, INDEPENDENT_MODULE),
scenario("Selected project with resume from and also make dependency (MNG-4960 IT#1)")
.activeRequiredProjects(MODULE_C_2)
.resumeFrom(MODULE_B)
.makeBehavior(REACTOR_MAKE_UPSTREAM)
.expectResult(PARENT_MODULE, MODULE_C, MODULE_A, MODULE_B, MODULE_C_2),
scenario("Selected project with resume from and also make dependent (MNG-4960 IT#2)")
.activeRequiredProjects(MODULE_B)
.resumeFrom(MODULE_C_2)
.makeBehavior(REACTOR_MAKE_DOWNSTREAM)
.expectResult(MODULE_C_2),
scenario("Excluding an also make dependency from selectedProject does take its transitive dependency")
.activeRequiredProjects(MODULE_C_2)
.inactiveRequiredProjects(MODULE_B)
.makeBehavior(REACTOR_MAKE_UPSTREAM)
.expectResult(PARENT_MODULE, MODULE_C, MODULE_A, MODULE_C_2),
scenario("Excluding a project also excludes its children")
.inactiveRequiredProjects(MODULE_C)
.expectResult(PARENT_MODULE, MODULE_A, MODULE_B, INDEPENDENT_MODULE),
scenario("Excluding an also make dependency from resumeFrom does take its transitive dependency")
.resumeFrom(MODULE_C_2)
.inactiveRequiredProjects(MODULE_B)
.makeBehavior(REACTOR_MAKE_UPSTREAM)
.expectResult(PARENT_MODULE, MODULE_C, MODULE_A, MODULE_C_2, INDEPENDENT_MODULE),
scenario("Resume from exclude project downstream")
.resumeFrom(MODULE_A)
.inactiveRequiredProjects(MODULE_B)
.expectResult(MODULE_A, MODULE_C_2, INDEPENDENT_MODULE),
scenario("Exclude the project we are resuming from (as proposed in MNG-6676)")
.resumeFrom(MODULE_B)
.inactiveRequiredProjects(MODULE_B)
.expectResult(MODULE_C_2, INDEPENDENT_MODULE),
scenario("Selected projects in wrong order are resumed correctly in order")
.activeRequiredProjects(MODULE_C_2, MODULE_B, MODULE_A)
.resumeFrom(MODULE_B)
.expectResult(MODULE_B, MODULE_C_2),
scenario("Duplicate projects are filtered out")
.activeRequiredProjects(MODULE_A, MODULE_A)
.expectResult(MODULE_A),
scenario("Select reactor by specific pom")
.requestedPom(MODULE_C)
.expectResult(MODULE_C, MODULE_C_1, MODULE_C_2),
scenario("Select reactor by specific pom with also make dependencies")
.requestedPom(MODULE_C)
.makeBehavior(REACTOR_MAKE_UPSTREAM)
.expectResult(PARENT_MODULE, MODULE_C, MODULE_C_1, MODULE_A, MODULE_B, MODULE_C_2),
scenario("Select reactor by specific pom with also make dependents")
.requestedPom(MODULE_B)
.makeBehavior(REACTOR_MAKE_DOWNSTREAM)
.expectResult(MODULE_B, MODULE_C_2));
}
interface ExpectedResult {}
static class SelectedProjectsResult implements ExpectedResult {
final List<String> projectNames;
public SelectedProjectsResult(List<String> projectSelectors) {
this.projectNames = projectSelectors;
}
}
static class ExceptionThrown implements ExpectedResult {
final Class<? extends Throwable> expected;
final String partOfMessage;
public ExceptionThrown(final Class<? extends Throwable> expected, final String partOfMessage) {
this.expected = expected;
this.partOfMessage = partOfMessage;
}
}
@ParameterizedTest
@MethodSource("parameters")
void testGetReactorProjects(
String parameterDescription,
List<String> parameterActiveRequiredProjects,
List<String> parameterActiveOptionalProjects,
List<String> parameterInactiveRequiredProjects,
List<String> parameterInactiveOptionalProjects,
String parameterResumeFrom,
String parameterMakeBehavior,
ExpectedResult parameterExpectedResult,
File parameterRequestedPom,
boolean parameterRecursive) {
// Given
ProjectActivation projectActivation = new ProjectActivation();
parameterActiveRequiredProjects.forEach(projectActivation::activateRequiredProject);
parameterActiveOptionalProjects.forEach(projectActivation::activateOptionalProject);
parameterInactiveRequiredProjects.forEach(projectActivation::deactivateRequiredProject);
parameterInactiveOptionalProjects.forEach(projectActivation::deactivateOptionalProject);
when(mavenExecutionRequest.getProjectActivation()).thenReturn(projectActivation);
when(mavenExecutionRequest.getMakeBehavior()).thenReturn(parameterMakeBehavior);
when(mavenExecutionRequest.getPom()).thenReturn(parameterRequestedPom);
when(mavenExecutionRequest.isRecursive()).thenReturn(parameterRecursive);
if (parameterResumeFrom != null && !parameterResumeFrom.isEmpty()) {
when(mavenExecutionRequest.getResumeFrom()).thenReturn(":" + parameterResumeFrom);
}
// When
Result<ProjectDependencyGraph> result = graphBuilder.build(session);
// Then
if (parameterExpectedResult instanceof SelectedProjectsResult) {
assertThat(result.hasErrors())
.withFailMessage("Expected result not to have errors")
.isFalse();
List<String> expectedProjectNames = ((SelectedProjectsResult) parameterExpectedResult).projectNames;
List<MavenProject> actualReactorProjects = result.get().getSortedProjects();
List<MavenProject> expectedReactorProjects =
expectedProjectNames.stream().map(artifactIdProjectMap::get).collect(toList());
assertEquals(expectedReactorProjects, actualReactorProjects, parameterDescription);
} else {
assertThat(result.hasErrors())
.withFailMessage("Expected result to have errors")
.isTrue();
Class<? extends Throwable> expectedException = ((ExceptionThrown) parameterExpectedResult).expected;
String partOfMessage = ((ExceptionThrown) parameterExpectedResult).partOfMessage;
assertThat(result.getProblems()).hasSize(1);
result.getProblems().forEach(p -> assertThat(p.getException())
.isInstanceOf(expectedException)
.hasMessageContaining(partOfMessage));
}
}
@Test
void testProcessPackagingAttribute() throws ProjectBuildingException {
graphBuilder = new DefaultGraphBuilder(
mock(BuildResumptionDataRepository.class),
pomlessCollectionStrategy,
multiModuleCollectionStrategy,
requestPomCollectionStrategy);
// Create projects
MavenProject projectParent = getMavenProject(PARENT_MODULE);
MavenProject projectModuleD = getMavenProject(MODULE_D, projectParent, "bom");
projectParent.setCollectedProjects(singletonList(projectModuleD));
// Set up needed mocks
when(session.getRequest()).thenReturn(mavenExecutionRequest);
when(session.getProjects()).thenReturn(null); // needed, otherwise it will be an empty list by default
when(mavenExecutionRequest.getProjectBuildingRequest()).thenReturn(mock(ProjectBuildingRequest.class));
List<ProjectBuildingResult> projectBuildingResults =
createProjectBuildingResultMocks(Stream.of(projectParent, projectModuleD)
.collect(Collectors.toMap(MavenProject::getArtifactId, identity()))
.values());
when(projectBuilder.build(anyList(), anyBoolean(), any(ProjectBuildingRequest.class)))
.thenReturn(projectBuildingResults);
ProjectActivation projectActivation = new ProjectActivation();
when(mavenExecutionRequest.getProjectActivation()).thenReturn(projectActivation);
when(mavenExecutionRequest.getPom()).thenReturn(new File(PARENT_MODULE, "pom.xml"));
Result<ProjectDependencyGraph> result = graphBuilder.build(session);
assertThat(result.hasErrors())
.withFailMessage("Expected result not to have errors")
.isFalse();
List<MavenProject> actualReactorProjects = result.get().getSortedProjects();
assertEquals(2, actualReactorProjects.size());
assertEquals("pom", actualReactorProjects.get(1).getPackaging());
}
@BeforeEach
void before() throws Exception {
graphBuilder = new DefaultGraphBuilder(
mock(BuildResumptionDataRepository.class),
pomlessCollectionStrategy,
multiModuleCollectionStrategy,
requestPomCollectionStrategy);
// Create projects
MavenProject projectParent = getMavenProject(PARENT_MODULE);
MavenProject projectIndependentModule = getMavenProject(INDEPENDENT_MODULE);
MavenProject projectModuleA = getMavenProject(MODULE_A, projectParent);
MavenProject projectModuleB = getMavenProject(MODULE_B, projectParent);
MavenProject projectModuleC = getMavenProject(MODULE_C, projectParent);
MavenProject projectModuleC1 = getMavenProject(MODULE_C_1, projectModuleC);
MavenProject projectModuleC2 = getMavenProject(MODULE_C_2, projectModuleC);
artifactIdProjectMap = Stream.of(
projectParent,
projectIndependentModule,
projectModuleA,
projectModuleB,
projectModuleC,
projectModuleC1,
projectModuleC2)
.collect(Collectors.toMap(MavenProject::getArtifactId, identity()));
// Set dependencies and modules
projectModuleB.setDependencies(singletonList(toDependency(projectModuleA)));
projectModuleC2.setDependencies(singletonList(toDependency(projectModuleB)));
projectParent.setCollectedProjects(asList(
projectIndependentModule,
projectModuleA,
projectModuleB,
projectModuleC,
projectModuleC1,
projectModuleC2));
projectModuleC.setCollectedProjects(asList(projectModuleC1, projectModuleC2));
// Set up needed mocks
when(session.getRequest()).thenReturn(mavenExecutionRequest);
when(session.getProjects()).thenReturn(null); // needed, otherwise it will be an empty list by default
when(mavenExecutionRequest.getProjectBuildingRequest()).thenReturn(mock(ProjectBuildingRequest.class));
List<ProjectBuildingResult> projectBuildingResults =
createProjectBuildingResultMocks(artifactIdProjectMap.values());
when(projectBuilder.build(anyList(), anyBoolean(), any(ProjectBuildingRequest.class)))
.thenReturn(projectBuildingResults);
}
private MavenProject getMavenProject(String artifactId, MavenProject parentProject) {
MavenProject project = getMavenProject(artifactId);
Parent parent = new Parent();
parent.setGroupId(parentProject.getGroupId());
parent.setArtifactId(parentProject.getArtifactId());
project.getModel().setParent(parent);
return project;
}
private MavenProject getMavenProject(String artifactId) {
MavenProject mavenProject = new MavenProject();
mavenProject.setGroupId(GROUP_ID);
mavenProject.setArtifactId(artifactId);
mavenProject.setVersion("1.0");
mavenProject.setPomFile(new File(artifactId, "pom.xml"));
mavenProject.setCollectedProjects(new ArrayList<>());
return mavenProject;
}
private MavenProject getMavenProject(String artifactId, MavenProject parentProject, String packaging) {
MavenProject project = getMavenProject(artifactId);
Parent parent = new Parent();
parent.setGroupId(parentProject.getGroupId());
parent.setArtifactId(parentProject.getArtifactId());
project.getModel().setParent(parent);
project.setPackaging(packaging);
return project;
}
private Dependency toDependency(MavenProject mavenProject) {
Dependency dependency = new Dependency();
dependency.setGroupId(mavenProject.getGroupId());
dependency.setArtifactId(mavenProject.getArtifactId());
dependency.setVersion(mavenProject.getVersion());
return dependency;
}
private List<ProjectBuildingResult> createProjectBuildingResultMocks(Collection<MavenProject> projects) {
return projects.stream()
.map(project -> {
ProjectBuildingResult result = mock(ProjectBuildingResult.class);
when(result.getProject()).thenReturn(project);
return result;
})
.collect(toList());
}
static class ScenarioBuilder {
private String description;
private List<String> activeRequiredProjects = emptyList();
private List<String> activeOptionalProjects = emptyList();
private List<String> inactiveRequiredProjects = emptyList();
private List<String> inactiveOptionalProjects = emptyList();
private String resumeFrom = "";
private String makeBehavior = "";
private File requestedPom = new File(PARENT_MODULE, "pom.xml");
private boolean recursive = true;
private ScenarioBuilder() {}
public static ScenarioBuilder scenario(String description) {
ScenarioBuilder scenarioBuilder = new ScenarioBuilder();
scenarioBuilder.description = description;
return scenarioBuilder;
}
public ScenarioBuilder activeRequiredProjects(String... activeRequiredProjects) {
this.activeRequiredProjects = prependWithColonIfNeeded(activeRequiredProjects);
return this;
}
public ScenarioBuilder activeOptionalProjects(String... activeOptionalProjects) {
this.activeOptionalProjects = prependWithColonIfNeeded(activeOptionalProjects);
return this;
}
public ScenarioBuilder inactiveRequiredProjects(String... inactiveRequiredProjects) {
this.inactiveRequiredProjects = prependWithColonIfNeeded(inactiveRequiredProjects);
return this;
}
public ScenarioBuilder inactiveOptionalProjects(String... inactiveOptionalProjects) {
this.inactiveOptionalProjects = prependWithColonIfNeeded(inactiveOptionalProjects);
return this;
}
public ScenarioBuilder resumeFrom(String resumeFrom) {
this.resumeFrom = resumeFrom;
return this;
}
public ScenarioBuilder makeBehavior(String makeBehavior) {
this.makeBehavior = makeBehavior;
return this;
}
public ScenarioBuilder requestedPom(String requestedPom) {
this.requestedPom = new File(requestedPom, "pom.xml");
return this;
}
public ScenarioBuilder nonRecursive() {
this.recursive = false;
return this;
}
public Arguments expectResult(String... expectedReactorProjects) {
ExpectedResult expectedResult = new SelectedProjectsResult(asList(expectedReactorProjects));
return createTestArguments(expectedResult);
}
public Arguments expectResult(Class<? extends Exception> expected, final String partOfMessage) {
ExpectedResult expectedResult = new ExceptionThrown(expected, partOfMessage);
return createTestArguments(expectedResult);
}
private Arguments createTestArguments(ExpectedResult expectedResult) {
return Arguments.arguments(
description,
activeRequiredProjects,
activeOptionalProjects,
inactiveRequiredProjects,
inactiveOptionalProjects,
resumeFrom,
makeBehavior,
expectedResult,
requestedPom,
recursive);
}
private List<String> prependWithColonIfNeeded(String[] selectors) {
return Arrays.stream(selectors).map(this::prependWithColonIfNeeded).collect(toList());
}
private String prependWithColonIfNeeded(String selector) {
return selector.indexOf(':') == -1 ? ":" + selector : selector;
}
}
}