ByteArrayBitStreamWriterTest.java
package zserio.runtime.io;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.math.BigInteger;
import javax.imageio.stream.ImageInputStream;
import javax.imageio.stream.MemoryCacheImageInputStream;
import org.junit.jupiter.api.Test;
public class ByteArrayBitStreamWriterTest
{
@Test
public void test1() throws Exception
{
writeReadTest(new WriteReadTestable() {
@Override
public void write(ByteArrayBitStreamWriter writer) throws IOException
{
for (int value : DATA)
{
writer.writeBits(value, 4);
}
}
@Override
public void read(ImageInputStream reader) throws IOException
{
for (int value : DATA)
{
assertEquals(value, reader.readBits(4));
}
}
private final int[] DATA = {
0x6,
0x7,
0x8,
0x9,
0x1,
0x2,
0x3,
0x4,
0xc,
0xd,
0xe,
0xf,
};
});
}
@Test
public void test2() throws Exception
{
writeReadTest(new WriteReadTestable() {
@Override
public void write(ByteArrayBitStreamWriter writer) throws IOException
{
writer.writeBits(6, 4);
writer.writeBits(0x78, 8);
writer.writeBits(0x91, 8);
writer.writeBits(0x23, 8);
writer.writeBits(0x4c, 8);
writer.writeBits(0xde, 8);
writer.writeBits(0xf, 4);
}
@Override
public void read(ImageInputStream reader) throws IOException
{
assertEquals(0x6, reader.readBits(4));
assertEquals(0x7, reader.readBits(4));
assertEquals(0x8, reader.readBits(4));
assertEquals(0x9, reader.readBits(4));
assertEquals(0x1, reader.readBits(4));
assertEquals(0x2, reader.readBits(4));
assertEquals(0x3, reader.readBits(4));
assertEquals(0x4, reader.readBits(4));
assertEquals(0xc, reader.readBits(4));
assertEquals(0xd, reader.readBits(4));
assertEquals(0xe, reader.readBits(4));
assertEquals(0xf, reader.readBits(4));
}
});
}
@Test
public void test3() throws Exception
{
writeReadTest(new WriteReadTestable() {
@Override
public void write(ByteArrayBitStreamWriter writer) throws IOException
{
writer.writeBits(6, 4);
writer.writeShort((short)0x7891);
writer.writeShort((short)0x234c);
writer.writeBits(0xd, 4);
writer.writeBits(0xef, 8);
}
@Override
public void read(ImageInputStream reader) throws IOException
{
assertEquals(0x6, reader.readBits(4));
assertEquals(0x7, reader.readBits(4));
assertEquals(0x8, reader.readBits(4));
assertEquals(0x9, reader.readBits(4));
assertEquals(0x1, reader.readBits(4));
assertEquals(0x2, reader.readBits(4));
assertEquals(0x3, reader.readBits(4));
assertEquals(0x4, reader.readBits(4));
assertEquals(0xc, reader.readBits(4));
assertEquals(0xd, reader.readBits(4));
assertEquals(0xe, reader.readBits(4));
assertEquals(0xf, reader.readBits(4));
}
});
}
@Test
public void test4() throws Exception
{
try (final ByteArrayBitStreamWriter writer = new ByteArrayBitStreamWriter())
{
writer.writeShort((short)0x234c);
writer.writeBits(0xef, 8);
writer.alignTo(32);
final byte[] b = writer.toByteArray();
assertEquals(b.length * 8L, 32);
}
}
@Test
public void writeUnalignedData() throws IOException
{
// number expected to be written at offset
final int testValue = 123;
for (int offset = 0; offset <= 63; ++offset)
{
final int bufferByteSize = (8 + offset + 7) / 8;
try (final ByteArrayBitStreamWriter writer = new ByteArrayBitStreamWriter(bufferByteSize))
{
// fill the buffer with 1s to check proper masking
for (int i = 0; i < bufferByteSize; ++i)
writer.writeBits(0xFF, 8);
writer.setBitPosition(0);
if (offset != 0)
writer.writeBits(0, offset);
writer.writeBits(testValue, 8);
// check written value
byte[] writtenData = writer.toByteArray();
int writtenTestValue = ((int)writtenData[offset / 8]) << (offset % 8);
if (offset % 8 != 0)
writtenTestValue |= (0xFF & writtenData[offset / 8 + 1]) >>> (8 - (offset % 8));
assertEquals(testValue, writtenTestValue, "offset: " + offset);
}
}
}
@Test
public void writeByte() throws IOException
{
writeReadTest(new WriteReadTestable() {
@Override
public void write(ByteArrayBitStreamWriter writer) throws IOException
{
for (byte value : DATA)
{
writer.writeByte(value);
}
}
@Override
public void read(ImageInputStream reader) throws IOException
{
for (byte value : DATA)
{
assertEquals(value, reader.readByte());
}
}
private final byte[] DATA = {
0,
1,
-1,
Byte.MAX_VALUE,
Byte.MIN_VALUE,
};
});
}
@Test
public void writeShort() throws IOException
{
writeReadTest(new WriteReadTestable() {
@Override
public void write(ByteArrayBitStreamWriter writer) throws IOException
{
for (short value : DATA)
{
writer.writeShort(value);
}
}
@Override
public void read(ImageInputStream reader) throws IOException
{
for (short value : DATA)
{
assertEquals(value, reader.readShort());
}
}
private final short[] DATA = {
0,
1,
-1,
Short.MAX_VALUE,
Short.MIN_VALUE,
};
});
}
@Test
public void writeInt() throws IOException
{
writeReadTest(new WriteReadTestable() {
@Override
public void write(ByteArrayBitStreamWriter writer) throws IOException
{
for (int value : DATA)
{
writer.writeInt(value);
}
}
@Override
public void read(ImageInputStream reader) throws IOException
{
for (int value : DATA)
{
assertEquals(value, reader.readInt());
}
}
private final int[] DATA = {
0,
1,
-1,
Integer.MIN_VALUE,
Integer.MAX_VALUE,
127,
137,
};
});
}
@Test
public void writeLong() throws IOException
{
writeReadTest(new WriteReadTestable() {
@Override
public void write(ByteArrayBitStreamWriter writer) throws IOException
{
for (long value : DATA)
{
writer.writeLong(value);
}
}
@Override
public void read(ImageInputStream reader) throws IOException
{
for (long value : DATA)
{
assertEquals(value, reader.readLong());
}
}
private final long[] DATA = {
0,
1,
-1,
Long.MAX_VALUE,
Long.MIN_VALUE,
1111111111L,
1212121212L,
};
});
}
@Test
public void writeUnsignedInt() throws IOException
{
writeReadTest(new WriteReadTestable() {
@Override
public void write(ByteArrayBitStreamWriter writer) throws IOException
{
for (long value : DATA)
{
writer.writeUnsignedInt(value);
}
}
@Override
public void read(ImageInputStream reader) throws IOException
{
for (long value : DATA)
{
assertEquals(value, reader.readUnsignedInt());
}
}
private final long[] DATA = {
0,
1,
Integer.MAX_VALUE,
};
});
}
@Test
public void writeUnsignedByte() throws IOException
{
writeReadTest(new WriteReadTestable() {
@Override
public void write(ByteArrayBitStreamWriter writer) throws IOException
{
for (short value : DATA)
{
writer.writeUnsignedByte(value);
}
}
@Override
public void read(ImageInputStream reader) throws IOException
{
for (short value : DATA)
{
assertEquals(value, reader.readUnsignedByte());
}
}
private final short[] DATA = {
5,
Byte.MAX_VALUE,
};
});
}
@Test
public void writeBigInteger() throws IOException
{
writeReadTest(new WriteReadTestable() {
@Override
public void write(ByteArrayBitStreamWriter writer) throws IOException
{
writer.writeBigInteger(BigInteger.valueOf(0x1), 4);
writer.writeBigInteger(BigInteger.valueOf(0x7), 4);
writer.writeBigInteger(BigInteger.valueOf(0x7f), 8);
writer.writeBigInteger(BigInteger.valueOf(0x7fff), 16);
writer.writeBigInteger(BigInteger.valueOf(0x7fffffff), 32);
writer.writeBigInteger(BigInteger.valueOf(0x7fffffffffffffffL), 64);
}
@Override
public void read(ImageInputStream reader) throws IOException
{
final int[] expectedBytes = {
0x17, // 0x1, 0x7 nibbles combined
0x7f,
0x7f,
0xff,
0x7f,
0xff,
0xff,
0xff,
0x7f,
0xff,
0xff,
0xff,
0xff,
0xff,
0xff,
0xff,
};
for (int value : expectedBytes)
{
assertEquals(value, reader.readUnsignedByte());
}
}
});
}
@Test
public void writeBigIntegerInvalidNumException() throws IOException
{
try (final ByteArrayBitStreamWriter writer = new ByteArrayBitStreamWriter())
{
final int numBits[] = {-1, 0}; //, 65};
for (int i = 0; i < numBits.length; ++i)
{
final int numBitsArg = numBits[i];
assertThrows(IllegalArgumentException.class,
() -> writer.writeBigInteger(BigInteger.ZERO, numBitsArg));
assertThrows(IllegalArgumentException.class,
() -> writer.writeBigInteger(BigInteger.ONE, numBitsArg));
} // for numbits
}
}
@Test
public void writeFloat16() throws IOException
{
writeReadTest(new WriteReadTestable() {
@Override
public void write(ByteArrayBitStreamWriter writer) throws IOException
{
writer.writeFloat16(1.0f);
writer.writeFloat16(2.0f);
}
@Override
public void read(ImageInputStream reader) throws IOException
{
// hand-encoded values
assertEquals(0x3c00, reader.readUnsignedShort());
assertEquals(0x4000, reader.readUnsignedShort());
}
});
}
/**
* Test capacity.
*
* @throws IOException if the writings and readings fail
*/
@Test
public void capacity() throws IOException
{
try (final ByteArrayBitStreamWriter writer = new ByteArrayBitStreamWriter())
{
writer.writeInt(10);
writer.writeLong(10);
try (final ByteArrayBitStreamReader reader = new ByteArrayBitStreamReader(writer.toByteArray()))
{
assertEquals(10, reader.readInt());
assertEquals(10, reader.readLong());
}
}
try (final ByteArrayBitStreamWriter writer = new ByteArrayBitStreamWriter(1234))
{
writer.writeByte((byte)127);
writer.writeBits(7, 4);
writer.writeInt(123);
writer.writeLong(12345678910L);
try (final ByteArrayBitStreamReader reader = new ByteArrayBitStreamReader(writer.toByteArray()))
{
assertEquals((byte)127, reader.readByte());
assertEquals(7, reader.readBits(4));
assertEquals(123, reader.readInt());
assertEquals(12345678910L, reader.readLong());
}
}
assertThrows(IllegalArgumentException.class, () -> new ByteArrayBitStreamWriter(Integer.MAX_VALUE));
assertThrows(IllegalArgumentException.class, () -> new ByteArrayBitStreamWriter(-1));
}
/**
* Test the writeBool method.
*
* @throws IOException if the writing fails
*/
@Test
public void writeBool() throws IOException
{
writeReadTest(new WriteReadTestable() {
@Override
public void write(ByteArrayBitStreamWriter writer) throws IOException
{
for (boolean value : DATA)
{
writer.writeBool(value);
}
}
@Override
public void read(ImageInputStream reader) throws IOException
{
for (boolean value : DATA)
{
assertEquals(value, reader.readBit() != 0 ? true : false);
}
assertEquals(DATA.length, getBitOffset(reader));
}
private final boolean[] DATA = {
true,
false,
};
});
}
/**
* Test the growBuffer method.
*
* @throws IOException if the ByteArrayBitStreamWriter cannot be closed
*/
@Test
public void growBuffer() throws IOException
{
try (final ByteArrayBitStreamWriter writer = new ByteArrayBitStreamWriter())
{
for (int i = 0; i < 8191; i++)
{
writer.writeByte((byte)1);
}
assertEquals(8191, writer.getBytePosition());
}
}
@Test
public void writeBitsInvalidNumException() throws IOException
{
try (final ByteArrayBitStreamWriter writer = new ByteArrayBitStreamWriter())
{
final int numBits[] = {-1, 0, 65};
for (int i = 0; i < numBits.length; ++i)
{
final int numBitsArg = numBits[i];
assertThrows(IllegalArgumentException.class, () -> writer.writeBits(0x0L, numBitsArg));
assertThrows(IllegalArgumentException.class, () -> writer.writeBits(0x1L, numBitsArg));
assertThrows(IllegalArgumentException.class, () -> writer.writeSignedBits(0x0L, numBitsArg));
assertThrows(IllegalArgumentException.class, () -> writer.writeSignedBits(0x1L, numBitsArg));
} // for numbits
}
}
@Test
public void writeBitsIllegalArgumentException() throws IOException
{
try (final ByteArrayBitStreamWriter writer = new ByteArrayBitStreamWriter())
{
for (int i = 1; i < 64; i++)
{
final long minSigned = -(1L << (i - 1));
final long maxUnsigned = (1L << (i)) - 1;
final long minSignedViolation = minSigned - 1;
final long maxUnsignedViolation = maxUnsigned + 1;
writer.writeBits(maxUnsigned, i);
writer.writeSignedBits(minSigned, i);
final int iArg = i;
assertThrows(IllegalArgumentException.class,
()
-> writer.writeSignedBits(minSignedViolation, iArg),
"unexpected success writeBits: " + minSignedViolation + " # " + i);
assertThrows(IllegalArgumentException.class,
()
-> writer.writeBits(maxUnsignedViolation, iArg),
"unexpected success writeBits: " + maxUnsignedViolation + " # " + i);
} // for numBits
}
}
@Test
public void writeIllegalArgumentException() throws IOException
{
try (final ByteArrayBitStreamWriter writer = new ByteArrayBitStreamWriter())
{
assertThrows(IllegalArgumentException.class, () -> writer.writeUnsignedByte((short)-1));
assertThrows(IllegalArgumentException.class, () -> writer.writeUnsignedShort(-1));
assertThrows(IllegalArgumentException.class, () -> writer.writeUnsignedInt(-1L));
assertThrows(IllegalArgumentException.class, () -> writer.writeUnsignedByte((short)(1 << 8)));
assertThrows(IllegalArgumentException.class, () -> writer.writeUnsignedShort(1 << 16));
assertThrows(IllegalArgumentException.class, () -> writer.writeUnsignedInt(1L << 32));
// Note: no range check for writeBigInteger
}
}
@Test
public void writeVarInt16() throws IOException
{
writeReadTest(new WriteReadTestable() {
@Override
public void write(ByteArrayBitStreamWriter writer) throws IOException
{
// 1 byte
writer.writeVarInt16((short)0);
writer.writeVarInt16((short)+0x3f);
writer.writeVarInt16((short)-0x3f);
// 2 bytes
writer.writeVarInt16((short)+0x7f);
writer.writeVarInt16((short)-0x7f);
writer.writeVarInt16((short)+0x3fff);
writer.writeVarInt16((short)-0x3fff);
}
@Override
public void read(ImageInputStream reader) throws IOException
{
// 1 byte
assertEquals(0x00, reader.readBits(8)); // 0
assertEquals(0x3f, reader.readBits(8)); // 111111b = 0x3f
assertEquals(0xbf, reader.readBits(8)); // -111111b = -0x3f
// 2 bytes
assertEquals(0x407f, reader.readBits(16)); // 0b1111111 = 0x7f
assertEquals(0xc07f, reader.readBits(16)); // -0b1111111 = -0x7f
assertEquals(0x7fff, reader.readBits(16)); // 0b11111111111111 = 0x3fff
assertEquals(0xffff, reader.readBits(16)); // -0b11111111111111 = -0x3fff
}
});
}
@Test
public void writeVarInt32() throws IOException
{
writeReadTest(new WriteReadTestable() {
@Override
public void write(ByteArrayBitStreamWriter writer) throws IOException
{
// 1 byte
writer.writeVarInt32(0);
writer.writeVarInt32(+0x3f);
writer.writeVarInt32(-0x3f);
// 2 bytes
writer.writeVarInt32(+0x7f);
writer.writeVarInt32(-0x7f);
writer.writeVarInt32(+0x1fff);
writer.writeVarInt32(-0x1fff);
// 3 bytes
writer.writeVarInt32(+0x3fff);
writer.writeVarInt32(-0x3fff);
writer.writeVarInt32(+0xfffff);
writer.writeVarInt32(-0xfffff);
// 4 bytes
writer.writeVarInt32(+0x3fffff);
writer.writeVarInt32(-0x3fffff);
writer.writeVarInt32(+0xfffffff);
writer.writeVarInt32(-0xfffffff);
}
@Override
public void read(ImageInputStream reader) throws IOException
{
// 1 byte
assertEquals(0x00, reader.readBits(8)); // 0
assertEquals(0x3f, reader.readBits(8)); // 111111b = 0x3f
assertEquals(0xbf, reader.readBits(8)); // -111111b = -0x3f
// 2 bytes
assertEquals(0x407f, reader.readBits(16)); // 0b1111111 = 0x7f
assertEquals(0xc07f, reader.readBits(16)); // -0b1111111 = -0x7f
assertEquals(0x7f7f, reader.readBits(16)); // 0b1111111111111 = 0x1fff
assertEquals(0xff7f, reader.readBits(16)); // -0b1111111111111 = -0x1fff
// 3 bytes
assertEquals(0x40ff7fL, reader.readBits(24)); // 0b11111111111111 = 0x3fff
assertEquals(0xc0ff7fL, reader.readBits(24)); // -0b11111111111111 = -0x3fff
assertEquals(0x7fff7fL, reader.readBits(24)); // 0b11111111111111 = 0xfffff
assertEquals(0xffff7fL, reader.readBits(24)); // -0b11111111111111 = -0xfffff
// 4 bytes
assertEquals(0x40ffffffL, reader.readBits(32)); // 0x1fffff
assertEquals(0xc0ffffffL, reader.readBits(32)); // -0x1fffff
assertEquals(0x7fffffffL, reader.readBits(32)); // 0xfffffff
assertEquals(0xffffffffL, reader.readBits(32)); // -0xfffffff
}
});
}
@Test
public void writeVarUInt16() throws IOException
{
writeReadTest(new WriteReadTestable() {
@Override
public void write(ByteArrayBitStreamWriter writer) throws IOException
{
// 1 byte
writer.writeVarUInt16((short)0);
writer.writeVarUInt16((short)0x7f);
// 2 bytes
writer.writeVarUInt16((short)0xff);
writer.writeVarUInt16((short)0x7fff);
}
@Override
public void read(ImageInputStream reader) throws IOException
{
// 1 byte
assertEquals(0x00, reader.readBits(8)); // 0
assertEquals(0x7f, reader.readBits(8)); // 1111111b = 0x7f
// 2 bytes
assertEquals(0x80ff, reader.readBits(16)); // 0b11111111 = 0xff
assertEquals(0xffff, reader.readBits(16)); // 0b111111111111111 = 0x7fff
}
});
}
@Test
public void writeVarUInt32() throws IOException
{
writeReadTest(new WriteReadTestable() {
@Override
public void write(ByteArrayBitStreamWriter writer) throws IOException
{
// 1 byte
writer.writeVarUInt32(0);
writer.writeVarUInt32(0x7f);
// 2 bytes
writer.writeVarUInt32(0xff);
writer.writeVarUInt32(0x3fff);
// 3 bytes
writer.writeVarUInt32(0x7fff);
writer.writeVarUInt32(0x1fffff);
// 4 bytes
writer.writeVarUInt32(0x3fffff);
writer.writeVarUInt32(0x1fffffff);
}
@Override
public void read(ImageInputStream reader) throws IOException
{
// 1 byte
assertEquals(0x00, reader.readBits(8)); // 0
assertEquals(0x7f, reader.readBits(8)); // 1111111b = 0x7f
// 2 bytes
assertEquals(0x817f, reader.readBits(16));
assertEquals(0xff7f, reader.readBits(16));
// 3 bytes
assertEquals(0x81ff7f, reader.readBits(24));
assertEquals(0xffff7f, reader.readBits(24));
// 4 bytes
assertEquals(0x80ffffffL, reader.readBits(32));
assertEquals(0xffffffffL, reader.readBits(32));
}
});
}
@Test
public void writeVarSize() throws IOException
{
writeReadTest(new WriteReadTestable() {
@Override
public void write(ByteArrayBitStreamWriter writer) throws IOException
{
// 1 byte
writer.writeVarSize(0);
writer.writeVarSize(0x7f);
// 2 bytes
writer.writeVarSize(0xff);
writer.writeVarSize(0x3fff);
// 3 bytes
writer.writeVarSize(0x7fff);
writer.writeVarSize(0x1fffff);
// 4 bytes
writer.writeVarSize(0x3fffff);
writer.writeVarSize(0xfffffff);
// 5 bytes
writer.writeVarSize(0x1fffffff);
writer.writeVarSize(0x7fffffff);
}
@Override
public void read(ImageInputStream reader) throws IOException
{
// 1 byte
assertEquals(0x00, reader.readBits(8)); // 0
assertEquals(0x7f, reader.readBits(8)); // 1111111b = 0x7f
// 2 bytes
assertEquals(0x817f, reader.readBits(16));
assertEquals(0xff7f, reader.readBits(16));
// 3 bytes
assertEquals(0x81ff7f, reader.readBits(24));
assertEquals(0xffff7f, reader.readBits(24));
// 4 bytes
assertEquals(0x81ffff7fL, reader.readBits(32));
assertEquals(0xffffff7fL, reader.readBits(32));
// 5 bytes
assertEquals(0x80ffffffffL, reader.readBits(40));
assertEquals(0x83ffffffffL, reader.readBits(40));
}
});
}
/**
* Describes a test method.
*/
private enum TestMethod
{
/**
* Test method: write aligned.
*/
ALIGNED,
/**
* Test method: write unaligned.
*/
UNALIGNED;
}
private interface WriteReadTestable
{
// don't use BitStreamReader so that this tests solely the writer
void write(ByteArrayBitStreamWriter writer) throws IOException;
void read(ImageInputStream reader) throws IOException;
}
private void writeReadTest(WriteReadTestable writeReadTest) throws IOException
{
for (final TestMethod method : TestMethod.values())
{
try (final ByteArrayBitStreamWriter writer = new ByteArrayBitStreamWriter())
{
if (method == TestMethod.UNALIGNED)
{
writer.writeBits(1, 1);
}
writeReadTest.write(writer);
final byte[] data = writer.toByteArray();
if (method == TestMethod.UNALIGNED)
trimBitFromLeft(data);
try (final ByteArrayInputStream inputStream = new ByteArrayInputStream(data);
final MemoryCacheImageInputStream reader =
new MemoryCacheImageInputStream(inputStream);)
{
writeReadTest.read(reader);
}
}
}
}
private static void trimBitFromLeft(byte[] data)
{
byte carry = 0;
for (int i = data.length; i > 0; --i)
{
final int currentValue = data[i - 1] & 0xff; // prevent sign extension later (signed byte->int)
data[i - 1] = (byte)((currentValue << 1) | carry);
carry = (byte)(currentValue >>> 7); // MSB move to carry
}
// the last carry bit is trimmed off
}
private static long getBitOffset(ImageInputStream inputStream) throws IOException
{
return 8 * inputStream.getStreamPosition() + inputStream.getBitOffset();
}
}