// =================================================================================================
// Copyright 2011 Twitter, Inc.
// -------------------------------------------------------------------------------------------------
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this work except in compliance with the License.
// You may obtain a copy of the License in the LICENSE file, or 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.twitter.common.util.caching;
import com.google.common.base.Predicate;
import org.easymock.IMocksControl;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;
import static org.easymock.EasyMock.createControl;
import static org.easymock.EasyMock.expect;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThat;
/**
* @author William Farner
*/
public class CachingMethodProxyTest {
private CachingMethodProxy<Math> proxyBuilder;
private Math uncachedMath;
private Math cachedMath;
private Cache<List, Integer> intCache;
private Predicate<Integer> intFilter;
private IMocksControl control;
@Before
@SuppressWarnings("unchecked")
public void setUp() {
control = createControl();
uncachedMath = control.createMock(Math.class);
intCache = control.createMock(Cache.class);
intFilter = control.createMock(Predicate.class);
proxyBuilder = CachingMethodProxy.proxyFor(uncachedMath, Math.class);
cachedMath = proxyBuilder.getCachingProxy();
}
@After
public void verifyControl() {
control.verify();
}
@Test
public void testCaches() throws Exception {
expectUncachedAdd(1, 2, true);
expectUncachedAdd(3, 4, true);
expect(intCache.get(Arrays.asList(1, 2))).andReturn(3);
expect(intCache.get(Arrays.asList(3, 4))).andReturn(7);
control.replay();
proxyBuilder.cache(cachedMath.sum(0, 0), intCache, intFilter)
.prepare();
assertThat(cachedMath.sum(1, 2), is(3));
assertThat(cachedMath.sum(3, 4), is(7));
assertThat(cachedMath.sum(1, 2), is(3));
assertThat(cachedMath.sum(3, 4), is(7));
}
@Test
public void testIgnoresUncachedMethod() throws Exception {
expect(uncachedMath.sub(2, 1)).andReturn(1);
expect(uncachedMath.sub(2, 1)).andReturn(1);
control.replay();
proxyBuilder.cache(cachedMath.sum(0, 0), intCache, intFilter)
.prepare();
assertThat(cachedMath.sub(2, 1), is(1));
assertThat(cachedMath.sub(2, 1), is(1));
}
@Test
public void testFilterValue() throws Exception {
expectUncachedAdd(1, 2, true);
expectUncachedAdd(3, 4, false);
expect(intCache.get(Arrays.asList(1, 2))).andReturn(3);
control.replay();
proxyBuilder.cache(cachedMath.sum(0, 0), intCache, intFilter)
.prepare();
assertThat(cachedMath.sum(1, 2), is(3));
assertThat(cachedMath.sum(3, 4), is(7));
assertThat(cachedMath.sum(1, 2), is(3));
}
@Test(expected = IllegalStateException.class)
public void testRequiresOneCache() throws Exception {
control.replay();
proxyBuilder.prepare();
}
@Test
public void testExceptionThrown() throws Exception {
List<Integer> args = Arrays.asList(1, 2);
expect(intCache.get(args)).andReturn(null);
Math.AddException thrown = new Math.AddException();
expect(uncachedMath.sum(1, 2)).andThrow(thrown);
control.replay();
proxyBuilder.cache(cachedMath.sum(0, 0), intCache, intFilter)
.prepare();
try {
cachedMath.sum(1, 2);
} catch (Math.AddException e) {
assertSame(e, thrown);
}
}
/* TODO(William Farner): Re-enable once the TODO for checking return value/cache value types is done.
@Test(expected = IllegalArgumentException.class)
public void testCacheValueAndMethodReturnTypeMismatch() throws Exception {
control.replay();
cachedMath.addDouble(0, 0);
proxyBuilder.cache(1, intCache, intFilter)
.prepare();
}
*/
@Test(expected = IllegalStateException.class)
public void testRejectsCacheSetupAfterPrepare() throws Exception {
control.replay();
proxyBuilder.cache(cachedMath.sum(0, 0), intCache, intFilter)
.prepare();
proxyBuilder.cache(null, intCache, intFilter);
}
@Test
@SuppressWarnings("unchecked")
public void testIgnoresNullValues() throws Exception {
// Null return values should not even be considered for entry into the cache, and therefore
// should not be passed to the filter.
Cache<List, Math> crazyCache = control.createMock(Cache.class);
Predicate<Math> crazyFilter = control.createMock(Predicate.class);
expect(crazyCache.get(Arrays.asList(null, null))).andReturn(null);
expect(uncachedMath.crazyMath(null, null)).andReturn(null);
control.replay();
proxyBuilder.cache(cachedMath.crazyMath(null, null), crazyCache, crazyFilter)
.prepare();
cachedMath.crazyMath(null, null);
}
@Test(expected = IllegalArgumentException.class)
@SuppressWarnings("unchecked")
public void testRejectsVoidReturn() throws Exception {
Cache<List, Void> voidCache = control.createMock(Cache.class);
Predicate<Void> voidFilter = control.createMock(Predicate.class);
control.replay();
cachedMath.doSomething(null);
proxyBuilder.cache(null, voidCache, voidFilter);
}
@Test(expected = IllegalStateException.class)
@SuppressWarnings("unchecked")
public void testFailsNoCachedCall() throws Exception {
Cache<List, Void> voidCache = control.createMock(Cache.class);
Predicate<Void> voidFilter = control.createMock(Predicate.class);
control.replay();
// No method call was recorded on the proxy, so the builder doesn't know what to cache.
proxyBuilder.cache(null, voidCache, voidFilter);
}
@Test(expected = IllegalArgumentException.class)
@SuppressWarnings("unchecked")
public void testRejectsZeroArgMethods() throws Exception {
Cache<List, Math> mathCache = control.createMock(Cache.class);
Predicate<Math> mathFilter = control.createMock(Predicate.class);
control.replay();
proxyBuilder.cache(cachedMath.doNothing(), mathCache, mathFilter);
}
@Test
public void testAllowsSuperclassMethod() throws Exception {
SubMath subMath = control.createMock(SubMath.class);
List<Integer> args = Arrays.asList(1, 2);
expect(intCache.get(args)).andReturn(null);
expect(subMath.sum(1, 2)).andReturn(3);
expect(intFilter.apply(3)).andReturn(true);
intCache.put(args, 3);
control.replay();
Method add = SubMath.class.getMethod("sum", int.class, int.class);
CachingMethodProxy<SubMath> proxyBuilder = CachingMethodProxy.proxyFor(subMath, SubMath.class);
SubMath cached = proxyBuilder.getCachingProxy();
proxyBuilder.cache(cached.sum(0, 0), intCache, intFilter)
.prepare();
cached.sum(1, 2);
}
private void expectUncachedAdd(int a, int b, boolean addToCache) throws Math.AddException {
List<Integer> args = Arrays.asList(a, b);
expect(intCache.get(args)).andReturn(null);
expect(uncachedMath.sum(a, b)).andReturn(a + b);
expect(intFilter.apply(a + b)).andReturn(addToCache);
if (addToCache) intCache.put(args, a + b);
}
private interface Math {
public int sum(int a, int b) throws AddException;
public double addDouble(double a, double b) throws AddException;
public int sub(int a, int b);
public Math crazyMath(Math a, Math b);
public Math doNothing();
public void doSomething(Math a);
class AddException extends Exception {}
}
private interface SubMath extends Math {
public int otherSum(int a, int b);
}
}