/*
* 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 java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.Mockito.when;
import java.io.ObjectStreamException;
import java.io.Serializable;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
import org.mockito.Mockito;
import com.github.benmanes.caffeine.cache.AsyncCacheLoader;
import com.github.benmanes.caffeine.cache.CacheLoader;
import com.github.benmanes.caffeine.cache.CacheWriter;
import com.github.benmanes.caffeine.cache.Expiry;
import com.github.benmanes.caffeine.cache.LoadingCache;
import com.github.benmanes.caffeine.cache.RemovalListener;
import com.github.benmanes.caffeine.cache.Weigher;
import com.github.benmanes.caffeine.cache.testing.RemovalListeners.ConsumingRemovalListener;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Interner;
import com.google.common.collect.Interners;
import com.google.common.util.concurrent.MoreExecutors;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
/**
* The cache test specification so that a {@link org.testng.annotations.DataProvider} can construct
* the maximum number of cache combinations to test against.
*
* @author ben.manes@gmail.com (Ben Manes)
*/
@SuppressWarnings("ImmutableEnumChecker")
@Target(METHOD) @Retention(RUNTIME)
public @interface CacheSpec {
/* ---------------- Compute -------------- */
/**
* Indicates whether the test supports a cache allowing for asynchronous computations. This is
* for implementation specific tests that may inspect the internal state of a down casted cache.
*/
Compute[] compute() default {
Compute.ASYNC,
Compute.SYNC
};
enum Compute {
ASYNC,
SYNC,
}
/* ---------------- Implementation -------------- */
/** The implementation, each resulting in a new combination. */
Implementation[] implementation() default {
Implementation.Caffeine,
Implementation.Guava,
};
enum Implementation {
Caffeine,
Guava
}
/* ---------------- Initial capacity -------------- */
InitialCapacity[] initialCapacity() default {
InitialCapacity.DEFAULT
};
/** The initial capacities, each resulting in a new combination. */
enum InitialCapacity {
/** A flag indicating that the initial capacity is not configured. */
DEFAULT(16),
/** A configuration where the table grows on the first addition. */
ZERO(0),
/** A configuration where the table grows on the second addition. */
ONE(1),
/** A configuration where the table grows after the {@link Population#FULL} count. */
FULL(50),
/** A configuration where the table grows after the 10 x {@link Population#FULL} count. */
EXCESSIVE(100);
private final int size;
private InitialCapacity(int size) {
this.size = size;
}
public int size() {
return size;
}
}
/* ---------------- Statistics -------------- */
Stats[] stats() default {
Stats.ENABLED,
Stats.DISABLED
};
enum Stats {
ENABLED,
DISABLED
}
/* ---------------- Maximum size -------------- */
/** The maximum size, each resulting in a new combination. */
Maximum[] maximumSize() default {
Maximum.DISABLED,
Maximum.UNREACHABLE
};
enum Maximum {
/** A flag indicating that entries are not evicted due to a maximum threshold. */
DISABLED(Long.MAX_VALUE),
/** A configuration where entries are evicted immediately. */
ZERO(0L),
/** A configuration that holds a single unit. */
ONE(1L),
/** A configuration that holds 10 units. */
TEN(10L),
/** A configuration that holds 150 units. */
ONE_FIFTY(150L),
/** A configuration that holds the {@link Population#FULL} unit count. */
FULL(InitialCapacity.FULL.size()),
/** A configuration where the threshold is too high for eviction to occur. */
UNREACHABLE(Long.MAX_VALUE);
private final long max;
private Maximum(long max) {
this.max = max;
}
public long max() {
return max;
}
}
/* ---------------- Weigher -------------- */
/** The weigher, each resulting in a new combination. */
CacheWeigher[] weigher() default {
CacheWeigher.DEFAULT,
CacheWeigher.ZERO,
CacheWeigher.TEN
};
enum CacheWeigher implements Weigher<Object, Object> {
/** A flag indicating that no weigher is set when building the cache. */
DEFAULT(1),
/** A flag indicating that every entry is valued at 10 units. */
TEN(10),
/** A flag indicating that every entry is valued at 0 unit. */
ZERO(0),
/** A flag indicating that every entry is valued at -1 unit. */
NEGATIVE(-1),
/** A flag indicating that every entry is valued at Integer.MAX_VALUE units. */
MAX_VALUE(Integer.MAX_VALUE),
/** A flag indicating that the entry is weighted by the integer value. */
VALUE(1) {
@Override public int weigh(Object key, Object value) {
return ((Integer) value).intValue();
}
},
/** A flag indicating that the entry is weighted by the value's collection size. */
COLLECTION(1) {
@Override public int weigh(Object key, Object value) {
return ((Collection<?>) value).size();
}
},
/** A flag indicating that the entry's weight is randomly changing. */
RANDOM(1) {
@Override public int weigh(Object key, Object value) {
return ThreadLocalRandom.current().nextInt(1, 10);
}
};
private final int units;
private CacheWeigher(int multiplier) {
this.units = multiplier;
}
@Override
public int weigh(Object key, Object value) {
return units;
}
public int unitsPerEntry() {
return units;
}
}
/* ---------------- Expiration -------------- */
/** Indicates that the combination must have any of the expiration settings. */
Expiration[] mustExpiresWithAnyOf() default {};
enum Expiration {
AFTER_WRITE, AFTER_ACCESS, VARIABLE
}
/** The expiration time-to-idle setting, each resulting in a new combination. */
Expire[] expireAfterAccess() default {
Expire.DISABLED,
Expire.FOREVER
};
/** The expiration time-to-live setting, each resulting in a new combination. */
Expire[] expireAfterWrite() default {
Expire.DISABLED,
Expire.FOREVER
};
/** The refresh setting, each resulting in a new combination. */
Expire[] refreshAfterWrite() default {
Expire.DISABLED,
Expire.FOREVER
};
/** The variable expiration setting, each resulting in a new combination. */
CacheExpiry[] expiry() default {
CacheExpiry.DISABLED,
CacheExpiry.ACCESS
};
/** The fixed duration for the expiry. */
Expire expiryTime() default Expire.FOREVER;
/** Indicates if the amount of time that should be auto-advance for each entry when populating. */
Advance[] advanceOnPopulation() default {
Advance.ZERO
};
enum CacheExpiry {
DISABLED {
@Override public <K, V> Expiry<K, V> createExpiry(Expire expiryTime) {
return null;
}
},
MOCKITO {
@Override public <K, V> Expiry<K, V> createExpiry(Expire expiryTime) {
@SuppressWarnings("unchecked")
Expiry<K, V> mock = Mockito.mock(Expiry.class);
when(mock.expireAfterCreate(any(), any(), anyLong()))
.thenReturn(expiryTime.timeNanos());
when(mock.expireAfterUpdate(any(), any(), anyLong(), anyLong()))
.thenReturn(expiryTime.timeNanos());
when(mock.expireAfterRead(any(), any(), anyLong(), anyLong()))
.thenReturn(expiryTime.timeNanos());
return mock;
}
},
ACCESS {
@Override public <K, V> Expiry<K, V> createExpiry(Expire expiryTime) {
return ExpiryBuilder
.expiringAfterCreate(expiryTime.timeNanos())
.expiringAfterUpdate(expiryTime.timeNanos())
.expiringAfterRead(expiryTime.timeNanos())
.build();
}
},
WRITE {
@Override public <K, V> Expiry<K, V> createExpiry(Expire expiryTime) {
return ExpiryBuilder
.expiringAfterCreate(expiryTime.timeNanos())
.expiringAfterUpdate(expiryTime.timeNanos())
.build();
}
};
public abstract <K, V> Expiry<K, V> createExpiry(Expire expiryTime);
}
enum Expire {
/** A flag indicating that entries are not evicted due to expiration. */
DISABLED(Long.MIN_VALUE),
/** A configuration where entries are evicted immediately. */
IMMEDIATELY(0L),
/** A configuration where entries are evicted almost immediately. */
ONE_MILLISECOND(TimeUnit.MILLISECONDS.toNanos(1L)),
/** A configuration that holds a single entry. */
ONE_MINUTE(TimeUnit.MINUTES.toNanos(1L)),
/** A configuration that holds the {@link Population#FULL} count. */
FOREVER(Long.MAX_VALUE);
private final long timeNanos;
private Expire(long timeNanos) {
this.timeNanos = timeNanos;
}
public long timeNanos() {
return timeNanos;
}
}
/** The time increment to advance by after each entry is added when populating the cache. */
enum Advance {
ZERO(0),
ONE_MINUTE(TimeUnit.MINUTES.toNanos(1L));
private final long timeNanos;
private Advance(long timeNanos) {
this.timeNanos = timeNanos;
}
public long timeNanos() {
return timeNanos;
}
}
/* ---------------- Reference-based -------------- */
/** Indicates that the combination must have a weak or soft reference collection setting. */
boolean requiresWeakOrSoft() default false;
/** The reference type of that the cache holds a key with (strong or weak only). */
ReferenceType[] keys() default {
ReferenceType.STRONG,
ReferenceType.WEAK
};
/** The reference type of that the cache holds a value with (strong, soft, or weak). */
ReferenceType[] values() default {
ReferenceType.STRONG,
ReferenceType.WEAK,
ReferenceType.SOFT
};
/** The reference type of cache keys and/or values. */
enum ReferenceType {
/** Prevents referent from being reclaimed by the garbage collector. */
STRONG,
/** Referent reclaimed when no strong or soft references exist. */
WEAK,
/**
* Referent reclaimed in an LRU fashion when the VM runs low on memory and no strong
* references exist.
*/
SOFT
}
/* ---------------- Removal -------------- */
/** The removal listeners, each resulting in a new combination. */
Listener[] removalListener() default {
Listener.CONSUMING,
Listener.DEFAULT,
};
enum Listener {
/** A flag indicating that no removal listener is configured. */
DEFAULT {
@Override public <K, V> RemovalListener<K, V> create() {
return null;
}
},
/** A removal listener that rejects all notifications. */
REJECTING {
@Override public <K, V> RemovalListener<K, V> create() {
return RemovalListeners.rejecting();
}
},
/** A {@link ConsumingRemovalListener} retains all notifications for evaluation by the test. */
CONSUMING {
@Override public <K, V> RemovalListener<K, V> create() {
return RemovalListeners.consuming();
}
};
public abstract <K, V> RemovalListener<K, V> create();
}
/* ---------------- CacheLoader -------------- */
// FIXME: A hack to allow the NEGATIVE loader's return value to be retained on refresh
static final ThreadLocal<Interner<Integer>> interner =
ThreadLocal.withInitial(Interners::newStrongInterner);
Loader[] loader() default {
Loader.NEGATIVE,
};
/** The {@link CacheLoader} for constructing the {@link LoadingCache}. */
enum Loader implements CacheLoader<Integer, Integer> {
/** A loader that always returns null (no mapping). */
NULL {
@Override public Integer load(Integer key) {
return null;
}
},
/** A loader that returns the key. */
IDENTITY {
@Override public Integer load(Integer key) {
return key;
}
},
/** A loader that returns the key's negation. */
NEGATIVE {
@Override public Integer load(Integer key) {
return interner.get().intern(-key);
}
},
/** A loader that always throws an exception. */
EXCEPTIONAL {
@Override public Integer load(Integer key) {
throw new IllegalStateException();
}
},
/** A loader that always returns null (no mapping). */
BULK_NULL {
@Override public Integer load(Integer key) {
throw new UnsupportedOperationException();
}
@Override public Map<Integer, Integer> loadAll(Iterable<? extends Integer> keys) {
return null;
}
},
BULK_IDENTITY {
@Override public Integer load(Integer key) {
throw new UnsupportedOperationException();
}
@Override public Map<Integer, Integer> loadAll(Iterable<? extends Integer> keys) {
Map<Integer, Integer> result = new HashMap<>();
keys.forEach(key -> result.put(key, key));
return result;
}
},
BULK_NEGATIVE {
@Override public Integer load(Integer key) {
throw new UnsupportedOperationException();
}
@Override public Map<Integer, Integer> loadAll(Iterable<? extends Integer> keys) {
Map<Integer, Integer> result = new HashMap<>();
keys.forEach(key -> result.put(key, interner.get().intern(-key)));
return result;
}
},
/** A bulk-only loader that loads more than requested. */
BULK_NEGATIVE_EXCEEDS {
@Override public Integer load(Integer key) {
throw new UnsupportedOperationException();
}
@Override public Map<Integer, Integer> loadAll(Iterable<? extends Integer> keys)
throws Exception {
List<Integer> moreKeys = new ArrayList<>(ImmutableList.copyOf(keys));
for (int i = 0; i < 10; i++) {
moreKeys.add(ThreadLocalRandom.current().nextInt());
}
return BULK_NEGATIVE.loadAll(moreKeys);
}
},
/** A bulk-only loader that always throws an exception. */
BULK_EXCEPTIONAL {
@Override public Integer load(Integer key) {
throw new UnsupportedOperationException();
}
@Override public Map<Integer, Integer> loadAll(Iterable<? extends Integer> keys) {
throw new IllegalStateException();
}
};
private final boolean bulk;
private final AsyncCacheLoader<Integer, Integer> asyncLoader;
private Loader() {
bulk = name().startsWith("BULK");
asyncLoader = bulk
? new BulkSeriazableAsyncCacheLoader(this)
: new SeriazableAsyncCacheLoader(this);
}
public boolean isBulk() {
return bulk;
}
/** Returns a serializable view restricted to the {@link AsyncCacheLoader} interface. */
public AsyncCacheLoader<Integer, Integer> async() {
return asyncLoader;
}
private static class SeriazableAsyncCacheLoader
implements AsyncCacheLoader<Integer, Integer>, Serializable {
private static final long serialVersionUID = 1L;
final Loader loader;
SeriazableAsyncCacheLoader(Loader loader) {
this.loader = loader;
}
@Override public CompletableFuture<Integer> asyncLoad(Integer key, Executor executor) {
return loader.asyncLoad(key, executor);
}
private Object readResolve() throws ObjectStreamException {
return loader.asyncLoader;
}
}
private static final class BulkSeriazableAsyncCacheLoader extends SeriazableAsyncCacheLoader {
private static final long serialVersionUID = 1L;
BulkSeriazableAsyncCacheLoader(Loader loader) {
super(loader);
}
@Override public CompletableFuture<Integer> asyncLoad(Integer key, Executor executor) {
throw new IllegalStateException();
}
@Override public CompletableFuture<Map<Integer, Integer>> asyncLoadAll(
Iterable<? extends Integer> keys, Executor executor) {
return loader.asyncLoadAll(keys, executor);
}
}
}
/* ---------------- CacheWriter -------------- */
/** Ignored if weak keys are configured. */
Writer[] writer() default {
Writer.MOCKITO,
};
/** The {@link CacheWriter} for the external resource. */
enum Writer {
/** A writer that does nothing. */
DISABLED {
@Override public <K, V> CacheWriter<K, V> create() {
return CacheWriter.disabledWriter();
}
},
/** A writer that records interactions. */
MOCKITO {
@Override public <K, V> CacheWriter<K, V> create() {
@SuppressWarnings("unchecked")
CacheWriter<K, V> mock = Mockito.mock(CacheWriter.class);
return mock;
}
},
/** A writer that always throws an exception. */
EXCEPTIONAL {
@Override public <K, V> CacheWriter<K, V> create() {
return new RejectingCacheWriter<K, V>();
}
};
public abstract <K, V> CacheWriter<K, V> create();
}
/* ---------------- Executor -------------- */
/** The executors retrieved from a supplier, each resulting in a new combination. */
CacheExecutor[] executor() default {
CacheExecutor.DIRECT,
};
/** If the executor is allowed to have failures. */
ExecutorFailure executorFailure() default ExecutorFailure.DISALLOWED;
enum ExecutorFailure {
EXPECTED, DISALLOWED, IGNORED
}
ExecutorService cachedExecutorService = Executors.newCachedThreadPool(
new ThreadFactoryBuilder().setDaemon(true).build());
/** The executors that the cache can be configured with. */
enum CacheExecutor {
DEFAULT { // fork-join common pool
@Override public Executor create() {
// Use with caution as may be unpredictable during tests if awaiting completion
return null;
}
},
DIRECT {
@Override public Executor create() {
// Cache implementations must avoid deadlocks by incorrectly assuming async execution
return new TrackingExecutor(MoreExecutors.newDirectExecutorService());
}
},
THREADED {
@Override public Executor create() {
return new TrackingExecutor(cachedExecutorService);
}
},
REJECTING {
@Override public Executor create() {
// Cache implementations must avoid corrupting internal state due to rejections
return new ForkJoinPool() {
@Override public void execute(Runnable task) {
throw new RejectedExecutionException();
}
};
}
};
public abstract Executor create();
}
/* ---------------- Populated -------------- */
/**
* The number of entries to populate the cache with. The keys and values are integers starting
* from above the integer cache limit, with the value being the negated key. The cache will never
* be populated to exceed the maximum size, if defined, thereby ensuring that no evictions occur
* prior to the test. Each configuration results in a new combination.
*/
Population[] population() default {
Population.EMPTY,
Population.SINGLETON,
Population.PARTIAL,
Population.FULL
};
/** The population scenarios. */
enum Population {
EMPTY(0),
SINGLETON(1),
PARTIAL(InitialCapacity.FULL.size() / 2),
FULL(InitialCapacity.FULL.size());
private final long size;
private Population(long size) {
this.size = size;
}
public long size() {
return size;
}
}
}