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