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

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


The following commit(s) were added to refs/heads/master by this push:
     new 98479a33c8 Add suppressed exceptions to BatchRequestException in 
AbstractRequestCache (#2431)
98479a33c8 is described below

commit 98479a33c8731de2c8733dfd830e1a68a74ad8c2
Author: Guillaume Nodet <gno...@gmail.com>
AuthorDate: Wed Jun 4 23:26:51 2025 +0200

    Add suppressed exceptions to BatchRequestException in AbstractRequestCache 
(#2431)
    
    When batch requests fail, individual exceptions are now added as suppressed
    exceptions to preserve stack traces and error context for debugging.
    
    - Modify AbstractRequestCache.requests() to call addSuppressed() for each 
failed request
    - Add comprehensive test suite in AbstractRequestCacheTest to verify 
exception handling
    - Test covers batch failures, mixed success/failure, and successful batch 
scenarios
---
 .../maven/impl/cache/AbstractRequestCache.java     |   9 +-
 .../maven/impl/cache/AbstractRequestCacheTest.java | 261 +++++++++++++++++++++
 2 files changed, 269 insertions(+), 1 deletion(-)

diff --git 
a/impl/maven-impl/src/main/java/org/apache/maven/impl/cache/AbstractRequestCache.java
 
b/impl/maven-impl/src/main/java/org/apache/maven/impl/cache/AbstractRequestCache.java
index d917f0a72a..0b7fac393b 100644
--- 
a/impl/maven-impl/src/main/java/org/apache/maven/impl/cache/AbstractRequestCache.java
+++ 
b/impl/maven-impl/src/main/java/org/apache/maven/impl/cache/AbstractRequestCache.java
@@ -150,7 +150,14 @@ public <REQ extends Request<?>, REP extends Result<REQ>> 
List<REP> requests(
         }
 
         if (hasFailures) {
-            throw new BatchRequestException("One or more requests failed", 
allResults);
+            BatchRequestException exception = new BatchRequestException("One 
or more requests failed", allResults);
+            // Add all individual exceptions as suppressed exceptions to 
preserve stack traces
+            for (RequestResult<REQ, REP> result : allResults) {
+                if (result.error() != null) {
+                    exception.addSuppressed(result.error());
+                }
+            }
+            throw exception;
         }
 
         return allResults.stream().map(RequestResult::result).toList();
diff --git 
a/impl/maven-impl/src/test/java/org/apache/maven/impl/cache/AbstractRequestCacheTest.java
 
b/impl/maven-impl/src/test/java/org/apache/maven/impl/cache/AbstractRequestCacheTest.java
new file mode 100644
index 0000000000..6683bee0de
--- /dev/null
+++ 
b/impl/maven-impl/src/test/java/org/apache/maven/impl/cache/AbstractRequestCacheTest.java
@@ -0,0 +1,261 @@
+/*
+ * 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.impl.cache;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.function.Function;
+
+import org.apache.maven.api.ProtoSession;
+import org.apache.maven.api.annotations.Nonnull;
+import org.apache.maven.api.cache.BatchRequestException;
+import org.apache.maven.api.cache.RequestResult;
+import org.apache.maven.api.services.Request;
+import org.apache.maven.api.services.RequestTrace;
+import org.apache.maven.api.services.Result;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.mock;
+
+class AbstractRequestCacheTest {
+
+    private TestRequestCache cache;
+
+    @BeforeEach
+    void setUp() {
+        cache = new TestRequestCache();
+    }
+
+    @Test
+    void testBatchRequestExceptionIncludesSuppressedExceptions() {
+        // Create mock requests and results
+        TestRequest request1 = createTestRequest("request1");
+        TestRequest request2 = createTestRequest("request2");
+        TestRequest request3 = createTestRequest("request3");
+
+        // Create specific exceptions with different messages and stack traces
+        RuntimeException exception1 = new RuntimeException("Error processing 
request1");
+        IllegalArgumentException exception2 = new 
IllegalArgumentException("Invalid argument in request2");
+        IllegalStateException exception3 = new IllegalStateException("Invalid 
state in request3");
+
+        // Set up the cache to return failures for all requests
+        cache.addFailure(request1, exception1);
+        cache.addFailure(request2, exception2);
+        cache.addFailure(request3, exception3);
+
+        List<TestRequest> requests = Arrays.asList(request1, request2, 
request3);
+
+        // Create a supplier that should not be called since we're simulating 
cached failures
+        Function<List<TestRequest>, List<TestResult>> supplier = reqs -> {
+            throw new AssertionError("Supplier should not be called in this 
test");
+        };
+
+        // Execute the batch request and expect BatchRequestException
+        BatchRequestException batchException =
+                assertThrows(BatchRequestException.class, () -> 
cache.requests(requests, supplier));
+
+        // Verify the main exception message
+        assertEquals("One or more requests failed", 
batchException.getMessage());
+
+        // Verify that all individual exceptions are included as suppressed 
exceptions
+        Throwable[] suppressedExceptions = batchException.getSuppressed();
+        assertNotNull(suppressedExceptions);
+        assertEquals(3, suppressedExceptions.length);
+
+        // Verify each suppressed exception
+        assertTrue(Arrays.asList(suppressedExceptions).contains(exception1));
+        assertTrue(Arrays.asList(suppressedExceptions).contains(exception2));
+        assertTrue(Arrays.asList(suppressedExceptions).contains(exception3));
+
+        // Verify the results contain the correct error information
+        List<RequestResult<?, ?>> results = batchException.getResults();
+        assertEquals(3, results.size());
+
+        for (RequestResult<?, ?> result : results) {
+            assertNotNull(result.error());
+            assertInstanceOf(RuntimeException.class, result.error());
+        }
+    }
+
+    @Test
+    void testBatchRequestWithMixedSuccessAndFailure() {
+        TestRequest successRequest = createTestRequest("success");
+        TestRequest failureRequest = createTestRequest("failure");
+
+        RuntimeException failureException = new RuntimeException("Processing 
failed");
+
+        // Set up mixed success/failure scenario
+        cache.addFailure(failureRequest, failureException);
+
+        List<TestRequest> requests = Arrays.asList(successRequest, 
failureRequest);
+
+        Function<List<TestRequest>, List<TestResult>> supplier = reqs -> {
+            // Only the success request should reach the supplier
+            assertEquals(1, reqs.size());
+            assertEquals(successRequest, reqs.get(0));
+            return List.of(new TestResult(successRequest));
+        };
+
+        BatchRequestException batchException =
+                assertThrows(BatchRequestException.class, () -> 
cache.requests(requests, supplier));
+
+        // Verify only the failure exception is suppressed
+        Throwable[] suppressedExceptions = batchException.getSuppressed();
+        assertEquals(1, suppressedExceptions.length);
+        assertEquals(failureException, suppressedExceptions[0]);
+
+        // Verify results: one success, one failure
+        List<RequestResult<?, ?>> results = batchException.getResults();
+        assertEquals(2, results.size());
+
+        RequestResult<?, ?> result1 = results.get(0);
+        RequestResult<?, ?> result2 = results.get(1);
+
+        // One should be success, one should be failure
+        boolean hasSuccess = (result1.error() == null) || (result2.error() == 
null);
+        boolean hasFailure = (result1.error() != null) || (result2.error() != 
null);
+
+        assertTrue(hasSuccess);
+        assertTrue(hasFailure);
+    }
+
+    @Test
+    void testSuccessfulBatchRequestDoesNotThrowException() {
+        TestRequest request1 = createTestRequest("success1");
+        TestRequest request2 = createTestRequest("success2");
+
+        List<TestRequest> requests = Arrays.asList(request1, request2);
+
+        Function<List<TestRequest>, List<TestResult>> supplier =
+                reqs -> reqs.stream().map(TestResult::new).toList();
+
+        // Should not throw any exception
+        List<TestResult> results = cache.requests(requests, supplier);
+
+        assertEquals(2, results.size());
+        assertEquals(request1, results.get(0).getRequest());
+        assertEquals(request2, results.get(1).getRequest());
+    }
+
+    // Helper methods and test classes
+
+    private TestRequest createTestRequest(String id) {
+        ProtoSession session = mock(ProtoSession.class);
+        return new TestRequestImpl(id, session);
+    }
+
+    // Test implementations
+
+    interface TestRequest extends Request<ProtoSession> {}
+
+    static class TestRequestImpl implements TestRequest {
+        private final String id;
+        private final ProtoSession session;
+
+        TestRequestImpl(String id, ProtoSession session) {
+            this.id = id;
+            this.session = session;
+        }
+
+        @Override
+        @Nonnull
+        public ProtoSession getSession() {
+            return session;
+        }
+
+        @Override
+        public RequestTrace getTrace() {
+            return null;
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            if (this == obj) {
+                return true;
+            }
+            if (obj == null || getClass() != obj.getClass()) {
+                return false;
+            }
+            TestRequestImpl that = (TestRequestImpl) obj;
+            return java.util.Objects.equals(id, that.id);
+        }
+
+        @Override
+        public int hashCode() {
+            return java.util.Objects.hash(id);
+        }
+
+        @Override
+        @Nonnull
+        public String toString() {
+            return "TestRequest[" + id + "]";
+        }
+    }
+
+    static class TestResult implements Result<TestRequest> {
+        private final TestRequest request;
+
+        TestResult(TestRequest request) {
+            this.request = request;
+        }
+
+        @Override
+        @Nonnull
+        public TestRequest getRequest() {
+            return request;
+        }
+    }
+
+    static class TestRequestCache extends AbstractRequestCache {
+        private final java.util.Map<TestRequest, RuntimeException> failures = 
new java.util.HashMap<>();
+
+        void addFailure(TestRequest request, RuntimeException exception) {
+            failures.put(request, exception);
+        }
+
+        @Override
+        protected <REQ extends Request<?>, REP extends Result<REQ>> 
CachingSupplier<REQ, REP> doCache(
+                REQ req, Function<REQ, REP> supplier) {
+            // Check if we have a pre-configured failure for this request
+            RuntimeException failure = failures.get(req);
+            if (failure != null) {
+                // Return a pre-cached failure by creating a supplier that 
always throws
+                return new PreCachedFailureCachingSupplier<>(failure);
+            }
+
+            // For non-failure cases, return a normal caching supplier
+            return new CachingSupplier<>(supplier);
+        }
+
+        // Custom CachingSupplier that simulates a pre-cached failure
+        private static class PreCachedFailureCachingSupplier<REQ, REP> extends 
CachingSupplier<REQ, REP> {
+            PreCachedFailureCachingSupplier(RuntimeException failure) {
+                super(null); // No supplier needed
+                // Pre-populate the value with the failure
+                this.value = new AltRes(failure);
+            }
+        }
+    }
+}

Reply via email to