/* * 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.expectLastCall; import static org.easymock.EasyMock.isA; import static org.fest.assertions.MapAssert.entry; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeMap; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.ThreadFactory; 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.junit.Assert; import org.junit.Before; import org.junit.Test; import com.carrotsearch.randomizedtesting.annotations.Nightly; import org.carrot2.shaded.guava.common.collect.ImmutableMap; import org.carrot2.shaded.guava.common.collect.Lists; import org.carrot2.shaded.guava.common.collect.Maps; import org.carrot2.shaded.guava.common.collect.Sets; import static org.junit.Assert.*; /** * Tests common functionality of a {@link Controller}. The fact that we need to resort to * having {@link #isCaching()} and {@link #isPooling()} methods here isn't pretty, but * makes testing a lot easier. */ public abstract class ControllerTestsCommon extends ControllerTestsBase { /** * Returns a controller that implements at least basic processing functionality. All * simple, pooling and caching controllers fit here. */ public abstract Controller getSimpleController(); @Before public void disableOrderChecking() { if (isCaching() && !isPooling()) { mocksControl.checkOrder(false); } } @Override public Controller prepareController() { return getSimpleController(); } @Test public void testNormalExecution1Component() { invokeInitForCache(component1Mock); invokeProcessingWithInit(component1Mock); invokeDisposal(component1Mock); mocksControl.replay(); processingAttributes.put("runtimeAttribute", "r"); processingAttributes.put("data", "d"); performProcessingDisposeAndVerifyMocks(Component1.class); assertEquals("dir", resultAttributes.get("data")); } @Test public void testNormalExecution3Components() { invokeInitForCache(component1Mock, component2Mock, component3Mock); invokeProcessingWithInit(component1Mock, component2Mock, component3Mock); invokeDisposal(component1Mock, component2Mock, component3Mock); mocksControl.replay(); processingAttributes.put("runtimeAttribute", "r"); processingAttributes.put("data", "d"); performProcessingDisposeAndVerifyMocks(Component1.class, Component2.class, Component3.class); assertEquals("diririr", resultAttributes.get("data")); } @Test public void testInputAttributesCopiedOnOutput() { testNormalExecution1Component(); assertEquals("r", resultAttributes.get("runtimeAttribute")); } @Test public void testUnrelatedProcessingAttributesCopiedOnOutput() { processingAttributes.put("unrelated", 10); testNormalExecution1Component(); assertEquals(10, resultAttributes.get("unrelated")); } /** * An attempt to validate the correctness of processing in a multithreaded setting. * Also demonstrates characteristics of each controller configuration, i.e. the number * of created component instances and processing requests. */ @Test @Nightly public void testStress() throws InterruptedException, ExecutionException { // If there's no caching, make fewer queries to speed up tests final int numberOfQueriesBase = isCaching() ? 1000 : 50; final int numberOfQueries = randomIntBetween(numberOfQueriesBase, 2 * numberOfQueriesBase); final int numberOfThreads = randomIntBetween(5, 30); final String [] data = new String [numberOfQueries]; final Set<String> uniqueQueries = Sets.newHashSet(); for (int i = 0; i < data.length; i++) { data[i] = Integer.toString(randomIntBetween(1, 30)); uniqueQueries.add(data[i]); } final int numberOfUniqueQueries = uniqueQueries.size(); // Calculated expected invocation counts final int numberOfCreatedComponentsMin; final int numberOfCreatedComponentsMax; final int numberOfProcessingRequests; if (!isCaching() && !isPooling()) { numberOfCreatedComponentsMin = numberOfCreatedComponentsMax = numberOfQueries; numberOfProcessingRequests = numberOfQueries; } else if (!isCaching() && isPooling()) { numberOfCreatedComponentsMin = 1; numberOfCreatedComponentsMax = numberOfThreads; numberOfProcessingRequests = numberOfQueries; } else if (isCaching() && !isPooling()) { // The +1 is to cover the fact that the cache needs to create a component // to read its attribute descriptors. This is done once per controller per // component configuration. numberOfCreatedComponentsMin = numberOfCreatedComponentsMax = numberOfUniqueQueries + 1; numberOfProcessingRequests = numberOfUniqueQueries; } else { numberOfCreatedComponentsMin = 1; numberOfCreatedComponentsMax = numberOfUniqueQueries; numberOfProcessingRequests = numberOfUniqueQueries; } // We're not using processing invocation utility methods which initialize // the controller, so we need to prepare one on our own. controller = prepareController(); controller.init(initAttributes); // Record calls mocksControl.checkOrder(false); component1Mock.init(isA(IControllerContext.class)); expectLastCall().times(numberOfCreatedComponentsMin, numberOfCreatedComponentsMax); for (int i = 0; i < numberOfProcessingRequests; i++) { component1Mock.beforeProcessing(); component1Mock.process(); expectLastCall().andAnswer(new DelayedAnswer<Object>(randomInt(100))); component1Mock.afterProcessing(); } component1Mock.dispose(); expectLastCall().times(numberOfCreatedComponentsMin, numberOfCreatedComponentsMax); mocksControl.replay(); // Perform processing final List<Thread> children = Collections.synchronizedList(Lists.<Thread>newArrayList()); ExecutorService executorService = Executors.newFixedThreadPool(numberOfThreads, new ThreadFactory() { public Thread newThread(Runnable r) { Thread t = new Thread(r); children.add(t); return t; } }); List<Callable<String>> callables = Lists.newArrayList(); for (final String string : data) { callables.add(new Callable<String>() { public String call() throws Exception { Map<String, Object> localAttributes = Maps.newHashMap(processingAttributes); localAttributes.put("runtimeAttribute", string); localAttributes.put("data", "d"); final ProcessingResult localResult = controller.process(localAttributes, Component1.class); return localResult.getAttribute("data"); } }); } // Validate results List<Future<String>> results = executorService.invokeAll(callables); int i = 0; for (Future<String> future : results) { assertEquals("di" + data[i++], future.get()); } executorService.shutdown(); for (Thread t : children) t.join(); controller.dispose(); controller = null; mocksControl.verify(); } @Test(expected = ComponentInitializationException.class) public void testExceptionWhileCreatingInstances() { invokeInitForCache(component1Mock); invokeProcessingWithInit(component1Mock); invokeDisposal(component1Mock); mocksControl.replay(); processingAttributes.put("data", "d"); performProcessingDisposeAndVerifyMocks(Component1.class, ComponentWithoutDefaultConstructor.class); } @Test(expected = ComponentInitializationException.class) public void testExceptionWhileInit() { invokeInitForCache(component1Mock, component2Mock); invokeProcessingWithInit(component1Mock); component2Mock.init(isA(IControllerContext.class)); expectLastCall().andThrow(new ComponentInitializationException((String) null)); component2Mock.dispose(); invokeDisposal(component1Mock); mocksControl.replay(); processingAttributes.put("data", "d"); performProcessingDisposeAndVerifyMocks(Component1.class, Component2.class); } @Test(expected = ProcessingException.class) public void testExceptionBeforeProcessing() { invokeInitForCache(component1Mock, component2Mock); invokeProcessingWithInit(component1Mock); invokeInit(component2Mock); component2Mock.beforeProcessing(); expectLastCall().andThrow(new ProcessingException("Before processing exception")); component2Mock.afterProcessing(); invokeDisposal(component1Mock, component2Mock); mocksControl.replay(); processingAttributes.put("data", "d"); performProcessingDisposeAndVerifyMocks(Component1.class, Component2.class); } @Test(expected = ProcessingException.class) public void testExceptionDuringProcessing() { invokeInitForCache(component1Mock); invokeProcessingWithInit(component1Mock); invokeInitForCache(component2Mock); invokeInit(component2Mock); component2Mock.beforeProcessing(); component2Mock.process(); expectLastCall().andThrow(new ProcessingException("Processing exception")); component2Mock.afterProcessing(); invokeDisposal(component1Mock, component2Mock); mocksControl.replay(); processingAttributes.put("data", "d"); performProcessingDisposeAndVerifyMocks(Component1.class, Component2.class); } @Test(expected = ProcessingException.class) public void testExceptionAfterProcessing() { invokeInitForCache(component1Mock, component2Mock); invokeProcessingWithInit(component1Mock); invokeInit(component2Mock); component2Mock.beforeProcessing(); component2Mock.process(); component2Mock.afterProcessing(); expectLastCall().andThrow(new ProcessingException("After processing exception")); invokeDisposal(component1Mock, component2Mock); mocksControl.replay(); processingAttributes.put("data", "d"); performProcessingDisposeAndVerifyMocks(Component1.class, Component2.class); } @Test @Nightly public void testNormalExecutionTimeMeasurement() { final long c1Time = 250; final long c2Time = 500; final long c3Time = 750; final long totalTime = c1Time + c2Time + c3Time; final double tolerance = 1; mocksControl.resetToNice(); component1Mock.process(); expectLastCall().andAnswer(new DelayedAnswer<Object>(c1Time)); component2Mock.beforeProcessing(); expectLastCall().andAnswer(new DelayedAnswer<Object>(c2Time)); component3Mock.afterProcessing(); expectLastCall().andAnswer(new DelayedAnswer<Object>(c3Time)); component1Mock.process(); expectLastCall().andAnswer(new DelayedAnswer<Object>(c1Time)); component2Mock.beforeProcessing(); expectLastCall().andAnswer(new DelayedAnswer<Object>(c2Time)); component3Mock.afterProcessing(); expectLastCall().andAnswer(new DelayedAnswer<Object>(c3Time)); mocksControl.replay(); processingAttributes.put("data", "d"); performProcessing(Component1.class, Component2.class, Component3.class); checkTimes(c1Time, c2Time, totalTime, tolerance); processingAttributes.put("data", "d"); performProcessingAndDispose(Component1.class, Component2.class, Component3.class); if (isCaching()) { checkTimes(0, 0, 0, tolerance); } else { checkTimes(c1Time, c2Time, totalTime, tolerance); } } @Test public void testContextDisposal() { final IProcessingComponent processingComponentWithContextListenerMock = mocksControl .createMock(IProcessingComponent.class); final IControllerContextListener contextListenerMock = mocksControl .createMock(IControllerContextListener.class); initAttributes.put("delegateWithContextListener", processingComponentWithContextListenerMock); initAttributes.put("contextListener", contextListenerMock); invokeInitForCache(processingComponentWithContextListenerMock); invokeProcessingWithInit(processingComponentWithContextListenerMock); invokeDisposal(processingComponentWithContextListenerMock); contextListenerMock.beforeDisposal(isA(IControllerContext.class)); mocksControl.replay(); processingAttributes.put("runtimeAttribute", "r"); processingAttributes.put("data", "d"); try { performProcessingDisposeAndVerifyMocks(ComponentWithContextListener.class); } finally { ComponentWithContextListener.contextListenerSubscribed.set(false); } } @Test public void testCollectionOfInitOutputAttributes() { performProcessingAndDispose(ComponentWithInitOutputAttribute.class); Assert.assertEquals("initOutput", resultAttributes.get("initOutput")); } /** * Verifies that {@link Output} attributes with <code>null</code> default values don't * clear default values in components further down the processing chain. */ @Test public void testOutputAttributesWithNullValues() { 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")); } @Test public void testProcessingInvocationMethods() { controller = prepareController().init( ImmutableMap.<String, Object> of(), new ProcessingComponentConfiguration(ComponentWithInitParameter.class, "component", ImmutableMap.<String, Object> of())); final Map<String, Object> attributes = Maps.newHashMap(); final ProcessingResult resultByClass = controller.process(attributes, ComponentWithInitParameter.class); final ProcessingResult resultByClassName = controller.process(attributes, ComponentWithInitParameter.class.getName()); final ProcessingResult resultById = controller.process(attributes, "component"); assertThat((String) resultByClass.getAttribute("result")).isEqualTo("defaultdefault"); assertThat((String) resultByClassName.getAttribute("result")).isEqualTo("defaultdefault"); assertThat((String) resultById.getAttribute("result")).isEqualTo("defaultdefault"); controller.dispose(); controller = null; } @Test public void testPassingRequiredProcessingAttribute() { controller = prepareController(); final Map<String, Object> attributes = Maps.newHashMap(); controller.process(attributes, ComponentWithOutputAttribute.class, ComponentWithRequiredProcessingAttribute.class); controller.dispose(); controller = null; } @Test public void testComponentConfigurationDifferentInitAttributes() { controller = prepareController().init( ImmutableMap.<String, Object> of(), new ProcessingComponentConfiguration(ComponentWithInitParameter.class, "component1", ImmutableMap.of("init", (Object) "v1")), new ProcessingComponentConfiguration(ComponentWithInitParameter.class, "component2", ImmutableMap.of("init", (Object) "v2"))); final Map<String, Object> attributes = Maps.newHashMap(); assertThat(controller.process(attributes, "component1").getAttributes()) .includes(entry("result", "v1v1")); assertThat(controller.process(attributes, "component2").getAttributes()) .includes(entry("result", "v2v2")); controller.dispose(); controller = null; } @Test public void testComponentConfigurationProcessingAttributeAtInitTime() { controller = prepareController().init( ImmutableMap.<String, Object> of(), new ProcessingComponentConfiguration(ComponentWithProcessingParameter.class, "component1", ImmutableMap.of("processing", (Object) "v1")), new ProcessingComponentConfiguration(ComponentWithProcessingParameter.class, "component2", ImmutableMap.of("processing", (Object) "v2"))); final Map<String, Object> attributes = Maps.newHashMap(); assertThat(controller.process(attributes, "component1").getAttributes()) .includes(entry("result", "v1v1")); assertThat(controller.process(attributes, "component2").getAttributes()) .includes(entry("result", "v2v2")); controller.dispose(); controller = null; } @Test public void testInitProcessingInputRequiredAttributeProvidedDuringProcessing() { processingAttributes.put("initProcessing", "test"); performProcessingAndDispose(ComponentWithInitProcessingInputRequiredAttribute.class); assertThat((String) resultAttributes.get("result")).isEqualTo("test"); } @Test(expected = IllegalArgumentException.class) public void testComponentConfigurationDuplicateComponentId() { Controller controller = prepareController(); try { controller.init( ImmutableMap.<String, Object> of(), new ProcessingComponentConfiguration(ComponentWithInitParameter.class, "component", ImmutableMap.of("init", (Object) "v1")), new ProcessingComponentConfiguration(ComponentWithInitParameter.class, "component", ImmutableMap.of("init", (Object) "v2"))); } finally { controller.dispose(); } } @Test public void testEmptyStatsInNewController() { final Controller controller = prepareController(); final ControllerStatistics statistics = controller.getStatistics(); assertThat(statistics).isNotNull(); assertThat(statistics.totalQueries).isEqualTo(0); assertThat(statistics.goodQueries).isEqualTo(0); assertThat(statistics.algorithmTimeAverageInWindow).isEqualTo(0); assertThat(statistics.algorithmTimeMeasurementsInWindow).isEqualTo(0); assertThat(statistics.sourceTimeAverageInWindow).isEqualTo(0); assertThat(statistics.sourceTimeMeasurementsInWindow).isEqualTo(0); assertThat(statistics.totalTimeAverageInWindow).isEqualTo(0); assertThat(statistics.totalTimeMeasurementsInWindow).isEqualTo(0); if (isCaching()) { assertThat(statistics.cacheMisses).isEqualTo(0); assertThat(statistics.cacheHitsTotal).isEqualTo(0); } else { assertThat((Object) statistics.cacheMisses).isNull(); assertThat((Object) statistics.cacheHitsTotal).isNull(); } controller.dispose(); } @Test public void testStatsOneGoodQuery() { final int delay = 100; final int halfDelay = delay / 2; processingAttributes.put("data", "d"); mocksControl.resetToNice(); component1Mock.process(); expectLastCall().andAnswer(new DelayedAnswer<Object>(delay)); component2Mock.process(); expectLastCall().andAnswer(new DelayedAnswer<Object>(delay)); mocksControl.replay(); performProcessing(Component1.class, Component2.class); final ControllerStatistics statistics = controller.getStatistics(); assertThat(statistics).isNotNull(); assertThat(statistics.totalQueries).isEqualTo(1); assertThat(statistics.goodQueries).isEqualTo(1); assertThat(statistics.algorithmTimeAverageInWindow).isGreaterThanOrEqualTo(halfDelay); assertThat(statistics.algorithmTimeMeasurementsInWindow).isEqualTo(1); assertThat(statistics.sourceTimeAverageInWindow).isGreaterThanOrEqualTo(halfDelay); assertThat(statistics.sourceTimeMeasurementsInWindow).isEqualTo(1); assertThat(statistics.totalTimeAverageInWindow).isGreaterThanOrEqualTo(2 * halfDelay); assertThat(statistics.totalTimeMeasurementsInWindow).isEqualTo(1); if (isCaching()) { assertThat(statistics.cacheMisses).isEqualTo(2); assertThat(statistics.cacheHitsTotal).isEqualTo(0); } controller.dispose(); controller = null; } @Test public void testStatsTwoGoodQueriesCached() { processingAttributes.put("data", "d"); performProcessing(Component1.class); processingAttributes.put("data", "d"); performProcessing(Component1.class); final ControllerStatistics statistics = controller.getStatistics(); assertThat(statistics).isNotNull(); assertThat(statistics.totalQueries).isEqualTo(2); assertThat(statistics.goodQueries).isEqualTo(2); if (isCaching()) { assertThat(statistics.cacheMisses).isEqualTo(1); assertThat(statistics.cacheHitsTotal).isEqualTo(1); } controller.dispose(); controller = null; } @Test(expected = RuntimeException.class) public void testStatsOneGoodQueryOneErrorQuery() { mocksControl.resetToNice(); processingAttributes.put("data", "d"); component1Mock.process(); expectLastCall().andThrow(new RuntimeException()); component1Mock.afterProcessing(); mocksControl.replay(); try { performProcessing(Component3.class); performProcessing(Component1.class, Component2.class); } finally { final ControllerStatistics statistics = controller.getStatistics(); assertThat(statistics).isNotNull(); assertThat(statistics.totalQueries).isEqualTo(2); assertThat(statistics.goodQueries).isEqualTo(1); if (isCaching()) { assertThat(statistics.cacheMisses).isEqualTo(2); assertThat(statistics.cacheHitsTotal).isEqualTo(0); } controller.dispose(); controller = null; } } @Test public void settingInitAttributeToNull() { invokeInitForCache(component1Mock); invokeProcessingWithInit(component1Mock); invokeDisposal(component1Mock); mocksControl.replay(); initAttributes.put("data", null); processingAttributes.put("runtimeAttribute", "r"); processingAttributes.put("data", "d"); performProcessingDisposeAndVerifyMocks(Component1.class); assertEquals("dir", resultAttributes.get("data")); } @Bindable public static class ComponentWithMapParameter extends ProcessingComponentBase { @Input @Processing @Attribute(key = "other") public Map<String,String> other; @Output @Processing @Attribute(key = "result") public String result; @Override public void process() throws ProcessingException { result = new TreeMap<>(other).toString(); } } @Test public void testMapWithKeysAttribute() { Map<String, String> map1 = new HashMap<String, String>(); map1.put("k1", "v1"); map1.put("k2", "v2"); processingAttributes.put("other", map1); ProcessingResult pr = performProcessing(ComponentWithMapParameter.class); assertThat((Object) pr.getAttribute("result")).isEqualTo("{k1=v1, k2=v2}"); if (isCaching()) { pr = performProcessing(ComponentWithMapParameter.class); assertThat((String) pr.getAttribute("result")).isEqualTo("{k1=v1, k2=v2}"); final ControllerStatistics statistics = controller.getStatistics(); assertThat(statistics.cacheMisses).isEqualTo(1); assertThat(statistics.cacheHitsTotal).isEqualTo(1); } Map<String, String> map2 = new HashMap<String, String>(); map2.putAll(map1); map1.put("k1", "v1_2"); pr = performProcessing(ComponentWithMapParameter.class); assertThat((Object) pr.getAttribute("result")).isEqualTo("{k1=v1_2, k2=v2}"); controller.dispose(); controller = null; } /** * 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 (isCaching() && !isPooling()) { for (IProcessingComponent component : components) { component.init(isA(IControllerContext.class)); component.dispose(); } } } }