/* * Copyright (c) 2012 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.eclipse.jetty.spdy; import static org.hamcrest.Matchers.greaterThan; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; import static org.junit.Assert.assertThat; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyInt; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import java.nio.ByteBuffer; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import org.eclipse.jetty.spdy.api.DataInfo; import org.eclipse.jetty.spdy.api.Handler; import org.eclipse.jetty.spdy.api.Headers; import org.eclipse.jetty.spdy.api.HeadersInfo; import org.eclipse.jetty.spdy.api.RstInfo; import org.eclipse.jetty.spdy.api.SPDY; import org.eclipse.jetty.spdy.api.Session; import org.eclipse.jetty.spdy.api.Stream; import org.eclipse.jetty.spdy.api.StreamFrameListener; import org.eclipse.jetty.spdy.api.StreamStatus; import org.eclipse.jetty.spdy.api.StringDataInfo; import org.eclipse.jetty.spdy.api.SynInfo; import org.eclipse.jetty.spdy.frames.DataFrame; import org.eclipse.jetty.spdy.frames.SynReplyFrame; import org.eclipse.jetty.spdy.frames.SynStreamFrame; import org.eclipse.jetty.spdy.generator.Generator; import org.junit.Before; import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.runners.MockitoJUnitRunner; @RunWith(MockitoJUnitRunner.class) public class StandardSessionTest { @Mock private ISession sessionMock; private ByteBufferPool bufferPool; private Executor threadPool; private StandardSession session; private Generator generator; private ScheduledExecutorService scheduler; private Headers headers; @Before public void setUp() throws Exception { bufferPool = new StandardByteBufferPool(); threadPool = Executors.newCachedThreadPool(); scheduler = Executors.newSingleThreadScheduledExecutor(); generator = new Generator(new StandardByteBufferPool(),new StandardCompressionFactory.StandardCompressor()); session = new StandardSession(SPDY.V2,bufferPool,threadPool,scheduler,new TestController(),null,1,null,generator); headers = new Headers(); } @Test public void testStreamIsRemovedFromSessionWhenReset() throws InterruptedException, ExecutionException, TimeoutException { IStream stream = createStream(); assertThatStreamIsInSession(stream); assertThat("stream is not reset",stream.isReset(),is(false)); session.rst(new RstInfo(stream.getId(),StreamStatus.STREAM_ALREADY_CLOSED)); assertThatStreamIsNotInSession(stream); assertThatStreamIsReset(stream); } @Test public void testStreamIsAddedAndRemovedFromSession() throws InterruptedException, ExecutionException, TimeoutException { IStream stream = createStream(); assertThatStreamIsInSession(stream); stream.updateCloseState(true,true); session.onControlFrame(new SynReplyFrame(SPDY.V2,SynInfo.FLAG_CLOSE,stream.getId(),null)); assertThatStreamIsClosed(stream); assertThatStreamIsNotInSession(stream); } @Test public void testStreamIsRemovedWhenHeadersWithCloseFlagAreSent() throws InterruptedException, ExecutionException, TimeoutException { IStream stream = createStream(); assertThatStreamIsInSession(stream); stream.updateCloseState(true,false); stream.headers(new HeadersInfo(headers,true)); assertThatStreamIsClosed(stream); assertThatStreamIsNotInSession(stream); } @Test public void testStreamIsUnidirectional() throws InterruptedException, ExecutionException, TimeoutException { IStream stream = createStream(); assertThat("stream is not unidirectional",stream.isUnidirectional(),not(true)); Stream pushStream = createPushStream(stream); assertThat("pushStream is unidirectional",pushStream.isUnidirectional(),is(true)); } @Test public void testPushStreamCreation() throws InterruptedException, ExecutionException, TimeoutException { Stream stream = createStream(); IStream pushStream = createPushStream(stream); assertThat("Push stream must be associated to the first stream created",pushStream.getAssociatedStream().getId(),is(stream.getId())); assertThat("streamIds need to be monotonic",pushStream.getId(),greaterThan(stream.getId())); } @Test public void testPushStreamIsNotClosedWhenAssociatedStreamIsClosed() throws InterruptedException, ExecutionException, TimeoutException { IStream stream = createStream(); Stream pushStream = createPushStream(stream); assertThatStreamIsNotHalfClosed(stream); assertThatStreamIsNotClosed(stream); assertThatPushStreamIsHalfClosed(pushStream); assertThatPushStreamIsNotClosed(pushStream); stream.updateCloseState(true,true); assertThatStreamIsHalfClosed(stream); assertThatStreamIsNotClosed(stream); assertThatPushStreamIsHalfClosed(pushStream); assertThatPushStreamIsNotClosed(pushStream); session.onControlFrame(new SynReplyFrame(SPDY.V2,SynInfo.FLAG_CLOSE,stream.getId(),null)); assertThatStreamIsClosed(stream); assertThatPushStreamIsNotClosed(pushStream); } @Test public void testCreatePushStreamOnClosedStream() throws InterruptedException, ExecutionException, TimeoutException { IStream stream = createStream(); stream.updateCloseState(true,true); assertThatStreamIsHalfClosed(stream); stream.updateCloseState(true,false); assertThatStreamIsClosed(stream); createPushStreamAndMakeSureItFails(stream); } private void createPushStreamAndMakeSureItFails(IStream stream) throws InterruptedException { final CountDownLatch failedLatch = new CountDownLatch(1); SynInfo synInfo = new SynInfo(headers,false,stream.getPriority()); stream.syn(synInfo,5,TimeUnit.SECONDS,new Handler<Stream>() { @Override public void completed(Stream context) { } @Override public void failed(Throwable x) { failedLatch.countDown(); } }); assertThat("pushStream creation failed",failedLatch.await(5,TimeUnit.SECONDS),is(true)); } @Test public void testPushStreamIsAddedAndRemovedFromParentAndSessionWhenClosed() throws InterruptedException, ExecutionException, TimeoutException { IStream stream = createStream(); IStream pushStream = createPushStream(stream); assertThatPushStreamIsHalfClosed(pushStream); assertThatPushStreamIsInSession(pushStream); assertThatStreamIsAssociatedWithPushStream(stream,pushStream); session.data(pushStream,new StringDataInfo("close",true),5,TimeUnit.SECONDS,null,null); assertThatPushStreamIsClosed(pushStream); assertThatPushStreamIsNotInSession(pushStream); assertThatStreamIsNotAssociatedWithPushStream(stream,pushStream); } @Test public void testPushStreamIsRemovedWhenReset() throws InterruptedException, ExecutionException, TimeoutException { IStream stream = createStream(); IStream pushStream = (IStream)stream.syn(new SynInfo(false)).get(); assertThatPushStreamIsInSession(pushStream); session.rst(new RstInfo(pushStream.getId(),StreamStatus.INVALID_STREAM)); assertThatPushStreamIsNotInSession(pushStream); assertThatStreamIsNotAssociatedWithPushStream(stream,pushStream); assertThatStreamIsReset(pushStream); } @Test public void testPushStreamWithSynInfoClosedTrue() throws InterruptedException, ExecutionException, TimeoutException { IStream stream = createStream(); SynInfo synInfo = new SynInfo(headers,true,stream.getPriority()); IStream pushStream = (IStream)stream.syn(synInfo).get(5,TimeUnit.SECONDS); assertThatPushStreamIsHalfClosed(pushStream); assertThatPushStreamIsClosed(pushStream); assertThatStreamIsNotAssociatedWithPushStream(stream,pushStream); assertThatStreamIsNotInSession(pushStream); } @Test public void testPushStreamSendHeadersWithCloseFlagIsRemovedFromSessionAndDisassociateFromParent() throws InterruptedException, ExecutionException, TimeoutException { IStream stream = createStream(); SynInfo synInfo = new SynInfo(headers,false,stream.getPriority()); IStream pushStream = (IStream)stream.syn(synInfo).get(5,TimeUnit.SECONDS); assertThatStreamIsAssociatedWithPushStream(stream,pushStream); assertThatPushStreamIsInSession(pushStream); pushStream.headers(new HeadersInfo(headers,true)); assertThatPushStreamIsNotInSession(pushStream); assertThatPushStreamIsHalfClosed(pushStream); assertThatPushStreamIsClosed(pushStream); assertThatStreamIsNotAssociatedWithPushStream(stream,pushStream); } @Test public void testCreatedAndClosedListenersAreCalledForNewStream() throws InterruptedException, ExecutionException, TimeoutException { final CountDownLatch createdListenerCalledLatch = new CountDownLatch(1); final CountDownLatch closedListenerCalledLatch = new CountDownLatch(1); session.addListener(new TestStreamListener(createdListenerCalledLatch,closedListenerCalledLatch)); IStream stream = createStream(); session.onDataFrame(new DataFrame(stream.getId(),SynInfo.FLAG_CLOSE,128),ByteBuffer.allocate(128)); stream.data(new StringDataInfo("close",true)); assertThat("onStreamCreated listener has been called",createdListenerCalledLatch.await(5,TimeUnit.SECONDS),is(true)); assertThatOnStreamClosedListenerHasBeenCalled(closedListenerCalledLatch); } @Test public void testListenerIsCalledForResetStream() throws InterruptedException, ExecutionException, TimeoutException { final CountDownLatch closedListenerCalledLatch = new CountDownLatch(1); session.addListener(new TestStreamListener(null,closedListenerCalledLatch)); IStream stream = createStream(); session.rst(new RstInfo(stream.getId(),StreamStatus.CANCEL_STREAM)); assertThatOnStreamClosedListenerHasBeenCalled(closedListenerCalledLatch); } @Test public void testCreatedAndClosedListenersAreCalledForNewPushStream() throws InterruptedException, ExecutionException, TimeoutException { final CountDownLatch createdListenerCalledLatch = new CountDownLatch(2); final CountDownLatch closedListenerCalledLatch = new CountDownLatch(1); session.addListener(new TestStreamListener(createdListenerCalledLatch,closedListenerCalledLatch)); IStream stream = createStream(); IStream pushStream = createPushStream(stream); session.data(pushStream,new StringDataInfo("close",true),5,TimeUnit.SECONDS,null,null); assertThat("onStreamCreated listener has been called twice. Once for the stream and once for the pushStream", createdListenerCalledLatch.await(5,TimeUnit.SECONDS),is(true)); assertThatOnStreamClosedListenerHasBeenCalled(closedListenerCalledLatch); } @Test public void testListenerIsCalledForResetPushStream() throws InterruptedException, ExecutionException, TimeoutException { final CountDownLatch closedListenerCalledLatch = new CountDownLatch(1); session.addListener(new TestStreamListener(null,closedListenerCalledLatch)); IStream stream = createStream(); IStream pushStream = createPushStream(stream); session.rst(new RstInfo(pushStream.getId(),StreamStatus.CANCEL_STREAM)); assertThatOnStreamClosedListenerHasBeenCalled(closedListenerCalledLatch); } private class TestStreamListener extends Session.StreamListener.Adapter { private CountDownLatch createdListenerCalledLatch; private CountDownLatch closedListenerCalledLatch; public TestStreamListener(CountDownLatch createdListenerCalledLatch, CountDownLatch closedListenerCalledLatch) { this.createdListenerCalledLatch = createdListenerCalledLatch; this.closedListenerCalledLatch = closedListenerCalledLatch; } @Override public void onStreamCreated(Stream stream) { if (createdListenerCalledLatch != null) createdListenerCalledLatch.countDown(); super.onStreamCreated(stream); } @Override public void onStreamClosed(Stream stream) { if (closedListenerCalledLatch != null) closedListenerCalledLatch.countDown(); super.onStreamClosed(stream); } } @SuppressWarnings("unchecked") @Test(expected = IllegalStateException.class) public void testSendDataOnHalfClosedStream() throws InterruptedException, ExecutionException, TimeoutException { SynStreamFrame synStreamFrame = new SynStreamFrame(SPDY.V2,SynInfo.FLAG_CLOSE,1,0,(byte)0,null); IStream stream = new StandardStream(synStreamFrame,sessionMock,8184,null); stream.updateCloseState(synStreamFrame.isClose(),true); assertThat("stream is half closed",stream.isHalfClosed(),is(true)); stream.data(new StringDataInfo("data on half closed stream",true)); verify(sessionMock,never()).data(any(IStream.class),any(DataInfo.class),anyInt(),any(TimeUnit.class),any(Handler.class),any(void.class)); } @Test @Ignore("In V3 we need to rst the stream if we receive data on a remotely half closed stream.") public void receiveDataOnRemotelyHalfClosedStreamResetsStreamInV3() throws InterruptedException, ExecutionException { IStream stream = (IStream)session.syn(new SynInfo(false),new StreamFrameListener.Adapter()).get(); stream.updateCloseState(true,false); assertThat("stream is half closed from remote side",stream.isHalfClosed(),is(true)); stream.process(new DataFrame(stream.getId(),(byte)0,256),ByteBuffer.allocate(256)); } @Test public void testReceiveDataOnRemotelyClosedStreamIsIgnored() throws InterruptedException, ExecutionException, TimeoutException { final CountDownLatch onDataCalledLatch = new CountDownLatch(1); Stream stream = session.syn(new SynInfo(false),new StreamFrameListener.Adapter() { @Override public void onData(Stream stream, DataInfo dataInfo) { onDataCalledLatch.countDown(); super.onData(stream,dataInfo); } }).get(5,TimeUnit.SECONDS); session.onControlFrame(new SynReplyFrame(SPDY.V2,SynInfo.FLAG_CLOSE,stream.getId(),headers)); session.onDataFrame(new DataFrame(stream.getId(),(byte)0,0),ByteBuffer.allocate(128)); assertThat("onData is never called",onDataCalledLatch.await(1,TimeUnit.SECONDS),not(true)); } private IStream createStream() throws InterruptedException, ExecutionException, TimeoutException { SynInfo synInfo = new SynInfo(headers,false,(byte)0); return (IStream)session.syn(synInfo,new StreamFrameListener.Adapter()).get(5,TimeUnit.SECONDS); } private IStream createPushStream(Stream stream) throws InterruptedException, ExecutionException, TimeoutException { SynInfo synInfo = new SynInfo(headers,false,stream.getPriority()); return (IStream)stream.syn(synInfo).get(5,TimeUnit.SECONDS); } private static class TestController implements Controller<StandardSession.FrameBytes> { @Override public int write(ByteBuffer buffer, Handler<StandardSession.FrameBytes> handler, StandardSession.FrameBytes context) { handler.completed(context); return buffer.remaining(); } @Override public void close(boolean onlyOutput) { } } private void assertThatStreamIsClosed(IStream stream) { assertThat("stream is closed",stream.isClosed(),is(true)); } private void assertThatStreamIsReset(IStream stream) { assertThat("stream is reset",stream.isReset(),is(true)); } private void assertThatStreamIsNotInSession(IStream stream) { assertThat("stream is not in session",session.getStreams().contains(stream),not(true)); } private void assertThatStreamIsInSession(IStream stream) { assertThat("stream is in session",session.getStreams().contains(stream),is(true)); } private void assertThatStreamIsNotClosed(IStream stream) { assertThat("stream is not closed",stream.isClosed(),not(true)); } private void assertThatStreamIsNotHalfClosed(IStream stream) { assertThat("stream is not halfClosed",stream.isHalfClosed(),not(true)); } private void assertThatPushStreamIsNotClosed(Stream pushStream) { assertThat("pushStream is not closed",pushStream.isClosed(),not(true)); } private void assertThatStreamIsHalfClosed(IStream stream) { assertThat("stream is halfClosed",stream.isHalfClosed(),is(true)); } private void assertThatStreamIsNotAssociatedWithPushStream(IStream stream, IStream pushStream) { assertThat("pushStream is removed from parent",stream.getPushedStreams().contains(pushStream),not(true)); } private void assertThatPushStreamIsNotInSession(Stream pushStream) { assertThat("pushStream is not in session",session.getStreams().contains(pushStream.getId()),not(true)); } private void assertThatPushStreamIsInSession(Stream pushStream) { assertThat("pushStream is in session",session.getStreams().contains(pushStream),is(true)); } private void assertThatStreamIsAssociatedWithPushStream(IStream stream, Stream pushStream) { assertThat("stream is associated with pushStream",stream.getPushedStreams().contains(pushStream),is(true)); } private void assertThatPushStreamIsClosed(Stream pushStream) { assertThat("pushStream is closed",pushStream.isClosed(),is(true)); } private void assertThatPushStreamIsHalfClosed(Stream pushStream) { assertThat("pushStream is half closed ",pushStream.isHalfClosed(),is(true)); } private void assertThatOnStreamClosedListenerHasBeenCalled(final CountDownLatch closedListenerCalledLatch) throws InterruptedException { assertThat("onStreamClosed listener has been called",closedListenerCalledLatch.await(5,TimeUnit.SECONDS),is(true)); } }