Motivation: We should convert Http2Exceptions that are produced because of STREAM_CLOSED to ClosedChannelException when hand-over to the child channel to make it more consistent with other transports. Modifications: - Check if STREAM_CLOSED is used and if so create a new ClosedChannelException (while preserve the original exception as cause) and use it in the child channel - Ensure STREAM_CLOSED is used in DefaultHttp2RemoteFlowController when writes are failed because of a closed stream. - Add testcase Result: More consistent and correct exception usage.
1117 lines
45 KiB
Java
1117 lines
45 KiB
Java
/*
|
|
* Copyright 2014 The Netty Project
|
|
*
|
|
* The Netty Project licenses this file to you 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 io.netty.handler.codec.http2;
|
|
|
|
import io.netty.channel.Channel;
|
|
import io.netty.channel.ChannelConfig;
|
|
import io.netty.channel.ChannelHandlerContext;
|
|
import io.netty.channel.ChannelPromise;
|
|
import io.netty.util.concurrent.EventExecutor;
|
|
import junit.framework.AssertionFailedError;
|
|
import org.junit.Before;
|
|
import org.junit.Test;
|
|
import org.mockito.Mock;
|
|
import org.mockito.MockitoAnnotations;
|
|
import org.mockito.invocation.InvocationOnMock;
|
|
import org.mockito.stubbing.Answer;
|
|
|
|
import java.util.concurrent.atomic.AtomicInteger;
|
|
|
|
import static io.netty.handler.codec.http2.Http2CodecUtil.CONNECTION_STREAM_ID;
|
|
import static io.netty.handler.codec.http2.Http2CodecUtil.DEFAULT_PRIORITY_WEIGHT;
|
|
import static io.netty.handler.codec.http2.Http2CodecUtil.DEFAULT_WINDOW_SIZE;
|
|
import static io.netty.handler.codec.http2.Http2CodecUtil.MAX_WEIGHT;
|
|
import static io.netty.handler.codec.http2.Http2CodecUtil.MIN_WEIGHT;
|
|
import static org.junit.Assert.assertEquals;
|
|
import static org.junit.Assert.assertFalse;
|
|
import static org.junit.Assert.assertNotNull;
|
|
import static org.junit.Assert.assertSame;
|
|
import static org.junit.Assert.assertTrue;
|
|
import static org.junit.Assert.fail;
|
|
import static org.mockito.Mockito.any;
|
|
import static org.mockito.Mockito.anyInt;
|
|
import static org.mockito.Mockito.atLeastOnce;
|
|
import static org.mockito.Mockito.doAnswer;
|
|
import static org.mockito.Mockito.doThrow;
|
|
import static org.mockito.Mockito.mock;
|
|
import static org.mockito.Mockito.never;
|
|
import static org.mockito.Mockito.reset;
|
|
import static org.mockito.Mockito.times;
|
|
import static org.mockito.Mockito.verify;
|
|
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
|
import static org.mockito.Mockito.verifyZeroInteractions;
|
|
import static org.mockito.Mockito.when;
|
|
|
|
/**
|
|
* Tests for {@link DefaultHttp2RemoteFlowController}.
|
|
*/
|
|
public abstract class DefaultHttp2RemoteFlowControllerTest {
|
|
private static final int STREAM_A = 1;
|
|
private static final int STREAM_B = 3;
|
|
private static final int STREAM_C = 5;
|
|
private static final int STREAM_D = 7;
|
|
|
|
private DefaultHttp2RemoteFlowController controller;
|
|
|
|
@Mock
|
|
private ChannelHandlerContext ctx;
|
|
|
|
@Mock
|
|
private Channel channel;
|
|
|
|
@Mock
|
|
private ChannelConfig config;
|
|
|
|
@Mock
|
|
private EventExecutor executor;
|
|
|
|
@Mock
|
|
private ChannelPromise promise;
|
|
|
|
@Mock
|
|
private Http2RemoteFlowController.Listener listener;
|
|
|
|
private DefaultHttp2Connection connection;
|
|
|
|
@Before
|
|
public void setup() throws Http2Exception {
|
|
MockitoAnnotations.initMocks(this);
|
|
|
|
when(ctx.newPromise()).thenReturn(promise);
|
|
when(ctx.flush()).thenThrow(new AssertionFailedError("forbidden"));
|
|
setChannelWritability(true);
|
|
when(channel.config()).thenReturn(config);
|
|
when(executor.inEventLoop()).thenReturn(true);
|
|
|
|
initConnectionAndController();
|
|
|
|
resetCtx();
|
|
// This is intentionally left out of initConnectionAndController so it can be tested below.
|
|
controller.channelHandlerContext(ctx);
|
|
assertWritabilityChanged(1, true);
|
|
reset(listener);
|
|
}
|
|
|
|
protected abstract StreamByteDistributor newDistributor(Http2Connection connection);
|
|
|
|
private void initConnectionAndController() throws Http2Exception {
|
|
connection = new DefaultHttp2Connection(false);
|
|
controller = new DefaultHttp2RemoteFlowController(connection, newDistributor(connection), listener);
|
|
connection.remote().flowController(controller);
|
|
|
|
connection.local().createStream(STREAM_A, false);
|
|
connection.local().createStream(STREAM_B, false);
|
|
Http2Stream streamC = connection.local().createStream(STREAM_C, false);
|
|
Http2Stream streamD = connection.local().createStream(STREAM_D, false);
|
|
controller.updateDependencyTree(streamC.id(), STREAM_A, DEFAULT_PRIORITY_WEIGHT, false);
|
|
controller.updateDependencyTree(streamD.id(), STREAM_A, DEFAULT_PRIORITY_WEIGHT, false);
|
|
}
|
|
|
|
@Test
|
|
public void initialWindowSizeShouldOnlyChangeStreams() throws Http2Exception {
|
|
controller.initialWindowSize(0);
|
|
assertEquals(DEFAULT_WINDOW_SIZE, window(CONNECTION_STREAM_ID));
|
|
assertEquals(0, window(STREAM_A));
|
|
assertEquals(0, window(STREAM_B));
|
|
assertEquals(0, window(STREAM_C));
|
|
assertEquals(0, window(STREAM_D));
|
|
assertWritabilityChanged(1, false);
|
|
}
|
|
|
|
@Test
|
|
public void windowUpdateShouldChangeConnectionWindow() throws Http2Exception {
|
|
incrementWindowSize(CONNECTION_STREAM_ID, 100);
|
|
assertEquals(DEFAULT_WINDOW_SIZE + 100, window(CONNECTION_STREAM_ID));
|
|
assertEquals(DEFAULT_WINDOW_SIZE, window(STREAM_A));
|
|
assertEquals(DEFAULT_WINDOW_SIZE, window(STREAM_B));
|
|
assertEquals(DEFAULT_WINDOW_SIZE, window(STREAM_C));
|
|
assertEquals(DEFAULT_WINDOW_SIZE, window(STREAM_D));
|
|
verifyZeroInteractions(listener);
|
|
}
|
|
|
|
@Test
|
|
public void windowUpdateShouldChangeStreamWindow() throws Http2Exception {
|
|
incrementWindowSize(STREAM_A, 100);
|
|
assertEquals(DEFAULT_WINDOW_SIZE, window(CONNECTION_STREAM_ID));
|
|
assertEquals(DEFAULT_WINDOW_SIZE + 100, window(STREAM_A));
|
|
assertEquals(DEFAULT_WINDOW_SIZE, window(STREAM_B));
|
|
assertEquals(DEFAULT_WINDOW_SIZE, window(STREAM_C));
|
|
assertEquals(DEFAULT_WINDOW_SIZE, window(STREAM_D));
|
|
verifyZeroInteractions(listener);
|
|
}
|
|
|
|
@Test
|
|
public void payloadSmallerThanWindowShouldBeWrittenImmediately() throws Http2Exception {
|
|
FakeFlowControlled data = new FakeFlowControlled(5);
|
|
sendData(STREAM_A, data);
|
|
data.assertNotWritten();
|
|
verifyZeroInteractions(listener);
|
|
controller.writePendingBytes();
|
|
data.assertFullyWritten();
|
|
verifyZeroInteractions(listener);
|
|
}
|
|
|
|
@Test
|
|
public void emptyPayloadShouldBeWrittenImmediately() throws Http2Exception {
|
|
FakeFlowControlled data = new FakeFlowControlled(0);
|
|
sendData(STREAM_A, data);
|
|
data.assertNotWritten();
|
|
controller.writePendingBytes();
|
|
data.assertFullyWritten();
|
|
verifyZeroInteractions(listener);
|
|
}
|
|
|
|
@Test
|
|
public void unflushedPayloadsShouldBeDroppedOnCancel() throws Http2Exception {
|
|
FakeFlowControlled data = new FakeFlowControlled(5);
|
|
Http2Stream streamA = stream(STREAM_A);
|
|
sendData(STREAM_A, data);
|
|
streamA.close();
|
|
controller.writePendingBytes();
|
|
data.assertNotWritten();
|
|
controller.writePendingBytes();
|
|
data.assertNotWritten();
|
|
verify(listener, times(1)).writabilityChanged(streamA);
|
|
assertFalse(controller.isWritable(streamA));
|
|
}
|
|
|
|
@Test
|
|
public void payloadsShouldMerge() throws Http2Exception {
|
|
controller.initialWindowSize(15);
|
|
FakeFlowControlled data1 = new FakeFlowControlled(5, true);
|
|
FakeFlowControlled data2 = new FakeFlowControlled(10, true);
|
|
sendData(STREAM_A, data1);
|
|
sendData(STREAM_A, data2);
|
|
data1.assertNotWritten();
|
|
data1.assertNotWritten();
|
|
data2.assertMerged();
|
|
controller.writePendingBytes();
|
|
data1.assertFullyWritten();
|
|
data2.assertNotWritten();
|
|
verify(listener, times(1)).writabilityChanged(stream(STREAM_A));
|
|
assertFalse(controller.isWritable(stream(STREAM_A)));
|
|
}
|
|
|
|
@Test
|
|
public void flowControllerCorrectlyAccountsForBytesWithMerge() throws Http2Exception {
|
|
controller.initialWindowSize(112); // This must be more than the total merged frame size 110
|
|
FakeFlowControlled data1 = new FakeFlowControlled(5, 2, true);
|
|
FakeFlowControlled data2 = new FakeFlowControlled(5, 100, true);
|
|
sendData(STREAM_A, data1);
|
|
sendData(STREAM_A, data2);
|
|
data1.assertNotWritten();
|
|
data1.assertNotWritten();
|
|
data2.assertMerged();
|
|
controller.writePendingBytes();
|
|
data1.assertFullyWritten();
|
|
data2.assertNotWritten();
|
|
verify(listener, never()).writabilityChanged(stream(STREAM_A));
|
|
assertTrue(controller.isWritable(stream(STREAM_A)));
|
|
}
|
|
|
|
@Test
|
|
public void stalledStreamShouldQueuePayloads() throws Http2Exception {
|
|
controller.initialWindowSize(0);
|
|
verify(listener, times(1)).writabilityChanged(stream(STREAM_A));
|
|
assertFalse(controller.isWritable(stream(STREAM_A)));
|
|
reset(listener);
|
|
|
|
FakeFlowControlled data = new FakeFlowControlled(15);
|
|
FakeFlowControlled moreData = new FakeFlowControlled(0);
|
|
sendData(STREAM_A, data);
|
|
controller.writePendingBytes();
|
|
data.assertNotWritten();
|
|
sendData(STREAM_A, moreData);
|
|
controller.writePendingBytes();
|
|
moreData.assertNotWritten();
|
|
verifyZeroInteractions(listener);
|
|
}
|
|
|
|
@Test
|
|
public void queuedPayloadsReceiveErrorOnStreamClose() throws Http2Exception {
|
|
controller.initialWindowSize(0);
|
|
verify(listener, times(1)).writabilityChanged(stream(STREAM_A));
|
|
assertFalse(controller.isWritable(stream(STREAM_A)));
|
|
reset(listener);
|
|
|
|
FakeFlowControlled data = new FakeFlowControlled(15);
|
|
FakeFlowControlled moreData = new FakeFlowControlled(0);
|
|
sendData(STREAM_A, data);
|
|
controller.writePendingBytes();
|
|
data.assertNotWritten();
|
|
sendData(STREAM_A, moreData);
|
|
controller.writePendingBytes();
|
|
moreData.assertNotWritten();
|
|
|
|
connection.stream(STREAM_A).close();
|
|
data.assertError(Http2Error.STREAM_CLOSED);
|
|
moreData.assertError(Http2Error.STREAM_CLOSED);
|
|
verifyZeroInteractions(listener);
|
|
}
|
|
|
|
@Test
|
|
public void payloadLargerThanWindowShouldWritePartial() throws Http2Exception {
|
|
controller.initialWindowSize(5);
|
|
verify(listener, never()).writabilityChanged(stream(STREAM_A));
|
|
assertTrue(controller.isWritable(stream(STREAM_A)));
|
|
reset(listener);
|
|
|
|
final FakeFlowControlled data = new FakeFlowControlled(10);
|
|
sendData(STREAM_A, data);
|
|
controller.writePendingBytes();
|
|
// Verify that a partial frame of 5 remains to be sent
|
|
data.assertPartiallyWritten(5);
|
|
verify(listener, times(1)).writabilityChanged(stream(STREAM_A));
|
|
assertFalse(controller.isWritable(stream(STREAM_A)));
|
|
verifyNoMoreInteractions(listener);
|
|
}
|
|
|
|
@Test
|
|
public void windowUpdateAndFlushShouldTriggerWrite() throws Http2Exception {
|
|
controller.initialWindowSize(10);
|
|
verify(listener, never()).writabilityChanged(stream(STREAM_A));
|
|
assertTrue(controller.isWritable(stream(STREAM_A)));
|
|
|
|
FakeFlowControlled data = new FakeFlowControlled(20);
|
|
FakeFlowControlled moreData = new FakeFlowControlled(10);
|
|
sendData(STREAM_A, data);
|
|
sendData(STREAM_A, moreData);
|
|
controller.writePendingBytes();
|
|
data.assertPartiallyWritten(10);
|
|
moreData.assertNotWritten();
|
|
verify(listener, times(1)).writabilityChanged(stream(STREAM_A));
|
|
assertFalse(controller.isWritable(stream(STREAM_A)));
|
|
reset(listener);
|
|
resetCtx();
|
|
|
|
// Update the window and verify that the rest of data and some of moreData are written
|
|
incrementWindowSize(STREAM_A, 15);
|
|
verify(listener, never()).writabilityChanged(stream(STREAM_A));
|
|
assertFalse(controller.isWritable(stream(STREAM_A)));
|
|
reset(listener);
|
|
|
|
controller.writePendingBytes();
|
|
|
|
data.assertFullyWritten();
|
|
moreData.assertPartiallyWritten(5);
|
|
verify(listener, never()).writabilityChanged(stream(STREAM_A));
|
|
assertFalse(controller.isWritable(stream(STREAM_A)));
|
|
|
|
assertEquals(DEFAULT_WINDOW_SIZE - 25, window(CONNECTION_STREAM_ID));
|
|
assertEquals(0, window(STREAM_A));
|
|
assertEquals(10, window(STREAM_B));
|
|
assertEquals(10, window(STREAM_C));
|
|
assertEquals(10, window(STREAM_D));
|
|
}
|
|
|
|
@Test
|
|
public void initialWindowUpdateShouldSendPayload() throws Http2Exception {
|
|
incrementWindowSize(CONNECTION_STREAM_ID, -window(CONNECTION_STREAM_ID) + 10);
|
|
assertWritabilityChanged(0, true);
|
|
reset(listener);
|
|
|
|
controller.initialWindowSize(0);
|
|
assertWritabilityChanged(1, false);
|
|
reset(listener);
|
|
|
|
FakeFlowControlled data = new FakeFlowControlled(10);
|
|
sendData(STREAM_A, data);
|
|
controller.writePendingBytes();
|
|
data.assertNotWritten();
|
|
|
|
// Verify that the entire frame was sent.
|
|
controller.initialWindowSize(10);
|
|
data.assertFullyWritten();
|
|
assertWritabilityChanged(0, false);
|
|
}
|
|
|
|
@Test
|
|
public void successiveSendsShouldNotInteract() throws Http2Exception {
|
|
// Collapse the connection window to force queueing.
|
|
incrementWindowSize(CONNECTION_STREAM_ID, -window(CONNECTION_STREAM_ID));
|
|
assertEquals(0, window(CONNECTION_STREAM_ID));
|
|
assertWritabilityChanged(1, false);
|
|
reset(listener);
|
|
|
|
FakeFlowControlled dataA = new FakeFlowControlled(10);
|
|
// Queue data for stream A and allow most of it to be written.
|
|
sendData(STREAM_A, dataA);
|
|
controller.writePendingBytes();
|
|
dataA.assertNotWritten();
|
|
incrementWindowSize(CONNECTION_STREAM_ID, 8);
|
|
assertWritabilityChanged(0, false);
|
|
reset(listener);
|
|
|
|
controller.writePendingBytes();
|
|
dataA.assertPartiallyWritten(8);
|
|
assertEquals(65527, window(STREAM_A));
|
|
assertEquals(0, window(CONNECTION_STREAM_ID));
|
|
assertWritabilityChanged(0, false);
|
|
reset(listener);
|
|
|
|
// Queue data for stream B and allow the rest of A and all of B to be written.
|
|
FakeFlowControlled dataB = new FakeFlowControlled(10);
|
|
sendData(STREAM_B, dataB);
|
|
controller.writePendingBytes();
|
|
dataB.assertNotWritten();
|
|
incrementWindowSize(CONNECTION_STREAM_ID, 12);
|
|
assertWritabilityChanged(0, false);
|
|
reset(listener);
|
|
|
|
controller.writePendingBytes();
|
|
assertEquals(0, window(CONNECTION_STREAM_ID));
|
|
assertWritabilityChanged(0, false);
|
|
|
|
// Verify the rest of A is written.
|
|
dataA.assertFullyWritten();
|
|
assertEquals(65525, window(STREAM_A));
|
|
|
|
dataB.assertFullyWritten();
|
|
assertEquals(65525, window(STREAM_B));
|
|
verifyNoMoreInteractions(listener);
|
|
}
|
|
|
|
@Test
|
|
public void negativeWindowShouldNotThrowException() throws Http2Exception {
|
|
final int initWindow = 20;
|
|
final int secondWindowSize = 10;
|
|
controller.initialWindowSize(initWindow);
|
|
assertWritabilityChanged(0, true);
|
|
reset(listener);
|
|
|
|
FakeFlowControlled data1 = new FakeFlowControlled(initWindow);
|
|
FakeFlowControlled data2 = new FakeFlowControlled(5);
|
|
|
|
// Deplete the stream A window to 0
|
|
sendData(STREAM_A, data1);
|
|
controller.writePendingBytes();
|
|
data1.assertFullyWritten();
|
|
assertTrue(window(CONNECTION_STREAM_ID) > 0);
|
|
verify(listener, times(1)).writabilityChanged(stream(STREAM_A));
|
|
verify(listener, never()).writabilityChanged(stream(STREAM_B));
|
|
verify(listener, never()).writabilityChanged(stream(STREAM_C));
|
|
verify(listener, never()).writabilityChanged(stream(STREAM_D));
|
|
assertFalse(controller.isWritable(stream(STREAM_A)));
|
|
assertTrue(controller.isWritable(stream(STREAM_B)));
|
|
assertTrue(controller.isWritable(stream(STREAM_C)));
|
|
assertTrue(controller.isWritable(stream(STREAM_D)));
|
|
reset(listener);
|
|
|
|
// Make the window size for stream A negative
|
|
controller.initialWindowSize(initWindow - secondWindowSize);
|
|
assertEquals(-secondWindowSize, window(STREAM_A));
|
|
verify(listener, never()).writabilityChanged(stream(STREAM_A));
|
|
verify(listener, never()).writabilityChanged(stream(STREAM_B));
|
|
verify(listener, never()).writabilityChanged(stream(STREAM_C));
|
|
verify(listener, never()).writabilityChanged(stream(STREAM_D));
|
|
assertFalse(controller.isWritable(stream(STREAM_A)));
|
|
assertTrue(controller.isWritable(stream(STREAM_B)));
|
|
assertTrue(controller.isWritable(stream(STREAM_C)));
|
|
assertTrue(controller.isWritable(stream(STREAM_D)));
|
|
reset(listener);
|
|
|
|
// Queue up a write. It should not be written now because the window is negative
|
|
sendData(STREAM_A, data2);
|
|
controller.writePendingBytes();
|
|
data2.assertNotWritten();
|
|
verify(listener, never()).writabilityChanged(stream(STREAM_A));
|
|
verify(listener, never()).writabilityChanged(stream(STREAM_B));
|
|
verify(listener, never()).writabilityChanged(stream(STREAM_C));
|
|
verify(listener, never()).writabilityChanged(stream(STREAM_D));
|
|
assertFalse(controller.isWritable(stream(STREAM_A)));
|
|
assertTrue(controller.isWritable(stream(STREAM_B)));
|
|
assertTrue(controller.isWritable(stream(STREAM_C)));
|
|
assertTrue(controller.isWritable(stream(STREAM_D)));
|
|
reset(listener);
|
|
|
|
// Open the window size back up a bit (no send should happen)
|
|
incrementWindowSize(STREAM_A, 5);
|
|
controller.writePendingBytes();
|
|
assertEquals(-5, window(STREAM_A));
|
|
data2.assertNotWritten();
|
|
verify(listener, never()).writabilityChanged(stream(STREAM_A));
|
|
verify(listener, never()).writabilityChanged(stream(STREAM_B));
|
|
verify(listener, never()).writabilityChanged(stream(STREAM_C));
|
|
verify(listener, never()).writabilityChanged(stream(STREAM_D));
|
|
assertFalse(controller.isWritable(stream(STREAM_A)));
|
|
assertTrue(controller.isWritable(stream(STREAM_B)));
|
|
assertTrue(controller.isWritable(stream(STREAM_C)));
|
|
assertTrue(controller.isWritable(stream(STREAM_D)));
|
|
reset(listener);
|
|
|
|
// Open the window size back up a bit (no send should happen)
|
|
incrementWindowSize(STREAM_A, 5);
|
|
controller.writePendingBytes();
|
|
assertEquals(0, window(STREAM_A));
|
|
data2.assertNotWritten();
|
|
verify(listener, never()).writabilityChanged(stream(STREAM_A));
|
|
verify(listener, never()).writabilityChanged(stream(STREAM_B));
|
|
verify(listener, never()).writabilityChanged(stream(STREAM_C));
|
|
verify(listener, never()).writabilityChanged(stream(STREAM_D));
|
|
assertFalse(controller.isWritable(stream(STREAM_A)));
|
|
assertTrue(controller.isWritable(stream(STREAM_B)));
|
|
assertTrue(controller.isWritable(stream(STREAM_C)));
|
|
assertTrue(controller.isWritable(stream(STREAM_D)));
|
|
reset(listener);
|
|
|
|
// Open the window size back up and allow the write to happen
|
|
incrementWindowSize(STREAM_A, 5);
|
|
controller.writePendingBytes();
|
|
data2.assertFullyWritten();
|
|
verify(listener, never()).writabilityChanged(stream(STREAM_A));
|
|
verify(listener, never()).writabilityChanged(stream(STREAM_B));
|
|
verify(listener, never()).writabilityChanged(stream(STREAM_C));
|
|
verify(listener, never()).writabilityChanged(stream(STREAM_D));
|
|
assertFalse(controller.isWritable(stream(STREAM_A)));
|
|
assertTrue(controller.isWritable(stream(STREAM_B)));
|
|
assertTrue(controller.isWritable(stream(STREAM_C)));
|
|
assertTrue(controller.isWritable(stream(STREAM_D)));
|
|
}
|
|
|
|
@Test
|
|
public void initialWindowUpdateShouldSendEmptyFrame() throws Http2Exception {
|
|
controller.initialWindowSize(0);
|
|
assertWritabilityChanged(1, false);
|
|
reset(listener);
|
|
|
|
// First send a frame that will get buffered.
|
|
FakeFlowControlled data = new FakeFlowControlled(10, false);
|
|
sendData(STREAM_A, data);
|
|
controller.writePendingBytes();
|
|
data.assertNotWritten();
|
|
|
|
// Now send an empty frame on the same stream and verify that it's also buffered.
|
|
FakeFlowControlled data2 = new FakeFlowControlled(0, false);
|
|
sendData(STREAM_A, data2);
|
|
controller.writePendingBytes();
|
|
data2.assertNotWritten();
|
|
|
|
// Re-expand the window and verify that both frames were sent.
|
|
controller.initialWindowSize(10);
|
|
verify(listener, never()).writabilityChanged(stream(STREAM_A));
|
|
verify(listener, times(1)).writabilityChanged(stream(STREAM_B));
|
|
verify(listener, times(1)).writabilityChanged(stream(STREAM_C));
|
|
verify(listener, times(1)).writabilityChanged(stream(STREAM_D));
|
|
assertFalse(controller.isWritable(stream(STREAM_A)));
|
|
assertTrue(controller.isWritable(stream(STREAM_B)));
|
|
assertTrue(controller.isWritable(stream(STREAM_C)));
|
|
assertTrue(controller.isWritable(stream(STREAM_D)));
|
|
|
|
data.assertFullyWritten();
|
|
data2.assertFullyWritten();
|
|
}
|
|
|
|
@Test
|
|
public void initialWindowUpdateShouldSendPartialFrame() throws Http2Exception {
|
|
controller.initialWindowSize(0);
|
|
assertWritabilityChanged(1, false);
|
|
reset(listener);
|
|
|
|
FakeFlowControlled data = new FakeFlowControlled(10);
|
|
sendData(STREAM_A, data);
|
|
controller.writePendingBytes();
|
|
data.assertNotWritten();
|
|
|
|
// Verify that a partial frame of 5 was sent.
|
|
controller.initialWindowSize(5);
|
|
verify(listener, never()).writabilityChanged(stream(STREAM_A));
|
|
verify(listener, times(1)).writabilityChanged(stream(STREAM_B));
|
|
verify(listener, times(1)).writabilityChanged(stream(STREAM_C));
|
|
verify(listener, times(1)).writabilityChanged(stream(STREAM_D));
|
|
assertFalse(controller.isWritable(stream(STREAM_A)));
|
|
assertTrue(controller.isWritable(stream(STREAM_B)));
|
|
assertTrue(controller.isWritable(stream(STREAM_C)));
|
|
assertTrue(controller.isWritable(stream(STREAM_D)));
|
|
|
|
data.assertPartiallyWritten(5);
|
|
}
|
|
|
|
@Test
|
|
public void connectionWindowUpdateShouldSendFrame() throws Http2Exception {
|
|
// Set the connection window size to zero.
|
|
exhaustStreamWindow(CONNECTION_STREAM_ID);
|
|
assertWritabilityChanged(1, false);
|
|
reset(listener);
|
|
|
|
FakeFlowControlled data = new FakeFlowControlled(10);
|
|
sendData(STREAM_A, data);
|
|
controller.writePendingBytes();
|
|
data.assertNotWritten();
|
|
assertWritabilityChanged(0, false);
|
|
reset(listener);
|
|
|
|
// Verify that the entire frame was sent.
|
|
incrementWindowSize(CONNECTION_STREAM_ID, 10);
|
|
assertWritabilityChanged(0, false);
|
|
reset(listener);
|
|
data.assertNotWritten();
|
|
|
|
controller.writePendingBytes();
|
|
data.assertFullyWritten();
|
|
assertWritabilityChanged(0, false);
|
|
assertEquals(0, window(CONNECTION_STREAM_ID));
|
|
assertEquals(DEFAULT_WINDOW_SIZE - 10, window(STREAM_A));
|
|
assertEquals(DEFAULT_WINDOW_SIZE, window(STREAM_B));
|
|
assertEquals(DEFAULT_WINDOW_SIZE, window(STREAM_C));
|
|
assertEquals(DEFAULT_WINDOW_SIZE, window(STREAM_D));
|
|
}
|
|
|
|
@Test
|
|
public void connectionWindowUpdateShouldSendPartialFrame() throws Http2Exception {
|
|
// Set the connection window size to zero.
|
|
exhaustStreamWindow(CONNECTION_STREAM_ID);
|
|
assertWritabilityChanged(1, false);
|
|
reset(listener);
|
|
|
|
FakeFlowControlled data = new FakeFlowControlled(10);
|
|
sendData(STREAM_A, data);
|
|
controller.writePendingBytes();
|
|
data.assertNotWritten();
|
|
|
|
// Verify that a partial frame of 5 was sent.
|
|
incrementWindowSize(CONNECTION_STREAM_ID, 5);
|
|
data.assertNotWritten();
|
|
assertWritabilityChanged(0, false);
|
|
reset(listener);
|
|
|
|
controller.writePendingBytes();
|
|
data.assertPartiallyWritten(5);
|
|
assertWritabilityChanged(0, false);
|
|
assertEquals(0, window(CONNECTION_STREAM_ID));
|
|
assertEquals(DEFAULT_WINDOW_SIZE - 5, window(STREAM_A));
|
|
assertEquals(DEFAULT_WINDOW_SIZE, window(STREAM_B));
|
|
assertEquals(DEFAULT_WINDOW_SIZE, window(STREAM_C));
|
|
assertEquals(DEFAULT_WINDOW_SIZE, window(STREAM_D));
|
|
}
|
|
|
|
@Test
|
|
public void streamWindowUpdateShouldSendFrame() throws Http2Exception {
|
|
// Set the stream window size to zero.
|
|
exhaustStreamWindow(STREAM_A);
|
|
verify(listener, times(1)).writabilityChanged(stream(STREAM_A));
|
|
verify(listener, never()).writabilityChanged(stream(STREAM_B));
|
|
verify(listener, never()).writabilityChanged(stream(STREAM_C));
|
|
verify(listener, never()).writabilityChanged(stream(STREAM_D));
|
|
assertFalse(controller.isWritable(stream(STREAM_A)));
|
|
assertTrue(controller.isWritable(stream(STREAM_B)));
|
|
assertTrue(controller.isWritable(stream(STREAM_C)));
|
|
assertTrue(controller.isWritable(stream(STREAM_D)));
|
|
reset(listener);
|
|
|
|
FakeFlowControlled data = new FakeFlowControlled(10);
|
|
sendData(STREAM_A, data);
|
|
controller.writePendingBytes();
|
|
data.assertNotWritten();
|
|
|
|
// Verify that the entire frame was sent.
|
|
incrementWindowSize(STREAM_A, 10);
|
|
verify(listener, never()).writabilityChanged(stream(STREAM_A));
|
|
verify(listener, never()).writabilityChanged(stream(STREAM_B));
|
|
verify(listener, never()).writabilityChanged(stream(STREAM_C));
|
|
verify(listener, never()).writabilityChanged(stream(STREAM_D));
|
|
assertFalse(controller.isWritable(stream(STREAM_A)));
|
|
assertTrue(controller.isWritable(stream(STREAM_B)));
|
|
assertTrue(controller.isWritable(stream(STREAM_C)));
|
|
assertTrue(controller.isWritable(stream(STREAM_D)));
|
|
reset(listener);
|
|
|
|
data.assertNotWritten();
|
|
controller.writePendingBytes();
|
|
data.assertFullyWritten();
|
|
verify(listener, never()).writabilityChanged(stream(STREAM_A));
|
|
verify(listener, never()).writabilityChanged(stream(STREAM_B));
|
|
verify(listener, never()).writabilityChanged(stream(STREAM_C));
|
|
verify(listener, never()).writabilityChanged(stream(STREAM_D));
|
|
assertFalse(controller.isWritable(stream(STREAM_A)));
|
|
assertTrue(controller.isWritable(stream(STREAM_B)));
|
|
assertTrue(controller.isWritable(stream(STREAM_C)));
|
|
assertTrue(controller.isWritable(stream(STREAM_D)));
|
|
assertEquals(DEFAULT_WINDOW_SIZE - 10, window(CONNECTION_STREAM_ID));
|
|
assertEquals(0, window(STREAM_A));
|
|
assertEquals(DEFAULT_WINDOW_SIZE, window(STREAM_B));
|
|
assertEquals(DEFAULT_WINDOW_SIZE, window(STREAM_C));
|
|
assertEquals(DEFAULT_WINDOW_SIZE, window(STREAM_D));
|
|
}
|
|
|
|
@Test
|
|
public void streamWindowUpdateShouldSendPartialFrame() throws Http2Exception {
|
|
// Set the stream window size to zero.
|
|
exhaustStreamWindow(STREAM_A);
|
|
verify(listener, times(1)).writabilityChanged(stream(STREAM_A));
|
|
verify(listener, never()).writabilityChanged(stream(STREAM_B));
|
|
verify(listener, never()).writabilityChanged(stream(STREAM_C));
|
|
verify(listener, never()).writabilityChanged(stream(STREAM_D));
|
|
assertFalse(controller.isWritable(stream(STREAM_A)));
|
|
assertTrue(controller.isWritable(stream(STREAM_B)));
|
|
assertTrue(controller.isWritable(stream(STREAM_C)));
|
|
assertTrue(controller.isWritable(stream(STREAM_D)));
|
|
reset(listener);
|
|
|
|
FakeFlowControlled data = new FakeFlowControlled(10);
|
|
sendData(STREAM_A, data);
|
|
controller.writePendingBytes();
|
|
data.assertNotWritten();
|
|
|
|
// Verify that a partial frame of 5 was sent.
|
|
incrementWindowSize(STREAM_A, 5);
|
|
verify(listener, never()).writabilityChanged(stream(STREAM_A));
|
|
verify(listener, never()).writabilityChanged(stream(STREAM_B));
|
|
verify(listener, never()).writabilityChanged(stream(STREAM_C));
|
|
verify(listener, never()).writabilityChanged(stream(STREAM_D));
|
|
assertFalse(controller.isWritable(stream(STREAM_A)));
|
|
assertTrue(controller.isWritable(stream(STREAM_B)));
|
|
assertTrue(controller.isWritable(stream(STREAM_C)));
|
|
assertTrue(controller.isWritable(stream(STREAM_D)));
|
|
reset(listener);
|
|
|
|
data.assertNotWritten();
|
|
controller.writePendingBytes();
|
|
data.assertPartiallyWritten(5);
|
|
assertEquals(DEFAULT_WINDOW_SIZE - 5, window(CONNECTION_STREAM_ID));
|
|
assertEquals(0, window(STREAM_A));
|
|
assertEquals(DEFAULT_WINDOW_SIZE, window(STREAM_B));
|
|
assertEquals(DEFAULT_WINDOW_SIZE, window(STREAM_C));
|
|
assertEquals(DEFAULT_WINDOW_SIZE, window(STREAM_D));
|
|
}
|
|
|
|
@Test
|
|
public void flowControlledWriteThrowsAnException() throws Exception {
|
|
final Http2RemoteFlowController.FlowControlled flowControlled = mockedFlowControlledThatThrowsOnWrite();
|
|
final Http2Stream stream = stream(STREAM_A);
|
|
doAnswer(new Answer<Void>() {
|
|
@Override
|
|
public Void answer(InvocationOnMock invocationOnMock) {
|
|
stream.closeLocalSide();
|
|
return null;
|
|
}
|
|
}).when(flowControlled).error(any(ChannelHandlerContext.class), any(Throwable.class));
|
|
|
|
int windowBefore = window(STREAM_A);
|
|
|
|
controller.addFlowControlled(stream, flowControlled);
|
|
controller.writePendingBytes();
|
|
|
|
verify(flowControlled, atLeastOnce()).write(any(ChannelHandlerContext.class), anyInt());
|
|
verify(flowControlled).error(any(ChannelHandlerContext.class), any(Throwable.class));
|
|
verify(flowControlled, never()).writeComplete();
|
|
|
|
assertEquals(90, windowBefore - window(STREAM_A));
|
|
verify(listener, times(1)).writabilityChanged(stream(STREAM_A));
|
|
verify(listener, never()).writabilityChanged(stream(STREAM_B));
|
|
verify(listener, never()).writabilityChanged(stream(STREAM_C));
|
|
verify(listener, never()).writabilityChanged(stream(STREAM_D));
|
|
assertFalse(controller.isWritable(stream(STREAM_A)));
|
|
assertTrue(controller.isWritable(stream(STREAM_B)));
|
|
assertTrue(controller.isWritable(stream(STREAM_C)));
|
|
assertTrue(controller.isWritable(stream(STREAM_D)));
|
|
}
|
|
|
|
@Test
|
|
public void flowControlledWriteAndErrorThrowAnException() throws Exception {
|
|
final Http2RemoteFlowController.FlowControlled flowControlled = mockedFlowControlledThatThrowsOnWrite();
|
|
final Http2Stream stream = stream(STREAM_A);
|
|
final RuntimeException fakeException = new RuntimeException("error failed");
|
|
doAnswer(new Answer<Void>() {
|
|
@Override
|
|
public Void answer(InvocationOnMock invocationOnMock) {
|
|
throw fakeException;
|
|
}
|
|
}).when(flowControlled).error(any(ChannelHandlerContext.class), any(Throwable.class));
|
|
|
|
int windowBefore = window(STREAM_A);
|
|
|
|
try {
|
|
controller.addFlowControlled(stream, flowControlled);
|
|
controller.writePendingBytes();
|
|
fail();
|
|
} catch (Http2Exception e) {
|
|
assertSame(fakeException, e.getCause());
|
|
} catch (Throwable t) {
|
|
fail();
|
|
}
|
|
|
|
verify(flowControlled, atLeastOnce()).write(any(ChannelHandlerContext.class), anyInt());
|
|
verify(flowControlled).error(any(ChannelHandlerContext.class), any(Throwable.class));
|
|
verify(flowControlled, never()).writeComplete();
|
|
|
|
assertEquals(90, windowBefore - window(STREAM_A));
|
|
verifyZeroInteractions(listener);
|
|
}
|
|
|
|
@Test
|
|
public void flowControlledWriteCompleteThrowsAnException() throws Exception {
|
|
final Http2RemoteFlowController.FlowControlled flowControlled =
|
|
mock(Http2RemoteFlowController.FlowControlled.class);
|
|
Http2Stream streamA = stream(STREAM_A);
|
|
final AtomicInteger size = new AtomicInteger(150);
|
|
doAnswer(new Answer<Integer>() {
|
|
@Override
|
|
public Integer answer(InvocationOnMock invocationOnMock) throws Throwable {
|
|
return size.get();
|
|
}
|
|
}).when(flowControlled).size();
|
|
doAnswer(new Answer<Void>() {
|
|
@Override
|
|
public Void answer(InvocationOnMock invocationOnMock) throws Throwable {
|
|
size.addAndGet(-50);
|
|
return null;
|
|
}
|
|
}).when(flowControlled).write(any(ChannelHandlerContext.class), anyInt());
|
|
|
|
final Http2Stream stream = stream(STREAM_A);
|
|
doAnswer(new Answer<Void>() {
|
|
@Override
|
|
public Void answer(InvocationOnMock invocationOnMock) {
|
|
throw new RuntimeException("writeComplete failed");
|
|
}
|
|
}).when(flowControlled).writeComplete();
|
|
|
|
int windowBefore = window(STREAM_A);
|
|
|
|
controller.addFlowControlled(stream, flowControlled);
|
|
controller.writePendingBytes();
|
|
|
|
verify(flowControlled, times(3)).write(any(ChannelHandlerContext.class), anyInt());
|
|
verify(flowControlled, never()).error(any(ChannelHandlerContext.class), any(Throwable.class));
|
|
verify(flowControlled).writeComplete();
|
|
|
|
assertEquals(150, windowBefore - window(STREAM_A));
|
|
verify(listener, times(1)).writabilityChanged(streamA);
|
|
verify(listener, never()).writabilityChanged(stream(STREAM_B));
|
|
verify(listener, never()).writabilityChanged(stream(STREAM_C));
|
|
verify(listener, never()).writabilityChanged(stream(STREAM_D));
|
|
assertFalse(controller.isWritable(streamA));
|
|
assertTrue(controller.isWritable(stream(STREAM_B)));
|
|
assertTrue(controller.isWritable(stream(STREAM_C)));
|
|
assertTrue(controller.isWritable(stream(STREAM_D)));
|
|
}
|
|
|
|
@Test
|
|
public void closeStreamInFlowControlledError() throws Exception {
|
|
final Http2RemoteFlowController.FlowControlled flowControlled =
|
|
mock(Http2RemoteFlowController.FlowControlled.class);
|
|
final Http2Stream stream = stream(STREAM_A);
|
|
when(flowControlled.size()).thenReturn(100);
|
|
doThrow(new RuntimeException("write failed"))
|
|
.when(flowControlled).write(any(ChannelHandlerContext.class), anyInt());
|
|
doAnswer(new Answer<Void>() {
|
|
@Override
|
|
public Void answer(InvocationOnMock invocationOnMock) {
|
|
stream.close();
|
|
return null;
|
|
}
|
|
}).when(flowControlled).error(any(ChannelHandlerContext.class), any(Throwable.class));
|
|
|
|
controller.addFlowControlled(stream, flowControlled);
|
|
controller.writePendingBytes();
|
|
|
|
verify(flowControlled).write(any(ChannelHandlerContext.class), anyInt());
|
|
verify(flowControlled).error(any(ChannelHandlerContext.class), any(Throwable.class));
|
|
verify(flowControlled, never()).writeComplete();
|
|
verify(listener, times(1)).writabilityChanged(stream);
|
|
verify(listener, never()).writabilityChanged(stream(STREAM_B));
|
|
verify(listener, never()).writabilityChanged(stream(STREAM_C));
|
|
verify(listener, never()).writabilityChanged(stream(STREAM_D));
|
|
assertFalse(controller.isWritable(stream));
|
|
assertTrue(controller.isWritable(stream(STREAM_B)));
|
|
assertTrue(controller.isWritable(stream(STREAM_C)));
|
|
assertTrue(controller.isWritable(stream(STREAM_D)));
|
|
}
|
|
|
|
@Test
|
|
public void nonWritableChannelDoesNotAttemptToWrite() throws Exception {
|
|
// Start the channel as not writable and exercise the public methods of the flow controller
|
|
// making sure no frames are written.
|
|
setChannelWritability(false);
|
|
assertWritabilityChanged(1, false);
|
|
reset(listener);
|
|
FakeFlowControlled dataA = new FakeFlowControlled(1);
|
|
FakeFlowControlled dataB = new FakeFlowControlled(1);
|
|
final Http2Stream stream = stream(STREAM_A);
|
|
|
|
controller.addFlowControlled(stream, dataA);
|
|
controller.writePendingBytes();
|
|
dataA.assertNotWritten();
|
|
|
|
controller.incrementWindowSize(stream, 100);
|
|
controller.writePendingBytes();
|
|
dataA.assertNotWritten();
|
|
|
|
controller.addFlowControlled(stream, dataB);
|
|
controller.writePendingBytes();
|
|
dataA.assertNotWritten();
|
|
dataB.assertNotWritten();
|
|
assertWritabilityChanged(0, false);
|
|
|
|
// Now change the channel to writable and make sure frames are written.
|
|
setChannelWritability(true);
|
|
assertWritabilityChanged(1, true);
|
|
controller.writePendingBytes();
|
|
dataA.assertFullyWritten();
|
|
dataB.assertFullyWritten();
|
|
}
|
|
|
|
@Test
|
|
public void contextShouldSendQueuedFramesWhenSet() throws Exception {
|
|
// Re-initialize the controller so we can ensure the context hasn't been set yet.
|
|
initConnectionAndController();
|
|
|
|
FakeFlowControlled dataA = new FakeFlowControlled(1);
|
|
final Http2Stream stream = stream(STREAM_A);
|
|
|
|
// Queue some frames
|
|
controller.addFlowControlled(stream, dataA);
|
|
dataA.assertNotWritten();
|
|
|
|
controller.incrementWindowSize(stream, 100);
|
|
dataA.assertNotWritten();
|
|
|
|
assertWritabilityChanged(0, false);
|
|
|
|
// Set the controller
|
|
controller.channelHandlerContext(ctx);
|
|
dataA.assertFullyWritten();
|
|
|
|
assertWritabilityChanged(1, true);
|
|
}
|
|
|
|
@Test
|
|
public void initialWindowSizeWithNoContextShouldNotThrow() throws Exception {
|
|
// Re-initialize the controller so we can ensure the context hasn't been set yet.
|
|
initConnectionAndController();
|
|
|
|
// This should not throw.
|
|
controller.initialWindowSize(1024 * 100);
|
|
|
|
FakeFlowControlled dataA = new FakeFlowControlled(1);
|
|
final Http2Stream stream = stream(STREAM_A);
|
|
|
|
// Queue some frames
|
|
controller.addFlowControlled(stream, dataA);
|
|
dataA.assertNotWritten();
|
|
|
|
// Set the controller
|
|
controller.channelHandlerContext(ctx);
|
|
dataA.assertFullyWritten();
|
|
}
|
|
|
|
@Test(expected = AssertionError.class)
|
|
public void invalidParentStreamIdThrows() {
|
|
controller.updateDependencyTree(STREAM_D, -1, DEFAULT_PRIORITY_WEIGHT, true);
|
|
}
|
|
|
|
@Test(expected = AssertionError.class)
|
|
public void invalidChildStreamIdThrows() {
|
|
controller.updateDependencyTree(-1, STREAM_D, DEFAULT_PRIORITY_WEIGHT, true);
|
|
}
|
|
|
|
@Test(expected = AssertionError.class)
|
|
public void connectionChildStreamIdThrows() {
|
|
controller.updateDependencyTree(0, STREAM_D, DEFAULT_PRIORITY_WEIGHT, true);
|
|
}
|
|
|
|
@Test(expected = AssertionError.class)
|
|
public void invalidWeightTooSmallThrows() {
|
|
controller.updateDependencyTree(STREAM_A, STREAM_D, (short) (MIN_WEIGHT - 1), true);
|
|
}
|
|
|
|
@Test(expected = AssertionError.class)
|
|
public void invalidWeightTooBigThrows() {
|
|
controller.updateDependencyTree(STREAM_A, STREAM_D, (short) (MAX_WEIGHT + 1), true);
|
|
}
|
|
|
|
@Test(expected = AssertionError.class)
|
|
public void dependencyOnSelfThrows() {
|
|
controller.updateDependencyTree(STREAM_A, STREAM_A, DEFAULT_PRIORITY_WEIGHT, true);
|
|
}
|
|
|
|
private void assertWritabilityChanged(int amt, boolean writable) {
|
|
verify(listener, times(amt)).writabilityChanged(stream(STREAM_A));
|
|
verify(listener, times(amt)).writabilityChanged(stream(STREAM_B));
|
|
verify(listener, times(amt)).writabilityChanged(stream(STREAM_C));
|
|
verify(listener, times(amt)).writabilityChanged(stream(STREAM_D));
|
|
if (writable) {
|
|
assertTrue(controller.isWritable(stream(STREAM_A)));
|
|
assertTrue(controller.isWritable(stream(STREAM_B)));
|
|
assertTrue(controller.isWritable(stream(STREAM_C)));
|
|
assertTrue(controller.isWritable(stream(STREAM_D)));
|
|
} else {
|
|
assertFalse(controller.isWritable(stream(STREAM_A)));
|
|
assertFalse(controller.isWritable(stream(STREAM_B)));
|
|
assertFalse(controller.isWritable(stream(STREAM_C)));
|
|
assertFalse(controller.isWritable(stream(STREAM_D)));
|
|
}
|
|
}
|
|
|
|
private static Http2RemoteFlowController.FlowControlled mockedFlowControlledThatThrowsOnWrite() throws Exception {
|
|
final Http2RemoteFlowController.FlowControlled flowControlled =
|
|
mock(Http2RemoteFlowController.FlowControlled.class);
|
|
when(flowControlled.size()).thenReturn(100);
|
|
doAnswer(new Answer<Void>() {
|
|
@Override
|
|
public Void answer(InvocationOnMock in) throws Throwable {
|
|
// Write most of the bytes and then fail
|
|
when(flowControlled.size()).thenReturn(10);
|
|
throw new RuntimeException("Write failed");
|
|
}
|
|
}).when(flowControlled).write(any(ChannelHandlerContext.class), anyInt());
|
|
return flowControlled;
|
|
}
|
|
|
|
private void sendData(int streamId, FakeFlowControlled data) {
|
|
Http2Stream stream = stream(streamId);
|
|
controller.addFlowControlled(stream, data);
|
|
}
|
|
|
|
private void exhaustStreamWindow(int streamId) throws Http2Exception {
|
|
incrementWindowSize(streamId, -window(streamId));
|
|
}
|
|
|
|
private int window(int streamId) {
|
|
return controller.windowSize(stream(streamId));
|
|
}
|
|
|
|
private void incrementWindowSize(int streamId, int delta) throws Http2Exception {
|
|
controller.incrementWindowSize(stream(streamId), delta);
|
|
}
|
|
|
|
private Http2Stream stream(int streamId) {
|
|
return connection.stream(streamId);
|
|
}
|
|
|
|
private void resetCtx() {
|
|
reset(ctx);
|
|
when(ctx.channel()).thenReturn(channel);
|
|
when(ctx.executor()).thenReturn(executor);
|
|
}
|
|
|
|
private void setChannelWritability(boolean isWritable) throws Http2Exception {
|
|
when(channel.bytesBeforeUnwritable()).thenReturn(isWritable ? Long.MAX_VALUE : 0);
|
|
when(channel.isWritable()).thenReturn(isWritable);
|
|
if (controller != null) {
|
|
controller.channelWritabilityChanged();
|
|
}
|
|
}
|
|
|
|
private static final class FakeFlowControlled implements Http2RemoteFlowController.FlowControlled {
|
|
private int currentPadding;
|
|
private int currentPayloadSize;
|
|
private int originalPayloadSize;
|
|
private int originalPadding;
|
|
private boolean writeCalled;
|
|
private final boolean mergeable;
|
|
private boolean merged;
|
|
|
|
private Throwable t;
|
|
|
|
private FakeFlowControlled(int size) {
|
|
this(size, false);
|
|
}
|
|
|
|
private FakeFlowControlled(int size, boolean mergeable) {
|
|
this(size, 0, mergeable);
|
|
}
|
|
|
|
private FakeFlowControlled(int payloadSize, int padding, boolean mergeable) {
|
|
currentPayloadSize = originalPayloadSize = payloadSize;
|
|
currentPadding = originalPadding = padding;
|
|
this.mergeable = mergeable;
|
|
}
|
|
|
|
@Override
|
|
public int size() {
|
|
return currentPayloadSize + currentPadding;
|
|
}
|
|
|
|
private int originalSize() {
|
|
return originalPayloadSize + originalPadding;
|
|
}
|
|
|
|
@Override
|
|
public void error(ChannelHandlerContext ctx, Throwable t) {
|
|
this.t = t;
|
|
}
|
|
|
|
@Override
|
|
public void writeComplete() {
|
|
}
|
|
|
|
@Override
|
|
public void write(ChannelHandlerContext ctx, int allowedBytes) {
|
|
if (allowedBytes <= 0 && size() != 0) {
|
|
// Write has been called but no data can be written
|
|
return;
|
|
}
|
|
writeCalled = true;
|
|
int written = Math.min(size(), allowedBytes);
|
|
if (written > currentPayloadSize) {
|
|
written -= currentPayloadSize;
|
|
currentPayloadSize = 0;
|
|
currentPadding -= written;
|
|
} else {
|
|
currentPayloadSize -= written;
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public boolean merge(ChannelHandlerContext ctx, Http2RemoteFlowController.FlowControlled next) {
|
|
if (mergeable && next instanceof FakeFlowControlled) {
|
|
FakeFlowControlled ffcNext = (FakeFlowControlled) next;
|
|
originalPayloadSize += ffcNext.originalPayloadSize;
|
|
currentPayloadSize += ffcNext.originalPayloadSize;
|
|
currentPadding = originalPadding = Math.max(originalPadding, ffcNext.originalPadding);
|
|
ffcNext.merged = true;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
public int written() {
|
|
return originalSize() - size();
|
|
}
|
|
|
|
public void assertNotWritten() {
|
|
assertFalse(writeCalled);
|
|
}
|
|
|
|
public void assertPartiallyWritten(int expectedWritten) {
|
|
assertPartiallyWritten(expectedWritten, 0);
|
|
}
|
|
|
|
public void assertPartiallyWritten(int expectedWritten, int delta) {
|
|
assertTrue(writeCalled);
|
|
assertEquals(expectedWritten, written(), delta);
|
|
}
|
|
|
|
public void assertFullyWritten() {
|
|
assertTrue(writeCalled);
|
|
assertEquals(0, currentPayloadSize);
|
|
assertEquals(0, currentPadding);
|
|
}
|
|
|
|
public boolean assertMerged() {
|
|
return merged;
|
|
}
|
|
|
|
public void assertError(Http2Error error) {
|
|
assertNotNull(t);
|
|
if (error != null) {
|
|
assertSame(error, ((Http2Exception) t).error());
|
|
}
|
|
}
|
|
}
|
|
}
|