View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.maven.graph;
20  
21  import java.io.File;
22  import java.util.ArrayList;
23  import java.util.Arrays;
24  import java.util.Collection;
25  import java.util.List;
26  import java.util.Map;
27  import java.util.stream.Collectors;
28  import java.util.stream.Stream;
29  
30  import org.apache.maven.MavenExecutionException;
31  import org.apache.maven.execution.BuildResumptionDataRepository;
32  import org.apache.maven.execution.MavenExecutionRequest;
33  import org.apache.maven.execution.MavenSession;
34  import org.apache.maven.execution.ProjectActivation;
35  import org.apache.maven.execution.ProjectDependencyGraph;
36  import org.apache.maven.model.Dependency;
37  import org.apache.maven.model.Parent;
38  import org.apache.maven.model.building.Result;
39  import org.apache.maven.model.locator.DefaultModelLocator;
40  import org.apache.maven.model.locator.ModelLocator;
41  import org.apache.maven.project.MavenProject;
42  import org.apache.maven.project.ProjectBuilder;
43  import org.apache.maven.project.ProjectBuildingRequest;
44  import org.apache.maven.project.ProjectBuildingResult;
45  import org.apache.maven.project.collector.DefaultProjectsSelector;
46  import org.apache.maven.project.collector.MultiModuleCollectionStrategy;
47  import org.apache.maven.project.collector.PomlessCollectionStrategy;
48  import org.apache.maven.project.collector.ProjectsSelector;
49  import org.apache.maven.project.collector.RequestPomCollectionStrategy;
50  import org.junit.jupiter.api.BeforeEach;
51  import org.junit.jupiter.params.ParameterizedTest;
52  import org.junit.jupiter.params.provider.Arguments;
53  import org.junit.jupiter.params.provider.MethodSource;
54  
55  import static java.util.Arrays.asList;
56  import static java.util.Collections.emptyList;
57  import static java.util.Collections.singletonList;
58  import static java.util.function.Function.identity;
59  import static java.util.stream.Collectors.toList;
60  import static org.apache.maven.execution.MavenExecutionRequest.REACTOR_MAKE_DOWNSTREAM;
61  import static org.apache.maven.execution.MavenExecutionRequest.REACTOR_MAKE_UPSTREAM;
62  import static org.apache.maven.graph.DefaultGraphBuilderTest.ScenarioBuilder.scenario;
63  import static org.assertj.core.api.Assertions.assertThat;
64  import static org.junit.jupiter.api.Assertions.assertEquals;
65  import static org.mockito.ArgumentMatchers.any;
66  import static org.mockito.ArgumentMatchers.anyBoolean;
67  import static org.mockito.ArgumentMatchers.anyList;
68  import static org.mockito.Mockito.mock;
69  import static org.mockito.Mockito.when;
70  
71  class DefaultGraphBuilderTest {
72      /*
73      The multi-module structure in this project is displayed as follows:
74  
75      module-parent
76      └─── module-independent     (without parent declaration)
77           module-a
78           module-b               (depends on module-a)
79           module-c
80           └─── module-c-1
81                module-c-2        (depends on module-b)
82       */
83      private static final String GROUP_ID = "unittest";
84      private static final String PARENT_MODULE = "module-parent";
85      private static final String INDEPENDENT_MODULE = "module-independent";
86      private static final String MODULE_A = "module-a";
87      private static final String MODULE_B = "module-b";
88      private static final String MODULE_C = "module-c";
89      private static final String MODULE_C_1 = "module-c-1";
90      private static final String MODULE_C_2 = "module-c-2";
91  
92      private DefaultGraphBuilder graphBuilder;
93  
94      private final ProjectBuilder projectBuilder = mock(ProjectBuilder.class);
95      private final MavenSession session = mock(MavenSession.class);
96      private final MavenExecutionRequest mavenExecutionRequest = mock(MavenExecutionRequest.class);
97  
98      private final ProjectsSelector projectsSelector = new DefaultProjectsSelector(projectBuilder);
99  
100     // Not using mocks for these strategies - a mock would just copy the actual implementation.
101 
102     private final ModelLocator modelLocator = new DefaultModelLocator();
103     private final PomlessCollectionStrategy pomlessCollectionStrategy = new PomlessCollectionStrategy(projectBuilder);
104     private final MultiModuleCollectionStrategy multiModuleCollectionStrategy =
105             new MultiModuleCollectionStrategy(modelLocator, projectsSelector);
106     private final RequestPomCollectionStrategy requestPomCollectionStrategy =
107             new RequestPomCollectionStrategy(projectsSelector);
108 
109     private Map<String, MavenProject> artifactIdProjectMap;
110 
111     public static Stream<Arguments> parameters() {
112         return Stream.of(
113                 scenario("Full reactor in order")
114                         .expectResult(
115                                 PARENT_MODULE,
116                                 MODULE_C,
117                                 MODULE_C_1,
118                                 MODULE_A,
119                                 MODULE_B,
120                                 MODULE_C_2,
121                                 INDEPENDENT_MODULE),
122                 scenario("Selected project").activeRequiredProjects(MODULE_B).expectResult(MODULE_B),
123                 scenario("Selected aggregator project (including child modules)")
124                         .activeRequiredProjects(MODULE_C)
125                         .expectResult(MODULE_C, MODULE_C_1, MODULE_C_2),
126                 scenario("Selected aggregator project with non-recursive")
127                         .activeRequiredProjects(MODULE_C)
128                         .nonRecursive()
129                         .expectResult(MODULE_C),
130                 scenario("Selected optional project")
131                         .activeOptionalProjects(MODULE_B)
132                         .expectResult(MODULE_B),
133                 scenario("Selected missing optional project")
134                         .activeOptionalProjects("non-existing-module")
135                         .expectResult(
136                                 PARENT_MODULE,
137                                 MODULE_C,
138                                 MODULE_C_1,
139                                 MODULE_A,
140                                 MODULE_B,
141                                 MODULE_C_2,
142                                 INDEPENDENT_MODULE),
143                 scenario("Selected missing optional and required project")
144                         .activeOptionalProjects("non-existing-module")
145                         .activeRequiredProjects(MODULE_B)
146                         .expectResult(MODULE_B),
147                 scenario("Excluded project")
148                         .inactiveRequiredProjects(MODULE_B)
149                         .expectResult(PARENT_MODULE, MODULE_C, MODULE_C_1, MODULE_A, MODULE_C_2, INDEPENDENT_MODULE),
150                 scenario("Excluded optional project")
151                         .inactiveOptionalProjects(MODULE_B)
152                         .expectResult(PARENT_MODULE, MODULE_C, MODULE_C_1, MODULE_A, MODULE_C_2, INDEPENDENT_MODULE),
153                 scenario("Excluded missing optional project")
154                         .inactiveOptionalProjects("non-existing-module")
155                         .expectResult(
156                                 PARENT_MODULE,
157                                 MODULE_C,
158                                 MODULE_C_1,
159                                 MODULE_A,
160                                 MODULE_B,
161                                 MODULE_C_2,
162                                 INDEPENDENT_MODULE),
163                 scenario("Excluded missing optional and required project")
164                         .inactiveOptionalProjects("non-existing-module")
165                         .inactiveRequiredProjects(MODULE_B)
166                         .expectResult(PARENT_MODULE, MODULE_C, MODULE_C_1, MODULE_A, MODULE_C_2, INDEPENDENT_MODULE),
167                 scenario("Excluded aggregator project with non-recursive")
168                         .inactiveRequiredProjects(MODULE_C)
169                         .nonRecursive()
170                         .expectResult(PARENT_MODULE, MODULE_C_1, MODULE_A, MODULE_B, MODULE_C_2, INDEPENDENT_MODULE),
171                 scenario("Selected and excluded same project")
172                         .activeRequiredProjects(MODULE_A)
173                         .inactiveRequiredProjects(MODULE_A)
174                         .expectResult(MavenExecutionException.class, "empty reactor"),
175                 scenario("Excluded aggregator, but selected child")
176                         .activeRequiredProjects(MODULE_C_1)
177                         .inactiveRequiredProjects(MODULE_C)
178                         .expectResult(MavenExecutionException.class, "empty reactor"),
179                 scenario("Project selected with different selector resolves to same project")
180                         .activeRequiredProjects(GROUP_ID + ":" + MODULE_A)
181                         .inactiveRequiredProjects(MODULE_A)
182                         .expectResult(MavenExecutionException.class, "empty reactor"),
183                 scenario("Selected and excluded same project, but also selected another project")
184                         .activeRequiredProjects(MODULE_A, MODULE_B)
185                         .inactiveRequiredProjects(MODULE_A)
186                         .expectResult(MODULE_B),
187                 scenario("Selected missing project as required and as optional")
188                         .activeRequiredProjects("non-existing-module")
189                         .activeOptionalProjects("non-existing-module")
190                         .expectResult(MavenExecutionException.class, "not find the selected project"),
191                 scenario("Resuming from project")
192                         .resumeFrom(MODULE_B)
193                         .expectResult(MODULE_B, MODULE_C_2, INDEPENDENT_MODULE),
194                 scenario("Selected project with also make dependencies")
195                         .activeRequiredProjects(MODULE_C_2)
196                         .makeBehavior(REACTOR_MAKE_UPSTREAM)
197                         .expectResult(PARENT_MODULE, MODULE_C, MODULE_A, MODULE_B, MODULE_C_2),
198                 scenario("Selected project with also make dependents")
199                         .activeRequiredProjects(MODULE_B)
200                         .makeBehavior(REACTOR_MAKE_DOWNSTREAM)
201                         .expectResult(MODULE_B, MODULE_C_2),
202                 scenario("Resuming from project with also make dependencies")
203                         .makeBehavior(REACTOR_MAKE_UPSTREAM)
204                         .resumeFrom(MODULE_C_2)
205                         .expectResult(PARENT_MODULE, MODULE_C, MODULE_A, MODULE_B, MODULE_C_2, INDEPENDENT_MODULE),
206                 scenario("Selected project with resume from and also make dependency (MNG-4960 IT#1)")
207                         .activeRequiredProjects(MODULE_C_2)
208                         .resumeFrom(MODULE_B)
209                         .makeBehavior(REACTOR_MAKE_UPSTREAM)
210                         .expectResult(PARENT_MODULE, MODULE_C, MODULE_A, MODULE_B, MODULE_C_2),
211                 scenario("Selected project with resume from and also make dependent (MNG-4960 IT#2)")
212                         .activeRequiredProjects(MODULE_B)
213                         .resumeFrom(MODULE_C_2)
214                         .makeBehavior(REACTOR_MAKE_DOWNSTREAM)
215                         .expectResult(MODULE_C_2),
216                 scenario("Excluding an also make dependency from selectedProject does take its transitive dependency")
217                         .activeRequiredProjects(MODULE_C_2)
218                         .inactiveRequiredProjects(MODULE_B)
219                         .makeBehavior(REACTOR_MAKE_UPSTREAM)
220                         .expectResult(PARENT_MODULE, MODULE_C, MODULE_A, MODULE_C_2),
221                 scenario("Excluding a project also excludes its children")
222                         .inactiveRequiredProjects(MODULE_C)
223                         .expectResult(PARENT_MODULE, MODULE_A, MODULE_B, INDEPENDENT_MODULE),
224                 scenario("Excluding an also make dependency from resumeFrom does take its transitive dependency")
225                         .resumeFrom(MODULE_C_2)
226                         .inactiveRequiredProjects(MODULE_B)
227                         .makeBehavior(REACTOR_MAKE_UPSTREAM)
228                         .expectResult(PARENT_MODULE, MODULE_C, MODULE_A, MODULE_C_2, INDEPENDENT_MODULE),
229                 scenario("Resume from exclude project downstream")
230                         .resumeFrom(MODULE_A)
231                         .inactiveRequiredProjects(MODULE_B)
232                         .expectResult(MODULE_A, MODULE_C_2, INDEPENDENT_MODULE),
233                 scenario("Exclude the project we are resuming from (as proposed in MNG-6676)")
234                         .resumeFrom(MODULE_B)
235                         .inactiveRequiredProjects(MODULE_B)
236                         .expectResult(MODULE_C_2, INDEPENDENT_MODULE),
237                 scenario("Selected projects in wrong order are resumed correctly in order")
238                         .activeRequiredProjects(MODULE_C_2, MODULE_B, MODULE_A)
239                         .resumeFrom(MODULE_B)
240                         .expectResult(MODULE_B, MODULE_C_2),
241                 scenario("Duplicate projects are filtered out")
242                         .activeRequiredProjects(MODULE_A, MODULE_A)
243                         .expectResult(MODULE_A),
244                 scenario("Select reactor by specific pom")
245                         .requestedPom(MODULE_C)
246                         .expectResult(MODULE_C, MODULE_C_1, MODULE_C_2),
247                 scenario("Select reactor by specific pom with also make dependencies")
248                         .requestedPom(MODULE_C)
249                         .makeBehavior(REACTOR_MAKE_UPSTREAM)
250                         .expectResult(PARENT_MODULE, MODULE_C, MODULE_C_1, MODULE_A, MODULE_B, MODULE_C_2),
251                 scenario("Select reactor by specific pom with also make dependents")
252                         .requestedPom(MODULE_B)
253                         .makeBehavior(REACTOR_MAKE_DOWNSTREAM)
254                         .expectResult(MODULE_B, MODULE_C_2));
255     }
256 
257     interface ExpectedResult {}
258 
259     static class SelectedProjectsResult implements ExpectedResult {
260         final List<String> projectNames;
261 
262         public SelectedProjectsResult(List<String> projectSelectors) {
263             this.projectNames = projectSelectors;
264         }
265     }
266 
267     static class ExceptionThrown implements ExpectedResult {
268         final Class<? extends Throwable> expected;
269         final String partOfMessage;
270 
271         public ExceptionThrown(final Class<? extends Throwable> expected, final String partOfMessage) {
272             this.expected = expected;
273             this.partOfMessage = partOfMessage;
274         }
275     }
276 
277     @ParameterizedTest
278     @MethodSource("parameters")
279     void testGetReactorProjects(
280             String parameterDescription,
281             List<String> parameterActiveRequiredProjects,
282             List<String> parameterActiveOptionalProjects,
283             List<String> parameterInactiveRequiredProjects,
284             List<String> parameterInactiveOptionalProjects,
285             String parameterResumeFrom,
286             String parameterMakeBehavior,
287             ExpectedResult parameterExpectedResult,
288             File parameterRequestedPom,
289             boolean parameterRecursive) {
290         // Given
291         ProjectActivation projectActivation = new ProjectActivation();
292         parameterActiveRequiredProjects.forEach(projectActivation::activateRequiredProject);
293         parameterActiveOptionalProjects.forEach(projectActivation::activateOptionalProject);
294         parameterInactiveRequiredProjects.forEach(projectActivation::deactivateRequiredProject);
295         parameterInactiveOptionalProjects.forEach(projectActivation::deactivateOptionalProject);
296 
297         when(mavenExecutionRequest.getProjectActivation()).thenReturn(projectActivation);
298         when(mavenExecutionRequest.getMakeBehavior()).thenReturn(parameterMakeBehavior);
299         when(mavenExecutionRequest.getPom()).thenReturn(parameterRequestedPom);
300         when(mavenExecutionRequest.isRecursive()).thenReturn(parameterRecursive);
301         if (parameterResumeFrom != null && !parameterResumeFrom.isEmpty()) {
302             when(mavenExecutionRequest.getResumeFrom()).thenReturn(":" + parameterResumeFrom);
303         }
304 
305         // When
306         Result<ProjectDependencyGraph> result = graphBuilder.build(session);
307 
308         // Then
309         if (parameterExpectedResult instanceof SelectedProjectsResult) {
310             assertThat(result.hasErrors())
311                     .withFailMessage("Expected result not to have errors")
312                     .isFalse();
313             List<String> expectedProjectNames = ((SelectedProjectsResult) parameterExpectedResult).projectNames;
314             List<MavenProject> actualReactorProjects = result.get().getSortedProjects();
315             List<MavenProject> expectedReactorProjects =
316                     expectedProjectNames.stream().map(artifactIdProjectMap::get).collect(toList());
317             assertEquals(expectedReactorProjects, actualReactorProjects, parameterDescription);
318         } else {
319             assertThat(result.hasErrors())
320                     .withFailMessage("Expected result to have errors")
321                     .isTrue();
322             Class<? extends Throwable> expectedException = ((ExceptionThrown) parameterExpectedResult).expected;
323             String partOfMessage = ((ExceptionThrown) parameterExpectedResult).partOfMessage;
324 
325             assertThat(result.getProblems()).hasSize(1);
326             result.getProblems().forEach(p -> assertThat(p.getException())
327                     .isInstanceOf(expectedException)
328                     .hasMessageContaining(partOfMessage));
329         }
330     }
331 
332     @BeforeEach
333     void before() throws Exception {
334         graphBuilder = new DefaultGraphBuilder(
335                 mock(BuildResumptionDataRepository.class),
336                 pomlessCollectionStrategy,
337                 multiModuleCollectionStrategy,
338                 requestPomCollectionStrategy);
339 
340         // Create projects
341         MavenProject projectParent = getMavenProject(PARENT_MODULE);
342         MavenProject projectIndependentModule = getMavenProject(INDEPENDENT_MODULE);
343         MavenProject projectModuleA = getMavenProject(MODULE_A, projectParent);
344         MavenProject projectModuleB = getMavenProject(MODULE_B, projectParent);
345         MavenProject projectModuleC = getMavenProject(MODULE_C, projectParent);
346         MavenProject projectModuleC1 = getMavenProject(MODULE_C_1, projectModuleC);
347         MavenProject projectModuleC2 = getMavenProject(MODULE_C_2, projectModuleC);
348 
349         artifactIdProjectMap = Stream.of(
350                         projectParent,
351                         projectIndependentModule,
352                         projectModuleA,
353                         projectModuleB,
354                         projectModuleC,
355                         projectModuleC1,
356                         projectModuleC2)
357                 .collect(Collectors.toMap(MavenProject::getArtifactId, identity()));
358 
359         // Set dependencies and modules
360         projectModuleB.setDependencies(singletonList(toDependency(projectModuleA)));
361         projectModuleC2.setDependencies(singletonList(toDependency(projectModuleB)));
362         projectParent.setCollectedProjects(asList(
363                 projectIndependentModule,
364                 projectModuleA,
365                 projectModuleB,
366                 projectModuleC,
367                 projectModuleC1,
368                 projectModuleC2));
369         projectModuleC.setCollectedProjects(asList(projectModuleC1, projectModuleC2));
370 
371         // Set up needed mocks
372         when(session.getRequest()).thenReturn(mavenExecutionRequest);
373         when(session.getProjects()).thenReturn(null); // needed, otherwise it will be an empty list by default
374         when(mavenExecutionRequest.getProjectBuildingRequest()).thenReturn(mock(ProjectBuildingRequest.class));
375         List<ProjectBuildingResult> projectBuildingResults =
376                 createProjectBuildingResultMocks(artifactIdProjectMap.values());
377         when(projectBuilder.build(anyList(), anyBoolean(), any(ProjectBuildingRequest.class)))
378                 .thenReturn(projectBuildingResults);
379     }
380 
381     private MavenProject getMavenProject(String artifactId, MavenProject parentProject) {
382         MavenProject project = getMavenProject(artifactId);
383         Parent parent = new Parent();
384         parent.setGroupId(parentProject.getGroupId());
385         parent.setArtifactId(parentProject.getArtifactId());
386         project.getModel().setParent(parent);
387         return project;
388     }
389 
390     private MavenProject getMavenProject(String artifactId) {
391         MavenProject mavenProject = new MavenProject();
392         mavenProject.setGroupId(GROUP_ID);
393         mavenProject.setArtifactId(artifactId);
394         mavenProject.setVersion("1.0");
395         mavenProject.setPomFile(new File(artifactId, "pom.xml"));
396         mavenProject.setCollectedProjects(new ArrayList<>());
397         return mavenProject;
398     }
399 
400     private Dependency toDependency(MavenProject mavenProject) {
401         Dependency dependency = new Dependency();
402         dependency.setGroupId(mavenProject.getGroupId());
403         dependency.setArtifactId(mavenProject.getArtifactId());
404         dependency.setVersion(mavenProject.getVersion());
405         return dependency;
406     }
407 
408     private List<ProjectBuildingResult> createProjectBuildingResultMocks(Collection<MavenProject> projects) {
409         return projects.stream()
410                 .map(project -> {
411                     ProjectBuildingResult result = mock(ProjectBuildingResult.class);
412                     when(result.getProject()).thenReturn(project);
413                     return result;
414                 })
415                 .collect(toList());
416     }
417 
418     static class ScenarioBuilder {
419         private String description;
420         private List<String> activeRequiredProjects = emptyList();
421         private List<String> activeOptionalProjects = emptyList();
422         private List<String> inactiveRequiredProjects = emptyList();
423         private List<String> inactiveOptionalProjects = emptyList();
424         private String resumeFrom = "";
425         private String makeBehavior = "";
426         private File requestedPom = new File(PARENT_MODULE, "pom.xml");
427         private boolean recursive = true;
428 
429         private ScenarioBuilder() {}
430 
431         public static ScenarioBuilder scenario(String description) {
432             ScenarioBuilder scenarioBuilder = new ScenarioBuilder();
433             scenarioBuilder.description = description;
434             return scenarioBuilder;
435         }
436 
437         public ScenarioBuilder activeRequiredProjects(String... activeRequiredProjects) {
438             this.activeRequiredProjects = prependWithColonIfNeeded(activeRequiredProjects);
439             return this;
440         }
441 
442         public ScenarioBuilder activeOptionalProjects(String... activeOptionalProjects) {
443             this.activeOptionalProjects = prependWithColonIfNeeded(activeOptionalProjects);
444             return this;
445         }
446 
447         public ScenarioBuilder inactiveRequiredProjects(String... inactiveRequiredProjects) {
448             this.inactiveRequiredProjects = prependWithColonIfNeeded(inactiveRequiredProjects);
449             return this;
450         }
451 
452         public ScenarioBuilder inactiveOptionalProjects(String... inactiveOptionalProjects) {
453             this.inactiveOptionalProjects = prependWithColonIfNeeded(inactiveOptionalProjects);
454             return this;
455         }
456 
457         public ScenarioBuilder resumeFrom(String resumeFrom) {
458             this.resumeFrom = resumeFrom;
459             return this;
460         }
461 
462         public ScenarioBuilder makeBehavior(String makeBehavior) {
463             this.makeBehavior = makeBehavior;
464             return this;
465         }
466 
467         public ScenarioBuilder requestedPom(String requestedPom) {
468             this.requestedPom = new File(requestedPom, "pom.xml");
469             return this;
470         }
471 
472         public ScenarioBuilder nonRecursive() {
473             this.recursive = false;
474             return this;
475         }
476 
477         public Arguments expectResult(String... expectedReactorProjects) {
478             ExpectedResult expectedResult = new SelectedProjectsResult(asList(expectedReactorProjects));
479             return createTestArguments(expectedResult);
480         }
481 
482         public Arguments expectResult(Class<? extends Exception> expected, final String partOfMessage) {
483             ExpectedResult expectedResult = new ExceptionThrown(expected, partOfMessage);
484             return createTestArguments(expectedResult);
485         }
486 
487         private Arguments createTestArguments(ExpectedResult expectedResult) {
488             return Arguments.arguments(
489                     description,
490                     activeRequiredProjects,
491                     activeOptionalProjects,
492                     inactiveRequiredProjects,
493                     inactiveOptionalProjects,
494                     resumeFrom,
495                     makeBehavior,
496                     expectedResult,
497                     requestedPom,
498                     recursive);
499         }
500 
501         private List<String> prependWithColonIfNeeded(String[] selectors) {
502             return Arrays.stream(selectors).map(this::prependWithColonIfNeeded).collect(toList());
503         }
504 
505         private String prependWithColonIfNeeded(String selector) {
506             return selector.indexOf(':') == -1 ? ":" + selector : selector;
507         }
508     }
509 }