AbstractRequestCacheTest.java

/*
 * 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);
            }
        }
    }
}