The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
grafana/pkg/services/annotations/annotationsimpl/composite_store_test.go

238 lines
5.8 KiB

package annotationsimpl
import (
"context"
"errors"
"fmt"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/annotations"
"github.com/grafana/grafana/pkg/services/annotations/accesscontrol"
)
var (
errGet = errors.New("get error")
errGetTags = errors.New("get tags error")
)
func TestCompositeStore(t *testing.T) {
t.Run("should handle panic", func(t *testing.T) {
r1 := newFakeReader()
getPanic := func(context.Context, annotations.ItemQuery, *accesscontrol.AccessResources) ([]*annotations.ItemDTO, error) {
panic("ohno")
}
r2 := newFakeReader(withGetFn(getPanic))
store := &CompositeStore{
log.NewNopLogger(),
[]readStore{r1, r2},
}
_, err := store.Get(context.Background(), annotations.ItemQuery{}, nil)
require.Error(t, err)
require.Contains(t, err.Error(), "concurrent job panic")
})
t.Run("should return first error", func(t *testing.T) {
err1 := errors.New("error 1")
r1 := newFakeReader(withError(err1))
err2 := errors.New("error 2")
r2 := newFakeReader(withError(err2), withWait(10*time.Millisecond))
store := &CompositeStore{
log.NewNopLogger(),
[]readStore{r1, r2},
}
tc := []struct {
f func() (any, error)
err error
}{
{
f: func() (any, error) { return store.Get(context.Background(), annotations.ItemQuery{}, nil) },
err: errGet,
},
{
f: func() (any, error) { return store.GetTags(context.Background(), annotations.TagsQuery{}) },
err: errGetTags,
},
}
for _, tt := range tc {
_, err := tt.f()
require.Error(t, err)
require.ErrorIs(t, err, err1)
require.NotErrorIs(t, err, err2)
}
})
t.Run("should combine and sort results from Get", func(t *testing.T) {
items1 := []*annotations.ItemDTO{
{TimeEnd: 1, Time: 2},
{TimeEnd: 2, Time: 1},
}
r1 := newFakeReader(withItems(items1))
items2 := []*annotations.ItemDTO{
{TimeEnd: 1, Time: 1},
{TimeEnd: 1, Time: 3},
}
r2 := newFakeReader(withItems(items2))
store := &CompositeStore{
log.NewNopLogger(),
[]readStore{r1, r2},
}
expected := []*annotations.ItemDTO{
{TimeEnd: 2, Time: 1},
{TimeEnd: 1, Time: 3},
{TimeEnd: 1, Time: 2},
{TimeEnd: 1, Time: 1},
}
items, _ := store.Get(context.Background(), annotations.ItemQuery{}, nil)
require.Equal(t, expected, items)
})
t.Run("should combine and sort results from GetTags", func(t *testing.T) {
tags1 := []*annotations.TagsDTO{
{Tag: "key1:val1"},
{Tag: "key2:val1"},
}
r1 := newFakeReader(withTags(tags1))
tags2 := []*annotations.TagsDTO{
{Tag: "key1:val2"},
{Tag: "key2:val2"},
}
r2 := newFakeReader(withTags(tags2))
store := &CompositeStore{
log.NewNopLogger(),
[]readStore{r1, r2},
}
expected := []*annotations.TagsDTO{
{Tag: "key1:val1"},
{Tag: "key1:val2"},
{Tag: "key2:val1"},
{Tag: "key2:val2"},
}
res, _ := store.GetTags(context.Background(), annotations.TagsQuery{})
require.Equal(t, expected, res.Tags)
})
// Check if reader is not modifying query since it might cause a race condition in case of composite store
t.Run("should not modify query", func(t *testing.T) {
getFn1 := func(ctx context.Context, query annotations.ItemQuery, resources *accesscontrol.AccessResources) ([]*annotations.ItemDTO, error) {
query.From = 1
return []*annotations.ItemDTO{}, nil
}
getFn2 := func(ctx context.Context, query annotations.ItemQuery, resources *accesscontrol.AccessResources) ([]*annotations.ItemDTO, error) {
return []*annotations.ItemDTO{}, nil
}
r1 := newFakeReader(withGetFn(getFn1))
r2 := newFakeReader(withGetFn(getFn2))
store := &CompositeStore{
log.NewNopLogger(),
[]readStore{r1, r2},
}
query := annotations.ItemQuery{}
_, err := store.Get(context.Background(), query, nil)
require.NoError(t, err)
require.Equal(t, int64(0), query.From)
})
}
type fakeReader struct {
items []*annotations.ItemDTO
tagRes annotations.FindTagsResult
getFn func(context.Context, annotations.ItemQuery, *accesscontrol.AccessResources) ([]*annotations.ItemDTO, error)
getTagFn func(context.Context, annotations.TagsQuery) (annotations.FindTagsResult, error)
wait time.Duration
err error
}
func (f *fakeReader) Type() string {
return "fake"
}
func (f *fakeReader) Get(ctx context.Context, query annotations.ItemQuery, accessResources *accesscontrol.AccessResources) ([]*annotations.ItemDTO, error) {
if f.getFn != nil {
return f.getFn(ctx, query, accessResources)
}
if f.wait > 0 {
time.Sleep(f.wait)
}
if f.err != nil {
err := fmt.Errorf("%w: %w", errGet, f.err)
return nil, err
}
return f.items, nil
}
func (f *fakeReader) GetTags(ctx context.Context, query annotations.TagsQuery) (annotations.FindTagsResult, error) {
if f.getTagFn != nil {
return f.getTagFn(ctx, query)
}
if f.wait > 0 {
time.Sleep(f.wait)
}
if f.err != nil {
err := fmt.Errorf("%w: %w", errGetTags, f.err)
return annotations.FindTagsResult{}, err
}
return f.tagRes, nil
}
func withWait(wait time.Duration) func(*fakeReader) {
return func(f *fakeReader) {
f.wait = wait
}
}
func withError(err error) func(*fakeReader) {
return func(f *fakeReader) {
f.err = err
}
}
func withItems(items []*annotations.ItemDTO) func(*fakeReader) {
return func(f *fakeReader) {
f.items = items
}
}
func withTags(tags []*annotations.TagsDTO) func(*fakeReader) {
return func(f *fakeReader) {
f.tagRes = annotations.FindTagsResult{Tags: tags}
}
}
func withGetFn(fn func(context.Context, annotations.ItemQuery, *accesscontrol.AccessResources) ([]*annotations.ItemDTO, error)) func(*fakeReader) {
return func(f *fakeReader) {
f.getFn = fn
}
}
func newFakeReader(opts ...func(*fakeReader)) *fakeReader {
f := &fakeReader{}
for _, opt := range opts {
opt(f)
}
return f
}