gnodet-bot commented on code in PR #13542:
URL: https://github.com/apache/maven/pull/13542#discussion_r4294778054


##########
impl/maven-core/src/main/java/org/apache/maven/internal/build/DefaultBuildInvocation.java:
##########
@@ -0,0 +1,161 @@
+/*
+ * 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.build;
+
+import java.nio.file.Path;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+
+import org.apache.maven.api.BuildInvocation;
+import org.apache.maven.api.ReactorFailureBehavior;
+import org.apache.maven.execution.MavenSession;
+
+/**
+ * Internal immutable implementation of {@link BuildInvocation}.
+ *
+ * @param goals                  requested goals/phases
+ * @param userProperties         user properties with sensitive values redacted
+ * @param systemInfo             curated system properties for reproducibility
+ * @param localRepository        local repository path
+ * @param activeProfiles         explicitly activated profiles
+ * @param selectedProjects       projects selected via {@code -pl}
+ * @param resumeFrom             resume-from selector, or {@code null}
+ * @param reactorFailureBehavior reactor failure behavior
+ * @param offline                whether offline mode was active
+ * @param updateSnapshots        whether snapshot updates were forced
+ * @param noTransferProgress     whether transfer progress was suppressed
+ * @param batchMode              whether batch mode was active
+ * @param threads                degree of concurrency
+ */
+public record DefaultBuildInvocation(
+        List<String> goals,
+        Map<String, String> userProperties,
+        Map<String, String> systemInfo,
+        Path localRepository,
+        List<String> activeProfiles,
+        List<String> selectedProjects,
+        String resumeFrom,
+        ReactorFailureBehavior reactorFailureBehavior,
+        boolean offline,
+        boolean updateSnapshots,
+        boolean noTransferProgress,
+        boolean batchMode,
+        int threads)
+        implements BuildInvocation {
+
+    private static final Set<String> SENSITIVE_KEY_FRAGMENTS =
+            Set.of("password", "passwd", "secret", "token", "apikey", 
"api_key", "credential", "passphrase");
+
+    public DefaultBuildInvocation {
+        goals = List.copyOf(goals);
+        userProperties = Map.copyOf(userProperties);
+        systemInfo = Map.copyOf(systemInfo);
+        activeProfiles = List.copyOf(activeProfiles);
+        selectedProjects = List.copyOf(selectedProjects);
+    }
+
+    /**
+     * Creates a {@link BuildInvocation} snapshot from the current session 
state.
+     *
+     * @param session the Maven session
+     * @return a new immutable build invocation instance
+     */
+    public static BuildInvocation from(MavenSession session) {
+        var request = session.getRequest();
+
+        List<String> goals = request.getGoals() != null ? 
List.copyOf(request.getGoals()) : List.of();
+
+        Map<String, String> userProps = new LinkedHashMap<>();
+        if (request.getUserProperties() != null) {
+            request.getUserProperties().forEach((k, v) -> {
+                String key = String.valueOf(k);
+                String value = isSensitiveKey(key) ? "***" : String.valueOf(v);
+                userProps.put(key, value);
+            });
+        }
+
+        Map<String, String> sysInfo = new TreeMap<>();
+        addSystemProp(sysInfo, "os.name");
+        addSystemProp(sysInfo, "os.arch");
+        addSystemProp(sysInfo, "os.version");
+        addSystemProp(sysInfo, "java.vendor");
+        addSystemProp(sysInfo, "java.vm.name");
+        addSystemProp(sysInfo, "java.vm.version");
+        addSystemProp(sysInfo, "maven.home");
+        addSystemProp(sysInfo, "user.home");
+        addSystemProp(sysInfo, "user.name");
+        sysInfo.put("available.processors", 
String.valueOf(Runtime.getRuntime().availableProcessors()));
+
+        Path localRepo = request.getLocalRepositoryPath() != null
+                ? request.getLocalRepositoryPath().toPath()
+                : Path.of("");

Review Comment:
   💡 **`Path.of("")` as null fallback is misleading**
   
   `Path.of("").toAbsolutePath()` resolves to the JVM working directory — not a 
Maven local repository. If any downstream consumer calls 
`localRepository().toAbsolutePath()`, it silently gets CWD instead of the 
expected repo path.
   
   Maven always sets `localRepositoryPath` in practice, so this branch may 
never fire — but the empty-path sentinel is a trap. Two cleaner options:
   
   1. Throw `IllegalStateException("localRepositoryPath is not set")` — hard 
contract, fails fast
   2. Keep `Path.of("")` but add a comment explaining this is intentional for 
the "no repo configured" case
   
   If option 2, the test `fromNullLocalRepositoryPath()` should also assert 
`assertEquals(Path.of(""), inv.localRepository())` so the intent is documented 
in the test suite.



##########
impl/maven-core/src/test/java/org/apache/maven/internal/build/DefaultBuildInvocationTest.java:
##########
@@ -0,0 +1,270 @@
+/*
+ * 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.build;
+
+import java.io.File;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+
+import org.apache.maven.api.BuildInvocation;
+import org.apache.maven.api.ReactorFailureBehavior;
+import org.apache.maven.execution.DefaultMavenExecutionRequest;
+import org.apache.maven.execution.MavenExecutionRequest;
+import org.apache.maven.execution.MavenSession;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+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;
+
+class DefaultBuildInvocationTest {
+
+    @Test
+    void fromBasicRequest() {
+        MavenExecutionRequest request = new DefaultMavenExecutionRequest();
+        request.setGoals(List.of("clean", "install"));
+        request.setOffline(true);
+        request.setUpdateSnapshots(true);
+        request.setInteractiveMode(false);
+        
request.setReactorFailureBehavior(MavenExecutionRequest.REACTOR_FAIL_AT_END);
+        request.setLocalRepositoryPath(new File("/tmp/repo"));
+
+        MavenSession session = mock(MavenSession.class);
+        when(session.getRequest()).thenReturn(request);
+
+        BuildInvocation inv = DefaultBuildInvocation.from(session);
+
+        assertEquals(List.of("clean", "install"), inv.goals());
+        assertTrue(inv.offline());
+        assertTrue(inv.updateSnapshots());
+        assertTrue(inv.batchMode());
+        assertEquals(ReactorFailureBehavior.FAIL_AT_END, 
inv.reactorFailureBehavior());
+        assertEquals(Path.of("/tmp/repo"), inv.localRepository());
+    }
+
+    @Test
+    void fromNullGoals() {
+        MavenExecutionRequest request = new DefaultMavenExecutionRequest();
+        MavenSession session = mock(MavenSession.class);
+        when(session.getRequest()).thenReturn(request);
+
+        BuildInvocation inv = DefaultBuildInvocation.from(session);
+
+        assertNotNull(inv.goals());
+        assertTrue(inv.goals().isEmpty());
+    }
+
+    @Test
+    void fromNullActiveProfiles() {
+        MavenExecutionRequest request = new DefaultMavenExecutionRequest();
+        MavenSession session = mock(MavenSession.class);
+        when(session.getRequest()).thenReturn(request);
+
+        BuildInvocation inv = DefaultBuildInvocation.from(session);
+
+        assertNotNull(inv.activeProfiles());
+        assertTrue(inv.activeProfiles().isEmpty());
+    }
+
+    @Test
+    void fromNullSelectedProjects() {
+        MavenExecutionRequest request = new DefaultMavenExecutionRequest();
+        MavenSession session = mock(MavenSession.class);
+        when(session.getRequest()).thenReturn(request);
+
+        BuildInvocation inv = DefaultBuildInvocation.from(session);
+
+        assertNotNull(inv.selectedProjects());
+        assertTrue(inv.selectedProjects().isEmpty());
+    }
+
+    @Test
+    void fromNullLocalRepositoryPath() {
+        MavenExecutionRequest request = new DefaultMavenExecutionRequest();
+        MavenSession session = mock(MavenSession.class);
+        when(session.getRequest()).thenReturn(request);
+
+        BuildInvocation inv = DefaultBuildInvocation.from(session);
+
+        assertNotNull(inv.localRepository());
+    }
+
+    @Test
+    void fromNullReactorFailureBehaviorDefaultsToFailFast() {
+        MavenExecutionRequest request = new DefaultMavenExecutionRequest();
+        MavenSession session = mock(MavenSession.class);
+        when(session.getRequest()).thenReturn(request);
+
+        BuildInvocation inv = DefaultBuildInvocation.from(session);
+
+        assertEquals(ReactorFailureBehavior.FAIL_FAST, 
inv.reactorFailureBehavior());
+    }
+
+    @Test
+    void resumeFrom() {
+        MavenExecutionRequest request = new DefaultMavenExecutionRequest();
+        request.setResumeFrom(":my-module");
+        MavenSession session = mock(MavenSession.class);
+        when(session.getRequest()).thenReturn(request);
+
+        BuildInvocation inv = DefaultBuildInvocation.from(session);
+
+        assertEquals(":my-module", inv.resumeFrom());
+    }
+
+    @Test
+    void resumeFromNull() {
+        MavenExecutionRequest request = new DefaultMavenExecutionRequest();
+        MavenSession session = mock(MavenSession.class);
+        when(session.getRequest()).thenReturn(request);
+
+        BuildInvocation inv = DefaultBuildInvocation.from(session);
+
+        assertNull(inv.resumeFrom());
+    }
+
+    @Test
+    void noTransferProgress() {
+        MavenExecutionRequest request = new DefaultMavenExecutionRequest();
+        request.setNoTransferProgress(true);
+        MavenSession session = mock(MavenSession.class);
+        when(session.getRequest()).thenReturn(request);
+
+        BuildInvocation inv = DefaultBuildInvocation.from(session);
+
+        assertTrue(inv.noTransferProgress());
+    }
+
+    @Test
+    void sensitiveKeyRedaction() {
+        MavenExecutionRequest request = new DefaultMavenExecutionRequest();
+        Properties userProps = new Properties();
+        userProps.setProperty("my.password", "secret123");
+        userProps.setProperty("api_key", "key123");
+        userProps.setProperty("auth.token", "tok123");
+        userProps.setProperty("safe.property", "visible");
+        request.setUserProperties(userProps);
+        MavenSession session = mock(MavenSession.class);
+        when(session.getRequest()).thenReturn(request);
+
+        BuildInvocation inv = DefaultBuildInvocation.from(session);
+
+        assertEquals("***", inv.userProperties().get("my.password"));
+        assertEquals("***", inv.userProperties().get("api_key"));
+        assertEquals("***", inv.userProperties().get("auth.token"));
+        assertEquals("visible", inv.userProperties().get("safe.property"));
+    }
+
+    @ParameterizedTest
+    @ValueSource(
+            strings = {
+                "password",
+                "PASSWORD",
+                "my.passwd",
+                "db_secret",
+                "auth_token",
+                "APIKEY",
+                "my.api_key",
+                "credential",
+                "ssh.passphrase"
+            })
+    void sensitiveKeyFragments(String key) {
+        assertTrue(DefaultBuildInvocation.isSensitiveKey(key));
+    }
+
+    @ParameterizedTest
+    @ValueSource(strings = {"safe.property", "skip.tests", 
"maven.compiler.source", "project.build.outputDirectory"})
+    void nonSensitiveKeys(String key) {
+        assertFalse(DefaultBuildInvocation.isSensitiveKey(key));
+    }
+
+    @Test
+    void systemInfoCapturesExpectedKeys() {
+        MavenExecutionRequest request = new DefaultMavenExecutionRequest();
+        MavenSession session = mock(MavenSession.class);
+        when(session.getRequest()).thenReturn(request);
+
+        BuildInvocation inv = DefaultBuildInvocation.from(session);
+
+        Map<String, String> sysInfo = inv.systemInfo();
+        assertNotNull(sysInfo.get("os.name"));
+        assertNotNull(sysInfo.get("os.arch"));
+        assertNotNull(sysInfo.get("java.vm.version"));
+        assertNotNull(sysInfo.get("available.processors"));
+    }
+
+    @Test
+    void collectionsAreImmutable() {
+        DefaultBuildInvocation inv = new DefaultBuildInvocation(
+                List.of("compile"),
+                Map.of("key", "val"),
+                Map.of("os.name", "Linux"),
+                Path.of("/tmp/repo"),
+                List.of("profile1"),
+                List.of(":module1"),
+                null,
+                ReactorFailureBehavior.FAIL_FAST,
+                false,
+                false,
+                false,
+                false,
+                1);
+
+        assertThrows(UnsupportedOperationException.class, () -> 
inv.goals().add("test"));
+        assertThrows(
+                UnsupportedOperationException.class, () -> 
inv.userProperties().put("new", "val"));
+        assertThrows(
+                UnsupportedOperationException.class, () -> 
inv.activeProfiles().add("p2"));
+        assertThrows(
+                UnsupportedOperationException.class,
+                () -> inv.selectedProjects().add(":m2"));
+        assertThrows(UnsupportedOperationException.class, () -> 
inv.systemInfo().put("k", "v"));
+    }
+
+    @Test
+    void reactorFailureBehaviorValues() {
+        MavenExecutionRequest request = new DefaultMavenExecutionRequest();
+        MavenSession session = mock(MavenSession.class);
+        when(session.getRequest()).thenReturn(request);
+
+        
request.setReactorFailureBehavior(MavenExecutionRequest.REACTOR_FAIL_FAST);
+        assertEquals(
+                ReactorFailureBehavior.FAIL_FAST,
+                DefaultBuildInvocation.from(session).reactorFailureBehavior());
+
+        
request.setReactorFailureBehavior(MavenExecutionRequest.REACTOR_FAIL_AT_END);
+        assertEquals(
+                ReactorFailureBehavior.FAIL_AT_END,
+                DefaultBuildInvocation.from(session).reactorFailureBehavior());
+
+        
request.setReactorFailureBehavior(MavenExecutionRequest.REACTOR_FAIL_NEVER);
+        assertEquals(
+                ReactorFailureBehavior.FAIL_NEVER,
+                DefaultBuildInvocation.from(session).reactorFailureBehavior());
+    }
+}

Review Comment:
   💡 **Missing test cases for edge conditions**
   
   Three untested paths:
   
   1. **Unrecognized `reactorFailureBehavior` string** — what happens when the 
request returns `"UNKNOWN_VALUE"`? Should either verify the silent `FAIL_FAST` 
fallback (if the current behavior is intentional) or the exception propagation 
(if it's changed per the comment above).
   
   2. **`localRepository()` when `localRepositoryPath` is null** — 
`fromNullLocalRepositoryPath()` only asserts 
`assertNotNull(inv.localRepository())`. It should also assert 
`assertEquals(Path.of(""), inv.localRepository())` to document the contract 
explicitly.
   
   3. **`threads()` default** — no test verifies the default 
`getDegreeOfConcurrency()` value (it's 0 from `DefaultMavenExecutionRequest` 
unless explicitly set, which may be surprising for callers expecting 1).



##########
impl/maven-core/src/main/java/org/apache/maven/internal/build/DefaultBuildInvocation.java:
##########
@@ -0,0 +1,161 @@
+/*
+ * 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.build;
+
+import java.nio.file.Path;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+
+import org.apache.maven.api.BuildInvocation;
+import org.apache.maven.api.ReactorFailureBehavior;
+import org.apache.maven.execution.MavenSession;
+
+/**
+ * Internal immutable implementation of {@link BuildInvocation}.
+ *
+ * @param goals                  requested goals/phases
+ * @param userProperties         user properties with sensitive values redacted
+ * @param systemInfo             curated system properties for reproducibility
+ * @param localRepository        local repository path
+ * @param activeProfiles         explicitly activated profiles
+ * @param selectedProjects       projects selected via {@code -pl}
+ * @param resumeFrom             resume-from selector, or {@code null}
+ * @param reactorFailureBehavior reactor failure behavior
+ * @param offline                whether offline mode was active
+ * @param updateSnapshots        whether snapshot updates were forced
+ * @param noTransferProgress     whether transfer progress was suppressed
+ * @param batchMode              whether batch mode was active
+ * @param threads                degree of concurrency
+ */
+public record DefaultBuildInvocation(
+        List<String> goals,
+        Map<String, String> userProperties,
+        Map<String, String> systemInfo,
+        Path localRepository,
+        List<String> activeProfiles,
+        List<String> selectedProjects,
+        String resumeFrom,
+        ReactorFailureBehavior reactorFailureBehavior,
+        boolean offline,
+        boolean updateSnapshots,
+        boolean noTransferProgress,
+        boolean batchMode,
+        int threads)
+        implements BuildInvocation {
+
+    private static final Set<String> SENSITIVE_KEY_FRAGMENTS =
+            Set.of("password", "passwd", "secret", "token", "apikey", 
"api_key", "credential", "passphrase");
+
+    public DefaultBuildInvocation {
+        goals = List.copyOf(goals);
+        userProperties = Map.copyOf(userProperties);
+        systemInfo = Map.copyOf(systemInfo);
+        activeProfiles = List.copyOf(activeProfiles);
+        selectedProjects = List.copyOf(selectedProjects);
+    }
+
+    /**
+     * Creates a {@link BuildInvocation} snapshot from the current session 
state.
+     *
+     * @param session the Maven session
+     * @return a new immutable build invocation instance
+     */
+    public static BuildInvocation from(MavenSession session) {
+        var request = session.getRequest();
+
+        List<String> goals = request.getGoals() != null ? 
List.copyOf(request.getGoals()) : List.of();
+
+        Map<String, String> userProps = new LinkedHashMap<>();
+        if (request.getUserProperties() != null) {
+            request.getUserProperties().forEach((k, v) -> {
+                String key = String.valueOf(k);
+                String value = isSensitiveKey(key) ? "***" : String.valueOf(v);
+                userProps.put(key, value);
+            });
+        }
+
+        Map<String, String> sysInfo = new TreeMap<>();
+        addSystemProp(sysInfo, "os.name");
+        addSystemProp(sysInfo, "os.arch");
+        addSystemProp(sysInfo, "os.version");
+        addSystemProp(sysInfo, "java.vendor");
+        addSystemProp(sysInfo, "java.vm.name");
+        addSystemProp(sysInfo, "java.vm.version");
+        addSystemProp(sysInfo, "maven.home");
+        addSystemProp(sysInfo, "user.home");
+        addSystemProp(sysInfo, "user.name");
+        sysInfo.put("available.processors", 
String.valueOf(Runtime.getRuntime().availableProcessors()));
+
+        Path localRepo = request.getLocalRepositoryPath() != null
+                ? request.getLocalRepositoryPath().toPath()
+                : Path.of("");
+
+        List<String> activeProfiles =
+                request.getActiveProfiles() != null ? 
List.copyOf(request.getActiveProfiles()) : List.of();
+
+        List<String> selectedProjects =
+                request.getSelectedProjects() != null ? 
List.copyOf(request.getSelectedProjects()) : List.of();
+
+        String resumeFrom = request.getResumeFrom();
+
+        ReactorFailureBehavior failureBehavior = 
ReactorFailureBehavior.FAIL_FAST;
+        if (request.getReactorFailureBehavior() != null) {
+            try {
+                failureBehavior = 
ReactorFailureBehavior.fromString(request.getReactorFailureBehavior());
+            } catch (IllegalArgumentException ignored) {

Review Comment:
   ⚠️ **Silent exception swallowing masks misconfiguration**
   
   If `request.getReactorFailureBehavior()` returns an unrecognized string 
(e.g. a user typo like `"FAIL-FAST"` instead of `"FAIL_FAST"`), `fromString()` 
throws `IllegalArgumentException` which is silently discarded here. The build 
then proceeds with `FAIL_FAST` as if it were the correct behavior — with no 
indication to the user that their `-fn`/`-fae` flag was ignored.
   
   At minimum, log a debug/warn message so the misconfiguration is visible:
   
   ```suggestion
               } catch (IllegalArgumentException e) {
                   // Unknown string — fall back to FAIL_FAST and warn
                   
org.slf4j.LoggerFactory.getLogger(DefaultBuildInvocation.class)
                           .warn("Unknown reactor failure behavior '{}', 
defaulting to FAIL_FAST",
                                   request.getReactorFailureBehavior());
               }
   ```
   
   Or if a logger field is not practical here (static factory), consider 
propagating the exception and letting `DefaultSession` handle it.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to