/*
* Carrot2 project.
*
* Copyright (C) 2002-2016, Dawid Weiss, Stanisław Osiński.
* All rights reserved.
*
* Refer to the full license file "carrot2.LICENSE"
* in the root folder of the repository checkout or at:
* http://www.carrot2.org/carrot2.LICENSE
*/
package org.carrot2.core;
import static org.easymock.EasyMock.isA;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import org.carrot2.core.attribute.AttributeNames;
import org.carrot2.core.attribute.Processing;
import org.carrot2.util.attribute.Attribute;
import org.carrot2.util.attribute.Bindable;
import org.carrot2.util.attribute.Input;
import org.carrot2.util.attribute.Required;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.carrot2.shaded.guava.common.collect.Maps;
import static org.junit.Assert.*;
/**
* Tests caching functionality of a {@link Controller}.
*/
public abstract class ControllerTestsCaching extends ControllerTestsBase
{
/**
* Returns a controller that implements basic processing and results caching
* functionality.
*/
@SuppressWarnings("unchecked")
public abstract Controller getCachingController(Class<? extends IProcessingComponent>... cachedComponentClasses);
/**
* @see ControllerTestsCaching#testConcurrentDocumentModifications()
*/
@Bindable
public static class ConcurrentComponent1 extends ProcessingComponentBase
{
volatile static CountDownLatch latch1;
volatile static CountDownLatch latch2;
@Processing
@Input
@Required
@Attribute(key = AttributeNames.DOCUMENTS)
public List<Document> documents;
@Override @SuppressWarnings("unused")
public void process() throws ProcessingException
{
/*
* Iterate over documents' fields, slowly...
*/
try
{
for (Document d : documents)
{
for (Map.Entry<String, Object> f : d.getFields().entrySet())
{
latch1.countDown();
latch2.await();
}
}
}
catch (InterruptedException e)
{
throw new RuntimeException(e);
}
}
}
/**
* @see ControllerTestsCaching#testConcurrentDocumentModifications()
*/
@Bindable
public static class ConcurrentComponent2 extends ConcurrentComponent1
{
@Override
public void process() throws ProcessingException
{
try
{
latch1.await();
for (Document d : documents)
{
d.setField("new-field", new Object());
}
latch2.countDown();
}
catch (InterruptedException e)
{
throw new RuntimeException(e);
}
}
}
@Test @Ignore("Demonstrates concurrent modification exceptions if documents " +
"are shared between components and processing chains.")
public void testConcurrentDocumentModifications() throws Exception
{
final Controller c = prepareController();
final HashMap<String, Object> attrs = Maps.newHashMap();
attrs.put(AttributeNames.DOCUMENTS, Arrays.asList(new Document("title", "summary")));
ConcurrentComponent1.latch1 = new CountDownLatch(1);
ConcurrentComponent1.latch2 = new CountDownLatch(1);
Thread t = new Thread() {
public void run() {
c.process(attrs, ConcurrentComponent2.class);
}
};
try {
t.start();
c.process(attrs, ConcurrentComponent1.class);
} finally {
t.join();
c.dispose();
}
}
@Before
public void disableOrderChecking()
{
if (!isPooling())
{
mocksControl.checkOrder(false);
}
}
@Override
@SuppressWarnings("unchecked")
public Controller prepareController()
{
return getCachingController(IProcessingComponent.class);
}
@Test
public void testCachingEqualCacheKeys()
{
invokeInitForCache(component1Mock);
invokeProcessingWithInit(component1Mock);
// second query runs entirely from cache
invokeDisposal(component1Mock);
mocksControl.replay();
processingAttributes.put("instanceAttribute", "i");
processingAttributes.put("runtimeAttribute", "r");
processingAttributes.put("data", "d");
performProcessing(Component1.class);
assertEquals("dir", resultAttributes.get("data"));
processingAttributes.put("data", "d");
performProcessingDisposeAndVerifyMocks(Component1.class);
assertEquals("dir", resultAttributes.get("data"));
}
@Test
public void testCachingDifferentCacheKeys()
{
invokeInitForCache(component1Mock);
invokeProcessingWithInit(component1Mock);
invokeDisposalForNoPool(component1Mock);
invokeProcessingWithInitForNoPool(component1Mock);
invokeDisposal(component1Mock);
mocksControl.replay();
processingAttributes.put("instanceAttribute", "i");
processingAttributes.put("runtimeAttribute", "r");
processingAttributes.put("data", "d");
performProcessing(Component1.class);
assertEquals("dir", resultAttributes.get("data"));
processingAttributes.put("data", "d");
processingAttributes.put("runtimeAttribute", "z");
performProcessingDisposeAndVerifyMocks(Component1.class);
assertEquals("diz", resultAttributes.get("data"));
}
@Test
public void testCachingReuseResultsInDifferentComponentPipeline()
{
invokeInitForCache(component1Mock, component2Mock);
invokeProcessingWithInit(component1Mock);
// Next Component1 results come from the cache
invokeProcessingWithInit(component2Mock);
invokeDisposal(component1Mock, component2Mock);
mocksControl.replay();
processingAttributes.put("instanceAttribute", "i");
processingAttributes.put("runtimeAttribute", "v");
processingAttributes.put("data", "d");
performProcessing(Component1.class);
assertEquals("div", resultAttributes.get("data"));
processingAttributes.put("data", "d");
processingAttributes.put("runtimeAttribute", "v");
performProcessingDisposeAndVerifyMocks(Component1.class, Component2.class);
assertEquals("diviv", resultAttributes.get("data"));
}
@Test
public void testCachingInitAttributesIgnoredInCacheKey()
{
invokeInitForCache(component1Mock);
invokeProcessingWithInit(component1Mock);
// second query runs entirely from cache
invokeDisposal(component1Mock);
mocksControl.replay();
processingAttributes.put("instanceAttribute", "i");
processingAttributes.put("runtimeAttribute", "r");
processingAttributes.put("data", "d");
performProcessing(Component1.class);
assertEquals("dir", resultAttributes.get("data"));
// Init attribute should be ignored during processing
processingAttributes.put("instanceAttribute", "j");
processingAttributes.put("data", "d");
performProcessingDisposeAndVerifyMocks(Component1.class);
assertEquals("dir", resultAttributes.get("data"));
}
@Test
@SuppressWarnings("unchecked")
public void testOutputAttributesWithNullValuesOneComponentCached()
{
this.controller = getCachingController(ComponentWithInputOutputAttributes1.class);
this.controller.init(Maps.<String, Object> newHashMap());
performProcessing(ComponentWithInputOutputAttributes1.class,
ComponentWithInputOutputAttributes2.class);
Assert.assertEquals("default", resultAttributes.get("key1"));
Assert.assertEquals("value", resultAttributes.get("key2"));
processingAttributes.clear();
processingAttributes.put("key1", null);
processingAttributes.put("key2", null);
performProcessingAndDispose(ComponentWithInputOutputAttributes1.class,
ComponentWithInputOutputAttributes2.class);
Assert.assertEquals(null, resultAttributes.get("key1"));
Assert.assertEquals("value", resultAttributes.get("key2"));
}
private void invokeProcessingWithInitForNoPool(IProcessingComponent... components)
{
if (isPooling())
{
invokeProcessing(components);
}
else
{
invokeProcessingWithInit(components);
}
}
private void invokeDisposalForNoPool(IProcessingComponent... components)
{
if (!isPooling())
{
invokeDisposal(components);
}
}
/**
* The tests invoking this method are almost the same for all controllers. The only
* exception is a caching non-pooling controller, which does extra component
* init/dispose cycles to prepare attribute descriptors. This method helps to cover
* this case.
*/
private void invokeInitForCache(final IProcessingComponent... components)
{
if (!isPooling())
{
for (IProcessingComponent component : components)
{
component.init(isA(IControllerContext.class));
component.dispose();
}
}
}
}