mukan-consensus/libs/bits/bit_array_test.go
Mukan Erkin Törük c6a41110d1
Some checks are pending
docker-build-cometbft / vars (push) Waiting to run
docker-build-cometbft / build-images (amd64, ubuntu-24.04) (push) Blocked by required conditions
docker-build-cometbft / build-images (arm64, ubuntu-24.04-arm) (push) Blocked by required conditions
docker-build-cometbft / merge-images (push) Blocked by required conditions
docker-build-e2e-node / vars (push) Waiting to run
docker-build-e2e-node / build-images (amd64, ubuntu-24.04) (push) Blocked by required conditions
docker-build-e2e-node / build-images (arm64, ubuntu-24.04-arm) (push) Blocked by required conditions
docker-build-e2e-node / merge-images (push) Blocked by required conditions
refactor: replace all github.com upstream refs with git.cw.tr/mukan-network
2026-05-11 03:36:20 +03:00

447 lines
12 KiB
Go

package bits
import (
"bytes"
"encoding/json"
"fmt"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
cmtrand "git.cw.tr/mukan-network/mukan-consensus/libs/rand"
)
var (
empty16Bits = "________________"
empty64Bits = empty16Bits + empty16Bits + empty16Bits + empty16Bits
full16bits = "xxxxxxxxxxxxxxxx"
full64bits = full16bits + full16bits + full16bits + full16bits
)
func randBitArray(bits int) *BitArray {
src := cmtrand.Bytes((bits + 7) / 8)
srcIndexToBit := func(i int) bool {
return src[i/8]&(1<<uint(i%8)) > 0
}
return NewBitArrayFromFn(bits, srcIndexToBit)
}
func TestAnd(t *testing.T) {
bA1 := randBitArray(51)
bA2 := randBitArray(31)
bA3 := bA1.And(bA2)
var bNil *BitArray
require.Equal(t, bNil.And(bA1), (*BitArray)(nil))
require.Equal(t, bA1.And(nil), (*BitArray)(nil))
require.Equal(t, bNil.And(nil), (*BitArray)(nil))
if bA3.Bits != 31 {
t.Error("Expected min bits", bA3.Bits)
}
if len(bA3.Elems) != len(bA2.Elems) {
t.Error("Expected min elems length")
}
for i := 0; i < bA3.Bits; i++ {
expected := bA1.GetIndex(i) && bA2.GetIndex(i)
if bA3.GetIndex(i) != expected {
t.Error("Wrong bit from bA3", i, bA1.GetIndex(i), bA2.GetIndex(i), bA3.GetIndex(i))
}
}
}
func TestOr(t *testing.T) {
bA1 := randBitArray(57)
bA2 := randBitArray(31)
bA3 := bA1.Or(bA2)
bNil := (*BitArray)(nil)
require.Equal(t, bNil.Or(bA1), bA1)
require.Equal(t, bA1.Or(nil), bA1)
require.Equal(t, bNil.Or(nil), (*BitArray)(nil))
if bA3.Bits != 57 {
t.Error("Expected max bits")
}
if len(bA3.Elems) != len(bA1.Elems) {
t.Error("Expected max elems length")
}
for i := 0; i < bA3.Bits; i++ {
expected := bA1.GetIndex(i) || bA2.GetIndex(i)
if bA3.GetIndex(i) != expected {
t.Error("Wrong bit from bA3", i, bA1.GetIndex(i), bA2.GetIndex(i), bA3.GetIndex(i))
}
}
if bA3.getNumTrueIndices() == 0 {
t.Error("Expected at least one true bit. " +
"This has a false positive rate that is less than 1 in 2^80 (cryptographically improbable).")
}
}
func TestSub(t *testing.T) {
testCases := []struct {
initBA string
subtractingBA string
expectedBA string
}{
{`null`, `null`, `null`},
{`"x"`, `null`, `null`},
{`null`, `"x"`, `null`},
{`"x"`, `"x"`, `"_"`},
{`"xxxxxx"`, `"x_x_x_"`, `"_x_x_x"`},
{`"x_x_x_"`, `"xxxxxx"`, `"______"`},
{`"xxxxxx"`, `"x_x_x_xxxx"`, `"_x_x_x"`},
{`"x_x_x_xxxx"`, `"xxxxxx"`, `"______xxxx"`},
{`"xxxxxxxxxx"`, `"x_x_x_"`, `"_x_x_xxxxx"`},
{`"x_x_x_"`, `"xxxxxxxxxx"`, `"______"`},
}
for _, tc := range testCases {
var bA *BitArray
err := json.Unmarshal([]byte(tc.initBA), &bA)
require.Nil(t, err)
var o *BitArray
err = json.Unmarshal([]byte(tc.subtractingBA), &o)
require.Nil(t, err)
got, _ := json.Marshal(bA.Sub(o))
require.Equal(
t,
tc.expectedBA,
string(got),
"%s minus %s doesn't equal %s",
tc.initBA,
tc.subtractingBA,
tc.expectedBA,
)
}
}
func TestPickRandom(t *testing.T) {
testCases := []struct {
bA string
ok bool
}{
{`null`, false},
{`"x"`, true},
{`"` + empty16Bits + `"`, false},
{`"x` + empty16Bits + `"`, true},
{`"` + empty16Bits + `x"`, true},
{`"x` + empty16Bits + `x"`, true},
{`"` + empty64Bits + `"`, false},
{`"x` + empty64Bits + `"`, true},
{`"` + empty64Bits + `x"`, true},
{`"x` + empty64Bits + `x"`, true},
{`"` + empty64Bits + `___x"`, true},
}
for _, tc := range testCases {
var bitArr *BitArray
err := json.Unmarshal([]byte(tc.bA), &bitArr)
require.NoError(t, err)
_, ok := bitArr.PickRandom()
require.Equal(t, tc.ok, ok, "PickRandom got an unexpected result on input %s", tc.bA)
}
}
func TestGetNumTrueIndices(t *testing.T) {
type testcase struct {
Input string
ExpectedResult int
}
testCases := []testcase{
{"x_x_x_", 3},
{"______", 0},
{"xxxxxx", 6},
{"x_x_x_x_x_x_x_x_x_", 9},
}
numOriginalTestCases := len(testCases)
for i := 0; i < numOriginalTestCases; i++ {
testCases = append(testCases, testcase{testCases[i].Input + "x", testCases[i].ExpectedResult + 1})
testCases = append(testCases, testcase{full64bits + testCases[i].Input, testCases[i].ExpectedResult + 64})
testCases = append(testCases, testcase{empty64Bits + testCases[i].Input, testCases[i].ExpectedResult})
}
for _, tc := range testCases {
var bitArr *BitArray
err := json.Unmarshal([]byte(`"`+tc.Input+`"`), &bitArr)
require.NoError(t, err)
result := bitArr.getNumTrueIndices()
require.Equal(t, tc.ExpectedResult, result, "for input %s, expected %d, got %d", tc.Input, tc.ExpectedResult, result)
result = bitArr.Not().getNumTrueIndices()
require.Equal(t, bitArr.Bits-result, bitArr.getNumTrueIndices())
}
}
func TestGetNumTrueIndicesInvalidStates(t *testing.T) {
testCases := []struct {
name string
bA1 *BitArray
exp int
}{
{"empty", &BitArray{}, 0},
{"explicit 0 bits nil elements", &BitArray{Bits: 0, Elems: nil}, 0},
{"explicit 0 bits 0 len elements", &BitArray{Bits: 0, Elems: make([]uint64, 0)}, 0},
{"nil", nil, 0},
{"with elements", NewBitArray(10), 0},
{"more elements than bits specifies", &BitArray{Bits: 0, Elems: make([]uint64, 5)}, 0},
{"less elements than bits specifies", &BitArray{Bits: 200, Elems: make([]uint64, 1)}, 0},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
n := tc.bA1.getNumTrueIndices()
require.Equal(t, n, tc.exp)
})
}
}
func TestGetNthTrueIndex(t *testing.T) {
type testcase struct {
Input string
N int
ExpectedResult int
}
testCases := []testcase{
// Basic cases
{"x_x_x_", 0, 0},
{"x_x_x_", 1, 2},
{"x_x_x_", 2, 4},
{"______", 1, -1}, // No true indices
{"xxxxxx", 5, 5}, // Last true index
{"x_x_x_x_x_x_x_", 9, -1}, // Out-of-range
// Edge cases
{"xxxxxx", 7, -1}, // Out-of-range
{"______", 0, -1}, // No true indices
{"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", 49, 49}, // Last true index
{"____________________________________________", 1, -1}, // No true indices
{"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", 63, 63}, // last index of first word
{"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", 64, 64}, // first index of second word
{"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", 100, -1}, // Out-of-range
// Input beyond 64 bits
{"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", 99, 99}, // Last true index
// Input less than 64 bits
{"x_x_x_", 3, -1}, // Out-of-range
}
numOriginalTestCases := len(testCases)
// Add 64 underscores to each test case
for i := 0; i < numOriginalTestCases; i++ {
expectedResult := testCases[i].ExpectedResult
if expectedResult != -1 {
expectedResult += 64
}
testCases = append(testCases, testcase{empty64Bits + testCases[i].Input, testCases[i].N, expectedResult})
}
for _, tc := range testCases {
var bitArr *BitArray
err := json.Unmarshal([]byte(`"`+tc.Input+`"`), &bitArr)
require.NoError(t, err)
// Get the nth true index
result := bitArr.getNthTrueIndex(tc.N)
require.Equal(t, tc.ExpectedResult, result, "for bit array %s, input %d, expected %d, got %d", tc.Input, tc.N, tc.ExpectedResult, result)
}
}
func TestBytes(t *testing.T) {
bA := NewBitArray(4)
bA.SetIndex(0, true)
check := func(bA *BitArray, bz []byte) {
if !bytes.Equal(bA.Bytes(), bz) {
panic(fmt.Sprintf("Expected %X but got %X", bz, bA.Bytes()))
}
}
check(bA, []byte{0x01})
bA.SetIndex(3, true)
check(bA, []byte{0x09})
bA = NewBitArray(9)
check(bA, []byte{0x00, 0x00})
bA.SetIndex(7, true)
check(bA, []byte{0x80, 0x00})
bA.SetIndex(8, true)
check(bA, []byte{0x80, 0x01})
bA = NewBitArray(16)
check(bA, []byte{0x00, 0x00})
bA.SetIndex(7, true)
check(bA, []byte{0x80, 0x00})
bA.SetIndex(8, true)
check(bA, []byte{0x80, 0x01})
bA.SetIndex(9, true)
check(bA, []byte{0x80, 0x03})
bA = NewBitArray(4)
bA.Elems = nil
require.False(t, bA.SetIndex(1, true))
}
func TestEmptyFull(t *testing.T) {
ns := []int{47, 123}
for _, n := range ns {
bA := NewBitArray(n)
if !bA.IsEmpty() {
t.Fatal("Expected bit array to be empty")
}
for i := 0; i < n; i++ {
bA.SetIndex(i, true)
}
if !bA.IsFull() {
t.Fatal("Expected bit array to be full")
}
}
}
func TestUpdateNeverPanics(_ *testing.T) {
newRandBitArray := func(n int) *BitArray {
ba := randBitArray(n)
return ba
}
pairs := []struct {
a, b *BitArray
}{
{nil, nil},
{newRandBitArray(10), newRandBitArray(12)},
{newRandBitArray(23), newRandBitArray(23)},
{newRandBitArray(37), nil},
{nil, NewBitArray(10)},
}
for _, pair := range pairs {
a, b := pair.a, pair.b
a.Update(b)
b.Update(a)
}
}
func TestNewBitArrayNeverCrashesOnNegatives(_ *testing.T) {
bitList := []int{-127, -128, -1 << 31}
for _, bits := range bitList {
_ = NewBitArray(bits)
}
}
func TestJSONMarshalUnmarshal(t *testing.T) {
bA1 := NewBitArray(0)
bA2 := NewBitArray(1)
bA3 := NewBitArray(1)
bA3.SetIndex(0, true)
bA4 := NewBitArray(5)
bA4.SetIndex(0, true)
bA4.SetIndex(1, true)
testCases := []struct {
bA *BitArray
marshalledBA string
}{
{nil, `null`},
{bA1, `null`},
{bA2, `"_"`},
{bA3, `"x"`},
{bA4, `"xx___"`},
}
for _, tc := range testCases {
tc := tc
t.Run(tc.bA.String(), func(t *testing.T) {
bz, err := json.Marshal(tc.bA)
require.NoError(t, err)
assert.Equal(t, tc.marshalledBA, string(bz))
var unmarshalledBA *BitArray
err = json.Unmarshal(bz, &unmarshalledBA)
require.NoError(t, err)
if tc.bA == nil {
require.Nil(t, unmarshalledBA)
} else {
require.NotNil(t, unmarshalledBA)
assert.EqualValues(t, tc.bA.Bits, unmarshalledBA.Bits)
if assert.EqualValues(t, tc.bA.String(), unmarshalledBA.String()) {
assert.EqualValues(t, tc.bA.Elems, unmarshalledBA.Elems)
}
}
})
}
}
func TestBitArrayProtoBuf(t *testing.T) {
testCases := []struct {
msg string
bA1 *BitArray
expPass bool
}{
{"success empty", &BitArray{}, true},
{"success", NewBitArray(1), true},
{"success", NewBitArray(2), true},
{"negative", NewBitArray(-1), false},
}
for _, tc := range testCases {
protoBA := tc.bA1.ToProto()
ba := new(BitArray)
ba.FromProto(protoBA)
if tc.expPass {
require.Equal(t, tc.bA1, ba, tc.msg)
} else {
require.NotEqual(t, tc.bA1, ba, tc.msg)
}
}
}
func TestBitArrayValidateBasic(t *testing.T) {
testCases := []struct {
name string
bA1 *BitArray
expPass bool
}{
{"valid empty", &BitArray{}, true},
{"valid explicit 0 bits nil elements", &BitArray{Bits: 0, Elems: nil}, true},
{"valid explicit 0 bits 0 len elements", &BitArray{Bits: 0, Elems: make([]uint64, 0)}, true},
{"valid nil", nil, true},
{"valid with elements", NewBitArray(10), true},
{"more elements than bits specifies", &BitArray{Bits: 0, Elems: make([]uint64, 5)}, false},
{"less elements than bits specifies", &BitArray{Bits: 200, Elems: make([]uint64, 1)}, false},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
err := tc.bA1.ValidateBasic()
require.Equal(t, err == nil, tc.expPass)
})
}
}
// Tests that UnmarshalJSON doesn't crash when no bits are passed into the JSON.
// See issue https://git.cw.tr/mukan-network/mukan-consensus/issues/2658
func TestUnmarshalJSONDoesntCrashOnZeroBits(t *testing.T) {
type indexCorpus struct {
BitArray *BitArray `json:"ba"`
Index int `json:"i"`
}
ic := new(indexCorpus)
blob := []byte(`{"BA":""}`)
err := json.Unmarshal(blob, ic)
require.NoError(t, err)
require.Equal(t, ic.BitArray, &BitArray{Bits: 0, Elems: nil})
}
func BenchmarkPickRandomBitArray(b *testing.B) {
// A random 150 bit string to use as the benchmark bit array
benchmarkBitArrayStr := "_______xx__xxx_xx__x_xx_x_x_x__x_x_x_xx__xx__xxx__xx_x_xxx_x__xx____x____xx__xx____x_x__x_____xx_xx_xxxxxxx__xx_x_xxxx_x___x_xxxxx_xx__xxxx_xx_x___x_x"
var bitArr *BitArray
err := json.Unmarshal([]byte(`"`+benchmarkBitArrayStr+`"`), &bitArr)
require.NoError(b, err)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = bitArr.PickRandom()
}
}