package kvstore;
import static autograder.TestUtils.kTimeoutDefault;
import static autograder.TestUtils.kTimeoutQuick;
import static kvstore.KVConstants.*;
import static kvstore.Utils.assertKVExceptionEquals;
import static org.junit.Assert.*;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Mockito.*;
import static org.powermock.api.mockito.PowerMockito.whenNew;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Random;
import java.util.Scanner;
import java.util.concurrent.locks.ReentrantLock;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.experimental.categories.Category;
import org.junit.runner.RunWith;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;
import autograder.AGCategories.AGTestDetails;
import autograder.AGCategories.AG_PROJ4_CODE;
@RunWith(PowerMockRunner.class)
@PrepareForTest(KVServer.class)
public class KVServerTest {
KVServer server;
static KVCache realCache;
KVCache mockCache;
static KVStore realStore;
KVStore mockStore;
/**
* Nick: This is necessary because once I start mocking constructors, I
* haven't figured out a way to use the real ones again. Also, this sucks
* because the cache doesn't get reset between tests -_- there is no method
* for that, though and I can't construct a new cache with the actual
* constructor. Only fix I thought of was creating an array of new caches
* and using a different one for each test, but that's even worse imo :(
*/
@BeforeClass
public static void setupRealDependencies() {
realCache = new KVCache(10, 10);
realStore = new KVStore();
}
public void setupRealServer() {
try {
whenNew(KVCache.class).withArguments(anyInt(), anyInt()).thenReturn(realCache);
whenNew(KVStore.class).withNoArguments().thenReturn(realStore);
server = new KVServer(10, 10);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public void setupMockServer() {
try {
mockCache = mock(KVCache.class);
mockStore = mock(KVStore.class);
whenNew(KVCache.class).withAnyArguments().thenReturn(mockCache);
whenNew(KVStore.class).withAnyArguments().thenReturn(mockStore);
server = new KVServer(10, 10);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Test(timeout = 30000)
@Category(AG_PROJ4_CODE.class)
@AGTestDetails(points = 3, desc = "Randomized put/get/del sequence test.")
public void randStressTest() throws KVException {
setupRealServer();
Random rand = new Random(8); // no reason for 8
Map<String, String> map = new HashMap<String, String>(10000);
String key, val;
for (int i = 0; i < 10000; i++) {
key = Integer.toString(rand.nextInt());
val = Integer.toString(rand.nextInt());
server.put(key, val);
map.put(key, val);
}
Iterator<Map.Entry<String, String>> mapIter = map.entrySet().iterator();
Map.Entry<String, String> pair;
while(mapIter.hasNext()) {
pair = mapIter.next();
assertTrue(server.hasKey(pair.getKey()));
assertEquals(pair.getValue(), server.get(pair.getKey()));
server.del(pair.getKey());
assertTrue(!server.hasKey(pair.getKey()));
mapIter.remove();
}
assertTrue(map.size() == 0);
}
@Test(timeout = kTimeoutQuick)
@Category(AG_PROJ4_CODE.class)
@AGTestDetails(points = 1, desc = "Test put throws ERROR_OVERSIZED_KEY")
public void testPutOversizedKeyKVException() {
setupMockServer();
Scanner s2 = null;
try {
final String filename = "gobears_maxkey.txt";
InputStream maxKeyStream = getClass().getClassLoader().getResourceAsStream(filename);
assertNotNull(String.format("Test file not found: %s - Please report to TA", filename), maxKeyStream);
s2 = new Scanner(maxKeyStream);
String oversizedKey = s2.nextLine();
when(mockCache.getLock(oversizedKey)).thenReturn(new ReentrantLock());
server.put(oversizedKey, "cal");
fail("Server was supposed to throw an exception for oversized key");
} catch (KVException pass) {
assertKVExceptionEquals(ERROR_OVERSIZED_KEY, pass);
}
}
@Test(timeout = kTimeoutQuick)
@Category(AG_PROJ4_CODE.class)
@AGTestDetails(points = 1, desc = "Test put throws ERROR_OVERSIZED_VALUE")
public void testPutOversizedValueKVException() {
setupMockServer();
Scanner s2 = null;
try {
final String filename = "gobears_maxvalue.txt";
InputStream maxValueStream = getClass().getClassLoader().getResourceAsStream(filename);
assertNotNull(String.format("Test file not found: %s - Please report to TA", filename), maxValueStream);
s2 = new Scanner(maxValueStream);
String oversizedValue = s2.nextLine();
when(mockCache.getLock(oversizedValue)).thenReturn(new ReentrantLock());
server.put("foo", oversizedValue);
fail("Server was supposed to throw an exception for oversized value");
} catch (KVException pass){
assertKVExceptionEquals(ERROR_OVERSIZED_VALUE, pass);
}
}
@Test(timeout = kTimeoutQuick)
@Category(AG_PROJ4_CODE.class)
@AGTestDetails(points = 2, desc = "Test get throws ERROR_NO_SUCH_KEY.")
public void testGetKVException() {
setupRealServer();
try {
server.get("this key shouldn't be here");
fail("get with nonexistent key should error");
} catch (KVException e) {
assertEquals(KVConstants.RESP, e.getKVMessage().getMsgType());
assertEquals(KVConstants.ERROR_NO_SUCH_KEY, e.getKVMessage().getMessage());
}
}
@Test(timeout = kTimeoutQuick)
@Category(AG_PROJ4_CODE.class)
@AGTestDetails(points = 1, desc = "Test get releases lock cleanly on exception")
public void testGetLockRelease() {
setupMockServer();
ReentrantLock l = new ReentrantLock();
try {
when(mockCache.getLock("go")).thenReturn(new ReentrantLock());
when(mockStore.get("go")).thenThrow(new KVException(ERROR_NO_SUCH_KEY));
server.get("go");
fail("Forced exception was not rethrown by KVServer");
} catch(KVException e1) {
}
assertTrue(!(l.isLocked()));
}
@Test(timeout = kTimeoutQuick)
@Category(AG_PROJ4_CODE.class)
@AGTestDetails(points = 2, desc = "Test del throws ERROR_NO_SUCH_KEY.")
public void testDelKVException() {
setupRealServer();
try {
server.del("this key shouldn't be here");
fail("del with nonexistent key should error");
} catch (KVException e) {
assertEquals(KVConstants.RESP, e.getKVMessage().getMsgType());
assertEquals(KVConstants.ERROR_NO_SUCH_KEY, e.getKVMessage().getMessage());
}
}
@Test(timeout = kTimeoutQuick)
@Category(AG_PROJ4_CODE.class)
@AGTestDetails(points = 1, desc = "Test del releases lock cleanly on exception")
public void testDelLockRelease() {
setupMockServer();
ReentrantLock l = new ReentrantLock();
try {
when(mockCache.getLock("go")).thenReturn(l);
doThrow(new KVException(ERROR_NO_SUCH_KEY)).when(mockStore).del("go");;
server.del("go");
fail("Forced exception was not rethrown by KVServer");
} catch (KVException e1) {
}
assertTrue(!(l.isLocked()));
}
@Test(timeout = kTimeoutQuick)
@Category(AG_PROJ4_CODE.class)
@AGTestDetails(points = 3,
desc = "Tests that the store is not accessed if a value can be " +
"retrieved from the cache.")
public void testGetFromCacheFirst() {
setupMockServer();
when(mockCache.getLock("go")).thenReturn(new ReentrantLock());
when(mockCache.get("go")).thenReturn("bears");
try {
server.get("go");
} catch (KVException e) {
fail("Threw unnecessary KVException during get");
}
try {
verify(mockStore, never()).get("go");
} catch (KVException e) {
}
}
@Test (timeout = kTimeoutQuick)
@Category(AG_PROJ4_CODE.class)
@AGTestDetails(points = 1,
desc = "Checks that gets are parallel across sets and serial within " +
"a set. Checks the set is locked to ensure atomicity within a set.")
public void testParallelGet() {
setupMockServer();
ReentrantLock l1 = new ReentrantLock();
ReentrantLock l2 = new ReentrantLock();
when(mockCache.getLock("cal")).thenReturn(l1);
when(mockCache.getLock("stan")).thenReturn(l2);
when(mockCache.get("cal")).thenAnswer(checkParallelSerial1);
try {
when(mockStore.get("cal")).thenAnswer(checkParallelSerial2);
} catch (KVException e){
fail("Unexpected exception on get");
}
when(mockCache.get("stan")).thenReturn("furd");
try {
String s = server.get("cal");
assertTrue(s.equals("gobears"));
} catch (KVException e){
fail("Unexpected failure.");
}
}
@Test (timeout = kTimeoutQuick)
@Category(AG_PROJ4_CODE.class)
@AGTestDetails(points = 1,
desc = "Checks that dels are parallel across sets and serial within " +
"a set. Checks the set is locked to ensure atomicity within a set.")
public void testParallelDel() {
setupMockServer();
ReentrantLock l1 = new ReentrantLock();
ReentrantLock l2 = new ReentrantLock();
when(mockCache.getLock("cal")).thenReturn(l1);
when(mockCache.getLock("stan")).thenReturn(l2);
doAnswer(checkParallelSerial1).when(mockCache).del("cal");
try {
doAnswer(checkParallelSerial2).when(mockStore).del("cal");
} catch (KVException e) {
fail("Unexpected exception on del");
}
when(mockCache.get("stan")).thenReturn("furd");
try {
server.del("cal");
} catch (KVException e) {
fail("Unexpected failure.");
}
}
@Test (timeout = kTimeoutQuick)
@Category(AG_PROJ4_CODE.class)
@AGTestDetails(points = 1,
desc = "Checks that puts are parallel across sets and serial within " +
"a set. Checks the set is locked to ensure atomicity within a set.")
public void testParallelPut() {
setupMockServer();
ReentrantLock l1 = new ReentrantLock();
ReentrantLock l2 = new ReentrantLock();
when(mockCache.getLock("cal")).thenReturn(l1);
when(mockCache.getLock("stan")).thenReturn(l2);
doAnswer(checkParallelSerial1).when(mockCache).put("cal", "gobears");
//try {
doAnswer(checkParallelSerial2).when(mockStore).put("cal", "gobears");
//} catch (KVException e) {
// fail("Unexpected exception on put");
//}
when(mockCache.get("stan")).thenReturn("furd");
try {
server.put("cal", "gobears");
} catch (KVException e) {
fail("Unexpected failure.");
}
}
/* ----------------------- BEGIN HELPER METHODS ------------------------ */
@SuppressWarnings("rawtypes")
Answer checkParallelSerial2 = new Answer() {
@Override
public Object answer(InvocationOnMock inv){
ReentrantLock l = (ReentrantLock) mockCache.getLock("cal");
assertTrue(l.isLocked());
try {
assertTrue(server.get("stan").equals("furd"));
} catch (KVException e) {
fail("unexpected exception");
}
return "gobears";
}
};
@SuppressWarnings("rawtypes")
Answer checkParallelSerial1 = new Answer() {
@Override
public Object answer(InvocationOnMock inv) {
ReentrantLock l = (ReentrantLock) mockCache.getLock("cal");
assertTrue(l.isLocked());
try {
assertTrue(server.get("stan").equals("furd"));
} catch (KVException e) {
fail("unexpected exception");
}
return null;
}
};
// George: Not sure why Isaac commented this out.
// @Test (timeout = 5000)
// public void testParallelOps(){
// setupMockServer();
// ReentrantLock l1 = new ReentrantLock();
// ReentrantLock l2 = new ReentrantLock();
// when(mockCache.getLock("cal")).thenReturn(l1);
// when(mockCache.getLock("stan")).thenReturn(l2);
// when(mockCache.get("cal")).thenReturn(l1.isLocked() ? "gobears" : "fail");
// l2.lock();
// try{
// when(mockStore.get("cal")).thenReturn(l1.isLocked() ? "gobears" : "fail");
// String s = server.get("cal");
// //System.out.println(s);
// assertTrue(s.equals("gobears"));
// } catch (KVException e){
// fail("Unexpected failure.");
// }
// }
}