MojoExecutorForkedExecutionTest.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.lifecycle.internal;
import javax.inject.Provider;
import java.util.List;
import org.apache.maven.api.Session;
import org.apache.maven.api.SessionData;
import org.apache.maven.api.services.MessageBuilderFactory;
import org.apache.maven.execution.MavenSession;
import org.apache.maven.lifecycle.LifecycleExecutionException;
import org.apache.maven.plugin.BuildPluginManager;
import org.apache.maven.plugin.MavenPluginManager;
import org.apache.maven.plugin.MojoExecution;
import org.apache.maven.plugin.MojosExecutionStrategy;
import org.apache.maven.plugin.descriptor.MojoDescriptor;
import org.apache.maven.project.MavenProject;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
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.Mockito.mock;
import static org.mockito.Mockito.when;
/**
* Tests for {@link MojoExecutor#executeForkedExecutions} null-guard behavior
* and {@link ProjectIndex} lookup semantics.
*
* @see <a href="https://github.com/apache/maven/issues/12600">GH-12600</a>
*/
@ExtendWith(MockitoExtension.class)
class MojoExecutorForkedExecutionTest {
@Mock
private BuildPluginManager pluginManager;
@Mock
private MavenPluginManager mavenPluginManager;
@Mock
private LifecycleDependencyResolver lifeCycleDependencyResolver;
@Mock
private ExecutionEventCatapult eventCatapult;
@Mock
private Provider<MojosExecutionStrategy> mojosExecutionStrategy;
@Mock
private MessageBuilderFactory messageBuilderFactory;
@InjectMocks
private MojoExecutor mojoExecutor;
/**
* Verifies that {@link ProjectIndex} returns null for projects not in the reactor,
* which is the precondition that caused the original NPE (issue #12600).
*/
@Test
void projectIndexReturnsNullForUnknownProject() {
MavenProject knownProject = createProject("org.test", "known", "1.0");
ProjectIndex index = new ProjectIndex(List.of(knownProject));
// Known project has an index entry
assertNotNull(index.getIndices().get("org.test:known:1.0"));
assertEquals(0, index.getIndices().get("org.test:known:1.0"));
assertNotNull(index.getProjects().get("org.test:known:1.0"));
// Unknown project returns null from both maps ��� this is the scenario
// that would cause NPE via auto-unboxing without the null guard
assertNull(index.getIndices().get("org.test:unknown:2.0"));
assertNull(index.getProjects().get("org.test:unknown:2.0"));
}
/**
* Verifies that {@link MojoExecutor#executeForkedExecutions} throws a
* {@link LifecycleExecutionException} with a descriptive message when the
* forked execution references a project not present in the reactor,
* instead of throwing a {@link NullPointerException} from auto-unboxing.
*/
@Test
void executeForkedExecutionsThrowsDescriptiveExceptionForUnknownProject() throws Exception {
MavenProject knownProject = createProject("org.test", "known", "1.0");
ProjectIndex projectIndex = new ProjectIndex(List.of(knownProject));
// Set up a MojoExecution with forked executions referencing an unknown project
MojoExecution mojoExecution = new MojoExecution(mock(MojoDescriptor.class));
String unknownProjectId = "org.test:unknown:2.0";
mojoExecution.setForkedExecutions(unknownProjectId, List.of());
// Mock the session chain: MavenSession -> Session -> SessionData -> ProjectIndex
MavenSession mavenSession = mock(MavenSession.class);
Session apiSession = mock(Session.class);
SessionData sessionData = mock(SessionData.class);
when(mavenSession.getSession()).thenReturn(apiSession);
when(mavenSession.getCurrentProject()).thenReturn(knownProject);
when(apiSession.getData()).thenReturn(sessionData);
when(sessionData.computeIfAbsent(any(), any())).thenReturn(projectIndex);
// Should throw LifecycleExecutionException, not NullPointerException
LifecycleExecutionException ex = assertThrows(
LifecycleExecutionException.class,
() -> mojoExecutor.executeForkedExecutions(mojoExecution, mavenSession));
assertTrue(
ex.getMessage().contains(unknownProjectId), "Exception message should contain the unknown project ID");
assertTrue(ex.getMessage().contains("not in the reactor"), "Exception message should mention the reactor");
}
private static MavenProject createProject(String groupId, String artifactId, String version) {
MavenProject project = new MavenProject();
project.setGroupId(groupId);
project.setArtifactId(artifactId);
project.setVersion(version);
return project;
}
}