You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maven.apache.org by gn...@apache.org on 2026/08/29 19:15:31 UTC
(maven) branch maven-4.0.x updated: Fix #12600: handle missing project index in forked executions (#12911)
This is an automated email from the ASF dual-hosted git repository.
gnodet pushed a commit to branch maven-4.0.x
in repository https://gitbox.apache.org/repos/asf/maven.git
The following commit(s) were added to refs/heads/maven-4.0.x by this push:
new 3591db2af5 Fix #12600: handle missing project index in forked executions (#12911)
3591db2af5 is described below
commit 3591db2af58dc33ca067baa1b856a7da8f9e7b4a
Author: 2ako <ul...@protonmail.com>
AuthorDate: Sat Aug 29 23:19:02 2026 +0800
Fix #12600: handle missing project index in forked executions (#12911)
* fix: handle missing forked project index
Signed-off-by: ulofiai <30...@users.noreply.github.com>
* Add test for forked execution project index null guard
Test originally written for PR #12907 (our parallel fix for #12600).
Contributed here since this PR has the better exception constructor
(includes MojoExecution and MavenProject context).
See: https://github.com/apache/maven/pull/12907
Co-Authored-By: Claude Opus 4.6 <no...@anthropic.com>
---------
Signed-off-by: ulofiai <30...@users.noreply.github.com>
Co-authored-by: ulofiai <30...@users.noreply.github.com>
Co-authored-by: Guillaume Nodet <gn...@gmail.com>
Co-authored-by: Claude Opus 4.6 <no...@anthropic.com>
---
.../maven/lifecycle/internal/MojoExecutor.java | 8 +-
.../internal/MojoExecutorForkedExecutionTest.java | 144 +++++++++++++++++++++
2 files changed, 151 insertions(+), 1 deletion(-)
diff --git a/impl/maven-core/src/main/java/org/apache/maven/lifecycle/internal/MojoExecutor.java b/impl/maven-core/src/main/java/org/apache/maven/lifecycle/internal/MojoExecutor.java
index 92b3f75bee..a3a761119c 100644
--- a/impl/maven-core/src/main/java/org/apache/maven/lifecycle/internal/MojoExecutor.java
+++ b/impl/maven-core/src/main/java/org/apache/maven/lifecycle/internal/MojoExecutor.java
@@ -438,7 +438,13 @@ public List<MavenProject> executeForkedExecutions(MojoExecution mojoExecution, M
.getData()
.computeIfAbsent(PROJECT_INDEX, () -> new ProjectIndex(session.getProjects()));
- int index = projectIndex.getIndices().get(projectId);
+ Integer index = projectIndex.getIndices().get(projectId);
+ if (index == null) {
+ throw new LifecycleExecutionException(
+ "Forked execution references project '" + projectId + "' which is not in the reactor",
+ mojoExecution,
+ project);
+ }
MavenProject forkedProject = projectIndex.getProjects().get(projectId);
diff --git a/impl/maven-core/src/test/java/org/apache/maven/lifecycle/internal/MojoExecutorForkedExecutionTest.java b/impl/maven-core/src/test/java/org/apache/maven/lifecycle/internal/MojoExecutorForkedExecutionTest.java
new file mode 100644
index 0000000000..f0dd934537
--- /dev/null
+++ b/impl/maven-core/src/test/java/org/apache/maven/lifecycle/internal/MojoExecutorForkedExecutionTest.java
@@ -0,0 +1,144 @@
+/*
+ * 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;
+ }
+}