/* * Copyright 2014 Ben Manes. All Rights Reserved. * * Licensed 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 com.github.benmanes.caffeine.cache.testing; import static com.google.common.base.Preconditions.checkNotNull; import static java.util.Objects.requireNonNull; import java.lang.reflect.Method; import java.lang.reflect.Parameter; import java.util.Arrays; import java.util.Iterator; import java.util.Map; import java.util.Map.Entry; import java.util.Objects; import java.util.concurrent.ConcurrentMap; import java.util.logging.LogManager; import java.util.stream.Stream; import org.testng.annotations.DataProvider; import com.github.benmanes.caffeine.cache.AsyncLoadingCache; import com.github.benmanes.caffeine.cache.Cache; import com.github.benmanes.caffeine.cache.Caffeine; import com.github.benmanes.caffeine.cache.LoadingCache; import com.github.benmanes.caffeine.cache.Policy; import com.github.benmanes.caffeine.cache.testing.CacheSpec.Compute; /** * A data provider that generates caches based on the {@link CacheSpec} configuration. * * @author ben.manes@gmail.com (Ben Manes) */ public final class CacheProvider { static { // disable logging warnings caused by exceptions in asynchronous computations LogManager.getLogManager().reset(); } private CacheProvider() {} /** Returns the lazily generated test scenarios. */ @DataProvider(name = "caches") public static Iterator<Object[]> providesCaches(Method testMethod) throws Exception { CacheGenerator generator = newCacheGenerator(testMethod); return asTestCases(testMethod, generator.generate()); } /** Returns a new cache generator. */ private static CacheGenerator newCacheGenerator(Method testMethod) { CacheSpec cacheSpec = testMethod.getAnnotation(CacheSpec.class); requireNonNull(cacheSpec, "@CacheSpec not found"); Options options = Options.fromSystemProperties(); // Inspect the test parameters for interface constraints (loading, async) boolean isAsyncLoadingOnly = hasCacheOfType(testMethod, AsyncLoadingCache.class); boolean isLoadingOnly = isAsyncLoadingOnly || hasCacheOfType(testMethod, LoadingCache.class) || options.compute().filter(Compute.ASYNC::equals).isPresent(); return new CacheGenerator(cacheSpec, options, isLoadingOnly, isAsyncLoadingOnly); } /** * Converts each scenario into test case parameters. Supports injecting {@link LoadingCache}, * {@link Cache}, {@link CacheContext}, the {@link ConcurrentMap} {@link Cache#asMap()} view, * {@link Policy.Eviction}, and {@link Policy.Expiration}. */ private static Iterator<Object[]> asTestCases(Method testMethod, Stream<Entry<CacheContext, Cache<Integer, Integer>>> scenarios) { Parameter[] parameters = testMethod.getParameters(); CacheContext[] stashed = new CacheContext[1]; return scenarios.map(entry -> { CacheContext context = entry.getKey(); Cache<Integer, Integer> cache = entry.getValue(); // Retain a strong reference to the context throughout the test execution so that the // cache entries are not collected due to the test not accepting the context parameter stashed[0] = context; Object[] params = new Object[parameters.length]; for (int i = 0; i < params.length; i++) { Class<?> clazz = parameters[i].getType(); if (clazz.isAssignableFrom(CacheContext.class)) { params[i] = context; } else if (clazz.isAssignableFrom(Caffeine.class)) { params[i] = context.caffeine; } else if (clazz.isAssignableFrom(cache.getClass())) { params[i] = cache; } else if (clazz.isAssignableFrom(AsyncLoadingCache.class)) { params[i] = context.asyncCache; } else if (clazz.isAssignableFrom(Map.class)) { params[i] = cache.asMap(); } else if (clazz.isAssignableFrom(Policy.Eviction.class)) { params[i] = cache.policy().eviction().get(); } else if (clazz.isAssignableFrom(Policy.Expiration.class)) { params[i] = expirationPolicy(parameters[i], cache); } else if (clazz.isAssignableFrom(Policy.VarExpiration.class)) { params[i] = cache.policy().expireVariably().get(); } if (params[i] == null) { checkNotNull(params[i], "Unknown parameter type: %s", clazz); } } return params; }).filter(Objects::nonNull).iterator(); } /** Returns the fixed expiration policy for the given parameter. */ private static Policy.Expiration<Integer, Integer> expirationPolicy( Parameter parameter, Cache<Integer, Integer> cache) { if (parameter.isAnnotationPresent(ExpireAfterAccess.class)) { return cache.policy().expireAfterAccess().get(); } else if (parameter.isAnnotationPresent(ExpireAfterWrite.class)) { return cache.policy().expireAfterWrite().get(); } else if (parameter.isAnnotationPresent(RefreshAfterWrite.class)) { return cache.policy().refreshAfterWrite().get(); } throw new AssertionError("Expiration parameter must have a qualifier annotation"); } /** Returns if the required cache matches the provided type. */ private static boolean hasCacheOfType(Method testMethod, Class<?> cacheType) { return Arrays.stream(testMethod.getParameterTypes()).anyMatch(cacheType::isAssignableFrom); } }