ByteArrayBitStreamVarNumTest.java

package zserio.runtime.io;

import static org.hamcrest.CoreMatchers.allOf;
import static org.hamcrest.CoreMatchers.endsWith;
import static org.hamcrest.CoreMatchers.startsWith;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;

import java.io.IOException;

import org.junit.jupiter.api.Test;

public class ByteArrayBitStreamVarNumTest
{
    @Test
    public void varOutOfRange() throws IOException
    {
        writeReadTest(new WriteReadTestable() {
            @Override
            public void write(BitStreamWriter writer) throws IOException
            {
                // make sure writer is working
                writeSentinel(writer);

                IOException thrown = assertThrows(
                        IOException.class, () -> writer.writeVarUInt16((short)(((short)1) << (7 + 8))));
                assertThat(thrown.getMessage(),
                        allOf(startsWith("BitSizeOfCalculator: Value '"),
                                endsWith("' is out of range for varuint16!")));

                thrown = assertThrows(IOException.class, () -> writer.writeVarUInt32(1 << (7 + 7 + 7 + 8)));
                assertThat(thrown.getMessage(),
                        allOf(startsWith("BitSizeOfCalculator: Value '"),
                                endsWith("' is out of range for varuint32!")));

                thrown = assertThrows(
                        IOException.class, () -> writer.writeVarUInt64(1L << (7 + 7 + 7 + 7 + 7 + 7 + 7 + 8)));
                assertThat(thrown.getMessage(),
                        allOf(startsWith("BitSizeOfCalculator: Value '"),
                                endsWith("' is out of range for varuint64!")));

                thrown = assertThrows(
                        IOException.class, () -> writer.writeVarInt16((short)(((short)1) << (6 + 8))));
                assertThat(thrown.getMessage(),
                        allOf(startsWith("BitSizeOfCalculator: Value '"),
                                endsWith("' is out of range for varint16!")));

                thrown = assertThrows(IOException.class, () -> writer.writeVarInt32(1 << (6 + 7 + 7 + 8)));
                assertThat(thrown.getMessage(),
                        allOf(startsWith("BitSizeOfCalculator: Value '"),
                                endsWith("' is out of range for varint32!")));

                thrown = assertThrows(
                        IOException.class, () -> writer.writeVarInt64(1L << (6 + 7 + 7 + 7 + 7 + 7 + 7 + 8)));
                assertThat(thrown.getMessage(),
                        allOf(startsWith("BitSizeOfCalculator: Value '"),
                                endsWith("' is out of range for varint64!")));

                thrown = assertThrows(IOException.class, () -> writer.writeVarSize(1 << (2 + 7 + 7 + 7 + 8)));
                assertThat(thrown.getMessage(),
                        allOf(startsWith("BitSizeOfCalculator: Value '"),
                                endsWith("' is out of range for varsize!")));

                {
                    // overflow, 2^32 - 1 is too much ({ 0x83, 0xFF, 0xFF, 0xFF, 0xFF } is the maximum)
                    final byte[] buffer =
                            new byte[] {(byte)0x8F, (byte)0xFF, (byte)0xFF, (byte)0xFF, (byte)0xFF};
                    try (final ByteArrayBitStreamReader reader = new ByteArrayBitStreamReader(buffer))
                    {
                        thrown = assertThrows(IOException.class, () -> reader.readVarSize());
                        assertThat(thrown.getMessage(),
                                allOf(startsWith("ByteArrayBitStreamReader: Read value '"),
                                        endsWith("' is out of range for varsize type!")));
                    }
                }

                {
                    // overflow, 2^36 - 1 is too much ({ 0x83, 0xFF, 0xFF, 0xFF, 0xFF } is the maximum)
                    final byte[] buffer =
                            new byte[] {(byte)0xFF, (byte)0xFF, (byte)0xFF, (byte)0xFF, (byte)0xFF};
                    try (final ByteArrayBitStreamReader reader = new ByteArrayBitStreamReader(buffer))
                    {
                        thrown = assertThrows(IOException.class, () -> reader.readVarSize());
                        assertThat(thrown.getMessage(),
                                allOf(startsWith("ByteArrayBitStreamReader: Read value '"),
                                        endsWith("' is out of range for varsize type!")));
                    }
                }
            }

            @Override
            public void read(BitStreamReader reader) throws IOException
            {
                readSentinel(reader);
            }
        });
    }

    @Test
    public void varInt64() throws IOException
    {
        final long varInt64Limits[] = {
                (1L << 0),
                (1L << 6) - 1,
                (1L << 6),
                (1L << (6 + 7)) - 1,
                (1L << (6 + 7)),
                (1L << (6 + 7 + 7)) - 1,
                (1L << (6 + 7 + 7)),
                (1L << (6 + 7 + 7 + 7)) - 1,
                (1L << (6 + 7 + 7 + 7)),
                (1L << (6 + 7 + 7 + 7 + 7)) - 1,
                (1L << (6 + 7 + 7 + 7 + 7)),
                (1L << (6 + 7 + 7 + 7 + 7 + 7)) - 1,
                (1L << (6 + 7 + 7 + 7 + 7 + 7)),
                (1L << (6 + 7 + 7 + 7 + 7 + 7 + 7)) - 1,
                (1L << (6 + 7 + 7 + 7 + 7 + 7 + 7)),
                (1L << (6 + 7 + 7 + 7 + 7 + 7 + 7 + 8)) - 1,
        };

        writeReadTest(new WriteReadTestable() {
            @Override
            public void write(BitStreamWriter writer) throws IOException
            {
                for (int i = 0; i < sanityVarNums.length; i++)
                {
                    writeSentinel(writer);
                    writer.writeVarInt64(sanityVarNums[i]);
                    writer.writeVarInt64(-sanityVarNums[i]);
                }

                for (int i = 0; i < varInt64Limits.length; i++)
                {
                    writeSentinel(writer);
                    writer.writeVarInt64(varInt64Limits[i]);
                    writer.writeVarInt64(-varInt64Limits[i]);
                }

                writeSentinel(writer);
            }

            @Override
            public void read(BitStreamReader reader) throws IOException
            {
                for (int i = 0; i < sanityVarNums.length; i++)
                {
                    readSentinel(reader);
                    assertEquals(sanityVarNums[i], reader.readVarInt64());
                    assertEquals(-sanityVarNums[i], reader.readVarInt64());
                }

                for (int i = 0; i < varInt64Limits.length; i++)
                {
                    readSentinel(reader);
                    assertEquals(varInt64Limits[i], reader.readVarInt64());
                    assertEquals(-varInt64Limits[i], reader.readVarInt64());
                }

                readSentinel(reader);
            }
        });
    }

    @Test
    public void varInt32() throws IOException
    {
        final int varInt32Limits[] = {
                (1 << 0),
                (1 << 6) - 1,
                (1 << 6),
                (1 << (6 + 7)) - 1,
                (1 << (6 + 7)),
                (1 << (6 + 7 + 7)) - 1,
                (1 << (6 + 7 + 7)),
                (1 << (6 + 7 + 7 + 8)) - 1,
        };

        writeReadTest(new WriteReadTestable() {
            @Override
            public void write(BitStreamWriter writer) throws IOException
            {
                for (int i = 0; i < sanityVarNums.length; i++)
                {
                    writeSentinel(writer);
                    writer.writeVarInt32(sanityVarNums[i]);
                    writer.writeVarInt32(-sanityVarNums[i]);
                }

                for (int i = 0; i < varInt32Limits.length; i++)
                {
                    writeSentinel(writer);
                    writer.writeVarInt32(varInt32Limits[i]);
                    writer.writeVarInt32(-varInt32Limits[i]);
                }

                writeSentinel(writer);
            }

            @Override
            public void read(BitStreamReader reader) throws IOException
            {
                for (int i = 0; i < sanityVarNums.length; i++)
                {
                    readSentinel(reader);
                    assertEquals(sanityVarNums[i], reader.readVarInt32());
                    assertEquals(-sanityVarNums[i], reader.readVarInt32());
                }

                for (int i = 0; i < varInt32Limits.length; i++)
                {
                    readSentinel(reader);
                    assertEquals(varInt32Limits[i], reader.readVarInt32());
                    assertEquals(-varInt32Limits[i], reader.readVarInt32());
                }

                readSentinel(reader);
            }
        });
    }

    @Test
    public void varInt16() throws IOException
    {
        final short varInt16Limits[] = {
                (((short)1) << 0),
                (((short)1) << 6) - 1,
                (((short)1) << 6),
                (((short)1) << (6 + 8)) - 1,
        };

        writeReadTest(new WriteReadTestable() {
            @Override
            public void write(BitStreamWriter writer) throws IOException
            {
                for (int i = 0; i < sanityVarNums.length; i++)
                {
                    writeSentinel(writer);
                    writer.writeVarInt16(sanityVarNums[i]);
                    writer.writeVarInt16((short)-sanityVarNums[i]);
                }

                for (int i = 0; i < varInt16Limits.length; i++)
                {
                    writeSentinel(writer);
                    writer.writeVarInt16(varInt16Limits[i]);
                    writer.writeVarInt16((short)-varInt16Limits[i]);
                }

                writeSentinel(writer);
            }

            @Override
            public void read(BitStreamReader reader) throws IOException
            {
                for (int i = 0; i < sanityVarNums.length; i++)
                {
                    readSentinel(reader);
                    assertEquals(sanityVarNums[i], reader.readVarInt16());
                    assertEquals(-sanityVarNums[i], reader.readVarInt16());
                }

                for (int i = 0; i < varInt16Limits.length; i++)
                {
                    readSentinel(reader);
                    assertEquals(varInt16Limits[i], reader.readVarInt16());
                    assertEquals(-varInt16Limits[i], reader.readVarInt16());
                }

                readSentinel(reader);
            }
        });
    }

    @Test
    public void varUInt64() throws IOException
    {
        final long varUInt64Limits[] = {
                (1L << 0),
                (1L << 7) - 1,
                (1L << 7),
                (1L << (7 + 7)) - 1,
                (1L << (7 + 7)),
                (1L << (7 + 7 + 7)) - 1,
                (1L << (7 + 7 + 7)),
                (1L << (7 + 7 + 7 + 7)) - 1,
                (1L << (7 + 7 + 7 + 7)),
                (1L << (7 + 7 + 7 + 7 + 7)) - 1,
                (1L << (7 + 7 + 7 + 7 + 7)),
                (1L << (7 + 7 + 7 + 7 + 7 + 7)) - 1,
                (1L << (7 + 7 + 7 + 7 + 7 + 7)),
                (1L << (7 + 7 + 7 + 7 + 7 + 7 + 7)) - 1,
                (1L << (7 + 7 + 7 + 7 + 7 + 7 + 7)),
                (1L << (7 + 7 + 7 + 7 + 7 + 7 + 7 + 8)) - 1,
        };

        writeReadTest(new WriteReadTestable() {
            @Override
            public void write(BitStreamWriter writer) throws IOException
            {
                for (int i = 0; i < sanityVarNums.length; i++)
                {
                    writeSentinel(writer);
                    writer.writeVarUInt64(sanityVarNums[i]);
                }

                for (int i = 0; i < varUInt64Limits.length; i++)
                {
                    writeSentinel(writer);
                    writer.writeVarUInt64(varUInt64Limits[i]);
                }

                writeSentinel(writer);
            }

            @Override
            public void read(BitStreamReader reader) throws IOException
            {
                for (int i = 0; i < sanityVarNums.length; i++)
                {
                    readSentinel(reader);
                    assertEquals(sanityVarNums[i], reader.readVarUInt64());
                }

                for (int i = 0; i < varUInt64Limits.length; i++)
                {
                    readSentinel(reader);
                    assertEquals(varUInt64Limits[i], reader.readVarUInt64());
                }

                readSentinel(reader);
            }
        });
    }

    @Test
    public void varUInt32() throws IOException
    {
        final int varUInt32Limits[] = {
                (1 << 0),
                (1 << 7) - 1,
                (1 << 7),
                (1 << (7 + 7)) - 1,
                (1 << (7 + 7)),
                (1 << (7 + 7 + 7)) - 1,
                (1 << (7 + 7 + 7)),
                (1 << (7 + 7 + 7 + 8)) - 1,
        };

        writeReadTest(new WriteReadTestable() {
            @Override
            public void write(BitStreamWriter writer) throws IOException
            {
                for (int i = 0; i < sanityVarNums.length; i++)
                {
                    writeSentinel(writer);
                    writer.writeVarUInt32(sanityVarNums[i]);
                }

                for (int i = 0; i < varUInt32Limits.length; i++)
                {
                    writeSentinel(writer);
                    writer.writeVarUInt32(varUInt32Limits[i]);
                }

                writeSentinel(writer);
            }

            @Override
            public void read(BitStreamReader reader) throws IOException
            {
                for (int i = 0; i < sanityVarNums.length; i++)
                {
                    readSentinel(reader);
                    assertEquals(sanityVarNums[i], reader.readVarUInt32());
                }

                for (int i = 0; i < varUInt32Limits.length; i++)
                {
                    readSentinel(reader);
                    assertEquals(varUInt32Limits[i], reader.readVarUInt32());
                }

                readSentinel(reader);
            }
        });
    }

    @Test
    public void varUInt16() throws IOException
    {
        final short varUInt16Limits[] = {
                (((short)1) << 0),
                (((short)1) << 7) - 1,
                (((short)1) << 7),
                (((short)1) << (7 + 8)) - 1,
        };

        writeReadTest(new WriteReadTestable() {
            @Override
            public void write(BitStreamWriter writer) throws IOException
            {
                for (int i = 0; i < sanityVarNums.length; i++)
                {
                    writeSentinel(writer);
                    writer.writeVarUInt16(sanityVarNums[i]);
                }

                for (int i = 0; i < varUInt16Limits.length; i++)
                {
                    writeSentinel(writer);
                    writer.writeVarUInt16(varUInt16Limits[i]);
                }

                writeSentinel(writer);
            }

            @Override
            public void read(BitStreamReader reader) throws IOException
            {
                for (int i = 0; i < sanityVarNums.length; i++)
                {
                    readSentinel(reader);
                    assertEquals(sanityVarNums[i], reader.readVarUInt16());
                }
                for (int i = 0; i < varUInt16Limits.length; i++)
                {
                    readSentinel(reader);
                    assertEquals(varUInt16Limits[i], reader.readVarUInt16());
                }

                readSentinel(reader);
            }
        });
    }

    @Test
    public void varSize() throws IOException
    {
        final int varSizeLimits[] = {
                (1 << 0),
                (1 << 7) - 1,
                (1 << 7),
                (1 << (7 + 7)) - 1,
                (1 << (7 + 7)),
                (1 << (7 + 7 + 7)) - 1,
                (1 << (7 + 7 + 7)),
                (1 << (7 + 7 + 7 + 7)) - 1,
                (1 << (7 + 7 + 7 + 7)),
                (1 << (2 + 7 + 7 + 7 + 8)) - 1,
        };

        writeReadTest(new WriteReadTestable() {
            @Override
            public void write(BitStreamWriter writer) throws IOException
            {
                for (int i = 0; i < sanityVarNums.length; i++)
                {
                    writeSentinel(writer);
                    writer.writeVarSize(sanityVarNums[i]);
                }

                for (int i = 0; i < varSizeLimits.length; i++)
                {
                    writeSentinel(writer);
                    writer.writeVarSize(varSizeLimits[i]);
                }

                writeSentinel(writer);
            }

            @Override
            public void read(BitStreamReader reader) throws IOException
            {
                for (int i = 0; i < sanityVarNums.length; i++)
                {
                    readSentinel(reader);
                    assertEquals(sanityVarNums[i], reader.readVarSize());
                }

                for (int i = 0; i < varSizeLimits.length; i++)
                {
                    readSentinel(reader);
                    assertEquals(varSizeLimits[i], reader.readVarSize());
                }

                readSentinel(reader);
            }
        });
    }

    private interface WriteReadTestable
    {
        void write(BitStreamWriter writer) throws IOException;
        void read(BitStreamReader reader) throws IOException;
    }

    private void writeReadTest(WriteReadTestable writeReadTest) throws IOException
    {
        try (final ByteArrayBitStreamWriter writer = new ByteArrayBitStreamWriter())
        {
            writeReadTest.write(writer);
            final byte[] data = writer.toByteArray();

            try (final ByteArrayBitStreamReader reader = new ByteArrayBitStreamReader(data))
            {
                writeReadTest.read(reader);
            }
        }
    }

    private static void writeSentinel(BitStreamWriter writer) throws IOException
    {
        writer.writeByte((byte)0x00);
        writer.writeByte((byte)0xFF);
        writer.writeByte((byte)0x00);
    }

    private static void readSentinel(BitStreamReader reader) throws IOException
    {
        assertEquals((byte)0x00, reader.readByte());
        assertEquals((byte)0xFF, reader.readByte());
        assertEquals((byte)0x00, reader.readByte());
    }

    private static short sanityVarNums[] = {0, 10, 100};
}