package org.molgenis.data.cache.l2;
import com.google.common.util.concurrent.UncheckedExecutionException;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.molgenis.data.*;
import org.molgenis.data.cache.utils.EntityHydration;
import org.molgenis.data.meta.model.EntityType;
import org.molgenis.data.support.DynamicEntity;
import org.molgenis.data.support.EntityWithComputedAttributes;
import org.molgenis.data.transaction.MolgenisTransactionManager;
import org.molgenis.data.transaction.TransactionInformation;
import org.molgenis.test.data.AbstractMolgenisSpringTest;
import org.molgenis.test.data.EntityTestHarness;
import org.molgenis.util.EntityUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.ContextConfiguration;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static com.google.common.collect.Lists.newArrayList;
import static java.util.Collections.emptySet;
import static java.util.Collections.singleton;
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toMap;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.*;
import static org.mockito.MockitoAnnotations.initMocks;
import static org.molgenis.data.EntityManager.CreationMode.NO_POPULATE;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertTrue;
@ContextConfiguration(classes = L2CacheTest.Config.class)
public class L2CacheTest extends AbstractMolgenisSpringTest
{
private L2Cache l2Cache;
@Autowired
private EntityHydration entityHydration;
@Autowired
private EntityTestHarness entityTestHarness;
@Autowired
private EntityManager entityManager;
@Mock
private MolgenisTransactionManager molgenisTransactionManager;
@Mock
private Repository<Entity> repository;
@Mock
private TransactionInformation transactionInformation;
@Captor
private ArgumentCaptor<Stream<Object>> idStreamCaptor;
private List<Entity> testEntities;
private EntityType emd;
@BeforeClass
public void beforeClass()
{
initMocks(this);
EntityType refEntityType = entityTestHarness.createDynamicRefEntityType();
emd = entityTestHarness.createDynamicTestEntityType();
List<Entity> refEntities = entityTestHarness.createTestRefEntities(refEntityType, 2);
testEntities = entityTestHarness.createTestEntities(emd, 4, refEntities).collect(toList());
when(entityManager.create(emd, NO_POPULATE)).thenAnswer(new Answer<Entity>()
{
@Override
public Entity answer(InvocationOnMock invocation) throws Throwable
{
return new EntityWithComputedAttributes(new DynamicEntity(emd));
}
});
when(entityManager.getReference(any(EntityType.class), eq("0"))).thenReturn(refEntities.get(0));
when(entityManager.getReference(any(EntityType.class), eq("1"))).thenReturn(refEntities.get(1));
when(entityManager.getReferences(any(EntityType.class), eq(newArrayList("0"))))
.thenReturn(newArrayList(refEntities.get(0)));
when(entityManager.getReferences(any(EntityType.class), eq(newArrayList("1"))))
.thenReturn(newArrayList(refEntities.get(1)));
}
@BeforeMethod
public void beforeMethod()
{
reset(repository, transactionInformation);
when(repository.getEntityType()).thenReturn(emd);
when(repository.getName()).thenReturn(emd.getName());
l2Cache = new L2Cache(molgenisTransactionManager, entityHydration, transactionInformation);
}
@Test
public void testAfterCommitTransactionRemovesCacheForDirtyRepository()
{
// load the entity through the cache
Entity entity2 = testEntities.get(2);
when(repository.findOneById("2")).thenReturn(entity2);
Entity result = l2Cache.get(repository, "2");
assertTrue(EntityUtils.equals(result, entity2));
// get the entity from the cache without touching the repository
result = l2Cache.get(repository, "2");
assertTrue(EntityUtils.equals(result, entity2));
verify(repository, times(1)).findOneById("2");
// Commit a transaction that has dirtied the repository
when(transactionInformation.getEntirelyDirtyRepositories()).thenReturn(singleton(emd.getName()));
l2Cache.afterCommitTransaction("transactionID");
// get the entity a third time
result = l2Cache.get(repository, "2");
assertTrue(EntityUtils.equals(result, entity2));
// it was no longer present in the cache and got loaded through the repository
verify(repository, times(2)).findOneById("2");
// But now it sits in the cache again
result = l2Cache.get(repository, "2");
assertTrue(EntityUtils.equals(result, entity2));
verify(repository, times(2)).findOneById("2");
}
@Test
public void testAfterCommitTransactionRemovesEntityForDirtyEntity()
{
Entity entity2 = testEntities.get(2);
Entity entity3 = testEntities.get(3);
when(repository.findOneById("2")).thenReturn(entity2);
when(repository.findOneById("3")).thenReturn(entity3);
// load the entities through the cache
assertTrue(EntityUtils.equals(l2Cache.get(repository, "2"), entity2));
assertTrue(EntityUtils.equals(l2Cache.get(repository, "3"), entity3));
// get the entities from the cache without touching the repository
assertTrue(EntityUtils.equals(l2Cache.get(repository, "2"), entity2));
assertTrue(EntityUtils.equals(l2Cache.get(repository, "3"), entity3));
verify(repository, times(1)).findOneById("2");
verify(repository, times(1)).findOneById("3");
// Commit a transaction that has dirtied entity3, but not entity2
when(transactionInformation.getEntirelyDirtyRepositories()).thenReturn(emptySet());
when(transactionInformation.getDirtyEntities()).thenReturn(singleton(EntityKey.create(entity3)));
l2Cache.afterCommitTransaction("transactionID");
// get the entities for a third time
assertTrue(EntityUtils.equals(l2Cache.get(repository, "2"), entity2));
assertTrue(EntityUtils.equals(l2Cache.get(repository, "3"), entity3));
// entity2 was still in the cache
// entity3 was no longer present in the cache and got loaded through the repository
verify(repository, times(1)).findOneById("2");
verify(repository, times(2)).findOneById("3");
// From now on, both sit in the cache again
assertTrue(EntityUtils.equals(l2Cache.get(repository, "2"), entity2));
assertTrue(EntityUtils.equals(l2Cache.get(repository, "3"), entity3));
verify(repository, times(1)).findOneById("2");
verify(repository, times(2)).findOneById("3");
}
@Test
public void testGetStringIdCachesLoadedData()
{
Entity entity2 = testEntities.get(2);
when(repository.findOneById("2")).thenReturn(entity2);
Entity result = l2Cache.get(repository, "2");
assertTrue(EntityUtils.equals(result, entity2));
result = l2Cache.get(repository, "2");
assertTrue(EntityUtils.equals(result, entity2));
verify(repository, times(1)).findOneById("2");
}
@Test(expectedExceptions = UncheckedExecutionException.class)
public void testGetStringIdLoaderThrowsException()
{
when(repository.findOneById("2"))
.thenThrow(new MolgenisDataException("Table is missing for entity TestEntity"));
l2Cache.get(repository, "2");
}
@SuppressWarnings("unchecked")
@Test(expectedExceptions = UncheckedExecutionException.class)
public void testGetBatchIdLoaderThrowsException()
{
when(repository.findAll(any(Stream.class))).thenThrow(new MolgenisDataException("Table is missing for entity TestEntity"));
l2Cache.getBatch(repository, newArrayList("1", "2"));
}
@Test
public void testFindAll()
{
when(repository.findAll(idStreamCaptor.capture())).thenReturn(testEntities.stream());
List<Entity> result = l2Cache.getBatch(repository, newArrayList("0", "1", "2", "3"));
Map<Object, Entity> retrievedEntities = result.stream().collect(toMap(Entity::getIdValue, e -> e));
assertEquals(retrievedEntities.size(), 4);
assertTrue(EntityUtils.equals(retrievedEntities.get("1"), testEntities.get(1)));
assertEquals(idStreamCaptor.getValue().collect(Collectors.toList()), newArrayList("0", "1", "2", "3"));
}
@Configuration
@Import({ EntityTestHarness.class, EntityHydration.class })
public static class Config
{
@Mock
private EntityManager entityManager;
public Config()
{
initMocks(this);
}
@Bean
public EntityManager entityManager()
{
return entityManager;
}
}
}