Add unit test for DefaultHttp2FrameWriter
Motivation: DefaultHttp2FrameWriter contains important logic for how http2 frame encode into binary, currently, netty had no unit test just for DefaultHttp2FrameWriter. Modifications: Write DefaultHttp2FrameWriterTest to test DefaultHttp2FrameWriter Result: The coverage of DefaultHttp2FrameWriter is increased, and the class was more reliable
This commit is contained in:
parent
b1436e80ef
commit
e148b53c67
@ -0,0 +1,279 @@
|
||||
/*
|
||||
* Copyright 2017 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.buffer.ByteBuf;
|
||||
import io.netty.buffer.Unpooled;
|
||||
import io.netty.buffer.UnpooledByteBufAllocator;
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelFuture;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.channel.ChannelPromise;
|
||||
import io.netty.channel.DefaultChannelPromise;
|
||||
import io.netty.util.ReferenceCountUtil;
|
||||
import io.netty.util.concurrent.ImmediateEventExecutor;
|
||||
import org.junit.After;
|
||||
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.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
/**
|
||||
* Tests for {@link DefaultHttp2FrameWriter}.
|
||||
*/
|
||||
public class DefaultHttp2FrameWriterTest {
|
||||
private DefaultHttp2FrameWriter frameWriter;
|
||||
|
||||
private ByteBuf outbound;
|
||||
|
||||
private ByteBuf expectedOutbound;
|
||||
|
||||
private ChannelPromise promise;
|
||||
|
||||
private Http2HeadersEncoder http2HeadersEncoder;
|
||||
|
||||
@Mock
|
||||
private Channel channel;
|
||||
|
||||
@Mock
|
||||
private ChannelFuture future;
|
||||
|
||||
@Mock
|
||||
private ChannelHandlerContext ctx;
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
MockitoAnnotations.initMocks(this);
|
||||
|
||||
frameWriter = new DefaultHttp2FrameWriter();
|
||||
|
||||
outbound = Unpooled.buffer();
|
||||
|
||||
expectedOutbound = Unpooled.EMPTY_BUFFER;
|
||||
|
||||
promise = new DefaultChannelPromise(channel, ImmediateEventExecutor.INSTANCE);
|
||||
|
||||
http2HeadersEncoder = new DefaultHttp2HeadersEncoder();
|
||||
|
||||
Answer<Object> answer = new Answer<Object>() {
|
||||
@Override
|
||||
public Object answer(InvocationOnMock var1) throws Throwable {
|
||||
Object msg = var1.getArgument(0);
|
||||
if (msg instanceof ByteBuf) {
|
||||
outbound.writeBytes((ByteBuf) msg);
|
||||
}
|
||||
ReferenceCountUtil.release(msg);
|
||||
return future;
|
||||
}
|
||||
};
|
||||
when(ctx.write(any())).then(answer);
|
||||
when(ctx.write(any(), any(ChannelPromise.class))).then(answer);
|
||||
when(ctx.alloc()).thenReturn(UnpooledByteBufAllocator.DEFAULT);
|
||||
when(ctx.channel()).thenReturn(channel);
|
||||
when(ctx.executor()).thenReturn(ImmediateEventExecutor.INSTANCE);
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() throws Exception {
|
||||
outbound.release();
|
||||
expectedOutbound.release();
|
||||
frameWriter.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void writeHeaders() throws Exception {
|
||||
int streamId = 1;
|
||||
Http2Headers headers = new DefaultHttp2Headers()
|
||||
.method("GET").path("/").authority("foo.com").scheme("https");
|
||||
|
||||
frameWriter.writeHeaders(ctx, streamId, headers, 0, true, promise);
|
||||
|
||||
byte[] expectedPayload = headerPayload(streamId, headers);
|
||||
byte[] expectedFrameBytes = {
|
||||
(byte) 0x00, (byte) 0x00, (byte) 0x0a, // payload length = 10
|
||||
(byte) 0x01, // payload type = 1
|
||||
(byte) 0x05, // flags = (0x01 | 0x04)
|
||||
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x01 // stream id = 1
|
||||
};
|
||||
expectedOutbound = Unpooled.copiedBuffer(expectedFrameBytes, expectedPayload);
|
||||
assertEquals(expectedOutbound, outbound);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void writeHeadersWithPadding() throws Exception {
|
||||
int streamId = 1;
|
||||
Http2Headers headers = new DefaultHttp2Headers()
|
||||
.method("GET").path("/").authority("foo.com").scheme("https");
|
||||
|
||||
frameWriter.writeHeaders(ctx, streamId, headers, 5, true, promise);
|
||||
|
||||
byte[] expectedPayload = headerPayload(streamId, headers, (byte) 4);
|
||||
byte[] expectedFrameBytes = {
|
||||
(byte) 0x00, (byte) 0x00, (byte) 0x0f, // payload length = 16
|
||||
(byte) 0x01, // payload type = 1
|
||||
(byte) 0x0d, // flags = (0x01 | 0x04 | 0x08)
|
||||
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x01 // stream id = 1
|
||||
};
|
||||
expectedOutbound = Unpooled.copiedBuffer(expectedFrameBytes, expectedPayload);
|
||||
assertEquals(expectedOutbound, outbound);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void writeHeadersNotEndStream() throws Exception {
|
||||
int streamId = 1;
|
||||
Http2Headers headers = new DefaultHttp2Headers()
|
||||
.method("GET").path("/").authority("foo.com").scheme("https");
|
||||
|
||||
frameWriter.writeHeaders(ctx, streamId, headers, 0, false, promise);
|
||||
|
||||
byte[] expectedPayload = headerPayload(streamId, headers);
|
||||
byte[] expectedFrameBytes = {
|
||||
(byte) 0x00, (byte) 0x00, (byte) 0x0a, // payload length = 10
|
||||
(byte) 0x01, // payload type = 1
|
||||
(byte) 0x04, // flags = 0x04
|
||||
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x01 // stream id = 1
|
||||
};
|
||||
ByteBuf expectedOutbound = Unpooled.copiedBuffer(expectedFrameBytes, expectedPayload);
|
||||
assertEquals(expectedOutbound, outbound);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test large headers that exceed {@link DefaultHttp2FrameWriter#maxFrameSize}
|
||||
* the remaining headers will be sent in a CONTINUATION frame
|
||||
*/
|
||||
@Test
|
||||
public void writeLargeHeaders() throws Exception {
|
||||
int streamId = 1;
|
||||
Http2Headers headers = new DefaultHttp2Headers()
|
||||
.method("GET").path("/").authority("foo.com").scheme("https");
|
||||
headers = dummyHeaders(headers, 20);
|
||||
|
||||
http2HeadersEncoder.configuration().maxHeaderListSize(Integer.MAX_VALUE);
|
||||
frameWriter.headersConfiguration().maxHeaderListSize(Integer.MAX_VALUE);
|
||||
frameWriter.maxFrameSize(Http2CodecUtil.MAX_FRAME_SIZE_LOWER_BOUND);
|
||||
frameWriter.writeHeaders(ctx, streamId, headers, 0, true, promise);
|
||||
|
||||
byte[] expectedPayload = headerPayload(streamId, headers);
|
||||
|
||||
// First frame: HEADER(length=0x4000, flags=0x01)
|
||||
assertEquals(Http2CodecUtil.MAX_FRAME_SIZE_LOWER_BOUND,
|
||||
outbound.readUnsignedMedium());
|
||||
assertEquals(0x01, outbound.readByte());
|
||||
assertEquals(0x01, outbound.readByte());
|
||||
assertEquals(streamId, outbound.readInt());
|
||||
|
||||
byte[] firstPayload = new byte[Http2CodecUtil.MAX_FRAME_SIZE_LOWER_BOUND];
|
||||
outbound.readBytes(firstPayload);
|
||||
|
||||
int remainPayloadLength = expectedPayload.length - Http2CodecUtil.MAX_FRAME_SIZE_LOWER_BOUND;
|
||||
// Second frame: CONTINUATION(length=remainPayloadLength, flags=0x04)
|
||||
assertEquals(remainPayloadLength, outbound.readUnsignedMedium());
|
||||
assertEquals(0x09, outbound.readByte());
|
||||
assertEquals(0x04, outbound.readByte());
|
||||
assertEquals(streamId, outbound.readInt());
|
||||
|
||||
byte[] secondPayload = new byte[remainPayloadLength];
|
||||
outbound.readBytes(secondPayload);
|
||||
|
||||
assertArrayEquals(Arrays.copyOfRange(expectedPayload, 0, firstPayload.length),
|
||||
firstPayload);
|
||||
assertArrayEquals(Arrays.copyOfRange(expectedPayload, firstPayload.length,
|
||||
expectedPayload.length),
|
||||
secondPayload);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void writeFrameZeroPayload() throws Exception {
|
||||
frameWriter.writeFrame(ctx, (byte) 0xf, 0, new Http2Flags(), Unpooled.EMPTY_BUFFER, promise);
|
||||
|
||||
byte[] expectedFrameBytes = {
|
||||
(byte) 0x00, (byte) 0x00, (byte) 0x00, // payload length
|
||||
(byte) 0x0f, // payload type
|
||||
(byte) 0x00, // flags
|
||||
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00 // stream id
|
||||
};
|
||||
|
||||
expectedOutbound = Unpooled.wrappedBuffer(expectedFrameBytes);
|
||||
assertEquals(expectedOutbound, outbound);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void writeFrameHasPayload() throws Exception {
|
||||
byte[] payload = {(byte) 0x01, (byte) 0x03, (byte) 0x05, (byte) 0x07, (byte) 0x09};
|
||||
|
||||
// will auto release after frameWriter.writeFrame succeed
|
||||
ByteBuf payloadByteBuf = Unpooled.wrappedBuffer(payload);
|
||||
frameWriter.writeFrame(ctx, (byte) 0xf, 0, new Http2Flags(), payloadByteBuf, promise);
|
||||
|
||||
byte[] expectedFrameHeaderBytes = {
|
||||
(byte) 0x00, (byte) 0x00, (byte) 0x05, // payload length
|
||||
(byte) 0x0f, // payload type
|
||||
(byte) 0x00, // flags
|
||||
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00 // stream id
|
||||
};
|
||||
expectedOutbound = Unpooled.copiedBuffer(expectedFrameHeaderBytes, payload);
|
||||
assertEquals(expectedOutbound, outbound);
|
||||
}
|
||||
|
||||
private byte[] headerPayload(int streamId, Http2Headers headers, byte padding) throws Http2Exception, IOException {
|
||||
if (padding == 0) {
|
||||
return headerPayload(streamId, headers);
|
||||
}
|
||||
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
try {
|
||||
outputStream.write(padding);
|
||||
outputStream.write(headerPayload(streamId, headers));
|
||||
outputStream.write(new byte[padding]);
|
||||
return outputStream.toByteArray();
|
||||
} finally {
|
||||
outputStream.close();
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] headerPayload(int streamId, Http2Headers headers) throws Http2Exception {
|
||||
ByteBuf byteBuf = Unpooled.buffer();
|
||||
try {
|
||||
http2HeadersEncoder.encodeHeaders(streamId, headers, byteBuf);
|
||||
byte[] bytes = new byte[byteBuf.readableBytes()];
|
||||
byteBuf.readBytes(bytes);
|
||||
return bytes;
|
||||
} finally {
|
||||
byteBuf.release();
|
||||
}
|
||||
}
|
||||
|
||||
private Http2Headers dummyHeaders(Http2Headers headers, int times) {
|
||||
final String largeValue = repeat("dummy-value", 100);
|
||||
for (int i = 0; i < times; i++) {
|
||||
headers.add(String.format("dummy-%d", i), largeValue);
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
|
||||
private String repeat(String str, int count) {
|
||||
return String.format(String.format("%%%ds", count), " ").replace(" ", str);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user