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:
chhsiao90 2017-02-18 16:39:11 +08:00 committed by Norman Maurer
parent b1436e80ef
commit e148b53c67

View File

@ -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);
}
}