diff options
author | Greg Wilkins | 2015-02-25 04:26:39 +0000 |
---|---|---|
committer | Greg Wilkins | 2015-02-25 04:26:51 +0000 |
commit | 0ee278902d381eda054c3a029ccf762c8f9a115d (patch) | |
tree | fd50d2f951a5fc9c83ed6df3b1c0116818a57a61 | |
parent | 9756ad9fad63f46a87b04bf32c0a0e8bdca20607 (diff) | |
download | org.eclipse.jetty.project-0ee278902d381eda054c3a029ccf762c8f9a115d.tar.gz org.eclipse.jetty.project-0ee278902d381eda054c3a029ccf762c8f9a115d.tar.xz org.eclipse.jetty.project-0ee278902d381eda054c3a029ccf762c8f9a115d.zip |
Added a HttpInput integration test
-rw-r--r-- | tests/test-integration/src/test/java/org/eclipse/jetty/test/HttpInputIntegrationTest.java | 548 |
1 files changed, 548 insertions, 0 deletions
diff --git a/tests/test-integration/src/test/java/org/eclipse/jetty/test/HttpInputIntegrationTest.java b/tests/test-integration/src/test/java/org/eclipse/jetty/test/HttpInputIntegrationTest.java new file mode 100644 index 0000000000..1b5c1e9743 --- /dev/null +++ b/tests/test-integration/src/test/java/org/eclipse/jetty/test/HttpInputIntegrationTest.java @@ -0,0 +1,548 @@ +package org.eclipse.jetty.test; + +import static org.hamcrest.Matchers.startsWith; +import static org.junit.Assert.assertThat; + +import java.io.IOException; +import java.io.OutputStream; +import java.net.Socket; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import javax.servlet.AsyncContext; +import javax.servlet.ReadListener; +import javax.servlet.ServletException; +import javax.servlet.ServletInputStream; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.eclipse.jetty.http2.server.HTTP2CServerConnectionFactory; +import org.eclipse.jetty.server.HttpChannelState; +import org.eclipse.jetty.server.HttpConfiguration; +import org.eclipse.jetty.server.HttpConnectionFactory; +import org.eclipse.jetty.server.LocalConnector; +import org.eclipse.jetty.server.LocalConnector.LocalEndPoint; +import org.eclipse.jetty.server.NetworkConnector; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.servlet.ServletContextHandler; +import org.eclipse.jetty.servlet.ServletHolder; +import org.eclipse.jetty.toolchain.test.annotation.Slow; +import org.eclipse.jetty.util.IO; +import org.hamcrest.Matchers; +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +@RunWith(Parameterized.class) +public class HttpInputIntegrationTest +{ + + enum Mode { BLOCKING, ASYNC_DISPATCHED, ASYNC_OTHER_DISPATCHED, ASYNC_OTHER_WAIT } + public final static String EOF = "__EOF__"; + public final static String DELAY = "__DELAY__"; + public final static String ABORT = "__ABORT__"; + + private static Server __server; + private static HttpConfiguration __config; + + @BeforeClass + public static void beforeClass() throws Exception + { + __config = new HttpConfiguration(); + + __server = new Server(); + __server.addConnector(new LocalConnector(__server,new HttpConnectionFactory(__config))); + __server.addConnector(new ServerConnector(__server,new HttpConnectionFactory(__config),new HTTP2CServerConnectionFactory(__config))); + + ServletContextHandler context = new ServletContextHandler(__server,"/ctx"); + ServletHolder holder = new ServletHolder(new TestServlet()); + holder.setAsyncSupported(true); + context.addServlet(holder,"/*"); + + __server.start(); + } + + @AfterClass + public static void afterClass() throws Exception + { + __server.stop(); + } + + interface TestClient + { + /* ------------------------------------------------------------ */ + /** + * @param uri The URI to test, typically /ctx/test?mode=THE_MODE + * @param delayInFrame If null, send the request with no delays, if FALSE then send with delays between frames, if TRUE send with delays within frames + * @param contentLength The content length header to send. + * @param content The content to send, with each string to be converted to a chunk or a frame + * @return The response received in HTTP/1 format + * @throws Exception + */ + String send(String uri,Boolean delayInFrame, int contentLength, List<String> content) throws Exception; + } + + @Parameterized.Parameters + public static Collection<Object[]> data() + { + List<Object[]> tests = new ArrayList<>(); + + + // TODO other client types! + // test with the following clients/protocols: + // + Local + // + HTTP/1 + // + SSL + HTTP/1 + // + HTTP/2 + // + SSL + HTTP/2 + // + FASTCGI + for (String c : new String[]{"LOCAL","HTTP/1"}) + { + TestClient client; + switch(c) + { + case "LOCAL": + client=new LocalClient(); + break; + case "HTTP/1": + client=new H1Client(); + break; + default: + throw new IllegalStateException(); + } + + // test async actions that are run: + // + By a thread in a container callback + // + By another thread while a container callback is active + // + By another thread while no container callback is active + for (Mode mode: Mode.values()) + { + + // test servlet dispatch with: + // + Delayed dispatch on + // + Delayed dispatch off + for (Boolean dispatch : new Boolean[]{false,true}) + { + // test send with + // + No delays between frames + // + Delays between frames + // + Delays within frames! + for (Boolean delayWithinFrame : new Boolean[]{null,false,true}) + { + // test content + // + unknown length + EOF + // + unknown length + content + EOF + // + unknown length + content + content + EOF + + // + known length + EOF + // + known length + content + EOF + // + known length + content + content + EOF + + tests.add(new Object[]{client,mode,dispatch,delayWithinFrame,200,0,-1,new String[]{}}); + tests.add(new Object[]{client,mode,dispatch,delayWithinFrame,200,8,-1,new String[]{"content0"}}); + tests.add(new Object[]{client,mode,dispatch,delayWithinFrame,200,16,-1,new String[]{"content0","CONTENT1"}}); + + tests.add(new Object[]{client,mode,dispatch,delayWithinFrame,200,0,0,new String[]{}}); + tests.add(new Object[]{client,mode,dispatch,delayWithinFrame,200,8,8,new String[]{"content0"}}); + tests.add(new Object[]{client,mode,dispatch,delayWithinFrame,200,16,16,new String[]{"content0","CONTENT1"}}); + + } + } + } + } + return tests; + } + + + final TestClient _client; + final Mode _mode; + final Boolean _delay; + final int _status; + final int _read; + final int _length; + final List<String> _send; + + public HttpInputIntegrationTest(TestClient client, Mode mode,boolean dispatch,Boolean delay,int status,int read,int length,String... send) + { + _client=client; + _mode=mode; + __config.setDelayDispatchUntilContent(dispatch); + _delay=delay; + _status=status; + _read=read; + _length=length; + _send = Arrays.asList(send); + } + + + private static void runmode(Mode mode,final Request request, final Runnable test) + { + switch(mode) + { + case ASYNC_DISPATCHED: + { + test.run(); + break; + } + case ASYNC_OTHER_DISPATCHED: + { + final CountDownLatch latch = new CountDownLatch(1); + new Thread() + { + @Override + public void run() + { + try + { + test.run(); + } + finally + { + latch.countDown(); + } + } + }.start(); + // prevent caller returning until other thread complete + try + { + if (!latch.await(5,TimeUnit.SECONDS)) + Assert.fail(); + } + catch(Exception e) + { + Assert.fail(); + } + break; + } + case ASYNC_OTHER_WAIT: + { + final CountDownLatch latch = new CountDownLatch(1); + final HttpChannelState.State S=request.getHttpChannelState().getState(); + new Thread() + { + @Override + public void run() + { + try + { + if (!latch.await(5,TimeUnit.SECONDS)) + Assert.fail(); + + // Spin until state change + HttpChannelState.State s=request.getHttpChannelState().getState(); + while(request.getHttpChannelState().getState()==S ) + { + Thread.yield(); + s=request.getHttpChannelState().getState(); + } + test.run(); + } + catch (Exception e) + { + e.printStackTrace(); + } + } + }.start(); + // ensure other thread running before trying to return + latch.countDown(); + break; + } + + default: + throw new IllegalStateException(); + } + + } + + + @Test + @Slow + public void test() throws Exception + { + System.err.printf("TEST c=%s, m=%s, d=%b D=%s content-length:%d expect=%d read=%d content:%s%n",_client.getClass().getSimpleName(),_mode,__config.isDelayDispatchUntilContent(),_delay,_length,_status,_read,_send); + + String response = _client.send("/ctx/test?mode="+_mode,_delay,_length,_send); + + assertThat(response,startsWith("HTTP")); + assertThat(response,Matchers.containsString(" "+_status+" ")); + assertThat(response,Matchers.containsString("read="+_read)); + } + + + + public static class TestServlet extends HttpServlet + { + + @Override + protected void doGet(final HttpServletRequest req, final HttpServletResponse resp) throws ServletException, IOException + { + final Mode mode = Mode.valueOf(req.getParameter("mode")); + resp.setContentType("text/plain"); + + if (mode==Mode.BLOCKING) + { + try + { + String content = IO.toString(req.getInputStream()); + resp.setStatus(200); + resp.setContentType("text/plain"); + resp.getWriter().println("read="+content.length()); + } + catch(Exception e) + { + e.printStackTrace(); + resp.setStatus(500); + resp.getWriter().println("read="+e); + } + } + else + { + // we are asynchronous + final AsyncContext context = req.startAsync(); + context.setTimeout(10000); + final ServletInputStream in = req.getInputStream(); + final Request request = Request.getBaseRequest(req); + final AtomicInteger read = new AtomicInteger(0); + + runmode(mode,request,new Runnable() + { + @Override + public void run() + { + in.setReadListener(new ReadListener() + { + @Override + public void onError(Throwable t) + { + t.printStackTrace(); + try + { + resp.sendError(500); + } + catch (IOException e) + { + e.printStackTrace(); + throw new RuntimeException(e); + } + context.complete(); + } + + @Override + public void onDataAvailable() throws IOException + { + runmode(mode,request,new Runnable() + { + @Override + public void run() + { + while(in.isReady() && !in.isFinished()) + { + try + { + int b = in.read(); + if (b<0) + return; + read.incrementAndGet(); + } + catch (IOException e) + { + onError(e); + } + } + } + }); + } + + @Override + public void onAllDataRead() throws IOException + { + resp.setStatus(200); + resp.setContentType("text/plain"); + resp.getWriter().println("read="+read.get()); + context.complete(); + } + }); + } + }); + } + } + } + + + public static class H1Client implements TestClient + { + + @Override + public String send(String uri, Boolean delayInFrame,int contentLength, List<String> content) throws Exception + { + int port=((NetworkConnector)__server.getConnectors()[1]).getLocalPort(); + + try (Socket client = new Socket("localhost", port)) + { + client.setSoTimeout(5000); + OutputStream out = client.getOutputStream(); + + StringBuilder buffer = new StringBuilder(); + buffer.append("GET ").append(uri).append(" HTTP/1.1\r\n"); + buffer.append("Host: localhost:").append(port).append("\r\n"); + buffer.append("Connection: close\r\n"); + + flush(out,buffer,delayInFrame,true); + + boolean chunked=contentLength<0; + if (chunked) + buffer.append("Transfer-Encoding: chunked\r\n"); + else + buffer.append("Content-Length: ").append(contentLength).append("\r\n"); + + if (contentLength>0) + buffer.append("Content-Type: text/plain\r\n"); + buffer.append("\r\n"); + + flush(out,buffer,delayInFrame,false); + + for (String c : content) + { + if (chunked) + { + buffer.append("\r\n").append(Integer.toHexString(c.length())).append("\r\n"); + flush(out,buffer,delayInFrame,true); + } + + buffer.append(c.substring(0,1)); + flush(out,buffer,delayInFrame,true); + buffer.append(c.substring(1)); + flush(out,buffer,delayInFrame,false); + } + + if (chunked) + { + buffer.append("\r\n0"); + flush(out,buffer,delayInFrame,true); + buffer.append("\r\n\r\n"); + } + + flush(out,buffer); + + return IO.toString(client.getInputStream()); + } + + } + + private void flush(OutputStream out, StringBuilder buffer, Boolean delayInFrame, boolean inFrame) throws Exception + { + // Flush now if we should delay + if (delayInFrame!=null && delayInFrame.equals(inFrame)) + { + flush(out,buffer); + } + } + + private void flush(OutputStream out, StringBuilder buffer) throws Exception + { + String flush=buffer.toString(); + buffer.setLength(0); + out.write(flush.getBytes(StandardCharsets.ISO_8859_1)); + Thread.sleep(50); + } + + } + + public static class LocalClient implements TestClient + { + + @Override + public String send(String uri, Boolean delayInFrame,int contentLength, List<String> content) throws Exception + { + LocalConnector connector = __server.getBean(LocalConnector.class); + + StringBuilder buffer = new StringBuilder(); + buffer.append("GET ").append(uri).append(" HTTP/1.1\r\n"); + buffer.append("Host: localhost\r\n"); + buffer.append("Connection: close\r\n"); + + LocalEndPoint local = connector.executeRequest(""); + + flush(local,buffer,delayInFrame,true); + + boolean chunked=contentLength<0; + if (chunked) + buffer.append("Transfer-Encoding: chunked\r\n"); + else + buffer.append("Content-Length: ").append(contentLength).append("\r\n"); + + if (contentLength>0) + buffer.append("Content-Type: text/plain\r\n"); + buffer.append("\r\n"); + + flush(local,buffer,delayInFrame,false); + + for (String c : content) + { + if (chunked) + { + buffer.append("\r\n").append(Integer.toHexString(c.length())).append("\r\n"); + flush(local,buffer,delayInFrame,true); + } + + buffer.append(c.substring(0,1)); + flush(local,buffer,delayInFrame,true); + buffer.append(c.substring(1)); + if (chunked) + buffer.append("\r\n"); + flush(local,buffer,delayInFrame,false); + } + + if (chunked) + { + buffer.append("0"); + flush(local,buffer,delayInFrame,true); + buffer.append("\r\n\r\n"); + } + + flush(local,buffer); + local.waitUntilClosed(); + return local.takeOutputString(); + } + + + private void flush(LocalEndPoint local, StringBuilder buffer, Boolean delayInFrame, boolean inFrame) throws Exception + { + // Flush now if we should delay + if (delayInFrame!=null && delayInFrame.equals(inFrame)) + { + flush(local,buffer); + } + } + + private void flush(final LocalEndPoint local, StringBuilder buffer) throws Exception + { + final String flush=buffer.toString(); + buffer.setLength(0); + // System.err.println("FLUSH:'"+flush+"'"); + new Thread() + { + @Override + public void run() + { + local.addInput(flush); + } + }.start(); + Thread.sleep(50); + } + + } + +} |