311 lines
8.1 KiB
Go
311 lines
8.1 KiB
Go
package index
|
|
|
|
import (
|
|
"bytes"
|
|
"reflect"
|
|
"testing"
|
|
|
|
"git.dvdt.dev/david/ingot/labels"
|
|
)
|
|
|
|
func writeIndex(t *testing.T, entries []SeriesEntry) []byte {
|
|
t.Helper()
|
|
var buf bytes.Buffer
|
|
w := NewWriter(&buf)
|
|
for _, e := range entries {
|
|
w.AddSeries(e)
|
|
}
|
|
_, err := w.WriteTo()
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
return buf.Bytes()
|
|
}
|
|
|
|
func TestIndexRoundTrip(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
entries []SeriesEntry
|
|
}{
|
|
{
|
|
name: "single_series_single_chunk",
|
|
entries: []SeriesEntry{
|
|
{
|
|
Ref: 1,
|
|
Labels: []labels.Label{{Name: "__name__", Value: "temp"}},
|
|
Chunks: []ChunkMeta{{MinT: 1000, MaxT: 2000, Ref: NewChunkRef(1, 0)}},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "single_series_multiple_chunks",
|
|
entries: []SeriesEntry{
|
|
{
|
|
Ref: 1,
|
|
Labels: []labels.Label{{Name: "__name__", Value: "temp"}, {Name: "room", Value: "office"}},
|
|
Chunks: []ChunkMeta{
|
|
{MinT: 1000, MaxT: 2000, Ref: NewChunkRef(1, 0)},
|
|
{MinT: 2001, MaxT: 3000, Ref: NewChunkRef(1, 500)},
|
|
{MinT: 3001, MaxT: 4000, Ref: NewChunkRef(1, 1000)},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "multiple_series",
|
|
entries: []SeriesEntry{
|
|
{
|
|
Ref: 1,
|
|
Labels: []labels.Label{{Name: "__name__", Value: "temp"}, {Name: "room", Value: "office"}},
|
|
Chunks: []ChunkMeta{{MinT: 1000, MaxT: 2000, Ref: NewChunkRef(1, 0)}},
|
|
},
|
|
{
|
|
Ref: 2,
|
|
Labels: []labels.Label{{Name: "__name__", Value: "humidity"}, {Name: "room", Value: "office"}},
|
|
Chunks: []ChunkMeta{{MinT: 1000, MaxT: 2000, Ref: NewChunkRef(1, 200)}},
|
|
},
|
|
{
|
|
Ref: 3,
|
|
Labels: []labels.Label{{Name: "__name__", Value: "temp"}, {Name: "room", Value: "kitchen"}},
|
|
Chunks: []ChunkMeta{{MinT: 1000, MaxT: 2000, Ref: NewChunkRef(1, 400)}},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "empty",
|
|
entries: nil,
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
data := writeIndex(t, tc.entries)
|
|
|
|
r, err := NewReader(data)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
// Verify series count and content.
|
|
gotSeries := r.Series()
|
|
if len(gotSeries) != len(tc.entries) {
|
|
t.Fatalf("got %v, want %v", len(gotSeries), len(tc.entries))
|
|
}
|
|
|
|
for i, want := range tc.entries {
|
|
got := gotSeries[i]
|
|
if got.Ref != want.Ref {
|
|
t.Errorf("series %d ref: got %v, want %v", i, got.Ref, want.Ref)
|
|
}
|
|
if !reflect.DeepEqual(got.Labels, want.Labels) {
|
|
t.Errorf("series %d labels: got %v, want %v", i, got.Labels, want.Labels)
|
|
}
|
|
if len(got.Chunks) != len(want.Chunks) {
|
|
t.Fatalf("series %d chunk count: got %v, want %v", i, len(got.Chunks), len(want.Chunks))
|
|
}
|
|
for j, wc := range want.Chunks {
|
|
if got.Chunks[j].MinT != wc.MinT {
|
|
t.Errorf("series %d chunk %d minT: got %v, want %v", i, j, got.Chunks[j].MinT, wc.MinT)
|
|
}
|
|
if got.Chunks[j].MaxT != wc.MaxT {
|
|
t.Errorf("series %d chunk %d maxT: got %v, want %v", i, j, got.Chunks[j].MaxT, wc.MaxT)
|
|
}
|
|
if got.Chunks[j].Ref != wc.Ref {
|
|
t.Errorf("series %d chunk %d ref: got %v, want %v", i, j, got.Chunks[j].Ref, wc.Ref)
|
|
}
|
|
}
|
|
|
|
// SeriesByRef lookup.
|
|
byRef, ok := r.SeriesByRef(want.Ref)
|
|
if !ok {
|
|
t.Errorf("series %d lookup by ref: got false, want true", i)
|
|
}
|
|
if byRef.Ref != want.Ref {
|
|
t.Errorf("got %v, want %v", byRef.Ref, want.Ref)
|
|
}
|
|
}
|
|
|
|
// Verify postings.
|
|
for _, e := range tc.entries {
|
|
for _, l := range e.Labels {
|
|
refs := r.Postings(l.Name, l.Value)
|
|
found := false
|
|
for _, ref := range refs {
|
|
if ref == e.Ref {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
t.Errorf("postings for %s=%s should contain ref %d, got %v", l.Name, l.Value, e.Ref, refs)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Verify missing lookups return empty/false.
|
|
_, ok := r.SeriesByRef(999999)
|
|
if ok {
|
|
t.Errorf("got true, want false")
|
|
}
|
|
if refs := r.Postings("nonexistent", "value"); refs != nil {
|
|
t.Errorf("got %v, want nil", refs)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestIndexPostingsSorted(t *testing.T) {
|
|
entries := []SeriesEntry{
|
|
{Ref: 5, Labels: []labels.Label{{Name: "room", Value: "office"}}, Chunks: []ChunkMeta{{MinT: 0, MaxT: 1, Ref: 0}}},
|
|
{Ref: 2, Labels: []labels.Label{{Name: "room", Value: "office"}}, Chunks: []ChunkMeta{{MinT: 0, MaxT: 1, Ref: 0}}},
|
|
{Ref: 8, Labels: []labels.Label{{Name: "room", Value: "office"}}, Chunks: []ChunkMeta{{MinT: 0, MaxT: 1, Ref: 0}}},
|
|
}
|
|
|
|
data := writeIndex(t, entries)
|
|
r, err := NewReader(data)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
refs := r.Postings("room", "office")
|
|
if len(refs) != 3 {
|
|
t.Fatalf("got %v, want %v", len(refs), 3)
|
|
}
|
|
if refs[0] != uint64(2) {
|
|
t.Errorf("got %v, want %v", refs[0], uint64(2))
|
|
}
|
|
if refs[1] != uint64(5) {
|
|
t.Errorf("got %v, want %v", refs[1], uint64(5))
|
|
}
|
|
if refs[2] != uint64(8) {
|
|
t.Errorf("got %v, want %v", refs[2], uint64(8))
|
|
}
|
|
}
|
|
|
|
func TestIndexChunkRefEncoding(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
segment uint32
|
|
offset uint32
|
|
}{
|
|
{"zero", 0, 0},
|
|
{"first_segment", 1, 0},
|
|
{"with_offset", 1, 12345},
|
|
{"large_values", 100, 536870912},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
ref := NewChunkRef(tc.segment, tc.offset)
|
|
if ref.Segment() != tc.segment {
|
|
t.Errorf("got %v, want %v", ref.Segment(), tc.segment)
|
|
}
|
|
if ref.Offset() != tc.offset {
|
|
t.Errorf("got %v, want %v", ref.Offset(), tc.offset)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestIndexLabelValues(t *testing.T) {
|
|
entries := []SeriesEntry{
|
|
{Ref: 1, Labels: []labels.Label{{Name: "__name__", Value: "temp"}, {Name: "room", Value: "office"}}, Chunks: []ChunkMeta{{MinT: 0, MaxT: 1, Ref: 0}}},
|
|
{Ref: 2, Labels: []labels.Label{{Name: "__name__", Value: "humidity"}, {Name: "room", Value: "office"}}, Chunks: []ChunkMeta{{MinT: 0, MaxT: 1, Ref: 0}}},
|
|
{Ref: 3, Labels: []labels.Label{{Name: "__name__", Value: "temp"}, {Name: "room", Value: "kitchen"}}, Chunks: []ChunkMeta{{MinT: 0, MaxT: 1, Ref: 0}}},
|
|
}
|
|
|
|
data := writeIndex(t, entries)
|
|
r, err := NewReader(data)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
label string
|
|
wantVals []string
|
|
}{
|
|
{name: "name_values", label: "__name__", wantVals: []string{"humidity", "temp"}},
|
|
{name: "room_values", label: "room", wantVals: []string{"kitchen", "office"}},
|
|
{name: "missing_label", label: "nonexistent", wantVals: []string{}},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
got := r.LabelValues(tc.label)
|
|
if !reflect.DeepEqual(got, tc.wantVals) {
|
|
t.Errorf("got %v, want %v", got, tc.wantVals)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestIndexAllPostings(t *testing.T) {
|
|
entries := []SeriesEntry{
|
|
{Ref: 5, Labels: []labels.Label{{Name: "__name__", Value: "a"}}, Chunks: []ChunkMeta{{MinT: 0, MaxT: 1, Ref: 0}}},
|
|
{Ref: 2, Labels: []labels.Label{{Name: "__name__", Value: "b"}}, Chunks: []ChunkMeta{{MinT: 0, MaxT: 1, Ref: 0}}},
|
|
{Ref: 8, Labels: []labels.Label{{Name: "__name__", Value: "c"}}, Chunks: []ChunkMeta{{MinT: 0, MaxT: 1, Ref: 0}}},
|
|
}
|
|
|
|
data := writeIndex(t, entries)
|
|
r, err := NewReader(data)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
refs := r.AllPostings()
|
|
if !reflect.DeepEqual(refs, []uint64{2, 5, 8}) {
|
|
t.Errorf("got %v, want %v", refs, []uint64{2, 5, 8})
|
|
}
|
|
}
|
|
|
|
func TestIndexCorruptData(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
data []byte
|
|
wantErr error
|
|
}{
|
|
{
|
|
name: "too_short",
|
|
data: []byte{1, 2, 3},
|
|
wantErr: ErrTooShort,
|
|
},
|
|
{
|
|
name: "bad_magic",
|
|
data: func() []byte {
|
|
d := writeIndex(t, nil)
|
|
d[0] = 0xFF
|
|
return d
|
|
}(),
|
|
wantErr: ErrInvalidMagic,
|
|
},
|
|
{
|
|
name: "bad_version",
|
|
data: func() []byte {
|
|
d := writeIndex(t, nil)
|
|
d[4] = 99
|
|
return d
|
|
}(),
|
|
wantErr: ErrInvalidVersion,
|
|
},
|
|
{
|
|
name: "corrupt_toc_crc",
|
|
data: func() []byte {
|
|
d := writeIndex(t, nil)
|
|
d[len(d)-1] ^= 0xFF // flip CRC bits
|
|
return d
|
|
}(),
|
|
wantErr: ErrCorruptTOC,
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
_, err := NewReader(tc.data)
|
|
if err != tc.wantErr {
|
|
t.Errorf("got %v, want %v", err, tc.wantErr)
|
|
}
|
|
})
|
|
}
|
|
}
|