167 lines
5.5 KiB
Go
167 lines
5.5 KiB
Go
package chunkenc
|
|
|
|
import (
|
|
"fmt"
|
|
"math/rand"
|
|
"testing"
|
|
)
|
|
|
|
// bstreamBytes writes (value, nbits) pairs and returns the encoded bytes.
|
|
func bstreamBytes(pairs [][2]uint64) []byte {
|
|
var b bstream
|
|
for _, p := range pairs {
|
|
b.writeBits(p[0], int(p[1]))
|
|
}
|
|
return b.bytes()
|
|
}
|
|
|
|
func TestBstream(t *testing.T) {
|
|
rnd := rand.New(rand.NewSource(42))
|
|
|
|
type readOp struct {
|
|
nbits int
|
|
wantVal uint64
|
|
wantErr error
|
|
}
|
|
|
|
ok := func(val uint64, nbits int) readOp { return readOp{nbits, val, nil} }
|
|
short := func(nbits int) readOp { return readOp{nbits, 0, ErrShortStream} }
|
|
|
|
tests := []struct {
|
|
name string
|
|
data []byte
|
|
reads []readOp
|
|
}{
|
|
// Error paths: raw bytes, reads that exceed the data.
|
|
{name: "nil_read_1", data: nil, reads: []readOp{short(1)}},
|
|
{name: "nil_read_64", data: nil, reads: []readOp{short(64)}},
|
|
{name: "nil_read_0", data: nil, reads: []readOp{ok(0, 0)}},
|
|
{name: "1_byte_read_16", data: []byte{0xFF}, reads: []readOp{short(16)}},
|
|
{name: "7_bytes_read_64", data: []byte{1, 2, 3, 4, 5, 6, 7}, reads: []readOp{short(64)}},
|
|
{name: "exact_byte_then_past_end", data: []byte{0xAB}, reads: []readOp{ok(0xAB, 8), short(1)}},
|
|
{name: "two_bytes_then_past_end", data: []byte{0xAB, 0xCD}, reads: []readOp{ok(0xAB, 8), ok(0xCD, 8), short(1)}},
|
|
{name: "partial_bits_then_short", data: []byte{0xFF}, reads: []readOp{ok(0b1111, 4), short(8)}},
|
|
{name: "8_bytes_exact_64", data: []byte{0xDE, 0xAD, 0xBE, 0xEF, 0xCA, 0xFE, 0xBA, 0xBE}, reads: []readOp{ok(0xDEADBEEFCAFEBABE, 64), short(1)}},
|
|
|
|
// Round-trip: write via bstream, read back.
|
|
{name: "zero_width", data: bstreamBytes([][2]uint64{{0, 0}}), reads: []readOp{ok(0, 0)}},
|
|
{name: "single_bit_true", data: bstreamBytes([][2]uint64{{1, 1}}), reads: []readOp{ok(1, 1)}},
|
|
{name: "single_bit_false", data: bstreamBytes([][2]uint64{{0, 1}}), reads: []readOp{ok(0, 1)}},
|
|
{name: "single_byte", data: bstreamBytes([][2]uint64{{0xAB, 8}}), reads: []readOp{ok(0xAB, 8)}},
|
|
{name: "full_64_bits", data: bstreamBytes([][2]uint64{{0xDEADBEEFCAFEBABE, 64}}), reads: []readOp{ok(0xDEADBEEFCAFEBABE, 64)}},
|
|
{name: "all_ones_8", data: bstreamBytes([][2]uint64{{0xFF, 8}}), reads: []readOp{ok(0xFF, 8)}},
|
|
{name: "all_zeros_8", data: bstreamBytes([][2]uint64{{0, 8}}), reads: []readOp{ok(0, 8)}},
|
|
{name: "all_ones_64", data: bstreamBytes([][2]uint64{{^uint64(0), 64}}), reads: []readOp{ok(^uint64(0), 64)}},
|
|
{name: "all_zeros_64", data: bstreamBytes([][2]uint64{{0, 64}}), reads: []readOp{ok(0, 64)}},
|
|
{
|
|
name: "byte_aligned_sequence",
|
|
data: bstreamBytes([][2]uint64{{0xAA, 8}, {0xBB, 8}, {0xCC, 8}}),
|
|
reads: []readOp{ok(0xAA, 8), ok(0xBB, 8), ok(0xCC, 8)},
|
|
},
|
|
{
|
|
name: "non_aligned_crossing_boundary",
|
|
data: bstreamBytes([][2]uint64{{0b101, 3}, {0b11001, 5}, {0xFF, 8}, {1, 1}}),
|
|
reads: []readOp{ok(0b101, 3), ok(0b11001, 5), ok(0xFF, 8), ok(1, 1)},
|
|
},
|
|
{
|
|
name: "alternating_single_bits",
|
|
data: bstreamBytes([][2]uint64{{1, 1}, {0, 1}, {1, 1}, {0, 1}, {1, 1}, {0, 1}, {1, 1}, {0, 1}}),
|
|
reads: []readOp{
|
|
ok(1, 1), ok(0, 1), ok(1, 1), ok(0, 1),
|
|
ok(1, 1), ok(0, 1), ok(1, 1), ok(0, 1),
|
|
},
|
|
},
|
|
{
|
|
name: "sequential_64_bit_writes",
|
|
data: bstreamBytes([][2]uint64{{0x0123456789ABCDEF, 64}, {0xFEDCBA9876543210, 64}, {0, 64}}),
|
|
reads: []readOp{ok(0x0123456789ABCDEF, 64), ok(0xFEDCBA9876543210, 64), ok(0, 64)},
|
|
},
|
|
{
|
|
name: "every_width_1_through_64",
|
|
data: func() []byte {
|
|
pairs := make([][2]uint64, 64)
|
|
for i := range pairs {
|
|
nbits := i + 1
|
|
val := uint64(1)
|
|
if nbits > 1 {
|
|
val |= uint64(1) << (nbits - 1)
|
|
}
|
|
pairs[i] = [2]uint64{val, uint64(nbits)}
|
|
}
|
|
return bstreamBytes(pairs)
|
|
}(),
|
|
reads: func() []readOp {
|
|
ops := make([]readOp, 64)
|
|
for i := range ops {
|
|
nbits := i + 1
|
|
val := uint64(1)
|
|
if nbits > 1 {
|
|
val |= uint64(1) << (nbits - 1)
|
|
}
|
|
ops[i] = ok(val, nbits)
|
|
}
|
|
return ops
|
|
}(),
|
|
},
|
|
{
|
|
name: "high_bits_ignored",
|
|
data: bstreamBytes([][2]uint64{{0xFFFFFFFFFFFFFFFF, 1}, {0xFFFFFFFFFFFFFFFF, 4}, {0xFFFFFFFFFFFFFFFF, 8}}),
|
|
reads: []readOp{ok(1, 1), ok(0xF, 4), ok(0xFF, 8)},
|
|
},
|
|
{
|
|
name: "mixed_widths",
|
|
data: bstreamBytes([][2]uint64{{0b1, 1}, {0xABCD, 16}, {0, 1}, {0xFF, 8}, {0b110, 3}, {0xDEADBEEF, 32}, {1, 1}, {0, 0}}),
|
|
reads: []readOp{ok(0b1, 1), ok(0xABCD, 16), ok(0, 1), ok(0xFF, 8), ok(0b110, 3), ok(0xDEADBEEF, 32), ok(1, 1), ok(0, 0)},
|
|
},
|
|
}
|
|
|
|
// Random round-trip cases.
|
|
for i := 0; i < 100; i++ {
|
|
n := rnd.Intn(200) + 1
|
|
pairs := make([][2]uint64, n)
|
|
reads := make([]readOp, n)
|
|
for j := range pairs {
|
|
nbits := rnd.Intn(64) + 1
|
|
val := rnd.Uint64() & (^uint64(0) >> (64 - uint(nbits)))
|
|
pairs[j] = [2]uint64{val, uint64(nbits)}
|
|
reads[j] = ok(val, nbits)
|
|
}
|
|
tests = append(tests, struct {
|
|
name string
|
|
data []byte
|
|
reads []readOp
|
|
}{
|
|
name: fmt.Sprintf("random_%03d", i),
|
|
data: bstreamBytes(pairs),
|
|
reads: reads,
|
|
})
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
r := newBReader(tc.data)
|
|
for i, op := range tc.reads {
|
|
got, err := r.readBits(op.nbits)
|
|
if got != op.wantVal {
|
|
t.Errorf("op %d val (nbits=%d): got %v, want %v", i, op.nbits, got, op.wantVal)
|
|
}
|
|
if err != op.wantErr {
|
|
t.Errorf("op %d err (nbits=%d): got %v, want %v", i, op.nbits, err, op.wantErr)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func FuzzBstreamReader(f *testing.F) {
|
|
f.Fuzz(func(t *testing.T, data []byte, nbits uint8) {
|
|
r := newBReader(data)
|
|
n := int(nbits%64) + 1
|
|
for {
|
|
if _, err := r.readBits(n); err != nil {
|
|
break // must terminate via error, never panic
|
|
}
|
|
}
|
|
})
|
|
}
|