From 04997a079ae189a4449e8e4cf20ebcb2034ff179 Mon Sep 17 00:00:00 2001 From: dmitryk-dk Date: Tue, 20 Feb 2024 15:34:50 +0100 Subject: [PATCH 1/3] Added tests for backend part --- pkg/plugin/query_test.go | 98 ++++++++++++++++++++ pkg/plugin/response.go | 5 +- pkg/plugin/response_test.go | 127 +++++++++++++++++++++++++ pkg/utils/utils_test.go | 179 ++++++++++++++++++++++++++++++++++++ 4 files changed, 407 insertions(+), 2 deletions(-) create mode 100644 pkg/plugin/query_test.go create mode 100644 pkg/plugin/response_test.go create mode 100644 pkg/utils/utils_test.go diff --git a/pkg/plugin/query_test.go b/pkg/plugin/query_test.go new file mode 100644 index 0000000..531d1a3 --- /dev/null +++ b/pkg/plugin/query_test.go @@ -0,0 +1,98 @@ +package plugin + +import ( + "testing" +) + +func TestQuery_getQueryURL(t *testing.T) { + type fields struct { + RefID string + Expr string + MaxLines int + } + type args struct { + rawURL string + queryParams string + } + tests := []struct { + name string + fields fields + args args + want string + wantErr bool + }{ + { + name: "empty values", + fields: fields{ + RefID: "1", + Expr: "", + MaxLines: 0, + }, + args: args{ + rawURL: "", + queryParams: "", + }, + want: "", + wantErr: true, + }, + { + name: "has rawURL without params", + fields: fields{ + RefID: "1", + Expr: "", + MaxLines: 0, + }, + args: args{ + rawURL: "http://127.0.0.1:9428", + queryParams: "", + }, + want: "http://127.0.0.1:9428/select/logsql/query?limit=1000&query=", + wantErr: false, + }, + { + name: "has expression and max lines", + fields: fields{ + RefID: "1", + Expr: "_time:1s", + MaxLines: 10, + }, + args: args{ + rawURL: "http://127.0.0.1:9428", + queryParams: "", + }, + want: "http://127.0.0.1:9428/select/logsql/query?limit=10&query=_time%3A1s", + wantErr: false, + }, + { + name: "has expression and max lines, with queryParams", + fields: fields{ + RefID: "1", + Expr: "_time:1s and syslog", + MaxLines: 10, + }, + args: args{ + rawURL: "http://127.0.0.1:9428", + queryParams: "a=1&b=2", + }, + want: "http://127.0.0.1:9428/select/logsql/query?a=1&b=2&limit=10&query=_time%3A1s+and+syslog", + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + q := &Query{ + RefID: tt.fields.RefID, + Expr: tt.fields.Expr, + MaxLines: tt.fields.MaxLines, + } + got, err := q.getQueryURL(tt.args.rawURL, tt.args.queryParams) + if (err != nil) != tt.wantErr { + t.Errorf("getQueryURL() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("getQueryURL() got = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/pkg/plugin/response.go b/pkg/plugin/response.go index 8b5ed2b..ec9d22d 100644 --- a/pkg/plugin/response.go +++ b/pkg/plugin/response.go @@ -2,6 +2,7 @@ package plugin import ( "encoding/json" + "fmt" "io" "github.com/VictoriaMetrics/metricsql" @@ -48,7 +49,7 @@ func parseStreamResponse(reader io.Reader) backend.DataResponse { var r Response err := dec.Decode(&r) if err != nil { - return newResponseError(err, backend.StatusInternal) + return newResponseError(fmt.Errorf("error decode response: %s", err), backend.StatusInternal) } for fieldName, value := range r { @@ -58,7 +59,7 @@ func parseStreamResponse(reader io.Reader) backend.DataResponse { case timeField: getTime, err := utils.GetTime(value) if err != nil { - return newResponseError(err, backend.StatusInternal) + return newResponseError(fmt.Errorf("error parse time from _time field: %s", err), backend.StatusInternal) } timeFd.Append(getTime) case streamField: diff --git a/pkg/plugin/response_test.go b/pkg/plugin/response_test.go new file mode 100644 index 0000000..407f46d --- /dev/null +++ b/pkg/plugin/response_test.go @@ -0,0 +1,127 @@ +package plugin + +import ( + "bytes" + "fmt" + "io" + "reflect" + "testing" + "time" + + "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana-plugin-sdk-go/data" +) + +func Test_parseStreamResponse(t *testing.T) { + f := func(remoteResp string) io.Reader { + return io.NopCloser(bytes.NewBuffer([]byte(remoteResp))) + } + tests := []struct { + name string + reader func(remoteResp string) io.Reader + response string + want func() backend.DataResponse + }{ + { + name: "empty response", + reader: func(remoteResp string) io.Reader { + return io.NopCloser(bytes.NewBuffer([]byte(remoteResp))) + }, + response: "", + want: func() backend.DataResponse { + labelsField := data.NewFieldFromFieldType(data.FieldTypeJSON, 0) + labelsField.Name = gLabelsField + + timeFd := data.NewFieldFromFieldType(data.FieldTypeTime, 0) + timeFd.Name = gTimeField + + lineField := data.NewFieldFromFieldType(data.FieldTypeString, 0) + lineField.Name = gLineField + + frame := data.NewFrame("", timeFd, lineField, labelsField) + + rsp := backend.DataResponse{} + frame.Meta = &data.FrameMeta{} + rsp.Frames = append(rsp.Frames, frame) + + return rsp + }, + }, + { + name: "incorrect response", + reader: f, + response: "abcd", + want: func() backend.DataResponse { + return newResponseError(fmt.Errorf("error decode response: invalid character 'a' looking for beginning of value"), backend.StatusInternal) + }, + }, + { + name: "incorrect time in the response", + reader: f, + response: `{"_time":"acdf"}`, + want: func() backend.DataResponse { + return newResponseError(fmt.Errorf("error parse time from _time field: cannot parse acdf: cannot parse duration \"acdf\""), backend.StatusInternal) + }, + }, + { + name: "incorrect time in the response", + reader: f, + response: `{"_time":"acdf"}`, + want: func() backend.DataResponse { + return newResponseError(fmt.Errorf("error parse time from _time field: cannot parse acdf: cannot parse duration \"acdf\""), backend.StatusInternal) + }, + }, + { + name: "invalid labels in the resposne", + reader: f, + response: `{"_time":"2024-02-20", "_stream":"{application=\"logs-benchmark-Apache.log-1708437847\",hostname=}"}`, + want: func() backend.DataResponse { + return newResponseError(fmt.Errorf("StringExpr: unexpected token \"}\"; want \"string\"; unparsed data: \"}\""), backend.StatusInternal) + }, + }, + { + name: "correct response line", + reader: f, + response: `{"_msg":"123","_stream":"{application=\"logs-benchmark-Apache.log-1708437847\",hostname=\"e28a622d7792\"}","_time":"2024-02-20T14:04:27Z"}`, + want: func() backend.DataResponse { + labelsField := data.NewFieldFromFieldType(data.FieldTypeJSON, 0) + labelsField.Name = gLabelsField + + timeFd := data.NewFieldFromFieldType(data.FieldTypeTime, 0) + timeFd.Name = gTimeField + + lineField := data.NewFieldFromFieldType(data.FieldTypeString, 0) + lineField.Name = gLineField + + timeFd.Append(time.Date(2024, 02, 20, 14, 04, 27, 0, time.UTC)) + + lineField.Append("123") + + labels := data.Labels{ + "application": "logs-benchmark-Apache.log-1708437847", + "hostname": "e28a622d7792", + } + + b, _ := labelsToJSON(labels) + + labelsField.Append(b) + frame := data.NewFrame("", timeFd, lineField, labelsField) + + rsp := backend.DataResponse{} + frame.Meta = &data.FrameMeta{} + rsp.Frames = append(rsp.Frames, frame) + + return rsp + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := tt.reader(tt.response) + w := tt.want() + if got := parseStreamResponse(r); !reflect.DeepEqual(got, w) { + t.Errorf("parseStreamResponse() = %#v, want %#v", got, w) + } + }) + } +} diff --git a/pkg/utils/utils_test.go b/pkg/utils/utils_test.go new file mode 100644 index 0000000..fde14e1 --- /dev/null +++ b/pkg/utils/utils_test.go @@ -0,0 +1,179 @@ +package utils + +import ( + "testing" + "time" +) + +func TestGetTime(t *testing.T) { + tests := []struct { + name string + s string + want func() time.Time + wantErr bool + }{ + { + name: "empty string", + s: "", + want: func() time.Time { return time.Time{} }, + wantErr: true, + }, + { + name: "only year", + s: "2019", + want: func() time.Time { + t := time.Date(2019, 1, 1, 0, 0, 0, 0, time.UTC) + return t + }, + }, + { + name: "year and month", + s: "2019-01", + want: func() time.Time { + t := time.Date(2019, 1, 1, 0, 0, 0, 0, time.UTC) + return t + }, + }, + { + name: "year and not first month", + s: "2019-02", + want: func() time.Time { + t := time.Date(2019, 2, 1, 0, 0, 0, 0, time.UTC) + return t + }, + }, + { + name: "year, month and day", + s: "2019-02-01", + want: func() time.Time { + t := time.Date(2019, 2, 1, 0, 0, 0, 0, time.UTC) + return t + }, + }, + { + name: "year, month and not first day", + s: "2019-02-10", + want: func() time.Time { + t := time.Date(2019, 2, 10, 0, 0, 0, 0, time.UTC) + return t + }, + }, + { + name: "year, month, day and time", + s: "2019-02-02T00", + want: func() time.Time { + t := time.Date(2019, 2, 2, 0, 0, 0, 0, time.UTC) + return t + }, + }, + { + name: "year, month, day and one hour time", + s: "2019-02-02T01", + want: func() time.Time { + t := time.Date(2019, 2, 2, 1, 0, 0, 0, time.UTC) + return t + }, + }, + { + name: "time with zero minutes", + s: "2019-02-02T01:00", + want: func() time.Time { + t := time.Date(2019, 2, 2, 1, 0, 0, 0, time.UTC) + return t + }, + }, + { + name: "time with one minute", + s: "2019-02-02T01:01", + want: func() time.Time { + t := time.Date(2019, 2, 2, 1, 1, 0, 0, time.UTC) + return t + }, + }, + { + name: "time with zero seconds", + s: "2019-02-02T01:01:00", + want: func() time.Time { + t := time.Date(2019, 2, 2, 1, 1, 0, 0, time.UTC) + return t + }, + }, + { + name: "timezone with one second", + s: "2019-02-02T01:01:01", + want: func() time.Time { + t := time.Date(2019, 2, 2, 1, 1, 1, 0, time.UTC) + return t + }, + }, + { + name: "time with two second and timezone", + s: "2019-07-07T20:01:02Z", + want: func() time.Time { + t := time.Date(2019, 7, 7, 20, 1, 02, 0, time.UTC) + return t + }, + }, + { + name: "time with seconds and timezone", + s: "2019-07-07T20:47:40+03:00", + want: func() time.Time { + l, _ := time.LoadLocation("Europe/Kiev") + t := time.Date(2019, 7, 7, 20, 47, 40, 0, l) + return t + }, + }, + { + name: "negative time", + s: "-292273086-05-16T16:47:06Z", + want: func() time.Time { return time.Time{} }, + wantErr: true, + }, + { + name: "float timestamp representation", + s: "1562529662.324", + want: func() time.Time { + t := time.Date(2019, 7, 7, 20, 01, 02, 324e6, time.UTC) + return t + }, + }, + { + name: "negative timestamp", + s: "-9223372036.855", + want: func() time.Time { + return time.Date(1970, 01, 01, 00, 00, 00, 00, time.UTC) + }, + wantErr: false, + }, + { + name: "big timestamp", + s: "1223372036855", + want: func() time.Time { + t := time.Date(2008, 10, 7, 9, 33, 56, 855e6, time.UTC) + return t + }, + wantErr: false, + }, + { + name: "duration time", + s: "1h5m", + want: func() time.Time { + t := time.Now().Add(-1 * time.Hour).Add(-5 * time.Minute) + return t + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := GetTime(tt.s) + if (err != nil) != tt.wantErr { + t.Errorf("ParseTime() error = %v, wantErr %v", err, tt.wantErr) + return + } + w := tt.want() + if got.Unix() != w.Unix() { + t.Errorf("ParseTime() got = %v, want %v", got, w) + } + }) + } +} From 8b216b0a4068913bd12d6da36d58e86fbbf1a49f Mon Sep 17 00:00:00 2001 From: dmitryk-dk Date: Tue, 20 Feb 2024 15:43:26 +0100 Subject: [PATCH 2/3] remove duplicated test, added missed test --- pkg/plugin/response_test.go | 44 ++++++++++++++++++++++++++++++------- 1 file changed, 36 insertions(+), 8 deletions(-) diff --git a/pkg/plugin/response_test.go b/pkg/plugin/response_test.go index 407f46d..d5cbc22 100644 --- a/pkg/plugin/response_test.go +++ b/pkg/plugin/response_test.go @@ -64,25 +64,52 @@ func Test_parseStreamResponse(t *testing.T) { }, }, { - name: "incorrect time in the response", + name: "invalid stream in the response", reader: f, - response: `{"_time":"acdf"}`, + response: `{"_time":"2024-02-20", "_stream":"{application=\"logs-benchmark-Apache.log-1708437847\",hostname=}"}`, want: func() backend.DataResponse { - return newResponseError(fmt.Errorf("error parse time from _time field: cannot parse acdf: cannot parse duration \"acdf\""), backend.StatusInternal) + return newResponseError(fmt.Errorf("StringExpr: unexpected token \"}\"; want \"string\"; unparsed data: \"}\""), backend.StatusInternal) }, }, { - name: "invalid labels in the resposne", + name: "correct response line", reader: f, - response: `{"_time":"2024-02-20", "_stream":"{application=\"logs-benchmark-Apache.log-1708437847\",hostname=}"}`, + response: `{"_msg":"123","_stream":"{application=\"logs-benchmark-Apache.log-1708437847\",hostname=\"e28a622d7792\"}","_time":"2024-02-20T14:04:27Z"}`, want: func() backend.DataResponse { - return newResponseError(fmt.Errorf("StringExpr: unexpected token \"}\"; want \"string\"; unparsed data: \"}\""), backend.StatusInternal) + labelsField := data.NewFieldFromFieldType(data.FieldTypeJSON, 0) + labelsField.Name = gLabelsField + + timeFd := data.NewFieldFromFieldType(data.FieldTypeTime, 0) + timeFd.Name = gTimeField + + lineField := data.NewFieldFromFieldType(data.FieldTypeString, 0) + lineField.Name = gLineField + + timeFd.Append(time.Date(2024, 02, 20, 14, 04, 27, 0, time.UTC)) + + lineField.Append("123") + + labels := data.Labels{ + "application": "logs-benchmark-Apache.log-1708437847", + "hostname": "e28a622d7792", + } + + b, _ := labelsToJSON(labels) + + labelsField.Append(b) + frame := data.NewFrame("", timeFd, lineField, labelsField) + + rsp := backend.DataResponse{} + frame.Meta = &data.FrameMeta{} + rsp.Frames = append(rsp.Frames, frame) + + return rsp }, }, { - name: "correct response line", + name: "response with different labels", reader: f, - response: `{"_msg":"123","_stream":"{application=\"logs-benchmark-Apache.log-1708437847\",hostname=\"e28a622d7792\"}","_time":"2024-02-20T14:04:27Z"}`, + response: `{"_msg":"123","_stream":"{application=\"logs-benchmark-Apache.log-1708437847\",hostname=\"e28a622d7792\"}","_time":"2024-02-20T14:04:27Z", "job": "vlogs"}`, want: func() backend.DataResponse { labelsField := data.NewFieldFromFieldType(data.FieldTypeJSON, 0) labelsField.Name = gLabelsField @@ -100,6 +127,7 @@ func Test_parseStreamResponse(t *testing.T) { labels := data.Labels{ "application": "logs-benchmark-Apache.log-1708437847", "hostname": "e28a622d7792", + "job": "vlogs", } b, _ := labelsToJSON(labels) From 9e87808f08ca5dad707d09199215a7d2087ac291 Mon Sep 17 00:00:00 2001 From: dmitryk-dk Date: Thu, 22 Feb 2024 16:57:56 +0100 Subject: [PATCH 3/3] remove redundant reader --- pkg/plugin/response_test.go | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/pkg/plugin/response_test.go b/pkg/plugin/response_test.go index d5cbc22..cee4e96 100644 --- a/pkg/plugin/response_test.go +++ b/pkg/plugin/response_test.go @@ -13,20 +13,13 @@ import ( ) func Test_parseStreamResponse(t *testing.T) { - f := func(remoteResp string) io.Reader { - return io.NopCloser(bytes.NewBuffer([]byte(remoteResp))) - } tests := []struct { name string - reader func(remoteResp string) io.Reader response string want func() backend.DataResponse }{ { - name: "empty response", - reader: func(remoteResp string) io.Reader { - return io.NopCloser(bytes.NewBuffer([]byte(remoteResp))) - }, + name: "empty response", response: "", want: func() backend.DataResponse { labelsField := data.NewFieldFromFieldType(data.FieldTypeJSON, 0) @@ -49,7 +42,6 @@ func Test_parseStreamResponse(t *testing.T) { }, { name: "incorrect response", - reader: f, response: "abcd", want: func() backend.DataResponse { return newResponseError(fmt.Errorf("error decode response: invalid character 'a' looking for beginning of value"), backend.StatusInternal) @@ -57,7 +49,6 @@ func Test_parseStreamResponse(t *testing.T) { }, { name: "incorrect time in the response", - reader: f, response: `{"_time":"acdf"}`, want: func() backend.DataResponse { return newResponseError(fmt.Errorf("error parse time from _time field: cannot parse acdf: cannot parse duration \"acdf\""), backend.StatusInternal) @@ -65,7 +56,6 @@ func Test_parseStreamResponse(t *testing.T) { }, { name: "invalid stream in the response", - reader: f, response: `{"_time":"2024-02-20", "_stream":"{application=\"logs-benchmark-Apache.log-1708437847\",hostname=}"}`, want: func() backend.DataResponse { return newResponseError(fmt.Errorf("StringExpr: unexpected token \"}\"; want \"string\"; unparsed data: \"}\""), backend.StatusInternal) @@ -73,7 +63,6 @@ func Test_parseStreamResponse(t *testing.T) { }, { name: "correct response line", - reader: f, response: `{"_msg":"123","_stream":"{application=\"logs-benchmark-Apache.log-1708437847\",hostname=\"e28a622d7792\"}","_time":"2024-02-20T14:04:27Z"}`, want: func() backend.DataResponse { labelsField := data.NewFieldFromFieldType(data.FieldTypeJSON, 0) @@ -108,7 +97,6 @@ func Test_parseStreamResponse(t *testing.T) { }, { name: "response with different labels", - reader: f, response: `{"_msg":"123","_stream":"{application=\"logs-benchmark-Apache.log-1708437847\",hostname=\"e28a622d7792\"}","_time":"2024-02-20T14:04:27Z", "job": "vlogs"}`, want: func() backend.DataResponse { labelsField := data.NewFieldFromFieldType(data.FieldTypeJSON, 0) @@ -145,7 +133,7 @@ func Test_parseStreamResponse(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - r := tt.reader(tt.response) + r := io.NopCloser(bytes.NewBuffer([]byte(tt.response))) w := tt.want() if got := parseStreamResponse(r); !reflect.DeepEqual(got, w) { t.Errorf("parseStreamResponse() = %#v, want %#v", got, w)