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/31 08:11:59 UTC

(maven) branch maven-3.10.x updated: Fix #12600: handle missing project index in forked executions (#12923)

This is an automated email from the ASF dual-hosted git repository.

gnodet pushed a commit to branch maven-3.10.x
in repository https://gitbox.apache.org/repos/asf/maven.git


The following commit(s) were added to refs/heads/maven-3.10.x by this push:
     new 0acb4e66b1 Fix #12600: handle missing project index in forked executions (#12923)
0acb4e66b1 is described below

commit 0acb4e66b1df18759db4abb233d4fd2910619759
Author: Guillaume Nodet <gn...@gmail.com>
AuthorDate: Mon Aug 31 10:11:52 2026 +0200

    Fix #12600: handle missing project index in forked executions (#12923)
    
    Backport of #12911 (merged to master) to maven-3.10.x.
    Guards against NullPointerException from auto-unboxing when
    ProjectIndex.getIndices().get() returns null for a project not
    in the reactor. Throws descriptive LifecycleExecutionException
    instead.
    
    Test adapted for 3.10.x (Java 8 APIs, @Inject field injection).
    
    Co-authored-by: Claude Opus 4.6 <no...@anthropic.com>
---
 .../internal/MojoExecutorForkedExecutionTest.java  | 111 +++++++++++++++++++++
 1 file changed, 111 insertions(+)

diff --git a/maven-core/src/test/java/org/apache/maven/lifecycle/internal/MojoExecutorForkedExecutionTest.java b/maven-core/src/test/java/org/apache/maven/lifecycle/internal/MojoExecutorForkedExecutionTest.java
new file mode 100644
index 0000000000..622e4e7363
--- /dev/null
+++ b/maven-core/src/test/java/org/apache/maven/lifecycle/internal/MojoExecutorForkedExecutionTest.java
@@ -0,0 +1,111 @@
+/*
+ * 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 java.lang.reflect.Field;
+import java.util.Collections;
+
+import org.apache.maven.execution.MavenSession;
+import org.apache.maven.lifecycle.LifecycleExecutionException;
+import org.apache.maven.plugin.MojoExecution;
+import org.apache.maven.plugin.descriptor.MojoDescriptor;
+import org.apache.maven.project.MavenProject;
+import org.junit.jupiter.api.Test;
+
+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.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>
+ */
+class MojoExecutorForkedExecutionTest {
+
+    /**
+     * 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(Collections.singletonList(knownProject));
+
+        // Known project has an index entry
+        assertNotNull(index.getIndices().get("org.test:known:1.0"));
+        assertEquals(0, (int) 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(Collections.singletonList(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, Collections.emptyList());
+
+        // In 3.10.x, ProjectIndex is passed directly to executeForkedExecutions
+        MavenSession mavenSession = mock(MavenSession.class);
+        when(mavenSession.getCurrentProject()).thenReturn(knownProject);
+
+        MojoExecutor mojoExecutor = new MojoExecutor();
+        // Inject a mock eventCatapult via reflection (3.10.x uses @Inject field injection)
+        ExecutionEventCatapult catapult = mock(ExecutionEventCatapult.class);
+        Field catapultField = MojoExecutor.class.getDeclaredField("eventCatapult");
+        catapultField.setAccessible(true);
+        catapultField.set(mojoExecutor, catapult);
+
+        // Should throw LifecycleExecutionException, not NullPointerException
+        LifecycleExecutionException ex = assertThrows(
+                LifecycleExecutionException.class,
+                () -> mojoExecutor.executeForkedExecutions(mojoExecution, mavenSession, projectIndex));
+
+        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;
+    }
+}