/* * 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; import static com.github.benmanes.caffeine.cache.testing.HasRemovalNotifications.hasRemovalNotifications; import static com.github.benmanes.caffeine.cache.testing.HasStats.hasHitCount; import static com.github.benmanes.caffeine.cache.testing.HasStats.hasLoadFailureCount; import static com.github.benmanes.caffeine.cache.testing.HasStats.hasLoadSuccessCount; import static com.github.benmanes.caffeine.cache.testing.HasStats.hasMissCount; import static com.github.benmanes.caffeine.testing.Awaits.await; import static java.util.stream.Collectors.toList; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.both; import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.nullValue; import static org.hamcrest.Matchers.sameInstance; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; import java.util.concurrent.ExecutionException; import java.util.concurrent.atomic.AtomicBoolean; import org.junit.Assert; import org.testng.annotations.Listeners; import org.testng.annotations.Test; 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.CacheExecutor; import com.github.benmanes.caffeine.cache.testing.CacheSpec.Compute; 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.Loader; import com.github.benmanes.caffeine.cache.testing.CacheSpec.Population; import com.github.benmanes.caffeine.cache.testing.CacheValidationListener; import com.github.benmanes.caffeine.cache.testing.CheckNoWriter; import com.github.benmanes.caffeine.cache.testing.RemovalNotification; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import com.google.common.primitives.Ints; /** * The test cases for the {@link LoadingCache} interface that simulate the most generic usages. * These tests do not validate eviction management, concurrency behavior, or the * {@link Cache#asMap()} view. * * @author ben.manes@gmail.com (Ben Manes) */ @Listeners(CacheValidationListener.class) @Test(dataProviderClass = CacheProvider.class) public final class LoadingCacheTest { /* ---------------- get -------------- */ @CacheSpec @CheckNoWriter @Test(dataProvider = "caches", expectedExceptions = NullPointerException.class) public void get_null(LoadingCache<Integer, Integer> cache, CacheContext context) { cache.get(null); } @CheckNoWriter @Test(dataProvider = "caches") @CacheSpec(loader = Loader.NULL) public void get_absent_null(LoadingCache<Integer, Integer> cache, CacheContext context) { assertThat(cache.get(context.absentKey()), is(nullValue())); assertThat(context, both(hasMissCount(1)).and(hasHitCount(0))); assertThat(context, both(hasLoadSuccessCount(0)).and(hasLoadFailureCount(1))); } @CheckNoWriter @CacheSpec(loader = Loader.EXCEPTIONAL) @Test(dataProvider = "caches", expectedExceptions = IllegalStateException.class) public void get_absent_failure(LoadingCache<Integer, Integer> cache, CacheContext context) { try { cache.get(context.absentKey()); } finally { assertThat(context, both(hasMissCount(1)).and(hasHitCount(0))); assertThat(context, both(hasLoadSuccessCount(0)).and(hasLoadFailureCount(1))); } } @CacheSpec @CheckNoWriter @Test(dataProvider = "caches") public void get_absent(LoadingCache<Integer, Integer> cache, CacheContext context) { Integer key = context.absentKey(); Integer value = cache.get(key); assertThat(value, is(-key)); assertThat(context, both(hasMissCount(1)).and(hasHitCount(0))); assertThat(context, both(hasLoadSuccessCount(1)).and(hasLoadFailureCount(0))); } @CheckNoWriter @Test(dataProvider = "caches") @CacheSpec(population = { Population.SINGLETON, Population.PARTIAL, Population.FULL }) public void get_present(LoadingCache<Integer, Integer> cache, CacheContext context) { assertThat(cache.get(context.firstKey()), is(-context.firstKey())); assertThat(cache.get(context.middleKey()), is(-context.middleKey())); assertThat(cache.get(context.lastKey()), is(-context.lastKey())); assertThat(context, both(hasMissCount(0)).and(hasHitCount(3))); assertThat(context, both(hasLoadSuccessCount(0)).and(hasLoadFailureCount(0))); } /* ---------------- getAll -------------- */ @CheckNoWriter @CacheSpec(removalListener = { Listener.DEFAULT, Listener.REJECTING }) @Test(dataProvider = "caches", expectedExceptions = NullPointerException.class) public void getAll_iterable_null(LoadingCache<Integer, Integer> cache, CacheContext context) { cache.getAll(null); } @CheckNoWriter @CacheSpec(loader = { Loader.NEGATIVE, Loader.BULK_NEGATIVE }, removalListener = { Listener.DEFAULT, Listener.REJECTING }) @Test(dataProvider = "caches", expectedExceptions = NullPointerException.class) public void getAll_iterable_nullKey(LoadingCache<Integer, Integer> cache, CacheContext context) { cache.getAll(Collections.singletonList(null)); } @CheckNoWriter @Test(dataProvider = "caches") @CacheSpec(loader = { Loader.NEGATIVE, Loader.BULK_NEGATIVE }, removalListener = { Listener.DEFAULT, Listener.REJECTING }) public void getAll_iterable_empty(LoadingCache<Integer, Integer> cache, CacheContext context) { Map<Integer, Integer> result = cache.getAll(ImmutableList.of()); assertThat(result.size(), is(0)); assertThat(context, both(hasMissCount(0)).and(hasHitCount(0))); } @CacheSpec @Test(dataProvider = "caches", expectedExceptions = UnsupportedOperationException.class) public void getAll_immutable(LoadingCache<Integer, Integer> cache, CacheContext context) { cache.getAll(context.absentKeys()).clear(); } @CheckNoWriter @Test(dataProvider = "caches") @CacheSpec(loader = Loader.NULL) public void getAll_absent_null(LoadingCache<Integer, Integer> cache, CacheContext context) { assertThat(cache.getAll(context.absentKeys()), is(ImmutableMap.of())); } @CheckNoWriter @CacheSpec(loader = Loader.BULK_NULL) @Test(dataProvider = "caches", expectedExceptions = Exception.class) public void getAll_absent_bulkNull(LoadingCache<Integer, Integer> cache, CacheContext context) { cache.getAll(context.absentKeys()); } @CheckNoWriter @CacheSpec(loader = { Loader.EXCEPTIONAL, Loader.BULK_EXCEPTIONAL }) @Test(dataProvider = "caches", expectedExceptions = IllegalStateException.class) public void getAll_absent_failure(LoadingCache<Integer, Integer> cache, CacheContext context) { try { cache.getAll(context.absentKeys()); } finally { int misses = context.absentKeys().size(); int loadFailures = context.loader().isBulk() ? 1 : (context.isAsync() ? misses : 1); assertThat(context, both(hasMissCount(misses)).and(hasHitCount(0))); assertThat(context, both(hasLoadSuccessCount(0)).and(hasLoadFailureCount(loadFailures))); } } @CheckNoWriter @CacheSpec(loader = { Loader.EXCEPTIONAL, Loader.BULK_EXCEPTIONAL }) @Test(dataProvider = "caches", expectedExceptions = IllegalStateException.class) public void getAll_absent_failure_iterable( LoadingCache<Integer, Integer> cache, CacheContext context) { try { cache.getAll(() -> context.absentKeys().iterator()); } finally { int misses = context.absentKeys().size(); int loadFailures = context.loader().isBulk() ? 1 : (context.isAsync() ? misses : 1); assertThat(context, both(hasMissCount(misses)).and(hasHitCount(0))); assertThat(context, both(hasLoadSuccessCount(0)).and(hasLoadFailureCount(loadFailures))); } } @CheckNoWriter @Test(dataProvider = "caches") @CacheSpec(loader = { Loader.NEGATIVE, Loader.BULK_NEGATIVE }, removalListener = { Listener.DEFAULT, Listener.REJECTING }) public void getAll_absent(LoadingCache<Integer, Integer> cache, CacheContext context) { Map<Integer, Integer> result = cache.getAll(context.absentKeys()); int count = context.absentKeys().size(); int loads = context.loader().isBulk() ? 1 : count; assertThat(result.size(), is(count)); assertThat(context, both(hasMissCount(count)).and(hasHitCount(0))); assertThat(context, both(hasLoadSuccessCount(loads)).and(hasLoadFailureCount(0))); } @CheckNoWriter @Test(dataProvider = "caches") @CacheSpec(loader = { Loader.NEGATIVE, Loader.BULK_NEGATIVE }, population = { Population.SINGLETON, Population.PARTIAL, Population.FULL }, removalListener = { Listener.DEFAULT, Listener.REJECTING }) public void getAll_present_partial(LoadingCache<Integer, Integer> cache, CacheContext context) { Map<Integer, Integer> expect = new HashMap<>(); expect.put(context.firstKey(), -context.firstKey()); expect.put(context.middleKey(), -context.middleKey()); expect.put(context.lastKey(), -context.lastKey()); Map<Integer, Integer> result = cache.getAll(expect.keySet()); assertThat(result, is(equalTo(expect))); assertThat(context, both(hasMissCount(0)).and(hasHitCount(expect.size()))); assertThat(context, both(hasLoadSuccessCount(0)).and(hasLoadFailureCount(0))); } @CheckNoWriter @Test(dataProvider = "caches") @CacheSpec(loader = { Loader.NEGATIVE, Loader.BULK_NEGATIVE }, population = { Population.SINGLETON, Population.PARTIAL, Population.FULL }, removalListener = { Listener.DEFAULT, Listener.REJECTING }) public void getAll_present_full(LoadingCache<Integer, Integer> cache, CacheContext context) { Map<Integer, Integer> result = cache.getAll(context.original().keySet()); assertThat(result, is(equalTo(context.original()))); assertThat(context, both(hasMissCount(0)).and(hasHitCount(result.size()))); assertThat(context, both(hasLoadSuccessCount(0)).and(hasLoadFailureCount(0))); } @CheckNoWriter @Test(dataProvider = "caches") @CacheSpec(loader = { Loader.NEGATIVE, Loader.BULK_NEGATIVE }, population = { Population.SINGLETON, Population.PARTIAL, Population.FULL }, removalListener = { Listener.DEFAULT, Listener.REJECTING }) public void getAll_duplicates(LoadingCache<Integer, Integer> cache, CacheContext context) { Set<Integer> absentKeys = ImmutableSet.copyOf(Iterables.limit(context.absentKeys(), Ints.saturatedCast(context.maximum().max() - context.initialSize()))); Iterable<Integer> keys = Iterables.concat(absentKeys, absentKeys, context.original().keySet(), context.original().keySet()); Map<Integer, Integer> result = cache.getAll(keys); assertThat(context, hasMissCount(absentKeys.size())); assertThat(context, hasHitCount(context.initialSize())); assertThat(result.keySet(), is(equalTo(ImmutableSet.copyOf(keys)))); int loads = context.loader().isBulk() ? 1 : absentKeys.size(); assertThat(context, both(hasLoadSuccessCount(loads)).and(hasLoadFailureCount(0))); } /* ---------------- refresh -------------- */ @CheckNoWriter @CacheSpec(removalListener = { Listener.DEFAULT, Listener.REJECTING }) @Test(dataProvider = "caches", expectedExceptions = NullPointerException.class) public void refresh_null(LoadingCache<Integer, Integer> cache, CacheContext context) { cache.refresh(null); } @CheckNoWriter @Test(dataProvider = "caches") @CacheSpec(implementation = Implementation.Caffeine, compute=Compute.SYNC, executor = CacheExecutor.DIRECT, loader = Loader.NULL, population = { Population.SINGLETON, Population.PARTIAL, Population.FULL }) public void refresh_remove(LoadingCache<Integer, Integer> cache, CacheContext context) { cache.refresh(context.firstKey()); assertThat(cache.estimatedSize(), is(context.initialSize() - 1)); assertThat(cache.getIfPresent(context.firstKey()), is(nullValue())); assertThat(cache, hasRemovalNotifications(context, 1, RemovalCause.EXPLICIT)); } @CheckNoWriter @Test(dataProvider = "caches") @CacheSpec(executor = CacheExecutor.DIRECT, loader = Loader.EXCEPTIONAL, removalListener = { Listener.DEFAULT, Listener.REJECTING }, population = { Population.SINGLETON, Population.PARTIAL, Population.FULL }) public void refresh_failure(LoadingCache<Integer, Integer> cache, CacheContext context) { // Shouldn't leak exception to caller and should retain stale entry cache.refresh(context.absentKey()); cache.refresh(context.firstKey()); assertThat(cache.estimatedSize(), is(context.initialSize())); assertThat(context, both(hasLoadSuccessCount(0)).and(hasLoadFailureCount(2))); } @CheckNoWriter @CacheSpec(loader = Loader.NULL) @Test(dataProvider = "caches") public void refresh_absent_null(LoadingCache<Integer, Integer> cache, CacheContext context) { cache.refresh(context.absentKey()); assertThat(cache.estimatedSize(), is(context.initialSize())); } @CheckNoWriter @Test(dataProvider = "caches") @CacheSpec(removalListener = { Listener.DEFAULT, Listener.REJECTING }) public void refresh_absent(LoadingCache<Integer, Integer> cache, CacheContext context) { cache.refresh(context.absentKey()); assertThat(cache.estimatedSize(), is(1 + context.initialSize())); assertThat(context, both(hasMissCount(0)).and(hasHitCount(0))); assertThat(context, both(hasLoadSuccessCount(1)).and(hasLoadFailureCount(0))); // records a hit assertThat(cache.get(context.absentKey()), is(-context.absentKey())); } @CheckNoWriter @Test(dataProvider = "caches") @CacheSpec(implementation = Implementation.Caffeine, loader = Loader.NULL, population = { Population.SINGLETON, Population.PARTIAL, Population.FULL }) public void refresh_present_null(LoadingCache<Integer, Integer> cache, CacheContext context) { for (Integer key : context.firstMiddleLastKeys()) { cache.refresh(key); } int count = context.firstMiddleLastKeys().size(); assertThat(context, both(hasMissCount(0)).and(hasHitCount(0))); assertThat(context, both(hasLoadSuccessCount(0)).and(hasLoadFailureCount(count))); for (Integer key : context.firstMiddleLastKeys()) { assertThat(cache.getIfPresent(key), is(nullValue())); } assertThat(cache.estimatedSize(), is(context.initialSize() - count)); assertThat(cache, hasRemovalNotifications(context, count, RemovalCause.EXPLICIT)); } @CheckNoWriter @Test(dataProvider = "caches") @CacheSpec(population = { Population.SINGLETON, Population.PARTIAL, Population.FULL }) public void refresh_present_sameValue( LoadingCache<Integer, Integer> cache, CacheContext context) { for (Integer key : context.firstMiddleLastKeys()) { cache.refresh(key); } int count = context.firstMiddleLastKeys().size(); assertThat(context, both(hasMissCount(0)).and(hasHitCount(0))); assertThat(context, both(hasLoadSuccessCount(count)).and(hasLoadFailureCount(0))); for (Integer key : context.firstMiddleLastKeys()) { assertThat(cache.get(key), is(context.original().get(key))); } assertThat(cache.estimatedSize(), is(context.initialSize())); assertThat(cache, hasRemovalNotifications(context, count, RemovalCause.REPLACED)); } @CheckNoWriter @Test(dataProvider = "caches") @CacheSpec(loader = Loader.IDENTITY, population = { Population.SINGLETON, Population.PARTIAL, Population.FULL }) public void refresh_present_differentValue( LoadingCache<Integer, Integer> cache, CacheContext context) { for (Integer key : context.firstMiddleLastKeys()) { cache.refresh(key); // records a hit assertThat(cache.get(key), is(key)); } int count = context.firstMiddleLastKeys().size(); assertThat(cache.estimatedSize(), is(context.initialSize())); assertThat(cache, hasRemovalNotifications(context, count, RemovalCause.REPLACED)); assertThat(context, both(hasMissCount(0)).and(hasHitCount(count))); assertThat(context, both(hasLoadSuccessCount(count)).and(hasLoadFailureCount(0))); } @Test(dataProvider = "caches") @CacheSpec(population = Population.EMPTY, executor = CacheExecutor.THREADED, removalListener = Listener.CONSUMING) public void refresh_conflict(CacheContext context) { AtomicBoolean refresh = new AtomicBoolean(); Integer key = context.absentKey(); Integer original = 1; Integer updated = 2; Integer refreshed = 3; LoadingCache<Integer, Integer> cache = context.build(k -> { await().untilTrue(refresh); return refreshed; }); cache.put(key, original); cache.refresh(key); assertThat(cache.asMap().put(key, updated), is(original)); refresh.set(true); await().until(() -> context.consumedNotifications().size(), is(2)); List<Integer> removed = context.consumedNotifications().stream() .map(RemovalNotification::getValue).collect(toList()); assertThat(cache.getIfPresent(key), is(updated)); assertThat(removed, containsInAnyOrder(original, refreshed)); assertThat(cache, hasRemovalNotifications(context, 2, RemovalCause.REPLACED)); assertThat(context, both(hasLoadSuccessCount(1)).and(hasLoadFailureCount(0))); } @Test(dataProvider = "caches") @CacheSpec(population = Population.EMPTY, executor = CacheExecutor.THREADED, removalListener = Listener.CONSUMING) public void refresh_invalidate(CacheContext context) { AtomicBoolean refresh = new AtomicBoolean(); Integer key = context.absentKey(); Integer original = 1; Integer refreshed = 2; LoadingCache<Integer, Integer> cache = context.build(k -> { await().untilTrue(refresh); return refreshed; }); cache.put(key, original); cache.refresh(key); cache.invalidate(key); refresh.set(true); await().until(() -> cache.getIfPresent(key), is(refreshed)); await().until(() -> cache, hasRemovalNotifications(context, 1, RemovalCause.EXPLICIT)); await().until(() -> context, both(hasLoadSuccessCount(1)).and(hasLoadFailureCount(0))); } /* ---------------- CacheLoader -------------- */ @Test(expectedExceptions = UnsupportedOperationException.class) public void loadAll() throws Exception { CacheLoader<Object, ?> loader = key -> key; loader.loadAll(Collections.emptyList()); } @Test public void reload() throws Exception { CacheLoader<Integer, Integer> loader = key -> key; assertThat(loader.reload(1, 1), is(1)); } @Test public void asyncLoad_exception() throws Exception { Exception e = new Exception(); CacheLoader<Integer, Integer> loader = key -> { throw e; }; try { loader.asyncLoad(1, Runnable::run).join(); } catch (CompletionException ex) { assertThat(ex.getCause(), is(sameInstance(e))); } } @Test public void asyncLoad() throws Exception { CacheLoader<Integer, ?> loader = key -> key; CompletableFuture<?> future = loader.asyncLoad(1, Runnable::run); assertThat(future.get(), is(1)); } @Test public void asyncLoadAll_exception() throws Exception { Exception e = new Exception(); CacheLoader<Integer, Integer> loader = new CacheLoader<Integer, Integer>() { @Override public Integer load(Integer key) throws Exception { throw new AssertionError(); } @Override public Map<Integer, Integer> loadAll( Iterable<? extends Integer> keys) throws Exception { throw e; } }; try { loader.asyncLoadAll(Arrays.asList(1), Runnable::run).join(); } catch (CompletionException ex) { assertThat(ex.getCause(), is(sameInstance(e))); } } @Test(expectedExceptions = UnsupportedOperationException.class) public void asyncLoadAll() throws Throwable { CacheLoader<Object, ?> loader = key -> key; try { loader.asyncLoadAll(Collections.emptyList(), Runnable::run).get(); } catch (ExecutionException e) { throw e.getCause(); } } @Test public void asyncReload_exception() throws Exception { for (Exception e : Arrays.asList(new Exception(), new RuntimeException())) { CacheLoader<Integer, Integer> loader = key -> { throw e; }; try { loader.asyncReload(1, 1, Runnable::run).join(); Assert.fail(); } catch (CompletionException ex) { assertThat(ex.getCause(), is(sameInstance(e))); } } } @Test public void asyncReload() throws Exception { CacheLoader<Integer, Integer> loader = key -> -key; CompletableFuture<?> future = loader.asyncReload(1, 2, Runnable::run); assertThat(future.get(), is(-1)); } }