netty5/codec-redis/src/test/java/io/netty/handler/codec/redis/RedisDecoderTest.java
Sergey Skrobotov f10bee9057 Change DefaultByteBufHolder.equals() to treat instances of different classes as not equal (#9855)
# Motivation:
`DefaultByteBufHolder.equals()` considers another object equal if it's an instance of `ByteBufferHolder` and if the contents of two objects are equal. However, the behavior of `equals` method is not a part of the `ByteBufHolder` contract so `DefaultByteBufHolder`'s version may be causing violation of the symmetric property if other classes have different logic.
There are already a few classes that are affected by this: `DefaultHttp2GoAwayFrame`, `DefaultHttp2UnknownFrame`, and `SctpMessage` are all overriding `equals` method breaking the symmetric property.
Another effect of this behavior is that all instances with empty data are considered equal. That may not be desireable in the situations when instances are created for predefined constants, e.g. `FullBulkStringRedisMessage.NULL_INSTANCE` and `FullBulkStringRedisMessage.EMPTY_INSTANCE` in `codec-redis`. 

# Modification:
Make `DefaultByteBufHolder.equals()` implementation only work for the objects of the same class.

# Result:
- The symmetric property of the `equals` method is restored for the classes in question.
- Instances of different classes are not considered equal even if the content of the data they hold are the same.
2019-12-10 11:30:23 +01:00

318 lines
12 KiB
Java

/*
* Copyright 2016 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.redis;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.embedded.EmbeddedChannel;
import io.netty.handler.codec.DecoderException;
import io.netty.util.IllegalReferenceCountException;
import io.netty.util.ReferenceCountUtil;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import java.util.List;
import static io.netty.handler.codec.redis.RedisCodecTestUtil.*;
import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
/**
* Verifies the correct functionality of the {@link RedisDecoder} and {@link RedisArrayAggregator}.
*/
public class RedisDecoderTest {
private EmbeddedChannel channel;
@Before
public void setup() throws Exception {
channel = newChannel(false);
}
private static EmbeddedChannel newChannel(boolean decodeInlineCommands) {
return new EmbeddedChannel(
new RedisDecoder(decodeInlineCommands),
new RedisBulkStringAggregator(),
new RedisArrayAggregator());
}
@After
public void teardown() throws Exception {
assertFalse(channel.finish());
}
@Test
public void splitEOLDoesNotInfiniteLoop() throws Exception {
assertFalse(channel.writeInbound(byteBufOf("$6\r\nfoobar\r")));
assertTrue(channel.writeInbound(byteBufOf("\n")));
RedisMessage msg = channel.readInbound();
assertTrue(msg instanceof FullBulkStringRedisMessage);
ReferenceCountUtil.release(msg);
}
@Test(expected = DecoderException.class)
public void shouldNotDecodeInlineCommandByDefault() {
assertFalse(channel.writeInbound(byteBufOf("P")));
assertFalse(channel.writeInbound(byteBufOf("I")));
assertFalse(channel.writeInbound(byteBufOf("N")));
assertFalse(channel.writeInbound(byteBufOf("G")));
assertTrue(channel.writeInbound(byteBufOf("\r\n")));
channel.readInbound();
}
@Test
public void shouldDecodeInlineCommand() {
channel = newChannel(true);
assertFalse(channel.writeInbound(byteBufOf("P")));
assertFalse(channel.writeInbound(byteBufOf("I")));
assertFalse(channel.writeInbound(byteBufOf("N")));
assertFalse(channel.writeInbound(byteBufOf("G")));
assertTrue(channel.writeInbound(byteBufOf("\r\n")));
InlineCommandRedisMessage msg = channel.readInbound();
assertThat(msg.content(), is("PING"));
ReferenceCountUtil.release(msg);
}
@Test
public void shouldDecodeSimpleString() {
assertFalse(channel.writeInbound(byteBufOf("+")));
assertFalse(channel.writeInbound(byteBufOf("O")));
assertFalse(channel.writeInbound(byteBufOf("K")));
assertTrue(channel.writeInbound(byteBufOf("\r\n")));
SimpleStringRedisMessage msg = channel.readInbound();
assertThat(msg.content(), is("OK"));
ReferenceCountUtil.release(msg);
}
@Test
public void shouldDecodeTwoSimpleStrings() {
assertFalse(channel.writeInbound(byteBufOf("+")));
assertFalse(channel.writeInbound(byteBufOf("O")));
assertFalse(channel.writeInbound(byteBufOf("K")));
assertTrue(channel.writeInbound(byteBufOf("\r\n+SEC")));
assertTrue(channel.writeInbound(byteBufOf("OND\r\n")));
SimpleStringRedisMessage msg1 = channel.readInbound();
assertThat(msg1.content(), is("OK"));
ReferenceCountUtil.release(msg1);
SimpleStringRedisMessage msg2 = channel.readInbound();
assertThat(msg2.content(), is("SECOND"));
ReferenceCountUtil.release(msg2);
}
@Test
public void shouldDecodeError() {
String content = "ERROR sample message";
assertFalse(channel.writeInbound(byteBufOf("-")));
assertFalse(channel.writeInbound(byteBufOf(content)));
assertFalse(channel.writeInbound(byteBufOf("\r")));
assertTrue(channel.writeInbound(byteBufOf("\n")));
ErrorRedisMessage msg = channel.readInbound();
assertThat(msg.content(), is(content));
ReferenceCountUtil.release(msg);
}
@Test
public void shouldDecodeInteger() {
long value = 1234L;
byte[] content = bytesOf(value);
assertFalse(channel.writeInbound(byteBufOf(":")));
assertFalse(channel.writeInbound(byteBufOf(content)));
assertTrue(channel.writeInbound(byteBufOf("\r\n")));
IntegerRedisMessage msg = channel.readInbound();
assertThat(msg.value(), is(value));
ReferenceCountUtil.release(msg);
}
@Test
public void shouldDecodeBulkString() {
String buf1 = "bulk\nst";
String buf2 = "ring\ntest\n1234";
byte[] content = bytesOf(buf1 + buf2);
assertFalse(channel.writeInbound(byteBufOf("$")));
assertFalse(channel.writeInbound(byteBufOf(Integer.toString(content.length))));
assertFalse(channel.writeInbound(byteBufOf("\r\n")));
assertFalse(channel.writeInbound(byteBufOf(buf1)));
assertFalse(channel.writeInbound(byteBufOf(buf2)));
assertTrue(channel.writeInbound(byteBufOf("\r\n")));
FullBulkStringRedisMessage msg = channel.readInbound();
assertThat(bytesOf(msg.content()), is(content));
ReferenceCountUtil.release(msg);
}
@Test
public void shouldDecodeEmptyBulkString() {
byte[] content = bytesOf("");
assertFalse(channel.writeInbound(byteBufOf("$")));
assertFalse(channel.writeInbound(byteBufOf(Integer.toString(content.length))));
assertFalse(channel.writeInbound(byteBufOf("\r\n")));
assertFalse(channel.writeInbound(byteBufOf(content)));
assertTrue(channel.writeInbound(byteBufOf("\r\n")));
FullBulkStringRedisMessage msg = channel.readInbound();
assertThat(bytesOf(msg.content()), is(content));
ReferenceCountUtil.release(msg);
}
@Test
public void shouldDecodeNullBulkString() {
assertFalse(channel.writeInbound(byteBufOf("$")));
assertFalse(channel.writeInbound(byteBufOf(Integer.toString(-1))));
assertTrue(channel.writeInbound(byteBufOf("\r\n")));
assertTrue(channel.writeInbound(byteBufOf("$")));
assertTrue(channel.writeInbound(byteBufOf(Integer.toString(-1))));
assertTrue(channel.writeInbound(byteBufOf("\r\n")));
FullBulkStringRedisMessage msg1 = channel.readInbound();
assertThat(msg1.isNull(), is(true));
ReferenceCountUtil.release(msg1);
FullBulkStringRedisMessage msg2 = channel.readInbound();
assertThat(msg2.isNull(), is(true));
ReferenceCountUtil.release(msg2);
FullBulkStringRedisMessage msg3 = channel.readInbound();
assertThat(msg3, is(nullValue()));
}
@Test
public void shouldDecodeSimpleArray() throws Exception {
assertFalse(channel.writeInbound(byteBufOf("*3\r\n")));
assertFalse(channel.writeInbound(byteBufOf(":1234\r\n")));
assertFalse(channel.writeInbound(byteBufOf("+sim")));
assertFalse(channel.writeInbound(byteBufOf("ple\r\n-err")));
assertTrue(channel.writeInbound(byteBufOf("or\r\n")));
ArrayRedisMessage msg = channel.readInbound();
List<RedisMessage> children = msg.children();
assertThat(msg.children().size(), is(equalTo(3)));
assertThat(children.get(0), instanceOf(IntegerRedisMessage.class));
assertThat(((IntegerRedisMessage) children.get(0)).value(), is(1234L));
assertThat(children.get(1), instanceOf(SimpleStringRedisMessage.class));
assertThat(((SimpleStringRedisMessage) children.get(1)).content(), is("simple"));
assertThat(children.get(2), instanceOf(ErrorRedisMessage.class));
assertThat(((ErrorRedisMessage) children.get(2)).content(), is("error"));
ReferenceCountUtil.release(msg);
}
@Test
public void shouldDecodeNestedArray() throws Exception {
ByteBuf buf = Unpooled.buffer();
buf.writeBytes(byteBufOf("*2\r\n"));
buf.writeBytes(byteBufOf("*3\r\n:1\r\n:2\r\n:3\r\n"));
buf.writeBytes(byteBufOf("*2\r\n+Foo\r\n-Bar\r\n"));
assertTrue(channel.writeInbound(buf));
ArrayRedisMessage msg = channel.readInbound();
List<RedisMessage> children = msg.children();
assertThat(msg.children().size(), is(2));
ArrayRedisMessage intArray = (ArrayRedisMessage) children.get(0);
ArrayRedisMessage strArray = (ArrayRedisMessage) children.get(1);
assertThat(intArray.children().size(), is(3));
assertThat(((IntegerRedisMessage) intArray.children().get(0)).value(), is(1L));
assertThat(((IntegerRedisMessage) intArray.children().get(1)).value(), is(2L));
assertThat(((IntegerRedisMessage) intArray.children().get(2)).value(), is(3L));
assertThat(strArray.children().size(), is(2));
assertThat(((SimpleStringRedisMessage) strArray.children().get(0)).content(), is("Foo"));
assertThat(((ErrorRedisMessage) strArray.children().get(1)).content(), is("Bar"));
ReferenceCountUtil.release(msg);
}
@Test(expected = IllegalReferenceCountException.class)
public void shouldErrorOnDoubleReleaseArrayReferenceCounted() throws Exception {
ByteBuf buf = Unpooled.buffer();
buf.writeBytes(byteBufOf("*2\r\n"));
buf.writeBytes(byteBufOf("*3\r\n:1\r\n:2\r\n:3\r\n"));
buf.writeBytes(byteBufOf("*2\r\n+Foo\r\n-Bar\r\n"));
assertTrue(channel.writeInbound(buf));
ArrayRedisMessage msg = channel.readInbound();
ReferenceCountUtil.release(msg);
ReferenceCountUtil.release(msg);
}
@Test(expected = IllegalReferenceCountException.class)
public void shouldErrorOnReleaseArrayChildReferenceCounted() throws Exception {
ByteBuf buf = Unpooled.buffer();
buf.writeBytes(byteBufOf("*2\r\n"));
buf.writeBytes(byteBufOf("*3\r\n:1\r\n:2\r\n:3\r\n"));
buf.writeBytes(byteBufOf("$3\r\nFoo\r\n"));
assertTrue(channel.writeInbound(buf));
ArrayRedisMessage msg = channel.readInbound();
List<RedisMessage> children = msg.children();
ReferenceCountUtil.release(msg);
ReferenceCountUtil.release(children.get(1));
}
@Test(expected = IllegalReferenceCountException.class)
public void shouldErrorOnReleasecontentOfArrayChildReferenceCounted() throws Exception {
ByteBuf buf = Unpooled.buffer();
buf.writeBytes(byteBufOf("*2\r\n"));
buf.writeBytes(byteBufOf("$3\r\nFoo\r\n$3\r\nBar\r\n"));
assertTrue(channel.writeInbound(buf));
ArrayRedisMessage msg = channel.readInbound();
List<RedisMessage> children = msg.children();
ByteBuf childBuf = ((FullBulkStringRedisMessage) children.get(0)).content();
ReferenceCountUtil.release(msg);
ReferenceCountUtil.release(childBuf);
}
@Test
public void testPredefinedMessagesNotEqual() {
// both EMPTY_INSTANCE and NULL_INSTANCE have EMPTY_BUFFER as their 'data',
// however we need to check that they are not equal between themselves.
assertNotEquals(FullBulkStringRedisMessage.EMPTY_INSTANCE, FullBulkStringRedisMessage.NULL_INSTANCE);
assertNotEquals(FullBulkStringRedisMessage.NULL_INSTANCE, FullBulkStringRedisMessage.EMPTY_INSTANCE);
}
}