/*
* 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;
}
}
}