Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
}