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