/*
* Copyright 2016 Oracle.
*
* 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.addthis.hydra.job.store;
import com.ning.compress.lzf.LZFDecoder;
import com.ning.compress.lzf.LZFEncoder;
import java.sql.Blob;
import java.sql.Connection;
import java.sql.Driver;
import java.sql.DriverManager;
import java.sql.DriverPropertyInfo;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.logging.Logger;
import com.addthis.basis.test.SlowTest;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.experimental.categories.Category;
import org.mockito.Matchers;
import org.mockito.Mockito;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
@Category(SlowTest.class)
public class CachedSpawnDataStoreTest {
private static final String DB_URL = "mock:driver/";
//mocks
private static String originalDriver;
private static Driver driver;
/** Container for our actual mock that the {@link DriverManager} can instantiate. */
private final MockDriver mockDriver = new MockDriver();
private Connection connection;
//class under test
private CachedSpawnDataStore cachedDataStore;
public CachedSpawnDataStoreTest() {
}
@BeforeClass
public static void setUpClass() {
//capture the original setting to revert it after the test is complete
originalDriver = System.getProperty("sql.datastore.driverclass");
System.setProperty("sql.datastore.driverclass", MockDriver.class.getName());
}
@AfterClass
public static void tearDownClass() {
//revert our overridden property
if (originalDriver == null) {
System.clearProperty("sql.datastore.driverclass");
} else {
System.setProperty("sql.datastore.driverclass", originalDriver);
}
}
@Before
public void setUp() throws Exception {
//register the driver
DriverManager.registerDriver(mockDriver);
//set up the mocked driver
driver = Mockito.mock(Driver.class);
connection = Mockito.mock(Connection.class);
Mockito.when(driver.acceptsURL(Mockito.startsWith(DB_URL))).thenReturn(Boolean.TRUE); //tell the driver manager we can handle this url
Mockito.doAnswer((InvocationOnMock invocation) -> connection).when(driver).connect(Mockito.startsWith(DB_URL), Mockito.any(Properties.class)); //go ahead and get a connection
Mockito.doAnswer((InvocationOnMock invocation) -> null).when(connection).setAutoCommit(Matchers.anyBoolean()); //resolve some weird sporatic mockito issue that pops up sometimes
Mockito.doAnswer((InvocationOnMock invocation) -> null).when(connection).clearWarnings(); //resolve some weird sporatic mockito issue that pops up sometimes
final PreparedStatement createDatabasePreparedStatement = Mockito.mock(PreparedStatement.class); //temporary mock for creating the database
Mockito.when(connection.prepareStatement(Mockito.startsWith("CREATE DATABASE IF NOT EXISTS"))).thenReturn(createDatabasePreparedStatement);
Mockito.when(createDatabasePreparedStatement.execute()).thenReturn(Boolean.TRUE);
final PreparedStatement createTablePreparedStatement = Mockito.mock(PreparedStatement.class); //temporary mock for creating the table
Mockito.when(connection.prepareStatement(Mockito.startsWith("CREATE TABLE IF NOT EXISTS"))).thenReturn(createTablePreparedStatement);
//create our class under test
final Properties properties = new Properties();
cachedDataStore = new CachedSpawnDataStore(new MysqlDataStore(DB_URL, "dbName", "tableName", properties), 10000000L);
//verify construction
Mockito.verify(driver, Mockito.atLeastOnce()).connect(Mockito.startsWith(DB_URL), Mockito.any(Properties.class));
Mockito.verify(createDatabasePreparedStatement).execute(); //make sure we called execute. This also verifies that the prepared statement was correctly constructed.
Mockito.verify(createTablePreparedStatement).execute(); //make sure we called execute. This also verifies that the prepared statement was correctly constructed.
}
@After
public void tearDown() throws SQLException {
//run some verifications
Mockito.verify(driver).acceptsURL(Mockito.startsWith(DB_URL));
//shut down the connection pooling
if (cachedDataStore != null) {
cachedDataStore.close();
Mockito.verify(connection, Mockito.atLeastOnce()).close(); //this tests the close method as well
}
//clear the mocks
Mockito.reset(driver, connection);
//deregister the driver we started out with
DriverManager.deregisterDriver(mockDriver);
}
/**
* Test of getDescription method, of class MysqlDataStore.
*/
@Test
public void testGetDescription() {
assertEquals("mysql", cachedDataStore.getDescription());
}
private PreparedStatement doGet(String key, String value) throws SQLException {
//set up mocks
final PreparedStatement selectPreparedStatement = Mockito.mock(PreparedStatement.class);
Mockito.doReturn(selectPreparedStatement).when(connection).prepareStatement(
Mockito.eq("SELECT val FROM tableName WHERE path=? AND child=?"));
final ResultSet resultSet = Mockito.mock(ResultSet.class);
Mockito.when(selectPreparedStatement.executeQuery()).thenReturn(resultSet);
Mockito.when(resultSet.next()).thenReturn(Boolean.TRUE, Boolean.FALSE);
final Blob blob = Mockito.mock(Blob.class);
Mockito.when(resultSet.getObject(1, Blob.class)).thenReturn(blob);
Mockito.when(blob.getBytes(Mockito.anyLong(), Mockito.anyInt())).thenReturn(LZFEncoder.encode(value.getBytes()));
//run method under test
final String result = cachedDataStore.get(key);
//assertions
assertNotNull(result);
assertEquals(value, result);
return selectPreparedStatement;
}
@Test
public void testGet_String() throws SQLException {
PreparedStatement selectPreparedStatement = doGet("key", "value");
//verifications
Mockito.verify(selectPreparedStatement, Mockito.atLeastOnce()).executeQuery();
}
@Test
public void testGet_StringArr() throws SQLException {
//set up data
final Map<String, String> expected = new HashMap<>();
expected.put("key1", "value1");
expected.put("key2", "value2");
expected.put("key3", "value3");
//set up mocks
final PreparedStatement selectPreparedStatement = Mockito.mock(PreparedStatement.class);
Mockito.doReturn(selectPreparedStatement).when(connection).prepareStatement(
Mockito.startsWith("SELECT path,val FROM tableName WHERE child=? AND path IN"));
final ResultSet resultSet = Mockito.mock(ResultSet.class);
Mockito.when(selectPreparedStatement.executeQuery()).thenReturn(resultSet);
Mockito.when(resultSet.next()).thenReturn(Boolean.TRUE, Boolean.TRUE, Boolean.TRUE, Boolean.FALSE);
final List<String> keyList = new ArrayList<>(expected.keySet());
final Blob blob = Mockito.mock(Blob.class);
Mockito.when(resultSet.getString("path")).thenReturn(keyList.get(0), keyList.get(1), keyList.get(2));
Mockito.when(resultSet.getObject("val", Blob.class)).thenReturn(blob);
Mockito.when(blob.getBytes(Mockito.anyLong(), Mockito.anyInt())).thenReturn(
LZFEncoder.encode(expected.get(keyList.get(0)).getBytes()),
LZFEncoder.encode(expected.get(keyList.get(1)).getBytes()),
LZFEncoder.encode(expected.get(keyList.get(2)).getBytes()));
//run method under test
final Map<String, String> result = cachedDataStore.get(keyList.toArray(new String[] {}));
//assertions
assertNotNull(result);
expected.keySet().stream().forEach((key) -> {
//iterate over the expected keys, which also makes sure they are all there
assertEquals(expected.get(key), result.get(key));
});
//verifications
Mockito.verify(selectPreparedStatement, Mockito.atLeastOnce()).executeQuery();
}
private PreparedStatement doPut(String key, String value) throws Exception {
//set up mocks
final PreparedStatement insertPreparedStatement = Mockito.mock(PreparedStatement.class);
Mockito.doReturn(insertPreparedStatement).when(connection).prepareStatement(Mockito.startsWith("REPLACE INTO "));
Mockito.doAnswer((Answer) (InvocationOnMock invocation) -> {
final Blob blob = (Blob)invocation.getArguments()[1];
assertEquals(new String(blob.getBytes(1l, (int) blob.length())), value);
return null;
}).when(insertPreparedStatement).setBlob(Mockito.eq(2), Mockito.any(Blob.class));
//run method under test
cachedDataStore.put(key, value);
return insertPreparedStatement;
}
@Test
public void testPut() throws Exception {
final String key = "key";
PreparedStatement insertPreparedStatement = doPut(key, "value");
//verifications
Mockito.verify(insertPreparedStatement).setString(Mockito.eq(1), Mockito.eq(key));
Mockito.verify(insertPreparedStatement).setBlob(Mockito.eq(2), Mockito.any(Blob.class));
Mockito.verify(insertPreparedStatement).setString(Mockito.eq(3), Mockito.eq("_root"));
Mockito.verify(insertPreparedStatement, Mockito.atLeastOnce()).execute();
}
@Test
public void testPutAsChild() throws Exception {
//set up mocks
final PreparedStatement insertPreparedStatement = Mockito.mock(PreparedStatement.class);
Mockito.doReturn(insertPreparedStatement).when(connection).prepareStatement(Mockito.startsWith("REPLACE INTO "));
Mockito.doAnswer((Answer) (InvocationOnMock invocation) -> {
final Blob blob = (Blob)invocation.getArguments()[1];
assertEquals(new String(blob.getBytes(1l, (int) blob.length())), "value");
return null;
}).when(insertPreparedStatement).setBlob(Mockito.eq(2), Mockito.any(Blob.class));
//run method under test
cachedDataStore.putAsChild("key", "childId", "value");
//verifications
Mockito.verify(insertPreparedStatement).setString(Mockito.eq(1), Mockito.eq("key"));
Mockito.verify(insertPreparedStatement).setBlob(Mockito.eq(2), Mockito.any(Blob.class));
Mockito.verify(insertPreparedStatement).setString(Mockito.eq(3), Mockito.eq("childId"));
Mockito.verify(insertPreparedStatement, Mockito.atLeastOnce()).execute();
}
/**
* Test of getChild method, of class MysqlDataStore.
*/
@Test
public void testGetChild() throws Exception {
//set up data
final String value = "value";
//set up mocksx
final PreparedStatement selectPreparedStatement = Mockito.mock(PreparedStatement.class);
Mockito.doReturn(selectPreparedStatement).when(connection).prepareStatement(Mockito.eq("SELECT val FROM tableName WHERE path=? AND child=?"));
final ResultSet resultSet = Mockito.mock(ResultSet.class);
Mockito.when(selectPreparedStatement.executeQuery()).thenReturn(resultSet);
Mockito.when(resultSet.next()).thenReturn(Boolean.TRUE, Boolean.FALSE);
final Blob blob = Mockito.mock(Blob.class);
Mockito.when(resultSet.getObject(1, Blob.class)).thenReturn(blob);
Mockito.when(blob.getBytes(Mockito.anyLong(), Mockito.anyInt())).thenReturn(LZFEncoder.encode(value.getBytes()));
//run method under test
final String result = cachedDataStore.getChild("key", "childId");
//assertions
assertNotNull(result);
assertEquals(value, result);
//verifications
Mockito.verify(selectPreparedStatement, Mockito.atLeastOnce()).executeQuery();
Mockito.verify(selectPreparedStatement).setString(Mockito.eq(1), Mockito.eq("key"));
Mockito.verify(selectPreparedStatement).setString(Mockito.eq(2), Mockito.eq("childId"));
}
@Test
public void testDeleteChild() throws SQLException {
//set up mocks
final PreparedStatement deletePreparedStatement = Mockito.mock(PreparedStatement.class);
Mockito.doReturn(deletePreparedStatement).when(connection).prepareStatement(Mockito.startsWith("DELETE FROM "));
//run method under test
cachedDataStore.deleteChild("key", "childId");
//verifications
Mockito.verify(deletePreparedStatement, Mockito.atLeastOnce()).execute();
Mockito.verify(deletePreparedStatement).setString(Mockito.eq(1), Mockito.eq("key"));
Mockito.verify(deletePreparedStatement).setString(Mockito.eq(2), Mockito.eq("childId"));
}
@Test
public void testGetGet() throws Exception {
final String key = "keyyy";
final String value0 = "value 0";
// Fills the cache
PreparedStatement select0 = doGet(key, value0);
Mockito.verify(select0, Mockito.atLeastOnce()).executeQuery();
// Should use the cache instead of SQL
PreparedStatement select1 = doGet(key, value0);
Mockito.verify(select1, Mockito.never()).executeQuery();
}
@Test
public void testGetPutGet() throws Exception {
final String key = "keyyy";
final String value0 = "value 0";
final String value1 = "value 1";
// Fills the cache
PreparedStatement select0 = doGet(key, value0);
Mockito.verify(select0, Mockito.atLeastOnce()).executeQuery();
// Overwrites value in cache and in DB
PreparedStatement insert0 = doPut(key, value1);
Mockito.verify(insert0).setString(Mockito.eq(1), Mockito.eq(key));
Mockito.verify(insert0).setBlob(Mockito.eq(2), Mockito.any(Blob.class));
Mockito.verify(insert0).setString(Mockito.eq(3), Mockito.eq("_root"));
Mockito.verify(insert0, Mockito.atLeastOnce()).execute();
// Should use the cache instead of SQL
PreparedStatement select1 = doGet(key, value1);
Mockito.verify(select1, Mockito.never()).executeQuery();
}
@Test
public void testGetDeleteGet() throws Exception {
final String key = "keyyy";
final String value0 = "value 0";
// Fills the cache
PreparedStatement select0 = doGet(key, value0);
Mockito.verify(select0, Mockito.atLeastOnce()).executeQuery();
// Overwrites value in cache and in DB
PreparedStatement delete0 = doDelete(key);
Mockito.verify(delete0, Mockito.atLeastOnce()).execute();
// Should use the SQL instead of cache
PreparedStatement select1 = doGet(key, value0);
Mockito.verify(select1, Mockito.atLeastOnce()).executeQuery();
}
private PreparedStatement doDelete(String key) throws SQLException {
//set up mocks
final PreparedStatement deletePreparedStatement = Mockito.mock(PreparedStatement.class);
Mockito.doReturn(deletePreparedStatement).when(connection).prepareStatement(Mockito.startsWith("DELETE FROM "));
//run method under test
cachedDataStore.delete(key);
return deletePreparedStatement;
}
@Test
public void testDelete() throws SQLException {
PreparedStatement deletePreparedStatement = doDelete("key");
//verifications
Mockito.verify(deletePreparedStatement, Mockito.atLeastOnce()).execute();
Mockito.verify(deletePreparedStatement).setString(Mockito.eq(1), Mockito.eq("key"));
}
/**
* Test of getChildrenNames method, of class MysqlDataStore.
*/
@Test
public void testGetChildrenNames() throws SQLException {
//set up mocks
final PreparedStatement selectPreparedStatement = Mockito.mock(PreparedStatement.class);
Mockito.doReturn(selectPreparedStatement).when(connection).prepareStatement(Mockito.startsWith("SELECT DISTINCT "));
final ResultSet resultSet = Mockito.mock(ResultSet.class);
Mockito.when(selectPreparedStatement.executeQuery()).thenReturn(resultSet);
Mockito.when(resultSet.next()).thenReturn(Boolean.TRUE, Boolean.TRUE, Boolean.FALSE); //two results
Mockito.when(resultSet.getString(1)).thenReturn("value1", "value2");
//run method under test
final List<String> result = cachedDataStore.getChildrenNames("key");
//assertions
assertNotNull(result);
assertEquals(2, result.size());
assertEquals("value1", result.get(0));
assertEquals("value2", result.get(1));
//verifications
Mockito.verify(selectPreparedStatement, Mockito.atLeastOnce()).executeQuery();
Mockito.verify(selectPreparedStatement).setString(Mockito.eq(1), Mockito.eq("key"));
Mockito.verify(selectPreparedStatement).setString(Mockito.eq(2), Mockito.eq("_root"));
}
@Test
public void testGetAllChildren() throws SQLException {
//set up data
final Map<String, String> expected = new HashMap<>();
expected.put("key1", "value1");
expected.put("key2", "value2");
expected.put("key3", "value3");
//set up mocks
final PreparedStatement selectPreparedStatement = Mockito.mock(PreparedStatement.class);
Mockito.doReturn(selectPreparedStatement).when(connection).prepareStatement(
Mockito.eq("SELECT child,val FROM tableName WHERE path=? AND child!=?"));
final ResultSet resultSet = Mockito.mock(ResultSet.class);
Mockito.when(selectPreparedStatement.executeQuery()).thenReturn(resultSet);
Mockito.when(resultSet.next()).thenReturn(Boolean.TRUE, Boolean.TRUE, Boolean.TRUE, Boolean.FALSE);
final List<String> keyList = new ArrayList<>(expected.keySet());
final Blob blob = Mockito.mock(Blob.class);
Mockito.when(resultSet.getString(1)).thenReturn(keyList.get(0), keyList.get(1), keyList.get(2));
Mockito.when(resultSet.getObject(2, Blob.class)).thenReturn(blob);
Mockito.when(blob.getBytes(Mockito.anyLong(), Mockito.anyInt())).thenReturn(
LZFEncoder.encode(expected.get(keyList.get(0)).getBytes()),
LZFEncoder.encode(expected.get(keyList.get(1)).getBytes()),
LZFEncoder.encode(expected.get(keyList.get(2)).getBytes()));
//run method under test
final Map<String, String> result = cachedDataStore.getAllChildren("key");
//assertions
assertNotNull(result);
expected.keySet().stream().forEach((key) -> {
//iterate over the expected keys, which also makes sure they are all there
assertEquals(expected.get(key), result.get(key));
});
//verifications
Mockito.verify(selectPreparedStatement, Mockito.atLeastOnce()).executeQuery();
Mockito.verify(selectPreparedStatement).setString(Mockito.eq(1), Mockito.eq("key"));
Mockito.verify(selectPreparedStatement).setString(Mockito.eq(2), Mockito.eq("_root"));
}
/**
* We need the qualified string representation of a concrete class that is a jdbc driver. This will just
* defer to the actual mocked version managed by Mockito that the unit test has access to.
*/
public static class MockDriver implements Driver {
@Override
public Connection connect(String url, Properties info) throws SQLException {
return driver.connect(url, info);
}
@Override
public boolean acceptsURL(String url) throws SQLException {
return driver.acceptsURL(url);
}
@Override
public DriverPropertyInfo[] getPropertyInfo(String url, Properties info) throws SQLException {
return driver.getPropertyInfo(url, info);
}
@Override
public int getMajorVersion() {
return driver.getMajorVersion();
}
@Override
public int getMinorVersion() {
return driver.getMinorVersion();
}
@Override
public boolean jdbcCompliant() {
return driver.jdbcCompliant();
}
@Override
public Logger getParentLogger() throws SQLFeatureNotSupportedException {
return driver.getParentLogger();
}
}
}