package org.molgenis.data.cache.l2; import com.google.common.collect.Lists; import com.google.common.collect.Sets; import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.Mock; import org.molgenis.data.Entity; import org.molgenis.data.EntityKey; import org.molgenis.data.Fetch; import org.molgenis.data.Repository; import org.molgenis.data.meta.model.EntityType; 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.testng.annotations.BeforeClass; import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; import java.util.List; import java.util.stream.IntStream; import java.util.stream.Stream; import static com.google.common.collect.Lists.newArrayList; import static java.util.stream.Collectors.toList; import static java.util.stream.Stream.of; import static org.mockito.Matchers.any; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.reset; import static org.mockito.Mockito.when; import static org.mockito.MockitoAnnotations.initMocks; import static org.molgenis.data.RepositoryCapability.CACHEABLE; import static org.molgenis.data.RepositoryCapability.WRITABLE; import static org.testng.Assert.*; public class L2CacheRepositoryDecoratorTest extends AbstractMolgenisSpringTest { public static final int NUMBER_OF_ENTITIES = 2500; private L2CacheRepositoryDecorator l2CacheRepositoryDecorator; @Mock private L2Cache l2Cache; @Mock private Repository<Entity> decoratedRepository; @Mock private TransactionInformation transactionInformation; @Autowired private EntityTestHarness entityTestHarness; private List<Entity> entities; @Captor private ArgumentCaptor<Iterable<Object>> cacheIdCaptor; @Captor private ArgumentCaptor<Stream<Object>> repoIdCaptor; private EntityType emd; @BeforeClass public void beforeClass() { initMocks(this); emd = entityTestHarness.createDynamicRefEntityType(); entities = entityTestHarness.createTestRefEntities(emd, 4); when(decoratedRepository.getCapabilities()).thenReturn(Sets.newHashSet(CACHEABLE, WRITABLE)); l2CacheRepositoryDecorator = new L2CacheRepositoryDecorator(decoratedRepository, l2Cache, transactionInformation); } @BeforeMethod public void beforeMethod() { reset(l2Cache, transactionInformation, decoratedRepository); when(decoratedRepository.getEntityType()).thenReturn(emd); when(decoratedRepository.getName()).thenReturn(emd.getName()); } @Test public void testFindOneByIdNotDirtyCacheableAndPresent() { when(transactionInformation.isEntireRepositoryDirty(emd.getName())).thenReturn(false); when(transactionInformation.isEntityDirty(EntityKey.create(emd, "0"))).thenReturn(false); when(l2Cache.get(decoratedRepository, "0")).thenReturn(entities.get(0)); assertEquals(l2CacheRepositoryDecorator.findOneById("0", new Fetch().field("id")), entities.get(0)); } @Test public void testFindOneByIdNotDirtyCacheableNotPresent() { when(transactionInformation.isEntireRepositoryDirty(emd.getName())).thenReturn(false); when(transactionInformation.isEntityDirty(EntityKey.create(emd, "0"))).thenReturn(false); when(l2Cache.get(decoratedRepository, "abcde")).thenReturn(null); assertNull(l2CacheRepositoryDecorator.findOneById("abcde")); } @Test public void testFindOneByIdDirty() { when(transactionInformation.isEntireRepositoryDirty(emd.getName())).thenReturn(false); when(transactionInformation.isEntityDirty(EntityKey.create(emd, "0"))).thenReturn(true); when(decoratedRepository.findOneById("0")).thenReturn(entities.get(0)); assertEquals(l2CacheRepositoryDecorator.findOneById("0"), entities.get(0)); } @Test public void testFindOneByIdEntireRepositoryDirty() { when(transactionInformation.isEntireRepositoryDirty(emd.getName())).thenReturn(true); when(decoratedRepository.findOneById("0")).thenReturn(entities.get(0)); assertEquals(l2CacheRepositoryDecorator.findOneById("0"), entities.get(0)); } @Test public void testFindAllSplitsIdsOnTransactionInformation() { // repository is clean // 0: Dirty, not present in repo // 1: Dirty, present in decorated repo // 2: Not dirty, present in cache // 3: Not dirty, absence stored in cache when(transactionInformation.isEntireRepositoryDirty(emd.getName())).thenReturn(false); when(transactionInformation.isEntityDirty(EntityKey.create(emd, "0"))).thenReturn(true); when(transactionInformation.isEntityDirty(EntityKey.create(emd, "1"))).thenReturn(true); when(transactionInformation.isEntityDirty(EntityKey.create(emd, "2"))).thenReturn(false); when(transactionInformation.isEntityDirty(EntityKey.create(emd, "3"))).thenReturn(false); Stream<Object> ids = Lists.<Object>newArrayList("0", "1", "2", "3").stream(); when(l2Cache.getBatch(eq(decoratedRepository), cacheIdCaptor.capture())) .thenReturn(newArrayList(entities.get(3))); when(decoratedRepository.findAll(repoIdCaptor.capture())).thenReturn(of(entities.get(1))); List<Entity> retrievedEntities = l2CacheRepositoryDecorator.findAll(ids).collect(toList()); List<Object> decoratedRepoIds = repoIdCaptor.getValue().collect(toList()); assertEquals(decoratedRepoIds, Lists.newArrayList("0", "1")); List<Object> cacheIds = Lists.newArrayList(cacheIdCaptor.getValue()); assertEquals(cacheIds, Lists.newArrayList("2", "3")); assertEquals(retrievedEntities.size(), 2); assertTrue(EntityUtils.equals(retrievedEntities.get(0), entities.get(1))); assertTrue(EntityUtils.equals(retrievedEntities.get(1), entities.get(3))); } @SuppressWarnings("unchecked") @Test public void testFindAllQueriesInBatches() { // repository is clean when(transactionInformation.isEntireRepositoryDirty(emd.getName())).thenReturn(false); // 2500 IDs make for 3 batches. // one third is dirty and needs to be queried from the decorated repository when(transactionInformation.isEntityDirty(any(EntityKey.class))).thenAnswer( invocation -> Integer.parseInt(((EntityKey) invocation.getArguments()[0]).getId().toString()) % 3 == 0); List<Entity> lotsOfEntities = entityTestHarness.createTestRefEntities(emd, NUMBER_OF_ENTITIES); Stream<Object> ids = IntStream.range(0, NUMBER_OF_ENTITIES).mapToObj(Integer::toString); when(l2Cache.getBatch(eq(decoratedRepository), any(Iterable.class))).thenAnswer(invocation -> { List<Object> queried = Lists.newArrayList((Iterable<Object>) invocation.getArguments()[1]); return Lists.transform(queried, id -> lotsOfEntities.get(Integer.parseInt(id.toString()))); }); when(decoratedRepository.findAll(any(Stream.class))).thenAnswer(invocation -> { List<Object> queried = ((Stream<Object>) invocation.getArguments()[0]).collect(toList()); return Lists.transform(queried, id -> lotsOfEntities.get(Integer.parseInt(id.toString()))).stream(); }); List<Entity> retrievedEntities = l2CacheRepositoryDecorator.findAll(ids).collect(toList()); assertEquals(retrievedEntities.size(), NUMBER_OF_ENTITIES); for (int i = 0; i < NUMBER_OF_ENTITIES; i++) { assertTrue(EntityUtils.equals(retrievedEntities.get(i), lotsOfEntities.get(i))); } } }