blob: d8f13dfb1eeed3cd84e2cb70e86187e886d9357d [file]
// Licensed to Apache Software Foundation (ASF) under one or more contributor
// license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright
// ownership. Apache Software Foundation (ASF) licenses this file to you under
// the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package encoding
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestEncodeAndDecodeRLE(t *testing.T) {
arr := []uint32{1, 1, 2, 2, 3, 2, 2, 2, 1, 2, 1}
encoded := make([]uint32, 0)
encoded = encodeRLE(encoded, arr)
require.Equal(t, []uint32{1, 2, 2, 2, 3, 1, 2, 3, 1, 1, 2, 1, 1, 1}, encoded)
decoded := make([]uint32, 0)
decoded = decodeRLE(decoded, encoded)
require.Equal(t, arr, decoded)
}
func TestEncodeAndDecodeBitPacking(t *testing.T) {
arr := []uint32{1, 2, 2, 2, 3, 1, 2, 3, 1, 1, 2, 1, 1, 1}
encoded := encodeBitPacking(arr)
decoded := make([]uint32, 0)
decoded, err := decodeBitPacking(decoded, encoded)
require.NoError(t, err)
require.Equal(t, arr, decoded)
}
func TestEncodeAndDecodeDictionary(t *testing.T) {
dict := NewDictionary()
values := [][]byte{
[]byte("skywalking"),
[]byte("banyandb"),
[]byte("hello"),
[]byte("world"),
[]byte("hello"),
}
for _, value := range values {
dict.Add(value)
}
encoded := dict.Encode(nil)
decoded := make([][]byte, 0)
dict.Reset()
decoded, err := dict.Decode(decoded, encoded, 5)
require.NoError(t, err)
expectedValues := [][]byte{[]byte("skywalking"), []byte("banyandb"), []byte("hello"), []byte("world")}
require.Equal(t, expectedValues, dict.values)
expectedIndices := []uint32{0, 1, 2, 3, 2}
require.Equal(t, expectedIndices, dict.indices)
require.Equal(t, values, decoded)
}
func TestEncodeAndDecodeDictionaryEdgeCases(t *testing.T) {
tests := []struct {
name string
input [][]byte
expected [][]byte
}{
{
name: "nil_slice",
input: [][]byte{nil},
expected: [][]byte{nil},
},
{
name: "empty_slice",
input: [][]byte{{}},
expected: [][]byte{{}},
},
{
name: "single_byte",
input: [][]byte{[]byte("a")},
expected: [][]byte{[]byte("a")},
},
{
name: "mixed_nil_and_empty",
input: [][]byte{nil, {}},
expected: [][]byte{nil, {}},
},
{
name: "mixed_empty_and_content",
input: [][]byte{{}, []byte("hello")},
expected: [][]byte{{}, []byte("hello")},
},
{
name: "all_nil",
input: [][]byte{nil, nil, nil},
expected: [][]byte{nil, nil, nil},
},
{
name: "all_empty",
input: [][]byte{{}, {}, {}},
expected: [][]byte{{}, {}, {}},
},
{
name: "nil_empty_and_content",
input: [][]byte{nil, {}, []byte("test"), nil, []byte("value")},
expected: [][]byte{nil, {}, []byte("test"), nil, []byte("value")},
},
{
name: "duplicate_empty_strings",
input: [][]byte{{}, []byte("a"), {}, []byte("a")},
expected: [][]byte{{}, []byte("a"), {}, []byte("a")},
},
{
name: "duplicate_nil_values",
input: [][]byte{nil, []byte("b"), nil, []byte("b")},
expected: [][]byte{nil, []byte("b"), nil, []byte("b")},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
dict := NewDictionary()
for _, value := range tt.input {
dict.Add(value)
}
encoded := dict.Encode(nil)
require.NotNil(t, encoded)
decodedDict := NewDictionary()
decoded, err := decodedDict.Decode(nil, encoded, uint64(len(tt.input)))
require.NoError(t, err)
require.Len(t, decoded, len(tt.expected))
for i, expected := range tt.expected {
if expected == nil {
assert.Nil(t, decoded[i], "index %d should be nil", i)
} else {
assert.NotNil(t, decoded[i], "index %d should not be nil", i)
assert.Equal(t, expected, decoded[i], "index %d should match expected", i)
}
}
})
}
}
func TestEncodeAndDecodeDictionaryRoundTripConsistency(t *testing.T) {
testCases := [][][]byte{
// Basic cases
{nil},
{[]byte{}},
{[]byte("a")},
{[]byte("hello world")},
// Mixed scenarios
{nil, []byte{}, []byte("a")},
{[]byte{}, nil, []byte("hello")},
{nil, []byte{}, nil, []byte("test"), []byte{}},
// Edge cases with different lengths
{[]byte{}, []byte("x"), []byte("xx"), []byte("xxx")},
{nil, nil, []byte{}, []byte{}, []byte("a"), []byte("ab")},
// Large content
{[]byte("this is a longer string for testing purposes")},
{nil, []byte("mixed with nil and empty"), []byte{}, []byte("another string")},
// Duplicate values (dictionary compression)
{[]byte("hello"), []byte("world"), []byte("hello"), []byte("world")},
{nil, []byte("test"), nil, []byte("test")},
{[]byte{}, []byte("value"), []byte{}, []byte("value")},
}
for i, original := range testCases {
t.Run(fmt.Sprintf("round_trip_%d", i), func(t *testing.T) {
// Encode
dict := NewDictionary()
for _, value := range original {
dict.Add(value)
}
encoded := dict.Encode(nil)
require.NotNil(t, encoded)
// Decode
decodedDict := NewDictionary()
decoded, err := decodedDict.Decode(nil, encoded, uint64(len(original)))
require.NoError(t, err)
require.Len(t, decoded, len(original))
// Verify round-trip consistency
for j, orig := range original {
if orig == nil {
assert.Nil(t, decoded[j], "position %d: nil should remain nil", j)
} else {
assert.NotNil(t, decoded[j], "position %d: non-nil should remain non-nil", j)
assert.Equal(t, orig, decoded[j], "position %d: content should match", j)
}
}
// Test that we can encode the decoded result again and get the same encoded bytes
reEncodedDict := NewDictionary()
for _, value := range decoded {
reEncodedDict.Add(value)
}
reEncoded := reEncodedDict.Encode(nil)
assert.Equal(t, encoded, reEncoded, "re-encoding decoded result should produce identical bytes")
})
}
}
func TestEncodeAndDecodeDictionaryNilEmptyStringDistinction(t *testing.T) {
t.Run("nil vs empty string distinction", func(t *testing.T) {
// Test that nil and empty string produce different encoded results
nilDict := NewDictionary()
nilDict.Add(nil)
nilEncoded := nilDict.Encode(nil)
emptyDict := NewDictionary()
emptyDict.Add([]byte{})
emptyEncoded := emptyDict.Encode(nil)
// They should be different (nil uses 0, empty uses 1)
assert.NotEqual(t, nilEncoded, emptyEncoded)
// Decode and verify they decode to different results
nilDecodedDict := NewDictionary()
nilDecoded, err := nilDecodedDict.Decode(nil, nilEncoded, 1)
require.NoError(t, err)
assert.Len(t, nilDecoded, 1)
assert.Nil(t, nilDecoded[0])
emptyDecodedDict := NewDictionary()
emptyDecoded, err := emptyDecodedDict.Decode(nil, emptyEncoded, 1)
require.NoError(t, err)
assert.Len(t, emptyDecoded, 1)
assert.NotNil(t, emptyDecoded[0])
assert.Equal(t, []byte{}, emptyDecoded[0])
})
t.Run("mixed nil and empty in same dictionary", func(t *testing.T) {
values := [][]byte{nil, {}, []byte("test"), nil, {}}
dict := NewDictionary()
for _, value := range values {
dict.Add(value)
}
encoded := dict.Encode(nil)
require.NotNil(t, encoded)
decodedDict := NewDictionary()
decoded, err := decodedDict.Decode(nil, encoded, uint64(len(values)))
require.NoError(t, err)
require.Len(t, decoded, len(values))
// Verify nil values remain nil
assert.Nil(t, decoded[0], "first value should be nil")
assert.Nil(t, decoded[3], "fourth value should be nil")
// Verify empty strings remain non-nil empty
assert.NotNil(t, decoded[1], "second value should not be nil")
assert.Equal(t, []byte{}, decoded[1], "second value should be empty string")
assert.NotNil(t, decoded[4], "fifth value should not be nil")
assert.Equal(t, []byte{}, decoded[4], "fifth value should be empty string")
// Verify content values
assert.Equal(t, []byte("test"), decoded[2], "third value should be 'test'")
})
}
type parameter struct {
count int
cardinality int
}
var pList = [3]parameter{
{count: 1000, cardinality: 100},
{count: 10000, cardinality: 100},
{count: 100000, cardinality: 100},
}
func BenchmarkEncodeDictionary(b *testing.B) {
b.ReportAllocs()
for _, p := range pList {
b.Run(fmt.Sprintf("size=%d_cardinality=%d", p.count, p.cardinality), func(b *testing.B) {
values, rawSize := generateData(p.count, p.cardinality)
dict := NewDictionary()
for _, value := range values {
dict.Add(value)
}
encoded := make([]byte, 0)
encoded = dict.Encode(encoded)
compressedSize := len(encoded)
compressRatio := float64(rawSize) / float64(compressedSize)
b.ResetTimer()
b.ReportMetric(compressRatio, "compress_ratio")
for i := 0; i < b.N; i++ {
dict.Encode(encoded[:0])
}
})
}
}
func BenchmarkDecodeDictionary(b *testing.B) {
b.ReportAllocs()
for _, p := range pList {
b.Run(fmt.Sprintf("size=%d_cardinality=%d", p.count, p.cardinality), func(b *testing.B) {
values, _ := generateData(p.count, p.cardinality)
dict := NewDictionary()
for _, value := range values {
dict.Add(value)
}
encoded := make([]byte, 0)
encoded = dict.Encode(encoded)
b.ResetTimer()
decoded := NewDictionary()
for i := 0; i < b.N; i++ {
decoded.Reset()
_, err := decoded.Decode(values[:0], encoded, uint64(p.count))
if err != nil {
b.Fatal(err)
}
}
})
}
}
func generateData(count int, cardinality int) ([][]byte, int) {
values := make([][]byte, 0)
size := 0
for i := 0; i < count; i++ {
values = append(values, []byte(fmt.Sprintf("value_%d", i%cardinality)))
size += len(values[i])
}
return values, size
}