DefaultLifecycleExecutorTest.java

/*
 * 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.internal.impl;

import java.util.Collections;
import java.util.List;
import java.util.Map;

import org.apache.maven.api.LifecyclePlan;
import org.apache.maven.api.services.LifecycleExecutionRequest;
import org.apache.maven.api.services.LifecycleExecutionResult;
import org.apache.maven.api.services.LifecycleExecutorException;
import org.apache.maven.api.services.LifecyclePlanRequest;
import org.apache.maven.api.services.LifecyclePlanResult;
import org.apache.maven.api.services.Lookup;
import org.apache.maven.execution.MavenSession;
import org.apache.maven.lifecycle.LifecycleExecutionException;
import org.apache.maven.lifecycle.internal.LifecycleTask;
import org.apache.maven.lifecycle.internal.LifecycleTaskSegmentCalculator;
import org.apache.maven.lifecycle.internal.TaskSegment;
import org.apache.maven.lifecycle.internal.concurrent.BuildPlan;
import org.apache.maven.lifecycle.internal.concurrent.BuildPlanExecutor;
import org.apache.maven.lifecycle.internal.concurrent.BuildStep;
import org.apache.maven.plugin.MojoExecution;
import org.apache.maven.plugin.descriptor.MojoDescriptor;
import org.apache.maven.plugin.descriptor.PluginDescriptor;
import org.apache.maven.project.MavenProject;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyList;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.same;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

class DefaultLifecycleExecutorTest {

    private InternalMavenSession session;
    private MavenSession mavenSession;
    private LifecycleTaskSegmentCalculator taskSegmentCalculator;
    private BuildPlanExecutor buildPlanExecutor;
    private MavenProject project;
    private MavenProject previousProject;
    private DefaultProject apiProject;
    private MojoExecution compile;
    private MojoExecution test;
    private DefaultLifecycleExecutor executor;
    private BuildPlan buildPlan;
    private BuildPlanExecutor.BuildContext buildContext;

    @BeforeEach
    void setUp() throws Exception {
        session = mock(InternalMavenSession.class);
        mavenSession = mock(MavenSession.class);
        taskSegmentCalculator = mock(LifecycleTaskSegmentCalculator.class);
        buildPlanExecutor = mock(BuildPlanExecutor.class);
        project = new MavenProject();
        previousProject = new MavenProject();
        apiProject = mock(DefaultProject.class);
        compile = execution("compile", "compile");
        test = execution("test", "test");
        TaskSegment segment = new TaskSegment(false, new LifecycleTask("verify"));

        when(session.getMavenSession()).thenReturn(mavenSession);
        when(session.getService(Lookup.class)).thenReturn(mock(Lookup.class));
        when(mavenSession.getCurrentProject()).thenReturn(previousProject);
        when(apiProject.getProject()).thenReturn(project);
        when(taskSegmentCalculator.calculateTaskSegments(mavenSession, List.of("verify")))
                .thenReturn(List.of(segment));

        // Create a BuildPlan with steps containing the mojos
        buildPlan = new BuildPlan(Collections.singletonMap(project, Collections.emptyList()));
        Map<String, BuildStep> steps = new java.util.LinkedHashMap<>();
        BuildStep compileStep = new BuildStep("compile", project, null);
        compileStep.addMojo(compile, 0);
        steps.put("compile", compileStep);
        BuildStep testStep = new BuildStep("test", project, null);
        testStep.addMojo(test, 0);
        compileStep.executeBefore(testStep);
        steps.put("test", testStep);
        buildPlan.addProject(project, steps);

        buildContext = mock(BuildPlanExecutor.BuildContext.class);
        when(buildContext.getPlan()).thenReturn(buildPlan);
        when(buildPlanExecutor.createProjectContext(same(mavenSession), same(project), anyList()))
                .thenReturn(buildContext);

        executor = new DefaultLifecycleExecutor(taskSegmentCalculator, buildPlanExecutor);
    }

    @Test
    void plansInspectsAndExecutesFilteredMojos() throws Exception {
        LifecyclePlanResult planResult =
                executor.plan(LifecyclePlanRequest.build(session, apiProject, List.of("verify")));
        LifecyclePlan plan = planResult.plan();

        assertSame(apiProject, plan.project());
        assertEquals(List.of("verify"), plan.tasks());
        assertEquals(
                List.of("compile", "test"),
                plan.executions().stream()
                        .map(org.apache.maven.api.MojoExecution::goal)
                        .toList());

        LifecycleExecutionResult result = executor.execute(LifecycleExecutionRequest.build(
                session, plan, execution -> execution.goal().equals("test")));

        assertEquals(
                List.of("test"),
                result.executions().stream()
                        .map(org.apache.maven.api.MojoExecution::goal)
                        .toList());
        verify(mavenSession, times(2)).setCurrentProject(same(project));
        verify(mavenSession, times(2)).setCurrentProject(same(previousProject));
    }

    @Test
    void executesAllMojosWhenFilterIsAbsent() throws Exception {
        LifecyclePlan plan = executor.plan(LifecyclePlanRequest.build(session, apiProject, List.of("verify")))
                .plan();

        LifecycleExecutionResult result = executor.execute(LifecycleExecutionRequest.build(session, plan));

        assertEquals(2, result.executions().size());
        assertEquals(2, result.executedSuccessfully().size());
        assertTrue(result.failedExecution().isEmpty());
        assertTrue(result.failureCause().isEmpty());
    }

    @Test
    void restoresCurrentProjectOnExecutionFailure() throws Exception {
        LifecyclePlan plan = executor.plan(LifecyclePlanRequest.build(session, apiProject, List.of("verify")))
                .plan();
        doThrow(new LifecycleExecutionException("boom"))
                .when(buildContext)
                .configureAndExecuteStep(eq(project), anyList());

        assertThrows(
                LifecycleExecutorException.class,
                () -> executor.execute(LifecycleExecutionRequest.build(session, plan)));

        verify(mavenSession, times(2)).setCurrentProject(same(previousProject));
    }

    @Test
    void doesNotWrapFilterFailures() throws Exception {
        LifecyclePlan plan = executor.plan(LifecyclePlanRequest.build(session, apiProject, List.of("verify")))
                .plan();
        IllegalStateException filterFailure = new IllegalStateException("broken filter");

        IllegalStateException thrown = assertThrows(
                IllegalStateException.class,
                () -> executor.execute(LifecycleExecutionRequest.build(session, plan, execution -> {
                    throw filterFailure;
                })));

        assertSame(filterFailure, thrown);
        verify(buildContext, never()).configureAndExecuteStep(any(), anyList());
        verify(mavenSession, times(2)).setCurrentProject(same(previousProject));
    }

    @Test
    void rejectsPlansNotProducedByThisServiceWithoutLeakingImplementationDetails() {
        LifecyclePlan externalPlan = mock(LifecyclePlan.class);

        IllegalArgumentException thrown = assertThrows(
                IllegalArgumentException.class,
                () -> executor.execute(LifecycleExecutionRequest.build(session, externalPlan)));

        assertEquals("plan was not produced by this lifecycle executor", thrown.getMessage());
        assertFalse(thrown.getMessage().contains(DefaultLifecyclePlan.class.getName()));
        verify(mavenSession, never()).setCurrentProject(project);
    }

    @Test
    void reportsPerExecutionStatus() throws Exception {
        LifecyclePlan plan = executor.plan(LifecyclePlanRequest.build(session, apiProject, List.of("verify")))
                .plan();

        // Make the second step (test) fail
        doAnswer(inv -> {
                    List<MojoExecution> execs = inv.getArgument(1);
                    if (execs.stream().anyMatch(e -> "test".equals(e.getGoal()))) {
                        throw new LifecycleExecutionException("test failed");
                    }
                    return null;
                })
                .when(buildContext)
                .configureAndExecuteStep(eq(project), anyList());

        LifecycleExecutorException ex = assertThrows(
                LifecycleExecutorException.class,
                () -> executor.execute(LifecycleExecutionRequest.build(session, plan)));

        assertTrue(ex.getMessage().contains("Unable to execute lifecycle plan"));
        LifecycleExecutionResult partialResult = ex.result().orElseThrow();
        assertEquals(
                List.of("compile"),
                partialResult.executedSuccessfully().stream()
                        .map(org.apache.maven.api.MojoExecution::goal)
                        .toList());
        assertTrue(partialResult.failedExecution().isPresent());
        assertEquals("test", partialResult.failedExecution().get().goal());
        assertTrue(partialResult.failureCause().isPresent());
    }

    @Test
    void phaseGrouping() throws Exception {
        LifecyclePlan plan = executor.plan(LifecyclePlanRequest.build(session, apiProject, List.of("verify")))
                .plan();

        var byPhase = plan.executionsByPhase();
        assertEquals(2, byPhase.size());
        assertTrue(byPhase.containsKey("compile"));
        assertTrue(byPhase.containsKey("test"));
    }

    private static MojoExecution execution(String goal, String phase) {
        PluginDescriptor pluginDescriptor = new PluginDescriptor();
        pluginDescriptor.setGroupId("org.apache.maven.plugins");
        pluginDescriptor.setArtifactId("maven-test-plugin");
        pluginDescriptor.setVersion("1.0");

        MojoDescriptor mojoDescriptor = new MojoDescriptor();
        mojoDescriptor.setGoal(goal);
        mojoDescriptor.setPluginDescriptor(pluginDescriptor);

        MojoExecution execution = new MojoExecution(mojoDescriptor, "default-" + goal);
        execution.setLifecyclePhase(phase);
        return execution;
    }
}