From 0e77707e384bf11ba4d609615ccec64974570aad Mon Sep 17 00:00:00 2001 From: Shinya Sasaki Date: Tue, 8 Nov 2022 16:22:05 +0900 Subject: [PATCH] BUG: handling string value data --- pkg/archiverappliance/aaclient.go | 28 ++++----- pkg/archiverappliance/aaclient_test.go | 62 ++++++++++++++++++++ pkg/archiverappliance/query_test.go | 74 +++++++++++++++++++++--- pkg/models/models.go | 24 ++++++++ pkg/models/singledata_test.go | 74 +++++++++++++++++++++++- pkg/models/strings.go | 34 +++++++++++ pkg/models/testhelpers.go | 18 ++++++ pkg/test_data/string_value_response.JSON | 8 +++ 8 files changed, 300 insertions(+), 22 deletions(-) create mode 100644 pkg/models/strings.go create mode 100644 pkg/test_data/string_value_response.JSON diff --git a/pkg/archiverappliance/aaclient.go b/pkg/archiverappliance/aaclient.go index 9750ec5..3188ad7 100644 --- a/pkg/archiverappliance/aaclient.go +++ b/pkg/archiverappliance/aaclient.go @@ -150,21 +150,23 @@ func archiverSingleQueryParser(jsonAsBytes []byte) (models.SingleData, error) { var d models.DataResponse if response[0].Meta.Waveform { - var data models.ArrayResponseModel - jsonErr = json.Unmarshal(response[0].Data, &data) - if jsonErr != nil { - log.DefaultLogger.Warn("Conversion of incoming data to JSON has failed", "Error", jsonErr) - return sD, jsonErr - } - d = data + d = &models.ArrayResponseModel{} } else { - var data models.ScalarResponseModel - jsonErr = json.Unmarshal(response[0].Data, &data) - if jsonErr != nil { - log.DefaultLogger.Warn("Conversion of incoming data to JSON has failed", "Error", jsonErr) - return sD, jsonErr + d = &models.ScalarResponseModel{} + } + + jsonErr = json.Unmarshal(response[0].Data, d) + + // If unmarshal is failed, response data might be string data + if jsonErr != nil { + d = &models.StringResponseModel{} + err := json.Unmarshal(response[0].Data, d) + + // If unmarshal is failed again, response data is not supported data type + if err != nil { + log.DefaultLogger.Warn("Conversion of incoming data to JSON has failed", "Error", err) + return sD, err } - d = data } // Obtain PV name diff --git a/pkg/archiverappliance/aaclient_test.go b/pkg/archiverappliance/aaclient_test.go index df9e57f..d65766c 100644 --- a/pkg/archiverappliance/aaclient_test.go +++ b/pkg/archiverappliance/aaclient_test.go @@ -286,6 +286,68 @@ func TestArchiverSingleQueryParserEmpty(t *testing.T) { } } +func TestArchiverSingleQueryParserString(t *testing.T) { + type responseParams struct { + length int + name string + firstVal string + lastVal string + } + + var dataNames = []struct { + fileName string + output responseParams + }{ + { + fileName: "../test_data/string_value_response.JSON", + output: responseParams{length: 4, name: "PFROP:RING:STATUS_STR", firstVal: "Injection", lastVal: "Top-up"}, + }, + } + + type testData struct { + input []byte + output responseParams + } + + var tests []testData + for _, entry := range dataNames { + fileData, err := ioutil.ReadFile(entry.fileName) + if err != nil { + t.Fatalf("Failed to load test data: %v", err) + } + tests = append(tests, testData{input: fileData, output: entry.output}) + } + + for idx, testCase := range tests { + testName := fmt.Sprintf("Case: %d", idx) + t.Run(testName, func(t *testing.T) { + // result := testCase.output + result, err := archiverSingleQueryParser(testCase.input) + if err != nil { + t.Fatalf("An unexpected error has occurred") + } + if result.Name != testCase.output.name { + t.Fatalf("Names differ - Wanted: %v Got: %v", testCase.output.name, result.Name) + } + + v := result.Values.(*models.Strings) + if len(v.Times) != len(v.Values) { + t.Fatalf("Lengths of Times and Values differ - Times: %v Values: %v", len(v.Times), len(v.Values)) + } + resultLength := len(v.Times) + if resultLength != testCase.output.length { + t.Fatalf("Lengths differ - Wanted: %v Got: %v", testCase.output.length, resultLength) + } + if v.Values[0] != testCase.output.firstVal { + t.Fatalf("First values differ - Wanted: %v Got: %v", testCase.output.firstVal, v.Values[0]) + } + if v.Values[resultLength-1] != testCase.output.lastVal { + t.Fatalf("Last values differ - Wanted: %v Got: %v", testCase.output.lastVal, v.Values[resultLength-1]) + } + }) + } +} + func TestArchiverSingleQueryParserInvalidData(t *testing.T) { var dataNames = []struct { name string diff --git a/pkg/archiverappliance/query_test.go b/pkg/archiverappliance/query_test.go index 3d03cc5..070779a 100644 --- a/pkg/archiverappliance/query_test.go +++ b/pkg/archiverappliance/query_test.go @@ -32,14 +32,19 @@ func (f fakeClient) ExecuteSingleQuery(target string, qm models.ArchiverQueryMod return models.SingleData{}, errors.New("test error") } - var values []float64 - if target == "PV:NAME1" { - values = []float64{0, 1, 2} - } else { - values = []float64{3, 4, 5} - } + var v models.Values - v := &models.Scalars{Times: testhelper.TimeArrayHelper(0, 3), Values: values} + switch target { + case "string": + s := []string{"test1", "test2", "test3"} + v = &models.Strings{Times: testhelper.TimeArrayHelper(0, 3), Values: s} + case "PV:NAME1": + values := []float64{0, 1, 2} + v = &models.Scalars{Times: testhelper.TimeArrayHelper(0, 3), Values: values} + default: + values := []float64{3, 4, 5} + v = &models.Scalars{Times: testhelper.TimeArrayHelper(0, 3), Values: values} + } sd := models.SingleData{ Name: target, @@ -228,6 +233,61 @@ func TestQuery(t *testing.T) { }, }, }, + { + name: "test string response", + req: &backend.QueryDataRequest{ + Queries: []backend.DataQuery{ + { + Interval: testhelper.MultiReturnHelperParseDuration(time.ParseDuration("0s")), + JSON: json.RawMessage(`{ + "alias": "", + "aliasPattern": "", + "constant":6.5, + "functions":[], + "hide":false , + "operator": "", + "refId":"A" , + "regex":false , + "target":"string" + }`), + MaxDataPoints: 1000, + QueryType: "", + RefID: "A", + TimeRange: backend.TimeRange{ + From: testhelper.MultiReturnHelperParse(time.Parse(TIME_FORMAT, "2021-01-27T14:30:41.678-08:00")), + To: testhelper.MultiReturnHelperParse(time.Parse(TIME_FORMAT, "2021-01-28T14:30:41.678-08:00")), + }, + }, + }, + }, + out: &backend.QueryDataResponse{ + Responses: map[string]backend.DataResponse{ + "A": { + Frames: data.Frames{ + &data.Frame{ + Name: "string", + RefID: "", + Fields: []*data.Field{ + { + Name: "Time", + }, + { + Name: "string", + Labels: data.Labels{ + "pvname": "string", + }, + Config: &data.FieldConfig{ + DisplayName: "string", + }, + }, + }, + Meta: &data.FrameMeta{}, + }, + }, + }, + }, + }, + }, } f := fakeClient{} for _, testCase := range tests { diff --git a/pkg/models/models.go b/pkg/models/models.go index 683690f..e1000ad 100644 --- a/pkg/models/models.go +++ b/pkg/models/models.go @@ -103,7 +103,15 @@ type SingleScalarResponseModel struct { Val json.Number `json:"val"` } +type SingleStringResponseModel struct { + Millis *json.Number `json:"millis,omitempty"` + Nanos *json.Number `json:"nanos,omitempty"` + Secs *json.Number `json:"secs,omitempty"` + Val string `json:"val"` +} + type ScalarResponseModel []SingleScalarResponseModel +type StringResponseModel []SingleStringResponseModel type ArrayResponseModel []SingleArrayResponseModel type DataResponse interface { @@ -131,6 +139,22 @@ func (response ScalarResponseModel) ToSingleDataValues() (Values, error) { return &Scalars{Times: times, Values: values}, nil } +func (response StringResponseModel) ToSingleDataValues() (Values, error) { + // Build output data block + dataSize := len(response) + + // initialize the slices with their final size so append operations are not necessary + times := make([]time.Time, dataSize) + values := make([]string, dataSize) + + for idx, dataPt := range response { + times[idx] = convertNanosec(dataPt.Millis) + values[idx] = dataPt.Val + } + + return &Strings{Times: times, Values: values}, nil +} + func (response ArrayResponseModel) ToSingleDataValues() (Values, error) { // Build output data block dataSize := len(response) diff --git a/pkg/models/singledata_test.go b/pkg/models/singledata_test.go index fd3927b..494d312 100644 --- a/pkg/models/singledata_test.go +++ b/pkg/models/singledata_test.go @@ -116,6 +116,60 @@ func TestToFrameScalar(t *testing.T) { } } +func TestToFrameString(t *testing.T) { + var tests = []struct { + sD SingleData + name string + pvname string + values []string + dataSize int + }{ + { + sD: SingleData{ + Name: "testing_name", + PVname: "pvname", + Values: &Strings{ + Times: []time.Time{testhelper.TimeHelper(0), testhelper.TimeHelper(1), testhelper.TimeHelper(2)}, + Values: []string{"1", "2", "3"}, + }, + }, + name: "testing_name", + pvname: "pvname", + values: []string{"1", "2", "3"}, + dataSize: 3, + }, + } + for idx, testCase := range tests { + testName := fmt.Sprintf("%d: %s", idx, testCase.name) + t.Run(testName, func(t *testing.T) { + result := testCase.sD.ToFrame(FormatOption(FORMAT_TIMESERIES)) + if testCase.name != result.Name { + t.Errorf("got %v, want %v", result.Name, testCase.name) + } + if result.Fields[0].Name != "time" { + t.Errorf("got %v, want time", result.Fields[0].Name) + } + if result.Fields[0].Len() != testCase.dataSize { + t.Errorf("got %d, want %d", result.Fields[0].Len(), testCase.dataSize) + } + if testCase.name != result.Fields[1].Config.DisplayName { + t.Errorf("got %v, want %v", result.Fields[1].Config.DisplayName, testCase.name) + } + if testCase.pvname != result.Fields[1].Labels["pvname"] { + t.Errorf("got %v, want %v", result.Fields[1].Labels["pvname"], testCase.pvname) + } + if testCase.name != result.Fields[1].Name { + t.Errorf("got %v, want %v", result.Fields[1].Name, testCase.name) + } + for i := 0; i < result.Fields[1].Len(); i++ { + if testCase.values[i] != result.Fields[1].CopyAt(i) { + t.Errorf("got %v, want %v", result.Fields[1].CopyAt(i), testCase.values[i]) + } + } + }) + } +} + func TestToFrameArray(t *testing.T) { var tests = []struct { sD SingleData @@ -333,7 +387,7 @@ func TestExtrapolation(t *testing.T) { Values: []float64{1}, }, }, - name: "extrapolation", + name: "scalars extrapolation", t: testhelper.TimeHelper(5), sDOut: SingleData{ Values: &Scalars{ @@ -342,6 +396,22 @@ func TestExtrapolation(t *testing.T) { }, }, }, + { + sDIn: SingleData{ + Values: &Strings{ + Times: []time.Time{testhelper.TimeHelper(0)}, + Values: []string{"1"}, + }, + }, + name: "strings extrapolation", + t: testhelper.TimeHelper(5), + sDOut: SingleData{ + Values: &Strings{ + Times: []time.Time{testhelper.TimeHelper(0)}, + Values: []string{"1"}, + }, + }, + }, { sDIn: SingleData{ Values: &Arrays{ @@ -349,7 +419,7 @@ func TestExtrapolation(t *testing.T) { Values: [][]float64{{1, 1}}, }, }, - name: "extrapolation", + name: "arrays extrapolation", t: testhelper.TimeHelper(5), sDOut: SingleData{ Values: &Arrays{ diff --git a/pkg/models/strings.go b/pkg/models/strings.go new file mode 100644 index 0000000..9dda121 --- /dev/null +++ b/pkg/models/strings.go @@ -0,0 +1,34 @@ +package models + +import ( + "time" + + "github.com/grafana/grafana-plugin-sdk-go/data" +) + +type Strings struct { + Times []time.Time + Values []string +} + +func (v *Strings) ToFields(pvname string, name string, format FormatOption) []*data.Field { + // ToFields doesn't use FormatOption in Strings for now + + var fields []*data.Field + + //add the time dimension + fields = append(fields, data.NewField("time", nil, v.Times)) + + // add values + labels := make(data.Labels, 1) + labels["pvname"] = pvname + + valueField := data.NewField(name, labels, v.Values) + valueField.Config = &data.FieldConfig{DisplayName: name} + fields = append(fields, valueField) + + return fields +} + +func (v *Strings) Extrapolation(t time.Time) { +} diff --git a/pkg/models/testhelpers.go b/pkg/models/testhelpers.go index 6e309c2..475f71e 100644 --- a/pkg/models/testhelpers.go +++ b/pkg/models/testhelpers.go @@ -54,6 +54,24 @@ func SingleDataCompareHelper(result []*SingleData, wanted []*SingleData, t *test } } } + case *Strings: + wantedv := wanted[udx].Values.(*Strings) + if len(wantedv.Times) != len(resultv.Times) { + t.Errorf("Input and output arrays' times differ in length. Wanted %v, got %v", len(wantedv.Times), len(resultv.Times)) + return + } + if len(wantedv.Values) != len(resultv.Values) { + t.Errorf("Input and output arrays' values differ in length. Wanted %v, got %v", len(wantedv.Values), len(resultv.Values)) + return + } + for idx := range wantedv.Values { + if resultv.Times[idx] != wantedv.Times[idx] { + t.Errorf("Times at index %v do not match, Wanted %v, got %v", idx, wantedv.Times[idx], resultv.Times[idx]) + } + if resultv.Values[idx] != wantedv.Values[idx] { + t.Errorf("Values at index %v do not match, Wanted %v, got %v", idx, wantedv.Values[idx], resultv.Values[idx]) + } + } default: t.Fatalf("Response Values are invalid") } diff --git a/pkg/test_data/string_value_response.JSON b/pkg/test_data/string_value_response.JSON new file mode 100644 index 0000000..85bdf49 --- /dev/null +++ b/pkg/test_data/string_value_response.JSON @@ -0,0 +1,8 @@ +[ +{ "meta": { "name": "PFROP:RING:STATUS_STR" , "waveform": false , "PREC": "0" }, +"data": [ +{ "millis": 1667779211808, "val": "Injection", "fields": { "DESC": "string input record"} }, +{ "millis": 1667842341792, "val": "Top-up" }, +{ "millis": 1667860584741, "val": "Injection" }, +{ "millis": 1667864794279, "val": "Top-up" }] } + ] \ No newline at end of file