// Copyright (c) 2019-2021 The Decred developers // Use of this source code is governed by an ISC // license that can be found in the LICENSE file. package gcs import ( "bytes" "encoding/hex" "errors" "io" "testing" ) // TestBitWriter ensures the bit writer and all associated methods work as // expected including all corner cases at byte boundaries. func TestBitWriter(t *testing.T) { // perWriterTest describes a test to run against the same bitstream writer. type perWriterTest struct { name string // test description bits uint64 // bits to write one at a time nBits uint // number of individual bits to write val uint64 // uint64 value from which to write nValBits nValBits uint // number of bits of the val to write wantBytes string // expected bytes in writer as of this test } tests := []struct { name string // test description perWriterTests []perWriterTest // tests to run against same writer }{{ name: "1 bit - 0", perWriterTests: []perWriterTest{{ name: "1st bit", bits: 0x0, nBits: 1, wantBytes: "00", }}, }, { name: "1 bit - 1", perWriterTests: []perWriterTest{{ name: "1st bit", bits: 0x1, nBits: 1, wantBytes: "80", }}, }, { name: "2 bits - 00", perWriterTests: []perWriterTest{{ name: "1st and 2nd bits", bits: 0x0, nBits: 2, wantBytes: "00", }}, }, { name: "2 bits - 01", perWriterTests: []perWriterTest{{ name: "1st and 2nd bits", bits: 0x1, nBits: 2, wantBytes: "40", }}, }, { name: "9 bits - 010101010", perWriterTests: []perWriterTest{{ name: "first 9 bits", bits: 0x0aa, nBits: 9, wantBytes: "5500", }}, }, { name: "9 bits - 010101011", perWriterTests: []perWriterTest{{ name: "first 9 bits", bits: 0x0ab, nBits: 9, wantBytes: "5580", }}, }, { name: "2 bits, 3 bits of uint64 - 01 110", perWriterTests: []perWriterTest{{ name: "first 2 bits", bits: 0x1, nBits: 2, wantBytes: "40", }, { name: "3 bits of uint64", val: 6, nValBits: 3, wantBytes: "70", }}, }, { name: "7 bits, 2 bits of uint64 - 1111111 00", perWriterTests: []perWriterTest{{ name: "first 7 bits", bits: 0x7f, nBits: 7, wantBytes: "fe", }, { name: "2 bits of uint64", val: 0, nValBits: 2, wantBytes: "fe00", }}, }, { name: "7 bits, 11 bits of uint64 - 1111111 00100000101", perWriterTests: []perWriterTest{{ name: "first 7 bits", bits: 0x7f, nBits: 7, wantBytes: "fe", }, { name: "11 bits of uint64", val: 261, nValBits: 11, wantBytes: "fe4140", }}, }, { name: "7 bits of uint64, 2 bits - 1010101 11", perWriterTests: []perWriterTest{{ name: "7 bits of uint64", val: 85, nValBits: 7, wantBytes: "aa", }, { name: "last 2 bits", bits: 0x3, nBits: 2, wantBytes: "ab80", }}, }} nextTest: for _, test := range tests { var w bitWriter for _, pwTest := range test.perWriterTests { // Start by writing the specified individual bits one a time. bitSource := pwTest.bits for i := uint(0); i < pwTest.nBits; i++ { switch (bitSource >> (pwTest.nBits - 1 - i)) & 0x1 { case 0: w.writeZero() case 1: w.writeOne() } } // Write the specified number of bits from the uint64. w.writeNBits(pwTest.val, pwTest.nValBits) // Parse the expected serialized bytes and ensure they match. wantBytes, err := hex.DecodeString(pwTest.wantBytes) if err != nil { t.Errorf("%q-%q: unexpected err parsing want bytes hex: %v", test.name, pwTest.name, err) continue nextTest } resultBytes := w.bytes if !bytes.Equal(resultBytes, wantBytes) { t.Errorf("%q-%q: mismatched bytes -- got %x, want %x", test.name, pwTest.name, resultBytes, wantBytes) continue nextTest } } } } // TestBitReader ensures the bit reader and all associated methods work as // expected including expected errors and corner cases at byte boundaries. func TestBitReader(t *testing.T) { // perReaderTest describes a test to run against the same bitstream reader. type perReaderTest struct { name string // test description doUnary bool // whether or not to perform a unary read wantUnary uint64 // expected number of consecutive ones unaryErr error // expected error on unary read nValBits uint // number of bits to read from bitstream as uint64 wantVal uint64 // expected value from nValBits read bitsErr error // expected error on bits read } tests := []struct { name string // test description bytes string // bytes to use as the bitstream perReaderTests []perReaderTest // tests to run against same reader }{{ name: "unary read on empty bytes error", bytes: "", perReaderTests: []perReaderTest{{ name: "unary read", doUnary: true, wantUnary: 0, unaryErr: io.EOF, }}, }, { name: "0 bits read on empty bytes (no error)", bytes: "", perReaderTests: []perReaderTest{{ name: "0 bit read", nValBits: 0, wantVal: 0, }}, }, { name: "1 bit read on empty bytes error", bytes: "", perReaderTests: []perReaderTest{{ name: "1 bit read", nValBits: 1, bitsErr: io.EOF, }}, }, { name: "9 bit read on single byte error (straddle byte boundary)", bytes: "0f", perReaderTests: []perReaderTest{{ name: "9 bit read", nValBits: 9, bitsErr: io.EOF, }}, }, { name: "16 bit read on single byte error (byte boundary)", bytes: "0f", perReaderTests: []perReaderTest{{ name: "16 bit read", nValBits: 16, bitsErr: io.EOF, }}, }, { name: "0 bits followed by 8 bits ", bytes: "ff", perReaderTests: []perReaderTest{{ name: "0 bit read", nValBits: 0, wantVal: 0, }, { name: "8 bit read", nValBits: 8, wantVal: 0xff, }}, }, { name: "unary 1", bytes: "80", perReaderTests: []perReaderTest{{ name: "first unary read", doUnary: true, wantUnary: 1, }}, }, { name: "unary 2", bytes: "c0", perReaderTests: []perReaderTest{{ name: "first unary read", doUnary: true, wantUnary: 2, }}, }, { name: "unary 9 (more than one byte)", bytes: "ff80", perReaderTests: []perReaderTest{{ name: "first unary read", doUnary: true, wantUnary: 9, }}, }, { name: "unary 0, 1 bit read", bytes: "40", perReaderTests: []perReaderTest{{ name: "unary read", doUnary: true, wantUnary: 0, }, { name: "1 bit read", nValBits: 1, wantVal: 1, }}, }, { name: "unary 0, 8 bits read (straddle byte)", bytes: "5a80", perReaderTests: []perReaderTest{{ name: "unary read", doUnary: true, wantUnary: 0, }, { name: "8 bit read", nValBits: 8, wantVal: 0xb5, }}, }, { name: "unary 0, 15 bits read (byte boundary)", bytes: "5ac5", perReaderTests: []perReaderTest{{ name: "unary read", doUnary: true, wantUnary: 0, }, { name: "15 bit read", nValBits: 15, wantVal: 0x5ac5, }}, }, { name: "unary 0, 16 bits read (straddle 2nd byte boundary)", bytes: "5ac580", perReaderTests: []perReaderTest{{ name: "unary read", doUnary: true, wantUnary: 0, }, { name: "16 bit read", nValBits: 16, wantVal: 0xb58b, }}, }, { name: "unary 3, 15 bits read, unary 2", bytes: "eac518", perReaderTests: []perReaderTest{{ name: "first unary read", doUnary: true, wantUnary: 3, }, { name: "15 bit read", nValBits: 15, wantVal: 0x5628, }, { name: "second unary read", doUnary: true, wantUnary: 2, }}, }} nextTest: for _, test := range tests { // Parse the specified bytes to read and create a bitstream reader from // them. testBytes, err := hex.DecodeString(test.bytes) if err != nil { t.Errorf("%q: unexpected err parsing bytes hex: %v", test.name, err) continue } r := newBitReader(testBytes) for _, prTest := range test.perReaderTests { // Read unary and ensure expected result if requested. if prTest.doUnary { gotUnary, err := r.readUnary() if !errors.Is(err, prTest.unaryErr) { t.Errorf("%q-%q: unexpected unary err -- got %v, want %v", test.name, prTest.name, err, prTest.unaryErr) continue nextTest } if err != nil { continue nextTest } if gotUnary != prTest.wantUnary { t.Errorf("%q-%q: unexpected unary value -- got %x, want %x", test.name, prTest.name, gotUnary, prTest.wantUnary) continue nextTest } } // Read specified number of bits as uint64 and ensure expected // result. gotVal, err := r.readNBits(prTest.nValBits) if !errors.Is(err, prTest.bitsErr) { t.Errorf("%q-%q: unexpected nbits err -- got %v, want %v", test.name, prTest.name, err, prTest.bitsErr) continue nextTest } if err != nil { continue nextTest } if gotVal != prTest.wantVal { t.Errorf("%q-%q: mismatched value -- got %x, want %x", test.name, prTest.name, gotVal, prTest.wantVal) continue nextTest } } } }