package io.neba.core.resourcemodels.registration;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import org.osgi.service.event.Event;
import org.slf4j.Logger;
import java.lang.reflect.Field;
import java.util.Dictionary;
import java.util.Hashtable;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.apache.commons.lang3.reflect.FieldUtils.getField;
import static org.apache.commons.lang3.reflect.FieldUtils.writeField;
import static org.mockito.Mockito.*;
/**
* @author Olaf Otto
*/
@RunWith(MockitoJUnitRunner.class)
public class MappableTypeHierarchyChangeListenerTest {
@Mock
private ModelRegistry modelRegistry;
@Mock
private Logger logger;
@InjectMocks
private MappableTypeHierarchyChangeListener testee;
@Before
public void setUp() throws Exception {
Field field = getField(MappableTypeHierarchyChangeListener.class, "logger", true);
writeField(field, this.testee, this.logger);
}
@Test
public void testChangeListenerShutsDownExecutorUponDeactivation() throws Exception {
activate();
deactivate();
withChangeOn("/apps/testapp/components/test");
sleep();
verifyModelRegistryCacheIsNotCleared();
}
@Test
public void testActivateListenerClearsModelRegistryUponEvent() throws Exception {
activate();
withChangeOn("/apps/testapp/components/test");
sleep();
verifyModelRegistryCacheIsCleared();
}
/**
* When multiple successive events are handled, only the first one shall cause the cache to be cleared, i.e.
* events do not queue up while the cache is cleared, as it is sufficient to clear the cache once.
*
*/
@Test
public void testMultipleEventsAreSummarized() throws Exception {
activate();
withChangeOn("/apps/testapp/components/test");
withChangeOn("/apps/testapp/components/test");
withChangeOn("/apps/testapp/components/test");
withChangeOn("/apps/testapp/components/test");
withChangeOn("/apps/testapp/components/test");
sleep();
verifyModelRegistryCacheIsClearedAtMost(2);
}
@Test
public void testLoggingOfInvalidatingChangeWhenLogLevelIsTrace() throws Exception {
activate();
withTraceLogging();
withChangeOn("/apps/testapp/components/test");
sleep();
verifyLoggerTraces(
"Invalidating the resource model registry lookup cache due to changes to {}.",
"/apps/testapp/components/test");
}
@Test
public void testLoggingOfInterruptionWithoutShutdown() throws Exception {
withInterruptedExceptionThrownWhileBlocked();
activate();
withChangeOn("/some/path");
sleep();
verifyLoggerDebugs("The type hierarchy change listener got interrupted, but was not shut down.");
}
private void verifyLoggerDebugs(String message) {
verify(this.logger, atLeast(1)).debug(eq(message), isA(InterruptedException.class));
}
private void withInterruptedExceptionThrownWhileBlocked() throws IllegalAccessException, InterruptedException {
Field field = getField(MappableTypeHierarchyChangeListener.class, "invalidationRequests", true);
BlockingQueue queue = mock(BlockingQueue.class);
writeField(field, this.testee, queue);
doThrow(new InterruptedException("THIS IS AN EXPECTED TEST EXCEPTION"))
.when(queue)
.poll(anyLong(), isA(TimeUnit.class));
}
private void verifyLoggerTraces(String format, String arg) {
verify(this.logger).trace(
format,
arg);
}
private void withTraceLogging() {
doReturn(true).when(this.logger).isTraceEnabled();
}
private void verifyModelRegistryCacheIsClearedAtMost(int times) {
verify(this.modelRegistry, atMost(times)).clearLookupCaches();
}
private void verifyModelRegistryCacheIsCleared() {
verify(this.modelRegistry).clearLookupCaches();
}
private void activate() {
this.testee.activate();
}
private void verifyModelRegistryCacheIsNotCleared() {
verify(this.modelRegistry, never()).clearLookupCaches();
}
private void sleep() throws InterruptedException {
Thread.sleep(SECONDS.toMillis(2));
}
private void withChangeOn(String path) {
Dictionary<String, Object> properties = new Hashtable<>();
properties.put("path", path);
Event event = new Event("test/topic", properties);
this.testee.handleEvent(event);
}
private void deactivate() {
this.testee.deactivate();
}
}