Skip to content

Commit

Permalink
#37 : MockAsyncContext for testing Async actions.
Browse files Browse the repository at this point in the history
  • Loading branch information
vankeisb committed Jan 18, 2016
1 parent 9354905 commit 362344a
Show file tree
Hide file tree
Showing 9 changed files with 210 additions and 24 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,8 @@ protected void service(final HttpServletRequest request, final HttpServletRespon
// remove currentContext ThreadLocal
ExecutionContext.clearContextThreadLocal();
// start async processing
AsyncContext asyncContext = request.startAsync();
log.debug("Starting async processing from action ", ctx.getActionBean());
AsyncContext asyncContext = request.startAsync(request, response);
final PageContext pc = pageContext;
// register listener for finalizing the async processing
asyncContext.addListener(new AsyncListener() {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,142 @@
package net.sourceforge.stripes.mock;

/**
* Created by vankeisb on 13/01/16.
*/
public class MockAsyncContext {
import net.sourceforge.stripes.util.Log;

import javax.servlet.*;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;

public class MockAsyncContext implements AsyncContext {

private static final Log log = Log.getInstance(MockAsyncContext.class);

private final ServletRequest request;
private final ServletResponse response;
private final List<AsyncListener> listeners = new ArrayList<>();


private boolean completed = false;
private long timeout = 30000;

public MockAsyncContext(ServletRequest request, ServletResponse response, ExecutorService executorService) {
this.request = request;
this.response = response;
long startTime = System.currentTimeMillis();
log.info("async started, request=", request, ", response=", response);
// trigger the timeout thread...
executorService.submit(new Runnable() {
@Override
public void run() {
try {
while (true) {
long now = System.currentTimeMillis();
long elapsed = now - startTime;
log.debug("timeout thread check : elapsed=", elapsed);
if (elapsed > timeout) {
// invoke listeners timeout
AsyncEvent timeoutEvent = new AsyncEvent(MockAsyncContext.this, request, response);
for (AsyncListener l : listeners) {
try {
l.onTimeout(timeoutEvent);
} catch (Exception e) {
log.warn("listener onTimeout threw exception", e);
}
}
break;
}
Thread.sleep(200);
}
}catch(InterruptedException e){
// exit the loop
log.warn("Async Context was Interrupted", e);
}
}
});
}

@Override
public ServletRequest getRequest() {
return request;
}

@Override
public ServletResponse getResponse() {
return response;
}

@Override
public boolean hasOriginalRequestAndResponse() {
return request != null && response != null;
}

private void checkNotCompleted() {
if (completed) {
throw new IllegalStateException("already dispatched or completed !");
}
}

@Override
public void dispatch() {
complete();
}

@Override
public void dispatch(String path) {
dispatch();
}

@Override
public void dispatch(ServletContext context, String path) {
dispatch();
}

@Override
public void complete() {
checkNotCompleted();
completed = true;
AsyncEvent evt = new AsyncEvent(this, request, response);
for (AsyncListener l : listeners) {
try {
l.onComplete(evt);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

@Override
public void start(Runnable run) {
throw new UnsupportedOperationException("TODO");
}

@Override
public void addListener(AsyncListener listener) {
listeners.add(listener);
}

@Override
public void addListener(AsyncListener listener, ServletRequest servletRequest, ServletResponse servletResponse) {
listeners.add(listener);
}

@Override
public <T extends AsyncListener> T createListener(Class<T> clazz) throws ServletException {
try {
return clazz.newInstance();
} catch (Exception e) {
throw new ServletException(e);
}
}

@Override
public void setTimeout(long timeout) {
this.timeout = timeout;
}

@Override
public long getTimeout() {
return timeout;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutorService;

import javax.servlet.*;
import javax.servlet.http.*;
Expand Down Expand Up @@ -79,17 +80,22 @@ public class MockHttpServletRequest implements HttpServletRequest {
private String pathInfo = "";
private String queryString = "";

private MockAsyncContext asyncContext = null;
private final ExecutorService executorService;


/**
* Minimal constructor that makes sense. Requires a context path (should be the same as
* the name of the servlet context, prepended with a '/') and a servlet path. E.g.
* new MockHttpServletRequest("/myapp", "/actionType/foo.action").
*
* @param contextPath
* @param contextPath
* @param servletPath
* @param executorService
*/
public MockHttpServletRequest(String contextPath, String servletPath) {
public MockHttpServletRequest(String contextPath, String servletPath, ExecutorService executorService) {
this.contextPath = contextPath;
this.servletPath = servletPath;
this.executorService = executorService;
}

/** Sets the auth type that will be reported by this request. */
Expand Down Expand Up @@ -464,23 +470,26 @@ public ServletContext getServletContext() {
}

public AsyncContext startAsync() throws IllegalStateException {
return null;
throw new UnsupportedOperationException("use request,response variant");
}

public AsyncContext startAsync(ServletRequest servletRequest, ServletResponse servletResponse) throws IllegalStateException {
return null;
if (asyncContext == null) {
asyncContext = new MockAsyncContext(servletRequest, servletResponse, executorService);
}
return asyncContext;
}

public boolean isAsyncStarted() {
return false;
return asyncContext != null;
}

public boolean isAsyncSupported() {
return false;
return true;
}

public AsyncContext getAsyncContext() {
return null;
return asyncContext;
}

public DispatcherType getDispatcherType() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import javax.servlet.Filter;

Expand Down Expand Up @@ -157,7 +159,7 @@ else if (parts.length == 2) {
}

this.context = context;
this.request = new MockHttpServletRequest("/" + context.getServletContextName(), path);
this.request = new MockHttpServletRequest("/" + context.getServletContextName(), path, context.getExecutorService());
this.request.setSession(session);
this.response = new MockHttpServletResponse();
setSourcePage(DEFAULT_SOURCE_PAGE);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@
import java.net.MalformedURLException;
import java.net.URL;
import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

/**
* <p>Mock implementation of a ServletContext. Provides implementation the most commonly used
Expand Down Expand Up @@ -49,6 +52,7 @@ public class MockServletContext implements ServletContext {
private List<Filter> filters = new ArrayList<Filter>();
private List<ServletContextListener> listeners = new ArrayList<ServletContextListener>();
private HttpServlet servlet;
private ExecutorService executorService = Executors.newSingleThreadExecutor();

/** Simple constructor that creates a new mock ServletContext with the supplied context name. */
public MockServletContext(String contextName) {
Expand All @@ -65,6 +69,10 @@ public ServletContext getContext(String url) {
}
}

public ExecutorService getExecutorService() {
return executorService;
}

/** Servlet 2.3 method. Returns the context name with a leading slash. */
public String getContextPath() {
return "/" + this.contextName;
Expand Down Expand Up @@ -315,6 +323,14 @@ public void close() {
}
}
}
executorService.shutdownNow();
try {
if (!executorService.awaitTermination(10, TimeUnit.SECONDS)) {
log("Unable to shut down executor service !");
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}

public int getEffectiveMajorVersion() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ public class RedirectResolutionTest extends FilterEnabledTestBase {

//helper method
private MockHttpServletRequest buildMockServletRequest(){
MockHttpServletRequest request = new MockHttpServletRequest("/context", "/whatever");
MockHttpServletRequest request = new MockHttpServletRequest("/context", "/whatever", null);
request.addLocale(Locale.US);
return request;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,34 @@

import net.sourceforge.stripes.FilterEnabledTestBase;
import net.sourceforge.stripes.action.*;
import org.testng.Assert;
import static org.testng.Assert.*;
import org.testng.annotations.Test;

import javax.servlet.AsyncContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class TestMockAsync extends FilterEnabledTestBase {

@Test(groups="fast")
public void testDefaultEvent() throws Exception {
@Test
public void testSuccess() throws Exception {
MockRoundtrip trip = new MockRoundtrip(getMockServletContext(), AsyncActionBean.class);
trip.execute();

AsyncActionBean bean = trip.getActionBean(AsyncActionBean.class);
Assert.assertNotNull(bean);
assertNotNull(bean);
assertTrue(bean.isCompleted());
}

@Test
public void testTimeout() throws Exception {
MockRoundtrip trip = new MockRoundtrip(getMockServletContext(), AsyncActionBean.class);
trip.execute("doAsyncTimeout");
AsyncActionBean bean = trip.getActionBean(AsyncActionBean.class);
// wait for longer than timeout
Thread.sleep(3000);
assertNotNull(bean);
assertTrue(!bean.isCompleted());
HttpServletResponse response = bean.getContext().getResponse();
assertEquals(response.getStatus(), 500);
}

@UrlBinding("/async")
public static class AsyncActionBean implements ActionBean {
Expand All @@ -42,12 +52,25 @@ public Resolution doAsync() {
protected void executeAsync() throws Exception {
Thread.sleep(5000);
getResponse().getWriter().write("DONE");
completed = true;
complete();
}
};
}

public Resolution doAsyncTimeout() {
return new AsyncResolution() {
@Override
protected void executeAsync() throws Exception {
getAsyncContext().setTimeout(1000);
// we never complete !
}
};
}

public boolean isCompleted() {
return completed;
}
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ public void testFetchingRequestAttributes() throws Exception {

@Test(groups="fast")
public void testRequestCaseInsensitive() {
final MockHttpServletRequest request = new MockHttpServletRequest("", "");
final MockHttpServletRequest request = new MockHttpServletRequest("", "", null);

String headerName = "User-Agent";
Object value = "Netscape/6.0";
Expand Down
2 changes: 1 addition & 1 deletion stripes/src/test/resources/log4j.properties
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@ log4j.appender.stdout.layout.ConversionPattern=%d{ABSOLUTE} %5p %c{1}:%L - %m%n

### set log levels - for more verbose logging change 'info' to 'debug' ###
log4j.rootLogger=INFO, stdout
log4j.logger.net.sourceforge.stripes=INFO
log4j.logger.net.sourceforge.stripes=DEBUG

0 comments on commit 362344a

Please sign in to comment.