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