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< 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() } }