/* * 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.createStrictControl; import static org.easymock.EasyMock.isA; import java.util.Collections; import java.util.Map; import java.util.concurrent.atomic.AtomicBoolean; import org.carrot2.core.ControllerTestsPooling.ComponentWithInstanceCounter; import org.carrot2.core.attribute.AttributeNames; import org.carrot2.core.attribute.Init; import org.carrot2.core.attribute.Internal; 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.Output; import org.carrot2.util.attribute.Required; import org.carrot2.util.attribute.constraint.ImplementingClasses; import org.carrot2.util.tests.CarrotTestCase; import org.easymock.IAnswer; import org.easymock.IMocksControl; import org.junit.After; import org.junit.Assert; import org.junit.Before; import com.carrotsearch.randomizedtesting.annotations.ThreadLeakLingering; import org.carrot2.shaded.guava.common.collect.Maps; /** * Base class for {@link Controller} tests. */ @ThreadLeakLingering(linger = 5000) public abstract class ControllerTestsBase extends CarrotTestCase { protected IMocksControl mocksControl; protected IProcessingComponent component1Mock; protected IProcessingComponent component2Mock; protected IProcessingComponent component3Mock; protected IControllerContextListener contextListenerMock; protected Controller controller; protected Map<String, Object> initAttributes; protected Map<String, Object> processingAttributes; protected Map<String, Object> resultAttributes; protected ProcessingResult result; public abstract Controller prepareController(); /** A caching controller is used.*/ boolean caching; /** A pooling controller is used. */ boolean pooling; public ControllerTestsBase() { Controller c = prepareController(); /* * Determine caching/ pooling setup. */ IProcessingComponentManager p = c.componentManager; if (p instanceof CachingProcessingComponentManager) { caching = !((CachingProcessingComponentManager) c.componentManager).cachedComponentClasses.isEmpty(); p = ((CachingProcessingComponentManager) c.componentManager).delegate; } if (p instanceof PoolingProcessingComponentManager) { pooling = true; } c.dispose(); } public final boolean isCaching() { return caching; } public final boolean isPooling() { return pooling; } @Before public void prepareMocks() { mocksControl = createStrictControl(); component1Mock = mocksControl.createMock(IProcessingComponent.class); component2Mock = mocksControl.createMock(IProcessingComponent.class); component3Mock = mocksControl.createMock(IProcessingComponent.class); initAttributes = Maps.newHashMap(); initAttributes.put("delegate1", component1Mock); initAttributes.put("delegate2", component2Mock); initAttributes.put("delegate3", component3Mock); initAttributes.put("instanceAttribute", "i"); processingAttributes = Maps.newHashMap(); } @After public void controllerDisposalCheck() { final boolean cleanedUp = (controller == null); if (!cleanedUp) { controller.dispose(); controller = null; Assert.fail("Each test must dispose the controller."); } } public int eagerlyInitializedInstances() { return 1; } protected ProcessingResult performProcessing(Object... classes) { if (controller == null) { controller = prepareController(); controller.init(initAttributes); } // Controller should not modify input attributes, so we wrap // them in an unmodifiable map. result = controller.process(Collections.unmodifiableMap(processingAttributes), classes); if (result != null) { resultAttributes = result.getAttributes(); } return result; } protected ProcessingResult performProcessingAndDispose(Object... classes) { try { return performProcessing(classes); } finally { controller.dispose(); controller = null; } } protected ProcessingResult performProcessingDisposeAndVerifyMocks(Object... classes) { try { return performProcessingAndDispose(classes); } finally { mocksControl.verify(); } } protected void invokeInit(final IProcessingComponent... components) { for (IProcessingComponent component : components) { for (int i = 0; i < eagerlyInitializedInstances(); i++) { component.init(isA(IControllerContext.class)); } } } protected void invokeProcessingWithInit(final IProcessingComponent... components) { for (IProcessingComponent component : components) { invokeInit(component); component.beforeProcessing(); component.process(); component.afterProcessing(); } } protected void invokeProcessing(final IProcessingComponent... components) { for (IProcessingComponent component : components) { component.beforeProcessing(); component.process(); component.afterProcessing(); } } protected void invokeDisposal(final IProcessingComponent... components) { // Depending on the component management strategy, order of disposal may or may // not be deterministic. It's not deterministic e.g. for pooled components // because the components are disposed of when the pool is being shut down. // In that case, the iteration order over the pool's entries is arbitrary. // For this reason, we don't care about disposal order. mocksControl.checkOrder(false); for (IProcessingComponent component : components) { for (int i = 0; i < eagerlyInitializedInstances(); i++) { component.dispose(); } } mocksControl.checkOrder(true); } protected void checkTimes(final long c1Time, final long c2Time, final long totalTime, final double tolerance) { assertThat( ((Long) (resultAttributes.get(AttributeNames.PROCESSING_TIME_TOTAL))) .longValue()).as("Total time") .isLessThan((long) (totalTime * (1 + tolerance) + 100 * tolerance)) .isGreaterThan((long) (totalTime * (1 - tolerance) - 100 * tolerance)); assertThat( ((Long) (resultAttributes.get(AttributeNames.PROCESSING_TIME_SOURCE))) .longValue()).as("Source time") .isLessThan((long) (c1Time * (1 + tolerance) + 100 * tolerance)) .isGreaterThan((long) (c1Time * (1 - tolerance) - 100 * tolerance)); assertThat( ((Long) (resultAttributes.get(AttributeNames.PROCESSING_TIME_ALGORITHM))) .longValue()).as("Algorithm time") .isLessThan((long) (c2Time * (1 + tolerance) + 100 * tolerance)) .isGreaterThan((long) (c2Time * (1 - tolerance) - 100 * tolerance)); } @Bindable public static class Component1 extends DelegatingProcessingComponent implements IDocumentSource { @Init @Input @Attribute(key = "delegate1") @ImplementingClasses(classes = { IProcessingComponent.class }, strict = false) public IProcessingComponent delegate1; @Override IProcessingComponent getDelegate() { return delegate1; } } @Bindable public static class Component2 extends DelegatingProcessingComponent implements IClusteringAlgorithm { @Init @Input @Attribute(key = "delegate2") @ImplementingClasses(classes = { IProcessingComponent.class }, strict = false) public IProcessingComponent delegate2; @Override IProcessingComponent getDelegate() { return delegate2; } } @Bindable public static class Component3 extends DelegatingProcessingComponent { @Init @Input @Attribute(key = "delegate3") @ImplementingClasses(classes = { IProcessingComponent.class }, strict = false) public IProcessingComponent delegate3; @Override IProcessingComponent getDelegate() { return delegate3; } } @Bindable public static class ComponentWithoutDefaultConstructor extends ProcessingComponentBase { private ComponentWithoutDefaultConstructor() { } } @Bindable public static class ComponentWithContextListener extends DelegatingProcessingComponent { @Init @Input @Attribute(key = "delegateWithContextListener") @ImplementingClasses(classes = { IProcessingComponent.class }, strict = false) public IProcessingComponent delegate; @Init @Input @Attribute(key = "contextListener") @ImplementingClasses(classes = { IControllerContextListener.class }, strict = false) public IControllerContextListener contextListener; static AtomicBoolean contextListenerSubscribed = new AtomicBoolean(); @Override public void init(IControllerContext context) { super.init(context); if (contextListenerSubscribed.compareAndSet(false, true)) { context.addListener(contextListener); } } @Override IProcessingComponent getDelegate() { return delegate; } } @Bindable public static class ComponentWithInputOutputAttributes1 extends ProcessingComponentBase implements IDocumentSource { @Processing @Input @Output @Attribute(key = "key1") public String key1; @Processing @Input @Output @Attribute(key = "key2") public String key2; @Override public void process() throws ProcessingException { super.process(); key2 = "value"; } } @Bindable public static class ComponentWithInputOutputAttributes2 extends ProcessingComponentBase implements IClusteringAlgorithm { @Processing @Input @Output @Attribute(key = "key1") public String key1 = "default"; @Processing @Input @Output @Attribute(key = "key2") public String key2 = "default"; } @Bindable public static class ComponentWithInitParameter extends ProcessingComponentBase { @Input @Init @Attribute(key = "init") public String init = "default"; @Output @Processing @Attribute(key = "result") public String result; @Override public void process() throws ProcessingException { result = init + init; } } @Bindable public static class ComponentWithInitOutputParameter extends ProcessingComponentBase { @Init @Output @Attribute(key = "nullValue") public String init; @Override public void process() throws ProcessingException {} } @Bindable public static class ComponentWithProcessingParameter extends ProcessingComponentBase { @Input @Processing @Attribute(key = "processing") public String processing = "default"; @Output @Processing @Attribute(key = "result") public String result; @Override public void process() throws ProcessingException { result = processing + processing; } } @Bindable public static class ComponentWithInitProcessingInputReferenceAttribute extends ProcessingComponentBase { @Input @Init @Processing @Attribute(key = "initProcessing") @ImplementingClasses(classes = { BindableInstanceCounter.class }) public BindableInstanceCounter initProcessing; } @Bindable public static class ComponentWithInitProcessingInputRequiredAttribute extends ProcessingComponentBase { @Input @Init @Processing @Required @Attribute(key = "initProcessing") public String initProcessingRequired; @Output @Processing @Attribute(key = "result") public String result; @Override public void process() throws ProcessingException { result = initProcessingRequired; } } @Bindable public static class ComponentWithBindableReference extends ProcessingComponentBase { @Processing @Input @Output @Attribute(key = "bindable") @ImplementingClasses(classes = BindableInstanceCounter.class) public BindableInstanceCounter bindable = new BindableInstanceCounter(); } @Bindable public static class ComponentWithInitOutputAttribute extends ProcessingComponentBase implements IClusteringAlgorithm { @Init @Output @Attribute(key = "initOutput") public String initOutput; @Init @Output @Attribute(key = "initOutputNull") public String initOutputNull; @Override public void init(IControllerContext context) { initOutput = "initOutput"; } } @Bindable public static class ComponentWithOutputAttribute extends ProcessingComponentBase { @Output @Processing @Attribute(key = "attr") public String result; @Override public void process() throws ProcessingException { result = "anything"; } } @Bindable public static class ComponentWithRequiredProcessingAttribute extends ProcessingComponentBase { @Input @Processing @Required @Internal @Attribute(key = "attr") public String result; @Override public void process() throws ProcessingException { if (result == null) throw new RuntimeException(); } } @Bindable public static class BindableInstanceCounter { static int createdInstances = 0; public BindableInstanceCounter() { synchronized (ComponentWithInstanceCounter.class) { createdInstances++; } } public static void reset() { createdInstances = 0; } } protected static class DelayedAnswer<T> implements IAnswer<T> { private long delayMilis; public DelayedAnswer(long delayMilis) { this.delayMilis = delayMilis; } public T answer() throws Throwable { Thread.sleep(delayMilis); return null; } } }