package ro.isdc.wro.model.resource.support.change;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.mockito.MockitoAnnotations.initMocks;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.concurrent.Callable;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import javax.servlet.FilterConfig;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ro.isdc.wro.cache.CacheKey;
import ro.isdc.wro.cache.CacheStrategy;
import ro.isdc.wro.cache.CacheValue;
import ro.isdc.wro.config.Context;
import ro.isdc.wro.config.support.ContextPropagatingCallable;
import ro.isdc.wro.http.WroFilter;
import ro.isdc.wro.manager.callback.LifecycleCallback;
import ro.isdc.wro.manager.callback.LifecycleCallbackRegistry;
import ro.isdc.wro.manager.callback.LifecycleCallbackSupport;
import ro.isdc.wro.manager.factory.BaseWroManagerFactory;
import ro.isdc.wro.manager.factory.WroManagerFactory;
import ro.isdc.wro.model.WroModel;
import ro.isdc.wro.model.factory.WroModelFactory;
import ro.isdc.wro.model.group.Group;
import ro.isdc.wro.model.group.Inject;
import ro.isdc.wro.model.group.processor.Injector;
import ro.isdc.wro.model.group.processor.InjectorBuilder;
import ro.isdc.wro.model.resource.Resource;
import ro.isdc.wro.model.resource.ResourceType;
import ro.isdc.wro.model.resource.locator.UriLocator;
import ro.isdc.wro.model.resource.locator.factory.AbstractUriLocatorFactory;
import ro.isdc.wro.model.resource.locator.factory.UriLocatorFactory;
import ro.isdc.wro.model.resource.support.change.ResourceWatcher.Callback;
import ro.isdc.wro.util.Function;
import ro.isdc.wro.util.ObjectFactory;
import ro.isdc.wro.util.WroTestUtils;
/**
* @author Alex Objelean
*/
public class TestResourceWatcher {
private static final Logger LOG = LoggerFactory.getLogger(TestResourceWatcher.class);
/**
* The uri to the first resource in a group.
*/
private static final String RESOURCE_JS_URI = "/path/1.js";
private static final String RESOURCE_CSS_URI = "/test.css";
private static final String GROUP_NAME = "g1";
private static final String MIXED_GROUP_NAME = "mixedGroup";
/**
* Group containing two js resources.
*/
private static final String GROUP_2 = "g2";
private final CacheKey cacheKey = new CacheKey(GROUP_NAME, ResourceType.CSS, true);
private final CacheKey cacheKey2 = new CacheKey(GROUP_2, ResourceType.JS, true);
@Mock
private HttpServletRequest request;
@Mock
private HttpServletResponse response;
@Mock
private FilterConfig filterConfig;
@Mock
private UriLocator mockLocator;
@Mock
private Callback resourceWatcherCallback;
@Mock
private CacheStrategy<CacheKey, CacheValue> cacheStrategy;
private ResourceWatcher victim;
@BeforeClass
public static void onBeforeClass() {
assertEquals(0, Context.countActive());
}
@AfterClass
public static void onAfterClass() {
assertEquals(0, Context.countActive());
}
@Before
public void setUp() {
initMocks(this);
Context.set(Context.webContext(request, response, filterConfig));
// spy the interface instead of WroTestUtils.createResourceMockingLocator() because of mockito bug which was
// reported on their mailing list.
mockLocator = Mockito.spy(new UriLocator() {
public InputStream locate(final String uri)
throws IOException {
return new ByteArrayInputStream(uri.getBytes());
}
public boolean accept(final String uri) {
return true;
}
});
// Add explicity the filter which makes the request allowed for async check
when(request.getAttribute(Mockito.eq(WroFilter.ATTRIBUTE_PASSED_THROUGH_FILTER))).thenReturn(true);
victim = new ResourceWatcher();
createDefaultInjector().inject(victim);
}
@After
public void tearDown()
throws Exception {
victim.destroy();
Context.unset();
}
public Injector createDefaultInjector() {
final UriLocatorFactory locatorFactory = new AbstractUriLocatorFactory() {
public UriLocator getInstance(final String uri) {
return mockLocator;
}
};
final WroModel model = new WroModel().addGroup(new Group(GROUP_NAME).addResource(Resource.create(RESOURCE_CSS_URI)));
model.addGroup(new Group(GROUP_2).addResource(Resource.create(RESOURCE_JS_URI)).addResource(
Resource.create("/path/2.js")));
model.addGroup(new Group(MIXED_GROUP_NAME).addResource(Resource.create(RESOURCE_CSS_URI)).addResource(
Resource.create(RESOURCE_JS_URI)));
final WroModelFactory modelFactory = WroTestUtils.simpleModelFactory(model);
final WroManagerFactory factory = new BaseWroManagerFactory().setModelFactory(modelFactory).setUriLocatorFactory(
locatorFactory).setCacheStrategy(cacheStrategy);
final Injector injector = InjectorBuilder.create(factory).build();
return injector;
}
@Test(expected = NullPointerException.class)
public void cannotCheckNullCacheEntry() {
Context.unset();
victim.check(null);
}
@Test
public void shouldNotDetectChangeAfterFirstRun()
throws Exception {
victim.check(cacheKey);
assertFalse(victim.getResourceChangeDetector().checkChangeForGroup(RESOURCE_CSS_URI, GROUP_NAME));
}
@Test
public void shouldDetectResourceChange()
throws Exception {
// flag used to assert that the expected code was invoked
createDefaultInjector().inject(victim);
victim.check(cacheKey, resourceWatcherCallback);
assertFalse(victim.getResourceChangeDetector().checkChangeForGroup(RESOURCE_CSS_URI, GROUP_NAME));
Mockito.when(mockLocator.locate(Mockito.anyString())).thenReturn(new ByteArrayInputStream("different".getBytes()));
final ArgumentCaptor<CacheKey> argumentCaptor = ArgumentCaptor.forClass(CacheKey.class);
victim.check(cacheKey);
assertTrue(victim.getResourceChangeDetector().checkChangeForGroup(RESOURCE_CSS_URI, GROUP_NAME));
Mockito.verify(resourceWatcherCallback).onGroupChanged(argumentCaptor.capture());
assertEquals(GROUP_NAME, argumentCaptor.getValue().getGroupName());
}
@Test
public void shouldAssumeResourceNotChangedWhenStreamIsUnavailable()
throws Exception {
createDefaultInjector().inject(victim);
final ResourceChangeDetector mockChangeDetector = Mockito.spy(victim.getResourceChangeDetector());
Mockito.when(mockLocator.locate(Mockito.anyString())).thenThrow(new IOException("Resource is unavailable"));
victim.check(cacheKey, resourceWatcherCallback);
verify(resourceWatcherCallback, never()).onGroupChanged(Mockito.any(CacheKey.class));
verify(mockChangeDetector, never()).checkChangeForGroup(Mockito.anyString(), Mockito.anyString());
}
@Test
public void shouldDetectChangeOfImportedResource()
throws Exception {
final String importResourceUri = "imported.css";
final CacheKey cacheEntry = new CacheKey(GROUP_NAME, ResourceType.CSS, true);
victim = new ResourceWatcher();
createDefaultInjector().inject(victim);
when(mockLocator.locate(Mockito.anyString())).thenAnswer(answerWithContent("initial"));
when(mockLocator.locate("/" + Mockito.eq(RESOURCE_CSS_URI))).thenAnswer(
answerWithContent(String.format("@import url(%s)", importResourceUri)));
victim.check(cacheEntry, resourceWatcherCallback);
when(mockLocator.locate(Mockito.anyString())).thenAnswer(answerWithContent("changed"));
when(mockLocator.locate("/" + Mockito.eq(RESOURCE_CSS_URI))).thenAnswer(
answerWithContent(String.format("@import url(%s)", importResourceUri)));
victim.check(cacheEntry);
verify(resourceWatcherCallback).onGroupChanged(Mockito.any(CacheKey.class));
verify(resourceWatcherCallback).onResourceChanged(Mockito.any(Resource.class));
}
/**
* Fix the issue described <a href="https://github.com/alexo/wro4j/issues/72">here</a>.
*/
@Test
public void shouldNotDetectErroneouslyChange()
throws Exception {
createDefaultInjector().inject(victim);
// first check will always detect changes.
victim.check(cacheKey2, resourceWatcherCallback);
when(mockLocator.locate(RESOURCE_JS_URI)).thenAnswer(answerWithContent("changed"));
victim.check(cacheKey2, resourceWatcherCallback);
verify(resourceWatcherCallback, Mockito.atLeastOnce()).onGroupChanged(Mockito.any(CacheKey.class));
verify(resourceWatcherCallback, Mockito.atLeastOnce()).onResourceChanged(Mockito.any(Resource.class));
Mockito.reset(resourceWatcherCallback);
// next check should find no change
victim.check(cacheKey2, resourceWatcherCallback);
verify(resourceWatcherCallback, Mockito.never()).onGroupChanged(Mockito.any(CacheKey.class));
verify(resourceWatcherCallback, Mockito.never()).onResourceChanged(Mockito.any(Resource.class));
}
private static class CallbackRegistryHolder {
@Inject
private LifecycleCallbackRegistry registry;
}
@Test
public void shouldInvokeCallbackWhenChangeIsDetected()
throws Exception {
final CallbackRegistryHolder callbackRegistryHolder = new CallbackRegistryHolder();
final AtomicBoolean flag = new AtomicBoolean();
final Injector injector = createDefaultInjector();
injector.inject(victim);
injector.inject(callbackRegistryHolder);
callbackRegistryHolder.registry.registerCallback(new ObjectFactory<LifecycleCallback>() {
public LifecycleCallback create() {
return new LifecycleCallbackSupport() {
@Override
public void onResourceChanged(final Resource resource) {
flag.set(true);
}
};
}
});
victim.check(cacheKey);
assertTrue(flag.get());
}
@Test
public void shouldCheckForChangeAsynchronously()
throws Exception {
final int timeout = 100;
Context.get().getConfig().setConnectionTimeout(timeout);
final String invalidUrl = "http://localhost:1/";
when(request.getRequestURL()).thenReturn(new StringBuffer(invalidUrl));
when(request.getServletPath()).thenReturn("");
final AtomicReference<Callable<Void>> asyncInvoker = new AtomicReference<Callable<Void>>();
final AtomicReference<Exception> exceptionHolder = new AtomicReference<Exception>();
victim = new ResourceWatcher() {
@Override
void submit(final Callable<Void> callable) {
try {
final Callable<Void> decorated = new ContextPropagatingCallable<Void>(callable) {
@Override
public Void call()
throws Exception {
try {
callable.call();
return null;
} catch (final Exception e) {
exceptionHolder.set(e);
throw e;
} finally {
asyncInvoker.set(callable);
}
}
};
super.submit(decorated);
} finally {
}
}
};
createDefaultInjector().inject(victim);
Context.get().getConfig().setResourceWatcherAsync(true);
victim.tryAsyncCheck(cacheKey);
WroTestUtils.waitUntil(new Function<Void, Boolean>() {
public Boolean apply(final Void input)
throws Exception {
return asyncInvoker.get() != null;
}
}, timeout * 2);
assertNotNull(asyncInvoker.get());
assertNotNull(exceptionHolder.get());
// We expect a request to fail, since a request a localhost using some port from where we expect to get no response.
LOG.debug("Exception: {}", exceptionHolder.get().getClass());
assertTrue(exceptionHolder.get() instanceof IOException);
}
@Test
public void shouldNotCheckAtAllWhenAsyncIsConfiguredButNotAllowed() {
Context.get().getConfig().setResourceWatcherAsync(true);
when(request.getAttribute(Mockito.eq(WroFilter.ATTRIBUTE_PASSED_THROUGH_FILTER))).thenReturn(null);
final ResourceWatcher victimSpy = Mockito.spy(victim);
victimSpy.tryAsyncCheck(cacheKey);
verify(victimSpy, Mockito.never()).check(Mockito.eq(cacheKey));
}
@Test
public void shouldRemoveKeyFromCacheStrategyWhenChangeDetected() {
victim.check(cacheKey);
final CacheValue cacheValue = null;
verify(cacheStrategy).put(Mockito.eq(cacheKey), Mockito.eq(cacheValue));
}
private Answer<InputStream> answerWithContent(final String content) {
return answerWithContent(content, 0);
}
private Answer<InputStream> answerWithContent(final String content, final long delay) {
return new Answer<InputStream>() {
public InputStream answer(final InvocationOnMock invocation)
throws Throwable {
if (delay > 0) {
Thread.sleep(delay);
}
return new ByteArrayInputStream(content.getBytes());
}
};
}
@Test
public void shouldCheckForResourceChangeAsynchronously()
throws Exception {
Context.get().getConfig().setResourceWatcherAsync(true);
final CacheKey cacheKey1 = new CacheKey(MIXED_GROUP_NAME, ResourceType.CSS, true);
final CacheKey cacheKey2 = new CacheKey(MIXED_GROUP_NAME, ResourceType.JS, true);
// First check is required to ensure that the subsequent changes do not detect any change
victim.check(cacheKey1);
victim.check(cacheKey2);
when(mockLocator.locate(RESOURCE_JS_URI)).thenAnswer(answerWithContent("changed"));
victim.check(cacheKey2, resourceWatcherCallback);
verify(resourceWatcherCallback).onGroupChanged(Mockito.any(CacheKey.class));
verify(resourceWatcherCallback).onResourceChanged(Mockito.any(Resource.class));
}
private ContextPropagatingCallable<Void> createCheckingCallable(final CacheKey cacheKey, final Callback callback) {
return new ContextPropagatingCallable<Void>(new Callable<Void>() {
public Void call()
throws Exception {
victim.check(cacheKey, callback);
return null;
}
});
}
}