<reponame>abhishekdvg/practice<filename>maven-core/src/test/java/org/apache/maven/graph/DefaultGraphBuilderTest.java
package org.apache.maven.graph;

/*
 * 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.
 */

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.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.codehaus.plexus.util.StringUtils;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;

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 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)
     */
    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_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 project (including child modules)" )
                        .activeRequiredProjects( MODULE_C )
                        .expectResult( MODULE_C, MODULE_C_1, MODULE_C_2 ),
                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( "Selected and excluded same project" )
                        .activeRequiredProjects( MODULE_A )
                        .inactiveRequiredProjects( MODULE_A )
                        .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)
    {
        // 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 );
        if ( StringUtils.isNotEmpty( parameterResumeFrom ) )
        {
            when( mavenExecutionRequest.getResumeFrom() ).thenReturn( ":" + parameterResumeFrom );
        }

        // When
        Result<ProjectDependencyGraph> result = graphBuilder.build( session );

        // Then
        if ( parameterExpectedResult instanceof SelectedProjectsResult )
        {
            assertThat( result.hasErrors() ).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() ).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 )
            );
        }
    }

    @BeforeEach
    public 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 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 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 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 );
        }

        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;
        }
    }
}