Skip to content

Commit

Permalink
#37 : MockAsyncContext for testing Async actions, better impl, no Exe…
Browse files Browse the repository at this point in the history
…cutorService needed. Async resolutions are handled as if blocking from trip.execute(). Behavior should be transparent for the test (sync or async, it should block)
  • Loading branch information
vankeisb committed Jan 18, 2016
1 parent 362344a commit bb1ded3
Show file tree
Hide file tree
Showing 7 changed files with 87 additions and 58 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,44 +16,16 @@ public class MockAsyncContext implements AsyncContext {
private final ServletResponse response;
private final List<AsyncListener> listeners = new ArrayList<>();


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

public MockAsyncContext(ServletRequest request, ServletResponse response, ExecutorService executorService) {
public MockAsyncContext(ServletRequest request, ServletResponse response) {
this.startedOn = System.currentTimeMillis();
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
Expand Down Expand Up @@ -139,4 +111,29 @@ public void setTimeout(long timeout) {
public long getTimeout() {
return timeout;
}

public void waitForCompletion() throws Exception {
log.debug("Waiting for completion...");
while(true) {
long elapsed = System.currentTimeMillis() - startedOn;
if (elapsed > timeout) {
timedOut = true;
// 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);
}
}
log.error("Operation timed out, will throw exception");
throw new RuntimeException("Operation timed out (elapsed=" + elapsed + ", timeout=" + timeout + ")");
} else if (completed) {
log.debug("...Completed in ", elapsed, "ms");
break;
}
Thread.sleep(200);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -81,21 +81,19 @@ public class MockHttpServletRequest implements HttpServletRequest {
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 servletPath
* @param executorService
*/
public MockHttpServletRequest(String contextPath, String servletPath, ExecutorService executorService) {
public MockHttpServletRequest(
String contextPath,
String servletPath) {
this.contextPath = contextPath;
this.servletPath = servletPath;
this.executorService = executorService;
}

/** Sets the auth type that will be reported by this request. */
Expand Down Expand Up @@ -475,7 +473,7 @@ public AsyncContext startAsync() throws IllegalStateException {

public AsyncContext startAsync(ServletRequest servletRequest, ServletResponse servletResponse) throws IllegalStateException {
if (asyncContext == null) {
asyncContext = new MockAsyncContext(servletRequest, servletResponse, executorService);
asyncContext = new MockAsyncContext(servletRequest, servletResponse);
}
return asyncContext;
}
Expand All @@ -488,7 +486,7 @@ public boolean isAsyncSupported() {
return true;
}

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ else if (parts.length == 2) {
}

this.context = context;
this.request = new MockHttpServletRequest("/" + context.getServletContextName(), path, context.getExecutorService());
this.request = new MockHttpServletRequest("/" + context.getServletContextName(), path);
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 @@ -52,7 +52,6 @@ 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 @@ -69,10 +68,6 @@ 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 @@ -290,6 +285,12 @@ public void acceptRequest(MockHttpServletRequest request, MockHttpServletRespons
chain.setServlet(this.servlet);
chain.addFilters(this.filters);
chain.doFilter(request, response);
// wait for any async context to finish (block)
if (request.isAsyncStarted()) {
MockAsyncContext asyncContext = request.getAsyncContext();
asyncContext.waitForCompletion();
}

}

/**
Expand Down Expand Up @@ -323,14 +324,6 @@ 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", null);
MockHttpServletRequest request = new MockHttpServletRequest("/context", "/whatever");
request.addLocale(Locale.US);
return request;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,44 @@ public void testSuccess() throws Exception {
@Test
public void testTimeout() throws Exception {
MockRoundtrip trip = new MockRoundtrip(getMockServletContext(), AsyncActionBean.class);
trip.execute("doAsyncTimeout");
boolean caught = false;
try {
trip.execute("doAsyncTimeout");
} catch(Exception e) {
caught = true;
}
assertTrue(caught);
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);
}

@Test
public void testRegularException() throws Exception {
boolean caught = false;
try {
MockRoundtrip trip = new MockRoundtrip(getMockServletContext(), AsyncActionBean.class);
trip.execute("doRegularException");
} catch(Exception e) {
caught = true;
}
assertTrue(caught);
}

@Test
public void testAsyncException() throws Exception {
boolean caught = false;
try {
MockRoundtrip trip = new MockRoundtrip(getMockServletContext(), AsyncActionBean.class);
trip.execute("doAsyncException");
} catch(Exception e) {
caught = true;
}
assertTrue(caught);
}

@UrlBinding("/async")
public static class AsyncActionBean implements ActionBean {

Expand Down Expand Up @@ -68,6 +96,19 @@ protected void executeAsync() throws Exception {
};
}

public Resolution doRegularException() {
throw new RuntimeException("boom");
}

public Resolution doAsyncException() {
return new AsyncResolution() {
@Override
protected void executeAsync() throws Exception {
throw new RuntimeException("Async boom");
}
};
}

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("", "", null);
final MockHttpServletRequest request = new MockHttpServletRequest("", "");

String headerName = "User-Agent";
Object value = "Netscape/6.0";
Expand Down

0 comments on commit bb1ded3

Please sign in to comment.