Chore: Code cleaning with unit tests in promlib (#99542)

* update request tests

* remove unused functions

* add unit tests

* remove commented code

* fix unit tests
This commit is contained in:
ismail simsek
2025-01-31 10:39:08 +01:00
committed by GitHub
parent 3b231c433f
commit ec836f2760
6 changed files with 177 additions and 60 deletions

View File

@ -2,7 +2,6 @@ package promlib
import (
"context"
"errors"
"fmt"
"strings"
@ -11,7 +10,6 @@ import (
sdkhttpclient "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
"github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt"
"github.com/grafana/grafana-plugin-sdk-go/backend/log"
apiv1 "github.com/prometheus/client_golang/api/prometheus/v1"
"github.com/grafana/grafana/pkg/promlib/client"
"github.com/grafana/grafana/pkg/promlib/instrumentation"
@ -138,18 +136,3 @@ func (s *Service) getInstance(ctx context.Context, pluginCtx backend.PluginConte
in := i.(instance)
return &in, nil
}
// IsAPIError returns whether err is or wraps a Prometheus error.
func IsAPIError(err error) bool {
// Check if the right error type is in err's chain.
var e *apiv1.Error
return errors.As(err, &e)
}
func ConvertAPIError(err error) error {
var e *apiv1.Error
if errors.As(err, &e) {
return fmt.Errorf("%s: %s", e.Msg, e.Detail)
}
return err
}

View File

@ -43,7 +43,7 @@ func BenchmarkExemplarJson(b *testing.B) {
StatusCode: 200,
Body: io.NopCloser(bytes.NewReader(responseBytes)),
}
tCtx.httpProvider.setResponse(&res)
tCtx.httpProvider.setResponse(&res, &res)
resp, err := tCtx.queryData.Execute(context.Background(), query)
require.NoError(b, err)
for _, r := range resp.Responses {
@ -74,7 +74,7 @@ func BenchmarkRangeJson(b *testing.B) {
StatusCode: 200,
Body: io.NopCloser(bytes.NewReader(body)),
}
tCtx.httpProvider.setResponse(&res)
tCtx.httpProvider.setResponse(&res, &res)
r, err = tCtx.queryData.Execute(context.Background(), q)
require.NoError(b, err)
}

View File

@ -149,6 +149,6 @@ func runQuery(response []byte, q *backend.QueryDataRequest) (*backend.QueryDataR
StatusCode: 200,
Body: io.NopCloser(bytes.NewReader(response)),
}
tCtx.httpProvider.setResponse(res)
tCtx.httpProvider.setResponse(res, res)
return tCtx.queryData.Execute(context.Background(), q)
}

View File

@ -27,7 +27,6 @@ import (
func TestPrometheus_parseTimeSeriesResponse(t *testing.T) {
t.Run("exemplars response should be sampled and parsed normally", func(t *testing.T) {
t.Skip()
exemplars := []apiv1.ExemplarQueryResult{
{
SeriesLabels: p.LabelSet{
@ -60,6 +59,22 @@ func TestPrometheus_parseTimeSeriesResponse(t *testing.T) {
},
}
values := []p.SamplePair{
{Value: 1, Timestamp: 1000},
{Value: 4, Timestamp: 4000},
{Value: 6, Timestamp: 7000},
{Value: 8, Timestamp: 1100},
}
rangeResult := queryResult{
Type: p.ValMatrix,
Result: p.Matrix{
&p.SampleStream{
Metric: p.Metric{"app": "Application", "tag2": "tag2"},
Values: values,
},
},
}
tctx, err := setup()
require.NoError(t, err)
@ -76,20 +91,20 @@ func TestPrometheus_parseTimeSeriesResponse(t *testing.T) {
RefID: "A",
JSON: b,
}
res, err := execute(tctx, query, exemplars)
res, err := execute(tctx, query, exemplars, rangeResult)
require.NoError(t, err)
// Test fields
require.Len(t, res, 1)
// require.Equal(t, res[0].Name, "exemplar")
require.Len(t, res, 2)
require.Equal(t, res[0].Name, "exemplar")
require.Equal(t, res[0].Fields[0].Name, "Time")
require.Equal(t, res[0].Fields[1].Name, "Value")
require.Len(t, res[0].Fields, 6)
// Test correct values (sampled to 2)
require.Equal(t, res[0].Fields[1].Len(), 2)
require.Equal(t, res[0].Fields[1].Len(), 4)
require.Equal(t, res[0].Fields[1].At(0), 0.009545445)
require.Equal(t, res[0].Fields[1].At(1), 0.003535405)
require.Equal(t, res[0].Fields[1].At(3), 0.003535405)
})
t.Run("matrix response should be parsed normally", func(t *testing.T) {
@ -128,7 +143,7 @@ func TestPrometheus_parseTimeSeriesResponse(t *testing.T) {
}
tctx, err := setup()
require.NoError(t, err)
res, err := execute(tctx, query, result)
res, err := execute(tctx, query, result, nil)
require.NoError(t, err)
require.Len(t, res, 1)
@ -177,7 +192,7 @@ func TestPrometheus_parseTimeSeriesResponse(t *testing.T) {
}
tctx, err := setup()
require.NoError(t, err)
res, err := execute(tctx, query, result)
res, err := execute(tctx, query, result, nil)
require.NoError(t, err)
require.Len(t, res, 1)
@ -222,7 +237,7 @@ func TestPrometheus_parseTimeSeriesResponse(t *testing.T) {
}
tctx, err := setup()
require.NoError(t, err)
res, err := execute(tctx, query, result)
res, err := execute(tctx, query, result, nil)
require.NoError(t, err)
require.Len(t, res, 1)
@ -266,7 +281,7 @@ func TestPrometheus_parseTimeSeriesResponse(t *testing.T) {
tctx, err := setup()
require.NoError(t, err)
res, err := execute(tctx, query, result)
res, err := execute(tctx, query, result, nil)
require.NoError(t, err)
require.Equal(t, `{app="Application"}`, res[0].Fields[1].Config.DisplayNameFromDS)
@ -298,7 +313,7 @@ func TestPrometheus_parseTimeSeriesResponse(t *testing.T) {
}
tctx, err := setup()
require.NoError(t, err)
res, err := execute(tctx, query, qr)
res, err := execute(tctx, query, qr, nil)
require.NoError(t, err)
require.Len(t, res, 1)
@ -317,7 +332,6 @@ func TestPrometheus_parseTimeSeriesResponse(t *testing.T) {
})
t.Run("scalar response should be parsed normally", func(t *testing.T) {
t.Skip("TODO: implement scalar responses")
qr := queryResult{
Type: p.ValScalar,
Result: &p.Scalar{
@ -339,14 +353,15 @@ func TestPrometheus_parseTimeSeriesResponse(t *testing.T) {
}
tctx, err := setup()
require.NoError(t, err)
res, err := execute(tctx, query, qr)
res, err := execute(tctx, query, qr, nil)
require.NoError(t, err)
require.Len(t, res, 1)
require.Len(t, res[0].Fields, 2)
require.Len(t, res[0].Fields[0].Labels, 0)
require.Equal(t, res[0].Fields[0].Name, "Time")
require.Equal(t, "1", res[0].Fields[1].Name)
require.Equal(t, "Value", res[0].Fields[1].Name)
require.Equal(t, float64(1), res[0].Fields[1].At(0))
// Ensure the timestamps are UTC zoned
testValue := res[0].Fields[0].At(0)
@ -360,22 +375,29 @@ type queryResult struct {
Result any `json:"result"`
}
func executeWithHeaders(tctx *testContext, query backend.DataQuery, qr any, headers map[string]string) (data.Frames, error) {
func executeWithHeaders(tctx *testContext, query backend.DataQuery, rqr any, eqr any, headers map[string]string) (data.Frames, error) {
req := backend.QueryDataRequest{
Queries: []backend.DataQuery{query},
Headers: headers,
}
promRes, err := toAPIResponse(qr)
defer func() {
if err := promRes.Body.Close(); err != nil {
fmt.Println(fmt.Errorf("response body close error: %v", err))
}
}()
rangeRes, err := toAPIResponse(rqr)
if err != nil {
return nil, err
}
tctx.httpProvider.setResponse(promRes)
exemplarRes, err := toAPIResponse(eqr)
if err != nil {
return nil, err
}
defer func() {
if err := rangeRes.Body.Close(); err != nil {
fmt.Println(fmt.Errorf("rangeRes body close error: %v", err))
}
if err := exemplarRes.Body.Close(); err != nil {
fmt.Println(fmt.Errorf("exemplarRes body close error: %v", err))
}
}()
tctx.httpProvider.setResponse(rangeRes, exemplarRes)
res, err := tctx.queryData.Execute(context.Background(), &req)
if err != nil {
@ -385,8 +407,8 @@ func executeWithHeaders(tctx *testContext, query backend.DataQuery, qr any, head
return res.Responses[req.Queries[0].RefID].Frames, nil
}
func execute(tctx *testContext, query backend.DataQuery, qr any) (data.Frames, error) {
return executeWithHeaders(tctx, query, qr, map[string]string{})
func execute(tctx *testContext, query backend.DataQuery, rqr any, eqr any) (data.Frames, error) {
return executeWithHeaders(tctx, query, rqr, eqr, map[string]string{})
}
type apiResponse struct {
@ -426,7 +448,11 @@ func setup() (*testContext, error) {
opts: httpclient.Options{
Timeouts: &httpclient.DefaultTimeoutOptions,
},
res: &http.Response{
rangeRes: &http.Response{
StatusCode: 200,
Body: io.NopCloser(bytes.NewReader([]byte(`{}`))),
},
exemplarRes: &http.Response{
StatusCode: 200,
Body: io.NopCloser(bytes.NewReader([]byte(`{}`))),
},
@ -456,9 +482,10 @@ func setup() (*testContext, error) {
type fakeHttpClientProvider struct {
httpclient.Provider
opts httpclient.Options
req *http.Request
res *http.Response
opts httpclient.Options
req *http.Request
rangeRes *http.Response
exemplarRes *http.Response
}
func (p *fakeHttpClientProvider) New(opts ...httpclient.Options) (*http.Client, error) {
@ -476,11 +503,18 @@ func (p *fakeHttpClientProvider) GetTransport(opts ...httpclient.Options) (http.
return http.DefaultTransport, nil
}
func (p *fakeHttpClientProvider) setResponse(res *http.Response) {
p.res = res
func (p *fakeHttpClientProvider) setResponse(rangeRes *http.Response, exemplarRes *http.Response) {
p.rangeRes = rangeRes
p.exemplarRes = exemplarRes
}
func (p *fakeHttpClientProvider) RoundTrip(req *http.Request) (*http.Response, error) {
p.req = req
return p.res, nil
switch req.URL.Path {
case "/api/v1/query_range", "/api/v1/query":
return p.rangeRes, nil
case "/api/v1/query_exemplars":
return p.exemplarRes, nil
}
return nil, fmt.Errorf("no such path: %s", req.URL.Path)
}

View File

@ -83,15 +83,6 @@ func (r *Resource) Execute(ctx context.Context, req *backend.CallResourceRequest
return callResponse, err
}
func (r *Resource) DetectVersion(ctx context.Context, req *backend.CallResourceRequest) (*backend.CallResourceResponse, error) {
newReq := &backend.CallResourceRequest{
PluginContext: req.PluginContext,
Path: "/api/v1/status/buildinfo",
}
return r.Execute(ctx, newReq)
}
func getSelectors(expr string) ([]string, error) {
parsed, err := parser.ParseExpr(expr)
if err != nil {

View File

@ -0,0 +1,109 @@
package resource_test
import (
"bytes"
"context"
"encoding/json"
"io"
"net/http"
"testing"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana-plugin-sdk-go/backend/log"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/promlib/resource"
)
type mockRoundTripper struct {
Response *http.Response
Err error
}
func (m *mockRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
return m.Response, m.Err
}
func setup() (*http.Client, backend.DataSourceInstanceSettings, log.Logger) {
// Mock HTTP Response
mockResponse := &http.Response{
StatusCode: 200,
Body: io.NopCloser(bytes.NewReader([]byte(`{"message": "success"}`))),
Header: make(http.Header),
}
// Create a mock RoundTripper
mockTransport := &mockRoundTripper{
Response: mockResponse,
}
// Create a mock HTTP client using the mock RoundTripper
mockClient := &http.Client{
Transport: mockTransport,
}
settings := backend.DataSourceInstanceSettings{
ID: 1,
URL: "http://mock-server",
JSONData: []byte(`{}`),
}
logger := log.DefaultLogger
return mockClient, settings, logger
}
func TestNewResource(t *testing.T) {
mockClient, settings, logger := setup()
res, err := resource.New(mockClient, settings, logger)
require.NoError(t, err)
assert.NotNil(t, res)
}
func TestResource_Execute(t *testing.T) {
mockClient, settings, logger := setup()
res, err := resource.New(mockClient, settings, logger)
require.NoError(t, err)
req := &backend.CallResourceRequest{
URL: "/test",
}
ctx := context.Background()
resp, err := res.Execute(ctx, req)
require.NoError(t, err)
assert.NotNil(t, resp)
}
func TestResource_GetSuggestions(t *testing.T) {
mockClient, _, logger := setup()
settings := backend.DataSourceInstanceSettings{
ID: 1,
URL: "http://localhost:9090",
JSONData: []byte(`{"httpMethod": "GET"}`),
}
res, err := resource.New(mockClient, settings, logger)
require.NoError(t, err)
suggestionReq := resource.SuggestionRequest{
LabelName: "instance",
Queries: []string{"up"},
Start: "1609459200",
End: "1609462800",
Limit: 10,
}
body, err := json.Marshal(suggestionReq)
require.NoError(t, err)
req := &backend.CallResourceRequest{
Body: body,
}
ctx := context.Background()
resp, err := res.GetSuggestions(ctx, req)
require.NoError(t, err)
assert.NotNil(t, resp)
}