/*
* 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.Predicates.equalTo;
import static com.google.common.base.Predicates.not;
import static org.mockito.Mockito.reset;
import java.util.Arrays;
import java.util.List;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.stream.Stream;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.testing.CacheSpec.Advance;
import com.github.benmanes.caffeine.cache.testing.CacheSpec.CacheExecutor;
import com.github.benmanes.caffeine.cache.testing.CacheSpec.CacheExpiry;
import com.github.benmanes.caffeine.cache.testing.CacheSpec.CacheWeigher;
import com.github.benmanes.caffeine.cache.testing.CacheSpec.Compute;
import com.github.benmanes.caffeine.cache.testing.CacheSpec.Expire;
import com.github.benmanes.caffeine.cache.testing.CacheSpec.Implementation;
import com.github.benmanes.caffeine.cache.testing.CacheSpec.InitialCapacity;
import com.github.benmanes.caffeine.cache.testing.CacheSpec.Listener;
import com.github.benmanes.caffeine.cache.testing.CacheSpec.Loader;
import com.github.benmanes.caffeine.cache.testing.CacheSpec.Maximum;
import com.github.benmanes.caffeine.cache.testing.CacheSpec.Population;
import com.github.benmanes.caffeine.cache.testing.CacheSpec.ReferenceType;
import com.github.benmanes.caffeine.cache.testing.CacheSpec.Stats;
import com.github.benmanes.caffeine.cache.testing.CacheSpec.Writer;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
/**
* Generates test case scenarios based on the {@link CacheSpec}.
*
* @author ben.manes@gmail.com (Ben Manes)
*/
final class CacheGenerator {
private final Options options;
private final CacheSpec cacheSpec;
private final boolean isLoadingOnly;
private final boolean isAsyncLoadingOnly;
public CacheGenerator(CacheSpec cacheSpec, Options options,
boolean isLoadingOnly, boolean isAsyncLoadingOnly) {
this.isAsyncLoadingOnly = isAsyncLoadingOnly;
this.isLoadingOnly = isLoadingOnly;
this.cacheSpec = cacheSpec;
this.options = options;
}
/** Returns a lazy stream so that the test case is GC-able after use. */
public Stream<Entry<CacheContext, Cache<Integer, Integer>>> generate() {
return combinations().stream()
.map(this::newCacheContext)
.filter(this::isCompatible)
.map(context -> {
Cache<Integer, Integer> cache = newCache(context);
populate(context, cache);
return Maps.immutableEntry(context, cache);
});
}
/** Returns the Cartesian set of the possible cache configurations. */
@SuppressWarnings("unchecked")
private Set<List<Object>> combinations() {
Set<Boolean> asyncLoading = ImmutableSet.of(true, false);
Set<Stats> statistics = filterTypes(options.stats(), cacheSpec.stats());
Set<ReferenceType> keys = filterTypes(options.keys(), cacheSpec.keys());
Set<ReferenceType> values = filterTypes(options.values(), cacheSpec.values());
Set<Compute> computations = filterTypes(options.compute(), cacheSpec.compute());
Set<Implementation> implementations = filterTypes(
options.implementation(), cacheSpec.implementation());
if (System.getProperty("java.version").contains("9")) {
values = Sets.filter(values, not(equalTo(ReferenceType.SOFT)));
}
if (isAsyncLoadingOnly) {
values = values.contains(ReferenceType.STRONG)
? ImmutableSet.of(ReferenceType.STRONG)
: ImmutableSet.of();
computations = Sets.filter(computations, Compute.ASYNC::equals);
}
if (isAsyncLoadingOnly || computations.equals(ImmutableSet.of(Compute.ASYNC))) {
implementations = implementations.contains(Implementation.Caffeine)
? ImmutableSet.of(Implementation.Caffeine)
: ImmutableSet.of();
}
if (computations.equals(ImmutableSet.of(Compute.SYNC))) {
asyncLoading = ImmutableSet.of(false);
}
if (computations.isEmpty() || implementations.isEmpty() || keys.isEmpty() || values.isEmpty()) {
return ImmutableSet.of();
}
return Sets.cartesianProduct(
ImmutableSet.copyOf(cacheSpec.initialCapacity()),
ImmutableSet.copyOf(statistics),
ImmutableSet.copyOf(cacheSpec.weigher()),
ImmutableSet.copyOf(cacheSpec.maximumSize()),
ImmutableSet.copyOf(cacheSpec.expiry()),
ImmutableSet.copyOf(cacheSpec.expireAfterAccess()),
ImmutableSet.copyOf(cacheSpec.expireAfterWrite()),
ImmutableSet.copyOf(cacheSpec.refreshAfterWrite()),
ImmutableSet.copyOf(cacheSpec.advanceOnPopulation()),
ImmutableSet.copyOf(keys),
ImmutableSet.copyOf(values),
ImmutableSet.copyOf(cacheSpec.executor()),
ImmutableSet.copyOf(cacheSpec.removalListener()),
ImmutableSet.copyOf(cacheSpec.population()),
ImmutableSet.of(true, isLoadingOnly),
ImmutableSet.copyOf(asyncLoading),
ImmutableSet.copyOf(computations),
ImmutableSet.copyOf(cacheSpec.loader()),
ImmutableSet.copyOf(cacheSpec.writer()),
ImmutableSet.copyOf(implementations));
}
/** Returns the set of options filtered if a specific type is specified. */
private static <T> Set<T> filterTypes(Optional<T> type, T[] options) {
if (type.isPresent()) {
return type.filter(Arrays.asList(options)::contains).isPresent()
? ImmutableSet.of(type.get())
: ImmutableSet.of();
}
return ImmutableSet.copyOf(Arrays.asList(options));
}
/** Returns a new cache context based on the combination. */
private CacheContext newCacheContext(List<Object> combination) {
int index = 0;
return new CacheContext(
(InitialCapacity) combination.get(index++),
(Stats) combination.get(index++),
(CacheWeigher) combination.get(index++),
(Maximum) combination.get(index++),
(CacheExpiry) combination.get(index++),
(Expire) combination.get(index++),
(Expire) combination.get(index++),
(Expire) combination.get(index++),
(Advance) combination.get(index++),
(ReferenceType) combination.get(index++),
(ReferenceType) combination.get(index++),
(CacheExecutor) combination.get(index++),
(Listener) combination.get(index++),
(Population) combination.get(index++),
(Boolean) combination.get(index++),
(Boolean) combination.get(index++),
(Compute) combination.get(index++),
(Loader) combination.get(index++),
(Writer) combination.get(index++),
(Implementation) combination.get(index++),
cacheSpec);
}
/** Returns if the context is a viable configuration. */
private boolean isCompatible(CacheContext context) {
boolean asyncIncompatible = context.isAsync()
&& ((context.implementation() != Implementation.Caffeine)
|| !context.isStrongValues() || !context.isLoading());
boolean asyncLoaderIncompatible = context.isAsyncLoading()
&& (!context.isAsync() || !context.isLoading());
boolean refreshIncompatible = context.refreshes() && !context.isLoading();
boolean weigherIncompatible = context.isUnbounded() && context.isWeighted();
boolean referenceIncompatible = cacheSpec.requiresWeakOrSoft()
&& (context.isWeakKeys() || context.isWeakValues() || context.isSoftValues());
boolean expiryIncompatible = (context.expiryType() != CacheExpiry.DISABLED)
&& ((context.implementation() != Implementation.Caffeine)
|| (context.expireAfterAccess() != Expire.DISABLED)
|| (context.expireAfterWrite() != Expire.DISABLED));
boolean expirationIncompatible = (cacheSpec.mustExpiresWithAnyOf().length > 0)
&& !Arrays.stream(cacheSpec.mustExpiresWithAnyOf()).anyMatch(context::expires);
boolean skip = asyncIncompatible || asyncLoaderIncompatible
|| refreshIncompatible || weigherIncompatible
|| expiryIncompatible || expirationIncompatible
|| referenceIncompatible;
return !skip;
}
/** Creates a new cache based on the context's configuration. */
public static <K, V> Cache<K, V> newCache(CacheContext context) {
switch (context.implementation()) {
case Caffeine:
return CaffeineCacheFromContext.newCaffeineCache(context);
case Guava:
return GuavaCacheFromContext.newGuavaCache(context);
}
throw new IllegalStateException();
}
/** Fills the cache up to the population size. */
@SuppressWarnings({"unchecked", "BoxedPrimitiveConstructor"})
private void populate(CacheContext context, Cache<Integer, Integer> cache) {
if (context.population.size() == 0) {
return;
}
// Integer caches the object identity semantics of autoboxing for values between
// -128 and 127 (inclusive) as required by JLS
int base = 1000;
int maximum = (int) Math.min(context.maximumSize(), context.population.size());
int first = base + (int) Math.min(1, context.population.size());
int last = base + maximum;
int middle = Math.max(first, base + ((last - first) / 2));
context.disableRejectingCacheWriter();
for (int i = 1; i <= maximum; i++) {
// Reference caching (weak, soft) require unique instances for identity comparison
Integer key = new Integer(base + i);
Integer value = new Integer(-key);
if (key == first) {
context.firstKey = key;
}
if (key == middle) {
context.middleKey = key;
}
if (key == last) {
context.lastKey = key;
}
cache.put(key, value);
context.original.put(key, value);
context.ticker().advance(context.advance.timeNanos(), TimeUnit.NANOSECONDS);
}
context.enableRejectingCacheWriter();
if (context.writer() == Writer.MOCKITO) {
reset(context.cacheWriter());
}
}
}