/* * Copyright (C) 2011 The Guava Authors * * 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.google.common.cache; import static com.google.common.cache.LocalCache.Strength.STRONG; import static com.google.common.cache.TestingRemovalListeners.countingRemovalListener; import static com.google.common.collect.Maps.immutableEntry; import static org.junit.contrib.truth.Truth.ASSERT; import com.google.common.base.Function; import com.google.common.cache.LocalCache.Strength; import com.google.common.cache.TestingRemovalListeners.CountingRemovalListener; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import junit.framework.TestCase; import java.lang.ref.WeakReference; /** * Tests of basic {@link LoadingCache} operations with all possible combinations of key & value * strengths. * * @author mike nonemacher */ public class CacheReferencesTest extends TestCase { private static final CacheLoader<Key,String> KEY_TO_STRING_LOADER = new CacheLoader<Key, String>() { @Override public String load(Key key) { return key.toString(); } }; private CacheBuilderFactory factoryWithAllKeyStrengths() { return new CacheBuilderFactory() .withKeyStrengths(ImmutableSet.of(STRONG, Strength.WEAK)) .withValueStrengths(ImmutableSet.of(STRONG, Strength.WEAK, Strength.SOFT)); } private Iterable<LoadingCache<Key, String>> caches() { CacheBuilderFactory factory = factoryWithAllKeyStrengths(); return Iterables.transform(factory.buildAllPermutations(), new Function<CacheBuilder<Object, Object>, LoadingCache<Key, String>>() { @Override public LoadingCache<Key, String> apply(CacheBuilder<Object, Object> builder) { return builder.build(KEY_TO_STRING_LOADER); } }); } public void testContainsKeyAndValue() { for (LoadingCache<Key, String> cache : caches()) { // maintain strong refs so these won't be collected, regardless of cache's key/value strength Key key = new Key(1); String value = key.toString(); assertSame(value, cache.getUnchecked(key)); assertTrue(cache.asMap().containsKey(key)); assertTrue(cache.asMap().containsValue(value)); assertEquals(1, cache.size()); } } public void testClear() { for (LoadingCache<Key, String> cache : caches()) { Key key = new Key(1); String value = key.toString(); assertSame(value, cache.getUnchecked(key)); assertFalse(cache.asMap().isEmpty()); cache.invalidateAll(); assertEquals(0, cache.size()); assertTrue(cache.asMap().isEmpty()); assertFalse(cache.asMap().containsKey(key)); assertFalse(cache.asMap().containsValue(value)); } } public void testKeySetEntrySetValues() { for (LoadingCache<Key, String> cache : caches()) { Key key1 = new Key(1); String value1 = key1.toString(); Key key2 = new Key(2); String value2 = key2.toString(); assertSame(value1, cache.getUnchecked(key1)); assertSame(value2, cache.getUnchecked(key2)); assertEquals(ImmutableSet.of(key1, key2), cache.asMap().keySet()); ASSERT.that(cache.asMap().values()).hasContentsAnyOrder(value1, value2); assertEquals(ImmutableSet.of(immutableEntry(key1, value1), immutableEntry(key2, value2)), cache.asMap().entrySet()); } } public void testInvalidate() { for (LoadingCache<Key, String> cache : caches()) { Key key1 = new Key(1); String value1 = key1.toString(); Key key2 = new Key(2); String value2 = key2.toString(); assertSame(value1, cache.getUnchecked(key1)); assertSame(value2, cache.getUnchecked(key2)); cache.invalidate(key1); assertFalse(cache.asMap().containsKey(key1)); assertTrue(cache.asMap().containsKey(key2)); assertEquals(1, cache.size()); assertEquals(ImmutableSet.of(key2), cache.asMap().keySet()); ASSERT.that(cache.asMap().values()).hasContentsAnyOrder(value2); assertEquals(ImmutableSet.of(immutableEntry(key2, value2)), cache.asMap().entrySet()); } } public void testCleanupOnReferenceCollection() { for (CacheBuilder<Object, Object> builder : factoryWithAllKeyStrengths().buildAllPermutations()) { if (builder.keyStrength == STRONG && builder.valueStrength == STRONG) { continue; } CountingRemovalListener<Integer, String> removalListener = countingRemovalListener(); CacheLoader<Integer, String> toStringLoader = new CacheLoader<Integer, String>() { @Override public String load(Integer key) { return key.toString(); } }; LoadingCache<Integer, String> cache = builder.removalListener(removalListener).build(toStringLoader); // ints in [-128, 127] have their wrappers precomputed and cached, so they won't be GCed Integer key1 = 1001; Integer key2 = 1002; String value1 = cache.getUnchecked(key1); String value2 = cache.getUnchecked(key2); // make (key1, value1) eligible for collection key1 = null; value1 = null; assertCleanup(cache, removalListener); // make sure the GC isn't going to see key2 or value2 as dead in assertCleanup assertSame(value2, cache.getUnchecked(key2)); } } private void assertCleanup(LoadingCache<Integer, String> cache, CountingRemovalListener<Integer, String> removalListener) { // initialSize will most likely be 2, but it's possible for the GC to have already run, so we'll // observe a size of 1 long initialSize = cache.size(); assertTrue(initialSize == 1 || initialSize == 2); // wait up to 5s byte[] filler = new byte[1024]; for (int i = 0; i < 500; i++) { System.gc(); CacheTesting.drainReferenceQueues(cache); if (cache.size() == 1) { break; } try { Thread.sleep(10); } catch (InterruptedException e) { /* ignore */} try { // Fill up heap so soft references get cleared. filler = new byte[Math.max(filler.length, filler.length * 2)]; } catch (OutOfMemoryError e) {} } CacheTesting.processPendingNotifications(cache); assertEquals(1, cache.size()); assertEquals(1, removalListener.getCount()); } // A simple type whose .toString() will return the same value each time, but without maintaining // a strong reference to that value. static class Key { private final int value; private WeakReference<String> toString; Key(int value) { this.value = value; } @Override public synchronized String toString() { String s; if (toString != null) { s = toString.get(); if (s != null) { return s; } } s = Integer.toString(value); toString = new WeakReference<String>(s); return s; } } }