/*
* Copyright 2017 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;
import static com.github.benmanes.caffeine.testing.IsEmptyMap.emptyMap;
import static java.util.concurrent.TimeUnit.NANOSECONDS;
import static java.util.function.Function.identity;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.is;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Optional;
import java.util.OptionalLong;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import org.testng.annotations.Listeners;
import org.testng.annotations.Test;
import com.github.benmanes.caffeine.cache.Policy.VarExpiration;
import com.github.benmanes.caffeine.cache.testing.CacheContext;
import com.github.benmanes.caffeine.cache.testing.CacheProvider;
import com.github.benmanes.caffeine.cache.testing.CacheSpec;
import com.github.benmanes.caffeine.cache.testing.CacheSpec.CacheExpiry;
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.Listener;
import com.github.benmanes.caffeine.cache.testing.CacheSpec.Population;
import com.github.benmanes.caffeine.cache.testing.CacheSpec.Writer;
import com.github.benmanes.caffeine.cache.testing.CacheValidationListener;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
/**
* The test cases for caches that support the variable expiration policy.
*
* @author ben.manes@gmail.com (Ben Manes)
*/
@Listeners(CacheValidationListener.class)
@Test(dataProviderClass = CacheProvider.class)
public final class ExpireAfterVarTest {
/* ---------------- Exceptional -------------- */
@CacheSpec(implementation = Implementation.Caffeine,
population = Population.FULL, expiry = CacheExpiry.MOCKITO)
@Test(dataProvider = "caches", expectedExceptions = ExpirationException.class)
public void getIfPresent_expiryFails(Cache<Integer, Integer> cache, CacheContext context) {
try {
context.ticker().advance(1, TimeUnit.HOURS);
when(context.expiry().expireAfterRead(any(), any(), anyLong(), anyLong()))
.thenThrow(ExpirationException.class);
cache.getIfPresent(context.firstKey());
} finally {
context.ticker().advance(-1, TimeUnit.HOURS);
assertThat(cache.asMap(), equalTo(context.original()));
}
}
@CacheSpec(implementation = Implementation.Caffeine, population = Population.FULL,
expiry = CacheExpiry.MOCKITO, writer = Writer.MOCKITO, removalListener = Listener.REJECTING)
@Test(dataProvider = "caches", expectedExceptions = ExpirationException.class)
public void get_expiryFails_create(Cache<Integer, Integer> cache, CacheContext context) {
try {
context.ticker().advance(1, TimeUnit.HOURS);
when(context.expiry().expireAfterCreate(any(), any(), anyLong()))
.thenThrow(ExpirationException.class);
cache.get(context.absentKey(), identity());
} finally {
context.ticker().advance(-1, TimeUnit.HOURS);
assertThat(cache.asMap(), equalTo(context.original()));
verify(context.cacheWriter(), never()).write(anyInt(), anyInt());
}
}
@CacheSpec(implementation = Implementation.Caffeine,
population = Population.FULL, expiry = CacheExpiry.MOCKITO)
@Test(dataProvider = "caches", expectedExceptions = ExpirationException.class)
public void get_expiryFails_read(Cache<Integer, Integer> cache, CacheContext context) {
try {
context.ticker().advance(1, TimeUnit.HOURS);
when(context.expiry().expireAfterRead(any(), any(), anyLong(), anyLong()))
.thenThrow(ExpirationException.class);
cache.get(context.firstKey(), identity());
} finally {
context.ticker().advance(-1, TimeUnit.HOURS);
assertThat(cache.asMap(), equalTo(context.original()));
}
}
@CacheSpec(implementation = Implementation.Caffeine,
population = Population.FULL, expiry = CacheExpiry.MOCKITO)
@Test(dataProvider = "caches", expectedExceptions = ExpirationException.class)
public void getAllPresent_expiryFails(Cache<Integer, Integer> cache, CacheContext context) {
try {
context.ticker().advance(1, TimeUnit.HOURS);
when(context.expiry().expireAfterRead(any(), any(), anyLong(), anyLong()))
.thenThrow(ExpirationException.class);
cache.getAllPresent(context.firstMiddleLastKeys());
} finally {
context.ticker().advance(-1, TimeUnit.HOURS);
assertThat(cache.asMap(), equalTo(context.original()));
}
}
@CacheSpec(implementation = Implementation.Caffeine,
population = Population.FULL, expiry = CacheExpiry.MOCKITO)
@Test(dataProvider = "caches", expectedExceptions = ExpirationException.class)
public void put_insert_expiryFails(Cache<Integer, Integer> cache, CacheContext context) {
try {
context.ticker().advance(1, TimeUnit.HOURS);
when(context.expiry().expireAfterCreate(any(), any(), anyLong()))
.thenThrow(ExpirationException.class);
cache.put(context.absentKey(), context.absentValue());
} finally {
context.ticker().advance(-1, TimeUnit.HOURS);
assertThat(cache.asMap(), equalTo(context.original()));
}
}
@CacheSpec(implementation = Implementation.Caffeine, expiryTime = Expire.ONE_MINUTE,
population = Population.FULL, expiry = CacheExpiry.MOCKITO)
@Test(dataProvider = "caches", expectedExceptions = ExpirationException.class)
public void put_insert_replaceExpred_expiryFails(Cache<Integer, Integer> cache,
CacheContext context, VarExpiration<Integer, Integer> expireVariably) {
OptionalLong duration = expireVariably.getExpiresAfter(context.firstKey(), NANOSECONDS);
try {
context.ticker().advance(1, TimeUnit.HOURS);
when(context.expiry().expireAfterCreate(any(), any(), anyLong()))
.thenThrow(ExpirationException.class);
cache.put(context.firstKey(), context.absentValue());
} finally {
context.ticker().advance(-1, TimeUnit.HOURS);
assertThat(cache.asMap(), equalTo(context.original()));
assertThat(expireVariably.getExpiresAfter(context.firstKey(), NANOSECONDS), is(duration));
}
}
@CacheSpec(implementation = Implementation.Caffeine,
population = Population.FULL, expiry = CacheExpiry.MOCKITO)
@Test(dataProvider = "caches", expectedExceptions = ExpirationException.class)
public void put_update_expiryFails(Cache<Integer, Integer> cache, CacheContext context,
VarExpiration<Integer, Integer> expireVariably) {
OptionalLong duration = expireVariably.getExpiresAfter(context.firstKey(), NANOSECONDS);
try {
context.ticker().advance(1, TimeUnit.HOURS);
when(context.expiry().expireAfterUpdate(any(), any(), anyLong(), anyLong()))
.thenThrow(ExpirationException.class);
cache.put(context.firstKey(), context.absentValue());
} finally {
context.ticker().advance(-1, TimeUnit.HOURS);
assertThat(cache.asMap(), equalTo(context.original()));
assertThat(expireVariably.getExpiresAfter(context.firstKey(), NANOSECONDS), is(duration));
}
}
static final class ExpirationException extends RuntimeException {
private static final long serialVersionUID = 1L;
}
/* ---------------- Policy -------------- */
@Test(dataProvider = "caches")
@CacheSpec(implementation = Implementation.Caffeine,
population = Population.EMPTY, expiry = CacheExpiry.DISABLED)
public void expireVariably_notEnabled(Cache<Integer, Integer> cache) {
assertThat(cache.policy().expireVariably(), is(Optional.empty()));
}
@Test(dataProvider = "caches")
@CacheSpec(implementation = Implementation.Caffeine, population = Population.FULL,
expiry = CacheExpiry.MOCKITO, expiryTime = Expire.ONE_MINUTE)
public void getExpiresAfter(Cache<Integer, Integer> cache, CacheContext context,
VarExpiration<Integer, Integer> expireAfterVar) {
assertThat(expireAfterVar.getExpiresAfter(context.absentKey(), TimeUnit.MINUTES),
is(OptionalLong.empty()));
assertThat(expireAfterVar.getExpiresAfter(context.firstKey(), TimeUnit.MINUTES),
is(OptionalLong.of(1)));
when(context.expiry().expireAfterUpdate(any(), any(), anyLong(), anyLong()))
.thenReturn(TimeUnit.HOURS.toNanos(1));
cache.put(context.firstKey(), context.absentValue());
assertThat(expireAfterVar.getExpiresAfter(context.firstKey(), TimeUnit.MINUTES),
is(OptionalLong.of(60)));
assertThat(expireAfterVar.getExpiresAfter(context.lastKey(), TimeUnit.MINUTES),
is(OptionalLong.of(1)));
}
@Test(dataProvider = "caches")
@CacheSpec(implementation = Implementation.Caffeine, population = Population.FULL,
expiry = CacheExpiry.MOCKITO, expiryTime = Expire.ONE_MINUTE)
public void setExpiresAfter(Cache<Integer, Integer> cache, CacheContext context,
VarExpiration<Integer, Integer> expireAfterVar) {
expireAfterVar.setExpiresAfter(context.firstKey(), 2, TimeUnit.MINUTES);
assertThat(expireAfterVar.getExpiresAfter(context.firstKey(), TimeUnit.MINUTES),
is(OptionalLong.of(2)));
expireAfterVar.setExpiresAfter(context.lastKey(), -2, TimeUnit.MINUTES);
assertThat(expireAfterVar.getExpiresAfter(context.lastKey(), TimeUnit.MINUTES),
is(OptionalLong.empty()));
expireAfterVar.setExpiresAfter(context.absentKey(), 4, TimeUnit.MINUTES);
assertThat(expireAfterVar.getExpiresAfter(context.absentKey(), TimeUnit.MINUTES),
is(OptionalLong.empty()));
context.ticker().advance(90, TimeUnit.SECONDS);
cache.cleanUp();
assertThat(cache.estimatedSize(), is(1L));
}
/* ---------------- Policy: oldest -------------- */
@CacheSpec(implementation = Implementation.Caffeine, expiry = CacheExpiry.ACCESS)
@Test(dataProvider = "caches", expectedExceptions = UnsupportedOperationException.class)
public void oldest_unmodifiable(CacheContext context,
VarExpiration<Integer, Integer> expireAfterVar) {
expireAfterVar.oldest(Integer.MAX_VALUE).clear();
}
@CacheSpec(implementation = Implementation.Caffeine, expiry = CacheExpiry.ACCESS)
@Test(dataProvider = "caches", expectedExceptions = IllegalArgumentException.class)
public void oldest_negative(CacheContext context,
VarExpiration<Integer, Integer> expireAfterVar) {
expireAfterVar.oldest(-1);
}
@Test(dataProvider = "caches")
@CacheSpec(implementation = Implementation.Caffeine, expiry = CacheExpiry.ACCESS)
public void oldest_zero(CacheContext context,
VarExpiration<Integer, Integer> expireAfterVar) {
assertThat(expireAfterVar.oldest(0), is(emptyMap()));
}
@Test(dataProvider = "caches")
@CacheSpec(implementation = Implementation.Caffeine,
population = Population.FULL, expiry = CacheExpiry.ACCESS)
public void oldest_partial(CacheContext context,
VarExpiration<Integer, Integer> expireAfterVar) {
int count = (int) context.initialSize() / 2;
assertThat(expireAfterVar.oldest(count).size(), is(count));
}
@Test(dataProvider = "caches")
@CacheSpec(implementation = Implementation.Caffeine,
population = {Population.PARTIAL, Population.FULL}, expiry = CacheExpiry.ACCESS,
removalListener = { Listener.DEFAULT, Listener.REJECTING })
public void oldest_order(CacheContext context,
VarExpiration<Integer, Integer> expireAfterVar) {
Map<Integer, Integer> oldest = expireAfterVar.oldest(Integer.MAX_VALUE);
assertThat(oldest.keySet(), contains(context.original().keySet().toArray(new Integer[0])));
}
@Test(dataProvider = "caches")
@CacheSpec(implementation = Implementation.Caffeine, expiry = CacheExpiry.ACCESS)
public void oldest_snapshot(Cache<Integer, Integer> cache, CacheContext context,
VarExpiration<Integer, Integer> expireAfterVar) {
Map<Integer, Integer> oldest = expireAfterVar.oldest(Integer.MAX_VALUE);
cache.invalidateAll();
assertThat(oldest, is(equalTo(context.original())));
}
/* ---------------- Policy: youngest -------------- */
@CacheSpec(implementation = Implementation.Caffeine, expiry = CacheExpiry.ACCESS)
@Test(dataProvider = "caches", expectedExceptions = UnsupportedOperationException.class)
public void youngest_unmodifiable(CacheContext context,
VarExpiration<Integer, Integer> expireAfterVar) {
expireAfterVar.youngest(Integer.MAX_VALUE).clear();;
}
@CacheSpec(implementation = Implementation.Caffeine, expiry = CacheExpiry.ACCESS)
@Test(dataProvider = "caches", expectedExceptions = IllegalArgumentException.class)
public void youngest_negative(CacheContext context,
VarExpiration<Integer, Integer> expireAfterVar) {
expireAfterVar.youngest(-1);
}
@Test(dataProvider = "caches")
@CacheSpec(implementation = Implementation.Caffeine, expiry = CacheExpiry.ACCESS)
public void youngest_zero(CacheContext context,
VarExpiration<Integer, Integer> expireAfterVar) {
assertThat(expireAfterVar.youngest(0), is(emptyMap()));
}
@Test(dataProvider = "caches")
@CacheSpec(implementation = Implementation.Caffeine,
population = Population.FULL, expiry = CacheExpiry.ACCESS)
public void youngest_partial(CacheContext context,
VarExpiration<Integer, Integer> expireAfterVar) {
int count = (int) context.initialSize() / 2;
assertThat(expireAfterVar.youngest(count).size(), is(count));
}
@Test(dataProvider = "caches")
@CacheSpec(implementation = Implementation.Caffeine,
population = {Population.PARTIAL, Population.FULL}, expiry = CacheExpiry.ACCESS,
removalListener = { Listener.DEFAULT, Listener.REJECTING })
public void youngest_order(CacheContext context,
VarExpiration<Integer, Integer> expireAfterVar) {
Map<Integer, Integer> youngest = expireAfterVar.youngest(Integer.MAX_VALUE);
Set<Integer> keys = new LinkedHashSet<>(ImmutableList.copyOf(youngest.keySet()).reverse());
assertThat(keys, contains(Iterables.toArray(keys, Integer.class)));
}
@Test(dataProvider = "caches")
@CacheSpec(implementation = Implementation.Caffeine, expiry = CacheExpiry.ACCESS)
public void youngest_snapshot(Cache<Integer, Integer> cache, CacheContext context,
VarExpiration<Integer, Integer> expireAfterVar) {
Map<Integer, Integer> youngest = expireAfterVar.youngest(Integer.MAX_VALUE);
cache.invalidateAll();
assertThat(youngest, is(equalTo(context.original())));
}
}