/**
Copyright (C) SYSTAP, LLC DBA Blazegraph 2006-2016. All rights reserved.
Contact:
SYSTAP, LLC DBA Blazegraph
2501 Calvert ST NW #106
Washington, DC 20008
licenses@blazegraph.com
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; version 2 of the License.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
/*
* Created on May 23, 2006
*/
package com.bigdata.cache;
import java.util.Iterator;
import java.util.Vector;
import junit.framework.TestCase2;
/**
* Abstract base class for cache policy test defines some test harness helper
* methods and utility classes.
*
* @author <a href="mailto:thompsonbry@users.sourceforge.net">Bryan Thompson</a>
* @version $Id$
*/
public abstract class AbstractCachePolicyTest extends TestCase2 {
/**
*
*/
public AbstractCachePolicyTest() {
super();
}
/**
* @param name
*/
public AbstractCachePolicyTest(String name) {
super(name);
}
/**
* Test helper used to generate expected data for testing cache behavior.
*
* @version $Id$
* @author <a href="mailto:thompsonbry@users.sourceforge.net">Bryan Thompson</a>
*/
public static final class CacheEntry<K,T> implements ICacheEntry<K, T>
{
private final K key;
private final T value;
private final boolean dirty;
public CacheEntry( K key, T value, boolean dirty ) {
if( key == null ) {
throw new IllegalArgumentException();
}
if( value == null ) {
throw new IllegalArgumentException();
}
this.key = key;
this.value = value;
this.dirty = dirty;
}
public K getKey() {
return key;
}
public T getObject() {
return value;
}
public boolean isDirty() {
return dirty;
}
public void setDirty(boolean dirty) {
throw new UnsupportedOperationException();
}
}
/**
* Method verifies that the <i>actual </i> {@link Iterator}produces the
* expected objects in the expected order. Objects are compared <strong>by
* reference </strong>. Errors are reported if too few or too many objects
* are produced, etc.
*
* @see TestCase2#assertSameIterator(String, Object[], Iterator)
*/
static public void assertSameIterator
( String msg,
Object[] expected,
Iterator actual
)
{
int i = 0;
while( actual.hasNext() ) {
if( i >= expected.length ) {
fail( msg+": The iterator is willing to visit more than "+
expected.length+
" objects."
);
}
Object g = actual.next();
assertTrue
( msg+": Different objects at index="+i+
": expected="+expected[ i ]+
", actual="+g,
expected[ i ].equals( g )
);
i++;
}
if( i < expected.length ) {
fail( msg+": The iterator SHOULD have visited "+expected.length+
" objects, but only visited "+i+
" objects."
);
}
}
/**
* Verify that the cache iterator visit {@link ICacheEntry}instances that
* are consistent with the expected entries in both order and data. The data
* consistency requirements are: (a) same oid/key (compared by value); same
* value object associated with that key (compared by reference); and same
* dirty flag state.
*
* @param msg
* Message.
* @param expected
* Array of expected cache entry objects in expected order.
* @param actual
* Iterator visiting {@link ICacheEntry}objects.
*/
public void assertSameEntryOrdering(String msg,ICacheEntry expected[], Iterator actual ) {
int i = 0;
while( actual.hasNext() ) {
if( i >= expected.length ) {
fail( msg+": The iterator is willing to visit more than "+
expected.length+
" objects."
);
}
ICacheEntry expectedEntry = expected[ i ];
ICacheEntry actualEntry = (ICacheEntry) actual.next();
assertEquals
( msg+": key differs at index="+i,
expectedEntry.getKey(),
actualEntry.getKey()
);
assertTrue
( msg+": value references differ at index="+i+", expected="+expected+", actual="+actual,
expectedEntry.getObject() == actualEntry.getObject()
);
assertEquals
( msg+": dirty flag differs at index="+i,
expectedEntry.isDirty(),
actualEntry.isDirty()
);
i++;
}
if( i < expected.length ) {
fail( msg+": The iterator SHOULD have visited "+expected.length+
" objects, but only visited "+i+
" objects."
);
}
}
/**
* Dumps the contents of the cache on {@link System#err} using
* {@link ICachePolicy#entryIterator()}.
*
*/
protected static void showCache(ICachePolicy cache) {
System.err.println("\nshowCache: "+cache.getClass());
System.err.println("\tsize="+cache.size());
System.err.println("\tcapacity="+cache.capacity());
Iterator itr = cache.entryIterator();
int i = 0;
while( itr.hasNext() ) {
ICacheEntry entry = (ICacheEntry) itr.next();
System.err.println("["+i+"]\tkey="+entry.getKey()+", value="+entry.getObject()+", dirty="+entry.isDirty());
i++;
}
}
/**
* You set whether or not a cache event is expected and what the expected
* data will be for that event. If an event occurs when none is expected
* then an exception is thrown. If an event occurs with unexpected data then
* an exception is thrown. Otherwise the listener silently accepts the
* event.
*/
public static class MyCacheListener<K,T> implements ICacheListener<K,T>
{
private boolean expectingEvent = false;
private boolean haveEvent = false;
private static class Event<K,T> {
private K expectedOid = null;
private T expectedObj = null;
private boolean expectedDirty = false;
}
private Vector<Event<K,T>> events = new Vector<Event<K,T>>();
/**
* Verify that event data is consistent with our expectations.
*
* @exception IllegalStateException
* If we already have an event.
* @exception AssertionFailedException
* If we are not expecting an event.
* @exception AssertionFailedException
* If the object identifier or object in the event are
* incorrect. The objects are compared by reference, not
* by equals().
*/
public void objectEvicted(ICacheEntry<K,T> entry) {
if(!expectingEvent) {
throw new IllegalStateException("Not expecting event: "+entry);
}
if( haveEvent ) {
throw new IllegalStateException("Already have an event: "+entry);
}
haveEvent = true;
if( events.size() == 0 ) {
throw new IllegalStateException("No expected events: "+entry);
}
Event e = (Event) events.remove(0); // pop off next event.
assertEquals("oid",e.expectedOid,entry.getKey());
assertTrue("obj",e.expectedObj == entry.getObject()); // compare by reference not equals().
assertEquals("dirty",e.expectedDirty,entry.isDirty());
}
/**
* Sets the listener to expect an event with the given object identifier
* and object.
*
* @param oid
* The expected object identifier.
* @param obj
* The expected object (comparison by reference).
* @param dirty
* Iff the expected object will be marked as dirty.
*
* @see #clearLastEvent()
* @see #denyEvents()
*/
public void setExpectedEvent(K oid,T obj,boolean dirty) {
clearExpectedEvents();
addExpectedEvent(oid,obj,dirty);
}
public void clearExpectedEvents() {
events.clear();
denyEvents();
}
public void addExpectedEvent( K oid, T obj, boolean dirty) {
assert oid != null;
assert obj != null;
Event<K,T> e = new Event<K,T>();
e.expectedOid = oid;
e.expectedObj = obj;
e.expectedDirty = dirty;
events.add(e);
allowEvents();
}
/**
* Causes an {@link IllegalStateException} to be thrown from the
* listener if an event is received.
*
* @see #setExpectedEvent(Object, Object, boolean)
*/
public void denyEvents()
{
expectingEvent = false;
}
/**
* Allows more events. If an event had already been received, then it
* is cleared now.
*/
public void allowEvents() {
expectingEvent = true;
if( haveEvent ) {
clearLastEvent();
}
}
/**
* Clear the last event so that a new event may be accepted. An
* exception is thrown if no event has been received so that this method
* may be used to test for the absence of an expected event.
*
* @exception IllegalStateException
* if no event has been received.
*/
public void clearLastEvent() {
if( ! haveEvent ) {
throw new IllegalStateException("no event");
}
haveEvent = false;
}
}
/**
* Implementation always throws an exception.
*
* @author <a href="mailto:thompsonbry@users.sourceforge.net">Bryan Thompson</a>
* @version $Id$
*/
public static class MyCacheListenerThrowsException<K,T> implements ICacheListener<K,T>
{
public void objectEvicted(ICacheEntry<K,T> entry) {
throw new UnsupportedOperationException();
}
}
/**
* <p>
* Abstract fixture factory for the cache policy. The use of this fixture
* factory makes it possible for us to reuse the test suite for the LRU
* cache policy on the weak reference cache since the latter is required to
* delegate the ordering of the cache to an inner hard reference cache.
* </p>
* <p>
* Note: LRU cache policy tests that will be reused for the weak reference
* cache MUST hold hard references to the objects that are inserted into the
* cache to ensure that the cache entries for those objects are not cleared
* from the weak reference cache. The weak reference cache facility for
* clearing cache entries once the objects in those entries become weakly
* reachable must be tested by specialized test methods.
* </p>
*
* @param capacity
* the capacity of the hard reference cache policy.
*
* @return The cache policy fixture.
*/
abstract public ICachePolicy getCachePolicy(int capacity);
/**
* <p>
* Test verifies that LRU ordering is correctly maintained on a series of
* insert, update, and remove operations and that eviction notices are fired
* as necessary.
* </p>
* <p>
* The test method retains a hard reference to the objects inserted into the
* cache so that the cache entries for such objects can not be cleared when
* {@link #getCachePolicy(int)} returns a cache with weak reference
* semantics. This makes the behavior of the test deterministic. Since we
* are holding hard references, the size of the weak reference cache is not
* capped and should refelect all objects inserted into the cache and not
* yet removed from the cache by the test method.
* </p>
*
* @see ICachePolicy
* @see WeakValueCache#size()
* @see WeakValueCache#iterator()
* @see WeakValueCache#entryIterator()
*/
public void test_maintainsLRUOrder()
{
final int CAPACITY = 4;
ICachePolicy<Long,String> cache = getCachePolicy( CAPACITY );
long[] oid = new long[] {
1, 2, 3, 4, 5
};
String[] obj = new String[] {
"o1",
"o2",
"o3",
"o4",
"o5"
};
MyCacheListener<Long,String> listener = new MyCacheListener<Long,String>();
listener.denyEvents(); // listener will deny events.
cache.setListener( listener ); // set listener on cache.
/*
* Insert objects until the cache is full. We verify the total cache
* ordering after each insert as well as the cache size.
*
* Note: The cache order and the iterator order are from the Least
* Recently Used to the Most Recently Used. This means that the last
* element put() into the cache always shows up on the right hand edge
* of the array used to test the cache ordering. When an element is
* evicted from the cache it is always the element on the left hand edge
* of that array.
*
* LRU <- - - - - -> MRU
*/
cache.put( oid[0], obj[0], true );
assertEquals("size", 1, cache.size() );
assertSameIterator("ordering",new Object[]{obj[0]},cache.iterator() );
assertSameEntryOrdering("ordering", new ICacheEntry[] {
new CacheEntry<Long, String>( oid[0], obj[0], true)
}, cache.entryIterator());
cache.put( oid[1], obj[1], true );
assertEquals("size", 2, cache.size() );
assertSameIterator("ordering",new Object[]{obj[0],obj[1]},cache.iterator() );
assertSameEntryOrdering("ordering", new ICacheEntry[] {
new CacheEntry<Long,String>(oid[0], obj[0], true),
new CacheEntry<Long,String>(oid[1], obj[1], true), }, cache.entryIterator());
cache.put( oid[2], obj[2], true );
assertEquals("size", 3, cache.size() );
assertSameIterator("ordering",new Object[]{obj[0],obj[1],obj[2]},cache.iterator() );
assertSameEntryOrdering("ordering", new ICacheEntry[] {
new CacheEntry<Long,String>(oid[0], obj[0], true),
new CacheEntry<Long,String>(oid[1], obj[1], true),
new CacheEntry<Long,String>(oid[2], obj[2], true) }, cache.entryIterator());
cache.put( oid[3], obj[3], true );
assertEquals("size", 4, cache.size() );
assertEquals("capacity", cache.size(), cache.capacity() );
assertSameIterator("ordering",new Object[]{obj[0],obj[1],obj[2],obj[3]},cache.iterator() );
assertSameEntryOrdering("ordering", new ICacheEntry[] {
new CacheEntry<Long,String>(oid[0], obj[0], true),
new CacheEntry<Long,String>(oid[1], obj[1], true),
new CacheEntry<Long,String>(oid[2], obj[2], true),
new CacheEntry<Long,String>(oid[3], obj[3], true) }, cache.entryIterator());
/*
* Insert another object into the cache. This should trigger a cache
* eviction event. We set the expected data for that event on the
* listener and then verify that the event was received.
*/
listener.setExpectedEvent( oid[0], obj[0], true );
cache.put( oid[4], obj[4], true );
listener.clearLastEvent();
assertEquals("size", 4, cache.size() );
assertEquals("capacity", cache.size(), cache.capacity() );
assertSameIterator("ordering",new Object[]{obj[1],obj[2],obj[3],obj[4]},cache.iterator() );
assertSameEntryOrdering("ordering", new ICacheEntry[] {
new CacheEntry<Long,String>(oid[1], obj[1], true),
new CacheEntry<Long,String>(oid[2], obj[2], true),
new CacheEntry<Long,String>(oid[3], obj[3], true),
new CacheEntry<Long,String>(oid[4], obj[4], true)
}, cache.entryIterator());
// another over capacity event.
listener.setExpectedEvent( oid[1], obj[1], true );
cache.put( oid[0], obj[0], true );
listener.clearLastEvent();
assertEquals("size", 4, cache.size() );
assertEquals("capacity", cache.size(), cache.capacity() );
assertSameIterator("ordering",new Object[]{obj[2],obj[3],obj[4],obj[0]},cache.iterator() );
assertSameEntryOrdering("ordering", new ICacheEntry[] {
new CacheEntry<Long,String>(oid[2], obj[2], true),
new CacheEntry<Long,String>(oid[3], obj[3], true),
new CacheEntry<Long,String>(oid[4], obj[4], true),
new CacheEntry<Long,String>(oid[0], obj[0], true),
}, cache.entryIterator());
// touch a cache member and verify the updated ordering.
listener.denyEvents();
cache.put( oid[3], obj[3], true );
assertEquals("size", 4, cache.size() );
assertEquals("capacity", cache.size(), cache.capacity() );
assertSameIterator("ordering",new Object[]{obj[2],obj[4],obj[0],obj[3]},cache.iterator() );
assertSameEntryOrdering("ordering", new ICacheEntry[] {
new CacheEntry<Long,String>(oid[2], obj[2], true),
new CacheEntry<Long,String>(oid[4], obj[4], true),
new CacheEntry<Long,String>(oid[0], obj[0], true),
new CacheEntry<Long,String>(oid[3], obj[3], true),
}, cache.entryIterator());
// touch the MRU cache member and verify NO update to ordering, but
// the dirty flag is updated as specified.
listener.denyEvents();
cache.put( oid[3], obj[3], false );
assertEquals("size", 4, cache.size() );
assertEquals("capacity", cache.size(), cache.capacity() );
assertSameIterator("ordering",new Object[]{obj[2],obj[4],obj[0],obj[3]},cache.iterator() );
assertSameEntryOrdering("ordering", new ICacheEntry[] {
new CacheEntry<Long,String>(oid[2], obj[2], true),
new CacheEntry<Long,String>(oid[4], obj[4], true),
new CacheEntry<Long,String>(oid[0], obj[0], true),
new CacheEntry<Long,String>(oid[3], obj[3], false),
}, cache.entryIterator());
// touch the LRU cache member and verify update to ordering.
listener.denyEvents();
cache.put( oid[2], obj[2], true );
assertEquals("size", 4, cache.size() );
assertEquals("capacity", cache.size(), cache.capacity() );
assertSameIterator("ordering",new Object[]{obj[4],obj[0],obj[3],obj[2]},cache.iterator() );
assertSameEntryOrdering("ordering", new ICacheEntry[] {
new CacheEntry<Long,String>(oid[4], obj[4], true),
new CacheEntry<Long,String>(oid[0], obj[0], true),
new CacheEntry<Long,String>(oid[3], obj[3], false),
new CacheEntry<Long,String>(oid[2], obj[2], true),
}, cache.entryIterator());
// verify another cache eviction now that we have perturbed the order a bit.
listener.setExpectedEvent( oid[4], obj[4], true );
cache.put( oid[1], obj[1], true );
listener.clearLastEvent();
assertEquals("size", 4, cache.size() );
assertEquals("capacity", cache.size(), cache.capacity() );
assertSameIterator("ordering",new Object[]{obj[0],obj[3],obj[2],obj[1]},cache.iterator() );
assertSameEntryOrdering("ordering", new ICacheEntry[] {
new CacheEntry<Long,String>(oid[0], obj[0], true),
new CacheEntry<Long,String>(oid[3], obj[3], false),
new CacheEntry<Long,String>(oid[2], obj[2], true),
new CacheEntry<Long,String>(oid[1], obj[1], true),
}, cache.entryIterator());
// remove a cache entry and verify the new ordering.
listener.denyEvents();
cache.remove( oid[3] );
assertEquals("size", 3, cache.size() );
assertEquals("capacity", CAPACITY, cache.capacity() );
assertSameIterator("ordering",new Object[]{obj[0],obj[2],obj[1]},cache.iterator() );
assertSameEntryOrdering("ordering", new ICacheEntry[] {
new CacheEntry<Long,String>(oid[0], obj[0], true),
new CacheEntry<Long,String>(oid[2], obj[2], true),
new CacheEntry<Long,String>(oid[1], obj[1], true),
}, cache.entryIterator());
// remove another cache entry and verify the new ordering.
listener.denyEvents();
cache.remove( oid[0] );
assertEquals("size", 2, cache.size() );
assertEquals("capacity", CAPACITY, cache.capacity() );
assertSameIterator("ordering",new Object[]{obj[2],obj[1]},cache.iterator() );
assertSameEntryOrdering("ordering", new ICacheEntry[] {
new CacheEntry<Long,String>(oid[2], obj[2], true),
new CacheEntry<Long,String>(oid[1], obj[1], true),
}, cache.entryIterator());
// add an entry back into the cache.
listener.denyEvents();
cache.put( oid[0], obj[0], false );
assertEquals("size", 3, cache.size() );
assertEquals("capacity", CAPACITY, cache.capacity() );
assertSameIterator("ordering",new Object[]{obj[2],obj[1],obj[0]},cache.iterator() );
assertSameEntryOrdering("ordering", new ICacheEntry[] {
new CacheEntry<Long,String>(oid[2], obj[2], true),
new CacheEntry<Long,String>(oid[1], obj[1], true),
new CacheEntry<Long,String>(oid[0], obj[0], false),
}, cache.entryIterator());
// add an entry back into the cache.
listener.denyEvents();
cache.put( oid[3], obj[3], false );
assertEquals("size", 4, cache.size() );
assertEquals("capacity", cache.size(), cache.capacity() );
assertSameIterator("ordering",new Object[]{obj[2],obj[1],obj[0],obj[3]},cache.iterator() );
assertSameEntryOrdering("ordering", new ICacheEntry[] {
new CacheEntry<Long,String>(oid[2], obj[2], true),
new CacheEntry<Long,String>(oid[1], obj[1], true),
new CacheEntry<Long,String>(oid[0], obj[0], false),
new CacheEntry<Long,String>(oid[3], obj[3], false),
}, cache.entryIterator());
// add an entry back into the cache, causing a cache eviction.
listener.setExpectedEvent(oid[2], obj[2], true);
cache.put( oid[4], obj[4], false );
assertEquals("size", 4, cache.size() );
assertEquals("capacity", cache.size(), cache.capacity() );
assertSameIterator("ordering",new Object[]{obj[1],obj[0],obj[3],obj[4]},cache.iterator() );
assertSameEntryOrdering("ordering", new ICacheEntry[] {
new CacheEntry<Long,String>(oid[1], obj[1], true),
new CacheEntry<Long,String>(oid[0], obj[0], false),
new CacheEntry<Long,String>(oid[3], obj[3], false),
new CacheEntry<Long,String>(oid[4], obj[4], false),
}, cache.entryIterator());
// get MRU object from the cache and verify no update of ordering.
cache.get( oid[4] );
assertEquals("size", 4, cache.size() );
assertEquals("capacity", cache.size(), cache.capacity() );
assertSameIterator("ordering",new Object[]{obj[1],obj[0],obj[3],obj[4]},cache.iterator() );
assertSameEntryOrdering("ordering", new ICacheEntry[] {
new CacheEntry<Long,String>(oid[1], obj[1], true),
new CacheEntry<Long,String>(oid[0], obj[0], false),
new CacheEntry<Long,String>(oid[3], obj[3], false),
new CacheEntry<Long,String>(oid[4], obj[4], false),
}, cache.entryIterator());
// get LRU object from the cache and verify update of ordering.
cache.get( oid[1] );
assertEquals("size", 4, cache.size() );
assertEquals("capacity", cache.size(), cache.capacity() );
assertSameIterator("ordering",new Object[]{obj[0],obj[3],obj[4],obj[1]},cache.iterator() );
assertSameEntryOrdering("ordering", new ICacheEntry[] {
new CacheEntry<Long,String>(oid[0], obj[0], false),
new CacheEntry<Long,String>(oid[3], obj[3], false),
new CacheEntry<Long,String>(oid[4], obj[4], false),
new CacheEntry<Long,String>(oid[1], obj[1], true),
}, cache.entryIterator());
// get LRU object from middle of the cache and verify update of ordering.
cache.get( oid[3] );
assertEquals("size", 4, cache.size() );
assertEquals("capacity", cache.size(), cache.capacity() );
assertSameIterator("ordering",new Object[]{obj[0],obj[4],obj[1],obj[3]},cache.iterator() );
assertSameEntryOrdering("ordering", new ICacheEntry[] {
new CacheEntry<Long,String>(oid[0], obj[0], false),
new CacheEntry<Long,String>(oid[4], obj[4], false),
new CacheEntry<Long,String>(oid[1], obj[1], true),
new CacheEntry<Long,String>(oid[3], obj[3], false),
}, cache.entryIterator());
// clear the cache and verify state.
listener.denyEvents();
cache.clear();
assertEquals("size", 0, cache.size() );
assertEquals("capacity", CAPACITY, cache.capacity() );
assertSameIterator("ordering",new Object[]{},cache.iterator() );
}
}